Source code

Revision control

Other Tools

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
7
"use strict";
8
9
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10
const { AppConstants } = ChromeUtils.import(
12
);
13
const { E10SUtils } = ChromeUtils.import(
15
);
16
17
ChromeUtils.defineModuleGetter(
18
this,
19
"AboutNewTab",
21
);
22
23
const TOPIC_APP_QUIT = "quit-application-granted";
24
const TOPIC_CONTENT_DOCUMENT_INTERACTIVE = "content-document-interactive";
25
26
const ABOUT_URL = "about:newtab";
27
const BASE_URL = "resource://activity-stream/";
28
const ACTIVITY_STREAM_PAGES = new Set(["home", "newtab", "welcome"]);
29
30
const IS_MAIN_PROCESS =
31
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
32
const IS_PRIVILEGED_PROCESS =
33
Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
34
35
const IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;
36
37
const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS =
38
"browser.tabs.remote.separatePrivilegedContentProcess";
39
const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
40
41
function AboutNewTabService() {
42
Services.obs.addObserver(this, TOPIC_APP_QUIT);
43
Services.prefs.addObserver(
44
PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS,
45
this
46
);
47
if (!IS_RELEASE_OR_BETA) {
48
Services.prefs.addObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
49
}
50
51
// More initialization happens here
52
this.toggleActivityStream(true);
53
this.initialized = true;
54
this.alreadyRecordedTopsitesPainted = false;
55
56
if (IS_MAIN_PROCESS) {
57
AboutNewTab.init();
58
} else if (IS_PRIVILEGED_PROCESS) {
59
Services.obs.addObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);
60
}
61
}
62
63
/*
64
* A service that allows for the overriding, at runtime, of the newtab page's url.
65
*
66
* There is tight coupling with browser/about/AboutRedirector.cpp.
67
*
68
* 1. Browser chrome access:
69
*
70
* When the user issues a command to open a new tab page, usually clicking a button
71
* in the browser chrome or using shortcut keys, the browser chrome code invokes the
72
* service to obtain the newtab URL. It then loads that URL in a new tab.
73
*
74
* When not overridden, the default URL emitted by the service is "about:newtab".
75
* When overridden, it returns the overriden URL.
76
*
77
* 2. Redirector Access:
78
*
79
* When the URL loaded is about:newtab, the default behavior, or when entered in the
80
* URL bar, the redirector is hit. The service is then called to return the
81
* appropriate activity stream url based on prefs.
82
*
83
* NOTE: "about:newtab" will always result in a default newtab page, and never an overridden URL.
84
*
85
* Access patterns:
86
*
87
* The behavior is different when accessing the service via browser chrome or via redirector
88
* largely to maintain compatibility with expectations of add-on developers.
89
*
90
* Loading a chrome resource, or an about: URL in the redirector with either the
91
* LOAD_NORMAL or LOAD_REPLACE flags yield unexpected behaviors, so a roundtrip
92
* to the redirector from browser chrome is avoided.
93
*/
94
AboutNewTabService.prototype = {
95
_newTabURL: ABOUT_URL,
96
_activityStreamEnabled: false,
97
_activityStreamDebug: false,
98
_privilegedAboutContentProcess: false,
99
_overridden: false,
100
willNotifyUser: false,
101
102
classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
103
QueryInterface: ChromeUtils.generateQI([
104
Ci.nsIAboutNewTabService,
105
Ci.nsIObserver,
106
]),
107
108
observe(subject, topic, data) {
109
switch (topic) {
110
case "nsPref:changed":
111
if (data === PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS) {
112
this._privilegedAboutContentProcess = Services.prefs.getBoolPref(
113
PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS
114
);
115
this.notifyChange();
116
} else if (!IS_RELEASE_OR_BETA && data === PREF_ACTIVITY_STREAM_DEBUG) {
117
this._activityStreamDebug = Services.prefs.getBoolPref(
118
PREF_ACTIVITY_STREAM_DEBUG,
119
false
120
);
121
this.notifyChange();
122
}
123
break;
124
case TOPIC_CONTENT_DOCUMENT_INTERACTIVE: {
125
const win = subject.defaultView;
126
127
// It seems like "content-document-interactive" is triggered multiple
128
// times for a single window. The first event always seems to be an
129
// HTMLDocument object that contains a non-null window reference
130
// whereas the remaining ones seem to be proxied objects.
132
if (win === null) {
133
break;
134
}
135
136
// We use win.location.pathname instead of win.location.toString()
137
// because we want to account for URLs that contain the location hash
138
// property or query strings (e.g. about:newtab#foo, about:home?bar).
139
// Asserting here would be ideal, but this code path is also taken
140
// by the view-source:// scheme, so we should probably just bail out
141
// and do nothing.
142
if (!ACTIVITY_STREAM_PAGES.has(win.location.pathname)) {
143
break;
144
}
145
146
const onLoaded = () => {
147
const debugString = this._activityStreamDebug ? "-dev" : "";
148
149
// This list must match any similar ones in render-activity-stream-html.js.
150
const scripts = [
153
`${BASE_URL}vendor/react${debugString}.js`,
154
`${BASE_URL}vendor/react-dom${debugString}.js`,
155
`${BASE_URL}vendor/prop-types.js`,
156
`${BASE_URL}vendor/react-transition-group.js`,
157
`${BASE_URL}vendor/redux.js`,
158
`${BASE_URL}vendor/react-redux.js`,
159
`${BASE_URL}data/content/activity-stream.bundle.js`,
160
];
161
162
for (let script of scripts) {
163
Services.scriptloader.loadSubScript(script, win); // Synchronous call
164
}
165
};
166
subject.addEventListener("DOMContentLoaded", onLoaded, { once: true });
167
168
// There is a possibility that DOMContentLoaded won't be fired. This
169
// unload event (which cannot be cancelled) will attempt to remove
170
// the listener for the DOMContentLoaded event.
171
const onUnloaded = () => {
172
subject.removeEventListener("DOMContentLoaded", onLoaded);
173
};
174
subject.addEventListener("unload", onUnloaded, { once: true });
175
break;
176
}
177
case TOPIC_APP_QUIT:
178
this.uninit();
179
if (IS_MAIN_PROCESS) {
180
AboutNewTab.uninit();
181
} else if (IS_PRIVILEGED_PROCESS) {
182
Services.obs.removeObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);
183
}
184
break;
185
}
186
},
187
188
notifyChange() {
189
Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
190
},
191
192
/**
193
* React to changes to the activity stream being enabled or not.
194
*
195
* This will only act if there is a change of state and if not overridden.
196
*
197
* @returns {Boolean} Returns if there has been a state change
198
*
199
* @param {Boolean} stateEnabled activity stream enabled state to set to
200
* @param {Boolean} forceState force state change
201
*/
202
toggleActivityStream(stateEnabled, forceState = false) {
203
if (
204
!forceState &&
205
(this.overridden || stateEnabled === this.activityStreamEnabled)
206
) {
207
// exit there is no change of state
208
return false;
209
}
210
if (stateEnabled) {
211
this._activityStreamEnabled = true;
212
} else {
213
this._activityStreamEnabled = false;
214
}
215
this._privilegedAboutContentProcess = Services.prefs.getBoolPref(
216
PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS
217
);
218
if (!IS_RELEASE_OR_BETA) {
219
this._activityStreamDebug = Services.prefs.getBoolPref(
220
PREF_ACTIVITY_STREAM_DEBUG,
221
false
222
);
223
}
224
this._newtabURL = ABOUT_URL;
225
return true;
226
},
227
228
/*
229
* Returns the default URL.
230
*
231
* This URL depends on various activity stream prefs. Overriding
232
* the newtab page has no effect on the result of this function.
233
*/
234
get defaultURL() {
235
// Generate the desired activity stream resource depending on state, e.g.,
239
return [
241
"activity-stream",
242
// Debug version loads dev scripts but noscripts separately loads scripts
243
this._activityStreamDebug && !this._privilegedAboutContentProcess
244
? "-debug"
245
: "",
246
this._privilegedAboutContentProcess ? "-noscripts" : "",
247
".html",
248
].join("");
249
},
250
251
/*
252
* Returns the about:welcome URL
253
*
254
* This is calculated in the same way the default URL is.
255
*/
256
get welcomeURL() {
257
return this.defaultURL;
258
},
259
260
get newTabURL() {
261
return this._newTabURL;
262
},
263
264
set newTabURL(aNewTabURL) {
265
let newTabURL = aNewTabURL.trim();
266
if (newTabURL === ABOUT_URL) {
267
// avoid infinite redirects in case one sets the URL to about:newtab
268
this.resetNewTabURL();
269
return;
270
} else if (newTabURL === "") {
271
newTabURL = "about:blank";
272
}
273
274
this.toggleActivityStream(false);
275
this._newTabURL = newTabURL;
276
this._overridden = true;
277
this.notifyChange();
278
},
279
280
get overridden() {
281
return this._overridden;
282
},
283
284
get activityStreamEnabled() {
285
return this._activityStreamEnabled;
286
},
287
288
get activityStreamDebug() {
289
return this._activityStreamDebug;
290
},
291
292
resetNewTabURL() {
293
this._overridden = false;
294
this._newTabURL = ABOUT_URL;
295
this.toggleActivityStream(true, true);
296
this.notifyChange();
297
},
298
299
maybeRecordTopsitesPainted(timestamp) {
300
if (this.alreadyRecordedTopsitesPainted) {
301
return;
302
}
303
304
const SCALAR_KEY = "timestamps.about_home_topsites_first_paint";
305
306
let startupInfo = Services.startup.getStartupInfo();
307
let processStartTs = startupInfo.process.getTime();
308
let delta = Math.round(timestamp - processStartTs);
309
Services.telemetry.scalarSet(SCALAR_KEY, delta);
310
this.alreadyRecordedTopsitesPainted = true;
311
},
312
313
uninit() {
314
if (!this.initialized) {
315
return;
316
}
317
Services.obs.removeObserver(this, TOPIC_APP_QUIT);
318
Services.prefs.removeObserver(
319
PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS,
320
this
321
);
322
if (!IS_RELEASE_OR_BETA) {
323
Services.prefs.removeObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
324
}
325
this.initialized = false;
326
},
327
};
328
329
const EXPORTED_SYMBOLS = ["AboutNewTabService"];