2014-10-08 17:01:13 +04:00
|
|
|
/* 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";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Asynchronous API for managing history.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* The API makes use of `PageInfo` and `VisitInfo` objects, defined as follows.
|
|
|
|
*
|
|
|
|
* A `PageInfo` object is any object that contains A SUBSET of the
|
|
|
|
* following properties:
|
|
|
|
* - guid: (string)
|
|
|
|
* The globally unique id of the page.
|
2014-10-22 15:36:58 +04:00
|
|
|
* - url: (URL)
|
2014-10-08 17:01:13 +04:00
|
|
|
* or (nsIURI)
|
|
|
|
* or (string)
|
|
|
|
* The full URI of the page. Note that `PageInfo` values passed as
|
2014-10-22 15:36:58 +04:00
|
|
|
* argument may hold `nsIURI` or `string` values for property `url`,
|
2014-10-08 17:01:13 +04:00
|
|
|
* but `PageInfo` objects returned by this module always hold `URL`
|
|
|
|
* values.
|
|
|
|
* - title: (string)
|
|
|
|
* The title associated with the page, if any.
|
|
|
|
* - frecency: (number)
|
|
|
|
* The frecency of the page, if any.
|
|
|
|
* See https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Places/Frecency_algorithm
|
|
|
|
* Note that this property may not be used to change the actualy frecency
|
|
|
|
* score of a page, only to retrieve it. In other words, any `frecency` field
|
|
|
|
* passed as argument to a function of this API will be ignored.
|
|
|
|
* - visits: (Array<VisitInfo>)
|
|
|
|
* All the visits for this page, if any.
|
|
|
|
*
|
|
|
|
* See the documentation of individual methods to find out which properties
|
|
|
|
* are required for `PageInfo` arguments or returned for `PageInfo` results.
|
|
|
|
*
|
|
|
|
* A `VisitInfo` object is any object that contains A SUBSET of the following
|
|
|
|
* properties:
|
|
|
|
* - date: (Date)
|
|
|
|
* The time the visit occurred.
|
|
|
|
* - transition: (number)
|
2016-05-26 18:49:40 +03:00
|
|
|
* How the user reached the page. See constants `TRANSITIONS.*`
|
2014-10-08 17:01:13 +04:00
|
|
|
* for the possible transition types.
|
|
|
|
* - referrer: (URL)
|
|
|
|
* or (nsIURI)
|
|
|
|
* or (string)
|
|
|
|
* The referring URI of this visit. Note that `VisitInfo` passed
|
|
|
|
* as argument may hold `nsIURI` or `string` values for property `referrer`,
|
|
|
|
* but `VisitInfo` objects returned by this module always hold `URL`
|
|
|
|
* values.
|
|
|
|
* See the documentation of individual methods to find out which properties
|
|
|
|
* are required for `VisitInfo` arguments or returned for `VisitInfo` results.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* Each successful operation notifies through the nsINavHistoryObserver
|
|
|
|
* interface. To listen to such notifications you must register using
|
|
|
|
* nsINavHistoryService `addObserver` and `removeObserver` methods.
|
|
|
|
* @see nsINavHistoryObserver
|
|
|
|
*/
|
|
|
|
|
|
|
|
this.EXPORTED_SYMBOLS = [ "History" ];
|
|
|
|
|
|
|
|
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
|
|
|
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
2014-11-06 17:24:26 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
|
|
|
"resource://gre/modules/AsyncShutdown.jsm");
|
2014-10-08 17:01:13 +04:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
|
|
|
"resource://gre/modules/Services.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
|
|
|
"resource://gre/modules/NetUtil.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
|
|
|
"resource://gre/modules/Promise.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
|
|
|
"resource://gre/modules/Task.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
|
|
|
"resource://gre/modules/Sqlite.jsm");
|
2014-10-22 15:36:58 +04:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
|
|
|
"resource://gre/modules/PlacesUtils.jsm");
|
|
|
|
Cu.importGlobalProperties(["URL"]);
|
2014-10-08 17:01:13 +04:00
|
|
|
|
2014-11-06 17:24:26 +03:00
|
|
|
/**
|
|
|
|
* Whenever we update or remove numerous pages, it is preferable
|
|
|
|
* to yield time to the main thread every so often to avoid janking.
|
2015-04-10 12:23:07 +03:00
|
|
|
* These constants determine the maximal number of notifications we
|
2014-11-06 17:24:26 +03:00
|
|
|
* may emit before we yield.
|
|
|
|
*/
|
|
|
|
const NOTIFICATION_CHUNK_SIZE = 300;
|
2015-04-10 12:23:07 +03:00
|
|
|
const ONRESULT_CHUNK_SIZE = 300;
|
2014-11-06 17:24:26 +03:00
|
|
|
|
2016-09-30 12:42:28 +03:00
|
|
|
// Timers resolution is not always good, it can have a 16ms precision on Win.
|
|
|
|
const TIMERS_RESOLUTION_SKEW_MS = 16;
|
|
|
|
|
2014-11-19 18:10:53 +03:00
|
|
|
/**
|
|
|
|
* Sends a bookmarks notification through the given observers.
|
|
|
|
*
|
|
|
|
* @param observers
|
|
|
|
* array of nsINavBookmarkObserver objects.
|
|
|
|
* @param notification
|
|
|
|
* the notification name.
|
|
|
|
* @param args
|
|
|
|
* array of arguments to pass to the notification.
|
|
|
|
*/
|
2015-01-13 13:23:16 +03:00
|
|
|
function notify(observers, notification, args = []) {
|
2014-11-19 18:10:53 +03:00
|
|
|
for (let observer of observers) {
|
|
|
|
try {
|
|
|
|
observer[notification](...args);
|
|
|
|
} catch (ex) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-08 17:01:13 +04:00
|
|
|
this.History = Object.freeze({
|
|
|
|
/**
|
|
|
|
* Fetch the available information for one page.
|
|
|
|
*
|
|
|
|
* @param guidOrURI: (URL or nsIURI)
|
|
|
|
* The full URI of the page.
|
|
|
|
* or (string)
|
|
|
|
* Either the full URI of the page or the GUID of the page.
|
|
|
|
*
|
|
|
|
* @return (Promise)
|
|
|
|
* A promise resolved once the operation is complete.
|
|
|
|
* @resolves (PageInfo | null) If the page could be found, the information
|
|
|
|
* on that page. Note that this `PageInfo` does NOT contain the visit
|
|
|
|
* data (i.e. `visits` is `undefined`).
|
|
|
|
*
|
|
|
|
* @throws (Error)
|
|
|
|
* If `guidOrURI` does not have the expected type or if it is a string
|
|
|
|
* that may be parsed neither as a valid URL nor as a valid GUID.
|
|
|
|
*/
|
2016-12-30 02:34:54 +03:00
|
|
|
fetch(guidOrURI) {
|
2014-10-08 17:01:13 +04:00
|
|
|
throw new Error("Method not implemented");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2016-05-13 18:09:06 +03:00
|
|
|
* Adds a number of visits for a single page.
|
2014-10-08 17:01:13 +04:00
|
|
|
*
|
|
|
|
* Any change may be observed through nsINavHistoryObserver
|
|
|
|
*
|
2016-05-13 18:09:06 +03:00
|
|
|
* @param pageInfo: (PageInfo)
|
2016-05-13 18:09:06 +03:00
|
|
|
* Information on a page. This `PageInfo` MUST contain
|
2016-05-13 18:09:06 +03:00
|
|
|
* - a property `url`, as specified by the definition of `PageInfo`.
|
2016-05-13 18:09:06 +03:00
|
|
|
* - a property `visits`, as specified by the definition of
|
|
|
|
* `PageInfo`, which MUST contain at least one visit.
|
|
|
|
* If a property `title` is provided, the title of the page
|
|
|
|
* is updated.
|
2016-05-13 18:09:06 +03:00
|
|
|
* If the `date` of a visit is not provided, it defaults
|
2016-05-13 18:09:06 +03:00
|
|
|
* to now.
|
2016-05-13 18:09:06 +03:00
|
|
|
* If the `transition` of a visit is not provided, it defaults to
|
|
|
|
* TRANSITION_LINK.
|
2016-05-13 18:09:06 +03:00
|
|
|
*
|
|
|
|
* @return (Promise)
|
2016-05-13 18:09:06 +03:00
|
|
|
* A promise resolved once the operation is complete.
|
|
|
|
* @resolves (PageInfo)
|
|
|
|
* A PageInfo object populated with data after the insert is complete.
|
|
|
|
* @rejects (Error)
|
|
|
|
* Rejects if the insert was unsuccessful.
|
2016-05-13 18:09:06 +03:00
|
|
|
*
|
|
|
|
* @throws (Error)
|
|
|
|
* If the `url` specified was for a protocol that should not be
|
|
|
|
* stored (e.g. "chrome:", "mailbox:", "about:", "imap:", "news:",
|
|
|
|
* "moz-anno:", "view-source:", "resource:", "data:", "wyciwyg:",
|
|
|
|
* "javascript:", "blob:").
|
|
|
|
* @throws (Error)
|
2016-05-13 18:09:06 +03:00
|
|
|
* If `pageInfo` has an unexpected type.
|
2016-05-26 11:32:33 +03:00
|
|
|
* @throws (Error)
|
2016-05-13 18:09:06 +03:00
|
|
|
* If `pageInfo` does not have a `url`.
|
2016-05-13 18:09:06 +03:00
|
|
|
* @throws (Error)
|
2016-05-13 18:09:06 +03:00
|
|
|
* If `pageInfo` does not have a `visits` property or if the
|
|
|
|
* value of `visits` is ill-typed or is an empty array.
|
|
|
|
* @throws (Error)
|
|
|
|
* If an element of `visits` has an invalid `date`.
|
|
|
|
* @throws (Error)
|
|
|
|
* If an element of `visits` has an invalid `transition`.
|
|
|
|
*/
|
2016-12-30 02:34:54 +03:00
|
|
|
insert(pageInfo) {
|
2016-05-13 18:09:06 +03:00
|
|
|
if (typeof pageInfo != "object" || !pageInfo) {
|
|
|
|
throw new TypeError("pageInfo must be an object");
|
|
|
|
}
|
|
|
|
|
|
|
|
let info = validatePageInfo(pageInfo);
|
|
|
|
|
|
|
|
return PlacesUtils.withConnectionWrapper("History.jsm: insert",
|
|
|
|
db => insert(db, info));
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a number of visits for a number of pages.
|
|
|
|
*
|
|
|
|
* Any change may be observed through nsINavHistoryObserver
|
|
|
|
*
|
|
|
|
* @param pageInfos: (Array<PageInfo>)
|
|
|
|
* Information on a page. This `PageInfo` MUST contain
|
|
|
|
* - a property `url`, as specified by the definition of `PageInfo`.
|
|
|
|
* - a property `visits`, as specified by the definition of
|
|
|
|
* `PageInfo`, which MUST contain at least one visit.
|
|
|
|
* If a property `title` is provided, the title of the page
|
|
|
|
* is updated.
|
|
|
|
* If the `date` of a visit is not provided, it defaults
|
|
|
|
* to now.
|
|
|
|
* If the `transition` of a visit is not provided, it defaults to
|
|
|
|
* TRANSITION_LINK.
|
|
|
|
* @param onResult: (function(PageInfo))
|
|
|
|
* A callback invoked for each page inserted.
|
|
|
|
* @param onError: (function(PageInfo))
|
|
|
|
* A callback invoked for each page which generated an error
|
|
|
|
* when an insert was attempted.
|
|
|
|
*
|
|
|
|
* @return (Promise)
|
|
|
|
* A promise resolved once the operation is complete.
|
|
|
|
* @resolves (null)
|
|
|
|
* @rejects (Error)
|
|
|
|
* Rejects if all of the inserts were unsuccessful.
|
|
|
|
*
|
|
|
|
* @throws (Error)
|
|
|
|
* If the `url` specified was for a protocol that should not be
|
|
|
|
* stored (e.g. "chrome:", "mailbox:", "about:", "imap:", "news:",
|
|
|
|
* "moz-anno:", "view-source:", "resource:", "data:", "wyciwyg:",
|
|
|
|
* "javascript:", "blob:").
|
|
|
|
* @throws (Error)
|
|
|
|
* If `pageInfos` has an unexpected type.
|
|
|
|
* @throws (Error)
|
|
|
|
* If a `pageInfo` does not have a `url`.
|
2014-10-08 17:01:13 +04:00
|
|
|
* @throws (Error)
|
|
|
|
* If a `PageInfo` does not have a `visits` property or if the
|
|
|
|
* value of `visits` is ill-typed or is an empty array.
|
|
|
|
* @throws (Error)
|
|
|
|
* If an element of `visits` has an invalid `date`.
|
|
|
|
* @throws (Error)
|
2016-05-13 18:09:06 +03:00
|
|
|
* If an element of `visits` has an invalid `transition`.
|
2014-10-08 17:01:13 +04:00
|
|
|
*/
|
2016-12-30 02:34:54 +03:00
|
|
|
insertMany(pageInfos, onResult, onError) {
|
2016-05-13 18:09:06 +03:00
|
|
|
let infos = [];
|
|
|
|
|
|
|
|
if (!Array.isArray(pageInfos)) {
|
|
|
|
throw new TypeError("pageInfos must be an array");
|
|
|
|
}
|
|
|
|
if (!pageInfos.length) {
|
|
|
|
throw new TypeError("pageInfos may not be an empty array");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (onResult && typeof onResult != "function") {
|
|
|
|
throw new TypeError(`onResult: ${onResult} is not a valid function`);
|
|
|
|
}
|
|
|
|
if (onError && typeof onError != "function") {
|
|
|
|
throw new TypeError(`onError: ${onError} is not a valid function`);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let pageInfo of pageInfos) {
|
|
|
|
let info = validatePageInfo(pageInfo);
|
|
|
|
infos.push(info);
|
|
|
|
}
|
|
|
|
|
|
|
|
return PlacesUtils.withConnectionWrapper("History.jsm: insertMany",
|
|
|
|
db => insertMany(db, infos, onResult, onError));
|
2014-10-08 17:01:13 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove pages from the database.
|
|
|
|
*
|
|
|
|
* Any change may be observed through nsINavHistoryObserver
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param page: (URL or nsIURI)
|
|
|
|
* The full URI of the page.
|
|
|
|
* or (string)
|
|
|
|
* Either the full URI of the page or the GUID of the page.
|
|
|
|
* or (Array<URL|nsIURI|string>)
|
|
|
|
* An array of the above, to batch requests.
|
|
|
|
* @param onResult: (function(PageInfo))
|
|
|
|
* A callback invoked for each page found.
|
|
|
|
*
|
|
|
|
* @return (Promise)
|
2015-01-13 13:23:16 +03:00
|
|
|
* A promise resolved once the operation is complete.
|
2014-10-08 17:01:13 +04:00
|
|
|
* @resolve (bool)
|
|
|
|
* `true` if at least one page was removed, `false` otherwise.
|
2014-10-22 15:36:58 +04:00
|
|
|
* @throws (TypeError)
|
2014-10-08 17:01:13 +04:00
|
|
|
* If `pages` has an unexpected type or if a string provided
|
2014-10-22 15:36:58 +04:00
|
|
|
* is neither a valid GUID nor a valid URI or if `pages`
|
|
|
|
* is an empty array.
|
2014-10-08 17:01:13 +04:00
|
|
|
*/
|
2016-12-30 02:34:54 +03:00
|
|
|
remove(pages, onResult = null) {
|
2014-10-22 15:36:58 +04:00
|
|
|
// Normalize and type-check arguments
|
|
|
|
if (Array.isArray(pages)) {
|
|
|
|
if (pages.length == 0) {
|
|
|
|
throw new TypeError("Expected at least one page");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
pages = [pages];
|
|
|
|
}
|
|
|
|
|
|
|
|
let guids = [];
|
|
|
|
let urls = [];
|
|
|
|
for (let page of pages) {
|
|
|
|
// Normalize to URL or GUID, or throw if `page` cannot
|
|
|
|
// be normalized.
|
|
|
|
let normalized = normalizeToURLOrGUID(page);
|
|
|
|
if (typeof normalized === "string") {
|
|
|
|
guids.push(normalized);
|
|
|
|
} else {
|
|
|
|
urls.push(normalized.href);
|
|
|
|
}
|
|
|
|
}
|
2016-12-30 02:34:54 +03:00
|
|
|
let normalizedPages = {guids, urls};
|
2014-11-06 17:24:26 +03:00
|
|
|
|
2014-10-22 15:36:58 +04:00
|
|
|
// At this stage, we know that either `guids` is not-empty
|
|
|
|
// or `urls` is not-empty.
|
|
|
|
|
|
|
|
if (onResult && typeof onResult != "function") {
|
|
|
|
throw new TypeError("Invalid function: " + onResult);
|
|
|
|
}
|
|
|
|
|
2015-05-05 13:44:16 +03:00
|
|
|
return PlacesUtils.withConnectionWrapper("History.jsm: remove",
|
|
|
|
db => remove(db, normalizedPages, onResult));
|
2014-10-08 17:01:13 +04:00
|
|
|
},
|
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
/**
|
|
|
|
* Remove visits matching specific characteristics.
|
|
|
|
*
|
|
|
|
* Any change may be observed through nsINavHistoryObserver.
|
|
|
|
*
|
|
|
|
* @param filter: (object)
|
|
|
|
* The `object` may contain some of the following
|
|
|
|
* properties:
|
|
|
|
* - beginDate: (Date) Remove visits that have
|
|
|
|
* been added since this date (inclusive).
|
|
|
|
* - endDate: (Date) Remove visits that have
|
|
|
|
* been added before this date (inclusive).
|
2016-12-21 01:49:17 +03:00
|
|
|
* - limit: (Number) Limit the number of visits
|
|
|
|
* we remove to this number
|
|
|
|
* - url: (URL) Only remove visits to this URL
|
2015-04-10 12:23:07 +03:00
|
|
|
* If both `beginDate` and `endDate` are specified,
|
|
|
|
* visits between `beginDate` (inclusive) and `end`
|
|
|
|
* (inclusive) are removed.
|
|
|
|
*
|
|
|
|
* @param onResult: (function(VisitInfo), [optional])
|
|
|
|
* A callback invoked for each visit found and removed.
|
|
|
|
* Note that the referrer property of `VisitInfo`
|
|
|
|
* is NOT populated.
|
|
|
|
*
|
|
|
|
* @return (Promise)
|
|
|
|
* @resolve (bool)
|
|
|
|
* `true` if at least one visit was removed, `false`
|
|
|
|
* otherwise.
|
|
|
|
* @throws (TypeError)
|
|
|
|
* If `filter` does not have the expected type, in
|
|
|
|
* particular if the `object` is empty.
|
|
|
|
*/
|
2016-12-30 02:34:54 +03:00
|
|
|
removeVisitsByFilter(filter, onResult = null) {
|
2015-04-10 12:23:07 +03:00
|
|
|
if (!filter || typeof filter != "object") {
|
|
|
|
throw new TypeError("Expected a filter");
|
|
|
|
}
|
|
|
|
|
|
|
|
let hasBeginDate = "beginDate" in filter;
|
|
|
|
let hasEndDate = "endDate" in filter;
|
2016-12-21 01:49:17 +03:00
|
|
|
let hasURL = "url" in filter;
|
|
|
|
let hasLimit = "limit" in filter;
|
2015-04-10 12:23:07 +03:00
|
|
|
if (hasBeginDate) {
|
|
|
|
ensureDate(filter.beginDate);
|
|
|
|
}
|
|
|
|
if (hasEndDate) {
|
|
|
|
ensureDate(filter.endDate);
|
|
|
|
}
|
|
|
|
if (hasBeginDate && hasEndDate && filter.beginDate > filter.endDate) {
|
|
|
|
throw new TypeError("`beginDate` should be at least as old as `endDate`");
|
|
|
|
}
|
2016-12-21 01:49:17 +03:00
|
|
|
if (!hasBeginDate && !hasEndDate && !hasURL && !hasLimit) {
|
2015-04-10 12:23:07 +03:00
|
|
|
throw new TypeError("Expected a non-empty filter");
|
|
|
|
}
|
|
|
|
|
2016-12-21 01:49:17 +03:00
|
|
|
if (hasURL && !(filter.url instanceof URL) && typeof filter.url != "string" &&
|
|
|
|
!(filter.url instanceof Ci.nsIURI)) {
|
|
|
|
throw new TypeError("Expected a valid URL for `url`");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasLimit &&
|
|
|
|
(typeof filter.limit != "number" ||
|
|
|
|
filter.limit <= 0 ||
|
|
|
|
!Number.isInteger(filter.limit))) {
|
|
|
|
throw new TypeError("Expected a non-zero positive integer as a limit");
|
|
|
|
}
|
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
if (onResult && typeof onResult != "function") {
|
|
|
|
throw new TypeError("Invalid function: " + onResult);
|
|
|
|
}
|
|
|
|
|
2015-05-05 13:44:16 +03:00
|
|
|
return PlacesUtils.withConnectionWrapper("History.jsm: removeVisitsByFilter",
|
|
|
|
db => removeVisitsByFilter(db, filter, onResult)
|
|
|
|
);
|
2015-04-10 12:23:07 +03:00
|
|
|
},
|
|
|
|
|
2014-10-08 17:01:13 +04:00
|
|
|
/**
|
|
|
|
* Determine if a page has been visited.
|
|
|
|
*
|
|
|
|
* @param pages: (URL or nsIURI)
|
|
|
|
* The full URI of the page.
|
|
|
|
* or (string)
|
|
|
|
* The full URI of the page or the GUID of the page.
|
|
|
|
*
|
|
|
|
* @return (Promise)
|
2015-01-13 13:23:16 +03:00
|
|
|
* A promise resolved once the operation is complete.
|
2014-10-08 17:01:13 +04:00
|
|
|
* @resolve (bool)
|
|
|
|
* `true` if the page has been visited, `false` otherwise.
|
|
|
|
* @throws (Error)
|
|
|
|
* If `pages` has an unexpected type or if a string provided
|
|
|
|
* is neither not a valid GUID nor a valid URI.
|
|
|
|
*/
|
2016-12-30 02:34:54 +03:00
|
|
|
hasVisits(page, onResult) {
|
2014-10-08 17:01:13 +04:00
|
|
|
throw new Error("Method not implemented");
|
|
|
|
},
|
|
|
|
|
2015-01-13 13:23:16 +03:00
|
|
|
/**
|
|
|
|
* Clear all history.
|
|
|
|
*
|
|
|
|
* @return (Promise)
|
|
|
|
* A promise resolved once the operation is complete.
|
|
|
|
*/
|
|
|
|
clear() {
|
2015-05-05 13:44:16 +03:00
|
|
|
return PlacesUtils.withConnectionWrapper("History.jsm: clear",
|
|
|
|
clear
|
|
|
|
);
|
2015-01-13 13:23:16 +03:00
|
|
|
},
|
|
|
|
|
2014-10-08 17:01:13 +04:00
|
|
|
/**
|
|
|
|
* Possible values for the `transition` property of `VisitInfo`
|
|
|
|
* objects.
|
|
|
|
*/
|
|
|
|
|
2016-05-26 18:49:40 +03:00
|
|
|
TRANSITIONS: {
|
|
|
|
/**
|
|
|
|
* The user followed a link and got a new toplevel window.
|
|
|
|
*/
|
|
|
|
LINK: Ci.nsINavHistoryService.TRANSITION_LINK,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The user typed the page's URL in the URL bar or selected it from
|
|
|
|
* URL bar autocomplete results, clicked on it from a history query
|
|
|
|
* (from the History sidebar, History menu, or history query in the
|
|
|
|
* personal toolbar or Places organizer.
|
|
|
|
*/
|
|
|
|
TYPED: Ci.nsINavHistoryService.TRANSITION_TYPED,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The user followed a bookmark to get to the page.
|
|
|
|
*/
|
|
|
|
BOOKMARK: Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Some inner content is loaded. This is true of all images on a
|
|
|
|
* page, and the contents of the iframe. It is also true of any
|
|
|
|
* content in a frame if the user did not explicitly follow a link
|
|
|
|
* to get there.
|
|
|
|
*/
|
|
|
|
EMBED: Ci.nsINavHistoryService.TRANSITION_EMBED,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set when the transition was a permanent redirect.
|
|
|
|
*/
|
|
|
|
REDIRECT_PERMANENT: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set when the transition was a temporary redirect.
|
|
|
|
*/
|
|
|
|
REDIRECT_TEMPORARY: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set when the transition is a download.
|
|
|
|
*/
|
|
|
|
DOWNLOAD: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The user followed a link and got a visit in a frame.
|
|
|
|
*/
|
|
|
|
FRAMED_LINK: Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The user reloaded a page.
|
|
|
|
*/
|
|
|
|
RELOAD: Ci.nsINavHistoryService.TRANSITION_RELOAD,
|
|
|
|
},
|
2014-10-08 17:01:13 +04:00
|
|
|
});
|
|
|
|
|
2016-05-13 18:09:06 +03:00
|
|
|
/**
|
|
|
|
* Validate an input PageInfo object, returning a valid PageInfo object.
|
|
|
|
*
|
|
|
|
* @param pageInfo: (PageInfo)
|
|
|
|
* @return (PageInfo)
|
|
|
|
*/
|
|
|
|
function validatePageInfo(pageInfo) {
|
|
|
|
let info = {
|
|
|
|
visits: [],
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!pageInfo.url) {
|
|
|
|
throw new TypeError("PageInfo object must have a url property");
|
|
|
|
}
|
|
|
|
|
|
|
|
info.url = normalizeToURLOrGUID(pageInfo.url);
|
|
|
|
|
2016-09-22 20:00:55 +03:00
|
|
|
if (typeof pageInfo.title === "string") {
|
2016-05-13 18:09:06 +03:00
|
|
|
info.title = pageInfo.title;
|
|
|
|
} else if (pageInfo.title != null && pageInfo.title != undefined) {
|
|
|
|
throw new TypeError(`title property of PageInfo object: ${pageInfo.title} must be a string if provided`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!pageInfo.visits || !Array.isArray(pageInfo.visits) || !pageInfo.visits.length) {
|
|
|
|
throw new TypeError("PageInfo object must have an array of visits");
|
|
|
|
}
|
|
|
|
for (let inVisit of pageInfo.visits) {
|
|
|
|
let visit = {
|
|
|
|
date: new Date(),
|
2016-05-26 18:49:40 +03:00
|
|
|
transition: inVisit.transition || History.TRANSITIONS.LINK,
|
2016-05-13 18:09:06 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
if (!isValidTransitionType(visit.transition)) {
|
|
|
|
throw new TypeError(`transition: ${visit.transition} is not a valid transition type`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inVisit.date) {
|
|
|
|
ensureDate(inVisit.date);
|
2016-09-30 12:42:28 +03:00
|
|
|
if (inVisit.date > (Date.now() + TIMERS_RESOLUTION_SKEW_MS)) {
|
2016-05-13 18:09:06 +03:00
|
|
|
throw new TypeError(`date: ${inVisit.date} cannot be a future date`);
|
|
|
|
}
|
|
|
|
visit.date = inVisit.date;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inVisit.referrer) {
|
|
|
|
visit.referrer = normalizeToURLOrGUID(inVisit.referrer);
|
|
|
|
}
|
|
|
|
info.visits.push(visit);
|
|
|
|
}
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert a PageInfo object into the format expected by updatePlaces.
|
|
|
|
*
|
|
|
|
* Note: this assumes that the PageInfo object has already been validated
|
|
|
|
* via validatePageInfo.
|
|
|
|
*
|
|
|
|
* @param pageInfo: (PageInfo)
|
|
|
|
* @return (info)
|
|
|
|
*/
|
|
|
|
function convertForUpdatePlaces(pageInfo) {
|
|
|
|
let info = {
|
|
|
|
uri: PlacesUtils.toURI(pageInfo.url),
|
|
|
|
title: pageInfo.title,
|
|
|
|
visits: [],
|
|
|
|
};
|
|
|
|
|
|
|
|
for (let inVisit of pageInfo.visits) {
|
|
|
|
let visit = {
|
|
|
|
visitDate: PlacesUtils.toPRTime(inVisit.date),
|
|
|
|
transitionType: inVisit.transition,
|
|
|
|
referrerURI: (inVisit.referrer) ? PlacesUtils.toURI(inVisit.referrer) : undefined,
|
|
|
|
};
|
|
|
|
info.visits.push(visit);
|
|
|
|
}
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Is a value a valid transition type?
|
|
|
|
*
|
|
|
|
* @param transitionType: (String)
|
|
|
|
* @return (Boolean)
|
|
|
|
*/
|
|
|
|
function isValidTransitionType(transitionType) {
|
2016-05-26 18:49:40 +03:00
|
|
|
return Object.values(History.TRANSITIONS).includes(transitionType);
|
2016-05-13 18:09:06 +03:00
|
|
|
}
|
2014-10-22 15:36:58 +04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Normalize a key to either a string (if it is a valid GUID) or an
|
|
|
|
* instance of `URL` (if it is a `URL`, `nsIURI`, or a string
|
|
|
|
* representing a valid url).
|
|
|
|
*
|
|
|
|
* @throws (TypeError)
|
|
|
|
* If the key is neither a valid guid nor a valid url.
|
|
|
|
*/
|
|
|
|
function normalizeToURLOrGUID(key) {
|
|
|
|
if (typeof key === "string") {
|
|
|
|
// A string may be a URL or a guid
|
2016-05-13 18:09:06 +03:00
|
|
|
if (PlacesUtils.isValidGuid(key)) {
|
2014-10-22 15:36:58 +04:00
|
|
|
return key;
|
|
|
|
}
|
|
|
|
return new URL(key);
|
|
|
|
}
|
|
|
|
if (key instanceof URL) {
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
if (key instanceof Ci.nsIURI) {
|
|
|
|
return new URL(key.spec);
|
|
|
|
}
|
|
|
|
throw new TypeError("Invalid url or guid: " + key);
|
|
|
|
}
|
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
/**
|
|
|
|
* Throw if an object is not a Date object.
|
|
|
|
*/
|
|
|
|
function ensureDate(arg) {
|
|
|
|
if (!arg || typeof arg != "object" || arg.constructor.name != "Date") {
|
|
|
|
throw new TypeError("Expected a Date, got " + arg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-22 15:36:58 +04:00
|
|
|
/**
|
|
|
|
* Convert a list of strings or numbers to its SQL
|
|
|
|
* representation as a string.
|
|
|
|
*/
|
|
|
|
function sqlList(list) {
|
|
|
|
return list.map(JSON.stringify).join();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidate and recompute the frecency of a list of pages,
|
|
|
|
* informing frecency observers.
|
|
|
|
*
|
|
|
|
* @param db: (Sqlite connection)
|
|
|
|
* @param idList: (Array)
|
|
|
|
* The `moz_places` identifiers for the places to invalidate.
|
|
|
|
* @return (Promise)
|
|
|
|
*/
|
2015-09-15 21:19:45 +03:00
|
|
|
var invalidateFrecencies = Task.async(function*(db, idList) {
|
2014-10-22 15:36:58 +04:00
|
|
|
if (idList.length == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let ids = sqlList(idList);
|
|
|
|
yield db.execute(
|
|
|
|
`UPDATE moz_places
|
|
|
|
SET frecency = NOTIFY_FRECENCY(
|
|
|
|
CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date
|
|
|
|
) WHERE id in (${ ids })`
|
|
|
|
);
|
|
|
|
yield db.execute(
|
|
|
|
`UPDATE moz_places
|
|
|
|
SET hidden = 0
|
|
|
|
WHERE id in (${ ids })
|
|
|
|
AND frecency <> 0`
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2015-01-13 13:23:16 +03:00
|
|
|
// Inner implementation of History.clear().
|
2015-09-15 21:19:45 +03:00
|
|
|
var clear = Task.async(function* (db) {
|
2015-01-13 13:23:16 +03:00
|
|
|
// Remove all history.
|
|
|
|
yield db.execute("DELETE FROM moz_historyvisits");
|
|
|
|
|
|
|
|
// Clear the registered embed visits.
|
|
|
|
PlacesUtils.history.clearEmbedVisits();
|
|
|
|
|
|
|
|
// Expiration will take care of orphans.
|
|
|
|
let observers = PlacesUtils.history.getObservers();
|
|
|
|
notify(observers, "onClearHistory");
|
|
|
|
|
|
|
|
// Invalidate frecencies for the remaining places. This must happen
|
|
|
|
// after the notification to ensure it runs enqueued to expiration.
|
|
|
|
yield db.execute(
|
|
|
|
`UPDATE moz_places SET frecency =
|
|
|
|
(CASE
|
2016-06-29 15:47:36 +03:00
|
|
|
WHEN url_hash BETWEEN hash("place", "prefix_lo") AND
|
|
|
|
hash("place", "prefix_hi")
|
2015-01-13 13:23:16 +03:00
|
|
|
THEN 0
|
|
|
|
ELSE -1
|
|
|
|
END)
|
|
|
|
WHERE frecency > 0`);
|
|
|
|
|
|
|
|
// Notify frecency change observers.
|
|
|
|
notify(observers, "onManyFrecenciesChanged");
|
|
|
|
});
|
2014-10-22 15:36:58 +04:00
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
/**
|
|
|
|
* Clean up pages whose history has been modified, by either
|
|
|
|
* removing them entirely (if they are marked for removal,
|
|
|
|
* typically because all visits have been removed and there
|
|
|
|
* are no more foreign keys such as bookmarks) or updating
|
|
|
|
* their frecency (otherwise).
|
|
|
|
*
|
|
|
|
* @param db: (Sqlite connection)
|
|
|
|
* The database.
|
|
|
|
* @param pages: (Array of objects)
|
|
|
|
* Pages that have been touched and that need cleaning up.
|
|
|
|
* Each object should have the following properties:
|
|
|
|
* - id: (number) The `moz_places` identifier for the place.
|
|
|
|
* - hasVisits: (boolean) If `true`, there remains at least one
|
|
|
|
* visit to this page, so the page should be kept and its
|
|
|
|
* frecency updated.
|
|
|
|
* - hasForeign: (boolean) If `true`, the page has at least
|
|
|
|
* one foreign reference (i.e. a bookmark), so the page should
|
|
|
|
* be kept and its frecency updated.
|
|
|
|
* @return (Promise)
|
|
|
|
*/
|
2015-09-15 21:19:45 +03:00
|
|
|
var cleanupPages = Task.async(function*(db, pages) {
|
2015-12-03 13:56:58 +03:00
|
|
|
yield invalidateFrecencies(db, pages.filter(p => p.hasForeign || p.hasVisits).map(p => p.id));
|
2016-05-25 23:58:20 +03:00
|
|
|
|
|
|
|
let pageIdsToRemove = pages.filter(p => !p.hasForeign && !p.hasVisits).map(p => p.id);
|
|
|
|
if (pageIdsToRemove.length > 0) {
|
|
|
|
let idsList = sqlList(pageIdsToRemove);
|
|
|
|
// Note, we are already in a transaction, since callers create it.
|
2017-03-23 19:51:52 +03:00
|
|
|
// Check relations regardless, to avoid creating orphans in case of
|
|
|
|
// async race conditions.
|
|
|
|
yield db.execute(`DELETE FROM moz_places WHERE id IN ( ${ idsList } )
|
|
|
|
AND foreign_count = 0 AND last_visit_date ISNULL`);
|
2016-05-25 23:58:20 +03:00
|
|
|
// Hosts accumulated during the places delete are updated through a trigger
|
|
|
|
// (see nsPlacesTriggers.h).
|
|
|
|
yield db.executeCached(`DELETE FROM moz_updatehosts_temp`);
|
|
|
|
|
|
|
|
// Expire orphans.
|
|
|
|
yield db.executeCached(`
|
|
|
|
DELETE FROM moz_favicons WHERE NOT EXISTS
|
|
|
|
(SELECT 1 FROM moz_places WHERE favicon_id = moz_favicons.id)`);
|
|
|
|
yield db.execute(`DELETE FROM moz_annos
|
|
|
|
WHERE place_id IN ( ${ idsList } )`);
|
|
|
|
yield db.execute(`DELETE FROM moz_inputhistory
|
|
|
|
WHERE place_id IN ( ${ idsList } )`);
|
|
|
|
}
|
2015-04-10 12:23:07 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify observers that pages have been removed/updated.
|
|
|
|
*
|
|
|
|
* @param db: (Sqlite connection)
|
|
|
|
* The database.
|
|
|
|
* @param pages: (Array of objects)
|
|
|
|
* Pages that have been touched and that need cleaning up.
|
|
|
|
* Each object should have the following properties:
|
|
|
|
* - id: (number) The `moz_places` identifier for the place.
|
|
|
|
* - hasVisits: (boolean) If `true`, there remains at least one
|
|
|
|
* visit to this page, so the page should be kept and its
|
|
|
|
* frecency updated.
|
|
|
|
* - hasForeign: (boolean) If `true`, the page has at least
|
|
|
|
* one foreign reference (i.e. a bookmark), so the page should
|
|
|
|
* be kept and its frecency updated.
|
|
|
|
* @return (Promise)
|
|
|
|
*/
|
2015-09-15 21:19:45 +03:00
|
|
|
var notifyCleanup = Task.async(function*(db, pages) {
|
2015-04-10 12:23:07 +03:00
|
|
|
let notifiedCount = 0;
|
|
|
|
let observers = PlacesUtils.history.getObservers();
|
|
|
|
|
|
|
|
let reason = Ci.nsINavHistoryObserver.REASON_DELETED;
|
|
|
|
|
|
|
|
for (let page of pages) {
|
|
|
|
let uri = NetUtil.newURI(page.url.href);
|
|
|
|
let guid = page.guid;
|
|
|
|
if (page.hasVisits) {
|
|
|
|
// For the moment, we do not have the necessary observer API
|
|
|
|
// to notify when we remove a subset of visits, see bug 937560.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (page.hasForeign) {
|
|
|
|
// We have removed all visits, but the page is still alive, e.g.
|
|
|
|
// because of a bookmark.
|
|
|
|
notify(observers, "onDeleteVisits",
|
2016-10-24 20:14:19 +03:00
|
|
|
[uri, /* last visit*/0, guid, reason, -1]);
|
2015-04-10 12:23:07 +03:00
|
|
|
} else {
|
|
|
|
// The page has been entirely removed.
|
|
|
|
notify(observers, "onDeleteURI",
|
|
|
|
[uri, guid, reason]);
|
|
|
|
}
|
|
|
|
if (++notifiedCount % NOTIFICATION_CHUNK_SIZE == 0) {
|
|
|
|
// Every few notifications, yield time back to the main
|
|
|
|
// thread to avoid jank.
|
|
|
|
yield Promise.resolve();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notify an `onResult` callback of a set of operations
|
|
|
|
* that just took place.
|
|
|
|
*
|
|
|
|
* @param data: (Array)
|
|
|
|
* The data to send to the callback.
|
|
|
|
* @param onResult: (function [optional])
|
|
|
|
* If provided, call `onResult` with `data[0]`, `data[1]`, etc.
|
|
|
|
* Otherwise, do nothing.
|
|
|
|
*/
|
2015-09-15 21:19:45 +03:00
|
|
|
var notifyOnResult = Task.async(function*(data, onResult) {
|
2015-04-10 12:23:07 +03:00
|
|
|
if (!onResult) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let notifiedCount = 0;
|
|
|
|
for (let info of data) {
|
|
|
|
try {
|
|
|
|
onResult(info);
|
|
|
|
} catch (ex) {
|
|
|
|
// Errors should be reported but should not stop the operation.
|
|
|
|
Promise.reject(ex);
|
|
|
|
}
|
|
|
|
if (++notifiedCount % ONRESULT_CHUNK_SIZE == 0) {
|
|
|
|
// Every few notifications, yield time back to the main
|
|
|
|
// thread to avoid jank.
|
|
|
|
yield Promise.resolve();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Inner implementation of History.removeVisitsByFilter.
|
2015-09-15 21:19:45 +03:00
|
|
|
var removeVisitsByFilter = Task.async(function*(db, filter, onResult = null) {
|
2015-04-10 12:23:07 +03:00
|
|
|
// 1. Determine visits that took place during the interval. Note
|
|
|
|
// that the database uses microseconds, while JS uses milliseconds,
|
|
|
|
// so we need to *1000 one way and /1000 the other way.
|
2016-12-21 01:49:17 +03:00
|
|
|
let conditions = [];
|
|
|
|
let args = {};
|
2015-04-10 12:23:07 +03:00
|
|
|
if ("beginDate" in filter) {
|
2016-12-21 01:49:17 +03:00
|
|
|
conditions.push("v.visit_date >= :begin * 1000");
|
|
|
|
args.begin = Number(filter.beginDate);
|
2015-04-10 12:23:07 +03:00
|
|
|
}
|
|
|
|
if ("endDate" in filter) {
|
2016-12-21 01:49:17 +03:00
|
|
|
conditions.push("v.visit_date <= :end * 1000");
|
|
|
|
args.end = Number(filter.endDate);
|
|
|
|
}
|
|
|
|
if ("limit" in filter) {
|
|
|
|
args.limit = Number(filter.limit);
|
2015-04-10 12:23:07 +03:00
|
|
|
}
|
|
|
|
|
2016-12-21 01:49:17 +03:00
|
|
|
let optionalJoin = "";
|
|
|
|
if ("url" in filter) {
|
|
|
|
let url = filter.url;
|
|
|
|
if (url instanceof Ci.nsIURI) {
|
|
|
|
url = filter.url.spec;
|
|
|
|
} else {
|
|
|
|
url = new URL(url).href;
|
|
|
|
}
|
|
|
|
optionalJoin = `JOIN moz_places h ON h.id = v.place_id`;
|
|
|
|
conditions.push("h.url_hash = hash(:url)", "h.url = :url");
|
|
|
|
args.url = url;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
let visitsToRemove = [];
|
|
|
|
let pagesToInspect = new Set();
|
|
|
|
let onResultData = onResult ? [] : null;
|
|
|
|
|
|
|
|
yield db.executeCached(
|
2016-12-21 01:49:17 +03:00
|
|
|
`SELECT v.id, place_id, visit_date / 1000 AS date, visit_type FROM moz_historyvisits v
|
|
|
|
${optionalJoin}
|
|
|
|
WHERE ${ conditions.join(" AND ") }${ args.limit ? " LIMIT :limit" : "" }`,
|
|
|
|
args,
|
2015-04-10 12:23:07 +03:00
|
|
|
row => {
|
|
|
|
let id = row.getResultByName("id");
|
|
|
|
let place_id = row.getResultByName("place_id");
|
|
|
|
visitsToRemove.push(id);
|
|
|
|
pagesToInspect.add(place_id);
|
|
|
|
|
|
|
|
if (onResult) {
|
|
|
|
onResultData.push({
|
|
|
|
date: new Date(row.getResultByName("date")),
|
|
|
|
transition: row.getResultByName("visit_type")
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (visitsToRemove.length == 0) {
|
|
|
|
// Nothing to do
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let pages = [];
|
|
|
|
yield db.executeTransaction(function*() {
|
|
|
|
// 2. Remove all offending visits.
|
|
|
|
yield db.execute(`DELETE FROM moz_historyvisits
|
|
|
|
WHERE id IN (${ sqlList(visitsToRemove) } )`);
|
|
|
|
|
|
|
|
// 3. Find out which pages have been orphaned
|
|
|
|
yield db.execute(
|
|
|
|
`SELECT id, url, guid,
|
|
|
|
(foreign_count != 0) AS has_foreign,
|
|
|
|
(last_visit_date NOTNULL) as has_visits
|
|
|
|
FROM moz_places
|
|
|
|
WHERE id IN (${ sqlList([...pagesToInspect]) })`,
|
|
|
|
null,
|
|
|
|
row => {
|
|
|
|
let page = {
|
|
|
|
id: row.getResultByName("id"),
|
|
|
|
guid: row.getResultByName("guid"),
|
|
|
|
hasForeign: row.getResultByName("has_foreign"),
|
|
|
|
hasVisits: row.getResultByName("has_visits"),
|
|
|
|
url: new URL(row.getResultByName("url")),
|
|
|
|
};
|
|
|
|
pages.push(page);
|
|
|
|
});
|
|
|
|
|
|
|
|
// 4. Clean up and notify
|
|
|
|
yield cleanupPages(db, pages);
|
|
|
|
});
|
|
|
|
|
|
|
|
notifyCleanup(db, pages);
|
|
|
|
notifyOnResult(onResultData, onResult); // don't wait
|
|
|
|
} finally {
|
|
|
|
// Ensure we cleanup embed visits, even if we bailed out early.
|
|
|
|
PlacesUtils.history.clearEmbedVisits();
|
|
|
|
}
|
|
|
|
|
|
|
|
return visitsToRemove.length != 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2014-10-22 15:36:58 +04:00
|
|
|
// Inner implementation of History.remove.
|
2015-09-15 21:19:45 +03:00
|
|
|
var remove = Task.async(function*(db, {guids, urls}, onResult = null) {
|
2014-10-22 15:36:58 +04:00
|
|
|
// 1. Find out what needs to be removed
|
|
|
|
let query =
|
2016-06-29 15:47:36 +03:00
|
|
|
`SELECT id, url, guid, foreign_count, title, frecency
|
|
|
|
FROM moz_places
|
2014-10-22 15:36:58 +04:00
|
|
|
WHERE guid IN (${ sqlList(guids) })
|
2016-06-29 15:47:36 +03:00
|
|
|
OR (url_hash IN (${ urls.map(u => "hash(" + JSON.stringify(u) + ")").join(",") })
|
|
|
|
AND url IN (${ sqlList(urls) }))
|
|
|
|
`;
|
2014-10-22 15:36:58 +04:00
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
let onResultData = onResult ? [] : null;
|
2014-10-22 15:36:58 +04:00
|
|
|
let pages = [];
|
|
|
|
let hasPagesToRemove = false;
|
|
|
|
yield db.execute(query, null, Task.async(function*(row) {
|
2015-04-10 12:23:07 +03:00
|
|
|
let hasForeign = row.getResultByName("foreign_count") != 0;
|
2016-11-08 16:54:20 +03:00
|
|
|
if (!hasForeign) {
|
2015-04-10 12:23:07 +03:00
|
|
|
hasPagesToRemove = true;
|
2014-10-22 15:36:58 +04:00
|
|
|
}
|
|
|
|
let id = row.getResultByName("id");
|
|
|
|
let guid = row.getResultByName("guid");
|
|
|
|
let url = row.getResultByName("url");
|
|
|
|
let page = {
|
2015-04-10 12:23:07 +03:00
|
|
|
id,
|
|
|
|
guid,
|
|
|
|
hasForeign,
|
|
|
|
hasVisits: false,
|
|
|
|
url: new URL(url),
|
2014-10-22 15:36:58 +04:00
|
|
|
};
|
|
|
|
pages.push(page);
|
|
|
|
if (onResult) {
|
2015-04-10 12:23:07 +03:00
|
|
|
onResultData.push({
|
2016-12-30 02:34:54 +03:00
|
|
|
guid,
|
2014-10-22 15:36:58 +04:00
|
|
|
title: row.getResultByName("title"),
|
|
|
|
frecency: row.getResultByName("frecency"),
|
|
|
|
url: new URL(url)
|
2015-04-10 12:23:07 +03:00
|
|
|
});
|
2014-10-22 15:36:58 +04:00
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
try {
|
|
|
|
if (pages.length == 0) {
|
|
|
|
// Nothing to do
|
|
|
|
return false;
|
2014-10-22 15:36:58 +04:00
|
|
|
}
|
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
yield db.executeTransaction(function*() {
|
|
|
|
// 2. Remove all visits to these pages.
|
|
|
|
yield db.execute(`DELETE FROM moz_historyvisits
|
2015-12-03 13:56:58 +03:00
|
|
|
WHERE place_id IN (${ sqlList(pages.map(p => p.id)) })
|
2014-10-22 15:36:58 +04:00
|
|
|
`);
|
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
// 3. Clean up and notify
|
|
|
|
yield cleanupPages(db, pages);
|
|
|
|
});
|
2014-10-22 15:36:58 +04:00
|
|
|
|
2015-04-10 12:23:07 +03:00
|
|
|
notifyCleanup(db, pages);
|
|
|
|
notifyOnResult(onResultData, onResult); // don't wait
|
|
|
|
} finally {
|
|
|
|
// Ensure we cleanup embed visits, even if we bailed out early.
|
|
|
|
PlacesUtils.history.clearEmbedVisits();
|
|
|
|
}
|
2014-10-22 15:36:58 +04:00
|
|
|
|
|
|
|
return hasPagesToRemove;
|
|
|
|
});
|
2016-05-13 18:09:06 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Merges an updateInfo object, as returned by asyncHistory.updatePlaces
|
|
|
|
* into a PageInfo object as defined in this file.
|
|
|
|
*
|
|
|
|
* @param updateInfo: (Object)
|
|
|
|
* An object that represents a page that is generated by
|
|
|
|
* asyncHistory.updatePlaces.
|
|
|
|
* @param pageInfo: (PageInfo)
|
|
|
|
* An PageInfo object into which to merge the data from updateInfo.
|
|
|
|
* Defaults to an empty object so that this method can be used
|
|
|
|
* to simply convert an updateInfo object into a PageInfo object.
|
|
|
|
*
|
|
|
|
* @return (PageInfo)
|
|
|
|
* A PageInfo object populated with data from updateInfo.
|
|
|
|
*/
|
2016-11-11 01:48:04 +03:00
|
|
|
function mergeUpdateInfoIntoPageInfo(updateInfo, pageInfo = {}) {
|
2016-05-13 18:09:06 +03:00
|
|
|
pageInfo.guid = updateInfo.guid;
|
|
|
|
if (!pageInfo.url) {
|
|
|
|
pageInfo.url = new URL(updateInfo.uri.spec);
|
|
|
|
pageInfo.title = updateInfo.title;
|
|
|
|
pageInfo.visits = updateInfo.visits.map(visit => {
|
|
|
|
return {
|
|
|
|
date: PlacesUtils.toDate(visit.visitDate),
|
|
|
|
transition: visit.transitionType,
|
|
|
|
referrer: (visit.referrerURI) ? new URL(visit.referrerURI.spec) : null
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return pageInfo;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Inner implementation of History.insert.
|
|
|
|
var insert = Task.async(function*(db, pageInfo) {
|
|
|
|
let info = convertForUpdatePlaces(pageInfo);
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
PlacesUtils.asyncHistory.updatePlaces(info, {
|
|
|
|
handleError: error => {
|
|
|
|
reject(error);
|
|
|
|
},
|
|
|
|
handleResult: result => {
|
|
|
|
pageInfo = mergeUpdateInfoIntoPageInfo(result, pageInfo);
|
|
|
|
},
|
|
|
|
handleCompletion: () => {
|
|
|
|
resolve(pageInfo);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// Inner implementation of History.insertMany.
|
|
|
|
var insertMany = Task.async(function*(db, pageInfos, onResult, onError) {
|
|
|
|
let infos = [];
|
|
|
|
let onResultData = [];
|
|
|
|
let onErrorData = [];
|
|
|
|
|
|
|
|
for (let pageInfo of pageInfos) {
|
|
|
|
let info = convertForUpdatePlaces(pageInfo);
|
|
|
|
infos.push(info);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
PlacesUtils.asyncHistory.updatePlaces(infos, {
|
|
|
|
handleError: (resultCode, result) => {
|
|
|
|
let pageInfo = mergeUpdateInfoIntoPageInfo(result);
|
|
|
|
onErrorData.push(pageInfo);
|
|
|
|
},
|
|
|
|
handleResult: result => {
|
|
|
|
let pageInfo = mergeUpdateInfoIntoPageInfo(result);
|
|
|
|
onResultData.push(pageInfo);
|
|
|
|
},
|
|
|
|
handleCompletion: () => {
|
|
|
|
notifyOnResult(onResultData, onResult);
|
|
|
|
notifyOnResult(onErrorData, onError);
|
|
|
|
if (onResultData.length) {
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
reject({message: "No items were added to history."})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|