toolkit/modules/NewTabUtils.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial