gecko-dev/browser/base/content/content.js
Hector Zhao 19a4cacc8a Bug 1409348 - Prefer the per-provider report{Malware,Phish}MistakeURL in about:blocked. r=francois
MozReview-Commit-ID: 6Wmzf4SvXnE

--HG--
extra : rebase_source : b253fe62b7682362c3a9d0522d90770825ea4963
2017-10-17 19:42:31 +08:00

1242 lines
43 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/. */
/* This content script should work in any browser or iframe and should not
* depend on the frame being contained in tabbrowser. */
/* eslint-env mozilla/frame-script */
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
ContentLinkHandler: "resource:///modules/ContentLinkHandler.jsm",
ContentMetaHandler: "resource:///modules/ContentMetaHandler.jsm",
ContentWebRTC: "resource:///modules/ContentWebRTC.jsm",
InlineSpellCheckerContent: "resource://gre/modules/InlineSpellCheckerContent.jsm",
LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
LoginFormFactory: "resource://gre/modules/LoginManagerContent.jsm",
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.jsm",
PluginContent: "resource:///modules/PluginContent.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
FormSubmitObserver: "resource:///modules/FormSubmitObserver.jsm",
PageMetadata: "resource://gre/modules/PageMetadata.jsm",
PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
Utils: "resource://gre/modules/sessionstore/Utils.jsm",
WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
Feeds: "resource:///modules/Feeds.jsm",
ContextMenu: "resource:///modules/ContextMenu.jsm",
});
// TabChildGlobal
var global = this;
var contextMenu = this.contextMenu = new ContextMenu(global);
// Load the form validation popup handler
var formSubmitObserver = new FormSubmitObserver(content, this);
addMessageListener("RemoteLogins:fillForm", function(message) {
// intercept if ContextMenu.jsm had sent a plain object for remote targets
message.objects.inputElement = contextMenu.getTarget(message, "inputElement");
LoginManagerContent.receiveMessage(message, content);
});
addEventListener("DOMFormHasPassword", function(event) {
LoginManagerContent.onDOMFormHasPassword(event, content);
let formLike = LoginFormFactory.createFromForm(event.target);
InsecurePasswordUtils.reportInsecurePasswords(formLike);
});
addEventListener("DOMInputPasswordAdded", function(event) {
LoginManagerContent.onDOMInputPasswordAdded(event, content);
let formLike = LoginFormFactory.createFromField(event.target);
InsecurePasswordUtils.reportInsecurePasswords(formLike);
});
addEventListener("pageshow", function(event) {
LoginManagerContent.onPageShow(event, content);
});
addEventListener("DOMAutoComplete", function(event) {
LoginManagerContent.onUsernameInput(event);
});
addEventListener("blur", function(event) {
LoginManagerContent.onUsernameInput(event);
});
// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
const TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN = 0;
const TLS_ERROR_REPORT_TELEMETRY_EXPANDED = 1;
const TLS_ERROR_REPORT_TELEMETRY_SUCCESS = 6;
const TLS_ERROR_REPORT_TELEMETRY_FAILURE = 7;
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_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30;
const SEC_ERROR_OCSP_FUTURE_RESPONSE = SEC_ERROR_BASE + 131;
const SEC_ERROR_OCSP_OLD_RESPONSE = SEC_ERROR_BASE + 132;
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 PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
function getSerializedSecurityInfo(docShell) {
let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
.getService(Ci.nsISerializationHelper);
let securityInfo = docShell.failedChannel && docShell.failedChannel.securityInfo;
if (!securityInfo) {
return "";
}
securityInfo.QueryInterface(Ci.nsITransportSecurityInfo)
.QueryInterface(Ci.nsISerializable);
return serhelper.serializeToString(securityInfo);
}
function getSiteBlockedErrorDetails(docShell) {
let blockedInfo = {};
if (docShell.failedChannel) {
let classifiedChannel = docShell.failedChannel.
QueryInterface(Ci.nsIClassifiedChannel);
if (classifiedChannel) {
let httpChannel = docShell.failedChannel.QueryInterface(Ci.nsIHttpChannel);
let reportUri = httpChannel.URI.clone();
// Remove the query to avoid leaking sensitive data
if (reportUri instanceof Ci.nsIURL) {
reportUri.query = "";
}
blockedInfo = { list: classifiedChannel.matchedList,
provider: classifiedChannel.matchedProvider,
uri: reportUri.asciiSpec };
}
}
return blockedInfo;
}
var AboutBlockedSiteListener = {
init(chromeGlobal) {
addMessageListener("DeceptiveBlockedDetails", this);
chromeGlobal.addEventListener("AboutBlockedLoaded", this, false, true);
},
get isBlockedSite() {
return content.document.documentURI.startsWith("about:blocked");
},
receiveMessage(msg) {
if (!this.isBlockedSite) {
return;
}
if (msg.name == "DeceptiveBlockedDetails") {
sendAsyncMessage("DeceptiveBlockedDetails:Result", {
blockedInfo: getSiteBlockedErrorDetails(docShell),
});
}
},
handleEvent(aEvent) {
if (!this.isBlockedSite) {
return;
}
if (aEvent.type != "AboutBlockedLoaded") {
return;
}
let blockedInfo = getSiteBlockedErrorDetails(docShell);
let provider = blockedInfo.provider || "";
let doc = content.document;
/**
* Set error description link in error details.
* For example, the "reported as a deceptive site" link for
* blocked phishing pages.
*/
let desc = Services.prefs.getCharPref(
"browser.safebrowsing.provider." + provider + ".reportURL", "");
if (desc) {
doc.getElementById("error_desc_link").setAttribute("href", desc + aEvent.detail.url);
}
// Set other links in error details.
switch (aEvent.detail.err) {
case "malware":
doc.getElementById("report_detection").setAttribute("href",
(SafeBrowsing.getReportURL("MalwareMistake", blockedInfo) ||
"https://www.stopbadware.org/firefox"));
doc.getElementById("learn_more_link").setAttribute("href",
"https://www.stopbadware.org/firefox");
break;
case "unwanted":
doc.getElementById("learn_more_link").setAttribute("href",
"https://www.google.com/about/unwanted-software-policy.html");
break;
case "phishing":
doc.getElementById("report_detection").setAttribute("href",
(SafeBrowsing.getReportURL("PhishMistake", blockedInfo) ||
"https://safebrowsing.google.com/safebrowsing/report_error/?tpl=mozilla"));
doc.getElementById("learn_more_link").setAttribute("href",
"https://www.antiphishing.org//");
break;
}
// Set the firefox support url.
doc.getElementById("firefox_support").setAttribute("href",
"https://support.mozilla.org/kb/how-does-phishing-and-malware-protection-work");
// Set safe browsing advisory link.
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;
},
};
var AboutNetAndCertErrorListener = {
init(chromeGlobal) {
addMessageListener("CertErrorDetails", this);
addMessageListener("Browser:CaptivePortalFreed", this);
chromeGlobal.addEventListener("AboutNetErrorLoad", this, false, true);
chromeGlobal.addEventListener("AboutNetErrorOpenCaptivePortal", this, false, true);
chromeGlobal.addEventListener("AboutNetErrorSetAutomatic", this, false, true);
chromeGlobal.addEventListener("AboutNetErrorResetPreferences", this, false, true);
},
get isAboutNetError() {
return content.document.documentURI.startsWith("about:neterror");
},
get isAboutCertError() {
return content.document.documentURI.startsWith("about:certerror");
},
receiveMessage(msg) {
if (!this.isAboutCertError) {
return;
}
switch (msg.name) {
case "CertErrorDetails":
this.onCertErrorDetails(msg);
break;
case "Browser:CaptivePortalFreed":
this.onCaptivePortalFreed(msg);
break;
}
},
onCertErrorDetails(msg) {
let div = content.document.getElementById("certificateErrorText");
div.textContent = msg.data.info;
let learnMoreLink = content.document.getElementById("learnMoreLink");
let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
switch (msg.data.code) {
case SEC_ERROR_UNKNOWN_ISSUER:
learnMoreLink.href = baseURL + "security-error";
break;
// In case the certificate expired we make sure the system clock
// matches the blocklist ping (Kinto) time and is not before the build date.
case SEC_ERROR_EXPIRED_CERTIFICATE:
case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE:
case SEC_ERROR_OCSP_FUTURE_RESPONSE:
case SEC_ERROR_OCSP_OLD_RESPONSE:
case MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE:
case MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE:
// We check against Kinto time first if available, because that allows us
// to give the user an approximation of what the correct time is.
let difference = 0;
if (Services.prefs.getPrefType(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS)) {
difference = Services.prefs.getIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS);
}
// If the difference is more than a day.
if (Math.abs(difference) > 60 * 60 * 24) {
let formatter = Services.intl.createDateTimeFormat(undefined, {
dateStyle: "short"
});
let systemDate = formatter.format(new Date());
// negative difference means local time is behind server time
let actualDate = formatter.format(new Date(Date.now() - difference * 1000));
content.document.getElementById("wrongSystemTime_URL")
.textContent = content.document.location.hostname;
content.document.getElementById("wrongSystemTime_systemDate")
.textContent = systemDate;
content.document.getElementById("wrongSystemTime_actualDate")
.textContent = actualDate;
content.document.getElementById("errorShortDesc")
.style.display = "none";
content.document.getElementById("wrongSystemTimePanel")
.style.display = "block";
// If there is no clock skew with Kinto servers, check against the build date.
// (The Kinto ping could have happened when the time was still right, or not at all)
} else {
let appBuildID = Services.appinfo.appBuildID;
let year = parseInt(appBuildID.substr(0, 4), 10);
let month = parseInt(appBuildID.substr(4, 2), 10) - 1;
let day = parseInt(appBuildID.substr(6, 2), 10);
let buildDate = new Date(year, month, day);
let systemDate = new Date();
if (buildDate > systemDate) {
let formatter = Services.intl.createDateTimeFormat(undefined, {
dateStyle: "short"
});
content.document.getElementById("wrongSystemTimeWithoutReference_URL")
.textContent = content.document.location.hostname;
content.document.getElementById("wrongSystemTimeWithoutReference_systemDate")
.textContent = formatter.format(systemDate);
content.document.getElementById("errorShortDesc")
.style.display = "none";
content.document.getElementById("wrongSystemTimeWithoutReferencePanel")
.style.display = "block";
}
}
learnMoreLink.href = baseURL + "time-errors";
break;
}
},
onCaptivePortalFreed(msg) {
content.dispatchEvent(new content.CustomEvent("AboutNetErrorCaptivePortalFreed"));
},
handleEvent(aEvent) {
if (!this.isAboutNetError && !this.isAboutCertError) {
return;
}
switch (aEvent.type) {
case "AboutNetErrorLoad":
this.onPageLoad(aEvent);
break;
case "AboutNetErrorOpenCaptivePortal":
this.openCaptivePortalPage(aEvent);
break;
case "AboutNetErrorSetAutomatic":
this.onSetAutomatic(aEvent);
break;
case "AboutNetErrorResetPreferences":
this.onResetPreferences(aEvent);
break;
}
},
changedCertPrefs() {
let prefSSLImpact = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => {
return prefs.concat(Services.prefs.getChildList(root));
}, []);
for (let prefName of prefSSLImpact) {
if (Services.prefs.prefHasUserValue(prefName)) {
return true;
}
}
return false;
},
onPageLoad(evt) {
if (this.isAboutCertError) {
let originalTarget = evt.originalTarget;
let ownerDoc = originalTarget.ownerDocument;
ClickEventHandler.onCertError(originalTarget, ownerDoc);
}
let automatic = Services.prefs.getBoolPref("security.ssl.errorReporting.automatic");
content.dispatchEvent(new content.CustomEvent("AboutNetErrorOptions", {
detail: JSON.stringify({
enabled: Services.prefs.getBoolPref("security.ssl.errorReporting.enabled"),
changedCertPrefs: this.changedCertPrefs(),
automatic
})
}));
sendAsyncMessage("Browser:SSLErrorReportTelemetry",
{reportStatus: TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN});
},
openCaptivePortalPage(evt) {
sendAsyncMessage("Browser:OpenCaptivePortalPage");
},
onResetPreferences(evt) {
sendAsyncMessage("Browser:ResetSSLPreferences");
},
onSetAutomatic(evt) {
sendAsyncMessage("Browser:SetSSLErrorReportAuto", {
automatic: evt.detail
});
// if we're enabling reports, send a report for this failure
if (evt.detail) {
let {host, port} = content.document.mozDocumentURIIfNotForErrorPages;
sendAsyncMessage("Browser:SendSSLErrorReport", {
uri: { host, port },
securityInfo: getSerializedSecurityInfo(docShell),
});
}
},
};
AboutNetAndCertErrorListener.init(this);
AboutBlockedSiteListener.init(this);
var ClickEventHandler = {
init: function init() {
Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService)
.addSystemEventListener(global, "click", this, true);
},
handleEvent(event) {
if (!event.isTrusted || event.defaultPrevented || event.button == 2) {
return;
}
let originalTarget = event.originalTarget;
let ownerDoc = originalTarget.ownerDocument;
if (!ownerDoc) {
return;
}
// Handle click events from about pages
if (event.button == 0) {
if (ownerDoc.documentURI.startsWith("about:certerror")) {
this.onCertError(originalTarget, ownerDoc);
return;
} else if (ownerDoc.documentURI.startsWith("about:blocked")) {
this.onAboutBlocked(originalTarget, ownerDoc);
return;
} else if (ownerDoc.documentURI.startsWith("about:neterror")) {
this.onAboutNetError(event, ownerDoc.documentURI);
return;
}
}
let [href, node, principal] = this._hrefAndLinkNodeForClickEvent(event);
// get referrer attribute from clicked link and parse it
// if per element referrer is enabled, the element referrer overrules
// the document wide referrer
let referrerPolicy = ownerDoc.referrerPolicy;
if (node) {
let referrerAttrValue = Services.netUtils.parseAttributePolicyString(node.
getAttribute("referrerpolicy"));
if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
referrerPolicy = referrerAttrValue;
}
}
let frameOuterWindowID = WebNavigationFrames.getFrameId(ownerDoc.defaultView);
let json = { button: event.button, shiftKey: event.shiftKey,
ctrlKey: event.ctrlKey, metaKey: event.metaKey,
altKey: event.altKey, href: null, title: null,
bookmark: false, frameOuterWindowID, referrerPolicy,
triggeringPrincipal: principal,
originAttributes: principal ? principal.originAttributes : {},
isContentWindowPrivate: PrivateBrowsingUtils.isContentWindowPrivate(ownerDoc.defaultView)};
if (href) {
try {
BrowserUtils.urlSecurityCheck(href, principal);
} catch (e) {
return;
}
json.href = href;
if (node) {
json.title = node.getAttribute("title");
if (event.button == 0 && !event.ctrlKey && !event.shiftKey &&
!event.altKey && !event.metaKey) {
json.bookmark = node.getAttribute("rel") == "sidebar";
if (json.bookmark) {
event.preventDefault(); // Need to prevent the pageload.
}
}
}
json.noReferrer = BrowserUtils.linkHasNoReferrer(node);
// Check if the link needs to be opened with mixed content allowed.
// Only when the owner doc has |mixedContentChannel| and the same origin
// should we allow mixed content.
json.allowMixedContent = false;
let docshell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
if (docShell.mixedContentChannel) {
const sm = Services.scriptSecurityManager;
try {
let targetURI = Services.io.newURI(href);
sm.checkSameOriginURI(docshell.mixedContentChannel.URI, targetURI, false);
json.allowMixedContent = true;
} catch (e) {}
}
json.originPrincipal = ownerDoc.nodePrincipal;
json.triggeringPrincipal = ownerDoc.nodePrincipal;
sendAsyncMessage("Content:Click", json);
return;
}
// This might be middle mouse navigation.
if (event.button == 1) {
sendAsyncMessage("Content:Click", json);
}
},
onCertError(targetElement, ownerDoc) {
let docShell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
sendAsyncMessage("Browser:CertExceptionError", {
location: ownerDoc.location.href,
elementId: targetElement.getAttribute("id"),
isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView),
securityInfoAsString: getSerializedSecurityInfo(docShell),
});
},
onAboutBlocked(targetElement, ownerDoc) {
var reason = "phishing";
if (/e=malwareBlocked/.test(ownerDoc.documentURI)) {
reason = "malware";
} else if (/e=unwantedBlocked/.test(ownerDoc.documentURI)) {
reason = "unwanted";
} else if (/e=harmfulBlocked/.test(ownerDoc.documentURI)) {
reason = "harmful";
}
let docShell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
sendAsyncMessage("Browser:SiteBlockedError", {
location: ownerDoc.location.href,
reason,
elementId: targetElement.getAttribute("id"),
isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView),
blockedInfo: getSiteBlockedErrorDetails(docShell),
});
},
onAboutNetError(event, documentURI) {
let elmId = event.originalTarget.getAttribute("id");
if (elmId == "returnButton") {
sendAsyncMessage("Browser:SSLErrorGoBack", {});
return;
}
if (elmId != "errorTryAgain" || !/e=netOffline/.test(documentURI)) {
return;
}
// browser front end will handle clearing offline mode and refreshing
// the page *if* we're in offline mode now. Otherwise let the error page
// handle the click.
if (Services.io.offline) {
event.preventDefault();
sendAsyncMessage("Browser:EnableOnlineMode", {});
}
},
/**
* Extracts linkNode and href for the current click target.
*
* @param event
* The click event.
* @return [href, linkNode, linkPrincipal].
*
* @note linkNode will be null if the click wasn't on an anchor
* element. This includes SVG links, because callers expect |node|
* to behave like an <a> element, which SVG links (XLink) don't.
*/
_hrefAndLinkNodeForClickEvent(event) {
function isHTMLLink(aNode) {
// Be consistent with what nsContextMenu.js does.
return ((aNode instanceof content.HTMLAnchorElement && aNode.href) ||
(aNode instanceof content.HTMLAreaElement && aNode.href) ||
aNode instanceof content.HTMLLinkElement);
}
let node = event.target;
while (node && !isHTMLLink(node)) {
node = node.parentNode;
}
if (node)
return [node.href, node, node.ownerDocument.nodePrincipal];
// If there is no linkNode, try simple XLink.
let href, baseURI;
node = event.target;
while (node && !href) {
if (node.nodeType == content.Node.ELEMENT_NODE &&
(node.localName == "a" ||
node.namespaceURI == "http://www.w3.org/1998/Math/MathML")) {
href = node.getAttribute("href") ||
node.getAttributeNS("http://www.w3.org/1999/xlink", "href");
if (href) {
baseURI = node.ownerDocument.baseURIObject;
break;
}
}
node = node.parentNode;
}
// In case of XLink, we don't return the node we got href from since
// callers expect <a>-like elements.
// Note: makeURI() will throw if aUri is not a valid URI.
return [href ? Services.io.newURI(href, null, baseURI).spec : null, null,
node && node.ownerDocument.nodePrincipal];
}
};
ClickEventHandler.init();
ContentLinkHandler.init(this);
ContentMetaHandler.init(this);
// TODO: Load this lazily so the JSM is run only if a relevant event/message fires.
var pluginContent = new PluginContent(global);
addEventListener("DOMWindowFocus", function(event) {
sendAsyncMessage("DOMWindowFocus", {});
}, false);
// We use this shim so that ContentWebRTC.jsm will not be loaded until
// it is actually needed.
var ContentWebRTCShim = message => ContentWebRTC.receiveMessage(message);
addMessageListener("rtcpeer:Allow", ContentWebRTCShim);
addMessageListener("rtcpeer:Deny", ContentWebRTCShim);
addMessageListener("webrtc:Allow", ContentWebRTCShim);
addMessageListener("webrtc:Deny", ContentWebRTCShim);
addMessageListener("webrtc:StopSharing", ContentWebRTCShim);
addEventListener("pageshow", function(event) {
if (event.target == content.document) {
sendAsyncMessage("PageVisibility:Show", {
persisted: event.persisted,
});
}
});
addEventListener("pagehide", function(event) {
if (event.target == content.document) {
sendAsyncMessage("PageVisibility:Hide", {
persisted: event.persisted,
});
}
});
var PageMetadataMessenger = {
init() {
addMessageListener("PageMetadata:GetPageData", this);
addMessageListener("PageMetadata:GetMicroformats", this);
},
receiveMessage(message) {
switch (message.name) {
case "PageMetadata:GetPageData": {
let target = contextMenu.getTarget(message);
let result = PageMetadata.getData(content.document, target);
sendAsyncMessage("PageMetadata:PageDataResult", result);
break;
}
case "PageMetadata:GetMicroformats": {
let target = contextMenu.getTarget(message);
let result = PageMetadata.getMicroformats(content.document, target);
sendAsyncMessage("PageMetadata:MicroformatsResult", result);
break;
}
}
}
};
PageMetadataMessenger.init();
addMessageListener("Bookmarks:GetPageDetails", (message) => {
let doc = content.document;
let isErrorPage = /^about:(neterror|certerror|blocked)/.test(doc.documentURI);
sendAsyncMessage("Bookmarks:GetPageDetails:Result",
{ isErrorPage,
description: PlacesUIUtils.getDescriptionFromDocument(doc) });
});
var LightWeightThemeWebInstallListener = {
_previewWindow: null,
init() {
addEventListener("InstallBrowserTheme", this, false, true);
addEventListener("PreviewBrowserTheme", this, false, true);
addEventListener("ResetBrowserThemePreview", this, false, true);
},
handleEvent(event) {
switch (event.type) {
case "InstallBrowserTheme": {
sendAsyncMessage("LightWeightThemeWebInstaller:Install", {
baseURI: event.target.baseURI,
themeData: event.target.getAttribute("data-browsertheme"),
});
break;
}
case "PreviewBrowserTheme": {
sendAsyncMessage("LightWeightThemeWebInstaller:Preview", {
baseURI: event.target.baseURI,
themeData: event.target.getAttribute("data-browsertheme"),
});
this._previewWindow = event.target.ownerGlobal;
this._previewWindow.addEventListener("pagehide", this, true);
break;
}
case "pagehide": {
sendAsyncMessage("LightWeightThemeWebInstaller:ResetPreview");
this._resetPreviewWindow();
break;
}
case "ResetBrowserThemePreview": {
if (this._previewWindow) {
sendAsyncMessage("LightWeightThemeWebInstaller:ResetPreview",
{baseURI: event.target.baseURI});
this._resetPreviewWindow();
}
break;
}
}
},
_resetPreviewWindow() {
this._previewWindow.removeEventListener("pagehide", this, true);
this._previewWindow = null;
}
};
LightWeightThemeWebInstallListener.init();
var PageInfoListener = {
init() {
addMessageListener("PageInfo:getData", this);
},
receiveMessage(message) {
let strings = message.data.strings;
let window;
let document;
let frameOuterWindowID = message.data.frameOuterWindowID;
// If inside frame then get the frame's window and document.
if (frameOuterWindowID != undefined) {
window = Services.wm.getOuterWindowWithId(frameOuterWindowID);
document = window.document;
} else {
window = content.window;
document = content.document;
}
let pageInfoData = {metaViewRows: this.getMetaInfo(document),
docInfo: this.getDocumentInfo(document),
feeds: this.getFeedsInfo(document, strings),
windowInfo: this.getWindowInfo(window)};
sendAsyncMessage("PageInfo:data", pageInfoData);
// Separate step so page info dialog isn't blank while waiting for this to finish.
this.getMediaInfo(document, window, strings);
},
getMetaInfo(document) {
let metaViewRows = [];
// Get the meta tags from the page.
let metaNodes = document.getElementsByTagName("meta");
for (let metaNode of metaNodes) {
metaViewRows.push([metaNode.name || metaNode.httpEquiv || metaNode.getAttribute("property"),
metaNode.content]);
}
return metaViewRows;
},
getWindowInfo(window) {
let windowInfo = {};
windowInfo.isTopWindow = window == window.top;
let hostName = null;
try {
hostName = Services.io.newURI(window.location.href).displayHost;
} catch (exception) { }
windowInfo.hostName = hostName;
return windowInfo;
},
getDocumentInfo(document) {
let docInfo = {};
docInfo.title = document.title;
docInfo.location = document.location.toString();
try {
docInfo.location = Services.io.newURI(document.location.toString()).displaySpec;
} catch (exception) { }
docInfo.referrer = document.referrer;
try {
if (document.referrer) {
docInfo.referrer = Services.io.newURI(document.referrer).displaySpec;
}
} catch (exception) { }
docInfo.compatMode = document.compatMode;
docInfo.contentType = document.contentType;
docInfo.characterSet = document.characterSet;
docInfo.lastModified = document.lastModified;
docInfo.principal = document.nodePrincipal;
let documentURIObject = {};
documentURIObject.spec = document.documentURIObject.spec;
docInfo.documentURIObject = documentURIObject;
docInfo.isContentWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(content);
return docInfo;
},
getFeedsInfo(document, strings) {
let feeds = [];
// Get the feeds from the page.
let linkNodes = document.getElementsByTagName("link");
let length = linkNodes.length;
for (let i = 0; i < length; i++) {
let link = linkNodes[i];
if (!link.href) {
continue;
}
let rel = link.rel && link.rel.toLowerCase();
let rels = {};
if (rel) {
for (let relVal of rel.split(/\s+/)) {
rels[relVal] = true;
}
}
if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) {
let type = Feeds.isValidFeed(link, document.nodePrincipal, "feed" in rels);
if (type) {
type = strings[type] || strings["application/rss+xml"];
feeds.push([link.title, type, link.href]);
}
}
}
return feeds;
},
// Only called once to get the media tab's media elements from the content page.
getMediaInfo(document, window, strings) {
let frameList = this.goThroughFrames(document, window);
this.processFrames(document, frameList, strings);
},
goThroughFrames(document, window) {
let frameList = [document];
if (window && window.frames.length > 0) {
let num = window.frames.length;
for (let i = 0; i < num; i++) {
// Recurse through the frames.
frameList.concat(this.goThroughFrames(window.frames[i].document,
window.frames[i]));
}
}
return frameList;
},
async processFrames(document, frameList, strings) {
let nodeCount = 0;
for (let doc of frameList) {
let iterator = doc.createTreeWalker(doc, content.NodeFilter.SHOW_ELEMENT);
// Goes through all the elements on the doc. imageViewRows takes only the media elements.
while (iterator.nextNode()) {
let mediaItems = this.getMediaItems(document, strings, iterator.currentNode);
if (mediaItems.length) {
sendAsyncMessage("PageInfo:mediaData",
{mediaItems, isComplete: false});
}
if (++nodeCount % 500 == 0) {
// setTimeout every 500 elements so we don't keep blocking the content process.
await new Promise(resolve => setTimeout(resolve, 10));
}
}
}
// Send that page info media fetching has finished.
sendAsyncMessage("PageInfo:mediaData", {isComplete: true});
},
getMediaItems(document, strings, elem) {
// Check for images defined in CSS (e.g. background, borders)
let computedStyle = elem.ownerGlobal.getComputedStyle(elem);
// A node can have multiple media items associated with it - for example,
// multiple background images.
let mediaItems = [];
let addImage = (url, type, alt, el, isBg) => {
let element = this.serializeElementInfo(document, url, type, alt, el, isBg);
mediaItems.push([url, type, alt, element, isBg]);
};
if (computedStyle) {
let addImgFunc = (label, val) => {
if (val.primitiveType == content.CSSPrimitiveValue.CSS_URI) {
addImage(val.getStringValue(), label, strings.notSet, elem, true);
} else if (val.primitiveType == content.CSSPrimitiveValue.CSS_STRING) {
// This is for -moz-image-rect.
// TODO: Reimplement once bug 714757 is fixed.
let strVal = val.getStringValue();
if (strVal.search(/^.*url\(\"?/) > -1) {
let url = strVal.replace(/^.*url\(\"?/, "").replace(/\"?\).*$/, "");
addImage(url, label, strings.notSet, elem, true);
}
} else if (val.cssValueType == content.CSSValue.CSS_VALUE_LIST) {
// Recursively resolve multiple nested CSS value lists.
for (let i = 0; i < val.length; i++) {
addImgFunc(label, val.item(i));
}
}
};
addImgFunc(strings.mediaBGImg, computedStyle.getPropertyCSSValue("background-image"));
addImgFunc(strings.mediaBorderImg, computedStyle.getPropertyCSSValue("border-image-source"));
addImgFunc(strings.mediaListImg, computedStyle.getPropertyCSSValue("list-style-image"));
addImgFunc(strings.mediaCursor, computedStyle.getPropertyCSSValue("cursor"));
}
// One swi^H^H^Hif-else to rule them all.
if (elem instanceof content.HTMLImageElement) {
addImage(elem.src, strings.mediaImg,
(elem.hasAttribute("alt")) ? elem.alt : strings.notSet, elem, false);
} else if (elem instanceof content.SVGImageElement) {
try {
// Note: makeURLAbsolute will throw if either the baseURI is not a valid URI
// or the URI formed from the baseURI and the URL is not a valid URI.
if (elem.href.baseVal) {
let href = Services.io.newURI(elem.href.baseVal, null, Services.io.newURI(elem.baseURI)).spec;
addImage(href, strings.mediaImg, "", elem, false);
}
} catch (e) { }
} else if (elem instanceof content.HTMLVideoElement) {
addImage(elem.currentSrc, strings.mediaVideo, "", elem, false);
} else if (elem instanceof content.HTMLAudioElement) {
addImage(elem.currentSrc, strings.mediaAudio, "", elem, false);
} else if (elem instanceof content.HTMLLinkElement) {
if (elem.rel && /\bicon\b/i.test(elem.rel)) {
addImage(elem.href, strings.mediaLink, "", elem, false);
}
} else if (elem instanceof content.HTMLInputElement || elem instanceof content.HTMLButtonElement) {
if (elem.type.toLowerCase() == "image") {
addImage(elem.src, strings.mediaInput,
(elem.hasAttribute("alt")) ? elem.alt : strings.notSet, elem, false);
}
} else if (elem instanceof content.HTMLObjectElement) {
addImage(elem.data, strings.mediaObject, this.getValueText(elem), elem, false);
} else if (elem instanceof content.HTMLEmbedElement) {
addImage(elem.src, strings.mediaEmbed, "", elem, false);
}
return mediaItems;
},
/**
* Set up a JSON element object with all the instanceOf and other infomation that
* makePreview in pageInfo.js uses to figure out how to display the preview.
*/
serializeElementInfo(document, url, type, alt, item, isBG) {
let result = {};
let imageText;
if (!isBG &&
!(item instanceof content.SVGImageElement) &&
!(document instanceof content.ImageDocument)) {
imageText = item.title || item.alt;
if (!imageText && !(item instanceof content.HTMLImageElement)) {
imageText = this.getValueText(item);
}
}
result.imageText = imageText;
result.longDesc = item.longDesc;
result.numFrames = 1;
if (item instanceof content.HTMLObjectElement ||
item instanceof content.HTMLEmbedElement ||
item instanceof content.HTMLLinkElement) {
result.mimeType = item.type;
}
if (!result.mimeType && !isBG && item instanceof Ci.nsIImageLoadingContent) {
// Interface for image loading content.
let imageRequest = item.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
if (imageRequest) {
result.mimeType = imageRequest.mimeType;
let image = !(imageRequest.imageStatus & imageRequest.STATUS_ERROR) && imageRequest.image;
if (image) {
result.numFrames = image.numFrames;
}
}
}
// If we have a data url, get the MIME type from the url.
if (!result.mimeType && url.startsWith("data:")) {
let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url);
if (dataMimeType)
result.mimeType = dataMimeType[1].toLowerCase();
}
result.HTMLLinkElement = item instanceof content.HTMLLinkElement;
result.HTMLInputElement = item instanceof content.HTMLInputElement;
result.HTMLImageElement = item instanceof content.HTMLImageElement;
result.HTMLObjectElement = item instanceof content.HTMLObjectElement;
result.SVGImageElement = item instanceof content.SVGImageElement;
result.HTMLVideoElement = item instanceof content.HTMLVideoElement;
result.HTMLAudioElement = item instanceof content.HTMLAudioElement;
if (isBG) {
// Items that are showing this image as a background
// image might not necessarily have a width or height,
// so we'll dynamically generate an image and send up the
// natural dimensions.
let img = content.document.createElement("img");
img.src = url;
result.naturalWidth = img.naturalWidth;
result.naturalHeight = img.naturalHeight;
} else {
// Otherwise, we can use the current width and height
// of the image.
result.width = item.width;
result.height = item.height;
}
if (item instanceof content.SVGImageElement) {
result.SVGImageElementWidth = item.width.baseVal.value;
result.SVGImageElementHeight = item.height.baseVal.value;
}
result.baseURI = item.baseURI;
return result;
},
// Other Misc Stuff
// Modified from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html
// parse a node to extract the contents of the node
getValueText(node) {
let valueText = "";
// Form input elements don't generally contain information that is useful to our callers, so return nothing.
if (node instanceof content.HTMLInputElement ||
node instanceof content.HTMLSelectElement ||
node instanceof content.HTMLTextAreaElement) {
return valueText;
}
// Otherwise recurse for each child.
let length = node.childNodes.length;
for (let i = 0; i < length; i++) {
let childNode = node.childNodes[i];
let nodeType = childNode.nodeType;
// Text nodes are where the goods are.
if (nodeType == content.Node.TEXT_NODE) {
valueText += " " + childNode.nodeValue;
} else if (nodeType == content.Node.ELEMENT_NODE) {
// And elements can have more text inside them.
// Images are special, we want to capture the alt text as if the image weren't there.
if (childNode instanceof content.HTMLImageElement) {
valueText += " " + this.getAltText(childNode);
} else {
valueText += " " + this.getValueText(childNode);
}
}
}
return this.stripWS(valueText);
},
// Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html.
// Traverse the tree in search of an img or area element and grab its alt tag.
getAltText(node) {
let altText = "";
if (node.alt) {
return node.alt;
}
let length = node.childNodes.length;
for (let i = 0; i < length; i++) {
if ((altText = this.getAltText(node.childNodes[i]) != undefined)) { // stupid js warning...
return altText;
}
}
return "";
},
// Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html.
// Strip leading and trailing whitespace, and replace multiple consecutive whitespace characters with a single space.
stripWS(text) {
let middleRE = /\s+/g;
let endRE = /(^\s+)|(\s+$)/g;
text = text.replace(middleRE, " ");
return text.replace(endRE, "");
}
};
PageInfoListener.init();
let OfflineApps = {
_docId: 0,
_docIdMap: new Map(),
_docManifestSet: new Set(),
_observerAdded: false,
registerWindow(aWindow) {
if (!this._observerAdded) {
this._observerAdded = true;
Services.obs.addObserver(this, "offline-cache-update-completed", true);
}
let manifestURI = this._getManifestURI(aWindow);
this._docManifestSet.add(manifestURI.spec);
},
handleEvent(event) {
if (event.type == "MozApplicationManifest") {
this.offlineAppRequested(event.originalTarget.defaultView);
}
},
_getManifestURI(aWindow) {
if (!aWindow.document.documentElement)
return null;
var attr = aWindow.document.documentElement.getAttribute("manifest");
if (!attr)
return null;
try {
return Services.io.newURI(attr, aWindow.document.characterSet,
Services.io.newURI(aWindow.location.href));
} catch (e) {
return null;
}
},
offlineAppRequested(aContentWindow) {
this.registerWindow(aContentWindow);
if (!Services.prefs.getBoolPref("browser.offline-apps.notify")) {
return;
}
let currentURI = aContentWindow.document.documentURIObject;
// don't bother showing UI if the user has already made a decision
if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION)
return;
try {
if (Services.prefs.getBoolPref("offline-apps.allow_by_default")) {
// all pages can use offline capabilities, no need to ask the user
return;
}
} catch (e) {
// this pref isn't set by default, ignore failures
}
let docId = ++this._docId;
this._docIdMap.set(docId, Cu.getWeakReference(aContentWindow.document));
sendAsyncMessage("OfflineApps:RequestPermission", {
uri: currentURI.spec,
docId,
});
},
_startFetching(aDocument) {
if (!aDocument.documentElement)
return;
let manifestURI = this._getManifestURI(aDocument.defaultView);
if (!manifestURI)
return;
var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"].
getService(Ci.nsIOfflineCacheUpdateService);
updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject,
aDocument.nodePrincipal, aDocument.defaultView);
},
receiveMessage(aMessage) {
if (aMessage.name == "OfflineApps:StartFetching") {
let doc = this._docIdMap.get(aMessage.data.docId);
doc = doc && doc.get();
if (doc) {
this._startFetching(doc);
}
this._docIdMap.delete(aMessage.data.docId);
}
},
observe(aSubject, aTopic, aState) {
if (aTopic == "offline-cache-update-completed") {
let cacheUpdate = aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate);
let uri = cacheUpdate.manifestURI;
if (uri && this._docManifestSet.has(uri.spec)) {
sendAsyncMessage("OfflineApps:CheckUsage", {uri: uri.spec});
}
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
};
addEventListener("MozApplicationManifest", OfflineApps, false);
addMessageListener("OfflineApps:StartFetching", OfflineApps);