toolkit/modules/NewTabUtils.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/NewTabUtils.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1183 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = ["NewTabUtils"];
    1.11 +
    1.12 +const Ci = Components.interfaces;
    1.13 +const Cc = Components.classes;
    1.14 +const Cu = Components.utils;
    1.15 +
    1.16 +Cu.import("resource://gre/modules/Services.jsm");
    1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.18 +
    1.19 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    1.20 +  "resource://gre/modules/PlacesUtils.jsm");
    1.21 +
    1.22 +XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
    1.23 +  "resource://gre/modules/PageThumbs.jsm");
    1.24 +
    1.25 +XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
    1.26 +  "resource://gre/modules/BinarySearch.jsm");
    1.27 +
    1.28 +XPCOMUtils.defineLazyGetter(this, "Timer", () => {
    1.29 +  return Cu.import("resource://gre/modules/Timer.jsm", {});
    1.30 +});
    1.31 +
    1.32 +XPCOMUtils.defineLazyGetter(this, "gPrincipal", function () {
    1.33 +  let uri = Services.io.newURI("about:newtab", null, null);
    1.34 +  return Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
    1.35 +});
    1.36 +
    1.37 +XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
    1.38 +  return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
    1.39 +});
    1.40 +
    1.41 +XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
    1.42 +  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
    1.43 +                    .createInstance(Ci.nsIScriptableUnicodeConverter);
    1.44 +  converter.charset = 'utf8';
    1.45 +  return converter;
    1.46 +});
    1.47 +
    1.48 +// The preference that tells whether this feature is enabled.
    1.49 +const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
    1.50 +
    1.51 +// The preference that tells the number of rows of the newtab grid.
    1.52 +const PREF_NEWTAB_ROWS = "browser.newtabpage.rows";
    1.53 +
    1.54 +// The preference that tells the number of columns of the newtab grid.
    1.55 +const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
    1.56 +
    1.57 +// The maximum number of results PlacesProvider retrieves from history.
    1.58 +const HISTORY_RESULTS_LIMIT = 100;
    1.59 +
    1.60 +// The maximum number of links Links.getLinks will return.
    1.61 +const LINKS_GET_LINKS_LIMIT = 100;
    1.62 +
    1.63 +// The gather telemetry topic.
    1.64 +const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
    1.65 +
    1.66 +// The amount of time we wait while coalescing updates for hidden pages.
    1.67 +const SCHEDULE_UPDATE_TIMEOUT_MS = 1000;
    1.68 +
    1.69 +/**
    1.70 + * Calculate the MD5 hash for a string.
    1.71 + * @param aValue
    1.72 + *        The string to convert.
    1.73 + * @return The base64 representation of the MD5 hash.
    1.74 + */
    1.75 +function toHash(aValue) {
    1.76 +  let value = gUnicodeConverter.convertToByteArray(aValue);
    1.77 +  gCryptoHash.init(gCryptoHash.MD5);
    1.78 +  gCryptoHash.update(value, value.length);
    1.79 +  return gCryptoHash.finish(true);
    1.80 +}
    1.81 +
    1.82 +/**
    1.83 + * Singleton that provides storage functionality.
    1.84 + */
    1.85 +XPCOMUtils.defineLazyGetter(this, "Storage", function() {
    1.86 +  return new LinksStorage();
    1.87 +});
    1.88 +
    1.89 +function LinksStorage() {
    1.90 +  // Handle migration of data across versions.
    1.91 +  try {
    1.92 +    if (this._storedVersion < this._version) {
    1.93 +      // This is either an upgrade, or version information is missing.
    1.94 +      if (this._storedVersion < 1) {
    1.95 +        // Version 1 moved data from DOM Storage to prefs.  Since migrating from
    1.96 +        // version 0 is no more supported, we just reportError a dataloss later.
    1.97 +        throw new Error("Unsupported newTab storage version");
    1.98 +      }
    1.99 +      // Add further migration steps here.
   1.100 +    }
   1.101 +    else {
   1.102 +      // This is a downgrade.  Since we cannot predict future, upgrades should
   1.103 +      // be backwards compatible.  We will set the version to the old value
   1.104 +      // regardless, so, on next upgrade, the migration steps will run again.
   1.105 +      // For this reason, they should also be able to run multiple times, even
   1.106 +      // on top of an already up-to-date storage.
   1.107 +    }
   1.108 +  } catch (ex) {
   1.109 +    // Something went wrong in the update process, we can't recover from here,
   1.110 +    // so just clear the storage and start from scratch (dataloss!).
   1.111 +    Components.utils.reportError(
   1.112 +      "Unable to migrate the newTab storage to the current version. "+
   1.113 +      "Restarting from scratch.\n" + ex);
   1.114 +    this.clear();
   1.115 +  }
   1.116 +
   1.117 +  // Set the version to the current one.
   1.118 +  this._storedVersion = this._version;
   1.119 +}
   1.120 +
   1.121 +LinksStorage.prototype = {
   1.122 +  get _version() 1,
   1.123 +
   1.124 +  get _prefs() Object.freeze({
   1.125 +    pinnedLinks: "browser.newtabpage.pinned",
   1.126 +    blockedLinks: "browser.newtabpage.blocked",
   1.127 +  }),
   1.128 +
   1.129 +  get _storedVersion() {
   1.130 +    if (this.__storedVersion === undefined) {
   1.131 +      try {
   1.132 +        this.__storedVersion =
   1.133 +          Services.prefs.getIntPref("browser.newtabpage.storageVersion");
   1.134 +      } catch (ex) {
   1.135 +        // The storage version is unknown, so either:
   1.136 +        // - it's a new profile
   1.137 +        // - it's a profile where versioning information got lost
   1.138 +        // In this case we still run through all of the valid migrations,
   1.139 +        // starting from 1, as if it was a downgrade.  As previously stated the
   1.140 +        // migrations should already support running on an updated store.
   1.141 +        this.__storedVersion = 1;
   1.142 +      }
   1.143 +    }
   1.144 +    return this.__storedVersion;
   1.145 +  },
   1.146 +  set _storedVersion(aValue) {
   1.147 +    Services.prefs.setIntPref("browser.newtabpage.storageVersion", aValue);
   1.148 +    this.__storedVersion = aValue;
   1.149 +    return aValue;
   1.150 +  },
   1.151 +
   1.152 +  /**
   1.153 +   * Gets the value for a given key from the storage.
   1.154 +   * @param aKey The storage key (a string).
   1.155 +   * @param aDefault A default value if the key doesn't exist.
   1.156 +   * @return The value for the given key.
   1.157 +   */
   1.158 +  get: function Storage_get(aKey, aDefault) {
   1.159 +    let value;
   1.160 +    try {
   1.161 +      let prefValue = Services.prefs.getComplexValue(this._prefs[aKey],
   1.162 +                                                     Ci.nsISupportsString).data;
   1.163 +      value = JSON.parse(prefValue);
   1.164 +    } catch (e) {}
   1.165 +    return value || aDefault;
   1.166 +  },
   1.167 +
   1.168 +  /**
   1.169 +   * Sets the storage value for a given key.
   1.170 +   * @param aKey The storage key (a string).
   1.171 +   * @param aValue The value to set.
   1.172 +   */
   1.173 +  set: function Storage_set(aKey, aValue) {
   1.174 +    // Page titles may contain unicode, thus use complex values.
   1.175 +    let string = Cc["@mozilla.org/supports-string;1"]
   1.176 +                   .createInstance(Ci.nsISupportsString);
   1.177 +    string.data = JSON.stringify(aValue);
   1.178 +    Services.prefs.setComplexValue(this._prefs[aKey], Ci.nsISupportsString,
   1.179 +                                   string);
   1.180 +  },
   1.181 +
   1.182 +  /**
   1.183 +   * Removes the storage value for a given key.
   1.184 +   * @param aKey The storage key (a string).
   1.185 +   */
   1.186 +  remove: function Storage_remove(aKey) {
   1.187 +    Services.prefs.clearUserPref(this._prefs[aKey]);
   1.188 +  },
   1.189 +
   1.190 +  /**
   1.191 +   * Clears the storage and removes all values.
   1.192 +   */
   1.193 +  clear: function Storage_clear() {
   1.194 +    for (let key in this._prefs) {
   1.195 +      this.remove(key);
   1.196 +    }
   1.197 +  }
   1.198 +};
   1.199 +
   1.200 +
   1.201 +/**
   1.202 + * Singleton that serves as a registry for all open 'New Tab Page's.
   1.203 + */
   1.204 +let AllPages = {
   1.205 +  /**
   1.206 +   * The array containing all active pages.
   1.207 +   */
   1.208 +  _pages: [],
   1.209 +
   1.210 +  /**
   1.211 +   * Cached value that tells whether the New Tab Page feature is enabled.
   1.212 +   */
   1.213 +  _enabled: null,
   1.214 +
   1.215 +  /**
   1.216 +   * Adds a page to the internal list of pages.
   1.217 +   * @param aPage The page to register.
   1.218 +   */
   1.219 +  register: function AllPages_register(aPage) {
   1.220 +    this._pages.push(aPage);
   1.221 +    this._addObserver();
   1.222 +  },
   1.223 +
   1.224 +  /**
   1.225 +   * Removes a page from the internal list of pages.
   1.226 +   * @param aPage The page to unregister.
   1.227 +   */
   1.228 +  unregister: function AllPages_unregister(aPage) {
   1.229 +    let index = this._pages.indexOf(aPage);
   1.230 +    if (index > -1)
   1.231 +      this._pages.splice(index, 1);
   1.232 +  },
   1.233 +
   1.234 +  /**
   1.235 +   * Returns whether the 'New Tab Page' is enabled.
   1.236 +   */
   1.237 +  get enabled() {
   1.238 +    if (this._enabled === null)
   1.239 +      this._enabled = Services.prefs.getBoolPref(PREF_NEWTAB_ENABLED);
   1.240 +
   1.241 +    return this._enabled;
   1.242 +  },
   1.243 +
   1.244 +  /**
   1.245 +   * Enables or disables the 'New Tab Page' feature.
   1.246 +   */
   1.247 +  set enabled(aEnabled) {
   1.248 +    if (this.enabled != aEnabled)
   1.249 +      Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, !!aEnabled);
   1.250 +  },
   1.251 +
   1.252 +  /**
   1.253 +   * Returns the number of registered New Tab Pages (i.e. the number of open
   1.254 +   * about:newtab instances).
   1.255 +   */
   1.256 +  get length() {
   1.257 +    return this._pages.length;
   1.258 +  },
   1.259 +
   1.260 +  /**
   1.261 +   * Updates all currently active pages but the given one.
   1.262 +   * @param aExceptPage The page to exclude from updating.
   1.263 +   * @param aHiddenPagesOnly If true, only pages hidden in the preloader are
   1.264 +   *                         updated.
   1.265 +   */
   1.266 +  update: function AllPages_update(aExceptPage, aHiddenPagesOnly=false) {
   1.267 +    this._pages.forEach(function (aPage) {
   1.268 +      if (aExceptPage != aPage)
   1.269 +        aPage.update(aHiddenPagesOnly);
   1.270 +    });
   1.271 +  },
   1.272 +
   1.273 +  /**
   1.274 +   * Many individual link changes may happen in a small amount of time over
   1.275 +   * multiple turns of the event loop.  This method coalesces updates by waiting
   1.276 +   * a small amount of time before updating hidden pages.
   1.277 +   */
   1.278 +  scheduleUpdateForHiddenPages: function AllPages_scheduleUpdateForHiddenPages() {
   1.279 +    if (!this._scheduleUpdateTimeout) {
   1.280 +      this._scheduleUpdateTimeout = Timer.setTimeout(() => {
   1.281 +        delete this._scheduleUpdateTimeout;
   1.282 +        this.update(null, true);
   1.283 +      }, SCHEDULE_UPDATE_TIMEOUT_MS);
   1.284 +    }
   1.285 +  },
   1.286 +
   1.287 +  get updateScheduledForHiddenPages() {
   1.288 +    return !!this._scheduleUpdateTimeout;
   1.289 +  },
   1.290 +
   1.291 +  /**
   1.292 +   * Implements the nsIObserver interface to get notified when the preference
   1.293 +   * value changes or when a new copy of a page thumbnail is available.
   1.294 +   */
   1.295 +  observe: function AllPages_observe(aSubject, aTopic, aData) {
   1.296 +    if (aTopic == "nsPref:changed") {
   1.297 +      // Clear the cached value.
   1.298 +      this._enabled = null;
   1.299 +    }
   1.300 +    // and all notifications get forwarded to each page.
   1.301 +    this._pages.forEach(function (aPage) {
   1.302 +      aPage.observe(aSubject, aTopic, aData);
   1.303 +    }, this);
   1.304 +  },
   1.305 +
   1.306 +  /**
   1.307 +   * Adds a preference and new thumbnail observer and turns itself into a
   1.308 +   * no-op after the first invokation.
   1.309 +   */
   1.310 +  _addObserver: function AllPages_addObserver() {
   1.311 +    Services.prefs.addObserver(PREF_NEWTAB_ENABLED, this, true);
   1.312 +    Services.obs.addObserver(this, "page-thumbnail:create", true);
   1.313 +    this._addObserver = function () {};
   1.314 +  },
   1.315 +
   1.316 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
   1.317 +                                         Ci.nsISupportsWeakReference])
   1.318 +};
   1.319 +
   1.320 +/**
   1.321 + * Singleton that keeps Grid preferences
   1.322 + */
   1.323 +let GridPrefs = {
   1.324 +  /**
   1.325 +   * Cached value that tells the number of rows of newtab grid.
   1.326 +   */
   1.327 +  _gridRows: null,
   1.328 +  get gridRows() {
   1.329 +    if (!this._gridRows) {
   1.330 +      this._gridRows = Math.max(1, Services.prefs.getIntPref(PREF_NEWTAB_ROWS));
   1.331 +    }
   1.332 +
   1.333 +    return this._gridRows;
   1.334 +  },
   1.335 +
   1.336 +  /**
   1.337 +   * Cached value that tells the number of columns of newtab grid.
   1.338 +   */
   1.339 +  _gridColumns: null,
   1.340 +  get gridColumns() {
   1.341 +    if (!this._gridColumns) {
   1.342 +      this._gridColumns = Math.max(1, Services.prefs.getIntPref(PREF_NEWTAB_COLUMNS));
   1.343 +    }
   1.344 +
   1.345 +    return this._gridColumns;
   1.346 +  },
   1.347 +
   1.348 +
   1.349 +  /**
   1.350 +   * Initializes object. Adds a preference observer
   1.351 +   */
   1.352 +  init: function GridPrefs_init() {
   1.353 +    Services.prefs.addObserver(PREF_NEWTAB_ROWS, this, false);
   1.354 +    Services.prefs.addObserver(PREF_NEWTAB_COLUMNS, this, false);
   1.355 +  },
   1.356 +
   1.357 +  /**
   1.358 +   * Implements the nsIObserver interface to get notified when the preference
   1.359 +   * value changes.
   1.360 +   */
   1.361 +  observe: function GridPrefs_observe(aSubject, aTopic, aData) {
   1.362 +    if (aData == PREF_NEWTAB_ROWS) {
   1.363 +      this._gridRows = null;
   1.364 +    } else {
   1.365 +      this._gridColumns = null;
   1.366 +    }
   1.367 +
   1.368 +    AllPages.update();
   1.369 +  }
   1.370 +};
   1.371 +
   1.372 +GridPrefs.init();
   1.373 +
   1.374 +/**
   1.375 + * Singleton that keeps track of all pinned links and their positions in the
   1.376 + * grid.
   1.377 + */
   1.378 +let PinnedLinks = {
   1.379 +  /**
   1.380 +   * The cached list of pinned links.
   1.381 +   */
   1.382 +  _links: null,
   1.383 +
   1.384 +  /**
   1.385 +   * The array of pinned links.
   1.386 +   */
   1.387 +  get links() {
   1.388 +    if (!this._links)
   1.389 +      this._links = Storage.get("pinnedLinks", []);
   1.390 +
   1.391 +    return this._links;
   1.392 +  },
   1.393 +
   1.394 +  /**
   1.395 +   * Pins a link at the given position.
   1.396 +   * @param aLink The link to pin.
   1.397 +   * @param aIndex The grid index to pin the cell at.
   1.398 +   */
   1.399 +  pin: function PinnedLinks_pin(aLink, aIndex) {
   1.400 +    // Clear the link's old position, if any.
   1.401 +    this.unpin(aLink);
   1.402 +
   1.403 +    this.links[aIndex] = aLink;
   1.404 +    this.save();
   1.405 +  },
   1.406 +
   1.407 +  /**
   1.408 +   * Unpins a given link.
   1.409 +   * @param aLink The link to unpin.
   1.410 +   */
   1.411 +  unpin: function PinnedLinks_unpin(aLink) {
   1.412 +    let index = this._indexOfLink(aLink);
   1.413 +    if (index == -1)
   1.414 +      return;
   1.415 +    let links = this.links;
   1.416 +    links[index] = null;
   1.417 +    // trim trailing nulls
   1.418 +    let i=links.length-1;
   1.419 +    while (i >= 0 && links[i] == null)
   1.420 +      i--;
   1.421 +    links.splice(i +1);
   1.422 +    this.save();
   1.423 +  },
   1.424 +
   1.425 +  /**
   1.426 +   * Saves the current list of pinned links.
   1.427 +   */
   1.428 +  save: function PinnedLinks_save() {
   1.429 +    Storage.set("pinnedLinks", this.links);
   1.430 +  },
   1.431 +
   1.432 +  /**
   1.433 +   * Checks whether a given link is pinned.
   1.434 +   * @params aLink The link to check.
   1.435 +   * @return whether The link is pinned.
   1.436 +   */
   1.437 +  isPinned: function PinnedLinks_isPinned(aLink) {
   1.438 +    return this._indexOfLink(aLink) != -1;
   1.439 +  },
   1.440 +
   1.441 +  /**
   1.442 +   * Resets the links cache.
   1.443 +   */
   1.444 +  resetCache: function PinnedLinks_resetCache() {
   1.445 +    this._links = null;
   1.446 +  },
   1.447 +
   1.448 +  /**
   1.449 +   * Finds the index of a given link in the list of pinned links.
   1.450 +   * @param aLink The link to find an index for.
   1.451 +   * @return The link's index.
   1.452 +   */
   1.453 +  _indexOfLink: function PinnedLinks_indexOfLink(aLink) {
   1.454 +    for (let i = 0; i < this.links.length; i++) {
   1.455 +      let link = this.links[i];
   1.456 +      if (link && link.url == aLink.url)
   1.457 +        return i;
   1.458 +    }
   1.459 +
   1.460 +    // The given link is unpinned.
   1.461 +    return -1;
   1.462 +  }
   1.463 +};
   1.464 +
   1.465 +/**
   1.466 + * Singleton that keeps track of all blocked links in the grid.
   1.467 + */
   1.468 +let BlockedLinks = {
   1.469 +  /**
   1.470 +   * The cached list of blocked links.
   1.471 +   */
   1.472 +  _links: null,
   1.473 +
   1.474 +  /**
   1.475 +   * The list of blocked links.
   1.476 +   */
   1.477 +  get links() {
   1.478 +    if (!this._links)
   1.479 +      this._links = Storage.get("blockedLinks", {});
   1.480 +
   1.481 +    return this._links;
   1.482 +  },
   1.483 +
   1.484 +  /**
   1.485 +   * Blocks a given link.
   1.486 +   * @param aLink The link to block.
   1.487 +   */
   1.488 +  block: function BlockedLinks_block(aLink) {
   1.489 +    this.links[toHash(aLink.url)] = 1;
   1.490 +    this.save();
   1.491 +
   1.492 +    // Make sure we unpin blocked links.
   1.493 +    PinnedLinks.unpin(aLink);
   1.494 +  },
   1.495 +
   1.496 +  /**
   1.497 +   * Unblocks a given link.
   1.498 +   * @param aLink The link to unblock.
   1.499 +   */
   1.500 +  unblock: function BlockedLinks_unblock(aLink) {
   1.501 +    if (this.isBlocked(aLink)) {
   1.502 +      delete this.links[toHash(aLink.url)];
   1.503 +      this.save();
   1.504 +    }
   1.505 +  },
   1.506 +
   1.507 +  /**
   1.508 +   * Saves the current list of blocked links.
   1.509 +   */
   1.510 +  save: function BlockedLinks_save() {
   1.511 +    Storage.set("blockedLinks", this.links);
   1.512 +  },
   1.513 +
   1.514 +  /**
   1.515 +   * Returns whether a given link is blocked.
   1.516 +   * @param aLink The link to check.
   1.517 +   */
   1.518 +  isBlocked: function BlockedLinks_isBlocked(aLink) {
   1.519 +    return (toHash(aLink.url) in this.links);
   1.520 +  },
   1.521 +
   1.522 +  /**
   1.523 +   * Checks whether the list of blocked links is empty.
   1.524 +   * @return Whether the list is empty.
   1.525 +   */
   1.526 +  isEmpty: function BlockedLinks_isEmpty() {
   1.527 +    return Object.keys(this.links).length == 0;
   1.528 +  },
   1.529 +
   1.530 +  /**
   1.531 +   * Resets the links cache.
   1.532 +   */
   1.533 +  resetCache: function BlockedLinks_resetCache() {
   1.534 +    this._links = null;
   1.535 +  }
   1.536 +};
   1.537 +
   1.538 +/**
   1.539 + * Singleton that serves as the default link provider for the grid. It queries
   1.540 + * the history to retrieve the most frequently visited sites.
   1.541 + */
   1.542 +let PlacesProvider = {
   1.543 +  /**
   1.544 +   * Set this to change the maximum number of links the provider will provide.
   1.545 +   */
   1.546 +  maxNumLinks: HISTORY_RESULTS_LIMIT,
   1.547 +
   1.548 +  /**
   1.549 +   * Must be called before the provider is used.
   1.550 +   */
   1.551 +  init: function PlacesProvider_init() {
   1.552 +    PlacesUtils.history.addObserver(this, true);
   1.553 +  },
   1.554 +
   1.555 +  /**
   1.556 +   * Gets the current set of links delivered by this provider.
   1.557 +   * @param aCallback The function that the array of links is passed to.
   1.558 +   */
   1.559 +  getLinks: function PlacesProvider_getLinks(aCallback) {
   1.560 +    let options = PlacesUtils.history.getNewQueryOptions();
   1.561 +    options.maxResults = this.maxNumLinks;
   1.562 +
   1.563 +    // Sort by frecency, descending.
   1.564 +    options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING
   1.565 +
   1.566 +    let links = [];
   1.567 +
   1.568 +    let callback = {
   1.569 +      handleResult: function (aResultSet) {
   1.570 +        let row;
   1.571 +
   1.572 +        while ((row = aResultSet.getNextRow())) {
   1.573 +          let url = row.getResultByIndex(1);
   1.574 +          if (LinkChecker.checkLoadURI(url)) {
   1.575 +            let title = row.getResultByIndex(2);
   1.576 +            let frecency = row.getResultByIndex(12);
   1.577 +            let lastVisitDate = row.getResultByIndex(5);
   1.578 +            links.push({
   1.579 +              url: url,
   1.580 +              title: title,
   1.581 +              frecency: frecency,
   1.582 +              lastVisitDate: lastVisitDate,
   1.583 +              bgColor: "transparent",
   1.584 +              type: "history",
   1.585 +              imageURI: null,
   1.586 +            });
   1.587 +          }
   1.588 +        }
   1.589 +      },
   1.590 +
   1.591 +      handleError: function (aError) {
   1.592 +        // Should we somehow handle this error?
   1.593 +        aCallback([]);
   1.594 +      },
   1.595 +
   1.596 +      handleCompletion: function (aReason) {
   1.597 +        // The Places query breaks ties in frecency by place ID descending, but
   1.598 +        // that's different from how Links.compareLinks breaks ties, because
   1.599 +        // compareLinks doesn't have access to place IDs.  It's very important
   1.600 +        // that the initial list of links is sorted in the same order imposed by
   1.601 +        // compareLinks, because Links uses compareLinks to perform binary
   1.602 +        // searches on the list.  So, ensure the list is so ordered.
   1.603 +        let i = 1;
   1.604 +        let outOfOrder = [];
   1.605 +        while (i < links.length) {
   1.606 +          if (Links.compareLinks(links[i - 1], links[i]) > 0)
   1.607 +            outOfOrder.push(links.splice(i, 1)[0]);
   1.608 +          else
   1.609 +            i++;
   1.610 +        }
   1.611 +        for (let link of outOfOrder) {
   1.612 +          i = BinarySearch.insertionIndexOf(links, link,
   1.613 +                                            Links.compareLinks.bind(Links));
   1.614 +          links.splice(i, 0, link);
   1.615 +        }
   1.616 +
   1.617 +        aCallback(links);
   1.618 +      }
   1.619 +    };
   1.620 +
   1.621 +    // Execute the query.
   1.622 +    let query = PlacesUtils.history.getNewQuery();
   1.623 +    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
   1.624 +    db.asyncExecuteLegacyQueries([query], 1, options, callback);
   1.625 +  },
   1.626 +
   1.627 +  /**
   1.628 +   * Registers an object that will be notified when the provider's links change.
   1.629 +   * @param aObserver An object with the following optional properties:
   1.630 +   *        * onLinkChanged: A function that's called when a single link
   1.631 +   *          changes.  It's passed the provider and the link object.  Only the
   1.632 +   *          link's `url` property is guaranteed to be present.  If its `title`
   1.633 +   *          property is present, then its title has changed, and the
   1.634 +   *          property's value is the new title.  If any sort properties are
   1.635 +   *          present, then its position within the provider's list of links may
   1.636 +   *          have changed, and the properties' values are the new sort-related
   1.637 +   *          values.  Note that this link may not necessarily have been present
   1.638 +   *          in the lists returned from any previous calls to getLinks.
   1.639 +   *        * onManyLinksChanged: A function that's called when many links
   1.640 +   *          change at once.  It's passed the provider.  You should call
   1.641 +   *          getLinks to get the provider's new list of links.
   1.642 +   */
   1.643 +  addObserver: function PlacesProvider_addObserver(aObserver) {
   1.644 +    this._observers.push(aObserver);
   1.645 +  },
   1.646 +
   1.647 +  _observers: [],
   1.648 +
   1.649 +  /**
   1.650 +   * Called by the history service.
   1.651 +   */
   1.652 +  onFrecencyChanged: function PlacesProvider_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) {
   1.653 +    // The implementation of the query in getLinks excludes hidden and
   1.654 +    // unvisited pages, so it's important to exclude them here, too.
   1.655 +    if (!aHidden && aLastVisitDate) {
   1.656 +      this._callObservers("onLinkChanged", {
   1.657 +        url: aURI.spec,
   1.658 +        frecency: aNewFrecency,
   1.659 +        lastVisitDate: aLastVisitDate,
   1.660 +      });
   1.661 +    }
   1.662 +  },
   1.663 +
   1.664 +  /**
   1.665 +   * Called by the history service.
   1.666 +   */
   1.667 +  onManyFrecenciesChanged: function PlacesProvider_onManyFrecenciesChanged() {
   1.668 +    this._callObservers("onManyLinksChanged");
   1.669 +  },
   1.670 +
   1.671 +  /**
   1.672 +   * Called by the history service.
   1.673 +   */
   1.674 +  onTitleChanged: function PlacesProvider_onTitleChanged(aURI, aNewTitle, aGUID) {
   1.675 +    this._callObservers("onLinkChanged", {
   1.676 +      url: aURI.spec,
   1.677 +      title: aNewTitle
   1.678 +    });
   1.679 +  },
   1.680 +
   1.681 +  _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
   1.682 +    for (let obs of this._observers) {
   1.683 +      if (obs[aMethodName]) {
   1.684 +        try {
   1.685 +          obs[aMethodName](this, aArg);
   1.686 +        } catch (err) {
   1.687 +          Cu.reportError(err);
   1.688 +        }
   1.689 +      }
   1.690 +    }
   1.691 +  },
   1.692 +
   1.693 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
   1.694 +                                         Ci.nsISupportsWeakReference]),
   1.695 +};
   1.696 +
   1.697 +/**
   1.698 + * Singleton that provides access to all links contained in the grid (including
   1.699 + * the ones that don't fit on the grid). A link is a plain object that looks
   1.700 + * like this:
   1.701 + *
   1.702 + * {
   1.703 + *   url: "http://www.mozilla.org/",
   1.704 + *   title: "Mozilla",
   1.705 + *   frecency: 1337,
   1.706 + *   lastVisitDate: 1394678824766431,
   1.707 + * }
   1.708 + */
   1.709 +let Links = {
   1.710 +  /**
   1.711 +   * The maximum number of links returned by getLinks.
   1.712 +   */
   1.713 +  maxNumLinks: LINKS_GET_LINKS_LIMIT,
   1.714 +
   1.715 +  /**
   1.716 +   * The link providers.
   1.717 +   */
   1.718 +  _providers: new Set(),
   1.719 +
   1.720 +  /**
   1.721 +   * A mapping from each provider to an object { sortedLinks, linkMap }.
   1.722 +   * sortedLinks is the cached, sorted array of links for the provider.  linkMap
   1.723 +   * is a Map from link URLs to link objects.
   1.724 +   */
   1.725 +  _providerLinks: new Map(),
   1.726 +
   1.727 +  /**
   1.728 +   * The properties of link objects used to sort them.
   1.729 +   */
   1.730 +  _sortProperties: [
   1.731 +    "frecency",
   1.732 +    "lastVisitDate",
   1.733 +    "url",
   1.734 +  ],
   1.735 +
   1.736 +  /**
   1.737 +   * List of callbacks waiting for the cache to be populated.
   1.738 +   */
   1.739 +  _populateCallbacks: [],
   1.740 +
   1.741 +  /**
   1.742 +   * Adds a link provider.
   1.743 +   * @param aProvider The link provider.
   1.744 +   */
   1.745 +  addProvider: function Links_addProvider(aProvider) {
   1.746 +    this._providers.add(aProvider);
   1.747 +    aProvider.addObserver(this);
   1.748 +  },
   1.749 +
   1.750 +  /**
   1.751 +   * Removes a link provider.
   1.752 +   * @param aProvider The link provider.
   1.753 +   */
   1.754 +  removeProvider: function Links_removeProvider(aProvider) {
   1.755 +    if (!this._providers.delete(aProvider))
   1.756 +      throw new Error("Unknown provider");
   1.757 +    this._providerLinks.delete(aProvider);
   1.758 +  },
   1.759 +
   1.760 +  /**
   1.761 +   * Populates the cache with fresh links from the providers.
   1.762 +   * @param aCallback The callback to call when finished (optional).
   1.763 +   * @param aForce When true, populates the cache even when it's already filled.
   1.764 +   */
   1.765 +  populateCache: function Links_populateCache(aCallback, aForce) {
   1.766 +    let callbacks = this._populateCallbacks;
   1.767 +
   1.768 +    // Enqueue the current callback.
   1.769 +    callbacks.push(aCallback);
   1.770 +
   1.771 +    // There was a callback waiting already, thus the cache has not yet been
   1.772 +    // populated.
   1.773 +    if (callbacks.length > 1)
   1.774 +      return;
   1.775 +
   1.776 +    function executeCallbacks() {
   1.777 +      while (callbacks.length) {
   1.778 +        let callback = callbacks.shift();
   1.779 +        if (callback) {
   1.780 +          try {
   1.781 +            callback();
   1.782 +          } catch (e) {
   1.783 +            // We want to proceed even if a callback fails.
   1.784 +          }
   1.785 +        }
   1.786 +      }
   1.787 +    }
   1.788 +
   1.789 +    let numProvidersRemaining = this._providers.size;
   1.790 +    for (let provider of this._providers) {
   1.791 +      this._populateProviderCache(provider, () => {
   1.792 +        if (--numProvidersRemaining == 0)
   1.793 +          executeCallbacks();
   1.794 +      }, aForce);
   1.795 +    }
   1.796 +
   1.797 +    this._addObserver();
   1.798 +  },
   1.799 +
   1.800 +  /**
   1.801 +   * Gets the current set of links contained in the grid.
   1.802 +   * @return The links in the grid.
   1.803 +   */
   1.804 +  getLinks: function Links_getLinks() {
   1.805 +    let pinnedLinks = Array.slice(PinnedLinks.links);
   1.806 +    let links = this._getMergedProviderLinks();
   1.807 +
   1.808 +    // Filter blocked and pinned links.
   1.809 +    links = links.filter(function (link) {
   1.810 +      return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
   1.811 +    });
   1.812 +
   1.813 +    // Try to fill the gaps between pinned links.
   1.814 +    for (let i = 0; i < pinnedLinks.length && links.length; i++)
   1.815 +      if (!pinnedLinks[i])
   1.816 +        pinnedLinks[i] = links.shift();
   1.817 +
   1.818 +    // Append the remaining links if any.
   1.819 +    if (links.length)
   1.820 +      pinnedLinks = pinnedLinks.concat(links);
   1.821 +
   1.822 +    return pinnedLinks;
   1.823 +  },
   1.824 +
   1.825 +  /**
   1.826 +   * Resets the links cache.
   1.827 +   */
   1.828 +  resetCache: function Links_resetCache() {
   1.829 +    this._providerLinks.clear();
   1.830 +  },
   1.831 +
   1.832 +  /**
   1.833 +   * Compares two links.
   1.834 +   * @param aLink1 The first link.
   1.835 +   * @param aLink2 The second link.
   1.836 +   * @return A negative number if aLink1 is ordered before aLink2, zero if
   1.837 +   *         aLink1 and aLink2 have the same ordering, or a positive number if
   1.838 +   *         aLink1 is ordered after aLink2.
   1.839 +   */
   1.840 +  compareLinks: function Links_compareLinks(aLink1, aLink2) {
   1.841 +    for (let prop of this._sortProperties) {
   1.842 +      if (!(prop in aLink1) || !(prop in aLink2))
   1.843 +        throw new Error("Comparable link missing required property: " + prop);
   1.844 +    }
   1.845 +    return aLink2.frecency - aLink1.frecency ||
   1.846 +           aLink2.lastVisitDate - aLink1.lastVisitDate ||
   1.847 +           aLink1.url.localeCompare(aLink2.url);
   1.848 +  },
   1.849 +
   1.850 +  /**
   1.851 +   * Calls getLinks on the given provider and populates our cache for it.
   1.852 +   * @param aProvider The provider whose cache will be populated.
   1.853 +   * @param aCallback The callback to call when finished.
   1.854 +   * @param aForce When true, populates the provider's cache even when it's
   1.855 +   *               already filled.
   1.856 +   */
   1.857 +  _populateProviderCache: function Links_populateProviderCache(aProvider, aCallback, aForce) {
   1.858 +    if (this._providerLinks.has(aProvider) && !aForce) {
   1.859 +      aCallback();
   1.860 +    } else {
   1.861 +      aProvider.getLinks(links => {
   1.862 +        // Filter out null and undefined links so we don't have to deal with
   1.863 +        // them in getLinks when merging links from providers.
   1.864 +        links = links.filter((link) => !!link);
   1.865 +        this._providerLinks.set(aProvider, {
   1.866 +          sortedLinks: links,
   1.867 +          linkMap: links.reduce((map, link) => {
   1.868 +            map.set(link.url, link);
   1.869 +            return map;
   1.870 +          }, new Map()),
   1.871 +        });
   1.872 +        aCallback();
   1.873 +      });
   1.874 +    }
   1.875 +  },
   1.876 +
   1.877 +  /**
   1.878 +   * Merges the cached lists of links from all providers whose lists are cached.
   1.879 +   * @return The merged list.
   1.880 +   */
   1.881 +  _getMergedProviderLinks: function Links__getMergedProviderLinks() {
   1.882 +    // Build a list containing a copy of each provider's sortedLinks list.
   1.883 +    let linkLists = [];
   1.884 +    for (let links of this._providerLinks.values()) {
   1.885 +      linkLists.push(links.sortedLinks.slice());
   1.886 +    }
   1.887 +
   1.888 +    function getNextLink() {
   1.889 +      let minLinks = null;
   1.890 +      for (let links of linkLists) {
   1.891 +        if (links.length &&
   1.892 +            (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0))
   1.893 +          minLinks = links;
   1.894 +      }
   1.895 +      return minLinks ? minLinks.shift() : null;
   1.896 +    }
   1.897 +
   1.898 +    let finalLinks = [];
   1.899 +    for (let nextLink = getNextLink();
   1.900 +         nextLink && finalLinks.length < this.maxNumLinks;
   1.901 +         nextLink = getNextLink()) {
   1.902 +      finalLinks.push(nextLink);
   1.903 +    }
   1.904 +
   1.905 +    return finalLinks;
   1.906 +  },
   1.907 +
   1.908 +  /**
   1.909 +   * Called by a provider to notify us when a single link changes.
   1.910 +   * @param aProvider The provider whose link changed.
   1.911 +   * @param aLink The link that changed.  If the link is new, it must have all
   1.912 +   *              of the _sortProperties.  Otherwise, it may have as few or as
   1.913 +   *              many as is convenient.
   1.914 +   */
   1.915 +  onLinkChanged: function Links_onLinkChanged(aProvider, aLink) {
   1.916 +    if (!("url" in aLink))
   1.917 +      throw new Error("Changed links must have a url property");
   1.918 +
   1.919 +    let links = this._providerLinks.get(aProvider);
   1.920 +    if (!links)
   1.921 +      // This is not an error, it just means that between the time the provider
   1.922 +      // was added and the future time we call getLinks on it, it notified us of
   1.923 +      // a change.
   1.924 +      return;
   1.925 +
   1.926 +    let { sortedLinks, linkMap } = links;
   1.927 +    let existingLink = linkMap.get(aLink.url);
   1.928 +    let insertionLink = null;
   1.929 +    let updatePages = false;
   1.930 +
   1.931 +    if (existingLink) {
   1.932 +      // Update our copy's position in O(lg n) by first removing it from its
   1.933 +      // list.  It's important to do this before modifying its properties.
   1.934 +      if (this._sortProperties.some(prop => prop in aLink)) {
   1.935 +        let idx = this._indexOf(sortedLinks, existingLink);
   1.936 +        if (idx < 0) {
   1.937 +          throw new Error("Link should be in _sortedLinks if in _linkMap");
   1.938 +        }
   1.939 +        sortedLinks.splice(idx, 1);
   1.940 +        // Update our copy's properties.
   1.941 +        for (let prop of this._sortProperties) {
   1.942 +          if (prop in aLink) {
   1.943 +            existingLink[prop] = aLink[prop];
   1.944 +          }
   1.945 +        }
   1.946 +        // Finally, reinsert our copy below.
   1.947 +        insertionLink = existingLink;
   1.948 +      }
   1.949 +      // Update our copy's title in O(1).
   1.950 +      if ("title" in aLink && aLink.title != existingLink.title) {
   1.951 +        existingLink.title = aLink.title;
   1.952 +        updatePages = true;
   1.953 +      }
   1.954 +    }
   1.955 +    else if (this._sortProperties.every(prop => prop in aLink)) {
   1.956 +      // Before doing the O(lg n) insertion below, do an O(1) check for the
   1.957 +      // common case where the new link is too low-ranked to be in the list.
   1.958 +      if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
   1.959 +        let lastLink = sortedLinks[sortedLinks.length - 1];
   1.960 +        if (this.compareLinks(lastLink, aLink) < 0) {
   1.961 +          return;
   1.962 +        }
   1.963 +      }
   1.964 +      // Copy the link object so that changes later made to it by the caller
   1.965 +      // don't affect our copy.
   1.966 +      insertionLink = {};
   1.967 +      for (let prop in aLink) {
   1.968 +        insertionLink[prop] = aLink[prop];
   1.969 +      }
   1.970 +      linkMap.set(aLink.url, insertionLink);
   1.971 +    }
   1.972 +
   1.973 +    if (insertionLink) {
   1.974 +      let idx = this._insertionIndexOf(sortedLinks, insertionLink);
   1.975 +      sortedLinks.splice(idx, 0, insertionLink);
   1.976 +      if (sortedLinks.length > aProvider.maxNumLinks) {
   1.977 +        let lastLink = sortedLinks.pop();
   1.978 +        linkMap.delete(lastLink.url);
   1.979 +      }
   1.980 +      updatePages = true;
   1.981 +    }
   1.982 +
   1.983 +    if (updatePages)
   1.984 +      AllPages.scheduleUpdateForHiddenPages();
   1.985 +  },
   1.986 +
   1.987 +  /**
   1.988 +   * Called by a provider to notify us when many links change.
   1.989 +   */
   1.990 +  onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
   1.991 +    this._populateProviderCache(aProvider, () => {
   1.992 +      AllPages.scheduleUpdateForHiddenPages();
   1.993 +    }, true);
   1.994 +  },
   1.995 +
   1.996 +  _indexOf: function Links__indexOf(aArray, aLink) {
   1.997 +    return this._binsearch(aArray, aLink, "indexOf");
   1.998 +  },
   1.999 +
  1.1000 +  _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
  1.1001 +    return this._binsearch(aArray, aLink, "insertionIndexOf");
  1.1002 +  },
  1.1003 +
  1.1004 +  _binsearch: function Links__binsearch(aArray, aLink, aMethod) {
  1.1005 +    return BinarySearch[aMethod](aArray, aLink, this.compareLinks.bind(this));
  1.1006 +  },
  1.1007 +
  1.1008 +  /**
  1.1009 +   * Implements the nsIObserver interface to get notified about browser history
  1.1010 +   * sanitization.
  1.1011 +   */
  1.1012 +  observe: function Links_observe(aSubject, aTopic, aData) {
  1.1013 +    // Make sure to update open about:newtab instances. If there are no opened
  1.1014 +    // pages we can just wait for the next new tab to populate the cache again.
  1.1015 +    if (AllPages.length && AllPages.enabled)
  1.1016 +      this.populateCache(function () { AllPages.update() }, true);
  1.1017 +    else
  1.1018 +      this.resetCache();
  1.1019 +  },
  1.1020 +
  1.1021 +  /**
  1.1022 +   * Adds a sanitization observer and turns itself into a no-op after the first
  1.1023 +   * invokation.
  1.1024 +   */
  1.1025 +  _addObserver: function Links_addObserver() {
  1.1026 +    Services.obs.addObserver(this, "browser:purge-session-history", true);
  1.1027 +    this._addObserver = function () {};
  1.1028 +  },
  1.1029 +
  1.1030 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
  1.1031 +                                         Ci.nsISupportsWeakReference])
  1.1032 +};
  1.1033 +
  1.1034 +/**
  1.1035 + * Singleton used to collect telemetry data.
  1.1036 + *
  1.1037 + */
  1.1038 +let Telemetry = {
  1.1039 +  /**
  1.1040 +   * Initializes object.
  1.1041 +   */
  1.1042 +  init: function Telemetry_init() {
  1.1043 +    Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY, false);
  1.1044 +  },
  1.1045 +
  1.1046 +  /**
  1.1047 +   * Collects data.
  1.1048 +   */
  1.1049 +  _collect: function Telemetry_collect() {
  1.1050 +    let probes = [
  1.1051 +      { histogram: "NEWTAB_PAGE_ENABLED",
  1.1052 +        value: AllPages.enabled },
  1.1053 +      { histogram: "NEWTAB_PAGE_PINNED_SITES_COUNT",
  1.1054 +        value: PinnedLinks.links.length },
  1.1055 +      { histogram: "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
  1.1056 +        value: Object.keys(BlockedLinks.links).length }
  1.1057 +    ];
  1.1058 +
  1.1059 +    probes.forEach(function Telemetry_collect_forEach(aProbe) {
  1.1060 +      Services.telemetry.getHistogramById(aProbe.histogram)
  1.1061 +        .add(aProbe.value);
  1.1062 +    });
  1.1063 +  },
  1.1064 +
  1.1065 +  /**
  1.1066 +   * Listens for gather telemetry topic.
  1.1067 +   */
  1.1068 +  observe: function Telemetry_observe(aSubject, aTopic, aData) {
  1.1069 +    this._collect();
  1.1070 +  }
  1.1071 +};
  1.1072 +
  1.1073 +/**
  1.1074 + * Singleton that checks if a given link should be displayed on about:newtab
  1.1075 + * or if we should rather not do it for security reasons. URIs that inherit
  1.1076 + * their caller's principal will be filtered.
  1.1077 + */
  1.1078 +let LinkChecker = {
  1.1079 +  _cache: {},
  1.1080 +
  1.1081 +  get flags() {
  1.1082 +    return Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
  1.1083 +           Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS;
  1.1084 +  },
  1.1085 +
  1.1086 +  checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
  1.1087 +    if (!(aURI in this._cache))
  1.1088 +      this._cache[aURI] = this._doCheckLoadURI(aURI);
  1.1089 +
  1.1090 +    return this._cache[aURI];
  1.1091 +  },
  1.1092 +
  1.1093 +  _doCheckLoadURI: function Links_doCheckLoadURI(aURI) {
  1.1094 +    try {
  1.1095 +      Services.scriptSecurityManager.
  1.1096 +        checkLoadURIStrWithPrincipal(gPrincipal, aURI, this.flags);
  1.1097 +      return true;
  1.1098 +    } catch (e) {
  1.1099 +      // We got a weird URI or one that would inherit the caller's principal.
  1.1100 +      return false;
  1.1101 +    }
  1.1102 +  }
  1.1103 +};
  1.1104 +
  1.1105 +let ExpirationFilter = {
  1.1106 +  init: function ExpirationFilter_init() {
  1.1107 +    PageThumbs.addExpirationFilter(this);
  1.1108 +  },
  1.1109 +
  1.1110 +  filterForThumbnailExpiration:
  1.1111 +  function ExpirationFilter_filterForThumbnailExpiration(aCallback) {
  1.1112 +    if (!AllPages.enabled) {
  1.1113 +      aCallback([]);
  1.1114 +      return;
  1.1115 +    }
  1.1116 +
  1.1117 +    Links.populateCache(function () {
  1.1118 +      let urls = [];
  1.1119 +
  1.1120 +      // Add all URLs to the list that we want to keep thumbnails for.
  1.1121 +      for (let link of Links.getLinks().slice(0, 25)) {
  1.1122 +        if (link && link.url)
  1.1123 +          urls.push(link.url);
  1.1124 +      }
  1.1125 +
  1.1126 +      aCallback(urls);
  1.1127 +    });
  1.1128 +  }
  1.1129 +};
  1.1130 +
  1.1131 +/**
  1.1132 + * Singleton that provides the public API of this JSM.
  1.1133 + */
  1.1134 +this.NewTabUtils = {
  1.1135 +  _initialized: false,
  1.1136 +
  1.1137 +  init: function NewTabUtils_init() {
  1.1138 +    if (this.initWithoutProviders()) {
  1.1139 +      PlacesProvider.init();
  1.1140 +      Links.addProvider(PlacesProvider);
  1.1141 +    }
  1.1142 +  },
  1.1143 +
  1.1144 +  initWithoutProviders: function NewTabUtils_initWithoutProviders() {
  1.1145 +    if (!this._initialized) {
  1.1146 +      this._initialized = true;
  1.1147 +      ExpirationFilter.init();
  1.1148 +      Telemetry.init();
  1.1149 +      return true;
  1.1150 +    }
  1.1151 +    return false;
  1.1152 +  },
  1.1153 +
  1.1154 +  /**
  1.1155 +   * Restores all sites that have been removed from the grid.
  1.1156 +   */
  1.1157 +  restore: function NewTabUtils_restore() {
  1.1158 +    Storage.clear();
  1.1159 +    Links.resetCache();
  1.1160 +    PinnedLinks.resetCache();
  1.1161 +    BlockedLinks.resetCache();
  1.1162 +
  1.1163 +    Links.populateCache(function () {
  1.1164 +      AllPages.update();
  1.1165 +    }, true);
  1.1166 +  },
  1.1167 +
  1.1168 +  /**
  1.1169 +   * Undoes all sites that have been removed from the grid and keep the pinned
  1.1170 +   * tabs.
  1.1171 +   * @param aCallback the callback method.
  1.1172 +   */
  1.1173 +  undoAll: function NewTabUtils_undoAll(aCallback) {
  1.1174 +    Storage.remove("blockedLinks");
  1.1175 +    Links.resetCache();
  1.1176 +    BlockedLinks.resetCache();
  1.1177 +    Links.populateCache(aCallback, true);
  1.1178 +  },
  1.1179 +
  1.1180 +  links: Links,
  1.1181 +  allPages: AllPages,
  1.1182 +  linkChecker: LinkChecker,
  1.1183 +  pinnedLinks: PinnedLinks,
  1.1184 +  blockedLinks: BlockedLinks,
  1.1185 +  gridPrefs: GridPrefs
  1.1186 +};

mercurial