forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			2361 lines
		
	
	
	
		
			68 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2361 lines
		
	
	
	
		
			68 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | |
|  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| // Android tests don't import these properly, so guard against that
 | |
| let shortURL = {};
 | |
| let searchShortcuts = {};
 | |
| let didSuccessfulImport = false;
 | |
| try {
 | |
|   shortURL = ChromeUtils.import("resource://activity-stream/lib/ShortURL.jsm");
 | |
|   searchShortcuts = ChromeUtils.importESModule(
 | |
|     "resource://activity-stream/lib/SearchShortcuts.sys.mjs"
 | |
|   );
 | |
|   didSuccessfulImport = true;
 | |
| } catch (e) {
 | |
|   // The test failed to import these files
 | |
| }
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs",
 | |
|   PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
 | |
|   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | |
|   Pocket: "chrome://pocket/content/Pocket.sys.mjs",
 | |
|   pktApi: "chrome://pocket/content/pktApi.sys.mjs",
 | |
| });
 | |
| 
 | |
| let BrowserWindowTracker;
 | |
| try {
 | |
|   BrowserWindowTracker = ChromeUtils.importESModule(
 | |
|     "resource:///modules/BrowserWindowTracker.sys.mjs"
 | |
|   ).BrowserWindowTracker;
 | |
| } catch (e) {
 | |
|   // BrowserWindowTracker is used to determine devicePixelRatio in
 | |
|   // _addFavicons. We fallback to the value 2 if we can't find a window,
 | |
|   // so it's safe to do nothing with this here.
 | |
| }
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "gCryptoHash", function () {
 | |
|   return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
 | |
| });
 | |
| 
 | |
| // Boolean preferences that control newtab content
 | |
| const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
 | |
| 
 | |
| // The maximum number of results PlacesProvider retrieves from history.
 | |
| const HISTORY_RESULTS_LIMIT = 100;
 | |
| 
 | |
| // The maximum number of links Links.getLinks will return.
 | |
| const LINKS_GET_LINKS_LIMIT = 100;
 | |
| 
 | |
| // The gather telemetry topic.
 | |
| const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
 | |
| 
 | |
| // Some default frecency threshold for Activity Stream requests
 | |
| const ACTIVITY_STREAM_DEFAULT_FRECENCY = 150;
 | |
| 
 | |
| // Some default query limit for Activity Stream requests
 | |
| const ACTIVITY_STREAM_DEFAULT_LIMIT = 12;
 | |
| 
 | |
| // Some default seconds ago for Activity Stream recent requests
 | |
| const ACTIVITY_STREAM_DEFAULT_RECENT = 5 * 24 * 60 * 60;
 | |
| 
 | |
| // The fallback value for the width of smallFavicon in pixels.
 | |
| // This value will be multiplied by the current window's devicePixelRatio.
 | |
| // If devicePixelRatio cannot be found, it will be multiplied by 2.
 | |
| const DEFAULT_SMALL_FAVICON_WIDTH = 16;
 | |
| 
 | |
| const POCKET_UPDATE_TIME = 24 * 60 * 60 * 1000; // 1 day
 | |
| const POCKET_INACTIVE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week
 | |
| const PREF_POCKET_LATEST_SINCE = "extensions.pocket.settings.latestSince";
 | |
| 
 | |
| /**
 | |
|  * Calculate the MD5 hash for a string.
 | |
|  * @param aValue
 | |
|  *        The string to convert.
 | |
|  * @return The base64 representation of the MD5 hash.
 | |
|  */
 | |
