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 +};