diff options
Diffstat (limited to 'components/weave/src/engines')
-rw-r--r-- | components/weave/src/engines/addons.js | 706 | ||||
-rw-r--r-- | components/weave/src/engines/bookmarks.js | 1542 | ||||
-rw-r--r-- | components/weave/src/engines/clients.js | 476 | ||||
-rw-r--r-- | components/weave/src/engines/forms.js | 246 | ||||
-rw-r--r-- | components/weave/src/engines/history.js | 417 | ||||
-rw-r--r-- | components/weave/src/engines/passwords.js | 305 | ||||
-rw-r--r-- | components/weave/src/engines/prefs.js | 260 | ||||
-rw-r--r-- | components/weave/src/engines/tabs.js | 376 |
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; - } - }, -}; |