789 строки
26 KiB
JavaScript
789 строки
26 KiB
JavaScript
/* globals XPCOMUtils, Services, gPrincipal, EventEmitter, PlacesUtils, Task, Bookmarks */
|
|
|
|
"use strict";
|
|
|
|
const {Ci, Cu} = require("chrome");
|
|
const base64 = require("sdk/base64");
|
|
const simplePrefs = require("sdk/simple-prefs");
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.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, "PlacesUtils",
|
|
"resource://gre/modules/PlacesUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks",
|
|
"resource://gre/modules/Bookmarks.jsm");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "gPrincipal", function() {
|
|
let uri = Services.io.newURI("about:newtab", null, null);
|
|
return Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
|
|
});
|
|
|
|
const {
|
|
LINKS_QUERY_LIMIT,
|
|
FRECENT_RESULTS_TIME_LIMIT,
|
|
HIGHLIGHTS_THRESHOLDS
|
|
} = require("../common/constants");
|
|
|
|
const REV_HOST_BLACKLIST = [
|
|
"moc.elgoog.www.",
|
|
"ac.elgoog.www.",
|
|
"moc.elgoog.radnelac.",
|
|
"moc.elgoog.liam.",
|
|
"moc.oohay.liam.",
|
|
"moc.oohay.hcraes.",
|
|
"tsohlacol.",
|
|
"oc.t.",
|
|
"."
|
|
].map(item => `'${item}'`);
|
|
|
|
const PREF_BLOCKED_URLS = "query.blockedURLs";
|
|
|
|
/**
|
|
* Singleton that manages the list of blocked urls
|
|
*/
|
|
class BlockedURLs extends Set {
|
|
|
|
constructor(prefName = PREF_BLOCKED_URLS) {
|
|
let urls = [];
|
|
try {
|
|
urls = JSON.parse(simplePrefs.prefs[prefName]);
|
|
if (typeof urls[Symbol.iterator] !== "function") {
|
|
urls = [];
|
|
simplePrefs.prefs[prefName] = "[]";
|
|
}
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
simplePrefs.prefs[prefName] = "[]";
|
|
}
|
|
|
|
super(urls);
|
|
|
|
this._prefName = prefName;
|
|
}
|
|
|
|
/**
|
|
* Add url and persist to pref
|
|
*
|
|
* @param {String} url a url to block
|
|
* @returns {Boolean} true if the item has been added
|
|
*/
|
|
save(url) {
|
|
if (!this.has(url)) {
|
|
this.add(url);
|
|
simplePrefs.prefs[this._prefName] = JSON.stringify(this.items());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove a url and persist to pref
|
|
*
|
|
* @param {String} url a url to unblock
|
|
* @returns {Boolean} true if the item has been remove
|
|
*/
|
|
remove(url) {
|
|
if (this.has(url)) {
|
|
this.delete(url);
|
|
simplePrefs.prefs[this._prefName] = JSON.stringify(this.items());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Clear blocked url set and persist
|
|
*
|
|
* @returns {Boolean} whether or not blocklist was cleared
|
|
*/
|
|
clear() {
|
|
if (this.size !== 0) {
|
|
simplePrefs.prefs[this._prefName] = "[]";
|
|
super.clear();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return url set as an array ordered by insertion time
|
|
*/
|
|
items() {
|
|
return [...this];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Singleton that checks if a given link should be displayed on about:newtab
|
|
* or if we should rather not do it for security reasons. URIs that inherit
|
|
* their caller's principal will be filtered.
|
|
*/
|
|
let LinkChecker = {
|
|
_cache: new Map(),
|
|
|
|
get flags() {
|
|
return Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
|
|
Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS;
|
|
},
|
|
|
|
checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
|
|
if (!this._cache.has(aURI)) {
|
|
this._cache.set(aURI, this._doCheckLoadURI(aURI));
|
|
}
|
|
|
|
return this._cache.get(aURI);
|
|
},
|
|
|
|
_doCheckLoadURI: function LinkChecker_doCheckLoadURI(aURI) {
|
|
let result = false;
|
|
// check for 'place:' protocol
|
|
if (aURI.startsWith("place:")) {
|
|
return false;
|
|
}
|
|
try {
|
|
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(gPrincipal, aURI, this.flags);
|
|
result = true;
|
|
} catch (e) {
|
|
// We got a weird URI or one that would inherit the caller's principal.
|
|
Cu.reportError(e);
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/* 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);
|
|
this.blockedURLs = new BlockedURLs();
|
|
};
|
|
|
|
Links.prototype = {
|
|
/**
|
|
* A set of functions called by @mozilla.org/browser/nav-historyservice
|
|
* All history events are emitted from this object.
|
|
*/
|
|
historyObserver: {
|
|
onDeleteURI: function historyObserver_onDeleteURI(aURI) {
|
|
// let observers remove sensitive 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) {
|
|
// The implementation of the query in getLinks excludes hidden and
|
|
// unvisited pages, so it's important to exclude them here, too.
|
|
if (!aHidden && aLastVisitDate) {
|
|
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");
|
|
},
|
|
|
|
onTitleChanged: function historyObserver_onTitleChanged(aURI, aNewTitle) {
|
|
gLinks.emit("linkChanged", {
|
|
url: aURI.spec,
|
|
title: aNewTitle
|
|
});
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
|
|
Ci.nsISupportsWeakReference])
|
|
},
|
|
|
|
/**
|
|
* A set of functions called by @mozilla.org/browser/nav-bookmarks-service
|
|
* All bookmark events are emitted from this object.
|
|
*/
|
|
bookmarksObserver: {
|
|
onItemAdded(id, folderId, index, type) {
|
|
if (type === Bookmarks.TYPE_BOOKMARK) {
|
|
gLinks.getBookmark({id}).then(bookmark => {
|
|
gLinks.emit("bookmarkAdded", bookmark);
|
|
});
|
|
}
|
|
},
|
|
|
|
onItemRemoved(id, folderId, index, type, uri) {
|
|
if (type === Bookmarks.TYPE_BOOKMARK) {
|
|
gLinks.emit("bookmarkRemoved", {bookmarkId: id, url: uri.spec});
|
|
}
|
|
},
|
|
|
|
onItemChanged(id, property, isAnnotation, value, lastModified, type) {
|
|
if (type === Bookmarks.TYPE_BOOKMARK) {
|
|
gLinks.getBookmark({id}).then(bookmark => {
|
|
gLinks.emit("bookmarkChanged", bookmark);
|
|
});
|
|
}
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver,
|
|
Ci.nsISupportsWeakReference])
|
|
},
|
|
|
|
/**
|
|
* Must be called before the provider is used.
|
|
* Makes it easy to disable under pref
|
|
*/
|
|
init: function PlacesProvider_init() {
|
|
PlacesUtils.history.addObserver(this.historyObserver, true);
|
|
PlacesUtils.bookmarks.addObserver(this.bookmarksObserver, true);
|
|
},
|
|
|
|
/**
|
|
* Must be called before the provider is unloaded.
|
|
*/
|
|
uninit: function PlacesProvider_uninit() {
|
|
PlacesUtils.history.removeObserver(this.historyObserver);
|
|
PlacesUtils.bookmarks.removeObserver(this.bookmarksObserver);
|
|
},
|
|
|
|
/**
|
|
* Removes a bookmark
|
|
*
|
|
* @param {String} bookmarkGuid the bookmark guid
|
|
* @returns {Promise} Returns a promise set to an object representing the removed bookmark
|
|
*/
|
|
asyncDeleteBookmark: function PlacesProvider_asyncDeleteBookmark(bookmarkGuid) {
|
|
return Bookmarks.remove(bookmarkGuid);
|
|
},
|
|
|
|
/**
|
|
* Adds a bookmark
|
|
*
|
|
* @param {String} url the url to bookmark
|
|
* @returns {Promise} Returns a promise set to an object representing the bookmark
|
|
*/
|
|
asyncAddBookmark: function PlacesProvider_asyncAddBookmark(url) {
|
|
return Bookmarks.insert({url, type: Bookmarks.TYPE_BOOKMARK, parentGuid: Bookmarks.unfiledGuid});
|
|
},
|
|
|
|
/**
|
|
* Removes a history link
|
|
*
|
|
* @param {String} url
|
|
* @returns {Promise} Returns a promise set to true if link was removed
|
|
*/
|
|
deleteHistoryLink: function PlacesProvider_deleteHistoryLink(url) {
|
|
return PlacesUtils.history.remove(url);
|
|
},
|
|
|
|
/**
|
|
* Blocks a URL
|
|
*/
|
|
blockURL(url) {
|
|
if (this.blockedURLs.save(url)) {
|
|
this.emit("linkChanged", {url, blocked: true});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unblocks a URL
|
|
*/
|
|
unblockURL(url) {
|
|
if (this.blockedURLs.remove(url)) {
|
|
this.emit("linkChanged", {url, blocked: false});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unblocks all URLs
|
|
*/
|
|
unblockAll(url) {
|
|
if (this.blockedURLs.clear()) {
|
|
this.emit("manyLinksChanged");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gets links from history based on supplied options
|
|
*
|
|
* @param {Object} options
|
|
* {Integer} limit: Maximum number of results to return. Max 20
|
|
* {Milliseconds} beforeDate: Only return links older than beforeDate
|
|
* {Milliseconds} afterDate: Only return links fresher than afterDate
|
|
* {String} order: if order=="bookmarksFrecency", orders by bookmarks and frecency, otherwise by recency
|
|
*
|
|
* @returns {Promise} Returns a promise with the array of links as payload.
|
|
*/
|
|
_getHistoryLinks: Task.async(function*(options = {}) {
|
|
let {limit, afterDate, beforeDate, order, ignoreBlocked} = options;
|
|
if (!limit || limit.options > LINKS_QUERY_LIMIT) {
|
|
limit = LINKS_QUERY_LIMIT;
|
|
}
|
|
|
|
let blockedURLs = ignoreBlocked ? [] : this.blockedURLs.items().map(item => `"${item}"`);
|
|
|
|
// setup binding parameters
|
|
let params = {limit};
|
|
|
|
// setup afterDate binding and sql clause
|
|
let afterDateClause = "";
|
|
if (afterDate) {
|
|
afterDateClause = "AND moz_places.last_visit_date > :afterDate * 1000";
|
|
params.afterDate = afterDate;
|
|
}
|
|
|
|
// setup beforeDate binding and sql clause
|
|
let beforeDateClause = "";
|
|
if (beforeDate) {
|
|
beforeDateClause = "AND moz_places.last_visit_date < :beforeDate * 1000";
|
|
params.beforeDate = beforeDate;
|
|
}
|
|
|
|
// setup order by clause
|
|
let orderbyClause = (order === "bookmarksFrecency") ?
|
|
"ORDER BY bookmarkDateCreated DESC, frecency DESC, lastVisitDate DESC, url" :
|
|
"ORDER BY lastVisitDate DESC, frecency DESC, url";
|
|
|
|
// construct sql query
|
|
let sqlQuery = `SELECT moz_places.url as url,
|
|
moz_places.guid as guid,
|
|
moz_favicons.data as favicon,
|
|
moz_favicons.mime_type as mimeType,
|
|
moz_places.title,
|
|
moz_places.frecency,
|
|
moz_places.last_visit_date / 1000 as lastVisitDate,
|
|
"history" as type,
|
|
moz_bookmarks.guid as bookmarkGuid,
|
|
moz_bookmarks.dateAdded / 1000 as bookmarkDateCreated
|
|
FROM moz_places
|
|
LEFT JOIN moz_favicons
|
|
ON moz_places.favicon_id = moz_favicons.id
|
|
LEFT JOIN moz_bookmarks
|
|
ON moz_places.id = moz_bookmarks.fk
|
|
WHERE hidden = 0 AND last_visit_date NOTNULL
|
|
AND moz_places.url NOT IN (${blockedURLs})
|
|
${afterDateClause}
|
|
${beforeDateClause}
|
|
${orderbyClause}
|
|
LIMIT :limit`;
|
|
|
|
let links = yield this.executePlacesQuery(sqlQuery, {
|
|
columns: ["url", "guid", "favicon", "mimeType", "title", "lastVisitDate",
|
|
"frecency", "type", "bookmarkGuid", "bookmarkDateCreated"],
|
|
params
|
|
});
|
|
|
|
links = this._faviconBytesToDataURI(links);
|
|
return links.filter(link => LinkChecker.checkLoadURI(link.url));
|
|
}),
|
|
|
|
/**
|
|
* Gets the top recent links
|
|
*
|
|
* @param {Integer} options
|
|
* limit: Maximum number of results to return. Max 20.
|
|
*
|
|
* @returns {Promise} Returns a promise with the array of links as payload.
|
|
*/
|
|
getRecentLinks: function PlacesProvider_getRecentLinks(options) {
|
|
return this._getHistoryLinks(options);
|
|
},
|
|
|
|
/**
|
|
* Gets the top frecent links - visited in last 72 hours and ordered by frecency
|
|
*
|
|
* @param {Integer} options
|
|
* limit: Maximum number of results to return. Max 20.
|
|
*
|
|
* @returns {Promise} Returns a promise with the array of links as payload.
|
|
*/
|
|
getFrecentLinks: function PlacesProvider_getFrecentLinks(options = {}) {
|
|
options.order = "bookmarksFrecency";
|
|
options.afterDate = (new Date()).getTime() - FRECENT_RESULTS_TIME_LIMIT;
|
|
return this._getHistoryLinks(options);
|
|
},
|
|
|
|
/**
|
|
* Gets the top frecent sites.
|
|
*
|
|
* @param {Object} options
|
|
* options.limit: Maximum number of results to return. Max 20.
|
|
*
|
|
* @returns {Promise} Returns a promise with the array of links as payload.
|
|
*/
|
|
getTopFrecentSites: Task.async(function*(options = {}) {
|
|
let {limit, ignoreBlocked} = options;
|
|
if (!limit || limit.options > LINKS_QUERY_LIMIT) {
|
|
limit = LINKS_QUERY_LIMIT;
|
|
}
|
|
|
|
let blockedURLs = ignoreBlocked ? [] : this.blockedURLs.items().map(item => `"${item}"`);
|
|
|
|
// this query does "GROUP BY rev_host" to remove urls from same domain.
|
|
// Note that unlike mysql, sqlite picks the last raw from groupby bucket.
|
|
// Which is why subselect orders frecency and last_visit_date backwards.
|
|
// In general the groupby behavior in the absence of aggregates is not
|
|
// defined in SQL, hence we are relying on sqlite implementation that may
|
|
// change in the future.
|
|
let sqlQuery = `SELECT url, title, frecency, guid,
|
|
last_visit_date / 1000 as lastVisitDate, favicon, mimeType,
|
|
"history" as type
|
|
FROM
|
|
(
|
|
SELECT rev_host, moz_places.url, moz_favicons.data as favicon, mime_type as mimeType, title, frecency, last_visit_date, moz_places.guid as guid
|
|
FROM moz_places
|
|
LEFT JOIN moz_favicons
|
|
ON favicon_id = moz_favicons.id
|
|
WHERE hidden = 0 AND last_visit_date NOTNULL
|
|
AND moz_places.url NOT IN (${blockedURLs})
|
|
ORDER BY rev_host, frecency, last_visit_date, moz_places.url DESC
|
|
)
|
|
GROUP BY rev_host
|
|
ORDER BY frecency DESC, lastVisitDate DESC, url
|
|
LIMIT :limit`;
|
|
|
|
let links = yield this.executePlacesQuery(sqlQuery, {
|
|
columns: ["url", "guid", "title", "lastVisitDate", "frecency", "favicon", "mimeType", "type"],
|
|
params: {limit}
|
|
});
|
|
|
|
links = this._faviconBytesToDataURI(links);
|
|
return links.filter(link => LinkChecker.checkLoadURI(link.url));
|
|
}),
|
|
|
|
/**
|
|
* Gets the most recent bookmarks
|
|
*
|
|
* @param {Object} options
|
|
* options.limit: Maximum number of results to return. Max 20.
|
|
*
|
|
* @returns {Promise} Returns a promise with the array of links as payload.
|
|
*/
|
|
getRecentBookmarks: Task.async(function*(options = {}) {
|
|
let {limit, afterDate, beforeDate, ignoreBlocked} = options;
|
|
if (!limit || limit.options > LINKS_QUERY_LIMIT) {
|
|
limit = LINKS_QUERY_LIMIT;
|
|
}
|
|
|
|
let blockedURLs = ignoreBlocked ? [] : this.blockedURLs.items().map(item => `"${item}"`);
|
|
|
|
// setup binding parameters
|
|
let params = {limit, type: Bookmarks.TYPE_BOOKMARK};
|
|
|
|
// setup afterDate binding and sql clause
|
|
let afterDateClause = "";
|
|
if (afterDate) {
|
|
afterDateClause = "AND b.lastModified > :afterDate * 1000";
|
|
params.afterDate = afterDate;
|
|
}
|
|
|
|
// setup beforeDate binding and sql clause
|
|
let beforeDateClause = "";
|
|
if (beforeDate) {
|
|
beforeDateClause = "AND b.lastModified < :beforeDate * 1000";
|
|
params.beforeDate = beforeDate;
|
|
}
|
|
|
|
let sqlQuery = `SELECT p.url, p.title, p.frecency, p.guid,
|
|
p.last_visit_date / 1000 as lastVisitDate,
|
|
b.lastModified / 1000 as lastModified,
|
|
b.dateAdded / 1000 as bookmarkDateCreated,
|
|
b.id as bookmarkId,
|
|
b.title as bookmarkTitle,
|
|
b.guid as bookmarkGuid,
|
|
"bookmark" as type,
|
|
f.data as favicon,
|
|
f.mime_type as mimeType
|
|
FROM moz_places p, moz_bookmarks b
|
|
LEFT JOIN moz_favicons f
|
|
ON p.favicon_id = f.id
|
|
WHERE b.fk = p.id
|
|
AND p.url NOT IN (${blockedURLs})
|
|
AND p.rev_host IS NOT NULL
|
|
AND b.type = :type
|
|
${afterDateClause}
|
|
${beforeDateClause}
|
|
ORDER BY bookmarkDateCreated DESC, b.lastModified DESC, lastVisitDate DESC, b.id DESC
|
|
LIMIT :limit`;
|
|
|
|
let links = yield this.executePlacesQuery(sqlQuery, {
|
|
columns: ["bookmarkId", "bookmarkTitle", "bookmarkGuid", "bookmarkDateCreated", "url", "guid",
|
|
"title", "lastVisitDate", "frecency", "type", "lastModified", "favicon", "mimeType"],
|
|
params
|
|
});
|
|
|
|
links = this._faviconBytesToDataURI(links);
|
|
return links.filter(link => LinkChecker.checkLoadURI(link.url));
|
|
}),
|
|
|
|
/**
|
|
* Gets a specific bookmark given an id
|
|
*
|
|
* @param {Object} options
|
|
* options.id: bookmark ID
|
|
*/
|
|
getBookmark: Task.async(function*(options = {}) {
|
|
let {id} = options;
|
|
|
|
let sqlQuery = `SELECT p.url, p.title, p.frecency, p.guid,
|
|
p.last_visit_date / 1000 as lastVisitDate,
|
|
b.lastModified / 1000 as lastModified,
|
|
b.id as bookmarkId,
|
|
b.title as bookmarkTitle,
|
|
b.guid as bookmarkGuid,
|
|
"bookmark" as type,
|
|
f.data as favicon,
|
|
f.mime_type as mimeType
|
|
FROM moz_places p, moz_bookmarks b
|
|
LEFT JOIN moz_favicons f
|
|
ON p.favicon_id = f.id
|
|
WHERE b.fk = p.id
|
|
AND p.rev_host IS NOT NULL
|
|
AND b.type = :type
|
|
AND b.id = :id
|
|
ORDER BY b.lastModified, lastVisitDate DESC, b.id`;
|
|
|
|
let links = yield this.executePlacesQuery(sqlQuery, {
|
|
columns: ["bookmarkId", "bookmarkTitle", "bookmarkGuid", "url", "guid", "title", "lastVisitDate", "frecency", "type", "lastModified", "favicon", "mimeType"],
|
|
params: {id, type: Bookmarks.TYPE_BOOKMARK}
|
|
});
|
|
|
|
links = this._faviconBytesToDataURI(links);
|
|
links.filter(link => LinkChecker.checkLoadURI(link.url));
|
|
if (links.length) {
|
|
return links[0];
|
|
}
|
|
return null;
|
|
}),
|
|
|
|
/**
|
|
* Obtain a set of links for highlights
|
|
*
|
|
* @param {Object} options
|
|
* {Integer} limit: Maximum number of results to return. Max 20
|
|
*
|
|
* @returns {Promise} Returns a promise with the array of links as payload.
|
|
*/
|
|
getHighlightsLinks: Task.async(function*(options = {}) {
|
|
let {limit, ignoreBlocked} = options;
|
|
if (!limit || limit.options > LINKS_QUERY_LIMIT) {
|
|
limit = LINKS_QUERY_LIMIT;
|
|
}
|
|
|
|
let blockedURLs = ignoreBlocked ? [] : this.blockedURLs.items().map(item => `"${item}"`);
|
|
|
|
let params = {limitBookmarks: 1, limitHistory: (limit - 1)};
|
|
|
|
let sqlQuery = `SELECT DISTINCT * FROM (
|
|
SELECT * FROM (
|
|
SELECT p.url as url,
|
|
p.guid as guid,
|
|
f.data as favicon,
|
|
f.mime_type as mimeType,
|
|
p.title as title,
|
|
p.frecency as frecency,
|
|
p.last_visit_date / 1000 as lastVisitDate,
|
|
"bookmark" as type,
|
|
b.id as bookmarkId,
|
|
b.guid as bookmarkGuid,
|
|
b.title as bookmarkTitle,
|
|
b.lastModified / 1000 as lastModified,
|
|
b.dateAdded / 1000 as bookmarkDateCreated
|
|
FROM moz_places p
|
|
INNER JOIN moz_bookmarks b
|
|
ON b.fk = p.id
|
|
LEFT JOIN moz_favicons f
|
|
ON p.favicon_id = f.id
|
|
WHERE date(b.dateAdded / 1000 / 1000, 'unixepoch') > date('now', '${HIGHLIGHTS_THRESHOLDS.created}')
|
|
AND p.url NOT IN (${blockedURLs})
|
|
AND p.visit_count <= 3
|
|
ORDER BY b.dateAdded DESC
|
|
LIMIT :limitBookmarks
|
|
)
|
|
|
|
UNION ALL
|
|
|
|
SELECT * FROM (
|
|
SELECT p.url as url,
|
|
p.guid as guid,
|
|
f.data as favicon,
|
|
f.mime_type as mimeType,
|
|
p.title as title,
|
|
p.frecency as frecency,
|
|
p.last_visit_date / 1000 as lastVisitDate,
|
|
"history" as type,
|
|
b.id as bookmarkId,
|
|
b.guid as bookmarkGuid,
|
|
b.title as bookmarkTitle,
|
|
b.lastModified / 1000 as lastModified,
|
|
b.dateAdded / 1000 as bookmarkDateCreated
|
|
FROM moz_places p
|
|
LEFT JOIN moz_bookmarks b
|
|
ON b.fk = p.id
|
|
LEFT JOIN moz_favicons f
|
|
ON p.favicon_id = f.id
|
|
WHERE datetime(p.last_visit_date / 1000 / 1000, 'unixepoch') < datetime('now', '${HIGHLIGHTS_THRESHOLDS.visited}')
|
|
AND p.url NOT IN (${blockedURLs})
|
|
AND p.visit_count <= 3
|
|
AND p.title NOT NULL
|
|
AND p.rev_host NOT IN (${REV_HOST_BLACKLIST})
|
|
GROUP BY p.rev_host
|
|
ORDER BY p.last_visit_date DESC
|
|
LIMIT :limitHistory
|
|
)
|
|
)`;
|
|
|
|
let links = yield this.executePlacesQuery(sqlQuery, {
|
|
columns: ["bookmarkId", "bookmarkTitle", "bookmarkGuid", "bookmarkDateCreated", "url", "guid",
|
|
"title", "lastVisitDate", "frecency", "type", "lastModified", "favicon", "mimeType"],
|
|
params
|
|
});
|
|
|
|
links = this._faviconBytesToDataURI(links);
|
|
return links.filter(link => LinkChecker.checkLoadURI(link.url));
|
|
}),
|
|
|
|
/**
|
|
* From an Array of links, if favicons are present, convert to data URIs
|
|
*
|
|
* @param {Array} links
|
|
* an array containing objects with favicon data and mimeTypes
|
|
*
|
|
* @returns {Array} an array of links with favicons as data uri
|
|
*/
|
|
_faviconBytesToDataURI(links) {
|
|
return links.map(link => {
|
|
if (link.favicon) {
|
|
let encodedData = base64.encode(String.fromCharCode.apply(null, link.favicon));
|
|
link.favicon = `data:${link.mimeType};base64,${encodedData}`;
|
|
}
|
|
delete link.mimeType;
|
|
return link;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Gets History size
|
|
*
|
|
* @returns {Promise} Returns a promise with the count of moz_places records
|
|
*/
|
|
getHistorySize: Task.async(function*(options = {}) {
|
|
let {ignoreBlocked} = options;
|
|
let blockedURLs = ignoreBlocked ? [] : this.blockedURLs.items().map(item => `"${item}"`);
|
|
|
|
let sqlQuery = `SELECT count(1) as count
|
|
FROM moz_places
|
|
WHERE hidden = 0 AND last_visit_date NOT NULL
|
|
AND url NOT IN (${blockedURLs})`;
|
|
|
|
let result = yield this.executePlacesQuery(sqlQuery);
|
|
return result[0][0];
|
|
}),
|
|
|
|
/**
|
|
* Gets Bookmarks count
|
|
*
|
|
* @returns {Promise} Returns a promise with the count of bookmarks
|
|
*/
|
|
getBookmarksSize: Task.async(function*(options = {}) {
|
|
let {ignoreBlocked} = options;
|
|
let blockedURLs = ignoreBlocked ? [] : this.blockedURLs.items().map(item => `"${item}"`);
|
|
|
|
let sqlQuery = `SELECT count(1) as count
|
|
FROM moz_bookmarks b, moz_places p
|
|
WHERE type = :type
|
|
AND b.fk = p.id
|
|
AND p.url NOT IN (${blockedURLs})`;
|
|
|
|
let result = yield this.executePlacesQuery(sqlQuery, {params: {type: Bookmarks.TYPE_BOOKMARK}});
|
|
return result[0][0];
|
|
}),
|
|
|
|
/**
|
|
* 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 return
|
|
* items will consists of objects keyed on column names. Otherwise
|
|
* 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 raws
|
|
*
|
|
* @returns {Promise} Returns a promise with the array of retrieved items
|
|
*/
|
|
executePlacesQuery: function PlacesProvider_executePlacesQuery(aSql, aOptions = {}) {
|
|
let {columns, params, callback} = aOptions;
|
|
let items = [];
|
|
let queryError = null;
|
|
return Task.spawn(function*() {
|
|
let conn = yield PlacesUtils.promiseDBConnection();
|
|
yield conn.executeCached(aSql, params, aRow => {
|
|
try {
|
|
// check if caller wants to handle query raws
|
|
if (callback) {
|
|
callback(aRow);
|
|
}
|
|
// otherwise fill in the item and add items array
|
|
else {
|
|
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();
|
|
|
|
exports.PlacesProvider = {
|
|
LinkChecker,
|
|
links: gLinks,
|
|
BlockedURLs
|
|
};
|