fune/browser/base/content/content.js
Masatoshi Kimura 8ed4a80a4e Bug 1322874 - Get rid of nsIURI.originCharset. r=valentin.gosu
nsIURI.originCharset had two use cases:
 1) Dealing with the spec-incompliant feature of escapes in the hash
    (reference) part of the URL.
 2) For UI display of non-UTF-8 URLs.

For hash part handling, we use the document charset instead. For pretty
display of query strings on legacy-encoded pages, we no longer care to them
(see bug 817374 comment 18).

Also, the URL Standard has no concept of "origin charset". This patch
removes nsIURI.originCharset for reducing complexity and spec compliance.

MozReview-Commit-ID: 3tHd0VCWSqF

--HG--
extra : rebase_source : b2caa01f75e5dd26078a7679fd7caa319a65af14
2017-08-02 20:43:30 +09:00

1619 lines
55 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.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
"resource:///modules/ContentLinkHandler.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ContentWebRTC",
"resource:///modules/ContentWebRTC.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SpellCheckHelper",
"resource://gre/modules/InlineSpellChecker.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "InlineSpellCheckerContent",
"resource://gre/modules/InlineSpellCheckerContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
"resource://gre/modules/LoginManagerContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory",
"resource://gre/modules/LoginManagerContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
"resource://gre/modules/InsecurePasswordUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
"resource:///modules/PluginContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
"resource:///modules/FormSubmitObserver.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
"resource://gre/modules/PageMetadata.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUIUtils",
"resource:///modules/PlacesUIUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://gre/modules/sessionstore/Utils.jsm");
XPCOMUtils.defineLazyGetter(this, "PageMenuChild", function() {
let tmp = {};
Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
return new tmp.PageMenuChild();
});
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
"resource:///modules/Feeds.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "findCssSelector",
"resource://gre/modules/css-selector.js");
Cu.importGlobalProperties(["URL"]);
// TabChildGlobal
var global = this;
// Load the form validation popup handler
var formSubmitObserver = new FormSubmitObserver(content, this);
addMessageListener("ContextMenu:DoCustomCommand", function(message) {
E10SUtils.wrapHandlingUserInput(
content, message.data.handlingUserInput,
() => PageMenuChild.executeMenu(message.data.generatedItemId));
});
addMessageListener("RemoteLogins:fillForm", function(message) {
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);
});
var handleContentContextMenu = function(event) {
let defaultPrevented = event.defaultPrevented;
if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
let plugin = null;
try {
plugin = event.target.QueryInterface(Ci.nsIObjectLoadingContent);
} catch (e) {}
if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
// Don't open a context menu for plugins.
return;
}
defaultPrevented = false;
}
if (defaultPrevented)
return;
let addonInfo = {};
let subject = {
event,
addonInfo,
};
subject.wrappedJSObject = subject;
Services.obs.notifyObservers(subject, "content-contextmenu");
let doc = event.target.ownerDocument;
let docLocation = doc.mozDocumentURIIfNotForErrorPages;
docLocation = docLocation && docLocation.spec;
let charSet = doc.characterSet;
let baseURI = doc.baseURI;
let referrer = doc.referrer;
let referrerPolicy = doc.referrerPolicy;
let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
let loginFillInfo = LoginManagerContent.getFieldContext(event.target);
// The same-origin check will be done in nsContextMenu.openLinkInTab.
let parentAllowsMixedContent = !!docShell.mixedContentChannel;
// get referrer attribute from clicked link and parse it
let referrerAttrValue = Services.netUtils.parseAttributePolicyString(event.target.
getAttribute("referrerpolicy"));
if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
referrerPolicy = referrerAttrValue;
}
let disableSetDesktopBg = null;
// Media related cache info parent needs for saving
let contentType = null;
let contentDisposition = null;
if (event.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
event.target instanceof Ci.nsIImageLoadingContent &&
event.target.currentURI) {
disableSetDesktopBg = disableSetDesktopBackground(event.target);
try {
let imageCache =
Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
.getImgCacheForDocument(doc);
let props =
imageCache.findEntryProperties(event.target.currentURI, doc);
try {
contentType = props.get("type", Ci.nsISupportsCString).data;
} catch (e) {}
try {
contentDisposition =
props.get("content-disposition", Ci.nsISupportsCString).data;
} catch (e) {}
} catch (e) {}
}
let selectionInfo = BrowserUtils.getSelectionDetails(content);
let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
let userContextId = loadContext.originAttributes.userContextId;
let popupNodeSelectors = getNodeSelectors(event.target);
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
let editFlags = SpellCheckHelper.isEditable(event.target, content);
let spellInfo;
if (editFlags &
(SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
spellInfo =
InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
}
// Set the event target first as the copy image command needs it to
// determine what was context-clicked on. Then, update the state of the
// commands on the context menu.
docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
.setCommandNode(event.target);
event.target.ownerGlobal.updateCommands("contentcontextmenu");
let customMenuItems = PageMenuChild.build(event.target);
let principal = doc.nodePrincipal;
sendRpcMessage("contextmenu",
{ editFlags, spellInfo, customMenuItems, addonInfo,
principal, docLocation, charSet, baseURI, referrer,
referrerPolicy, contentType, contentDisposition,
frameOuterWindowID, selectionInfo, disableSetDesktopBg,
loginFillInfo, parentAllowsMixedContent, userContextId,
popupNodeSelectors,
}, {
event,
popupNode: event.target,
});
} else {
// Break out to the parent window and pass the add-on info along
let browser = docShell.chromeEventHandler;
let mainWin = browser.ownerGlobal;
mainWin.setContextMenuContentData({
isRemote: false,
event,
popupNode: event.target,
popupNodeSelectors,
browser,
addonInfo,
documentURIObject: doc.documentURIObject,
docLocation,
charSet,
referrer,
referrerPolicy,
contentType,
contentDisposition,
selectionInfo,
disableSetDesktopBackground: disableSetDesktopBg,
loginFillInfo,
parentAllowsMixedContent,
userContextId,
});
}
}
Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService)
.addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
// 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."];
const PREF_SSL_IMPACT = PREF_SSL_IMPACT_ROOTS.reduce((prefs, root) => {
return prefs.concat(Services.prefs.getChildList(root));
}, []);
/**
* Retrieve the array of CSS selectors corresponding to the provided node. The first item
* of the array is the selector of the node in its owner document. Additional items are
* used if the node is inside a frame, each representing the CSS selector for finding the
* frame element in its parent document.
*
* This format is expected by DevTools in order to handle the Inspect Node context menu
* item.
*
* @param {Node}
* The node for which the CSS selectors should be computed
* @return {Array} array of css selectors (strings).
*/
function getNodeSelectors(node) {
let selectors = [];
while (node) {
selectors.push(findCssSelector(node));
node = node.ownerGlobal.frameElement;
}
return selectors;
}
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 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;
},
}
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() {
for (let prefName of PREF_SSL_IMPACT) {
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 (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";
}
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);
// 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 = message.objects.target;
let result = PageMetadata.getData(content.document, target);
sendAsyncMessage("PageMetadata:PageDataResult", result);
break;
}
case "PageMetadata:GetMicroformats": {
let target = message.objects.target;
let result = PageMetadata.getMicroformats(content.document, target);
sendAsyncMessage("PageMetadata:MicroformatsResult", result);
break;
}
}
}
}
PageMetadataMessenger.init();
addEventListener("ActivateSocialFeature", function(aEvent) {
let document = content.document;
let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
if (!dwu.isHandlingUserInput) {
Cu.reportError("attempt to activate provider without user input from " + document.nodePrincipal.origin);
return;
}
let node = aEvent.target;
let ownerDocument = node.ownerDocument;
let data = node.getAttribute("data-service");
if (data) {
try {
data = JSON.parse(data);
} catch (e) {
Cu.reportError("Social Service manifest parse error: " + e);
return;
}
} else {
Cu.reportError("Social Service manifest not available");
return;
}
sendAsyncMessage("Social:Activation", {
url: ownerDocument.location.href,
origin: ownerDocument.nodePrincipal.origin,
manifest: data,
triggeringPrincipal: Utils.serializePrincipal(ownerDocument.nodePrincipal),
});
}, true, true);
addMessageListener("ContextMenu:SaveVideoFrameAsImage", (message) => {
let video = message.objects.target;
let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
let ctxDraw = canvas.getContext("2d");
ctxDraw.drawImage(video, 0, 0);
sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", {
dataURL: canvas.toDataURL("image/jpeg", ""),
});
});
addMessageListener("ContextMenu:MediaCommand", (message) => {
E10SUtils.wrapHandlingUserInput(
content, message.data.handlingUserInput,
() => {
let media = message.objects.element;
switch (message.data.command) {
case "play":
media.play();
break;
case "pause":
media.pause();
break;
case "loop":
media.loop = !media.loop;
break;
case "mute":
media.muted = true;
break;
case "unmute":
media.muted = false;
break;
case "playbackRate":
media.playbackRate = message.data.data;
break;
case "hidecontrols":
media.removeAttribute("controls");
break;
case "showcontrols":
media.setAttribute("controls", "true");
break;
case "fullscreen":
if (content.document.fullscreenEnabled)
media.requestFullscreen();
break;
}
});
});
addMessageListener("ContextMenu:Canvas:ToBlobURL", (message) => {
message.objects.target.toBlob((blob) => {
let blobURL = URL.createObjectURL(blob);
sendAsyncMessage("ContextMenu:Canvas:ToBlobURL:Result", { blobURL });
});
});
addMessageListener("ContextMenu:ReloadFrame", (message) => {
message.objects.target.ownerDocument.location.reload();
});
addMessageListener("ContextMenu:ReloadImage", (message) => {
let image = message.objects.target;
if (image instanceof Ci.nsIImageLoadingContent)
image.forceReload();
});
addMessageListener("ContextMenu:BookmarkFrame", (message) => {
let frame = message.objects.target.ownerDocument;
sendAsyncMessage("ContextMenu:BookmarkFrame:Result",
{ title: frame.title,
description: PlacesUIUtils.getDescriptionFromDocument(frame) });
});
addMessageListener("ContextMenu:SearchFieldBookmarkData", (message) => {
let node = message.objects.target;
let charset = node.ownerDocument.characterSet;
let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
let formURI = Services.io.newURI(node.form.getAttribute("action"), charset,
formBaseURI);
let spec = formURI.spec;
let isURLEncoded =
(node.form.method.toUpperCase() == "POST"
&& (node.form.enctype == "application/x-www-form-urlencoded" ||
node.form.enctype == ""));
let title = node.ownerDocument.title;
let description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument);
let formData = [];
function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) {
if (aIsFormUrlEncoded) {
return escape(aName + "=" + aValue);
}
return escape(aName) + "=" + escape(aValue);
}
for (let el of node.form.elements) {
if (!el.type) // happens with fieldsets
continue;
if (el == node) {
formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) :
// Don't escape "%s", just append
escapeNameValuePair(el.name, "", false) + "%s");
continue;
}
let type = el.type.toLowerCase();
if (((el instanceof content.HTMLInputElement && el.mozIsTextField(true)) ||
type == "hidden" || type == "textarea") ||
((type == "checkbox" || type == "radio") && el.checked)) {
formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded));
} else if (el instanceof content.HTMLSelectElement && el.selectedIndex >= 0) {
for (let j = 0; j < el.options.length; j++) {
if (el.options[j].selected)
formData.push(escapeNameValuePair(el.name, el.options[j].value,
isURLEncoded));
}
}
}
let postData;
if (isURLEncoded)
postData = formData.join("&");
else {
let separator = spec.includes("?") ? "&" : "?";
spec += separator + formData.join("&");
}
sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result",
{ spec, title, description, postData, charset });
});
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();
function disableSetDesktopBackground(aTarget) {
// Disable the Set as Desktop Background menu item if we're still trying
// to load the image or the load failed.
if (!(aTarget instanceof Ci.nsIImageLoadingContent))
return true;
if (("complete" in aTarget) && !aTarget.complete)
return true;
if (aTarget.currentURI.schemeIs("javascript"))
return true;
let request = aTarget.QueryInterface(Ci.nsIImageLoadingContent)
.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
if (!request)
return true;
return false;
}
addMessageListener("ContextMenu:SetAsDesktopBackground", (message) => {
let target = message.objects.target;
// Paranoia: check disableSetDesktopBackground again, in case the
// image changed since the context menu was initiated.
let disable = disableSetDesktopBackground(target);
if (!disable) {
try {
BrowserUtils.urlSecurityCheck(target.currentURI.spec, target.ownerDocument.nodePrincipal);
let canvas = content.document.createElement("canvas");
canvas.width = target.naturalWidth;
canvas.height = target.naturalHeight;
let ctx = canvas.getContext("2d");
ctx.drawImage(target, 0, 0);
let dataUrl = canvas.toDataURL();
sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
{ dataUrl });
} catch (e) {
Cu.reportError(e);
disable = true;
}
}
if (disable)
sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result", { disable });
});
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 imageElement = message.objects.imageElement;
let pageInfoData = {metaViewRows: this.getMetaInfo(document),
docInfo: this.getDocumentInfo(document),
feeds: this.getFeedsInfo(document, strings),
windowInfo: this.getWindowInfo(window),
imageInfo: this.getImageInfo(imageElement)};
sendAsyncMessage("PageInfo:data", pageInfoData);
// Separate step so page info dialog isn't blank while waiting for this to finish.
this.getMediaInfo(document, window, strings);
},
getImageInfo(imageElement) {
let imageInfo = null;
if (imageElement) {
imageInfo = {
currentSrc: imageElement.currentSrc,
width: imageElement.width,
height: imageElement.height,
imageText: imageElement.title || imageElement.alt
};
}
return imageInfo;
},
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 = window.location.host;
} catch (exception) { }
windowInfo.hostName = hostName;
return windowInfo;
},
getDocumentInfo(document) {
let docInfo = {};
docInfo.title = document.title;
docInfo.location = document.location.toString();
docInfo.referrer = document.referrer;
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);