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