/* 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/. */ /* global XPCOMUtils, Services, PlacesUtils, EventEmitter */ /* global gLinks */ /* exported PlacesProvider */ "use strict"; this.EXPORTED_SYMBOLS = ["PlacesProvider"]; const {interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() { const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {}); return EventEmitter; }); XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"); // The maximum number of results PlacesProvider retrieves from history. const HISTORY_RESULTS_LIMIT = 100; /* Queries history to retrieve the most visited sites. Emits events when the * history changes. * Implements the EventEmitter interface. */ let Links = function Links() { EventEmitter.decorate(this); }; Links.prototype = { /** * Set this to change the maximum number of links the provider will provide. */ get maxNumLinks() { // getter, so it can't be replaced dynamically return HISTORY_RESULTS_LIMIT; }, /** * A set of functions called by @mozilla.org/browser/nav-historyservice * All history events are emitted from this object. */ historyObserver: { _batchProcessingDepth: 0, _batchCalledFrecencyChanged: false, /** * Called by the history service. */ onBeginUpdateBatch() { this._batchProcessingDepth += 1; }, onEndUpdateBatch() { this._batchProcessingDepth -= 1; if (this._batchProcessingDepth == 0 && this._batchCalledFrecencyChanged) { this.onManyFrecenciesChanged(); this._batchCalledFrecencyChanged = false; } }, onDeleteURI: function historyObserver_onDeleteURI(aURI) { // let observers remove sensetive data associated with deleted visit gLinks.emit("deleteURI", { url: aURI.spec, }); }, onClearHistory: function historyObserver_onClearHistory() { gLinks.emit("clearHistory"); }, onFrecencyChanged: function historyObserver_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) { // jshint ignore:line // If something is doing a batch update of history entries we don't want // to do lots of work for each record. So we just track the fact we need // to call onManyFrecenciesChanged() once the batch is complete. if (this._batchProcessingDepth > 0) { this._batchCalledFrecencyChanged = true; return; } // The implementation of the query in getLinks excludes hidden and // unvisited pages, so it's important to exclude them here, too. if (!aHidden && aLastVisitDate && NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) { gLinks.emit("linkChanged", { url: aURI.spec, frecency: aNewFrecency, lastVisitDate: aLastVisitDate, type: "history", }); } }, onManyFrecenciesChanged: function historyObserver_onManyFrecenciesChanged() { // Called when frecencies are invalidated and also when clearHistory is called // See toolkit/components/places/tests/unit/test_frecency_observers.js gLinks.emit("manyLinksChanged"); }, onVisit(aURI, aVisitId, aTime, aSessionId, aReferrerVisitId, aTransitionType, aGuid, aHidden, aVisitCount, aTyped, aLastKnownTitle) { // For new visits, if we're not batch processing, notify for a title update if (!this._batchProcessingDepth && aVisitCount == 1 && aLastKnownTitle) { this.onTitleChanged(aURI, aLastKnownTitle, aGuid); } }, onTitleChanged: function historyObserver_onTitleChanged(aURI, aNewTitle) { if (NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) { gLinks.emit("linkChanged", { url: aURI.spec, title: aNewTitle }); } }, QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver, Ci.nsISupportsWeakReference]) }, /** * Must be called before the provider is used. * Makes it easy to disable under pref */ init: function PlacesProvider_init() { try { PlacesUtils.history.addObserver(this.historyObserver, true); } catch (e) { Cu.reportError(e); } }, /** * Gets the current set of links delivered by this provider. * * @returns {Promise} Returns a promise with the array of links as payload. */ getLinks: Task.async(function*() { // Select a single page per host with highest frecency, highest recency. // Choose N top such pages. Note +rev_host, to turn off optimizer per :mak // suggestion. let sqlQuery = `SELECT url, title, frecency, last_visit_date as lastVisitDate, "history" as type FROM moz_places WHERE frecency in ( SELECT MAX(frecency) as frecency FROM moz_places WHERE hidden = 0 AND last_visit_date NOTNULL GROUP BY +rev_host ORDER BY frecency DESC LIMIT :limit ) GROUP BY rev_host HAVING MAX(lastVisitDate) ORDER BY frecency DESC, lastVisitDate DESC, url`; let links = yield this.executePlacesQuery(sqlQuery, { columns: ["url", "title", "lastVisitDate", "frecency", "type"], params: {limit: this.maxNumLinks} }); return links.filter(link => NewTabUtils.linkChecker.checkLoadURI(link.url)); }), /** * Executes arbitrary query against places database * * @param {String} aSql * SQL query to execute * @param {Object} [optional] aOptions * aOptions.columns - an array of column names. if supplied the returned * items will consist of objects keyed on column names. Otherwise * an array of raw values is returned in the select order * aOptions.param - an object of SQL binding parameters * aOptions.callback - a callback to handle query rows * * @returns {Promise} Returns a promise with the array of retrieved items */ executePlacesQuery: Task.async(function*(aSql, aOptions = {}) { let {columns, params, callback} = aOptions; let items = []; let queryError = null; let conn = yield PlacesUtils.promiseDBConnection(); yield conn.executeCached(aSql, params, aRow => { try { // check if caller wants to handle query raws if (callback) { callback(aRow); } else { // otherwise fill in the item and add items array 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; throw StopIteration; } }); if (queryError) { throw new Error(queryError); } return items; }), }; /** * Singleton that serves as the default link provider for the grid. */ const gLinks = new Links(); // jshint ignore:line let PlacesProvider = { links: gLinks, }; // Kept only for backwards-compatibility XPCOMUtils.defineLazyGetter(PlacesProvider, "LinkChecker", () => NewTabUtils.linkChecker);