summaryrefslogtreecommitdiff
path: root/components/weave/src/engines
diff options
context:
space:
mode:
Diffstat (limited to 'components/weave/src/engines')
-rw-r--r--components/weave/src/engines/addons.js706
-rw-r--r--components/weave/src/engines/bookmarks.js1542
-rw-r--r--components/weave/src/engines/clients.js476
-rw-r--r--components/weave/src/engines/forms.js246
-rw-r--r--components/weave/src/engines/history.js417
-rw-r--r--components/weave/src/engines/passwords.js305
-rw-r--r--components/weave/src/engines/prefs.js260
-rw-r--r--components/weave/src/engines/tabs.js376
8 files changed, 0 insertions, 4328 deletions
diff --git a/components/weave/src/engines/addons.js b/components/weave/src/engines/addons.js
deleted file mode 100644
index 86f94e80b..000000000
--- a/components/weave/src/engines/addons.js
+++ /dev/null
@@ -1,706 +0,0 @@
-/* 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/. */
-
-/*
- * This file defines the add-on sync functionality.
- *
- * There are currently a number of known limitations:
- * - We only sync XPI extensions and themes available from addons.mozilla.org.
- * We hope to expand support for other add-ons eventually.
- * - We only attempt syncing of add-ons between applications of the same type.
- * This means add-ons will not synchronize between Firefox desktop and
- * Firefox mobile, for example. This is because of significant add-on
- * incompatibility between application types.
- *
- * Add-on records exist for each known {add-on, app-id} pair in the Sync client
- * set. Each record has a randomly chosen GUID. The records then contain
- * basic metadata about the add-on.
- *
- * We currently synchronize:
- *
- * - Installations
- * - Uninstallations
- * - User enabling and disabling
- *
- * Synchronization is influenced by the following preferences:
- *
- * - services.sync.addons.ignoreRepositoryChecking
- * - services.sync.addons.ignoreUserEnabledChanges
- * - services.sync.addons.trustedSourceHostnames
- *
- * See the documentation in services-sync.js for the behavior of these prefs.
- */
-"use strict";
-
-var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://services-sync/addonutils.js");
-Cu.import("resource://services-sync/addonsreconciler.js");
-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/constants.js");
-Cu.import("resource://gre/modules/Async.jsm");
-
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
- "resource://gre/modules/AddonManager.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
- "resource://gre/modules/addons/AddonRepository.jsm");
-
-this.EXPORTED_SYMBOLS = ["AddonsEngine"];
-
-// 7 days in milliseconds.
-const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
-
-/**
- * AddonRecord represents the state of an add-on in an application.
- *
- * Each add-on has its own record for each application ID it is installed
- * on.
- *
- * The ID of add-on records is a randomly-generated GUID. It is random instead
- * of deterministic so the URIs of the records cannot be guessed and so
- * compromised server credentials won't result in disclosure of the specific
- * add-ons present in a Sync account.
- *
- * The record contains the following fields:
- *
- * addonID
- * ID of the add-on. This correlates to the "id" property on an Addon type.
- *
- * applicationID
- * The application ID this record is associated with.
- *
- * enabled
- * Boolean stating whether add-on is enabled or disabled by the user.
- *
- * source
- * String indicating where an add-on is from. Currently, we only support
- * the value "amo" which indicates that the add-on came from the official
- * add-ons repository, addons.mozilla.org. In the future, we may support
- * installing add-ons from other sources. This provides a future-compatible
- * mechanism for clients to only apply records they know how to handle.
- */
-function AddonRecord(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-AddonRecord.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Record.Addon"
-};
-
-Utils.deferGetSet(AddonRecord, "cleartext", ["addonID",
- "applicationID",
- "enabled",
- "source"]);
-
-/**
- * The AddonsEngine handles synchronization of add-ons between clients.
- *
- * The engine maintains an instance of an AddonsReconciler, which is the entity
- * maintaining state for add-ons. It provides the history and tracking APIs
- * that AddonManager doesn't.
- *
- * The engine instance overrides a handful of functions on the base class. The
- * rationale for each is documented by that function.
- */
-this.AddonsEngine = function AddonsEngine(service) {
- SyncEngine.call(this, "Addons", service);
-
- this._reconciler = new AddonsReconciler();
-}
-AddonsEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _storeObj: AddonsStore,
- _trackerObj: AddonsTracker,
- _recordObj: AddonRecord,
- version: 1,
-
- syncPriority: 5,
-
- _reconciler: null,
-
- /**
- * Override parent method to find add-ons by their public ID, not Sync GUID.
- */
- _findDupe: function _findDupe(item) {
- let id = item.addonID;
-
- // The reconciler should have been updated at the top of the sync, so we
- // can assume it is up to date when this function is called.
- let addons = this._reconciler.addons;
- if (!(id in addons)) {
- return null;
- }
-
- let addon = addons[id];
- if (addon.guid != item.id) {
- return addon.guid;
- }
-
- return null;
- },
-
- /**
- * Override getChangedIDs to pull in tracker changes plus changes from the
- * reconciler log.
- */
- getChangedIDs: function getChangedIDs() {
- let changes = {};
- for (let [id, modified] of Object.entries(this._tracker.changedIDs)) {
- changes[id] = modified;
- }
-
- let lastSyncDate = new Date(this.lastSync * 1000);
-
- // The reconciler should have been refreshed at the beginning of a sync and
- // we assume this function is only called from within a sync.
- let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate);
- let addons = this._reconciler.addons;
- for (let change of reconcilerChanges) {
- let changeTime = change[0];
- let id = change[2];
-
- if (!(id in addons)) {
- continue;
- }
-
- // Keep newest modified time.
- if (id in changes && changeTime < changes[id]) {
- continue;
- }
-
- if (!this._store.isAddonSyncable(addons[id])) {
- continue;
- }
-
- this._log.debug("Adding changed add-on from changes log: " + id);
- let addon = addons[id];
- changes[addon.guid] = changeTime.getTime() / 1000;
- }
-
- return changes;
- },
-
- /**
- * Override start of sync function to refresh reconciler.
- *
- * Many functions in this class assume the reconciler is refreshed at the
- * top of a sync. If this ever changes, those functions should be revisited.
- *
- * Technically speaking, we don't need to refresh the reconciler on every
- * sync since it is installed as an AddonManager listener. However, add-ons
- * are complicated and we force a full refresh, just in case the listeners
- * missed something.
- */
- _syncStartup: function _syncStartup() {
- // We refresh state before calling parent because syncStartup in the parent
- // looks for changed IDs, which is dependent on add-on state being up to
- // date.
- this._refreshReconcilerState();
-
- SyncEngine.prototype._syncStartup.call(this);
- },
-
- /**
- * Override end of sync to perform a little housekeeping on the reconciler.
- *
- * We prune changes to prevent the reconciler state from growing without
- * bound. Even if it grows unbounded, there would have to be many add-on
- * changes (thousands) for it to slow things down significantly. This is
- * highly unlikely to occur. Still, we exercise defense just in case.
- */
- _syncCleanup: function _syncCleanup() {
- let ms = 1000 * this.lastSync - PRUNE_ADDON_CHANGES_THRESHOLD;
- this._reconciler.pruneChangesBeforeDate(new Date(ms));
-
- SyncEngine.prototype._syncCleanup.call(this);
- },
-
- /**
- * Helper function to ensure reconciler is up to date.
- *
- * This will synchronously load the reconciler's state from the file
- * system (if needed) and refresh the state of the reconciler.
- */
- _refreshReconcilerState: function _refreshReconcilerState() {
- this._log.debug("Refreshing reconciler state");
- let cb = Async.makeSpinningCallback();
- this._reconciler.refreshGlobalState(cb);
- cb.wait();
- }
-};
-
-/**
- * This is the primary interface between Sync and the Addons Manager.
- *
- * In addition to the core store APIs, we provide convenience functions to wrap
- * Add-on Manager APIs with Sync-specific semantics.
- */
-function AddonsStore(name, engine) {
- Store.call(this, name, engine);
-}
-AddonsStore.prototype = {
- __proto__: Store.prototype,
-
- // Define the add-on types (.type) that we support.
- _syncableTypes: ["extension", "theme"],
-
- _extensionsPrefs: new Preferences("extensions."),
-
- get reconciler() {
- return this.engine._reconciler;
- },
-
- /**
- * Override applyIncoming to filter out records we can't handle.
- */
- applyIncoming: function applyIncoming(record) {
- // The fields we look at aren't present when the record is deleted.
- if (!record.deleted) {
- // Ignore records not belonging to our application ID because that is the
- // current policy.
- if (record.applicationID != Services.appinfo.ID) {
- this._log.info("Ignoring incoming record from other App ID: " +
- record.id);
- return;
- }
-
- // Ignore records that aren't from the official add-on repository, as that
- // is our current policy.
- if (record.source != "amo") {
- this._log.info("Ignoring unknown add-on source (" + record.source + ")" +
- " for " + record.id);
- return;
- }
- }
-
- Store.prototype.applyIncoming.call(this, record);
- },
-
-
- /**
- * Provides core Store API to create/install an add-on from a record.
- */
- create: function create(record) {
- let cb = Async.makeSpinningCallback();
- AddonUtils.installAddons([{
- id: record.addonID,
- syncGUID: record.id,
- enabled: record.enabled,
- requireSecureURI: this._extensionsPrefs.get("install.requireSecureOrigin", true),
- }], cb);
-
- // This will throw if there was an error. This will get caught by the sync
- // engine and the record will try to be applied later.
- let results = cb.wait();
-
- let addon;
- for (let a of results.addons) {
- if (a.id == record.addonID) {
- addon = a;
- break;
- }
- }
-
- // This should never happen, but is present as a fail-safe.
- if (!addon) {
- throw new Error("Add-on not found after install: " + record.addonID);
- }
-
- this._log.info("Add-on installed: " + record.addonID);
- },
-
- /**
- * Provides core Store API to remove/uninstall an add-on from a record.
- */
- remove: function remove(record) {
- // If this is called, the payload is empty, so we have to find by GUID.
- let addon = this.getAddonByGUID(record.id);
- if (!addon) {
- // We don't throw because if the add-on could not be found then we assume
- // it has already been uninstalled and there is nothing for this function
- // to do.
- return;
- }
-
- this._log.info("Uninstalling add-on: " + addon.id);
- let cb = Async.makeSpinningCallback();
- AddonUtils.uninstallAddon(addon, cb);
- cb.wait();
- },
-
- /**
- * Provides core Store API to update an add-on from a record.
- */
- update: function update(record) {
- let addon = this.getAddonByID(record.addonID);
-
- // update() is called if !this.itemExists. And, since itemExists consults
- // the reconciler only, we need to take care of some corner cases.
- //
- // First, the reconciler could know about an add-on that was uninstalled
- // and no longer present in the add-ons manager.
- if (!addon) {
- this.create(record);
- return;
- }
-
- // It's also possible that the add-on is non-restartless and has pending
- // install/uninstall activity.
- //
- // We wouldn't get here if the incoming record was for a deletion. So,
- // check for pending uninstall and cancel if necessary.
- if (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
- addon.cancelUninstall();
-
- // We continue with processing because there could be state or ID change.
- }
-
- let cb = Async.makeSpinningCallback();
- this.updateUserDisabled(addon, !record.enabled, cb);
- cb.wait();
- },
-
- /**
- * Provide core Store API to determine if a record exists.
- */
- itemExists: function itemExists(guid) {
- let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
-
- return !!addon;
- },
-
- /**
- * Create an add-on record from its GUID.
- *
- * @param guid
- * Add-on GUID (from extensions DB)
- * @param collection
- * Collection to add record to.
- *
- * @return AddonRecord instance
- */
- createRecord: function createRecord(guid, collection) {
- let record = new AddonRecord(collection, guid);
- record.applicationID = Services.appinfo.ID;
-
- let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
-
- // If we don't know about this GUID or if it has been uninstalled, we mark
- // the record as deleted.
- if (!addon || !addon.installed) {
- record.deleted = true;
- return record;
- }
-
- record.modified = addon.modified.getTime() / 1000;
-
- record.addonID = addon.id;
- record.enabled = addon.enabled;
-
- // This needs to be dynamic when add-ons don't come from AddonRepository.
- record.source = "amo";
-
- return record;
- },
-
- /**
- * Changes the id of an add-on.
- *
- * This implements a core API of the store.
- */
- changeItemID: function changeItemID(oldID, newID) {
- // We always update the GUID in the reconciler because it will be
- // referenced later in the sync process.
- let state = this.reconciler.getAddonStateFromSyncGUID(oldID);
- if (state) {
- state.guid = newID;
- let cb = Async.makeSpinningCallback();
- this.reconciler.saveState(null, cb);
- cb.wait();
- }
-
- let addon = this.getAddonByGUID(oldID);
- if (!addon) {
- this._log.debug("Cannot change item ID (" + oldID + ") in Add-on " +
- "Manager because old add-on not present: " + oldID);
- return;
- }
-
- addon.syncGUID = newID;
- },
-
- /**
- * Obtain the set of all syncable add-on Sync GUIDs.
- *
- * This implements a core Store API.
- */
- getAllIDs: function getAllIDs() {
- let ids = {};
-
- let addons = this.reconciler.addons;
- for (let id in addons) {
- let addon = addons[id];
- if (this.isAddonSyncable(addon)) {
- ids[addon.guid] = true;
- }
- }
-
- return ids;
- },
-
- /**
- * Wipe engine data.
- *
- * This uninstalls all syncable addons from the application. In case of
- * error, it logs the error and keeps trying with other add-ons.
- */
- wipe: function wipe() {
- this._log.info("Processing wipe.");
-
- this.engine._refreshReconcilerState();
-
- // We only wipe syncable add-ons. Wipe is a Sync feature not a security
- // feature.
- for (let guid in this.getAllIDs()) {
- let addon = this.getAddonByGUID(guid);
- if (!addon) {
- this._log.debug("Ignoring add-on because it couldn't be obtained: " +
- guid);
- continue;
- }
-
- this._log.info("Uninstalling add-on as part of wipe: " + addon.id);
- Utils.catch(addon.uninstall)();
- }
- },
-
- /***************************************************************************
- * Functions below are unique to this store and not part of the Store API *
- ***************************************************************************/
-
- /**
- * Synchronously obtain an add-on from its public ID.
- *
- * @param id
- * Add-on ID
- * @return Addon or undefined if not found
- */
- getAddonByID: function getAddonByID(id) {
- let cb = Async.makeSyncCallback();
- AddonManager.getAddonByID(id, cb);
- return Async.waitForSyncCallback(cb);
- },
-
- /**
- * Synchronously obtain an add-on from its Sync GUID.
- *
- * @param guid
- * Add-on Sync GUID
- * @return DBAddonInternal or null
- */
- getAddonByGUID: function getAddonByGUID(guid) {
- let cb = Async.makeSyncCallback();
- AddonManager.getAddonBySyncGUID(guid, cb);
- return Async.waitForSyncCallback(cb);
- },
-
- /**
- * Determines whether an add-on is suitable for Sync.
- *
- * @param addon
- * Addon instance
- * @return Boolean indicating whether it is appropriate for Sync
- */
- isAddonSyncable: function isAddonSyncable(addon) {
- // Currently, we limit syncable add-ons to those that are:
- // 1) In a well-defined set of types
- // 2) Installed in the current profile
- // 3) Not installed by a foreign entity (i.e. installed by the app)
- // since they act like global extensions.
- // 4) Is not a hotfix.
- // 5) Are installed from AMO
-
- // We could represent the test as a complex boolean expression. We go the
- // verbose route so the failure reason is logged.
- if (!addon) {
- this._log.debug("Null object passed to isAddonSyncable.");
- return false;
- }
-
- if (this._syncableTypes.indexOf(addon.type) == -1) {
- this._log.debug(addon.id + " not syncable: type not in whitelist: " +
- addon.type);
- return false;
- }
-
- if (!(addon.scope & AddonManager.SCOPE_PROFILE)) {
- this._log.debug(addon.id + " not syncable: not installed in profile.");
- return false;
- }
-
- // This may be too aggressive. If an add-on is downloaded from AMO and
- // manually placed in the profile directory, foreignInstall will be set.
- // Arguably, that add-on should be syncable.
- // TODO Address the edge case and come up with more robust heuristics.
- if (addon.foreignInstall) {
- this._log.debug(addon.id + " not syncable: is foreign install.");
- return false;
- }
-
- // Ignore hotfix extensions (bug 741670). The pref may not be defined.
- if (this._extensionsPrefs.get("hotfix.id", null) == addon.id) {
- this._log.debug(addon.id + " not syncable: is a hotfix.");
- return false;
- }
-
- // We provide a back door to skip the repository checking of an add-on.
- // This is utilized by the tests to make testing easier. Users could enable
- // this, but it would sacrifice security.
- if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) {
- return true;
- }
-
- let cb = Async.makeSyncCallback();
- AddonRepository.getCachedAddonByID(addon.id, cb);
- let result = Async.waitForSyncCallback(cb);
-
- if (!result) {
- this._log.debug(addon.id + " not syncable: add-on not found in add-on " +
- "repository.");
- return false;
- }
-
- return this.isSourceURITrusted(result.sourceURI);
- },
-
- /**
- * Determine whether an add-on's sourceURI field is trusted and the add-on
- * can be installed.
- *
- * This function should only ever be called from isAddonSyncable(). It is
- * exposed as a separate function to make testing easier.
- *
- * @param uri
- * nsIURI instance to validate
- * @return bool
- */
- isSourceURITrusted: function isSourceURITrusted(uri) {
- // For security reasons, we currently limit synced add-ons to those
- // installed from trusted hostname(s). We additionally require TLS with
- // the add-ons site to help prevent forgeries.
- let trustedHostnames = Svc.Prefs.get("addons.trustedSourceHostnames", "")
- .split(",");
-
- if (!uri) {
- this._log.debug("Undefined argument to isSourceURITrusted().");
- return false;
- }
-
- // Scheme is validated before the hostname because uri.host may not be
- // populated for certain schemes. It appears to always be populated for
- // https, so we avoid the potential NS_ERROR_FAILURE on field access.
- if (uri.scheme != "https") {
- this._log.debug("Source URI not HTTPS: " + uri.spec);
- return false;
- }
-
- if (trustedHostnames.indexOf(uri.host) == -1) {
- this._log.debug("Source hostname not trusted: " + uri.host);
- return false;
- }
-
- return true;
- },
-
- /**
- * Update the userDisabled flag on an add-on.
- *
- * This will enable or disable an add-on and call the supplied callback when
- * the action is complete. If no action is needed, the callback gets called
- * immediately.
- *
- * @param addon
- * Addon instance to manipulate.
- * @param value
- * Boolean to which to set userDisabled on the passed Addon.
- * @param callback
- * Function to be called when action is complete. Will receive 2
- * arguments, a truthy value that signifies error, and the Addon
- * instance passed to this function.
- */
- updateUserDisabled: function updateUserDisabled(addon, value, callback) {
- if (addon.userDisabled == value) {
- callback(null, addon);
- return;
- }
-
- // A pref allows changes to the enabled flag to be ignored.
- if (Svc.Prefs.get("addons.ignoreUserEnabledChanges", false)) {
- this._log.info("Ignoring enabled state change due to preference: " +
- addon.id);
- callback(null, addon);
- return;
- }
-
- AddonUtils.updateUserDisabled(addon, value, callback);
- },
-};
-
-/**
- * The add-ons tracker keeps track of real-time changes to add-ons.
- *
- * It hooks up to the reconciler and receives notifications directly from it.
- */
-function AddonsTracker(name, engine) {
- Tracker.call(this, name, engine);
-}
-AddonsTracker.prototype = {
- __proto__: Tracker.prototype,
-
- get reconciler() {
- return this.engine._reconciler;
- },
-
- get store() {
- return this.engine._store;
- },
-
- /**
- * This callback is executed whenever the AddonsReconciler sends out a change
- * notification. See AddonsReconciler.addChangeListener().
- */
- changeListener: function changeHandler(date, change, addon) {
- this._log.debug("changeListener invoked: " + change + " " + addon.id);
- // Ignore changes that occur during sync.
- if (this.ignoreAll) {
- return;
- }
-
- if (!this.store.isAddonSyncable(addon)) {
- this._log.debug("Ignoring change because add-on isn't syncable: " +
- addon.id);
- return;
- }
-
- this.addChangedID(addon.guid, date.getTime() / 1000);
- this.score += SCORE_INCREMENT_XLARGE;
- },
-
- startTracking: function() {
- if (this.engine.enabled) {
- this.reconciler.startListening();
- }
-
- this.reconciler.addChangeListener(this);
- },
-
- stopTracking: function() {
- this.reconciler.removeChangeListener(this);
- this.reconciler.stopListening();
- },
-};
diff --git a/components/weave/src/engines/bookmarks.js b/components/weave/src/engines/bookmarks.js
deleted file mode 100644
index 61e771134..000000000
--- a/components/weave/src/engines/bookmarks.js
+++ /dev/null
@@ -1,1542 +0,0 @@
-/* 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/. */
-
-this.EXPORTED_SYMBOLS = ['BookmarksEngine', "PlacesItem", "Bookmark",
- "BookmarkFolder", "BookmarkQuery",
- "Livemark", "BookmarkSeparator"];
-
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cu = Components.utils;
-
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Async.jsm");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/PlacesBackups.jsm");
-
-const ALLBOOKMARKS_ANNO = "AllBookmarks";
-const DESCRIPTION_ANNO = "bookmarkProperties/description";
-const SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
-const MOBILEROOT_ANNO = "mobile/bookmarksRoot";
-const MOBILE_ANNO = "MobileBookmarks";
-const EXCLUDEBACKUP_ANNO = "places/excludeFromBackup";
-const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
-const PARENT_ANNO = "sync/parent";
-const ORGANIZERQUERY_ANNO = "PlacesOrganizer/OrganizerQuery";
-const ANNOS_TO_TRACK = [DESCRIPTION_ANNO, SIDEBAR_ANNO,
- PlacesUtils.LMANNO_FEEDURI, PlacesUtils.LMANNO_SITEURI];
-
-const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
-const FOLDER_SORTINDEX = 1000000;
-
-this.PlacesItem = function PlacesItem(collection, id, type) {
- CryptoWrapper.call(this, collection, id);
- this.type = type || "item";
-}
-PlacesItem.prototype = {
- decrypt: function PlacesItem_decrypt(keyBundle) {
- // Do the normal CryptoWrapper decrypt, but change types before returning
- let clear = CryptoWrapper.prototype.decrypt.call(this, keyBundle);
-
- // 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":
- case "microsummary":
- return Bookmark;
- 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: "Sync.Record.PlacesItem",
-};
-
-Utils.deferGetSet(PlacesItem,
- "cleartext",
- ["hasDupe", "parentid", "parentName", "type"]);
-
-this.Bookmark = function Bookmark(collection, id, type) {
- PlacesItem.call(this, collection, id, type || "bookmark");
-}
-Bookmark.prototype = {
- __proto__: PlacesItem.prototype,
- _logName: "Sync.Record.Bookmark",
-};
-
-Utils.deferGetSet(Bookmark,
- "cleartext",
- ["title", "bmkUri", "description",
- "loadInSidebar", "tags", "keyword"]);
-
-this.BookmarkQuery = function BookmarkQuery(collection, id) {
- Bookmark.call(this, collection, id, "query");
-}
-BookmarkQuery.prototype = {
- __proto__: Bookmark.prototype,
- _logName: "Sync.Record.BookmarkQuery",
-};
-
-Utils.deferGetSet(BookmarkQuery,
- "cleartext",
- ["folderName", "queryId"]);
-
-this.BookmarkFolder = function BookmarkFolder(collection, id, type) {
- PlacesItem.call(this, collection, id, type || "folder");
-}
-BookmarkFolder.prototype = {
- __proto__: PlacesItem.prototype,
- _logName: "Sync.Record.Folder",
-};
-
-Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title",
- "children"]);
-
-this.Livemark = function Livemark(collection, id) {
- BookmarkFolder.call(this, collection, id, "livemark");
-}
-Livemark.prototype = {
- __proto__: BookmarkFolder.prototype,
- _logName: "Sync.Record.Livemark",
-};
-
-Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]);
-
-this.BookmarkSeparator = function BookmarkSeparator(collection, id) {
- PlacesItem.call(this, collection, id, "separator");
-}
-BookmarkSeparator.prototype = {
- __proto__: PlacesItem.prototype,
- _logName: "Sync.Record.Separator",
-};
-
-Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
-
-
-let kSpecialIds = {
-
- // Special IDs. Note that mobile can attempt to create a record on
- // dereference; special accessors are provided to prevent recursion within
- // observers.
- guids: ["menu", "places", "tags", "toolbar", "unfiled", "mobile"],
-
- // Create the special mobile folder to store mobile bookmarks.
- createMobileRoot: function createMobileRoot() {
- let root = PlacesUtils.placesRootId;
- let mRoot = PlacesUtils.bookmarks.createFolder(root, "mobile", -1);
- PlacesUtils.annotations.setItemAnnotation(
- mRoot, MOBILEROOT_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
- PlacesUtils.annotations.setItemAnnotation(
- mRoot, EXCLUDEBACKUP_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
- return mRoot;
- },
-
- findMobileRoot: function findMobileRoot(create) {
- // Use the (one) mobile root if it already exists.
- let root = PlacesUtils.annotations.getItemsWithAnnotation(MOBILEROOT_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() {
- return PlacesUtils.bookmarksMenuFolderId;
- },
- get places() {
- return PlacesUtils.placesRootId;
- },
- get tags() {
- return PlacesUtils.tagsFolderId;
- },
- get toolbar() {
- return PlacesUtils.toolbarFolderId;
- },
- get unfiled() {
- return PlacesUtils.unfiledBookmarksFolderId;
- },
- get mobile() {
- return this.findMobileRoot(true);
- },
-};
-
-this.BookmarksEngine = function BookmarksEngine(service) {
- SyncEngine.call(this, "Bookmarks", service);
-}
-BookmarksEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _recordObj: PlacesItem,
- _storeObj: BookmarksStore,
- _trackerObj: BookmarksTracker,
- version: 2,
- _defaultSort: "index",
-
- syncPriority: 4,
-
- _sync: function _sync() {
- let engine = this;
- let batchEx = null;
-
- // Try running sync in batch mode
- PlacesUtils.bookmarks.runInBatchMode({
- runBatched: function wrappedSync() {
- try {
- SyncEngine.prototype._sync.call(engine);
- }
- catch(ex) {
- batchEx = ex;
- }
- }
- }, null);
-
- // Expose the exception if something inside the batch failed
- if (batchEx != null) {
- throw batchEx;
- }
- },
-
- _guidMapFailed: false,
- _buildGUIDMap: function _buildGUIDMap() {
- let guidMap = {};
- for (let guid in this._store.getAllIDs()) {
- // Figure out with which key to store the mapping.
- let key;
- let id = this._store.idForGUID(guid);
- switch (PlacesUtils.bookmarks.getItemType(id)) {
- case PlacesUtils.bookmarks.TYPE_BOOKMARK:
-
- // Smart bookmarks map to their annotation value.
- let queryId;
- try {
- queryId = PlacesUtils.annotations.getItemAnnotation(
- id, SMART_BOOKMARKS_ANNO);
- } catch(ex) {}
-
- if (queryId)
- key = "q" + queryId;
- else
- key = "b" + PlacesUtils.bookmarks.getBookmarkURI(id).spec + ":" +
- PlacesUtils.bookmarks.getItemTitle(id);
- break;
- case PlacesUtils.bookmarks.TYPE_FOLDER:
- key = "f" + PlacesUtils.bookmarks.getItemTitle(id);
- break;
- case PlacesUtils.bookmarks.TYPE_SEPARATOR:
- key = "s" + PlacesUtils.bookmarks.getItemIndex(id);
- break;
- default:
- continue;
- }
-
- // The mapping is on a per parent-folder-name basis.
- let parent = PlacesUtils.bookmarks.getFolderIdForItem(id);
- if (parent <= 0)
- continue;
-
- let parentName = PlacesUtils.bookmarks.getItemTitle(parent);
- if (guidMap[parentName] == null)
- guidMap[parentName] = {};
-
- // If the entry already exists, remember that there are explicit dupes.
- let entry = new String(guid);
- entry.hasDupe = guidMap[parentName][key] != null;
-
- // Remember this item's GUID for its parent-name/key pair.
- guidMap[parentName][key] = entry;
- this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]);
- }
-
- return guidMap;
- },
-
- // Helper function to get a dupe GUID for an item.
- _mapDupe: function _mapDupe(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;
- }
-
- // Figure out if we have a map to use!
- // This will throw in some circumstances. That's fine.
- let guidMap = this._guidMap;
-
- // Give the GUID if we have the matching pair.
- this._log.trace("Finding mapping: " + item.parentName + ", " + key);
- let parent = guidMap[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;
- },
-
- _syncStartup: function _syncStart() {
- SyncEngine.prototype._syncStartup.call(this);
-
- let cb = Async.makeSpinningCallback();
- Task.spawn(function() {
- // For first-syncs, make a backup for the user to restore
- if (this.lastSync == 0) {
- this._log.debug("Bookmarks backup starting.");
- yield PlacesBackups.create(null, true);
- this._log.debug("Bookmarks backup done.");
- }
- }.bind(this)).then(
- cb, 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("Got exception backing up bookmarks, but continuing with sync.", ex);
- cb();
- }
- );
-
- cb.wait();
-
- this.__defineGetter__("_guidMap", function() {
- // Create a mapping of folder titles and separator positions to GUID.
- // We do this lazily so that we don't do any work unless we reconcile
- // incoming items.
- let guidMap;
- try {
- guidMap = this._buildGUIDMap();
- } catch (ex) {
- this._log.warn("Got exception building GUID map." +
- " Skipping all other incoming items.", ex);
- throw {code: Engine.prototype.eEngineAbortApplyIncoming,
- cause: ex};
- }
- delete this._guidMap;
- return this._guidMap = guidMap;
- });
-
- this._store._childrenToOrder = {};
- },
-
- _processIncoming: function (newitems) {
- try {
- SyncEngine.prototype._processIncoming.call(this, newitems);
- } 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);
- this._tracker._ensureMobileQuery();
- },
-
- _syncCleanup: function _syncCleanup() {
- SyncEngine.prototype._syncCleanup.call(this);
- delete this._guidMap;
- },
-
- _createRecord: function _createRecord(id) {
- // Create the record as usual, but mark it as having dupes if necessary.
- let record = SyncEngine.prototype._createRecord.call(this, id);
- let entry = this._mapDupe(record);
- if (entry != null && entry.hasDupe) {
- record.hasDupe = true;
- }
- return record;
- },
-
- _findDupe: function _findDupe(item) {
- this._log.trace("Finding dupe for " + item.id +
- " (already duped: " + item.hasDupe + ").");
-
- // Don't bother finding a dupe if the incoming item has duplicates.
- if (item.hasDupe) {
- this._log.trace(item.id + " already a dupe: not finding one.");
- return;
- }
- let mapped = this._mapDupe(item);
- this._log.debug(item.id + " mapped to " + mapped);
- return mapped;
- }
-};
-
-function BookmarksStore(name, engine) {
- Store.call(this, name, engine);
-
- // Explicitly nullify our references to our cached services so we don't leak
- Svc.Obs.add("places-shutdown", function() {
- for each (let [query, stmt] in Iterator(this._stmts)) {
- stmt.finalize();
- }
- this._stmts = {};
- }, this);
-}
-BookmarksStore.prototype = {
- __proto__: Store.prototype,
-
- 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)
- return;
-
- // Yes, this works without chopping off the "place:" prefix.
- let uri = record.bmkUri
- let queriesRef = {};
- let queryCountRef = {};
- let optionsRef = {};
- PlacesUtils.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");
- PlacesUtils.tagging.tagURI(dummyURI, [tag]);
-
- // Look for the id of the tag, which might just have been added.
- let tags = this._getNode(PlacesUtils.tagsFolderId);
- if (!(tags instanceof Ci.nsINavHistoryQueryResultNode)) {
- this._log.debug("tags isn't an nsINavHistoryQueryResultNode; aborting.");
- return;
- }
-
- tags.containerOpen = true;
- try {
- 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 = PlacesUtils.history.queriesToQueryString(
- queriesRef.value, queryCountRef.value, optionsRef.value);
- return;
- }
- }
- }
- finally {
- tags.containerOpen = false;
- }
- },
-
- applyIncoming: function BStore_applyIncoming(record) {
- this._log.debug("Applying record " + record.id);
- let isSpecial = record.id in kSpecialIds;
-
- if (record.deleted) {
- if (isSpecial) {
- this._log.warn("Ignoring deletion for special record " + record.id);
- return;
- }
-
- // Don't bother with pre and post-processing for deletions.
- Store.prototype.applyIncoming.call(this, record);
- return;
- }
-
- // For special folders we're only interested in child ordering.
- if (isSpecial && record.children) {
- this._log.debug("Processing special node: " + record.id);
- // Reorder children later
- this._childrenToOrder[record.id] = record.children;
- return;
- }
-
- // Skip malformed records. (Bug 806460.)
- if (record.type == "query" &&
- !record.bmkUri) {
- this._log.warn("Skipping malformed query bookmark: " + record.id);
- 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;
- }
- this._log.debug("Local parent is " + parentGUID);
-
- let parentId = this.idForGUID(parentGUID);
- if (parentId > 0) {
- // Save the parent id for modifying the bookmark later
- record._parent = parentId;
- record._orphan = false;
- this._log.debug("Record " + record.id + " is not an orphan.");
- } 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.call(this, record);
-
- // 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) {
- PlacesUtils.annotations.setItemAnnotation(
- itemId, PARENT_ANNO, parentGUID, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- }
- }
- },
-
- /**
- * Find all ids of items that have a given value for an annotation
- */
- _findAnnoItems: function BStore__findAnnoItems(anno, val) {
- return PlacesUtils.annotations.getItemsWithAnnotation(anno, {})
- .filter(function(id) {
- return PlacesUtils.annotations.getItemAnnotation(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)) {
- PlacesUtils.annotations.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) {
- PlacesUtils.bookmarks.moveItem(itemId, parentId,
- PlacesUtils.bookmarks.DEFAULT_INDEX);
- return true;
- }
- } catch(ex) {
- this._log.debug("Failed to reparent item. ", ex);
- }
- return false;
- },
-
- // Turn a record's nsINavBookmarksService constant and other attributes into
- // a granular type for comparison.
- _recordType: function _recordType(itemId) {
- let bms = PlacesUtils.bookmarks;
- let type = bms.getItemType(itemId);
-
- switch (type) {
- case bms.TYPE_FOLDER:
- if (PlacesUtils.annotations
- .itemHasAnnotation(itemId, PlacesUtils.LMANNO_FEEDURI)) {
- return "livemark";
- }
- return "folder";
-
- case bms.TYPE_BOOKMARK:
- let bmkUri = bms.getBookmarkURI(itemId).spec;
- if (bmkUri.indexOf("place:") == 0) {
- return "query";
- }
- return "bookmark";
-
- case bms.TYPE_SEPARATOR:
- return "separator";
-
- default:
- return null;
- }
- },
-
- create: function BStore_create(record) {
- // Default to unfiled if we don't have the parent yet.
-
- // Valid parent IDs are all positive integers. Other values -- undefined,
- // null, -1 -- all compare false for > 0, so this catches them all. We
- // don't just use <= without the !, because undefined and null compare
- // false for that, too!
- if (!(record._parent > 0)) {
- this._log.debug("Parent is " + record._parent + "; reparenting to unfiled.");
- record._parent = kSpecialIds.unfiled;
- }
-
- let newId;
- switch (record.type) {
- case "bookmark":
- case "query":
- case "microsummary": {
- let uri = Utils.makeURI(record.bmkUri);
- newId = PlacesUtils.bookmarks.insertBookmark(
- record._parent, uri, PlacesUtils.bookmarks.DEFAULT_INDEX, record.title);
- this._log.debug("created bookmark " + newId + " under " + record._parent
- + " as " + record.title + " " + record.bmkUri);
-
- // Smart bookmark annotations are strings.
- if (record.queryId) {
- PlacesUtils.annotations.setItemAnnotation(
- newId, SMART_BOOKMARKS_ANNO, record.queryId, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- }
-
- if (Array.isArray(record.tags)) {
- this._tagURI(uri, record.tags);
- }
- PlacesUtils.bookmarks.setKeywordForBookmark(newId, record.keyword);
- if (record.description) {
- PlacesUtils.annotations.setItemAnnotation(
- newId, DESCRIPTION_ANNO, record.description, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- }
-
- if (record.loadInSidebar) {
- PlacesUtils.annotations.setItemAnnotation(
- newId, SIDEBAR_ANNO, true, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- }
-
- } break;
- case "folder":
- newId = PlacesUtils.bookmarks.createFolder(
- record._parent, record.title, PlacesUtils.bookmarks.DEFAULT_INDEX);
- this._log.debug("created folder " + newId + " under " + record._parent
- + " as " + record.title);
-
- if (record.description) {
- PlacesUtils.annotations.setItemAnnotation(
- newId, DESCRIPTION_ANNO, record.description, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- }
-
- // record.children will be dealt with in _orderChildren.
- break;
- case "livemark":
- let siteURI = null;
- if (!record.feedUri) {
- this._log.debug("No feed URI: skipping livemark record " + record.id);
- return;
- }
- if (PlacesUtils.annotations
- .itemHasAnnotation(record._parent, PlacesUtils.LMANNO_FEEDURI)) {
- this._log.debug("Invalid parent: skipping livemark record " + record.id);
- return;
- }
-
- if (record.siteUri != null)
- siteURI = Utils.makeURI(record.siteUri);
-
- // Until this engine can handle asynchronous error reporting, we need to
- // detect errors on creation synchronously.
- let spinningCb = Async.makeSpinningCallback();
-
- let livemarkObj = {title: record.title,
- parentId: record._parent,
- index: PlacesUtils.bookmarks.DEFAULT_INDEX,
- feedURI: Utils.makeURI(record.feedUri),
- siteURI: siteURI,
- guid: record.id};
- PlacesUtils.livemarks.addLivemark(livemarkObj).then(
- aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
- () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
- );
-
- let [status, livemark] = spinningCb.wait();
- if (!Components.isSuccessCode(status)) {
- throw status;
- }
-
- this._log.debug("Created livemark " + livemark.id + " under " +
- livemark.parentId + " as " + livemark.title +
- ", " + livemark.siteURI.spec + ", " +
- livemark.feedURI.spec + ", GUID " +
- livemark.guid);
- break;
- case "separator":
- newId = PlacesUtils.bookmarks.insertSeparator(
- record._parent, PlacesUtils.bookmarks.DEFAULT_INDEX);
- this._log.debug("created separator " + newId + " under " + record._parent);
- 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;
- }
-
- if (newId) {
- // Livemarks can set the GUID through the API, so there's no need to
- // do that here.
- this._log.trace("Setting GUID of new item " + newId + " to " + record.id);
- this._setGUID(newId, record.id);
- }
- },
-
- // Factored out of `remove` to avoid redundant DB queries when the Places ID
- // is already known.
- removeById: function removeById(itemId, guid) {
- let type = PlacesUtils.bookmarks.getItemType(itemId);
-
- switch (type) {
- case PlacesUtils.bookmarks.TYPE_BOOKMARK:
- this._log.debug(" -> removing bookmark " + guid);
- PlacesUtils.bookmarks.removeItem(itemId);
- break;
- case PlacesUtils.bookmarks.TYPE_FOLDER:
- this._log.debug(" -> removing folder " + guid);
- PlacesUtils.bookmarks.removeItem(itemId);
- break;
- case PlacesUtils.bookmarks.TYPE_SEPARATOR:
- this._log.debug(" -> removing separator " + guid);
- PlacesUtils.bookmarks.removeItem(itemId);
- break;
- default:
- this._log.error("remove: Unknown item type: " + type);
- break;
- }
- },
-
- remove: function BStore_remove(record) {
- if (kSpecialIds.isSpecialGUID(record.id)) {
- this._log.warn("Refusing to remove special folder " + record.id);
- return;
- }
-
- let itemId = this.idForGUID(record.id);
- if (itemId <= 0) {
- this._log.debug("Item " + record.id + " already removed");
- return;
- }
- this.removeById(itemId, record.id);
- },
-
- _taggableTypes: ["bookmark", "microsummary", "query"],
- isTaggable: function isTaggable(recordType) {
- return this._taggableTypes.indexOf(recordType) != -1;
- },
-
- 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;
- }
-
- // Two items are the same type if they have the same ItemType in Places,
- // and also share some key characteristics (e.g., both being livemarks).
- // We figure this out by examining the item to find the equivalent granular
- // (string) type.
- // If they're not the same type, we can't just update attributes. Delete
- // then recreate the record instead.
- let localItemType = this._recordType(itemId);
- let remoteRecordType = record.type;
- this._log.trace("Local type: " + localItemType + ". " +
- "Remote type: " + remoteRecordType + ".");
-
- if (localItemType != remoteRecordType) {
- this._log.debug("Local record and remote record differ in type. " +
- "Deleting and recreating.");
- this.removeById(itemId, record.id);
- this.create(record);
- return;
- }
-
- this._log.trace("Updating " + record.id + " (" + itemId + ")");
-
- // Move the bookmark to a new parent or new position if necessary
- if (record._parent > 0 &&
- PlacesUtils.bookmarks.getFolderIdForItem(itemId) != record._parent) {
- this._reparentItem(itemId, record._parent);
- }
-
- for (let [key, val] in Iterator(record.cleartext)) {
- switch (key) {
- case "title":
- PlacesUtils.bookmarks.setItemTitle(itemId, val);
- break;
- case "bmkUri":
- PlacesUtils.bookmarks.changeBookmarkURI(itemId, Utils.makeURI(val));
- break;
- case "tags":
- if (Array.isArray(val)) {
- if (this.isTaggable(remoteRecordType)) {
- this._tagID(itemId, val);
- } else {
- this._log.debug("Remote record type is invalid for tags: " + remoteRecordType);
- }
- }
- break;
- case "keyword":
- PlacesUtils.bookmarks.setKeywordForBookmark(itemId, val);
- break;
- case "description":
- if (val) {
- PlacesUtils.annotations.setItemAnnotation(
- itemId, DESCRIPTION_ANNO, val, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- } else {
- PlacesUtils.annotations.removeItemAnnotation(itemId, DESCRIPTION_ANNO);
- }
- break;
- case "loadInSidebar":
- if (val) {
- PlacesUtils.annotations.setItemAnnotation(
- itemId, SIDEBAR_ANNO, true, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- } else {
- PlacesUtils.annotations.removeItemAnnotation(itemId, SIDEBAR_ANNO);
- }
- break;
- case "queryId":
- PlacesUtils.annotations.setItemAnnotation(
- itemId, SMART_BOOKMARKS_ANNO, val, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- 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;
- let parent = null;
- 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 {
- // This code path could be optimized by caching the parent earlier.
- // Doing so should take in count any edge case due to reparenting
- // or parent invalidations though.
- if (!parent) {
- parent = PlacesUtils.bookmarks.getFolderIdForItem(itemid);
- }
- PlacesUtils.bookmarks.moveItem(itemid, parent, 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 = PlacesUtils.history.getNewQuery();
- query.setFolders([folder], 1);
- return PlacesUtils.history.executeQuery(
- query, PlacesUtils.history.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 PlacesUtils.tagging.getTagsForURI(uri, {});
- },
-
- _getDescription: function BStore__getDescription(id) {
- try {
- return PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO);
- } catch (e) {
- return null;
- }
- },
-
- _isLoadInSidebar: function BStore__isLoadInSidebar(id) {
- return PlacesUtils.annotations.itemHasAnnotation(id, SIDEBAR_ANNO);
- },
-
- get _childGUIDsStm() {
- return this._getStmt(
- "SELECT id AS item_id, guid " +
- "FROM moz_bookmarks " +
- "WHERE parent = :parent " +
- "ORDER BY position");
- },
- _childGUIDsCols: ["item_id", "guid"],
-
- _getChildGUIDsForId: function _getChildGUIDsForId(itemid) {
- let stmt = this._childGUIDsStm;
- stmt.params.parent = itemid;
- let rows = Async.querySpinningly(stmt, this._childGUIDsCols);
- 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 = PlacesUtils.bookmarks.getFolderIdForItem(placeId);
- switch (PlacesUtils.bookmarks.getItemType(placeId)) {
- case PlacesUtils.bookmarks.TYPE_BOOKMARK:
- let bmkUri = PlacesUtils.bookmarks.getBookmarkURI(placeId).spec;
- if (bmkUri.indexOf("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 = PlacesUtils.bookmarks.getItemTitle(folder);
- this._log.trace("query id: " + folder + " = " + record.folderName);
- }
- }
- catch(ex) {}
-
- // Persist the Smart Bookmark anno, if found.
- try {
- let anno = PlacesUtils.annotations.getItemAnnotation(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 = PlacesUtils.bookmarks.getItemTitle(placeId);
-
- record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
- record.bmkUri = bmkUri;
- record.tags = this._getTags(record.bmkUri);
- record.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(placeId);
- record.description = this._getDescription(placeId);
- record.loadInSidebar = this._isLoadInSidebar(placeId);
- break;
-
- case PlacesUtils.bookmarks.TYPE_FOLDER:
- if (PlacesUtils.annotations
- .itemHasAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI)) {
- record = new Livemark(collection, id);
- let as = PlacesUtils.annotations;
- record.feedUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI);
- try {
- record.siteUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_SITEURI);
- } catch (ex) {}
- } else {
- record = new BookmarkFolder(collection, id);
- }
-
- if (parent > 0)
- record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
- record.title = PlacesUtils.bookmarks.getItemTitle(placeId);
- record.description = this._getDescription(placeId);
- record.children = this._getChildGUIDsForId(placeId);
- break;
-
- case PlacesUtils.bookmarks.TYPE_SEPARATOR:
- record = new BookmarkSeparator(collection, id);
- if (parent > 0)
- record.parentName = PlacesUtils.bookmarks.getItemTitle(parent);
- // Create a positioning identifier for the separator, used by _mapDupe
- record.pos = PlacesUtils.bookmarks.getItemIndex(placeId);
- break;
-
- default:
- record = new PlacesItem(collection, id);
- this._log.warn("Unknown item type, cannot serialize: " +
- PlacesUtils.bookmarks.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);
- let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
- .DBConnection;
- return this._stmts[query] = db.createAsyncStatement(query);
- },
-
- get _frecencyStm() {
- return this._getStmt(
- "SELECT frecency " +
- "FROM moz_places " +
- "WHERE url = :url " +
- "LIMIT 1");
- },
- _frecencyCols: ["frecency"],
-
- get _setGUIDStm() {
- return this._getStmt(
- "UPDATE moz_bookmarks " +
- "SET guid = :guid " +
- "WHERE id = :item_id");
- },
-
- // Some helper functions to handle GUIDs
- _setGUID: function _setGUID(id, guid) {
- if (!guid)
- guid = Utils.makeGUID();
-
- let stmt = this._setGUIDStm;
- stmt.params.guid = guid;
- stmt.params.item_id = id;
- Async.querySpinningly(stmt);
- return guid;
- },
-
- get _guidForIdStm() {
- return this._getStmt(
- "SELECT guid " +
- "FROM moz_bookmarks " +
- "WHERE id = :item_id");
- },
- _guidForIdCols: ["guid"],
-
- 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 = Async.querySpinningly(stmt, this._guidForIdCols)[0];
- if (result && result.guid)
- return result.guid;
-
- // Give the uri a GUID if it doesn't have one
- return this._setGUID(id);
- },
-
- get _idForGUIDStm() {
- return this._getStmt(
- "SELECT id AS item_id " +
- "FROM moz_bookmarks " +
- "WHERE guid = :guid");
- },
- _idForGUIDCols: ["item_id"],
-
- // 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 = Async.querySpinningly(stmt, this._idForGUIDCols);
- this._log.trace("Number of rows matching GUID " + guid + ": "
- + results.length);
-
- // Here's the one we care about: the first.
- let result = results[0];
-
- if (!result)
- return -1;
-
- 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 = Async.querySpinningly(this._frecencyStm, this._frecencyCols);
- 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) {
- node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
- node.containerOpen = true;
- try {
- // Remember all the children GUIDs and recursively get more
- for (let i = 0; i < node.childCount; i++) {
- let child = node.getChild(i);
- items[this.GUIDForId(child.itemId)] = true;
- this._getChildren(child, items);
- }
- }
- finally {
- node.containerOpen = false;
- }
- }
-
- return items;
- },
-
- /**
- * Associates the URI of the item with the provided ID with the
- * provided array of tags.
- * If the provided ID does not identify an item with a URI,
- * returns immediately.
- */
- _tagID: function _tagID(itemID, tags) {
- if (!itemID || !tags) {
- return;
- }
-
- try {
- let u = PlacesUtils.bookmarks.getBookmarkURI(itemID);
- this._tagURI(u, tags);
- } catch (e) {
- this._log.warn("Got exception fetching URI for " + itemID + ": not tagging. ", e);
-
- // I guess it doesn't have a URI. Don't try to tag it.
- return;
- }
- },
-
- /**
- * Associate the provided URI with the provided array of tags.
- * If the provided URI is falsy, returns immediately.
- */
- _tagURI: function _tagURI(bookmarkURI, tags) {
- if (!bookmarkURI || !tags) {
- return;
- }
-
- // Filter out any null/undefined/empty tags.
- tags = tags.filter(t => t);
-
- // Temporarily tag a dummy URI to preserve tag ids when untagging.
- let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
- PlacesUtils.tagging.tagURI(dummyURI, tags);
- PlacesUtils.tagging.untagURI(bookmarkURI, null);
- PlacesUtils.tagging.tagURI(bookmarkURI, tags);
- PlacesUtils.tagging.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() {
- let cb = Async.makeSpinningCallback();
- Task.spawn(function() {
- // Save a backup before clearing out all bookmarks.
- yield PlacesBackups.create(null, true);
- for each (let guid in kSpecialIds.guids)
- if (guid != "places") {
- let id = kSpecialIds.specialIdForGUID(guid);
- if (id)
- PlacesUtils.bookmarks.removeFolderChildren(id);
- }
- cb();
- });
- cb.wait();
- }
-};
-
-function BookmarksTracker(name, engine) {
- Tracker.call(this, name, engine);
-
- Svc.Obs.add("places-shutdown", this);
-}
-BookmarksTracker.prototype = {
- __proto__: Tracker.prototype,
-
- startTracking: function() {
- PlacesUtils.bookmarks.addObserver(this, true);
- Svc.Obs.add("bookmarks-restore-begin", this);
- Svc.Obs.add("bookmarks-restore-success", this);
- Svc.Obs.add("bookmarks-restore-failed", this);
- },
-
- stopTracking: function() {
- PlacesUtils.bookmarks.removeObserver(this);
- Svc.Obs.remove("bookmarks-restore-begin", this);
- Svc.Obs.remove("bookmarks-restore-success", this);
- Svc.Obs.remove("bookmarks-restore-failed", this);
- },
-
- observe: function observe(subject, topic, data) {
- Tracker.prototype.observe.call(this, subject, topic, data);
-
- switch (topic) {
- 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.");
- this.engine.service.resetClient([this.name]);
- this.engine.service.wipeServer([this.name]);
- this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name]);
- break;
- case "bookmarks-restore-failed":
- this._log.debug("Tracking all items on failed import.");
- this.ignoreAll = false;
- break;
- }
- },
-
- QueryInterface: XPCOMUtils.generateQI([
- Ci.nsINavBookmarkObserver,
- Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS,
- Ci.nsISupportsWeakReference
- ]),
-
- /**
- * Add a bookmark GUID to be uploaded and bump up the sync score.
- *
- * @param itemGuid
- * GUID of the bookmark to upload.
- */
- _add: function BMT__add(itemId, guid) {
- guid = kSpecialIds.specialGUIDForId(itemId) || guid;
- if (this.addChangedID(guid))
- this._upScore();
- },
-
- /* Every add/remove/change will trigger a sync for MULTI_DEVICE. */
- _upScore: function BMT__upScore() {
- this.score += SCORE_INCREMENT_XLARGE;
- },
-
- /**
- * Determine if a change should be ignored.
- *
- * @param itemId
- * Item under consideration to ignore
- * @param folder (optional)
- * Folder of the item being changed
- */
- _ignore: function BMT__ignore(itemId, folder, guid) {
- // Ignore unconditionally if the engine tells us to.
- if (this.ignoreAll)
- return true;
-
- // Get the folder id if we weren't given one.
- if (folder == null) {
- try {
- folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
- } catch (ex) {
- this._log.debug("getFolderIdForItem(" + itemId +
- ") threw; calling _ensureMobileQuery.");
- // I'm guessing that gFIFI can throw, and perhaps that's why
- // _ensureMobileQuery is here at all. Try not to call it.
- this._ensureMobileQuery();
- folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
- }
- }
-
- // Ignore changes to tags (folders under the tags folder).
- let tags = kSpecialIds.tags;
- if (folder == tags)
- return true;
-
- // Ignore tag items (the actual instance of a tag for a bookmark).
- if (PlacesUtils.bookmarks.getFolderIdForItem(folder) == tags)
- return true;
-
- // Make sure to remove items that have the exclude annotation.
- if (PlacesUtils.annotations.itemHasAnnotation(itemId, EXCLUDEBACKUP_ANNO)) {
- this.removeChangedID(guid);
- return true;
- }
-
- return false;
- },
-
- onItemAdded: function BMT_onItemAdded(itemId, folder, index,
- itemType, uri, title, dateAdded,
- guid, parentGuid) {
- if (this._ignore(itemId, folder, guid))
- return;
-
- this._log.trace("onItemAdded: " + itemId);
- this._add(itemId, guid);
- this._add(folder, parentGuid);
- },
-
- onItemRemoved: function (itemId, parentId, index, type, uri,
- guid, parentGuid) {
- if (this._ignore(itemId, parentId, guid)) {
- return;
- }
-
- this._log.trace("onItemRemoved: " + itemId);
- this._add(itemId, guid);
- this._add(parentId, parentGuid);
- },
-
- _ensureMobileQuery: function _ensureMobileQuery() {
- let find = val =>
- PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter(
- id => PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
- );
-
- // Don't continue if the Library isn't ready
- let all = find(ALLBOOKMARKS_ANNO);
- if (all.length == 0)
- return;
-
- // Disable handling of notifications while changing the mobile query
- this.ignoreAll = true;
-
- let mobile = find(MOBILE_ANNO);
- let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile);
- let title = Str.sync.get("mobile.label");
-
- // Don't add OR remove the mobile bookmarks if there's nothing.
- if (PlacesUtils.bookmarks.getIdForItemAt(kSpecialIds.mobile, 0) == -1) {
- if (mobile.length != 0)
- PlacesUtils.bookmarks.removeItem(mobile[0]);
- }
- // Add the mobile bookmarks query if it doesn't exist
- else if (mobile.length == 0) {
- let query = PlacesUtils.bookmarks.insertBookmark(all[0], queryURI, -1, title);
- PlacesUtils.annotations.setItemAnnotation(query, ORGANIZERQUERY_ANNO, MOBILE_ANNO, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- PlacesUtils.annotations.setItemAnnotation(query, EXCLUDEBACKUP_ANNO, 1, 0,
- PlacesUtils.annotations.EXPIRE_NEVER);
- }
- // Make sure the existing title is correct
- else if (PlacesUtils.bookmarks.getItemTitle(mobile[0]) != title) {
- PlacesUtils.bookmarks.setItemTitle(mobile[0], title);
- }
-
- this.ignoreAll = false;
- },
-
- // This method is oddly structured, but the idea is to return as quickly as
- // possible -- this handler gets called *every time* a bookmark changes, for
- // *each change*.
- onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value,
- lastModified, itemType, parentId,
- guid, parentGuid) {
- // Quicker checks first.
- if (this.ignoreAll)
- return;
-
- if (isAnno && (ANNOS_TO_TRACK.indexOf(property) == -1))
- // Ignore annotations except for the ones that we sync.
- return;
-
- // Ignore favicon changes to avoid unnecessary churn.
- if (property == "favicon")
- return;
-
- if (this._ignore(itemId, parentId, guid))
- return;
-
- this._log.trace("onItemChanged: " + itemId +
- (", " + property + (isAnno? " (anno)" : "")) +
- (value ? (" = \"" + value + "\"") : ""));
- this._add(itemId, guid);
- },
-
- onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex,
- newParent, newIndex, itemType,
- guid, oldParentGuid, newParentGuid) {
- if (this._ignore(itemId, newParent, guid))
- return;
-
- this._log.trace("onItemMoved: " + itemId);
- this._add(oldParent, oldParentGuid);
- if (oldParent != newParent) {
- this._add(itemId, guid);
- this._add(newParent, newParentGuid);
- }
-
- // Remove any position annotations now that the user moved the item
- PlacesUtils.annotations.removeItemAnnotation(itemId, PARENT_ANNO);
- },
-
- onBeginUpdateBatch: function () {},
- onEndUpdateBatch: function () {},
- onItemVisited: function () {}
-};
diff --git a/components/weave/src/engines/clients.js b/components/weave/src/engines/clients.js
deleted file mode 100644
index 6c8e37a7b..000000000
--- a/components/weave/src/engines/clients.js
+++ /dev/null
@@ -1,476 +0,0 @@
-/* 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/. */
-
-this.EXPORTED_SYMBOLS = [
- "ClientEngine",
- "ClientsRec"
-];
-
-var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://services-common/stringbundle.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/util.js");
-
-const CLIENTS_TTL = 1814400; // 21 days
-const CLIENTS_TTL_REFRESH = 604800; // 7 days
-
-const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
-
-this.ClientsRec = function ClientsRec(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-ClientsRec.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Sync.Record.Clients",
- ttl: CLIENTS_TTL
-};
-
-Utils.deferGetSet(ClientsRec,
- "cleartext",
- ["name", "type", "commands",
- "version", "protocols",
- "formfactor", "os", "appPackage", "application", "device"]);
-
-
-this.ClientEngine = function ClientEngine(service) {
- SyncEngine.call(this, "Clients", service);
-
- // Reset the client on every startup so that we fetch recent clients
- this._resetClient();
-}
-ClientEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _storeObj: ClientStore,
- _recordObj: ClientsRec,
- _trackerObj: ClientsTracker,
-
- // Always sync client data as it controls other sync behavior
- get enabled() true,
-
- get lastRecordUpload() {
- return Svc.Prefs.get(this.name + ".lastRecordUpload", 0);
- },
- set lastRecordUpload(value) {
- Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value));
- },
-
- // Aggregate some stats on the composition of clients on this account
- get stats() {
- let stats = {
- hasMobile: this.localType == "mobile",
- names: [this.localName],
- numClients: 1,
- };
-
- for (let id in this._store._remoteClients) {
- let {name, type, stale} = this._store._remoteClients[id];
- if (!stale) {
- stats.hasMobile = stats.hasMobile || type == DEVICE_TYPE_MOBILE;
- stats.names.push(name);
- stats.numClients++;
- }
- }
-
- return stats;
- },
-
- /**
- * Obtain information about device types.
- *
- * Returns a Map of device types to integer counts.
- */
- get deviceTypes() {
- let counts = new Map();
-
- counts.set(this.localType, 1);
-
- for (let id in this._store._remoteClients) {
- let record = this._store._remoteClients[id];
- if (record.stale) {
- continue; // pretend "stale" records don't exist.
- }
- let type = record.type;
- if (!counts.has(type)) {
- counts.set(type, 0);
- }
-
- counts.set(type, counts.get(type) + 1);
- }
-
- return counts;
- },
-
- get localID() {
- // Generate a random GUID id we don't have one
- let localID = Svc.Prefs.get("client.GUID", "");
- return localID == "" ? this.localID = Utils.makeGUID() : localID;
- },
- set localID(value) Svc.Prefs.set("client.GUID", value),
-
- get brandName() {
- let brand = new StringBundle("chrome://branding/locale/brand.properties");
- return brand.get("brandShortName");
- },
-
- get localName() {
- let localName = Svc.Prefs.get("client.name", "");
- if (localName != "")
- return localName;
-
- return this.localName = Utils.getDefaultDeviceName();
- },
- set localName(value) Svc.Prefs.set("client.name", value),
-
- get localType() Svc.Prefs.get("client.type", "desktop"),
- set localType(value) Svc.Prefs.set("client.type", value),
-
- isMobile: function isMobile(id) {
- if (this._store._remoteClients[id])
- return this._store._remoteClients[id].type == "mobile";
- return false;
- },
-
- _syncStartup: function _syncStartup() {
- // Reupload new client record periodically.
- if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) {
- this._tracker.addChangedID(this.localID);
- this.lastRecordUpload = Date.now() / 1000;
- }
- SyncEngine.prototype._syncStartup.call(this);
- },
-
- // Always process incoming items because they might have commands
- _reconcile: function _reconcile() {
- return true;
- },
-
- // Treat reset the same as wiping for locally cached clients
- _resetClient() {
- this._wipeClient();
- },
-
- _wipeClient: function _wipeClient() {
- SyncEngine.prototype._resetClient.call(this);
- this._store.wipe();
- },
-
- removeClientData: function removeClientData() {
- let res = this.service.resource(this.engineURL + "/" + this.localID);
- res.delete();
- },
-
- // Override the default behavior to delete bad records from the server.
- handleHMACMismatch: function handleHMACMismatch(item, mayRetry) {
- this._log.debug("Handling HMAC mismatch for " + item.id);
-
- let base = SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry);
- if (base != SyncEngine.kRecoveryStrategy.error)
- return base;
-
- // It's a bad client record. Save it to be deleted at the end of the sync.
- this._log.debug("Bad client record detected. Scheduling for deletion.");
- this._deleteId(item.id);
-
- // Neither try again nor error; we're going to delete it.
- return SyncEngine.kRecoveryStrategy.ignore;
- },
-
- /**
- * A hash of valid commands that the client knows about. The key is a command
- * and the value is a hash containing information about the command such as
- * number of arguments and description.
- */
- _commands: {
- resetAll: { args: 0, desc: "Clear temporary local data for all engines" },
- resetEngine: { args: 1, desc: "Clear temporary local data for engine" },
- wipeAll: { args: 0, desc: "Delete all client data for all engines" },
- wipeEngine: { args: 1, desc: "Delete all client data for engine" },
- logout: { args: 0, desc: "Log out client" },
- displayURI: { args: 3, desc: "Instruct a client to display a URI" },
- },
-
- /**
- * Remove any commands for the local client and mark it for upload.
- */
- clearCommands: function clearCommands() {
- delete this.localCommands;
- this._tracker.addChangedID(this.localID);
- },
-
- /**
- * Sends a command+args pair to a specific client.
- *
- * @param command Command string
- * @param args Array of arguments/data for command
- * @param clientId Client to send command to
- */
- _sendCommandToClient: function sendCommandToClient(command, args, clientId) {
- this._log.trace("Sending " + command + " to " + clientId);
-
- let client = this._store._remoteClients[clientId];
- if (!client) {
- throw new Error("Unknown remote client ID: '" + clientId + "'.");
- }
-
- // notDupe compares two commands and returns if they are not equal.
- let notDupe = function(other) {
- return other.command != command || !Utils.deepEquals(other.args, args);
- };
-
- let action = {
- command: command,
- args: args,
- };
-
- if (!client.commands) {
- client.commands = [action];
- }
- // Add the new action if there are no duplicates.
- else if (client.commands.every(notDupe)) {
- client.commands.push(action);
- }
- // It must be a dupe. Skip.
- else {
- return;
- }
-
- this._log.trace("Client " + clientId + " got a new action: " + [command, args]);
- this._tracker.addChangedID(clientId);
- },
-
- /**
- * Check if the local client has any remote commands and perform them.
- *
- * @return false to abort sync
- */
- processIncomingCommands: function processIncomingCommands() {
- return this._notify("clients:process-commands", "", function() {
- let commands = this.localCommands;
-
- // Immediately clear out the commands as we've got them locally.
- this.clearCommands();
-
- // Process each command in order.
- for each (let {command, args} in commands) {
- this._log.debug("Processing command: " + command + "(" + args + ")");
-
- let engines = [args[0]];
- switch (command) {
- case "resetAll":
- engines = null;
- // Fallthrough
- case "resetEngine":
- this.service.resetClient(engines);
- break;
- case "wipeAll":
- engines = null;
- // Fallthrough
- case "wipeEngine":
- this.service.wipeClient(engines);
- break;
- case "logout":
- this.service.logout();
- return false;
- case "displayURI":
- this._handleDisplayURI.apply(this, args);
- break;
- default:
- this._log.debug("Received an unknown command: " + command);
- break;
- }
- }
-
- return true;
- })();
- },
-
- /**
- * Validates and sends a command to a client or all clients.
- *
- * Calling this does not actually sync the command data to the server. If the
- * client already has the command/args pair, it won't receive a duplicate
- * command.
- *
- * @param command
- * Command to invoke on remote clients
- * @param args
- * Array of arguments to give to the command
- * @param clientId
- * Client ID to send command to. If undefined, send to all remote
- * clients.
- */
- sendCommand: function sendCommand(command, args, clientId) {
- let commandData = this._commands[command];
- // Don't send commands that we don't know about.
- if (!commandData) {
- this._log.error("Unknown command to send: " + command);
- return;
- }
- // Don't send a command with the wrong number of arguments.
- else if (!args || args.length != commandData.args) {
- this._log.error("Expected " + commandData.args + " args for '" +
- command + "', but got " + args);
- return;
- }
-
- if (clientId) {
- this._sendCommandToClient(command, args, clientId);
- } else {
- for (let id in this._store._remoteClients) {
- this._sendCommandToClient(command, args, id);
- }
- }
- },
-
- /**
- * Send a URI to another client for display.
- *
- * A side effect is the score is increased dramatically to incur an
- * immediate sync.
- *
- * If an unknown client ID is specified, sendCommand() will throw an
- * Error object.
- *
- * @param uri
- * URI (as a string) to send and display on the remote client
- * @param clientId
- * ID of client to send the command to. If not defined, will be sent
- * to all remote clients.
- * @param title
- * Title of the page being sent.
- */
- sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId, title) {
- this._log.info("Sending URI to client: " + uri + " -> " +
- clientId + " (" + title + ")");
- this.sendCommand("displayURI", [uri, this.localID, title], clientId);
-
- this._tracker.score += SCORE_INCREMENT_XLARGE;
- },
-
- /**
- * Handle a single received 'displayURI' command.
- *
- * Interested parties should observe the "weave:engine:clients:display-uri"
- * topic. The callback will receive an object as the subject parameter with
- * the following keys:
- *
- * uri URI (string) that is requested for display.
- * clientId ID of client that sent the command.
- * title Title of page that loaded URI (likely) corresponds to.
- *
- * The 'data' parameter to the callback will not be defined.
- *
- * @param uri
- * String URI that was received
- * @param clientId
- * ID of client that sent URI
- * @param title
- * String title of page that URI corresponds to. Older clients may not
- * send this.
- */
- _handleDisplayURI: function _handleDisplayURI(uri, clientId, title) {
- this._log.info("Received a URI for display: " + uri + " (" + title +
- ") from " + clientId);
-
- let subject = {uri: uri, client: clientId, title: title};
- Svc.Obs.notify("weave:engine:clients:display-uri", subject);
- }
-};
-
-function ClientStore(name, engine) {
- Store.call(this, name, engine);
-}
-ClientStore.prototype = {
- __proto__: Store.prototype,
-
- create(record) {
- this.update(record)
- },
-
- update: function update(record) {
- // Only grab commands from the server; local name/type always wins
- if (record.id == this.engine.localID)
- this.engine.localCommands = record.commands;
- else
- this._remoteClients[record.id] = record.cleartext;
- },
-
- createRecord: function createRecord(id, collection) {
- let record = new ClientsRec(collection, id);
-
- // Package the individual components into a record for the local client
- if (id == this.engine.localID) {
- record.name = this.engine.localName;
- record.type = this.engine.localType;
- record.commands = this.engine.localCommands;
- record.version = Services.appinfo.version;
- record.protocols = SUPPORTED_PROTOCOL_VERSIONS;
-
- // Optional fields.
- record.os = Services.appinfo.OS; // "Darwin"
- record.appPackage = Services.appinfo.ID;
- record.application = this.engine.brandName // "Nightly"
-
- // We can't compute these yet.
- // record.device = ""; // Bug 1100723
- // record.formfactor = ""; // Bug 1100722
- } else {
- record.cleartext = this._remoteClients[id];
- }
-
- return record;
- },
-
- itemExists(id) {
- return id in this.getAllIDs();
- },
-
- getAllIDs: function getAllIDs() {
- let ids = {};
- ids[this.engine.localID] = true;
- for (let id in this._remoteClients)
- ids[id] = true;
- return ids;
- },
-
- wipe: function wipe() {
- this._remoteClients = {};
- },
-};
-
-function ClientsTracker(name, engine) {
- Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
-}
-ClientsTracker.prototype = {
- __proto__: Tracker.prototype,
-
- _enabled: false,
-
- observe: function observe(subject, topic, data) {
- switch (topic) {
- case "weave:engine:start-tracking":
- if (!this._enabled) {
- Svc.Prefs.observe("client.name", this);
- this._enabled = true;
- }
- break;
- case "weave:engine:stop-tracking":
- if (this._enabled) {
- Svc.Prefs.ignore("clients.name", this);
- this._enabled = false;
- }
- break;
- case "nsPref:changed":
- this._log.debug("client.name preference changed");
- this.addChangedID(Svc.Prefs.get("client.GUID"));
- this.score += SCORE_INCREMENT_XLARGE;
- break;
- }
- }
-};
diff --git a/components/weave/src/engines/forms.js b/components/weave/src/engines/forms.js
deleted file mode 100644
index b5b3f7732..000000000
--- a/components/weave/src/engines/forms.js
+++ /dev/null
@@ -1,246 +0,0 @@
-/* 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/. */
-
-this.EXPORTED_SYMBOLS = ['FormEngine', 'FormRec'];
-
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cu = Components.utils;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://gre/modules/Async.jsm");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://gre/modules/Log.jsm");
-
-const FORMS_TTL = 5184000; // 60 days
-
-this.FormRec = function FormRec(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-FormRec.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Sync.Record.Form",
- ttl: FORMS_TTL
-};
-
-Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]);
-
-
-var FormWrapper = {
- _log: Log.repository.getLogger("Sync.Engine.Forms"),
-
- _getEntryCols: ["fieldname", "value"],
- _guidCols: ["guid"],
-
- // Do a "sync" search by spinning the event loop until it completes.
- _searchSpinningly: function(terms, searchData) {
- let results = [];
- let cb = Async.makeSpinningCallback();
- let callbacks = {
- handleResult: function(result) {
- results.push(result);
- },
- handleCompletion: function(reason) {
- cb(null, results);
- }
- };
- Svc.FormHistory.search(terms, searchData, callbacks);
- return cb.wait();
- },
-
- _updateSpinningly: function(changes) {
- if (!Svc.FormHistory.enabled) {
- return; // update isn't going to do anything.
- }
- let cb = Async.makeSpinningCallback();
- let callbacks = {
- handleCompletion: function(reason) {
- cb();
- }
- };
- Svc.FormHistory.update(changes, callbacks);
- return cb.wait();
- },
-
- getEntry: function (guid) {
- let results = this._searchSpinningly(this._getEntryCols, {guid: guid});
- if (!results.length) {
- return null;
- }
- return {name: results[0].fieldname, value: results[0].value};
- },
-
- getGUID: function (name, value) {
- // Query for the provided entry.
- let query = { fieldname: name, value: value };
- let results = this._searchSpinningly(this._guidCols, query);
- return results.length ? results[0].guid : null;
- },
-
- hasGUID: function (guid) {
- // We could probably use a count function here, but searchSpinningly exists...
- return this._searchSpinningly(this._guidCols, {guid: guid}).length != 0;
- },
-
- replaceGUID: function (oldGUID, newGUID) {
- let changes = {
- op: "update",
- guid: oldGUID,
- newGuid: newGUID,
- }
- this._updateSpinningly(changes);
- }
-
-};
-
-this.FormEngine = function FormEngine(service) {
- SyncEngine.call(this, "Forms", service);
-}
-FormEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _storeObj: FormStore,
- _trackerObj: FormTracker,
- _recordObj: FormRec,
- applyIncomingBatchSize: FORMS_STORE_BATCH_SIZE,
-
- syncPriority: 6,
-
- get prefName() "history",
-
- _findDupe: function _findDupe(item) {
- return FormWrapper.getGUID(item.name, item.value);
- }
-};
-
-function FormStore(name, engine) {
- Store.call(this, name, engine);
-}
-FormStore.prototype = {
- __proto__: Store.prototype,
-
- _processChange: function (change) {
- // If this._changes is defined, then we are applying a batch, so we
- // can defer it.
- if (this._changes) {
- this._changes.push(change);
- return;
- }
-
- // Otherwise we must handle the change synchronously, right now.
- FormWrapper._updateSpinningly(change);
- },
-
- applyIncomingBatch: function (records) {
- // We collect all the changes to be made then apply them all at once.
- this._changes = [];
- let failures = Store.prototype.applyIncomingBatch.call(this, records);
- if (this._changes.length) {
- FormWrapper._updateSpinningly(this._changes);
- }
- delete this._changes;
- return failures;
- },
-
- getAllIDs: function () {
- let results = FormWrapper._searchSpinningly(["guid"], [])
- let guids = {};
- for (let result of results) {
- guids[result.guid] = true;
- }
- return guids;
- },
-
- changeItemID: function (oldID, newID) {
- FormWrapper.replaceGUID(oldID, newID);
- },
-
- itemExists: function (id) {
- return FormWrapper.hasGUID(id);
- },
-
- createRecord: function (id, collection) {
- let record = new FormRec(collection, id);
- let entry = FormWrapper.getEntry(id);
- if (entry != null) {
- record.name = entry.name;
- record.value = entry.value;
- } else {
- record.deleted = true;
- }
- return record;
- },
-
- create: function (record) {
- this._log.trace("Adding form record for " + record.name);
- let change = {
- op: "add",
- fieldname: record.name,
- value: record.value
- };
- this._processChange(change);
- },
-
- remove: function (record) {
- this._log.trace("Removing form record: " + record.id);
- let change = {
- op: "remove",
- guid: record.id
- };
- this._processChange(change);
- },
-
- update: function (record) {
- this._log.trace("Ignoring form record update request!");
- },
-
- wipe: function () {
- let change = {
- op: "remove"
- };
- FormWrapper._updateSpinningly(change);
- }
-};
-
-function FormTracker(name, engine) {
- Tracker.call(this, name, engine);
-}
-FormTracker.prototype = {
- __proto__: Tracker.prototype,
-
- QueryInterface: XPCOMUtils.generateQI([
- Ci.nsIObserver,
- Ci.nsISupportsWeakReference]),
-
- startTracking: function() {
- Svc.Obs.add("satchel-storage-changed", this);
- },
-
- stopTracking: function() {
- Svc.Obs.remove("satchel-storage-changed", this);
- },
-
- observe: function (subject, topic, data) {
- Tracker.prototype.observe.call(this, subject, topic, data);
- if (this.ignoreAll) {
- return;
- }
-
- switch (topic) {
- case "satchel-storage-changed":
- if (data == "formhistory-add" || data == "formhistory-remove") {
- let guid = subject.QueryInterface(Ci.nsISupportsString).toString();
- this.trackEntry(guid);
- }
- break;
- }
- },
-
- trackEntry: function (guid) {
- this.addChangedID(guid);
- this.score += SCORE_INCREMENT_MEDIUM;
- },
-};
diff --git a/components/weave/src/engines/history.js b/components/weave/src/engines/history.js
deleted file mode 100644
index 35dfd7811..000000000
--- a/components/weave/src/engines/history.js
+++ /dev/null
@@ -1,417 +0,0 @@
-/* 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/. */
-
-this.EXPORTED_SYMBOLS = ['HistoryEngine', 'HistoryRec'];
-
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cu = Components.utils;
-var Cr = Components.results;
-
-const HISTORY_TTL = 5184000; // 60 days
-
-Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Async.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/util.js");
-
-this.HistoryRec = function HistoryRec(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-HistoryRec.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Sync.Record.History",
- ttl: HISTORY_TTL
-};
-
-Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]);
-
-
-this.HistoryEngine = function HistoryEngine(service) {
- SyncEngine.call(this, "History", service);
-}
-HistoryEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _recordObj: HistoryRec,
- _storeObj: HistoryStore,
- _trackerObj: HistoryTracker,
- downloadLimit: MAX_HISTORY_DOWNLOAD,
- applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE,
-
- syncPriority: 7,
-};
-
-function HistoryStore(name, engine) {
- Store.call(this, name, engine);
-
- // Explicitly nullify our references to our cached services so we don't leak
- Svc.Obs.add("places-shutdown", function() {
- for (let query in this._stmts) {
- let stmt = this._stmts;
- stmt.finalize();
- }
- this._stmts = {};
- }, this);
-}
-HistoryStore.prototype = {
- __proto__: Store.prototype,
-
- __asyncHistory: null,
- get _asyncHistory() {
- if (!this.__asyncHistory) {
- this.__asyncHistory = Cc["@mozilla.org/browser/history;1"]
- .getService(Ci.mozIAsyncHistory);
- }
- return this.__asyncHistory;
- },
-
- _stmts: {},
- _getStmt: function(query) {
- if (query in this._stmts) {
- return this._stmts[query];
- }
-
- this._log.trace("Creating SQL statement: " + query);
- let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
- .DBConnection;
- return this._stmts[query] = db.createAsyncStatement(query);
- },
-
- get _setGUIDStm() {
- return this._getStmt(
- "UPDATE moz_places " +
- "SET guid = :guid " +
- "WHERE url = :page_url");
- },
-
- // Some helper functions to handle GUIDs
- setGUID: function setGUID(uri, guid) {
- uri = uri.spec ? uri.spec : uri;
-
- if (!guid) {
- guid = Utils.makeGUID();
- }
-
- let stmt = this._setGUIDStm;
- stmt.params.guid = guid;
- stmt.params.page_url = uri;
- Async.querySpinningly(stmt);
- return guid;
- },
-
- get _guidStm() {
- return this._getStmt(
- "SELECT guid " +
- "FROM moz_places " +
- "WHERE url = :page_url");
- },
- _guidCols: ["guid"],
-
- GUIDForUri: function GUIDForUri(uri, create) {
- let stm = this._guidStm;
- stm.params.page_url = uri.spec ? uri.spec : uri;
-
- // Use the existing GUID if it exists
- let result = Async.querySpinningly(stm, this._guidCols)[0];
- if (result && result.guid)
- return result.guid;
-
- // Give the uri a GUID if it doesn't have one
- if (create)
- return this.setGUID(uri);
- },
-
- get _visitStm() {
- return this._getStmt(
- "/* do not warn (bug 599936) */ " +
- "SELECT visit_type type, visit_date date " +
- "FROM moz_historyvisits " +
- "WHERE place_id = (SELECT id FROM moz_places WHERE url = :url) " +
- "ORDER BY date DESC LIMIT 10");
- },
- _visitCols: ["date", "type"],
-
- get _urlStm() {
- return this._getStmt(
- "SELECT url, title, frecency " +
- "FROM moz_places " +
- "WHERE guid = :guid");
- },
- _urlCols: ["url", "title", "frecency"],
-
- get _allUrlStm() {
- return this._getStmt(
- "SELECT url " +
- "FROM moz_places " +
- "WHERE last_visit_date > :cutoff_date " +
- "ORDER BY frecency DESC " +
- "LIMIT :max_results");
- },
- _allUrlCols: ["url"],
-
- // See bug 320831 for why we use SQL here
- _getVisits: function HistStore__getVisits(uri) {
- this._visitStm.params.url = uri;
- return Async.querySpinningly(this._visitStm, this._visitCols);
- },
-
- // See bug 468732 for why we use SQL here
- _findURLByGUID: function HistStore__findURLByGUID(guid) {
- this._urlStm.params.guid = guid;
- return Async.querySpinningly(this._urlStm, this._urlCols)[0];
- },
-
- changeItemID: function HStore_changeItemID(oldID, newID) {
- this.setGUID(this._findURLByGUID(oldID).url, newID);
- },
-
-
- getAllIDs: function HistStore_getAllIDs() {
- // Only get places visited within the last 30 days (30*24*60*60*1000ms)
- this._allUrlStm.params.cutoff_date = (Date.now() - 2592000000) * 1000;
- this._allUrlStm.params.max_results = MAX_HISTORY_UPLOAD;
-
- let urls = Async.querySpinningly(this._allUrlStm, this._allUrlCols);
- let self = this;
- return urls.reduce(function(ids, item) {
- ids[self.GUIDForUri(item.url, true)] = item.url;
- return ids;
- }, {});
- },
-
- applyIncomingBatch: function applyIncomingBatch(records) {
- let failed = [];
-
- // Convert incoming records to mozIPlaceInfo objects. Some records can be
- // ignored or handled directly, so we're rewriting the array in-place.
- let i, k;
- for (i = 0, k = 0; i < records.length; i++) {
- let record = records[k] = records[i];
- let shouldApply;
-
- // This is still synchronous I/O for now.
- try {
- if (record.deleted) {
- // Consider using nsIBrowserHistory::removePages() here.
- this.remove(record);
- // No further processing needed. Remove it from the list.
- shouldApply = false;
- } else {
- shouldApply = this._recordToPlaceInfo(record);
- }
- } catch(ex) {
- failed.push(record.id);
- shouldApply = false;
- }
-
- if (shouldApply) {
- k += 1;
- }
- }
- records.length = k; // truncate array
-
- // Nothing to do.
- if (!records.length) {
- return failed;
- }
-
- let updatePlacesCallback = {
- handleResult: function handleResult() {},
- handleError: function handleError(resultCode, placeInfo) {
- failed.push(placeInfo.guid);
- },
- handleCompletion: Async.makeSyncCallback()
- };
- this._asyncHistory.updatePlaces(records, updatePlacesCallback);
- Async.waitForSyncCallback(updatePlacesCallback.handleCompletion);
- return failed;
- },
-
- /**
- * Converts a Sync history record to a mozIPlaceInfo.
- *
- * Throws if an invalid record is encountered (invalid URI, etc.),
- * returns true if the record is to be applied, false otherwise
- * (no visits to add, etc.),
- */
- _recordToPlaceInfo: function _recordToPlaceInfo(record) {
- // Sort out invalid URIs and ones Places just simply doesn't want.
- record.uri = Utils.makeURI(record.histUri);
- if (!record.uri) {
- this._log.warn("Attempted to process invalid URI, skipping.");
- throw "Invalid URI in record";
- }
-
- if (!Utils.checkGUID(record.id)) {
- this._log.warn("Encountered record with invalid GUID: " + record.id);
- return false;
- }
- record.guid = record.id;
-
- if (!PlacesUtils.history.canAddURI(record.uri)) {
- this._log.trace("Ignoring record " + record.id + " with URI "
- + record.uri.spec + ": can't add this URI.");
- return false;
- }
-
- // We dupe visits by date and type. So an incoming visit that has
- // the same timestamp and type as a local one won't get applied.
- // To avoid creating new objects, we rewrite the query result so we
- // can simply check for containment below.
- let curVisits = this._getVisits(record.histUri);
- let i, k;
- for (i = 0; i < curVisits.length; i++) {
- curVisits[i] = curVisits[i].date + "," + curVisits[i].type;
- }
-
- // Walk through the visits, make sure we have sound data, and eliminate
- // dupes. The latter is done by rewriting the array in-place.
- for (i = 0, k = 0; i < record.visits.length; i++) {
- let visit = record.visits[k] = record.visits[i];
-
- if (!visit.date || typeof visit.date != "number") {
- this._log.warn("Encountered record with invalid visit date: "
- + visit.date);
- throw "Visit has no date!";
- }
-
- if (!visit.type || !(visit.type >= PlacesUtils.history.TRANSITION_LINK &&
- visit.type <= PlacesUtils.history.TRANSITION_RELOAD)) {
- this._log.warn("Encountered record with invalid visit type: "
- + visit.type);
- throw "Invalid visit type!";
- }
-
- // Dates need to be integers.
- visit.date = Math.round(visit.date);
-
- if (curVisits.indexOf(visit.date + "," + visit.type) != -1) {
- // Visit is a dupe, don't increment 'k' so the element will be
- // overwritten.
- continue;
- }
- visit.visitDate = visit.date;
- visit.transitionType = visit.type;
- k += 1;
- }
- record.visits.length = k; // truncate array
-
- // No update if there aren't any visits to apply.
- // mozIAsyncHistory::updatePlaces() wants at least one visit.
- // In any case, the only thing we could change would be the title
- // and that shouldn't change without a visit.
- if (!record.visits.length) {
- this._log.trace("Ignoring record " + record.id + " with URI "
- + record.uri.spec + ": no visits to add.");
- return false;
- }
-
- return true;
- },
-
- remove: function HistStore_remove(record) {
- let page = this._findURLByGUID(record.id);
- if (page == null) {
- this._log.debug("Page already removed: " + record.id);
- return;
- }
-
- let uri = Utils.makeURI(page.url);
- PlacesUtils.history.removePage(uri);
- this._log.trace("Removed page: " + [record.id, page.url, page.title]);
- },
-
- itemExists: function HistStore_itemExists(id) {
- return !!this._findURLByGUID(id);
- },
-
- createRecord: function createRecord(id, collection) {
- let foo = this._findURLByGUID(id);
- let record = new HistoryRec(collection, id);
- if (foo) {
- record.histUri = foo.url;
- record.title = foo.title;
- record.sortindex = foo.frecency;
- record.visits = this._getVisits(record.histUri);
- } else {
- record.deleted = true;
- }
-
- return record;
- },
-
- wipe: function HistStore_wipe() {
- PlacesUtils.history.clear();
- }
-};
-
-function HistoryTracker(name, engine) {
- Tracker.call(this, name, engine);
-}
-HistoryTracker.prototype = {
- __proto__: Tracker.prototype,
-
- startTracking: function() {
- this._log.info("Adding Places observer.");
- PlacesUtils.history.addObserver(this, true);
- },
-
- stopTracking: function() {
- this._log.info("Removing Places observer.");
- PlacesUtils.history.removeObserver(this);
- },
-
- QueryInterface: XPCOMUtils.generateQI([
- Ci.nsINavHistoryObserver,
- Ci.nsISupportsWeakReference
- ]),
-
- onDeleteAffectsGUID: function (uri, guid, reason, source, increment) {
- if (this.ignoreAll || reason == Ci.nsINavHistoryObserver.REASON_EXPIRED) {
- return;
- }
- this._log.trace(source + ": " + uri.spec + ", reason " + reason);
- if (this.addChangedID(guid)) {
- this.score += increment;
- }
- },
-
- onDeleteVisits: function (uri, visitTime, guid, reason) {
- this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteVisits", SCORE_INCREMENT_SMALL);
- },
-
- onDeleteURI: function (uri, guid, reason) {
- this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteURI", SCORE_INCREMENT_XLARGE);
- },
-
- onVisit: function (uri, vid, time, session, referrer, trans, guid) {
- if (this.ignoreAll) {
- this._log.trace("ignoreAll: ignoring visit for " + guid);
- return;
- }
-
- this._log.trace("onVisit: " + uri.spec);
- if (this.addChangedID(guid)) {
- this.score += SCORE_INCREMENT_SMALL;
- }
- },
-
- onClearHistory: function () {
- this._log.trace("onClearHistory");
- // Note that we're going to trigger a sync, but none of the cleared
- // pages are tracked, so the deletions will not be propagated.
- // See Bug 578694.
- this.score += SCORE_INCREMENT_XLARGE;
- },
-
- onBeginUpdateBatch: function () {},
- onEndUpdateBatch: function () {},
- onPageChanged: function () {},
- onTitleChanged: function () {},
- onBeforeDeleteURI: function () {},
-};
diff --git a/components/weave/src/engines/passwords.js b/components/weave/src/engines/passwords.js
deleted file mode 100644
index 0ccd2e7b0..000000000
--- a/components/weave/src/engines/passwords.js
+++ /dev/null
@@ -1,305 +0,0 @@
-/* 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/. */
-
-this.EXPORTED_SYMBOLS = ['PasswordEngine', 'LoginRec'];
-
-var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/util.js");
-
-this.LoginRec = function LoginRec(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-LoginRec.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Sync.Record.Login",
-};
-
-Utils.deferGetSet(LoginRec, "cleartext", [
- "hostname", "formSubmitURL",
- "httpRealm", "username", "password", "usernameField", "passwordField",
- ]);
-
-
-this.PasswordEngine = function PasswordEngine(service) {
- SyncEngine.call(this, "Passwords", service);
-}
-PasswordEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _storeObj: PasswordStore,
- _trackerObj: PasswordTracker,
- _recordObj: LoginRec,
-
- applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE,
-
- syncPriority: 2,
-
- _syncFinish: function () {
- SyncEngine.prototype._syncFinish.call(this);
-
- // Delete the Weave credentials from the server once.
- if (!Svc.Prefs.get("deletePwdFxA", false)) {
- try {
- let ids = [];
- for (let host of Utils.getSyncCredentialsHosts()) {
- for (let info of Services.logins.findLogins({}, host, "", "")) {
- ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid);
- }
- }
- if (ids.length) {
- let coll = new Collection(this.engineURL, null, this.service);
- coll.ids = ids;
- let ret = coll.delete();
- this._log.debug("Delete result: " + ret);
- if (!ret.success && ret.status != 400) {
- // A non-400 failure means try again next time.
- return;
- }
- } else {
- this._log.debug("Didn't find any passwords to delete");
- }
- // If there were no ids to delete, or we succeeded, or got a 400,
- // record success.
- Svc.Prefs.set("deletePwdFxA", true);
- Svc.Prefs.reset("deletePwd"); // The old prefname we previously used.
- } catch (ex) {
- this._log.debug("Password deletes failed: ", ex);
- }
- }
- },
-
- _findDupe: function (item) {
- let login = this._store._nsLoginInfoFromRecord(item);
- if (!login) {
- return;
- }
-
- let logins = Services.logins.findLogins({}, login.hostname, login.formSubmitURL, login.httpRealm);
-
- this._store._sleep(0); // Yield back to main thread after synchronous operation.
-
- // Look for existing logins that match the hostname, but ignore the password.
- for (let local of logins) {
- if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) {
- return local.guid;
- }
- }
- },
-};
-
-function PasswordStore(name, engine) {
- Store.call(this, name, engine);
- this._nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
-}
-PasswordStore.prototype = {
- __proto__: Store.prototype,
-
- _nsLoginInfoFromRecord: function (record) {
- function nullUndefined(x) {
- return (x == undefined) ? null : x;
- }
-
- if (record.formSubmitURL && record.httpRealm) {
- this._log.warn("Record " + record.id + " has both formSubmitURL and httpRealm. Skipping.");
- return null;
- }
-
- // Passing in "undefined" results in an empty string, which later
- // counts as a value. Explicitly `|| null` these fields according to JS
- // truthiness. Records with empty strings or null will be unmolested.
- let info = new this._nsLoginInfo(record.hostname,
- nullUndefined(record.formSubmitURL),
- nullUndefined(record.httpRealm),
- record.username,
- record.password,
- record.usernameField,
- record.passwordField);
- info.QueryInterface(Ci.nsILoginMetaInfo);
- info.guid = record.id;
- return info;
- },
-
- _getLoginFromGUID: function (id) {
- let prop = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
- prop.setPropertyAsAUTF8String("guid", id);
-
- let logins = Services.logins.searchLogins({}, prop);
- this._sleep(0); // Yield back to main thread after synchronous operation.
-
- if (logins.length > 0) {
- this._log.trace(logins.length + " items matching " + id + " found.");
- return logins[0];
- }
-
- this._log.trace("No items matching " + id + " found. Ignoring");
- return null;
- },
-
- getAllIDs: function () {
- let items = {};
- let logins = Services.logins.getAllLogins({});
-
- for (let i = 0; i < logins.length; i++) {
- // Skip over Weave password/passphrase entries.
- let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo);
- if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) {
- continue;
- }
-
- items[metaInfo.guid] = metaInfo;
- }
-
- return items;
- },
-
- changeItemID: function (oldID, newID) {
- this._log.trace("Changing item ID: " + oldID + " to " + newID);
-
- let oldLogin = this._getLoginFromGUID(oldID);
- if (!oldLogin) {
- this._log.trace("Can't change item ID: item doesn't exist");
- return;
- }
- if (this._getLoginFromGUID(newID)) {
- this._log.trace("Can't change item ID: new ID already in use");
- return;
- }
-
- let prop = Cc["@mozilla.org/hash-property-bag;1"]
- .createInstance(Ci.nsIWritablePropertyBag2);
- prop.setPropertyAsAUTF8String("guid", newID);
-
- Services.logins.modifyLogin(oldLogin, prop);
- },
-
- itemExists: function (id) {
- return !!this._getLoginFromGUID(id);
- },
-
- createRecord: function (id, collection) {
- let record = new LoginRec(collection, id);
- let login = this._getLoginFromGUID(id);
-
- if (!login) {
- record.deleted = true;
- return record;
- }
-
- record.hostname = login.hostname;
- record.formSubmitURL = login.formSubmitURL;
- record.httpRealm = login.httpRealm;
- record.username = login.username;
- record.password = login.password;
- record.usernameField = login.usernameField;
- record.passwordField = login.passwordField;
-
- return record;
- },
-
- create: function (record) {
- let login = this._nsLoginInfoFromRecord(record);
- if (!login) {
- return;
- }
-
- this._log.debug("Adding login for " + record.hostname);
- this._log.trace("httpRealm: " + JSON.stringify(login.httpRealm) + "; " +
- "formSubmitURL: " + JSON.stringify(login.formSubmitURL));
- try {
- Services.logins.addLogin(login);
- } catch(ex) {
- this._log.debug("Adding record " + record.id +
- " resulted in exception ", ex);
- }
- },
-
- remove: function (record) {
- this._log.trace("Removing login " + record.id);
-
- let loginItem = this._getLoginFromGUID(record.id);
- if (!loginItem) {
- this._log.trace("Asked to remove record that doesn't exist, ignoring");
- return;
- }
-
- Services.logins.removeLogin(loginItem);
- },
-
- update: function (record) {
- let loginItem = this._getLoginFromGUID(record.id);
- if (!loginItem) {
- this._log.debug("Skipping update for unknown item: " + record.hostname);
- return;
- }
-
- this._log.debug("Updating " + record.hostname);
- let newinfo = this._nsLoginInfoFromRecord(record);
- if (!newinfo) {
- return;
- }
-
- try {
- Services.logins.modifyLogin(loginItem, newinfo);
- } catch(ex) {
- this._log.debug("Modifying record " + record.id +
- " resulted in exception. Not modifying.", ex);
- }
- },
-
- wipe: function () {
- Services.logins.removeAllLogins();
- },
-};
-
-function PasswordTracker(name, engine) {
- Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
-}
-PasswordTracker.prototype = {
- __proto__: Tracker.prototype,
-
- startTracking: function () {
- Svc.Obs.add("passwordmgr-storage-changed", this);
- },
-
- stopTracking: function () {
- Svc.Obs.remove("passwordmgr-storage-changed", this);
- },
-
- observe: function (subject, topic, data) {
- Tracker.prototype.observe.call(this, subject, topic, data);
-
- if (this.ignoreAll) {
- return;
- }
-
- // A single add, remove or change or removing all items
- // will trigger a sync for MULTI_DEVICE.
- switch (data) {
- case "modifyLogin":
- subject = subject.QueryInterface(Ci.nsIArray).queryElementAt(1, Ci.nsILoginMetaInfo);
- // Fall through.
- case "addLogin":
- case "removeLogin":
- // Skip over Weave password/passphrase changes.
- subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
- if (Utils.getSyncCredentialsHosts().has(subject.hostname)) {
- break;
- }
-
- this.score += SCORE_INCREMENT_XLARGE;
- this._log.trace(data + ": " + subject.guid);
- this.addChangedID(subject.guid);
- break;
- case "removeAllLogins":
- this._log.trace(data);
- this.score += SCORE_INCREMENT_XLARGE;
- break;
- }
- },
-};
diff --git a/components/weave/src/engines/prefs.js b/components/weave/src/engines/prefs.js
deleted file mode 100644
index 1a61c517d..000000000
--- a/components/weave/src/engines/prefs.js
+++ /dev/null
@@ -1,260 +0,0 @@
-/* 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/. */
-
-this.EXPORTED_SYMBOLS = ['PrefsEngine', 'PrefRec'];
-
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cu = Components.utils;
-
-const SYNC_PREFS_PREFIX = "services.sync.prefs.sync.";
-
-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/constants.js");
-Cu.import("resource://gre/CommonUtils.jsm");
-Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-
-const PREFS_GUID = CommonUtils.encodeBase64URL(Services.appinfo.ID);
-
-this.PrefRec = function PrefRec(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-PrefRec.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Sync.Record.Pref",
-};
-
-Utils.deferGetSet(PrefRec, "cleartext", ["value"]);
-
-
-this.PrefsEngine = function PrefsEngine(service) {
- SyncEngine.call(this, "Prefs", service);
-}
-PrefsEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _storeObj: PrefStore,
- _trackerObj: PrefTracker,
- _recordObj: PrefRec,
- version: 2,
-
- syncPriority: 1,
-
- getChangedIDs: function () {
- // No need for a proper timestamp (no conflict resolution needed).
- let changedIDs = {};
- if (this._tracker.modified)
- changedIDs[PREFS_GUID] = 0;
- return changedIDs;
- },
-
- _wipeClient: function () {
- SyncEngine.prototype._wipeClient.call(this);
- this.justWiped = true;
- },
-
- _reconcile: function (item) {
- // Apply the incoming item if we don't care about the local data
- if (this.justWiped) {
- this.justWiped = false;
- return true;
- }
- return SyncEngine.prototype._reconcile.call(this, item);
- }
-};
-
-
-function PrefStore(name, engine) {
- Store.call(this, name, engine);
- Svc.Obs.add("profile-before-change", function () {
- this.__prefs = null;
- }, this);
-}
-PrefStore.prototype = {
- __proto__: Store.prototype,
-
- __prefs: null,
- get _prefs() {
- if (!this.__prefs) {
- this.__prefs = new Preferences();
- }
- return this.__prefs;
- },
-
- _getSyncPrefs: function () {
- let syncPrefs = Cc["@mozilla.org/preferences-service;1"]
- .getService(Ci.nsIPrefService)
- .getBranch(SYNC_PREFS_PREFIX)
- .getChildList("", {});
- // Also sync preferences that determine which prefs get synced.
- let controlPrefs = syncPrefs.map(pref => SYNC_PREFS_PREFIX + pref);
- return controlPrefs.concat(syncPrefs);
- },
-
- _isSynced: function (pref) {
- return pref.startsWith(SYNC_PREFS_PREFIX) ||
- this._prefs.get(SYNC_PREFS_PREFIX + pref, false);
- },
-
- _getAllPrefs: function () {
- let values = {};
- for each (let pref in this._getSyncPrefs()) {
- if (this._isSynced(pref)) {
- // Missing prefs get the null value.
- values[pref] = this._prefs.get(pref, null);
- }
- }
- return values;
- },
-
- _setAllPrefs: function (values) {
- let enabledPref = "lightweightThemes.isThemeSelected";
- let enabledBefore = this._prefs.get(enabledPref, false);
- let prevTheme = LightweightThemeManager.currentTheme;
-
- // Update 'services.sync.prefs.sync.foo.pref' before 'foo.pref', otherwise
- // _isSynced returns false when 'foo.pref' doesn't exist (e.g., on a new device).
- let prefs = Object.keys(values).sort(a => -a.indexOf(SYNC_PREFS_PREFIX));
- for (let pref of prefs) {
- if (!this._isSynced(pref)) {
- continue;
- }
-
- let value = values[pref];
-
- // Pref has gone missing. The best we can do is reset it.
- if (value == null) {
- this._prefs.reset(pref);
- continue;
- }
-
- try {
- this._prefs.set(pref, value);
- } catch(ex) {
- this._log.trace("Failed to set pref: " + pref + ": " + ex);
- }
- }
-
- // Notify the lightweight theme manager of all the new values
- let enabledNow = this._prefs.get(enabledPref, false);
- if (enabledBefore && !enabledNow) {
- LightweightThemeManager.currentTheme = null;
- } else if (enabledNow && LightweightThemeManager.usedThemes[0] != prevTheme) {
- LightweightThemeManager.currentTheme = null;
- LightweightThemeManager.currentTheme = LightweightThemeManager.usedThemes[0];
- }
- },
-
- getAllIDs: function () {
- /* We store all prefs in just one WBO, with just one GUID */
- let allprefs = {};
- allprefs[PREFS_GUID] = true;
- return allprefs;
- },
-
- changeItemID: function (oldID, newID) {
- this._log.trace("PrefStore GUID is constant!");
- },
-
- itemExists: function (id) {
- return (id === PREFS_GUID);
- },
-
- createRecord: function (id, collection) {
- let record = new PrefRec(collection, id);
-
- if (id == PREFS_GUID) {
- record.value = this._getAllPrefs();
- } else {
- record.deleted = true;
- }
-
- return record;
- },
-
- create: function (record) {
- this._log.trace("Ignoring create request");
- },
-
- remove: function (record) {
- this._log.trace("Ignoring remove request");
- },
-
- update: function (record) {
- // Silently ignore pref updates that are for other apps.
- if (record.id != PREFS_GUID)
- return;
-
- this._log.trace("Received pref updates, applying...");
- this._setAllPrefs(record.value);
- },
-
- wipe: function () {
- this._log.trace("Ignoring wipe request");
- }
-};
-
-function PrefTracker(name, engine) {
- Tracker.call(this, name, engine);
- Svc.Obs.add("profile-before-change", this);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
-}
-PrefTracker.prototype = {
- __proto__: Tracker.prototype,
-
- get modified() {
- return Svc.Prefs.get("engine.prefs.modified", false);
- },
- set modified(value) {
- Svc.Prefs.set("engine.prefs.modified", value);
- },
-
- loadChangedIDs: function loadChangedIDs() {
- // Don't read changed IDs from disk at start up.
- },
-
- clearChangedIDs: function clearChangedIDs() {
- this.modified = false;
- },
-
- __prefs: null,
- get _prefs() {
- if (!this.__prefs) {
- this.__prefs = new Preferences();
- }
- return this.__prefs;
- },
-
- startTracking: function () {
- Services.prefs.addObserver("", this, false);
- },
-
- stopTracking: function () {
- this.__prefs = null;
- Services.prefs.removeObserver("", this);
- },
-
- observe: function (subject, topic, data) {
- Tracker.prototype.observe.call(this, subject, topic, data);
-
- switch (topic) {
- case "profile-before-change":
- this.stopTracking();
- break;
- case "nsPref:changed":
- // Trigger a sync for MULTI-DEVICE for a change that determines
- // which prefs are synced or a regular pref change.
- if (data.indexOf(SYNC_PREFS_PREFIX) == 0 ||
- this._prefs.get(SYNC_PREFS_PREFIX + data, false)) {
- this.score += SCORE_INCREMENT_XLARGE;
- this.modified = true;
- this._log.trace("Preference " + data + " changed");
- }
- break;
- }
- }
-};
diff --git a/components/weave/src/engines/tabs.js b/components/weave/src/engines/tabs.js
deleted file mode 100644
index 167faf625..000000000
--- a/components/weave/src/engines/tabs.js
+++ /dev/null
@@ -1,376 +0,0 @@
-/* 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/. */
-
-this.EXPORTED_SYMBOLS = ["TabEngine", "TabSetRecord"];
-
-var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-const TABS_TTL = 604800; // 7 days.
-const TAB_ENTRIES_LIMIT = 25; // How many URLs to include in tab history.
-
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/engines/clients.js");
-Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/constants.js");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
- "resource://gre/modules/PrivateBrowsingUtils.jsm");
-
-this.TabSetRecord = function TabSetRecord(collection, id) {
- CryptoWrapper.call(this, collection, id);
-}
-TabSetRecord.prototype = {
- __proto__: CryptoWrapper.prototype,
- _logName: "Sync.Record.Tabs",
- ttl: TABS_TTL,
-};
-
-Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]);
-
-
-this.TabEngine = function TabEngine(service) {
- SyncEngine.call(this, "Tabs", service);
-
- // Reset the client on every startup so that we fetch recent tabs.
- this._resetClient();
-}
-TabEngine.prototype = {
- __proto__: SyncEngine.prototype,
- _storeObj: TabStore,
- _trackerObj: TabTracker,
- _recordObj: TabSetRecord,
-
- syncPriority: 3,
-
- getChangedIDs: function () {
- // No need for a proper timestamp (no conflict resolution needed).
- let changedIDs = {};
- if (this._tracker.modified)
- changedIDs[this.service.clientsEngine.localID] = 0;
- return changedIDs;
- },
-
- // API for use by Sync UI code to give user choices of tabs to open.
- getAllClients: function () {
- return this._store._remoteClients;
- },
-
- getClientById: function (id) {
- return this._store._remoteClients[id];
- },
-
- _resetClient: function () {
- SyncEngine.prototype._resetClient.call(this);
- this._store.wipe();
- this._tracker.modified = true;
- },
-
- removeClientData: function () {
- let url = this.engineURL + "/" + this.service.clientsEngine.localID;
- this.service.resource(url).delete();
- },
-
- /**
- * Return a Set of open URLs.
- */
- getOpenURLs: function () {
- let urls = new Set();
- for (let entry of this._store.getAllTabs()) {
- urls.add(entry.urlHistory[0]);
- }
- return urls;
- },
-
- _reconcile: function (item) {
- // Skip our own record.
- // TabStore.itemExists tests only against our local client ID.
- if (this._store.itemExists(item.id)) {
- this._log.trace("Ignoring incoming tab item because of its id: " + item.id);
- return false;
- }
-
- return SyncEngine.prototype._reconcile.call(this, item);
- }
-};
-
-
-function TabStore(name, engine) {
- Store.call(this, name, engine);
-}
-TabStore.prototype = {
- __proto__: Store.prototype,
-
- itemExists: function (id) {
- return id == this.engine.service.clientsEngine.localID;
- },
-
- getWindowEnumerator: function () {
- return Services.wm.getEnumerator("navigator:browser");
- },
-
- shouldSkipWindow: function (win) {
- return win.closed ||
- PrivateBrowsingUtils.isWindowPrivate(win);
- },
-
- getTabState: function (tab) {
- return JSON.parse(Svc.Session.getTabState(tab));
- },
-
- getAllTabs: function (filter) {
- let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i");
-
- let allTabs = [];
-
- let winEnum = this.getWindowEnumerator();
- while (winEnum.hasMoreElements()) {
- let win = winEnum.getNext();
- if (this.shouldSkipWindow(win)) {
- continue;
- }
-
- for (let tab of win.gBrowser.tabs) {
- tabState = this.getTabState(tab);
-
- // Make sure there are history entries to look at.
- if (!tabState || !tabState.entries.length) {
- continue;
- }
-
- let acceptable = !filter ? (url) => url :
- (url) => url && !filteredUrls.test(url);
-
- let entries = tabState.entries;
- let index = tabState.index;
- let current = entries[index - 1];
-
- // We ignore the tab completely if the current entry url is
- // not acceptable (we need something accurate to open).
- if (!acceptable(current.url)) {
- continue;
- }
-
- // The element at `index` is the current page. Previous URLs were
- // previously visited URLs; subsequent URLs are in the 'forward' stack,
- // which we can't represent in Sync, so we truncate here.
- let candidates = (entries.length == index) ?
- entries :
- entries.slice(0, index);
-
- let urls = candidates.map((entry) => entry.url)
- .filter(acceptable)
- .reverse(); // Because Sync puts current at index 0, and history after.
-
- // Truncate if necessary.
- if (urls.length > TAB_ENTRIES_LIMIT) {
- urls.length = TAB_ENTRIES_LIMIT;
- }
-
- allTabs.push({
- title: current.title || "",
- urlHistory: urls,
- icon: tabState.attributes && tabState.attributes.image || "",
- lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000),
- });
- }
- }
-
- return allTabs;
- },
-
- createRecord: function (id, collection) {
- let record = new TabSetRecord(collection, id);
- record.clientName = this.engine.service.clientsEngine.localName;
-
- // Sort tabs in descending-used order to grab the most recently used
- let tabs = this.getAllTabs(true).sort(function (a, b) {
- return b.lastUsed - a.lastUsed;
- });
-
- // Figure out how many tabs we can pack into a payload. Starting with a 28KB
- // payload, we can estimate various overheads from encryption/JSON/WBO.
- let size = JSON.stringify(tabs).length;
- let origLength = tabs.length;
- const MAX_TAB_SIZE = 20000;
- if (size > MAX_TAB_SIZE) {
- // Estimate a little more than the direct fraction to maximize packing
- let cutoff = Math.ceil(tabs.length * MAX_TAB_SIZE / size);
- tabs = tabs.slice(0, cutoff + 1);
-
- // Keep dropping off the last entry until the data fits
- while (JSON.stringify(tabs).length > MAX_TAB_SIZE)
- tabs.pop();
- }
-
- this._log.trace("Created tabs " + tabs.length + " of " + origLength);
- tabs.forEach(function (tab) {
- this._log.trace("Wrapping tab: " + JSON.stringify(tab));
- }, this);
-
- record.tabs = tabs;
- return record;
- },
-
- getAllIDs: function () {
- // Don't report any tabs if all windows are in private browsing for
- // first syncs.
- let ids = {};
- let allWindowsArePrivate = false;
- let wins = Services.wm.getEnumerator("navigator:browser");
- while (wins.hasMoreElements()) {
- if (PrivateBrowsingUtils.isWindowPrivate(wins.getNext())) {
- // Ensure that at least there is a private window.
- allWindowsArePrivate = true;
- } else {
- // If there is a not private windown then finish and continue.
- allWindowsArePrivate = false;
- break;
- }
- }
-
- if (allWindowsArePrivate &&
- !PrivateBrowsingUtils.permanentPrivateBrowsing) {
- return ids;
- }
-
- ids[this.engine.service.clientsEngine.localID] = true;
- return ids;
- },
-
- wipe: function () {
- this._remoteClients = {};
- },
-
- create: function (record) {
- this._log.debug("Adding remote tabs from " + record.clientName);
- this._remoteClients[record.id] = record.cleartext;
-
- // Lose some precision, but that's good enough (seconds).
- let roundModify = Math.floor(record.modified / 1000);
- let notifyState = Svc.Prefs.get("notifyTabState");
-
- // If there's no existing pref, save this first modified time.
- if (notifyState == null) {
- Svc.Prefs.set("notifyTabState", roundModify);
- return;
- }
-
- // Don't change notifyState if it's already 0 (don't notify).
- if (notifyState == 0) {
- return;
- }
-
- // We must have gotten a new tab that isn't the same as last time.
- if (notifyState != roundModify) {
- Svc.Prefs.set("notifyTabState", 0);
- }
- },
-
- update: function (record) {
- this._log.trace("Ignoring tab updates as local ones win");
- },
-};
-
-
-function TabTracker(name, engine) {
- Tracker.call(this, name, engine);
- Svc.Obs.add("weave:engine:start-tracking", this);
- Svc.Obs.add("weave:engine:stop-tracking", this);
-
- // Make sure "this" pointer is always set correctly for event listeners.
- this.onTab = Utils.bind2(this, this.onTab);
- this._unregisterListeners = Utils.bind2(this, this._unregisterListeners);
-}
-TabTracker.prototype = {
- __proto__: Tracker.prototype,
-
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
-
- loadChangedIDs: function () {
- // Don't read changed IDs from disk at start up.
- },
-
- clearChangedIDs: function () {
- this.modified = false;
- },
-
- _topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"],
-
- _registerListenersForWindow: function (window) {
- this._log.trace("Registering tab listeners in window");
- for (let topic of this._topics) {
- window.addEventListener(topic, this.onTab, false);
- }
- window.addEventListener("unload", this._unregisterListeners, false);
- },
-
- _unregisterListeners: function (event) {
- this._unregisterListenersForWindow(event.target);
- },
-
- _unregisterListenersForWindow: function (window) {
- this._log.trace("Removing tab listeners in window");
- window.removeEventListener("unload", this._unregisterListeners, false);
- for (let topic of this._topics) {
- window.removeEventListener(topic, this.onTab, false);
- }
- },
-
- startTracking: function () {
- Svc.Obs.add("domwindowopened", this);
- let wins = Services.wm.getEnumerator("navigator:browser");
- while (wins.hasMoreElements()) {
- this._registerListenersForWindow(wins.getNext());
- }
- },
-
- stopTracking: function () {
- Svc.Obs.remove("domwindowopened", this);
- let wins = Services.wm.getEnumerator("navigator:browser");
- while (wins.hasMoreElements()) {
- this._unregisterListenersForWindow(wins.getNext());
- }
- },
-
- observe: function (subject, topic, data) {
- Tracker.prototype.observe.call(this, subject, topic, data);
-
- switch (topic) {
- case "domwindowopened":
- let onLoad = () => {
- subject.removeEventListener("load", onLoad, false);
- // Only register after the window is done loading to avoid unloads.
- this._registerListenersForWindow(subject);
- };
-
- // Add tab listeners now that a window has opened.
- subject.addEventListener("load", onLoad, false);
- break;
- }
- },
-
- onTab: function (event) {
- if (event.originalTarget.linkedBrowser) {
- let browser = event.originalTarget.linkedBrowser;
- if (PrivateBrowsingUtils.isBrowserPrivate(browser) &&
- !PrivateBrowsingUtils.permanentPrivateBrowsing) {
- this._log.trace("Ignoring tab event from private browsing.");
- return;
- }
- }
-
- this._log.trace("onTab event: " + event.type);
- this.modified = true;
-
- // For page shows, bump the score 10% of the time, emulating a partial
- // score. We don't want to sync too frequently. For all other page
- // events, always bump the score.
- if (event.type != "pageshow" || Math.random() < .1) {
- this.score += SCORE_INCREMENT_SMALL;
- }
- },
-};