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/. */
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", {});
29 XPCOMUtils.defineLazyGetter(this, "gPrincipal", function () {
30 let uri = Services.io.newURI("about:newtab", null, null);
31 return Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
34 XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
35 return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
38 XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
39 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
40 .createInstance(Ci.nsIScriptableUnicodeConverter);
41 converter.charset = 'utf8';
45 // Boolean preferences that control newtab content
46 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
47 const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
49 // The preference that tells the number of rows of the newtab grid.
50 const PREF_NEWTAB_ROWS = "browser.newtabpage.rows";
52 // The preference that tells the number of columns of the newtab grid.
53 const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
55 // The maximum number of results PlacesProvider retrieves from history.
56 const HISTORY_RESULTS_LIMIT = 100;
58 // The maximum number of links Links.getLinks will return.
59 const LINKS_GET_LINKS_LIMIT = 100;
61 // The gather telemetry topic.
62 const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
64 // The amount of time we wait while coalescing updates for hidden pages.
65 const SCHEDULE_UPDATE_TIMEOUT_MS = 1000;
68 * Calculate the MD5 hash for a string.
70 * The string to convert.
71 * @return The base64 representation of the MD5 hash.
73 function toHash(aValue) {
74 let value = gUnicodeConverter.convertToByteArray(aValue);
75 gCryptoHash.init(gCryptoHash.MD5);
76 gCryptoHash.update(value, value.length);
77 return gCryptoHash.finish(true);
81 * Singleton that provides storage functionality.
83 XPCOMUtils.defineLazyGetter(this, "Storage", function() {
84 return new LinksStorage();
87 function LinksStorage() {
88 // Handle migration of data across versions.
90 if (this._storedVersion < this._version) {
91 // This is either an upgrade, or version information is missing.
92 if (this._storedVersion < 1) {
93 // Version 1 moved data from DOM Storage to prefs. Since migrating from
94 // version 0 is no more supported, we just reportError a dataloss later.
95 throw new Error("Unsupported newTab storage version");
97 // Add further migration steps here.
100 // This is a downgrade. Since we cannot predict future, upgrades should
101 // be backwards compatible. We will set the version to the old value
102 // regardless, so, on next upgrade, the migration steps will run again.
103 // For this reason, they should also be able to run multiple times, even
104 // on top of an already up-to-date storage.
107 // Something went wrong in the update process, we can't recover from here,
108 // so just clear the storage and start from scratch (dataloss!).
109 Components.utils.reportError(
110 "Unable to migrate the newTab storage to the current version. "+
111 "Restarting from scratch.\n" + ex);
115 // Set the version to the current one.
116 this._storedVersion = this._version;
119 LinksStorage.prototype = {
122 get _prefs() Object.freeze({
123 pinnedLinks: "browser.newtabpage.pinned",
124 blockedLinks: "browser.newtabpage.blocked",
127 get _storedVersion() {
128 if (this.__storedVersion === undefined) {
130 this.__storedVersion =
131 Services.prefs.getIntPref("browser.newtabpage.storageVersion");
133 // The storage version is unknown, so either:
134 // - it's a new profile
135 // - it's a profile where versioning information got lost
136 // In this case we still run through all of the valid migrations,
137 // starting from 1, as if it was a downgrade. As previously stated the
138 // migrations should already support running on an updated store.
139 this.__storedVersion = 1;
142 return this.__storedVersion;
144 set _storedVersion(aValue) {
145 Services.prefs.setIntPref("browser.newtabpage.storageVersion", aValue);
146 this.__storedVersion = aValue;
151 * Gets the value for a given key from the storage.
152 * @param aKey The storage key (a string).
153 * @param aDefault A default value if the key doesn't exist.
154 * @return The value for the given key.
156 get: function Storage_get(aKey, aDefault) {
159 let prefValue = Services.prefs.getComplexValue(this._prefs[aKey],
160 Ci.nsISupportsString).data;
161 value = JSON.parse(prefValue);
163 return value || aDefault;
167 * Sets the storage value for a given key.
168 * @param aKey The storage key (a string).
169 * @param aValue The value to set.
171 set: function Storage_set(aKey, aValue) {
172 // Page titles may contain unicode, thus use complex values.
173 let string = Cc["@mozilla.org/supports-string;1"]
174 .createInstance(Ci.nsISupportsString);
175 string.data = JSON.stringify(aValue);
176 Services.prefs.setComplexValue(this._prefs[aKey], Ci.nsISupportsString,
181 * Removes the storage value for a given key.
182 * @param aKey The storage key (a string).
184 remove: function Storage_remove(aKey) {
185 Services.prefs.clearUserPref(this._prefs[aKey]);
189 * Clears the storage and removes all values.
191 clear: function Storage_clear() {
192 for (let key in this._prefs) {
200 * Singleton that serves as a registry for all open 'New Tab Page's.
204 * The array containing all active pages.
209 * Cached value that tells whether the New Tab Page feature is enabled.
214 * Cached value that tells whether the New Tab Page feature is enhanced.
219 * Adds a page to the internal list of pages.
220 * @param aPage The page to register.
222 register: function AllPages_register(aPage) {
223 this._pages.push(aPage);
228 * Removes a page from the internal list of pages.
229 * @param aPage The page to unregister.
231 unregister: function AllPages_unregister(aPage) {
232 let index = this._pages.indexOf(aPage);
234 this._pages.splice(index, 1);
238 * Returns whether the 'New Tab Page' is enabled.
241 if (this._enabled === null)
242 this._enabled = Services.prefs.getBoolPref(PREF_NEWTAB_ENABLED);
244 return this._enabled;
248 * Enables or disables the 'New Tab Page' feature.
250 set enabled(aEnabled) {
251 if (this.enabled != aEnabled)
252 Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, !!aEnabled);
256 * Returns whether the history tiles are enhanced.
259 if (this._enhanced === null)
260 this._enhanced = Services.prefs.getBoolPref(PREF_NEWTAB_ENHANCED);
262 return this._enhanced;
266 * Enables or disables the enhancement of history tiles feature.
268 set enhanced(aEnhanced) {
269 if (this.enhanced != aEnhanced)
270 Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, !!aEnhanced);
274 * Returns the number of registered New Tab Pages (i.e. the number of open
275 * about:newtab instances).
278 return this._pages.length;
282 * Updates all currently active pages but the given one.
283 * @param aExceptPage The page to exclude from updating.
284 * @param aHiddenPagesOnly If true, only pages hidden in the preloader are
287 update: function AllPages_update(aExceptPage, aHiddenPagesOnly=false) {
288 this._pages.forEach(function (aPage) {
289 if (aExceptPage != aPage)
290 aPage.update(aHiddenPagesOnly);
295 * Many individual link changes may happen in a small amount of time over
296 * multiple turns of the event loop. This method coalesces updates by waiting
297 * a small amount of time before updating hidden pages.
299 scheduleUpdateForHiddenPages: function AllPages_scheduleUpdateForHiddenPages() {
300 if (!this._scheduleUpdateTimeout) {
301 this._scheduleUpdateTimeout = Timer.setTimeout(() => {
302 delete this._scheduleUpdateTimeout;
303 this.update(null, true);
304 }, SCHEDULE_UPDATE_TIMEOUT_MS);
309 * Implements the nsIObserver interface to get notified when the preference
310 * value changes or when a new copy of a page thumbnail is available.
312 observe: function AllPages_observe(aSubject, aTopic, aData) {
313 if (aTopic == "nsPref:changed") {
314 // Clear the cached value.
316 case PREF_NEWTAB_ENABLED:
317 this._enabled = null;
319 case PREF_NEWTAB_ENHANCED:
320 this._enhanced = null;
324 // and all notifications get forwarded to each page.
325 this._pages.forEach(function (aPage) {
326 aPage.observe(aSubject, aTopic, aData);
331 * Adds a preference and new thumbnail observer and turns itself into a
332 * no-op after the first invokation.
334 _addObserver: function AllPages_addObserver() {
335 Services.prefs.addObserver(PREF_NEWTAB_ENABLED, this, true);
336 Services.prefs.addObserver(PREF_NEWTAB_ENHANCED, this, true);
337 Services.obs.addObserver(this, "page-thumbnail:create", true);
338 this._addObserver = function () {};
341 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
342 Ci.nsISupportsWeakReference])
346 * Singleton that keeps Grid preferences
350 * Cached value that tells the number of rows of newtab grid.
354 if (!this._gridRows) {
355 this._gridRows = Math.max(1, Services.prefs.getIntPref(PREF_NEWTAB_ROWS));
358 return this._gridRows;
362 * Cached value that tells the number of columns of newtab grid.
366 if (!this._gridColumns) {
367 this._gridColumns = Math.max(1, Services.prefs.getIntPref(PREF_NEWTAB_COLUMNS));
370 return this._gridColumns;
375 * Initializes object. Adds a preference observer
377 init: function GridPrefs_init() {
378 Services.prefs.addObserver(PREF_NEWTAB_ROWS, this, false);
379 Services.prefs.addObserver(PREF_NEWTAB_COLUMNS, this, false);
383 * Implements the nsIObserver interface to get notified when the preference
386 observe: function GridPrefs_observe(aSubject, aTopic, aData) {
387 if (aData == PREF_NEWTAB_ROWS) {
388 this._gridRows = null;
390 this._gridColumns = null;
400 * Singleton that keeps track of all pinned links and their positions in the
405 * The cached list of pinned links.
410 * The array of pinned links.
414 this._links = Storage.get("pinnedLinks", []);
420 * Pins a link at the given position.
421 * @param aLink The link to pin.
422 * @param aIndex The grid index to pin the cell at.
424 pin: function PinnedLinks_pin(aLink, aIndex) {
425 // Clear the link's old position, if any.
428 this.links[aIndex] = aLink;
433 * Unpins a given link.
434 * @param aLink The link to unpin.
436 unpin: function PinnedLinks_unpin(aLink) {
437 let index = this._indexOfLink(aLink);
440 let links = this.links;
442 // trim trailing nulls
443 let i=links.length-1;
444 while (i >= 0 && links[i] == null)
451 * Saves the current list of pinned links.
453 save: function PinnedLinks_save() {
454 Storage.set("pinnedLinks", this.links);
458 * Checks whether a given link is pinned.
459 * @params aLink The link to check.
460 * @return whether The link is pinned.
462 isPinned: function PinnedLinks_isPinned(aLink) {
463 return this._indexOfLink(aLink) != -1;
467 * Resets the links cache.
469 resetCache: function PinnedLinks_resetCache() {
474 * Finds the index of a given link in the list of pinned links.
475 * @param aLink The link to find an index for.
476 * @return The link's index.
478 _indexOfLink: function PinnedLinks_indexOfLink(aLink) {
479 for (let i = 0; i < this.links.length; i++) {
480 let link = this.links[i];
481 if (link && link.url == aLink.url)
485 // The given link is unpinned.
491 * Singleton that keeps track of all blocked links in the grid.
495 * The cached list of blocked links.
500 * The list of blocked links.
504 this._links = Storage.get("blockedLinks", {});
510 * Blocks a given link.
511 * @param aLink The link to block.
513 block: function BlockedLinks_block(aLink) {
514 this.links[toHash(aLink.url)] = 1;
517 // Make sure we unpin blocked links.
518 PinnedLinks.unpin(aLink);
522 * Unblocks a given link.
523 * @param aLink The link to unblock.
525 unblock: function BlockedLinks_unblock(aLink) {
526 if (this.isBlocked(aLink)) {
527 delete this.links[toHash(aLink.url)];
533 * Saves the current list of blocked links.
535 save: function BlockedLinks_save() {
536 Storage.set("blockedLinks", this.links);
540 * Returns whether a given link is blocked.
541 * @param aLink The link to check.
543 isBlocked: function BlockedLinks_isBlocked(aLink) {
544 return (toHash(aLink.url) in this.links);
548 * Checks whether the list of blocked links is empty.
549 * @return Whether the list is empty.
551 isEmpty: function BlockedLinks_isEmpty() {
552 return Object.keys(this.links).length == 0;
556 * Resets the links cache.
558 resetCache: function BlockedLinks_resetCache() {
564 * Singleton that serves as the default link provider for the grid. It queries
565 * the history to retrieve the most frequently visited sites.
567 let PlacesProvider = {
569 * Set this to change the maximum number of links the provider will provide.
571 maxNumLinks: HISTORY_RESULTS_LIMIT,
574 * Must be called before the provider is used.
576 init: function PlacesProvider_init() {
577 PlacesUtils.history.addObserver(this, true);
581 * Gets the current set of links delivered by this provider.
582 * @param aCallback The function that the array of links is passed to.
584 getLinks: function PlacesProvider_getLinks(aCallback) {
585 let options = PlacesUtils.history.getNewQueryOptions();
586 options.maxResults = this.maxNumLinks;
588 // Sort by frecency, descending.
589 options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING
594 handleResult: function (aResultSet) {
597 while ((row = aResultSet.getNextRow())) {
598 let url = row.getResultByIndex(1);
599 if (LinkChecker.checkLoadURI(url)) {
600 let title = row.getResultByIndex(2);
601 let frecency = row.getResultByIndex(12);
602 let lastVisitDate = row.getResultByIndex(5);
607 lastVisitDate: lastVisitDate,
614 handleError: function (aError) {
615 // Should we somehow handle this error?
619 handleCompletion: function (aReason) {
620 // The Places query breaks ties in frecency by place ID descending, but
621 // that's different from how Links.compareLinks breaks ties, because
622 // compareLinks doesn't have access to place IDs. It's very important
623 // that the initial list of links is sorted in the same order imposed by
624 // compareLinks, because Links uses compareLinks to perform binary
625 // searches on the list. So, ensure the list is so ordered.
628 while (i < links.length) {
629 if (Links.compareLinks(links[i - 1], links[i]) > 0)
630 outOfOrder.push(links.splice(i, 1)[0]);
634 for (let link of outOfOrder) {
635 i = BinarySearch.insertionIndexOf(links, link,
636 Links.compareLinks.bind(Links));
637 links.splice(i, 0, link);
644 // Execute the query.
645 let query = PlacesUtils.history.getNewQuery();
646 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
647 db.asyncExecuteLegacyQueries([query], 1, options, callback);
651 * Registers an object that will be notified when the provider's links change.
652 * @param aObserver An object with the following optional properties:
653 * * onLinkChanged: A function that's called when a single link
654 * changes. It's passed the provider and the link object. Only the
655 * link's `url` property is guaranteed to be present. If its `title`
656 * property is present, then its title has changed, and the
657 * property's value is the new title. If any sort properties are
658 * present, then its position within the provider's list of links may
659 * have changed, and the properties' values are the new sort-related
660 * values. Note that this link may not necessarily have been present
661 * in the lists returned from any previous calls to getLinks.
662 * * onManyLinksChanged: A function that's called when many links
663 * change at once. It's passed the provider. You should call
664 * getLinks to get the provider's new list of links.
666 addObserver: function PlacesProvider_addObserver(aObserver) {
667 this._observers.push(aObserver);
673 * Called by the history service.
675 onFrecencyChanged: function PlacesProvider_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) {
676 // The implementation of the query in getLinks excludes hidden and
677 // unvisited pages, so it's important to exclude them here, too.
678 if (!aHidden && aLastVisitDate) {
679 this._callObservers("onLinkChanged", {
681 frecency: aNewFrecency,
682 lastVisitDate: aLastVisitDate,
689 * Called by the history service.
691 onManyFrecenciesChanged: function PlacesProvider_onManyFrecenciesChanged() {
692 this._callObservers("onManyLinksChanged");
696 * Called by the history service.
698 onTitleChanged: function PlacesProvider_onTitleChanged(aURI, aNewTitle, aGUID) {
699 this._callObservers("onLinkChanged", {
705 _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
706 for (let obs of this._observers) {
707 if (obs[aMethodName]) {
709 obs[aMethodName](this, aArg);
717 QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
718 Ci.nsISupportsWeakReference]),
722 * Singleton that provides access to all links contained in the grid (including
723 * the ones that don't fit on the grid). A link is a plain object that looks
727 * url: "http://www.mozilla.org/",
730 * lastVisitDate: 1394678824766431,
735 * The maximum number of links returned by getLinks.
737 maxNumLinks: LINKS_GET_LINKS_LIMIT,
740 * The link providers.
742 _providers: new Set(),
745 * A mapping from each provider to an object { sortedLinks, linkMap }.
746 * sortedLinks is the cached, sorted array of links for the provider. linkMap
747 * is a Map from link URLs to link objects.
749 _providerLinks: new Map(),
752 * The properties of link objects used to sort them.
761 * List of callbacks waiting for the cache to be populated.
763 _populateCallbacks: [],
766 * Adds a link provider.
767 * @param aProvider The link provider.
769 addProvider: function Links_addProvider(aProvider) {
770 this._providers.add(aProvider);
771 aProvider.addObserver(this);
775 * Removes a link provider.
776 * @param aProvider The link provider.
778 removeProvider: function Links_removeProvider(aProvider) {
779 if (!this._providers.delete(aProvider))
780 throw new Error("Unknown provider");
781 this._providerLinks.delete(aProvider);
785 * Populates the cache with fresh links from the providers.
786 * @param aCallback The callback to call when finished (optional).
787 * @param aForce When true, populates the cache even when it's already filled.
789 populateCache: function Links_populateCache(aCallback, aForce) {
790 let callbacks = this._populateCallbacks;
792 // Enqueue the current callback.
793 callbacks.push(aCallback);
795 // There was a callback waiting already, thus the cache has not yet been
797 if (callbacks.length > 1)
800 function executeCallbacks() {
801 while (callbacks.length) {
802 let callback = callbacks.shift();
807 // We want to proceed even if a callback fails.
813 let numProvidersRemaining = this._providers.size;
814 for (let provider of this._providers) {
815 this._populateProviderCache(provider, () => {
816 if (--numProvidersRemaining == 0)
825 * Gets the current set of links contained in the grid.
826 * @return The links in the grid.
828 getLinks: function Links_getLinks() {
829 let pinnedLinks = Array.slice(PinnedLinks.links);
830 let links = this._getMergedProviderLinks();
832 let sites = new Set();
833 for (let link of pinnedLinks) {
835 sites.add(NewTabUtils.extractSite(link.url));
838 // Filter blocked and pinned links and duplicate base domains.
839 links = links.filter(function (link) {
840 let site = NewTabUtils.extractSite(link.url);
841 if (site == null || sites.has(site))
845 return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
848 // Try to fill the gaps between pinned links.
849 for (let i = 0; i < pinnedLinks.length && links.length; i++)
851 pinnedLinks[i] = links.shift();
853 // Append the remaining links if any.
855 pinnedLinks = pinnedLinks.concat(links);
861 * Resets the links cache.
863 resetCache: function Links_resetCache() {
864 this._providerLinks.clear();
868 * Compares two links.
869 * @param aLink1 The first link.
870 * @param aLink2 The second link.
871 * @return A negative number if aLink1 is ordered before aLink2, zero if
872 * aLink1 and aLink2 have the same ordering, or a positive number if
873 * aLink1 is ordered after aLink2.
875 compareLinks: function Links_compareLinks(aLink1, aLink2) {
876 for (let prop of this._sortProperties) {
877 if (!(prop in aLink1) || !(prop in aLink2))
878 throw new Error("Comparable link missing required property: " + prop);
880 return aLink2.frecency - aLink1.frecency ||
881 aLink2.lastVisitDate - aLink1.lastVisitDate ||
882 aLink1.url.localeCompare(aLink2.url);
886 * Calls getLinks on the given provider and populates our cache for it.
887 * @param aProvider The provider whose cache will be populated.
888 * @param aCallback The callback to call when finished.
889 * @param aForce When true, populates the provider's cache even when it's
892 _populateProviderCache: function Links_populateProviderCache(aProvider, aCallback, aForce) {
893 if (this._providerLinks.has(aProvider) && !aForce) {
896 aProvider.getLinks(links => {
897 // Filter out null and undefined links so we don't have to deal with
898 // them in getLinks when merging links from providers.
899 links = links.filter((link) => !!link);
900 this._providerLinks.set(aProvider, {
902 linkMap: links.reduce((map, link) => {
903 map.set(link.url, link);
913 * Merges the cached lists of links from all providers whose lists are cached.
914 * @return The merged list.
916 _getMergedProviderLinks: function Links__getMergedProviderLinks() {
917 // Build a list containing a copy of each provider's sortedLinks list.
919 for (let links of this._providerLinks.values()) {
920 linkLists.push(links.sortedLinks.slice());
923 function getNextLink() {
925 for (let links of linkLists) {
927 (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0))
930 return minLinks ? minLinks.shift() : null;
934 for (let nextLink = getNextLink();
935 nextLink && finalLinks.length < this.maxNumLinks;
936 nextLink = getNextLink()) {
937 finalLinks.push(nextLink);
944 * Called by a provider to notify us when a single link changes.
945 * @param aProvider The provider whose link changed.
946 * @param aLink The link that changed. If the link is new, it must have all
947 * of the _sortProperties. Otherwise, it may have as few or as
948 * many as is convenient.
950 onLinkChanged: function Links_onLinkChanged(aProvider, aLink) {
951 if (!("url" in aLink))
952 throw new Error("Changed links must have a url property");
954 let links = this._providerLinks.get(aProvider);
956 // This is not an error, it just means that between the time the provider
957 // was added and the future time we call getLinks on it, it notified us of
961 let { sortedLinks, linkMap } = links;
962 let existingLink = linkMap.get(aLink.url);
963 let insertionLink = null;
964 let updatePages = false;
967 // Update our copy's position in O(lg n) by first removing it from its
968 // list. It's important to do this before modifying its properties.
969 if (this._sortProperties.some(prop => prop in aLink)) {
970 let idx = this._indexOf(sortedLinks, existingLink);
972 throw new Error("Link should be in _sortedLinks if in _linkMap");
974 sortedLinks.splice(idx, 1);
975 // Update our copy's properties.
976 for (let prop of this._sortProperties) {
978 existingLink[prop] = aLink[prop];
981 // Finally, reinsert our copy below.
982 insertionLink = existingLink;
984 // Update our copy's title in O(1).
985 if ("title" in aLink && aLink.title != existingLink.title) {
986 existingLink.title = aLink.title;
990 else if (this._sortProperties.every(prop => prop in aLink)) {
991 // Before doing the O(lg n) insertion below, do an O(1) check for the
992 // common case where the new link is too low-ranked to be in the list.
993 if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
994 let lastLink = sortedLinks[sortedLinks.length - 1];
995 if (this.compareLinks(lastLink, aLink) < 0) {
999 // Copy the link object so that changes later made to it by the caller
1000 // don't affect our copy.
1002 for (let prop in aLink) {
1003 insertionLink[prop] = aLink[prop];
1005 linkMap.set(aLink.url, insertionLink);
1008 if (insertionLink) {
1009 let idx = this._insertionIndexOf(sortedLinks, insertionLink);
1010 sortedLinks.splice(idx, 0, insertionLink);
1011 if (sortedLinks.length > aProvider.maxNumLinks) {
1012 let lastLink = sortedLinks.pop();
1013 linkMap.delete(lastLink.url);
1019 AllPages.scheduleUpdateForHiddenPages();
1023 * Called by a provider to notify us when many links change.
1025 onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
1026 this._populateProviderCache(aProvider, () => {
1027 AllPages.scheduleUpdateForHiddenPages();
1031 _indexOf: function Links__indexOf(aArray, aLink) {
1032 return this._binsearch(aArray, aLink, "indexOf");
1035 _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
1036 return this._binsearch(aArray, aLink, "insertionIndexOf");
1039 _binsearch: function Links__binsearch(aArray, aLink, aMethod) {
1040 return BinarySearch[aMethod](aArray, aLink, this.compareLinks.bind(this));
1044 * Implements the nsIObserver interface to get notified about browser history
1047 observe: function Links_observe(aSubject, aTopic, aData) {
1048 // Make sure to update open about:newtab instances. If there are no opened
1049 // pages we can just wait for the next new tab to populate the cache again.
1050 if (AllPages.length && AllPages.enabled)
1051 this.populateCache(function () { AllPages.update() }, true);
1057 * Adds a sanitization observer and turns itself into a no-op after the first
1060 _addObserver: function Links_addObserver() {
1061 Services.obs.addObserver(this, "browser:purge-session-history", true);
1062 this._addObserver = function () {};
1065 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
1066 Ci.nsISupportsWeakReference])
1070 * Singleton used to collect telemetry data.
1075 * Initializes object.
1077 init: function Telemetry_init() {
1078 Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY, false);
1084 _collect: function Telemetry_collect() {
1086 { histogram: "NEWTAB_PAGE_ENABLED",
1087 value: AllPages.enabled },
1088 { histogram: "NEWTAB_PAGE_PINNED_SITES_COUNT",
1089 value: PinnedLinks.links.length },
1090 { histogram: "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
1091 value: Object.keys(BlockedLinks.links).length }
1094 probes.forEach(function Telemetry_collect_forEach(aProbe) {
1095 Services.telemetry.getHistogramById(aProbe.histogram)
1101 * Listens for gather telemetry topic.
1103 observe: function Telemetry_observe(aSubject, aTopic, aData) {
1109 * Singleton that checks if a given link should be displayed on about:newtab
1110 * or if we should rather not do it for security reasons. URIs that inherit
1111 * their caller's principal will be filtered.
1117 return Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
1118 Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS;
1121 checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
1122 if (!(aURI in this._cache))
1123 this._cache[aURI] = this._doCheckLoadURI(aURI);
1125 return this._cache[aURI];
1128 _doCheckLoadURI: function Links_doCheckLoadURI(aURI) {
1130 Services.scriptSecurityManager.
1131 checkLoadURIStrWithPrincipal(gPrincipal, aURI, this.flags);
1134 // We got a weird URI or one that would inherit the caller's principal.
1140 let ExpirationFilter = {
1141 init: function ExpirationFilter_init() {
1142 PageThumbs.addExpirationFilter(this);
1145 filterForThumbnailExpiration:
1146 function ExpirationFilter_filterForThumbnailExpiration(aCallback) {
1147 if (!AllPages.enabled) {
1152 Links.populateCache(function () {
1155 // Add all URLs to the list that we want to keep thumbnails for.
1156 for (let link of Links.getLinks().slice(0, 25)) {
1157 if (link && link.url)
1158 urls.push(link.url);
1167 * Singleton that provides the public API of this JSM.
1169 this.NewTabUtils = {
1170 _initialized: false,
1173 * Extract a "site" from a url in a way that multiple urls of a "site" returns
1175 * @param aUrl Url spec string
1176 * @return The "site" string or null
1178 extractSite: function Links_extractSite(url) {
1181 uri = Services.io.newURI(url, null, null);
1186 // Strip off common subdomains of the same site (e.g., www, load balancer)
1187 return uri.asciiHost.replace(/^(m|mobile|www\d*)\./, "");
1190 init: function NewTabUtils_init() {
1191 if (this.initWithoutProviders()) {
1192 PlacesProvider.init();
1193 Links.addProvider(PlacesProvider);
1197 initWithoutProviders: function NewTabUtils_initWithoutProviders() {
1198 if (!this._initialized) {
1199 this._initialized = true;
1200 ExpirationFilter.init();
1208 * Restores all sites that have been removed from the grid.
1210 restore: function NewTabUtils_restore() {
1213 PinnedLinks.resetCache();
1214 BlockedLinks.resetCache();
1216 Links.populateCache(function () {
1222 * Undoes all sites that have been removed from the grid and keep the pinned
1224 * @param aCallback the callback method.
1226 undoAll: function NewTabUtils_undoAll(aCallback) {
1227 Storage.remove("blockedLinks");
1229 BlockedLinks.resetCache();
1230 Links.populateCache(aCallback, true);
1235 linkChecker: LinkChecker,
1236 pinnedLinks: PinnedLinks,
1237 blockedLinks: BlockedLinks,
1238 gridPrefs: GridPrefs