326 lines
9.4 KiB
JavaScript
326 lines
9.4 KiB
JavaScript
/* -*- 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() {
|
|
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(),
|
|
},
|
|
};
|
|
}
|
|
};
|