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