diff options
Diffstat (limited to 'browser/components/extensions/parent/ext-history.js')
-rw-r--r-- | browser/components/extensions/parent/ext-history.js | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/browser/components/extensions/parent/ext-history.js b/browser/components/extensions/parent/ext-history.js new file mode 100644 index 0000000000..49b52f2f6c --- /dev/null +++ b/browser/components/extensions/parent/ext-history.js @@ -0,0 +1,326 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* 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/. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +var { normalizeTime } = ExtensionCommon; + +let nsINavHistoryService = Ci.nsINavHistoryService; +const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([ + ["link", nsINavHistoryService.TRANSITION_LINK], + ["typed", nsINavHistoryService.TRANSITION_TYPED], + ["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK], + ["auto_subframe", nsINavHistoryService.TRANSITION_EMBED], + ["manual_subframe", nsINavHistoryService.TRANSITION_FRAMED_LINK], + ["reload", nsINavHistoryService.TRANSITION_RELOAD], +]); + +let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map(); +for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) { + TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition); +} + +const getTransitionType = transition => { + // cannot set a default value for the transition argument as the framework sets it to null + transition = transition || "link"; + let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition); + if (!transitionType) { + throw new Error( + `|${transition}| is not a supported transition for history` + ); + } + return transitionType; +}; + +const getTransition = transitionType => { + return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link"; +}; + +/* + * Converts a mozIStorageRow into a HistoryItem + */ +const convertRowToHistoryItem = row => { + return { + id: row.getResultByName("guid"), + url: row.getResultByName("url"), + title: row.getResultByName("page_title"), + lastVisitTime: PlacesUtils.toDate( + row.getResultByName("last_visit_date") + ).getTime(), + visitCount: row.getResultByName("visit_count"), + }; +}; + +/* + * Converts a mozIStorageRow into a VisitItem + */ +const convertRowToVisitItem = row => { + return { + id: row.getResultByName("guid"), + visitId: String(row.getResultByName("id")), + visitTime: PlacesUtils.toDate(row.getResultByName("visit_date")).getTime(), + referringVisitId: String(row.getResultByName("from_visit")), + transition: getTransition(row.getResultByName("visit_type")), + }; +}; + +/* + * Converts a mozIStorageResultSet into an array of objects + */ +const accumulateNavHistoryResults = (resultSet, converter, results) => { + let row; + while ((row = resultSet.getNextRow())) { + results.push(converter(row)); + } +}; + +function executeAsyncQuery(historyQuery, options, resultConverter) { + let results = []; + return new Promise((resolve, reject) => { + PlacesUtils.history.asyncExecuteLegacyQuery(historyQuery, options, { + handleResult(resultSet) { + accumulateNavHistoryResults(resultSet, resultConverter, results); + }, + handleError(error) { + reject( + new Error( + "Async execution error (" + error.result + "): " + error.message + ) + ); + }, + handleCompletion(reason) { + resolve(results); + }, + }); + }); +} + +this.history = class extends ExtensionAPIPersistent { + PERSISTENT_EVENTS = { + onVisited({ fire }) { + const listener = events => { + for (const event of events) { + const visit = { + id: event.pageGuid, + url: event.url, + title: event.lastKnownTitle || "", + lastVisitTime: event.visitTime, + visitCount: event.visitCount, + typedCount: event.typedCount, + }; + fire.sync(visit); + } + }; + + PlacesUtils.observers.addListener(["page-visited"], listener); + return { + unregister() { + PlacesUtils.observers.removeListener(["page-visited"], listener); + }, + convert(_fire) { + fire = _fire; + }, + }; + }, + onVisitRemoved({ fire }) { + const listener = events => { + const removedURLs = []; + + for (const event of events) { + switch (event.type) { + case "history-cleared": { + fire.sync({ allHistory: true, urls: [] }); + break; + } + case "page-removed": { + if (!event.isPartialVisistsRemoval) { + removedURLs.push(event.url); + } + break; + } + } + } + + if (removedURLs.length) { + fire.sync({ allHistory: false, urls: removedURLs }); + } + }; + + PlacesUtils.observers.addListener( + ["history-cleared", "page-removed"], + listener + ); + return { + unregister() { + PlacesUtils.observers.removeListener( + ["history-cleared", "page-removed"], + listener + ); + }, + convert(_fire) { + fire = _fire; + }, + }; + }, + onTitleChanged({ fire }) { + const listener = events => { + for (const event of events) { + const titleChanged = { + id: event.pageGuid, + url: event.url, + title: event.title, + }; + fire.sync(titleChanged); + } + }; + + PlacesUtils.observers.addListener(["page-title-changed"], listener); + return { + unregister() { + PlacesUtils.observers.removeListener( + ["page-title-changed"], + listener + ); + }, + convert(_fire) { + fire = _fire; + }, + }; + }, + }; + + getAPI(context) { + return { + history: { + addUrl: function(details) { + let transition, date; + try { + transition = getTransitionType(details.transition); + } catch (error) { + return Promise.reject({ message: error.message }); + } + if (details.visitTime) { + date = normalizeTime(details.visitTime); + } + let pageInfo = { + title: details.title, + url: details.url, + visits: [ + { + transition, + date, + }, + ], + }; + try { + return PlacesUtils.history.insert(pageInfo).then(() => undefined); + } catch (error) { + return Promise.reject({ message: error.message }); + } + }, + + deleteAll: function() { + return PlacesUtils.history.clear(); + }, + + deleteRange: function(filter) { + let newFilter = { + beginDate: normalizeTime(filter.startTime), + endDate: normalizeTime(filter.endTime), + }; + // History.removeVisitsByFilter returns a boolean, but our API should return nothing + return PlacesUtils.history + .removeVisitsByFilter(newFilter) + .then(() => undefined); + }, + + deleteUrl: function(details) { + let url = details.url; + // History.remove returns a boolean, but our API should return nothing + return PlacesUtils.history.remove(url).then(() => undefined); + }, + + search: function(query) { + let beginTime = + query.startTime == null + ? PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000) + : PlacesUtils.toPRTime(normalizeTime(query.startTime)); + let endTime = + query.endTime == null + ? Number.MAX_VALUE + : PlacesUtils.toPRTime(normalizeTime(query.endTime)); + if (beginTime > endTime) { + return Promise.reject({ + message: "The startTime cannot be after the endTime", + }); + } + + let options = PlacesUtils.history.getNewQueryOptions(); + options.includeHidden = true; + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.maxResults = query.maxResults || 100; + + let historyQuery = PlacesUtils.history.getNewQuery(); + historyQuery.searchTerms = query.text; + historyQuery.beginTime = beginTime; + historyQuery.endTime = endTime; + return executeAsyncQuery( + historyQuery, + options, + convertRowToHistoryItem + ); + }, + + getVisits: function(details) { + let url = details.url; + if (!url) { + return Promise.reject({ + message: "A URL must be provided for getVisits", + }); + } + + let options = PlacesUtils.history.getNewQueryOptions(); + options.includeHidden = true; + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.resultType = options.RESULTS_AS_VISIT; + + let historyQuery = PlacesUtils.history.getNewQuery(); + historyQuery.uri = Services.io.newURI(url); + return executeAsyncQuery( + historyQuery, + options, + convertRowToVisitItem + ); + }, + + onVisited: new EventManager({ + context, + module: "history", + event: "onVisited", + extensionApi: this, + }).api(), + + onVisitRemoved: new EventManager({ + context, + module: "history", + event: "onVisitRemoved", + extensionApi: this, + }).api(), + + onTitleChanged: new EventManager({ + context, + module: "history", + event: "onTitleChanged", + extensionApi: this, + }).api(), + }, + }; + } +}; |