Bug 1857998 [wpt PR 42432] - [css-nesting-ident] Enable relaxed syntax, a=testonly
[gecko.git] / toolkit / modules / NewTabUtils.sys.mjs
blob40f53af982f3e5fbb1747bfca642c2f7720f89ab
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 // Android tests don't import these properly, so guard against that
6 let shortURL = {};
7 let searchShortcuts = {};
8 let didSuccessfulImport = false;
9 try {
10   shortURL = ChromeUtils.import("resource://activity-stream/lib/ShortURL.jsm");
11   searchShortcuts = ChromeUtils.importESModule(
12     "resource://activity-stream/lib/SearchShortcuts.sys.mjs"
13   );
14   didSuccessfulImport = true;
15 } catch (e) {
16   // The test failed to import these files
19 const lazy = {};
21 ChromeUtils.defineESModuleGetters(lazy, {
22   BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs",
23   PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
24   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
25   Pocket: "chrome://pocket/content/Pocket.sys.mjs",
26   pktApi: "chrome://pocket/content/pktApi.sys.mjs",
27 });
29 let BrowserWindowTracker;
30 try {
31   BrowserWindowTracker = ChromeUtils.importESModule(
32     "resource:///modules/BrowserWindowTracker.sys.mjs"
33   ).BrowserWindowTracker;
34 } catch (e) {
35   // BrowserWindowTracker is used to determine devicePixelRatio in
36   // _addFavicons. We fallback to the value 2 if we can't find a window,
37   // so it's safe to do nothing with this here.
40 ChromeUtils.defineLazyGetter(lazy, "gCryptoHash", function () {
41   return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
42 });
44 // Boolean preferences that control newtab content
45 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
47 // The maximum number of results PlacesProvider retrieves from history.
48 const HISTORY_RESULTS_LIMIT = 100;
50 // The maximum number of links Links.getLinks will return.
51 const LINKS_GET_LINKS_LIMIT = 100;
53 // The gather telemetry topic.
54 const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
56 // Some default frecency threshold for Activity Stream requests
57 const ACTIVITY_STREAM_DEFAULT_FRECENCY = 150;
59 // Some default query limit for Activity Stream requests
60 const ACTIVITY_STREAM_DEFAULT_LIMIT = 12;
62 // Some default seconds ago for Activity Stream recent requests
63 const ACTIVITY_STREAM_DEFAULT_RECENT = 5 * 24 * 60 * 60;
65 // The fallback value for the width of smallFavicon in pixels.
66 // This value will be multiplied by the current window's devicePixelRatio.
67 // If devicePixelRatio cannot be found, it will be multiplied by 2.
68 const DEFAULT_SMALL_FAVICON_WIDTH = 16;
70 const POCKET_UPDATE_TIME = 24 * 60 * 60 * 1000; // 1 day
71 const POCKET_INACTIVE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week
72 const PREF_POCKET_LATEST_SINCE = "extensions.pocket.settings.latestSince";
74 /**
75  * Calculate the MD5 hash for a string.
76  * @param aValue
77  *        The string to convert.
78  * @return The base64 representation of the MD5 hash.
79  */
80 function toHash(aValue) {
81   let value = new TextEncoder().encode(aValue);
82   lazy.gCryptoHash.init(lazy.gCryptoHash.MD5);
83   lazy.gCryptoHash.update(value, value.length);
84   return lazy.gCryptoHash.finish(true);
87 /**
88  * Singleton that provides storage functionality.
89  */
90 ChromeUtils.defineLazyGetter(lazy, "Storage", function () {
91   return new LinksStorage();
92 });
94 function LinksStorage() {
95   // Handle migration of data across versions.
96   try {
97     if (this._storedVersion < this._version) {
98       // This is either an upgrade, or version information is missing.
99       if (this._storedVersion < 1) {
100         // Version 1 moved data from DOM Storage to prefs.  Since migrating from
101         // version 0 is no more supported, we just reportError a dataloss later.
102         throw new Error("Unsupported newTab storage version");
103       }
104       // Add further migration steps here.
105     } else {
106       // This is a downgrade.  Since we cannot predict future, upgrades should
107       // be backwards compatible.  We will set the version to the old value
108       // regardless, so, on next upgrade, the migration steps will run again.
109       // For this reason, they should also be able to run multiple times, even
110       // on top of an already up-to-date storage.
111     }
112   } catch (ex) {
113     // Something went wrong in the update process, we can't recover from here,
114     // so just clear the storage and start from scratch (dataloss!).
115     console.error(
116       "Unable to migrate the newTab storage to the current version. " +
117         "Restarting from scratch.\n",
118       ex
119     );
120     this.clear();
121   }
123   // Set the version to the current one.
124   this._storedVersion = this._version;
127 LinksStorage.prototype = {
128   get _version() {
129     return 1;
130   },
132   get _prefs() {
133     return Object.freeze({
134       pinnedLinks: "browser.newtabpage.pinned",
135       blockedLinks: "browser.newtabpage.blocked",
136     });
137   },
139   get _storedVersion() {
140     if (this.__storedVersion === undefined) {
141       // When the pref is not set, the storage version is unknown, so either:
142       // - it's a new profile
143       // - it's a profile where versioning information got lost
144       // In this case we still run through all of the valid migrations,
145       // starting from 1, as if it was a downgrade.  As previously stated the
146       // migrations should already support running on an updated store.
147       this.__storedVersion = Services.prefs.getIntPref(
148         "browser.newtabpage.storageVersion",
149         1
150       );
151     }
152     return this.__storedVersion;
153   },
154   set _storedVersion(aValue) {
155     Services.prefs.setIntPref("browser.newtabpage.storageVersion", aValue);
156     this.__storedVersion = aValue;
157   },
159   /**
160    * Gets the value for a given key from the storage.
161    * @param aKey The storage key (a string).
162    * @param aDefault A default value if the key doesn't exist.
163    * @return The value for the given key.
164    */
165   get: function Storage_get(aKey, aDefault) {
166     let value;
167     try {
168       let prefValue = Services.prefs.getStringPref(this._prefs[aKey]);
169       value = JSON.parse(prefValue);
170     } catch (e) {}
171     return value || aDefault;
172   },
174   /**
175    * Sets the storage value for a given key.
176    * @param aKey The storage key (a string).
177    * @param aValue The value to set.
178    */
179   set: function Storage_set(aKey, aValue) {
180     // Page titles may contain unicode, thus use complex values.
181     Services.prefs.setStringPref(this._prefs[aKey], JSON.stringify(aValue));
182   },
184   /**
185    * Removes the storage value for a given key.
186    * @param aKey The storage key (a string).
187    */
188   remove: function Storage_remove(aKey) {
189     Services.prefs.clearUserPref(this._prefs[aKey]);
190   },
192   /**
193    * Clears the storage and removes all values.
194    */
195   clear: function Storage_clear() {
196     for (let key in this._prefs) {
197       this.remove(key);
198     }
199   },
203  * Singleton that serves as a registry for all open 'New Tab Page's.
204  */
205 var AllPages = {
206   /**
207    * The array containing all active pages.
208    */
209   _pages: [],
211   /**
212    * Cached value that tells whether the New Tab Page feature is enabled.
213    */
214   _enabled: null,
216   /**
217    * Adds a page to the internal list of pages.
218    * @param aPage The page to register.
219    */
220   register: function AllPages_register(aPage) {
221     this._pages.push(aPage);
222     this._addObserver();
223   },
225   /**
226    * Removes a page from the internal list of pages.
227    * @param aPage The page to unregister.
228    */
229   unregister: function AllPages_unregister(aPage) {
230     let index = this._pages.indexOf(aPage);
231     if (index > -1) {
232       this._pages.splice(index, 1);
233     }
234   },
236   /**
237    * Returns whether the 'New Tab Page' is enabled.
238    */
239   get enabled() {
240     if (this._enabled === null) {
241       this._enabled = Services.prefs.getBoolPref(PREF_NEWTAB_ENABLED);
242     }
244     return this._enabled;
245   },
247   /**
248    * Enables or disables the 'New Tab Page' feature.
249    */
250   set enabled(aEnabled) {
251     if (this.enabled != aEnabled) {
252       Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, !!aEnabled);
253     }
254   },
256   /**
257    * Returns the number of registered New Tab Pages (i.e. the number of open
258    * about:newtab instances).
259    */
260   get length() {
261     return this._pages.length;
262   },
264   /**
265    * Updates all currently active pages but the given one.
266    * @param aExceptPage The page to exclude from updating.
267    * @param aReason The reason for updating all pages.
268    */
269   update(aExceptPage, aReason = "") {
270     for (let page of this._pages.slice()) {
271       if (aExceptPage != page) {
272         page.update(aReason);
273       }
274     }
275   },
277   /**
278    * Implements the nsIObserver interface to get notified when the preference
279    * value changes or when a new copy of a page thumbnail is available.
280    */
281   observe: function AllPages_observe(aSubject, aTopic, aData) {
282     if (aTopic == "nsPref:changed") {
283       // Clear the cached value.
284       switch (aData) {
285         case PREF_NEWTAB_ENABLED:
286           this._enabled = null;
287           break;
288       }
289     }
290     // and all notifications get forwarded to each page.
291     this._pages.forEach(function (aPage) {
292       aPage.observe(aSubject, aTopic, aData);
293     }, this);
294   },
296   /**
297    * Adds a preference and new thumbnail observer and turns itself into a
298    * no-op after the first invokation.
299    */
300   _addObserver: function AllPages_addObserver() {
301     Services.prefs.addObserver(PREF_NEWTAB_ENABLED, this, true);
302     Services.obs.addObserver(this, "page-thumbnail:create", true);
303     this._addObserver = function () {};
304   },
306   QueryInterface: ChromeUtils.generateQI([
307     "nsIObserver",
308     "nsISupportsWeakReference",
309   ]),
313  * Singleton that keeps track of all pinned links and their positions in the
314  * grid.
315  */
316 var PinnedLinks = {
317   /**
318    * The cached list of pinned links.
319    */
320   _links: null,
322   /**
323    * The array of pinned links.
324    */
325   get links() {
326     if (!this._links) {
327       this._links = lazy.Storage.get("pinnedLinks", []);
328     }
330     return this._links;
331   },
333   /**
334    * Pins a link at the given position.
335    * @param aLink The link to pin.
336    * @param aIndex The grid index to pin the cell at.
337    * @return true if link changes, false otherwise
338    */
339   pin: function PinnedLinks_pin(aLink, aIndex) {
340     // Clear the link's old position, if any.
341     this.unpin(aLink);
343     // change pinned link into a history link
344     let changed = this._makeHistoryLink(aLink);
345     this.links[aIndex] = aLink;
346     this.save();
347     return changed;
348   },
350   /**
351    * Unpins a given link.
352    * @param aLink The link to unpin.
353    */
354   unpin: function PinnedLinks_unpin(aLink) {
355     let index = this._indexOfLink(aLink);
356     if (index == -1) {
357       return;
358     }
359     let links = this.links;
360     links[index] = null;
361     // trim trailing nulls
362     let i = links.length - 1;
363     while (i >= 0 && links[i] == null) {
364       i--;
365     }
366     links.splice(i + 1);
367     this.save();
368   },
370   /**
371    * Saves the current list of pinned links.
372    */
373   save: function PinnedLinks_save() {
374     lazy.Storage.set("pinnedLinks", this.links);
375   },
377   /**
378    * Checks whether a given link is pinned.
379    * @params aLink The link to check.
380    * @return whether The link is pinned.
381    */
382   isPinned: function PinnedLinks_isPinned(aLink) {
383     return this._indexOfLink(aLink) != -1;
384   },
386   /**
387    * Resets the links cache.
388    */
389   resetCache: function PinnedLinks_resetCache() {
390     this._links = null;
391   },
393   /**
394    * Finds the index of a given link in the list of pinned links.
395    * @param aLink The link to find an index for.
396    * @return The link's index.
397    */
398   _indexOfLink: function PinnedLinks_indexOfLink(aLink) {
399     for (let i = 0; i < this.links.length; i++) {
400       let link = this.links[i];
401       if (link && link.url == aLink.url) {
402         return i;
403       }
404     }
406     // The given link is unpinned.
407     return -1;
408   },
410   /**
411    * Transforms link into a "history" link
412    * @param aLink The link to change
413    * @return true if link changes, false otherwise
414    */
415   _makeHistoryLink: function PinnedLinks_makeHistoryLink(aLink) {
416     if (!aLink.type || aLink.type == "history") {
417       return false;
418     }
419     aLink.type = "history";
420     return true;
421   },
423   /**
424    * Replaces existing link with another link.
425    * @param aUrl The url of existing link
426    * @param aLink The replacement link
427    */
428   replace: function PinnedLinks_replace(aUrl, aLink) {
429     let index = this._indexOfLink({ url: aUrl });
430     if (index == -1) {
431       return;
432     }
433     this.links[index] = aLink;
434     this.save();
435   },
439  * Singleton that keeps track of all blocked links in the grid.
440  */
441 var BlockedLinks = {
442   /**
443    * A list of objects that are observing blocked link changes.
444    */
445   _observers: [],
447   /**
448    * The cached list of blocked links.
449    */
450   _links: null,
452   /**
453    * Registers an object that will be notified when the blocked links change.
454    */
455   addObserver(aObserver) {
456     this._observers.push(aObserver);
457   },
459   /**
460    * Remove the observers.
461    */
462   removeObservers() {
463     this._observers = [];
464   },
466   /**
467    * The list of blocked links.
468    */
469   get links() {
470     if (!this._links) {
471       this._links = lazy.Storage.get("blockedLinks", {});
472     }
474     return this._links;
475   },
477   /**
478    * Blocks a given link. Adjusts siteMap accordingly, and notifies listeners.
479    * @param aLink The link to block.
480    */
481   block: function BlockedLinks_block(aLink) {
482     this._callObservers("onLinkBlocked", aLink);
483     this.links[toHash(aLink.url)] = 1;
484     this.save();
486     // Make sure we unpin blocked links.
487     PinnedLinks.unpin(aLink);
488   },
490   /**
491    * Unblocks a given link. Adjusts siteMap accordingly, and notifies listeners.
492    * @param aLink The link to unblock.
493    */
494   unblock: function BlockedLinks_unblock(aLink) {
495     if (this.isBlocked(aLink)) {
496       delete this.links[toHash(aLink.url)];
497       this.save();
498       this._callObservers("onLinkUnblocked", aLink);
499     }
500   },
502   /**
503    * Saves the current list of blocked links.
504    */
505   save: function BlockedLinks_save() {
506     lazy.Storage.set("blockedLinks", this.links);
507   },
509   /**
510    * Returns whether a given link is blocked.
511    * @param aLink The link to check.
512    */
513   isBlocked: function BlockedLinks_isBlocked(aLink) {
514     return toHash(aLink.url) in this.links;
515   },
517   /**
518    * Checks whether the list of blocked links is empty.
519    * @return Whether the list is empty.
520    */
521   isEmpty: function BlockedLinks_isEmpty() {
522     return !Object.keys(this.links).length;
523   },
525   /**
526    * Resets the links cache.
527    */
528   resetCache: function BlockedLinks_resetCache() {
529     this._links = null;
530   },
532   _callObservers(methodName, ...args) {
533     for (let obs of this._observers) {
534       if (typeof obs[methodName] == "function") {
535         try {
536           obs[methodName](...args);
537         } catch (err) {
538           console.error(err);
539         }
540       }
541     }
542   },
546  * Singleton that serves as the default link provider for the grid. It queries
547  * the history to retrieve the most frequently visited sites.
548  */
549 var PlacesProvider = {
550   /**
551    * Set this to change the maximum number of links the provider will provide.
552    */
553   maxNumLinks: HISTORY_RESULTS_LIMIT,
555   /**
556    * Must be called before the provider is used.
557    */
558   init: function PlacesProvider_init() {
559     this._placesObserver = new PlacesWeakCallbackWrapper(
560       this.handlePlacesEvents.bind(this)
561     );
562     PlacesObservers.addListener(
563       ["page-visited", "page-title-changed", "pages-rank-changed"],
564       this._placesObserver
565     );
566   },
568   /**
569    * Gets the current set of links delivered by this provider.
570    * @param aCallback The function that the array of links is passed to.
571    */
572   getLinks: function PlacesProvider_getLinks(aCallback) {
573     let options = lazy.PlacesUtils.history.getNewQueryOptions();
574     options.maxResults = this.maxNumLinks;
576     // Sort by frecency, descending.
577     options.sortingMode =
578       Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING;
580     let links = [];
582     let callback = {
583       handleResult(aResultSet) {
584         let row;
586         while ((row = aResultSet.getNextRow())) {
587           let url = row.getResultByIndex(1);
588           if (LinkChecker.checkLoadURI(url)) {
589             let title = row.getResultByIndex(2);
590             let frecency = row.getResultByIndex(12);
591             let lastVisitDate = row.getResultByIndex(5);
592             links.push({
593               url,
594               title,
595               frecency,
596               lastVisitDate,
597               type: "history",
598             });
599           }
600         }
601       },
603       handleError(aError) {
604         // Should we somehow handle this error?
605         aCallback([]);
606       },
608       handleCompletion(aReason) {
609         // The Places query breaks ties in frecency by place ID descending, but
610         // that's different from how Links.compareLinks breaks ties, because
611         // compareLinks doesn't have access to place IDs.  It's very important
612         // that the initial list of links is sorted in the same order imposed by
613         // compareLinks, because Links uses compareLinks to perform binary
614         // searches on the list.  So, ensure the list is so ordered.
615         let i = 1;
616         let outOfOrder = [];
617         while (i < links.length) {
618           if (Links.compareLinks(links[i - 1], links[i]) > 0) {
619             outOfOrder.push(links.splice(i, 1)[0]);
620           } else {
621             i++;
622           }
623         }
624         for (let link of outOfOrder) {
625           i = lazy.BinarySearch.insertionIndexOf(
626             Links.compareLinks,
627             links,
628             link
629           );
630           links.splice(i, 0, link);
631         }
633         aCallback(links);
634       },
635     };
637     // Execute the query.
638     let query = lazy.PlacesUtils.history.getNewQuery();
639     lazy.PlacesUtils.history.asyncExecuteLegacyQuery(query, options, callback);
640   },
642   /**
643    * Registers an object that will be notified when the provider's links change.
644    * @param aObserver An object with the following optional properties:
645    *        * onLinkChanged: A function that's called when a single link
646    *          changes.  It's passed the provider and the link object.  Only the
647    *          link's `url` property is guaranteed to be present.  If its `title`
648    *          property is present, then its title has changed, and the
649    *          property's value is the new title.  If any sort properties are
650    *          present, then its position within the provider's list of links may
651    *          have changed, and the properties' values are the new sort-related
652    *          values.  Note that this link may not necessarily have been present
653    *          in the lists returned from any previous calls to getLinks.
654    *        * onManyLinksChanged: A function that's called when many links
655    *          change at once.  It's passed the provider.  You should call
656    *          getLinks to get the provider's new list of links.
657    */
658   addObserver: function PlacesProvider_addObserver(aObserver) {
659     this._observers.push(aObserver);
660   },
662   _observers: [],
664   handlePlacesEvents(aEvents) {
665     for (let event of aEvents) {
666       switch (event.type) {
667         case "page-visited": {
668           if (event.visitCount == 1 && event.lastKnownTitle) {
669             this._callObservers("onLinkChanged", {
670               url: event.url,
671               title: event.lastKnownTitle,
672             });
673           }
674           break;
675         }
676         case "page-title-changed": {
677           this._callObservers("onLinkChanged", {
678             url: event.url,
679             title: event.title,
680           });
681           break;
682         }
683         case "pages-rank-changed": {
684           this._callObservers("onManyLinksChanged");
685           break;
686         }
687       }
688     }
689   },
691   _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
692     for (let obs of this._observers) {
693       if (obs[aMethodName]) {
694         try {
695           obs[aMethodName](this, aArg);
696         } catch (err) {
697           console.error(err);
698         }
699       }
700     }
701   },
705  * Queries history to retrieve the most frecent sites. Emits events when the
706  * history changes.
707  */
708 var ActivityStreamProvider = {
709   THUMB_FAVICON_SIZE: 96,
711   /**
712    * Shared adjustment for selecting potentially blocked links.
713    */
714   _adjustLimitForBlocked({ ignoreBlocked, numItems }) {
715     // Just use the usual number if blocked links won't be filtered out
716     if (ignoreBlocked) {
717       return numItems;
718     }
719     // Additionally select the number of blocked links in case they're removed
720     return Object.keys(BlockedLinks.links).length + numItems;
721   },
723   /**
724    * Shared sub-SELECT to get the guid of a bookmark of the current url while
725    * avoiding LEFT JOINs on moz_bookmarks. This avoids gettings tags. The guid
726    * could be one of multiple possible guids. Assumes `moz_places h` is in FROM.
727    */
728   _commonBookmarkGuidSelect: `(
729     SELECT guid
730     FROM moz_bookmarks b
731     WHERE fk = h.id
732       AND type = :bookmarkType
733       AND (
734         SELECT id
735         FROM moz_bookmarks p
736         WHERE p.id = b.parent
737           AND p.parent <> :tagsFolderId
738       ) NOTNULL
739     ) AS bookmarkGuid`,
741   /**
742    * Shared WHERE expression filtering out undesired pages, e.g., hidden,
743    * unvisited, and non-http/s urls. Assumes moz_places is in FROM / JOIN.
744    *
745    * NB: SUBSTR(url) is used even without an index instead of url_hash because
746    * most desired pages will match http/s, so it will only run on the ~10s of
747    * rows matched. If url_hash were to be used, it should probably *not* be used
748    * by the query optimizer as we primarily want it optimized for the other
749    * conditions, e.g., most frecent first.
750    */
751   _commonPlacesWhere: `
752     AND hidden = 0
753     AND last_visit_date > 0
754     AND (SUBSTR(url, 1, 6) == "https:"
755       OR SUBSTR(url, 1, 5) == "http:")
756   `,
758   /**
759    * Shared parameters for getting correct bookmarks and LIMITed queries.
760    */
761   _getCommonParams(aOptions, aParams = {}) {
762     return Object.assign(
763       {
764         bookmarkType: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK,
765         limit: this._adjustLimitForBlocked(aOptions),
766         tagsFolderId: lazy.PlacesUtils.tagsFolderId,
767       },
768       aParams
769     );
770   },
772   /**
773    * Shared columns for Highlights related queries.
774    */
775   _highlightsColumns: [
776     "bookmarkGuid",
777     "description",
778     "guid",
779     "preview_image_url",
780     "title",
781     "url",
782   ],
784   /**
785    * Shared post-processing of Highlights links.
786    */
787   _processHighlights(aLinks, aOptions, aType) {
788     // Filter out blocked if necessary
789     if (!aOptions.ignoreBlocked) {
790       aLinks = aLinks.filter(
791         link =>
792           !BlockedLinks.isBlocked(
793             link.pocket_id ? { url: link.open_url } : link
794           )
795       );
796     }
798     // Limit the results to the requested number and set a type corresponding to
799     // which query selected it
800     return aLinks.slice(0, aOptions.numItems).map(item =>
801       Object.assign(item, {
802         type: aType,
803       })
804     );
805   },
807   /**
808    * From an Array of links, if favicons are present, convert to data URIs
809    *
810    * @param {Array} aLinks
811    *          an array containing objects with favicon data and mimeTypes
812    *
813    * @returns {Array} an array of links with favicons as data uri
814    */
815   _faviconBytesToDataURI(aLinks) {
816     return aLinks.map(link => {
817       if (link.favicon) {
818         let encodedData = btoa(String.fromCharCode.apply(null, link.favicon));
819         link.favicon = `data:${link.mimeType};base64,${encodedData}`;
820         delete link.mimeType;
821       }
823       if (link.smallFavicon) {
824         let encodedData = btoa(
825           String.fromCharCode.apply(null, link.smallFavicon)
826         );
827         link.smallFavicon = `data:${link.smallFaviconMimeType};base64,${encodedData}`;
828         delete link.smallFaviconMimeType;
829       }
831       return link;
832     });
833   },
835   /**
836    * Get favicon data (and metadata) for a uri. Fetches both the largest favicon
837    * available, for Activity Stream; and a normal-sized favicon, for the Urlbar.
838    *
839    * @param {nsIURI} aUri Page to check for favicon data
840    * @param {number} preferredFaviconWidth
841    *   The preferred width of the of the normal-sized favicon in pixels.
842    * @returns A promise of an object (possibly empty) containing the data.
843    */
844   async _loadIcons(aUri, preferredFaviconWidth) {
845     let iconData = {};
846     // Fetch the largest icon available.
847     let faviconData;
848     try {
849       faviconData = await lazy.PlacesUtils.promiseFaviconData(
850         aUri,
851         this.THUMB_FAVICON_SIZE
852       );
853       Object.assign(iconData, {
854         favicon: faviconData.data,
855         faviconLength: faviconData.dataLen,
856         faviconRef: faviconData.uri.ref,
857         faviconSize: faviconData.size,
858         mimeType: faviconData.mimeType,
859       });
860     } catch (e) {
861       // Return early because fetching the largest favicon is the primary
862       // purpose of NewTabUtils.
863       return null;
864     }
866     // Also fetch a smaller icon.
867     try {
868       faviconData = await lazy.PlacesUtils.promiseFaviconData(
869         aUri,
870         preferredFaviconWidth
871       );
872       Object.assign(iconData, {
873         smallFavicon: faviconData.data,
874         smallFaviconLength: faviconData.dataLen,
875         smallFaviconRef: faviconData.uri.ref,
876         smallFaviconSize: faviconData.size,
877         smallFaviconMimeType: faviconData.mimeType,
878       });
879     } catch (e) {
880       // Do nothing with the error since we still have the large favicon fields.
881     }
883     return iconData;
884   },
886   /**
887    * Computes favicon data for each url in a set of links
888    *
889    * @param {Array} links
890    *          an array containing objects without favicon data or mimeTypes yet
891    *
892    * @returns {Promise} Returns a promise with the array of links with the largest
893    *                    favicon available (as a byte array), mimeType, byte array
894    *                    length, and favicon size (width)
895    */
896   _addFavicons(aLinks) {
897     let win;
898     if (BrowserWindowTracker) {
899       win = BrowserWindowTracker.getTopWindow();
900     }
901     // We fetch two copies of a page's favicon: the largest available, for
902     // Activity Stream; and a smaller size appropriate for the Urlbar.
903     const preferredFaviconWidth =
904       DEFAULT_SMALL_FAVICON_WIDTH * (win ? win.devicePixelRatio : 2);
905     // Each link in the array needs a favicon for it's page - so we fire off a
906     // promise for each link to compute the favicon data and attach it back to
907     // the original link object. We must wait until all favicons for the array
908     // of links are computed before returning
909     return Promise.all(
910       aLinks.map(
911         link =>
912           // eslint-disable-next-line no-async-promise-executor
913           new Promise(async resolve => {
914             // Never add favicon data for pocket items
915             if (link.type === "pocket") {
916               resolve(link);
917               return;
918             }
919             let iconData;
920             try {
921               let linkUri = Services.io.newURI(link.url);
922               iconData = await this._loadIcons(linkUri, preferredFaviconWidth);
924               // Switch the scheme to try again with the other
925               if (!iconData) {
926                 linkUri = linkUri
927                   .mutate()
928                   .setScheme(linkUri.scheme === "https" ? "http" : "https")
929                   .finalize();
930                 iconData = await this._loadIcons(
931                   linkUri,
932                   preferredFaviconWidth
933                 );
934               }
935             } catch (e) {
936               // We just won't put icon data on the link
937             }
939             // Add the icon data to the link if we have any
940             resolve(Object.assign(link, iconData));
941           })
942       )
943     );
944   },
946   /**
947    * Helper function which makes the call to the Pocket API to fetch the user's
948    * saved Pocket items.
949    */
950   fetchSavedPocketItems(requestData) {
951     const latestSince =
952       Services.prefs.getStringPref(PREF_POCKET_LATEST_SINCE, 0) * 1000;
954     // Do not fetch Pocket items for users that have been inactive for too long, or are not logged in
955     if (
956       !lazy.pktApi.isUserLoggedIn() ||
957       Date.now() - latestSince > POCKET_INACTIVE_TIME
958     ) {
959       return Promise.resolve(null);
960     }
962     return new Promise((resolve, reject) => {
963       lazy.pktApi.retrieve(requestData, {
964         success(data) {
965           resolve(data);
966         },
967         error(error) {
968           reject(error);
969         },
970       });
971     });
972   },
974   /**
975    * Get the most recently Pocket-ed items from a user's Pocket list. See:
976    * https://getpocket.com/developer/docs/v3/retrieve for details
977    *
978    * @param {Object} aOptions
979    *   {int} numItems: The max number of pocket items to fetch
980    */
981   async getRecentlyPocketed(aOptions) {
982     const pocketSecondsAgo =
983       Math.floor(Date.now() / 1000) - ACTIVITY_STREAM_DEFAULT_RECENT;
984     const requestData = {
985       detailType: "complete",
986       count: aOptions.numItems,
987       since: pocketSecondsAgo,
988     };
989     let data;
990     try {
991       data = await this.fetchSavedPocketItems(requestData);
992       if (!data) {
993         return [];
994       }
995     } catch (e) {
996       console.error(e);
997       return [];
998     }
999     /* Extract relevant parts needed to show this card as a highlight:
1000      * url, preview image, title, description, and the unique item_id
1001      * necessary for Pocket to identify the item
1002      */
1003     let items = Object.values(data.list)
1004       // status "0" means not archived or deleted
1005       .filter(item => item.status === "0")
1006       .map(item => ({
1007         date_added: item.time_added * 1000,
1008         description: item.excerpt,
1009         preview_image_url: item.image && item.image.src,
1010         title: item.resolved_title,
1011         url: item.resolved_url,
1012         pocket_id: item.item_id,
1013         open_url: item.open_url,
1014       }));
1016     // Append the query param to let Pocket know this item came from highlights
1017     for (let item of items) {
1018       let url = new URL(item.open_url);
1019       url.searchParams.append("src", "fx_new_tab");
1020       item.open_url = url.href;
1021     }
1023     return this._processHighlights(items, aOptions, "pocket");
1024   },
1026   /**
1027    * Get most-recently-created visited bookmarks for Activity Stream.
1028    *
1029    * @param {Object} aOptions
1030    *   {num}  bookmarkSecondsAgo: Maximum age of added bookmark.
1031    *   {bool} ignoreBlocked: Do not filter out blocked links.
1032    *   {int}  numItems: Maximum number of items to return.
1033    */
1034   async getRecentBookmarks(aOptions) {
1035     const options = Object.assign(
1036       {
1037         bookmarkSecondsAgo: ACTIVITY_STREAM_DEFAULT_RECENT,
1038         ignoreBlocked: false,
1039         numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
1040       },
1041       aOptions || {}
1042     );
1044     const sqlQuery = `
1045       SELECT
1046         b.guid AS bookmarkGuid,
1047         description,
1048         h.guid,
1049         preview_image_url,
1050         b.title,
1051         b.dateAdded / 1000 AS date_added,
1052         url
1053       FROM moz_bookmarks b
1054       JOIN moz_bookmarks p
1055         ON p.id = b.parent
1056       JOIN moz_places h
1057         ON h.id = b.fk
1058       WHERE b.dateAdded >= :dateAddedThreshold
1059         AND b.title NOTNULL
1060         AND b.type = :bookmarkType
1061         AND p.parent <> :tagsFolderId
1062         ${this._commonPlacesWhere}
1063       ORDER BY b.dateAdded DESC
1064       LIMIT :limit
1065     `;
1067     return this._processHighlights(
1068       await this.executePlacesQuery(sqlQuery, {
1069         columns: [...this._highlightsColumns, "date_added"],
1070         params: this._getCommonParams(options, {
1071           dateAddedThreshold:
1072             (Date.now() - options.bookmarkSecondsAgo * 1000) * 1000,
1073         }),
1074       }),
1075       options,
1076       "bookmark"
1077     );
1078   },
1080   /**
1081    * Get total count of all bookmarks.
1082    * Note: this includes default bookmarks
1083    *
1084    * @return {int} The number bookmarks in the places DB.
1085    */
1086   async getTotalBookmarksCount() {
1087     let sqlQuery = `
1088       SELECT count(*) FROM moz_bookmarks b
1089       JOIN moz_bookmarks t ON t.id = b.parent
1090       AND t.parent <> :tags_folder
1091      WHERE b.type = :type_bookmark
1092     `;
1094     const result = await this.executePlacesQuery(sqlQuery, {
1095       params: {
1096         tags_folder: lazy.PlacesUtils.tagsFolderId,
1097         type_bookmark: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK,
1098       },
1099     });
1101     return result[0][0];
1102   },
1104   /**
1105    * Get most-recently-visited history with metadata for Activity Stream.
1106    *
1107    * @param {Object} aOptions
1108    *   {bool} ignoreBlocked: Do not filter out blocked links.
1109    *   {int}  numItems: Maximum number of items to return.
1110    */
1111   async getRecentHistory(aOptions) {
1112     const options = Object.assign(
1113       {
1114         ignoreBlocked: false,
1115         numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
1116       },
1117       aOptions || {}
1118     );
1120     const sqlQuery = `
1121       SELECT
1122         ${this._commonBookmarkGuidSelect},
1123         description,
1124         guid,
1125         preview_image_url,
1126         title,
1127         url
1128       FROM moz_places h
1129       WHERE description NOTNULL
1130         AND preview_image_url NOTNULL
1131         ${this._commonPlacesWhere}
1132       ORDER BY last_visit_date DESC
1133       LIMIT :limit
1134     `;
1136     return this._processHighlights(
1137       await this.executePlacesQuery(sqlQuery, {
1138         columns: this._highlightsColumns,
1139         params: this._getCommonParams(options),
1140       }),
1141       options,
1142       "history"
1143     );
1144   },
1146   /*
1147    * Gets the top frecent sites for Activity Stream.
1148    *
1149    * @param {Object} aOptions
1150    *   {bool} ignoreBlocked: Do not filter out blocked links.
1151    *   {int}  numItems: Maximum number of items to return.
1152    *   {int}  topsiteFrecency: Minimum amount of frecency for a site.
1153    *   {bool} onePerDomain: Dedupe the resulting list.
1154    *   {bool} includeFavicon: Include favicons if available.
1155    *   {string} hideWithSearchParam: URLs that contain this search param will be
1156    *     excluded from the returned links. This value should be either undefined
1157    *     or a string with one of the following forms:
1158    *     - undefined: Fall back to the value of pref
1159    *       `browser.newtabpage.activity-stream.hideTopSitesWithSearchParam`
1160    *     - "" (empty) - Disable this feature
1161    *     - "key" - Search param named "key" with any or no value
1162    *     - "key=" - Search param named "key" with no value
1163    *     - "key=value" - Search param named "key" with value "value"
1164    *
1165    * @returns {Promise} Returns a promise with the array of links as payload.
1166    */
1167   async getTopFrecentSites(aOptions) {
1168     const options = Object.assign(
1169       {
1170         ignoreBlocked: false,
1171         numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
1172         topsiteFrecency: ACTIVITY_STREAM_DEFAULT_FRECENCY,
1173         onePerDomain: true,
1174         includeFavicon: true,
1175         hideWithSearchParam: Services.prefs.getCharPref(
1176           "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam",
1177           ""
1178         ),
1179       },
1180       aOptions || {}
1181     );
1183     // Double the item count in case the host is deduped between with www or
1184     // not-www (i.e., 2 hosts) and an extra buffer for multiple pages per host.
1185     const origNumItems = options.numItems;
1186     if (options.onePerDomain) {
1187       options.numItems *= 2 * 10;
1188     }
1190     // Keep this query fast with frecency-indexed lookups (even with excess
1191     // rows) and shift the more complex logic to post-processing afterwards
1192     const sqlQuery = `
1193       SELECT
1194         ${this._commonBookmarkGuidSelect},
1195         frecency,
1196         guid,
1197         last_visit_date / 1000 AS lastVisitDate,
1198         rev_host,
1199         title,
1200         url,
1201         "history" as type
1202       FROM moz_places h
1203       WHERE frecency >= :frecencyThreshold
1204         ${this._commonPlacesWhere}
1205       ORDER BY frecency DESC
1206       LIMIT :limit
1207     `;
1209     let links = await this.executePlacesQuery(sqlQuery, {
1210       columns: [
1211         "bookmarkGuid",
1212         "frecency",
1213         "guid",
1214         "lastVisitDate",
1215         "title",
1216         "url",
1217         "type",
1218       ],
1219       params: this._getCommonParams(options, {
1220         frecencyThreshold: options.topsiteFrecency,
1221       }),
1222     });
1224     // Determine if the other link is "better" (larger frecency, more recent,
1225     // lexicographically earlier url)
1226     function isOtherBetter(link, other) {
1227       if (other.frecency === link.frecency) {
1228         if (other.lastVisitDate === link.lastVisitDate) {
1229           return other.url < link.url;
1230         }
1231         return other.lastVisitDate > link.lastVisitDate;
1232       }
1233       return other.frecency > link.frecency;
1234     }
1236     // Update a host Map with the better link
1237     function setBetterLink(map, link, hostMatcher, combiner = () => {}) {
1238       const host = hostMatcher(link.url)[1];
1239       if (map.has(host)) {
1240         const other = map.get(host);
1241         if (isOtherBetter(link, other)) {
1242           link = other;
1243         }
1244         combiner(link, other);
1245       }
1246       map.set(host, link);
1247     }
1249     // Convert all links that are supposed to be a seach shortcut to its canonical URL
1250     if (
1251       didSuccessfulImport &&
1252       Services.prefs.getBoolPref(
1253         `browser.newtabpage.activity-stream.${searchShortcuts.SEARCH_SHORTCUTS_EXPERIMENT}`
1254       )
1255     ) {
1256       links.forEach(link => {
1257         let searchProvider = searchShortcuts.getSearchProvider(
1258           shortURL.shortURL(link)
1259         );
1260         if (searchProvider) {
1261           link.url = searchProvider.url;
1262         }
1263       });
1264     }
1266     // Remove links that contain the hide-with search param.
1267     if (options.hideWithSearchParam) {
1268       let [key, value] = options.hideWithSearchParam.split("=");
1269       links = links.filter(link => {
1270         try {
1271           let { searchParams } = new URL(link.url);
1272           return value === undefined
1273             ? !searchParams.has(key)
1274             : !searchParams.getAll(key).includes(value);
1275         } catch (error) {}
1276         return true;
1277       });
1278     }
1280     // Remove any blocked links.
1281     if (!options.ignoreBlocked) {
1282       links = links.filter(link => !BlockedLinks.isBlocked(link));
1283     }
1285     if (options.onePerDomain) {
1286       // De-dup the links.
1287       const exactHosts = new Map();
1288       for (const link of links) {
1289         // First we want to find the best link for an exact host
1290         setBetterLink(exactHosts, link, url => url.match(/:\/\/([^\/]+)/));
1291       }
1293       // Clean up exact hosts to dedupe as non-www hosts
1294       const hosts = new Map();
1295       for (const link of exactHosts.values()) {
1296         setBetterLink(
1297           hosts,
1298           link,
1299           url => url.match(/:\/\/(?:www\.)?([^\/]+)/),
1300           // Combine frecencies when deduping these links
1301           (targetLink, otherLink) => {
1302             targetLink.frecency = link.frecency + otherLink.frecency;
1303           }
1304         );
1305       }
1307       links = [...hosts.values()];
1308     }
1309     // Pick out the top links using the same comparer as before
1310     links = links.sort(isOtherBetter).slice(0, origNumItems);
1312     if (!options.includeFavicon) {
1313       return links;
1314     }
1315     // Get the favicons as data URI for now (until we use the favicon protocol)
1316     return this._faviconBytesToDataURI(await this._addFavicons(links));
1317   },
1319   /**
1320    * Gets a specific bookmark given some info about it
1321    *
1322    * @param {Obj} aInfo
1323    *          An object with one and only one of the following properties:
1324    *            - url
1325    *            - guid
1326    *            - parentGuid and index
1327    */
1328   async getBookmark(aInfo) {
1329     let bookmark = await lazy.PlacesUtils.bookmarks.fetch(aInfo);
1330     if (!bookmark) {
1331       return null;
1332     }
1333     let result = {};
1334     result.bookmarkGuid = bookmark.guid;
1335     result.bookmarkTitle = bookmark.title;
1336     result.lastModified = bookmark.lastModified.getTime();
1337     result.url = bookmark.url.href;
1338     return result;
1339   },
1341   /**
1342    * Count the number of visited urls grouped by day
1343    */
1344   getUserMonthlyActivity() {
1345     let sqlQuery = `
1346       SELECT count(*),
1347         strftime('%Y-%m-%d', visit_date/1000000.0, 'unixepoch', 'localtime') as date_format
1348       FROM moz_historyvisits
1349       WHERE visit_date > 0
1350       AND visit_date > strftime('%s','now','localtime','start of day','-27 days','utc') * 1000000
1351       GROUP BY date_format
1352       ORDER BY date_format ASC
1353     `;
1355     return this.executePlacesQuery(sqlQuery);
1356   },
1358   /**
1359    * Executes arbitrary query against places database
1360    *
1361    * @param {String} aQuery
1362    *        SQL query to execute
1363    * @param {Object} [optional] aOptions
1364    *          aOptions.columns - an array of column names. if supplied the return
1365    *          items will consists of objects keyed on column names. Otherwise
1366    *          array of raw values is returned in the select order
1367    *          aOptions.param - an object of SQL binding parameters
1368    *
1369    * @returns {Promise} Returns a promise with the array of retrieved items
1370    */
1371   async executePlacesQuery(aQuery, aOptions = {}) {
1372     let { columns, params } = aOptions;
1373     let items = [];
1374     let queryError = null;
1375     let conn = await lazy.PlacesUtils.promiseDBConnection();
1376     await conn.executeCached(aQuery, params, (aRow, aCancel) => {
1377       try {
1378         let item = null;
1379         // if columns array is given construct an object
1380         if (columns && Array.isArray(columns)) {
1381           item = {};
1382           columns.forEach(column => {
1383             item[column] = aRow.getResultByName(column);
1384           });
1385         } else {
1386           // if no columns - make an array of raw values
1387           item = [];
1388           for (let i = 0; i < aRow.numEntries; i++) {
1389             item.push(aRow.getResultByIndex(i));
1390           }
1391         }
1392         items.push(item);
1393       } catch (e) {
1394         queryError = e;
1395         aCancel();
1396       }
1397     });
1398     if (queryError) {
1399       throw new Error(queryError);
1400     }
1401     return items;
1402   },
1406  * A set of actions which influence what sites shown on the Activity Stream page
1407  */
1408 var ActivityStreamLinks = {
1409   _savedPocketStories: null,
1410   _pocketLastUpdated: 0,
1411   _pocketLastLatest: 0,
1413   /**
1414    * Block a url
1415    *
1416    * @param {Object} aLink
1417    *          The link which contains a URL to add to the block list
1418    */
1419   blockURL(aLink) {
1420     BlockedLinks.block(aLink);
1421     // If we're blocking a pocket item, invalidate the cache too
1422     if (aLink.pocket_id) {
1423       this._savedPocketStories = null;
1424     }
1425   },
1427   onLinkBlocked(aLink) {
1428     Services.obs.notifyObservers(null, "newtab-linkBlocked", aLink.url);
1429   },
1431   /**
1432    * Adds a bookmark and opens up the Bookmark Dialog to show feedback that
1433    * the bookmarking action has been successful
1434    *
1435    * @param {Object} aData
1436    *          aData.url The url to bookmark
1437    *          aData.title The title of the page to bookmark
1438    * @param {Window} aBrowserWindow
1439    *          The current browser chrome window
1440    *
1441    * @returns {Promise} Returns a promise set to an object representing the bookmark
1442    */
1443   addBookmark(aData, aBrowserWindow) {
1444     const { url, title } = aData;
1445     return aBrowserWindow.PlacesCommandHook.bookmarkLink(url, title);
1446   },
1448   /**
1449    * Removes a bookmark
1450    *
1451    * @param {String} aBookmarkGuid
1452    *          The bookmark guid associated with the bookmark to remove
1453    *
1454    * @returns {Promise} Returns a promise at completion.
1455    */
1456   deleteBookmark(aBookmarkGuid) {
1457     return lazy.PlacesUtils.bookmarks.remove(aBookmarkGuid);
1458   },
1460   /**
1461    * Removes a history link and unpins the URL if previously pinned
1462    *
1463    * @param {String} aUrl
1464    *           The url to be removed from history
1465    *
1466    * @returns {Promise} Returns a promise set to true if link was removed
1467    */
1468   deleteHistoryEntry(aUrl) {
1469     const url = aUrl;
1470     PinnedLinks.unpin({ url });
1471     return lazy.PlacesUtils.history.remove(url);
1472   },
1474   /**
1475    * Helper function which makes the call to the Pocket API to delete an item from
1476    * a user's saved to Pocket feed. Also, invalidate the Pocket stories cache
1477    *
1478    * @param {Integer} aItemID
1479    *           The unique pocket ID used to find the item to be deleted
1480    *
1481    *@returns {Promise} Returns a promise at completion
1482    */
1483   deletePocketEntry(aItemID) {
1484     this._savedPocketStories = null;
1485     return new Promise((success, error) =>
1486       lazy.pktApi.deleteItem(aItemID, { success, error })
1487     );
1488   },
1490   /**
1491    * Helper function which makes the call to the Pocket API to archive an item from
1492    * a user's saved to Pocket feed. Also, invalidate the Pocket stories cache
1493    *
1494    * @param {Integer} aItemID
1495    *           The unique pocket ID used to find the item to be archived
1496    *
1497    *@returns {Promise} Returns a promise at completion
1498    */
1499   archivePocketEntry(aItemID) {
1500     this._savedPocketStories = null;
1501     return new Promise((success, error) =>
1502       lazy.pktApi.archiveItem(aItemID, { success, error })
1503     );
1504   },
1506   /**
1507    * Helper function which makes the call to the Pocket API to save an item to
1508    * a user's saved to Pocket feed if they are logged in. Also, invalidate the
1509    * Pocket stories cache
1510    *
1511    * @param {String} aUrl
1512    *           The URL belonging to the story being saved
1513    * @param {String} aTitle
1514    *           The title belonging to the story being saved
1515    * @param {Browser} aBrowser
1516    *           The target browser to show the doorhanger in
1517    *
1518    *@returns {Promise} Returns a promise at completion
1519    */
1520   addPocketEntry(aUrl, aTitle, aBrowser) {
1521     // If the user is not logged in, show the panel to prompt them to log in
1522     if (!lazy.pktApi.isUserLoggedIn()) {
1523       lazy.Pocket.savePage(aBrowser, aUrl, aTitle);
1524       return Promise.resolve(null);
1525     }
1527     // If the user is logged in, just save the link to Pocket and Activity Stream
1528     // will update the page
1529     this._savedPocketStories = null;
1530     return new Promise((success, error) => {
1531       lazy.pktApi.addLink(aUrl, {
1532         title: aTitle,
1533         success,
1534         error,
1535       });
1536     });
1537   },
1539   /**
1540    * Get the Highlights links to show on Activity Stream
1541    *
1542    * @param {Object} aOptions
1543    *   {bool} excludeBookmarks: Don't add bookmark items.
1544    *   {bool} excludeHistory: Don't add history items.
1545    *   {bool} excludePocket: Don't add Pocket items.
1546    *   {bool} withFavicons: Add favicon data: URIs, when possible.
1547    *   {int}  numItems: Maximum number of (bookmark or history) items to return.
1548    *
1549    * @return {Promise} Returns a promise with the array of links as the payload
1550    */
1551   async getHighlights(aOptions = {}) {
1552     aOptions.numItems = aOptions.numItems || ACTIVITY_STREAM_DEFAULT_LIMIT;
1553     const results = [];
1555     // First get bookmarks if we want them
1556     if (!aOptions.excludeBookmarks) {
1557       results.push(
1558         ...(await ActivityStreamProvider.getRecentBookmarks(aOptions))
1559       );
1560     }
1562     // Add the Pocket items if we need more and want them
1563     if (aOptions.numItems - results.length > 0 && !aOptions.excludePocket) {
1564       const latestSince = ~~Services.prefs.getStringPref(
1565         PREF_POCKET_LATEST_SINCE,
1566         0
1567       );
1568       // Invalidate the cache, get new stories, and update timestamps if:
1569       //  1. we do not have saved to Pocket stories already cached OR
1570       //  2. it has been too long since we last got Pocket stories OR
1571       //  3. there has been a paged saved to pocket since we last got new stories
1572       if (
1573         !this._savedPocketStories ||
1574         Date.now() - this._pocketLastUpdated > POCKET_UPDATE_TIME ||
1575         this._pocketLastLatest < latestSince
1576       ) {
1577         this._savedPocketStories =
1578           await ActivityStreamProvider.getRecentlyPocketed(aOptions);
1579         this._pocketLastUpdated = Date.now();
1580         this._pocketLastLatest = latestSince;
1581       }
1582       results.push(...this._savedPocketStories);
1583     }
1585     // Add in history if we need more and want them
1586     if (aOptions.numItems - results.length > 0 && !aOptions.excludeHistory) {
1587       // Use the same numItems as bookmarks above in case we remove duplicates
1588       const history = await ActivityStreamProvider.getRecentHistory(aOptions);
1590       // Only include a url once in the result preferring the bookmark
1591       const bookmarkUrls = new Set(results.map(({ url }) => url));
1592       for (const page of history) {
1593         if (!bookmarkUrls.has(page.url)) {
1594           results.push(page);
1596           // Stop adding pages once we reach the desired maximum
1597           if (results.length === aOptions.numItems) {
1598             break;
1599           }
1600         }
1601       }
1602     }
1604     if (aOptions.withFavicons) {
1605       return ActivityStreamProvider._faviconBytesToDataURI(
1606         await ActivityStreamProvider._addFavicons(results)
1607       );
1608     }
1610     return results;
1611   },
1613   /**
1614    * Get the top sites to show on Activity Stream
1615    *
1616    * @return {Promise} Returns a promise with the array of links as the payload
1617    */
1618   async getTopSites(aOptions = {}) {
1619     return ActivityStreamProvider.getTopFrecentSites(aOptions);
1620   },
1624  * Singleton that provides access to all links contained in the grid (including
1625  * the ones that don't fit on the grid). A link is a plain object that looks
1626  * like this:
1628  * {
1629  *   url: "http://www.mozilla.org/",
1630  *   title: "Mozilla",
1631  *   frecency: 1337,
1632  *   lastVisitDate: 1394678824766431,
1633  * }
1634  */
1635 var Links = {
1636   /**
1637    * The maximum number of links returned by getLinks.
1638    */
1639   maxNumLinks: LINKS_GET_LINKS_LIMIT,
1641   /**
1642    * A mapping from each provider to an object { sortedLinks, siteMap, linkMap }.
1643    * sortedLinks is the cached, sorted array of links for the provider.
1644    * siteMap is a mapping from base domains to URL count associated with the domain.
1645    *         The count does not include blocked URLs. siteMap is used to look up a
1646    *         user's top sites that can be targeted with a suggested tile.
1647    * linkMap is a Map from link URLs to link objects.
1648    */
1649   _providers: new Map(),
1651   /**
1652    * The properties of link objects used to sort them.
1653    */
1654   _sortProperties: ["frecency", "lastVisitDate", "url"],
1656   /**
1657    * List of callbacks waiting for the cache to be populated.
1658    */
1659   _populateCallbacks: [],
1661   /**
1662    * A list of objects that are observing links updates.
1663    */
1664   _observers: [],
1666   /**
1667    * Registers an object that will be notified when links updates.
1668    */
1669   addObserver(aObserver) {
1670     this._observers.push(aObserver);
1671   },
1673   /**
1674    * Adds a link provider.
1675    * @param aProvider The link provider.
1676    */
1677   addProvider: function Links_addProvider(aProvider) {
1678     this._providers.set(aProvider, null);
1679     aProvider.addObserver(this);
1680   },
1682   /**
1683    * Removes a link provider.
1684    * @param aProvider The link provider.
1685    */
1686   removeProvider: function Links_removeProvider(aProvider) {
1687     if (!this._providers.delete(aProvider)) {
1688       throw new Error("Unknown provider");
1689     }
1690   },
1692   /**
1693    * Populates the cache with fresh links from the providers.
1694    * @param aCallback The callback to call when finished (optional).
1695    * @param aForce When true, populates the cache even when it's already filled.
1696    */
1697   populateCache: function Links_populateCache(aCallback, aForce) {
1698     let callbacks = this._populateCallbacks;
1700     // Enqueue the current callback.
1701     callbacks.push(aCallback);
1703     // There was a callback waiting already, thus the cache has not yet been
1704     // populated.
1705     if (callbacks.length > 1) {
1706       return;
1707     }
1709     function executeCallbacks() {
1710       while (callbacks.length) {
1711         let callback = callbacks.shift();
1712         if (callback) {
1713           try {
1714             callback();
1715           } catch (e) {
1716             // We want to proceed even if a callback fails.
1717           }
1718         }
1719       }
1720     }
1722     let numProvidersRemaining = this._providers.size;
1723     for (let [provider /* , links */] of this._providers) {
1724       this._populateProviderCache(
1725         provider,
1726         () => {
1727           if (--numProvidersRemaining == 0) {
1728             executeCallbacks();
1729           }
1730         },
1731         aForce
1732       );
1733     }
1735     this._addObserver();
1736   },
1738   /**
1739    * Gets the current set of links contained in the grid.
1740    * @return The links in the grid.
1741    */
1742   getLinks: function Links_getLinks() {
1743     let pinnedLinks = Array.from(PinnedLinks.links);
1744     let links = this._getMergedProviderLinks();
1746     let sites = new Set();
1747     for (let link of pinnedLinks) {
1748       if (link) {
1749         sites.add(NewTabUtils.extractSite(link.url));
1750       }
1751     }
1753     // Filter blocked and pinned links and duplicate base domains.
1754     links = links.filter(function (link) {
1755       let site = NewTabUtils.extractSite(link.url);
1756       if (site == null || sites.has(site)) {
1757         return false;
1758       }
1759       sites.add(site);
1761       return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
1762     });
1764     // Try to fill the gaps between pinned links.
1765     for (let i = 0; i < pinnedLinks.length && links.length; i++) {
1766       if (!pinnedLinks[i]) {
1767         pinnedLinks[i] = links.shift();
1768       }
1769     }
1771     // Append the remaining links if any.
1772     if (links.length) {
1773       pinnedLinks = pinnedLinks.concat(links);
1774     }
1776     for (let link of pinnedLinks) {
1777       if (link) {
1778         link.baseDomain = NewTabUtils.extractSite(link.url);
1779       }
1780     }
1781     return pinnedLinks;
1782   },
1784   /**
1785    * Resets the links cache.
1786    */
1787   resetCache: function Links_resetCache() {
1788     for (let provider of this._providers.keys()) {
1789       this._providers.set(provider, null);
1790     }
1791   },
1793   /**
1794    * Compares two links.
1795    * @param aLink1 The first link.
1796    * @param aLink2 The second link.
1797    * @return A negative number if aLink1 is ordered before aLink2, zero if
1798    *         aLink1 and aLink2 have the same ordering, or a positive number if
1799    *         aLink1 is ordered after aLink2.
1800    *
1801    * @note compareLinks's this object is bound to Links below.
1802    */
1803   compareLinks: function Links_compareLinks(aLink1, aLink2) {
1804     for (let prop of this._sortProperties) {
1805       if (!(prop in aLink1) || !(prop in aLink2)) {
1806         throw new Error("Comparable link missing required property: " + prop);
1807       }
1808     }
1809     return (
1810       aLink2.frecency - aLink1.frecency ||
1811       aLink2.lastVisitDate - aLink1.lastVisitDate ||
1812       aLink1.url.localeCompare(aLink2.url)
1813     );
1814   },
1816   _incrementSiteMap(map, link) {
1817     if (NewTabUtils.blockedLinks.isBlocked(link)) {
1818       // Don't count blocked URLs.
1819       return;
1820     }
1821     let site = NewTabUtils.extractSite(link.url);
1822     map.set(site, (map.get(site) || 0) + 1);
1823   },
1825   _decrementSiteMap(map, link) {
1826     if (NewTabUtils.blockedLinks.isBlocked(link)) {
1827       // Blocked URLs are not included in map.
1828       return;
1829     }
1830     let site = NewTabUtils.extractSite(link.url);
1831     let previousURLCount = map.get(site);
1832     if (previousURLCount === 1) {
1833       map.delete(site);
1834     } else {
1835       map.set(site, previousURLCount - 1);
1836     }
1837   },
1839   /**
1840    * Update the siteMap cache based on the link given and whether we need
1841    * to increment or decrement it. We do this by iterating over all stored providers
1842    * to find which provider this link already exists in. For providers that
1843    * have this link, we will adjust siteMap for them accordingly.
1844    *
1845    * @param aLink The link that will affect siteMap
1846    * @param increment A boolean for whether to increment or decrement siteMap
1847    */
1848   _adjustSiteMapAndNotify(aLink, increment = true) {
1849     for (let [, /* provider */ cache] of this._providers) {
1850       // We only update siteMap if aLink is already stored in linkMap.
1851       if (cache.linkMap.get(aLink.url)) {
1852         if (increment) {
1853           this._incrementSiteMap(cache.siteMap, aLink);
1854           continue;
1855         }
1856         this._decrementSiteMap(cache.siteMap, aLink);
1857       }
1858     }
1859     this._callObservers("onLinkChanged", aLink);
1860   },
1862   onLinkBlocked(aLink) {
1863     this._adjustSiteMapAndNotify(aLink, false);
1864   },
1866   onLinkUnblocked(aLink) {
1867     this._adjustSiteMapAndNotify(aLink);
1868   },
1870   populateProviderCache(provider, callback) {
1871     if (!this._providers.has(provider)) {
1872       throw new Error(
1873         "Can only populate provider cache for existing provider."
1874       );
1875     }
1877     return this._populateProviderCache(provider, callback, false);
1878   },
1880   /**
1881    * Calls getLinks on the given provider and populates our cache for it.
1882    * @param aProvider The provider whose cache will be populated.
1883    * @param aCallback The callback to call when finished.
1884    * @param aForce When true, populates the provider's cache even when it's
1885    *               already filled.
1886    */
1887   _populateProviderCache(aProvider, aCallback, aForce) {
1888     let cache = this._providers.get(aProvider);
1889     let createCache = !cache;
1890     if (createCache) {
1891       cache = {
1892         // Start with a resolved promise.
1893         populatePromise: new Promise(resolve => resolve()),
1894       };
1895       this._providers.set(aProvider, cache);
1896     }
1897     // Chain the populatePromise so that calls are effectively queued.
1898     cache.populatePromise = cache.populatePromise.then(() => {
1899       return new Promise(resolve => {
1900         if (!createCache && !aForce) {
1901           aCallback();
1902           resolve();
1903           return;
1904         }
1905         aProvider.getLinks(links => {
1906           // Filter out null and undefined links so we don't have to deal with
1907           // them in getLinks when merging links from providers.
1908           links = links.filter(link => !!link);
1909           cache.sortedLinks = links;
1910           cache.siteMap = links.reduce((map, link) => {
1911             this._incrementSiteMap(map, link);
1912             return map;
1913           }, new Map());
1914           cache.linkMap = links.reduce((map, link) => {
1915             map.set(link.url, link);
1916             return map;
1917           }, new Map());
1918           aCallback();
1919           resolve();
1920         });
1921       });
1922     });
1923   },
1925   /**
1926    * Merges the cached lists of links from all providers whose lists are cached.
1927    * @return The merged list.
1928    */
1929   _getMergedProviderLinks: function Links__getMergedProviderLinks() {
1930     // Build a list containing a copy of each provider's sortedLinks list.
1931     let linkLists = [];
1932     for (let provider of this._providers.keys()) {
1933       let links = this._providers.get(provider);
1934       if (links && links.sortedLinks) {
1935         linkLists.push(links.sortedLinks.slice());
1936       }
1937     }
1939     return this.mergeLinkLists(linkLists);
1940   },
1942   mergeLinkLists: function Links_mergeLinkLists(linkLists) {
1943     if (linkLists.length == 1) {
1944       return linkLists[0];
1945     }
1947     function getNextLink() {
1948       let minLinks = null;
1949       for (let links of linkLists) {
1950         if (
1951           links.length &&
1952           (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0)
1953         ) {
1954           minLinks = links;
1955         }
1956       }
1957       return minLinks ? minLinks.shift() : null;
1958     }
1960     let finalLinks = [];
1961     for (
1962       let nextLink = getNextLink();
1963       nextLink && finalLinks.length < this.maxNumLinks;
1964       nextLink = getNextLink()
1965     ) {
1966       finalLinks.push(nextLink);
1967     }
1969     return finalLinks;
1970   },
1972   /**
1973    * Called by a provider to notify us when a single link changes.
1974    * @param aProvider The provider whose link changed.
1975    * @param aLink The link that changed.  If the link is new, it must have all
1976    *              of the _sortProperties.  Otherwise, it may have as few or as
1977    *              many as is convenient.
1978    * @param aIndex The current index of the changed link in the sortedLinks
1979                    cache in _providers. Defaults to -1 if the provider doesn't know the index
1980    * @param aDeleted Boolean indicating if the provider has deleted the link.
1981    */
1982   onLinkChanged: function Links_onLinkChanged(
1983     aProvider,
1984     aLink,
1985     aIndex = -1,
1986     aDeleted = false
1987   ) {
1988     if (!("url" in aLink)) {
1989       throw new Error("Changed links must have a url property");
1990     }
1992     let links = this._providers.get(aProvider);
1993     if (!links) {
1994       // This is not an error, it just means that between the time the provider
1995       // was added and the future time we call getLinks on it, it notified us of
1996       // a change.
1997       return;
1998     }
2000     let { sortedLinks, siteMap, linkMap } = links;
2001     let existingLink = linkMap.get(aLink.url);
2002     let insertionLink = null;
2003     let updatePages = false;
2005     if (existingLink) {
2006       // Update our copy's position in O(lg n) by first removing it from its
2007       // list.  It's important to do this before modifying its properties.
2008       if (this._sortProperties.some(prop => prop in aLink)) {
2009         let idx = aIndex;
2010         if (idx < 0) {
2011           idx = this._indexOf(sortedLinks, existingLink);
2012         } else if (this.compareLinks(aLink, sortedLinks[idx]) != 0) {
2013           throw new Error("aLink should be the same as sortedLinks[idx]");
2014         }
2016         if (idx < 0) {
2017           throw new Error("Link should be in _sortedLinks if in _linkMap");
2018         }
2019         sortedLinks.splice(idx, 1);
2021         if (aDeleted) {
2022           updatePages = true;
2023           linkMap.delete(existingLink.url);
2024           this._decrementSiteMap(siteMap, existingLink);
2025         } else {
2026           // Update our copy's properties.
2027           Object.assign(existingLink, aLink);
2029           // Finally, reinsert our copy below.
2030           insertionLink = existingLink;
2031         }
2032       }
2033       // Update our copy's title in O(1).
2034       if ("title" in aLink && aLink.title != existingLink.title) {
2035         existingLink.title = aLink.title;
2036         updatePages = true;
2037       }
2038     } else if (this._sortProperties.every(prop => prop in aLink)) {
2039       // Before doing the O(lg n) insertion below, do an O(1) check for the
2040       // common case where the new link is too low-ranked to be in the list.
2041       if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
2042         let lastLink = sortedLinks[sortedLinks.length - 1];
2043         if (this.compareLinks(lastLink, aLink) < 0) {
2044           return;
2045         }
2046       }
2047       // Copy the link object so that changes later made to it by the caller
2048       // don't affect our copy.
2049       insertionLink = {};
2050       for (let prop in aLink) {
2051         insertionLink[prop] = aLink[prop];
2052       }
2053       linkMap.set(aLink.url, insertionLink);
2054       this._incrementSiteMap(siteMap, aLink);
2055     }
2057     if (insertionLink) {
2058       let idx = this._insertionIndexOf(sortedLinks, insertionLink);
2059       sortedLinks.splice(idx, 0, insertionLink);
2060       if (sortedLinks.length > aProvider.maxNumLinks) {
2061         let lastLink = sortedLinks.pop();
2062         linkMap.delete(lastLink.url);
2063         this._decrementSiteMap(siteMap, lastLink);
2064       }
2065       updatePages = true;
2066     }
2068     if (updatePages) {
2069       AllPages.update(null, "links-changed");
2070     }
2071   },
2073   /**
2074    * Called by a provider to notify us when many links change.
2075    */
2076   onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
2077     this._populateProviderCache(
2078       aProvider,
2079       () => {
2080         AllPages.update(null, "links-changed");
2081       },
2082       true
2083     );
2084   },
2086   _indexOf: function Links__indexOf(aArray, aLink) {
2087     return this._binsearch(aArray, aLink, "indexOf");
2088   },
2090   _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
2091     return this._binsearch(aArray, aLink, "insertionIndexOf");
2092   },
2094   _binsearch: function Links__binsearch(aArray, aLink, aMethod) {
2095     return lazy.BinarySearch[aMethod](this.compareLinks, aArray, aLink);
2096   },
2098   /**
2099    * Implements the nsIObserver interface to get notified about browser history
2100    * sanitization.
2101    */
2102   observe: function Links_observe(aSubject, aTopic, aData) {
2103     // Make sure to update open about:newtab instances. If there are no opened
2104     // pages we can just wait for the next new tab to populate the cache again.
2105     if (AllPages.length && AllPages.enabled) {
2106       this.populateCache(function () {
2107         AllPages.update();
2108       }, true);
2109     } else {
2110       this.resetCache();
2111     }
2112   },
2114   _callObservers(methodName, ...args) {
2115     for (let obs of this._observers) {
2116       if (typeof obs[methodName] == "function") {
2117         try {
2118           obs[methodName](this, ...args);
2119         } catch (err) {
2120           console.error(err);
2121         }
2122       }
2123     }
2124   },
2126   /**
2127    * Adds a sanitization observer and turns itself into a no-op after the first
2128    * invokation.
2129    */
2130   _addObserver: function Links_addObserver() {
2131     Services.obs.addObserver(this, "browser:purge-session-history", true);
2132     this._addObserver = function () {};
2133   },
2135   QueryInterface: ChromeUtils.generateQI([
2136     "nsIObserver",
2137     "nsISupportsWeakReference",
2138   ]),
2141 Links.compareLinks = Links.compareLinks.bind(Links);
2144  * Singleton used to collect telemetry data.
2146  */
2147 var Telemetry = {
2148   /**
2149    * Initializes object.
2150    */
2151   init: function Telemetry_init() {
2152     Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY);
2153   },
2155   uninit: function Telemetry_uninit() {
2156     Services.obs.removeObserver(this, TOPIC_GATHER_TELEMETRY);
2157   },
2159   /**
2160    * Collects data.
2161    */
2162   _collect: function Telemetry_collect() {
2163     let probes = [
2164       { histogram: "NEWTAB_PAGE_ENABLED", value: AllPages.enabled },
2165       {
2166         histogram: "NEWTAB_PAGE_PINNED_SITES_COUNT",
2167         value: PinnedLinks.links.length,
2168       },
2169       {
2170         histogram: "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
2171         value: Object.keys(BlockedLinks.links).length,
2172       },
2173     ];
2175     probes.forEach(function Telemetry_collect_forEach(aProbe) {
2176       Services.telemetry.getHistogramById(aProbe.histogram).add(aProbe.value);
2177     });
2178   },
2180   /**
2181    * Listens for gather telemetry topic.
2182    */
2183   observe: function Telemetry_observe(aSubject, aTopic, aData) {
2184     this._collect();
2185   },
2189  * Singleton that checks if a given link should be displayed on about:newtab
2190  * or if we should rather not do it for security reasons. URIs that inherit
2191  * their caller's principal will be filtered.
2192  */
2193 var LinkChecker = {
2194   _cache: {},
2196   get flags() {
2197     return (
2198       Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
2199       Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS
2200     );
2201   },
2203   checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
2204     if (!(aURI in this._cache)) {
2205       this._cache[aURI] = this._doCheckLoadURI(aURI);
2206     }
2208     return this._cache[aURI];
2209   },
2211   _doCheckLoadURI: function Links_doCheckLoadURI(aURI) {
2212     try {
2213       // about:newtab is currently privileged. In any case, it should be
2214       // possible for tiles to point to pretty much everything - but not
2215       // to stuff that inherits the system principal, so we check:
2216       let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
2217       Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
2218         systemPrincipal,
2219         aURI,
2220         this.flags
2221       );
2222       return true;
2223     } catch (e) {
2224       // We got a weird URI or one that would inherit the caller's principal.
2225       return false;
2226     }
2227   },
2230 var ExpirationFilter = {
2231   init: function ExpirationFilter_init() {
2232     lazy.PageThumbs.addExpirationFilter(this);
2233   },
2235   filterForThumbnailExpiration:
2236     function ExpirationFilter_filterForThumbnailExpiration(aCallback) {
2237       if (!AllPages.enabled) {
2238         aCallback([]);
2239         return;
2240       }
2242       Links.populateCache(function () {
2243         let urls = [];
2245         // Add all URLs to the list that we want to keep thumbnails for.
2246         for (let link of Links.getLinks().slice(0, 25)) {
2247           if (link && link.url) {
2248             urls.push(link.url);
2249           }
2250         }
2252         aCallback(urls);
2253       });
2254     },
2258  * Singleton that provides the public API of this JSM.
2259  */
2260 export var NewTabUtils = {
2261   _initialized: false,
2263   /**
2264    * Extract a "site" from a url in a way that multiple urls of a "site" returns
2265    * the same "site."
2266    * @param aUrl Url spec string
2267    * @return The "site" string or null
2268    */
2269   extractSite: function Links_extractSite(url) {
2270     let host;
2271     try {
2272       // Note that nsIURI.asciiHost throws NS_ERROR_FAILURE for some types of
2273       // URIs, including jar and moz-icon URIs.
2274       host = Services.io.newURI(url).asciiHost;
2275     } catch (ex) {
2276       return null;
2277     }
2279     // Strip off common subdomains of the same site (e.g., www, load balancer)
2280     return host.replace(/^(m|mobile|www\d*)\./, "");
2281   },
2283   init: function NewTabUtils_init() {
2284     if (this.initWithoutProviders()) {
2285       PlacesProvider.init();
2286       Links.addProvider(PlacesProvider);
2287       BlockedLinks.addObserver(Links);
2288       BlockedLinks.addObserver(ActivityStreamLinks);
2289     }
2290   },
2292   initWithoutProviders: function NewTabUtils_initWithoutProviders() {
2293     if (!this._initialized) {
2294       this._initialized = true;
2295       ExpirationFilter.init();
2296       Telemetry.init();
2297       return true;
2298     }
2299     return false;
2300   },
2302   uninit: function NewTabUtils_uninit() {
2303     if (this.initialized) {
2304       Telemetry.uninit();
2305       BlockedLinks.removeObservers();
2306     }
2307   },
2309   getProviderLinks(aProvider) {
2310     let cache = Links._providers.get(aProvider);
2311     if (cache && cache.sortedLinks) {
2312       return cache.sortedLinks;
2313     }
2314     return [];
2315   },
2317   isTopSiteGivenProvider(aSite, aProvider) {
2318     let cache = Links._providers.get(aProvider);
2319     if (cache && cache.siteMap) {
2320       return cache.siteMap.has(aSite);
2321     }
2322     return false;
2323   },
2325   isTopPlacesSite(aSite) {
2326     return this.isTopSiteGivenProvider(aSite, PlacesProvider);
2327   },
2329   /**
2330    * Restores all sites that have been removed from the grid.
2331    */
2332   restore: function NewTabUtils_restore() {
2333     lazy.Storage.clear();
2334     Links.resetCache();
2335     PinnedLinks.resetCache();
2336     BlockedLinks.resetCache();
2338     Links.populateCache(function () {
2339       AllPages.update();
2340     }, true);
2341   },
2343   /**
2344    * Undoes all sites that have been removed from the grid and keep the pinned
2345    * tabs.
2346    * @param aCallback the callback method.
2347    */
2348   undoAll: function NewTabUtils_undoAll(aCallback) {
2349     lazy.Storage.remove("blockedLinks");
2350     Links.resetCache();
2351     BlockedLinks.resetCache();
2352     Links.populateCache(aCallback, true);
2353   },
2355   links: Links,
2356   allPages: AllPages,
2357   pinnedLinks: PinnedLinks,
2358   blockedLinks: BlockedLinks,
2359   activityStreamLinks: ActivityStreamLinks,
2360   activityStreamProvider: ActivityStreamProvider,