| function toHash(aValue) {
 | |
|   let value = new TextEncoder().encode(aValue);
 | |
|   lazy.gCryptoHash.init(lazy.gCryptoHash.MD5);
 | |
|   lazy.gCryptoHash.update(value, value.length);
 | |
|   return lazy.gCryptoHash.finish(true);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Singleton that provides storage functionality.
 | |
|  */
 | |
| ChromeUtils.defineLazyGetter(lazy, "Storage", function () {
 | |
|   return new LinksStorage();
 | |
| });
 | |
| 
 | |
| function LinksStorage() {
 | |
|   // Handle migration of data across versions.
 | |
|   try {
 | |
|     if (this._storedVersion < this._version) {
 | |
|       // This is either an upgrade, or version information is missing.
 | |
|       if (this._storedVersion < 1) {
 | |
|         // Version 1 moved data from DOM Storage to prefs.  Since migrating from
 | |
|         // version 0 is no more supported, we just reportError a dataloss later.
 | |
|         throw new Error("Unsupported newTab storage version");
 | |
|       }
 | |
|       // Add further migration steps here.
 | |
|     } else {
 | |
|       // This is a downgrade.  Since we cannot predict future, upgrades should
 | |
|       // be backwards compatible.  We will set the version to the old value
 | |
|       // regardless, so, on next upgrade, the migration steps will run again.
 | |
|       // For this reason, they should also be able to run multiple times, even
 | |
|       // on top of an already up-to-date storage.
 | |
|     }
 | |
|   } catch (ex) {
 | |
|     // Something went wrong in the update process, we can't recover from here,
 | |
|     // so just clear the storage and start from scratch (dataloss!).
 | |
|     console.error(
 | |
|       "Unable to migrate the newTab storage to the current version. " +
 | |
|         "Restarting from scratch.\n",
 | |
|       ex
 | |
|     );
 | |
|     this.clear();
 | |
|   }
 | |
| 
 | |
|   // Set the version to the current one.
 | |
|   this._storedVersion = this._version;
 | |
| }
 | |
| 
 | |
| LinksStorage.prototype = {
 | |
|   get _version() {
 | |
|     return 1;
 | |
|   },
 | |
| 
 | |
|   get _prefs() {
 | |
|     return Object.freeze({
 | |
|       pinnedLinks: "browser.newtabpage.pinned",
 | |
|       blockedLinks: "browser.newtabpage.blocked",
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   get _storedVersion() {
 | |
|     if (this.__storedVersion === undefined) {
 | |
|       // When the pref is not set, the storage version is unknown, so either:
 | |
|       // - it's a new profile
 | |
|       // - it's a profile where versioning information got lost
 | |
|       // In this case we still run through all of the valid migrations,
 | |
|       // starting from 1, as if it was a downgrade.  As previously stated the
 | |
|       // migrations should already support running on an updated store.
 | |
|       this.__storedVersion = Services.prefs.getIntPref(
 | |
|         "browser.newtabpage.storageVersion",
 | |
|         1
 | |
|       );
 | |
|     }
 | |
|     return this.__storedVersion;
 | |
|   },
 | |
|   set _storedVersion(aValue) {
 | |
|     Services.prefs.setIntPref("browser.newtabpage.storageVersion", aValue);
 | |
|     this.__storedVersion = aValue;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets the value for a given key from the storage.
 | |
|    * @param aKey The storage key (a string).
 | |
|    * @param aDefault A default value if the key doesn't exist.
 | |
|    * @return The value for the given key.
 | |
|    */
 | |
|   get: function Storage_get(aKey, aDefault) {
 | |
|     let value;
 | |
|     try {
 | |
|       let prefValue = Services.prefs.getStringPref(this._prefs[aKey]);
 | |
|       value = JSON.parse(prefValue);
 | |
|     } catch (e) {}
 | |
|     return value || aDefault;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets the storage value for a given key.
 | |
|    * @param aKey The storage key (a string).
 | |
|    * @param aValue The value to set.
 | |
|    */
 | |
|   set: function Storage_set(aKey, aValue) {
 | |
|     // Page titles may contain unicode, thus use complex values.
 | |
|     Services.prefs.setStringPref(this._prefs[aKey], JSON.stringify(aValue));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes the storage value for a given key.
 | |
|    * @param aKey The storage key (a string).
 | |
|    */
 | |
|   remove: function Storage_remove(aKey) {
 | |
|     Services.prefs.clearUserPref(this._prefs[aKey]);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Clears the storage and removes all values.
 | |
|    */
 | |
|   clear: function Storage_clear() {
 | |
|     for (let key in this._prefs) {
 | |
|       this.remove(key);
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Singleton that serves as a registry for all open 'New Tab Page's.
 | |
|  */
 | |
| var AllPages = {
 | |
|   /**
 | |
|    * The array containing all active pages.
 | |
|    */
 | |
|   _pages: [],
 | |
| 
 | |
|   /**
 | |
|    * Cached value that tells whether the New Tab Page feature is enabled.
 | |
|    */
 | |
|   _enabled: null,
 | |
| 
 | |
|   /**
 | |
|    * Adds a page to the internal list of pages.
 | |
|    * @param aPage The page to register.
 | |
|    */
 | |
|   register: function AllPages_register(aPage) {
 | |
|     this._pages.push(aPage);
 | |
|     this._addObserver();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes a page from the internal list of pages.
 | |
|    * @param aPage The page to unregister.
 | |
|    */
 | |
|   unregister: function AllPages_unregister(aPage) {
 | |
|     let index = this._pages.indexOf(aPage);
 | |
|     if (index > -1) {
 | |
|       this._pages.splice(index, 1);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns whether the 'New Tab Page' is enabled.
 | |
|    */
 | |
|   get enabled() {
 | |
|     if (this._enabled === null) {
 | |
|       this._enabled = Services.prefs.getBoolPref(PREF_NEWTAB_ENABLED);
 | |
|     }
 | |
| 
 | |
|     return this._enabled;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Enables or disables the 'New Tab Page' feature.
 | |
|    */
 | |
|   set enabled(aEnabled) {
 | |
|     if (this.enabled != aEnabled) {
 | |
|       Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, !!aEnabled);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns the number of registered New Tab Pages (i.e. the number of open
 | |
|    * about:newtab instances).
 | |
|    */
 | |
|   get length() {
 | |
|     return this._pages.length;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Updates all currently active pages but the given one.
 | |
|    * @param aExceptPage The page to exclude from updating.
 | |
|    * @param aReason The reason for updating all pages.
 | |
|    */
 | |
|   update(aExceptPage, aReason = "") {
 | |
|     for (let page of this._pages.slice()) {
 | |
|       if (aExceptPage != page) {
 | |
|         page.update(aReason);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Implements the nsIObserver interface to get notified when the preference
 | |
|    * value changes or when a new copy of a page thumbnail is available.
 | |
|    */
 | |
|   observe: function AllPages_observe(aSubject, aTopic, aData) {
 | |
|     if (aTopic == "nsPref:changed") {
 | |
|       // Clear the cached value.
 | |
|       switch (aData) {
 | |
|         case PREF_NEWTAB_ENABLED:
 | |
|           this._enabled = null;
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|     // and all notifications get forwarded to each page.
 | |
|     this._pages.forEach(function (aPage) {
 | |
|       aPage.observe(aSubject, aTopic, aData);
 | |
|     }, this);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds a preference and new thumbnail observer and turns itself into a
 | |
|    * no-op after the first invokation.
 | |
|    */
 | |
|   _addObserver: function AllPages_addObserver() {
 | |
|     Services.prefs.addObserver(PREF_NEWTAB_ENABLED, this, true);
 | |
|     Services.obs.addObserver(this, "page-thumbnail:create", true);
 | |
|     this._addObserver = function () {};
 | |
|   },
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI([
 | |
|     "nsIObserver",
 | |
|     "nsISupportsWeakReference",
 | |
|   ]),
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Singleton that keeps track of all pinned links and their positions in the
 | |
|  * grid.
 | |
|  */
 | |
| var PinnedLinks = {
 | |
|   /**
 | |
|    * The cached list of pinned links.
 | |
|    */
 | |
|   _links: null,
 | |
| 
 | |
|   /**
 | |
|    * The array of pinned links.
 | |
|    */
 | |
|   get links() {
 | |
|     if (!this._links) {
 | |
|       this._links = lazy.Storage.get("pinnedLinks", []);
 | |
|     }
 | |
| 
 | |
|     return this._links;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Pins a link at the given position.
 | |
|    * @param aLink The link to pin.
 | |
|    * @param aIndex The grid index to pin the cell at.
 | |
|    * @return true if link changes, false otherwise
 | |
|    */
 | |
|   pin: function PinnedLinks_pin(aLink, aIndex) {
 | |
|     // Clear the link's old position, if any.
 | |
|     this.unpin(aLink);
 | |
| 
 | |
|     // change pinned link into a history link
 | |
|     let changed = this._makeHistoryLink(aLink);
 | |
|     this.links[aIndex] = aLink;
 | |
|     this.save();
 | |
|     return changed;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unpins a given link.
 | |
|    * @param aLink The link to unpin.
 | |
|    */
 | |
|   unpin: function PinnedLinks_unpin(aLink) {
 | |
|     let index = this._indexOfLink(aLink);
 | |
|     if (index == -1) {
 | |
|       return;
 | |
|     }
 | |
|     let links = this.links;
 | |
|     links[index] = null;
 | |
|     // trim trailing nulls
 | |
|     let i = links.length - 1;
 | |
|     while (i >= 0 && links[i] == null) {
 | |
|       i--;
 | |
|     }
 | |
|     links.splice(i + 1);
 | |
|     this.save();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Saves the current list of pinned links.
 | |
|    */
 | |
|   save: function PinnedLinks_save() {
 | |
|     lazy.Storage.set("pinnedLinks", this.links);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks whether a given link is pinned.
 | |
|    * @params aLink The link to check.
 | |
|    * @return whether The link is pinned.
 | |
|    */
 | |
|   isPinned: function PinnedLinks_isPinned(aLink) {
 | |
|     return this._indexOfLink(aLink) != -1;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Resets the links cache.
 | |
|    */
 | |
|   resetCache: function PinnedLinks_resetCache() {
 | |
|     this._links = null;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Finds the index of a given link in the list of pinned links.
 | |
|    * @param aLink The link to find an index for.
 | |
|    * @return The link's index.
 | |
|    */
 | |
|   _indexOfLink: function PinnedLinks_indexOfLink(aLink) {
 | |
|     for (let i = 0; i < this.links.length; i++) {
 | |
|       let link = this.links[i];
 | |
|       if (link && link.url == aLink.url) {
 | |
|         return i;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // The given link is unpinned.
 | |
|     return -1;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Transforms link into a "history" link
 | |
|    * @param aLink The link to change
 | |
|    * @return true if link changes, false otherwise
 | |
|    */
 | |
|   _makeHistoryLink: function PinnedLinks_makeHistoryLink(aLink) {
 | |
|     if (!aLink.type || aLink.type == "history") {
 | |
|       return false;
 | |
|     }
 | |
|     aLink.type = "history";
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Replaces existing link with another link.
 | |
|    * @param aUrl The url of existing link
 | |
|    * @param aLink The replacement link
 | |
|    */
 | |
|   replace: function PinnedLinks_replace(aUrl, aLink) {
 | |
|     let index = this._indexOfLink({ url: aUrl });
 | |
|     if (index == -1) {
 | |
|       return;
 | |
|     }
 | |
|     this.links[index] = aLink;
 | |
|     this.save();
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Singleton that keeps track of all blocked links in the grid.
 | |
|  */
 | |
| var BlockedLinks = {
 | |
|   /**
 | |
|    * A list of objects that are observing blocked link changes.
 | |
|    */
 | |
|   _observers: [],
 | |
| 
 | |
|   /**
 | |
|    * The cached list of blocked links.
 | |
|    */
 | |
|   _links: null,
 | |
| 
 | |
|   /**
 | |
|    * Registers an object that will be notified when the blocked links change.
 | |
|    */
 | |
|   addObserver(aObserver) {
 | |
|     this._observers.push(aObserver);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Remove the observers.
 | |
|    */
 | |
|   removeObservers() {
 | |
|     this._observers = [];
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * The list of blocked links.
 | |
|    */
 | |
|   get links() {
 | |
|     if (!this._links) {
 | |
|       this._links = lazy.Storage.get("blockedLinks", {});
 | |
|     }
 | |
| 
 | |
|     return this._links;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Blocks a given link. Adjusts siteMap accordingly, and notifies listeners.
 | |
|    * @param aLink The link to block.
 | |
|    */
 | |
|   block: function BlockedLinks_block(aLink) {
 | |
|     this._callObservers("onLinkBlocked", aLink);
 | |
|     this.links[toHash(aLink.url)] = 1;
 | |
|     this.save();
 | |
| 
 | |
|     // Make sure we unpin blocked links.
 | |
|     PinnedLinks.unpin(aLink);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unblocks a given link. Adjusts siteMap accordingly, and notifies listeners.
 | |
|    * @param aLink The link to unblock.
 | |
|    */
 | |
|   unblock: function BlockedLinks_unblock(aLink) {
 | |
|     if (this.isBlocked(aLink)) {
 | |
|       delete this.links[toHash(aLink.url)];
 | |
|       this.save();
 | |
|       this._callObservers("onLinkUnblocked", aLink);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Saves the current list of blocked links.
 | |
|    */
 | |
|   save: function BlockedLinks_save() {
 | |
|     lazy.Storage.set("blockedLinks", this.links);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns whether a given link is blocked.
 | |
|    * @param aLink The link to check.
 | |
|    */
 | |
|   isBlocked: function BlockedLinks_isBlocked(aLink) {
 | |
|     return toHash(aLink.url) in this.links;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Checks whether the list of blocked links is empty.
 | |
|    * @return Whether the list is empty.
 | |
|    */
 | |
|   isEmpty: function BlockedLinks_isEmpty() {
 | |
|     return !Object.keys(this.links).length;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Resets the links cache.
 | |
|    */
 | |
|   resetCache: function BlockedLinks_resetCache() {
 | |
|     this._links = null;
 | |
|   },
 | |
| 
 | |
|   _callObservers(methodName, ...args) {
 | |
|     for (let obs of this._observers) {
 | |
|       if (typeof obs[methodName] == "function") {
 | |
|         try {
 | |
|           obs[methodName](...args);
 | |
|         } catch (err) {
 | |
|           console.error(err);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Singleton that serves as the default link provider for the grid. It queries
 | |
|  * the history to retrieve the most frequently visited sites.
 | |
|  */
 | |
| var PlacesProvider = {
 | |
|   /**
 | |
|    * Set this to change the maximum number of links the provider will provide.
 | |
|    */
 | |
|   maxNumLinks: HISTORY_RESULTS_LIMIT,
 | |
| 
 | |
|   /**
 | |
|    * Must be called before the provider is used.
 | |
|    */
 | |
|   init: function PlacesProvider_init() {
 | |
|     this._placesObserver = new PlacesWeakCallbackWrapper(
 | |
|       this.handlePlacesEvents.bind(this)
 | |
|     );
 | |
|     PlacesObservers.addListener(
 | |
|       ["page-visited", "page-title-changed", "pages-rank-changed"],
 | |
|       this._placesObserver
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets the current set of links delivered by this provider.
 | |
|    * @param aCallback The function that the array of links is passed to.
 | |
|    */
 | |
|   getLinks: function PlacesProvider_getLinks(aCallback) {
 | |
|     let options = lazy.PlacesUtils.history.getNewQueryOptions();
 | |
|     options.maxResults = this.maxNumLinks;
 | |
| 
 | |
|     // Sort by frecency, descending.
 | |
|     options.sortingMode =
 | |
|       Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING;
 | |
| 
 | |
|     let links = [];
 | |
| 
 | |
|     let callback = {
 | |
|       handleResult(aResultSet) {
 | |
|         let row;
 | |
| 
 | |
|         while ((row = aResultSet.getNextRow())) {
 | |
|           let url = row.getResultByIndex(1);
 | |
|           if (LinkChecker.checkLoadURI(url)) {
 | |
|             let title = row.getResultByIndex(2);
 | |
|             let frecency = row.getResultByIndex(12);
 | |
|             let lastVisitDate = row.getResultByIndex(5);
 | |
|             links.push({
 | |
|               url,
 | |
|               title,
 | |
|               frecency,
 | |
|               lastVisitDate,
 | |
|               type: "history",
 | |
|             });
 | |
|           }
 | |
|         }
 | |
|       },
 | |
| 
 | |
|       handleError(aError) {
 | |
|         // Should we somehow handle this error?
 | |
|         aCallback([]);
 | |
|       },
 | |
| 
 | |
|       handleCompletion(aReason) {
 | |
|         // The Places query breaks ties in frecency by place ID descending, but
 | |
|         // that's different from how Links.compareLinks breaks ties, because
 | |
|         // compareLinks doesn't have access to place IDs.  It's very important
 | |
|         // that the initial list of links is sorted in the same order imposed by
 | |
|         // compareLinks, because Links uses compareLinks to perform binary
 | |
|         // searches on the list.  So, ensure the list is so ordered.
 | |
|         let i = 1;
 | |
|         let outOfOrder = [];
 | |
|         while (i < links.length) {
 | |
|           if (Links.compareLinks(links[i - 1], links[i]) > 0) {
 | |
|             outOfOrder.push(links.splice(i, 1)[0]);
 | |
|           } else {
 | |
|             i++;
 | |
|           }
 | |
|         }
 | |
|         for (let link of outOfOrder) {
 | |
|           i = lazy.BinarySearch.insertionIndexOf(
 | |
|             Links.compareLinks,
 | |
|             links,
 | |
|             link
 | |
|           );
 | |
|           links.splice(i, 0, link);
 | |
|         }
 | |
| 
 | |
|         aCallback(links);
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     // Execute the query.
 | |
|     let query = lazy.PlacesUtils.history.getNewQuery();
 | |
|     lazy.PlacesUtils.history.asyncExecuteLegacyQuery(query, options, callback);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Registers an object that will be notified when the provider's links change.
 | |
|    * @param aObserver An object with the following optional properties:
 | |
|    *        * onLinkChanged: A function that's called when a single link
 | |
|    *          changes.  It's passed the provider and the link object.  Only the
 | |
|    *          link's `url` property is guaranteed to be present.  If its `title`
 | |
|    *          property is present, then its title has changed, and the
 | |
|    *          property's value is the new title.  If any sort properties are
 | |
|    *          present, then its position within the provider's list of links may
 | |
|    *          have changed, and the properties' values are the new sort-related
 | |
|    *          values.  Note that this link may not necessarily have been present
 | |
|    *          in the lists returned from any previous calls to getLinks.
 | |
|    *        * onManyLinksChanged: A function that's called when many links
 | |
|    *          change at once.  It's passed the provider.  You should call
 | |
|    *          getLinks to get the provider's new list of links.
 | |
|    */
 | |
|   addObserver: function PlacesProvider_addObserver(aObserver) {
 | |
|     this._observers.push(aObserver);
 | |
|   },
 | |
| 
 | |
|   _observers: [],
 | |
| 
 | |
|   handlePlacesEvents(aEvents) {
 | |
|     for (let event of aEvents) {
 | |
|       switch (event.type) {
 | |
|         case "page-visited": {
 | |
|           if (event.visitCount == 1 && event.lastKnownTitle) {
 | |
|             this._callObservers("onLinkChanged", {
 | |
|               url: event.url,
 | |
|               title: event.lastKnownTitle,
 | |
|             });
 | |
|           }
 | |
|           break;
 | |
|         }
 | |
|         case "page-title-changed": {
 | |
|           this._callObservers("onLinkChanged", {
 | |
|             url: event.url,
 | |
|             title: event.title,
 | |
|           });
 | |
|           break;
 | |
|         }
 | |
|         case "pages-rank-changed": {
 | |
|           this._callObservers("onManyLinksChanged");
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
 | |
|     for (let obs of this._observers) {
 | |
|       if (obs[aMethodName]) {
 | |
|         try {
 | |
|           obs[aMethodName](this, aArg);
 | |
|         } catch (err) {
 | |
|           console.error(err);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Queries history to retrieve the most frecent sites. Emits events when the
 | |
|  * history changes.
 | |
|  */
 | |
| var ActivityStreamProvider = {
 | |
|   THUMB_FAVICON_SIZE: 96,
 | |
| 
 | |
|   /**
 | |
|    * Shared adjustment for selecting potentially blocked links.
 | |
|    */
 | |
|   _adjustLimitForBlocked({ ignoreBlocked, numItems }) {
 | |
|     // Just use the usual number if blocked links won't be filtered out
 | |
|     if (ignoreBlocked) {
 | |
|       return numItems;
 | |
|     }
 | |
|     // Additionally select the number of blocked links in case they're removed
 | |
|     return Object.keys(BlockedLinks.links).length + numItems;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shared sub-SELECT to get the guid of a bookmark of the current url while
 | |
|    * avoiding LEFT JOINs on moz_bookmarks. This avoids gettings tags. The guid
 | |
|    * could be one of multiple possible guids. Assumes `moz_places h` is in FROM.
 | |
|    */
 | |
|   _commonBookmarkGuidSelect: `(
 | |
|     SELECT guid
 | |
|     FROM moz_bookmarks b
 | |
|     WHERE fk = h.id
 | |
|       AND type = :bookmarkType
 | |
|       AND (
 | |
|         SELECT id
 | |
|         FROM moz_bookmarks p
 | |
|         WHERE p.id = b.parent
 | |
|           AND p.parent <> :tagsFolderId
 | |
|       ) NOTNULL
 | |
|     ) AS bookmarkGuid`,
 | |
| 
 | |
|   /**
 | |
|    * Shared WHERE expression filtering out undesired pages, e.g., hidden,
 | |
|    * unvisited, and non-http/s urls. Assumes moz_places is in FROM / JOIN.
 | |
|    *
 | |
|    * NB: SUBSTR(url) is used even without an index instead of url_hash because
 | |
|    * most desired pages will match http/s, so it will only run on the ~10s of
 | |
|    * rows matched. If url_hash were to be used, it should probably *not* be used
 | |
|    * by the query optimizer as we primarily want it optimized for the other
 | |
|    * conditions, e.g., most frecent first.
 | |
|    */
 | |
|   _commonPlacesWhere: `
 | |
|     AND hidden = 0
 | |
|     AND last_visit_date > 0
 | |
|     AND (SUBSTR(url, 1, 6) == "https:"
 | |
|       OR SUBSTR(url, 1, 5) == "http:")
 | |
|   `,
 | |
| 
 | |
|   /**
 | |
|    * Shared parameters for getting correct bookmarks and LIMITed queries.
 | |
|    */
 | |
|   _getCommonParams(aOptions, aParams = {}) {
 | |
|     return Object.assign(
 | |
|       {
 | |
|         bookmarkType: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK,
 | |
|         limit: this._adjustLimitForBlocked(aOptions),
 | |
|         tagsFolderId: lazy.PlacesUtils.tagsFolderId,
 | |
|       },
 | |
|       aParams
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shared columns for Highlights related queries.
 | |
|    */
 | |
|   _highlightsColumns: [
 | |
|     "bookmarkGuid",
 | |
|     "description",
 | |
|     "guid",
 | |
|     "preview_image_url",
 | |
|     "title",
 | |
|     "url",
 | |
|   ],
 | |
| 
 | |
|   /**
 | |
|    * Shared post-processing of Highlights links.
 | |
|    */
 | |
|   _processHighlights(aLinks, aOptions, aType) {
 | |
|     // Filter out blocked if necessary
 | |
|     if (!aOptions.ignoreBlocked) {
 | |
|       aLinks = aLinks.filter(
 | |
|         link =>
 | |
|           !BlockedLinks.isBlocked(
 | |
|             link.pocket_id ? { url: link.open_url } : link
 | |
|           )
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // Limit the results to the requested number and set a type corresponding to
 | |
|     // which query selected it
 | |
|     return aLinks.slice(0, aOptions.numItems).map(item =>
 | |
|       Object.assign(item, {
 | |
|         type: aType,
 | |
|       })
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * From an Array of links, if favicons are present, convert to data URIs
 | |
|    *
 | |
|    * @param {Array} aLinks
 | |
|    *          an array containing objects with favicon data and mimeTypes
 | |
|    *
 | |
|    * @returns {Array} an array of links with favicons as data uri
 | |
|    */
 | |
|   _faviconBytesToDataURI(aLinks) {
 | |
|     return aLinks.map(link => {
 | |
|       if (link.favicon) {
 | |
|         let encodedData = btoa(String.fromCharCode.apply(null, link.favicon));
 | |
|         link.favicon = `data:${link.mimeType};base64,${encodedData}`;
 | |
|         delete link.mimeType;
 | |
|       }
 | |
| 
 | |
|       if (link.smallFavicon) {
 | |
|         let encodedData = btoa(
 | |
|           String.fromCharCode.apply(null, link.smallFavicon)
 | |
|         );
 | |
|         link.smallFavicon = `data:${link.smallFaviconMimeType};base64,${encodedData}`;
 | |
|         delete link.smallFaviconMimeType;
 | |
|       }
 | |
| 
 | |
|       return link;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get favicon data (and metadata) for a uri. Fetches both the largest favicon
 | |
|    * available, for Activity Stream; and a normal-sized favicon, for the Urlbar.
 | |
|    *
 | |
|    * @param {nsIURI} aUri Page to check for favicon data
 | |
|    * @param {number} preferredFaviconWidth
 | |
|    *   The preferred width of the of the normal-sized favicon in pixels.
 | |
|    * @returns A promise of an object (possibly empty) containing the data.
 | |
|    */
 | |
|   async _loadIcons(aUri, preferredFaviconWidth) {
 | |
|     let iconData = {};
 | |
|     // Fetch the largest icon available.
 | |
|     let faviconData;
 | |
|     try {
 | |
|       faviconData = await lazy.PlacesUtils.promiseFaviconData(
 | |
|         aUri,
 | |
|         this.THUMB_FAVICON_SIZE
 | |
|       );
 | |
|       Object.assign(iconData, {
 | |
|         favicon: faviconData.data,
 | |
|         faviconLength: faviconData.dataLen,
 | |
|         faviconRef: faviconData.uri.ref,
 | |
|         faviconSize: faviconData.size,
 | |
|         mimeType: faviconData.mimeType,
 | |
|       });
 | |
|     } catch (e) {
 | |
|       // Return early because fetching the largest favicon is the primary
 | |
|       // purpose of NewTabUtils.
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     // Also fetch a smaller icon.
 | |
|     try {
 | |
|       faviconData = await lazy.PlacesUtils.promiseFaviconData(
 | |
|         aUri,
 | |
|         preferredFaviconWidth
 | |
|       );
 | |
|       Object.assign(iconData, {
 | |
|         smallFavicon: faviconData.data,
 | |
|         smallFaviconLength: faviconData.dataLen,
 | |
|         smallFaviconRef: faviconData.uri.ref,
 | |
|         smallFaviconSize: faviconData.size,
 | |
|         smallFaviconMimeType: faviconData.mimeType,
 | |
|       });
 | |
|     } catch (e) {
 | |
|       // Do nothing with the error since we still have the large favicon fields.
 | |
|     }
 | |
| 
 | |
|     return iconData;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Computes favicon data for each url in a set of links
 | |
|    *
 | |
|    * @param {Array} links
 | |
|    *          an array containing objects without favicon data or mimeTypes yet
 | |
|    *
 | |
|    * @returns {Promise} Returns a promise with the array of links with the largest
 | |
|    *                    favicon available (as a byte array), mimeType, byte array
 | |
|    *                    length, and favicon size (width)
 | |
|    */
 | |
|   _addFavicons(aLinks) {
 | |
|     let win;
 | |
|     if (BrowserWindowTracker) {
 | |
|       win = BrowserWindowTracker.getTopWindow();
 | |
|     }
 | |
|     // We fetch two copies of a page's favicon: the largest available, for
 | |
|     // Activity Stream; and a smaller size appropriate for the Urlbar.
 | |
|     const preferredFaviconWidth =
 | |
|       DEFAULT_SMALL_FAVICON_WIDTH * (win ? win.devicePixelRatio : 2);
 | |
|     // Each link in the array needs a favicon for it's page - so we fire off a
 | |
|     // promise for each link to compute the favicon data and attach it back to
 | |
|     // the original link object. We must wait until all favicons for the array
 | |
|     // of links are computed before returning
 | |
|     return Promise.all(
 | |
|       aLinks.map(
 | |
|         link =>
 | |
|           // eslint-disable-next-line no-async-promise-executor
 | |
|           new Promise(async resolve => {
 | |
|             // Never add favicon data for pocket items
 | |
|             if (link.type === "pocket") {
 | |
|               resolve(link);
 | |
|               return;
 | |
|             }
 | |
|             let iconData;
 | |
|             try {
 | |
|               let linkUri = Services.io.newURI(link.url);
 | |
|               iconData = await this._loadIcons(linkUri, preferredFaviconWidth);
 | |
| 
 | |
|               // Switch the scheme to try again with the other
 | |
|               if (!iconData) {
 | |
|                 linkUri = linkUri
 | |
|                   .mutate()
 | |
|                   .setScheme(linkUri.scheme === "https" ? "http" : "https")
 | |
|                   .finalize();
 | |
|                 iconData = await this._loadIcons(
 | |
|                   linkUri,
 | |
|                   preferredFaviconWidth
 | |
|                 );
 | |
|               }
 | |
|             } catch (e) {
 | |
|               // We just won't put icon data on the link
 | |
|             }
 | |
| 
 | |
|             // Add the icon data to the link if we have any
 | |
|             resolve(Object.assign(link, iconData));
 | |
|           })
 | |
|       )
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Helper function which makes the call to the Pocket API to fetch the user's
 | |
|    * saved Pocket items.
 | |
|    */
 | |
|   fetchSavedPocketItems(requestData) {
 | |
|     const latestSince =
 | |
|       Services.prefs.getStringPref(PREF_POCKET_LATEST_SINCE, 0) * 1000;
 | |
| 
 | |
|     // Do not fetch Pocket items for users that have been inactive for too long, or are not logged in
 | |
|     if (
 | |
|       !lazy.pktApi.isUserLoggedIn() ||
 | |
|       Date.now() - latestSince > POCKET_INACTIVE_TIME
 | |
|     ) {
 | |
|       return Promise.resolve(null);
 | |
|     }
 | |
| 
 | |
|     return new Promise((resolve, reject) => {
 | |
|       lazy.pktApi.retrieve(requestData, {
 | |
|         success(data) {
 | |
|           resolve(data);
 | |
|         },
 | |
|         error(error) {
 | |
|           reject(error);
 | |
|         },
 | |
|       });
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the most recently Pocket-ed items from a user's Pocket list. See:
 | |
|    * https://getpocket.com/developer/docs/v3/retrieve for details
 | |
|    *
 | |
|    * @param {Object} aOptions
 | |
|    *   {int} numItems: The max number of pocket items to fetch
 | |
|    */
 | |
|   async getRecentlyPocketed(aOptions) {
 | |
|     const pocketSecondsAgo =
 | |
|       Math.floor(Date.now() / 1000) - ACTIVITY_STREAM_DEFAULT_RECENT;
 | |
|     const requestData = {
 | |
|       detailType: "complete",
 | |
|       count: aOptions.numItems,
 | |
|       since: pocketSecondsAgo,
 | |
|     };
 | |
|     let data;
 | |
|     try {
 | |
|       data = await this.fetchSavedPocketItems(requestData);
 | |
|       if (!data) {
 | |
|         return [];
 | |
|       }
 | |
|     } catch (e) {
 | |
|       console.error(e);
 | |
|       return [];
 | |
|     }
 | |
|     /* Extract relevant parts needed to show this card as a highlight:
 | |
|      * url, preview image, title, description, and the unique item_id
 | |
|      * necessary for Pocket to identify the item
 | |
|      */
 | |
|     let items = Object.values(data.list)
 | |
|       // status "0" means not archived or deleted
 | |
|       .filter(item => item.status === "0")
 | |
|       .map(item => ({
 | |
|         date_added: item.time_added * 1000,
 | |
|         description: item.excerpt,
 | |
|         preview_image_url: item.image && item.image.src,
 | |
|         title: item.resolved_title,
 | |
|         url: item.resolved_url,
 | |
|         pocket_id: item.item_id,
 | |
|         open_url: item.open_url,
 | |
|       }));
 | |
| 
 | |
|     // Append the query param to let Pocket know this item came from highlights
 | |
|     for (let item of items) {
 | |
|       let url = new URL(item.open_url);
 | |
|       url.searchParams.append("src", "fx_new_tab");
 | |
|       item.open_url = url.href;
 | |
|     }
 | |
| 
 | |
|     return this._processHighlights(items, aOptions, "pocket");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get most-recently-created visited bookmarks for Activity Stream.
 | |
|    *
 | |
|    * @param {Object} aOptions
 | |
|    *   {num}  bookmarkSecondsAgo: Maximum age of added bookmark.
 | |
|    *   {bool} ignoreBlocked: Do not filter out blocked links.
 | |
|    *   {int}  numItems: Maximum number of items to return.
 | |
|    */
 | |
|   async getRecentBookmarks(aOptions) {
 | |
|     const options = Object.assign(
 | |
|       {
 | |
|         bookmarkSecondsAgo: ACTIVITY_STREAM_DEFAULT_RECENT,
 | |
|         ignoreBlocked: false,
 | |
|         numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
 | |
|       },
 | |
|       aOptions || {}
 | |
|     );
 | |
| 
 | |
|     const sqlQuery = `
 | |
|       SELECT
 | |
|         b.guid AS bookmarkGuid,
 | |
|         description,
 | |
|         h.guid,
 | |
|         preview_image_url,
 | |
|         b.title,
 | |
|         b.dateAdded / 1000 AS date_added,
 | |
|         url
 | |
|       FROM moz_bookmarks b
 | |
|       JOIN moz_bookmarks p
 | |
|         ON p.id = b.parent
 | |
|       JOIN moz_places h
 | |
|         ON h.id = b.fk
 | |
|       WHERE b.dateAdded >= :dateAddedThreshold
 | |
|         AND b.title NOTNULL
 | |
|         AND b.type = :bookmarkType
 | |
|         AND p.parent <> :tagsFolderId
 | |
|         ${this._commonPlacesWhere}
 | |
|       ORDER BY b.dateAdded DESC
 | |
|       LIMIT :limit
 | |
|     `;
 | |
| 
 | |
|     return this._processHighlights(
 | |
|       await this.executePlacesQuery(sqlQuery, {
 | |
|         columns: [...this._highlightsColumns, "date_added"],
 | |
|         params: this._getCommonParams(options, {
 | |
|           dateAddedThreshold:
 | |
|             (Date.now() - options.bookmarkSecondsAgo * 1000) * 1000,
 | |
|         }),
 | |
|       }),
 | |
|       options,
 | |
|       "bookmark"
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get total count of all bookmarks.
 | |
|    * Note: this includes default bookmarks
 | |
|    *
 | |
|    * @return {int} The number bookmarks in the places DB.
 | |
|    */
 | |
|   async getTotalBookmarksCount() {
 | |
|     let sqlQuery = `
 | |
|       SELECT count(*) FROM moz_bookmarks b
 | |
|       JOIN moz_bookmarks t ON t.id = b.parent
 | |
|       AND t.parent <> :tags_folder
 | |
|      WHERE b.type = :type_bookmark
 | |
|     `;
 | |
| 
 | |
|     const result = await this.executePlacesQuery(sqlQuery, {
 | |
|       params: {
 | |
|         tags_folder: lazy.PlacesUtils.tagsFolderId,
 | |
|         type_bookmark: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK,
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     return result[0][0];
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get most-recently-visited history with metadata for Activity Stream.
 | |
|    *
 | |
|    * @param {Object} aOptions
 | |
|    *   {bool} ignoreBlocked: Do not filter out blocked links.
 | |
|    *   {int}  numItems: Maximum number of items to return.
 | |
|    */
 | |
|   async getRecentHistory(aOptions) {
 | |
|     const options = Object.assign(
 | |
|       {
 | |
|         ignoreBlocked: false,
 | |
|         numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
 | |
|       },
 | |
|       aOptions || {}
 | |
|     );
 | |
| 
 | |
|     const sqlQuery = `
 | |
|       SELECT
 | |
|         ${this._commonBookmarkGuidSelect},
 | |
|         description,
 | |
|         guid,
 | |
|         preview_image_url,
 | |
|         title,
 | |
|         url
 | |
|       FROM moz_places h
 | |
|       WHERE description NOTNULL
 | |
|         AND preview_image_url NOTNULL
 | |
|         ${this._commonPlacesWhere}
 | |
|       ORDER BY last_visit_date DESC
 | |
|       LIMIT :limit
 | |
|     `;
 | |
| 
 | |
|     return this._processHighlights(
 | |
|       await this.executePlacesQuery(sqlQuery, {
 | |
|         columns: this._highlightsColumns,
 | |
|         params: this._getCommonParams(options),
 | |
|       }),
 | |
|       options,
 | |
|       "history"
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /*
 | |
|    * Gets the top frecent sites for Activity Stream.
 | |
|    *
 | |
|    * @param {Object} aOptions
 | |
|    *   {bool} ignoreBlocked: Do not filter out blocked links.
 | |
|    *   {int}  numItems: Maximum number of items to return.
 | |
|    *   {int}  topsiteFrecency: Minimum amount of frecency for a site.
 | |
|    *   {bool} onePerDomain: Dedupe the resulting list.
 | |
|    *   {bool} includeFavicon: Include favicons if available.
 | |
|    *   {string} hideWithSearchParam: URLs that contain this search param will be
 | |
|    *     excluded from the returned links. This value should be either undefined
 | |
|    *     or a string with one of the following forms:
 | |
|    *     - undefined: Fall back to the value of pref
 | |
|    *       `browser.newtabpage.activity-stream.hideTopSitesWithSearchParam`
 | |
|    *     - "" (empty) - Disable this feature
 | |
|    *     - "key" - Search param named "key" with any or no value
 | |
|    *     - "key=" - Search param named "key" with no value
 | |
|    *     - "key=value" - Search param named "key" with value "value"
 | |
|    *
 | |
|    * @returns {Promise} Returns a promise with the array of links as payload.
 | |
|    */
 | |
|   async getTopFrecentSites(aOptions) {
 | |
|     const options = Object.assign(
 | |
|       {
 | |
|         ignoreBlocked: false,
 | |
|         numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
 | |
|         topsiteFrecency: ACTIVITY_STREAM_DEFAULT_FRECENCY,
 | |
|         onePerDomain: true,
 | |
|         includeFavicon: true,
 | |
|         hideWithSearchParam: Services.prefs.getCharPref(
 | |
|           "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam",
 | |
|           ""
 | |
|         ),
 | |
|       },
 | |
|       aOptions || {}
 | |
|     );
 | |
| 
 | |
|     // Double the item count in case the host is deduped between with www or
 | |
|     // not-www (i.e., 2 hosts) and an extra buffer for multiple pages per host.
 | |
|     const origNumItems = options.numItems;
 | |
|     if (options.onePerDomain) {
 | |
|       options.numItems *= 2 * 10;
 | |
|     }
 | |
| 
 | |
|     // Keep this query fast with frecency-indexed lookups (even with excess
 | |
|     // rows) and shift the more complex logic to post-processing afterwards
 | |
|     const sqlQuery = `
 | |
|       SELECT
 | |
|         ${this._commonBookmarkGuidSelect},
 | |
|         frecency,
 | |
|         guid,
 | |
|         last_visit_date / 1000 AS lastVisitDate,
 | |
|         rev_host,
 | |
|         title,
 | |
|         url,
 | |
|         "history" as type
 | |
|       FROM moz_places h
 | |
|       WHERE frecency >= :frecencyThreshold
 | |
|         ${this._commonPlacesWhere}
 | |
|       ORDER BY frecency DESC
 | |
|       LIMIT :limit
 | |
|     `;
 | |
| 
 | |
|     let links = await this.executePlacesQuery(sqlQuery, {
 | |
|       columns: [
 | |
|         "bookmarkGuid",
 | |
|         "frecency",
 | |
|         "guid",
 | |
|         "lastVisitDate",
 | |
|         "title",
 | |
|         "url",
 | |
|         "type",
 | |
|       ],
 | |
|       params: this._getCommonParams(options, {
 | |
|         frecencyThreshold: options.topsiteFrecency,
 | |
|       }),
 | |
|     });
 | |
| 
 | |
|     // Determine if the other link is "better" (larger frecency, more recent,
 | |
|     // lexicographically earlier url)
 | |
|     function isOtherBetter(link, other) {
 | |
|       if (other.frecency === link.frecency) {
 | |
|         if (other.lastVisitDate === link.lastVisitDate) {
 | |
|           return other.url < link.url;
 | |
|         }
 | |
|         return other.lastVisitDate > link.lastVisitDate;
 | |
|       }
 | |
|       return other.frecency > link.frecency;
 | |
|     }
 | |
| 
 | |
|     // Update a host Map with the better link
 | |
|     function setBetterLink(map, link, hostMatcher, combiner = () => {}) {
 | |
|       const host = hostMatcher(link.url)[1];
 | |
|       if (map.has(host)) {
 | |
|         const other = map.get(host);
 | |
|         if (isOtherBetter(link, other)) {
 | |
|           link = other;
 | |
|         }
 | |
|         combiner(link, other);
 | |
|       }
 | |
|       map.set(host, link);
 | |
|     }
 | |
| 
 | |
|     // Convert all links that are supposed to be a seach shortcut to its canonical URL
 | |
|     if (
 | |
|       didSuccessfulImport &&
 | |
|       Services.prefs.getBoolPref(
 | |
|         `browser.newtabpage.activity-stream.${searchShortcuts.SEARCH_SHORTCUTS_EXPERIMENT}`
 | |
|       )
 | |
|     ) {
 | |
|       links.forEach(link => {
 | |
|         let searchProvider = searchShortcuts.getSearchProvider(
 | |
|           shortURL.shortURL(link)
 | |
|         );
 | |
|         if (searchProvider) {
 | |
|           link.url = searchProvider.url;
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // Remove links that contain the hide-with search param.
 | |
|     if (options.hideWithSearchParam) {
 | |
|       let [key, value] = options.hideWithSearchParam.split("=");
 | |
|       links = links.filter(link => {
 | |
|         try {
 | |
|           let { searchParams } = new URL(link.url);
 | |
|           return value === undefined
 | |
|             ? !searchParams.has(key)
 | |
|             : !searchParams.getAll(key).includes(value);
 | |
|         } catch (error) {}
 | |
|         return true;
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // Remove any blocked links.
 | |
|     if (!options.ignoreBlocked) {
 | |
|       links = links.filter(link => !BlockedLinks.isBlocked(link));
 | |
|     }
 | |
| 
 | |
|     if (options.onePerDomain) {
 | |
|       // De-dup the links.
 | |
|       const exactHosts = new Map();
 | |
|       for (const link of links) {
 | |
|         // First we want to find the best link for an exact host
 | |
|         setBetterLink(exactHosts, link, url => url.match(/:\/\/([^\/]+)/));
 | |
|       }
 | |
| 
 | |
|       // Clean up exact hosts to dedupe as non-www hosts
 | |
|       const hosts = new Map();
 | |
|       for (const link of exactHosts.values()) {
 | |
|         setBetterLink(
 | |
|           hosts,
 | |
|           link,
 | |
|           url => url.match(/:\/\/(?:www\.)?([^\/]+)/),
 | |
|           // Combine frecencies when deduping these links
 | |
|           (targetLink, otherLink) => {
 | |
|             targetLink.frecency = link.frecency + otherLink.frecency;
 | |
|           }
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       links = [...hosts.values()];
 | |
|     }
 | |
|     // Pick out the top links using the same comparer as before
 | |
|     links = links.sort(isOtherBetter).slice(0, origNumItems);
 | |
| 
 | |
|     if (!options.includeFavicon) {
 | |
|       return links;
 | |
|     }
 | |
|     // Get the favicons as data URI for now (until we use the favicon protocol)
 | |
|     return this._faviconBytesToDataURI(await this._addFavicons(links));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets a specific bookmark given some info about it
 | |
|    *
 | |
|    * @param {Obj} aInfo
 | |
|    *          An object with one and only one of the following properties:
 | |
|    *            - url
 | |
|    *            - guid
 | |
|    *            - parentGuid and index
 | |
|    */
 | |
|   async getBookmark(aInfo) {
 | |
|     let bookmark = await lazy.PlacesUtils.bookmarks.fetch(aInfo);
 | |
|     if (!bookmark) {
 | |
|       return null;
 | |
|     }
 | |
|     let result = {};
 | |
|     result.bookmarkGuid = bookmark.guid;
 | |
|     result.bookmarkTitle = bookmark.title;
 | |
|     result.lastModified = bookmark.lastModified.getTime();
 | |
|     result.url = bookmark.url.href;
 | |
|     return result;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Count the number of visited urls grouped by day
 | |
|    */
 | |
|   getUserMonthlyActivity() {
 | |
|     let sqlQuery = `
 | |
|       SELECT count(*),
 | |
|         strftime('%Y-%m-%d', visit_date/1000000.0, 'unixepoch', 'localtime') as date_format
 | |
|       FROM moz_historyvisits
 | |
|       WHERE visit_date > 0
 | |
|       AND visit_date > strftime('%s','now','localtime','start of day','-27 days','utc') * 1000000
 | |
|       GROUP BY date_format
 | |
|       ORDER BY date_format ASC
 | |
|     `;
 | |
| 
 | |
|     return this.executePlacesQuery(sqlQuery);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Executes arbitrary query against places database
 | |
|    *
 | |
|    * @param {String} aQuery
 | |
|    *        SQL query to execute
 | |
|    * @param {Object} [optional] aOptions
 | |
|    *          aOptions.columns - an array of column names. if supplied the return
 | |
|    *          items will consists of objects keyed on column names. Otherwise
 | |
|    *          array of raw values is returned in the select order
 | |
|    *          aOptions.param - an object of SQL binding parameters
 | |
|    *
 | |
|    * @returns {Promise} Returns a promise with the array of retrieved items
 | |
|    */
 | |
|   async executePlacesQuery(aQuery, aOptions = {}) {
 | |
|     let { columns, params } = aOptions;
 | |
|     let items = [];
 | |
|     let queryError = null;
 | |
|     let conn = await lazy.PlacesUtils.promiseDBConnection();
 | |
|     await conn.executeCached(aQuery, params, (aRow, aCancel) => {
 | |
|       try {
 | |
|         let item = null;
 | |
|         // if columns array is given construct an object
 | |
|         if (columns && Array.isArray(columns)) {
 | |
|           item = {};
 | |
|           columns.forEach(column => {
 | |
|             item[column] = aRow.getResultByName(column);
 | |
|           });
 | |
|         } else {
 | |
|           // if no columns - make an array of raw values
 | |
|           item = [];
 | |
|           for (let i = 0; i < aRow.numEntries; i++) {
 | |
|             item.push(aRow.getResultByIndex(i));
 | |
|           }
 | |
|         }
 | |
|         items.push(item);
 | |
|       } catch (e) {
 | |
|         queryError = e;
 | |
|         aCancel();
 | |
|       }
 | |
|     });
 | |
|     if (queryError) {
 | |
|       throw new Error(queryError);
 | |
|     }
 | |
|     return items;
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * A set of actions which influence what sites shown on the Activity Stream page
 | |
|  */
 | |
| var ActivityStreamLinks = {
 | |
|   _savedPocketStories: null,
 | |
|   _pocketLastUpdated: 0,
 | |
|   _pocketLastLatest: 0,
 | |
| 
 | |
|   /**
 | |
|    * Block a url
 | |
|    *
 | |
|    * @param {Object} aLink
 | |
|    *          The link which contains a URL to add to the block list
 | |
|    */
 | |
|   blockURL(aLink) {
 | |
|     BlockedLinks.block(aLink);
 | |
|     // If we're blocking a pocket item, invalidate the cache too
 | |
|     if (aLink.pocket_id) {
 | |
|       this._savedPocketStories = null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onLinkBlocked(aLink) {
 | |
|     Services.obs.notifyObservers(null, "newtab-linkBlocked", aLink.url);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds a bookmark and opens up the Bookmark Dialog to show feedback that
 | |
|    * the bookmarking action has been successful
 | |
|    *
 | |
|    * @param {Object} aData
 | |
|    *          aData.url The url to bookmark
 | |
|    *          aData.title The title of the page to bookmark
 | |
|    * @param {Window} aBrowserWindow
 | |
|    *          The current browser chrome window
 | |
|    *
 | |
|    * @returns {Promise} Returns a promise set to an object representing the bookmark
 | |
|    */
 | |
|   addBookmark(aData, aBrowserWindow) {
 | |
|     const { url, title } = aData;
 | |
|     return aBrowserWindow.PlacesCommandHook.bookmarkLink(url, title);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes a bookmark
 | |
|    *
 | |
|    * @param {String} aBookmarkGuid
 | |
|    *          The bookmark guid associated with the bookmark to remove
 | |
|    *
 | |
|    * @returns {Promise} Returns a promise at completion.
 | |
|    */
 | |
|   deleteBookmark(aBookmarkGuid) {
 | |
|     return lazy.PlacesUtils.bookmarks.remove(aBookmarkGuid);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes a history link and unpins the URL if previously pinned
 | |
|    *
 | |
|    * @param {String} aUrl
 | |
|    *           The url to be removed from history
 | |
|    *
 | |
|    * @returns {Promise} Returns a promise set to true if link was removed
 | |
|    */
 | |
|   deleteHistoryEntry(aUrl) {
 | |
|     const url = aUrl;
 | |
|     PinnedLinks.unpin({ url });
 | |
|     return lazy.PlacesUtils.history.remove(url);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Helper function which makes the call to the Pocket API to delete an item from
 | |
|    * a user's saved to Pocket feed. Also, invalidate the Pocket stories cache
 | |
|    *
 | |
|    * @param {Integer} aItemID
 | |
|    *           The unique pocket ID used to find the item to be deleted
 | |
|    *
 | |
|    *@returns {Promise} Returns a promise at completion
 | |
|    */
 | |
|   deletePocketEntry(aItemID) {
 | |
|     this._savedPocketStories = null;
 | |
|     return new Promise((success, error) =>
 | |
|       lazy.pktApi.deleteItem(aItemID, { success, error })
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Helper function which makes the call to the Pocket API to archive an item from
 | |
|    * a user's saved to Pocket feed. Also, invalidate the Pocket stories cache
 | |
|    *
 | |
|    * @param {Integer} aItemID
 | |
|    *           The unique pocket ID used to find the item to be archived
 | |
|    *
 | |
|    *@returns {Promise} Returns a promise at completion
 | |
|    */
 | |
|   archivePocketEntry(aItemID) {
 | |
|     this._savedPocketStories = null;
 | |
|     return new Promise((success, error) =>
 | |
|       lazy.pktApi.archiveItem(aItemID, { success, error })
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Helper function which makes the call to the Pocket API to save an item to
 | |
|    * a user's saved to Pocket feed if they are logged in. Also, invalidate the
 | |
|    * Pocket stories cache
 | |
|    *
 | |
|    * @param {String} aUrl
 | |
|    *           The URL belonging to the story being saved
 | |
|    * @param {String} aTitle
 | |
|    *           The title belonging to the story being saved
 | |
|    * @param {Browser} aBrowser
 | |
|    *           The target browser to show the doorhanger in
 | |
|    *
 | |
|    *@returns {Promise} Returns a promise at completion
 | |
|    */
 | |
|   addPocketEntry(aUrl, aTitle, aBrowser) {
 | |
|     // If the user is not logged in, show the panel to prompt them to log in
 | |
|     if (!lazy.pktApi.isUserLoggedIn()) {
 | |
|       lazy.Pocket.savePage(aBrowser, aUrl, aTitle);
 | |
|       return Promise.resolve(null);
 | |
|     }
 | |
| 
 | |
|     // If the user is logged in, just save the link to Pocket and Activity Stream
 | |
|     // will update the page
 | |
|     this._savedPocketStories = null;
 | |
|     return new Promise((success, error) => {
 | |
|       lazy.pktApi.addLink(aUrl, {
 | |
|         title: aTitle,
 | |
|         success,
 | |
|         error,
 | |
|       });
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the Highlights links to show on Activity Stream
 | |
|    *
 | |
|    * @param {Object} aOptions
 | |
|    *   {bool} excludeBookmarks: Don't add bookmark items.
 | |
|    *   {bool} excludeHistory: Don't add history items.
 | |
|    *   {bool} excludePocket: Don't add Pocket items.
 | |
|    *   {bool} withFavicons: Add favicon data: URIs, when possible.
 | |
|    *   {int}  numItems: Maximum number of (bookmark or history) items to return.
 | |
|    *
 | |
|    * @return {Promise} Returns a promise with the array of links as the payload
 | |
|    */
 | |
|   async getHighlights(aOptions = {}) {
 | |
|     aOptions.numItems = aOptions.numItems || ACTIVITY_STREAM_DEFAULT_LIMIT;
 | |
|     const results = [];
 | |
| 
 | |
|     // First get bookmarks if we want them
 | |
|     if (!aOptions.excludeBookmarks) {
 | |
|       results.push(
 | |
|         ...(await ActivityStreamProvider.getRecentBookmarks(aOptions))
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // Add the Pocket items if we need more and want them
 | |
|     if (aOptions.numItems - results.length > 0 && !aOptions.excludePocket) {
 | |
|       const latestSince = ~~Services.prefs.getStringPref(
 | |
|         PREF_POCKET_LATEST_SINCE,
 | |
|         0
 | |
|       );
 | |
|       // Invalidate the cache, get new stories, and update timestamps if:
 | |
|       //  1. we do not have saved to Pocket stories already cached OR
 | |
|       //  2. it has been too long since we last got Pocket stories OR
 | |
|       //  3. there has been a paged saved to pocket since we last got new stories
 | |
|       if (
 | |
|         !this._savedPocketStories ||
 | |
|         Date.now() - this._pocketLastUpdated > POCKET_UPDATE_TIME ||
 | |
|         this._pocketLastLatest < latestSince
 | |
|       ) {
 | |
|         this._savedPocketStories =
 | |
|           await ActivityStreamProvider.getRecentlyPocketed(aOptions);
 | |
|         this._pocketLastUpdated = Date.now();
 | |
|         this._pocketLastLatest = latestSince;
 | |
|       }
 | |
|       results.push(...this._savedPocketStories);
 | |
|     }
 | |
| 
 | |
|     // Add in history if we need more and want them
 | |
|     if (aOptions.numItems - results.length > 0 && !aOptions.excludeHistory) {
 | |
|       // Use the same numItems as bookmarks above in case we remove duplicates
 | |
|       const history = await ActivityStreamProvider.getRecentHistory(aOptions);
 | |
| 
 | |
|       // Only include a url once in the result preferring the bookmark
 | |
|       const bookmarkUrls = new Set(results.map(({ url }) => url));
 | |
|       for (const page of history) {
 | |
|         if (!bookmarkUrls.has(page.url)) {
 | |
|           results.push(page);
 | |
| 
 | |
|           // Stop adding pages once we reach the desired maximum
 | |
|           if (results.length === aOptions.numItems) {
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (aOptions.withFavicons) {
 | |
|       return ActivityStreamProvider._faviconBytesToDataURI(
 | |
|         await ActivityStreamProvider._addFavicons(results)
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return results;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the top sites to show on Activity Stream
 | |
|    *
 | |
|    * @return {Promise} Returns a promise with the array of links as the payload
 | |
|    */
 | |
|   async getTopSites(aOptions = {}) {
 | |
|     return ActivityStreamProvider.getTopFrecentSites(aOptions);
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Singleton that provides access to all links contained in the grid (including
 | |
|  * the ones that don't fit on the grid). A link is a plain object that looks
 | |
|  * like this:
 | |
|  *
 | |
|  * {
 | |
|  *   url: "http://www.mozilla.org/",
 | |
|  *   title: "Mozilla",
 | |
|  *   frecency: 1337,
 | |
|  *   lastVisitDate: 1394678824766431,
 | |
|  * }
 | |
|  */
 | |
| var Links = {
 | |
|   /**
 | |
|    * The maximum number of links returned by getLinks.
 | |
|    */
 | |
|   maxNumLinks: LINKS_GET_LINKS_LIMIT,
 | |
| 
 | |
|   /**
 | |
|    * A mapping from each provider to an object { sortedLinks, siteMap, linkMap }.
 | |
|    * sortedLinks is the cached, sorted array of links for the provider.
 | |
|    * siteMap is a mapping from base domains to URL count associated with the domain.
 | |
|    *         The count does not include blocked URLs. siteMap is used to look up a
 | |
|    *         user's top sites that can be targeted with a suggested tile.
 | |
|    * linkMap is a Map from link URLs to link objects.
 | |
|    */
 | |
|   _providers: new Map(),
 | |
| 
 | |
|   /**
 | |
|    * The properties of link objects used to sort them.
 | |
|    */
 | |
|   _sortProperties: ["frecency", "lastVisitDate", "url"],
 | |
| 
 | |
|   /**
 | |
|    * List of callbacks waiting for the cache to be populated.
 | |
|    */
 | |
|   _populateCallbacks: [],
 | |
| 
 | |
|   /**
 | |
|    * A list of objects that are observing links updates.
 | |
|    */
 | |
|   _observers: [],
 | |
| 
 | |
|   /**
 | |
|    * Registers an object that will be notified when links updates.
 | |
|    */
 | |
|   addObserver(aObserver) {
 | |
|     this._observers.push(aObserver);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds a link provider.
 | |
|    * @param aProvider The link provider.
 | |
|    */
 | |
|   addProvider: function Links_addProvider(aProvider) {
 | |
|     this._providers.set(aProvider, null);
 | |
|     aProvider.addObserver(this);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes a link provider.
 | |
|    * @param aProvider The link provider.
 | |
|    */
 | |
|   removeProvider: function Links_removeProvider(aProvider) {
 | |
|     if (!this._providers.delete(aProvider)) {
 | |
|       throw new Error("Unknown provider");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Populates the cache with fresh links from the providers.
 | |
|    * @param aCallback The callback to call when finished (optional).
 | |
|    * @param aForce When true, populates the cache even when it's already filled.
 | |
|    */
 | |
|   populateCache: function Links_populateCache(aCallback, aForce) {
 | |
|     let callbacks = this._populateCallbacks;
 | |
| 
 | |
|     // Enqueue the current callback.
 | |
|     callbacks.push(aCallback);
 | |
| 
 | |
|     // There was a callback waiting already, thus the cache has not yet been
 | |
|     // populated.
 | |
|     if (callbacks.length > 1) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     function executeCallbacks() {
 | |
|       while (callbacks.length) {
 | |
|         let callback = callbacks.shift();
 | |
|         if (callback) {
 | |
|           try {
 | |
|             callback();
 | |
|           } catch (e) {
 | |
|             // We want to proceed even if a callback fails.
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let numProvidersRemaining = this._providers.size;
 | |
|     for (let [provider /* , links */] of this._providers) {
 | |
|       this._populateProviderCache(
 | |
|         provider,
 | |
|         () => {
 | |
|           if (--numProvidersRemaining == 0) {
 | |
|             executeCallbacks();
 | |
|           }
 | |
|         },
 | |
|         aForce
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this._addObserver();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets the current set of links contained in the grid.
 | |
|    * @return The links in the grid.
 | |
|    */
 | |
|   getLinks: function Links_getLinks() {
 | |
|     let pinnedLinks = Array.from(PinnedLinks.links);
 | |
|     let links = this._getMergedProviderLinks();
 | |
| 
 | |
|     let sites = new Set();
 | |
|     for (let link of pinnedLinks) {
 | |
|       if (link) {
 | |
|         sites.add(NewTabUtils.extractSite(link.url));
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Filter blocked and pinned links and duplicate base domains.
 | |
|     links = links.filter(function (link) {
 | |
|       let site = NewTabUtils.extractSite(link.url);
 | |
|       if (site == null || sites.has(site)) {
 | |
|         return false;
 | |
|       }
 | |
|       sites.add(site);
 | |
| 
 | |
|       return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
 | |
|     });
 | |
| 
 | |
|     // Try to fill the gaps between pinned links.
 | |
|     for (let i = 0; i < pinnedLinks.length && links.length; i++) {
 | |
|       if (!pinnedLinks[i]) {
 | |
|         pinnedLinks[i] = links.shift();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Append the remaining links if any.
 | |
|     if (links.length) {
 | |
|       pinnedLinks = pinnedLinks.concat(links);
 | |
|     }
 | |
| 
 | |
|     for (let link of pinnedLinks) {
 | |
|       if (link) {
 | |
|         link.baseDomain = NewTabUtils.extractSite(link.url);
 | |
|       }
 | |
|     }
 | |
|     return pinnedLinks;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Resets the links cache.
 | |
|    */
 | |
|   resetCache: function Links_resetCache() {
 | |
|     for (let provider of this._providers.keys()) {
 | |
|       this._providers.set(provider, null);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Compares two links.
 | |
|    * @param aLink1 The first link.
 | |
|    * @param aLink2 The second link.
 | |
|    * @return A negative number if aLink1 is ordered before aLink2, zero if
 | |
|    *         aLink1 and aLink2 have the same ordering, or a positive number if
 | |
|    *         aLink1 is ordered after aLink2.
 | |
|    *
 | |
|    * @note compareLinks's this object is bound to Links below.
 | |
|    */
 | |
|   compareLinks: function Links_compareLinks(aLink1, aLink2) {
 | |
|     for (let prop of this._sortProperties) {
 | |
|       if (!(prop in aLink1) || !(prop in aLink2)) {
 | |
|         throw new Error("Comparable link missing required property: " + prop);
 | |
|       }
 | |
|     }
 | |
|     return (
 | |
|       aLink2.frecency - aLink1.frecency ||
 | |
|       aLink2.lastVisitDate - aLink1.lastVisitDate ||
 | |
|       aLink1.url.localeCompare(aLink2.url)
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   _incrementSiteMap(map, link) {
 | |
|     if (NewTabUtils.blockedLinks.isBlocked(link)) {
 | |
|       // Don't count blocked URLs.
 | |
|       return;
 | |
|     }
 | |
|     let site = NewTabUtils.extractSite(link.url);
 | |
|     map.set(site, (map.get(site) || 0) + 1);
 | |
|   },
 | |
| 
 | |
|   _decrementSiteMap(map, link) {
 | |
|     if (NewTabUtils.blockedLinks.isBlocked(link)) {
 | |
|       // Blocked URLs are not included in map.
 | |
|       return;
 | |
|     }
 | |
|     let site = NewTabUtils.extractSite(link.url);
 | |
|     let previousURLCount = map.get(site);
 | |
|     if (previousURLCount === 1) {
 | |
|       map.delete(site);
 | |
|     } else {
 | |
|       map.set(site, previousURLCount - 1);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update the siteMap cache based on the link given and whether we need
 | |
|    * to increment or decrement it. We do this by iterating over all stored providers
 | |
|    * to find which provider this link already exists in. For providers that
 | |
|    * have this link, we will adjust siteMap for them accordingly.
 | |
|    *
 | |
|    * @param aLink The link that will affect siteMap
 | |
|    * @param increment A boolean for whether to increment or decrement siteMap
 | |
|    */
 | |
|   _adjustSiteMapAndNotify(aLink, increment = true) {
 | |
|     for (let [, /* provider */ cache] of this._providers) {
 | |
|       // We only update siteMap if aLink is already stored in linkMap.
 | |
|       if (cache.linkMap.get(aLink.url)) {
 | |
|         if (increment) {
 | |
|           this._incrementSiteMap(cache.siteMap, aLink);
 | |
|           continue;
 | |
|         }
 | |
|         this._decrementSiteMap(cache.siteMap, aLink);
 | |
|       }
 | |
|     }
 | |
|     this._callObservers("onLinkChanged", aLink);
 | |
|   },
 | |
| 
 | |
|   onLinkBlocked(aLink) {
 | |
|     this._adjustSiteMapAndNotify(aLink, false);
 | |
|   },
 | |
| 
 | |
|   onLinkUnblocked(aLink) {
 | |
|     this._adjustSiteMapAndNotify(aLink);
 | |
|   },
 | |
| 
 | |
|   populateProviderCache(provider, callback) {
 | |
|     if (!this._providers.has(provider)) {
 | |
|       throw new Error(
 | |
|         "Can only populate provider cache for existing provider."
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return this._populateProviderCache(provider, callback, false);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Calls getLinks on the given provider and populates our cache for it.
 | |
|    * @param aProvider The provider whose cache will be populated.
 | |
|    * @param aCallback The callback to call when finished.
 | |
|    * @param aForce When true, populates the provider's cache even when it's
 | |
|    *               already filled.
 | |
|    */
 | |
|   _populateProviderCache(aProvider, aCallback, aForce) {
 | |
|     let cache = this._providers.get(aProvider);
 | |
|     let createCache = !cache;
 | |
|     if (createCache) {
 | |
|       cache = {
 | |
|         // Start with a resolved promise.
 | |
|         populatePromise: new Promise(resolve => resolve()),
 | |
|       };
 | |
|       this._providers.set(aProvider, cache);
 | |
|     }
 | |
|     // Chain the populatePromise so that calls are effectively queued.
 | |
|     cache.populatePromise = cache.populatePromise.then(() => {
 | |
|       return new Promise(resolve => {
 | |
|         if (!createCache && !aForce) {
 | |
|           aCallback();
 | |
|           resolve();
 | |
|           return;
 | |
|         }
 | |
|         aProvider.getLinks(links => {
 | |
|           // Filter out null and undefined links so we don't have to deal with
 | |
|           // them in getLinks when merging links from providers.
 | |
|           links = links.filter(link => !!link);
 | |
|           cache.sortedLinks = links;
 | |
|           cache.siteMap = links.reduce((map, link) => {
 | |
|             this._incrementSiteMap(map, link);
 | |
|             return map;
 | |
|           }, new Map());
 | |
|           cache.linkMap = links.reduce((map, link) => {
 | |
|             map.set(link.url, link);
 | |
|             return map;
 | |
|           }, new Map());
 | |
|           aCallback();
 | |
|           resolve();
 | |
|         });
 | |
|       });
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Merges the cached lists of links from all providers whose lists are cached.
 | |
|    * @return The merged list.
 | |
|    */
 | |
|   _getMergedProviderLinks: function Links__getMergedProviderLinks() {
 | |
|     // Build a list containing a copy of each provider's sortedLinks list.
 | |
|     let linkLists = [];
 | |
|     for (let provider of this._providers.keys()) {
 | |
|       let links = this._providers.get(provider);
 | |
|       if (links && links.sortedLinks) {
 | |
|         linkLists.push(links.sortedLinks.slice());
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return this.mergeLinkLists(linkLists);
 | |
|   },
 | |
| 
 | |
|   mergeLinkLists: function Links_mergeLinkLists(linkLists) {
 | |
|     if (linkLists.length == 1) {
 | |
|       return linkLists[0];
 | |
|     }
 | |
| 
 | |
|     function getNextLink() {
 | |
|       let minLinks = null;
 | |
|       for (let links of linkLists) {
 | |
|         if (
 | |
|           links.length &&
 | |
|           (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0)
 | |
|         ) {
 | |
|           minLinks = links;
 | |
|         }
 | |
|       }
 | |
|       return minLinks ? minLinks.shift() : null;
 | |
|     }
 | |
| 
 | |
|     let finalLinks = [];
 | |
|     for (
 | |
|       let nextLink = getNextLink();
 | |
|       nextLink && finalLinks.length < this.maxNumLinks;
 | |
|       nextLink = getNextLink()
 | |
|     ) {
 | |
|       finalLinks.push(nextLink);
 | |
|     }
 | |
| 
 | |
|     return finalLinks;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called by a provider to notify us when a single link changes.
 | |
|    * @param aProvider The provider whose link changed.
 | |
|    * @param aLink The link that changed.  If the link is new, it must have all
 | |
|    *              of the _sortProperties.  Otherwise, it may have as few or as
 | |
|    *              many as is convenient.
 | |
|    * @param aIndex The current index of the changed link in the sortedLinks
 | |
|                    cache in _providers. Defaults to -1 if the provider doesn't know the index
 | |
|    * @param aDeleted Boolean indicating if the provider has deleted the link.
 | |
|    */
 | |
|   onLinkChanged: function Links_onLinkChanged(
 | |
|     aProvider,
 | |
|     aLink,
 | |
|     aIndex = -1,
 | |
|     aDeleted = false
 | |
|   ) {
 | |
|     if (!("url" in aLink)) {
 | |
|       throw new Error("Changed links must have a url property");
 | |
|     }
 | |
| 
 | |
|     let links = this._providers.get(aProvider);
 | |
|     if (!links) {
 | |
|       // This is not an error, it just means that between the time the provider
 | |
|       // was added and the future time we call getLinks on it, it notified us of
 | |
|       // a change.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let { sortedLinks, siteMap, linkMap } = links;
 | |
|     let existingLink = linkMap.get(aLink.url);
 | |
|     let insertionLink = null;
 | |
|     let updatePages = false;
 | |
| 
 | |
|     if (existingLink) {
 | |
|       // Update our copy's position in O(lg n) by first removing it from its
 | |
|       // list.  It's important to do this before modifying its properties.
 | |
|       if (this._sortProperties.some(prop => prop in aLink)) {
 | |
|         let idx = aIndex;
 | |
|         if (idx < 0) {
 | |
|           idx = this._indexOf(sortedLinks, existingLink);
 | |
|         } else if (this.compareLinks(aLink, sortedLinks[idx]) != 0) {
 | |
|           throw new Error("aLink should be the same as sortedLinks[idx]");
 | |
|         }
 | |
| 
 | |
|         if (idx < 0) {
 | |
|           throw new Error("Link should be in _sortedLinks if in _linkMap");
 | |
|         }
 | |
|         sortedLinks.splice(idx, 1);
 | |
| 
 | |
|         if (aDeleted) {
 | |
|           updatePages = true;
 | |
|           linkMap.delete(existingLink.url);
 | |
|           this._decrementSiteMap(siteMap, existingLink);
 | |
|         } else {
 | |
|           // Update our copy's properties.
 | |
|           Object.assign(existingLink, aLink);
 | |
| 
 | |
|           // Finally, reinsert our copy below.
 | |
|           insertionLink = existingLink;
 | |
|         }
 | |
|       }
 | |
|       // Update our copy's title in O(1).
 | |
|       if ("title" in aLink && aLink.title != existingLink.title) {
 | |
|         existingLink.title = aLink.title;
 | |
|         updatePages = true;
 | |
|       }
 | |
|     } else if (this._sortProperties.every(prop => prop in aLink)) {
 | |
|       // Before doing the O(lg n) insertion below, do an O(1) check for the
 | |
|       // common case where the new link is too low-ranked to be in the list.
 | |
|       if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
 | |
|         let lastLink = sortedLinks[sortedLinks.length - 1];
 | |
|         if (this.compareLinks(lastLink, aLink) < 0) {
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
|       // Copy the link object so that changes later made to it by the caller
 | |
|       // don't affect our copy.
 | |
|       insertionLink = {};
 | |
|       for (let prop in aLink) {
 | |
|         insertionLink[prop] = aLink[prop];
 | |
|       }
 | |
|       linkMap.set(aLink.url, insertionLink);
 | |
|       this._incrementSiteMap(siteMap, aLink);
 | |
|     }
 | |
| 
 | |
|     if (insertionLink) {
 | |
|       let idx = this._insertionIndexOf(sortedLinks, insertionLink);
 | |
|       sortedLinks.splice(idx, 0, insertionLink);
 | |
|       if (sortedLinks.length > aProvider.maxNumLinks) {
 | |
|         let lastLink = sortedLinks.pop();
 | |
|         linkMap.delete(lastLink.url);
 | |
|         this._decrementSiteMap(siteMap, lastLink);
 | |
|       }
 | |
|       updatePages = true;
 | |
|     }
 | |
| 
 | |
|     if (updatePages) {
 | |
|       AllPages.update(null, "links-changed");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called by a provider to notify us when many links change.
 | |
|    */
 | |
|   onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
 | |
|     this._populateProviderCache(
 | |
|       aProvider,
 | |
|       () => {
 | |
|         AllPages.update(null, "links-changed");
 | |
|       },
 | |
|       true
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   _indexOf: function Links__indexOf(aArray, aLink) {
 | |
|     return this._binsearch(aArray, aLink, "indexOf");
 | |
|   },
 | |
| 
 | |
|   _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
 | |
|     return this._binsearch(aArray, aLink, "insertionIndexOf");
 | |
|   },
 | |
| 
 | |
|   _binsearch: function Links__binsearch(aArray, aLink, aMethod) {
 | |
|     return lazy.BinarySearch[aMethod](this.compareLinks, aArray, aLink);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Implements the nsIObserver interface to get notified about browser history
 | |
|    * sanitization.
 | |
|    */
 | |
|   observe: function Links_observe(aSubject, aTopic, aData) {
 | |
|     // Make sure to update open about:newtab instances. If there are no opened
 | |
|     // pages we can just wait for the next new tab to populate the cache again.
 | |
|     if (AllPages.length && AllPages.enabled) {
 | |
|       this.populateCache(function () {
 | |
|         AllPages.update();
 | |
|       }, true);
 | |
|     } else {
 | |
|       this.resetCache();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _callObservers(methodName, ...args) {
 | |
|     for (let obs of this._observers) {
 | |
|       if (typeof obs[methodName] == "function") {
 | |
|         try {
 | |
|           obs[methodName](this, ...args);
 | |
|         } catch (err) {
 | |
|           console.error(err);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Adds a sanitization observer and turns itself into a no-op after the first
 | |
|    * invokation.
 | |
|    */
 | |
|   _addObserver: function Links_addObserver() {
 | |
|     Services.obs.addObserver(this, "browser:purge-session-history", true);
 | |
|     this._addObserver = function () {};
 | |
|   },
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI([
 | |
|     "nsIObserver",
 | |
|     "nsISupportsWeakReference",
 | |
|   ]),
 | |
| };
 | |
| 
 | |
| Links.compareLinks = Links.compareLinks.bind(Links);
 | |
| 
 | |
| /**
 | |
|  * Singleton used to collect telemetry data.
 | |
|  *
 | |
|  */
 | |
| var Telemetry = {
 | |
|   /**
 | |
|    * Initializes object.
 | |
|    */
 | |
|   init: function Telemetry_init() {
 | |
|     Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY);
 | |
|   },
 | |
| 
 | |
|   uninit: function Telemetry_uninit() {
 | |
|     Services.obs.removeObserver(this, TOPIC_GATHER_TELEMETRY);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Collects data.
 | |
|    */
 | |
|   _collect: function Telemetry_collect() {
 | |
|     let probes = [
 | |
|       { histogram: "NEWTAB_PAGE_ENABLED", value: AllPages.enabled },
 | |
|       {
 | |
|         histogram: "NEWTAB_PAGE_PINNED_SITES_COUNT",
 | |
|         value: PinnedLinks.links.length,
 | |
|       },
 | |
|       {
 | |
|         histogram: "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
 | |
|         value: Object.keys(BlockedLinks.links).length,
 | |
|       },
 | |
|     ];
 | |
| 
 | |
|     probes.forEach(function Telemetry_collect_forEach(aProbe) {
 | |
|       Services.telemetry.getHistogramById(aProbe.histogram).add(aProbe.value);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Listens for gather telemetry topic.
 | |
|    */
 | |
|   observe: function Telemetry_observe(aSubject, aTopic, aData) {
 | |
|     this._collect();
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Singleton that checks if a given link should be displayed on about:newtab
 | |
|  * or if we should rather not do it for security reasons. URIs that inherit
 | |
|  * their caller's principal will be filtered.
 | |
|  */
 | |
| var LinkChecker = {
 | |
|   _cache: {},
 | |
| 
 | |
|   get flags() {
 | |
|     return (
 | |
|       Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
 | |
|       Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
 | |
|     if (!(aURI in this._cache)) {
 | |
|       this._cache[aURI] = this._doCheckLoadURI(aURI);
 | |
|     }
 | |
| 
 | |
|     return this._cache[aURI];
 | |
|   },
 | |
| 
 | |
|   _doCheckLoadURI: function Links_doCheckLoadURI(aURI) {
 | |
|     try {
 | |
|       // about:newtab is currently privileged. In any case, it should be
 | |
|       // possible for tiles to point to pretty much everything - but not
 | |
|       // to stuff that inherits the system principal, so we check:
 | |
|       let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
 | |
|       Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
 | |
|         systemPrincipal,
 | |
|         aURI,
 | |
|         this.flags
 | |
|       );
 | |
|       return true;
 | |
|     } catch (e) {
 | |
|       // We got a weird URI or one that would inherit the caller's principal.
 | |
|       return false;
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| var ExpirationFilter = {
 | |
|   init: function ExpirationFilter_init() {
 | |
|     lazy.PageThumbs.addExpirationFilter(this);
 | |
|   },
 | |
| 
 | |
|   filterForThumbnailExpiration:
 | |
|     function ExpirationFilter_filterForThumbnailExpiration(aCallback) {
 | |
|       if (!AllPages.enabled) {
 | |
|         aCallback([]);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       Links.populateCache(function () {
 | |
|         let urls = [];
 | |
| 
 | |
|         // Add all URLs to the list that we want to keep thumbnails for.
 | |
|         for (let link of Links.getLinks().slice(0, 25)) {
 | |
|           if (link && link.url) {
 | |
|             urls.push(link.url);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         aCallback(urls);
 | |
|       });
 | |
|     },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Singleton that provides the public API of this JSM.
 | |
|  */
 | |
| export var NewTabUtils = {
 | |
|   _initialized: false,
 | |
| 
 | |
|   /**
 | |
|    * Extract a "site" from a url in a way that multiple urls of a "site" returns
 | |
|    * the same "site."
 | |
|    * @param aUrl Url spec string
 | |
|    * @return The "site" string or null
 | |
|    */
 | |
|   extractSite: function Links_extractSite(url) {
 | |
|     let host;
 | |
|     try {
 | |
|       // Note that nsIURI.asciiHost throws NS_ERROR_FAILURE for some types of
 | |
|       // URIs, including jar and moz-icon URIs.
 | |
|       host = Services.io.newURI(url).asciiHost;
 | |
|     } catch (ex) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     // Strip off common subdomains of the same site (e.g., www, load balancer)
 | |
|     return host.replace(/^(m|mobile|www\d*)\./, "");
 | |
|   },
 | |
| 
 | |
|   init: function NewTabUtils_init() {
 | |
|     if (this.initWithoutProviders()) {
 | |
|       PlacesProvider.init();
 | |
|       Links.addProvider(PlacesProvider);
 | |
|       BlockedLinks.addObserver(Links);
 | |
|       BlockedLinks.addObserver(ActivityStreamLinks);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   initWithoutProviders: function NewTabUtils_initWithoutProviders() {
 | |
|     if (!this._initialized) {
 | |
|       this._initialized = true;
 | |
|       ExpirationFilter.init();
 | |
|       Telemetry.init();
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   uninit: function NewTabUtils_uninit() {
 | |
|     if (this.initialized) {
 | |
|       Telemetry.uninit();
 | |
|       BlockedLinks.removeObservers();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   getProviderLinks(aProvider) {
 | |
|     let cache = Links._providers.get(aProvider);
 | |
|     if (cache && cache.sortedLinks) {
 | |
|       return cache.sortedLinks;
 | |
|     }
 | |
|     return [];
 | |
|   },
 | |
| 
 | |
|   isTopSiteGivenProvider(aSite, aProvider) {
 | |
|     let cache = Links._providers.get(aProvider);
 | |
|     if (cache && cache.siteMap) {
 | |
|       return cache.siteMap.has(aSite);
 | |
|     }
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   isTopPlacesSite(aSite) {
 | |
|     return this.isTopSiteGivenProvider(aSite, PlacesProvider);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Restores all sites that have been removed from the grid.
 | |
|    */
 | |
|   restore: function NewTabUtils_restore() {
 | |
|     lazy.Storage.clear();
 | |
|     Links.resetCache();
 | |
|     PinnedLinks.resetCache();
 | |
|     BlockedLinks.resetCache();
 | |
| 
 | |
|     Links.populateCache(function () {
 | |
|       AllPages.update();
 | |
|     }, true);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Undoes all sites that have been removed from the grid and keep the pinned
 | |
|    * tabs.
 | |
|    * @param aCallback the callback method.
 | |
|    */
 | |
|   undoAll: function NewTabUtils_undoAll(aCallback) {
 | |
|     lazy.Storage.remove("blockedLinks");
 | |
|     Links.resetCache();
 | |
|     BlockedLinks.resetCache();
 | |
|     Links.populateCache(aCallback, true);
 | |
|   },
 | |
| 
 | |
|   links: Links,
 | |
|   allPages: AllPages,
 | |
|   pinnedLinks: PinnedLinks,
 | |
|   blockedLinks: BlockedLinks,
 | |
|   activityStreamLinks: ActivityStreamLinks,
 | |
|   activityStreamProvider: ActivityStreamProvider,
 | |
| };
 | 
