Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["NewTabUtils"];
9 const Ci = Components.interfaces;
10 const Cc = Components.classes;
11 const Cu = Components.utils;
13 Cu.import("resource://gre/modules/Services.jsm");
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
16 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
17 "resource://gre/modules/PlacesUtils.jsm");
19 XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
20 "resource://gre/modules/PageThumbs.jsm");
22 XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
23 "resource://gre/modules/BinarySearch.jsm");
25 XPCOMUtils.defineLazyGetter(this, "Timer", () => {
26 return Cu.import("resource://gre/modules/Timer.jsm", {});
27 });
29 XPCOMUtils.defineLazyGetter(this, "gPrincipal", function () {
30 let uri = Services.io.newURI("about:newtab", null, null);
31 return Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
32 });
34 XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
35 return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
36 });
38 XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
39 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
40 .createInstance(Ci.nsIScriptableUnicodeConverter);
41 converter.charset = 'utf8';
42 return converter;
43 });
45 // The preference that tells whether this feature is enabled.
46 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
48 // The preference that tells the number of rows of the newtab grid.
49 const PREF_NEWTAB_ROWS = "browser.newtabpage.rows";
51 // The preference that tells the number of columns of the newtab grid.
52 const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
54 // The maximum number of results PlacesProvider retrieves from history.
55 const HISTORY_RESULTS_LIMIT = 100;
57 // The maximum number of links Links.getLinks will return.
58 const LINKS_GET_LINKS_LIMIT = 100;
60 // The gather telemetry topic.
61 const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
63 // The amount of time we wait while coalescing updates for hidden pages.
64 const SCHEDULE_UPDATE_TIMEOUT_MS = 1000;
66 /**
67 * Calculate the MD5 hash for a string.
68 * @param aValue
69 * The string to convert.
70 * @return The base64 representation of the MD5 hash.
71 */
72 function toHash(aValue) {
73 let value = gUnicodeConverter.convertToByteArray(aValue);
74 gCryptoHash.init(gCryptoHash.MD5);
75 gCryptoHash.update(value, value.length);
76 return gCryptoHash.finish(true);
77 }
79 /**
80 * Singleton that provides storage functionality.
81 */
82 XPCOMUtils.defineLazyGetter(this, "Storage", function() {
83 return new LinksStorage();
84 });
86 function LinksStorage() {
87 // Handle migration of data across versions.
88 try {
89 if (this._storedVersion < this._version) {
90 // This is either an upgrade, or version information is missing.
91 if (this._storedVersion < 1) {
92 // Version 1 moved data from DOM Storage to prefs. Since migrating from
93 // version 0 is no more supported, we just reportError a dataloss later.
94 throw new Error("Unsupported newTab storage version");
95 }
96 // Add further migration steps here.
97 }
98 else {
99 // This is a downgrade. Since we cannot predict future, upgrades should
100 // be backwards compatible. We will set the version to the old value
101 // regardless, so, on next upgrade, the migration steps will run again.
102 // For this reason, they should also be able to run multiple times, even
103 // on top of an already up-to-date storage.
104 }
105 } catch (ex) {
106 // Something went wrong in the update process, we can't recover from here,
107 // so just clear the storage and start from scratch (dataloss!).
108 Components.utils.reportError(
109 "Unable to migrate the newTab storage to the current version. "+
110 "Restarting from scratch.\n" + ex);
111 this.clear();
112 }
114 // Set the version to the current one.
115 this._storedVersion = this._version;
116 }
118 LinksStorage.prototype = {
119 get _version() 1,
121 get _prefs() Object.freeze({
122 pinnedLinks: "browser.newtabpage.pinned",
123 blockedLinks: "browser.newtabpage.blocked",
124 }),
126 get _storedVersion() {
127 if (this.__storedVersion === undefined) {
128 try {
129 this.__storedVersion =
130 Services.prefs.getIntPref("browser.newtabpage.storageVersion");
131 } catch (ex) {
132 // The storage version is unknown, so either:
133 // - it's a new profile
134 // - it's a profile where versioning information got lost
135 // In this case we still run through all of the valid migrations,
136 // starting from 1, as if it was a downgrade. As previously stated the
137 // migrations should already support running on an updated store.
138 this.__storedVersion = 1;
139 }
140 }
141 return this.__storedVersion;
142 },
143 set _storedVersion(aValue) {
144 Services.prefs.setIntPref("browser.newtabpage.storageVersion", aValue);
145 this.__storedVersion = aValue;
146 return aValue;
147 },
149 /**
150 * Gets the value for a given key from the storage.
151 * @param aKey The storage key (a string).
152 * @param aDefault A default value if the key doesn't exist.
153 * @return The value for the given key.
154 */
155 get: function Storage_get(aKey, aDefault) {
156 let value;
157 try {
158 let prefValue = Services.prefs.getComplexValue(this._prefs[aKey],
159 Ci.nsISupportsString).data;
160 value = JSON.parse(prefValue);
161 } catch (e) {}
162 return value || aDefault;
163 },
165 /**
166 * Sets the storage value for a given key.
167 * @param aKey The storage key (a string).
168 * @param aValue The value to set.
169 */
170 set: function Storage_set(aKey, aValue) {
171 // Page titles may contain unicode, thus use complex values.
172 let string = Cc["@mozilla.org/supports-string;1"]
173 .createInstance(Ci.nsISupportsString);
174 string.data = JSON.stringify(aValue);
175 Services.prefs.setComplexValue(this._prefs[aKey], Ci.nsISupportsString,
176 string);
177 },
179 /**
180 * Removes the storage value for a given key.
181 * @param aKey The storage key (a string).
182 */
183 remove: function Storage_remove(aKey) {
184 Services.prefs.clearUserPref(this._prefs[aKey]);
185 },
187 /**
188 * Clears the storage and removes all values.
189 */
190 clear: function Storage_clear() {
191 for (let key in this._prefs) {
192 this.remove(key);
193 }
194 }
195 };
198 /**
199 * Singleton that serves as a registry for all open 'New Tab Page's.
200 */
201 let AllPages = {
202 /**
203 * The array containing all active pages.
204 */
205 _pages: [],
207 /**
208 * Cached value that tells whether the New Tab Page feature is enabled.
209 */
210 _enabled: null,
212 /**
213 * Adds a page to the internal list of pages.
214 * @param aPage The page to register.
215 */
216 register: function AllPages_register(aPage) {
217 this._pages.push(aPage);
218 this._addObserver();
219 },
221 /**
222 * Removes a page from the internal list of pages.
223 * @param aPage The page to unregister.
224 */
225 unregister: function AllPages_unregister(aPage) {
226 let index = this._pages.indexOf(aPage);
227 if (index > -1)
228 this._pages.splice(index, 1);
229 },
231 /**
232 * Returns whether the 'New Tab Page' is enabled.
233 */
234 get enabled() {
235 if (this._enabled === null)
236 this._enabled = Services.prefs.getBoolPref(PREF_NEWTAB_ENABLED);
238 return this._enabled;
239 },
241 /**
242 * Enables or disables the 'New Tab Page' feature.
243 */
244 set enabled(aEnabled) {
245 if (this.enabled != aEnabled)
246 Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, !!aEnabled);
247 },
249 /**
250 * Returns the number of registered New Tab Pages (i.e. the number of open
251 * about:newtab instances).
252 */
253 get length() {
254 return this._pages.length;
255 },
257 /**
258 * Updates all currently active pages but the given one.
259 * @param aExceptPage The page to exclude from updating.
260 * @param aHiddenPagesOnly If true, only pages hidden in the preloader are
261 * updated.
262 */
263 update: function AllPages_update(aExceptPage, aHiddenPagesOnly=false) {
264 this._pages.forEach(function (aPage) {
265 if (aExceptPage != aPage)
266 aPage.update(aHiddenPagesOnly);
267 });
268 },
270 /**
271 * Many individual link changes may happen in a small amount of time over
272 * multiple turns of the event loop. This method coalesces updates by waiting
273 * a small amount of time before updating hidden pages.
274 */
275 scheduleUpdateForHiddenPages: function AllPages_scheduleUpdateForHiddenPages() {
276 if (!this._scheduleUpdateTimeout) {
277 this._scheduleUpdateTimeout = Timer.setTimeout(() => {
278 delete this._scheduleUpdateTimeout;
279 this.update(null, true);
280 }, SCHEDULE_UPDATE_TIMEOUT_MS);
281 }
282 },
284 get updateScheduledForHiddenPages() {
285 return !!this._scheduleUpdateTimeout;
286 },
288 /**
289 * Implements the nsIObserver interface to get notified when the preference
290 * value changes or when a new copy of a page thumbnail is available.
291 */
292 observe: function AllPages_observe(aSubject, aTopic, aData) {
293 if (aTopic == "nsPref:changed") {
294 // Clear the cached value.
295 this._enabled = null;
296 }
297 // and all notifications get forwarded to each page.
298 this._pages.forEach(function (aPage) {
299 aPage.observe(aSubject, aTopic, aData);
300 }, this);
301 },
303 /**
304 * Adds a preference and new thumbnail observer and turns itself into a
305 * no-op after the first invokation.
306 */
307 _addObserver: function AllPages_addObserver() {
308 Services.prefs.addObserver(PREF_NEWTAB_ENABLED, this, true);
309 Services.obs.addObserver(this, "page-thumbnail:create", true);
310 this._addObserver = function () {};
311 },
313 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
314 Ci.nsISupportsWeakReference])
315 };
317 /**
318 * Singleton that keeps Grid preferences
319 */
320 let GridPrefs = {
321 /**
322 * Cached value that tells the number of rows of newtab grid.
323 */
324 _gridRows: null,
325 get gridRows() {
326 if (!this._gridRows) {
327 this._gridRows = Math.max(1, Services.prefs.getIntPref(PREF_NEWTAB_ROWS));
328 }
330 return this._gridRows;
331 },
333 /**
334 * Cached value that tells the number of columns of newtab grid.
335 */
336 _gridColumns: null,
337 get gridColumns() {
338 if (!this._gridColumns) {
339 this._gridColumns = Math.max(1, Services.prefs.getIntPref(PREF_NEWTAB_COLUMNS));
340 }
342 return this._gridColumns;
343 },
346 /**
347 * Initializes object. Adds a preference observer
348 */
349 init: function GridPrefs_init() {
350 Services.prefs.addObserver(PREF_NEWTAB_ROWS, this, false);
351 Services.prefs.addObserver(PREF_NEWTAB_COLUMNS, this, false);
352 },
354 /**
355 * Implements the nsIObserver interface to get notified when the preference
356 * value changes.
357 */
358 observe: function GridPrefs_observe(aSubject, aTopic, aData) {
359 if (aData == PREF_NEWTAB_ROWS) {
360 this._gridRows = null;
361 } else {
362 this._gridColumns = null;
363 }
365 AllPages.update();
366 }
367 };
369 GridPrefs.init();
371 /**
372 * Singleton that keeps track of all pinned links and their positions in the
373 * grid.
374 */
375 let PinnedLinks = {
376 /**
377 * The cached list of pinned links.
378 */
379 _links: null,
381 /**
382 * The array of pinned links.
383 */
384 get links() {
385 if (!this._links)
386 this._links = Storage.get("pinnedLinks", []);
388 return this._links;
389 },
391 /**
392 * Pins a link at the given position.
393 * @param aLink The link to pin.
394 * @param aIndex The grid index to pin the cell at.
395 */
396 pin: function PinnedLinks_pin(aLink, aIndex) {
397 // Clear the link's old position, if any.
398 this.unpin(aLink);
400 this.links[aIndex] = aLink;
401 this.save();
402 },
404 /**
405 * Unpins a given link.
406 * @param aLink The link to unpin.
407 */
408 unpin: function PinnedLinks_unpin(aLink) {
409 let index = this._indexOfLink(aLink);
410 if (index == -1)
411 return;
412 let links = this.links;
413 links[index] = null;
414 // trim trailing nulls
415 let i=links.length-1;
416 while (i >= 0 && links[i] == null)
417 i--;
418 links.splice(i +1);
419 this.save();
420 },
422 /**
423 * Saves the current list of pinned links.
424 */
425 save: function PinnedLinks_save() {
426 Storage.set("pinnedLinks", this.links);
427 },
429 /**
430 * Checks whether a given link is pinned.
431 * @params aLink The link to check.
432 * @return whether The link is pinned.
433 */
434 isPinned: function PinnedLinks_isPinned(aLink) {
435 return this._indexOfLink(aLink) != -1;
436 },
438 /**
439 * Resets the links cache.
440 */
441 resetCache: function PinnedLinks_resetCache() {
442 this._links = null;
443 },
445 /**
446 * Finds the index of a given link in the list of pinned links.
447 * @param aLink The link to find an index for.
448 * @return The link's index.
449 */
450 _indexOfLink: function PinnedLinks_indexOfLink(aLink) {
451 for (let i = 0; i < this.links.length; i++) {
452 let link = this.links[i];
453 if (link && link.url == aLink.url)
454 return i;
455 }
457 // The given link is unpinned.
458 return -1;
459 }
460 };
462 /**
463 * Singleton that keeps track of all blocked links in the grid.
464 */
465 let BlockedLinks = {
466 /**
467 * The cached list of blocked links.
468 */
469 _links: null,
471 /**
472 * The list of blocked links.
473 */
474 get links() {
475 if (!this._links)
476 this._links = Storage.get("blockedLinks", {});
478 return this._links;
479 },
481 /**
482 * Blocks a given link.
483 * @param aLink The link to block.
484 */
485 block: function BlockedLinks_block(aLink) {
486 this.links[toHash(aLink.url)] = 1;
487 this.save();
489 // Make sure we unpin blocked links.
490 PinnedLinks.unpin(aLink);
491 },
493 /**
494 * Unblocks a given link.
495 * @param aLink The link to unblock.
496 */
497 unblock: function BlockedLinks_unblock(aLink) {
498 if (this.isBlocked(aLink)) {
499 delete this.links[toHash(aLink.url)];
500 this.save();
501 }
502 },
504 /**
505 * Saves the current list of blocked links.
506 */
507 save: function BlockedLinks_save() {
508 Storage.set("blockedLinks", this.links);
509 },
511 /**
512 * Returns whether a given link is blocked.
513 * @param aLink The link to check.
514 */
515 isBlocked: function BlockedLinks_isBlocked(aLink) {
516 return (toHash(aLink.url) in this.links);
517 },
519 /**
520 * Checks whether the list of blocked links is empty.
521 * @return Whether the list is empty.
522 */
523 isEmpty: function BlockedLinks_isEmpty() {
524 return Object.keys(this.links).length == 0;
525 },
527 /**
528 * Resets the links cache.
529 */
530 resetCache: function BlockedLinks_resetCache() {
531 this._links = null;
532 }
533 };
535 /**
536 * Singleton that serves as the default link provider for the grid. It queries
537 * the history to retrieve the most frequently visited sites.
538 */
539 let PlacesProvider = {
540 /**
541 * Set this to change the maximum number of links the provider will provide.
542 */
543 maxNumLinks: HISTORY_RESULTS_LIMIT,
545 /**
546 * Must be called before the provider is used.
547 */
548 init: function PlacesProvider_init() {
549 PlacesUtils.history.addObserver(this, true);
550 },
552 /**
553 * Gets the current set of links delivered by this provider.
554 * @param aCallback The function that the array of links is passed to.
555 */
556 getLinks: function PlacesProvider_getLinks(aCallback) {
557 let options = PlacesUtils.history.getNewQueryOptions();
558 options.maxResults = this.maxNumLinks;
560 // Sort by frecency, descending.
561 options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING
563 let links = [];
565 let callback = {
566 handleResult: function (aResultSet) {
567 let row;
569 while ((row = aResultSet.getNextRow())) {
570 let url = row.getResultByIndex(1);
571 if (LinkChecker.checkLoadURI(url)) {
572 let title = row.getResultByIndex(2);
573 let frecency = row.getResultByIndex(12);
574 let lastVisitDate = row.getResultByIndex(5);
575 links.push({
576 url: url,
577 title: title,
578 frecency: frecency,
579 lastVisitDate: lastVisitDate,
580 bgColor: "transparent",
581 type: "history",
582 imageURI: null,
583 });
584 }
585 }
586 },
588 handleError: function (aError) {
589 // Should we somehow handle this error?
590 aCallback([]);
591 },
593 handleCompletion: function (aReason) {
594 // The Places query breaks ties in frecency by place ID descending, but
595 // that's different from how Links.compareLinks breaks ties, because
596 // compareLinks doesn't have access to place IDs. It's very important
597 // that the initial list of links is sorted in the same order imposed by
598 // compareLinks, because Links uses compareLinks to perform binary
599 // searches on the list. So, ensure the list is so ordered.
600 let i = 1;
601 let outOfOrder = [];
602 while (i < links.length) {
603 if (Links.compareLinks(links[i - 1], links[i]) > 0)
604 outOfOrder.push(links.splice(i, 1)[0]);
605 else
606 i++;
607 }
608 for (let link of outOfOrder) {
609 i = BinarySearch.insertionIndexOf(links, link,
610 Links.compareLinks.bind(Links));
611 links.splice(i, 0, link);
612 }
614 aCallback(links);
615 }
616 };
618 // Execute the query.
619 let query = PlacesUtils.history.getNewQuery();
620 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
621 db.asyncExecuteLegacyQueries([query], 1, options, callback);
622 },
624 /**
625 * Registers an object that will be notified when the provider's links change.
626 * @param aObserver An object with the following optional properties:
627 * * onLinkChanged: A function that's called when a single link
628 * changes. It's passed the provider and the link object. Only the
629 * link's `url` property is guaranteed to be present. If its `title`
630 * property is present, then its title has changed, and the
631 * property's value is the new title. If any sort properties are
632 * present, then its position within the provider's list of links may
633 * have changed, and the properties' values are the new sort-related
634 * values. Note that this link may not necessarily have been present
635 * in the lists returned from any previous calls to getLinks.
636 * * onManyLinksChanged: A function that's called when many links
637 * change at once. It's passed the provider. You should call
638 * getLinks to get the provider's new list of links.
639 */
640 addObserver: function PlacesProvider_addObserver(aObserver) {
641 this._observers.push(aObserver);
642 },
644 _observers: [],
646 /**
647 * Called by the history service.
648 */
649 onFrecencyChanged: function PlacesProvider_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) {
650 // The implementation of the query in getLinks excludes hidden and
651 // unvisited pages, so it's important to exclude them here, too.
652 if (!aHidden && aLastVisitDate) {
653 this._callObservers("onLinkChanged", {
654 url: aURI.spec,
655 frecency: aNewFrecency,
656 lastVisitDate: aLastVisitDate,
657 });
658 }
659 },
661 /**
662 * Called by the history service.
663 */
664 onManyFrecenciesChanged: function PlacesProvider_onManyFrecenciesChanged() {
665 this._callObservers("onManyLinksChanged");
666 },
668 /**
669 * Called by the history service.
670 */
671 onTitleChanged: function PlacesProvider_onTitleChanged(aURI, aNewTitle, aGUID) {
672 this._callObservers("onLinkChanged", {
673 url: aURI.spec,
674 title: aNewTitle
675 });
676 },
678 _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
679 for (let obs of this._observers) {
680 if (obs[aMethodName]) {
681 try {
682 obs[aMethodName](this, aArg);
683 } catch (err) {
684 Cu.reportError(err);
685 }
686 }
687 }
688 },
690 QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
691 Ci.nsISupportsWeakReference]),
692 };
694 /**
695 * Singleton that provides access to all links contained in the grid (including
696 * the ones that don't fit on the grid). A link is a plain object that looks
697 * like this:
698 *
699 * {
700 * url: "http://www.mozilla.org/",
701 * title: "Mozilla",
702 * frecency: 1337,
703 * lastVisitDate: 1394678824766431,
704 * }
705 */
706 let Links = {
707 /**
708 * The maximum number of links returned by getLinks.
709 */
710 maxNumLinks: LINKS_GET_LINKS_LIMIT,
712 /**
713 * The link providers.
714 */
715 _providers: new Set(),
717 /**
718 * A mapping from each provider to an object { sortedLinks, linkMap }.
719 * sortedLinks is the cached, sorted array of links for the provider. linkMap
720 * is a Map from link URLs to link objects.
721 */
722 _providerLinks: new Map(),
724 /**
725 * The properties of link objects used to sort them.
726 */
727 _sortProperties: [
728 "frecency",
729 "lastVisitDate",
730 "url",
731 ],
733 /**
734 * List of callbacks waiting for the cache to be populated.
735 */
736 _populateCallbacks: [],
738 /**
739 * Adds a link provider.
740 * @param aProvider The link provider.
741 */
742 addProvider: function Links_addProvider(aProvider) {
743 this._providers.add(aProvider);
744 aProvider.addObserver(this);
745 },
747 /**
748 * Removes a link provider.
749 * @param aProvider The link provider.
750 */
751 removeProvider: function Links_removeProvider(aProvider) {
752 if (!this._providers.delete(aProvider))
753 throw new Error("Unknown provider");
754 this._providerLinks.delete(aProvider);
755 },
757 /**
758 * Populates the cache with fresh links from the providers.
759 * @param aCallback The callback to call when finished (optional).
760 * @param aForce When true, populates the cache even when it's already filled.
761 */
762 populateCache: function Links_populateCache(aCallback, aForce) {
763 let callbacks = this._populateCallbacks;
765 // Enqueue the current callback.
766 callbacks.push(aCallback);
768 // There was a callback waiting already, thus the cache has not yet been
769 // populated.
770 if (callbacks.length > 1)
771 return;
773 function executeCallbacks() {
774 while (callbacks.length) {
775 let callback = callbacks.shift();
776 if (callback) {
777 try {
778 callback();
779 } catch (e) {
780 // We want to proceed even if a callback fails.
781 }
782 }
783 }
784 }
786 let numProvidersRemaining = this._providers.size;
787 for (let provider of this._providers) {
788 this._populateProviderCache(provider, () => {
789 if (--numProvidersRemaining == 0)
790 executeCallbacks();
791 }, aForce);
792 }
794 this._addObserver();
795 },
797 /**
798 * Gets the current set of links contained in the grid.
799 * @return The links in the grid.
800 */
801 getLinks: function Links_getLinks() {
802 let pinnedLinks = Array.slice(PinnedLinks.links);
803 let links = this._getMergedProviderLinks();
805 // Filter blocked and pinned links.
806 links = links.filter(function (link) {
807 return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
808 });
810 // Try to fill the gaps between pinned links.
811 for (let i = 0; i < pinnedLinks.length && links.length; i++)
812 if (!pinnedLinks[i])
813 pinnedLinks[i] = links.shift();
815 // Append the remaining links if any.
816 if (links.length)
817 pinnedLinks = pinnedLinks.concat(links);
819 return pinnedLinks;
820 },
822 /**
823 * Resets the links cache.
824 */
825 resetCache: function Links_resetCache() {
826 this._providerLinks.clear();
827 },
829 /**
830 * Compares two links.
831 * @param aLink1 The first link.
832 * @param aLink2 The second link.
833 * @return A negative number if aLink1 is ordered before aLink2, zero if
834 * aLink1 and aLink2 have the same ordering, or a positive number if
835 * aLink1 is ordered after aLink2.
836 */
837 compareLinks: function Links_compareLinks(aLink1, aLink2) {
838 for (let prop of this._sortProperties) {
839 if (!(prop in aLink1) || !(prop in aLink2))
840 throw new Error("Comparable link missing required property: " + prop);
841 }
842 return aLink2.frecency - aLink1.frecency ||
843 aLink2.lastVisitDate - aLink1.lastVisitDate ||
844 aLink1.url.localeCompare(aLink2.url);
845 },
847 /**
848 * Calls getLinks on the given provider and populates our cache for it.
849 * @param aProvider The provider whose cache will be populated.
850 * @param aCallback The callback to call when finished.
851 * @param aForce When true, populates the provider's cache even when it's
852 * already filled.
853 */
854 _populateProviderCache: function Links_populateProviderCache(aProvider, aCallback, aForce) {
855 if (this._providerLinks.has(aProvider) && !aForce) {
856 aCallback();
857 } else {
858 aProvider.getLinks(links => {
859 // Filter out null and undefined links so we don't have to deal with
860 // them in getLinks when merging links from providers.
861 links = links.filter((link) => !!link);
862 this._providerLinks.set(aProvider, {
863 sortedLinks: links,
864 linkMap: links.reduce((map, link) => {
865 map.set(link.url, link);
866 return map;
867 }, new Map()),
868 });
869 aCallback();
870 });
871 }
872 },
874 /**
875 * Merges the cached lists of links from all providers whose lists are cached.
876 * @return The merged list.
877 */
878 _getMergedProviderLinks: function Links__getMergedProviderLinks() {
879 // Build a list containing a copy of each provider's sortedLinks list.
880 let linkLists = [];
881 for (let links of this._providerLinks.values()) {
882 linkLists.push(links.sortedLinks.slice());
883 }
885 function getNextLink() {
886 let minLinks = null;
887 for (let links of linkLists) {
888 if (links.length &&
889 (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0))
890 minLinks = links;
891 }
892 return minLinks ? minLinks.shift() : null;
893 }
895 let finalLinks = [];
896 for (let nextLink = getNextLink();
897 nextLink && finalLinks.length < this.maxNumLinks;
898 nextLink = getNextLink()) {
899 finalLinks.push(nextLink);
900 }
902 return finalLinks;
903 },
905 /**
906 * Called by a provider to notify us when a single link changes.
907 * @param aProvider The provider whose link changed.
908 * @param aLink The link that changed. If the link is new, it must have all
909 * of the _sortProperties. Otherwise, it may have as few or as
910 * many as is convenient.
911 */
912 onLinkChanged: function Links_onLinkChanged(aProvider, aLink) {
913 if (!("url" in aLink))
914 throw new Error("Changed links must have a url property");
916 let links = this._providerLinks.get(aProvider);
917 if (!links)
918 // This is not an error, it just means that between the time the provider
919 // was added and the future time we call getLinks on it, it notified us of
920 // a change.
921 return;
923 let { sortedLinks, linkMap } = links;
924 let existingLink = linkMap.get(aLink.url);
925 let insertionLink = null;
926 let updatePages = false;
928 if (existingLink) {
929 // Update our copy's position in O(lg n) by first removing it from its
930 // list. It's important to do this before modifying its properties.
931 if (this._sortProperties.some(prop => prop in aLink)) {
932 let idx = this._indexOf(sortedLinks, existingLink);
933 if (idx < 0) {
934 throw new Error("Link should be in _sortedLinks if in _linkMap");
935 }
936 sortedLinks.splice(idx, 1);
937 // Update our copy's properties.
938 for (let prop of this._sortProperties) {
939 if (prop in aLink) {
940 existingLink[prop] = aLink[prop];
941 }
942 }
943 // Finally, reinsert our copy below.
944 insertionLink = existingLink;
945 }
946 // Update our copy's title in O(1).
947 if ("title" in aLink && aLink.title != existingLink.title) {
948 existingLink.title = aLink.title;
949 updatePages = true;
950 }
951 }
952 else if (this._sortProperties.every(prop => prop in aLink)) {
953 // Before doing the O(lg n) insertion below, do an O(1) check for the
954 // common case where the new link is too low-ranked to be in the list.
955 if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
956 let lastLink = sortedLinks[sortedLinks.length - 1];
957 if (this.compareLinks(lastLink, aLink) < 0) {
958 return;
959 }
960 }
961 // Copy the link object so that changes later made to it by the caller
962 // don't affect our copy.
963 insertionLink = {};
964 for (let prop in aLink) {
965 insertionLink[prop] = aLink[prop];
966 }
967 linkMap.set(aLink.url, insertionLink);
968 }
970 if (insertionLink) {
971 let idx = this._insertionIndexOf(sortedLinks, insertionLink);
972 sortedLinks.splice(idx, 0, insertionLink);
973 if (sortedLinks.length > aProvider.maxNumLinks) {
974 let lastLink = sortedLinks.pop();
975 linkMap.delete(lastLink.url);
976 }
977 updatePages = true;
978 }
980 if (updatePages)
981 AllPages.scheduleUpdateForHiddenPages();
982 },
984 /**
985 * Called by a provider to notify us when many links change.
986 */
987 onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
988 this._populateProviderCache(aProvider, () => {
989 AllPages.scheduleUpdateForHiddenPages();
990 }, true);
991 },
993 _indexOf: function Links__indexOf(aArray, aLink) {
994 return this._binsearch(aArray, aLink, "indexOf");
995 },
997 _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
998 return this._binsearch(aArray, aLink, "insertionIndexOf");
999 },
1001 _binsearch: function Links__binsearch(aArray, aLink, aMethod) {
1002 return BinarySearch[aMethod](aArray, aLink, this.compareLinks.bind(this));
1003 },
1005 /**
1006 * Implements the nsIObserver interface to get notified about browser history
1007 * sanitization.
1008 */
1009 observe: function Links_observe(aSubject, aTopic, aData) {
1010 // Make sure to update open about:newtab instances. If there are no opened
1011 // pages we can just wait for the next new tab to populate the cache again.
1012 if (AllPages.length && AllPages.enabled)
1013 this.populateCache(function () { AllPages.update() }, true);
1014 else
1015 this.resetCache();
1016 },
1018 /**
1019 * Adds a sanitization observer and turns itself into a no-op after the first
1020 * invokation.
1021 */
1022 _addObserver: function Links_addObserver() {
1023 Services.obs.addObserver(this, "browser:purge-session-history", true);
1024 this._addObserver = function () {};
1025 },
1027 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
1028 Ci.nsISupportsWeakReference])
1029 };
1031 /**
1032 * Singleton used to collect telemetry data.
1033 *
1034 */
1035 let Telemetry = {
1036 /**
1037 * Initializes object.
1038 */
1039 init: function Telemetry_init() {
1040 Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY, false);
1041 },
1043 /**
1044 * Collects data.
1045 */
1046 _collect: function Telemetry_collect() {
1047 let probes = [
1048 { histogram: "NEWTAB_PAGE_ENABLED",
1049 value: AllPages.enabled },
1050 { histogram: "NEWTAB_PAGE_PINNED_SITES_COUNT",
1051 value: PinnedLinks.links.length },
1052 { histogram: "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
1053 value: Object.keys(BlockedLinks.links).length }
1054 ];
1056 probes.forEach(function Telemetry_collect_forEach(aProbe) {
1057 Services.telemetry.getHistogramById(aProbe.histogram)
1058 .add(aProbe.value);
1059 });
1060 },
1062 /**
1063 * Listens for gather telemetry topic.
1064 */
1065 observe: function Telemetry_observe(aSubject, aTopic, aData) {
1066 this._collect();
1067 }
1068 };
1070 /**
1071 * Singleton that checks if a given link should be displayed on about:newtab
1072 * or if we should rather not do it for security reasons. URIs that inherit
1073 * their caller's principal will be filtered.
1074 */
1075 let LinkChecker = {
1076 _cache: {},
1078 get flags() {
1079 return Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
1080 Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS;
1081 },
1083 checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
1084 if (!(aURI in this._cache))
1085 this._cache[aURI] = this._doCheckLoadURI(aURI);
1087 return this._cache[aURI];
1088 },
1090 _doCheckLoadURI: function Links_doCheckLoadURI(aURI) {
1091 try {
1092 Services.scriptSecurityManager.
1093 checkLoadURIStrWithPrincipal(gPrincipal, aURI, this.flags);
1094 return true;
1095 } catch (e) {
1096 // We got a weird URI or one that would inherit the caller's principal.
1097 return false;
1098 }
1099 }
1100 };
1102 let ExpirationFilter = {
1103 init: function ExpirationFilter_init() {
1104 PageThumbs.addExpirationFilter(this);
1105 },
1107 filterForThumbnailExpiration:
1108 function ExpirationFilter_filterForThumbnailExpiration(aCallback) {
1109 if (!AllPages.enabled) {
1110 aCallback([]);
1111 return;
1112 }
1114 Links.populateCache(function () {
1115 let urls = [];
1117 // Add all URLs to the list that we want to keep thumbnails for.
1118 for (let link of Links.getLinks().slice(0, 25)) {
1119 if (link && link.url)
1120 urls.push(link.url);
1121 }
1123 aCallback(urls);
1124 });
1125 }
1126 };
1128 /**
1129 * Singleton that provides the public API of this JSM.
1130 */
1131 this.NewTabUtils = {
1132 _initialized: false,
1134 init: function NewTabUtils_init() {
1135 if (this.initWithoutProviders()) {
1136 PlacesProvider.init();
1137 Links.addProvider(PlacesProvider);
1138 }
1139 },
1141 initWithoutProviders: function NewTabUtils_initWithoutProviders() {
1142 if (!this._initialized) {
1143 this._initialized = true;
1144 ExpirationFilter.init();
1145 Telemetry.init();
1146 return true;
1147 }
1148 return false;
1149 },
1151 /**
1152 * Restores all sites that have been removed from the grid.
1153 */
1154 restore: function NewTabUtils_restore() {
1155 Storage.clear();
1156 Links.resetCache();
1157 PinnedLinks.resetCache();
1158 BlockedLinks.resetCache();
1160 Links.populateCache(function () {
1161 AllPages.update();
1162 }, true);
1163 },
1165 /**
1166 * Undoes all sites that have been removed from the grid and keep the pinned
1167 * tabs.
1168 * @param aCallback the callback method.
1169 */
1170 undoAll: function NewTabUtils_undoAll(aCallback) {
1171 Storage.remove("blockedLinks");
1172 Links.resetCache();
1173 BlockedLinks.resetCache();
1174 Links.populateCache(aCallback, true);
1175 },
1177 links: Links,
1178 allPages: AllPages,
1179 linkChecker: LinkChecker,
1180 pinnedLinks: PinnedLinks,
1181 blockedLinks: BlockedLinks,
1182 gridPrefs: GridPrefs
1183 };