Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
import {
Changeset,
Store,
SyncEngine,
Tracker,
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
const PLACES_MAINTENANCE_INTERVAL_SECONDS = 4 * 60 * 60; // 4 hours.
const FOLDER_SORTINDEX = 1000000;
// Roots that should be deleted from the server, instead of applied locally.
// This matches `AndroidBrowserBookmarksRepositorySession::forbiddenGUID`,
// but allows tags because we don't want to reparent tag folders or tag items
// to "unfiled".
const FORBIDDEN_INCOMING_IDS = ["pinned", "places", "readinglist"];
// Items with these parents should be deleted from the server. We allow
// children of the Places root, to avoid orphaning left pane queries and other
// descendants of custom roots.
const FORBIDDEN_INCOMING_PARENT_IDS = ["pinned", "readinglist"];
// The tracker ignores changes made by import and restore, to avoid bumping the
// score and triggering syncs during the process, as well as changes made by
// Sync.
ChromeUtils.defineLazyGetter(lazy, "IGNORED_SOURCES", () => [
lazy.PlacesUtils.bookmarks.SOURCES.SYNC,
lazy.PlacesUtils.bookmarks.SOURCES.IMPORT,
lazy.PlacesUtils.bookmarks.SOURCES.RESTORE,
lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
lazy.PlacesUtils.bookmarks.SOURCES.SYNC_REPARENT_REMOVED_FOLDER_CHILDREN,
]);
// The validation telemetry version for the engine. Version 1 is collected
// by `bookmark_validator.js`, and checks value as well as structure
// differences. Version 2 is collected by the engine as part of building the
// remote tree, and checks structure differences only.
const BOOKMARK_VALIDATOR_VERSION = 2;
// The maximum time that the engine should wait before aborting a bookmark
// merge.
const BOOKMARK_APPLY_TIMEOUT_MS = 5 * 60 * 60 * 1000; // 5 minutes
// The default frecency value to use when not known.
const FRECENCY_UNKNOWN = -1;
// Returns the constructor for a bookmark record type.
function getTypeObject(type) {
switch (type) {
case "bookmark":
return Bookmark;
case "query":
return BookmarkQuery;
case "folder":
return BookmarkFolder;
case "livemark":
return Livemark;
case "separator":
return BookmarkSeparator;
case "item":
return PlacesItem;
}
return null;
}
export function PlacesItem(collection, id, type) {
CryptoWrapper.call(this, collection, id);
this.type = type || "item";
}
PlacesItem.prototype = {
async decrypt(keyBundle) {
// Do the normal CryptoWrapper decrypt, but change types before returning
let clear = await CryptoWrapper.prototype.decrypt.call(this, keyBundle);
// Convert the abstract places item to the actual object type
if (!this.deleted) {
Object.setPrototypeOf(this, this.getTypeObject(this.type).prototype);
}
return clear;
},
getTypeObject: function PlacesItem_getTypeObject(type) {
let recordObj = getTypeObject(type);
if (!recordObj) {
throw new Error("Unknown places item object type: " + type);
}
return recordObj;
},
_logName: "Sync.Record.PlacesItem",
// Converts the record to a Sync bookmark object that can be passed to
// `PlacesSyncUtils.bookmarks.{insert, update}`.
toSyncBookmark() {
let result = {
kind: this.type,
recordId: this.id,
parentRecordId: this.parentid,
};
let dateAdded = lazy.PlacesSyncUtils.bookmarks.ratchetTimestampBackwards(
this.dateAdded,
+this.modified * 1000
);
if (dateAdded > 0) {
result.dateAdded = dateAdded;
}
return result;
},
// Populates the record from a Sync bookmark object returned from
// `PlacesSyncUtils.bookmarks.fetch`.
fromSyncBookmark(item) {
this.parentid = item.parentRecordId;
this.parentName = item.parentTitle;
if (item.dateAdded) {
this.dateAdded = item.dateAdded;
}
},
};
Object.setPrototypeOf(PlacesItem.prototype, CryptoWrapper.prototype);
Utils.deferGetSet(PlacesItem, "cleartext", [
"hasDupe",
"parentid",
"parentName",
"type",
"dateAdded",
]);
export function Bookmark(collection, id, type) {
PlacesItem.call(this, collection, id, type || "bookmark");
}
Bookmark.prototype = {
_logName: "Sync.Record.Bookmark",
toSyncBookmark() {
let info = PlacesItem.prototype.toSyncBookmark.call(this);
info.title = this.title;
info.url = this.bmkUri;
info.description = this.description;
info.tags = this.tags;
info.keyword = this.keyword;
return info;
},
fromSyncBookmark(item) {
PlacesItem.prototype.fromSyncBookmark.call(this, item);
this.title = item.title;
this.bmkUri = item.url.href;
this.description = item.description;
this.tags = item.tags;
this.keyword = item.keyword;
},
};
Object.setPrototypeOf(Bookmark.prototype, PlacesItem.prototype);
Utils.deferGetSet(Bookmark, "cleartext", [
"title",
"bmkUri",
"description",
"tags",
"keyword",
]);
export function BookmarkQuery(collection, id) {
Bookmark.call(this, collection, id, "query");
}
BookmarkQuery.prototype = {
_logName: "Sync.Record.BookmarkQuery",
toSyncBookmark() {
let info = Bookmark.prototype.toSyncBookmark.call(this);
info.folder = this.folderName || undefined; // empty string -> undefined
info.query = this.queryId;
return info;
},
fromSyncBookmark(item) {
Bookmark.prototype.fromSyncBookmark.call(this, item);
this.folderName = item.folder || undefined; // empty string -> undefined
this.queryId = item.query;
},
};
Object.setPrototypeOf(BookmarkQuery.prototype, Bookmark.prototype);
Utils.deferGetSet(BookmarkQuery, "cleartext", ["folderName", "queryId"]);
export function BookmarkFolder(collection, id, type) {
PlacesItem.call(this, collection, id, type || "folder");
}
BookmarkFolder.prototype = {
_logName: "Sync.Record.Folder",
toSyncBookmark() {
let info = PlacesItem.prototype.toSyncBookmark.call(this);
info.description = this.description;
info.title = this.title;
return info;
},
fromSyncBookmark(item) {
PlacesItem.prototype.fromSyncBookmark.call(this, item);
this.title = item.title;
this.description = item.description;
this.children = item.childRecordIds;
},
};
Object.setPrototypeOf(BookmarkFolder.prototype, PlacesItem.prototype);
Utils.deferGetSet(BookmarkFolder, "cleartext", [
"description",
"title",
"children",
]);
export function Livemark(collection, id) {
BookmarkFolder.call(this, collection, id, "livemark");
}
Livemark.prototype = {
_logName: "Sync.Record.Livemark",
toSyncBookmark() {
let info = BookmarkFolder.prototype.toSyncBookmark.call(this);
info.feed = this.feedUri;
info.site = this.siteUri;
return info;
},
fromSyncBookmark(item) {
BookmarkFolder.prototype.fromSyncBookmark.call(this, item);
this.feedUri = item.feed.href;
if (item.site) {
this.siteUri = item.site.href;
}
},
};
Object.setPrototypeOf(Livemark.prototype, BookmarkFolder.prototype);
Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]);
export function BookmarkSeparator(collection, id) {
PlacesItem.call(this, collection, id, "separator");
}
BookmarkSeparator.prototype = {
_logName: "Sync.Record.Separator",
fromSyncBookmark(item) {
PlacesItem.prototype.fromSyncBookmark.call(this, item);
this.pos = item.index;
},
};
Object.setPrototypeOf(BookmarkSeparator.prototype, PlacesItem.prototype);
Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
/**
* The bookmarks engine uses a different store that stages downloaded bookmarks
* in a separate database, instead of writing directly to Places. The buffer
* handles reconciliation, so we stub out `_reconcile`, and wait to pull changes
* until we're ready to upload.
*/
export function BookmarksEngine(service) {
SyncEngine.call(this, "Bookmarks", service);
}
BookmarksEngine.prototype = {
_recordObj: PlacesItem,
_trackerObj: BookmarksTracker,
_storeObj: BookmarksStore,
version: 2,
// Used to override the engine name in telemetry, so that we can distinguish
// this engine from the old, now removed non-buffered engine.
overrideTelemetryName: "bookmarks-buffered",
// Needed to ensure we don't miss items when resuming a sync that failed or
// aborted early.
_defaultSort: "oldest",
syncPriority: 4,
allowSkippedRecord: false,
async _ensureCurrentSyncID(newSyncID) {
await lazy.PlacesSyncUtils.bookmarks.ensureCurrentSyncId(newSyncID);
let buf = await this._store.ensureOpenMirror();
await buf.ensureCurrentSyncId(newSyncID);
},
async ensureCurrentSyncID(newSyncID) {
let shouldWipeRemote =
await lazy.PlacesSyncUtils.bookmarks.shouldWipeRemote();
if (!shouldWipeRemote) {
this._log.debug(
"Checking if server sync ID ${newSyncID} matches existing",
{ newSyncID }
);
await this._ensureCurrentSyncID(newSyncID);
return newSyncID;
}
// We didn't take the new sync ID because we need to wipe the server
// and other clients after a restore. Send the command, wipe the
// server, and reset our sync ID to reupload everything.
this._log.debug(
"Ignoring server sync ID ${newSyncID} after restore; " +
"wiping server and resetting sync ID",
{ newSyncID }
);
await this.service.clientsEngine.sendCommand(
"wipeEngine",
[this.name],
null,
{ reason: "bookmark-restore" }
);
let assignedSyncID = await this.resetSyncID();
return assignedSyncID;
},
async getSyncID() {
return lazy.PlacesSyncUtils.bookmarks.getSyncId();
},
async resetSyncID() {
await this._deleteServerCollection();
return this.resetLocalSyncID();
},
async resetLocalSyncID() {
let newSyncID = await lazy.PlacesSyncUtils.bookmarks.resetSyncId();
this._log.debug("Assigned new sync ID ${newSyncID}", { newSyncID });
let buf = await this._store.ensureOpenMirror();
await buf.ensureCurrentSyncId(newSyncID);
return newSyncID;
},
async getLastSync() {
let mirror = await this._store.ensureOpenMirror();
return mirror.getCollectionHighWaterMark();
},
async setLastSync(lastSync) {
let mirror = await this._store.ensureOpenMirror();
await mirror.setCollectionLastModified(lastSync);
// Update the last sync time in Places so that reverting to the original
// bookmarks engine doesn't download records we've already applied.
await lazy.PlacesSyncUtils.bookmarks.setLastSync(lastSync);
},
async _syncStartup() {
await super._syncStartup();
try {
// For first syncs, back up the user's bookmarks.
let lastSync = await this.getLastSync();
if (!lastSync) {
this._log.debug("Bookmarks backup starting");
await lazy.PlacesBackups.create(null, true);
this._log.debug("Bookmarks backup done");
}
} catch (ex) {
// Failure to create a backup is somewhat bad, but probably not bad
// enough to prevent syncing of bookmarks - so just log the error and
// continue.
this._log.warn(
"Error while backing up bookmarks, but continuing with sync",
ex
);
}
},
async _sync() {
try {
await super._sync();
if (this._ranMaintenanceOnLastSync) {
// If the last sync failed, we ran maintenance, and this sync succeeded,
// maintenance likely fixed the issue.
this._ranMaintenanceOnLastSync = false;
this.service.recordTelemetryEvent("maintenance", "fix", "bookmarks");
}
} catch (ex) {
if (
lazy.Async.isShutdownException(ex) ||
ex.status > 0 ||
ex.name == "InterruptedError"
) {
// Don't run maintenance on shutdown or HTTP errors, or if we aborted
// the sync because the user changed their bookmarks during merging.
throw ex;
}
if (ex.name == "MergeConflictError") {
this._log.warn(
"Bookmark syncing ran into a merge conflict error...will retry later"
);
return;
}
// Run Places maintenance periodically to try to recover from corruption
// that might have caused the sync to fail. We cap the interval because
// persistent failures likely indicate a problem that won't be fixed by
// running maintenance after every failed sync.
let elapsedSinceMaintenance =
Date.now() / 1000 -
Services.prefs.getIntPref("places.database.lastMaintenance", 0);
if (elapsedSinceMaintenance >= PLACES_MAINTENANCE_INTERVAL_SECONDS) {
this._log.error(
"Bookmark sync failed, ${elapsedSinceMaintenance}s " +
"elapsed since last run; running Places maintenance",
{ elapsedSinceMaintenance }
);
await lazy.PlacesDBUtils.maintenanceOnIdle();
this._ranMaintenanceOnLastSync = true;
this.service.recordTelemetryEvent("maintenance", "run", "bookmarks");
} else {
this._ranMaintenanceOnLastSync = false;
}
throw ex;
}
},
async _syncFinish() {
await SyncEngine.prototype._syncFinish.call(this);
await lazy.PlacesSyncUtils.bookmarks.ensureMobileQuery();
},
async pullAllChanges() {
return this.pullNewChanges();
},
async trackRemainingChanges() {
let changes = this._modified.changes;
await lazy.PlacesSyncUtils.bookmarks.pushChanges(changes);
},
_deleteId(id) {
this._noteDeletedId(id);
},
// The bookmarks engine rarely calls this method directly, except in tests or
// when handling a `reset{All, Engine}` command from another client. We
// usually reset local Sync metadata on a sync ID mismatch, which both engines
// override with logic that lives in Places and the mirror.
async _resetClient() {
await super._resetClient();
await lazy.PlacesSyncUtils.bookmarks.reset();
let buf = await this._store.ensureOpenMirror();
await buf.reset();
},
// Cleans up the Places root, reading list items (ignored in bug 762118,
// removed in bug 1155684), and pinned sites.
_shouldDeleteRemotely(incomingItem) {
return (
FORBIDDEN_INCOMING_IDS.includes(incomingItem.id) ||
FORBIDDEN_INCOMING_PARENT_IDS.includes(incomingItem.parentid)
);
},
emptyChangeset() {
return new BookmarksChangeset();
},
async _apply() {
let buf = await this._store.ensureOpenMirror();
let watchdog = this._newWatchdog();
watchdog.start(BOOKMARK_APPLY_TIMEOUT_MS);
try {
let recordsToUpload = await buf.apply({
remoteTimeSeconds: lazy.Resource.serverTime,
signal: watchdog.signal,
});
this._modified.replace(recordsToUpload);
} finally {
watchdog.stop();
if (watchdog.abortReason) {
this._log.warn(`Aborting bookmark merge: ${watchdog.abortReason}`);
}
}
},
async _processIncoming(newitems) {
await super._processIncoming(newitems);
await this._apply();
},
async _reconcile() {
return true;
},
async _createRecord(id) {
let record = await this._doCreateRecord(id);
if (!record.deleted) {
// Set hasDupe on all (non-deleted) records since we don't use it and we
// want to minimize the risk of older clients corrupting records. Note
// that the SyncedBookmarksMirror sets it for all records that it created,
// but we would like to ensure that weakly uploaded records are marked as
// hasDupe as well.
record.hasDupe = true;
}
return record;
},
async _doCreateRecord(id) {
let change = this._modified.changes[id];
if (!change) {
this._log.error(
"Creating record for item ${id} not in strong changeset",
{ id }
);
throw new TypeError("Can't create record for unchanged item");
}
let record = this._recordFromCleartext(id, change.cleartext);
record.sortindex = await this._store._calculateIndex(record);
return record;
},
_recordFromCleartext(id, cleartext) {
let recordObj = getTypeObject(cleartext.type);
if (!recordObj) {
this._log.warn(
"Creating record for item ${id} with unknown type ${type}",
{ id, type: cleartext.type }
);
recordObj = PlacesItem;
}
let record = new recordObj(this.name, id);
record.cleartext = cleartext;
return record;
},
async pullChanges() {
return {};
},
/**
* Writes successfully uploaded records back to the mirror, so that the
* mirror matches the server. We update the mirror before updating Places,
* which has implications for interrupted syncs.
*
* 1. Sync interrupted during upload; server doesn't support atomic uploads.
* We'll download and reapply everything that we uploaded before the
* interruption. All locally changed items retain their change counters.
* 2. Sync interrupted during upload; atomic uploads enabled. The server
* discards the batch. All changed local items retain their change
* counters, so the next sync resumes cleanly.
* 3. Sync interrupted during upload; outgoing records can't fit in a single
* batch. We'll download and reapply all records through the most recent
* committed batch. This is a variation of (1).
* 4. Sync interrupted after we update the mirror, but before cleanup. The
* mirror matches the server, but locally changed items retain their change
* counters. Reuploading them on the next sync should be idempotent, though
* unnecessary. If another client makes a conflicting remote change before
* we sync again, we may incorrectly prefer the local state.
* 5. Sync completes successfully. We'll update the mirror, and reset the
* change counters for all items.
*/
async _onRecordsWritten(succeeded, failed, serverModifiedTime) {
let records = [];
for (let id of succeeded) {
let change = this._modified.changes[id];
if (!change) {
// TODO (Bug 1433178): Write weakly uploaded records back to the mirror.
this._log.info("Uploaded record not in strong changeset", id);
continue;
}
if (!change.synced) {
this._log.info("Record in strong changeset not uploaded", id);
continue;
}
let cleartext = change.cleartext;
if (!cleartext) {
this._log.error(
"Missing Sync record cleartext for ${id} in ${change}",
{ id, change }
);
throw new TypeError("Missing cleartext for uploaded Sync record");
}
let record = this._recordFromCleartext(id, cleartext);
record.modified = serverModifiedTime;
records.push(record);
}
let buf = await this._store.ensureOpenMirror();
await buf.store(records, { needsMerge: false });
},
async finalize() {
await super.finalize();
await this._store.finalize();
},
};
Object.setPrototypeOf(BookmarksEngine.prototype, SyncEngine.prototype);
/**
* The bookmarks store delegates to the mirror for staging and applying
* records. Most `Store` methods intentionally remain abstract, so you can't use
* this store to create or update bookmarks in Places. All changes must go
* through the mirror, which takes care of merging and producing a valid tree.
*/
function BookmarksStore(name, engine) {
Store.call(this, name, engine);
}
BookmarksStore.prototype = {
_openMirrorPromise: null,
// For tests.
_batchChunkSize: 500,
// Create a record starting from the weave id (places guid)
async createRecord(id, collection) {
let item = await lazy.PlacesSyncUtils.bookmarks.fetch(id);
if (!item) {
// deleted item
let record = new PlacesItem(collection, id);
record.deleted = true;
return record;
}
let recordObj = getTypeObject(item.kind);
if (!recordObj) {
this._log.warn("Unknown item type, cannot serialize: " + item.kind);
recordObj = PlacesItem;
}
let record = new recordObj(collection, id);
record.fromSyncBookmark(item);
record.sortindex = await this._calculateIndex(record);
return record;
},
async _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) {
let frecency = FRECENCY_UNKNOWN;
try {
frecency = await lazy.PlacesSyncUtils.history.fetchURLFrecency(
record.bmkUri
);
} catch (ex) {
this._log.warn(
`Failed to fetch frecency for ${record.id}; assuming default`,
ex
);
this._log.trace("Record {id} has invalid URL ${bmkUri}", record);
}
if (frecency != FRECENCY_UNKNOWN) {
index += frecency;
}
}
return index;
},
async wipe() {
// Save a backup before clearing out all bookmarks.
await lazy.PlacesBackups.create(null, true);
await lazy.PlacesSyncUtils.bookmarks.wipe();
},
ensureOpenMirror() {
if (!this._openMirrorPromise) {
this._openMirrorPromise = this._openMirror().catch(err => {
// We may have failed to open the mirror temporarily; for example, if
// the database is locked. Clear the promise so that subsequent
// `ensureOpenMirror` calls can try to open the mirror again.
this._openMirrorPromise = null;
throw err;
});
}
return this._openMirrorPromise;
},
async _openMirror() {
let mirrorPath = PathUtils.join(
PathUtils.profileDir,
"weave",
"bookmarks.sqlite"
);
await IOUtils.makeDirectory(PathUtils.parent(mirrorPath), {
createAncestors: true,
});
return lazy.SyncedBookmarksMirror.open({
path: mirrorPath,
recordStepTelemetry: (name, took, counts) => {
lazy.Observers.notify(
"weave:engine:sync:step",
{
name,
took,
counts,
},
this.name
);
},
recordValidationTelemetry: (took, checked, problems) => {
lazy.Observers.notify(
"weave:engine:validate:finish",
{
version: BOOKMARK_VALIDATOR_VERSION,
took,
checked,
problems,
},
this.name
);
},
});
},
async applyIncomingBatch(records) {
let buf = await this.ensureOpenMirror();
for (let chunk of lazy.PlacesUtils.chunkArray(
records,
this._batchChunkSize
)) {
await buf.store(chunk);
}
// Array of failed records.
return [];
},
async applyIncoming(record) {
let buf = await this.ensureOpenMirror();
await buf.store([record]);
},
async finalize() {
if (!this._openMirrorPromise) {
return;
}
let buf = await this._openMirrorPromise;
await buf.finalize();
},
};
Object.setPrototypeOf(BookmarksStore.prototype, Store.prototype);
// The bookmarks tracker is a special flower. Instead of listening for changes
// via observer notifications, it queries Places for the set of items that have
// changed since the last sync. Because it's a "pull-based" tracker, it ignores
// all concepts of "add a changed ID." However, it still registers an observer
// to bump the score, so that changed bookmarks are synced immediately.
function BookmarksTracker(name, engine) {
Tracker.call(this, name, engine);
}
BookmarksTracker.prototype = {
onStart() {
this._placesListener = new PlacesWeakCallbackWrapper(
this.handlePlacesEvents.bind(this)
);
lazy.PlacesUtils.observers.addListener(
[
"bookmark-added",
"bookmark-removed",
"bookmark-moved",
"bookmark-guid-changed",
"bookmark-keyword-changed",
"bookmark-tags-changed",
"bookmark-time-changed",
"bookmark-title-changed",
"bookmark-url-changed",
],
this._placesListener
);
Svc.Obs.add("bookmarks-restore-begin", this);
Svc.Obs.add("bookmarks-restore-success", this);
Svc.Obs.add("bookmarks-restore-failed", this);
},
onStop() {
lazy.PlacesUtils.observers.removeListener(
[
"bookmark-added",
"bookmark-removed",
"bookmark-moved",
"bookmark-guid-changed",
"bookmark-keyword-changed",
"bookmark-tags-changed",
"bookmark-time-changed",
"bookmark-title-changed",
"bookmark-url-changed",
],
this._placesListener
);
Svc.Obs.remove("bookmarks-restore-begin", this);
Svc.Obs.remove("bookmarks-restore-success", this);
Svc.Obs.remove("bookmarks-restore-failed", this);
},
async getChangedIDs() {
return lazy.PlacesSyncUtils.bookmarks.pullChanges();
},
observe(subject, topic, data) {
switch (topic) {
case "bookmarks-restore-begin":
this._log.debug("Ignoring changes from importing bookmarks.");
break;
case "bookmarks-restore-success":
this._log.debug("Tracking all items on successful import.");
if (data == "json") {
this._log.debug(
"Restore succeeded: wiping server and other clients."
);
// Trigger an immediate sync. `ensureCurrentSyncID` will notice we
// restored, wipe the server and other clients, reset the sync ID, and
// upload the restored tree.
this.score += SCORE_INCREMENT_XLARGE;
} else {
// "html", "html-initial", or "json-append"
this._log.debug("Import succeeded.");
}
break;
case "bookmarks-restore-failed":
this._log.debug("Tracking all items on failed import.");
break;
}
},
QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
/* Every add/remove/change will trigger a sync for MULTI_DEVICE */
_upScore: function BMT__upScore() {
this.score += SCORE_INCREMENT_XLARGE;
},
handlePlacesEvents(events) {
for (let event of events) {
switch (event.type) {
case "bookmark-added":
case "bookmark-removed":
case "bookmark-moved":
case "bookmark-keyword-changed":
case "bookmark-tags-changed":
case "bookmark-time-changed":
case "bookmark-title-changed":
case "bookmark-url-changed":
if (lazy.IGNORED_SOURCES.includes(event.source)) {
continue;
}
this._log.trace(`'${event.type}': ${event.id}`);
this._upScore();
break;
case "bookmark-guid-changed":
if (event.source !== lazy.PlacesUtils.bookmarks.SOURCES.SYNC) {
this._log.warn(
"The source of bookmark-guid-changed event shoud be sync."
);
continue;
}
this._log.trace(`'${event.type}': ${event.id}`);
this._upScore();
break;
case "purge-caches":
this._log.trace("purge-caches");
this._upScore();
break;
}
}
},
};
Object.setPrototypeOf(BookmarksTracker.prototype, Tracker.prototype);
/**
* A changeset that stores extra metadata in a change record for each ID. The
* engine updates this metadata when uploading Sync records, and writes it back
* to Places in `BookmarksEngine#trackRemainingChanges`.
*
* The `synced` property on a change record means its corresponding item has
* been uploaded, and we should pretend it doesn't exist in the changeset.
*/
class BookmarksChangeset extends Changeset {
// Only `_reconcile` calls `getModifiedTimestamp` and `has`, and the engine
// does its own reconciliation.
getModifiedTimestamp() {
throw new Error("Don't use timestamps to resolve bookmark conflicts");
}
has() {
throw new Error("Don't use the changeset to resolve bookmark conflicts");
}
delete(id) {
let change = this.changes[id];
if (change) {
// Mark the change as synced without removing it from the set. We do this
// so that we can update Places in `trackRemainingChanges`.
change.synced = true;
}
}
ids() {
let results = new Set();
for (let id in this.changes) {
if (!this.changes[id].synced) {
results.add(id);
}
}
return [...results];
}
}