AboutNewTabService.jsm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /*
  2. * This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  5. */
  6. "use strict";
  7. const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
  8. const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
  9. const {E10SUtils} = ChromeUtils.import("resource://gre/modules/E10SUtils.jsm");
  10. ChromeUtils.defineModuleGetter(this, "AboutNewTab",
  11. "resource:///modules/AboutNewTab.jsm");
  12. const TOPIC_APP_QUIT = "quit-application-granted";
  13. const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
  14. const TOPIC_CONTENT_DOCUMENT_INTERACTIVE = "content-document-interactive";
  15. // Automated tests ensure packaged locales are in this list. Copied output of:
  16. // https://github.com/mozilla/activity-stream/blob/master/bin/render-activity-stream-html.js
  17. const ACTIVITY_STREAM_BCP47 = "en-US ach an ar ast az be bg bn 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 is it ja ja-JP-macos ka kab kk km kn ko lij lo lt ltg lv mk 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 trs uk ur uz vi zh-CN zh-TW".split(" ");
  18. const ABOUT_URL = "about:newtab";
  19. const BASE_URL = "resource://activity-stream/";
  20. const ACTIVITY_STREAM_PAGES = new Set(["home", "newtab", "welcome"]);
  21. const IS_MAIN_PROCESS = Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
  22. const IS_PRIVILEGED_PROCESS = Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
  23. const IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;
  24. const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS = "browser.tabs.remote.separatePrivilegedContentProcess";
  25. const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
  26. function AboutNewTabService() {
  27. Services.obs.addObserver(this, TOPIC_APP_QUIT);
  28. Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
  29. Services.prefs.addObserver(PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS, this);
  30. if (!IS_RELEASE_OR_BETA) {
  31. Services.prefs.addObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
  32. }
  33. // More initialization happens here
  34. this.toggleActivityStream(true);
  35. this.initialized = true;
  36. this.alreadyRecordedTopsitesPainted = false;
  37. if (IS_MAIN_PROCESS) {
  38. AboutNewTab.init();
  39. } else if (IS_PRIVILEGED_PROCESS) {
  40. Services.obs.addObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);
  41. }
  42. }
  43. /*
  44. * A service that allows for the overriding, at runtime, of the newtab page's url.
  45. *
  46. * There is tight coupling with browser/about/AboutRedirector.cpp.
  47. *
  48. * 1. Browser chrome access:
  49. *
  50. * When the user issues a command to open a new tab page, usually clicking a button
  51. * in the browser chrome or using shortcut keys, the browser chrome code invokes the
  52. * service to obtain the newtab URL. It then loads that URL in a new tab.
  53. *
  54. * When not overridden, the default URL emitted by the service is "about:newtab".
  55. * When overridden, it returns the overriden URL.
  56. *
  57. * 2. Redirector Access:
  58. *
  59. * When the URL loaded is about:newtab, the default behavior, or when entered in the
  60. * URL bar, the redirector is hit. The service is then called to return the
  61. * appropriate activity stream url based on prefs and locales.
  62. *
  63. * NOTE: "about:newtab" will always result in a default newtab page, and never an overridden URL.
  64. *
  65. * Access patterns:
  66. *
  67. * The behavior is different when accessing the service via browser chrome or via redirector
  68. * largely to maintain compatibility with expectations of add-on developers.
  69. *
  70. * Loading a chrome resource, or an about: URL in the redirector with either the
  71. * LOAD_NORMAL or LOAD_REPLACE flags yield unexpected behaviors, so a roundtrip
  72. * to the redirector from browser chrome is avoided.
  73. */
  74. AboutNewTabService.prototype = {
  75. _newTabURL: ABOUT_URL,
  76. _activityStreamEnabled: false,
  77. _activityStreamPath: "",
  78. _activityStreamDebug: false,
  79. _privilegedAboutContentProcess: false,
  80. _overridden: false,
  81. willNotifyUser: false,
  82. classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
  83. QueryInterface: ChromeUtils.generateQI([
  84. Ci.nsIAboutNewTabService,
  85. Ci.nsIObserver,
  86. ]),
  87. observe(subject, topic, data) {
  88. switch (topic) {
  89. case "nsPref:changed":
  90. if (data === PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS) {
  91. this._privilegedAboutContentProcess = Services.prefs.getBoolPref(PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS);
  92. this.updatePrerenderedPath();
  93. this.notifyChange();
  94. } else if (!IS_RELEASE_OR_BETA && data === PREF_ACTIVITY_STREAM_DEBUG) {
  95. this._activityStreamDebug = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_DEBUG, false);
  96. this.updatePrerenderedPath();
  97. this.notifyChange();
  98. }
  99. break;
  100. case TOPIC_CONTENT_DOCUMENT_INTERACTIVE: {
  101. const win = subject.defaultView;
  102. // It seems like "content-document-interactive" is triggered multiple
  103. // times for a single window. The first event always seems to be an
  104. // HTMLDocument object that contains a non-null window reference
  105. // whereas the remaining ones seem to be proxied objects.
  106. // https://searchfox.org/mozilla-central/rev/d2966246905102b36ef5221b0e3cbccf7ea15a86/devtools/server/actors/object.js#100-102
  107. if (win === null) {
  108. break;
  109. }
  110. // We use win.location.pathname instead of win.location.toString()
  111. // because we want to account for URLs that contain the location hash
  112. // property or query strings (e.g. about:newtab#foo, about:home?bar).
  113. // Asserting here would be ideal, but this code path is also taken
  114. // by the view-source:// scheme, so we should probably just bail out
  115. // and do nothing.
  116. if (!ACTIVITY_STREAM_PAGES.has(win.location.pathname)) {
  117. break;
  118. }
  119. const onLoaded = () => {
  120. const debugString = this._activityStreamDebug ? "-dev" : "";
  121. // This list must match any similar ones in render-activity-stream-html.js.
  122. const scripts = [
  123. "chrome://browser/content/contentSearchUI.js",
  124. "chrome://browser/content/contentTheme.js",
  125. `${BASE_URL}vendor/react${debugString}.js`,
  126. `${BASE_URL}vendor/react-dom${debugString}.js`,
  127. `${BASE_URL}vendor/prop-types.js`,
  128. `${BASE_URL}vendor/react-intl.js`,
  129. `${BASE_URL}vendor/redux.js`,
  130. `${BASE_URL}vendor/react-redux.js`,
  131. `${BASE_URL}prerendered/${this.activityStreamLocale}/activity-stream-strings.js`,
  132. `${BASE_URL}data/content/activity-stream.bundle.js`,
  133. ];
  134. for (let script of scripts) {
  135. Services.scriptloader.loadSubScript(script, win); // Synchronous call
  136. }
  137. };
  138. subject.addEventListener("DOMContentLoaded", onLoaded, {once: true});
  139. // There is a possibility that DOMContentLoaded won't be fired. This
  140. // unload event (which cannot be cancelled) will attempt to remove
  141. // the listener for the DOMContentLoaded event.
  142. const onUnloaded = () => {
  143. subject.removeEventListener("DOMContentLoaded", onLoaded);
  144. };
  145. subject.addEventListener("unload", onUnloaded, {once: true});
  146. break;
  147. }
  148. case TOPIC_APP_QUIT:
  149. this.uninit();
  150. if (IS_MAIN_PROCESS) {
  151. AboutNewTab.uninit();
  152. } else if (IS_PRIVILEGED_PROCESS) {
  153. Services.obs.removeObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);
  154. }
  155. break;
  156. case TOPIC_LOCALES_CHANGE:
  157. this.updatePrerenderedPath();
  158. this.notifyChange();
  159. break;
  160. }
  161. },
  162. notifyChange() {
  163. Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
  164. },
  165. /**
  166. * React to changes to the activity stream being enabled or not.
  167. *
  168. * This will only act if there is a change of state and if not overridden.
  169. *
  170. * @returns {Boolean} Returns if there has been a state change
  171. *
  172. * @param {Boolean} stateEnabled activity stream enabled state to set to
  173. * @param {Boolean} forceState force state change
  174. */
  175. toggleActivityStream(stateEnabled, forceState = false) {
  176. if (!forceState && (this.overridden || stateEnabled === this.activityStreamEnabled)) {
  177. // exit there is no change of state
  178. return false;
  179. }
  180. if (stateEnabled) {
  181. this._activityStreamEnabled = true;
  182. } else {
  183. this._activityStreamEnabled = false;
  184. }
  185. this._privilegedAboutContentProcess = Services.prefs.getBoolPref(PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS);
  186. if (!IS_RELEASE_OR_BETA) {
  187. this._activityStreamDebug = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_DEBUG, false);
  188. }
  189. this.updatePrerenderedPath();
  190. this._newtabURL = ABOUT_URL;
  191. return true;
  192. },
  193. /**
  194. * Figure out what path under prerendered to use based on current state.
  195. */
  196. updatePrerenderedPath() {
  197. // Debug files are specially packaged in a non-localized directory, but with
  198. // dynamic script loading, localized debug is supported.
  199. this._activityStreamPath = `${this._activityStreamDebug &&
  200. !this._privilegedAboutContentProcess ? "static" : this.activityStreamLocale}/`;
  201. },
  202. /*
  203. * Returns the default URL.
  204. *
  205. * This URL depends on various activity stream prefs and locales. Overriding
  206. * the newtab page has no effect on the result of this function.
  207. */
  208. get defaultURL() {
  209. // Generate the desired activity stream resource depending on state, e.g.,
  210. // resource://activity-stream/prerendered/ar/activity-stream.html
  211. // resource://activity-stream/prerendered/static/activity-stream-debug.html
  212. return [
  213. "resource://activity-stream/prerendered/",
  214. this._activityStreamPath,
  215. "activity-stream",
  216. // Debug version loads dev scripts but noscripts separately loads scripts
  217. this._activityStreamDebug && !this._privilegedAboutContentProcess ? "-debug" : "",
  218. this._privilegedAboutContentProcess ? "-noscripts" : "",
  219. ".html",
  220. ].join("");
  221. },
  222. /*
  223. * Returns the about:welcome URL
  224. *
  225. * This is calculated in the same way the default URL is.
  226. */
  227. get welcomeURL() {
  228. return this.defaultURL;
  229. },
  230. get newTabURL() {
  231. return this._newTabURL;
  232. },
  233. set newTabURL(aNewTabURL) {
  234. let newTabURL = aNewTabURL.trim();
  235. if (newTabURL === ABOUT_URL) {
  236. // avoid infinite redirects in case one sets the URL to about:newtab
  237. this.resetNewTabURL();
  238. return;
  239. } else if (newTabURL === "") {
  240. newTabURL = "about:blank";
  241. }
  242. this.toggleActivityStream(false);
  243. this._newTabURL = newTabURL;
  244. this._overridden = true;
  245. this.notifyChange();
  246. },
  247. get overridden() {
  248. return this._overridden;
  249. },
  250. get activityStreamEnabled() {
  251. return this._activityStreamEnabled;
  252. },
  253. get activityStreamDebug() {
  254. return this._activityStreamDebug;
  255. },
  256. get activityStreamLocale() {
  257. // Pick the best available locale to match the app locales
  258. return Services.locale.negotiateLanguages(
  259. // Fix up incorrect BCP47 that are actually lang tags as a workaround for
  260. // bug 1479606 returning the wrong values in the content process
  261. Services.locale.appLocalesAsBCP47.map(l => l.replace(/^(ja-JP-mac)$/, "$1os")),
  262. ACTIVITY_STREAM_BCP47,
  263. // defaultLocale's strings aren't necessarily packaged, but en-US' are
  264. "en-US",
  265. Services.locale.langNegStrategyLookup
  266. // Convert the BCP47 to lang tag, which is what is used in our paths, as a
  267. // workaround for bug 1478930 negotiating incorrectly with lang tags
  268. )[0].replace(/^(ja-JP-mac)os$/, "$1");
  269. },
  270. resetNewTabURL() {
  271. this._overridden = false;
  272. this._newTabURL = ABOUT_URL;
  273. this.toggleActivityStream(true, true);
  274. this.notifyChange();
  275. },
  276. maybeRecordTopsitesPainted(timestamp) {
  277. if (this.alreadyRecordedTopsitesPainted) {
  278. return;
  279. }
  280. const SCALAR_KEY = "timestamps.about_home_topsites_first_paint";
  281. let startupInfo = Services.startup.getStartupInfo();
  282. let processStartTs = startupInfo.process.getTime();
  283. let delta = Math.round(timestamp - processStartTs);
  284. Services.telemetry.scalarSet(SCALAR_KEY, delta);
  285. this.alreadyRecordedTopsitesPainted = true;
  286. },
  287. uninit() {
  288. if (!this.initialized) {
  289. return;
  290. }
  291. Services.obs.removeObserver(this, TOPIC_APP_QUIT);
  292. Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGE);
  293. Services.prefs.removeObserver(PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS, this);
  294. if (!IS_RELEASE_OR_BETA) {
  295. Services.prefs.removeObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
  296. }
  297. this.initialized = false;
  298. },
  299. };
  300. const EXPORTED_SYMBOLS = ["AboutNewTabService"];