Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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, {
});
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(),
},
};
}
};