Skip to content
This repository has been archived by the owner on Feb 29, 2020. It is now read-only.

Bookmarks API #110

Merged
merged 2 commits into from Feb 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions content-src/actions/action-manager.js
Expand Up @@ -7,6 +7,7 @@ const am = new ActionManager([
"RECEIVE_PLACES_CHANGES",
"RECENT_BOOKMARKS_REQUEST",
"RECENT_BOOKMARKS_RESPONSE",
"RECEIVE_BOOKMARKS_CHANGES",
]);

function Response(type, data, options = {}) {
Expand Down
37 changes: 29 additions & 8 deletions lib/ActivityStreams.js
Expand Up @@ -21,7 +21,6 @@ const DEFAULT_OPTIONS = {
};

function ActivityStreams(options = {}) {
console.debug(`ActivityStreams.init`);
this.options = Object.assign({}, DEFAULT_OPTIONS, options);

EventEmitter.decorate(this);
Expand Down Expand Up @@ -56,9 +55,18 @@ ActivityStreams.prototype = {
*/
_respondToPlacesRequests(msgName, params) {
let {msg, worker} = params;
PlacesProvider.links.getTopFrecentSites(msg.data).then(links => {
this.send(am.actions.Response("TOP_FRECENT_SITES_RESPONSE", links), worker);
});
switch (msgName) {
case am.type("TOP_FRECENT_SITES_REQUEST"):
PlacesProvider.links.getTopFrecentSites(msg.data).then(links => {
this.send(am.actions.Response("TOP_FRECENT_SITES_RESPONSE", links), worker);
});
break;
case am.type("RECENT_BOOKMARKS_REQUEST"):
PlacesProvider.links.getRecentBookmarks(msg.data).then(links => {
this.send(am.actions.Response("RECENT_BOOKMARKS_RESPONSE", links), worker);
});
break;
}
},

/**
Expand All @@ -68,6 +76,13 @@ ActivityStreams.prototype = {
this.broadcast(am.actions.Response("RECEIVE_PLACES_CHANGES", data));
},

/**
* Broadcast bookmark changes to pages
*/
_handleBookmarkChanges(eventName, data) {
this.broadcast(am.actions.Response("RECEIVE_BOOKMARKS_CHANGES", data));
},

/**
* Sets up various listeners for the pages
*/
Expand All @@ -76,8 +91,12 @@ ActivityStreams.prototype = {
PlacesProvider.links.on("clearHistory", this._handlePlacesChanges.bind(this));
PlacesProvider.links.on("linkChanged", this._handlePlacesChanges.bind(this));
PlacesProvider.links.on("manyLinksChanged", this._handlePlacesChanges.bind(this));
PlacesProvider.links.on("bookmarkAdded", this._handleBookmarkChanges.bind(this));
PlacesProvider.links.on("bookmarkRemoved", this._handleBookmarkChanges.bind(this));
PlacesProvider.links.on("bookmarkChanged", this._handleBookmarkChanges.bind(this));

this.on(am.type("TOP_FRECENT_SITES_REQUEST"), this._respondToPlacesRequests.bind(this));
this.on(am.type("RECENT_BOOKMARKS_REQUEST"), this._respondToPlacesRequests.bind(this));
},

/**
Expand All @@ -88,8 +107,12 @@ ActivityStreams.prototype = {
PlacesProvider.links.off("clearHistory", this._handlePlacesChanges);
PlacesProvider.links.off("linkChanged", this._handlePlacesChanges);
PlacesProvider.links.off("manyLinksChanged", this._handlePlacesChanges);
PlacesProvider.links.off("bookmarkAdded", this._handleBookmarkChanges);
PlacesProvider.links.off("bookmarkRemoved", this._handleBookmarkChanges);
PlacesProvider.links.off("bookmarkChanged", this._handleBookmarkChanges);

this.off(am.type("TOP_FRECENT_SITES_REQUEST"), this._respondToPlacesRequests);
this.off(am.type("RECENT_BOOKMARKS_REQUEST"), this._respondToPlacesRequests);
},

/**
Expand All @@ -111,19 +134,18 @@ ActivityStreams.prototype = {

worker.port.on("content-to-addon", msg => {
if (!msg.type) {
console.warn(`ActivityStreams.dispatch error: unknown message type`);
Cu.reportError(`ActivityStreams.dispatch error: unknown message type`);
return;
}
// it is important to remove the worker from the set, otherwise we will leak memory
if (msg.type === "pagehide") {
console.debug(`ActivityStreams.pagemod unloading worker`);
this.workers.delete(worker);
}
this.emit(msg.type, {msg, worker});
});
},
onError: err => {
console.error(err);
Cu.reportError(err);
}
});
},
Expand All @@ -132,7 +154,6 @@ ActivityStreams.prototype = {
* Unload the application
*/
unload(reason) { // eslint-disable-line no-unused-vars
console.debug(`ActivityStreams.unload on ${reason}`);

switch (reason){
// can be one of: uninstall/disable/shutdown/upgrade/downgrade
Expand Down
187 changes: 155 additions & 32 deletions lib/PlacesProvider.js
@@ -1,4 +1,4 @@
/* globals XPCOMUtils, Services, gPrincipal, EventEmitter, PlacesUtils, Task */
/* globals XPCOMUtils, Services, gPrincipal, EventEmitter, PlacesUtils, Task, Bookmarks */

"use strict";

Expand All @@ -19,6 +19,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
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);
Expand Down Expand Up @@ -115,7 +118,38 @@ Links.prototype = {
},

QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
Ci.nsISupportsWeakReference])
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])
},

