fune/mobile/android/chrome/content/content.js
2019-10-21 18:18:02 +00:00

637 lines
21 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"AboutReader",
"resource://gre/modules/AboutReader.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ReaderMode",
"resource://gre/modules/ReaderMode.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"Readerable",
"resource://gre/modules/Readerable.jsm"
);
XPCOMUtils.defineLazyGetter(this, "gPipNSSBundle", function() {
return Services.strings.createBundle(
"chrome://pipnss/locale/pipnss.properties"
);
});
XPCOMUtils.defineLazyGetter(this, "gNSSErrorsBundle", function() {
return Services.strings.createBundle(
"chrome://pipnss/locale/nsserrors.properties"
);
});
var dump = ChromeUtils.import(
"resource://gre/modules/AndroidLog.jsm",
{}
).AndroidLog.d.bind(null, "Content");
var global = this;
var AboutBlockedSiteListener = {
init(chromeGlobal) {
addEventListener("AboutBlockedLoaded", this, false, true);
},
get isBlockedSite() {
return content.document.documentURI.startsWith("about:blocked");
},
handleEvent(aEvent) {
if (!this.isBlockedSite) {
return;
}
if (aEvent.type != "AboutBlockedLoaded") {
return;
}
let provider = "";
if (docShell.failedChannel) {
let classifiedChannel = docShell.failedChannel.QueryInterface(
Ci.nsIClassifiedChannel
);
if (classifiedChannel) {
provider = classifiedChannel.matchedProvider;
}
}
let advisoryUrl = Services.prefs.getCharPref(
"browser.safebrowsing.provider." + provider + ".advisoryURL",
""
);
if (!advisoryUrl) {
let el = content.document.getElementById("advisoryDesc");
el.remove();
return;
}
let advisoryLinkText = Services.prefs.getCharPref(
"browser.safebrowsing.provider." + provider + ".advisoryName",
""
);
if (!advisoryLinkText) {
let el = content.document.getElementById("advisoryDesc");
el.remove();
return;
}
let anchorEl = content.document.getElementById("advisory_provider");
anchorEl.setAttribute("href", advisoryUrl);
anchorEl.textContent = advisoryLinkText;
},
};
AboutBlockedSiteListener.init();
/* The following code, in particular AboutCertErrorListener and
* AboutNetErrorListener, is mostly copied from content browser.js and content.js.
* Certificate error handling should be unified to remove this duplicated code.
*/
const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;
const SEC_ERROR_EXPIRED_CERTIFICATE = SEC_ERROR_BASE + 11;
const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13;
const SEC_ERROR_UNTRUSTED_ISSUER = SEC_ERROR_BASE + 20;
const SEC_ERROR_UNTRUSTED_CERT = SEC_ERROR_BASE + 21;
const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30;
const SEC_ERROR_CA_CERT_INVALID = SEC_ERROR_BASE + 36;
const SEC_ERROR_OCSP_FUTURE_RESPONSE = SEC_ERROR_BASE + 131;
const SEC_ERROR_OCSP_OLD_RESPONSE = SEC_ERROR_BASE + 132;
const SEC_ERROR_REUSED_ISSUER_AND_SERIAL = SEC_ERROR_BASE + 138;
const SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED = SEC_ERROR_BASE + 176;
const MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE =
MOZILLA_PKIX_ERROR_BASE + 5;
const MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE =
MOZILLA_PKIX_ERROR_BASE + 6;
const MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED =
MOZILLA_PKIX_ERROR_BASE + 13;
const SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE;
const SSL_ERROR_SSL_DISABLED = SSL_ERROR_BASE + 20;
const SSL_ERROR_SSL2_DISABLED = SSL_ERROR_BASE + 14;
var AboutNetErrorListener = {
init(chromeGlobal) {
addEventListener("AboutNetErrorLoad", this, false, true);
},
get isNetErrorSite() {
return content.document.documentURI.startsWith("about:neterror");
},
_getErrorMessageFromCode(securityInfo, doc) {
let uri = Services.io.newURI(doc.location);
let hostString = uri.host;
if (uri.port != 443 && uri.port != -1) {
hostString += ":" + uri.port;
}
let id_str = "";
switch (securityInfo.errorCode) {
case SSL_ERROR_SSL_DISABLED:
id_str = "PSMERR_SSL_Disabled";
break;
case SSL_ERROR_SSL2_DISABLED:
id_str = "PSMERR_SSL2_Disabled";
break;
case SEC_ERROR_REUSED_ISSUER_AND_SERIAL:
id_str = "PSMERR_HostReusedIssuerSerial";
break;
}
let nss_error_id_str = securityInfo.errorCodeString;
let msg2 = "";
if (id_str) {
msg2 = gPipNSSBundle.GetStringFromName(id_str) + "\n";
} else if (nss_error_id_str) {
msg2 = gNSSErrorsBundle.GetStringFromName(nss_error_id_str) + "\n";
}
if (!msg2) {
// We couldn't get an error message. Use the error string.
// Note that this is different from before where we used PR_ErrorToString.
msg2 = nss_error_id_str;
}
let msg = gPipNSSBundle.formatStringFromName("SSLConnectionErrorPrefix2", [
hostString,
msg2,
]);
if (nss_error_id_str) {
msg += gPipNSSBundle.formatStringFromName("certErrorCodePrefix3", [
nss_error_id_str,
]);
}
return msg;
},
handleEvent(aEvent) {
if (!this.isNetErrorSite) {
return;
}
if (aEvent.type != "AboutNetErrorLoad") {
return;
}
let { securityInfo } = docShell.failedChannel;
// We don't have a securityInfo when this is for example a DNS error.
if (securityInfo) {
securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
let msg = this._getErrorMessageFromCode(
securityInfo,
aEvent.originalTarget.ownerGlobal
);
let id = content.document.getElementById("errorShortDescText");
id.textContent = msg;
}
},
};
AboutNetErrorListener.init();
var AboutCertErrorListener = {
init(chromeGlobal) {
addEventListener("AboutCertErrorLoad", this, false, true);
},
get isCertErrorSite() {
return content.document.documentURI.startsWith("about:certerror");
},
_setTechDetailsMsgPart1(hostString, securityInfo, technicalInfo, doc) {
let msg = gPipNSSBundle.formatStringFromName("certErrorIntro", [
hostString,
]);
msg += "\n\n";
if (securityInfo.isUntrusted && !securityInfo.serverCert.isSelfSigned) {
switch (securityInfo.errorCode) {
case SEC_ERROR_UNKNOWN_ISSUER:
msg +=
gPipNSSBundle.GetStringFromName("certErrorTrust_UnknownIssuer") +
"\n";
msg +=
gPipNSSBundle.GetStringFromName("certErrorTrust_UnknownIssuer2") +
"\n";
msg +=
gPipNSSBundle.GetStringFromName("certErrorTrust_UnknownIssuer3") +
"\n";
break;
case SEC_ERROR_CA_CERT_INVALID:
msg +=
gPipNSSBundle.GetStringFromName("certErrorTrust_CaInvalid") + "\n";
break;
case SEC_ERROR_UNTRUSTED_ISSUER:
msg +=
gPipNSSBundle.GetStringFromName("certErrorTrust_Issuer") + "\n";
break;
case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED:
msg +=
gPipNSSBundle.GetStringFromName(
"certErrorTrust_SignatureAlgorithmDisabled"
) + "\n";
break;
case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE:
msg +=
gPipNSSBundle.GetStringFromName("certErrorTrust_ExpiredIssuer") +
"\n";
break;
// This error code currently only exists for the Symantec distrust, we may need to adjust
// it to fit other distrusts later.
case MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED:
msg +=
gPipNSSBundle.formatStringFromName("certErrorTrust_Symantec", [
hostString,
]) + "\n";
break;
case SEC_ERROR_UNTRUSTED_CERT:
default:
msg +=
gPipNSSBundle.GetStringFromName("certErrorTrust_Untrusted") + "\n";
}
}
if (securityInfo.isUntrusted && securityInfo.serverCert.isSelfSigned) {
msg +=
gPipNSSBundle.GetStringFromName("certErrorTrust_SelfSigned") + "\n";
}
technicalInfo.appendChild(doc.createTextNode(msg));
},
_setTechDetails(securityInfo, location) {
if (!securityInfo || !location) {
return;
}
let validity = securityInfo.serverCert.validity;
let doc = content.document;
// CSS class and error code are set from nsDocShell.
let searchParams = new URLSearchParams(doc.documentURI.split("?")[1]);
let cssClass = searchParams.get("s");
let error = searchParams.get("e");
let technicalInfo = doc.getElementById("technicalContentText");
let uri = Services.io.newURI(location);
let hostString = uri.host;
if (uri.port != 443 && uri.port != -1) {
hostString += ":" + uri.port;
}
// This error code currently only exists for the Symantec distrust
// in Firefox 63, so we add copy explaining that to the user.
// In case of future distrusts of that scale we might need to add
// additional parameters that allow us to identify the affected party
// without replicating the complex logic from certverifier code.
if (
securityInfo.errorCode ==
MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED
) {
let introContent = doc.getElementById("introContent");
let description = doc.createElement("p");
description.textContent = gPipNSSBundle.formatStringFromName(
"certErrorSymantecDistrustDescription",
[hostString]
);
introContent.append(description);
// The regular "what should I do" message does not make sense in this case.
doc.getElementById(
"whatShouldIDoContentText"
).textContent = gPipNSSBundle.GetStringFromName(
"certErrorSymantecDistrustAdministrator"
);
}
this._setTechDetailsMsgPart1(hostString, securityInfo, technicalInfo, doc);
if (securityInfo.isDomainMismatch) {
let subjectAltNamesList = securityInfo.serverCert.subjectAltNames;
let subjectAltNames = subjectAltNamesList.split(",");
let numSubjectAltNames = subjectAltNames.length;
let msgPrefix = "";
if (numSubjectAltNames != 0) {
if (numSubjectAltNames == 1) {
msgPrefix = gPipNSSBundle.GetStringFromName(
"certErrorMismatchSinglePrefix"
);
// Let's check if we want to make this a link.
let okHost = subjectAltNamesList;
let href = "";
let thisHost = doc.location.hostname;
let proto = doc.location.protocol + "//";
// If okHost is a wildcard domain ("*.example.com") let's
// use "www" instead. "*.example.com" isn't going to
// get anyone anywhere useful. bug 432491
okHost = okHost.replace(/^\*\./, "www.");
/* case #1:
* example.com uses an invalid security certificate.
*
* The certificate is only valid for www.example.com
*
* Make sure to include the "." ahead of thisHost so that
* a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
*
* We'd normally just use a RegExp here except that we lack a
* library function to escape them properly (bug 248062), and
* domain names are famous for having '.' characters in them,
* which would allow spurious and possibly hostile matches.
*/
if (okHost.endsWith("." + thisHost)) {
href = proto + okHost;
}
/* case #2:
* browser.garage.maemo.org uses an invalid security certificate.
*
* The certificate is only valid for garage.maemo.org
*/
if (thisHost.endsWith("." + okHost)) {
href = proto + okHost;
}
// If we set a link, meaning there's something helpful for
// the user here, expand the section by default
if (href && cssClass != "expertBadCert") {
doc.getElementById("technicalContentText").style.display = "block";
}
// Set the link if we want it.
if (href) {
let referrerlink = doc.createElement("a");
referrerlink.append(subjectAltNamesList + "\n");
referrerlink.title = subjectAltNamesList;
referrerlink.id = "cert_domain_link";
referrerlink.href = href;
let fragment = BrowserUtils.getLocalizedFragment(
doc,
msgPrefix,
referrerlink
);
technicalInfo.appendChild(fragment);
} else {
let fragment = BrowserUtils.getLocalizedFragment(
doc,
msgPrefix,
subjectAltNamesList
);
technicalInfo.appendChild(fragment);
}
technicalInfo.append("\n");
} else {
let msg =
gPipNSSBundle.GetStringFromName("certErrorMismatchMultiple") + "\n";
for (let i = 0; i < numSubjectAltNames; i++) {
msg += subjectAltNames[i];
if (i != numSubjectAltNames - 1) {
msg += ", ";
}
}
technicalInfo.append(msg + "\n");
}
} else {
let msg = gPipNSSBundle.formatStringFromName("certErrorMismatch", [
hostString,
]);
technicalInfo.append(msg + "\n");
}
}
if (securityInfo.isNotValidAtThisTime) {
let nowTime = new Date().getTime() * 1000;
let dateOptions = {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
};
let now = new Services.intl.DateTimeFormat(undefined, dateOptions).format(
new Date()
);
let msg = "";
if (validity.notBefore) {
if (nowTime > validity.notAfter) {
msg +=
gPipNSSBundle.formatStringFromName("certErrorExpiredNow", [
validity.notAfterLocalTime,
now,
]) + "\n";
} else {
msg +=
gPipNSSBundle.formatStringFromName("certErrorNotYetValidNow", [
validity.notBeforeLocalTime,
now,
]) + "\n";
}
} else {
// If something goes wrong, we assume the cert expired.
msg +=
gPipNSSBundle.formatStringFromName("certErrorExpiredNow", ["", now]) +
"\n";
}
technicalInfo.append(msg);
}
technicalInfo.append("\n");
// Add link to certificate and error message.
let errorCodeMsg = gPipNSSBundle.formatStringFromName(
"certErrorCodePrefix3",
[securityInfo.errorCodeString]
);
technicalInfo.append(errorCodeMsg);
},
handleEvent(aEvent) {
if (!this.isCertErrorSite) {
return;
}
if (aEvent.type != "AboutCertErrorLoad") {
return;
}
let ownerDoc = aEvent.originalTarget.ownerGlobal;
let securityInfo =
docShell.failedChannel && docShell.failedChannel.securityInfo;
securityInfo
.QueryInterface(Ci.nsITransportSecurityInfo)
.QueryInterface(Ci.nsISerializable);
this._setTechDetails(securityInfo, ownerDoc.location.href);
},
};
AboutCertErrorListener.init();
// This is copied from desktop's tab-content.js. See bug 1153485 about sharing this code somehow.
var AboutReaderListener = {
_articlePromise: null,
_isLeavingReaderableReaderMode: false,
init: function() {
addEventListener("AboutReaderContentLoaded", this, false, true);
addEventListener("DOMContentLoaded", this, false);
addEventListener("pageshow", this, false);
addEventListener("pagehide", this, false);
addMessageListener("Reader:ToggleReaderMode", this);
addMessageListener("Reader:PushState", this);
},
receiveMessage: function(message) {
switch (message.name) {
case "Reader:ToggleReaderMode":
let url = content.document.location.href;
if (!this.isAboutReader) {
this._articlePromise = ReaderMode.parseDocument(
content.document
).catch(Cu.reportError);
ReaderMode.enterReaderMode(docShell, content);
} else {
this._isLeavingReaderableReaderMode = this.isReaderableAboutReader;
ReaderMode.leaveReaderMode(docShell, content);
}
break;
case "Reader:PushState":
this.updateReaderButton(!!(message.data && message.data.isArticle));
break;
}
},
get isAboutReader() {
return content.document.documentURI.startsWith("about:reader");
},
get isReaderableAboutReader() {
return (
this.isAboutReader && !content.document.documentElement.dataset.isError
);
},
get isErrorPage() {
return (
content.document.documentURI.startsWith("about:neterror") ||
content.document.documentURI.startsWith("about:certerror") ||
content.document.documentURI.startsWith("about:blocked")
);
},
handleEvent: function(aEvent) {
if (aEvent.originalTarget.defaultView != content) {
return;
}
switch (aEvent.type) {
case "AboutReaderContentLoaded":
if (!this.isAboutReader) {
return;
}
// If we are restoring multiple reader mode tabs during session restore, duplicate "DOMContentLoaded"
// events may be fired for the visible tab. The inital "DOMContentLoaded" may be received before the
// document body is available, so we avoid instantiating an AboutReader object, expecting that a
// valid message will follow. See bug 925983.
if (content.document.body) {
new AboutReader(global, content, this._articlePromise);
this._articlePromise = null;
}
break;
case "pagehide":
// this._isLeavingReaderableReaderMode is used here to keep the Reader Mode icon
// visible in the location bar when transitioning from reader-mode page
// back to the source page.
sendAsyncMessage("Reader:UpdateReaderButton", {
isArticle: this._isLeavingReaderableReaderMode,
});
if (this._isLeavingReaderableReaderMode) {
this._isLeavingReaderableReaderMode = false;
}
break;
case "pageshow":
// If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
// event, so we need to rely on "pageshow" in this case.
if (aEvent.persisted) {
this.updateReaderButton();
}
break;
case "DOMContentLoaded":
this.updateReaderButton();
break;
}
},
updateReaderButton: function(forceNonArticle) {
// Do not show Reader View icon on error pages (bug 1320900)
if (this.isErrorPage) {
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
} else if (
!Readerable.isEnabledForParseOnLoad ||
this.isAboutReader ||
!(content.document instanceof content.HTMLDocument) ||
content.document.mozSyntheticDocument
) {
} else {
this.scheduleReadabilityCheckPostPaint(forceNonArticle);
}
},
cancelPotentialPendingReadabilityCheck: function() {
if (this._pendingReadabilityCheck) {
removeEventListener("MozAfterPaint", this._pendingReadabilityCheck);
delete this._pendingReadabilityCheck;
}
},
scheduleReadabilityCheckPostPaint: function(forceNonArticle) {
if (this._pendingReadabilityCheck) {
// We need to stop this check before we re-add one because we don't know
// if forceNonArticle was true or false last time.
this.cancelPotentialPendingReadabilityCheck();
}
this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(
this,
forceNonArticle
);
addEventListener("MozAfterPaint", this._pendingReadabilityCheck);
},
onPaintWhenWaitedFor: function(forceNonArticle, event) {
// In non-e10s, we'll get called for paints other than ours, and so it's
// possible that this page hasn't been laid out yet, in which case we
// should wait until we get an event that does relate to our layout. We
// determine whether any of our content got painted by checking if there
// are any painted rects.
if (!event.clientRects.length) {
return;
}
Services.console.logStringMessage(`ON PAINT WHEN WAITED FOR\n`);
this.cancelPotentialPendingReadabilityCheck();
// Only send updates when there are articles; there's no point updating with
// |false| all the time.
if (Readerable.isProbablyReaderable(content.document)) {
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
} else if (forceNonArticle) {
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
}
},
};
AboutReaderListener.init();
Services.obs.notifyObservers(this, "tab-content-frameloader-created");