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