1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // Android tests don't import these properly, so guard against that
7 let searchShortcuts = {};
8 let didSuccessfulImport = false;
10 shortURL = ChromeUtils.import("resource://activity-stream/lib/ShortURL.jsm");
11 searchShortcuts = ChromeUtils.importESModule(
12 "resource://activity-stream/lib/SearchShortcuts.sys.mjs"
14 didSuccessfulImport = true;
16 // The test failed to import these files
21 ChromeUtils.defineESModuleGetters(lazy, {
22 BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs",
23 PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
24 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
25 Pocket: "chrome://pocket/content/Pocket.sys.mjs",
26 pktApi: "chrome://pocket/content/pktApi.sys.mjs",
29 let BrowserWindowTracker;
31 BrowserWindowTracker = ChromeUtils.importESModule(
32 "resource:///modules/BrowserWindowTracker.sys.mjs"
33 ).BrowserWindowTracker;
35 // BrowserWindowTracker is used to determine devicePixelRatio in
36 // _addFavicons. We fallback to the value 2 if we can't find a window,
37 // so it's safe to do nothing with this here.
40 ChromeUtils.defineLazyGetter(lazy, "gCryptoHash", function () {
41 return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
44 // Boolean preferences that control newtab content
45 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
47 // The maximum number of results PlacesProvider retrieves from history.
48 const HISTORY_RESULTS_LIMIT = 100;
50 // The maximum number of links Links.getLinks will return.
51 const LINKS_GET_LINKS_LIMIT = 100;
53 // The gather telemetry topic.
54 const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
56 // Some default frecency threshold for Activity Stream requests
57 const ACTIVITY_STREAM_DEFAULT_FRECENCY = 150;
59 // Some default query limit for Activity Stream requests
60 const ACTIVITY_STREAM_DEFAULT_LIMIT = 12;
62 // Some default seconds ago for Activity Stream recent requests
63 const ACTIVITY_STREAM_DEFAULT_RECENT = 5 * 24 * 60 * 60;
65 // The fallback value for the width of smallFavicon in pixels.
66 // This value will be multiplied by the current window's devicePixelRatio.
67 // If devicePixelRatio cannot be found, it will be multiplied by 2.
68 const DEFAULT_SMALL_FAVICON_WIDTH = 16;
70 const POCKET_UPDATE_TIME = 24 * 60 * 60 * 1000; // 1 day
71 const POCKET_INACTIVE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week
72 const PREF_POCKET_LATEST_SINCE = "extensions.pocket.settings.latestSince";
75 * Calculate the MD5 hash for a string.
77 * The string to convert.
78 * @return The base64 representation of the MD5 hash.
80 function toHash(aValue) {
81 let value = new TextEncoder().encode(aValue);
82 lazy.gCryptoHash.init(lazy.gCryptoHash.MD5);
83 lazy.gCryptoHash.update(value, value.length);
84 return lazy.gCryptoHash.finish(true);
88 * Singleton that provides storage functionality.
90 ChromeUtils.defineLazyGetter(lazy, "Storage", function () {
91 return new LinksStorage();
94 function LinksStorage() {
95 // Handle migration of data across versions.
97 if (this._storedVersion < this._version) {
98 // This is either an upgrade, or version information is missing.
99 if (this._storedVersion < 1) {
100 // Version 1 moved data from DOM Storage to prefs. Since migrating from
101 // version 0 is no more supported, we just reportError a dataloss later.
102 throw new Error("Unsupported newTab storage version");
104 // Add further migration steps here.
106 // This is a downgrade. Since we cannot predict future, upgrades should
107 // be backwards compatible. We will set the version to the old value
108 // regardless, so, on next upgrade, the migration steps will run again.
109 // For this reason, they should also be able to run multiple times, even
110 // on top of an already up-to-date storage.
113 // Something went wrong in the update process, we can't recover from here,
114 // so just clear the storage and start from scratch (dataloss!).
116 "Unable to migrate the newTab storage to the current version. " +
117 "Restarting from scratch.\n",
123 // Set the version to the current one.
124 this._storedVersion = this._version;
127 LinksStorage.prototype = {
133 return Object.freeze({
134 pinnedLinks: "browser.newtabpage.pinned",
135 blockedLinks: "browser.newtabpage.blocked",
139 get _storedVersion() {
140 if (this.__storedVersion === undefined) {
141 // When the pref is not set, the storage version is unknown, so either:
142 // - it's a new profile
143 // - it's a profile where versioning information got lost
144 // In this case we still run through all of the valid migrations,
145 // starting from 1, as if it was a downgrade. As previously stated the
146 // migrations should already support running on an updated store.
147 this.__storedVersion = Services.prefs.getIntPref(
148 "browser.newtabpage.storageVersion",
152 return this.__storedVersion;
154 set _storedVersion(aValue) {
155 Services.prefs.setIntPref("browser.newtabpage.storageVersion", aValue);
156 this.__storedVersion = aValue;
160 * Gets the value for a given key from the storage.
161 * @param aKey The storage key (a string).
162 * @param aDefault A default value if the key doesn't exist.
163 * @return The value for the given key.
165 get: function Storage_get(aKey, aDefault) {
168 let prefValue = Services.prefs.getStringPref(this._prefs[aKey]);
169 value = JSON.parse(prefValue);
171 return value || aDefault;
175 * Sets the storage value for a given key.
176 * @param aKey The storage key (a string).
177 * @param aValue The value to set.
179 set: function Storage_set(aKey, aValue) {
180 // Page titles may contain unicode, thus use complex values.
181 Services.prefs.setStringPref(this._prefs[aKey], JSON.stringify(aValue));
185 * Removes the storage value for a given key.
186 * @param aKey The storage key (a string).
188 remove: function Storage_remove(aKey) {
189 Services.prefs.clearUserPref(this._prefs[aKey]);
193 * Clears the storage and removes all values.
195 clear: function Storage_clear() {
196 for (let key in this._prefs) {
203 * Singleton that serves as a registry for all open 'New Tab Page's.
207 * The array containing all active pages.
212 * Cached value that tells whether the New Tab Page feature is enabled.
217 * Adds a page to the internal list of pages.
218 * @param aPage The page to register.
220 register: function AllPages_register(aPage) {
221 this._pages.push(aPage);
226 * Removes a page from the internal list of pages.
227 * @param aPage The page to unregister.
229 unregister: function AllPages_unregister(aPage) {
230 let index = this._pages.indexOf(aPage);
232 this._pages.splice(index, 1);
237 * Returns whether the 'New Tab Page' is enabled.
240 if (this._enabled === null) {
241 this._enabled = Services.prefs.getBoolPref(PREF_NEWTAB_ENABLED);
244 return this._enabled;
248 * Enables or disables the 'New Tab Page' feature.
250 set enabled(aEnabled) {
251 if (this.enabled != aEnabled) {
252 Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, !!aEnabled);
257 * Returns the number of registered New Tab Pages (i.e. the number of open
258 * about:newtab instances).
261 return this._pages.length;
265 * Updates all currently active pages but the given one.
266 * @param aExceptPage The page to exclude from updating.
267 * @param aReason The reason for updating all pages.
269 update(aExceptPage, aReason = "") {
270 for (let page of this._pages.slice()) {
271 if (aExceptPage != page) {
272 page.update(aReason);
278 * Implements the nsIObserver interface to get notified when the preference
279 * value changes or when a new copy of a page thumbnail is available.
281 observe: function AllPages_observe(aSubject, aTopic, aData) {
282 if (aTopic == "nsPref:changed") {
283 // Clear the cached value.
285 case PREF_NEWTAB_ENABLED:
286 this._enabled = null;
290 // and all notifications get forwarded to each page.
291 this._pages.forEach(function (aPage) {
292 aPage.observe(aSubject, aTopic, aData);
297 * Adds a preference and new thumbnail observer and turns itself into a
298 * no-op after the first invokation.
300 _addObserver: function AllPages_addObserver() {
301 Services.prefs.addObserver(PREF_NEWTAB_ENABLED, this, true);
302 Services.obs.addObserver(this, "page-thumbnail:create", true);
303 this._addObserver = function () {};
306 QueryInterface: ChromeUtils.generateQI([
308 "nsISupportsWeakReference",
313 * Singleton that keeps track of all pinned links and their positions in the
318 * The cached list of pinned links.
323 * The array of pinned links.
327 this._links = lazy.Storage.get("pinnedLinks", []);
334 * Pins a link at the given position.
335 * @param aLink The link to pin.
336 * @param aIndex The grid index to pin the cell at.
337 * @return true if link changes, false otherwise
339 pin: function PinnedLinks_pin(aLink, aIndex) {
340 // Clear the link's old position, if any.
343 // change pinned link into a history link
344 let changed = this._makeHistoryLink(aLink);
345 this.links[aIndex] = aLink;
351 * Unpins a given link.
352 * @param aLink The link to unpin.
354 unpin: function PinnedLinks_unpin(aLink) {
355 let index = this._indexOfLink(aLink);
359 let links = this.links;
361 // trim trailing nulls
362 let i = links.length - 1;
363 while (i >= 0 && links[i] == null) {
371 * Saves the current list of pinned links.
373 save: function PinnedLinks_save() {
374 lazy.Storage.set("pinnedLinks", this.links);
378 * Checks whether a given link is pinned.
379 * @params aLink The link to check.
380 * @return whether The link is pinned.
382 isPinned: function PinnedLinks_isPinned(aLink) {
383 return this._indexOfLink(aLink) != -1;
387 * Resets the links cache.
389 resetCache: function PinnedLinks_resetCache() {
394 * Finds the index of a given link in the list of pinned links.
395 * @param aLink The link to find an index for.
396 * @return The link's index.
398 _indexOfLink: function PinnedLinks_indexOfLink(aLink) {
399 for (let i = 0; i < this.links.length; i++) {
400 let link = this.links[i];
401 if (link && link.url == aLink.url) {
406 // The given link is unpinned.
411 * Transforms link into a "history" link
412 * @param aLink The link to change
413 * @return true if link changes, false otherwise
415 _makeHistoryLink: function PinnedLinks_makeHistoryLink(aLink) {
416 if (!aLink.type || aLink.type == "history") {
419 aLink.type = "history";
424 * Replaces existing link with another link.
425 * @param aUrl The url of existing link
426 * @param aLink The replacement link
428 replace: function PinnedLinks_replace(aUrl, aLink) {
429 let index = this._indexOfLink({ url: aUrl });
433 this.links[index] = aLink;
439 * Singleton that keeps track of all blocked links in the grid.
443 * A list of objects that are observing blocked link changes.
448 * The cached list of blocked links.
453 * Registers an object that will be notified when the blocked links change.
455 addObserver(aObserver) {
456 this._observers.push(aObserver);
460 * Remove the observers.
463 this._observers = [];
467 * The list of blocked links.
471 this._links = lazy.Storage.get("blockedLinks", {});
478 * Blocks a given link. Adjusts siteMap accordingly, and notifies listeners.
479 * @param aLink The link to block.
481 block: function BlockedLinks_block(aLink) {
482 this._callObservers("onLinkBlocked", aLink);
483 this.links[toHash(aLink.url)] = 1;
486 // Make sure we unpin blocked links.
487 PinnedLinks.unpin(aLink);
491 * Unblocks a given link. Adjusts siteMap accordingly, and notifies listeners.
492 * @param aLink The link to unblock.
494 unblock: function BlockedLinks_unblock(aLink) {
495 if (this.isBlocked(aLink)) {
496 delete this.links[toHash(aLink.url)];
498 this._callObservers("onLinkUnblocked", aLink);
503 * Saves the current list of blocked links.
505 save: function BlockedLinks_save() {
506 lazy.Storage.set("blockedLinks", this.links);
510 * Returns whether a given link is blocked.
511 * @param aLink The link to check.
513 isBlocked: function BlockedLinks_isBlocked(aLink) {
514 return toHash(aLink.url) in this.links;
518 * Checks whether the list of blocked links is empty.
519 * @return Whether the list is empty.
521 isEmpty: function BlockedLinks_isEmpty() {
522 return !Object.keys(this.links).length;
526 * Resets the links cache.
528 resetCache: function BlockedLinks_resetCache() {
532 _callObservers(methodName, ...args) {
533 for (let obs of this._observers) {
534 if (typeof obs[methodName] == "function") {
536 obs[methodName](...args);
546 * Singleton that serves as the default link provider for the grid. It queries
547 * the history to retrieve the most frequently visited sites.
549 var PlacesProvider = {
551 * Set this to change the maximum number of links the provider will provide.
553 maxNumLinks: HISTORY_RESULTS_LIMIT,
556 * Must be called before the provider is used.
558 init: function PlacesProvider_init() {
559 this._placesObserver = new PlacesWeakCallbackWrapper(
560 this.handlePlacesEvents.bind(this)
562 PlacesObservers.addListener(
563 ["page-visited", "page-title-changed", "pages-rank-changed"],
569 * Gets the current set of links delivered by this provider.
570 * @param aCallback The function that the array of links is passed to.
572 getLinks: function PlacesProvider_getLinks(aCallback) {
573 let options = lazy.PlacesUtils.history.getNewQueryOptions();
574 options.maxResults = this.maxNumLinks;
576 // Sort by frecency, descending.
577 options.sortingMode =
578 Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING;
583 handleResult(aResultSet) {
586 while ((row = aResultSet.getNextRow())) {
587 let url = row.getResultByIndex(1);
588 if (LinkChecker.checkLoadURI(url)) {
589 let title = row.getResultByIndex(2);
590 let frecency = row.getResultByIndex(12);
591 let lastVisitDate = row.getResultByIndex(5);
603 handleError(aError) {
604 // Should we somehow handle this error?
608 handleCompletion(aReason) {
609 // The Places query breaks ties in frecency by place ID descending, but
610 // that's different from how Links.compareLinks breaks ties, because
611 // compareLinks doesn't have access to place IDs. It's very important
612 // that the initial list of links is sorted in the same order imposed by
613 // compareLinks, because Links uses compareLinks to perform binary
614 // searches on the list. So, ensure the list is so ordered.
617 while (i < links.length) {
618 if (Links.compareLinks(links[i - 1], links[i]) > 0) {
619 outOfOrder.push(links.splice(i, 1)[0]);
624 for (let link of outOfOrder) {
625 i = lazy.BinarySearch.insertionIndexOf(
630 links.splice(i, 0, link);
637 // Execute the query.
638 let query = lazy.PlacesUtils.history.getNewQuery();
639 lazy.PlacesUtils.history.asyncExecuteLegacyQuery(query, options, callback);
643 * Registers an object that will be notified when the provider's links change.
644 * @param aObserver An object with the following optional properties:
645 * * onLinkChanged: A function that's called when a single link
646 * changes. It's passed the provider and the link object. Only the
647 * link's `url` property is guaranteed to be present. If its `title`
648 * property is present, then its title has changed, and the
649 * property's value is the new title. If any sort properties are
650 * present, then its position within the provider's list of links may
651 * have changed, and the properties' values are the new sort-related
652 * values. Note that this link may not necessarily have been present
653 * in the lists returned from any previous calls to getLinks.
654 * * onManyLinksChanged: A function that's called when many links
655 * change at once. It's passed the provider. You should call
656 * getLinks to get the provider's new list of links.
658 addObserver: function PlacesProvider_addObserver(aObserver) {
659 this._observers.push(aObserver);
664 handlePlacesEvents(aEvents) {
665 for (let event of aEvents) {
666 switch (event.type) {
667 case "page-visited": {
668 if (event.visitCount == 1 && event.lastKnownTitle) {
669 this._callObservers("onLinkChanged", {
671 title: event.lastKnownTitle,
676 case "page-title-changed": {
677 this._callObservers("onLinkChanged", {
683 case "pages-rank-changed": {
684 this._callObservers("onManyLinksChanged");
691 _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
692 for (let obs of this._observers) {
693 if (obs[aMethodName]) {
695 obs[aMethodName](this, aArg);
705 * Queries history to retrieve the most frecent sites. Emits events when the
708 var ActivityStreamProvider = {
709 THUMB_FAVICON_SIZE: 96,
712 * Shared adjustment for selecting potentially blocked links.
714 _adjustLimitForBlocked({ ignoreBlocked, numItems }) {
715 // Just use the usual number if blocked links won't be filtered out
719 // Additionally select the number of blocked links in case they're removed
720 return Object.keys(BlockedLinks.links).length + numItems;
724 * Shared sub-SELECT to get the guid of a bookmark of the current url while
725 * avoiding LEFT JOINs on moz_bookmarks. This avoids gettings tags. The guid
726 * could be one of multiple possible guids. Assumes `moz_places h` is in FROM.
728 _commonBookmarkGuidSelect: `(
732 AND type = :bookmarkType
736 WHERE p.id = b.parent
737 AND p.parent <> :tagsFolderId
742 * Shared WHERE expression filtering out undesired pages, e.g., hidden,
743 * unvisited, and non-http/s urls. Assumes moz_places is in FROM / JOIN.
745 * NB: SUBSTR(url) is used even without an index instead of url_hash because
746 * most desired pages will match http/s, so it will only run on the ~10s of
747 * rows matched. If url_hash were to be used, it should probably *not* be used
748 * by the query optimizer as we primarily want it optimized for the other
749 * conditions, e.g., most frecent first.
751 _commonPlacesWhere: `
753 AND last_visit_date > 0
754 AND (SUBSTR(url, 1, 6) == "https:"
755 OR SUBSTR(url, 1, 5) == "http:")
759 * Shared parameters for getting correct bookmarks and LIMITed queries.
761 _getCommonParams(aOptions, aParams = {}) {
762 return Object.assign(
764 bookmarkType: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK,
765 limit: this._adjustLimitForBlocked(aOptions),
766 tagsFolderId: lazy.PlacesUtils.tagsFolderId,
773 * Shared columns for Highlights related queries.
775 _highlightsColumns: [
785 * Shared post-processing of Highlights links.
787 _processHighlights(aLinks, aOptions, aType) {
788 // Filter out blocked if necessary
789 if (!aOptions.ignoreBlocked) {
790 aLinks = aLinks.filter(
792 !BlockedLinks.isBlocked(
793 link.pocket_id ? { url: link.open_url } : link
798 // Limit the results to the requested number and set a type corresponding to
799 // which query selected it
800 return aLinks.slice(0, aOptions.numItems).map(item =>
801 Object.assign(item, {
808 * From an Array of links, if favicons are present, convert to data URIs
810 * @param {Array} aLinks
811 * an array containing objects with favicon data and mimeTypes
813 * @returns {Array} an array of links with favicons as data uri
815 _faviconBytesToDataURI(aLinks) {
816 return aLinks.map(link => {
818 let encodedData = btoa(String.fromCharCode.apply(null, link.favicon));
819 link.favicon = `data:${link.mimeType};base64,${encodedData}`;
820 delete link.mimeType;
823 if (link.smallFavicon) {
824 let encodedData = btoa(
825 String.fromCharCode.apply(null, link.smallFavicon)
827 link.smallFavicon = `data:${link.smallFaviconMimeType};base64,${encodedData}`;
828 delete link.smallFaviconMimeType;
836 * Get favicon data (and metadata) for a uri. Fetches both the largest favicon
837 * available, for Activity Stream; and a normal-sized favicon, for the Urlbar.
839 * @param {nsIURI} aUri Page to check for favicon data
840 * @param {number} preferredFaviconWidth
841 * The preferred width of the of the normal-sized favicon in pixels.
842 * @returns A promise of an object (possibly empty) containing the data.
844 async _loadIcons(aUri, preferredFaviconWidth) {
846 // Fetch the largest icon available.
849 faviconData = await lazy.PlacesUtils.promiseFaviconData(
851 this.THUMB_FAVICON_SIZE
853 Object.assign(iconData, {
854 favicon: faviconData.data,
855 faviconLength: faviconData.dataLen,
856 faviconRef: faviconData.uri.ref,
857 faviconSize: faviconData.size,
858 mimeType: faviconData.mimeType,
861 // Return early because fetching the largest favicon is the primary
862 // purpose of NewTabUtils.
866 // Also fetch a smaller icon.
868 faviconData = await lazy.PlacesUtils.promiseFaviconData(
870 preferredFaviconWidth
872 Object.assign(iconData, {
873 smallFavicon: faviconData.data,
874 smallFaviconLength: faviconData.dataLen,
875 smallFaviconRef: faviconData.uri.ref,
876 smallFaviconSize: faviconData.size,
877 smallFaviconMimeType: faviconData.mimeType,
880 // Do nothing with the error since we still have the large favicon fields.
887 * Computes favicon data for each url in a set of links
889 * @param {Array} links
890 * an array containing objects without favicon data or mimeTypes yet
892 * @returns {Promise} Returns a promise with the array of links with the largest
893 * favicon available (as a byte array), mimeType, byte array
894 * length, and favicon size (width)
896 _addFavicons(aLinks) {
898 if (BrowserWindowTracker) {
899 win = BrowserWindowTracker.getTopWindow();
901 // We fetch two copies of a page's favicon: the largest available, for
902 // Activity Stream; and a smaller size appropriate for the Urlbar.
903 const preferredFaviconWidth =
904 DEFAULT_SMALL_FAVICON_WIDTH * (win ? win.devicePixelRatio : 2);
905 // Each link in the array needs a favicon for it's page - so we fire off a
906 // promise for each link to compute the favicon data and attach it back to
907 // the original link object. We must wait until all favicons for the array
908 // of links are computed before returning
912 // eslint-disable-next-line no-async-promise-executor
913 new Promise(async resolve => {
914 // Never add favicon data for pocket items
915 if (link.type === "pocket") {
921 let linkUri = Services.io.newURI(link.url);
922 iconData = await this._loadIcons(linkUri, preferredFaviconWidth);
924 // Switch the scheme to try again with the other
928 .setScheme(linkUri.scheme === "https" ? "http" : "https")
930 iconData = await this._loadIcons(
932 preferredFaviconWidth
936 // We just won't put icon data on the link
939 // Add the icon data to the link if we have any
940 resolve(Object.assign(link, iconData));
947 * Helper function which makes the call to the Pocket API to fetch the user's
948 * saved Pocket items.
950 fetchSavedPocketItems(requestData) {
952 Services.prefs.getStringPref(PREF_POCKET_LATEST_SINCE, 0) * 1000;
954 // Do not fetch Pocket items for users that have been inactive for too long, or are not logged in
956 !lazy.pktApi.isUserLoggedIn() ||
957 Date.now() - latestSince > POCKET_INACTIVE_TIME
959 return Promise.resolve(null);
962 return new Promise((resolve, reject) => {
963 lazy.pktApi.retrieve(requestData, {
975 * Get the most recently Pocket-ed items from a user's Pocket list. See:
976 * https://getpocket.com/developer/docs/v3/retrieve for details
978 * @param {Object} aOptions
979 * {int} numItems: The max number of pocket items to fetch
981 async getRecentlyPocketed(aOptions) {
982 const pocketSecondsAgo =
983 Math.floor(Date.now() / 1000) - ACTIVITY_STREAM_DEFAULT_RECENT;
984 const requestData = {
985 detailType: "complete",
986 count: aOptions.numItems,
987 since: pocketSecondsAgo,
991 data = await this.fetchSavedPocketItems(requestData);
999 /* Extract relevant parts needed to show this card as a highlight:
1000 * url, preview image, title, description, and the unique item_id
1001 * necessary for Pocket to identify the item
1003 let items = Object.values(data.list)
1004 // status "0" means not archived or deleted
1005 .filter(item => item.status === "0")
1007 date_added: item.time_added * 1000,
1008 description: item.excerpt,
1009 preview_image_url: item.image && item.image.src,
1010 title: item.resolved_title,
1011 url: item.resolved_url,
1012 pocket_id: item.item_id,
1013 open_url: item.open_url,
1016 // Append the query param to let Pocket know this item came from highlights
1017 for (let item of items) {
1018 let url = new URL(item.open_url);
1019 url.searchParams.append("src", "fx_new_tab");
1020 item.open_url = url.href;
1023 return this._processHighlights(items, aOptions, "pocket");
1027 * Get most-recently-created visited bookmarks for Activity Stream.
1029 * @param {Object} aOptions
1030 * {num} bookmarkSecondsAgo: Maximum age of added bookmark.
1031 * {bool} ignoreBlocked: Do not filter out blocked links.
1032 * {int} numItems: Maximum number of items to return.
1034 async getRecentBookmarks(aOptions) {
1035 const options = Object.assign(
1037 bookmarkSecondsAgo: ACTIVITY_STREAM_DEFAULT_RECENT,
1038 ignoreBlocked: false,
1039 numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
1046 b.guid AS bookmarkGuid,
1051 b.dateAdded / 1000 AS date_added,
1053 FROM moz_bookmarks b
1054 JOIN moz_bookmarks p
1058 WHERE b.dateAdded >= :dateAddedThreshold
1060 AND b.type = :bookmarkType
1061 AND p.parent <> :tagsFolderId
1062 ${this._commonPlacesWhere}
1063 ORDER BY b.dateAdded DESC
1067 return this._processHighlights(
1068 await this.executePlacesQuery(sqlQuery, {
1069 columns: [...this._highlightsColumns, "date_added"],
1070 params: this._getCommonParams(options, {
1072 (Date.now() - options.bookmarkSecondsAgo * 1000) * 1000,
1081 * Get total count of all bookmarks.
1082 * Note: this includes default bookmarks
1084 * @return {int} The number bookmarks in the places DB.
1086 async getTotalBookmarksCount() {
1088 SELECT count(*) FROM moz_bookmarks b
1089 JOIN moz_bookmarks t ON t.id = b.parent
1090 AND t.parent <> :tags_folder
1091 WHERE b.type = :type_bookmark
1094 const result = await this.executePlacesQuery(sqlQuery, {
1096 tags_folder: lazy.PlacesUtils.tagsFolderId,
1097 type_bookmark: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK,
1101 return result[0][0];
1105 * Get most-recently-visited history with metadata for Activity Stream.
1107 * @param {Object} aOptions
1108 * {bool} ignoreBlocked: Do not filter out blocked links.
1109 * {int} numItems: Maximum number of items to return.
1111 async getRecentHistory(aOptions) {
1112 const options = Object.assign(
1114 ignoreBlocked: false,
1115 numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
1122 ${this._commonBookmarkGuidSelect},
1129 WHERE description NOTNULL
1130 AND preview_image_url NOTNULL
1131 ${this._commonPlacesWhere}
1132 ORDER BY last_visit_date DESC
1136 return this._processHighlights(
1137 await this.executePlacesQuery(sqlQuery, {
1138 columns: this._highlightsColumns,
1139 params: this._getCommonParams(options),
1147 * Gets the top frecent sites for Activity Stream.
1149 * @param {Object} aOptions
1150 * {bool} ignoreBlocked: Do not filter out blocked links.
1151 * {int} numItems: Maximum number of items to return.
1152 * {int} topsiteFrecency: Minimum amount of frecency for a site.
1153 * {bool} onePerDomain: Dedupe the resulting list.
1154 * {bool} includeFavicon: Include favicons if available.
1155 * {string} hideWithSearchParam: URLs that contain this search param will be
1156 * excluded from the returned links. This value should be either undefined
1157 * or a string with one of the following forms:
1158 * - undefined: Fall back to the value of pref
1159 * `browser.newtabpage.activity-stream.hideTopSitesWithSearchParam`
1160 * - "" (empty) - Disable this feature
1161 * - "key" - Search param named "key" with any or no value
1162 * - "key=" - Search param named "key" with no value
1163 * - "key=value" - Search param named "key" with value "value"
1165 * @returns {Promise} Returns a promise with the array of links as payload.
1167 async getTopFrecentSites(aOptions) {
1168 const options = Object.assign(
1170 ignoreBlocked: false,
1171 numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
1172 topsiteFrecency: ACTIVITY_STREAM_DEFAULT_FRECENCY,
1174 includeFavicon: true,
1175 hideWithSearchParam: Services.prefs.getCharPref(
1176 "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam",
1183 // Double the item count in case the host is deduped between with www or
1184 // not-www (i.e., 2 hosts) and an extra buffer for multiple pages per host.
1185 const origNumItems = options.numItems;
1186 if (options.onePerDomain) {
1187 options.numItems *= 2 * 10;
1190 // Keep this query fast with frecency-indexed lookups (even with excess
1191 // rows) and shift the more complex logic to post-processing afterwards
1194 ${this._commonBookmarkGuidSelect},
1197 last_visit_date / 1000 AS lastVisitDate,
1203 WHERE frecency >= :frecencyThreshold
1204 ${this._commonPlacesWhere}
1205 ORDER BY frecency DESC
1209 let links = await this.executePlacesQuery(sqlQuery, {
1219 params: this._getCommonParams(options, {
1220 frecencyThreshold: options.topsiteFrecency,
1224 // Determine if the other link is "better" (larger frecency, more recent,
1225 // lexicographically earlier url)
1226 function isOtherBetter(link, other) {
1227 if (other.frecency === link.frecency) {
1228 if (other.lastVisitDate === link.lastVisitDate) {
1229 return other.url < link.url;
1231 return other.lastVisitDate > link.lastVisitDate;
1233 return other.frecency > link.frecency;
1236 // Update a host Map with the better link
1237 function setBetterLink(map, link, hostMatcher, combiner = () => {}) {
1238 const host = hostMatcher(link.url)[1];
1239 if (map.has(host)) {
1240 const other = map.get(host);
1241 if (isOtherBetter(link, other)) {
1244 combiner(link, other);
1246 map.set(host, link);
1249 // Convert all links that are supposed to be a seach shortcut to its canonical URL
1251 didSuccessfulImport &&
1252 Services.prefs.getBoolPref(
1253 `browser.newtabpage.activity-stream.${searchShortcuts.SEARCH_SHORTCUTS_EXPERIMENT}`
1256 links.forEach(link => {
1257 let searchProvider = searchShortcuts.getSearchProvider(
1258 shortURL.shortURL(link)
1260 if (searchProvider) {
1261 link.url = searchProvider.url;
1266 // Remove links that contain the hide-with search param.
1267 if (options.hideWithSearchParam) {
1268 let [key, value] = options.hideWithSearchParam.split("=");
1269 links = links.filter(link => {
1271 let { searchParams } = new URL(link.url);
1272 return value === undefined
1273 ? !searchParams.has(key)
1274 : !searchParams.getAll(key).includes(value);
1280 // Remove any blocked links.
1281 if (!options.ignoreBlocked) {
1282 links = links.filter(link => !BlockedLinks.isBlocked(link));
1285 if (options.onePerDomain) {
1286 // De-dup the links.
1287 const exactHosts = new Map();
1288 for (const link of links) {
1289 // First we want to find the best link for an exact host
1290 setBetterLink(exactHosts, link, url => url.match(/:\/\/([^\/]+)/));
1293 // Clean up exact hosts to dedupe as non-www hosts
1294 const hosts = new Map();
1295 for (const link of exactHosts.values()) {
1299 url => url.match(/:\/\/(?:www\.)?([^\/]+)/),
1300 // Combine frecencies when deduping these links
1301 (targetLink, otherLink) => {
1302 targetLink.frecency = link.frecency + otherLink.frecency;
1307 links = [...hosts.values()];
1309 // Pick out the top links using the same comparer as before
1310 links = links.sort(isOtherBetter).slice(0, origNumItems);
1312 if (!options.includeFavicon) {
1315 // Get the favicons as data URI for now (until we use the favicon protocol)
1316 return this._faviconBytesToDataURI(await this._addFavicons(links));
1320 * Gets a specific bookmark given some info about it
1322 * @param {Obj} aInfo
1323 * An object with one and only one of the following properties:
1326 * - parentGuid and index
1328 async getBookmark(aInfo) {
1329 let bookmark = await lazy.PlacesUtils.bookmarks.fetch(aInfo);
1334 result.bookmarkGuid = bookmark.guid;
1335 result.bookmarkTitle = bookmark.title;
1336 result.lastModified = bookmark.lastModified.getTime();
1337 result.url = bookmark.url.href;
1342 * Count the number of visited urls grouped by day
1344 getUserMonthlyActivity() {
1347 strftime('%Y-%m-%d', visit_date/1000000.0, 'unixepoch', 'localtime') as date_format
1348 FROM moz_historyvisits
1349 WHERE visit_date > 0
1350 AND visit_date > strftime('%s','now','localtime','start of day','-27 days','utc') * 1000000
1351 GROUP BY date_format
1352 ORDER BY date_format ASC
1355 return this.executePlacesQuery(sqlQuery);
1359 * Executes arbitrary query against places database
1361 * @param {String} aQuery
1362 * SQL query to execute
1363 * @param {Object} [optional] aOptions
1364 * aOptions.columns - an array of column names. if supplied the return
1365 * items will consists of objects keyed on column names. Otherwise
1366 * array of raw values is returned in the select order
1367 * aOptions.param - an object of SQL binding parameters
1369 * @returns {Promise} Returns a promise with the array of retrieved items
1371 async executePlacesQuery(aQuery, aOptions = {}) {
1372 let { columns, params } = aOptions;
1374 let queryError = null;
1375 let conn = await lazy.PlacesUtils.promiseDBConnection();
1376 await conn.executeCached(aQuery, params, (aRow, aCancel) => {
1379 // if columns array is given construct an object
1380 if (columns && Array.isArray(columns)) {
1382 columns.forEach(column => {
1383 item[column] = aRow.getResultByName(column);
1386 // if no columns - make an array of raw values
1388 for (let i = 0; i < aRow.numEntries; i++) {
1389 item.push(aRow.getResultByIndex(i));
1399 throw new Error(queryError);
1406 * A set of actions which influence what sites shown on the Activity Stream page
1408 var ActivityStreamLinks = {
1409 _savedPocketStories: null,
1410 _pocketLastUpdated: 0,
1411 _pocketLastLatest: 0,
1416 * @param {Object} aLink
1417 * The link which contains a URL to add to the block list
1420 BlockedLinks.block(aLink);
1421 // If we're blocking a pocket item, invalidate the cache too
1422 if (aLink.pocket_id) {
1423 this._savedPocketStories = null;
1427 onLinkBlocked(aLink) {
1428 Services.obs.notifyObservers(null, "newtab-linkBlocked", aLink.url);
1432 * Adds a bookmark and opens up the Bookmark Dialog to show feedback that
1433 * the bookmarking action has been successful
1435 * @param {Object} aData
1436 * aData.url The url to bookmark
1437 * aData.title The title of the page to bookmark
1438 * @param {Window} aBrowserWindow
1439 * The current browser chrome window
1441 * @returns {Promise} Returns a promise set to an object representing the bookmark
1443 addBookmark(aData, aBrowserWindow) {
1444 const { url, title } = aData;
1445 return aBrowserWindow.PlacesCommandHook.bookmarkLink(url, title);
1449 * Removes a bookmark
1451 * @param {String} aBookmarkGuid
1452 * The bookmark guid associated with the bookmark to remove
1454 * @returns {Promise} Returns a promise at completion.
1456 deleteBookmark(aBookmarkGuid) {
1457 return lazy.PlacesUtils.bookmarks.remove(aBookmarkGuid);
1461 * Removes a history link and unpins the URL if previously pinned
1463 * @param {String} aUrl
1464 * The url to be removed from history
1466 * @returns {Promise} Returns a promise set to true if link was removed
1468 deleteHistoryEntry(aUrl) {
1470 PinnedLinks.unpin({ url });
1471 return lazy.PlacesUtils.history.remove(url);
1475 * Helper function which makes the call to the Pocket API to delete an item from
1476 * a user's saved to Pocket feed. Also, invalidate the Pocket stories cache
1478 * @param {Integer} aItemID
1479 * The unique pocket ID used to find the item to be deleted
1481 *@returns {Promise} Returns a promise at completion
1483 deletePocketEntry(aItemID) {
1484 this._savedPocketStories = null;
1485 return new Promise((success, error) =>
1486 lazy.pktApi.deleteItem(aItemID, { success, error })
1491 * Helper function which makes the call to the Pocket API to archive an item from
1492 * a user's saved to Pocket feed. Also, invalidate the Pocket stories cache
1494 * @param {Integer} aItemID
1495 * The unique pocket ID used to find the item to be archived
1497 *@returns {Promise} Returns a promise at completion
1499 archivePocketEntry(aItemID) {
1500 this._savedPocketStories = null;
1501 return new Promise((success, error) =>
1502 lazy.pktApi.archiveItem(aItemID, { success, error })
1507 * Helper function which makes the call to the Pocket API to save an item to
1508 * a user's saved to Pocket feed if they are logged in. Also, invalidate the
1509 * Pocket stories cache
1511 * @param {String} aUrl
1512 * The URL belonging to the story being saved
1513 * @param {String} aTitle
1514 * The title belonging to the story being saved
1515 * @param {Browser} aBrowser
1516 * The target browser to show the doorhanger in
1518 *@returns {Promise} Returns a promise at completion
1520 addPocketEntry(aUrl, aTitle, aBrowser) {
1521 // If the user is not logged in, show the panel to prompt them to log in
1522 if (!lazy.pktApi.isUserLoggedIn()) {
1523 lazy.Pocket.savePage(aBrowser, aUrl, aTitle);
1524 return Promise.resolve(null);
1527 // If the user is logged in, just save the link to Pocket and Activity Stream
1528 // will update the page
1529 this._savedPocketStories = null;
1530 return new Promise((success, error) => {
1531 lazy.pktApi.addLink(aUrl, {
1540 * Get the Highlights links to show on Activity Stream
1542 * @param {Object} aOptions
1543 * {bool} excludeBookmarks: Don't add bookmark items.
1544 * {bool} excludeHistory: Don't add history items.
1545 * {bool} excludePocket: Don't add Pocket items.
1546 * {bool} withFavicons: Add favicon data: URIs, when possible.
1547 * {int} numItems: Maximum number of (bookmark or history) items to return.
1549 * @return {Promise} Returns a promise with the array of links as the payload
1551 async getHighlights(aOptions = {}) {
1552 aOptions.numItems = aOptions.numItems || ACTIVITY_STREAM_DEFAULT_LIMIT;
1555 // First get bookmarks if we want them
1556 if (!aOptions.excludeBookmarks) {
1558 ...(await ActivityStreamProvider.getRecentBookmarks(aOptions))
1562 // Add the Pocket items if we need more and want them
1563 if (aOptions.numItems - results.length > 0 && !aOptions.excludePocket) {
1564 const latestSince = ~~Services.prefs.getStringPref(
1565 PREF_POCKET_LATEST_SINCE,
1568 // Invalidate the cache, get new stories, and update timestamps if:
1569 // 1. we do not have saved to Pocket stories already cached OR
1570 // 2. it has been too long since we last got Pocket stories OR
1571 // 3. there has been a paged saved to pocket since we last got new stories
1573 !this._savedPocketStories ||
1574 Date.now() - this._pocketLastUpdated > POCKET_UPDATE_TIME ||
1575 this._pocketLastLatest < latestSince
1577 this._savedPocketStories =
1578 await ActivityStreamProvider.getRecentlyPocketed(aOptions);
1579 this._pocketLastUpdated = Date.now();
1580 this._pocketLastLatest = latestSince;
1582 results.push(...this._savedPocketStories);
1585 // Add in history if we need more and want them
1586 if (aOptions.numItems - results.length > 0 && !aOptions.excludeHistory) {
1587 // Use the same numItems as bookmarks above in case we remove duplicates
1588 const history = await ActivityStreamProvider.getRecentHistory(aOptions);
1590 // Only include a url once in the result preferring the bookmark
1591 const bookmarkUrls = new Set(results.map(({ url }) => url));
1592 for (const page of history) {
1593 if (!bookmarkUrls.has(page.url)) {
1596 // Stop adding pages once we reach the desired maximum
1597 if (results.length === aOptions.numItems) {
1604 if (aOptions.withFavicons) {
1605 return ActivityStreamProvider._faviconBytesToDataURI(
1606 await ActivityStreamProvider._addFavicons(results)
1614 * Get the top sites to show on Activity Stream
1616 * @return {Promise} Returns a promise with the array of links as the payload
1618 async getTopSites(aOptions = {}) {
1619 return ActivityStreamProvider.getTopFrecentSites(aOptions);
1624 * Singleton that provides access to all links contained in the grid (including
1625 * the ones that don't fit on the grid). A link is a plain object that looks
1629 * url: "http://www.mozilla.org/",
1632 * lastVisitDate: 1394678824766431,
1637 * The maximum number of links returned by getLinks.
1639 maxNumLinks: LINKS_GET_LINKS_LIMIT,
1642 * A mapping from each provider to an object { sortedLinks, siteMap, linkMap }.
1643 * sortedLinks is the cached, sorted array of links for the provider.
1644 * siteMap is a mapping from base domains to URL count associated with the domain.
1645 * The count does not include blocked URLs. siteMap is used to look up a
1646 * user's top sites that can be targeted with a suggested tile.
1647 * linkMap is a Map from link URLs to link objects.
1649 _providers: new Map(),
1652 * The properties of link objects used to sort them.
1654 _sortProperties: ["frecency", "lastVisitDate", "url"],
1657 * List of callbacks waiting for the cache to be populated.
1659 _populateCallbacks: [],
1662 * A list of objects that are observing links updates.
1667 * Registers an object that will be notified when links updates.
1669 addObserver(aObserver) {
1670 this._observers.push(aObserver);
1674 * Adds a link provider.
1675 * @param aProvider The link provider.
1677 addProvider: function Links_addProvider(aProvider) {
1678 this._providers.set(aProvider, null);
1679 aProvider.addObserver(this);
1683 * Removes a link provider.
1684 * @param aProvider The link provider.
1686 removeProvider: function Links_removeProvider(aProvider) {
1687 if (!this._providers.delete(aProvider)) {
1688 throw new Error("Unknown provider");
1693 * Populates the cache with fresh links from the providers.
1694 * @param aCallback The callback to call when finished (optional).
1695 * @param aForce When true, populates the cache even when it's already filled.
1697 populateCache: function Links_populateCache(aCallback, aForce) {
1698 let callbacks = this._populateCallbacks;
1700 // Enqueue the current callback.
1701 callbacks.push(aCallback);
1703 // There was a callback waiting already, thus the cache has not yet been
1705 if (callbacks.length > 1) {
1709 function executeCallbacks() {
1710 while (callbacks.length) {
1711 let callback = callbacks.shift();
1716 // We want to proceed even if a callback fails.
1722 let numProvidersRemaining = this._providers.size;
1723 for (let [provider /* , links */] of this._providers) {
1724 this._populateProviderCache(
1727 if (--numProvidersRemaining == 0) {
1735 this._addObserver();
1739 * Gets the current set of links contained in the grid.
1740 * @return The links in the grid.
1742 getLinks: function Links_getLinks() {
1743 let pinnedLinks = Array.from(PinnedLinks.links);
1744 let links = this._getMergedProviderLinks();
1746 let sites = new Set();
1747 for (let link of pinnedLinks) {
1749 sites.add(NewTabUtils.extractSite(link.url));
1753 // Filter blocked and pinned links and duplicate base domains.
1754 links = links.filter(function (link) {
1755 let site = NewTabUtils.extractSite(link.url);
1756 if (site == null || sites.has(site)) {
1761 return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
1764 // Try to fill the gaps between pinned links.
1765 for (let i = 0; i < pinnedLinks.length && links.length; i++) {
1766 if (!pinnedLinks[i]) {
1767 pinnedLinks[i] = links.shift();
1771 // Append the remaining links if any.
1773 pinnedLinks = pinnedLinks.concat(links);
1776 for (let link of pinnedLinks) {
1778 link.baseDomain = NewTabUtils.extractSite(link.url);
1785 * Resets the links cache.
1787 resetCache: function Links_resetCache() {
1788 for (let provider of this._providers.keys()) {
1789 this._providers.set(provider, null);
1794 * Compares two links.
1795 * @param aLink1 The first link.
1796 * @param aLink2 The second link.
1797 * @return A negative number if aLink1 is ordered before aLink2, zero if
1798 * aLink1 and aLink2 have the same ordering, or a positive number if
1799 * aLink1 is ordered after aLink2.
1801 * @note compareLinks's this object is bound to Links below.
1803 compareLinks: function Links_compareLinks(aLink1, aLink2) {
1804 for (let prop of this._sortProperties) {
1805 if (!(prop in aLink1) || !(prop in aLink2)) {
1806 throw new Error("Comparable link missing required property: " + prop);
1810 aLink2.frecency - aLink1.frecency ||
1811 aLink2.lastVisitDate - aLink1.lastVisitDate ||
1812 aLink1.url.localeCompare(aLink2.url)
1816 _incrementSiteMap(map, link) {
1817 if (NewTabUtils.blockedLinks.isBlocked(link)) {
1818 // Don't count blocked URLs.
1821 let site = NewTabUtils.extractSite(link.url);
1822 map.set(site, (map.get(site) || 0) + 1);
1825 _decrementSiteMap(map, link) {
1826 if (NewTabUtils.blockedLinks.isBlocked(link)) {
1827 // Blocked URLs are not included in map.
1830 let site = NewTabUtils.extractSite(link.url);
1831 let previousURLCount = map.get(site);
1832 if (previousURLCount === 1) {
1835 map.set(site, previousURLCount - 1);
1840 * Update the siteMap cache based on the link given and whether we need
1841 * to increment or decrement it. We do this by iterating over all stored providers
1842 * to find which provider this link already exists in. For providers that
1843 * have this link, we will adjust siteMap for them accordingly.
1845 * @param aLink The link that will affect siteMap
1846 * @param increment A boolean for whether to increment or decrement siteMap
1848 _adjustSiteMapAndNotify(aLink, increment = true) {
1849 for (let [, /* provider */ cache] of this._providers) {
1850 // We only update siteMap if aLink is already stored in linkMap.
1851 if (cache.linkMap.get(aLink.url)) {
1853 this._incrementSiteMap(cache.siteMap, aLink);
1856 this._decrementSiteMap(cache.siteMap, aLink);
1859 this._callObservers("onLinkChanged", aLink);
1862 onLinkBlocked(aLink) {
1863 this._adjustSiteMapAndNotify(aLink, false);
1866 onLinkUnblocked(aLink) {
1867 this._adjustSiteMapAndNotify(aLink);
1870 populateProviderCache(provider, callback) {
1871 if (!this._providers.has(provider)) {
1873 "Can only populate provider cache for existing provider."
1877 return this._populateProviderCache(provider, callback, false);
1881 * Calls getLinks on the given provider and populates our cache for it.
1882 * @param aProvider The provider whose cache will be populated.
1883 * @param aCallback The callback to call when finished.
1884 * @param aForce When true, populates the provider's cache even when it's
1887 _populateProviderCache(aProvider, aCallback, aForce) {
1888 let cache = this._providers.get(aProvider);
1889 let createCache = !cache;
1892 // Start with a resolved promise.
1893 populatePromise: new Promise(resolve => resolve()),
1895 this._providers.set(aProvider, cache);
1897 // Chain the populatePromise so that calls are effectively queued.
1898 cache.populatePromise = cache.populatePromise.then(() => {
1899 return new Promise(resolve => {
1900 if (!createCache && !aForce) {
1905 aProvider.getLinks(links => {
1906 // Filter out null and undefined links so we don't have to deal with
1907 // them in getLinks when merging links from providers.
1908 links = links.filter(link => !!link);
1909 cache.sortedLinks = links;
1910 cache.siteMap = links.reduce((map, link) => {
1911 this._incrementSiteMap(map, link);
1914 cache.linkMap = links.reduce((map, link) => {
1915 map.set(link.url, link);
1926 * Merges the cached lists of links from all providers whose lists are cached.
1927 * @return The merged list.
1929 _getMergedProviderLinks: function Links__getMergedProviderLinks() {
1930 // Build a list containing a copy of each provider's sortedLinks list.
1932 for (let provider of this._providers.keys()) {
1933 let links = this._providers.get(provider);
1934 if (links && links.sortedLinks) {
1935 linkLists.push(links.sortedLinks.slice());
1939 return this.mergeLinkLists(linkLists);
1942 mergeLinkLists: function Links_mergeLinkLists(linkLists) {
1943 if (linkLists.length == 1) {
1944 return linkLists[0];
1947 function getNextLink() {
1948 let minLinks = null;
1949 for (let links of linkLists) {
1952 (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0)
1957 return minLinks ? minLinks.shift() : null;
1960 let finalLinks = [];
1962 let nextLink = getNextLink();
1963 nextLink && finalLinks.length < this.maxNumLinks;
1964 nextLink = getNextLink()
1966 finalLinks.push(nextLink);
1973 * Called by a provider to notify us when a single link changes.
1974 * @param aProvider The provider whose link changed.
1975 * @param aLink The link that changed. If the link is new, it must have all
1976 * of the _sortProperties. Otherwise, it may have as few or as
1977 * many as is convenient.
1978 * @param aIndex The current index of the changed link in the sortedLinks
1979 cache in _providers. Defaults to -1 if the provider doesn't know the index
1980 * @param aDeleted Boolean indicating if the provider has deleted the link.
1982 onLinkChanged: function Links_onLinkChanged(
1988 if (!("url" in aLink)) {
1989 throw new Error("Changed links must have a url property");
1992 let links = this._providers.get(aProvider);
1994 // This is not an error, it just means that between the time the provider
1995 // was added and the future time we call getLinks on it, it notified us of
2000 let { sortedLinks, siteMap, linkMap } = links;
2001 let existingLink = linkMap.get(aLink.url);
2002 let insertionLink = null;
2003 let updatePages = false;
2006 // Update our copy's position in O(lg n) by first removing it from its
2007 // list. It's important to do this before modifying its properties.
2008 if (this._sortProperties.some(prop => prop in aLink)) {
2011 idx = this._indexOf(sortedLinks, existingLink);
2012 } else if (this.compareLinks(aLink, sortedLinks[idx]) != 0) {
2013 throw new Error("aLink should be the same as sortedLinks[idx]");
2017 throw new Error("Link should be in _sortedLinks if in _linkMap");
2019 sortedLinks.splice(idx, 1);
2023 linkMap.delete(existingLink.url);
2024 this._decrementSiteMap(siteMap, existingLink);
2026 // Update our copy's properties.
2027 Object.assign(existingLink, aLink);
2029 // Finally, reinsert our copy below.
2030 insertionLink = existingLink;
2033 // Update our copy's title in O(1).
2034 if ("title" in aLink && aLink.title != existingLink.title) {
2035 existingLink.title = aLink.title;
2038 } else if (this._sortProperties.every(prop => prop in aLink)) {
2039 // Before doing the O(lg n) insertion below, do an O(1) check for the
2040 // common case where the new link is too low-ranked to be in the list.
2041 if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
2042 let lastLink = sortedLinks[sortedLinks.length - 1];
2043 if (this.compareLinks(lastLink, aLink) < 0) {
2047 // Copy the link object so that changes later made to it by the caller
2048 // don't affect our copy.
2050 for (let prop in aLink) {
2051 insertionLink[prop] = aLink[prop];
2053 linkMap.set(aLink.url, insertionLink);
2054 this._incrementSiteMap(siteMap, aLink);
2057 if (insertionLink) {
2058 let idx = this._insertionIndexOf(sortedLinks, insertionLink);
2059 sortedLinks.splice(idx, 0, insertionLink);
2060 if (sortedLinks.length > aProvider.maxNumLinks) {
2061 let lastLink = sortedLinks.pop();
2062 linkMap.delete(lastLink.url);
2063 this._decrementSiteMap(siteMap, lastLink);
2069 AllPages.update(null, "links-changed");
2074 * Called by a provider to notify us when many links change.
2076 onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
2077 this._populateProviderCache(
2080 AllPages.update(null, "links-changed");
2086 _indexOf: function Links__indexOf(aArray, aLink) {
2087 return this._binsearch(aArray, aLink, "indexOf");
2090 _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
2091 return this._binsearch(aArray, aLink, "insertionIndexOf");
2094 _binsearch: function Links__binsearch(aArray, aLink, aMethod) {
2095 return lazy.BinarySearch[aMethod](this.compareLinks, aArray, aLink);
2099 * Implements the nsIObserver interface to get notified about browser history
2102 observe: function Links_observe(aSubject, aTopic, aData) {
2103 // Make sure to update open about:newtab instances. If there are no opened
2104 // pages we can just wait for the next new tab to populate the cache again.
2105 if (AllPages.length && AllPages.enabled) {
2106 this.populateCache(function () {
2114 _callObservers(methodName, ...args) {
2115 for (let obs of this._observers) {
2116 if (typeof obs[methodName] == "function") {
2118 obs[methodName](this, ...args);
2127 * Adds a sanitization observer and turns itself into a no-op after the first
2130 _addObserver: function Links_addObserver() {
2131 Services.obs.addObserver(this, "browser:purge-session-history", true);
2132 this._addObserver = function () {};
2135 QueryInterface: ChromeUtils.generateQI([
2137 "nsISupportsWeakReference",
2141 Links.compareLinks = Links.compareLinks.bind(Links);
2144 * Singleton used to collect telemetry data.
2149 * Initializes object.
2151 init: function Telemetry_init() {
2152 Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY);
2155 uninit: function Telemetry_uninit() {
2156 Services.obs.removeObserver(this, TOPIC_GATHER_TELEMETRY);
2162 _collect: function Telemetry_collect() {
2164 { histogram: "NEWTAB_PAGE_ENABLED", value: AllPages.enabled },
2166 histogram: "NEWTAB_PAGE_PINNED_SITES_COUNT",
2167 value: PinnedLinks.links.length,
2170 histogram: "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
2171 value: Object.keys(BlockedLinks.links).length,
2175 probes.forEach(function Telemetry_collect_forEach(aProbe) {
2176 Services.telemetry.getHistogramById(aProbe.histogram).add(aProbe.value);
2181 * Listens for gather telemetry topic.
2183 observe: function Telemetry_observe(aSubject, aTopic, aData) {
2189 * Singleton that checks if a given link should be displayed on about:newtab
2190 * or if we should rather not do it for security reasons. URIs that inherit
2191 * their caller's principal will be filtered.
2198 Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
2199 Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS
2203 checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
2204 if (!(aURI in this._cache)) {
2205 this._cache[aURI] = this._doCheckLoadURI(aURI);
2208 return this._cache[aURI];
2211 _doCheckLoadURI: function Links_doCheckLoadURI(aURI) {
2213 // about:newtab is currently privileged. In any case, it should be
2214 // possible for tiles to point to pretty much everything - but not
2215 // to stuff that inherits the system principal, so we check:
2216 let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
2217 Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
2224 // We got a weird URI or one that would inherit the caller's principal.
2230 var ExpirationFilter = {
2231 init: function ExpirationFilter_init() {
2232 lazy.PageThumbs.addExpirationFilter(this);
2235 filterForThumbnailExpiration:
2236 function ExpirationFilter_filterForThumbnailExpiration(aCallback) {
2237 if (!AllPages.enabled) {
2242 Links.populateCache(function () {
2245 // Add all URLs to the list that we want to keep thumbnails for.
2246 for (let link of Links.getLinks().slice(0, 25)) {
2247 if (link && link.url) {
2248 urls.push(link.url);
2258 * Singleton that provides the public API of this JSM.
2260 export var NewTabUtils = {
2261 _initialized: false,
2264 * Extract a "site" from a url in a way that multiple urls of a "site" returns
2266 * @param aUrl Url spec string
2267 * @return The "site" string or null
2269 extractSite: function Links_extractSite(url) {
2272 // Note that nsIURI.asciiHost throws NS_ERROR_FAILURE for some types of
2273 // URIs, including jar and moz-icon URIs.
2274 host = Services.io.newURI(url).asciiHost;
2279 // Strip off common subdomains of the same site (e.g., www, load balancer)
2280 return host.replace(/^(m|mobile|www\d*)\./, "");
2283 init: function NewTabUtils_init() {
2284 if (this.initWithoutProviders()) {
2285 PlacesProvider.init();
2286 Links.addProvider(PlacesProvider);
2287 BlockedLinks.addObserver(Links);
2288 BlockedLinks.addObserver(ActivityStreamLinks);
2292 initWithoutProviders: function NewTabUtils_initWithoutProviders() {
2293 if (!this._initialized) {
2294 this._initialized = true;
2295 ExpirationFilter.init();
2302 uninit: function NewTabUtils_uninit() {
2303 if (this.initialized) {
2305 BlockedLinks.removeObservers();
2309 getProviderLinks(aProvider) {
2310 let cache = Links._providers.get(aProvider);
2311 if (cache && cache.sortedLinks) {
2312 return cache.sortedLinks;
2317 isTopSiteGivenProvider(aSite, aProvider) {
2318 let cache = Links._providers.get(aProvider);
2319 if (cache && cache.siteMap) {
2320 return cache.siteMap.has(aSite);
2325 isTopPlacesSite(aSite) {
2326 return this.isTopSiteGivenProvider(aSite, PlacesProvider);
2330 * Restores all sites that have been removed from the grid.
2332 restore: function NewTabUtils_restore() {
2333 lazy.Storage.clear();
2335 PinnedLinks.resetCache();
2336 BlockedLinks.resetCache();
2338 Links.populateCache(function () {
2344 * Undoes all sites that have been removed from the grid and keep the pinned
2346 * @param aCallback the callback method.
2348 undoAll: function NewTabUtils_undoAll(aCallback) {
2349 lazy.Storage.remove("blockedLinks");
2351 BlockedLinks.resetCache();
2352 Links.populateCache(aCallback, true);
2357 pinnedLinks: PinnedLinks,
2358 blockedLinks: BlockedLinks,
2359 activityStreamLinks: ActivityStreamLinks,
2360 activityStreamProvider: ActivityStreamProvider,