/**
Expand All @@ -124,28 +158,26 @@ Links.prototype = {
*/
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);
},

/**
* Gets the top frecent sites.
*
* @param {Integer} options
* limit: Maximum number of results to return. Max 100.
* @param {Object} options
* options.limit: Maximum number of results to return. Max 100.
*
* @returns {Promise} Returns a promise with the array of links as payload.
*/
getTopFrecentSites: Task.async(function*(options) {

if (!options) {
options = {};
}
getTopFrecentSites: Task.async(function*(options={}) {

let {limit} = options;
if (!limit || limit.options > HISTORY_RESULTS_LIMIT) {
Expand All @@ -158,38 +190,129 @@ Links.prototype = {
// 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, " +
" last_visit_date as lastVisitDate, favicon, mime_type, " +
" \"history\" as type " +
"FROM " +
"( " +
" SELECT rev_host, moz_places.url, moz_favicons.data as favicon, mime_type, title, frecency, last_visit_date " +
" FROM moz_places " +
" LEFT JOIN moz_favicons " +
" ON favicon_id = moz_favicons.id " +
" WHERE hidden = 0 AND last_visit_date NOTNULL " +
" 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 sqlQuery = `SELECT url, title, frecency,
last_visit_date 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
FROM moz_places
LEFT JOIN moz_favicons
ON favicon_id = moz_favicons.id
WHERE hidden = 0 AND last_visit_date NOTNULL
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", "title", "lastVisitDate", "frecency", "favicon", "mime_type", "type"],
params: {limit: limit}
columns: ["url", "title", "lastVisitDate", "frecency", "favicon", "mimeType", "type"],
params: {limit}
});

links = links.map(function(link) {
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 100.
*
* @returns {Promise} Returns a promise with the array of links as payload.
*/
getRecentBookmarks: Task.async(function*(options={}) {
let {limit} = options;
if (!limit || limit.options > HISTORY_RESULTS_LIMIT) {
limit = HISTORY_RESULTS_LIMIT;
}

let sqlQuery = `SELECT p.url, p.title, p.frecency,
p.last_visit_date as lastVisitDate,
b.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
ORDER BY b.lastModified DESC, lastVisitDate DESC, b.id DESC
LIMIT :limit`;

let links = yield this.executePlacesQuery(sqlQuery, {
columns: ["bookmarkId", "bookmarkTitle", "bookmarkGuid", "url", "title", "lastVisitDate", "frecency", "type", "lastModified", "favicon", "mimeType"],
params: {limit, type: Bookmarks.TYPE_BOOKMARK}
});

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.last_visit_date as lastVisitDate,
b.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", "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;
}),
/**
* 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.mime_type};base64,${encodedData}`;
link.favicon = `data:${link.mimeType};base64,${encodedData}`;
}
delete link.mime_type;
delete link.mimeType;
return link;
});

return links.filter(link => LinkChecker.checkLoadURI(link.url));
}),
},

/**
* Executes arbitrary query against places database
Expand Down