342 строки
13 KiB
JavaScript
342 строки
13 KiB
JavaScript
|
/*
|
||
|
* 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/.
|
||
|
*/
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||
|
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
||
|
ChromeUtils.import("resource://gre/modules/E10SUtils.jsm");
|
||
|
|
||
|
ChromeUtils.defineModuleGetter(this, "AboutNewTab",
|
||
|
"resource:///modules/AboutNewTab.jsm");
|
||
|
|
||
|
const TOPIC_APP_QUIT = "quit-application-granted";
|
||
|
const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
|
||
|
const TOPIC_CONTENT_DOCUMENT_INTERACTIVE = "content-document-interactive";
|
||
|
|
||
|
// Automated tests ensure packaged locales are in this list. Copied output of:
|
||
|
// https://github.com/mozilla/activity-stream/blob/master/bin/render-activity-stream-html.js
|
||
|
const ACTIVITY_STREAM_LOCALES = "en-US ach an ar ast az be bg bn-BD bn-IN br bs ca cak crh cs cy da de dsb el en-CA en-GB eo es-AR es-CL es-ES es-MX et eu fa ff fi fr fy-NL ga-IE gd gl gn gu-IN he hi-IN hr hsb hu hy-AM ia id it ja ja-JP-mac ka kab kk km kn ko lij lo lt ltg lv mai mk ml mr ms my nb-NO ne-NP nl nn-NO oc pa-IN pl pt-BR pt-PT rm ro ru si sk sl sq sr sv-SE ta te th tl tr uk ur uz vi zh-CN zh-TW".split(" ");
|
||
|
|
||
|
const ABOUT_URL = "about:newtab";
|
||
|
const BASE_URL = "resource://activity-stream/";
|
||
|
const ACTIVITY_STREAM_PAGES = new Set(["home", "newtab", "welcome"]);
|
||
|
|
||
|
const IS_MAIN_PROCESS = Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
|
||
|
const IS_PRIVILEGED_PROCESS = Services.appinfo.remoteType === E10SUtils.PRIVILEGED_REMOTE_TYPE;
|
||
|
|
||
|
const IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;
|
||
|
|
||
|
const PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS = "browser.tabs.remote.separatePrivilegedContentProcess";
|
||
|
const PREF_ACTIVITY_STREAM_PRERENDER_ENABLED = "browser.newtabpage.activity-stream.prerender";
|
||
|
const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
|
||
|
|
||
|
function AboutNewTabService() {
|
||
|
Services.obs.addObserver(this, TOPIC_APP_QUIT);
|
||
|
Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
|
||
|
Services.prefs.addObserver(PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS, this);
|
||
|
Services.prefs.addObserver(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED, this);
|
||
|
if (!IS_RELEASE_OR_BETA) {
|
||
|
Services.prefs.addObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
|
||
|
}
|
||
|
|
||
|
// More initialization happens here
|
||
|
this.toggleActivityStream(true);
|
||
|
this.initialized = true;
|
||
|
|
||
|
if (IS_MAIN_PROCESS) {
|
||
|
AboutNewTab.init();
|
||
|
} else if (IS_PRIVILEGED_PROCESS) {
|
||
|
Services.obs.addObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* A service that allows for the overriding, at runtime, of the newtab page's url.
|
||
|
*
|
||
|
* There is tight coupling with browser/about/AboutRedirector.cpp.
|
||
|
*
|
||
|
* 1. Browser chrome access:
|
||
|
*
|
||
|
* When the user issues a command to open a new tab page, usually clicking a button
|
||
|
* in the browser chrome or using shortcut keys, the browser chrome code invokes the
|
||
|
* service to obtain the newtab URL. It then loads that URL in a new tab.
|
||
|
*
|
||
|
* When not overridden, the default URL emitted by the service is "about:newtab".
|
||
|
* When overridden, it returns the overriden URL.
|
||
|
*
|
||
|
* 2. Redirector Access:
|
||
|
*
|
||
|
* When the URL loaded is about:newtab, the default behavior, or when entered in the
|
||
|
* URL bar, the redirector is hit. The service is then called to return the
|
||
|
* appropriate activity stream url based on prefs and locales.
|
||
|
*
|
||
|
* NOTE: "about:newtab" will always result in a default newtab page, and never an overridden URL.
|
||
|
*
|
||
|
* Access patterns:
|
||
|
*
|
||
|
* The behavior is different when accessing the service via browser chrome or via redirector
|
||
|
* largely to maintain compatibility with expectations of add-on developers.
|
||
|
*
|
||
|
* Loading a chrome resource, or an about: URL in the redirector with either the
|
||
|
* LOAD_NORMAL or LOAD_REPLACE flags yield unexpected behaviors, so a roundtrip
|
||
|
* to the redirector from browser chrome is avoided.
|
||
|
*/
|
||
|
AboutNewTabService.prototype = {
|
||
|
|
||
|
_newTabURL: ABOUT_URL,
|
||
|
_activityStreamEnabled: false,
|
||
|
_activityStreamPrerender: false,
|
||
|
_activityStreamPath: "",
|
||
|
_activityStreamDebug: false,
|
||
|
_privilegedContentProcess: false,
|
||
|
_overridden: false,
|
||
|
willNotifyUser: false,
|
||
|
|
||
|
classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
|
||
|
QueryInterface: ChromeUtils.generateQI([
|
||
|
Ci.nsIAboutNewTabService,
|
||
|
Ci.nsIObserver
|
||
|
]),
|
||
|
_xpcom_categories: [{service: true}],
|
||
|
|
||
|
observe(subject, topic, data) {
|
||
|
switch (topic) {
|
||
|
case "nsPref:changed":
|
||
|
if (data === PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS) {
|
||
|
this._privilegedContentProcess = Services.prefs.getBoolPref(PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS);
|
||
|
} else if (data === PREF_ACTIVITY_STREAM_PRERENDER_ENABLED) {
|
||
|
this._activityStreamPrerender = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED);
|
||
|
this.notifyChange();
|
||
|
} else if (!IS_RELEASE_OR_BETA && data === PREF_ACTIVITY_STREAM_DEBUG) {
|
||
|
this._activityStreamDebug = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_DEBUG, false);
|
||
|
this.updatePrerenderedPath();
|
||
|
this.notifyChange();
|
||
|
}
|
||
|
break;
|
||
|
case TOPIC_CONTENT_DOCUMENT_INTERACTIVE: {
|
||
|
const win = subject.defaultView;
|
||
|
|
||
|
// It seems like "content-document-interactive" is triggered multiple
|
||
|
// times for a single window. The first event always seems to be an
|
||
|
// HTMLDocument object that contains a non-null window reference
|
||
|
// whereas the remaining ones seem to be proxied objects.
|
||
|
// https://searchfox.org/mozilla-central/rev/d2966246905102b36ef5221b0e3cbccf7ea15a86/devtools/server/actors/object.js#100-102
|
||
|
if (win === null) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// We use win.location.pathname instead of win.location.toString()
|
||
|
// because we want to account for URLs that contain the location hash
|
||
|
// property or query strings (e.g. about:newtab#foo, about:home?bar).
|
||
|
// Asserting here would be ideal, but this code path is also taken
|
||
|
// by the view-source:// scheme, so we should probably just bail out
|
||
|
// and do nothing.
|
||
|
if (!ACTIVITY_STREAM_PAGES.has(win.location.pathname)) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
const onLoaded = () => {
|
||
|
const debugString = this._activityStreamDebug ? "-dev" : "";
|
||
|
|
||
|
// This list must match any similar ones in render-activity-stream-html.js.
|
||
|
const scripts = [
|
||
|
"chrome://browser/content/contentSearchUI.js",
|
||
|
"chrome://browser/content/contentTheme.js",
|
||
|
`${BASE_URL}vendor/react${debugString}.js`,
|
||
|
`${BASE_URL}vendor/react-dom${debugString}.js`,
|
||
|
`${BASE_URL}vendor/prop-types.js`,
|
||
|
`${BASE_URL}vendor/react-intl.js`,
|
||
|
`${BASE_URL}vendor/redux.js`,
|
||
|
`${BASE_URL}vendor/react-redux.js`,
|
||
|
`${BASE_URL}prerendered/${this.activityStreamLocale}/activity-stream-strings.js`,
|
||
|
`${BASE_URL}data/content/activity-stream.bundle.js`
|
||
|
];
|
||
|
|
||
|
if (this._activityStreamPrerender) {
|
||
|
scripts.unshift(`${BASE_URL}prerendered/static/activity-stream-initial-state.js`);
|
||
|
}
|
||
|
|
||
|
for (let script of scripts) {
|
||
|
Services.scriptloader.loadSubScript(script, win); // Synchronous call
|
||
|
}
|
||
|
};
|
||
|
subject.addEventListener("DOMContentLoaded", onLoaded, {once: true});
|
||
|
|
||
|
// There is a possibility that DOMContentLoaded won't be fired. This
|
||
|
// unload event (which cannot be cancelled) will attempt to remove
|
||
|
// the listener for the DOMContentLoaded event.
|
||
|
const onUnloaded = () => {
|
||
|
subject.removeEventListener("DOMContentLoaded", onLoaded);
|
||
|
};
|
||
|
subject.addEventListener("unload", onUnloaded, {once: true});
|
||
|
break;
|
||
|
}
|
||
|
case TOPIC_APP_QUIT:
|
||
|
this.uninit();
|
||
|
if (IS_MAIN_PROCESS) {
|
||
|
AboutNewTab.uninit();
|
||
|
} else if (IS_PRIVILEGED_PROCESS) {
|
||
|
Services.obs.removeObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);
|
||
|
}
|
||
|
break;
|
||
|
case TOPIC_LOCALES_CHANGE:
|
||
|
this.updatePrerenderedPath();
|
||
|
this.notifyChange();
|
||
|
break;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
notifyChange() {
|
||
|
Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* React to changes to the activity stream being enabled or not.
|
||
|
*
|
||
|
* This will only act if there is a change of state and if not overridden.
|
||
|
*
|
||
|
* @returns {Boolean} Returns if there has been a state change
|
||
|
*
|
||
|
* @param {Boolean} stateEnabled activity stream enabled state to set to
|
||
|
* @param {Boolean} forceState force state change
|
||
|
*/
|
||
|
toggleActivityStream(stateEnabled, forceState = false) {
|
||
|
if (!forceState && (this.overridden || stateEnabled === this.activityStreamEnabled)) {
|
||
|
// exit there is no change of state
|
||
|
return false;
|
||
|
}
|
||
|
if (stateEnabled) {
|
||
|
this._activityStreamEnabled = true;
|
||
|
} else {
|
||
|
this._activityStreamEnabled = false;
|
||
|
}
|
||
|
this._privilegedContentProcess = Services.prefs.getBoolPref(PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS);
|
||
|
this._activityStreamPrerender = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED);
|
||
|
if (!IS_RELEASE_OR_BETA) {
|
||
|
this._activityStreamDebug = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_DEBUG, false);
|
||
|
}
|
||
|
this.updatePrerenderedPath();
|
||
|
this._newtabURL = ABOUT_URL;
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Figure out what path under prerendered to use based on current state.
|
||
|
*/
|
||
|
updatePrerenderedPath() {
|
||
|
// Debug files are specially packaged in a non-localized directory
|
||
|
this._activityStreamPath = `${this._activityStreamDebug ? "static" :
|
||
|
this.activityStreamLocale}/`;
|
||
|
},
|
||
|
|
||
|
/*
|
||
|
* Returns the default URL.
|
||
|
*
|
||
|
* This URL depends on various activity stream prefs and locales. Overriding
|
||
|
* the newtab page has no effect on the result of this function.
|
||
|
*/
|
||
|
get defaultURL() {
|
||
|
// Generate the desired activity stream resource depending on state, e.g.,
|
||
|
// resource://activity-stream/prerendered/ar/activity-stream.html
|
||
|
// resource://activity-stream/prerendered/en-US/activity-stream-prerendered.html
|
||
|
// resource://activity-stream/prerendered/static/activity-stream-debug.html
|
||
|
return [
|
||
|
"resource://activity-stream/prerendered/",
|
||
|
this._activityStreamPath,
|
||
|
"activity-stream",
|
||
|
this._activityStreamPrerender ? "-prerendered" : "",
|
||
|
this._activityStreamDebug ? "-debug" : "",
|
||
|
this._privilegedContentProcess ? "-noscripts" : "",
|
||
|
".html"
|
||
|
].join("");
|
||
|
},
|
||
|
|
||
|
/*
|
||
|
* Returns the about:welcome URL
|
||
|
*
|
||
|
* This is calculated in the same way the default URL is, except that we don't
|
||
|
* allow prerendering.
|
||
|
*/
|
||
|
get welcomeURL() {
|
||
|
const prerenderEnabled = this._activityStreamPrerender;
|
||
|
this._activityStreamPrerender = false;
|
||
|
const url = this.defaultURL;
|
||
|
this._activityStreamPrerender = prerenderEnabled;
|
||
|
return url;
|
||
|
},
|
||
|
|
||
|
get newTabURL() {
|
||
|
return this._newTabURL;
|
||
|
},
|
||
|
|
||
|
set newTabURL(aNewTabURL) {
|
||
|
let newTabURL = aNewTabURL.trim();
|
||
|
if (newTabURL === ABOUT_URL) {
|
||
|
// avoid infinite redirects in case one sets the URL to about:newtab
|
||
|
this.resetNewTabURL();
|
||
|
return;
|
||
|
} else if (newTabURL === "") {
|
||
|
newTabURL = "about:blank";
|
||
|
}
|
||
|
|
||
|
this.toggleActivityStream(false);
|
||
|
this._newTabURL = newTabURL;
|
||
|
this._overridden = true;
|
||
|
this.notifyChange();
|
||
|
},
|
||
|
|
||
|
get overridden() {
|
||
|
return this._overridden;
|
||
|
},
|
||
|
|
||
|
get activityStreamEnabled() {
|
||
|
return this._activityStreamEnabled;
|
||
|
},
|
||
|
|
||
|
get activityStreamPrerender() {
|
||
|
return this._activityStreamPrerender;
|
||
|
},
|
||
|
|
||
|
get activityStreamDebug() {
|
||
|
return this._activityStreamDebug;
|
||
|
},
|
||
|
|
||
|
get activityStreamLocale() {
|
||
|
// Pick the best available locale to match the app locales
|
||
|
return Services.locale.negotiateLanguages(
|
||
|
Services.locale.getAppLocalesAsLangTags(),
|
||
|
ACTIVITY_STREAM_LOCALES,
|
||
|
// defaultLocale's strings aren't necessarily packaged, but en-US' are
|
||
|
"en-US"
|
||
|
)[0];
|
||
|
},
|
||
|
|
||
|
resetNewTabURL() {
|
||
|
this._overridden = false;
|
||
|
this._newTabURL = ABOUT_URL;
|
||
|
this.toggleActivityStream(true, true);
|
||
|
this.notifyChange();
|
||
|
},
|
||
|
|
||
|
uninit() {
|
||
|
if (!this.initialized) {
|
||
|
return;
|
||
|
}
|
||
|
Services.obs.removeObserver(this, TOPIC_APP_QUIT);
|
||
|
Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGE);
|
||
|
Services.prefs.removeObserver(PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS, this);
|
||
|
Services.prefs.removeObserver(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED, this);
|
||
|
if (!IS_RELEASE_OR_BETA) {
|
||
|
Services.prefs.removeObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
|
||
|
}
|
||
|
this.initialized = false;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AboutNewTabService]);
|