mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1618 lines
51 KiB
JavaScript
1618 lines
51 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Bookmarks Sync.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* the Mozilla Foundation.
|
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Dan Mills <thunder@mozilla.com>
|
|
* Jono DiCarlo <jdicarlo@mozilla.org>
|
|
* Anant Narayanan <anant@kix.in>
|
|
* Philipp von Weitershausen <philipp@weitershausen.de>
|
|
* Richard Newman <rnewman@mozilla.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
const EXPORTED_SYMBOLS = ['BookmarksEngine', "PlacesItem", "Bookmark",
|
|
"BookmarkFolder", "BookmarkMicsum", "BookmarkQuery",
|
|
"Livemark", "BookmarkSeparator"];
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cu = Components.utils;
|
|
|
|
const GUID_ANNO = "sync/guid";
|
|
const MOBILE_ANNO = "mobile/bookmarksRoot";
|
|
const PARENT_ANNO = "sync/parent";
|
|
const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
|
|
const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
|
|
const FOLDER_SORTINDEX = 1000000;
|
|
|
|
try {
|
|
Cu.import("resource://gre/modules/PlacesUtils.jsm");
|
|
}
|
|
catch(ex) {
|
|
Cu.import("resource://gre/modules/utils.js");
|
|
}
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://services-sync/engines.js");
|
|
Cu.import("resource://services-sync/record.js");
|
|
Cu.import("resource://services-sync/util.js");
|
|
|
|
Cu.import("resource://services-sync/main.js"); // For access to Service.
|
|
|
|
function PlacesItem(collection, id, type) {
|
|
CryptoWrapper.call(this, collection, id);
|
|
this.type = type || "item";
|
|
}
|
|
PlacesItem.prototype = {
|
|
decrypt: function PlacesItem_decrypt() {
|
|
// Do the normal CryptoWrapper decrypt, but change types before returning
|
|
let clear = CryptoWrapper.prototype.decrypt.apply(this, arguments);
|
|
|
|
// Convert the abstract places item to the actual object type
|
|
if (!this.deleted)
|
|
this.__proto__ = this.getTypeObject(this.type).prototype;
|
|
|
|
return clear;
|
|
},
|
|
|
|
getTypeObject: function PlacesItem_getTypeObject(type) {
|
|
switch (type) {
|
|
case "bookmark":
|
|
return Bookmark;
|
|
case "microsummary":
|
|
return BookmarkMicsum;
|
|
case "query":
|
|
return BookmarkQuery;
|
|
case "folder":
|
|
return BookmarkFolder;
|
|
case "livemark":
|
|
return Livemark;
|
|
case "separator":
|
|
return BookmarkSeparator;
|
|
case "item":
|
|
return PlacesItem;
|
|
}
|
|
throw "Unknown places item object type: " + type;
|
|
},
|
|
|
|
__proto__: CryptoWrapper.prototype,
|
|
_logName: "Record.PlacesItem",
|
|
};
|
|
|
|
Utils.deferGetSet(PlacesItem, "cleartext", ["hasDupe", "parentid", "parentName",
|
|
"type"]);
|
|
|
|
function Bookmark(collection, id, type) {
|
|
PlacesItem.call(this, collection, id, type || "bookmark");
|
|
}
|
|
Bookmark.prototype = {
|
|
__proto__: PlacesItem.prototype,
|
|
_logName: "Record.Bookmark",
|
|
};
|
|
|
|
Utils.deferGetSet(Bookmark, "cleartext", ["title", "bmkUri", "description",
|
|
"loadInSidebar", "tags", "keyword"]);
|
|
|
|
function BookmarkMicsum(collection, id) {
|
|
Bookmark.call(this, collection, id, "microsummary");
|
|
}
|
|
BookmarkMicsum.prototype = {
|
|
__proto__: Bookmark.prototype,
|
|
_logName: "Record.BookmarkMicsum",
|
|
};
|
|
|
|
Utils.deferGetSet(BookmarkMicsum, "cleartext", ["generatorUri", "staticTitle"]);
|
|
|
|
function BookmarkQuery(collection, id) {
|
|
Bookmark.call(this, collection, id, "query");
|
|
}
|
|
BookmarkQuery.prototype = {
|
|
__proto__: Bookmark.prototype,
|
|
_logName: "Record.BookmarkQuery",
|
|
};
|
|
|
|
Utils.deferGetSet(BookmarkQuery, "cleartext", ["folderName",
|
|
"queryId"]);
|
|
|
|
function BookmarkFolder(collection, id, type) {
|
|
PlacesItem.call(this, collection, id, type || "folder");
|
|
}
|
|
BookmarkFolder.prototype = {
|
|
__proto__: PlacesItem.prototype,
|
|
_logName: "Record.Folder",
|
|
};
|
|
|
|
Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title",
|
|
"children"]);
|
|
|
|
function Livemark(collection, id) {
|
|
BookmarkFolder.call(this, collection, id, "livemark");
|
|
}
|
|
Livemark.prototype = {
|
|
__proto__: BookmarkFolder.prototype,
|
|
_logName: "Record.Livemark",
|
|
};
|
|
|
|
Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]);
|
|
|
|
function BookmarkSeparator(collection, id) {
|
|
PlacesItem.call(this, collection, id, "separator");
|
|
}
|
|
BookmarkSeparator.prototype = {
|
|
__proto__: PlacesItem.prototype,
|
|
_logName: "Record.Separator",
|
|
};
|
|
|
|
Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
|
|
|
|
|
|
function archiveBookmarks() {
|
|
// Some nightly builds of 3.7 don't have this function
|
|
try {
|
|
PlacesUtils.archiveBookmarksFile(null, true);
|
|
}
|
|
catch(ex) {}
|
|
}
|
|
|
|
let kSpecialIds = {
|
|
|
|
// Special IDs. Note that mobile can attempt to create a record on
|
|
// dereference; special accessors are provided to prevent recursion within
|
|
// observers.
|
|
get guids()
|
|
["menu", "places", "tags", "toolbar", "unfiled", "mobile"],
|
|
|
|
// Create the special mobile folder to store mobile bookmarks.
|
|
createMobileRoot: function createMobileRoot() {
|
|
let root = Svc.Bookmark.placesRoot;
|
|
let mRoot = Svc.Bookmark.createFolder(root, "mobile", -1);
|
|
Utils.anno(mRoot, MOBILE_ANNO, 1);
|
|
return mRoot;
|
|
},
|
|
|
|
findMobileRoot: function findMobileRoot(create) {
|
|
// Use the (one) mobile root if it already exists.
|
|
let root = Svc.Annos.getItemsWithAnnotation(MOBILE_ANNO, {});
|
|
if (root.length != 0)
|
|
return root[0];
|
|
|
|
if (create)
|
|
return this.createMobileRoot();
|
|
|
|
return null;
|
|
},
|
|
|
|
// Accessors for IDs.
|
|
isSpecialGUID: function isSpecialGUID(g) {
|
|
return this.guids.indexOf(g) != -1;
|
|
},
|
|
|
|
specialIdForGUID: function specialIdForGUID(guid, create) {
|
|
if (guid == "mobile") {
|
|
return this.findMobileRoot(create);
|
|
}
|
|
return this[guid];
|
|
},
|
|
|
|
// Don't bother creating mobile: if it doesn't exist, this ID can't be it!
|
|
specialGUIDForId: function specialGUIDForId(id) {
|
|
for each (let guid in this.guids)
|
|
if (this.specialIdForGUID(guid, false) == id)
|
|
return guid;
|
|
return null;
|
|
},
|
|
|
|
get menu() Svc.Bookmark.bookmarksMenuFolder,
|
|
get places() Svc.Bookmark.placesRoot,
|
|
get tags() Svc.Bookmark.tagsFolder,
|
|
get toolbar() Svc.Bookmark.toolbarFolder,
|
|
get unfiled() Svc.Bookmark.unfiledBookmarksFolder,
|
|
get mobile() this.findMobileRoot(true),
|
|
};
|
|
|
|
function BookmarksEngine() {
|
|
SyncEngine.call(this, "Bookmarks");
|
|
}
|
|
BookmarksEngine.prototype = {
|
|
__proto__: SyncEngine.prototype,
|
|
_recordObj: PlacesItem,
|
|
_storeObj: BookmarksStore,
|
|
_trackerObj: BookmarksTracker,
|
|
version: 2,
|
|
|
|
_sync: Utils.batchSync("Bookmark", SyncEngine),
|
|
|
|
_syncStartup: function _syncStart() {
|
|
SyncEngine.prototype._syncStartup.call(this);
|
|
|
|
// For first-syncs, make a backup for the user to restore
|
|
if (this.lastSync == 0)
|
|
archiveBookmarks();
|
|
|
|
// Lazily create a mapping of folder titles and separator positions to GUID
|
|
this.__defineGetter__("_lazyMap", function() {
|
|
delete this._lazyMap;
|
|
|
|
let lazyMap = {};
|
|
for (let guid in this._store.getAllIDs()) {
|
|
// Figure out what key to store the mapping
|
|
let key;
|
|
let id = this._store.idForGUID(guid);
|
|
switch (Svc.Bookmark.getItemType(id)) {
|
|
case Svc.Bookmark.TYPE_BOOKMARK:
|
|
|
|
// Smart bookmarks map to their annotation value.
|
|
let queryId;
|
|
try {
|
|
queryId = Utils.anno(id, SMART_BOOKMARKS_ANNO);
|
|
} catch(ex) {}
|
|
|
|
if (queryId)
|
|
key = "q" + queryId;
|
|
else
|
|
key = "b" + Svc.Bookmark.getBookmarkURI(id).spec + ":" +
|
|
Svc.Bookmark.getItemTitle(id);
|
|
break;
|
|
case Svc.Bookmark.TYPE_FOLDER:
|
|
key = "f" + Svc.Bookmark.getItemTitle(id);
|
|
break;
|
|
case Svc.Bookmark.TYPE_SEPARATOR:
|
|
key = "s" + Svc.Bookmark.getItemIndex(id);
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
// The mapping is on a per parent-folder-name basis
|
|
let parent = Svc.Bookmark.getFolderIdForItem(id);
|
|
if (parent <= 0)
|
|
continue;
|
|
|
|
let parentName = Svc.Bookmark.getItemTitle(parent);
|
|
if (lazyMap[parentName] == null)
|
|
lazyMap[parentName] = {};
|
|
|
|
// If the entry already exists, remember that there are explicit dupes
|
|
let entry = new String(guid);
|
|
entry.hasDupe = lazyMap[parentName][key] != null;
|
|
|
|
// Remember this item's guid for its parent-name/key pair
|
|
lazyMap[parentName][key] = entry;
|
|
this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]);
|
|
}
|
|
|
|
// Expose a helper function to get a dupe guid for an item
|
|
return this._lazyMap = function(item) {
|
|
// Figure out if we have something to key with
|
|
let key;
|
|
let altKey;
|
|
switch (item.type) {
|
|
case "query":
|
|
// Prior to Bug 610501, records didn't carry their Smart Bookmark
|
|
// anno, so we won't be able to dupe them correctly. This altKey
|
|
// hack should get them to dupe correctly.
|
|
if (item.queryId) {
|
|
key = "q" + item.queryId;
|
|
altKey = "b" + item.bmkUri + ":" + item.title;
|
|
break;
|
|
}
|
|
// No queryID? Fall through to the regular bookmark case.
|
|
case "bookmark":
|
|
case "microsummary":
|
|
key = "b" + item.bmkUri + ":" + item.title;
|
|
break;
|
|
case "folder":
|
|
case "livemark":
|
|
key = "f" + item.title;
|
|
break;
|
|
case "separator":
|
|
key = "s" + item.pos;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// Give the guid if we have the matching pair
|
|
this._log.trace("Finding mapping: " + item.parentName + ", " + key);
|
|
let parent = lazyMap[item.parentName];
|
|
|
|
if (!parent) {
|
|
this._log.trace("No parent => no dupe.");
|
|
return undefined;
|
|
}
|
|
|
|
let dupe = parent[key];
|
|
|
|
if (dupe) {
|
|
this._log.trace("Mapped dupe: " + dupe);
|
|
return dupe;
|
|
}
|
|
|
|
if (altKey) {
|
|
dupe = parent[altKey];
|
|
if (dupe) {
|
|
this._log.trace("Mapped dupe using altKey " + altKey + ": " + dupe);
|
|
return dupe;
|
|
}
|
|
}
|
|
|
|
this._log.trace("No dupe found for key " + key + "/" + altKey + ".");
|
|
return undefined;
|
|
};
|
|
});
|
|
|
|
this._store._childrenToOrder = {};
|
|
},
|
|
|
|
_processIncoming: function _processIncoming() {
|
|
try {
|
|
SyncEngine.prototype._processIncoming.call(this);
|
|
} finally {
|
|
// Reorder children.
|
|
this._tracker.ignoreAll = true;
|
|
this._store._orderChildren();
|
|
this._tracker.ignoreAll = false;
|
|
delete this._store._childrenToOrder;
|
|
}
|
|
},
|
|
|
|
_syncFinish: function _syncFinish() {
|
|
SyncEngine.prototype._syncFinish.call(this);
|
|
delete this._lazyMap;
|
|
this._tracker._ensureMobileQuery();
|
|
},
|
|
|
|
_createRecord: function _createRecord(id) {
|
|
// Create the record like normal but mark it as having dupes if necessary
|
|
let record = SyncEngine.prototype._createRecord.call(this, id);
|
|
let entry = this._lazyMap(record);
|
|
if (entry != null && entry.hasDupe)
|
|
record.hasDupe = true;
|
|
return record;
|
|
},
|
|
|
|
_findDupe: function _findDupe(item) {
|
|
// Don't bother finding a dupe if the incoming item has duplicates
|
|
if (item.hasDupe)
|
|
return;
|
|
return this._lazyMap(item);
|
|
},
|
|
|
|
_handleDupe: function _handleDupe(item, dupeId) {
|
|
// Always change the local GUID to the incoming one.
|
|
this._store.changeItemID(dupeId, item.id);
|
|
this._deleteId(dupeId);
|
|
this._tracker.addChangedID(item.id, 0);
|
|
if (item.parentid) {
|
|
this._tracker.addChangedID(item.parentid, 0);
|
|
}
|
|
}
|
|
};
|
|
|
|
function BookmarksStore(name) {
|
|
Store.call(this, name);
|
|
|
|
// Explicitly nullify our references to our cached services so we don't leak
|
|
Svc.Obs.add("places-shutdown", function() {
|
|
this.__bms = null;
|
|
this.__hsvc = null;
|
|
this.__ls = null;
|
|
this.__ms = null;
|
|
this.__ts = null;
|
|
for each ([query, stmt] in Iterator(this._stmts))
|
|
stmt.finalize();
|
|
}, this);
|
|
}
|
|
BookmarksStore.prototype = {
|
|
__proto__: Store.prototype,
|
|
|
|
__bms: null,
|
|
get _bms() {
|
|
if (!this.__bms)
|
|
this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
|
getService(Ci.nsINavBookmarksService);
|
|
return this.__bms;
|
|
},
|
|
|
|
__hsvc: null,
|
|
get _hsvc() {
|
|
if (!this.__hsvc)
|
|
this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
|
|
getService(Ci.nsINavHistoryService).
|
|
QueryInterface(Ci.nsPIPlacesDatabase);
|
|
return this.__hsvc;
|
|
},
|
|
|
|
__ls: null,
|
|
get _ls() {
|
|
if (!this.__ls)
|
|
this.__ls = Cc["@mozilla.org/browser/livemark-service;2"].
|
|
getService(Ci.nsILivemarkService);
|
|
return this.__ls;
|
|
},
|
|
|
|
__ms: null,
|
|
get _ms() {
|
|
if (!this.__ms) {
|
|
try {
|
|
this.__ms = Cc["@mozilla.org/microsummary/service;1"].
|
|
getService(Ci.nsIMicrosummaryService);
|
|
} catch (e) {
|
|
this._log.warn("Could not load microsummary service");
|
|
this._log.debug(e);
|
|
// Redefine our getter so we won't keep trying to get the service
|
|
this.__defineGetter__("_ms", function() null);
|
|
}
|
|
}
|
|
return this.__ms;
|
|
},
|
|
|
|
__ts: null,
|
|
get _ts() {
|
|
if (!this.__ts)
|
|
this.__ts = Cc["@mozilla.org/browser/tagging-service;1"].
|
|
getService(Ci.nsITaggingService);
|
|
return this.__ts;
|
|
},
|
|
|
|
|
|
itemExists: function BStore_itemExists(id) {
|
|
return this.idForGUID(id, true) > 0;
|
|
},
|
|
|
|
/*
|
|
* If the record is a tag query, rewrite it to refer to the local tag ID.
|
|
*
|
|
* Otherwise, just return.
|
|
*/
|
|
preprocessTagQuery: function preprocessTagQuery(record) {
|
|
if (record.type != "query" ||
|
|
record.bmkUri == null ||
|
|
record.folderName == null)
|
|
return;
|
|
|
|
// Yes, this works without chopping off the "place:" prefix.
|
|
let uri = record.bmkUri
|
|
let queriesRef = {};
|
|
let queryCountRef = {};
|
|
let optionsRef = {};
|
|
Svc.History.queryStringToQueries(uri, queriesRef, queryCountRef, optionsRef);
|
|
|
|
// We only process tag URIs.
|
|
if (optionsRef.value.resultType != optionsRef.value.RESULTS_AS_TAG_CONTENTS)
|
|
return;
|
|
|
|
// Tag something to ensure that the tag exists.
|
|
let tag = record.folderName;
|
|
let dummyURI = Utils.makeURI("about:weave#BStore_preprocess");
|
|
this._ts.tagURI(dummyURI, [tag]);
|
|
|
|
// Look for the id of the tag, which might just have been added.
|
|
let tags = this._getNode(this._bms.tagsFolder);
|
|
if (!(tags instanceof Ci.nsINavHistoryQueryResultNode)) {
|
|
this._log.debug("tags isn't an nsINavHistoryQueryResultNode; aborting.");
|
|
return;
|
|
}
|
|
|
|
tags.containerOpen = true;
|
|
for (let i = 0; i < tags.childCount; i++) {
|
|
let child = tags.getChild(i);
|
|
if (child.title == tag) {
|
|
// Found the tag, so fix up the query to use the right id.
|
|
this._log.debug("Tag query folder: " + tag + " = " + child.itemId);
|
|
|
|
this._log.trace("Replacing folders in: " + uri);
|
|
for each (let q in queriesRef.value)
|
|
q.setFolders([child.itemId], 1);
|
|
|
|
record.bmkUri = Svc.History.queriesToQueryString(queriesRef.value,
|
|
queryCountRef.value,
|
|
optionsRef.value);
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
applyIncoming: function BStore_applyIncoming(record) {
|
|
// Don't bother with pre and post-processing for deletions.
|
|
if (record.deleted) {
|
|
Store.prototype.applyIncoming.apply(this, arguments);
|
|
return;
|
|
}
|
|
|
|
// For special folders we're only interested in child ordering.
|
|
if ((record.id in kSpecialIds) && record.children) {
|
|
this._log.debug("Processing special node: " + record.id);
|
|
// Reorder children later
|
|
this._childrenToOrder[record.id] = record.children;
|
|
return;
|
|
}
|
|
|
|
// Preprocess the record before doing the normal apply.
|
|
this.preprocessTagQuery(record);
|
|
|
|
// Figure out the local id of the parent GUID if available
|
|
let parentGUID = record.parentid;
|
|
if (!parentGUID) {
|
|
throw "Record " + record.id + " has invalid parentid: " + parentGUID;
|
|
}
|
|
|
|
let parentId = this.idForGUID(parentGUID);
|
|
if (parentId > 0) {
|
|
// Save the parent id for modifying the bookmark later
|
|
record._parent = parentId;
|
|
record._orphan = false;
|
|
} else {
|
|
this._log.trace("Record " + record.id +
|
|
" is an orphan: could not find parent " + parentGUID);
|
|
record._orphan = true;
|
|
}
|
|
|
|
// Do the normal processing of incoming records
|
|
Store.prototype.applyIncoming.apply(this, arguments);
|
|
|
|
// Do some post-processing if we have an item
|
|
let itemId = this.idForGUID(record.id);
|
|
if (itemId > 0) {
|
|
// Move any children that are looking for this folder as a parent
|
|
if (record.type == "folder") {
|
|
this._reparentOrphans(itemId);
|
|
// Reorder children later
|
|
if (record.children)
|
|
this._childrenToOrder[record.id] = record.children;
|
|
}
|
|
|
|
// Create an annotation to remember that it needs reparenting.
|
|
if (record._orphan)
|
|
Utils.anno(itemId, PARENT_ANNO, parentGUID);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Find all ids of items that have a given value for an annotation
|
|
*/
|
|
_findAnnoItems: function BStore__findAnnoItems(anno, val) {
|
|
return Svc.Annos.getItemsWithAnnotation(anno, {}).filter(function(id)
|
|
Utils.anno(id, anno) == val);
|
|
},
|
|
|
|
/**
|
|
* For the provided parent item, attach its children to it
|
|
*/
|
|
_reparentOrphans: function _reparentOrphans(parentId) {
|
|
// Find orphans and reunite with this folder parent
|
|
let parentGUID = this.GUIDForId(parentId);
|
|
let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID);
|
|
|
|
this._log.debug("Reparenting orphans " + orphans + " to " + parentId);
|
|
orphans.forEach(function(orphan) {
|
|
// Move the orphan to the parent and drop the missing parent annotation
|
|
if (this._reparentItem(orphan, parentId)) {
|
|
Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO);
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
_reparentItem: function _reparentItem(itemId, parentId) {
|
|
this._log.trace("Attempting to move item " + itemId + " to new parent " +
|
|
parentId);
|
|
try {
|
|
if (parentId > 0) {
|
|
Svc.Bookmark.moveItem(itemId, parentId, Svc.Bookmark.DEFAULT_INDEX);
|
|
return true;
|
|
}
|
|
} catch(ex) {
|
|
this._log.debug("Failed to reparent item. " + Utils.exceptionStr(ex));
|
|
}
|
|
return false;
|
|
},
|
|
|
|
create: function BStore_create(record) {
|
|
// Default to unfiled if we don't have the parent yet
|
|
if (!record._parent) {
|
|
record._parent = kSpecialIds.unfiled;
|
|
}
|
|
|
|
let newId;
|
|
switch (record.type) {
|
|
case "bookmark":
|
|
case "query":
|
|
case "microsummary": {
|
|
let uri = Utils.makeURI(record.bmkUri);
|
|
newId = this._bms.insertBookmark(record._parent, uri,
|
|
Svc.Bookmark.DEFAULT_INDEX, record.title);
|
|
this._log.debug(["created bookmark", newId, "under", record._parent,
|
|
"as", record.title, record.bmkUri].join(" "));
|
|
|
|
// Smart bookmark annotations are strings.
|
|
if (record.queryId) {
|
|
Utils.anno(newId, SMART_BOOKMARKS_ANNO, record.queryId);
|
|
}
|
|
|
|
if (Utils.isArray(record.tags)) {
|
|
this._tagURI(uri, record.tags);
|
|
}
|
|
this._bms.setKeywordForBookmark(newId, record.keyword);
|
|
if (record.description)
|
|
Utils.anno(newId, "bookmarkProperties/description", record.description);
|
|
|
|
if (record.loadInSidebar)
|
|
Utils.anno(newId, "bookmarkProperties/loadInSidebar", true);
|
|
|
|
if (record.type == "microsummary") {
|
|
this._log.debug(" \-> is a microsummary");
|
|
Utils.anno(newId, "bookmarks/staticTitle", record.staticTitle || "");
|
|
let genURI = Utils.makeURI(record.generatorUri);
|
|
if (this._ms) {
|
|
try {
|
|
let micsum = this._ms.createMicrosummary(uri, genURI);
|
|
this._ms.setMicrosummary(newId, micsum);
|
|
}
|
|
catch(ex) { /* ignore "missing local generator" exceptions */ }
|
|
}
|
|
else
|
|
this._log.warn("Can't create microsummary -- not supported.");
|
|
}
|
|
} break;
|
|
case "folder":
|
|
newId = this._bms.createFolder(record._parent, record.title,
|
|
Svc.Bookmark.DEFAULT_INDEX);
|
|
this._log.debug(["created folder", newId, "under", record._parent,
|
|
"as", record.title].join(" "));
|
|
|
|
if (record.description)
|
|
Utils.anno(newId, "bookmarkProperties/description", record.description);
|
|
|
|
// record.children will be dealt with in _orderChildren.
|
|
break;
|
|
case "livemark":
|
|
let siteURI = null;
|
|
if (record.siteUri != null)
|
|
siteURI = Utils.makeURI(record.siteUri);
|
|
|
|
newId = this._ls.createLivemark(record._parent, record.title, siteURI,
|
|
Utils.makeURI(record.feedUri),
|
|
Svc.Bookmark.DEFAULT_INDEX);
|
|
this._log.debug(["created livemark", newId, "under", record._parent, "as",
|
|
record.title, record.siteUri, record.feedUri].join(" "));
|
|
break;
|
|
case "separator":
|
|
newId = this._bms.insertSeparator(record._parent,
|
|
Svc.Bookmark.DEFAULT_INDEX);
|
|
this._log.debug(["created separator", newId, "under", record._parent]
|
|
.join(" "));
|
|
break;
|
|
case "item":
|
|
this._log.debug(" -> got a generic places item.. do nothing?");
|
|
return;
|
|
default:
|
|
this._log.error("_create: Unknown item type: " + record.type);
|
|
return;
|
|
}
|
|
|
|
this._log.trace("Setting GUID of new item " + newId + " to " + record.id);
|
|
this._setGUID(newId, record.id);
|
|
},
|
|
|
|
remove: function BStore_remove(record) {
|
|
let itemId = this.idForGUID(record.id);
|
|
if (itemId <= 0) {
|
|
this._log.debug("Item " + record.id + " already removed");
|
|
return;
|
|
}
|
|
var type = this._bms.getItemType(itemId);
|
|
|
|
switch (type) {
|
|
case this._bms.TYPE_BOOKMARK:
|
|
this._log.debug(" -> removing bookmark " + record.id);
|
|
this._ts.untagURI(this._bms.getBookmarkURI(itemId), null);
|
|
this._bms.removeItem(itemId);
|
|
break;
|
|
case this._bms.TYPE_FOLDER:
|
|
this._log.debug(" -> removing folder " + record.id);
|
|
Svc.Bookmark.removeItem(itemId);
|
|
break;
|
|
case this._bms.TYPE_SEPARATOR:
|
|
this._log.debug(" -> removing separator " + record.id);
|
|
this._bms.removeItem(itemId);
|
|
break;
|
|
default:
|
|
this._log.error("remove: Unknown item type: " + type);
|
|
break;
|
|
}
|
|
},
|
|
|
|
update: function BStore_update(record) {
|
|
let itemId = this.idForGUID(record.id);
|
|
|
|
if (itemId <= 0) {
|
|
this._log.debug("Skipping update for unknown item: " + record.id);
|
|
return;
|
|
}
|
|
|
|
this._log.trace("Updating " + record.id + " (" + itemId + ")");
|
|
|
|
// Move the bookmark to a new parent or new position if necessary
|
|
if (record._parent > 0 &&
|
|
Svc.Bookmark.getFolderIdForItem(itemId) != record._parent) {
|
|
this._reparentItem(itemId, record._parent);
|
|
}
|
|
|
|
for (let [key, val] in Iterator(record.cleartext)) {
|
|
switch (key) {
|
|
case "title":
|
|
val = val || "";
|
|
this._bms.setItemTitle(itemId, val);
|
|
break;
|
|
case "bmkUri":
|
|
this._bms.changeBookmarkURI(itemId, Utils.makeURI(val));
|
|
break;
|
|
case "tags":
|
|
if (Utils.isArray(val)) {
|
|
this._tagURI(this._bms.getBookmarkURI(itemId), val);
|
|
}
|
|
break;
|
|
case "keyword":
|
|
this._bms.setKeywordForBookmark(itemId, val);
|
|
break;
|
|
case "description":
|
|
val = val || "";
|
|
Utils.anno(itemId, "bookmarkProperties/description", val);
|
|
break;
|
|
case "loadInSidebar":
|
|
if (val)
|
|
Utils.anno(itemId, "bookmarkProperties/loadInSidebar", true);
|
|
else
|
|
Svc.Annos.removeItemAnnotation(itemId, "bookmarkProperties/loadInSidebar");
|
|
break;
|
|
case "generatorUri": {
|
|
try {
|
|
let micsumURI = this._bms.getBookmarkURI(itemId);
|
|
let genURI = Utils.makeURI(val);
|
|
if (this._ms == SERVICE_NOT_SUPPORTED)
|
|
this._log.warn("Can't create microsummary -- not supported.");
|
|
else {
|
|
let micsum = this._ms.createMicrosummary(micsumURI, genURI);
|
|
this._ms.setMicrosummary(itemId, micsum);
|
|
}
|
|
} catch (e) {
|
|
this._log.debug("Could not set microsummary generator URI: " + e);
|
|
}
|
|
} break;
|
|
case "queryId":
|
|
Utils.anno(itemId, SMART_BOOKMARKS_ANNO, val);
|
|
break;
|
|
case "siteUri":
|
|
this._ls.setSiteURI(itemId, Utils.makeURI(val));
|
|
break;
|
|
case "feedUri":
|
|
this._ls.setFeedURI(itemId, Utils.makeURI(val));
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
_orderChildren: function _orderChildren() {
|
|
for (let [guid, children] in Iterator(this._childrenToOrder)) {
|
|
// Reorder children according to the GUID list. Gracefully deal
|
|
// with missing items, e.g. locally deleted.
|
|
let delta = 0;
|
|
for (let idx = 0; idx < children.length; idx++) {
|
|
let itemid = this.idForGUID(children[idx]);
|
|
if (itemid == -1) {
|
|
delta += 1;
|
|
this._log.trace("Could not locate record " + children[idx]);
|
|
continue;
|
|
}
|
|
try {
|
|
Svc.Bookmark.setItemIndex(itemid, idx - delta);
|
|
} catch (ex) {
|
|
this._log.debug("Could not move item " + children[idx] + ": " + ex);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
changeItemID: function BStore_changeItemID(oldID, newID) {
|
|
this._log.debug("Changing GUID " + oldID + " to " + newID);
|
|
|
|
// Make sure there's an item to change GUIDs
|
|
let itemId = this.idForGUID(oldID);
|
|
if (itemId <= 0)
|
|
return;
|
|
|
|
this._setGUID(itemId, newID);
|
|
},
|
|
|
|
_getNode: function BStore__getNode(folder) {
|
|
let query = this._hsvc.getNewQuery();
|
|
query.setFolders([folder], 1);
|
|
return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root;
|
|
},
|
|
|
|
_getTags: function BStore__getTags(uri) {
|
|
try {
|
|
if (typeof(uri) == "string")
|
|
uri = Utils.makeURI(uri);
|
|
} catch(e) {
|
|
this._log.warn("Could not parse URI \"" + uri + "\": " + e);
|
|
}
|
|
return this._ts.getTagsForURI(uri, {});
|
|
},
|
|
|
|
_getDescription: function BStore__getDescription(id) {
|
|
try {
|
|
return Utils.anno(id, "bookmarkProperties/description");
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
_isLoadInSidebar: function BStore__isLoadInSidebar(id) {
|
|
return Svc.Annos.itemHasAnnotation(id, "bookmarkProperties/loadInSidebar");
|
|
},
|
|
|
|
_getStaticTitle: function BStore__getStaticTitle(id) {
|
|
try {
|
|
return Utils.anno(id, "bookmarks/staticTitle");
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
},
|
|
|
|
__childGUIDsStm: null,
|
|
get _childGUIDsStm() {
|
|
if (this.__childGUIDsStm) {
|
|
return this.__childGUIDsStm;
|
|
}
|
|
|
|
let stmt;
|
|
if (this._haveGUIDColumn) {
|
|
stmt = this._getStmt(
|
|
"SELECT id AS item_id, guid " +
|
|
"FROM moz_bookmarks " +
|
|
"WHERE parent = :parent " +
|
|
"ORDER BY position");
|
|
} else {
|
|
stmt = this._getStmt(
|
|
"SELECT b.id AS item_id, " +
|
|
"(SELECT id FROM moz_anno_attributes WHERE name = '" + GUID_ANNO + "') AS name_id," +
|
|
"a.content AS guid " +
|
|
"FROM moz_bookmarks b " +
|
|
"LEFT JOIN moz_items_annos a ON a.item_id = b.id " +
|
|
"AND a.anno_attribute_id = name_id " +
|
|
"WHERE b.parent = :parent " +
|
|
"ORDER BY b.position");
|
|
}
|
|
return this.__childGUIDsStm = stmt;
|
|
},
|
|
|
|
_getChildGUIDsForId: function _getChildGUIDsForId(itemid) {
|
|
let stmt = this._childGUIDsStm;
|
|
stmt.params.parent = itemid;
|
|
let rows = Utils.queryAsync(stmt, ["item_id", "guid"]);
|
|
return rows.map(function (row) {
|
|
if (row.guid) {
|
|
return row.guid;
|
|
}
|
|
// A GUID hasn't been assigned to this item yet, do this now.
|
|
return this.GUIDForId(row.item_id);
|
|
}, this);
|
|
},
|
|
|
|
// Create a record starting from the weave id (places guid)
|
|
createRecord: function createRecord(id, collection) {
|
|
let placeId = this.idForGUID(id);
|
|
let record;
|
|
if (placeId <= 0) { // deleted item
|
|
record = new PlacesItem(collection, id);
|
|
record.deleted = true;
|
|
return record;
|
|
}
|
|
|
|
let parent = Svc.Bookmark.getFolderIdForItem(placeId);
|
|
switch (this._bms.getItemType(placeId)) {
|
|
case this._bms.TYPE_BOOKMARK:
|
|
let bmkUri = this._bms.getBookmarkURI(placeId).spec;
|
|
if (this._ms && this._ms.hasMicrosummary(placeId)) {
|
|
record = new BookmarkMicsum(collection, id);
|
|
let micsum = this._ms.getMicrosummary(placeId);
|
|
record.generatorUri = micsum.generator.uri.spec; // breaks local generators
|
|
record.staticTitle = this._getStaticTitle(placeId);
|
|
}
|
|
else {
|
|
if (bmkUri.search(/^place:/) == 0) {
|
|
record = new BookmarkQuery(collection, id);
|
|
|
|
// Get the actual tag name instead of the local itemId
|
|
let folder = bmkUri.match(/[:&]folder=(\d+)/);
|
|
try {
|
|
// There might not be the tag yet when creating on a new client
|
|
if (folder != null) {
|
|
folder = folder[1];
|
|
record.folderName = this._bms.getItemTitle(folder);
|
|
this._log.trace("query id: " + folder + " = " + record.folderName);
|
|
}
|
|
}
|
|
catch(ex) {}
|
|
|
|
// Persist the Smart Bookmark anno, if found.
|
|
try {
|
|
let anno = Utils.anno(placeId, SMART_BOOKMARKS_ANNO);
|
|
if (anno != null) {
|
|
this._log.trace("query anno: " + SMART_BOOKMARKS_ANNO +
|
|
" = " + anno);
|
|
record.queryId = anno;
|
|
}
|
|
}
|
|
catch(ex) {}
|
|
}
|
|
else
|
|
record = new Bookmark(collection, id);
|
|
record.title = this._bms.getItemTitle(placeId);
|
|
}
|
|
|
|
record.parentName = Svc.Bookmark.getItemTitle(parent);
|
|
record.bmkUri = bmkUri;
|
|
record.tags = this._getTags(record.bmkUri);
|
|
record.keyword = this._bms.getKeywordForBookmark(placeId);
|
|
record.description = this._getDescription(placeId);
|
|
record.loadInSidebar = this._isLoadInSidebar(placeId);
|
|
break;
|
|
|
|
case this._bms.TYPE_FOLDER:
|
|
if (this._ls.isLivemark(placeId)) {
|
|
record = new Livemark(collection, id);
|
|
|
|
let siteURI = this._ls.getSiteURI(placeId);
|
|
if (siteURI != null)
|
|
record.siteUri = siteURI.spec;
|
|
record.feedUri = this._ls.getFeedURI(placeId).spec;
|
|
|
|
} else {
|
|
record = new BookmarkFolder(collection, id);
|
|
}
|
|
|
|
if (parent > 0)
|
|
record.parentName = Svc.Bookmark.getItemTitle(parent);
|
|
record.title = this._bms.getItemTitle(placeId);
|
|
record.description = this._getDescription(placeId);
|
|
record.children = this._getChildGUIDsForId(placeId);
|
|
break;
|
|
|
|
case this._bms.TYPE_SEPARATOR:
|
|
record = new BookmarkSeparator(collection, id);
|
|
if (parent > 0)
|
|
record.parentName = Svc.Bookmark.getItemTitle(parent);
|
|
// Create a positioning identifier for the separator, used by _lazyMap
|
|
record.pos = Svc.Bookmark.getItemIndex(placeId);
|
|
break;
|
|
|
|
case this._bms.TYPE_DYNAMIC_CONTAINER:
|
|
record = new PlacesItem(collection, id);
|
|
this._log.warn("Don't know how to serialize dynamic containers yet");
|
|
break;
|
|
|
|
default:
|
|
record = new PlacesItem(collection, id);
|
|
this._log.warn("Unknown item type, cannot serialize: " +
|
|
this._bms.getItemType(placeId));
|
|
}
|
|
|
|
record.parentid = this.GUIDForId(parent);
|
|
record.sortindex = this._calculateIndex(record);
|
|
|
|
return record;
|
|
},
|
|
|
|
_stmts: {},
|
|
_getStmt: function(query) {
|
|
if (query in this._stmts)
|
|
return this._stmts[query];
|
|
|
|
this._log.trace("Creating SQL statement: " + query);
|
|
return this._stmts[query] = Utils.createStatement(this._hsvc.DBConnection,
|
|
query);
|
|
},
|
|
|
|
__haveGUIDColumn: null,
|
|
get _haveGUIDColumn() {
|
|
if (this.__haveGUIDColumn !== null) {
|
|
return this.__haveGUIDColumn;
|
|
}
|
|
let stmt;
|
|
try {
|
|
stmt = this._hsvc.DBConnection.createStatement(
|
|
"SELECT guid FROM moz_places");
|
|
stmt.finalize();
|
|
return this.__haveGUIDColumn = true;
|
|
} catch(ex) {
|
|
return this.__haveGUIDColumn = false;
|
|
}
|
|
},
|
|
|
|
get _frecencyStm() {
|
|
return this._getStmt(
|
|
"SELECT frecency " +
|
|
"FROM moz_places " +
|
|
"WHERE url = :url " +
|
|
"LIMIT 1");
|
|
},
|
|
|
|
get _addGUIDAnnotationNameStm() {
|
|
let stmt = this._getStmt(
|
|
"INSERT OR IGNORE INTO moz_anno_attributes (name) VALUES (:anno_name)");
|
|
stmt.params.anno_name = GUID_ANNO;
|
|
return stmt;
|
|
},
|
|
|
|
get _checkGUIDItemAnnotationStm() {
|
|
// Gecko <2.0 only
|
|
let stmt = this._getStmt(
|
|
"SELECT b.id AS item_id, " +
|
|
"(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) AS name_id, " +
|
|
"a.id AS anno_id, a.dateAdded AS anno_date " +
|
|
"FROM moz_bookmarks b " +
|
|
"LEFT JOIN moz_items_annos a ON a.item_id = b.id " +
|
|
"AND a.anno_attribute_id = name_id " +
|
|
"WHERE b.id = :item_id");
|
|
stmt.params.anno_name = GUID_ANNO;
|
|
return stmt;
|
|
},
|
|
|
|
get _addItemAnnotationStm() {
|
|
return this._getStmt(
|
|
"INSERT OR REPLACE INTO moz_items_annos " +
|
|
"(id, item_id, anno_attribute_id, mime_type, content, flags, " +
|
|
"expiration, type, dateAdded, lastModified) " +
|
|
"VALUES (:id, :item_id, :name_id, :mime_type, :content, :flags, " +
|
|
":expiration, :type, :date_added, :last_modified)");
|
|
},
|
|
|
|
__setGUIDStm: null,
|
|
get _setGUIDStm() {
|
|
if (this.__setGUIDStm !== null) {
|
|
return this.__setGUIDStm;
|
|
}
|
|
|
|
// Obtains a statement to set the guid iff the guid column exists.
|
|
let stmt;
|
|
if (this._haveGUIDColumn) {
|
|
stmt = this._getStmt(
|
|
"UPDATE moz_bookmarks " +
|
|
"SET guid = :guid " +
|
|
"WHERE id = :item_id");
|
|
} else {
|
|
stmt = false;
|
|
}
|
|
return this.__setGUIDStm = stmt;
|
|
},
|
|
|
|
// Some helper functions to handle GUIDs
|
|
_setGUID: function _setGUID(id, guid) {
|
|
if (arguments.length == 1)
|
|
guid = Utils.makeGUID();
|
|
|
|
// If we can, set the GUID on moz_bookmarks and do not do any other work.
|
|
let (stmt = this._setGUIDStm) {
|
|
if (stmt) {
|
|
stmt.params.guid = guid;
|
|
stmt.params.item_id = id;
|
|
Utils.queryAsync(stmt);
|
|
return guid;
|
|
}
|
|
}
|
|
|
|
// Ensure annotation name exists
|
|
Utils.queryAsync(this._addGUIDAnnotationNameStm);
|
|
|
|
let stmt = this._checkGUIDItemAnnotationStm;
|
|
stmt.params.item_id = id;
|
|
let result = Utils.queryAsync(stmt, ["item_id", "name_id", "anno_id",
|
|
"anno_date"])[0];
|
|
if (!result) {
|
|
this._log.warn("Couldn't annotate bookmark id " + id);
|
|
return guid;
|
|
}
|
|
|
|
stmt = this._addItemAnnotationStm;
|
|
if (result.anno_id) {
|
|
stmt.params.id = result.anno_id;
|
|
stmt.params.date_added = result.anno_date;
|
|
} else {
|
|
stmt.params.id = null;
|
|
stmt.params.date_added = Date.now() * 1000;
|
|
}
|
|
stmt.params.item_id = result.item_id;
|
|
stmt.params.name_id = result.name_id;
|
|
stmt.params.content = guid;
|
|
stmt.params.flags = 0;
|
|
stmt.params.expiration = Ci.nsIAnnotationService.EXPIRE_NEVER;
|
|
stmt.params.type = Ci.nsIAnnotationService.TYPE_STRING;
|
|
stmt.params.last_modified = Date.now() * 1000;
|
|
Utils.queryAsync(stmt);
|
|
|
|
return guid;
|
|
},
|
|
|
|
__guidForIdStm: null,
|
|
get _guidForIdStm() {
|
|
if (this.__guidForIdStm) {
|
|
return this.__guidForIdStm;
|
|
}
|
|
|
|
// Try to first read from moz_bookmarks. Creating the statement will
|
|
// fail, however, if the guid column does not exist. We fallback to just
|
|
// reading the annotation table in this case.
|
|
let stmt;
|
|
if (this._haveGUIDColumn) {
|
|
stmt = this._getStmt(
|
|
"SELECT guid " +
|
|
"FROM moz_bookmarks " +
|
|
"WHERE id = :item_id");
|
|
} else {
|
|
stmt = this._getStmt(
|
|
"SELECT a.content AS guid " +
|
|
"FROM moz_items_annos a " +
|
|
"JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " +
|
|
"JOIN moz_bookmarks b ON b.id = a.item_id " +
|
|
"WHERE n.name = '" + GUID_ANNO + "' " +
|
|
"AND b.id = :item_id");
|
|
}
|
|
|
|
return this.__guidForIdStm = stmt;
|
|
},
|
|
|
|
GUIDForId: function GUIDForId(id) {
|
|
let special = kSpecialIds.specialGUIDForId(id);
|
|
if (special)
|
|
return special;
|
|
|
|
let stmt = this._guidForIdStm;
|
|
stmt.params.item_id = id;
|
|
|
|
// Use the existing GUID if it exists
|
|
let result = Utils.queryAsync(stmt, ["guid"])[0];
|
|
if (result && result.guid)
|
|
return result.guid;
|
|
|
|
// Give the uri a GUID if it doesn't have one
|
|
return this._setGUID(id);
|
|
},
|
|
|
|
__idForGUIDStm: null,
|
|
get _idForGUIDStm() {
|
|
if (this.__idForGUIDStm) {
|
|
return this.__idForGUIDStm;
|
|
}
|
|
|
|
|
|
// Try to first read from moz_bookmarks. Creating the statement will
|
|
// fail, however, if the guid column does not exist. We fallback to just
|
|
// reading the annotation table in this case.
|
|
let stmt;
|
|
if (this._haveGUIDColumn) {
|
|
stmt = this._getStmt(
|
|
"SELECT id AS item_id " +
|
|
"FROM moz_bookmarks " +
|
|
"WHERE guid = :guid");
|
|
} else {
|
|
// Order results by lastModified so we can preserve the ID of the oldest bookmark.
|
|
// Copying a record preserves its dateAdded, and only modifying the
|
|
// bookmark alters its lastModified, so we also order by its item_id --
|
|
// lowest wins ties. Of course, Places can still screw us by reassigning IDs...
|
|
stmt = this._getStmt(
|
|
"SELECT a.item_id AS item_id " +
|
|
"FROM moz_items_annos a " +
|
|
"JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " +
|
|
"WHERE n.name = '" + GUID_ANNO + "' " +
|
|
"AND a.content = :guid " +
|
|
"ORDER BY a.lastModified, a.item_id");
|
|
}
|
|
|
|
return this.__idForGUIDStm = stmt;
|
|
},
|
|
|
|
// noCreate is provided as an optional argument to prevent the creation of
|
|
// non-existent special records, such as "mobile".
|
|
idForGUID: function idForGUID(guid, noCreate) {
|
|
if (kSpecialIds.isSpecialGUID(guid))
|
|
return kSpecialIds.specialIdForGUID(guid, !noCreate);
|
|
|
|
let stmt = this._idForGUIDStm;
|
|
// guid might be a String object rather than a string.
|
|
stmt.params.guid = guid.toString();
|
|
|
|
let results = Utils.queryAsync(stmt, ["item_id"]);
|
|
this._log.trace("Rows matching GUID " + guid + ": " +
|
|
results.map(function(x) x.item_id));
|
|
|
|
// Here's the one we care about: the first.
|
|
let result = results[0];
|
|
|
|
if (!result)
|
|
return -1;
|
|
|
|
if (!this._haveGUIDColumn) {
|
|
try {
|
|
// Assign new GUIDs to any that came later.
|
|
for (let i = 1; i < results.length; ++i) {
|
|
let surplus = results[i];
|
|
this._log.debug("Assigning new GUID to copied row " + surplus.item_id);
|
|
this._setGUID(surplus.item_id);
|
|
}
|
|
} catch (ex) {
|
|
// Just skip it and carry on. This shouldn't happen, but if it does we
|
|
// don't want to fail hard.
|
|
this._log.debug("Got exception assigning new GUIDs: " +
|
|
Utils.exceptionStr(ex));
|
|
}
|
|
}
|
|
|
|
return result.item_id;
|
|
},
|
|
|
|
_calculateIndex: function _calculateIndex(record) {
|
|
// Ensure folders have a very high sort index so they're not synced last.
|
|
if (record.type == "folder")
|
|
return FOLDER_SORTINDEX;
|
|
|
|
// For anything directly under the toolbar, give it a boost of more than an
|
|
// unvisited bookmark
|
|
let index = 0;
|
|
if (record.parentid == "toolbar")
|
|
index += 150;
|
|
|
|
// Add in the bookmark's frecency if we have something
|
|
if (record.bmkUri != null) {
|
|
this._frecencyStm.params.url = record.bmkUri;
|
|
let result = Utils.queryAsync(this._frecencyStm, ["frecency"]);
|
|
if (result.length)
|
|
index += result[0].frecency;
|
|
}
|
|
|
|
return index;
|
|
},
|
|
|
|
_getChildren: function BStore_getChildren(guid, items) {
|
|
let node = guid; // the recursion case
|
|
if (typeof(node) == "string") { // callers will give us the guid as the first arg
|
|
let nodeID = this.idForGUID(guid, true);
|
|
if (!nodeID) {
|
|
this._log.debug("No node for GUID " + guid + "; returning no children.");
|
|
return items;
|
|
}
|
|
node = this._getNode(nodeID);
|
|
}
|
|
|
|
if (node.type == node.RESULT_TYPE_FOLDER &&
|
|
!this._ls.isLivemark(node.itemId)) {
|
|
node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
|
|
node.containerOpen = true;
|
|
|
|
// Remember all the children GUIDs and recursively get more
|
|
for (var i = 0; i < node.childCount; i++) {
|
|
let child = node.getChild(i);
|
|
items[this.GUIDForId(child.itemId)] = true;
|
|
this._getChildren(child, items);
|
|
}
|
|
}
|
|
|
|
return items;
|
|
},
|
|
|
|
_tagURI: function BStore_tagURI(bmkURI, tags) {
|
|
// Filter out any null/undefined/empty tags
|
|
tags = tags.filter(function(t) t);
|
|
|
|
// Temporarily tag a dummy uri to preserve tag ids when untagging
|
|
let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
|
|
this._ts.tagURI(dummyURI, tags);
|
|
this._ts.untagURI(bmkURI, null);
|
|
this._ts.tagURI(bmkURI, tags);
|
|
this._ts.untagURI(dummyURI, null);
|
|
},
|
|
|
|
getAllIDs: function BStore_getAllIDs() {
|
|
let items = {"menu": true,
|
|
"toolbar": true};
|
|
for each (let guid in kSpecialIds.guids) {
|
|
if (guid != "places" && guid != "tags")
|
|
this._getChildren(guid, items);
|
|
}
|
|
return items;
|
|
},
|
|
|
|
wipe: function BStore_wipe() {
|
|
// Save a backup before clearing out all bookmarks
|
|
archiveBookmarks();
|
|
|
|
for each (let guid in kSpecialIds.guids)
|
|
if (guid != "places") {
|
|
let id = kSpecialIds.specialIdForGUID(guid);
|
|
if (id)
|
|
this._bms.removeFolderChildren(id);
|
|
}
|
|
}
|
|
};
|
|
|
|
function BookmarksTracker(name) {
|
|
Tracker.call(this, name);
|
|
|
|
Svc.Obs.add("places-shutdown", this);
|
|
Svc.Obs.add("weave:engine:start-tracking", this);
|
|
Svc.Obs.add("weave:engine:stop-tracking", this);
|
|
}
|
|
BookmarksTracker.prototype = {
|
|
__proto__: Tracker.prototype,
|
|
|
|
_enabled: false,
|
|
observe: function observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case "weave:engine:start-tracking":
|
|
if (!this._enabled) {
|
|
Svc.Bookmark.addObserver(this, true);
|
|
Svc.Obs.add("bookmarks-restore-begin", this);
|
|
Svc.Obs.add("bookmarks-restore-success", this);
|
|
Svc.Obs.add("bookmarks-restore-failed", this);
|
|
this._enabled = true;
|
|
}
|
|
break;
|
|
case "weave:engine:stop-tracking":
|
|
if (this._enabled) {
|
|
Svc.Bookmark.removeObserver(this);
|
|
Svc.Obs.remove("bookmarks-restore-begin", this);
|
|
Svc.Obs.remove("bookmarks-restore-success", this);
|
|
Svc.Obs.remove("bookmarks-restore-failed", this);
|
|
this._enabled = false;
|
|
}
|
|
// Fall through to clean up.
|
|
case "places-shutdown":
|
|
// Explicitly nullify our references to our cached services so
|
|
// we don't leak
|
|
this.__ls = null;
|
|
this.__bms = null;
|
|
break;
|
|
|
|
case "bookmarks-restore-begin":
|
|
this._log.debug("Ignoring changes from importing bookmarks.");
|
|
this.ignoreAll = true;
|
|
break;
|
|
case "bookmarks-restore-success":
|
|
this._log.debug("Tracking all items on successful import.");
|
|
this.ignoreAll = false;
|
|
|
|
this._log.debug("Restore succeeded: wiping server and other clients.");
|
|
Weave.Service.resetClient([this.name]);
|
|
Weave.Service.wipeServer([this.name]);
|
|
Weave.Service.prepCommand("wipeEngine", [this.name]);
|
|
break;
|
|
case "bookmarks-restore-failed":
|
|
this._log.debug("Tracking all items on failed import.");
|
|
this.ignoreAll = false;
|
|
break;
|
|
}
|
|
},
|
|
|
|
__bms: null,
|
|
get _bms() {
|
|
if (!this.__bms)
|
|
this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
|
getService(Ci.nsINavBookmarksService);
|
|
return this.__bms;
|
|
},
|
|
|
|
__ls: null,
|
|
get _ls() {
|
|
if (!this.__ls)
|
|
this.__ls = Cc["@mozilla.org/browser/livemark-service;2"].
|
|
getService(Ci.nsILivemarkService);
|
|
return this.__ls;
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsINavBookmarkObserver,
|
|
Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS,
|
|
Ci.nsISupportsWeakReference
|
|
]),
|
|
|
|
_idForGUID: function _idForGUID(item_id) {
|
|
// Isn't indirection fun...
|
|
return Engines.get("bookmarks")._store.idForGUID(item_id);
|
|
},
|
|
|
|
_GUIDForId: function _GUIDForId(item_id) {
|
|
// Isn't indirection fun...
|
|
return Engines.get("bookmarks")._store.GUIDForId(item_id);
|
|
},
|
|
|
|
/**
|
|
* Add a bookmark (places) id to be uploaded and bump up the sync score
|
|
*
|
|
* @param itemId
|
|
* Places internal id of the bookmark to upload
|
|
*/
|
|
_addId: function BMT__addId(itemId) {
|
|
if (this.addChangedID(this._GUIDForId(itemId)))
|
|
this._upScore();
|
|
},
|
|
|
|
/* Every add/remove/change is worth 10 points */
|
|
_upScore: function BMT__upScore() {
|
|
this.score += 10;
|
|
},
|
|
|
|
/**
|
|
* Determine if a change should be ignored: we're ignoring everything or the
|
|
* folder is for livemarks
|
|
*
|
|
* @param itemId
|
|
* Item under consideration to ignore
|
|
* @param folder (optional)
|
|
* Folder of the item being changed
|
|
*/
|
|
_ignore: function BMT__ignore(itemId, folder) {
|
|
// Ignore unconditionally if the engine tells us to
|
|
if (this.ignoreAll)
|
|
return true;
|
|
|
|
// Ensure that the mobile bookmarks query is correct in the UI
|
|
this._ensureMobileQuery();
|
|
|
|
// Make sure to remove items that have the exclude annotation
|
|
if (Svc.Annos.itemHasAnnotation(itemId, "places/excludeFromBackup")) {
|
|
this.removeChangedID(this._GUIDForId(itemId));
|
|
return true;
|
|
}
|
|
|
|
// Get the folder id if we weren't given one
|
|
if (folder == null)
|
|
folder = this._bms.getFolderIdForItem(itemId);
|
|
|
|
let tags = kSpecialIds.tags;
|
|
// Ignore changes to tags (folders under the tags folder)
|
|
if (folder == tags)
|
|
return true;
|
|
|
|
// Ignore tag items (the actual instance of a tag for a bookmark)
|
|
if (this._bms.getFolderIdForItem(folder) == tags)
|
|
return true;
|
|
|
|
// Ignore livemark children
|
|
return this._ls.isLivemark(folder);
|
|
},
|
|
|
|
onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) {
|
|
if (this._ignore(itemId, folder))
|
|
return;
|
|
|
|
this._log.trace("onItemAdded: " + itemId);
|
|
this._addId(itemId);
|
|
this._addId(folder);
|
|
},
|
|
|
|
onBeforeItemRemoved: function BMT_onBeforeItemRemoved(itemId) {
|
|
if (this._ignore(itemId))
|
|
return;
|
|
|
|
this._log.trace("onBeforeItemRemoved: " + itemId);
|
|
this._addId(itemId);
|
|
let folder = Svc.Bookmark.getFolderIdForItem(itemId);
|
|
this._addId(folder);
|
|
},
|
|
|
|
_ensureMobileQuery: function _ensureMobileQuery() {
|
|
let anno = "PlacesOrganizer/OrganizerQuery";
|
|
let find = function(val) Svc.Annos.getItemsWithAnnotation(anno, {}).filter(
|
|
function(id) Utils.anno(id, anno) == val);
|
|
|
|
// Don't continue if the Library isn't ready
|
|
let all = find("AllBookmarks");
|
|
if (all.length == 0)
|
|
return;
|
|
|
|
// Disable handling of notifications while changing the mobile query
|
|
this.ignoreAll = true;
|
|
|
|
let mobile = find("MobileBookmarks");
|
|
let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile);
|
|
let title = Str.sync.get("mobile.label");
|
|
|
|
// Don't add OR do remove the mobile bookmarks if there's nothing
|
|
if (Svc.Bookmark.getIdForItemAt(kSpecialIds.mobile, 0) == -1) {
|
|
if (mobile.length != 0)
|
|
Svc.Bookmark.removeItem(mobile[0]);
|
|
}
|
|
// Add the mobile bookmarks query if it doesn't exist
|
|
else if (mobile.length == 0) {
|
|
let query = Svc.Bookmark.insertBookmark(all[0], queryURI, -1, title);
|
|
Utils.anno(query, anno, "MobileBookmarks");
|
|
Utils.anno(query, "places/excludeFromBackup", 1);
|
|
}
|
|
// Make sure the existing title is correct
|
|
else if (Svc.Bookmark.getItemTitle(mobile[0]) != title)
|
|
Svc.Bookmark.setItemTitle(mobile[0], title);
|
|
|
|
this.ignoreAll = false;
|
|
},
|
|
|
|
onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) {
|
|
if (this._ignore(itemId))
|
|
return;
|
|
|
|
// Allocate a new GUID if necessary.
|
|
// We only want to do it if there's a dupe, so use idForGUID to achieve that.
|
|
if (isAnno && (property == GUID_ANNO)) {
|
|
this._log.trace("onItemChanged for " + GUID_ANNO +
|
|
": probably needs a new one.");
|
|
this._idForGUID(this._GUIDForId(itemId));
|
|
this._addId(itemId);
|
|
return;
|
|
}
|
|
|
|
// ignore annotations except for the ones that we sync
|
|
let annos = ["bookmarkProperties/description",
|
|
"bookmarkProperties/loadInSidebar", "bookmarks/staticTitle",
|
|
"livemark/feedURI", "livemark/siteURI", "microsummary/generatorURI"];
|
|
if (isAnno && annos.indexOf(property) == -1)
|
|
return;
|
|
|
|
// Ignore favicon changes to avoid unnecessary churn
|
|
if (property == "favicon")
|
|
return;
|
|
|
|
this._log.trace("onItemChanged: " + itemId +
|
|
(", " + property + (isAnno? " (anno)" : "")) +
|
|
(value ? (" = \"" + value + "\"") : ""));
|
|
this._addId(itemId);
|
|
},
|
|
|
|
onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) {
|
|
if (this._ignore(itemId))
|
|
return;
|
|
|
|
this._log.trace("onItemMoved: " + itemId);
|
|
this._addId(oldParent);
|
|
if (oldParent != newParent) {
|
|
this._addId(itemId);
|
|
this._addId(newParent);
|
|
}
|
|
|
|
// Remove any position annotations now that the user moved the item
|
|
Svc.Annos.removeItemAnnotation(itemId, PARENT_ANNO);
|
|
},
|
|
|
|
onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {},
|
|
onEndUpdateBatch: function BMT_onEndUpdateBatch() {},
|
|
onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) {},
|
|
onItemVisited: function BMT_onItemVisited(itemId, aVisitID, time) {}
|
|
};
|