From d87f95e16df72f4fd61442cf67eee602e27c4bcd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 12 Sep 2007 17:03:30 -0400 Subject: [PATCH 0001/1860] Bookmarks sync extension - initial import --- services/sync/nsBookmarksSyncService.js | 672 ++++++++++++++++++++++ services/sync/nsIBookmarksSyncService.idl | 45 ++ services/sync/xptgen | 2 + 3 files changed, 719 insertions(+) create mode 100644 services/sync/nsBookmarksSyncService.js create mode 100644 services/sync/nsIBookmarksSyncService.idl create mode 100755 services/sync/xptgen diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js new file mode 100644 index 000000000000..ff581015f4f2 --- /dev/null +++ b/services/sync/nsBookmarksSyncService.js @@ -0,0 +1,672 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Places. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function BookmarksSyncService() { this._init(); } +BookmarksSyncService.prototype = { + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + __hsvc: null, + get _hsvc() { + if (!this.__hsvc) + this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + return this.__hsvc; + }, + + __ans: null, + get _ans() { + if (!this.__ans) + this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + return this.__ans; + }, + + // DAVCollection object + _dav: null, + + // sync.js + _sync: {}, + + // PlacesUtils + _utils: {}, + + // Last synced tree + // FIXME: this should be serialized to disk + _snapshot: {}, + _snapshotVersion: 0, + + _init: function BSS__init() { + + var serverUrl = "http://sync.server.url/"; + try { + var branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + serverUrl = branch.getCharPref("browser.places.sync.serverUrl"); + } + catch (ex) { /* use defaults */ } + LOG("Bookmarks sync server: " + serverUrl); + this._dav = new DAVCollection(serverUrl); + + var jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + jsLoader.loadSubScript("chrome://sync/content/sync-engine.js", this._sync); + jsLoader.loadSubScript("chrome://browser/content/places/utils.js", this._utils); + }, + + _applyCommands: function BSS__applyCommands(node, commandList) { + for (var i = 0; i < commandList.length; i++) { + var command = commandList[i]; + LOG("Processing command: " + uneval(command)); + switch (command["action"]) { + case "create": + this._createCommand(node, this._snapshot, command); + break; + case "remove": + this._removeCommand(node, command); + break; + case "edit": + this._editCommand(node, command); + break; + default: + LOG("unknown action in command: " + command["action"]); + break; + } + } + }, + + _nodeFromPath: function BSS__nodeFromPath (aNodeRoot, aPath) { + var node = aNodeRoot; + for (var i = 0; i < aPath.length; i = i + 2) { + if (aPath[i] != "children") + break; + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + var openState = node.containerOpen; + node.containerOpen = true; + node = node.getChild(aPath[i + 1]); + //node.containerOpen = openState; // fixme? + } + return node; + }, + + _createCommand: function BSS__createCommand(aNode, aJsonNode, aCommand) { + var path = aCommand["path"]; + if (path[path.length - 2] != "children") + path = aCommand["path"].slice(0, path.length - 1); + + var json = this._sync.pathToReference(aJsonNode, path); + if (json["_done"] == true) + return; + json["_done"] = true; + + var index = path[path.length - 1]; + var node = this._nodeFromPath(aNode, path.slice(0, path.length - 2)); + + switch (json["type"]) { + case 0: + LOG(" -> creating a bookmark: '" + json["title"] + "' -> " + json["uri"]); + this._bms.insertBookmark(node.itemId, makeURI(json["uri"]), index, json["title"]); + break; + case 6: + LOG(" -> creating a folder: '" + json["title"] + "'"); + this._bms.createFolder(node.itemId, json["title"], index); + break; + case 7: + LOG(" -> creating a separator"); + this._bms.insertSeparator(node.itemId, index); + break; + default: + LOG("createCommand: Unknown item type: " + json["type"]); + break; + } + }, + + _removeCommand: function BSS__removeCommand(node, command) { + if (command["path"].length == 0) { + LOG("removing item"); + switch (node.type) { + case node.RESULT_TYPE_URI: + // FIXME: check it's an actual bookmark? + this._bms.removeItem(node.itemId); + break; + case node.RESULT_TYPE_FOLDER: + this._bms.removeFolder(node.itemId); + break; + case node.RESULT_TYPE_SEPARATOR: + this._bms.removeItem(node.itemId); + break; + default: + LOG("removeCommand: Unknown item type: " + node.type); + break; + } + } else if (command["path"].shift() == "children") { + if (command["path"].length == 0) { + LOG("invalid command?"); + return; + } + + var index = command["path"].shift(); + + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + var openState = node.containerOpen; + node.containerOpen = true; + this._removeCommand(node.getChild(index), command); + node.containerOpen = openState; + } + }, + + _editCommand: function BSS__editCommand(node, command) { + switch (command["path"].shift()) { + case "children": + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + var openState = node.containerOpen; + node.containerOpen = true; + this._editCommand(node.getChild(command["path"].shift()), command); + node.containerOpen = openState; + break; + case "type": + LOG("Can't change item type!"); // FIXME: is this valid? + break; + case "title": + this._bms.setItemTitle(node.itemId, command["value"]); + break; + case "uri": + this._bms.changeBookmarkURI(node.itemId, makeURI(command["value"])); + break; + } + }, + + // FIXME - hack to make sure we have Commands, not just eval'ed hashes + _sanitizeCommands: function BSS__sanitizeCommands(hashes) { + var commands = []; + for (var i = 0; i < hashes.length; i++) { + commands.push(new this._sync.Command(hashes[i]["action"], + hashes[i]["path"], + hashes[i]["value"])); + } + return commands; + }, + + _getLocalBookmarks: function BMS__getLocalBookmarks() { + var query = this._hsvc.getNewQuery(); + query.setFolders([this._bms.bookmarksRoot], 1); + return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + }, + + // FIXME: temp version here because we can't yet get to PlacesUtils.wrapNode + _wrapNode: function BSS__wrapNode(node) { + //var guid = this._bms.getItemGuid(node.itemId); + var item = {"type": node.type}; //, + // "guid": guid}; + + if (node.type == node.RESULT_TYPE_FOLDER) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + var openState = node.containerOpen; + node.containerOpen = true; + var children = []; + for (var i = 0; i < node.childCount; i++) { + children.push(this._wrapNode(node.getChild(i))); + } + item["children"] = children; + item["title"] = node.title; + node.containerOpen = openState; + } else if (node.type == node.RESULT_TYPE_SEPARATOR) { + } else if (node.type == node.RESULT_TYPE_URI) { + // FIXME: need to verify that it's a bookmark, it could be a history result! + item["title"] = node.title; + item["uri"] = node.uri; + } else { + // what do we do? + } + + return item; + }, + + // 1) Fetch server deltas + // 1.1) Construct current server status from snapshot + server deltas + // 1.2) Generate single delta from snapshot -> current server status + // 2) Generate local deltas from snapshot -> current client status + // 3) Reconcile client/server deltas and generate new deltas for them. + // 3.1) Apply local delta with server changes + // 3.2) Append server delta to the delta file and upload + + _doSync: function BSS__doSync() { + var generator = yield; + var handlers = this._handlersForGenerator(generator); + + LOG("Beginning sync"); + + try { + //this._dav.lock(handlers); + //var data = yield; + var data; + + var localBookmarks = this._getLocalBookmarks(); + var localJson = this._wrapNode(localBookmarks); + + // 1) Fetch server deltas + asyncRun(bind2(this, this._getServerData), handlers['complete'], localJson); + var server = yield; + + if (server['status'] == 2) { + LOG("Sync complete"); + return; + } else if (server['status'] != 0 && server['status'] != 1) { + LOG("Sync error"); + return; + } + + // 2) Generate local deltas from snapshot -> current client status + LOG("Generating local updates"); + var localUpdates = this._sanitizeCommands(this._sync.detectUpdates(this._snapshot, localJson)); + + // 3) Reconcile client/server deltas and generate new deltas for them. + + if (!(server['status'] == 1 || localUpdates.length > 0)) { + LOG("Sync complete: no changes needed on client or server"); + return; + } + + var propagations = [server['updates'], localUpdates]; + + if (server['status'] == 1 && localUpdates.length > 0) { + LOG("Reconciling updates"); + var propagations = this._sync.reconcile([localUpdates, server['updates']]); + } + LOG("Local:" + uneval(propagations[0])); + LOG("To server:" + uneval(propagations[1])); + + LOG("Local snapshot version: " + this._snapshotVersion); + LOG("Latest server version: " + server['version']); + this._snapshotVersion = server['version']; + + if (!(propagations[0].length || propagations[1].length)) { + this._snapshot = this._wrapNode(localBookmarks); + LOG("Sync complete: no changes needed on client or server"); + return; + } + + // 3.1) Apply server changes to local store + if (propagations[0].length) { + LOG("Applying changes locally"); + localBookmarks = this._getLocalBookmarks(); // fixme: wtf + this._snapshot = this._wrapNode(localBookmarks); + // applyCommands changes the imput commands, so we eval(uneval()) them to make a copy :-/ + this._sync.applyCommands(this._snapshot, eval(uneval(propagations[0]))); + this._applyCommands(localBookmarks, propagations[0]); + this._snapshot = this._wrapNode(localBookmarks); + } + + // 3.2) Append server delta to the delta file and upload + if (propagations[1].length) { + LOG("Uploading changes to server"); + this._snapshotVersion++; + server['deltas'][this._snapshotVersion] = propagations[1]; + this._dav.PUT("bookmarks.delta", uneval(server['deltas']), handlers); + data = yield; + + if (data.target.status >= 200 || data.target.status < 300) + LOG("Successfully updated deltas on server"); + else + LOG("Error: could not update deltas on server"); + } + + LOG("Sync complete"); + } finally { + //this._dav.unlock(handlers); + //data = yield; + } + }, + + + /* Get the deltas/combined updates from the server + * Returns: + * status: + * -1: error + * 0: no changes from server + * 1: ok + * 2: ok, initial sync + * version: + * the latest version on the server + * deltas: + * the individual deltas on the server + * updates: + * the relevant deltas (from our snapshot version to current), + * combined into a single set. + */ + _getServerData: function BSS__getServerData(onComplete, localJson) { + var generator = yield; + var handlers = this._handlersForGenerator(generator); + + var ret = {status: -1, version: -1, deltas: null, updates: null}; + + LOG("Getting bookmarks delta from server"); + this._dav.GET("bookmarks.delta", handlers); + var data = yield; + + switch (data.target.status) { + case 200: + LOG("Got bookmarks delta from server"); + + ret.deltas = eval(data.target.responseText); + var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack + + if (ret.deltas[this._snapshotVersion + 1]) { + // Merge the matching deltas into one, find highest version + var keys = []; + for (var v in ret.deltas) { + if (v > this._snapshotVersion) + keys.push(v); + if (v > ret.version) + ret.version = v; + } + keys = keys.sort(); + for (var i = 0; i < keys.length; i++) { + this._sync.applyCommands(tmp, this._sanitizeCommands(ret.deltas[keys[i]])); + } + ret.status = 1; + ret.updates = this._sync.detectUpdates(this._snapshot, tmp); + + } else if (ret.deltas[this._snapshotVersion]) { + LOG("No changes from server"); + ret.status = 0; + ret.version = this._snapshotVersion; + ret.updates = []; + + } else { + LOG("Server delta can't update from our snapshot version, getting full file"); + // generate updates from full local->remote snapshot diff + asyncRun(bind2(this, this._getServerUpdatesFull), handlers['complete'], localJson); + data = yield; + if (data.status == 2) { + // we have a delta file but no snapshot on the server. bad. + // fixme? + LOG("Error: Delta file on server, but snapshot file missing. New snapshot uploaded, may be inconsistent with deltas!"); + } + + var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack + this._sync.applyCommands(tmp, this._sanitizeCommands(data.updates)); + + // fixme: this is duplicated from above, need to do some refactoring + + var keys = []; + for (var v in ret.deltas) { + if (v > this._snapshotVersion) + keys.push(v); + if (v > ret.version) + ret.version = v; + } + keys = keys.sort(); + for (var i = 0; i < keys.length; i++) { + this._sync.applyCommands(tmp, this._sanitizeCommands(ret.deltas[keys[i]])); + } + + ret.status = data.status; + ret.updates = this._sync.detectUpdates(this._snapshot, tmp); + ret.version = data.version; + var keys = []; + for (var v in ret.deltas) { + if (v > ret.version) + ret.version = v; + } + } + break; + case 404: + LOG("Server has no delta file. Getting full bookmarks file from server"); + // generate updates from full local->remote snapshot diff + asyncRun(bind2(this, this._getServerUpdatesFull), handlers['complete'], localJson); + ret = yield; + ret.deltas = {}; + break; + default: + LOG("Could not get bookmarks.delta: unknown HTTP status code " + data.target.status); + break; + } + onComplete(ret); + }, + + _getServerUpdatesFull: function BSS__getServerUpdatesFull(onComplete, localJson) { + var generator = yield; + var handlers = this._handlersForGenerator(generator); + + var ret = {status: -1, version: -1, updates: null}; + + this._dav.GET("bookmarks.json", handlers); + data = yield; + + switch (data.target.status) { + case 200: + LOG("Got full bookmarks file from server"); + var tmp = eval(data.target.responseText); + ret.status = 1; + ret.updates = this._sync.detectUpdates(this._snapshot, tmp.snapshot); + ret.version = tmp.version; + break; + case 404: + LOG("No bookmarks on server. Starting initial sync to server"); + + this._snapshot = localJson; + this._snapshotVersion = 1; + this._dav.PUT("bookmarks.json", uneval({version: 1, snapshot: this._snapshot}), handlers); + data = yield; + + if (data.target.status >= 200 || data.target.status < 300) { + LOG("Initial sync to server successful"); + ret.status = 2; + } else { + LOG("Initial sync to server failed"); + } + break; + default: + LOG("Could not get bookmarks.json: unknown HTTP status code " + data.target.status); + break; + } + onComplete(ret); + }, + + _handlersForGenerator: function BSS__handlersForGenerator(generator) { + var h = {load: bind2(this, function(event) { handleEvent(generator, event); }), + error: bind2(this, function(event) { LOG("Request failed: " + uneval(event)); })}; + h['complete'] = h['load']; + return h; + }, + + // Interfaces this component implements. + interfaces: [Ci.nsIBookmarksSyncService, Ci.nsISupports], + + // nsISupports + + // XPCOM registration + classDescription: "Bookmarks Sync Service", + contractID: "@mozilla.org/places/sync-service;1", + classID: Components.ID("{6efd73bf-6a5a-404f-9246-f70a1286a3d6}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBookmarksSyncService, Ci.nsISupports]), + + // nsIBookmarksSyncService + + sync: function BSS_sync() { asyncRun(bind2(this, this._doSync)); } +}; + +function asyncRun(func, handler, data) { + var generator = func(handler, data); + generator.next(); + generator.send(generator); +} + +function handleEvent(generator, data) { + try { generator.send(data); } + catch (e) { + if (e instanceof StopIteration) + generator = null; + else + throw e; + } +} + +function EventListener(handler) { + this._handler = handler; +} +EventListener.prototype = { + handleEvent: function EL_handleEvent(event) { + this._handler(event); + } +}; + +function DAVCollection(baseUrl) { + this._baseUrl = baseUrl; +} +DAVCollection.prototype = { + _addHandler: function DC__addHandler(request, handlers, eventName) { + if (handlers[eventName]) + request.addEventListener(eventName, new EventListener(handlers[eventName]), false); + }, + _makeRequest: function DC__makeRequest(op, path, handlers, headers) { + var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); + request = request.QueryInterface(Ci.nsIDOMEventTarget); + + if (!handlers) + handlers = {}; + this._addHandler(request, handlers, "load"); + this._addHandler(request, handlers, "error"); + + request = request.QueryInterface(Ci.nsIXMLHttpRequest); + request.open(op, this._baseUrl + path, true); + + if (headers) { + for (var key in headers) { + request.setRequestHeader(key, headers[key]); + } + } + + return request; + }, + GET: function DC_GET(path, handlers, headers) { + if (!headers) + headers = {'Content-type': 'text/plain'}; + var request = this._makeRequest("GET", path, handlers, headers); + request.send(null); + }, + PUT: function DC_PUT(path, data, handlers, headers) { + if (!headers) + headers = {'Content-type': 'text/plain'}; + var request = this._makeRequest("PUT", path, handlers, headers); + request.send(data); + }, + _runLockHandler: function DC__runLockHandler(name, event) { + if (this._lockHandlers && this._lockHandlers[name]) + this._lockHandlers[name](event); + }, + // FIXME: make this function not reentrant + lock: function DC_lock(handlers) { + this._lockHandlers = handlers; + internalHandlers = {load: bind2(this, this._onLock), + error: bind2(this, this._onLockError)}; + headers = {'Content-Type': 'text/xml; charset="utf-8"'}; + var request = this._makeRequest("LOCK", "", internalHandlers, headers); + request.send("\n" + + "\n" + + " \n" + + " \n" + + ""); + }, + _onLock: function DC__onLock(event) { + LOG("acquired lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); + this._token = "woo"; + this._runLockHandler("load", event); + }, + _onLockError: function DC__onLockError(event) { + LOG("lock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); + this._runLockHandler("error", event); + }, + // FIXME: make this function not reentrant + unlock: function DC_unlock(handlers) { + this._lockHandlers = handlers; + internalHandlers = {load: bind2(this, this._onUnlock), + error: bind2(this, this._onUnlockError)}; + headers = {'Lock-Token': "<" + this._token + ">"}; + var request = this._makeRequest("UNLOCK", "", internalHandlers, headers); + request.send(null); + }, + _onUnlock: function DC__onUnlock(event) { + LOG("removed lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); + this._token = null; + this._runLockHandler("load", event); + }, + _onUnlockError: function DC__onUnlockError(event) { + LOG("unlock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); + this._runLockHandler("error", event); + }, +}; + +function makeFile(path) { + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return file; +} + +function makeURI(uriString) { + var ioservice = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return ioservice.newURI(uriString, null, null); +} + +function bind2(object, method) { + return function innerBind() { return method.apply(object, arguments); } +} + +function LOG(aText) { + dump(aText + "\n"); + var consoleService = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + consoleService.logStringMessage(aText); +} + +function NSGetModule(compMgr, fileSpec) { + return XPCOMUtils.generateModule([BookmarksSyncService]); +} diff --git a/services/sync/nsIBookmarksSyncService.idl b/services/sync/nsIBookmarksSyncService.idl new file mode 100644 index 000000000000..f7ad26c2a606 --- /dev/null +++ b/services/sync/nsIBookmarksSyncService.idl @@ -0,0 +1,45 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Places. + * + * The Initial Developer of the Original Code is + * Mozilla Corp. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsISupports.idl" + +[scriptable, uuid(1f00216a-4d2d-40e8-b4c5-afa3338a2d6c)] +interface nsIBookmarksSyncService : nsISupports +{ + void sync(); +}; diff --git a/services/sync/xptgen b/services/sync/xptgen new file mode 100755 index 000000000000..84eba2b6ee4b --- /dev/null +++ b/services/sync/xptgen @@ -0,0 +1,2 @@ +#!/bin/bash +../../mozilla-trunk/obj/firefox/dist/bin/xpidl -m typelib -I ../../mozilla-trunk/mozilla/xpcom/base/ -o nsIBookmarksSyncService nsIBookmarksSyncService.idl From c7bed26b2a2194fed8c282dc97f5af2ee1910a21 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 12 Sep 2007 17:12:33 -0400 Subject: [PATCH 0002/1860] update from chris' work --- services/sync/nsBookmarksSyncService.js | 42 +++++++++++-------------- services/sync/services-sync.js | 4 +++ 2 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 services/sync/services-sync.js diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index ff581015f4f2..b955c9b59b4d 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -302,49 +302,46 @@ BookmarksSyncService.prototype = { return; } + LOG("Local snapshot version: " + this._snapshotVersion); + LOG("Latest server version: " + server['version']); + // 2) Generate local deltas from snapshot -> current client status LOG("Generating local updates"); var localUpdates = this._sanitizeCommands(this._sync.detectUpdates(this._snapshot, localJson)); - - // 3) Reconcile client/server deltas and generate new deltas for them. - if (!(server['status'] == 1 || localUpdates.length > 0)) { - LOG("Sync complete: no changes needed on client or server"); + LOG("Sync complete (1): no changes needed on client or server"); return; } - + + // 3) Reconcile client/server deltas and generate new deltas for them. var propagations = [server['updates'], localUpdates]; - if (server['status'] == 1 && localUpdates.length > 0) { LOG("Reconciling updates"); - var propagations = this._sync.reconcile([localUpdates, server['updates']]); + propagations = this._sync.reconcile([localUpdates, server['updates']]); } - LOG("Local:" + uneval(propagations[0])); - LOG("To server:" + uneval(propagations[1])); - - LOG("Local snapshot version: " + this._snapshotVersion); - LOG("Latest server version: " + server['version']); - this._snapshotVersion = server['version']; - - if (!(propagations[0].length || propagations[1].length)) { + + if (!((propagations[0] && propagations[0].length > 0) || (propagations[1] && propagations[1].length > 0))) { this._snapshot = this._wrapNode(localBookmarks); - LOG("Sync complete: no changes needed on client or server"); + LOG("Sync complete (2): no changes needed on client or server"); return; - } + } + + this._snapshotVersion = server['version']; // 3.1) Apply server changes to local store - if (propagations[0].length) { + if (propagations[0] && propagations[0].length > 0) { LOG("Applying changes locally"); localBookmarks = this._getLocalBookmarks(); // fixme: wtf this._snapshot = this._wrapNode(localBookmarks); - // applyCommands changes the imput commands, so we eval(uneval()) them to make a copy :-/ - this._sync.applyCommands(this._snapshot, eval(uneval(propagations[0]))); + // applyCommands changes the input commands, so we eval(uneval()) them to make a copy :-/ + this._sync.applyCommands(this._snapshot, eval(uneval(propagations[0]))); this._applyCommands(localBookmarks, propagations[0]); this._snapshot = this._wrapNode(localBookmarks); + this._snapshotVersion = server['version']; } // 3.2) Append server delta to the delta file and upload - if (propagations[1].length) { + if (propagations[1] && propagations[1].length) { LOG("Uploading changes to server"); this._snapshotVersion++; server['deltas'][this._snapshotVersion] = propagations[1]; @@ -356,8 +353,7 @@ BookmarksSyncService.prototype = { else LOG("Error: could not update deltas on server"); } - - LOG("Sync complete"); + LOG("Sync complete"); } finally { //this._dav.unlock(handlers); //data = yield; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js new file mode 100644 index 000000000000..aaa6ec22181e --- /dev/null +++ b/services/sync/services-sync.js @@ -0,0 +1,4 @@ +pref("browser.places.sync.serverUrl", "http://places.sync.url/"); +pref("extensions.sync.lastversion", "firstrun"); +pref("extensions.sync.lastsync", ""); + From ebe27d2b8e6ef526808edda4a6261c48ef89e012 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 14 Sep 2007 14:56:06 -0700 Subject: [PATCH 0003/1860] run xptgen from build script, use env vars from xptgen to get mozilla objdir/srcdir paths --- services/sync/xptgen | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/sync/xptgen b/services/sync/xptgen index 84eba2b6ee4b..033f63ce017b 100755 --- a/services/sync/xptgen +++ b/services/sync/xptgen @@ -1,2 +1,12 @@ #!/bin/bash -../../mozilla-trunk/obj/firefox/dist/bin/xpidl -m typelib -I ../../mozilla-trunk/mozilla/xpcom/base/ -o nsIBookmarksSyncService nsIBookmarksSyncService.idl + +if [[ ! -d $MOZOBJDIR ]]; then + echo "MOZOBJDIR environment variable must point to a Mozilla build obj tree (with xptgen)" + exit 1 +fi +if [[ ! -d $MOZSRCDIR ]]; then + echo "MOZSRCDIR environment variable must point to a Mozilla source tree" + exit 1 +fi + +$MOZOBJDIR/dist/bin/xpidl -m typelib -I $MOZSRCDIR/xpcom/base/ -o nsIBookmarksSyncService nsIBookmarksSyncService.idl From e1da419711aa2bdda7fec819a709364a5cc144d3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 18 Sep 2007 11:18:01 -0700 Subject: [PATCH 0004/1860] Return conflicts from sync engine; change bookmarks serialization to use a flat dictionary keyed by guid, instead of a deep structure. --- services/sync/nsBookmarksSyncService.js | 248 ++++++++++++++++-------- 1 file changed, 169 insertions(+), 79 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index b955c9b59b4d..a095423e6c82 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -97,22 +97,145 @@ BookmarksSyncService.prototype = { var jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. getService(Ci.mozIJSSubScriptLoader); jsLoader.loadSubScript("chrome://sync/content/sync-engine.js", this._sync); - jsLoader.loadSubScript("chrome://browser/content/places/utils.js", this._utils); }, - _applyCommands: function BSS__applyCommands(node, commandList) { + _wrapNode: function BSS__wrapNode(node) { + var items = {}; + this._wrapNodeInternal(node, items); + + // sanity check + var rootGuid = this._bms.getItemGUID(node.itemId); + for (var wanted in items) { + if (rootGuid == wanted) + continue; + var found = false; + for (var parent in items) { + if (items[parent].children && items[parent].children.indexOf(wanted) >= 0) { + found = true; + continue; + } + } + if (!found) { + LOG("wrapNode error: node has no parent (" + wanted + ")"); + } + } + + return items; + }, + + _wrapNodeInternal: function BSS__wrapNodeInternal(node, items) { + var guid = this._bms.getItemGUID(node.itemId); + var item = {"type": node.type}; + + if (node.type == node.RESULT_TYPE_FOLDER) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + var openState = node.containerOpen; + node.containerOpen = true; + var children = []; + for (var i = 0; i < node.childCount; i++) { + var child = node.getChild(i); + this._wrapNodeInternal(child, items); + children.push(this._bms.getItemGUID(child.itemId)); + } + item["children"] = children; + item["title"] = node.title; + node.containerOpen = openState; + } else if (node.type == node.RESULT_TYPE_SEPARATOR) { + } else if (node.type == node.RESULT_TYPE_URI) { + // FIXME: need to verify that it's a bookmark, it could be a history result! + item["title"] = node.title; + item["uri"] = node.uri; + } else { + // what do we do? + } + + items[guid] = item; + }, + + // find parent & index + // note that this._snapshot needs to be up-to-date! + _findItemParent: function BSS__findItemParent(itemGuid) { + var parent; + var index; + for (var item in this._snapshot) { + if (this._snapshot[item].children) { + index = this._snapshot[item].children.indexOf(itemGuid); + if (index >= 0) { + parent = item; + break; + } + } + } + return [parent, index]; + }, + + _combineCommands: function BSS__combineCommands(commandList) { + var newList = []; + var lastObj; + if (newList.length) + lastObj = newList[newList.length - 1]; + + for (var i = 0; i < commandList.length; i++) { + LOG("Command: " + uneval(commandList[i]) + "\n"); + var action = commandList[i].action; + var path = commandList[i].path; + var guid = path.shift(); + + // Note: this only works when the commands to be collapsed are + // contiguous in the array (this is ok right?) + if ((action == "create" || action == "remove") && + (!newList.length || + (lastObj && lastObj.guid != guid && lastObj.action != action))) { + // Avoid the commands that edit the parent's children property + if (path.length != 1) + continue; + + let [parent, index] = this._findItemParent(guid); + if (!parent) { + LOG("Warning: item has no parent!\n"); + continue; + } + + newList.push({action: action, + guid: guid, + parentGuid: parent, + index: index, + data: this._snapshot[guid]}); + + } else if (action == "edit") { + // FIXME: will we never edit anything deeper? + if (path.length != 1) { + LOG("Warning: editing deep property - dropping"); + continue; + } + + if (!newList.length || + (lastObj && lastObj.guid != guid && lastObj.action != action)) + newList.push({action: action, guid: guid}); + + var key = path[path.length - 1]; + var value = this._snapshot[guid][key]; + newList[newList.length - 1].data[key] = value; + } + } + LOG("Combined list:\n"); + LOG(uneval(newList) + "\n"); + return newList; + }, + + _applyCommands: function BSS__applyCommands(commandList) { for (var i = 0; i < commandList.length; i++) { var command = commandList[i]; LOG("Processing command: " + uneval(command)); switch (command["action"]) { case "create": - this._createCommand(node, this._snapshot, command); + this._createCommand(command); break; case "remove": - this._removeCommand(node, command); + this._removeCommand(command); break; case "edit": - this._editCommand(node, command); + this._editCommand(command); break; default: LOG("unknown action in command: " + command["action"]); @@ -121,53 +244,38 @@ BookmarksSyncService.prototype = { } }, - _nodeFromPath: function BSS__nodeFromPath (aNodeRoot, aPath) { - var node = aNodeRoot; - for (var i = 0; i < aPath.length; i = i + 2) { - if (aPath[i] != "children") - break; - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - var openState = node.containerOpen; - node.containerOpen = true; - node = node.getChild(aPath[i + 1]); - //node.containerOpen = openState; // fixme? - } - return node; - }, - - _createCommand: function BSS__createCommand(aNode, aJsonNode, aCommand) { - var path = aCommand["path"]; - if (path[path.length - 2] != "children") - path = aCommand["path"].slice(0, path.length - 1); - - var json = this._sync.pathToReference(aJsonNode, path); - if (json["_done"] == true) - return; - json["_done"] = true; - - var index = path[path.length - 1]; - var node = this._nodeFromPath(aNode, path.slice(0, path.length - 2)); - - switch (json["type"]) { + _createCommand: function BSS__createCommand(command) { + var newId; + var parentId = this._bms.getItemIdForGUID(command.parentGuid); + switch (command.data.type) { case 0: - LOG(" -> creating a bookmark: '" + json["title"] + "' -> " + json["uri"]); - this._bms.insertBookmark(node.itemId, makeURI(json["uri"]), index, json["title"]); + LOG(" -> creating a bookmark: '" + command.data.title + + "' -> " + command.data.uri); + newId = this._bms.insertBookmark(parentId, + makeURI(command.data.uri), + command.index, + command.data.title); break; case 6: - LOG(" -> creating a folder: '" + json["title"] + "'"); - this._bms.createFolder(node.itemId, json["title"], index); + LOG(" -> creating a folder: '" + command.data.title + "'"); + newId = this._bms.createFolder(parentId, + command.data.title, + command.index); break; case 7: LOG(" -> creating a separator"); - this._bms.insertSeparator(node.itemId, index); + newId = this._bms.insertSeparator(parentId, command.index); break; default: - LOG("createCommand: Unknown item type: " + json["type"]); + LOG("createCommand: Unknown item type: " + command.data.type); break; } + if (newId) + this._bms.setItemGUID(newId, command.guid); }, - _removeCommand: function BSS__removeCommand(node, command) { + _removeCommand: function BSS__removeCommand(command) { + var iid = this._bss.getItemIdForGUID(aCommand.guid); if (command["path"].length == 0) { LOG("removing item"); switch (node.type) { @@ -239,35 +347,6 @@ BookmarksSyncService.prototype = { return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; }, - // FIXME: temp version here because we can't yet get to PlacesUtils.wrapNode - _wrapNode: function BSS__wrapNode(node) { - //var guid = this._bms.getItemGuid(node.itemId); - var item = {"type": node.type}; //, - // "guid": guid}; - - if (node.type == node.RESULT_TYPE_FOLDER) { - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - var openState = node.containerOpen; - node.containerOpen = true; - var children = []; - for (var i = 0; i < node.childCount; i++) { - children.push(this._wrapNode(node.getChild(i))); - } - item["children"] = children; - item["title"] = node.title; - node.containerOpen = openState; - } else if (node.type == node.RESULT_TYPE_SEPARATOR) { - } else if (node.type == node.RESULT_TYPE_URI) { - // FIXME: need to verify that it's a bookmark, it could be a history result! - item["title"] = node.title; - item["uri"] = node.uri; - } else { - // what do we do? - } - - return item; - }, - // 1) Fetch server deltas // 1.1) Construct current server status from snapshot + server deltas // 1.2) Generate single delta from snapshot -> current server status @@ -306,6 +385,7 @@ BookmarksSyncService.prototype = { LOG("Latest server version: " + server['version']); // 2) Generate local deltas from snapshot -> current client status + LOG("Generating local updates"); var localUpdates = this._sanitizeCommands(this._sync.detectUpdates(this._snapshot, localJson)); if (!(server['status'] == 1 || localUpdates.length > 0)) { @@ -314,35 +394,45 @@ BookmarksSyncService.prototype = { } // 3) Reconcile client/server deltas and generate new deltas for them. + var propagations = [server['updates'], localUpdates]; + var conflicts; + if (server['status'] == 1 && localUpdates.length > 0) { LOG("Reconciling updates"); - propagations = this._sync.reconcile([localUpdates, server['updates']]); + var ret = this._sync.reconcile([localUpdates, server['updates']]); + propagations = ret.propagations; + conflicts = ret.conflicts; } + + LOG("\n" + uneval(conflicts) + "\n"); - if (!((propagations[0] && propagations[0].length > 0) || (propagations[1] && propagations[1].length > 0))) { + this._snapshotVersion = server['version']; + + LOG(uneval(propagations)); + if (!((propagations[0] && propagations[0].length > 0) || + (propagations[1] && propagations[1].length > 0))) { this._snapshot = this._wrapNode(localBookmarks); LOG("Sync complete (2): no changes needed on client or server"); return; - } - - this._snapshotVersion = server['version']; + } // 3.1) Apply server changes to local store if (propagations[0] && propagations[0].length > 0) { LOG("Applying changes locally"); localBookmarks = this._getLocalBookmarks(); // fixme: wtf this._snapshot = this._wrapNode(localBookmarks); - // applyCommands changes the input commands, so we eval(uneval()) them to make a copy :-/ - this._sync.applyCommands(this._snapshot, eval(uneval(propagations[0]))); - this._applyCommands(localBookmarks, propagations[0]); + // Note: propagations[0] is changed by applyCommands, so we make a deep copy + this._sync.applyCommands(this._snapshot, eval(uneval(propagations[0]))); + var combinedCommands = this._combineCommands(propagations[0]); + this._applyCommands(combinedCommands); this._snapshot = this._wrapNode(localBookmarks); - this._snapshotVersion = server['version']; } // 3.2) Append server delta to the delta file and upload if (propagations[1] && propagations[1].length) { LOG("Uploading changes to server"); + this._snapshot = this._wrapNode(localBookmarks); this._snapshotVersion++; server['deltas'][this._snapshotVersion] = propagations[1]; this._dav.PUT("bookmarks.delta", uneval(server['deltas']), handlers); @@ -353,7 +443,7 @@ BookmarksSyncService.prototype = { else LOG("Error: could not update deltas on server"); } - LOG("Sync complete"); + LOG("Sync complete"); } finally { //this._dav.unlock(handlers); //data = yield; From eaeacaef66c6f86e7631bbc49a4e18a7dfb3eda1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 18 Sep 2007 16:00:52 -0700 Subject: [PATCH 0005/1860] Sprinkle the awesome (we correctly create items in the correct place (index) now) --- services/sync/nsBookmarksSyncService.js | 149 ++++++++---------------- 1 file changed, 50 insertions(+), 99 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index a095423e6c82..217117b12e94 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -101,50 +101,30 @@ BookmarksSyncService.prototype = { _wrapNode: function BSS__wrapNode(node) { var items = {}; - this._wrapNodeInternal(node, items); - - // sanity check - var rootGuid = this._bms.getItemGUID(node.itemId); - for (var wanted in items) { - if (rootGuid == wanted) - continue; - var found = false; - for (var parent in items) { - if (items[parent].children && items[parent].children.indexOf(wanted) >= 0) { - found = true; - continue; - } - } - if (!found) { - LOG("wrapNode error: node has no parent (" + wanted + ")"); - } - } - + this._wrapNodeInternal(node, items, null, null); return items; }, - _wrapNodeInternal: function BSS__wrapNodeInternal(node, items) { + _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGuid, index) { var guid = this._bms.getItemGUID(node.itemId); - var item = {"type": node.type}; + var item = {type: node.type, + parentGuid: parentGuid, + index: index}; if (node.type == node.RESULT_TYPE_FOLDER) { + item.title = node.title; + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - var openState = node.containerOpen; node.containerOpen = true; - var children = []; + for (var i = 0; i < node.childCount; i++) { - var child = node.getChild(i); - this._wrapNodeInternal(child, items); - children.push(this._bms.getItemGUID(child.itemId)); + this._wrapNodeInternal(node.getChild(i), items, guid, i); } - item["children"] = children; - item["title"] = node.title; - node.containerOpen = openState; } else if (node.type == node.RESULT_TYPE_SEPARATOR) { } else if (node.type == node.RESULT_TYPE_URI) { // FIXME: need to verify that it's a bookmark, it could be a history result! - item["title"] = node.title; - item["uri"] = node.uri; + item.title = node.title; + item.uri = node.uri; } else { // what do we do? } @@ -152,74 +132,38 @@ BookmarksSyncService.prototype = { items[guid] = item; }, - // find parent & index - // note that this._snapshot needs to be up-to-date! - _findItemParent: function BSS__findItemParent(itemGuid) { - var parent; - var index; - for (var item in this._snapshot) { - if (this._snapshot[item].children) { - index = this._snapshot[item].children.indexOf(itemGuid); - if (index >= 0) { - parent = item; - break; - } - } - } - return [parent, index]; - }, - _combineCommands: function BSS__combineCommands(commandList) { var newList = []; - var lastObj; - if (newList.length) - lastObj = newList[newList.length - 1]; for (var i = 0; i < commandList.length; i++) { - LOG("Command: " + uneval(commandList[i]) + "\n"); + LOG("Combining command: " + uneval(commandList[i])); + var action = commandList[i].action; + var value = commandList[i].value; var path = commandList[i].path; + + // ignore commands about creating the item container itself + if (path.length <= 1) + continue; + var guid = path.shift(); + var key = path.pop(); - // Note: this only works when the commands to be collapsed are - // contiguous in the array (this is ok right?) - if ((action == "create" || action == "remove") && - (!newList.length || - (lastObj && lastObj.guid != guid && lastObj.action != action))) { - // Avoid the commands that edit the parent's children property - if (path.length != 1) - continue; - - let [parent, index] = this._findItemParent(guid); - if (!parent) { - LOG("Warning: item has no parent!\n"); - continue; - } - - newList.push({action: action, - guid: guid, - parentGuid: parent, - index: index, - data: this._snapshot[guid]}); - - } else if (action == "edit") { - // FIXME: will we never edit anything deeper? - if (path.length != 1) { - LOG("Warning: editing deep property - dropping"); - continue; - } - - if (!newList.length || - (lastObj && lastObj.guid != guid && lastObj.action != action)) - newList.push({action: action, guid: guid}); - - var key = path[path.length - 1]; - var value = this._snapshot[guid][key]; - newList[newList.length - 1].data[key] = value; + if (path.length) { + LOG("Warning: editing deep property - dropping"); + continue; } + + if (!newList.length || + newList[newList.length - 1].guid != guid || + newList[newList.length - 1].action != action) { + newList.push({action: action, guid: guid, data: {}}); + } + + newList[newList.length - 1].data[key] = value; } - LOG("Combined list:\n"); - LOG(uneval(newList) + "\n"); + + LOG("Combined list: " + uneval(newList) + "\n"); return newList; }, @@ -246,32 +190,32 @@ BookmarksSyncService.prototype = { _createCommand: function BSS__createCommand(command) { var newId; - var parentId = this._bms.getItemIdForGUID(command.parentGuid); + var parentId = this._bms.getItemIdForGUID(command.data.parentGuid); switch (command.data.type) { case 0: LOG(" -> creating a bookmark: '" + command.data.title + "' -> " + command.data.uri); newId = this._bms.insertBookmark(parentId, makeURI(command.data.uri), - command.index, + command.data.index, command.data.title); break; case 6: LOG(" -> creating a folder: '" + command.data.title + "'"); newId = this._bms.createFolder(parentId, command.data.title, - command.index); + command.data.index); break; case 7: LOG(" -> creating a separator"); - newId = this._bms.insertSeparator(parentId, command.index); + newId = this._bms.insertSeparator(parentId, command.data.index); break; default: LOG("createCommand: Unknown item type: " + command.data.type); break; } if (newId) - this._bms.setItemGUID(newId, command.guid); + this._bms.setItemGUID(newId, command.data.guid); }, _removeCommand: function BSS__removeCommand(command) { @@ -405,20 +349,27 @@ BookmarksSyncService.prototype = { conflicts = ret.conflicts; } - LOG("\n" + uneval(conflicts) + "\n"); + LOG("Propagations: " + uneval(propagations) + "\n"); + LOG("Conflicts: " + uneval(conflicts) + "\n"); this._snapshotVersion = server['version']; - LOG(uneval(propagations)); - if (!((propagations[0] && propagations[0].length > 0) || - (propagations[1] && propagations[1].length > 0))) { + if (!((propagations[0] && propagations[0].length) || + (propagations[1] && propagations[1].length) || + (conflicts[0] && conflicts[0].length) || + (conflicts[1] && conflicts[1].length))) { this._snapshot = this._wrapNode(localBookmarks); LOG("Sync complete (2): no changes needed on client or server"); return; } + if ((conflicts[0] && conflicts[0].length) || + (conflicts[1] && conflicts[1].length)) { + LOG("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); + } + // 3.1) Apply server changes to local store - if (propagations[0] && propagations[0].length > 0) { + if (propagations[0] && propagations[0].length) { LOG("Applying changes locally"); localBookmarks = this._getLocalBookmarks(); // fixme: wtf this._snapshot = this._wrapNode(localBookmarks); From 2f3156d9e784c94495722dee41c5b6081868cc0c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Sep 2007 21:08:00 -0700 Subject: [PATCH 0006/1860] Add more awesome. Still some bugs, will try to replace sync-engine.js next (with a more domain-specific version) - beginnings of that already in the code (unused) --- services/sync/nsBookmarksSyncService.js | 295 ++++++++++++++++++------ 1 file changed, 227 insertions(+), 68 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 217117b12e94..46a719cfa6fb 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -132,6 +132,10 @@ BookmarksSyncService.prototype = { items[guid] = item; }, + // Takes a vanilla command list (fro sync-engine.js) and combines + // them so that each create/edit/remove fully creates/edits/removes + // an item (sync-engine.js will give us one command per object + // property, i.e., a bunch of commands per item) _combineCommands: function BSS__combineCommands(commandList) { var newList = []; @@ -156,17 +160,47 @@ BookmarksSyncService.prototype = { if (!newList.length || newList[newList.length - 1].guid != guid || - newList[newList.length - 1].action != action) { + newList[newList.length - 1].action != action) newList.push({action: action, guid: guid, data: {}}); - } newList[newList.length - 1].data[key] = value; } - - LOG("Combined list: " + uneval(newList) + "\n"); return newList; }, + _nodeDepth: function BSS__nodeDepth(guid, depth) { + let parent = this._snapshot[guid].parentGuid; + if (parent == null) + return depth; + return this._nodeDepth(parent, ++depth); + }, + + // Takes *combined* commands and sorts them so that parent folders + // get created first, then children in reverse index order, then + // removes. + // Note: this method uses this._snapshot to calculate node depths; + // so this._snapshot must have server commands applied to it before + // calling this method. + _sortCommands: function BSS__sortCommands(commandList) { + for (let i = 0; i < commandList.length; i++) { + commandList[i].depth = this._nodeDepth(commandList[i].guid, 0); + } + commandList.sort(function compareNodes(a, b) { + if (a.depth > b.depth) + return 1; + if (a.depth < b.depth) + return -1; + if (a.index > b.index) + return -1; + if (a.index < b.index) + return 1; + return 0; // should never happen, but not a big deal if it does + }); + return commandList; + }, + + // Takes *combined* and *sorted* commands and applies them to the + // places db _applyCommands: function BSS__applyCommands(commandList) { for (var i = 0; i < commandList.length; i++) { var command = commandList[i]; @@ -188,89 +222,190 @@ BookmarksSyncService.prototype = { } }, + _compareItems: function BSS__compareItems(node, data) { + switch (data.type) { + case 0: + if (node && + node.type == node.RESULT_TYPE_URI && + node.uri == data.uri && node.title == data.title) + return true; + return false; + case 6: + if (node && + node.type == node.RESULT_TYPE_FOLDER && + node.title == data.title) + return true; + return false; + case 7: + if (node && node.type == node.RESULT_TYPE_SEPARATOR) + return true; + return false; + default: + LOG("_compareItems: Unknown item type: " + data.type); + return false; + } + }, + + // FIXME: can't skip creations here; they need to get pruned out + // during reconciliation, sincethere will be "new" items being sent + // upstream too _createCommand: function BSS__createCommand(command) { - var newId; - var parentId = this._bms.getItemIdForGUID(command.data.parentGuid); + let newId; + + // check if it's the root + if (command.data.parentGuid == null) { + this._bms.setItemGUID(this._bms.bookmarksRoot, command.guid); + return; + } + + let parentId = this._bms.getItemIdForGUID(command.data.parentGuid); + if (parentId <= 0) { + LOG("Warning: creating node with unknown parent -> reparenting to root"); + parentId = this._bms.bookmarksRoot; + } + let parent = this._getBookmarks(parentId); + parent.QueryInterface(Ci.nsINavHistoryQueryResultNode); + parent.containerOpen = true; + + let curItem; + if (parent.childCount > command.data.index) + curItem = parent.getChild(command.data.index); + + if (this._compareItems(curItem, command.data)) { + LOG(" -> skipping item (already exists)"); + this._bms.setItemGUID(curItem.itemId, command.guid); + return; + } + + LOG(" -> creating item"); + switch (command.data.type) { case 0: - LOG(" -> creating a bookmark: '" + command.data.title + - "' -> " + command.data.uri); newId = this._bms.insertBookmark(parentId, makeURI(command.data.uri), command.data.index, command.data.title); break; case 6: - LOG(" -> creating a folder: '" + command.data.title + "'"); newId = this._bms.createFolder(parentId, command.data.title, command.data.index); break; case 7: - LOG(" -> creating a separator"); newId = this._bms.insertSeparator(parentId, command.data.index); break; default: - LOG("createCommand: Unknown item type: " + command.data.type); + LOG("_createCommand: Unknown item type: " + command.data.type); break; } if (newId) - this._bms.setItemGUID(newId, command.data.guid); + this._bms.setItemGUID(newId, command.guid); }, _removeCommand: function BSS__removeCommand(command) { - var iid = this._bss.getItemIdForGUID(aCommand.guid); - if (command["path"].length == 0) { - LOG("removing item"); - switch (node.type) { - case node.RESULT_TYPE_URI: - // FIXME: check it's an actual bookmark? - this._bms.removeItem(node.itemId); - break; - case node.RESULT_TYPE_FOLDER: - this._bms.removeFolder(node.itemId); - break; - case node.RESULT_TYPE_SEPARATOR: - this._bms.removeItem(node.itemId); - break; - default: - LOG("removeCommand: Unknown item type: " + node.type); - break; - } - } else if (command["path"].shift() == "children") { - if (command["path"].length == 0) { - LOG("invalid command?"); - return; - } + var itemId = this._bms.getItemIdForGUID(command.guid); + var type = this._bms.getItemType(itemId); - var index = command["path"].shift(); - - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - var openState = node.containerOpen; - node.containerOpen = true; - this._removeCommand(node.getChild(index), command); - node.containerOpen = openState; + switch (type) { + case this._bms.TYPE_BOOKMARK: + // FIXME: check it's an actual bookmark? + LOG(" -> removing bookmark " + command.guid); + this._bms.removeItem(itemId); + break; + case this._bms.TYPE_FOLDER: + LOG(" -> removing folder " + command.guid); + this._bms.removeFolder(itemId); + break; + case this._bms.TYPE_SEPARATOR: + LOG(" -> removing separator " + command.guid); + this._bms.removeItem(itemId); + break; + default: + LOG("removeCommand: Unknown item type: " + type); + break; } }, - _editCommand: function BSS__editCommand(node, command) { - switch (command["path"].shift()) { - case "children": - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - var openState = node.containerOpen; - node.containerOpen = true; - this._editCommand(node.getChild(command["path"].shift()), command); - node.containerOpen = openState; - break; - case "type": - LOG("Can't change item type!"); // FIXME: is this valid? - break; - case "title": - this._bms.setItemTitle(node.itemId, command["value"]); - break; - case "uri": - this._bms.changeBookmarkURI(node.itemId, makeURI(command["value"])); - break; + _editCommand: function BSS__editCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.guid); + var type = this._bms.getItemType(itemId); + + for (let key in command.data) { + switch (key) { + case "title": + this._bms.setItemTitle(itemId, command.data.title); + break; + case "uri": + this._bms.changeBookmarkURI(itemId, makeURI(command.data.uri)); + break; + case "index": + this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), + command.data.index); + break; + case "parentGuid": + this._bms.moveItem( + itemId, this._bms.getItemIdForGUID(command.data.parentGuid), -1); + break; + default: + LOG("Warning: Can't change item property: " + key); + break; + } + } + }, + + _getEdits: function BSS__getEdits(a, b) { + // check the type separately, just in case + if (a.type != b.type) + return -1; + + let ret = {}; + for (prop in a) { + if (a[prop] != b[prop]) + ret[prop] = b[prop]; + } + + // FIXME: prune out properties we don't care about + + return ret; + }, + + _detectUpdates: function BSS__detectUpdates(a, b) { + let cmds = []; + for (let guid in a) { + if (guid in b) { + let edits = this._getEdits(a[guid], b[guid]); + if (edits == -1) // something went very wrong -- FIXME + continue; + if (edits == {}) // no changes - skip + continue; + cmds.push({action: "edit", guid: guid, data: edits}); + } else { + cmds.push({action: "remove", guid: guid}); + } + } + for (let guid in b) { + if (guid in a) + continue; + cmds.push({action: "create", guid: guid, data: b[guid]}); + } + }, + + _reconcile: function BSS__reconcile(a, b) { + }, + + _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { + for (let i = 0; i < commands.length; i++) { + switch (command.action) { + case "create": + obj[command.guid] = eval(uneval(command.data)); + case "edit": + for (let prop in command.data) { + obj[command.guid][prop] = command.data[prop]; + } + case "remove": + delete obj[command.guid]; + break; + } } }, @@ -285,9 +420,11 @@ BookmarksSyncService.prototype = { return commands; }, - _getLocalBookmarks: function BMS__getLocalBookmarks() { + _getBookmarks: function BMS__getBookmarks(folder) { + if (!folder) + folder = this._bms.bookmarksRoot; var query = this._hsvc.getNewQuery(); - query.setFolders([this._bms.bookmarksRoot], 1); + query.setFolders([folder], 1); return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; }, @@ -310,7 +447,7 @@ BookmarksSyncService.prototype = { //var data = yield; var data; - var localBookmarks = this._getLocalBookmarks(); + var localBookmarks = this._getBookmarks(); var localJson = this._wrapNode(localBookmarks); // 1) Fetch server deltas @@ -356,26 +493,37 @@ BookmarksSyncService.prototype = { if (!((propagations[0] && propagations[0].length) || (propagations[1] && propagations[1].length) || - (conflicts[0] && conflicts[0].length) || - (conflicts[1] && conflicts[1].length))) { + (conflicts && + (conflicts[0] && conflicts[0].length) || + (conflicts[1] && conflicts[1].length)))) { this._snapshot = this._wrapNode(localBookmarks); LOG("Sync complete (2): no changes needed on client or server"); return; } - if ((conflicts[0] && conflicts[0].length) || - (conflicts[1] && conflicts[1].length)) { + if (conflicts && conflicts[0] && conflicts[0].length) { + //var combinedCommands = this._combineCommands(propagations[0]); LOG("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); + LOG("Conflicts(1) " + uneval(this._combineCommands(conflicts[0]))); + } + + if (conflicts && conflicts[1] && conflicts[1].length) { + //var combinedCommands = this._combineCommands(propagations[0]); + LOG("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); + LOG("Conflicts(2) " + uneval(this._combineCommands(conflicts[1]))); } // 3.1) Apply server changes to local store if (propagations[0] && propagations[0].length) { LOG("Applying changes locally"); - localBookmarks = this._getLocalBookmarks(); // fixme: wtf + localBookmarks = this._getBookmarks(); // fixme: wtf this._snapshot = this._wrapNode(localBookmarks); // Note: propagations[0] is changed by applyCommands, so we make a deep copy this._sync.applyCommands(this._snapshot, eval(uneval(propagations[0]))); var combinedCommands = this._combineCommands(propagations[0]); + LOG("Combined commands: " + uneval(combinedCommands) + "\n"); + var sortedCommands = this._sortCommands(combinedCommands); + LOG("Sorted commands: " + uneval(sortedCommands) + "\n"); this._applyCommands(combinedCommands); this._snapshot = this._wrapNode(localBookmarks); } @@ -434,6 +582,17 @@ BookmarksSyncService.prototype = { ret.deltas = eval(data.target.responseText); var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack + // FIXME: debug here for conditional below... + LOG("[sync bowels] local version: " + this._snapshotVersion); + for (var z in ret.deltas) { + LOG("[sync bowels] remote version: " + z); + } + LOG("foo: " + uneval(ret.deltas[this._snapshotVersion + 1])); + if (ret.deltas[this._snapshotVersion + 1]) + LOG("-> is true"); + else + LOG("-> is false"); + if (ret.deltas[this._snapshotVersion + 1]) { // Merge the matching deltas into one, find highest version var keys = []; From a4d023f57709a5ee37abd6e75f7ee91d4b0f53b0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Sep 2007 16:56:05 -0700 Subject: [PATCH 0007/1860] flesh out new sync engine - cleanup needed --- services/sync/nsBookmarksSyncService.js | 191 ++++++++++++++++++------ 1 file changed, 149 insertions(+), 42 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 46a719cfa6fb..037b34ff3a54 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -71,12 +71,6 @@ BookmarksSyncService.prototype = { // DAVCollection object _dav: null, - // sync.js - _sync: {}, - - // PlacesUtils - _utils: {}, - // Last synced tree // FIXME: this should be serialized to disk _snapshot: {}, @@ -93,10 +87,6 @@ BookmarksSyncService.prototype = { catch (ex) { /* use defaults */ } LOG("Bookmarks sync server: " + serverUrl); this._dav = new DAVCollection(serverUrl); - - var jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. - getService(Ci.mozIJSSubScriptLoader); - jsLoader.loadSubScript("chrome://sync/content/sync-engine.js", this._sync); }, _wrapNode: function BSS__wrapNode(node) { @@ -272,7 +262,7 @@ BookmarksSyncService.prototype = { curItem = parent.getChild(command.data.index); if (this._compareItems(curItem, command.data)) { - LOG(" -> skipping item (already exists)"); + LOG(" -> FIXME - skipping item (already exists)"); this._bms.setItemGUID(curItem.itemId, command.guid); return; } @@ -369,6 +359,17 @@ BookmarksSyncService.prototype = { return ret; }, + _nodeParents: function BSS__nodeParents(guid, tree) { + return this._nodeParentsInt(guid, tree, []); + }, + + _nodeParentsInt: function BSS__nodeParentsInt(guid, tree, parents) { + if (tree[guid].parentGuid == null) + return parents; + parents.push(tree[guid].parentGuid); + return this._nodeParentsInt(tree[guid].parentGuid, tree, parents); + }, + _detectUpdates: function BSS__detectUpdates(a, b) { let cmds = []; for (let guid in a) { @@ -378,19 +379,133 @@ BookmarksSyncService.prototype = { continue; if (edits == {}) // no changes - skip continue; - cmds.push({action: "edit", guid: guid, data: edits}); + let parents = this._nodeParents(guid, b); + cmds.push({action: "edit", guid: guid, + depth: parents.length, parents: parents, + data: edits}); } else { - cmds.push({action: "remove", guid: guid}); + let parents = this._nodeParents(guid, a); // ??? + cmds.push({action: "remove", guid: guid, + depth: parents.length, parents: parents}); } } for (let guid in b) { if (guid in a) continue; - cmds.push({action: "create", guid: guid, data: b[guid]}); + let parents = this._nodeParents(guid, b); + cmds.push({action: "create", guid: guid, + depth: parents.length, parents: parents, + data: b[guid]}); + } + return cmds; + }, + + _conflicts: function BSS__conflicts(a, b) { + if ((a.depth < b.depth) && + (b.parents.indexOf(a.guid) >= 0) && + a.action == "remove") + return true; + if ((a.guid == b.guid) && a != b) + return true; +// FIXME - how else can two commands conflict? + return false; + }, + + // NEED TO also look at the parent chain & index; only items in the + // same "spot" qualify for likeness + _commandLike: function BSS__commandLike(a, b) { + if (!a || !b) + return false; + + if (a.action != b.action) + return false; + + switch (a.data.type) { + case 0: + if (b.data.type == a.data.type && + b.data.uri == a.data.uri && + b.data.title == a.data.title) + return true; + return false; + case 6: + if (b.data.type == a.data.type && + b.data.title == a.data.title) + return true; + return false; + case 7: + // fixme: we need to enable this after we +// if (b.data.type == a.data.type) +// return true; + return false; + default: + LOG("_commandLike: Unknown item type: " + uneval(a)); + return false; } }, - _reconcile: function BSS__reconcile(a, b) { + _deepEquals: function BSS__commandEquals(a, b) { + if (!a && !b) + return true; + if (!a || !b) + return false; + + for (let key in a) { + if (typeof(a[key]) == "object") { + if (!typeof(b[key]) == "object") + return false; + if (!this._deepEquals(a[key], b[key])) + return false; + } else { + if (a[key] != b[key]) + return false; + } + } + return true; + }, + + _reconcile: function BSS__reconcile(listA, listB) { + let propagations = [[], []]; + let conflicts = [[], []]; + + for (let i = 0; i < listA.length; i++) { + for (let j = 0; j < listB.length; j++) { + if (this._commandLike(listA[i], listB[j]) || + this._deepEquals(listA[i], listB[j])) { + delete listA[i]; + delete listB[j]; + } + } + } + + listA = listA.filter(function(elt) { return elt }); + listB = listB.filter(function(elt) { return elt }); + + for (let i = 0; i < listA.length; i++) { + for (let j = 0; j < listB.length; j++) { + if (this._conflicts(listA[i], listB[j]) || + this._conflicts(listB[j], listA[i])) { + if (conflicts[0].some( + function(elt) { return elt.guid == listA[i].guid })) + conflicts[0].push(listA[i]); + if (conflicts[1].some( + function(elt) { return elt.guid == listB[j].guid })) + conflicts[1].push(listB[j]); + } + } + } + for (let i = 0; i < listA.length; i++) { + // need to check if a previous conflict might break this cmd + if (!conflicts[0].some( + function(elt) { return elt.guid == listA[i].guid })) + propagations[1].push(listA[i]); + } + for (let j = 0; j < listB.length; j++) { + // need to check if a previous conflict might break this cmd + if (!conflicts[1].some( + function(elt) { return elt.guid == listB[j].guid })) + propagations[0].push(listB[j]); + } + return {propagations: propagations, conflicts: conflicts}; }, _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { @@ -409,17 +524,6 @@ BookmarksSyncService.prototype = { } }, - // FIXME - hack to make sure we have Commands, not just eval'ed hashes - _sanitizeCommands: function BSS__sanitizeCommands(hashes) { - var commands = []; - for (var i = 0; i < hashes.length; i++) { - commands.push(new this._sync.Command(hashes[i]["action"], - hashes[i]["path"], - hashes[i]["value"])); - } - return commands; - }, - _getBookmarks: function BMS__getBookmarks(folder) { if (!folder) folder = this._bms.bookmarksRoot; @@ -449,11 +553,13 @@ BookmarksSyncService.prototype = { var localBookmarks = this._getBookmarks(); var localJson = this._wrapNode(localBookmarks); + LOG("local json: " + uneval(localJson)); // 1) Fetch server deltas asyncRun(bind2(this, this._getServerData), handlers['complete'], localJson); var server = yield; + LOG("server: " + uneval(server)); if (server['status'] == 2) { LOG("Sync complete"); return; @@ -468,7 +574,8 @@ BookmarksSyncService.prototype = { // 2) Generate local deltas from snapshot -> current client status LOG("Generating local updates"); - var localUpdates = this._sanitizeCommands(this._sync.detectUpdates(this._snapshot, localJson)); + var localUpdates = this._detectUpdates(this._snapshot, localJson); + LOG("updates: " + uneval(localUpdates)); if (!(server['status'] == 1 || localUpdates.length > 0)) { LOG("Sync complete (1): no changes needed on client or server"); return; @@ -481,7 +588,7 @@ BookmarksSyncService.prototype = { if (server['status'] == 1 && localUpdates.length > 0) { LOG("Reconciling updates"); - var ret = this._sync.reconcile([localUpdates, server['updates']]); + var ret = this._reconcile(localUpdates, server['updates']); propagations = ret.propagations; conflicts = ret.conflicts; } @@ -516,15 +623,15 @@ BookmarksSyncService.prototype = { // 3.1) Apply server changes to local store if (propagations[0] && propagations[0].length) { LOG("Applying changes locally"); - localBookmarks = this._getBookmarks(); // fixme: wtf + //localBookmarks = this._getBookmarks(); // fixme: wtf this._snapshot = this._wrapNode(localBookmarks); - // Note: propagations[0] is changed by applyCommands, so we make a deep copy - this._sync.applyCommands(this._snapshot, eval(uneval(propagations[0]))); - var combinedCommands = this._combineCommands(propagations[0]); - LOG("Combined commands: " + uneval(combinedCommands) + "\n"); - var sortedCommands = this._sortCommands(combinedCommands); - LOG("Sorted commands: " + uneval(sortedCommands) + "\n"); - this._applyCommands(combinedCommands); + this._applyCommandsToObj(this._snapshot, propagations[0]); + //var combinedCommands = this._combineCommands(propagations[0]); + //LOG("Combined commands: " + uneval(combinedCommands) + "\n"); + //var sortedCommands = this._sortCommands(combinedCommands); + //LOG("Sorted commands: " + uneval(sortedCommands) + "\n"); + //this._applyCommands(combinedCommands); + this._applyCommands(propagations[0]); this._snapshot = this._wrapNode(localBookmarks); } @@ -604,10 +711,10 @@ BookmarksSyncService.prototype = { } keys = keys.sort(); for (var i = 0; i < keys.length; i++) { - this._sync.applyCommands(tmp, this._sanitizeCommands(ret.deltas[keys[i]])); + this._applyCommandsToObj(tmp, ret.deltas[keys[i]]); } ret.status = 1; - ret.updates = this._sync.detectUpdates(this._snapshot, tmp); + ret.updates = this._detectUpdates(this._snapshot, tmp); } else if (ret.deltas[this._snapshotVersion]) { LOG("No changes from server"); @@ -627,7 +734,7 @@ BookmarksSyncService.prototype = { } var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack - this._sync.applyCommands(tmp, this._sanitizeCommands(data.updates)); + this._applyCommandsToObj(tmp, data.updates); // fixme: this is duplicated from above, need to do some refactoring @@ -640,11 +747,11 @@ BookmarksSyncService.prototype = { } keys = keys.sort(); for (var i = 0; i < keys.length; i++) { - this._sync.applyCommands(tmp, this._sanitizeCommands(ret.deltas[keys[i]])); + this._applyCommandsToObj(tmp, ret.deltas[keys[i]]); } ret.status = data.status; - ret.updates = this._sync.detectUpdates(this._snapshot, tmp); + ret.updates = this._detectUpdates(this._snapshot, tmp); ret.version = data.version; var keys = []; for (var v in ret.deltas) { @@ -681,7 +788,7 @@ BookmarksSyncService.prototype = { LOG("Got full bookmarks file from server"); var tmp = eval(data.target.responseText); ret.status = 1; - ret.updates = this._sync.detectUpdates(this._snapshot, tmp.snapshot); + ret.updates = this._detectUpdates(this._snapshot, tmp.snapshot); ret.version = tmp.version; break; case 404: From 8f3422515c84748340af2b55daa8a42b0cf8589f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Sep 2007 17:26:20 -0700 Subject: [PATCH 0008/1860] woo! first sync across profiles works now --- services/sync/nsBookmarksSyncService.js | 38 +++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 037b34ff3a54..56766cb7fc22 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -322,13 +322,16 @@ BookmarksSyncService.prototype = { for (let key in command.data) { switch (key) { + case "guid": + this._bms.setItemGUID(itemId, command.data.guid); + break; case "title": this._bms.setItemTitle(itemId, command.data.title); break; case "uri": this._bms.changeBookmarkURI(itemId, makeURI(command.data.uri)); break; - case "index": + case "index": // FIXME: what if we do this one before parentGuid ? that'd be wrong this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), command.data.index); break; @@ -397,6 +400,17 @@ BookmarksSyncService.prototype = { depth: parents.length, parents: parents, data: b[guid]}); } + cmds.sort(function(a, b) { + if (a.depth > b.depth) + return 1; + if (a.depth < b.depth) + return -1; + if (a.index > b.index) + return -1; + if (a.index < b.index) + return 1; + return 0; // should never happen, but not a big deal if it does + }); return cmds; }, @@ -463,16 +477,34 @@ BookmarksSyncService.prototype = { return true; }, + _fixParents: function BSS__fixParents(list, oldGuid, newGuid) { + for (let i = 0; i < list.length; i++) { + if (!list[i]) + continue; + if (list[i].parentGuid == oldGuid) + list[i].parentGuid = newGuid; + for (let j = 0; j < list[i].parents.length; j++) { + if (list[i].parents[j] == oldGuid) + list[i].parents[j] = newGuid; + } + } + }, + _reconcile: function BSS__reconcile(listA, listB) { let propagations = [[], []]; let conflicts = [[], []]; for (let i = 0; i < listA.length; i++) { for (let j = 0; j < listB.length; j++) { - if (this._commandLike(listA[i], listB[j]) || - this._deepEquals(listA[i], listB[j])) { + if (this._deepEquals(listA[i], listB[j])) { delete listA[i]; delete listB[j]; + } else if (this._commandLike(listA[i], listB[j])) { + this._fixParents(listA, listA[i].guid, listB[j].guid); + listB[j].data = {guid: listB[j].guid}; + listB[j].guid = listA[i].guid; + listB[j].action = "edit"; + delete listA[i]; } } } From 95c45d691edfd6d1b2294a1c0d6de7090bfa7383 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Sep 2007 23:57:18 -0700 Subject: [PATCH 0009/1860] sync.js: move code into an object. Add observer implementation, use observer service to listen to sync events. sync.xul: cleanup, point into the global sync object from sync.js. nsBookmarksSyncService.js: add login code, use observer service to publish events. nsIBookmarksSyncService.idl: add login methods, add comments. --- services/sync/nsBookmarksSyncService.js | 205 ++++++++++++++++++---- services/sync/nsIBookmarksSyncService.idl | 20 ++- 2 files changed, 187 insertions(+), 38 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 56766cb7fc22..026a73065d20 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -68,6 +68,14 @@ BookmarksSyncService.prototype = { return this.__ans; }, + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + // DAVCollection object _dav: null, @@ -76,6 +84,14 @@ BookmarksSyncService.prototype = { _snapshot: {}, _snapshotVersion: 0, + get currentUser() { + // FIXME - need to expose that info some other way + if (this._dav._currentUserPath) + return this._dav._currentUserPath + "@mozilla.com"; + else + return null; + }, + _init: function BSS__init() { var serverUrl = "http://sync.server.url/"; @@ -351,10 +367,12 @@ BookmarksSyncService.prototype = { if (a.type != b.type) return -1; - let ret = {}; + let ret = {numProps: 0, props: {}}; for (prop in a) { - if (a[prop] != b[prop]) - ret[prop] = b[prop]; + if (a[prop] != b[prop]) { + ret.numProps++; + ret.props[prop] = b[prop]; + } } // FIXME: prune out properties we don't care about @@ -380,12 +398,12 @@ BookmarksSyncService.prototype = { let edits = this._getEdits(a[guid], b[guid]); if (edits == -1) // something went very wrong -- FIXME continue; - if (edits == {}) // no changes - skip + if (edits.numProps == 0) // no changes - skip continue; let parents = this._nodeParents(guid, b); cmds.push({action: "edit", guid: guid, depth: parents.length, parents: parents, - data: edits}); + data: edits.props}); } else { let parents = this._nodeParents(guid, a); // ??? cmds.push({action: "remove", guid: guid, @@ -425,8 +443,9 @@ BookmarksSyncService.prototype = { return false; }, - // NEED TO also look at the parent chain & index; only items in the - // same "spot" qualify for likeness + // Bookmarks are allowed to be in a different index as long as they + // are in the same folder. Folders and separators must be at the + // same index to qualify for 'likeness'. _commandLike: function BSS__commandLike(a, b) { if (!a || !b) return false; @@ -434,6 +453,11 @@ BookmarksSyncService.prototype = { if (a.action != b.action) return false; + // this check works because reconcile() fixes up the parent guids + // as it runs, and the command list is sorted by depth + if (a.parentGuid != b.parentGuid) + return false; + switch (a.data.type) { case 0: if (b.data.type == a.data.type && @@ -442,14 +466,15 @@ BookmarksSyncService.prototype = { return true; return false; case 6: - if (b.data.type == a.data.type && + if (b.index == a.index && + b.data.type == a.data.type && b.data.title == a.data.title) return true; return false; case 7: - // fixme: we need to enable this after we -// if (b.data.type == a.data.type) -// return true; + if (b.index == a.index && + b.data.type == a.data.type) + return true; return false; default: LOG("_commandLike: Unknown item type: " + uneval(a)); @@ -616,14 +641,16 @@ BookmarksSyncService.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. var propagations = [server['updates'], localUpdates]; - var conflicts; + var conflicts = [[],[]]; - if (server['status'] == 1 && localUpdates.length > 0) { - LOG("Reconciling updates"); - var ret = this._reconcile(localUpdates, server['updates']); - propagations = ret.propagations; - conflicts = ret.conflicts; - } + // reconciliation was wrapped in this - why? + //if (server['status'] == 1 && localUpdates.length > 0) { + //} + + LOG("Reconciling updates"); + var ret = this._reconcile(localUpdates, server['updates']); + propagations = ret.propagations; + conflicts = ret.conflicts; LOG("Propagations: " + uneval(propagations) + "\n"); LOG("Conflicts: " + uneval(conflicts) + "\n"); @@ -742,8 +769,10 @@ BookmarksSyncService.prototype = { ret.version = v; } keys = keys.sort(); + LOG("TMP: " + uneval(tmp)); for (var i = 0; i < keys.length; i++) { this._applyCommandsToObj(tmp, ret.deltas[keys[i]]); + LOG("TMP: " + uneval(tmp)); } ret.status = 1; ret.updates = this._detectUpdates(this._snapshot, tmp); @@ -757,12 +786,14 @@ BookmarksSyncService.prototype = { } else { LOG("Server delta can't update from our snapshot version, getting full file"); // generate updates from full local->remote snapshot diff - asyncRun(bind2(this, this._getServerUpdatesFull), handlers['complete'], localJson); + asyncRun(bind2(this, this._getServerUpdatesFull), + handlers['complete'], localJson); data = yield; if (data.status == 2) { // we have a delta file but no snapshot on the server. bad. // fixme? - LOG("Error: Delta file on server, but snapshot file missing. New snapshot uploaded, may be inconsistent with deltas!"); + LOG("Error: Delta file on server, but snapshot file missing. " + + "New snapshot uploaded, may be inconsistent with deltas!"); } var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack @@ -852,20 +883,39 @@ BookmarksSyncService.prototype = { return h; }, + _onLogin: function BSS__onLogin(event) { + this._os.notifyObservers(null, "bookmarks-sync:login", ""); + }, + + _onLoginError: function BSS__onLoginError(event) { + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + }, + // Interfaces this component implements. interfaces: [Ci.nsIBookmarksSyncService, Ci.nsISupports], - // nsISupports - // XPCOM registration classDescription: "Bookmarks Sync Service", contractID: "@mozilla.org/places/sync-service;1", classID: Components.ID("{6efd73bf-6a5a-404f-9246-f70a1286a3d6}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIBookmarksSyncService, Ci.nsISupports]), + // nsISupports + // nsIBookmarksSyncService - sync: function BSS_sync() { asyncRun(bind2(this, this._doSync)); } + sync: function BSS_sync() { asyncRun(bind2(this, this._doSync)); }, + + login: function BSS_login() { + this._dav.login("USER@mozilla.com", "PASSWORD", // FIXME + {load: bind2(this, this._onLogin), + error: bind2(this, this._onLoginError)}); + }, + + logout: function BSS_logout() { + this._dav.logout(); + this._os.notifyObservers(null, "bookmarks-sync:logout", ""); + } }; function asyncRun(func, handler, data) { @@ -897,6 +947,23 @@ function DAVCollection(baseUrl) { this._baseUrl = baseUrl; } DAVCollection.prototype = { + _loggedIn: false, + + __base64: {}, + __vase64loaded: false, + get _base64() { + if (!this.__base64loaded) { + let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + jsLoader.loadSubScript("chrome://sync/content/base64.js", this.__base64); + this.__base64loaded = true; + } + return this.__base64; + }, + + _authString: null, + _currentUserPath: "nobody", + _addHandler: function DC__addHandler(request, handlers, eventName) { if (handlers[eventName]) request.addEventListener(eventName, new EventListener(handlers[eventName]), false); @@ -921,22 +988,79 @@ DAVCollection.prototype = { return request; }, + GET: function DC_GET(path, handlers, headers) { if (!headers) headers = {'Content-type': 'text/plain'}; + if (this._authString) + headers['Authorization'] = this._authString; + var request = this._makeRequest("GET", path, handlers, headers); request.send(null); }, + PUT: function DC_PUT(path, data, handlers, headers) { if (!headers) headers = {'Content-type': 'text/plain'}; + if (this._authString) + headers['Authorization'] = this._authString; + var request = this._makeRequest("PUT", path, handlers, headers); request.send(data); }, - _runLockHandler: function DC__runLockHandler(name, event) { - if (this._lockHandlers && this._lockHandlers[name]) - this._lockHandlers[name](event); + + // Login / Logout + + login: function DC_login(username, password, handlers) { + this._loginHandlers = handlers; + internalHandlers = {load: bind2(this, this._onLogin), + error: bind2(this, this._onLoginError)}; + + this._authString = "Basic " + + this._base64.Base64.encode(username + ":" + password); + headers = {'Authorization': this._authString}; + + let request = this._makeRequest("GET", "", internalHandlers, headers); + request.send(null); }, + + logout: function DC_logout() { + this._authString = null; + }, + + _onLogin: function DC__onLogin(event) { + //LOG("logged in (" + event.target.status + "):\n" + + // event.target.responseText + "\n"); + + if (event.target.status != 200) { + this._onLoginError(event); + return; + } + + let hello = /Hello (.+)@mozilla.com/.exec(event.target.responseText) + if (hello) { + this._currentUserPath = hello[1]; + this._baseUrl = "http://dotmoz.mozilla.org/~" + + this._currentUserPath + "/"; + } + + this._loggedIn = true; + + if (this._loginHandlers && this._loginHandlers.load) + this._loginHandlers.load(event); + }, + _onLoginError: function DC__onLoginError(event) { + LOG("login failed (" + event.target.status + "):\n" + + event.target.responseText + "\n"); + + this._loggedIn = false; + + if (this._loginHandlers && this._loginHandlers.error) + this._loginHandlers.error(event); + }, + + // Locking + // FIXME: make this function not reentrant lock: function DC_lock(handlers) { this._lockHandlers = handlers; @@ -950,15 +1074,7 @@ DAVCollection.prototype = { " \n" + ""); }, - _onLock: function DC__onLock(event) { - LOG("acquired lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); - this._token = "woo"; - this._runLockHandler("load", event); - }, - _onLockError: function DC__onLockError(event) { - LOG("lock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); - this._runLockHandler("error", event); - }, + // FIXME: make this function not reentrant unlock: function DC_unlock(handlers) { this._lockHandlers = handlers; @@ -968,14 +1084,29 @@ DAVCollection.prototype = { var request = this._makeRequest("UNLOCK", "", internalHandlers, headers); request.send(null); }, + + _onLock: function DC__onLock(event) { + LOG("acquired lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); + this._token = "woo"; + if (this._lockHandlers && this._lockHandlers.load) + this._lockHandlers.load(event); + }, + _onLockError: function DC__onLockError(event) { + LOG("lock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); + if (this._lockHandlers && this._lockHandlers.error) + this._lockHandlers.error(event); + }, + _onUnlock: function DC__onUnlock(event) { LOG("removed lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); this._token = null; - this._runLockHandler("load", event); + if (this._lockHandlers && this._lockHandlers.load) + this._lockHandlers.load(event); }, _onUnlockError: function DC__onUnlockError(event) { LOG("unlock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); - this._runLockHandler("error", event); + if (this._lockHandlers && this._lockHandlers.error) + this._lockHandlers.error(event); }, }; diff --git a/services/sync/nsIBookmarksSyncService.idl b/services/sync/nsIBookmarksSyncService.idl index f7ad26c2a606..395ca8c17178 100644 --- a/services/sync/nsIBookmarksSyncService.idl +++ b/services/sync/nsIBookmarksSyncService.idl @@ -38,8 +38,26 @@ #include "nsISupports.idl" -[scriptable, uuid(1f00216a-4d2d-40e8-b4c5-afa3338a2d6c)] +[scriptable, uuid(b3e52c09-5c33-4d07-a3e6-7c453d0c4be8)] interface nsIBookmarksSyncService : nsISupports { + /** + * The currently logged-in user + */ + readonly attribute AString currentUser; + + /** + * Log into the server. Pre-requisite for sync(). + */ + void login(); + + /** + * Log out of the server. + */ + void logout(); + + /** + * Initiate a sync operation. + */ void sync(); }; From 99dff049be118d38fc333df96b0665e6d1fc3f35 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Sep 2007 01:23:05 -0700 Subject: [PATCH 0010/1860] Yay, sync works again! --- services/sync/nsBookmarksSyncService.js | 388 +++++++++--------------- 1 file changed, 142 insertions(+), 246 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 026a73065d20..1d6d8d760666 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -85,11 +85,7 @@ BookmarksSyncService.prototype = { _snapshotVersion: 0, get currentUser() { - // FIXME - need to expose that info some other way - if (this._dav._currentUserPath) - return this._dav._currentUserPath + "@mozilla.com"; - else - return null; + return this._dav.currentUser; }, _init: function BSS__init() { @@ -138,230 +134,6 @@ BookmarksSyncService.prototype = { items[guid] = item; }, - // Takes a vanilla command list (fro sync-engine.js) and combines - // them so that each create/edit/remove fully creates/edits/removes - // an item (sync-engine.js will give us one command per object - // property, i.e., a bunch of commands per item) - _combineCommands: function BSS__combineCommands(commandList) { - var newList = []; - - for (var i = 0; i < commandList.length; i++) { - LOG("Combining command: " + uneval(commandList[i])); - - var action = commandList[i].action; - var value = commandList[i].value; - var path = commandList[i].path; - - // ignore commands about creating the item container itself - if (path.length <= 1) - continue; - - var guid = path.shift(); - var key = path.pop(); - - if (path.length) { - LOG("Warning: editing deep property - dropping"); - continue; - } - - if (!newList.length || - newList[newList.length - 1].guid != guid || - newList[newList.length - 1].action != action) - newList.push({action: action, guid: guid, data: {}}); - - newList[newList.length - 1].data[key] = value; - } - return newList; - }, - - _nodeDepth: function BSS__nodeDepth(guid, depth) { - let parent = this._snapshot[guid].parentGuid; - if (parent == null) - return depth; - return this._nodeDepth(parent, ++depth); - }, - - // Takes *combined* commands and sorts them so that parent folders - // get created first, then children in reverse index order, then - // removes. - // Note: this method uses this._snapshot to calculate node depths; - // so this._snapshot must have server commands applied to it before - // calling this method. - _sortCommands: function BSS__sortCommands(commandList) { - for (let i = 0; i < commandList.length; i++) { - commandList[i].depth = this._nodeDepth(commandList[i].guid, 0); - } - commandList.sort(function compareNodes(a, b) { - if (a.depth > b.depth) - return 1; - if (a.depth < b.depth) - return -1; - if (a.index > b.index) - return -1; - if (a.index < b.index) - return 1; - return 0; // should never happen, but not a big deal if it does - }); - return commandList; - }, - - // Takes *combined* and *sorted* commands and applies them to the - // places db - _applyCommands: function BSS__applyCommands(commandList) { - for (var i = 0; i < commandList.length; i++) { - var command = commandList[i]; - LOG("Processing command: " + uneval(command)); - switch (command["action"]) { - case "create": - this._createCommand(command); - break; - case "remove": - this._removeCommand(command); - break; - case "edit": - this._editCommand(command); - break; - default: - LOG("unknown action in command: " + command["action"]); - break; - } - } - }, - - _compareItems: function BSS__compareItems(node, data) { - switch (data.type) { - case 0: - if (node && - node.type == node.RESULT_TYPE_URI && - node.uri == data.uri && node.title == data.title) - return true; - return false; - case 6: - if (node && - node.type == node.RESULT_TYPE_FOLDER && - node.title == data.title) - return true; - return false; - case 7: - if (node && node.type == node.RESULT_TYPE_SEPARATOR) - return true; - return false; - default: - LOG("_compareItems: Unknown item type: " + data.type); - return false; - } - }, - - // FIXME: can't skip creations here; they need to get pruned out - // during reconciliation, sincethere will be "new" items being sent - // upstream too - _createCommand: function BSS__createCommand(command) { - let newId; - - // check if it's the root - if (command.data.parentGuid == null) { - this._bms.setItemGUID(this._bms.bookmarksRoot, command.guid); - return; - } - - let parentId = this._bms.getItemIdForGUID(command.data.parentGuid); - if (parentId <= 0) { - LOG("Warning: creating node with unknown parent -> reparenting to root"); - parentId = this._bms.bookmarksRoot; - } - let parent = this._getBookmarks(parentId); - parent.QueryInterface(Ci.nsINavHistoryQueryResultNode); - parent.containerOpen = true; - - let curItem; - if (parent.childCount > command.data.index) - curItem = parent.getChild(command.data.index); - - if (this._compareItems(curItem, command.data)) { - LOG(" -> FIXME - skipping item (already exists)"); - this._bms.setItemGUID(curItem.itemId, command.guid); - return; - } - - LOG(" -> creating item"); - - switch (command.data.type) { - case 0: - newId = this._bms.insertBookmark(parentId, - makeURI(command.data.uri), - command.data.index, - command.data.title); - break; - case 6: - newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); - break; - case 7: - newId = this._bms.insertSeparator(parentId, command.data.index); - break; - default: - LOG("_createCommand: Unknown item type: " + command.data.type); - break; - } - if (newId) - this._bms.setItemGUID(newId, command.guid); - }, - - _removeCommand: function BSS__removeCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.guid); - var type = this._bms.getItemType(itemId); - - switch (type) { - case this._bms.TYPE_BOOKMARK: - // FIXME: check it's an actual bookmark? - LOG(" -> removing bookmark " + command.guid); - this._bms.removeItem(itemId); - break; - case this._bms.TYPE_FOLDER: - LOG(" -> removing folder " + command.guid); - this._bms.removeFolder(itemId); - break; - case this._bms.TYPE_SEPARATOR: - LOG(" -> removing separator " + command.guid); - this._bms.removeItem(itemId); - break; - default: - LOG("removeCommand: Unknown item type: " + type); - break; - } - }, - - _editCommand: function BSS__editCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.guid); - var type = this._bms.getItemType(itemId); - - for (let key in command.data) { - switch (key) { - case "guid": - this._bms.setItemGUID(itemId, command.data.guid); - break; - case "title": - this._bms.setItemTitle(itemId, command.data.title); - break; - case "uri": - this._bms.changeBookmarkURI(itemId, makeURI(command.data.uri)); - break; - case "index": // FIXME: what if we do this one before parentGuid ? that'd be wrong - this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), - command.data.index); - break; - case "parentGuid": - this._bms.moveItem( - itemId, this._bms.getItemIdForGUID(command.data.parentGuid), -1); - break; - default: - LOG("Warning: Can't change item property: " + key); - break; - } - } - }, - _getEdits: function BSS__getEdits(a, b) { // check the type separately, just in case if (a.type != b.type) @@ -567,15 +339,139 @@ BookmarksSyncService.prototype = { _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { for (let i = 0; i < commands.length; i++) { - switch (command.action) { + LOG("Applying cmd to obj: " + uneval(commands[i])); + switch (commands[i].action) { case "create": - obj[command.guid] = eval(uneval(command.data)); + obj[commands[i].guid] = eval(uneval(commands[i].data)); + break; case "edit": - for (let prop in command.data) { - obj[command.guid][prop] = command.data[prop]; + for (let prop in commands[i].data) { + obj[commands[i].guid][prop] = commands[i].data[prop]; } + break; case "remove": - delete obj[command.guid]; + delete obj[commands[i].guid]; + break; + } + } + return obj; + }, + + // Applies commands to the places db + _applyCommands: function BSS__applyCommands(commandList) { + for (var i = 0; i < commandList.length; i++) { + var command = commandList[i]; + LOG("Processing command: " + uneval(command)); + switch (command["action"]) { + case "create": + this._createCommand(command); + break; + case "remove": + this._removeCommand(command); + break; + case "edit": + this._editCommand(command); + break; + default: + LOG("unknown action in command: " + command["action"]); + break; + } + } + }, + + _createCommand: function BSS__createCommand(command) { + let newId; + let parentId = this._bms.getItemIdForGUID(command.data.parentGuid); + + if (parentId <= 0) { + LOG("Warning: creating node with unknown parent -> reparenting to root"); + parentId = this._bms.bookmarksRoot; + } + + LOG(" -> creating item"); + + switch (command.data.type) { + case 0: + newId = this._bms.insertBookmark(parentId, + makeURI(command.data.uri), + command.data.index, + command.data.title); + break; + case 6: + newId = this._bms.createFolder(parentId, + command.data.title, + command.data.index); + break; + case 7: + newId = this._bms.insertSeparator(parentId, command.data.index); + break; + default: + LOG("_createCommand: Unknown item type: " + command.data.type); + break; + } + if (newId) + this._bms.setItemGUID(newId, command.guid); + }, + + _removeCommand: function BSS__removeCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.guid); + var type = this._bms.getItemType(itemId); + + switch (type) { + case this._bms.TYPE_BOOKMARK: + // FIXME: check it's an actual bookmark? + LOG(" -> removing bookmark " + command.guid); + this._bms.removeItem(itemId); + break; + case this._bms.TYPE_FOLDER: + LOG(" -> removing folder " + command.guid); + this._bms.removeFolder(itemId); + break; + case this._bms.TYPE_SEPARATOR: + LOG(" -> removing separator " + command.guid); + this._bms.removeItem(itemId); + break; + default: + LOG("removeCommand: Unknown item type: " + type); + break; + } + }, + + _editCommand: function BSS__editCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.guid); + if (itemId == -1) { + LOG("Warning: item for guid " + command.guid + " not found. Skipping."); + return; + } + + var type = this._bms.getItemType(itemId); + + for (let key in command.data) { + switch (key) { + case "guid": + var existing = this._bms.getItemIdForGUID(command.guid); + if (existing == -1) + this._bms.setItemGUID(itemId, command.data.guid); + else + LOG("Warning: can't change guid " + command.guid + + " to " + command.data.guid + ": guid already exists."); + break; + case "title": + this._bms.setItemTitle(itemId, command.data.title); + break; + case "uri": + this._bms.changeBookmarkURI(itemId, makeURI(command.data.uri)); + break; + case "index": // FIXME: what if we do this one before parentGuid ? that'd be wrong + this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), + command.data.index); + break; + case "parentGuid": + this._bms.moveItem( + itemId, this._bms.getItemIdForGUID(command.data.parentGuid), -1); + break; + default: + LOG("Warning: Can't change item property: " + key); break; } } @@ -682,14 +578,8 @@ BookmarksSyncService.prototype = { // 3.1) Apply server changes to local store if (propagations[0] && propagations[0].length) { LOG("Applying changes locally"); - //localBookmarks = this._getBookmarks(); // fixme: wtf this._snapshot = this._wrapNode(localBookmarks); - this._applyCommandsToObj(this._snapshot, propagations[0]); - //var combinedCommands = this._combineCommands(propagations[0]); - //LOG("Combined commands: " + uneval(combinedCommands) + "\n"); - //var sortedCommands = this._sortCommands(combinedCommands); - //LOG("Sorted commands: " + uneval(sortedCommands) + "\n"); - //this._applyCommands(combinedCommands); + propagations[0] = this._applyCommandsToObj(this._snapshot, propagations[0]); this._applyCommands(propagations[0]); this._snapshot = this._wrapNode(localBookmarks); } @@ -771,11 +661,11 @@ BookmarksSyncService.prototype = { keys = keys.sort(); LOG("TMP: " + uneval(tmp)); for (var i = 0; i < keys.length; i++) { - this._applyCommandsToObj(tmp, ret.deltas[keys[i]]); + tmp = this._applyCommandsToObj(ret.deltas[keys[i]], tmp); LOG("TMP: " + uneval(tmp)); } ret.status = 1; - ret.updates = this._detectUpdates(this._snapshot, tmp); + ret["updates"] = this._detectUpdates(this._snapshot, tmp); } else if (ret.deltas[this._snapshotVersion]) { LOG("No changes from server"); @@ -797,7 +687,7 @@ BookmarksSyncService.prototype = { } var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack - this._applyCommandsToObj(tmp, data.updates); + tmp = this._applyCommandsToObj(data.updates, tmp); // fixme: this is duplicated from above, need to do some refactoring @@ -810,7 +700,7 @@ BookmarksSyncService.prototype = { } keys = keys.sort(); for (var i = 0; i < keys.length; i++) { - this._applyCommandsToObj(tmp, ret.deltas[keys[i]]); + tmp = this._applyCommandsToObj(ret.deltas[keys[i]], tmp); } ret.status = data.status; @@ -907,7 +797,7 @@ BookmarksSyncService.prototype = { sync: function BSS_sync() { asyncRun(bind2(this, this._doSync)); }, login: function BSS_login() { - this._dav.login("USER@mozilla.com", "PASSWORD", // FIXME + this._dav.login("nobody@mozilla.com", "password", // FIXME {load: bind2(this, this._onLogin), error: bind2(this, this._onLoginError)}); }, @@ -964,6 +854,11 @@ DAVCollection.prototype = { _authString: null, _currentUserPath: "nobody", + _currentUser: "nobody@mozilla.com", + get currentUser() { + return this._currentUser; + }, + _addHandler: function DC__addHandler(request, handlers, eventName) { if (handlers[eventName]) request.addEventListener(eventName, new EventListener(handlers[eventName]), false); @@ -1040,6 +935,7 @@ DAVCollection.prototype = { let hello = /Hello (.+)@mozilla.com/.exec(event.target.responseText) if (hello) { this._currentUserPath = hello[1]; + this._currentUser = this._currentUserPath + "@mozilla.com"; this._baseUrl = "http://dotmoz.mozilla.org/~" + this._currentUserPath + "/"; } From 5de8300ebe8177b2cd1ca27df425d3f8c8d7a28d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Sep 2007 01:47:01 -0700 Subject: [PATCH 0011/1860] Add xpt file, for the xpidl-deprived ;) --- services/sync/nsIBookmarksSyncService.xpt | Bin 0 -> 383 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/sync/nsIBookmarksSyncService.xpt diff --git a/services/sync/nsIBookmarksSyncService.xpt b/services/sync/nsIBookmarksSyncService.xpt new file mode 100644 index 0000000000000000000000000000000000000000..47791d8fa7895d586a0a173ee104c6055d05906a GIT binary patch literal 383 zcmazDaQ64*3aKne^~p@)<&t7#Vqj)qV610gU{C_$(gq*_1_vON8<1jzFc}z1yzZ0`<`sJemlhP{7nKw< z0NGCY`PsRNMcKu{m3hhjNyVu}WvN9VYgj-Ud>I(nKy)aSj)T%!V7ehc&nG`UGmimG z=a-fM86abvQXzsMwsU@NK~8E(Du!LbK$|j?Q$bd-H2|%+#JFN7#Ik!(OP)aK*HHQc zW5Xl{CWx9paCSp-X;D#XUP&m>=?pnw=Kx8NV~T;+GbE;@z Date: Fri, 21 Sep 2007 16:38:19 -0700 Subject: [PATCH 0012/1860] Yield to main loop during reconciliation, so as to not block the UI. Fix a broken check when changing the guid of an item. --- services/sync/nsBookmarksSyncService.js | 55 +++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 1d6d8d760666..e768d4a87e12 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -76,6 +76,9 @@ BookmarksSyncService.prototype = { return this.__os; }, + // Timer object for yielding to the main loop + _timer: null, + // DAVCollection object _dav: null, @@ -89,7 +92,6 @@ BookmarksSyncService.prototype = { }, _init: function BSS__init() { - var serverUrl = "http://sync.server.url/"; try { var branch = Cc["@mozilla.org/preferences-service;1"]. @@ -209,7 +211,7 @@ BookmarksSyncService.prototype = { (b.parents.indexOf(a.guid) >= 0) && a.action == "remove") return true; - if ((a.guid == b.guid) && a != b) + if ((a.guid == b.guid) && !this._deepEquals(a, b)) return true; // FIXME - how else can two commands conflict? return false; @@ -274,6 +276,9 @@ BookmarksSyncService.prototype = { return true; }, + // When we change the guid of a local item (because we detect it as + // being the same item as a remote one), we need to fix any other + // local items that have it as their parent _fixParents: function BSS__fixParents(list, oldGuid, newGuid) { for (let i = 0; i < list.length; i++) { if (!list[i]) @@ -287,11 +292,23 @@ BookmarksSyncService.prototype = { } }, - _reconcile: function BSS__reconcile(listA, listB) { + _reconcile: function BSS__reconcile(onComplete, commandLists) { + let generator = yield; + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let callback = bind2(this, + function(event) { handleEvent(generator, event); }); + let listener = new EventListener(callback); + + let [listA, listB] = commandLists; let propagations = [[], []]; let conflicts = [[], []]; for (let i = 0; i < listA.length; i++) { + + // Yield to main loop + this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + yield; + for (let j = 0; j < listB.length; j++) { if (this._deepEquals(listA[i], listB[j])) { delete listA[i]; @@ -310,6 +327,11 @@ BookmarksSyncService.prototype = { listB = listB.filter(function(elt) { return elt }); for (let i = 0; i < listA.length; i++) { + + // Yield to main loop + this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + yield; + for (let j = 0; j < listB.length; j++) { if (this._conflicts(listA[i], listB[j]) || this._conflicts(listB[j], listA[i])) { @@ -334,7 +356,9 @@ BookmarksSyncService.prototype = { function(elt) { return elt.guid == listB[j].guid })) propagations[0].push(listB[j]); } - return {propagations: propagations, conflicts: conflicts}; + + this._timer = null; + onComplete({propagations: propagations, conflicts: conflicts}); }, _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { @@ -449,7 +473,7 @@ BookmarksSyncService.prototype = { for (let key in command.data) { switch (key) { case "guid": - var existing = this._bms.getItemIdForGUID(command.guid); + var existing = this._bms.getItemIdForGUID(command.data.guid); if (existing == -1) this._bms.setItemGUID(itemId, command.data.guid); else @@ -544,7 +568,9 @@ BookmarksSyncService.prototype = { //} LOG("Reconciling updates"); - var ret = this._reconcile(localUpdates, server['updates']); + asyncRun(bind2(this, this._reconcile), + handlers['complete'], [localUpdates, server.updates]); + let ret = yield; propagations = ret.propagations; conflicts = ret.conflicts; @@ -828,8 +854,23 @@ function EventListener(handler) { this._handler = handler; } EventListener.prototype = { + QueryInterface: function(iid) { + if (iid.Equals(Components.interfaces.nsITimerCallback) || + iid.Equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + // DOM event listener + handleEvent: function EL_handleEvent(event) { this._handler(event); + }, + + // nsITimerCallback + + notify: function EL_notify(timer) { + this._handler(timer); } }; @@ -1003,7 +1044,7 @@ DAVCollection.prototype = { LOG("unlock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); if (this._lockHandlers && this._lockHandlers.error) this._lockHandlers.error(event); - }, + } }; function makeFile(path) { From 1d4cbb99cd19f784d6e8fe2ced1189af37de1312 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 24 Sep 2007 00:21:22 -0700 Subject: [PATCH 0013/1860] Add code to disable any auth prompt and turn them into failures (from the microsummary svc). Get/save sync service password in the password manager (ui missing). Fix a bug in the reconciler that prevented it from returning any conflicts. Other misc cleanup. --- services/sync/nsBookmarksSyncService.js | 282 +++++++++++++++++++----- services/sync/services-sync.js | 3 +- 2 files changed, 233 insertions(+), 52 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index e768d4a87e12..6aa774631ade 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -92,15 +92,15 @@ BookmarksSyncService.prototype = { }, _init: function BSS__init() { - var serverUrl = "http://sync.server.url/"; + let serverURL = 'https://dotmoz.mozilla.org/'; try { - var branch = Cc["@mozilla.org/preferences-service;1"]. + let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); - serverUrl = branch.getCharPref("browser.places.sync.serverUrl"); + serverURL = branch.getCharPref("browser.places.sync.serverURL"); } catch (ex) { /* use defaults */ } - LOG("Bookmarks sync server: " + serverUrl); - this._dav = new DAVCollection(serverUrl); + LOG("Bookmarks login server: " + serverURL); + this._dav = new DAVCollection(serverURL); }, _wrapNode: function BSS__wrapNode(node) { @@ -159,7 +159,7 @@ BookmarksSyncService.prototype = { }, _nodeParentsInt: function BSS__nodeParentsInt(guid, tree, parents) { - if (tree[guid].parentGuid == null) + if (tree[guid] && tree[guid].parentGuid == null) return parents; parents.push(tree[guid].parentGuid); return this._nodeParentsInt(tree[guid].parentGuid, tree, parents); @@ -292,12 +292,26 @@ BookmarksSyncService.prototype = { } }, + // FIXME: todo: change the conflicts data structure to hold more + // information about which specific command pars conflict; an what + // commands would apply once those are resolved. Perhaps something + // like: + + // [[direct-conflictA, 2nd-degree-conflictA1, ...], + // [direct-conflictB, 2nd-degree-conflict-B1, ...], ...] + + // possible problem: a 2nd-degree conflict could show up in multiple + // lists (that is, there are multiple direct conflicts that prevent + // another command pair from cleanly applying). maybe: + + // [[[dcA1, dcB1], [dcA2. dcB2], ...], + // [2dcA1, 2dcA2, ...], [2dcB1, 2dcB2, ...]] + _reconcile: function BSS__reconcile(onComplete, commandLists) { let generator = yield; this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let callback = bind2(this, - function(event) { handleEvent(generator, event); }); - let listener = new EventListener(callback); + let handlers = this._handlersForGenerator(generator); + let listener = new EventListener(handlers['complete']); let [listA, listB] = commandLists; let propagations = [[], []]; @@ -335,10 +349,10 @@ BookmarksSyncService.prototype = { for (let j = 0; j < listB.length; j++) { if (this._conflicts(listA[i], listB[j]) || this._conflicts(listB[j], listA[i])) { - if (conflicts[0].some( + if (!conflicts[0].some( function(elt) { return elt.guid == listA[i].guid })) conflicts[0].push(listA[i]); - if (conflicts[1].some( + if (!conflicts[1].some( function(elt) { return elt.guid == listB[j].guid })) conflicts[1].push(listB[j]); } @@ -563,10 +577,6 @@ BookmarksSyncService.prototype = { var propagations = [server['updates'], localUpdates]; var conflicts = [[],[]]; - // reconciliation was wrapped in this - why? - //if (server['status'] == 1 && localUpdates.length > 0) { - //} - LOG("Reconciling updates"); asyncRun(bind2(this, this._reconcile), handlers['complete'], [localUpdates, server.updates]); @@ -590,15 +600,13 @@ BookmarksSyncService.prototype = { } if (conflicts && conflicts[0] && conflicts[0].length) { - //var combinedCommands = this._combineCommands(propagations[0]); LOG("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); - LOG("Conflicts(1) " + uneval(this._combineCommands(conflicts[0]))); + LOG("Conflicts(1) " + uneval(conflicts[0])); } if (conflicts && conflicts[1] && conflicts[1].length) { - //var combinedCommands = this._combineCommands(propagations[0]); LOG("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); - LOG("Conflicts(2) " + uneval(this._combineCommands(conflicts[1]))); + LOG("Conflicts(2) " + uneval(conflicts[1])); } // 3.1) Apply server changes to local store @@ -793,13 +801,14 @@ BookmarksSyncService.prototype = { }, _handlersForGenerator: function BSS__handlersForGenerator(generator) { - var h = {load: bind2(this, function(event) { handleEvent(generator, event); }), + var h = {load: bind2(this, function(event) { continueGenerator(generator, event); }), error: bind2(this, function(event) { LOG("Request failed: " + uneval(event)); })}; h['complete'] = h['load']; return h; }, _onLogin: function BSS__onLogin(event) { + LOG("Bookmarks sync server: " + this._dav.baseURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); }, @@ -823,8 +832,7 @@ BookmarksSyncService.prototype = { sync: function BSS_sync() { asyncRun(bind2(this, this._doSync)); }, login: function BSS_login() { - this._dav.login("nobody@mozilla.com", "password", // FIXME - {load: bind2(this, this._onLogin), + this._dav.login({load: bind2(this, this._onLogin), error: bind2(this, this._onLoginError)}); }, @@ -834,15 +842,39 @@ BookmarksSyncService.prototype = { } }; +function makeFile(path) { + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return file; +} + +function makeURI(uriString) { + var ioservice = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return ioservice.newURI(uriString, null, null); +} + +function LOG(aText) { + dump(aText + "\n"); + var consoleService = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + consoleService.logStringMessage(aText); +} + +function bind2(object, method) { + return function innerBind() { return method.apply(object, arguments); } +} + function asyncRun(func, handler, data) { var generator = func(handler, data); generator.next(); generator.send(generator); } -function handleEvent(generator, data) { +function continueGenerator(generator, data) { try { generator.send(data); } catch (e) { + generator.close(); if (e instanceof StopIteration) generator = null; else @@ -874,12 +906,17 @@ EventListener.prototype = { } }; -function DAVCollection(baseUrl) { - this._baseUrl = baseUrl; +function DAVCollection(baseURL) { + this._baseURL = baseURL; + this._authProvider = new DummyAuthProvider(); } DAVCollection.prototype = { _loggedIn: false, + get baseURL() { + return this._baseURL; + }, + __base64: {}, __vase64loaded: false, get _base64() { @@ -914,7 +951,7 @@ DAVCollection.prototype = { this._addHandler(request, handlers, "error"); request = request.QueryInterface(Ci.nsIXMLHttpRequest); - request.open(op, this._baseUrl + path, true); + request.open(op, this._baseURL + path, true); if (headers) { for (var key in headers) { @@ -922,6 +959,8 @@ DAVCollection.prototype = { } } + request.channel.notificationCallbacks = this._authProvider; + return request; }, @@ -947,14 +986,41 @@ DAVCollection.prototype = { // Login / Logout - login: function DC_login(username, password, handlers) { + login: function DC_login(handlers) { this._loginHandlers = handlers; internalHandlers = {load: bind2(this, this._onLogin), error: bind2(this, this._onLoginError)}; - this._authString = "Basic " + - this._base64.Base64.encode(username + ":" + password); - headers = {'Authorization': this._authString}; + try { + let uri = makeURI(this._baseURL); + let username = 'nobody@mozilla.com'; + let password; + + let branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + username = branch.getCharPref("browser.places.sync.username"); + + // fixme: make a request and get the realm + let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, uri.hostPort, null, + 'Use your ldap username/password - dotmoz'); + LOG("Found " + logins.length + " logins"); + + for (let i = 0; i < logins.length; i++) { + if (logins[i].username == username) { + password = logins[i].password; + break; + } + } + + // FIXME: should we regen this each time to prevent it from staying in memory? + this._authString = "Basic " + + this._base64.Base64.encode(username + ":" + password); + headers = {'Authorization': this._authString}; + } catch (e) { + // try without the auth header? + // fixme: may want to just fail here + } let request = this._makeRequest("GET", "", internalHandlers, headers); request.send(null); @@ -968,16 +1034,17 @@ DAVCollection.prototype = { //LOG("logged in (" + event.target.status + "):\n" + // event.target.responseText + "\n"); - if (event.target.status != 200) { + if (this._authProvider._authFailed || event.target.status >= 400) { this._onLoginError(event); return; } + // fixme: hacktastic let hello = /Hello (.+)@mozilla.com/.exec(event.target.responseText) if (hello) { this._currentUserPath = hello[1]; this._currentUser = this._currentUserPath + "@mozilla.com"; - this._baseUrl = "http://dotmoz.mozilla.org/~" + + this._baseURL = "http://dotmoz.mozilla.org/~" + this._currentUserPath + "/"; } @@ -1047,28 +1114,141 @@ DAVCollection.prototype = { } }; -function makeFile(path) { - var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - file.initWithPath(path); - return file; -} -function makeURI(uriString) { - var ioservice = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - return ioservice.newURI(uriString, null, null); -} +// Taken from nsMicrosummaryService.js and massaged slightly +function DummyAuthProvider() {} +DummyAuthProvider.prototype = { + // Implement notification callback interfaces so we can suppress UI + // and abort loads for bad SSL certs and HTTP authorization requests. + + // Interfaces this component implements. + interfaces: [Ci.nsIBadCertListener, + Ci.nsIAuthPromptProvider, + Ci.nsIAuthPrompt, + Ci.nsIPrompt, + Ci.nsIInterfaceRequestor, + Ci.nsISupports], -function bind2(object, method) { - return function innerBind() { return method.apply(object, arguments); } -} + // Auth requests appear to succeed when we cancel them (since the server + // redirects us to a "you're not authorized" page), so we have to set a flag + // to let the load handler know to treat the load as a failure. + get _authFailed() { return this.__authFailed; }, + set _authFailed(newValue) { return this.__authFailed = newValue }, -function LOG(aText) { - dump(aText + "\n"); - var consoleService = Cc["@mozilla.org/consoleservice;1"]. - getService(Ci.nsIConsoleService); - consoleService.logStringMessage(aText); -} + // nsISupports + + QueryInterface: function DAP_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + + // nsIAuthPrompt and nsIPrompt need separate implementations because + // their method signatures conflict. The other interfaces we implement + // within DummyAuthProvider itself. + switch(iid) { + case Ci.nsIAuthPrompt: + return this.authPrompt; + case Ci.nsIPrompt: + return this.prompt; + default: + return this; + } + }, + + // nsIInterfaceRequestor + + getInterface: function DAP_getInterface(iid) { + return this.QueryInterface(iid); + }, + + // nsIBadCertListener + + // Suppress UI and abort secure loads from servers with bad SSL certificates. + + confirmUnknownIssuer: function DAP_confirmUnknownIssuer(socketInfo, cert, certAddType) { + return false; + }, + + confirmMismatchDomain: function DAP_confirmMismatchDomain(socketInfo, targetURL, cert) { + return false; + }, + + confirmCertExpired: function DAP_confirmCertExpired(socketInfo, cert) { + return false; + }, + + notifyCrlNextupdate: function DAP_notifyCrlNextupdate(socketInfo, targetURL, cert) { + }, + + // nsIAuthPromptProvider + + getAuthPrompt: function(aPromptReason, aIID) { + this._authFailed = true; + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + // HTTP always requests nsIAuthPromptProvider first, so it never needs + // nsIAuthPrompt, but not all channels use nsIAuthPromptProvider, so we + // implement nsIAuthPrompt too. + + // nsIAuthPrompt + + get authPrompt() { + var resource = this; + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) { + resource._authFailed = true; + return false; + }, + promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) { + resource._authFailed = true; + return false; + }, + promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) { + resource._authFailed = true; + return false; + } + }; + }, + + // nsIPrompt + + get prompt() { + var resource = this; + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + alert: function(dialogTitle, text) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + alertCheck: function(dialogTitle, text, checkMessage, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirm: function(dialogTitle, text) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirmCheck: function(dialogTitle, text, checkMessage, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirmEx: function(dialogTitle, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + prompt: function(dialogTitle, text, value, checkMsg, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + promptPassword: function(dialogTitle, text, password, checkMsg, checkValue) { + resource._authFailed = true; + return false; + }, + promptUsernameAndPassword: function(dialogTitle, text, username, password, checkMsg, checkValue) { + resource._authFailed = true; + return false; + }, + select: function(dialogTitle, text, count, selectList, outSelection) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + }; + } +}; function NSGetModule(compMgr, fileSpec) { return XPCOMUtils.generateModule([BookmarksSyncService]); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index aaa6ec22181e..7c58c3749e1e 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,5 @@ -pref("browser.places.sync.serverUrl", "http://places.sync.url/"); +pref("browser.places.sync.serverURL", "http://dotmoz.mozilla.org/"); +pref("browser.places.sync.username", "nobody@mozilla.com"); pref("extensions.sync.lastversion", "firstrun"); pref("extensions.sync.lastsync", ""); From b5882545810f50eaf7b42cad36308046bbdf57ef Mon Sep 17 00:00:00 2001 From: Date: Mon, 24 Sep 2007 15:10:25 -0700 Subject: [PATCH 0014/1860] - tree[guid] is null for some (as yet) unknown reason --- services/sync/nsBookmarksSyncService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 6aa774631ade..4337781284ea 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -159,7 +159,7 @@ BookmarksSyncService.prototype = { }, _nodeParentsInt: function BSS__nodeParentsInt(guid, tree, parents) { - if (tree[guid] && tree[guid].parentGuid == null) + if (!tree[guid] || tree[guid].parentGuid == null) return parents; parents.push(tree[guid].parentGuid); return this._nodeParentsInt(tree[guid].parentGuid, tree, parents); From 4fa1a5e790d6f1e5ff17796d3cba0c73b5dd0b57 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 24 Sep 2007 18:34:03 -0700 Subject: [PATCH 0015/1860] Fix some nasty bugs with the way js generators were being used. Have the snapshot [de]serialized to the profile --- services/sync/nsBookmarksSyncService.js | 221 ++++++++++++++++++------ 1 file changed, 164 insertions(+), 57 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 4337781284ea..73a462048374 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -39,6 +39,15 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +const MODE_RDONLY = 0x01; +const MODE_WRONLY = 0x02; +const MODE_CREATE = 0x08; +const MODE_APPEND = 0x10; +const MODE_TRUNCATE = 0x20; + +const PERMS_FILE = 0644; +const PERMS_DIRECTORY = 0755; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function BookmarksSyncService() { this._init(); } @@ -101,6 +110,59 @@ BookmarksSyncService.prototype = { catch (ex) { /* use defaults */ } LOG("Bookmarks login server: " + serverURL); this._dav = new DAVCollection(serverURL); + this._readSnapshot(); + }, + + _saveSnapshot: function BSS__saveSnapshot() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + + let file = dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync-snapshot.json"); + file.QueryInterface(Ci.nsILocalFile); + + if (!file.exists()) + file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); + + let fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; + fos.init(file, flags, PERMS_FILE, 0); + + let out = {version: this._snapshotVersion, snapshot: this._snapshot}; + out = uneval(out); + fos.write(out, out.length); + fos.flush(); + fos.close(); + }, + + _readSnapshot: function BSS__readSnapshot() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + + let file = dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync-snapshot.json"); + + if (!file.exists()) + return; + + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(file, MODE_RDONLY, PERMS_FILE, 0); + fis.QueryInterface(Ci.nsILineInputStream); + + let json = ""; + while (fis.available()) { + let ret = {}; + fis.readLine(ret); + json += ret.value; + } + json = eval(json); + + if (json.snapshot && json.version) { + this._snapshot = json.snapshot; + this._snapshotVersion = json.version; + } }, _wrapNode: function BSS__wrapNode(node) { @@ -319,9 +381,8 @@ BookmarksSyncService.prototype = { for (let i = 0; i < listA.length; i++) { - // Yield to main loop this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; + yield; // Yield to main loop for (let j = 0; j < listB.length; j++) { if (this._deepEquals(listA[i], listB[j])) { @@ -342,9 +403,8 @@ BookmarksSyncService.prototype = { for (let i = 0; i < listA.length; i++) { - // Yield to main loop this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; + yield; // Yield to main loop for (let j = 0; j < listB.length; j++) { if (this._conflicts(listA[i], listB[j]) || @@ -372,7 +432,13 @@ BookmarksSyncService.prototype = { } this._timer = null; - onComplete({propagations: propagations, conflicts: conflicts}); + let ret = {propagations: propagations, conflicts: conflicts}; + this._generatorDone(onComplete, ret); + + // shutdown protocol + let cookie = yield; + if (cookie != "generator shutdown") + LOG("_reconcile: Error: generator not properly shut down.") }, _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { @@ -547,8 +613,10 @@ BookmarksSyncService.prototype = { LOG("local json: " + uneval(localJson)); // 1) Fetch server deltas - asyncRun(bind2(this, this._getServerData), handlers['complete'], localJson); - var server = yield; + let gsd_gen = this._getServerData(handlers['complete'], localJson); + gsd_gen.next(); // must initialize before sending + gsd_gen.send(gsd_gen); + let server = yield; LOG("server: " + uneval(server)); if (server['status'] == 2) { @@ -574,63 +642,79 @@ BookmarksSyncService.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. - var propagations = [server['updates'], localUpdates]; - var conflicts = [[],[]]; - LOG("Reconciling updates"); - asyncRun(bind2(this, this._reconcile), - handlers['complete'], [localUpdates, server.updates]); + let callback = function(retval) { continueGenerator(generator, retval); }; + let rec_gen = this._reconcile(callback, [localUpdates, server.updates]); + rec_gen.next(); // must initialize before sending + rec_gen.send(rec_gen); let ret = yield; - propagations = ret.propagations; - conflicts = ret.conflicts; + // FIXME: Need to come up with a closing protocol for generators + rec_gen.close(); - LOG("Propagations: " + uneval(propagations) + "\n"); - LOG("Conflicts: " + uneval(conflicts) + "\n"); - - this._snapshotVersion = server['version']; + let clientChanges = []; + let serverChanges = []; + let clientConflicts = []; + let serverConflicts = []; - if (!((propagations[0] && propagations[0].length) || - (propagations[1] && propagations[1].length) || - (conflicts && - (conflicts[0] && conflicts[0].length) || - (conflicts[1] && conflicts[1].length)))) { - this._snapshot = this._wrapNode(localBookmarks); + if (ret.propagations[0]) + clientChanges = ret.propagations[0]; + if (ret.propagations[1]) + serverChanges = ret.propagations[1]; + + if (ret.conflicts && ret.conflicts[0]) + clientConflicts = ret.conflicts[0]; + if (ret.conflicts && ret.conflicts[1]) + serverConflicts = ret.conflicts[1]; + + LOG("Changes for client: " + uneval(clientChanges)); + LOG("Changes for server: " + uneval(serverChanges)); + LOG("Client conflicts: " + uneval(clientConflicts)); + LOG("Server conflicts: " + uneval(serverConflicts)); + + if (!(clientChanges.length || serverChanges.length || + clientConflicts.length || serverConflicts.length)) { LOG("Sync complete (2): no changes needed on client or server"); + this._snapshot = this._wrapNode(localBookmarks); + this._snapshotVersion = server['version']; + this._saveSnapshot(); return; } - if (conflicts && conflicts[0] && conflicts[0].length) { + if (clientConflicts.length || serverConflicts.length) { LOG("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); - LOG("Conflicts(1) " + uneval(conflicts[0])); - } - - if (conflicts && conflicts[1] && conflicts[1].length) { - LOG("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); - LOG("Conflicts(2) " + uneval(conflicts[1])); } // 3.1) Apply server changes to local store - if (propagations[0] && propagations[0].length) { + if (clientChanges.length) { LOG("Applying changes locally"); + // Note that we need to need to apply client changes to the + // current tree, not the saved snapshot + this._snapshot = this._applyCommandsToObj(clientChanges, + this._wrapNode(localBookmarks)); + this._snapshotVersion = server['version']; + this._applyCommands(clientChanges); + // FIXME: should wrapNode to a separate variable and make sure + // that snapshot -> that is an empty diff. this._snapshot = this._wrapNode(localBookmarks); - propagations[0] = this._applyCommandsToObj(this._snapshot, propagations[0]); - this._applyCommands(propagations[0]); - this._snapshot = this._wrapNode(localBookmarks); + this._saveSnapshot(); } // 3.2) Append server delta to the delta file and upload - if (propagations[1] && propagations[1].length) { + if (serverChanges.length) { LOG("Uploading changes to server"); this._snapshot = this._wrapNode(localBookmarks); - this._snapshotVersion++; - server['deltas'][this._snapshotVersion] = propagations[1]; + this._snapshotVersion = server['version'] + 1; + server['deltas'][this._snapshotVersion] = serverChanges; this._dav.PUT("bookmarks.delta", uneval(server['deltas']), handlers); data = yield; - if (data.target.status >= 200 || data.target.status < 300) + if (data.target.status >= 200 || data.target.status < 300) { LOG("Successfully updated deltas on server"); - else + this._saveSnapshot(); + } else { + // FIXME: revert snapshot here? LOG("Error: could not update deltas on server"); + } } LOG("Sync complete"); } finally { @@ -710,8 +794,10 @@ BookmarksSyncService.prototype = { } else { LOG("Server delta can't update from our snapshot version, getting full file"); // generate updates from full local->remote snapshot diff - asyncRun(bind2(this, this._getServerUpdatesFull), - handlers['complete'], localJson); + let gsdf_gen = this._getServerUpdatesFull(handlers['complete'], + localJson); + gsdf_gen.next(); // must initialize before sending + gsdf_gen.send(gsdf_gen); data = yield; if (data.status == 2) { // we have a delta file but no snapshot on the server. bad. @@ -724,7 +810,6 @@ BookmarksSyncService.prototype = { tmp = this._applyCommandsToObj(data.updates, tmp); // fixme: this is duplicated from above, need to do some refactoring - var keys = []; for (var v in ret.deltas) { if (v > this._snapshotVersion) @@ -750,7 +835,9 @@ BookmarksSyncService.prototype = { case 404: LOG("Server has no delta file. Getting full bookmarks file from server"); // generate updates from full local->remote snapshot diff - asyncRun(bind2(this, this._getServerUpdatesFull), handlers['complete'], localJson); + let gsdf_gen = this._getServerUpdatesFull(handlers['complete'], localJson); + gsdf_gen.next(); // must initialize before sending + gsdf_gen.send(gsdf_gen); ret = yield; ret.deltas = {}; break; @@ -758,12 +845,12 @@ BookmarksSyncService.prototype = { LOG("Could not get bookmarks.delta: unknown HTTP status code " + data.target.status); break; } - onComplete(ret); + this._generatorDone(onComplete, ret) }, _getServerUpdatesFull: function BSS__getServerUpdatesFull(onComplete, localJson) { - var generator = yield; - var handlers = this._handlersForGenerator(generator); + let generator = yield; + let handlers = this._handlersForGenerator(generator); var ret = {status: -1, version: -1, updates: null}; @@ -797,16 +884,36 @@ BookmarksSyncService.prototype = { LOG("Could not get bookmarks.json: unknown HTTP status code " + data.target.status); break; } - onComplete(ret); + this._generatorDone(onComplete, ret); }, _handlersForGenerator: function BSS__handlersForGenerator(generator) { - var h = {load: bind2(this, function(event) { continueGenerator(generator, event); }), - error: bind2(this, function(event) { LOG("Request failed: " + uneval(event)); })}; + var h = {load: bind2(this, function(data) { continueGenerator(generator, data); }), + error: bind2(this, function(data) { LOG("Request failed: " + uneval(data)); })}; h['complete'] = h['load']; return h; }, + // generators called using asyncRun can't simply call the callback + // with the return value, since that would cause the calling + // function to end up running (after the yield) from inside the + // generator. Instead, generators can call this method which sets + // up a timer to call the callback from a timer (and cleans up the + // timer to avoid leaks). + _generatorDone: function BSS__generatorDone(callback, retval) { + if (this._timer) + throw "Called generatorDone when there is a timer already set." + + let cb = bind2(this, function(event) { + this._timer = null; + callback(retval); + }); + + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback(new EventListener(cb), + 0, this._timer.TYPE_ONE_SHOT); + }, + _onLogin: function BSS__onLogin(event) { LOG("Bookmarks sync server: " + this._dav.baseURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); @@ -829,7 +936,11 @@ BookmarksSyncService.prototype = { // nsIBookmarksSyncService - sync: function BSS_sync() { asyncRun(bind2(this, this._doSync)); }, + sync: function BSS_sync() { + let sync_gen = this._doSync(); + sync_gen.next(); // must initialize before sending + sync_gen.send(sync_gen); + }, login: function BSS_login() { this._dav.login({load: bind2(this, this._onLogin), @@ -865,16 +976,12 @@ function bind2(object, method) { return function innerBind() { return method.apply(object, arguments); } } -function asyncRun(func, handler, data) { - var generator = func(handler, data); - generator.next(); - generator.send(generator); -} + function continueGenerator(generator, data) { try { generator.send(data); } catch (e) { - generator.close(); + LOG("continueGenerator exception! - " + e); if (e instanceof StopIteration) generator = null; else From 8919304a9b6b16d2fbb977c03a956e70f913da22 Mon Sep 17 00:00:00 2001 From: Date: Tue, 25 Sep 2007 16:33:23 -0700 Subject: [PATCH 0016/1860] - super fun spinning throbber goodness! --- services/sync/nsBookmarksSyncService.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 73a462048374..9820fbcf0a06 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -601,6 +601,7 @@ BookmarksSyncService.prototype = { var generator = yield; var handlers = this._handlersForGenerator(generator); + this._os.notifyObservers(null, "bookmarks-sync:start", ""); LOG("Beginning sync"); try { @@ -620,9 +621,11 @@ BookmarksSyncService.prototype = { LOG("server: " + uneval(server)); if (server['status'] == 2) { + this._os.notifyObservers(null, "bookmarks-sync:end", ""); LOG("Sync complete"); return; } else if (server['status'] != 0 && server['status'] != 1) { + this._os.notifyObservers(null, "bookmarks-sync:end", ""); LOG("Sync error"); return; } @@ -636,6 +639,7 @@ BookmarksSyncService.prototype = { var localUpdates = this._detectUpdates(this._snapshot, localJson); LOG("updates: " + uneval(localUpdates)); if (!(server['status'] == 1 || localUpdates.length > 0)) { + this._os.notifyObservers(null, "bookmarks-sync:end", ""); LOG("Sync complete (1): no changes needed on client or server"); return; } @@ -673,6 +677,7 @@ BookmarksSyncService.prototype = { if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { + this._os.notifyObservers(null, "bookmarks-sync:end", ""); LOG("Sync complete (2): no changes needed on client or server"); this._snapshot = this._wrapNode(localBookmarks); this._snapshotVersion = server['version']; @@ -716,6 +721,7 @@ BookmarksSyncService.prototype = { LOG("Error: could not update deltas on server"); } } + this._os.notifyObservers(null, "bookmarks-sync:end", ""); LOG("Sync complete"); } finally { //this._dav.unlock(handlers); From 1bc67996871b25a8f52d062ad63de72e700b9db4 Mon Sep 17 00:00:00 2001 From: Date: Tue, 25 Sep 2007 17:19:01 -0700 Subject: [PATCH 0017/1860] wired up the "cancel" and "sync now" menu items, although they don't do a whole lot yet --- services/sync/nsBookmarksSyncService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 9820fbcf0a06..e6095dd8ffea 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -726,6 +726,7 @@ BookmarksSyncService.prototype = { } finally { //this._dav.unlock(handlers); //data = yield; + this._os.notifyObservers(null, "bookmarks-sync:end", ""); } }, From 4c7cb2bc07df16993c84a299b6cf42367c33f6c1 Mon Sep 17 00:00:00 2001 From: Date: Wed, 26 Sep 2007 12:08:53 -0700 Subject: [PATCH 0018/1860] added preferences pane to main pref dialog and wired everything up (except the password bit) added autoconnect pref and actions removing sync-engine.js as it doesn't look like we're using it anymore --- services/sync/services-sync.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 7c58c3749e1e..0b00a722c36a 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -2,4 +2,5 @@ pref("browser.places.sync.serverURL", "http://dotmoz.mozilla.org/"); pref("browser.places.sync.username", "nobody@mozilla.com"); pref("extensions.sync.lastversion", "firstrun"); pref("extensions.sync.lastsync", ""); +pref("extensions.sync.remember", false); From 82dfb27579b8761a42268a59a04c42e922eb0f2e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Sep 2007 15:28:01 -0700 Subject: [PATCH 0019/1860] unify logging routines, log via the component. Log to a file. Add a couple of crappy attempts at nicer logging output --- services/sync/nsBookmarksSyncService.js | 257 +++++++++++++++------- services/sync/nsIBookmarksSyncService.idl | 10 +- services/sync/nsIBookmarksSyncService.xpt | Bin 383 -> 213 bytes 3 files changed, 186 insertions(+), 81 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index e6095dd8ffea..736f250ed0d4 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -11,7 +11,7 @@ * for the specific language governing rights and limitations under the * License. * - * The Original Code is Places. + * The Original Code is Bookmarks Sync. * * The Initial Developer of the Original Code is Mozilla. * Portions created by the Initial Developer are Copyright (C) 2007 @@ -85,6 +85,25 @@ BookmarksSyncService.prototype = { return this.__os; }, + __console: null, + get _console() { + if (!this.__console) + this.__console = Cc["@mozilla.org/consoleservice;1"] + .getService(Ci.nsIConsoleService); + return this.__console; + }, + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + return this.__dirSvc; + }, + + // File output stream to a log file + _log: null, + // Timer object for yielding to the main loop _timer: null, @@ -101,6 +120,7 @@ BookmarksSyncService.prototype = { }, _init: function BSS__init() { + this._initLog(); let serverURL = 'https://dotmoz.mozilla.org/'; try { let branch = Cc["@mozilla.org/preferences-service;1"]. @@ -108,11 +128,33 @@ BookmarksSyncService.prototype = { serverURL = branch.getCharPref("browser.places.sync.serverURL"); } catch (ex) { /* use defaults */ } - LOG("Bookmarks login server: " + serverURL); + this.notice("Bookmarks login server: " + serverURL); this._dav = new DAVCollection(serverURL); this._readSnapshot(); }, + _initLog: function BSS__initLog() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + + let file = dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync.log"); + file.QueryInterface(Ci.nsILocalFile); + + if (file.exists()) + file.remove(false); + file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); + + this._log = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; + this._log.init(file, flags, PERMS_FILE, 0); + + let str = "Bookmarks Sync Log\n------------------\n\n"; + this._log.write(str, str.length); + this._log.flush(); + }, + _saveSnapshot: function BSS__saveSnapshot() { let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); @@ -132,7 +174,6 @@ BookmarksSyncService.prototype = { let out = {version: this._snapshotVersion, snapshot: this._snapshot}; out = uneval(out); fos.write(out, out.length); - fos.flush(); fos.close(); }, @@ -221,7 +262,7 @@ BookmarksSyncService.prototype = { }, _nodeParentsInt: function BSS__nodeParentsInt(guid, tree, parents) { - if (!tree[guid] || tree[guid].parentGuid == null) + if (tree[guid] && tree[guid].parentGuid == null) return parents; parents.push(tree[guid].parentGuid); return this._nodeParentsInt(tree[guid].parentGuid, tree, parents); @@ -313,7 +354,7 @@ BookmarksSyncService.prototype = { return true; return false; default: - LOG("_commandLike: Unknown item type: " + uneval(a)); + this.notice("_commandLike: Unknown item type: " + uneval(a)); return false; } }, @@ -438,12 +479,12 @@ BookmarksSyncService.prototype = { // shutdown protocol let cookie = yield; if (cookie != "generator shutdown") - LOG("_reconcile: Error: generator not properly shut down.") + this.notice("_reconcile: Error: generator not properly shut down.") }, _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { for (let i = 0; i < commands.length; i++) { - LOG("Applying cmd to obj: " + uneval(commands[i])); + this.notice("Applying cmd to obj: " + uneval(commands[i])); switch (commands[i].action) { case "create": obj[commands[i].guid] = eval(uneval(commands[i].data)); @@ -465,7 +506,7 @@ BookmarksSyncService.prototype = { _applyCommands: function BSS__applyCommands(commandList) { for (var i = 0; i < commandList.length; i++) { var command = commandList[i]; - LOG("Processing command: " + uneval(command)); + this.notice("Processing command: " + uneval(command)); switch (command["action"]) { case "create": this._createCommand(command); @@ -477,7 +518,7 @@ BookmarksSyncService.prototype = { this._editCommand(command); break; default: - LOG("unknown action in command: " + command["action"]); + this.notice("unknown action in command: " + command["action"]); break; } } @@ -488,11 +529,11 @@ BookmarksSyncService.prototype = { let parentId = this._bms.getItemIdForGUID(command.data.parentGuid); if (parentId <= 0) { - LOG("Warning: creating node with unknown parent -> reparenting to root"); + this.notice("Warning: creating node with unknown parent -> reparenting to root"); parentId = this._bms.bookmarksRoot; } - LOG(" -> creating item"); + this.notice(" -> creating item"); switch (command.data.type) { case 0: @@ -510,7 +551,7 @@ BookmarksSyncService.prototype = { newId = this._bms.insertSeparator(parentId, command.data.index); break; default: - LOG("_createCommand: Unknown item type: " + command.data.type); + this.notice("_createCommand: Unknown item type: " + command.data.type); break; } if (newId) @@ -524,19 +565,19 @@ BookmarksSyncService.prototype = { switch (type) { case this._bms.TYPE_BOOKMARK: // FIXME: check it's an actual bookmark? - LOG(" -> removing bookmark " + command.guid); + this.notice(" -> removing bookmark " + command.guid); this._bms.removeItem(itemId); break; case this._bms.TYPE_FOLDER: - LOG(" -> removing folder " + command.guid); + this.notice(" -> removing folder " + command.guid); this._bms.removeFolder(itemId); break; case this._bms.TYPE_SEPARATOR: - LOG(" -> removing separator " + command.guid); + this.notice(" -> removing separator " + command.guid); this._bms.removeItem(itemId); break; default: - LOG("removeCommand: Unknown item type: " + type); + this.notice("removeCommand: Unknown item type: " + type); break; } }, @@ -544,7 +585,7 @@ BookmarksSyncService.prototype = { _editCommand: function BSS__editCommand(command) { var itemId = this._bms.getItemIdForGUID(command.guid); if (itemId == -1) { - LOG("Warning: item for guid " + command.guid + " not found. Skipping."); + this.notice("Warning: item for guid " + command.guid + " not found. Skipping."); return; } @@ -557,7 +598,7 @@ BookmarksSyncService.prototype = { if (existing == -1) this._bms.setItemGUID(itemId, command.data.guid); else - LOG("Warning: can't change guid " + command.guid + + this.notice("Warning: can't change guid " + command.guid + " to " + command.data.guid + ": guid already exists."); break; case "title": @@ -575,7 +616,7 @@ BookmarksSyncService.prototype = { itemId, this._bms.getItemIdForGUID(command.data.parentGuid), -1); break; default: - LOG("Warning: Can't change item property: " + key); + this.notice("Warning: Can't change item property: " + key); break; } } @@ -589,6 +630,57 @@ BookmarksSyncService.prototype = { return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; }, + // FIXME: these print functions need some love... + _mungeJSON: function BSS__mungeJSON(json) { + json.replace(":{type", ":\n\t{type"); + json.replace(", ", ",\n\t"); + return json; + }, + + _printNodes: function BSS__printNodes(nodes) { + let nodeList = []; + for (let guid in nodes) { + switch (nodes[guid].type) { + case 0: + nodeList.push(nodes[guid].parentGuid + " b " + guid + "\n\t" + + nodes[guid].title + nodes[guid].uri); + break; + case 6: + nodeList.push(nodes[guid].parentGuid + " f " + guid + "\n\t" + + nodes[guid].title); + break; + case 7: + nodeList.push(nodes[guid].parentGuid + " s " + guid); + break; + default: + nodeList.push("error: unknown item type!\n" + uneval(nodes[guid])); + break; + } + } + nodeList.sort(); + return nodeList.join("\n"); + }, + + _printCommands: function BSS__printCommands(commands) { + let ret = []; + for (let i = 0; i < commands.length; i++) { + switch (commands[i].action) { + case "create": + ret.push("create"); + break; + case "edit": + ret.push(); + break; + case "remove": + ret.push(); + break; + default: + ret.push("error: unknown command action!\n" + uneval(commands[i])); + break; + } + } + }, + // 1) Fetch server deltas // 1.1) Construct current server status from snapshot + server deltas // 1.2) Generate single delta from snapshot -> current server status @@ -602,7 +694,7 @@ BookmarksSyncService.prototype = { var handlers = this._handlersForGenerator(generator); this._os.notifyObservers(null, "bookmarks-sync:start", ""); - LOG("Beginning sync"); + this.notice("Beginning sync"); try { //this._dav.lock(handlers); @@ -611,7 +703,7 @@ BookmarksSyncService.prototype = { var localBookmarks = this._getBookmarks(); var localJson = this._wrapNode(localBookmarks); - LOG("local json: " + uneval(localJson)); + this.notice("local json:\n" + this._mungeJSON(uneval(localJson))); // 1) Fetch server deltas let gsd_gen = this._getServerData(handlers['complete'], localJson); @@ -619,34 +711,34 @@ BookmarksSyncService.prototype = { gsd_gen.send(gsd_gen); let server = yield; - LOG("server: " + uneval(server)); + this.notice("server: " + uneval(server)); if (server['status'] == 2) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); - LOG("Sync complete"); + this.notice("Sync complete"); return; } else if (server['status'] != 0 && server['status'] != 1) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); - LOG("Sync error"); + this.notice("Sync error"); return; } - LOG("Local snapshot version: " + this._snapshotVersion); - LOG("Latest server version: " + server['version']); + this.notice("Local snapshot version: " + this._snapshotVersion); + this.notice("Latest server version: " + server['version']); // 2) Generate local deltas from snapshot -> current client status - LOG("Generating local updates"); + this.notice("Generating local updates"); var localUpdates = this._detectUpdates(this._snapshot, localJson); - LOG("updates: " + uneval(localUpdates)); + this.notice("updates: " + uneval(localUpdates)); if (!(server['status'] == 1 || localUpdates.length > 0)) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); - LOG("Sync complete (1): no changes needed on client or server"); + this.notice("Sync complete (1): no changes needed on client or server"); return; } // 3) Reconcile client/server deltas and generate new deltas for them. - LOG("Reconciling updates"); + this.notice("Reconciling updates"); let callback = function(retval) { continueGenerator(generator, retval); }; let rec_gen = this._reconcile(callback, [localUpdates, server.updates]); rec_gen.next(); // must initialize before sending @@ -670,15 +762,15 @@ BookmarksSyncService.prototype = { if (ret.conflicts && ret.conflicts[1]) serverConflicts = ret.conflicts[1]; - LOG("Changes for client: " + uneval(clientChanges)); - LOG("Changes for server: " + uneval(serverChanges)); - LOG("Client conflicts: " + uneval(clientConflicts)); - LOG("Server conflicts: " + uneval(serverConflicts)); + this.notice("Changes for client: " + uneval(clientChanges)); + this.notice("Changes for server: " + uneval(serverChanges)); + this.notice("Client conflicts: " + uneval(clientConflicts)); + this.notice("Server conflicts: " + uneval(serverConflicts)); if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); - LOG("Sync complete (2): no changes needed on client or server"); + this.notice("Sync complete (2): no changes needed on client or server"); this._snapshot = this._wrapNode(localBookmarks); this._snapshotVersion = server['version']; this._saveSnapshot(); @@ -686,12 +778,12 @@ BookmarksSyncService.prototype = { } if (clientConflicts.length || serverConflicts.length) { - LOG("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); + this.notice("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); } // 3.1) Apply server changes to local store if (clientChanges.length) { - LOG("Applying changes locally"); + this.notice("Applying changes locally"); // Note that we need to need to apply client changes to the // current tree, not the saved snapshot this._snapshot = this._applyCommandsToObj(clientChanges, @@ -706,7 +798,7 @@ BookmarksSyncService.prototype = { // 3.2) Append server delta to the delta file and upload if (serverChanges.length) { - LOG("Uploading changes to server"); + this.notice("Uploading changes to server"); this._snapshot = this._wrapNode(localBookmarks); this._snapshotVersion = server['version'] + 1; server['deltas'][this._snapshotVersion] = serverChanges; @@ -714,15 +806,15 @@ BookmarksSyncService.prototype = { data = yield; if (data.target.status >= 200 || data.target.status < 300) { - LOG("Successfully updated deltas on server"); + this.notice("Successfully updated deltas on server"); this._saveSnapshot(); } else { // FIXME: revert snapshot here? - LOG("Error: could not update deltas on server"); + this.notice("Error: could not update deltas on server"); } } this._os.notifyObservers(null, "bookmarks-sync:end", ""); - LOG("Sync complete"); + this.notice("Sync complete"); } finally { //this._dav.unlock(handlers); //data = yield; @@ -752,27 +844,27 @@ BookmarksSyncService.prototype = { var ret = {status: -1, version: -1, deltas: null, updates: null}; - LOG("Getting bookmarks delta from server"); + this.notice("Getting bookmarks delta from server"); this._dav.GET("bookmarks.delta", handlers); var data = yield; switch (data.target.status) { case 200: - LOG("Got bookmarks delta from server"); + this.notice("Got bookmarks delta from server"); ret.deltas = eval(data.target.responseText); var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack // FIXME: debug here for conditional below... - LOG("[sync bowels] local version: " + this._snapshotVersion); + this.notice("[sync bowels] local version: " + this._snapshotVersion); for (var z in ret.deltas) { - LOG("[sync bowels] remote version: " + z); + this.notice("[sync bowels] remote version: " + z); } - LOG("foo: " + uneval(ret.deltas[this._snapshotVersion + 1])); + this.notice("foo: " + uneval(ret.deltas[this._snapshotVersion + 1])); if (ret.deltas[this._snapshotVersion + 1]) - LOG("-> is true"); + this.notice("-> is true"); else - LOG("-> is false"); + this.notice("-> is false"); if (ret.deltas[this._snapshotVersion + 1]) { // Merge the matching deltas into one, find highest version @@ -784,22 +876,22 @@ BookmarksSyncService.prototype = { ret.version = v; } keys = keys.sort(); - LOG("TMP: " + uneval(tmp)); + this.notice("TMP: " + uneval(tmp)); for (var i = 0; i < keys.length; i++) { tmp = this._applyCommandsToObj(ret.deltas[keys[i]], tmp); - LOG("TMP: " + uneval(tmp)); + this.notice("TMP: " + uneval(tmp)); } ret.status = 1; ret["updates"] = this._detectUpdates(this._snapshot, tmp); } else if (ret.deltas[this._snapshotVersion]) { - LOG("No changes from server"); + this.notice("No changes from server"); ret.status = 0; ret.version = this._snapshotVersion; ret.updates = []; } else { - LOG("Server delta can't update from our snapshot version, getting full file"); + this.notice("Server delta can't update from our snapshot version, getting full file"); // generate updates from full local->remote snapshot diff let gsdf_gen = this._getServerUpdatesFull(handlers['complete'], localJson); @@ -809,7 +901,7 @@ BookmarksSyncService.prototype = { if (data.status == 2) { // we have a delta file but no snapshot on the server. bad. // fixme? - LOG("Error: Delta file on server, but snapshot file missing. " + + this.notice("Error: Delta file on server, but snapshot file missing. " + "New snapshot uploaded, may be inconsistent with deltas!"); } @@ -840,7 +932,7 @@ BookmarksSyncService.prototype = { } break; case 404: - LOG("Server has no delta file. Getting full bookmarks file from server"); + this.notice("Server has no delta file. Getting full bookmarks file from server"); // generate updates from full local->remote snapshot diff let gsdf_gen = this._getServerUpdatesFull(handlers['complete'], localJson); gsdf_gen.next(); // must initialize before sending @@ -849,7 +941,7 @@ BookmarksSyncService.prototype = { ret.deltas = {}; break; default: - LOG("Could not get bookmarks.delta: unknown HTTP status code " + data.target.status); + this.notice("Could not get bookmarks.delta: unknown HTTP status code " + data.target.status); break; } this._generatorDone(onComplete, ret) @@ -866,14 +958,14 @@ BookmarksSyncService.prototype = { switch (data.target.status) { case 200: - LOG("Got full bookmarks file from server"); + this.notice("Got full bookmarks file from server"); var tmp = eval(data.target.responseText); ret.status = 1; ret.updates = this._detectUpdates(this._snapshot, tmp.snapshot); ret.version = tmp.version; break; case 404: - LOG("No bookmarks on server. Starting initial sync to server"); + this.notice("No bookmarks on server. Starting initial sync to server"); this._snapshot = localJson; this._snapshotVersion = 1; @@ -881,14 +973,14 @@ BookmarksSyncService.prototype = { data = yield; if (data.target.status >= 200 || data.target.status < 300) { - LOG("Initial sync to server successful"); + this.notice("Initial sync to server successful"); ret.status = 2; } else { - LOG("Initial sync to server failed"); + this.notice("Initial sync to server failed"); } break; default: - LOG("Could not get bookmarks.json: unknown HTTP status code " + data.target.status); + this.notice("Could not get bookmarks.json: unknown HTTP status code " + data.target.status); break; } this._generatorDone(onComplete, ret); @@ -896,7 +988,7 @@ BookmarksSyncService.prototype = { _handlersForGenerator: function BSS__handlersForGenerator(generator) { var h = {load: bind2(this, function(data) { continueGenerator(generator, data); }), - error: bind2(this, function(data) { LOG("Request failed: " + uneval(data)); })}; + error: bind2(this, function(data) { this.notice("Request failed: " + uneval(data)); })}; h['complete'] = h['load']; return h; }, @@ -922,7 +1014,7 @@ BookmarksSyncService.prototype = { }, _onLogin: function BSS__onLogin(event) { - LOG("Bookmarks sync server: " + this._dav.baseURL); + this.notice("Bookmarks sync server: " + this._dav.baseURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); }, @@ -957,6 +1049,20 @@ BookmarksSyncService.prototype = { logout: function BSS_logout() { this._dav.logout(); this._os.notifyObservers(null, "bookmarks-sync:logout", ""); + }, + + log: function BSS_log(line) { + }, + + notice: function BSS_notice(line) { + let fullLine = line + "\n"; + dump(fullLine); + this._console.logStringMessage(line); + this._log.write(fullLine, fullLine.length); + this._log.flush(); + }, + + error: function BSS_error(line) { } }; @@ -972,23 +1078,14 @@ function makeURI(uriString) { return ioservice.newURI(uriString, null, null); } -function LOG(aText) { - dump(aText + "\n"); - var consoleService = Cc["@mozilla.org/consoleservice;1"]. - getService(Ci.nsIConsoleService); - consoleService.logStringMessage(aText); -} - function bind2(object, method) { return function innerBind() { return method.apply(object, arguments); } } - - function continueGenerator(generator, data) { try { generator.send(data); } catch (e) { - LOG("continueGenerator exception! - " + e); + //notice("continueGenerator exception! - " + e); if (e instanceof StopIteration) generator = null; else @@ -1118,7 +1215,7 @@ DAVCollection.prototype = { let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); let logins = lm.findLogins({}, uri.hostPort, null, 'Use your ldap username/password - dotmoz'); - LOG("Found " + logins.length + " logins"); + //notice("Found " + logins.length + " logins"); for (let i = 0; i < logins.length; i++) { if (logins[i].username == username) { @@ -1145,7 +1242,7 @@ DAVCollection.prototype = { }, _onLogin: function DC__onLogin(event) { - //LOG("logged in (" + event.target.status + "):\n" + + //notice("logged in (" + event.target.status + "):\n" + // event.target.responseText + "\n"); if (this._authProvider._authFailed || event.target.status >= 400) { @@ -1168,8 +1265,8 @@ DAVCollection.prototype = { this._loginHandlers.load(event); }, _onLoginError: function DC__onLoginError(event) { - LOG("login failed (" + event.target.status + "):\n" + - event.target.responseText + "\n"); + //notice("login failed (" + event.target.status + "):\n" + + // event.target.responseText + "\n"); this._loggedIn = false; @@ -1204,25 +1301,25 @@ DAVCollection.prototype = { }, _onLock: function DC__onLock(event) { - LOG("acquired lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); + //notice("acquired lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); this._token = "woo"; if (this._lockHandlers && this._lockHandlers.load) this._lockHandlers.load(event); }, _onLockError: function DC__onLockError(event) { - LOG("lock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); + //notice("lock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); if (this._lockHandlers && this._lockHandlers.error) this._lockHandlers.error(event); }, _onUnlock: function DC__onUnlock(event) { - LOG("removed lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); + //notice("removed lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); this._token = null; if (this._lockHandlers && this._lockHandlers.load) this._lockHandlers.load(event); }, _onUnlockError: function DC__onUnlockError(event) { - LOG("unlock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); + //notice("unlock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); if (this._lockHandlers && this._lockHandlers.error) this._lockHandlers.error(event); } diff --git a/services/sync/nsIBookmarksSyncService.idl b/services/sync/nsIBookmarksSyncService.idl index 395ca8c17178..0629126f3b2f 100644 --- a/services/sync/nsIBookmarksSyncService.idl +++ b/services/sync/nsIBookmarksSyncService.idl @@ -38,7 +38,7 @@ #include "nsISupports.idl" -[scriptable, uuid(b3e52c09-5c33-4d07-a3e6-7c453d0c4be8)] +[scriptable, uuid(0a1eed46-f832-495c-bce7-cb308ccfd599)] interface nsIBookmarksSyncService : nsISupports { /** @@ -60,4 +60,12 @@ interface nsIBookmarksSyncService : nsISupports * Initiate a sync operation. */ void sync(); + + /** + * Write a notice to the logfile + * + * @param line + * The line of text to print to the logfile + */ + void notice(in AString line); }; diff --git a/services/sync/nsIBookmarksSyncService.xpt b/services/sync/nsIBookmarksSyncService.xpt index 47791d8fa7895d586a0a173ee104c6055d05906a..29a8d498b5173151f789218d2594ef7d2cd517cc 100644 GIT binary patch delta 139 zcmey*bd^ydBEZ?-mn)>QAk`-`iI+=?k%@tcfq~&F5Gw(3P(Af*5R literal 383 zcmazDaQ64*3aKne^~p@)<&t7#Vqj)qV610gU{C_$(gq*_1_vON8<1jzFc}z1yzZ0`<`sJemlhP{7nKw< z0NGCY`PsRNMcKu{m3hhjNyVu}WvN9VYgj-Ud>I(nKy)aSj)T%!V7ehc&nG`UGmimG z=a-fM86abvQXzsMwsU@NK~8E(Du!LbK$|j?Q$bd-H2|%+#JFN7#Ik!(OP)aK*HHQc zW5Xl{CWx9paCSp-X;D#XUP&m>=?pnw=Kx8NV~T;+GbE;@z Date: Wed, 26 Sep 2007 17:34:36 -0700 Subject: [PATCH 0020/1860] added wizard to setup service and get login information for new users updated server pointers to point to the new services.mozilla.com backend --- services/sync/nsBookmarksSyncService.js | 32 ++++++++++++++++++------- services/sync/services-sync.js | 8 +++---- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index e6095dd8ffea..e62778b27b9b 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -1120,7 +1120,7 @@ DAVCollection.prototype = { 'Use your ldap username/password - dotmoz'); LOG("Found " + logins.length + " logins"); - for (let i = 0; i < logins.length; i++) { + for (let i = 0; i < logins.length; i++) { if (logins[i].username == username) { password = logins[i].password; break; @@ -1154,14 +1154,30 @@ DAVCollection.prototype = { } // fixme: hacktastic - let hello = /Hello (.+)@mozilla.com/.exec(event.target.responseText) - if (hello) { - this._currentUserPath = hello[1]; - this._currentUser = this._currentUserPath + "@mozilla.com"; - this._baseURL = "http://dotmoz.mozilla.org/~" + - this._currentUserPath + "/"; - } +// let hello = /Hello (.+)@mozilla.com/.exec(event.target.responseText) +// let hello = /Index of/.exec(event.target.responseText) +// if (hello) { +// this._currentUserPath = hello[1]; +// this._currentUser = this._currentUserPath + "@mozilla.com"; +// this._baseURL = "http://dotmoz.mozilla.org/~" + +// this._currentUserPath + "/"; +// } + // XXX we need to refine some server response codes + let branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + this._currentUser = branch.getCharPref("browser.places.sync.username"); + + // XXX + let path = this._currentUser.split("@"); + this._currentUserPath = path[0]; + +LOG("currentUser: "+this._currentUser); +LOG("currentUSerPath: "+this._currentUserPath); + + let serverURL = branch.getCharPref("browser.places.sync.serverURL"); + this._baseURL = serverURL + this._currentUserPath + "/"; + this._loggedIn = true; if (this._loginHandlers && this._loginHandlers.load) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 0b00a722c36a..9767dc7ea7b1 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,6 +1,4 @@ -pref("browser.places.sync.serverURL", "http://dotmoz.mozilla.org/"); +pref("browser.places.sync.serverURL", "https://services.mozilla.com/user/"); pref("browser.places.sync.username", "nobody@mozilla.com"); -pref("extensions.sync.lastversion", "firstrun"); -pref("extensions.sync.lastsync", ""); -pref("extensions.sync.remember", false); - +pref("extensions.sync.autoconnect", false); +pref("extensions.sync.lastsync", "0"); From 7297a5fba256dde457f56738f74d0b1af9b153dd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Sep 2007 19:25:55 -0700 Subject: [PATCH 0021/1860] activity log fixes --- services/sync/nsBookmarksSyncService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 736f250ed0d4..a57e8dc8397b 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -198,6 +198,7 @@ BookmarksSyncService.prototype = { fis.readLine(ret); json += ret.value; } + fis.close(); json = eval(json); if (json.snapshot && json.version) { From 2731de31b4ba27216373349ec640375af3cda7ca Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 27 Sep 2007 14:40:21 -0700 Subject: [PATCH 0022/1860] better log viewer performance thanks to chris; small bugfixes in the component; temporarily disable some very verbose logging output --- services/sync/nsBookmarksSyncService.js | 34 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index e5c6cf71c17c..463d366def88 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -110,8 +110,7 @@ BookmarksSyncService.prototype = { // DAVCollection object _dav: null, - // Last synced tree - // FIXME: this should be serialized to disk + // Last synced tree and version _snapshot: {}, _snapshotVersion: 0, @@ -263,7 +262,7 @@ BookmarksSyncService.prototype = { }, _nodeParentsInt: function BSS__nodeParentsInt(guid, tree, parents) { - if (tree[guid] && tree[guid].parentGuid == null) + if (!tree[guid] || !tree[guid].parentGuid) return parents; parents.push(tree[guid].parentGuid); return this._nodeParentsInt(tree[guid].parentGuid, tree, parents); @@ -317,7 +316,6 @@ BookmarksSyncService.prototype = { return true; if ((a.guid == b.guid) && !this._deepEquals(a, b)) return true; -// FIXME - how else can two commands conflict? return false; }, @@ -608,13 +606,16 @@ BookmarksSyncService.prototype = { case "uri": this._bms.changeBookmarkURI(itemId, makeURI(command.data.uri)); break; - case "index": // FIXME: what if we do this one before parentGuid ? that'd be wrong + case "index": this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), command.data.index); break; case "parentGuid": + let index = -1; + if (command.data.index && command.data.index >= 0) + index = command.data.index; this._bms.moveItem( - itemId, this._bms.getItemIdForGUID(command.data.parentGuid), -1); + itemId, this._bms.getItemIdForGUID(command.data.parentGuid), index); break; default: this.notice("Warning: Can't change item property: " + key); @@ -791,9 +792,15 @@ BookmarksSyncService.prototype = { this._wrapNode(localBookmarks)); this._snapshotVersion = server['version']; this._applyCommands(clientChanges); - // FIXME: should wrapNode to a separate variable and make sure - // that snapshot -> that is an empty diff. - this._snapshot = this._wrapNode(localBookmarks); + + let newSnapshot = this._wrapNode(localBookmarks); + let diff = this._detectUpdates(this._snapshot, newSnapshot); + if (diff.length != 0) { + this.notice("Error: commands did not apply correctly. Diff:\n" + + uneval(diff)); + this._snapshot = this._wrapNode(localBookmarks); + // FIXME: What else can we do? + } this._saveSnapshot(); } @@ -810,7 +817,8 @@ BookmarksSyncService.prototype = { this.notice("Successfully updated deltas on server"); this._saveSnapshot(); } else { - // FIXME: revert snapshot here? + // FIXME: revert snapshot here? - can't, we already applied + // updates locally! - need to save and retry this.notice("Error: could not update deltas on server"); } } @@ -857,6 +865,7 @@ BookmarksSyncService.prototype = { var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack // FIXME: debug here for conditional below... + /* this.notice("[sync bowels] local version: " + this._snapshotVersion); for (var z in ret.deltas) { this.notice("[sync bowels] remote version: " + z); @@ -866,6 +875,7 @@ BookmarksSyncService.prototype = { this.notice("-> is true"); else this.notice("-> is false"); + */ if (ret.deltas[this._snapshotVersion + 1]) { // Merge the matching deltas into one, find highest version @@ -877,10 +887,10 @@ BookmarksSyncService.prototype = { ret.version = v; } keys = keys.sort(); - this.notice("TMP: " + uneval(tmp)); + //this.notice("TMP: " + uneval(tmp)); for (var i = 0; i < keys.length; i++) { tmp = this._applyCommandsToObj(ret.deltas[keys[i]], tmp); - this.notice("TMP: " + uneval(tmp)); + //this.notice("TMP: " + uneval(tmp)); } ret.status = 1; ret["updates"] = this._detectUpdates(this._snapshot, tmp); From d9bd5c39edd9ca535d910b8d7fcc5c5a60e90ffc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 28 Sep 2007 03:02:15 -0700 Subject: [PATCH 0023/1860] sync engine fixes; logging improvements; try to ensure that property keys are always strings --- services/sync/nsBookmarksSyncService.js | 188 ++++++++++++------------ 1 file changed, 90 insertions(+), 98 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 463d366def88..7a771b97287c 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -155,6 +155,8 @@ BookmarksSyncService.prototype = { }, _saveSnapshot: function BSS__saveSnapshot() { + this.notice("Saving snapshot to disk"); + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); @@ -329,6 +331,11 @@ BookmarksSyncService.prototype = { if (a.action != b.action) return false; + // Items with the same guid do not qualify - they need to be + // processed for edits + if (a.guid == b.guid) + return false; + // this check works because reconcile() fixes up the parent guids // as it runs, and the command list is sorted by depth if (a.parentGuid != b.parentGuid) @@ -429,6 +436,9 @@ BookmarksSyncService.prototype = { delete listA[i]; delete listB[j]; } else if (this._commandLike(listA[i], listB[j])) { + // Disregard likeness if the target guid already exists locally + if (this._bms.getItemIdForGUID(listB[j].guid) >= 0) + continue; this._fixParents(listA, listA[i].guid, listB[j].guid); listB[j].data = {guid: listB[j].guid}; listB[j].guid = listA[i].guid; @@ -629,58 +639,33 @@ BookmarksSyncService.prototype = { folder = this._bms.bookmarksRoot; var query = this._hsvc.getNewQuery(); query.setFolders([folder], 1); - return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + let root = this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + return this._wrapNode(root); }, - // FIXME: these print functions need some love... - _mungeJSON: function BSS__mungeJSON(json) { - json.replace(":{type", ":\n\t{type"); - json.replace(", ", ",\n\t"); + _mungeNodes: function BSS__mungeNodes(nodes) { + let json = uneval(nodes); + json = json.replace(/:{type/g, ":\n\t{type"); + json = json.replace(/}, /g, "},\n "); + json = json.replace(/, parentGuid/g, ",\n\t parentGuid"); + json = json.replace(/, index/g, ",\n\t index"); + json = json.replace(/, title/g, ",\n\t title"); + json = json.replace(/, uri/g, ",\n\t uri"); return json; }, - _printNodes: function BSS__printNodes(nodes) { - let nodeList = []; - for (let guid in nodes) { - switch (nodes[guid].type) { - case 0: - nodeList.push(nodes[guid].parentGuid + " b " + guid + "\n\t" + - nodes[guid].title + nodes[guid].uri); - break; - case 6: - nodeList.push(nodes[guid].parentGuid + " f " + guid + "\n\t" + - nodes[guid].title); - break; - case 7: - nodeList.push(nodes[guid].parentGuid + " s " + guid); - break; - default: - nodeList.push("error: unknown item type!\n" + uneval(nodes[guid])); - break; - } - } - nodeList.sort(); - return nodeList.join("\n"); + _mungeCommands: function BSS__mungeCommands(commands) { + let json = uneval(commands); + json = json.replace(/ {action/g, "\n {action"); + //json = json.replace(/, data/g, ",\n data"); + return json; }, - _printCommands: function BSS__printCommands(commands) { - let ret = []; - for (let i = 0; i < commands.length; i++) { - switch (commands[i].action) { - case "create": - ret.push("create"); - break; - case "edit": - ret.push(); - break; - case "remove": - ret.push(); - break; - default: - ret.push("error: unknown command action!\n" + uneval(commands[i])); - break; - } - } + _mungeConflicts: function BSS__mungeConflicts(conflicts) { + let json = uneval(conflicts); + json = json.replace(/ {action/g, "\n {action"); + //json = json.replace(/, data/g, ",\n data"); + return json; }, // 1) Fetch server deltas @@ -703,9 +688,8 @@ BookmarksSyncService.prototype = { //var data = yield; var data; - var localBookmarks = this._getBookmarks(); - var localJson = this._wrapNode(localBookmarks); - this.notice("local json:\n" + this._mungeJSON(uneval(localJson))); + var localJson = this._getBookmarks(); + this.notice("local json:\n" + this._mungeNodes(localJson)); // 1) Fetch server deltas let gsd_gen = this._getServerData(handlers['complete'], localJson); @@ -713,7 +697,16 @@ BookmarksSyncService.prototype = { gsd_gen.send(gsd_gen); let server = yield; - this.notice("server: " + uneval(server)); + this.notice("Server status: " + server.status); + this.notice("Server version: " + server.version); + this.notice("Server version type: " + typeof server.version); + this.notice("Local snapshot version: " + this._snapshotVersion); + + for (version in server.deltas) { + this.notice("Server delta " + version + ":\n" + + this._mungeCommands(server.deltas[version])); + } + if (server['status'] == 2) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); this.notice("Sync complete"); @@ -724,14 +717,10 @@ BookmarksSyncService.prototype = { return; } - this.notice("Local snapshot version: " + this._snapshotVersion); - this.notice("Latest server version: " + server['version']); - // 2) Generate local deltas from snapshot -> current client status - this.notice("Generating local updates"); - var localUpdates = this._detectUpdates(this._snapshot, localJson); - this.notice("updates: " + uneval(localUpdates)); + let localUpdates = this._detectUpdates(this._snapshot, localJson); + this.notice("Local updates: " + this._mungeCommands(localUpdates)); if (!(server['status'] == 1 || localUpdates.length > 0)) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); this.notice("Sync complete (1): no changes needed on client or server"); @@ -740,7 +729,7 @@ BookmarksSyncService.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. - this.notice("Reconciling updates"); + this.notice("Reconciling client/server updates"); let callback = function(retval) { continueGenerator(generator, retval); }; let rec_gen = this._reconcile(callback, [localUpdates, server.updates]); rec_gen.next(); // must initialize before sending @@ -764,17 +753,17 @@ BookmarksSyncService.prototype = { if (ret.conflicts && ret.conflicts[1]) serverConflicts = ret.conflicts[1]; - this.notice("Changes for client: " + uneval(clientChanges)); - this.notice("Changes for server: " + uneval(serverChanges)); - this.notice("Client conflicts: " + uneval(clientConflicts)); - this.notice("Server conflicts: " + uneval(serverConflicts)); + this.notice("Changes for client: " + this._mungeCommands(clientChanges)); + this.notice("Changes for server: " + this._mungeCommands(serverChanges)); + this.notice("Client conflicts: " + this._mungeConflicts(clientConflicts)); + this.notice("Server conflicts: " + this._mungeConflicts(serverConflicts)); if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); this.notice("Sync complete (2): no changes needed on client or server"); - this._snapshot = this._wrapNode(localBookmarks); - this._snapshotVersion = server['version']; + this._snapshot = localJson; + this._snapshotVersion = server.version; this._saveSnapshot(); return; } @@ -788,17 +777,17 @@ BookmarksSyncService.prototype = { this.notice("Applying changes locally"); // Note that we need to need to apply client changes to the // current tree, not the saved snapshot - this._snapshot = this._applyCommandsToObj(clientChanges, - this._wrapNode(localBookmarks)); + + this._snapshot = this._applyCommandsToObj(clientChanges, localJson); this._snapshotVersion = server['version']; this._applyCommands(clientChanges); - let newSnapshot = this._wrapNode(localBookmarks); + let newSnapshot = this._getBookmarks(); let diff = this._detectUpdates(this._snapshot, newSnapshot); if (diff.length != 0) { this.notice("Error: commands did not apply correctly. Diff:\n" + uneval(diff)); - this._snapshot = this._wrapNode(localBookmarks); + this._snapshot = newSnapshot; // FIXME: What else can we do? } this._saveSnapshot(); @@ -807,9 +796,9 @@ BookmarksSyncService.prototype = { // 3.2) Append server delta to the delta file and upload if (serverChanges.length) { this.notice("Uploading changes to server"); - this._snapshot = this._wrapNode(localBookmarks); + this._snapshot = this._getBookmarks(); this._snapshotVersion = server['version'] + 1; - server['deltas'][this._snapshotVersion] = serverChanges; + server['deltas']['version ' + this._snapshotVersion] = serverChanges; this._dav.PUT("bookmarks.delta", uneval(server['deltas']), handlers); data = yield; @@ -862,40 +851,31 @@ BookmarksSyncService.prototype = { this.notice("Got bookmarks delta from server"); ret.deltas = eval(data.target.responseText); - var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack - // FIXME: debug here for conditional below... - /* - this.notice("[sync bowels] local version: " + this._snapshotVersion); - for (var z in ret.deltas) { - this.notice("[sync bowels] remote version: " + z); - } - this.notice("foo: " + uneval(ret.deltas[this._snapshotVersion + 1])); - if (ret.deltas[this._snapshotVersion + 1]) - this.notice("-> is true"); - else - this.notice("-> is false"); - */ + let next = "version " + (this._snapshotVersion + 1); + let cur = "version " + this._snapshotVersion; - if (ret.deltas[this._snapshotVersion + 1]) { + if (next in ret.deltas) { // Merge the matching deltas into one, find highest version - var keys = []; - for (var v in ret.deltas) { + let keys = []; + for (var vstr in ret.deltas) { + let v = parseInt(vstr.replace(/^version /, '')); if (v > this._snapshotVersion) - keys.push(v); + keys.push(vstr); if (v > ret.version) ret.version = v; } keys = keys.sort(); - //this.notice("TMP: " + uneval(tmp)); + + let tmp = eval(uneval(this._snapshot)); // fixme hack hack hack for (var i = 0; i < keys.length; i++) { tmp = this._applyCommandsToObj(ret.deltas[keys[i]], tmp); - //this.notice("TMP: " + uneval(tmp)); } - ret.status = 1; - ret["updates"] = this._detectUpdates(this._snapshot, tmp); - } else if (ret.deltas[this._snapshotVersion]) { + ret.status = 1; + ret.updates = this._detectUpdates(this._snapshot, tmp); + + } else if (cur in ret.deltas) { this.notice("No changes from server"); ret.status = 0; ret.version = this._snapshotVersion; @@ -916,18 +896,19 @@ BookmarksSyncService.prototype = { "New snapshot uploaded, may be inconsistent with deltas!"); } - var tmp = eval(uneval(this._snapshot)); // fixme hack hack hack - tmp = this._applyCommandsToObj(data.updates, tmp); - // fixme: this is duplicated from above, need to do some refactoring - var keys = []; - for (var v in ret.deltas) { + let keys = []; + for (var vstr in ret.deltas) { + let v = parseInt(vstr.replace(/^version /, '')); if (v > this._snapshotVersion) - keys.push(v); + keys.push(vstr); if (v > ret.version) ret.version = v; } keys = keys.sort(); + + let tmp = eval(uneval(this._snapshot)); // fixme hack hack hack + tmp = this._applyCommandsToObj(data.updates, tmp); for (var i = 0; i < keys.length; i++) { tmp = this._applyCommandsToObj(ret.deltas[keys[i]], tmp); } @@ -935,11 +916,18 @@ BookmarksSyncService.prototype = { ret.status = data.status; ret.updates = this._detectUpdates(this._snapshot, tmp); ret.version = data.version; - var keys = []; - for (var v in ret.deltas) { + + for (var vstr in ret.deltas) { + let v = parseInt(vstr.replace(/^version /, '')); if (v > ret.version) ret.version = v; } + + if (typeof ret.version != "number") { + this.notice("Error: version is not a number! Correcting..."); + ret.version = parseInt(ret.version); + } + } break; case 404: @@ -974,6 +962,9 @@ BookmarksSyncService.prototype = { ret.status = 1; ret.updates = this._detectUpdates(this._snapshot, tmp.snapshot); ret.version = tmp.version; + if (typeof ret.version != "number") + this.notice("Error: version is not a number! Full server response text:\n" + + data.target.responseText); break; case 404: this.notice("No bookmarks on server. Starting initial sync to server"); @@ -985,6 +976,7 @@ BookmarksSyncService.prototype = { if (data.target.status >= 200 || data.target.status < 300) { this.notice("Initial sync to server successful"); + this._saveSnapshot(); ret.status = 2; } else { this.notice("Initial sync to server failed"); From cfe9c0f4b99b870648aa7f7b70174631243cb8a6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 28 Sep 2007 17:42:37 -0700 Subject: [PATCH 0024/1860] keep a guid when the snapshot is first generated and uplaoded to the server, and propagate it to all clients. this way we know if the server store gets completely wiped out --- services/sync/nsBookmarksSyncService.js | 62 ++++++++++++++++++++----- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 7a771b97287c..51206cc66bfc 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -110,10 +110,24 @@ BookmarksSyncService.prototype = { // DAVCollection object _dav: null, - // Last synced tree and version + // Last synced tree, version, and GUID (to detect if the store has + // been completely replaced and invalidate the snapshot) _snapshot: {}, _snapshotVersion: 0, + __snapshotGuid: null, + get _snapshotGuid() { + if (!this.__snapshotGuid) { + let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator); + this.__snapshotGuid = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); + } + return this.__snapshotGuid; + }, + set _snapshotGuid(guid) { + this.__snapshotGuid = guid; + }, + get currentUser() { return this._dav.currentUser; }, @@ -172,7 +186,9 @@ BookmarksSyncService.prototype = { let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; fos.init(file, flags, PERMS_FILE, 0); - let out = {version: this._snapshotVersion, snapshot: this._snapshot}; + let out = {version: this._snapshotVersion, + guid: this._snapshotGuid, + snapshot: this._snapshot}; out = uneval(out); fos.write(out, out.length); fos.close(); @@ -202,9 +218,10 @@ BookmarksSyncService.prototype = { fis.close(); json = eval(json); - if (json.snapshot && json.version) { + if (json && json.snapshot && json.version && json.guid) { this._snapshot = json.snapshot; this._snapshotVersion = json.version; + this._snapshotGuid = json.guid; } }, @@ -699,8 +716,9 @@ BookmarksSyncService.prototype = { this.notice("Server status: " + server.status); this.notice("Server version: " + server.version); - this.notice("Server version type: " + typeof server.version); + this.notice("Server guid: " + server.guid); this.notice("Local snapshot version: " + this._snapshotVersion); + this.notice("Local snapshot guid: " + this._snapshotGuid); for (version in server.deltas) { this.notice("Server delta " + version + ":\n" + @@ -717,6 +735,13 @@ BookmarksSyncService.prototype = { return; } + if (this._snapshotGuid != server.guid) { + this.notice("Snapshot GUIDs differ, local snapshot is not valid"); + this._snapshot = {}; + this._snapshotVersion = -1; + this._snapshotGuid = server.guid; + } + // 2) Generate local deltas from snapshot -> current client status let localUpdates = this._detectUpdates(this._snapshot, localJson); @@ -799,7 +824,9 @@ BookmarksSyncService.prototype = { this._snapshot = this._getBookmarks(); this._snapshotVersion = server['version'] + 1; server['deltas']['version ' + this._snapshotVersion] = serverChanges; - this._dav.PUT("bookmarks.delta", uneval(server['deltas']), handlers); + + let out = {guid: this._snapshotGuid, deltas: server['deltas']}; + this._dav.PUT("bookmarks.delta", uneval(out), handlers); data = yield; if (data.target.status >= 200 || data.target.status < 300) { @@ -830,6 +857,9 @@ BookmarksSyncService.prototype = { * 2: ok, initial sync * version: * the latest version on the server + * guid: + * the guid that was created when the first snapshot was uploaded + * (will only change if the server store is completely wiped) * deltas: * the individual deltas on the server * updates: @@ -840,7 +870,8 @@ BookmarksSyncService.prototype = { var generator = yield; var handlers = this._handlersForGenerator(generator); - var ret = {status: -1, version: -1, deltas: null, updates: null}; + var ret = {status: -1, version: -1, + guid: null, deltas: null, updates: null}; this.notice("Getting bookmarks delta from server"); this._dav.GET("bookmarks.delta", handlers); @@ -850,7 +881,9 @@ BookmarksSyncService.prototype = { case 200: this.notice("Got bookmarks delta from server"); - ret.deltas = eval(data.target.responseText); + let resp = eval(data.target.responseText); + ret.guid = resp.guid; + ret.deltas = resp.deltas; let next = "version " + (this._snapshotVersion + 1); let cur = "version " + this._snapshotVersion; @@ -858,7 +891,7 @@ BookmarksSyncService.prototype = { if (next in ret.deltas) { // Merge the matching deltas into one, find highest version let keys = []; - for (var vstr in ret.deltas) { + for (let vstr in ret.deltas) { let v = parseInt(vstr.replace(/^version /, '')); if (v > this._snapshotVersion) keys.push(vstr); @@ -898,7 +931,7 @@ BookmarksSyncService.prototype = { // fixme: this is duplicated from above, need to do some refactoring let keys = []; - for (var vstr in ret.deltas) { + for (let vstr in ret.deltas) { let v = parseInt(vstr.replace(/^version /, '')); if (v > this._snapshotVersion) keys.push(vstr); @@ -916,8 +949,9 @@ BookmarksSyncService.prototype = { ret.status = data.status; ret.updates = this._detectUpdates(this._snapshot, tmp); ret.version = data.version; + ret.guid = data.guid; - for (var vstr in ret.deltas) { + for (let vstr in ret.deltas) { let v = parseInt(vstr.replace(/^version /, '')); if (v > ret.version) ret.version = v; @@ -950,7 +984,7 @@ BookmarksSyncService.prototype = { let generator = yield; let handlers = this._handlersForGenerator(generator); - var ret = {status: -1, version: -1, updates: null}; + var ret = {status: -1, version: -1, guid: null, updates: null}; this._dav.GET("bookmarks.json", handlers); data = yield; @@ -962,6 +996,7 @@ BookmarksSyncService.prototype = { ret.status = 1; ret.updates = this._detectUpdates(this._snapshot, tmp.snapshot); ret.version = tmp.version; + ret.guid = tmp.guid; if (typeof ret.version != "number") this.notice("Error: version is not a number! Full server response text:\n" + data.target.responseText); @@ -971,7 +1006,10 @@ BookmarksSyncService.prototype = { this._snapshot = localJson; this._snapshotVersion = 1; - this._dav.PUT("bookmarks.json", uneval({version: 1, snapshot: this._snapshot}), handlers); + this._dav.PUT("bookmarks.json", uneval({version: 1, + guid: this._snapshotGuid, + snapshot: this._snapshot}), + handlers); data = yield; if (data.target.status >= 200 || data.target.status < 300) { From a36a27a2765d3520e4de46779ca5e85650dd166a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 2 Oct 2007 01:39:55 -0700 Subject: [PATCH 0025/1860] rewrote network logic to be far cleaner and more correct. Add initial support for a 'format version'. --- services/sync/nsBookmarksSyncService.js | 360 ++++++++++++------------ 1 file changed, 177 insertions(+), 183 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 51206cc66bfc..f0f79fbefe44 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -48,6 +48,8 @@ const MODE_TRUNCATE = 0x20; const PERMS_FILE = 0644; const PERMS_DIRECTORY = 0755; +const STORAGE_FORMAT_VERSION = 0; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function BookmarksSyncService() { this._init(); } @@ -706,7 +708,7 @@ BookmarksSyncService.prototype = { var data; var localJson = this._getBookmarks(); - this.notice("local json:\n" + this._mungeNodes(localJson)); + //this.notice("local json:\n" + this._mungeNodes(localJson)); // 1) Fetch server deltas let gsd_gen = this._getServerData(handlers['complete'], localJson); @@ -714,39 +716,33 @@ BookmarksSyncService.prototype = { gsd_gen.send(gsd_gen); let server = yield; - this.notice("Server status: " + server.status); - this.notice("Server version: " + server.version); - this.notice("Server guid: " + server.guid); this.notice("Local snapshot version: " + this._snapshotVersion); - this.notice("Local snapshot guid: " + this._snapshotGuid); + this.notice("Server status: " + server.status); - for (version in server.deltas) { - this.notice("Server delta " + version + ":\n" + - this._mungeCommands(server.deltas[version])); - } - - if (server['status'] == 2) { + if (server['status'] != 0) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); - this.notice("Sync complete"); - return; - } else if (server['status'] != 0 && server['status'] != 1) { - this._os.notifyObservers(null, "bookmarks-sync:end", ""); - this.notice("Sync error"); + this.notice("Sync error: could not get server status, " + + "or initial upload failed."); return; } - if (this._snapshotGuid != server.guid) { - this.notice("Snapshot GUIDs differ, local snapshot is not valid"); - this._snapshot = {}; - this._snapshotVersion = -1; - this._snapshotGuid = server.guid; - } + this.notice("Server maxVersion: " + server.maxVersion); + this.notice("Server snapVersion: " + server.snapVersion); + this.notice("Server updates: " + this._mungeCommands(server.updates)); + + // if (server['status'] == 2) { + // this._os.notifyObservers(null, "bookmarks-sync:end", ""); + // this.notice("Sync complete"); + // return; + // } else // 2) Generate local deltas from snapshot -> current client status let localUpdates = this._detectUpdates(this._snapshot, localJson); this.notice("Local updates: " + this._mungeCommands(localUpdates)); - if (!(server['status'] == 1 || localUpdates.length > 0)) { + + if (server.updates.length == 0 && localUpdates.length == 0) { + this._snapshotVersion = server.maxVersion; this._os.notifyObservers(null, "bookmarks-sync:end", ""); this.notice("Sync complete (1): no changes needed on client or server"); return; @@ -755,8 +751,8 @@ BookmarksSyncService.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. this.notice("Reconciling client/server updates"); - let callback = function(retval) { continueGenerator(generator, retval); }; - let rec_gen = this._reconcile(callback, [localUpdates, server.updates]); + let rec_gen = this._reconcile(handlers.load, + [localUpdates, server.updates]); rec_gen.next(); // must initialize before sending rec_gen.send(rec_gen); let ret = yield; @@ -788,7 +784,7 @@ BookmarksSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:end", ""); this.notice("Sync complete (2): no changes needed on client or server"); this._snapshot = localJson; - this._snapshotVersion = server.version; + this._snapshotVersion = server.maxVersion; this._saveSnapshot(); return; } @@ -804,7 +800,7 @@ BookmarksSyncService.prototype = { // current tree, not the saved snapshot this._snapshot = this._applyCommandsToObj(clientChanges, localJson); - this._snapshotVersion = server['version']; + this._snapshotVersion = server.maxVersion; this._applyCommands(clientChanges); let newSnapshot = this._getBookmarks(); @@ -822,15 +818,24 @@ BookmarksSyncService.prototype = { if (serverChanges.length) { this.notice("Uploading changes to server"); this._snapshot = this._getBookmarks(); - this._snapshotVersion = server['version'] + 1; - server['deltas']['version ' + this._snapshotVersion] = serverChanges; + this._snapshotVersion = ++server.maxVersion; + server.deltas.push(serverChanges); - let out = {guid: this._snapshotGuid, deltas: server['deltas']}; - this._dav.PUT("bookmarks.delta", uneval(out), handlers); - data = yield; + this._dav.PUT("bookmarks-deltas.json", uneval(server.deltas), handlers); + let deltasPut = yield; - if (data.target.status >= 200 || data.target.status < 300) { - this.notice("Successfully updated deltas on server"); + // FIXME: need to watch out for the storage format version changing, + // in that case we'll have to re-upload all the files, not just these + this._dav.PUT("bookmarks-status.json", + uneval({guid: this._snapshotGuid, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshotVersion}), handlers); + let statusPut = yield; + + if (deltasPut.target.status >= 200 && deltasPut.target.status < 300 && + statusPut.target.status >= 200 && statusPut.target.status < 300) { + this.notice("Successfully updated deltas and status on server"); this._saveSnapshot(); } else { // FIXME: revert snapshot here? - can't, we already applied @@ -847,184 +852,173 @@ BookmarksSyncService.prototype = { } }, - /* Get the deltas/combined updates from the server * Returns: * status: * -1: error - * 0: no changes from server - * 1: ok - * 2: ok, initial sync - * version: + * 0: ok + * These fields may be null when status is -1: + * formatVersion: + * version of the data format itself. For compatibility checks. + * maxVersion: * the latest version on the server - * guid: - * the guid that was created when the first snapshot was uploaded - * (will only change if the server store is completely wiped) + * snapVersion: + * the version of the current snapshot on the server (deltas not applied) + * snapshot: + * full snapshot of the latest server version (deltas applied) * deltas: - * the individual deltas on the server + * all of the individual deltas on the server * updates: * the relevant deltas (from our snapshot version to current), * combined into a single set. */ + // FIXME: this function needs to get check the remote format version and bail out earlier if needed + // FIXME: deal with errors! _getServerData: function BSS__getServerData(onComplete, localJson) { - var generator = yield; - var handlers = this._handlersForGenerator(generator); - - var ret = {status: -1, version: -1, - guid: null, deltas: null, updates: null}; - - this.notice("Getting bookmarks delta from server"); - this._dav.GET("bookmarks.delta", handlers); - var data = yield; - - switch (data.target.status) { - case 200: - this.notice("Got bookmarks delta from server"); - - let resp = eval(data.target.responseText); - ret.guid = resp.guid; - ret.deltas = resp.deltas; - - let next = "version " + (this._snapshotVersion + 1); - let cur = "version " + this._snapshotVersion; - - if (next in ret.deltas) { - // Merge the matching deltas into one, find highest version - let keys = []; - for (let vstr in ret.deltas) { - let v = parseInt(vstr.replace(/^version /, '')); - if (v > this._snapshotVersion) - keys.push(vstr); - if (v > ret.version) - ret.version = v; - } - keys = keys.sort(); - - let tmp = eval(uneval(this._snapshot)); // fixme hack hack hack - for (var i = 0; i < keys.length; i++) { - tmp = this._applyCommandsToObj(ret.deltas[keys[i]], tmp); - } - - ret.status = 1; - ret.updates = this._detectUpdates(this._snapshot, tmp); - - } else if (cur in ret.deltas) { - this.notice("No changes from server"); - ret.status = 0; - ret.version = this._snapshotVersion; - ret.updates = []; - - } else { - this.notice("Server delta can't update from our snapshot version, getting full file"); - // generate updates from full local->remote snapshot diff - let gsdf_gen = this._getServerUpdatesFull(handlers['complete'], - localJson); - gsdf_gen.next(); // must initialize before sending - gsdf_gen.send(gsdf_gen); - data = yield; - if (data.status == 2) { - // we have a delta file but no snapshot on the server. bad. - // fixme? - this.notice("Error: Delta file on server, but snapshot file missing. " + - "New snapshot uploaded, may be inconsistent with deltas!"); - } - - // fixme: this is duplicated from above, need to do some refactoring - let keys = []; - for (let vstr in ret.deltas) { - let v = parseInt(vstr.replace(/^version /, '')); - if (v > this._snapshotVersion) - keys.push(vstr); - if (v > ret.version) - ret.version = v; - } - keys = keys.sort(); - - let tmp = eval(uneval(this._snapshot)); // fixme hack hack hack - tmp = this._applyCommandsToObj(data.updates, tmp); - for (var i = 0; i < keys.length; i++) { - tmp = this._applyCommandsToObj(ret.deltas[keys[i]], tmp); - } - - ret.status = data.status; - ret.updates = this._detectUpdates(this._snapshot, tmp); - ret.version = data.version; - ret.guid = data.guid; - - for (let vstr in ret.deltas) { - let v = parseInt(vstr.replace(/^version /, '')); - if (v > ret.version) - ret.version = v; - } - - if (typeof ret.version != "number") { - this.notice("Error: version is not a number! Correcting..."); - ret.version = parseInt(ret.version); - } - - } - break; - case 404: - this.notice("Server has no delta file. Getting full bookmarks file from server"); - // generate updates from full local->remote snapshot diff - let gsdf_gen = this._getServerUpdatesFull(handlers['complete'], localJson); - gsdf_gen.next(); // must initialize before sending - gsdf_gen.send(gsdf_gen); - ret = yield; - ret.deltas = {}; - break; - default: - this.notice("Could not get bookmarks.delta: unknown HTTP status code " + data.target.status); - break; - } - this._generatorDone(onComplete, ret) - }, - - _getServerUpdatesFull: function BSS__getServerUpdatesFull(onComplete, localJson) { let generator = yield; let handlers = this._handlersForGenerator(generator); - var ret = {status: -1, version: -1, guid: null, updates: null}; + let ret = {status: -1, + formatVersion: null, maxVersion: null, snapVersion: null, + snapshot: null, deltas: null, updates: null}; - this._dav.GET("bookmarks.json", handlers); - data = yield; + this.notice("Getting bookmarks status from server"); + this._dav.GET("bookmarks-status.json", handlers); + let statusResp = yield; - switch (data.target.status) { + switch (statusResp.target.status) { case 200: - this.notice("Got full bookmarks file from server"); - var tmp = eval(data.target.responseText); - ret.status = 1; - ret.updates = this._detectUpdates(this._snapshot, tmp.snapshot); - ret.version = tmp.version; - ret.guid = tmp.guid; - if (typeof ret.version != "number") - this.notice("Error: version is not a number! Full server response text:\n" + - data.target.responseText); + this.notice("Got bookmarks status from server"); + + let status = eval(statusResp.target.responseText); + let snap, deltas, allDeltas; + + if (status.guid != this._snapshotGuid) { + this.notice("Remote/local sync guids do not match. " + + "Forcing initial sync."); + this._snapshot = {}; + this._snapshotVersion = -1; + this._snapshotGuid = status.guid; + } + + if (this._snapshotVersion < status.snapVersion) { + if (this._snapshotVersion >= 0) + this.notice("Local snapshot is out of date"); + + this.notice("Downloading server snapshot"); + this._dav.GET("bookmarks-snapshot.json", handlers); + let snapResp = yield; + if (snapResp.target.status == 200) + snap = eval(snapResp.target.responseText); + else + this.notice("Error: could not download server snapshot"); + + this.notice("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", handlers); + let deltasResp = yield; + if (deltasResp.target.status == 200) + allDeltas = eval(deltasResp.target.responseText); + else + this.notice("Error: could not download server deltas"); + + deltas = eval(uneval(allDeltas)); + + } else if (this._snapshotVersion >= status.snapVersion && + this._snapshotVersion < status.maxVersion) { + snap = eval(uneval(this._snapshot)); + + this.notice("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", handlers); + let deltasResp = yield; + if (deltasResp.target.status == 200) + allDeltas = eval(deltasResp.target.responseText); + else + this.notice("Error: could not download server deltas"); + + let start = this._snapshotVersion - status.snapVersion; + deltas = allDeltas.slice(start); + + } else if (this._snapshotVersion == status.maxVersion) { + snap = eval(uneval(this._snapshot)); + + // FIXME: could optimize this case by caching deltas file + this.notice("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", handlers); + let deltasResp = yield; + if (deltasResp.target.status == 200) + allDeltas = eval(deltasResp.target.responseText); + else + this.notice("Error: could not download server deltas"); + + deltas = []; + + } else { // this._snapshotVersion > status.maxVersion + this.notice("Error: server snapshot is older than local snapshot"); + // FIXME: eep? + } + + for (var i = 0; i < deltas.length; i++) { + snap = this._applyCommandsToObj(deltas[i], snap); + } + + ret.status = 0; + ret.formatVersion = status.formatVersion; + ret.maxVersion = status.maxVersion; + ret.snapVersion = status.snapVersion; + ret.snapshot = snap; + ret.deltas = allDeltas; + ret.updates = this._detectUpdates(this._snapshot, snap); break; + case 404: - this.notice("No bookmarks on server. Starting initial sync to server"); + this.notice("Server has no status file, Initial upload to server"); this._snapshot = localJson; - this._snapshotVersion = 1; - this._dav.PUT("bookmarks.json", uneval({version: 1, - guid: this._snapshotGuid, - snapshot: this._snapshot}), - handlers); - data = yield; + this._snapshotVersion = 0; + this._snapshotGuid = null; // in case there are other snapshots out there - if (data.target.status >= 200 || data.target.status < 300) { - this.notice("Initial sync to server successful"); + // FIXME: hmm check status for each and bail out earlier on error? + + this._dav.PUT("bookmarks-snapshot.json", + uneval(this._snapshot), handlers); + let snapPut = yield; + + this._dav.PUT("bookmarks-deltas.json", uneval([]), handlers); + let deltasPut = yield; + + this._dav.PUT("bookmarks-status.json", + uneval({guid: this._snapshotGuid, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshotVersion, + maxVersion: this._snapshotVersion}), handlers); + let statusPut = yield; + + if (snapPut.target.status >= 200 && snapPut.target.status < 300 && + deltasPut.target.status >= 200 && deltasPut.target.status < 300 && + statusPut.target.status >= 200 && statusPut.target.status < 300) { + this.notice("Initial upload to server successful"); this._saveSnapshot(); - ret.status = 2; } else { - this.notice("Initial sync to server failed"); + // FIXME: eep? + this.notice("Error: could not upload files to server"); } + + ret.status = 0; + ret.formatVersion = STORAGE_FORMAT_VERSION; + ret.maxVersion = this._snapshotVersion; + ret.snapVersion = this._snapshotVersion; + ret.snapshot = eval(uneval(this._snapshot)); + ret.deltas = []; + ret.updates = []; break; + default: - this.notice("Could not get bookmarks.json: unknown HTTP status code " + data.target.status); + this.notice("Could not get bookmarks.status: unknown HTTP status code " + + statusResp.target.status); break; } - this._generatorDone(onComplete, ret); + this._generatorDone(onComplete, ret) + }, _handlersForGenerator: function BSS__handlersForGenerator(generator) { From f6dd3921496937d86313a15adc88af7e32241cac Mon Sep 17 00:00:00 2001 From: Dave Camp Date: Tue, 2 Oct 2007 15:07:19 -0700 Subject: [PATCH 0026/1860] simple tag syncing --- services/sync/nsBookmarksSyncService.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index f0f79fbefe44..bd62cfb02743 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -79,6 +79,14 @@ BookmarksSyncService.prototype = { return this.__ans; }, + __ts: null, + get _ts() { + if (!this.__ts) + this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + return this.__ts; + }, + __os: null, get _os() { if (!this.__os) @@ -253,6 +261,7 @@ BookmarksSyncService.prototype = { // FIXME: need to verify that it's a bookmark, it could be a history result! item.title = node.title; item.uri = node.uri; + item.tags = this._ts.getTagsForURI(makeURI(node.uri)); } else { // what do we do? } @@ -267,7 +276,7 @@ BookmarksSyncService.prototype = { let ret = {numProps: 0, props: {}}; for (prop in a) { - if (a[prop] != b[prop]) { + if (!this._deepEquals(a[prop], b[prop])) { ret.numProps++; ret.props[prop] = b[prop]; } @@ -565,10 +574,13 @@ BookmarksSyncService.prototype = { switch (command.data.type) { case 0: + let uri = makeURI(command.data.uri); newId = this._bms.insertBookmark(parentId, - makeURI(command.data.uri), + uri, command.data.index, command.data.title); + this._ts.untagURI(uri, null); + this._ts.tagURI(uri, command.data.tags); break; case 6: newId = this._bms.createFolder(parentId, @@ -646,6 +658,11 @@ BookmarksSyncService.prototype = { this._bms.moveItem( itemId, this._bms.getItemIdForGUID(command.data.parentGuid), index); break; + case "tags": + let uri = this._bms.getBookmarkURI(itemId); + this._ts.untagURI(uri, null); + this._ts.tagURI(uri, command.data.tags); + break; default: this.notice("Warning: Can't change item property: " + key); break; @@ -670,6 +687,7 @@ BookmarksSyncService.prototype = { json = json.replace(/, index/g, ",\n\t index"); json = json.replace(/, title/g, ",\n\t title"); json = json.replace(/, uri/g, ",\n\t uri"); + json = json.replace(/, tags/g, ",\n\t tags"); return json; }, From fafb08b965ab8638777f9a61f858fa7c0f5d25b7 Mon Sep 17 00:00:00 2001 From: Dave Camp Date: Tue, 2 Oct 2007 15:30:20 -0700 Subject: [PATCH 0027/1860] sync keywords --- services/sync/nsBookmarksSyncService.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index bd62cfb02743..554793bbbe32 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -262,6 +262,9 @@ BookmarksSyncService.prototype = { item.title = node.title; item.uri = node.uri; item.tags = this._ts.getTagsForURI(makeURI(node.uri)); + let keyword = this._bms.getKeywordForBookmark(node.itemId); + if (keyword) + item.keyword = keyword; } else { // what do we do? } @@ -581,6 +584,8 @@ BookmarksSyncService.prototype = { command.data.title); this._ts.untagURI(uri, null); this._ts.tagURI(uri, command.data.tags); + if (command.data.keyword) + this._bms.setKeywordForBookmark(newId, command.data.keyword); break; case 6: newId = this._bms.createFolder(parentId, @@ -663,6 +668,9 @@ BookmarksSyncService.prototype = { this._ts.untagURI(uri, null); this._ts.tagURI(uri, command.data.tags); break; + case "keyword": + this._bms.setKeywordForBookmark(itemId, command.data.keyword); + break; default: this.notice("Warning: Can't change item property: " + key); break; @@ -688,6 +696,7 @@ BookmarksSyncService.prototype = { json = json.replace(/, title/g, ",\n\t title"); json = json.replace(/, uri/g, ",\n\t uri"); json = json.replace(/, tags/g, ",\n\t tags"); + json = json.replace(/, keyword/g, ",\n\t keyword"); return json; }, From 307686a7f0a7c285d54b7662bd9c5b59599ae9f6 Mon Sep 17 00:00:00 2001 From: Date: Wed, 3 Oct 2007 18:00:16 -0700 Subject: [PATCH 0028/1860] fleshing out of the setup wizard --- services/sync/nsBookmarksSyncService.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 554793bbbe32..71c6560236eb 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -1294,7 +1294,8 @@ DAVCollection.prototype = { // try without the auth header? // fixme: may want to just fail here } - + this._authProvider._authFailed = false; + let request = this._makeRequest("GET", "", internalHandlers, headers); request.send(null); }, @@ -1304,9 +1305,9 @@ DAVCollection.prototype = { }, _onLogin: function DC__onLogin(event) { - //notice("logged in (" + event.target.status + "):\n" + - // event.target.responseText + "\n"); - +// dump("logged in (" + event.target.status + "):\n" + +// event.target.responseText + "\n"); + if (this._authProvider._authFailed || event.target.status >= 400) { this._onLoginError(event); return; From 304c00cf64f6c396c0d2a46f28997ab43a9b4a4f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 3 Oct 2007 19:16:47 -0700 Subject: [PATCH 0029/1860] locking fixes (still commented out); better error checking; fail if the remote format version is higher than we can read; refactor generator code, bring back asyncRun() --- services/sync/nsBookmarksSyncService.js | 397 ++++++++++++++++-------- 1 file changed, 269 insertions(+), 128 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 71c6560236eb..795886c68411 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -272,6 +272,7 @@ BookmarksSyncService.prototype = { items[guid] = item; }, + // FIXME: this won't work for deep objects, or objects with optional properties _getEdits: function BSS__getEdits(a, b) { // check the type separately, just in case if (a.type != b.type) @@ -447,13 +448,12 @@ BookmarksSyncService.prototype = { // [[[dcA1, dcB1], [dcA2. dcB2], ...], // [2dcA1, 2dcA2, ...], [2dcB1, 2dcB2, ...]] - _reconcile: function BSS__reconcile(onComplete, commandLists) { + _reconcile: function BSS__reconcile(onComplete, listA, listB) { let generator = yield; this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let handlers = this._handlersForGenerator(generator); + let handlers = generatorHandlers(generator); let listener = new EventListener(handlers['complete']); - let [listA, listB] = commandLists; let propagations = [[], []]; let conflicts = [[], []]; @@ -514,7 +514,7 @@ BookmarksSyncService.prototype = { this._timer = null; let ret = {propagations: propagations, conflicts: conflicts}; - this._generatorDone(onComplete, ret); + generatorDone(this, onComplete, ret); // shutdown protocol let cookie = yield; @@ -724,24 +724,43 @@ BookmarksSyncService.prototype = { _doSync: function BSS__doSync() { var generator = yield; - var handlers = this._handlersForGenerator(generator); + var handlers = generatorHandlers(generator); this._os.notifyObservers(null, "bookmarks-sync:start", ""); this.notice("Beginning sync"); try { - //this._dav.lock(handlers); - //var data = yield; - var data; +// if (!asyncRun(this._dav, this._dav.lock, handlers.complete)) +// return; +// let locked = yield; +// +// if (locked) +// this.notice("Lock acquired"); +// else { +// this.notice("Could not acquire lock, aborting"); +// FIXME +// this.notice("Could not acquire lock, attempting to steal it"); +// +// if (!asyncRun(this._dav, this._dav.stealLock, handlers.complete)) +// return; +// let stolen = yield; +// +// if (stolen) +// this.notice("Lock stolen"); +// else { +// this.notice("Could not steal lock, aborting"); +// return; +// } +// } var localJson = this._getBookmarks(); //this.notice("local json:\n" + this._mungeNodes(localJson)); // 1) Fetch server deltas - let gsd_gen = this._getServerData(handlers['complete'], localJson); - gsd_gen.next(); // must initialize before sending - gsd_gen.send(gsd_gen); - let server = yield; + if (asyncRun(this, this._getServerData, handlers.complete, localJson)) + let server = yield; + else + return this.notice("Local snapshot version: " + this._snapshotVersion); this.notice("Server status: " + server.status); @@ -757,12 +776,6 @@ BookmarksSyncService.prototype = { this.notice("Server snapVersion: " + server.snapVersion); this.notice("Server updates: " + this._mungeCommands(server.updates)); - // if (server['status'] == 2) { - // this._os.notifyObservers(null, "bookmarks-sync:end", ""); - // this.notice("Sync complete"); - // return; - // } else - // 2) Generate local deltas from snapshot -> current client status let localUpdates = this._detectUpdates(this._snapshot, localJson); @@ -778,13 +791,13 @@ BookmarksSyncService.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. this.notice("Reconciling client/server updates"); - let rec_gen = this._reconcile(handlers.load, - [localUpdates, server.updates]); - rec_gen.next(); // must initialize before sending - rec_gen.send(rec_gen); - let ret = yield; + if (asyncRun(this, this._reconcile, handlers.complete, + localUpdates, server.updates)) + let ret = yield; + else + return // FIXME: Need to come up with a closing protocol for generators - rec_gen.close(); + //rec_gen.close(); let clientChanges = []; let serverChanges = []; @@ -873,8 +886,14 @@ BookmarksSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:end", ""); this.notice("Sync complete"); } finally { - //this._dav.unlock(handlers); - //data = yield; +// FIXME +// if (!asyncRun(this._dav, this._dav.unlock, handlers.complete)) +// return; +// let unlocked = yield; +// if (unlocked) +// this.notice("Lock released"); +// else +// this.notice("Error: could not unlock DAV collection"); this._os.notifyObservers(null, "bookmarks-sync:end", ""); } }, @@ -899,11 +918,9 @@ BookmarksSyncService.prototype = { * the relevant deltas (from our snapshot version to current), * combined into a single set. */ - // FIXME: this function needs to get check the remote format version and bail out earlier if needed - // FIXME: deal with errors! _getServerData: function BSS__getServerData(onComplete, localJson) { let generator = yield; - let handlers = this._handlersForGenerator(generator); + let handlers = generatorHandlers(generator); let ret = {status: -1, formatVersion: null, maxVersion: null, snapVersion: null, @@ -920,6 +937,14 @@ BookmarksSyncService.prototype = { let status = eval(statusResp.target.responseText); let snap, deltas, allDeltas; + // Bail out if the server has a newer format version than we can parse + if (status.formatVersion > STORAGE_FORMAT_VERSION) { + this.notice("Error: server uses storage format v" + status.formatVersion + + ", this client understands up to v" + STORAGE_FORMAT_VERSION); + generatorDone(this, onComplete, ret) + return; + } + if (status.guid != this._snapshotGuid) { this.notice("Remote/local sync guids do not match. " + "Forcing initial sync."); @@ -935,19 +960,22 @@ BookmarksSyncService.prototype = { this.notice("Downloading server snapshot"); this._dav.GET("bookmarks-snapshot.json", handlers); let snapResp = yield; - if (snapResp.target.status == 200) - snap = eval(snapResp.target.responseText); - else + if (snapResp.target.status < 200 || snapResp.target.status >= 300) { this.notice("Error: could not download server snapshot"); + generatorDone(this, onComplete, ret) + return; + } + snap = eval(snapResp.target.responseText); this.notice("Downloading server deltas"); this._dav.GET("bookmarks-deltas.json", handlers); let deltasResp = yield; - if (deltasResp.target.status == 200) - allDeltas = eval(deltasResp.target.responseText); - else + if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { this.notice("Error: could not download server deltas"); - + generatorDone(this, onComplete, ret) + return; + } + allDeltas = eval(deltasResp.target.responseText); deltas = eval(uneval(allDeltas)); } else if (this._snapshotVersion >= status.snapVersion && @@ -957,13 +985,13 @@ BookmarksSyncService.prototype = { this.notice("Downloading server deltas"); this._dav.GET("bookmarks-deltas.json", handlers); let deltasResp = yield; - if (deltasResp.target.status == 200) - allDeltas = eval(deltasResp.target.responseText); - else + if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { this.notice("Error: could not download server deltas"); - - let start = this._snapshotVersion - status.snapVersion; - deltas = allDeltas.slice(start); + generatorDone(this, onComplete, ret) + return; + } + allDeltas = eval(deltasResp.target.responseText); + deltas = allDeltas.slice(this._snapshotVersion - status.snapVersion); } else if (this._snapshotVersion == status.maxVersion) { snap = eval(uneval(this._snapshot)); @@ -972,16 +1000,18 @@ BookmarksSyncService.prototype = { this.notice("Downloading server deltas"); this._dav.GET("bookmarks-deltas.json", handlers); let deltasResp = yield; - if (deltasResp.target.status == 200) - allDeltas = eval(deltasResp.target.responseText); - else + if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { this.notice("Error: could not download server deltas"); - + generatorDone(this, onComplete, ret) + return; + } + allDeltas = eval(deltasResp.target.responseText); deltas = []; } else { // this._snapshotVersion > status.maxVersion this.notice("Error: server snapshot is older than local snapshot"); - // FIXME: eep? + generatorDone(this, onComplete, ret) + return; } for (var i = 0; i < deltas.length; i++) { @@ -1004,14 +1034,22 @@ BookmarksSyncService.prototype = { this._snapshotVersion = 0; this._snapshotGuid = null; // in case there are other snapshots out there - // FIXME: hmm check status for each and bail out earlier on error? - this._dav.PUT("bookmarks-snapshot.json", uneval(this._snapshot), handlers); let snapPut = yield; + if (snapPut.target.status < 200 || snapPut.target.status >= 300) { + this.notice("Error: could not upload snapshot to server"); + generatorDone(this, onComplete, ret) + return; + } this._dav.PUT("bookmarks-deltas.json", uneval([]), handlers); let deltasPut = yield; + if (deltasPut.target.status < 200 || deltasPut.target.status >= 300) { + this.notice("Error: could not upload deltas to server"); + generatorDone(this, onComplete, ret) + return; + } this._dav.PUT("bookmarks-status.json", uneval({guid: this._snapshotGuid, @@ -1019,17 +1057,15 @@ BookmarksSyncService.prototype = { snapVersion: this._snapshotVersion, maxVersion: this._snapshotVersion}), handlers); let statusPut = yield; - - if (snapPut.target.status >= 200 && snapPut.target.status < 300 && - deltasPut.target.status >= 200 && deltasPut.target.status < 300 && - statusPut.target.status >= 200 && statusPut.target.status < 300) { - this.notice("Initial upload to server successful"); - this._saveSnapshot(); - } else { - // FIXME: eep? - this.notice("Error: could not upload files to server"); + if (statusPut.target.status < 200 || statusPut.target.status >= 300) { + this.notice("Error: could not upload status file to server"); + generatorDone(this, onComplete, ret) + return; } + this.notice("Initial upload to server successful"); + this._saveSnapshot(); + ret.status = 0; ret.formatVersion = STORAGE_FORMAT_VERSION; ret.maxVersion = this._snapshotVersion; @@ -1044,35 +1080,7 @@ BookmarksSyncService.prototype = { statusResp.target.status); break; } - this._generatorDone(onComplete, ret) - - }, - - _handlersForGenerator: function BSS__handlersForGenerator(generator) { - var h = {load: bind2(this, function(data) { continueGenerator(generator, data); }), - error: bind2(this, function(data) { this.notice("Request failed: " + uneval(data)); })}; - h['complete'] = h['load']; - return h; - }, - - // generators called using asyncRun can't simply call the callback - // with the return value, since that would cause the calling - // function to end up running (after the yield) from inside the - // generator. Instead, generators can call this method which sets - // up a timer to call the callback from a timer (and cleans up the - // timer to avoid leaks). - _generatorDone: function BSS__generatorDone(callback, retval) { - if (this._timer) - throw "Called generatorDone when there is a timer already set." - - let cb = bind2(this, function(event) { - this._timer = null; - callback(retval); - }); - - this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._timer.initWithCallback(new EventListener(cb), - 0, this._timer.TYPE_ONE_SHOT); + generatorDone(this, onComplete, ret) }, _onLogin: function BSS__onLogin(event) { @@ -1098,9 +1106,7 @@ BookmarksSyncService.prototype = { // nsIBookmarksSyncService sync: function BSS_sync() { - let sync_gen = this._doSync(); - sync_gen.next(); // must initialize before sending - sync_gen.send(sync_gen); + asyncRun(this, this._doSync); }, login: function BSS_login() { @@ -1144,17 +1150,60 @@ function bind2(object, method) { return function innerBind() { return method.apply(object, arguments); } } +function asyncRun(object, method, onComplete, extra) { + let args = Array.prototype.slice.call(arguments, 2); // onComplete + extra + ... + let ret = false; + try { + let gen = method.apply(object, args); + gen.next(); // must initialize before sending + gen.send(gen); + ret = true; + } catch (e) { + if (e instanceof StopIteration) + dump("Warning: generator stopped unexpectedly"); + else + throw e; + } + return ret; +} + function continueGenerator(generator, data) { try { generator.send(data); } catch (e) { - //notice("continueGenerator exception! - " + e); if (e instanceof StopIteration) generator = null; else - throw e; + throw e; } } +function generatorHandlers(generator) { + let cb = function(data) { + continueGenerator(generator, data); + }; + return {load: cb, error: cb, complete: cb}; +} + +// generators called using asyncRun can't simply call the callback +// with the return value, since that would cause the calling +// function to end up running (after the yield) from inside the +// generator. Instead, generators can call this method which sets +// up a timer to call the callback from a timer (and cleans up the +// timer to avoid leaks). +function generatorDone(object, callback, retval) { + if (object._timer) + throw "Called generatorDone when there is a timer already set." + + let cb = bind2(object, function(event) { + object._timer = null; + callback(retval); + }); + + object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + object._timer.initWithCallback(new EventListener(cb), + 0, object._timer.TYPE_ONE_SHOT); +} + function EventListener(handler) { this._handler = handler; } @@ -1179,15 +1228,26 @@ EventListener.prototype = { } }; +function xpath(xmlDoc, xpathString) { + let root = xmlDoc.ownerDocument == null ? + xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement + let nsResolver = xmlDoc.createNSResolver(root); + + return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, + Ci.nsIDOMXPathResult.ANY_TYPE, null); +} + function DAVCollection(baseURL) { this._baseURL = baseURL; this._authProvider = new DummyAuthProvider(); } DAVCollection.prototype = { - _loggedIn: false, - - get baseURL() { - return this._baseURL; + __dp: null, + get _dp() { + if (!this.__dp) + this.__dp = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + return this.__dp; }, __base64: {}, @@ -1202,6 +1262,11 @@ DAVCollection.prototype = { return this.__base64; }, + get baseURL() { + return this._baseURL; + }, + + _loggedIn: false, _authString: null, _currentUserPath: "nobody", @@ -1242,6 +1307,8 @@ DAVCollection.prototype = { headers = {'Content-type': 'text/plain'}; if (this._authString) headers['Authorization'] = this._authString; + if (this._token) + headers['If'] = "(<" + this._token + ">)"; var request = this._makeRequest("GET", path, handlers, headers); request.send(null); @@ -1252,6 +1319,8 @@ DAVCollection.prototype = { headers = {'Content-type': 'text/plain'}; if (this._authString) headers['Authorization'] = this._authString; + if (this._token) + headers['If'] = "(<" + this._token + ">)"; var request = this._makeRequest("PUT", path, handlers, headers); request.send(data); @@ -1352,52 +1421,113 @@ DAVCollection.prototype = { // Locking - // FIXME: make this function not reentrant - lock: function DC_lock(handlers) { - this._lockHandlers = handlers; - internalHandlers = {load: bind2(this, this._onLock), - error: bind2(this, this._onLockError)}; + _getActiveLock: function DC__getActiveLock(onComplete) { + let generator = yield; + let handlers = generatorHandlers(generator); + + let headers = {'Content-Type': 'text/xml; charset="utf-8"', + 'Depth': '0'}; + if (this._authString) + headers['Authorization'] = this._authString; + + let request = this._makeRequest("PROPFIND", "", handlers, headers); + request.send("" + + "" + + "" + + ""); + let event = yield; + let tokens = xpath(event.target.responseXML, '//D:locktoken'); + let token = tokens.iterateNext(); + if (token) + generatorDone(this, onComplete, token.textContent); + else + generatorDone(this, onComplete, null); + }, + + lock: function DC_lock(onComplete) { + let generator = yield; + let handlers = generatorHandlers(generator); + headers = {'Content-Type': 'text/xml; charset="utf-8"'}; - var request = this._makeRequest("LOCK", "", internalHandlers, headers); + if (this._authString) + headers['Authorization'] = this._authString; + + let request = this._makeRequest("LOCK", "", handlers, headers); request.send("\n" + "\n" + " \n" + " \n" + ""); + let event = yield; + + if (event.target.status < 200 || event.target.status >= 300) { + generatorDone(this, onComplete, false); + return; + } + + let tokens = xpath(event.target.responseXML, '//D:locktoken'); + let token = tokens.iterateNext(); + if (token) { + this._token = token.textContent; + this._token = this._token.replace("\n", "").replace("\n", ""); + generatorDone(this, onComplete, true); + } + + generatorDone(this, onComplete, false); }, - // FIXME: make this function not reentrant - unlock: function DC_unlock(handlers) { - this._lockHandlers = handlers; - internalHandlers = {load: bind2(this, this._onUnlock), - error: bind2(this, this._onUnlockError)}; + unlock: function DC_unlock(onComplete) { + let generator = yield; + let handlers = generatorHandlers(generator); + + if (!this._token) { + generatorDone(this, onComplete, false); + return; + } + headers = {'Lock-Token': "<" + this._token + ">"}; - var request = this._makeRequest("UNLOCK", "", internalHandlers, headers); + if (this._authString) + headers['Authorization'] = this._authString; + + let request = this._makeRequest("UNLOCK", "", handlers, headers); request.send(null); - }, + let event = yield; - _onLock: function DC__onLock(event) { - //notice("acquired lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); - this._token = "woo"; - if (this._lockHandlers && this._lockHandlers.load) - this._lockHandlers.load(event); - }, - _onLockError: function DC__onLockError(event) { - //notice("lock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); - if (this._lockHandlers && this._lockHandlers.error) - this._lockHandlers.error(event); - }, + if (event.target.status < 200 || event.target.status >= 300) { + generatorDone(this, onComplete, false); + return; + } - _onUnlock: function DC__onUnlock(event) { - //notice("removed lock (" + event.target.status + "):\n" + event.target.responseText + "\n"); this._token = null; - if (this._lockHandlers && this._lockHandlers.load) - this._lockHandlers.load(event); + + generatorDone(this, onComplete, true); }, - _onUnlockError: function DC__onUnlockError(event) { - //notice("unlock failed (" + event.target.status + "):\n" + event.target.responseText + "\n"); - if (this._lockHandlers && this._lockHandlers.error) - this._lockHandlers.error(event); + + stealLock: function DC_stealLock(onComplete) { + let generator = yield; + let handlers = generatorHandlers(generator); + let stolen = false; + + try { + if (!asyncRun(this, this._getActiveLock, handlers.complete)) + return; + this._token = yield; + this._token = this._token.replace("\n", "").replace("\n", ""); + + if (!asyncRun(this, this.unlock, handlers.complete)) + return; + let unlocked = yield; + + if (!unlocked) + return; + + if (!asyncRun(this, this.lock, handlers.complete)) + return; + stolen = yield; + + } finally { + generatorDone(this, onComplete, stolen); + } } }; @@ -1413,6 +1543,7 @@ DummyAuthProvider.prototype = { Ci.nsIAuthPromptProvider, Ci.nsIAuthPrompt, Ci.nsIPrompt, + Ci.nsIProgressEventSink, Ci.nsIInterfaceRequestor, Ci.nsISupports], @@ -1534,6 +1665,16 @@ DummyAuthProvider.prototype = { throw Cr.NS_ERROR_NOT_IMPLEMENTED; } }; + }, + + // nsIProgressEventSink + + onProgress: function DAP_onProgress(aRequest, aContext, + aProgress, aProgressMax) { + }, + + onStatus: function DAP_onStatus(aRequest, aContext, + aStatus, aStatusArg) { } }; From 9c7747d0e5637068f2e96245d36903b558dd1a1b Mon Sep 17 00:00:00 2001 From: Date: Thu, 4 Oct 2007 02:25:57 -0700 Subject: [PATCH 0030/1860] print http status codes on errors; improve xpath queries we use to get the dav lock tokens --- services/sync/nsBookmarksSyncService.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 795886c68411..127507545a1f 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -1038,7 +1038,8 @@ BookmarksSyncService.prototype = { uneval(this._snapshot), handlers); let snapPut = yield; if (snapPut.target.status < 200 || snapPut.target.status >= 300) { - this.notice("Error: could not upload snapshot to server"); + this.notice("Error: could not upload snapshot to server, error code: " + + snapPut.target.status); generatorDone(this, onComplete, ret) return; } @@ -1046,7 +1047,8 @@ BookmarksSyncService.prototype = { this._dav.PUT("bookmarks-deltas.json", uneval([]), handlers); let deltasPut = yield; if (deltasPut.target.status < 200 || deltasPut.target.status >= 300) { - this.notice("Error: could not upload deltas to server"); + this.notice("Error: could not upload deltas to server, error code: " + + deltasPut.target.status); generatorDone(this, onComplete, ret) return; } @@ -1058,7 +1060,8 @@ BookmarksSyncService.prototype = { maxVersion: this._snapshotVersion}), handlers); let statusPut = yield; if (statusPut.target.status < 200 || statusPut.target.status >= 300) { - this.notice("Error: could not upload status file to server"); + this.notice("Error: could not upload status file to server, error code: " + + statusPut.target.status); generatorDone(this, onComplete, ret) return; } @@ -1433,10 +1436,10 @@ DAVCollection.prototype = { let request = this._makeRequest("PROPFIND", "", handlers, headers); request.send("" + "" + - "" + + " " + ""); let event = yield; - let tokens = xpath(event.target.responseXML, '//D:locktoken'); + let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); if (token) generatorDone(this, onComplete, token.textContent); @@ -1448,7 +1451,8 @@ DAVCollection.prototype = { let generator = yield; let handlers = generatorHandlers(generator); - headers = {'Content-Type': 'text/xml; charset="utf-8"'}; + headers = {'Content-Type': 'text/xml; charset="utf-8"', + 'Depth': 'infinity'}; if (this._authString) headers['Authorization'] = this._authString; @@ -1465,11 +1469,10 @@ DAVCollection.prototype = { return; } - let tokens = xpath(event.target.responseXML, '//D:locktoken'); + let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); if (token) { this._token = token.textContent; - this._token = this._token.replace("\n", "").replace("\n", ""); generatorDone(this, onComplete, true); } @@ -1512,7 +1515,6 @@ DAVCollection.prototype = { if (!asyncRun(this, this._getActiveLock, handlers.complete)) return; this._token = yield; - this._token = this._token.replace("\n", "").replace("\n", ""); if (!asyncRun(this, this.unlock, handlers.complete)) return; From 99bfa93c919781a1cdda6771261d3481b67ec9dc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 4 Oct 2007 23:41:09 -0700 Subject: [PATCH 0031/1860] * add support for livemarks and microsummaries * add locking support * don't use db type constants, define our own instead * standardize capitalization of acronyms Note: server-side needs to be wiped, no migration code included. --- services/sync/nsBookmarksSyncService.js | 373 +++++++++++++----------- 1 file changed, 210 insertions(+), 163 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 127507545a1f..a7dbc981ab2c 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -87,6 +87,22 @@ BookmarksSyncService.prototype = { return this.__ts; }, + __ls: null, + get _ls() { + if (!this.__ls) + this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. + getService(Ci.nsILivemarkService); + return this.__ls; + }, + + __ms: null, + get _ms() { + if (!this.__ms) + this.__ms = Cc["@mozilla.org/microsummary/service;1"]. + getService(Ci.nsIMicrosummaryService); + return this.__ms; + }, + __os: null, get _os() { if (!this.__os) @@ -125,17 +141,17 @@ BookmarksSyncService.prototype = { _snapshot: {}, _snapshotVersion: 0, - __snapshotGuid: null, - get _snapshotGuid() { - if (!this.__snapshotGuid) { + __snapshotGUID: null, + get _snapshotGUID() { + if (!this.__snapshotGUID) { let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. getService(Ci.nsIUUIDGenerator); - this.__snapshotGuid = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); + this.__snapshotGUID = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); } - return this.__snapshotGuid; + return this.__snapshotGUID; }, - set _snapshotGuid(guid) { - this.__snapshotGuid = guid; + set _snapshotGUID(GUID) { + this.__snapshotGUID = GUID; }, get currentUser() { @@ -197,7 +213,7 @@ BookmarksSyncService.prototype = { fos.init(file, flags, PERMS_FILE, 0); let out = {version: this._snapshotVersion, - guid: this._snapshotGuid, + GUID: this._snapshotGUID, snapshot: this._snapshot}; out = uneval(out); fos.write(out, out.length); @@ -228,10 +244,10 @@ BookmarksSyncService.prototype = { fis.close(); json = eval(json); - if (json && json.snapshot && json.version && json.guid) { + if (json && json.snapshot && json.version && json.GUID) { this._snapshot = json.snapshot; this._snapshotVersion = json.version; - this._snapshotGuid = json.guid; + this._snapshotGUID = json.GUID; } }, @@ -241,35 +257,47 @@ BookmarksSyncService.prototype = { return items; }, - _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGuid, index) { - var guid = this._bms.getItemGUID(node.itemId); - var item = {type: node.type, - parentGuid: parentGuid, + _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) { + let GUID = this._bms.getItemGUID(node.itemId); + let item = {parentGUID: parentGUID, index: index}; if (node.type == node.RESULT_TYPE_FOLDER) { - item.title = node.title; - - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - - for (var i = 0; i < node.childCount; i++) { - this._wrapNodeInternal(node.getChild(i), items, guid, i); + if (this._ls.isLivemark(node.itemId)) { + item.type = "livemark"; + item.siteURI = this._ls.getSiteURI(node.itemId).spec; + item.feedURI = this._ls.getFeedURI(node.itemId).spec; + } else { + item.type = "folder"; + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._wrapNodeInternal(node.getChild(i), items, GUID, i); + } } - } else if (node.type == node.RESULT_TYPE_SEPARATOR) { - } else if (node.type == node.RESULT_TYPE_URI) { - // FIXME: need to verify that it's a bookmark, it could be a history result! item.title = node.title; - item.uri = node.uri; + } else if (node.type == node.RESULT_TYPE_URI) { + if (this._ms.hasMicrosummary(node.itemId)) { + item.type = "microsummary"; + let micsum = this._ms.getMicrosummary(node.itemId); + item.generatorURI = micsum.generator.uri.spec; // FIXME: might not be a remote generator! + } else { + item.type = "bookmark"; + item.title = node.title; + } + item.URI = node.uri; item.tags = this._ts.getTagsForURI(makeURI(node.uri)); let keyword = this._bms.getKeywordForBookmark(node.itemId); if (keyword) item.keyword = keyword; + } else if (node.type == node.RESULT_TYPE_SEPARATOR) { + item.type = "separator"; } else { - // what do we do? + this.notice("Warning: unknown item type, cannot serialize: " + node.type); + return; } - items[guid] = item; + items[GUID] = item; }, // FIXME: this won't work for deep objects, or objects with optional properties @@ -291,43 +319,43 @@ BookmarksSyncService.prototype = { return ret; }, - _nodeParents: function BSS__nodeParents(guid, tree) { - return this._nodeParentsInt(guid, tree, []); + _nodeParents: function BSS__nodeParents(GUID, tree) { + return this._nodeParentsInt(GUID, tree, []); }, - _nodeParentsInt: function BSS__nodeParentsInt(guid, tree, parents) { - if (!tree[guid] || !tree[guid].parentGuid) + _nodeParentsInt: function BSS__nodeParentsInt(GUID, tree, parents) { + if (!tree[GUID] || !tree[GUID].parentGUID) return parents; - parents.push(tree[guid].parentGuid); - return this._nodeParentsInt(tree[guid].parentGuid, tree, parents); + parents.push(tree[GUID].parentGUID); + return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); }, _detectUpdates: function BSS__detectUpdates(a, b) { let cmds = []; - for (let guid in a) { - if (guid in b) { - let edits = this._getEdits(a[guid], b[guid]); + for (let GUID in a) { + if (GUID in b) { + let edits = this._getEdits(a[GUID], b[GUID]); if (edits == -1) // something went very wrong -- FIXME continue; if (edits.numProps == 0) // no changes - skip continue; - let parents = this._nodeParents(guid, b); - cmds.push({action: "edit", guid: guid, + let parents = this._nodeParents(GUID, b); + cmds.push({action: "edit", GUID: GUID, depth: parents.length, parents: parents, data: edits.props}); } else { - let parents = this._nodeParents(guid, a); // ??? - cmds.push({action: "remove", guid: guid, + let parents = this._nodeParents(GUID, a); // ??? + cmds.push({action: "remove", GUID: GUID, depth: parents.length, parents: parents}); } } - for (let guid in b) { - if (guid in a) + for (let GUID in b) { + if (GUID in a) continue; - let parents = this._nodeParents(guid, b); - cmds.push({action: "create", guid: guid, + let parents = this._nodeParents(GUID, b); + cmds.push({action: "create", GUID: GUID, depth: parents.length, parents: parents, - data: b[guid]}); + data: b[GUID]}); } cmds.sort(function(a, b) { if (a.depth > b.depth) @@ -345,10 +373,10 @@ BookmarksSyncService.prototype = { _conflicts: function BSS__conflicts(a, b) { if ((a.depth < b.depth) && - (b.parents.indexOf(a.guid) >= 0) && + (b.parents.indexOf(a.GUID) >= 0) && a.action == "remove") return true; - if ((a.guid == b.guid) && !this._deepEquals(a, b)) + if ((a.GUID == b.GUID) && !this._deepEquals(a, b)) return true; return false; }, @@ -357,38 +385,46 @@ BookmarksSyncService.prototype = { // are in the same folder. Folders and separators must be at the // same index to qualify for 'likeness'. _commandLike: function BSS__commandLike(a, b) { - if (!a || !b) - return false; - if (a.action != b.action) - return false; - - // Items with the same guid do not qualify - they need to be - // processed for edits - if (a.guid == b.guid) - return false; - - // this check works because reconcile() fixes up the parent guids - // as it runs, and the command list is sorted by depth - if (a.parentGuid != b.parentGuid) + // Check that neither command is null, that their actions, types, + // and parents are the same, and that they don't have the same + // GUID. + // Items with the same GUID do not qualify because they need to be + // processed for edits. + // The parent GUID check works because reconcile() fixes up the + // parent GUIDs as it runs, and the command list is sorted by + // depth + if (!a || !b || + a.action != b.action || + a.data.type != b.data.type || + a.parentGUID != b.parentGUID || + a.GUID == b.GUID) return false; switch (a.data.type) { - case 0: - if (b.data.type == a.data.type && - b.data.uri == a.data.uri && - b.data.title == a.data.title) + case "bookmark": + if (a.data.URI == b.data.URI && + a.data.title == b.data.title) return true; return false; - case 6: - if (b.index == a.index && - b.data.type == a.data.type && - b.data.title == a.data.title) + case "microsummary": + if (a.data.URI == b.data.URI && + a.data.generatorURI == b.data.generatorURI) return true; return false; - case 7: - if (b.index == a.index && - b.data.type == a.data.type) + case "folder": + if (a.index == b.index && + a.data.title == b.data.title) + return true; + return false; + case "livemark": + if (a.data.title == b.data.title && + a.data.siteURI == b.data.siteURI && + a.data.feedURI == b.data.feedURI) + return true; + return false; + case "separator": + if (a.index == b.index) return true; return false; default: @@ -417,18 +453,18 @@ BookmarksSyncService.prototype = { return true; }, - // When we change the guid of a local item (because we detect it as + // When we change the GUID of a local item (because we detect it as // being the same item as a remote one), we need to fix any other // local items that have it as their parent - _fixParents: function BSS__fixParents(list, oldGuid, newGuid) { + _fixParents: function BSS__fixParents(list, oldGUID, newGUID) { for (let i = 0; i < list.length; i++) { if (!list[i]) continue; - if (list[i].parentGuid == oldGuid) - list[i].parentGuid = newGuid; + if (list[i].parentGUID == oldGUID) + list[i].parentGUID = newGUID; for (let j = 0; j < list[i].parents.length; j++) { - if (list[i].parents[j] == oldGuid) - list[i].parents[j] = newGuid; + if (list[i].parents[j] == oldGUID) + list[i].parents[j] = newGUID; } } }, @@ -467,12 +503,12 @@ BookmarksSyncService.prototype = { delete listA[i]; delete listB[j]; } else if (this._commandLike(listA[i], listB[j])) { - // Disregard likeness if the target guid already exists locally - if (this._bms.getItemIdForGUID(listB[j].guid) >= 0) + // Disregard likeness if the target GUID already exists locally + if (this._bms.getItemIdForGUID(listB[j].GUID) >= 0) continue; - this._fixParents(listA, listA[i].guid, listB[j].guid); - listB[j].data = {guid: listB[j].guid}; - listB[j].guid = listA[i].guid; + this._fixParents(listA, listA[i].GUID, listB[j].GUID); + listB[j].data = {GUID: listB[j].GUID}; + listB[j].GUID = listA[i].GUID; listB[j].action = "edit"; delete listA[i]; } @@ -491,10 +527,10 @@ BookmarksSyncService.prototype = { if (this._conflicts(listA[i], listB[j]) || this._conflicts(listB[j], listA[i])) { if (!conflicts[0].some( - function(elt) { return elt.guid == listA[i].guid })) + function(elt) { return elt.GUID == listA[i].GUID })) conflicts[0].push(listA[i]); if (!conflicts[1].some( - function(elt) { return elt.guid == listB[j].guid })) + function(elt) { return elt.GUID == listB[j].GUID })) conflicts[1].push(listB[j]); } } @@ -502,13 +538,13 @@ BookmarksSyncService.prototype = { for (let i = 0; i < listA.length; i++) { // need to check if a previous conflict might break this cmd if (!conflicts[0].some( - function(elt) { return elt.guid == listA[i].guid })) + function(elt) { return elt.GUID == listA[i].GUID })) propagations[1].push(listA[i]); } for (let j = 0; j < listB.length; j++) { // need to check if a previous conflict might break this cmd if (!conflicts[1].some( - function(elt) { return elt.guid == listB[j].guid })) + function(elt) { return elt.GUID == listB[j].GUID })) propagations[0].push(listB[j]); } @@ -527,15 +563,15 @@ BookmarksSyncService.prototype = { this.notice("Applying cmd to obj: " + uneval(commands[i])); switch (commands[i].action) { case "create": - obj[commands[i].guid] = eval(uneval(commands[i].data)); + obj[commands[i].GUID] = eval(uneval(commands[i].data)); break; case "edit": for (let prop in commands[i].data) { - obj[commands[i].guid][prop] = commands[i].data[prop]; + obj[commands[i].GUID][prop] = commands[i].data[prop]; } break; case "remove": - delete obj[commands[i].guid]; + delete obj[commands[i].GUID]; break; } } @@ -566,7 +602,7 @@ BookmarksSyncService.prototype = { _createCommand: function BSS__createCommand(command) { let newId; - let parentId = this._bms.getItemIdForGUID(command.data.parentGuid); + let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); if (parentId <= 0) { this.notice("Warning: creating node with unknown parent -> reparenting to root"); @@ -576,23 +612,37 @@ BookmarksSyncService.prototype = { this.notice(" -> creating item"); switch (command.data.type) { - case 0: - let uri = makeURI(command.data.uri); + case "bookmark": + case "microsummary": + let URI = makeURI(command.data.URI); newId = this._bms.insertBookmark(parentId, - uri, + URI, command.data.index, command.data.title); - this._ts.untagURI(uri, null); - this._ts.tagURI(uri, command.data.tags); + this._ts.untagURI(URI, null); + this._ts.tagURI(URI, command.data.tags); if (command.data.keyword) this._bms.setKeywordForBookmark(newId, command.data.keyword); + + if (command.data.type == "microsummary") { + let genURI = makeURI(command.data.generatorURI); + let micsum = this._ms.createMicrosummary(URI, genURI); + this._ms.setMicrosummary(newId, micsum); + } break; - case 6: + case "folder": newId = this._bms.createFolder(parentId, command.data.title, command.data.index); break; - case 7: + case "livemark": + newId = this._ls.createLivemark(parentId, + command.data.title, + makeURI(command.data.siteURI), + makeURI(command.data.feedURI), + command.data.index); + break; + case "separator": newId = this._bms.insertSeparator(parentId, command.data.index); break; default: @@ -600,25 +650,24 @@ BookmarksSyncService.prototype = { break; } if (newId) - this._bms.setItemGUID(newId, command.guid); + this._bms.setItemGUID(newId, command.GUID); }, _removeCommand: function BSS__removeCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.guid); + var itemId = this._bms.getItemIdForGUID(command.GUID); var type = this._bms.getItemType(itemId); switch (type) { case this._bms.TYPE_BOOKMARK: - // FIXME: check it's an actual bookmark? - this.notice(" -> removing bookmark " + command.guid); + this.notice(" -> removing bookmark " + command.GUID); this._bms.removeItem(itemId); break; case this._bms.TYPE_FOLDER: - this.notice(" -> removing folder " + command.guid); + this.notice(" -> removing folder " + command.GUID); this._bms.removeFolder(itemId); break; case this._bms.TYPE_SEPARATOR: - this.notice(" -> removing separator " + command.guid); + this.notice(" -> removing separator " + command.GUID); this._bms.removeItem(itemId); break; default: @@ -628,49 +677,59 @@ BookmarksSyncService.prototype = { }, _editCommand: function BSS__editCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.guid); + var itemId = this._bms.getItemIdForGUID(command.GUID); if (itemId == -1) { - this.notice("Warning: item for guid " + command.guid + " not found. Skipping."); + this.notice("Warning: item for GUID " + command.GUID + " not found. Skipping."); return; } - var type = this._bms.getItemType(itemId); - for (let key in command.data) { switch (key) { - case "guid": - var existing = this._bms.getItemIdForGUID(command.data.guid); + case "GUID": + var existing = this._bms.getItemIdForGUID(command.data.GUID); if (existing == -1) - this._bms.setItemGUID(itemId, command.data.guid); + this._bms.setItemGUID(itemId, command.data.GUID); else - this.notice("Warning: can't change guid " + command.guid + - " to " + command.data.guid + ": guid already exists."); + this.notice("Warning: can't change GUID " + command.GUID + + " to " + command.data.GUID + ": GUID already exists."); break; case "title": this._bms.setItemTitle(itemId, command.data.title); break; - case "uri": - this._bms.changeBookmarkURI(itemId, makeURI(command.data.uri)); + case "URI": + this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI)); break; case "index": this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), command.data.index); break; - case "parentGuid": + case "parentGUID": let index = -1; if (command.data.index && command.data.index >= 0) index = command.data.index; this._bms.moveItem( - itemId, this._bms.getItemIdForGUID(command.data.parentGuid), index); + itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); break; case "tags": - let uri = this._bms.getBookmarkURI(itemId); - this._ts.untagURI(uri, null); - this._ts.tagURI(uri, command.data.tags); + let tagsURI = this._bms.getBookmarkURI(itemId); + this._ts.untagURI(URI, null); + this._ts.tagURI(tagsURI, command.data.tags); break; case "keyword": this._bms.setKeywordForBookmark(itemId, command.data.keyword); break; + case "generatorURI": + let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); + let genURI = makeURI(command.data.generatorURI); + let micsum = this._ms.createMicrosummary(micsumURI, genURI); + this._ms.setMicrosummary(itemId, micsum); + break; + case "siteURI": + this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); + break; + case "feedURI": + this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); + break; default: this.notice("Warning: Can't change item property: " + key); break; @@ -691,10 +750,10 @@ BookmarksSyncService.prototype = { let json = uneval(nodes); json = json.replace(/:{type/g, ":\n\t{type"); json = json.replace(/}, /g, "},\n "); - json = json.replace(/, parentGuid/g, ",\n\t parentGuid"); + json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); json = json.replace(/, index/g, ",\n\t index"); json = json.replace(/, title/g, ",\n\t title"); - json = json.replace(/, uri/g, ",\n\t uri"); + json = json.replace(/, URI/g, ",\n\t URI"); json = json.replace(/, tags/g, ",\n\t tags"); json = json.replace(/, keyword/g, ",\n\t keyword"); return json; @@ -730,28 +789,16 @@ BookmarksSyncService.prototype = { this.notice("Beginning sync"); try { -// if (!asyncRun(this._dav, this._dav.lock, handlers.complete)) -// return; -// let locked = yield; -// -// if (locked) -// this.notice("Lock acquired"); -// else { -// this.notice("Could not acquire lock, aborting"); -// FIXME -// this.notice("Could not acquire lock, attempting to steal it"); -// -// if (!asyncRun(this._dav, this._dav.stealLock, handlers.complete)) -// return; -// let stolen = yield; -// -// if (stolen) -// this.notice("Lock stolen"); -// else { -// this.notice("Could not steal lock, aborting"); -// return; -// } -// } + if (!asyncRun(this._dav, this._dav.lock, handlers.complete)) + return; + let locked = yield; + + if (locked) + this.notice("Lock acquired"); + else { + this.notice("Could not acquire lock, aborting sync"); + return; + } var localJson = this._getBookmarks(); //this.notice("local json:\n" + this._mungeNodes(localJson)); @@ -867,7 +914,7 @@ BookmarksSyncService.prototype = { // FIXME: need to watch out for the storage format version changing, // in that case we'll have to re-upload all the files, not just these this._dav.PUT("bookmarks-status.json", - uneval({guid: this._snapshotGuid, + uneval({GUID: this._snapshotGUID, formatVersion: STORAGE_FORMAT_VERSION, snapVersion: server.snapVersion, maxVersion: this._snapshotVersion}), handlers); @@ -886,14 +933,13 @@ BookmarksSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:end", ""); this.notice("Sync complete"); } finally { -// FIXME -// if (!asyncRun(this._dav, this._dav.unlock, handlers.complete)) -// return; -// let unlocked = yield; -// if (unlocked) -// this.notice("Lock released"); -// else -// this.notice("Error: could not unlock DAV collection"); + if (!asyncRun(this._dav, this._dav.unlock, handlers.complete)) + return; + let unlocked = yield; + if (unlocked) + this.notice("Lock released"); + else + this.notice("Error: could not unlock DAV collection"); this._os.notifyObservers(null, "bookmarks-sync:end", ""); } }, @@ -945,12 +991,12 @@ BookmarksSyncService.prototype = { return; } - if (status.guid != this._snapshotGuid) { - this.notice("Remote/local sync guids do not match. " + + if (status.GUID != this._snapshotGUID) { + this.notice("Remote/local sync GUIDs do not match. " + "Forcing initial sync."); this._snapshot = {}; this._snapshotVersion = -1; - this._snapshotGuid = status.guid; + this._snapshotGUID = status.GUID; } if (this._snapshotVersion < status.snapVersion) { @@ -1032,7 +1078,7 @@ BookmarksSyncService.prototype = { this._snapshot = localJson; this._snapshotVersion = 0; - this._snapshotGuid = null; // in case there are other snapshots out there + this._snapshotGUID = null; // in case there are other snapshots out there this._dav.PUT("bookmarks-snapshot.json", uneval(this._snapshot), handlers); @@ -1054,7 +1100,7 @@ BookmarksSyncService.prototype = { } this._dav.PUT("bookmarks-status.json", - uneval({guid: this._snapshotGuid, + uneval({GUID: this._snapshotGUID, formatVersion: STORAGE_FORMAT_VERSION, snapVersion: this._snapshotVersion, maxVersion: this._snapshotVersion}), handlers); @@ -1143,10 +1189,10 @@ function makeFile(path) { return file; } -function makeURI(uriString) { +function makeURI(URIString) { var ioservice = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); - return ioservice.newURI(uriString, null, null); + return ioservice.newURI(URIString, null, null); } function bind2(object, method) { @@ -1311,7 +1357,7 @@ DAVCollection.prototype = { if (this._authString) headers['Authorization'] = this._authString; if (this._token) - headers['If'] = "(<" + this._token + ">)"; + headers['If'] = "<" + this._bashURL + "> (<" + this._token + ">)"; var request = this._makeRequest("GET", path, handlers, headers); request.send(null); @@ -1323,7 +1369,7 @@ DAVCollection.prototype = { if (this._authString) headers['Authorization'] = this._authString; if (this._token) - headers['If'] = "(<" + this._token + ">)"; + headers['If'] = "<" + this._bashURL + "> (<" + this._token + ">)"; var request = this._makeRequest("PUT", path, handlers, headers); request.send(data); @@ -1337,7 +1383,7 @@ DAVCollection.prototype = { error: bind2(this, this._onLoginError)}; try { - let uri = makeURI(this._baseURL); + let URI = makeURI(this._baseURL); let username = 'nobody@mozilla.com'; let password; @@ -1347,7 +1393,7 @@ DAVCollection.prototype = { // fixme: make a request and get the realm let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, uri.hostPort, null, + let logins = lm.findLogins({}, URI.hostPort, null, 'Use your ldap username/password - dotmoz'); //notice("Found " + logins.length + " logins"); @@ -1452,6 +1498,7 @@ DAVCollection.prototype = { let handlers = generatorHandlers(generator); headers = {'Content-Type': 'text/xml; charset="utf-8"', + 'Timeout': 'Second-600', 'Depth': 'infinity'}; if (this._authString) headers['Authorization'] = this._authString; From be13fa7bb0e47efad23f3564dd264e38653fc31a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Oct 2007 03:05:01 -0700 Subject: [PATCH 0032/1860] * Fix broken check when loading the locally saved snapshot from disk. * Add syntactic sugar for "async" functions (generators that can continue themselves) * Do away with separate load & error handlers in general - we never used them. * Wrap generator bodies in a try block to ensure (with a finally block) that we execute the generator closing protocol. * Refactor login code somewhat. --- services/sync/nsBookmarksSyncService.js | 417 +++++++++++------------- 1 file changed, 191 insertions(+), 226 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index a7dbc981ab2c..402c1c9c19d0 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -244,7 +244,8 @@ BookmarksSyncService.prototype = { fis.close(); json = eval(json); - if (json && json.snapshot && json.version && json.GUID) { + if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { + this.notice("Reading saved snapshot from disk"); this._snapshot = json.snapshot; this._snapshotVersion = json.version; this._snapshotGUID = json.GUID; @@ -485,10 +486,9 @@ BookmarksSyncService.prototype = { // [2dcA1, 2dcA2, ...], [2dcB1, 2dcB2, ...]] _reconcile: function BSS__reconcile(onComplete, listA, listB) { - let generator = yield; + let cont = yield; + let listener = new EventListener(cont); this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let handlers = generatorHandlers(generator); - let listener = new EventListener(handlers['complete']); let propagations = [[], []]; let conflicts = [[], []]; @@ -782,14 +782,13 @@ BookmarksSyncService.prototype = { // 3.2) Append server delta to the delta file and upload _doSync: function BSS__doSync() { - var generator = yield; - var handlers = generatorHandlers(generator); + let cont = yield; this._os.notifyObservers(null, "bookmarks-sync:start", ""); this.notice("Beginning sync"); try { - if (!asyncRun(this._dav, this._dav.lock, handlers.complete)) + if (!this._dav.lock.async(this._dav, cont)) return; let locked = yield; @@ -804,7 +803,7 @@ BookmarksSyncService.prototype = { //this.notice("local json:\n" + this._mungeNodes(localJson)); // 1) Fetch server deltas - if (asyncRun(this, this._getServerData, handlers.complete, localJson)) + if (this._getServerData.async(this, cont, localJson)) let server = yield; else return @@ -838,8 +837,8 @@ BookmarksSyncService.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. this.notice("Reconciling client/server updates"); - if (asyncRun(this, this._reconcile, handlers.complete, - localUpdates, server.updates)) + if (this._reconcile.async(this, cont, + localUpdates, server.updates)) let ret = yield; else return @@ -908,7 +907,7 @@ BookmarksSyncService.prototype = { this._snapshotVersion = ++server.maxVersion; server.deltas.push(serverChanges); - this._dav.PUT("bookmarks-deltas.json", uneval(server.deltas), handlers); + this._dav.PUT("bookmarks-deltas.json", uneval(server.deltas), cont); let deltasPut = yield; // FIXME: need to watch out for the storage format version changing, @@ -917,7 +916,7 @@ BookmarksSyncService.prototype = { uneval({GUID: this._snapshotGUID, formatVersion: STORAGE_FORMAT_VERSION, snapVersion: server.snapVersion, - maxVersion: this._snapshotVersion}), handlers); + maxVersion: this._snapshotVersion}), cont); let statusPut = yield; if (deltasPut.target.status >= 200 && deltasPut.target.status < 300 && @@ -933,7 +932,7 @@ BookmarksSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:end", ""); this.notice("Sync complete"); } finally { - if (!asyncRun(this._dav, this._dav.unlock, handlers.complete)) + if (!this._dav.unlock.async(this._dav, cont)) return; let unlocked = yield; if (unlocked) @@ -965,15 +964,14 @@ BookmarksSyncService.prototype = { * combined into a single set. */ _getServerData: function BSS__getServerData(onComplete, localJson) { - let generator = yield; - let handlers = generatorHandlers(generator); + let cont = yield; let ret = {status: -1, formatVersion: null, maxVersion: null, snapVersion: null, snapshot: null, deltas: null, updates: null}; this.notice("Getting bookmarks status from server"); - this._dav.GET("bookmarks-status.json", handlers); + this._dav.GET("bookmarks-status.json", cont); let statusResp = yield; switch (statusResp.target.status) { @@ -1004,7 +1002,7 @@ BookmarksSyncService.prototype = { this.notice("Local snapshot is out of date"); this.notice("Downloading server snapshot"); - this._dav.GET("bookmarks-snapshot.json", handlers); + this._dav.GET("bookmarks-snapshot.json", cont); let snapResp = yield; if (snapResp.target.status < 200 || snapResp.target.status >= 300) { this.notice("Error: could not download server snapshot"); @@ -1014,7 +1012,7 @@ BookmarksSyncService.prototype = { snap = eval(snapResp.target.responseText); this.notice("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", handlers); + this._dav.GET("bookmarks-deltas.json", cont); let deltasResp = yield; if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { this.notice("Error: could not download server deltas"); @@ -1029,7 +1027,7 @@ BookmarksSyncService.prototype = { snap = eval(uneval(this._snapshot)); this.notice("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", handlers); + this._dav.GET("bookmarks-deltas.json", cont); let deltasResp = yield; if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { this.notice("Error: could not download server deltas"); @@ -1044,7 +1042,7 @@ BookmarksSyncService.prototype = { // FIXME: could optimize this case by caching deltas file this.notice("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", handlers); + this._dav.GET("bookmarks-deltas.json", cont); let deltasResp = yield; if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { this.notice("Error: could not download server deltas"); @@ -1081,7 +1079,7 @@ BookmarksSyncService.prototype = { this._snapshotGUID = null; // in case there are other snapshots out there this._dav.PUT("bookmarks-snapshot.json", - uneval(this._snapshot), handlers); + uneval(this._snapshot), cont); let snapPut = yield; if (snapPut.target.status < 200 || snapPut.target.status >= 300) { this.notice("Error: could not upload snapshot to server, error code: " + @@ -1090,7 +1088,7 @@ BookmarksSyncService.prototype = { return; } - this._dav.PUT("bookmarks-deltas.json", uneval([]), handlers); + this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); let deltasPut = yield; if (deltasPut.target.status < 200 || deltasPut.target.status >= 300) { this.notice("Error: could not upload deltas to server, error code: " + @@ -1103,7 +1101,7 @@ BookmarksSyncService.prototype = { uneval({GUID: this._snapshotGUID, formatVersion: STORAGE_FORMAT_VERSION, snapVersion: this._snapshotVersion, - maxVersion: this._snapshotVersion}), handlers); + maxVersion: this._snapshotVersion}), cont); let statusPut = yield; if (statusPut.target.status < 200 || statusPut.target.status >= 300) { this.notice("Error: could not upload status file to server, error code: " + @@ -1132,13 +1130,13 @@ BookmarksSyncService.prototype = { generatorDone(this, onComplete, ret) }, - _onLogin: function BSS__onLogin(event) { - this.notice("Bookmarks sync server: " + this._dav.baseURL); - this._os.notifyObservers(null, "bookmarks-sync:login", ""); - }, - - _onLoginError: function BSS__onLoginError(event) { - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + _onLogin: function BSS__onLogin(success) { + if (success) { + this.notice("Bookmarks sync server: " + this._dav.baseURL); + this._os.notifyObservers(null, "bookmarks-sync:login", ""); + } else { + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + } }, // Interfaces this component implements. @@ -1155,12 +1153,13 @@ BookmarksSyncService.prototype = { // nsIBookmarksSyncService sync: function BSS_sync() { - asyncRun(this, this._doSync); + this._doSync.async(this); }, login: function BSS_login() { - this._dav.login({load: bind2(this, this._onLogin), - error: bind2(this, this._onLoginError)}); + let callback = bind2(this, this._onLogin); + if (!this._dav.login.async(this._dav, callback)) + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); }, logout: function BSS_logout() { @@ -1199,13 +1198,13 @@ function bind2(object, method) { return function innerBind() { return method.apply(object, arguments); } } -function asyncRun(object, method, onComplete, extra) { - let args = Array.prototype.slice.call(arguments, 2); // onComplete + extra + ... +Function.prototype.async = function(self, extra_args) { + let args = Array.prototype.slice.call(arguments, 1); let ret = false; try { - let gen = method.apply(object, args); + let gen = this.apply(self, args); gen.next(); // must initialize before sending - gen.send(gen); + gen.send(function(data) { continueGenerator(gen, data); }); ret = true; } catch (e) { if (e instanceof StopIteration) @@ -1226,19 +1225,12 @@ function continueGenerator(generator, data) { } } -function generatorHandlers(generator) { - let cb = function(data) { - continueGenerator(generator, data); - }; - return {load: cb, error: cb, complete: cb}; -} - -// generators called using asyncRun can't simply call the callback -// with the return value, since that would cause the calling +// generators called using Function.async can't simply call the +// callback with the return value, since that would cause the calling // function to end up running (after the yield) from inside the -// generator. Instead, generators can call this method which sets -// up a timer to call the callback from a timer (and cleans up the -// timer to avoid leaks). +// generator. Instead, generators can call this method which sets up +// a timer to call the callback from a timer (and cleans up the timer +// to avoid leaks). function generatorDone(object, callback, retval) { if (object._timer) throw "Called generatorDone when there is a timer already set." @@ -1311,12 +1303,52 @@ DAVCollection.prototype = { return this.__base64; }, + // FIXME: should we regen this each time to prevent it from staying in memory? + __auth: null, + get _auth() { + if (this.__auth) + return this.__auth; + + try { + let URI = makeURI(this._baseURL); + let username = 'nobody@mozilla.com'; + let password; + + let branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + username = branch.getCharPref("browser.places.sync.username"); + + if (!username) + return null; + + // fixme: make a request and get the realm + let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, URI.hostPort, null, + 'Use your ldap username/password - dotmoz'); + + for (let i = 0; i < logins.length; i++) { + if (logins[i].username == username) { + password = logins[i].password; + break; + } + } + + if (!password) + return null; + + this.__auth = "Basic " + + this._base64.Base64.encode(username + ":" + password); + + } catch (e) {} + + return this.__auth; + }, + get baseURL() { return this._baseURL; }, _loggedIn: false, - _authString: null, _currentUserPath: "nobody", _currentUser: "nobody@mozilla.com", @@ -1324,19 +1356,12 @@ DAVCollection.prototype = { return this._currentUser; }, - _addHandler: function DC__addHandler(request, handlers, eventName) { - if (handlers[eventName]) - request.addEventListener(eventName, new EventListener(handlers[eventName]), false); - }, - _makeRequest: function DC__makeRequest(op, path, handlers, headers) { + _makeRequest: function DC__makeRequest(op, path, onComplete, headers) { var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); - if (!handlers) - handlers = {}; - this._addHandler(request, handlers, "load"); - this._addHandler(request, handlers, "error"); - + request.addEventListener("load", new EventListener(onComplete), false); + request.addEventListener("error", new EventListener(onComplete), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); request.open(op, this._baseURL + path, true); @@ -1351,229 +1376,169 @@ DAVCollection.prototype = { return request; }, - GET: function DC_GET(path, handlers, headers) { + GET: function DC_GET(path, onComplete, headers) { if (!headers) headers = {'Content-type': 'text/plain'}; - if (this._authString) - headers['Authorization'] = this._authString; + if (this._auth) + headers['Authorization'] = this._auth; if (this._token) headers['If'] = "<" + this._bashURL + "> (<" + this._token + ">)"; - - var request = this._makeRequest("GET", path, handlers, headers); + let request = this._makeRequest("GET", path, onComplete, headers); request.send(null); }, - PUT: function DC_PUT(path, data, handlers, headers) { + PUT: function DC_PUT(path, data, onComplete, headers) { if (!headers) headers = {'Content-type': 'text/plain'}; - if (this._authString) - headers['Authorization'] = this._authString; + if (this._auth) + headers['Authorization'] = this._auth; if (this._token) headers['If'] = "<" + this._bashURL + "> (<" + this._token + ">)"; - - var request = this._makeRequest("PUT", path, handlers, headers); + let request = this._makeRequest("PUT", path, onComplete, headers); request.send(data); }, // Login / Logout - login: function DC_login(handlers) { - this._loginHandlers = handlers; - internalHandlers = {load: bind2(this, this._onLogin), - error: bind2(this, this._onLoginError)}; + login: function DC_login(onComplete) { + let cont = yield; + this._loggedIn = false; try { - let URI = makeURI(this._baseURL); - let username = 'nobody@mozilla.com'; - let password; + let headers = {}; + if (this._auth) + headers['Authorization'] = this._auth; + + this._authProvider._authFailed = false; + let request = this._makeRequest("GET", "", cont, headers); + request.send(null); + let event = yield; + + if (this._authProvider._authFailed || event.target.status >= 400) + return; + + // XXX we need to refine some server response codes let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); - username = branch.getCharPref("browser.places.sync.username"); + this._currentUser = branch.getCharPref("browser.places.sync.username"); + + // XXX + let path = this._currentUser.split("@"); + this._currentUserPath = path[0]; + this._baseURL = this._baseURL + this._currentUserPath + "/"; + this._loggedIn = true; - // fixme: make a request and get the realm - let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, URI.hostPort, null, - 'Use your ldap username/password - dotmoz'); - //notice("Found " + logins.length + " logins"); - - for (let i = 0; i < logins.length; i++) { - if (logins[i].username == username) { - password = logins[i].password; - break; - } - } - - // FIXME: should we regen this each time to prevent it from staying in memory? - this._authString = "Basic " + - this._base64.Base64.encode(username + ":" + password); - headers = {'Authorization': this._authString}; - } catch (e) { - // try without the auth header? - // fixme: may want to just fail here + } finally { + generatorDone(this, onComplete, this._loggedIn); } - this._authProvider._authFailed = false; - - let request = this._makeRequest("GET", "", internalHandlers, headers); - request.send(null); }, logout: function DC_logout() { - this._authString = null; - }, - - _onLogin: function DC__onLogin(event) { -// dump("logged in (" + event.target.status + "):\n" + -// event.target.responseText + "\n"); - - if (this._authProvider._authFailed || event.target.status >= 400) { - this._onLoginError(event); - return; - } - - // fixme: hacktastic -// let hello = /Hello (.+)@mozilla.com/.exec(event.target.responseText) -// let hello = /Index of/.exec(event.target.responseText) -// if (hello) { -// this._currentUserPath = hello[1]; -// this._currentUser = this._currentUserPath + "@mozilla.com"; -// this._baseURL = "http://dotmoz.mozilla.org/~" + -// this._currentUserPath + "/"; -// } - - // XXX we need to refine some server response codes - let branch = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch); - this._currentUser = branch.getCharPref("browser.places.sync.username"); - - // XXX - let path = this._currentUser.split("@"); - this._currentUserPath = path[0]; - - let serverURL = branch.getCharPref("browser.places.sync.serverURL"); - this._baseURL = serverURL + this._currentUserPath + "/"; - - this._loggedIn = true; - - if (this._loginHandlers && this._loginHandlers.load) - this._loginHandlers.load(event); - }, - _onLoginError: function DC__onLoginError(event) { - //notice("login failed (" + event.target.status + "):\n" + - // event.target.responseText + "\n"); - this._loggedIn = false; - - if (this._loginHandlers && this._loginHandlers.error) - this._loginHandlers.error(event); + this.__auth = null; }, // Locking _getActiveLock: function DC__getActiveLock(onComplete) { - let generator = yield; - let handlers = generatorHandlers(generator); + let cont = yield; + let ret = null; - let headers = {'Content-Type': 'text/xml; charset="utf-8"', - 'Depth': '0'}; - if (this._authString) - headers['Authorization'] = this._authString; + try { + let headers = {'Content-Type': 'text/xml; charset="utf-8"', + 'Depth': '0'}; + if (this._auth) + headers['Authorization'] = this._auth; - let request = this._makeRequest("PROPFIND", "", handlers, headers); - request.send("" + - "" + - " " + - ""); - let event = yield; - let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - if (token) - generatorDone(this, onComplete, token.textContent); - else - generatorDone(this, onComplete, null); + let request = this._makeRequest("PROPFIND", "", cont, headers); + request.send("" + + "" + + " " + + ""); + let event = yield; + let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + ret = token.textContent; + } finally { + generatorDone(this, onComplete, ret); + } }, lock: function DC_lock(onComplete) { - let generator = yield; - let handlers = generatorHandlers(generator); + let cont = yield; + this._token = null; - headers = {'Content-Type': 'text/xml; charset="utf-8"', - 'Timeout': 'Second-600', - 'Depth': 'infinity'}; - if (this._authString) - headers['Authorization'] = this._authString; + try { + headers = {'Content-Type': 'text/xml; charset="utf-8"', + 'Timeout': 'Second-600', + 'Depth': 'infinity'}; + if (this._auth) + headers['Authorization'] = this._auth; - let request = this._makeRequest("LOCK", "", handlers, headers); - request.send("\n" + - "\n" + - " \n" + - " \n" + - ""); - let event = yield; + let request = this._makeRequest("LOCK", "", cont, headers); + request.send("\n" + + "\n" + + " \n" + + " \n" + + ""); + let event = yield; - if (event.target.status < 200 || event.target.status >= 300) { - generatorDone(this, onComplete, false); - return; + if (event.target.status < 200 || event.target.status >= 300) + return; + + let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + if (token) + this._token = token.textContent; + + } finally { + generatorDone(this, onComplete, this._token); } - - let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - if (token) { - this._token = token.textContent; - generatorDone(this, onComplete, true); - } - - generatorDone(this, onComplete, false); }, unlock: function DC_unlock(onComplete) { - let generator = yield; - let handlers = generatorHandlers(generator); + let cont = yield; - if (!this._token) { - generatorDone(this, onComplete, false); - return; + try { + if (!this._token) + return; + + let headers = {'Lock-Token': "<" + this._token + ">"}; + if (this._auth) + headers['Authorization'] = this._auth; + + let request = this._makeRequest("UNLOCK", "", cont, headers); + request.send(null); + let event = yield; + + if (event.target.status < 200 || event.target.status >= 300) + return; + + this._token = null; + } finally { + if (this._token) + generatorDone(this, onComplete, false); + else + generatorDone(this, onComplete, true); } - - headers = {'Lock-Token': "<" + this._token + ">"}; - if (this._authString) - headers['Authorization'] = this._authString; - - let request = this._makeRequest("UNLOCK", "", handlers, headers); - request.send(null); - let event = yield; - - if (event.target.status < 200 || event.target.status >= 300) { - generatorDone(this, onComplete, false); - return; - } - - this._token = null; - - generatorDone(this, onComplete, true); }, stealLock: function DC_stealLock(onComplete) { - let generator = yield; - let handlers = generatorHandlers(generator); - let stolen = false; + let cont = yield; + let stolen = null; try { - if (!asyncRun(this, this._getActiveLock, handlers.complete)) + if (!this._getActiveLock.async(this, cont)) return; this._token = yield; - if (!asyncRun(this, this.unlock, handlers.complete)) + if (!this.unlock.async(this, cont)) return; let unlocked = yield; - if (!unlocked) - return; - - if (!asyncRun(this, this.lock, handlers.complete)) - return; - stolen = yield; - + if (unlocked && this.lock.async(this, cont)) + stolen = yield; } finally { generatorDone(this, onComplete, stolen); } From faadc4cc7a2e697fecfc9b038d680adf31016917 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Oct 2007 18:38:31 -0700 Subject: [PATCH 0033/1860] sync starred (but unfiled) items; fix autoconnect on browser startup; move all prefs under the same root (though we may need to move them all again later) --- services/sync/nsBookmarksSyncService.js | 31 +++++++++++++++++-------- services/sync/services-sync.js | 4 ++-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/nsBookmarksSyncService.js index 402c1c9c19d0..e4fbcf43ba2f 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/nsBookmarksSyncService.js @@ -288,9 +288,7 @@ BookmarksSyncService.prototype = { } item.URI = node.uri; item.tags = this._ts.getTagsForURI(makeURI(node.uri)); - let keyword = this._bms.getKeywordForBookmark(node.itemId); - if (keyword) - item.keyword = keyword; + item.keyword = this._bms.getKeywordForBookmark(node.itemId); } else if (node.type == node.RESULT_TYPE_SEPARATOR) { item.type = "separator"; } else { @@ -621,8 +619,7 @@ BookmarksSyncService.prototype = { command.data.title); this._ts.untagURI(URI, null); this._ts.tagURI(URI, command.data.tags); - if (command.data.keyword) - this._bms.setKeywordForBookmark(newId, command.data.keyword); + this._bms.setKeywordForBookmark(newId, command.data.keyword); if (command.data.type == "microsummary") { let genURI = makeURI(command.data.generatorURI); @@ -737,15 +734,29 @@ BookmarksSyncService.prototype = { } }, - _getBookmarks: function BMS__getBookmarks(folder) { - if (!folder) - folder = this._bms.bookmarksRoot; - var query = this._hsvc.getNewQuery(); + _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { + let query = this._hsvc.getNewQuery(); query.setFolders([folder], 1); - let root = this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + let root = this._hsvc.executeQuery(query, + this._hsvc.getNewQueryOptions()).root; return this._wrapNode(root); }, + _getBookmarks: function BSS__getBookmarks() { + let filed = this._getWrappedBookmarks(this._bms.bookmarksRoot); + let unfiled = this._getWrappedBookmarks(this._bms.unfiledRoot); + + for (let guid in unfiled) { + if (guid in filed) + this.notice("Warning: same bookmark (guid) in both " + + "filed and unfiled trees!"); + else + filed[guid] = unfiled[guid]; + } + + return filed; // (combined) + }, + _mungeNodes: function BSS__mungeNodes(nodes) { let json = uneval(nodes); json = json.replace(/:{type/g, ":\n\t{type"); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9767dc7ea7b1..2936e2da08ff 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,4 @@ pref("browser.places.sync.serverURL", "https://services.mozilla.com/user/"); pref("browser.places.sync.username", "nobody@mozilla.com"); -pref("extensions.sync.autoconnect", false); -pref("extensions.sync.lastsync", "0"); +pref("browser.places.sync.autoconnect", false); +pref("browser.places.sync.lastsync", "0"); From 9e79c2b406f6a2b7e88e5ba0e7ed56252bd7a616 Mon Sep 17 00:00:00 2001 From: Date: Wed, 10 Oct 2007 02:09:28 -0700 Subject: [PATCH 0034/1860] Add 'log4moz', a log4net clone for Mozilla --- ...SyncService.js => BookmarksSyncService.js} | 243 +++++---- ...cService.idl => IBookmarksSyncService.idl} | 14 +- services/sync/IBookmarksSyncService.xpt | Bin 0 -> 194 bytes services/sync/ILog4MozService.idl | 137 +++++ services/sync/ILog4MozService.idl~ | 132 +++++ services/sync/ILog4MozService.xpt | Bin 0 -> 1246 bytes services/sync/Log4MozService.js | 472 ++++++++++++++++++ services/sync/nsIBookmarksSyncService.xpt | Bin 213 -> 0 bytes services/sync/xptgen | 5 +- 9 files changed, 866 insertions(+), 137 deletions(-) rename services/sync/{nsBookmarksSyncService.js => BookmarksSyncService.js} (88%) rename services/sync/{nsIBookmarksSyncService.idl => IBookmarksSyncService.idl} (88%) create mode 100644 services/sync/IBookmarksSyncService.xpt create mode 100644 services/sync/ILog4MozService.idl create mode 100644 services/sync/ILog4MozService.idl~ create mode 100644 services/sync/ILog4MozService.xpt create mode 100644 services/sync/Log4MozService.js delete mode 100644 services/sync/nsIBookmarksSyncService.xpt diff --git a/services/sync/nsBookmarksSyncService.js b/services/sync/BookmarksSyncService.js similarity index 88% rename from services/sync/nsBookmarksSyncService.js rename to services/sync/BookmarksSyncService.js index e4fbcf43ba2f..7f7c54014720 100644 --- a/services/sync/nsBookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -50,6 +50,10 @@ const PERMS_DIRECTORY = 0755; const STORAGE_FORMAT_VERSION = 0; +const ONE_BYTE = 1; +const ONE_KILOBYTE = 1024 * ONE_BYTE; +const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function BookmarksSyncService() { this._init(); } @@ -127,7 +131,7 @@ BookmarksSyncService.prototype = { return this.__dirSvc; }, - // File output stream to a log file + // Logger object _log: null, // Timer object for yielding to the main loop @@ -159,7 +163,7 @@ BookmarksSyncService.prototype = { }, _init: function BSS__init() { - this._initLog(); + this._initLogs(); let serverURL = 'https://dotmoz.mozilla.org/'; try { let branch = Cc["@mozilla.org/preferences-service;1"]. @@ -167,35 +171,48 @@ BookmarksSyncService.prototype = { serverURL = branch.getCharPref("browser.places.sync.serverURL"); } catch (ex) { /* use defaults */ } - this.notice("Bookmarks login server: " + serverURL); + this._log.config("Bookmarks login server: " + serverURL); this._dav = new DAVCollection(serverURL); this._readSnapshot(); }, - _initLog: function BSS__initLog() { + _initLogs: function BSS__initLogs() { + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + + this._log = logSvc.getLogger("main"); + + let formatter = logSvc.newFormatter("basic"); + let root = logSvc.rootLogger; + root.level = root.LEVEL_ALL; + + let capp = logSvc.newAppender("console", formatter); + capp.level = root.LEVEL_INFO; + root.addAppender(capp); + + let dapp = logSvc.newAppender("dump", formatter); + dapp.level = root.LEVEL_INFO; + root.addAppender(dapp); + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); + getService(Ci.nsIProperties); + let logFile = dirSvc.get("ProfD", Ci.nsIFile); + let verboseFile = logFile.clone(); + logFile.append("bm-sync.log"); + logFile.QueryInterface(Ci.nsILocalFile); + verboseFile.append("bm-sync-verbose.log"); + verboseFile.QueryInterface(Ci.nsILocalFile); - let file = dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync.log"); - file.QueryInterface(Ci.nsILocalFile); - - if (file.exists()) - file.remove(false); - file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); - - this._log = Cc["@mozilla.org/network/file-output-stream;1"] - .createInstance(Ci.nsIFileOutputStream); - let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; - this._log.init(file, flags, PERMS_FILE, 0); - - let str = "Bookmarks Sync Log\n------------------\n\n"; - this._log.write(str, str.length); - this._log.flush(); + let fapp = logSvc.newFileAppender("rotating", logFile, formatter); + fapp.level = root.LEVEL_CONFIG; + root.addAppender(fapp); + let vapp = logSvc.newFileAppender("rotating", verboseFile, formatter); + vapp.level = root.LEVEL_ALL; + root.addAppender(vapp); }, _saveSnapshot: function BSS__saveSnapshot() { - this.notice("Saving snapshot to disk"); + this._log.info("Saving snapshot to disk"); let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); @@ -245,7 +262,7 @@ BookmarksSyncService.prototype = { json = eval(json); if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { - this.notice("Reading saved snapshot from disk"); + this._log.info("Read saved snapshot from disk"); this._snapshot = json.snapshot; this._snapshotVersion = json.version; this._snapshotGUID = json.GUID; @@ -292,7 +309,7 @@ BookmarksSyncService.prototype = { } else if (node.type == node.RESULT_TYPE_SEPARATOR) { item.type = "separator"; } else { - this.notice("Warning: unknown item type, cannot serialize: " + node.type); + this._log.logWarn("Warning: unknown item type, cannot serialize: " + node.type); return; } @@ -427,7 +444,7 @@ BookmarksSyncService.prototype = { return true; return false; default: - this.notice("_commandLike: Unknown item type: " + uneval(a)); + this._log.error("_commandLike: Unknown item type: " + uneval(a)); return false; } }, @@ -549,16 +566,11 @@ BookmarksSyncService.prototype = { this._timer = null; let ret = {propagations: propagations, conflicts: conflicts}; generatorDone(this, onComplete, ret); - - // shutdown protocol - let cookie = yield; - if (cookie != "generator shutdown") - this.notice("_reconcile: Error: generator not properly shut down.") }, _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { for (let i = 0; i < commands.length; i++) { - this.notice("Applying cmd to obj: " + uneval(commands[i])); + this._log.info("Applying cmd to obj: " + uneval(commands[i])); switch (commands[i].action) { case "create": obj[commands[i].GUID] = eval(uneval(commands[i].data)); @@ -580,7 +592,7 @@ BookmarksSyncService.prototype = { _applyCommands: function BSS__applyCommands(commandList) { for (var i = 0; i < commandList.length; i++) { var command = commandList[i]; - this.notice("Processing command: " + uneval(command)); + this._log.info("Processing command: " + uneval(command)); switch (command["action"]) { case "create": this._createCommand(command); @@ -592,7 +604,7 @@ BookmarksSyncService.prototype = { this._editCommand(command); break; default: - this.notice("unknown action in command: " + command["action"]); + this._log.error("unknown action in command: " + command["action"]); break; } } @@ -603,11 +615,11 @@ BookmarksSyncService.prototype = { let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); if (parentId <= 0) { - this.notice("Warning: creating node with unknown parent -> reparenting to root"); + this._log.warning("Creating node with unknown parent -> reparenting to root"); parentId = this._bms.bookmarksRoot; } - this.notice(" -> creating item"); + this._log.info(" -> creating item"); switch (command.data.type) { case "bookmark": @@ -643,7 +655,7 @@ BookmarksSyncService.prototype = { newId = this._bms.insertSeparator(parentId, command.data.index); break; default: - this.notice("_createCommand: Unknown item type: " + command.data.type); + this._log.error("_createCommand: Unknown item type: " + command.data.type); break; } if (newId) @@ -656,19 +668,19 @@ BookmarksSyncService.prototype = { switch (type) { case this._bms.TYPE_BOOKMARK: - this.notice(" -> removing bookmark " + command.GUID); + this._log.info(" -> removing bookmark " + command.GUID); this._bms.removeItem(itemId); break; case this._bms.TYPE_FOLDER: - this.notice(" -> removing folder " + command.GUID); + this._log.info(" -> removing folder " + command.GUID); this._bms.removeFolder(itemId); break; case this._bms.TYPE_SEPARATOR: - this.notice(" -> removing separator " + command.GUID); + this._log.info(" -> removing separator " + command.GUID); this._bms.removeItem(itemId); break; default: - this.notice("removeCommand: Unknown item type: " + type); + this._log.error("removeCommand: Unknown item type: " + type); break; } }, @@ -676,7 +688,7 @@ BookmarksSyncService.prototype = { _editCommand: function BSS__editCommand(command) { var itemId = this._bms.getItemIdForGUID(command.GUID); if (itemId == -1) { - this.notice("Warning: item for GUID " + command.GUID + " not found. Skipping."); + this._log.warning("Item for GUID " + command.GUID + " not found. Skipping."); return; } @@ -687,7 +699,7 @@ BookmarksSyncService.prototype = { if (existing == -1) this._bms.setItemGUID(itemId, command.data.GUID); else - this.notice("Warning: can't change GUID " + command.GUID + + this._log.warning("Can't change GUID " + command.GUID + " to " + command.data.GUID + ": GUID already exists."); break; case "title": @@ -728,7 +740,7 @@ BookmarksSyncService.prototype = { this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); break; default: - this.notice("Warning: Can't change item property: " + key); + this._log.warning("Can't change item property: " + key); break; } } @@ -748,7 +760,7 @@ BookmarksSyncService.prototype = { for (let guid in unfiled) { if (guid in filed) - this.notice("Warning: same bookmark (guid) in both " + + this._log.warning("Same bookmark (guid) in both " + "filed and unfiled trees!"); else filed[guid] = unfiled[guid]; @@ -796,7 +808,7 @@ BookmarksSyncService.prototype = { let cont = yield; this._os.notifyObservers(null, "bookmarks-sync:start", ""); - this.notice("Beginning sync"); + this._log.info("Beginning sync"); try { if (!this._dav.lock.async(this._dav, cont)) @@ -804,14 +816,14 @@ BookmarksSyncService.prototype = { let locked = yield; if (locked) - this.notice("Lock acquired"); + this._log.info("Lock acquired"); else { - this.notice("Could not acquire lock, aborting sync"); + this._log.warning("Could not acquire lock, aborting sync"); return; } var localJson = this._getBookmarks(); - //this.notice("local json:\n" + this._mungeNodes(localJson)); + //this._log.info("local json:\n" + this._mungeNodes(localJson)); // 1) Fetch server deltas if (this._getServerData.async(this, cont, localJson)) @@ -819,35 +831,35 @@ BookmarksSyncService.prototype = { else return - this.notice("Local snapshot version: " + this._snapshotVersion); - this.notice("Server status: " + server.status); + this._log.info("Local snapshot version: " + this._snapshotVersion); + this._log.info("Server status: " + server.status); if (server['status'] != 0) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); - this.notice("Sync error: could not get server status, " + - "or initial upload failed."); + this._log.error("Sync error: could not get server status, " + + "or initial upload failed."); return; } - this.notice("Server maxVersion: " + server.maxVersion); - this.notice("Server snapVersion: " + server.snapVersion); - this.notice("Server updates: " + this._mungeCommands(server.updates)); + this._log.info("Server maxVersion: " + server.maxVersion); + this._log.info("Server snapVersion: " + server.snapVersion); + this._log.debug("Server updates: " + this._mungeCommands(server.updates)); // 2) Generate local deltas from snapshot -> current client status let localUpdates = this._detectUpdates(this._snapshot, localJson); - this.notice("Local updates: " + this._mungeCommands(localUpdates)); + this._log.debug("Local updates: " + this._mungeCommands(localUpdates)); if (server.updates.length == 0 && localUpdates.length == 0) { this._snapshotVersion = server.maxVersion; this._os.notifyObservers(null, "bookmarks-sync:end", ""); - this.notice("Sync complete (1): no changes needed on client or server"); + this._log.info("Sync complete (1): no changes needed on client or server"); return; } // 3) Reconcile client/server deltas and generate new deltas for them. - this.notice("Reconciling client/server updates"); + this._log.info("Reconciling client/server updates"); if (this._reconcile.async(this, cont, localUpdates, server.updates)) let ret = yield; @@ -871,15 +883,19 @@ BookmarksSyncService.prototype = { if (ret.conflicts && ret.conflicts[1]) serverConflicts = ret.conflicts[1]; - this.notice("Changes for client: " + this._mungeCommands(clientChanges)); - this.notice("Changes for server: " + this._mungeCommands(serverChanges)); - this.notice("Client conflicts: " + this._mungeConflicts(clientConflicts)); - this.notice("Server conflicts: " + this._mungeConflicts(serverConflicts)); + this._log.info("Changes for client: " + clientChanges.length); + this._log.info("Changes for server: " + serverChanges.length); + this._log.info("Client conflicts: " + clientConflicts.length); + this._log.info("Server conflicts: " + serverConflicts.length); + this._log.debug("Changes for client: " + this._mungeCommands(clientChanges)); + this._log.debug("Changes for server: " + this._mungeCommands(serverChanges)); + this._log.debug("Client conflicts: " + this._mungeConflicts(clientConflicts)); + this._log.debug("Server conflicts: " + this._mungeConflicts(serverConflicts)); if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { this._os.notifyObservers(null, "bookmarks-sync:end", ""); - this.notice("Sync complete (2): no changes needed on client or server"); + this._log.info("Sync complete (2): no changes needed on client or server"); this._snapshot = localJson; this._snapshotVersion = server.maxVersion; this._saveSnapshot(); @@ -887,12 +903,12 @@ BookmarksSyncService.prototype = { } if (clientConflicts.length || serverConflicts.length) { - this.notice("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); + this._log.error("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); } // 3.1) Apply server changes to local store if (clientChanges.length) { - this.notice("Applying changes locally"); + this._log.info("Applying changes locally"); // Note that we need to need to apply client changes to the // current tree, not the saved snapshot @@ -903,8 +919,9 @@ BookmarksSyncService.prototype = { let newSnapshot = this._getBookmarks(); let diff = this._detectUpdates(this._snapshot, newSnapshot); if (diff.length != 0) { - this.notice("Error: commands did not apply correctly. Diff:\n" + - uneval(diff)); + this._log.error("Commands did not apply correctly"); + this._log.debug("Diff from snapshot+commands -> " + + "new snapshot after commands:\n" + uneval(diff)); this._snapshot = newSnapshot; // FIXME: What else can we do? } @@ -913,7 +930,7 @@ BookmarksSyncService.prototype = { // 3.2) Append server delta to the delta file and upload if (serverChanges.length) { - this.notice("Uploading changes to server"); + this._log.info("Uploading changes to server"); this._snapshot = this._getBookmarks(); this._snapshotVersion = ++server.maxVersion; server.deltas.push(serverChanges); @@ -932,24 +949,24 @@ BookmarksSyncService.prototype = { if (deltasPut.target.status >= 200 && deltasPut.target.status < 300 && statusPut.target.status >= 200 && statusPut.target.status < 300) { - this.notice("Successfully updated deltas and status on server"); + this._log.info("Successfully updated deltas and status on server"); this._saveSnapshot(); } else { // FIXME: revert snapshot here? - can't, we already applied // updates locally! - need to save and retry - this.notice("Error: could not update deltas on server"); + this._log.error("Could not update deltas on server"); } } this._os.notifyObservers(null, "bookmarks-sync:end", ""); - this.notice("Sync complete"); + this._log.info("Sync complete"); } finally { if (!this._dav.unlock.async(this._dav, cont)) return; let unlocked = yield; if (unlocked) - this.notice("Lock released"); + this._log.info("Lock released"); else - this.notice("Error: could not unlock DAV collection"); + this._log.error("Could not unlock DAV collection"); this._os.notifyObservers(null, "bookmarks-sync:end", ""); } }, @@ -981,27 +998,27 @@ BookmarksSyncService.prototype = { formatVersion: null, maxVersion: null, snapVersion: null, snapshot: null, deltas: null, updates: null}; - this.notice("Getting bookmarks status from server"); + this._log.info("Getting bookmarks status from server"); this._dav.GET("bookmarks-status.json", cont); let statusResp = yield; switch (statusResp.target.status) { case 200: - this.notice("Got bookmarks status from server"); + this._log.info("Got bookmarks status from server"); let status = eval(statusResp.target.responseText); let snap, deltas, allDeltas; // Bail out if the server has a newer format version than we can parse if (status.formatVersion > STORAGE_FORMAT_VERSION) { - this.notice("Error: server uses storage format v" + status.formatVersion + - ", this client understands up to v" + STORAGE_FORMAT_VERSION); + this._log.error("Server uses storage format v" + status.formatVersion + + ", this client understands up to v" + STORAGE_FORMAT_VERSION); generatorDone(this, onComplete, ret) return; } if (status.GUID != this._snapshotGUID) { - this.notice("Remote/local sync GUIDs do not match. " + + this._log.info("Remote/local sync GUIDs do not match. " + "Forcing initial sync."); this._snapshot = {}; this._snapshotVersion = -1; @@ -1010,23 +1027,23 @@ BookmarksSyncService.prototype = { if (this._snapshotVersion < status.snapVersion) { if (this._snapshotVersion >= 0) - this.notice("Local snapshot is out of date"); + this._log.info("Local snapshot is out of date"); - this.notice("Downloading server snapshot"); + this._log.info("Downloading server snapshot"); this._dav.GET("bookmarks-snapshot.json", cont); let snapResp = yield; if (snapResp.target.status < 200 || snapResp.target.status >= 300) { - this.notice("Error: could not download server snapshot"); + this._log.error("Could not download server snapshot"); generatorDone(this, onComplete, ret) return; } snap = eval(snapResp.target.responseText); - this.notice("Downloading server deltas"); + this._log.info("Downloading server deltas"); this._dav.GET("bookmarks-deltas.json", cont); let deltasResp = yield; if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { - this.notice("Error: could not download server deltas"); + this._log.error("Could not download server deltas"); generatorDone(this, onComplete, ret) return; } @@ -1037,11 +1054,11 @@ BookmarksSyncService.prototype = { this._snapshotVersion < status.maxVersion) { snap = eval(uneval(this._snapshot)); - this.notice("Downloading server deltas"); + this._log.info("Downloading server deltas"); this._dav.GET("bookmarks-deltas.json", cont); let deltasResp = yield; if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { - this.notice("Error: could not download server deltas"); + this._log.error("Could not download server deltas"); generatorDone(this, onComplete, ret) return; } @@ -1052,11 +1069,11 @@ BookmarksSyncService.prototype = { snap = eval(uneval(this._snapshot)); // FIXME: could optimize this case by caching deltas file - this.notice("Downloading server deltas"); + this._log.info("Downloading server deltas"); this._dav.GET("bookmarks-deltas.json", cont); let deltasResp = yield; if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { - this.notice("Error: could not download server deltas"); + this._log.error("Could not download server deltas"); generatorDone(this, onComplete, ret) return; } @@ -1064,7 +1081,7 @@ BookmarksSyncService.prototype = { deltas = []; } else { // this._snapshotVersion > status.maxVersion - this.notice("Error: server snapshot is older than local snapshot"); + this._log.error("Server snapshot is older than local snapshot"); generatorDone(this, onComplete, ret) return; } @@ -1083,7 +1100,7 @@ BookmarksSyncService.prototype = { break; case 404: - this.notice("Server has no status file, Initial upload to server"); + this._log.info("Server has no status file, Initial upload to server"); this._snapshot = localJson; this._snapshotVersion = 0; @@ -1093,7 +1110,7 @@ BookmarksSyncService.prototype = { uneval(this._snapshot), cont); let snapPut = yield; if (snapPut.target.status < 200 || snapPut.target.status >= 300) { - this.notice("Error: could not upload snapshot to server, error code: " + + this._log.error("Could not upload snapshot to server, error code: " + snapPut.target.status); generatorDone(this, onComplete, ret) return; @@ -1102,7 +1119,7 @@ BookmarksSyncService.prototype = { this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); let deltasPut = yield; if (deltasPut.target.status < 200 || deltasPut.target.status >= 300) { - this.notice("Error: could not upload deltas to server, error code: " + + this._log.error("Could not upload deltas to server, error code: " + deltasPut.target.status); generatorDone(this, onComplete, ret) return; @@ -1115,13 +1132,13 @@ BookmarksSyncService.prototype = { maxVersion: this._snapshotVersion}), cont); let statusPut = yield; if (statusPut.target.status < 200 || statusPut.target.status >= 300) { - this.notice("Error: could not upload status file to server, error code: " + + this._log.error("Could not upload status file to server, error code: " + statusPut.target.status); generatorDone(this, onComplete, ret) return; } - this.notice("Initial upload to server successful"); + this._log.info("Initial upload to server successful"); this._saveSnapshot(); ret.status = 0; @@ -1134,8 +1151,8 @@ BookmarksSyncService.prototype = { break; default: - this.notice("Could not get bookmarks.status: unknown HTTP status code " + - statusResp.target.status); + this._log.error("Could not get bookmarks.status: unknown HTTP status code " + + statusResp.target.status); break; } generatorDone(this, onComplete, ret) @@ -1143,25 +1160,23 @@ BookmarksSyncService.prototype = { _onLogin: function BSS__onLogin(success) { if (success) { - this.notice("Bookmarks sync server: " + this._dav.baseURL); + this._log.config("Bookmarks sync server: " + this._dav.baseURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); } else { this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); } }, - // Interfaces this component implements. - interfaces: [Ci.nsIBookmarksSyncService, Ci.nsISupports], - // XPCOM registration classDescription: "Bookmarks Sync Service", contractID: "@mozilla.org/places/sync-service;1", - classID: Components.ID("{6efd73bf-6a5a-404f-9246-f70a1286a3d6}"), - QueryInterface: XPCOMUtils.generateQI([Ci.nsIBookmarksSyncService, Ci.nsISupports]), + classID: Components.ID("{efb3ba58-69bc-42d5-a430-0746fa4b1a7f}"), + QueryInterface: XPCOMUtils.generateQI([Ci.IBookmarksSyncService, + Ci.nsISupports]), // nsISupports - // nsIBookmarksSyncService + // IBookmarksSyncService sync: function BSS_sync() { this._doSync.async(this); @@ -1176,20 +1191,6 @@ BookmarksSyncService.prototype = { logout: function BSS_logout() { this._dav.logout(); this._os.notifyObservers(null, "bookmarks-sync:logout", ""); - }, - - log: function BSS_log(line) { - }, - - notice: function BSS_notice(line) { - let fullLine = line + "\n"; - dump(fullLine); - this._console.logStringMessage(line); - this._log.write(fullLine, fullLine.length); - this._log.flush(); - }, - - error: function BSS_error(line) { } }; @@ -1260,21 +1261,14 @@ function EventListener(handler) { this._handler = handler; } EventListener.prototype = { - QueryInterface: function(iid) { - if (iid.Equals(Components.interfaces.nsITimerCallback) || - iid.Equals(Components.interfaces.nsISupports)) - return this; - throw Components.results.NS_ERROR_NO_INTERFACE; - }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), // DOM event listener - handleEvent: function EL_handleEvent(event) { this._handler(event); }, // nsITimerCallback - notify: function EL_notify(timer) { this._handler(timer); } @@ -1706,3 +1700,4 @@ DummyAuthProvider.prototype = { function NSGetModule(compMgr, fileSpec) { return XPCOMUtils.generateModule([BookmarksSyncService]); } + diff --git a/services/sync/nsIBookmarksSyncService.idl b/services/sync/IBookmarksSyncService.idl similarity index 88% rename from services/sync/nsIBookmarksSyncService.idl rename to services/sync/IBookmarksSyncService.idl index 0629126f3b2f..1ab991512b2a 100644 --- a/services/sync/nsIBookmarksSyncService.idl +++ b/services/sync/IBookmarksSyncService.idl @@ -15,7 +15,7 @@ * The Original Code is Places. * * The Initial Developer of the Original Code is - * Mozilla Corp. + * Mozilla Corporation * Portions created by the Initial Developer are Copyright (C) 2007 * the Initial Developer. All Rights Reserved. * @@ -38,8 +38,8 @@ #include "nsISupports.idl" -[scriptable, uuid(0a1eed46-f832-495c-bce7-cb308ccfd599)] -interface nsIBookmarksSyncService : nsISupports +[scriptable, uuid(efb3ba58-69bc-42d5-a430-0746fa4b1a7f)] +interface IBookmarksSyncService : nsISupports { /** * The currently logged-in user @@ -60,12 +60,4 @@ interface nsIBookmarksSyncService : nsISupports * Initiate a sync operation. */ void sync(); - - /** - * Write a notice to the logfile - * - * @param line - * The line of text to print to the logfile - */ - void notice(in AString line); }; diff --git a/services/sync/IBookmarksSyncService.xpt b/services/sync/IBookmarksSyncService.xpt new file mode 100644 index 0000000000000000000000000000000000000000..ae6d3d02afe39d59a601c653ba4be53aca015f0e GIT binary patch literal 194 zcmazDaQ64*3aKne^~p@)<&t7#VqjumU^oQCN + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsISupports.idl" + +interface nsIFile; + +[scriptable, uuid(eeb0adbc-afa3-4326-9a52-040ce4855e9d)] +interface ILogMessage : nsISupports +{ + attribute PRInt32 level; + attribute AString levelDesc; + attribute AString message; + attribute PRTime date; +}; + +[scriptable, uuid(ac7f40df-49e7-4270-86ec-9c905047e70e)] +interface IFormatter : nsISupports +{ + AString format(in ILogMessage message); +}; + +[scriptable, uuid(abe56c32-79f4-465e-ac20-8e39d33cba0b)] +interface IAppender : nsISupports +{ + attribute PRInt32 level; + void append(in ILogMessage message); +}; + +[scriptable, uuid(2c1a0f87-f544-479c-80e0-acd11ae52ebf)] +interface ILogger : nsISupports +{ + /* + * Level constants + * Also here (in addition to the service) for convenience + */ + const PRInt32 LEVEL_FATAL = 70; + const PRInt32 LEVEL_ERROR = 60; + const PRInt32 LEVEL_WARN = 50; + const PRInt32 LEVEL_INFO = 40; + const PRInt32 LEVEL_CONFIG = 30; + const PRInt32 LEVEL_DEBUG = 20; + const PRInt32 LEVEL_TRACE = 10; + const PRInt32 LEVEL_ALL = 0; + + /* + * The assigned level for this logger. Applies also to descendant + * loggers unless overridden. + */ + attribute PRInt32 level; + + /* + * Add an appender to this logger's list. Anything logged via this + * logger will be sent to the list of appenders (including its + * parent logger's appenders). + */ + void addAppender(in IAppender appender); + + /* + * Log messages at various levels + */ + + void fatal(in AString message); + void error(in AString message); + void warning(in AString message); + void info(in AString message); + void config(in AString message); + void debug(in AString message); + void trace(in AString message); +}; + +[scriptable, uuid(cbfb3abe-4ca9-498a-a692-581f8da52323)] +interface ILoggerRepository : nsISupports +{ +}; + +[scriptable, uuid(a60e50d7-90b8-4a12-ad0c-79e6a1896978)] +interface ILog4MozService : nsISupports +{ + /* + * Level constants + */ + const PRInt32 LEVEL_FATAL = 70; + const PRInt32 LEVEL_ERROR = 60; + const PRInt32 LEVEL_WARN = 50; + const PRInt32 LEVEL_INFO = 40; + const PRInt32 LEVEL_CONFIG = 30; + const PRInt32 LEVEL_DEBUG = 20; + const PRInt32 LEVEL_TRACE = 10; + const PRInt32 LEVEL_ALL = 0; + + /* + * The root logger. Appenders added to this logger will be + * inherited by all other loggers + */ + readonly attribute ILogger rootLogger; + + /* + * Get a Logger object that can be used to print debug/info/error + * messages to various places. + * + * @param name + * The name of the logger. A new logger will be created + * with that name if it doesn't already exist. + * @returns The ILogger object that can be used to log messages. + */ + + ILogger getLogger(in AString name); + + IAppender newAppender(in AString kind, in IFormatter formatter); + IAppender newFileAppender(in AString kind, in nsIFile file, + in IFormatter formatter); + + IFormatter newFormatter(in AString kind); +}; diff --git a/services/sync/ILog4MozService.idl~ b/services/sync/ILog4MozService.idl~ new file mode 100644 index 000000000000..3c2f4d94c759 --- /dev/null +++ b/services/sync/ILog4MozService.idl~ @@ -0,0 +1,132 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is log4moz + * + * The Initial Developer of the Original Code is + * Michael Johnston + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsISupports.idl" + +[scriptable, uuid(eeb0adbc-afa3-4326-9a52-040ce4855e9d)] +interface ILogMessage : nsISupports +{ + attribute PRInt32 level; + attribute AString message; + attribute PRTime date; +}; + +[scriptable, uuid(ac7f40df-49e7-4270-86ec-9c905047e70e)] +interface IFormatter : nsISupports +{ + AString format(in ILogMessage message); +}; + +[scriptable, uuid(abe56c32-79f4-465e-ac20-8e39d33cba0b)] +interface IAppender : nsISupports +{ + attribute PRInt32 level; + void append(in ILogMessage message); +}; + +[scriptable, uuid(2c1a0f87-f544-479c-80e0-acd11ae52ebf)] +interface ILogger : nsISupports +{ + /* + * Level constants + * Also here (in addition to the service) for convenience + */ + const PRInt32 LEVEL_FATAL = 70; + const PRInt32 LEVEL_ERROR = 60; + const PRInt32 LEVEL_WARN = 50; + const PRInt32 LEVEL_INFO = 40; + const PRInt32 LEVEL_CONFIG = 30; + const PRInt32 LEVEL_DEBUG = 20; + const PRInt32 LEVEL_TRACE = 10; + const PRInt32 LEVEL_ALL = 0; + + /* + * The assigned level for this logger. Applies also to descendant + * loggers unless overridden. + */ + attribute PRInt32 level; + + /* + * Add an appender to this logger's list. Anything logged via this + * logger will be sent to the list of appenders (including its + * parent logger's appenders). + */ + void addAppender(in IAppender appender); + + /* + * Log messages at various levels + */ + + void fatal(in AString message); + void error(in AString message); + void warning(in AString message); + void info(in AString message); + void config(in AString message); + void debug(in AString message); + void trace(in AString message); +}; + +[scriptable, uuid(cbfb3abe-4ca9-498a-a692-581f8da52323)] +interface ILoggerRepository : nsISupports +{ +}; + +[scriptable, uuid(a60e50d7-90b8-4a12-ad0c-79e6a1896978)] +interface ILog4MozService : nsISupports +{ + /* + * Level constants + */ + const PRInt32 LEVEL_FATAL = 70; + const PRInt32 LEVEL_ERROR = 60; + const PRInt32 LEVEL_WARN = 50; + const PRInt32 LEVEL_INFO = 40; + const PRInt32 LEVEL_CONFIG = 30; + const PRInt32 LEVEL_DEBUG = 20; + const PRInt32 LEVEL_TRACE = 10; + const PRInt32 LEVEL_ALL = 0; + + /* + * The root logger. Appenders added to this logger will be + * inherited by all other loggers + */ + readonly attribute ILogger rootLogger; + + /* + * Get a Logger object that can be used to print debug/info/error + * messages to various places. + * + * @param name + * The name of the logger. A new logger will be created + * with that name if it doesn't already exist. + * @returns The ILogger object that can be used to log messages. + */ + + ILogger getLogger(in AString name); + + IAppender newAppender(in AString kind, in IFormatter formatter); + + IFormatter newFormatter(in AString kind); +}; diff --git a/services/sync/ILog4MozService.xpt b/services/sync/ILog4MozService.xpt new file mode 100644 index 0000000000000000000000000000000000000000..0e757a3806fd89bc8afec9124871870379f7df49 GIT binary patch literal 1246 zcmcJOOGp(_7{|YJM$K+!5!FKIr4TL3g#@9v@OtkZnV9Rf(H@)3(R=PNaGe=u#>Z{b zfK(JD;<9WJmA#aNHYvRpL5te-fNEncG9f}*Sc`(_JM${tH;Z$A=l{()-#7pH&asYE zE}Q5oSxnY@*C%QSg-U?3c>t&dAjCsh{gBndlRyx0S{udo)@(TYJ>53peVlq(vrzvS z^(|p++hlde+p+rxH%(WSK3_Sf4`U)Hh>?*y3&n=gve-SfbF}ew^V~WB`Y?#mXH%zI zK1hqL)|sypV;yaa)c`_Ri%|9aSJNZ;mUMn{{Me2Q*X!yqwkOypG%d|cKe&4}wfmw{ zR`uz0_hpPF!&ugEBvCIiz`1k4vP|1`K$1;OV>YISgoiJ_B0Ur;T9Dom4+#P0i8l`A z0e>OhY(U>)`VrIbh=>qVBBqT2q3L8R#dFa_Qp=$kp`JqnLL&$K9+Kv;9pMm%%?QUi zBoK5C;1$^*D?%`;`Fzr{n32biDX6Ym1ZLZ&4MVDJ=!OQmQ81y;Gzz)~dDc6iforRM z4CKs_jC?{&b|vMIGm4T^LOz;Q+C!Au#axJ~T)QZ>g^P5i^>D;pN-~uR^`tEO?(WZ; zCp(!vsADp8x!l9Cmh_B+;QL7TQE&F|>VV+QqT3ay1dh{GnXmAF@Q>OV?5e;s&`*j# z&G(ZQIiR0(m;?GrM>wFLbd&@7NiT6gKj{q)=qJ6$0X*9@-LM}t7Lmb*Vq=kc2d!9} zcE9Sn==%S4GR}z}3Le(H6m=?Hjyje8z%rYI_HSH26^X=hxCCeN~)%-tMiB$;KqAG;jeidAsA^gA=q&dIix%M>HUc}lP h(z7Ob?>+I5R#olyb(%SSL5KU)AEuAyyy`OWhd-@+Au0d> literal 0 HcmV?d00001 diff --git a/services/sync/Log4MozService.js b/services/sync/Log4MozService.js new file mode 100644 index 000000000000..c5b42f32ff35 --- /dev/null +++ b/services/sync/Log4MozService.js @@ -0,0 +1,472 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is log4moz + * + * The Initial Developer of the Original Code is + * Michael Johnston + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Michael Johnston + * Dan Mills + * + * ***** END LICENSE BLOCK ***** */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +const MODE_RDONLY = 0x01; +const MODE_WRONLY = 0x02; +const MODE_CREATE = 0x08; +const MODE_APPEND = 0x10; +const MODE_TRUNCATE = 0x20; + +const PERMS_FILE = 0644; +const PERMS_DIRECTORY = 0755; + +// Note: these are also available in the idl +const LEVEL_FATAL = 70; +const LEVEL_ERROR = 60; +const LEVEL_WARN = 50; +const LEVEL_INFO = 40; +const LEVEL_CONFIG = 30; +const LEVEL_DEBUG = 20; +const LEVEL_TRACE = 10; +const LEVEL_ALL = 0; + +const LEVEL_DESC = { + 70: "FATAL ", + 60: "ERROR ", + 50: "WARN ", + 40: "INFO ", + 30: "CONFIG", + 20: "DEBUG ", + 10: "TRACE ", + 0: "ALL " +}; + +const ONE_BYTE = 1; +const ONE_KILOBYTE = 1024 * ONE_BYTE; +const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +/* + * LoggingService + */ + +function Log4MozService() { + this._repository = new LoggerRepository(); +} +Log4MozService.prototype = { + classDescription: "Log4moz Logging Service", + contractID: "@mozilla.org/log4moz/service;1", + classID: Components.ID("{a60e50d7-90b8-4a12-ad0c-79e6a1896978}"), + QueryInterface: XPCOMUtils.generateQI([Ci.ILog4MozService, + Ci.nsISupports]), + + get rootLogger() { + return this._repository.rootLogger; + }, + + getLogger: function LogSvc_getLogger(name) { + return this._repository.getLogger(name); + }, + + newAppender: function LogSvc_newAppender(kind, formatter) { + switch (kind) { + case "dump": + return new DumpAppender(formatter); + case "console": + return new ConsoleAppender(formatter); + default: + dump("log4moz: unknown appender kind: " + kind); + return; + } + }, + + newFileAppender: function LogSvc_newAppender(kind, file, formatter) { + switch (kind) { + case "file": + return new FileAppender(file, formatter); + case "rotating": + // FIXME: hardcoded constants + return new RotatingFileAppender(file, formatter, ONE_MEGABYTE * 5, 0); + default: + dump("log4moz: unknown appender kind: " + kind); + return; + } + }, + + newFormatter: function LogSvc_newFormatter(kind) { + switch (kind) { + case "basic": + return new BasicFormatter(); + default: + dump("log4moz: unknown formatter kind: " + kind); + return; + } + } +}; + +function NSGetModule(compMgr, fileSpec) { + return XPCOMUtils.generateModule([Log4MozService]); +} + +/* + * LoggerRepository + * Implements a hierarchy of Loggers + */ + +function LoggerRepository() {} +LoggerRepository.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.ILoggerRepository, Ci.nsISupports]), + + _loggers: {}, + + _rootLogger: null, + get rootLogger() { + if (!this._rootLogger) { + this._rootLogger = new Logger("root", this); + this._rootLogger.level = LEVEL_ALL; + } + return this._rootLogger; + }, + // FIXME: need to update all parent values if we do this + //set rootLogger(logger) { + // this._rootLogger = logger; + //}, + + _updateParents: function LogRep__updateParents(name) { + let pieces = name.split('.'); + let cur, parent; + + // find the closest parent + for (let i = 0; i < pieces.length; i++) { + if (cur) + cur += '.' + pieces[i]; + else + cur = pieces[i]; + if (cur in this._loggers) + parent = cur; + } + + // if they are the same it has no parent + if (parent == name) + this._loggers[name].parent = this.rootLogger; + else + this._loggers[name].parent = this._loggers[parent]; + + // trigger updates for any possible descendants of this logger + for (let logger in this._loggers) { + if (logger != name && name.indexOf(logger) == 0) + this._updateParents(logger); + } + }, + + getLogger: function LogRep_getLogger(name) { + if (!(name in this._loggers)) { + this._loggers[name] = new Logger(name, this); + this._updateParents(name); + } + return this._loggers[name]; + } +} + +/* + * Logger + * Hierarchical version. Logs to all appenders, assigned or inherited + */ + +function Logger(name, repository) { + this._name = name; + this._repository = repository; + this._appenders = []; +} +Logger.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.ILogger, Ci.nsISupports]), + + parent: null, + + _level: -1, + get level() { + if (this._level >= 0) + return this._level; + if (this.parent) + return this.parent.level; + dump("log4moz warning: root logger configuration error: no level defined\n"); + return LEVEL_ALL; + }, + set level(level) { + this._level = level; + }, + + _appenders: null, + get appenders() { + if (!this.parent) + return this._appenders; + return this._appenders.concat(this.parent.appenders); + }, + + addAppender: function Logger_addAppender(appender) { + for (let i = 0; i < this._appenders.length; i++) { + if (this._appenders[i] == appender) + return; + } + this._appenders.push(appender); + }, + + log: function Logger_log(message) { + if (this.level > message.level) + return; + let appenders = this.appenders; + for (let i = 0; i < appenders.length; i++){ + appenders[i].append(message); + } + }, + + fatal: function Logger_fatal(string) { + this.log(new LogMessage(LEVEL_FATAL, string)); + }, + error: function Logger_error(string) { + this.log(new LogMessage(LEVEL_ERROR, string)); + }, + warning: function Logger_warning(string) { + this.log(new LogMessage(LEVEL_WARN, string)); + }, + info: function Logger_info(string) { + this.log(new LogMessage(LEVEL_INFO, string)); + }, + config: function Logger_config(string) { + this.log(new LogMessage(LEVEL_CONFIG, string)); + }, + debug: function Logger_debug(string) { + this.log(new LogMessage(LEVEL_DEBUG, string)); + }, + trace: function Logger_trace(string) { + this.log(new LogMessage(LEVEL_TRACE, string)); + } +}; + +/* + * Appenders + * These can be attached to Loggers to log to different places + * Simply subclass and override doAppend to implement a new one + */ + +function Appender(formatter) { + this._name = "Appender"; + this._formatter = formatter? formatter : new BasicFormatter(); +} +Appender.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.IAppender, Ci.nsISupports]), + + _level: LEVEL_ALL, + get level() { return this._level; }, + set level(level) { this._level = level; }, + + append: function App_append(message) { + if(this._level <= message.level) + this.doAppend(this._formatter.format(message)); + }, + toString: function App_toString() { + return this._name + " [level=" + this._level + + ", formatter=" + this._formatter + "]"; + }, + doAppend: function App_doAppend(message) {} +}; + +/* + * DumpAppender + * Logs to standard out + */ + +function DumpAppender(formatter) { + this._name = "DumpAppender"; + this._formatter = formatter; + this._level = LEVEL_ALL; +} +DumpAppender.prototype = new Appender(); +DumpAppender.prototype.doAppend = function DApp_doAppend(message) { + dump(message); +}; + +/* + * ConsoleAppender + * Logs to the javascript console + */ + +function ConsoleAppender(formatter) { + this._name = "ConsoleAppender"; + this._formatter = formatter; + this._level = LEVEL_ALL; +} +ConsoleAppender.prototype = new Appender(); +ConsoleAppender.prototype.doAppend = function CApp_doAppend(message) { + //error or normal? + if(message.level > LEVEL_WARN){ + Cu.reportError(message); + } else { + //get the js console + var consoleService = Components.classes["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + consoleService.logStringMessage(message); + } +}; + +/* + * FileAppender + * Logs to a file + */ + +function FileAppender(file, formatter) { + this._name = "FileAppender"; + this._file = file; // nsIFile + this._formatter = formatter; + this._level = LEVEL_ALL; + this.__fos = null; +} +FileAppender.prototype = new Appender(); +FileAppender.prototype._fos = function FApp__fos_get() { + if (!this.__fos) + this.openStream(); + return this.__fos; +}; +FileAppender.prototype.openStream = function FApp_openStream() { + this.__fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; + this.__fos.init(this._file, flags, PERMS_FILE, 0); +}; +FileAppender.prototype.closeStream = function FApp_closeStream() { + if (!this.__fos) + return; + try { + this.__fos.close(); + this.__fos = null; + } catch(e) { + dump("Failed to close file output stream\n" + e); + } +}; +FileAppender.prototype.doAppend = function FApp_doAppend(message) { + if (message === null || message.length <= 0) + return; + try { + this._fos().write(message, message.length); + } catch(e) { + dump("Error writing file:\n" + e); + } +}; + +/* + * RotatingFileAppender + * Similar to FileAppender, but rotates logs when they become too large + */ + +function RotatingFileAppender(file, formatter, maxSize, maxBackups) { + this._name = "RotatingFileAppender"; + this._file = file; // nsIFile + this._formatter = formatter; + this._level = LEVEL_ALL; + this._maxSize = maxSize; + this._maxBackups = maxBackups; +} +RotatingFileAppender.prototype = new FileAppender(); +RotatingFileAppender.prototype.doAppend = function RFApp_doAppend(message) { + if (message === null || message.length <= 0) + return; + try { + this.rotateLogs(); + this._fos().write(message, message.length); + } catch(e) { + dump("Error writing file:\n" + e); + } +}; +RotatingFileAppender.prototype.rotateLogs = function RFApp_rotateLogs() { + if(this._file.exists() && + this._file.fileSize < this._maxSize) + return; + + this.closeStream(); + + for (let i = this.maxBackups - 1; i > 0; i--){ + let backup = this._file.parent.clone(); + backup.append(this._file.leafName + "." + i); + if (backup.exists()) + backup.moveTo(this._file.parent, this._file.leafName + "." + (i + 1)); + } + + let cur = this._file.clone(); + if (cur.exists()) + cur.moveTo(cur.parent, cur.leafName + ".1"); + + // this._file still points to the same file +}; + +/* + * Formatters + * These massage a LogMessage into whatever output is desired + * Only the BasicFormatter is currently implemented + */ + +// FIXME: should allow for formatting the whole string, not just the date +function BasicFormatter(dateFormat) { + if (dateFormat) + this.dateFormat = dateFormat; +} +BasicFormatter.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.IFormatter, Ci.nsISupports]), + + _dateFormat: null, + + get dateFormat() { + if (!this._dateFormat) + this._dateFormat = "%Y-%m-%d %H:%M:%S"; + return this._dateFormat; + }, + set dateFormat(format) { + this._dateFormat = format; + }, + format: function BF_format(message) { + // FIXME: message.date.toLocaleFormat(this.dateFormat) + " " + + + return message.levelDesc + " " + message.message + "\n"; + } +}; + +/* + * LogMessage + * Encapsulates a single log event's data + */ +function LogMessage(level, message){ + this.message = message; + this.level = level; + this.date = new Date(); +} +LogMessage.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.ILogMessage, Ci.nsISupports]), + + get levelDesc() { + if (this.level in LEVEL_DESC) + return LEVEL_DESC[this.level]; + return "UNKNOWN"; + }, + toString: function LogMsg_toString(){ + return "LogMessage [" + this.date + " " + this.level + " " + + this.message + "]"; + } +}; diff --git a/services/sync/nsIBookmarksSyncService.xpt b/services/sync/nsIBookmarksSyncService.xpt deleted file mode 100644 index 29a8d498b5173151f789218d2594ef7d2cd517cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 213 zcmazDaQ64*3aKne^~p@)<&t7#VqjumV7LmzN9wh+tf?lYtGSGlc;{XF=&gC|$ Date: Wed, 10 Oct 2007 14:02:56 -0700 Subject: [PATCH 0035/1860] log the logger name; add a timestamp to logs --- services/sync/BookmarksSyncService.js | 30 ++++++------ services/sync/ILog4MozService.idl | 16 +------ services/sync/ILog4MozService.idl~ | 5 ++ services/sync/ILog4MozService.xpt | Bin 1246 -> 1123 bytes services/sync/Log4MozService.js | 63 ++++++++++++-------------- 5 files changed, 50 insertions(+), 64 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 7f7c54014720..f25959c6637e 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -115,14 +115,6 @@ BookmarksSyncService.prototype = { return this.__os; }, - __console: null, - get _console() { - if (!this.__console) - this.__console = Cc["@mozilla.org/consoleservice;1"] - .getService(Ci.nsIConsoleService); - return this.__console; - }, - __dirSvc: null, get _dirSvc() { if (!this.__dirSvc) @@ -171,7 +163,7 @@ BookmarksSyncService.prototype = { serverURL = branch.getCharPref("browser.places.sync.serverURL"); } catch (ex) { /* use defaults */ } - this._log.config("Bookmarks login server: " + serverURL); + this._log.info("Bookmarks login server: " + serverURL); this._dav = new DAVCollection(serverURL); this._readSnapshot(); }, @@ -187,11 +179,11 @@ BookmarksSyncService.prototype = { root.level = root.LEVEL_ALL; let capp = logSvc.newAppender("console", formatter); - capp.level = root.LEVEL_INFO; + capp.level = root.LEVEL_WARN; root.addAppender(capp); let dapp = logSvc.newAppender("dump", formatter); - dapp.level = root.LEVEL_INFO; + dapp.level = root.LEVEL_WARN; root.addAppender(dapp); let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. @@ -204,11 +196,12 @@ BookmarksSyncService.prototype = { verboseFile.QueryInterface(Ci.nsILocalFile); let fapp = logSvc.newFileAppender("rotating", logFile, formatter); - fapp.level = root.LEVEL_CONFIG; + fapp.level = root.LEVEL_INFO; root.addAppender(fapp); let vapp = logSvc.newFileAppender("rotating", verboseFile, formatter); vapp.level = root.LEVEL_ALL; root.addAppender(vapp); + this._log.info("WHEE"); }, _saveSnapshot: function BSS__saveSnapshot() { @@ -570,7 +563,7 @@ BookmarksSyncService.prototype = { _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { for (let i = 0; i < commands.length; i++) { - this._log.info("Applying cmd to obj: " + uneval(commands[i])); + this._log.debug("Applying cmd to obj: " + uneval(commands[i])); switch (commands[i].action) { case "create": obj[commands[i].GUID] = eval(uneval(commands[i].data)); @@ -592,7 +585,7 @@ BookmarksSyncService.prototype = { _applyCommands: function BSS__applyCommands(commandList) { for (var i = 0; i < commandList.length; i++) { var command = commandList[i]; - this._log.info("Processing command: " + uneval(command)); + this._log.debug("Processing command: " + uneval(command)); switch (command["action"]) { case "create": this._createCommand(command); @@ -619,11 +612,10 @@ BookmarksSyncService.prototype = { parentId = this._bms.bookmarksRoot; } - this._log.info(" -> creating item"); - switch (command.data.type) { case "bookmark": case "microsummary": + this._log.info(" -> creating bookmark \"" + command.data.title + "\""); let URI = makeURI(command.data.URI); newId = this._bms.insertBookmark(parentId, URI, @@ -634,17 +626,20 @@ BookmarksSyncService.prototype = { this._bms.setKeywordForBookmark(newId, command.data.keyword); if (command.data.type == "microsummary") { + this._log.info(" \-> is a microsummary"); let genURI = makeURI(command.data.generatorURI); let micsum = this._ms.createMicrosummary(URI, genURI); this._ms.setMicrosummary(newId, micsum); } break; case "folder": + this._log.info(" -> creating folder \"" + command.data.title + "\""); newId = this._bms.createFolder(parentId, command.data.title, command.data.index); break; case "livemark": + this._log.info(" -> creating livemark \"" + command.data.title + "\""); newId = this._ls.createLivemark(parentId, command.data.title, makeURI(command.data.siteURI), @@ -652,6 +647,7 @@ BookmarksSyncService.prototype = { command.data.index); break; case "separator": + this._log.info(" -> creating separator"); newId = this._bms.insertSeparator(parentId, command.data.index); break; default: @@ -1160,7 +1156,7 @@ BookmarksSyncService.prototype = { _onLogin: function BSS__onLogin(success) { if (success) { - this._log.config("Bookmarks sync server: " + this._dav.baseURL); + this._log.info("Bookmarks sync server: " + this._dav.baseURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); } else { this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); diff --git a/services/sync/ILog4MozService.idl b/services/sync/ILog4MozService.idl index b5b6eef80315..43f2e17538c3 100644 --- a/services/sync/ILog4MozService.idl +++ b/services/sync/ILog4MozService.idl @@ -30,10 +30,11 @@ interface nsIFile; [scriptable, uuid(eeb0adbc-afa3-4326-9a52-040ce4855e9d)] interface ILogMessage : nsISupports { + attribute AString loggerName; attribute PRInt32 level; attribute AString levelDesc; attribute AString message; - attribute PRTime date; + attribute PRTime time; }; [scriptable, uuid(ac7f40df-49e7-4270-86ec-9c905047e70e)] @@ -54,7 +55,6 @@ interface ILogger : nsISupports { /* * Level constants - * Also here (in addition to the service) for convenience */ const PRInt32 LEVEL_FATAL = 70; const PRInt32 LEVEL_ERROR = 60; @@ -99,18 +99,6 @@ interface ILoggerRepository : nsISupports [scriptable, uuid(a60e50d7-90b8-4a12-ad0c-79e6a1896978)] interface ILog4MozService : nsISupports { - /* - * Level constants - */ - const PRInt32 LEVEL_FATAL = 70; - const PRInt32 LEVEL_ERROR = 60; - const PRInt32 LEVEL_WARN = 50; - const PRInt32 LEVEL_INFO = 40; - const PRInt32 LEVEL_CONFIG = 30; - const PRInt32 LEVEL_DEBUG = 20; - const PRInt32 LEVEL_TRACE = 10; - const PRInt32 LEVEL_ALL = 0; - /* * The root logger. Appenders added to this logger will be * inherited by all other loggers diff --git a/services/sync/ILog4MozService.idl~ b/services/sync/ILog4MozService.idl~ index 3c2f4d94c759..b5b6eef80315 100644 --- a/services/sync/ILog4MozService.idl~ +++ b/services/sync/ILog4MozService.idl~ @@ -25,10 +25,13 @@ #include "nsISupports.idl" +interface nsIFile; + [scriptable, uuid(eeb0adbc-afa3-4326-9a52-040ce4855e9d)] interface ILogMessage : nsISupports { attribute PRInt32 level; + attribute AString levelDesc; attribute AString message; attribute PRTime date; }; @@ -127,6 +130,8 @@ interface ILog4MozService : nsISupports ILogger getLogger(in AString name); IAppender newAppender(in AString kind, in IFormatter formatter); + IAppender newFileAppender(in AString kind, in nsIFile file, + in IFormatter formatter); IFormatter newFormatter(in AString kind); }; diff --git a/services/sync/ILog4MozService.xpt b/services/sync/ILog4MozService.xpt index 0e757a3806fd89bc8afec9124871870379f7df49..0d212ff8414b898094cbd4200f82766d5f4005da 100644 GIT binary patch delta 386 zcmcb|`IuvZIAiieiJn*q1~6b!T2t?E-}AXsLED=-69U|y^D!_m#X#lKPye>s=d;qY zYuTg-`QD|<%0RhhsNB?d8`kbwzt~xARuBu%lh(MoK)E?kxfPok89Nz$8E-KrOk!YW zU;_#>K4ogyiQv6wZrIr{iGd9$!U~c9%>ou-L=k1;hUj8rV4Ez@?5OVPSWu9fmy%ip zw2Xnd0Z4i>CNMFuIRIJyj152*&?=_T$xoQ|nV4!Oi?UePa{*N`3p1|R30B1{$JhX5 zfy6XnI+^vsIzeKlaCP=@b)ImsK)6^WV*)$Gyg0@NAd7*aAtygQJ+;U$F*lU~nK^kW Ni#}ILCQuYi004_qTMhsK delta 419 zcmaFNagTF?IODyE5PlwOK(dJWpEV<^ts$p>n>P85uhn>zR}o6DBb* zGq3@LnRJ;Nb|QG@%ndsmCNZ!9MOY#7&MaUNMpV&Yh%Ppu8V;Z#2~0qv+<=T65W@z@ zr~omHfQ)7k1L!!WJ`h6=$Pk+iVu=7*%Rvk-AY(I-G5I=^<76?WI5o$Dg4Dc})FPlb z19JnAe8&hhpUnZt`oh=%WC1lX{hDmgtk1-(IJtn?I)nqLig^i474s^vDv;O)#uYok tI+=GdHUL>5u_JJ?GjOphj0x-zbvGFsfGh@vhRK>N23#qLC8-Qx0st6*SGE8E diff --git a/services/sync/Log4MozService.js b/services/sync/Log4MozService.js index c5b42f32ff35..5d0db9025110 100644 --- a/services/sync/Log4MozService.js +++ b/services/sync/Log4MozService.js @@ -49,14 +49,14 @@ const LEVEL_TRACE = 10; const LEVEL_ALL = 0; const LEVEL_DESC = { - 70: "FATAL ", - 60: "ERROR ", - 50: "WARN ", - 40: "INFO ", + 70: "FATAL", + 60: "ERROR", + 50: "WARN", + 40: "INFO", 30: "CONFIG", - 20: "DEBUG ", - 10: "TRACE ", - 0: "ALL " + 20: "DEBUG", + 10: "TRACE", + 0: "ALL" }; const ONE_BYTE = 1; @@ -202,9 +202,9 @@ Logger.prototype = { parent: null, - _level: -1, + _level: null, get level() { - if (this._level >= 0) + if (this._level != null) return this._level; if (this.parent) return this.parent.level; @@ -240,25 +240,25 @@ Logger.prototype = { }, fatal: function Logger_fatal(string) { - this.log(new LogMessage(LEVEL_FATAL, string)); + this.log(new LogMessage(this._name, LEVEL_FATAL, string)); }, error: function Logger_error(string) { - this.log(new LogMessage(LEVEL_ERROR, string)); + this.log(new LogMessage(this._name, LEVEL_ERROR, string)); }, warning: function Logger_warning(string) { - this.log(new LogMessage(LEVEL_WARN, string)); + this.log(new LogMessage(this._name, LEVEL_WARN, string)); }, info: function Logger_info(string) { - this.log(new LogMessage(LEVEL_INFO, string)); + this.log(new LogMessage(this._name, LEVEL_INFO, string)); }, config: function Logger_config(string) { - this.log(new LogMessage(LEVEL_CONFIG, string)); + this.log(new LogMessage(this._name, LEVEL_CONFIG, string)); }, debug: function Logger_debug(string) { - this.log(new LogMessage(LEVEL_DEBUG, string)); + this.log(new LogMessage(this._name, LEVEL_DEBUG, string)); }, trace: function Logger_trace(string) { - this.log(new LogMessage(LEVEL_TRACE, string)); + this.log(new LogMessage(this._name, LEVEL_TRACE, string)); } }; @@ -298,7 +298,6 @@ Appender.prototype = { function DumpAppender(formatter) { this._name = "DumpAppender"; this._formatter = formatter; - this._level = LEVEL_ALL; } DumpAppender.prototype = new Appender(); DumpAppender.prototype.doAppend = function DApp_doAppend(message) { @@ -313,19 +312,15 @@ DumpAppender.prototype.doAppend = function DApp_doAppend(message) { function ConsoleAppender(formatter) { this._name = "ConsoleAppender"; this._formatter = formatter; - this._level = LEVEL_ALL; } ConsoleAppender.prototype = new Appender(); ConsoleAppender.prototype.doAppend = function CApp_doAppend(message) { - //error or normal? - if(message.level > LEVEL_WARN){ + if (message.level > LEVEL_WARN) { Cu.reportError(message); - } else { - //get the js console - var consoleService = Components.classes["@mozilla.org/consoleservice;1"]. - getService(Ci.nsIConsoleService); - consoleService.logStringMessage(message); + return; } + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService).logStringMessage(message); }; /* @@ -337,7 +332,6 @@ function FileAppender(file, formatter) { this._name = "FileAppender"; this._file = file; // nsIFile this._formatter = formatter; - this._level = LEVEL_ALL; this.__fos = null; } FileAppender.prototype = new Appender(); @@ -381,7 +375,6 @@ function RotatingFileAppender(file, formatter, maxSize, maxBackups) { this._name = "RotatingFileAppender"; this._file = file; // nsIFile this._formatter = formatter; - this._level = LEVEL_ALL; this._maxSize = maxSize; this._maxBackups = maxBackups; } @@ -442,9 +435,11 @@ BasicFormatter.prototype = { this._dateFormat = format; }, format: function BF_format(message) { - // FIXME: message.date.toLocaleFormat(this.dateFormat) + " " + - - return message.levelDesc + " " + message.message + "\n"; + dump("time is " + message.time + "\n"); + let date = new Date(message.time); + return date.toLocaleFormat(this.dateFormat) + "\t" + + message.loggerName + "\t" + message.levelDesc + "\t" + + message.message + "\n"; } }; @@ -452,10 +447,11 @@ BasicFormatter.prototype = { * LogMessage * Encapsulates a single log event's data */ -function LogMessage(level, message){ +function LogMessage(loggerName, level, message){ + this.loggerName = loggerName; this.message = message; this.level = level; - this.date = new Date(); + this.time = Date.now(); } LogMessage.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.ILogMessage, Ci.nsISupports]), @@ -465,8 +461,9 @@ LogMessage.prototype = { return LEVEL_DESC[this.level]; return "UNKNOWN"; }, + toString: function LogMsg_toString(){ - return "LogMessage [" + this.date + " " + this.level + " " + + return "LogMessage [" + this._date + " " + this.level + " " + this.message + "]"; } }; From d3fbdcb2d70ba2466d5ff9d37bbf769774226a93 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 10 Oct 2007 17:08:58 -0700 Subject: [PATCH 0036/1860] additional logging; use __proto__ hack for inheriting object properties in subclassed Appenders --- services/sync/BookmarksSyncService.js | 11 +- services/sync/Log4MozService.js | 165 +++++++++++++++----------- 2 files changed, 102 insertions(+), 74 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index f25959c6637e..aee9b9523016 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -156,6 +156,8 @@ BookmarksSyncService.prototype = { _init: function BSS__init() { this._initLogs(); + this._log.info("Bookmarks Sync Service Initializing"); + let serverURL = 'https://dotmoz.mozilla.org/'; try { let branch = Cc["@mozilla.org/preferences-service;1"]. @@ -163,6 +165,7 @@ BookmarksSyncService.prototype = { serverURL = branch.getCharPref("browser.places.sync.serverURL"); } catch (ex) { /* use defaults */ } + this._log.info("Bookmarks login server: " + serverURL); this._dav = new DAVCollection(serverURL); this._readSnapshot(); @@ -172,7 +175,7 @@ BookmarksSyncService.prototype = { let logSvc = Cc["@mozilla.org/log4moz/service;1"]. getService(Ci.ILog4MozService); - this._log = logSvc.getLogger("main"); + this._log = logSvc.getLogger("Service.Main"); let formatter = logSvc.newFormatter("basic"); let root = logSvc.rootLogger; @@ -201,7 +204,6 @@ BookmarksSyncService.prototype = { let vapp = logSvc.newFileAppender("rotating", verboseFile, formatter); vapp.level = root.LEVEL_ALL; root.addAppender(vapp); - this._log.info("WHEE"); }, _saveSnapshot: function BSS__saveSnapshot() { @@ -1156,9 +1158,11 @@ BookmarksSyncService.prototype = { _onLogin: function BSS__onLogin(success) { if (success) { + this._log.info("Logged in"); this._log.info("Bookmarks sync server: " + this._dav.baseURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); } else { + this._log.info("Could not log in"); this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); } }, @@ -1175,16 +1179,19 @@ BookmarksSyncService.prototype = { // IBookmarksSyncService sync: function BSS_sync() { + this._log.info("Syncing"); this._doSync.async(this); }, login: function BSS_login() { + this._log.info("Logging in"); let callback = bind2(this, this._onLogin); if (!this._dav.login.async(this._dav, callback)) this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); }, logout: function BSS_logout() { + this._log.info("Logging out"); this._dav.logout(); this._os.notifyObservers(null, "bookmarks-sync:logout", ""); } diff --git a/services/sync/Log4MozService.js b/services/sync/Log4MozService.js index 5d0db9025110..c83081ff9cb6 100644 --- a/services/sync/Log4MozService.js +++ b/services/sync/Log4MozService.js @@ -299,10 +299,12 @@ function DumpAppender(formatter) { this._name = "DumpAppender"; this._formatter = formatter; } -DumpAppender.prototype = new Appender(); -DumpAppender.prototype.doAppend = function DApp_doAppend(message) { - dump(message); +DumpAppender.prototype = { + doAppend: function DApp_doAppend(message) { + dump(message); + } }; +DumpAppender.prototype.__proto__ = new Appender(); /* * ConsoleAppender @@ -313,15 +315,17 @@ function ConsoleAppender(formatter) { this._name = "ConsoleAppender"; this._formatter = formatter; } -ConsoleAppender.prototype = new Appender(); -ConsoleAppender.prototype.doAppend = function CApp_doAppend(message) { - if (message.level > LEVEL_WARN) { - Cu.reportError(message); - return; +ConsoleAppender.prototype = { + doAppend: function CApp_doAppend(message) { + if (message.level > LEVEL_WARN) { + Cu.reportError(message); + return; + } + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService).logStringMessage(message); } - Cc["@mozilla.org/consoleservice;1"]. - getService(Ci.nsIConsoleService).logStringMessage(message); }; +ConsoleAppender.prototype.__proto__ = new Appender(); /* * FileAppender @@ -332,39 +336,47 @@ function FileAppender(file, formatter) { this._name = "FileAppender"; this._file = file; // nsIFile this._formatter = formatter; - this.__fos = null; } -FileAppender.prototype = new Appender(); -FileAppender.prototype._fos = function FApp__fos_get() { - if (!this.__fos) - this.openStream(); - return this.__fos; -}; -FileAppender.prototype.openStream = function FApp_openStream() { - this.__fos = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; - this.__fos.init(this._file, flags, PERMS_FILE, 0); -}; -FileAppender.prototype.closeStream = function FApp_closeStream() { - if (!this.__fos) - return; - try { - this.__fos.close(); - this.__fos = null; - } catch(e) { - dump("Failed to close file output stream\n" + e); - } -}; -FileAppender.prototype.doAppend = function FApp_doAppend(message) { - if (message === null || message.length <= 0) - return; - try { - this._fos().write(message, message.length); - } catch(e) { - dump("Error writing file:\n" + e); +FileAppender.prototype = { + __fos: null, + get _fos() { + if (!this.__fos) + this.openStream(); + return this.__fos; + }, + + openStream: function FApp_openStream() { + dump("OPENING STREAM\n"); + if (!this._file) + dump("THERE IS NO FILE\n"); + this.__fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; + this.__fos.init(this._file, flags, PERMS_FILE, 0); + }, + + closeStream: function FApp_closeStream() { + if (!this.__fos) + return; + try { + this.__fos.close(); + this.__fos = null; + } catch(e) { + dump("Failed to close file output stream\n" + e); + } + }, + + doAppend: function FApp_doAppend(message) { + if (message === null || message.length <= 0) + return; + try { + this._fos().write(message, message.length); + } catch(e) { + dump("Error writing file:\n" + e); + } } }; +FileAppender.prototype.__proto__ = new Appender(); /* * RotatingFileAppender @@ -378,37 +390,39 @@ function RotatingFileAppender(file, formatter, maxSize, maxBackups) { this._maxSize = maxSize; this._maxBackups = maxBackups; } -RotatingFileAppender.prototype = new FileAppender(); -RotatingFileAppender.prototype.doAppend = function RFApp_doAppend(message) { - if (message === null || message.length <= 0) - return; - try { - this.rotateLogs(); - this._fos().write(message, message.length); - } catch(e) { - dump("Error writing file:\n" + e); +RotatingFileAppender.prototype = { + doAppend: function RFApp_doAppend(message) { + if (message === null || message.length <= 0) + return; + try { + this.rotateLogs(); + this._fos.write(message, message.length); + } catch(e) { + dump("Error writing file:\n" + e); + } + }, + rotateLogs: function RFApp_rotateLogs() { + if(this._file.exists() && + this._file.fileSize < this._maxSize) + return; + + this.closeStream(); + + for (let i = this.maxBackups - 1; i > 0; i--){ + let backup = this._file.parent.clone(); + backup.append(this._file.leafName + "." + i); + if (backup.exists()) + backup.moveTo(this._file.parent, this._file.leafName + "." + (i + 1)); + } + + let cur = this._file.clone(); + if (cur.exists()) + cur.moveTo(cur.parent, cur.leafName + ".1"); + + // Note: this._file still points to the same file } }; -RotatingFileAppender.prototype.rotateLogs = function RFApp_rotateLogs() { - if(this._file.exists() && - this._file.fileSize < this._maxSize) - return; - - this.closeStream(); - - for (let i = this.maxBackups - 1; i > 0; i--){ - let backup = this._file.parent.clone(); - backup.append(this._file.leafName + "." + i); - if (backup.exists()) - backup.moveTo(this._file.parent, this._file.leafName + "." + (i + 1)); - } - - let cur = this._file.clone(); - if (cur.exists()) - cur.moveTo(cur.parent, cur.leafName + ".1"); - - // this._file still points to the same file -}; +RotatingFileAppender.prototype.__proto__ = new FileAppender(); /* * Formatters @@ -416,14 +430,19 @@ RotatingFileAppender.prototype.rotateLogs = function RFApp_rotateLogs() { * Only the BasicFormatter is currently implemented */ +// Abstract formatter +function Formatter() {} +Formatter.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.IFormatter, Ci.nsISupports]), + format: function Formatter_format(message) {} +}; + // FIXME: should allow for formatting the whole string, not just the date function BasicFormatter(dateFormat) { if (dateFormat) this.dateFormat = dateFormat; } BasicFormatter.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.IFormatter, Ci.nsISupports]), - _dateFormat: null, get dateFormat() { @@ -431,17 +450,19 @@ BasicFormatter.prototype = { this._dateFormat = "%Y-%m-%d %H:%M:%S"; return this._dateFormat; }, + set dateFormat(format) { this._dateFormat = format; }, + format: function BF_format(message) { - dump("time is " + message.time + "\n"); let date = new Date(message.time); return date.toLocaleFormat(this.dateFormat) + "\t" + message.loggerName + "\t" + message.levelDesc + "\t" + message.message + "\n"; } }; +BasicFormatter.prototype.__proto__ = new Formatter(); /* * LogMessage From 7e9fdf32d1ff74596203708e149fcc25e3caae2e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 10 Oct 2007 17:12:20 -0700 Subject: [PATCH 0037/1860] tri-license log4moz --- services/sync/ILog4MozService.idl | 14 +++++++++++++- services/sync/Log4MozService.js | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/services/sync/ILog4MozService.idl b/services/sync/ILog4MozService.idl index 43f2e17538c3..ddc2f040c8d8 100644 --- a/services/sync/ILog4MozService.idl +++ b/services/sync/ILog4MozService.idl @@ -1,5 +1,5 @@ /* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1 + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with @@ -21,6 +21,18 @@ * Contributor(s): * Dan Mills * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * * ***** END LICENSE BLOCK ***** */ #include "nsISupports.idl" diff --git a/services/sync/Log4MozService.js b/services/sync/Log4MozService.js index c83081ff9cb6..cca46b205521 100644 --- a/services/sync/Log4MozService.js +++ b/services/sync/Log4MozService.js @@ -1,5 +1,5 @@ /* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1 + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with @@ -22,6 +22,18 @@ * Michael Johnston * Dan Mills * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * * ***** END LICENSE BLOCK ***** */ const Cc = Components.classes; From 5e23d3fede7f7ac7c14b6caa38d23b366652fc3c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Oct 2007 13:29:33 -0700 Subject: [PATCH 0038/1860] calculate server deltas based on server latest -> final state diff. fix some logger calls. --- services/sync/BookmarksSyncService.js | 117 +++++++++++++++------- services/sync/ILog4MozService.idl | 2 +- services/sync/ILog4MozService.idl~ | 137 -------------------------- services/sync/ILog4MozService.xpt | Bin 1123 -> 1120 bytes services/sync/Log4MozService.js | 2 +- 5 files changed, 85 insertions(+), 173 deletions(-) delete mode 100644 services/sync/ILog4MozService.idl~ diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index aee9b9523016..09e459d796cc 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -303,8 +303,10 @@ BookmarksSyncService.prototype = { item.keyword = this._bms.getKeywordForBookmark(node.itemId); } else if (node.type == node.RESULT_TYPE_SEPARATOR) { item.type = "separator"; + } else if (node.type == node.RESULT_TYPE_QUERY) { + item.type = "query"; } else { - this._log.logWarn("Warning: unknown item type, cannot serialize: " + node.type); + this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); return; } @@ -545,16 +547,39 @@ BookmarksSyncService.prototype = { } } } + for (let i = 0; i < listA.length; i++) { - // need to check if a previous conflict might break this cmd - if (!conflicts[0].some( - function(elt) { return elt.GUID == listA[i].GUID })) + + this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + let alsoConflictsA = function(elt) { + return listA[i].parents.indexOf(elt.GUID) >= 0; + }; + if (conflicts[0].some(alsoConflictsA)) + conflicts[0].push(listA[i]); + + let conflictsA = function(elt) { + return elt.GUID == listA[i].GUID; + }; + if (!conflicts[0].some(conflictsA)) propagations[1].push(listA[i]); } for (let j = 0; j < listB.length; j++) { - // need to check if a previous conflict might break this cmd - if (!conflicts[1].some( - function(elt) { return elt.GUID == listB[j].GUID })) + + this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + let alsoConflictsB = function(elt) { + return listB[j].parents.indexOf(elt.GUID) >= 0; + }; + if (conflicts[1].some(alsoConflictsB)) + conflicts[1].push(listB[j]); + + let conflictsB = function(elt) { + return elt.GUID == listB[j].GUID; + }; + if (!conflicts[1].some(conflictsB)) propagations[0].push(listB[j]); } @@ -794,20 +819,36 @@ BookmarksSyncService.prototype = { return json; }, + // original + // / \ + // A / \ B + // / \ + // client --C-> server + // \ / + // D \ / C + // \ / + // final + + // If we have a saved snapshot, original == snapshot. Otherwise, + // it's the empty set {}. + + // C is really the diff between server -> final, so if we determine + // D we can calculate C from that. In the case where A and B have + // no conflicts, C == A and D == B. + + // Sync flow: // 1) Fetch server deltas // 1.1) Construct current server status from snapshot + server deltas - // 1.2) Generate single delta from snapshot -> current server status - // 2) Generate local deltas from snapshot -> current client status + // 1.2) Generate single delta from snapshot -> current server status ("B") + // 2) Generate local deltas from snapshot -> current client status ("A") // 3) Reconcile client/server deltas and generate new deltas for them. - // 3.1) Apply local delta with server changes - // 3.2) Append server delta to the delta file and upload + // Reconciliation won't generate C directly, we will simply diff + // server->final after step 3.1. + // 3.1) Apply local delta with server changes ("D") + // 3.2) Append server delta to the delta file and upload ("C") _doSync: function BSS__doSync() { let cont = yield; - - this._os.notifyObservers(null, "bookmarks-sync:start", ""); - this._log.info("Beginning sync"); - try { if (!this._dav.lock.async(this._dav, cont)) return; @@ -821,21 +862,19 @@ BookmarksSyncService.prototype = { } var localJson = this._getBookmarks(); - //this._log.info("local json:\n" + this._mungeNodes(localJson)); + this._log.debug("local json:\n" + this._mungeNodes(localJson)); // 1) Fetch server deltas - if (this._getServerData.async(this, cont, localJson)) - let server = yield; - else + if (!this._getServerData.async(this, cont, localJson)) return + let server = yield; this._log.info("Local snapshot version: " + this._snapshotVersion); this._log.info("Server status: " + server.status); - if (server['status'] != 0) { - this._os.notifyObservers(null, "bookmarks-sync:end", ""); - this._log.error("Sync error: could not get server status, " + - "or initial upload failed."); + if (server.status != 0) { + this._log.fatal("Sync error: could not get server status, " + + "or initial upload failed. Aborting sync."); return; } @@ -850,7 +889,6 @@ BookmarksSyncService.prototype = { if (server.updates.length == 0 && localUpdates.length == 0) { this._snapshotVersion = server.maxVersion; - this._os.notifyObservers(null, "bookmarks-sync:end", ""); this._log.info("Sync complete (1): no changes needed on client or server"); return; } @@ -858,11 +896,10 @@ BookmarksSyncService.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. this._log.info("Reconciling client/server updates"); - if (this._reconcile.async(this, cont, - localUpdates, server.updates)) - let ret = yield; - else + if (!this._reconcile.async(this, cont, localUpdates, server.updates)) return + let ret = yield; + // FIXME: Need to come up with a closing protocol for generators //rec_gen.close(); @@ -892,7 +929,6 @@ BookmarksSyncService.prototype = { if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { - this._os.notifyObservers(null, "bookmarks-sync:end", ""); this._log.info("Sync complete (2): no changes needed on client or server"); this._snapshot = localJson; this._snapshotVersion = server.maxVersion; @@ -927,11 +963,24 @@ BookmarksSyncService.prototype = { } // 3.2) Append server delta to the delta file and upload - if (serverChanges.length) { + + // Generate a new diff, from the current server snapshot to the + // current client snapshot. In the case where there are no + // conflicts, it should be the same as what the resolver returned + + let serverDelta = this._detectUpdates(this._snapshot, server.snapshot); + + // Log an error if not the same + if (!(serverConflicts.length || + this.deepEquals(serverChanges, serverDelta))) + this._log.error("Predicted server changes differ from " + + "actual server->client diff"); + + if (serverDelta.length) { this._log.info("Uploading changes to server"); this._snapshot = this._getBookmarks(); this._snapshotVersion = ++server.maxVersion; - server.deltas.push(serverChanges); + server.deltas.push(serverDelta); this._dav.PUT("bookmarks-deltas.json", uneval(server.deltas), cont); let deltasPut = yield; @@ -955,9 +1004,9 @@ BookmarksSyncService.prototype = { this._log.error("Could not update deltas on server"); } } - this._os.notifyObservers(null, "bookmarks-sync:end", ""); this._log.info("Sync complete"); } finally { + this._os.notifyObservers(null, "bookmarks-sync:end", ""); if (!this._dav.unlock.async(this._dav, cont)) return; let unlocked = yield; @@ -965,7 +1014,6 @@ BookmarksSyncService.prototype = { this._log.info("Lock released"); else this._log.error("Could not unlock DAV collection"); - this._os.notifyObservers(null, "bookmarks-sync:end", ""); } }, @@ -1179,7 +1227,8 @@ BookmarksSyncService.prototype = { // IBookmarksSyncService sync: function BSS_sync() { - this._log.info("Syncing"); + this._log.info("Beginning sync"); + this._os.notifyObservers(null, "bookmarks-sync:start", ""); this._doSync.async(this); }, diff --git a/services/sync/ILog4MozService.idl b/services/sync/ILog4MozService.idl index ddc2f040c8d8..f82595f0c59e 100644 --- a/services/sync/ILog4MozService.idl +++ b/services/sync/ILog4MozService.idl @@ -96,7 +96,7 @@ interface ILogger : nsISupports void fatal(in AString message); void error(in AString message); - void warning(in AString message); + void warn(in AString message); void info(in AString message); void config(in AString message); void debug(in AString message); diff --git a/services/sync/ILog4MozService.idl~ b/services/sync/ILog4MozService.idl~ deleted file mode 100644 index b5b6eef80315..000000000000 --- a/services/sync/ILog4MozService.idl~ +++ /dev/null @@ -1,137 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is log4moz - * - * The Initial Developer of the Original Code is - * Michael Johnston - * Portions created by the Initial Developer are Copyright (C) 2006 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * ***** END LICENSE BLOCK ***** */ - -#include "nsISupports.idl" - -interface nsIFile; - -[scriptable, uuid(eeb0adbc-afa3-4326-9a52-040ce4855e9d)] -interface ILogMessage : nsISupports -{ - attribute PRInt32 level; - attribute AString levelDesc; - attribute AString message; - attribute PRTime date; -}; - -[scriptable, uuid(ac7f40df-49e7-4270-86ec-9c905047e70e)] -interface IFormatter : nsISupports -{ - AString format(in ILogMessage message); -}; - -[scriptable, uuid(abe56c32-79f4-465e-ac20-8e39d33cba0b)] -interface IAppender : nsISupports -{ - attribute PRInt32 level; - void append(in ILogMessage message); -}; - -[scriptable, uuid(2c1a0f87-f544-479c-80e0-acd11ae52ebf)] -interface ILogger : nsISupports -{ - /* - * Level constants - * Also here (in addition to the service) for convenience - */ - const PRInt32 LEVEL_FATAL = 70; - const PRInt32 LEVEL_ERROR = 60; - const PRInt32 LEVEL_WARN = 50; - const PRInt32 LEVEL_INFO = 40; - const PRInt32 LEVEL_CONFIG = 30; - const PRInt32 LEVEL_DEBUG = 20; - const PRInt32 LEVEL_TRACE = 10; - const PRInt32 LEVEL_ALL = 0; - - /* - * The assigned level for this logger. Applies also to descendant - * loggers unless overridden. - */ - attribute PRInt32 level; - - /* - * Add an appender to this logger's list. Anything logged via this - * logger will be sent to the list of appenders (including its - * parent logger's appenders). - */ - void addAppender(in IAppender appender); - - /* - * Log messages at various levels - */ - - void fatal(in AString message); - void error(in AString message); - void warning(in AString message); - void info(in AString message); - void config(in AString message); - void debug(in AString message); - void trace(in AString message); -}; - -[scriptable, uuid(cbfb3abe-4ca9-498a-a692-581f8da52323)] -interface ILoggerRepository : nsISupports -{ -}; - -[scriptable, uuid(a60e50d7-90b8-4a12-ad0c-79e6a1896978)] -interface ILog4MozService : nsISupports -{ - /* - * Level constants - */ - const PRInt32 LEVEL_FATAL = 70; - const PRInt32 LEVEL_ERROR = 60; - const PRInt32 LEVEL_WARN = 50; - const PRInt32 LEVEL_INFO = 40; - const PRInt32 LEVEL_CONFIG = 30; - const PRInt32 LEVEL_DEBUG = 20; - const PRInt32 LEVEL_TRACE = 10; - const PRInt32 LEVEL_ALL = 0; - - /* - * The root logger. Appenders added to this logger will be - * inherited by all other loggers - */ - readonly attribute ILogger rootLogger; - - /* - * Get a Logger object that can be used to print debug/info/error - * messages to various places. - * - * @param name - * The name of the logger. A new logger will be created - * with that name if it doesn't already exist. - * @returns The ILogger object that can be used to log messages. - */ - - ILogger getLogger(in AString name); - - IAppender newAppender(in AString kind, in IFormatter formatter); - IAppender newFileAppender(in AString kind, in nsIFile file, - in IFormatter formatter); - - IFormatter newFormatter(in AString kind); -}; diff --git a/services/sync/ILog4MozService.xpt b/services/sync/ILog4MozService.xpt index 0d212ff8414b898094cbd4200f82766d5f4005da..0b632e5397b2d4c6ff5705bab8af350ad0f721a8 100644 GIT binary patch delta 436 zcmaFN@qlB3IAg*@i2}|71~6c3m^e{5RusZvl3!EraNqN}Q$gFCITHfhpYt&=FhxS; zQcnN2+UK*ITn%E~~w2B_SmcN^C3S-;p>ZB`Hq&y&`;xj?y@P`Ra(85xr@ ze=|1hWMBgdF*3ke>~Iz@gv9}r7G?q}asx8tK@1xpLmR{}0y4}%3=JT|5yX%KGWPB>cyE{+b~a36U;~P;M1& delta 440 zcmaFB@t9+RIAiiei2}}I1~6c3o;XoBRszCdQd(2*aNqN}Q$gFCITHfhpYt&=FvURS z(og@k+UK*ITn%E~~wW~kiMcN^C3S-;p>ZB`Hq&y&`;xj?x&P`MS885xr@ z|1mb~WMBgdF*3thTyT~Egv9}r7GnY`asx7yKnxooLl49-0x~Q?3=JT|1;mg8G6F#i z5g;QL#NYxlGAEy5)ML)fOP~CUvCsJyW5Of`W{AB{nHqK?c<-4Tb~a36U;~PAJxL6=uERr#S9b#VGWDb@H0GmTsoB#j- diff --git a/services/sync/Log4MozService.js b/services/sync/Log4MozService.js index cca46b205521..0b110817bd20 100644 --- a/services/sync/Log4MozService.js +++ b/services/sync/Log4MozService.js @@ -257,7 +257,7 @@ Logger.prototype = { error: function Logger_error(string) { this.log(new LogMessage(this._name, LEVEL_ERROR, string)); }, - warning: function Logger_warning(string) { + warn: function Logger_warn(string) { this.log(new LogMessage(this._name, LEVEL_WARN, string)); }, info: function Logger_info(string) { From 93c21841d78c8146fa559df41956af1e5875f7a1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Oct 2007 15:08:22 -0700 Subject: [PATCH 0039/1860] logging fixes + make verbose logs more readable; fix guid rename commands when applying to objects; make stealLock work even when the resource isn't locked; remove extra tmp logging --- services/sync/BookmarksSyncService.js | 56 +++++++++++++++++++-------- services/sync/Log4MozService.js | 3 -- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 09e459d796cc..ec0df63fb356 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -385,10 +385,12 @@ BookmarksSyncService.prototype = { }, _conflicts: function BSS__conflicts(a, b) { + /* if ((a.depth < b.depth) && (b.parents.indexOf(a.GUID) >= 0) && a.action == "remove") return true; + */ if ((a.GUID == b.GUID) && !this._deepEquals(a, b)) return true; return false; @@ -596,7 +598,22 @@ BookmarksSyncService.prototype = { obj[commands[i].GUID] = eval(uneval(commands[i].data)); break; case "edit": + if ("GUID" in commands[i].data) { + // special-case guid changes + let newGUID = commands[i].data.GUID, + oldGUID = commands[i].GUID; + + obj[newGUID] = obj[oldGUID]; + delete obj[oldGUID] + + for (let GUID in obj) { + if (obj[GUID].parentGUID == oldGUID) + obj[GUID].parentGUID = newGUID; + } + } for (let prop in commands[i].data) { + if (prop == "GUID") + continue; obj[commands[i].GUID][prop] = commands[i].data[prop]; } break; @@ -635,7 +652,7 @@ BookmarksSyncService.prototype = { let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); if (parentId <= 0) { - this._log.warning("Creating node with unknown parent -> reparenting to root"); + this._log.warn("Creating node with unknown parent -> reparenting to root"); parentId = this._bms.bookmarksRoot; } @@ -711,7 +728,7 @@ BookmarksSyncService.prototype = { _editCommand: function BSS__editCommand(command) { var itemId = this._bms.getItemIdForGUID(command.GUID); if (itemId == -1) { - this._log.warning("Item for GUID " + command.GUID + " not found. Skipping."); + this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); return; } @@ -722,8 +739,8 @@ BookmarksSyncService.prototype = { if (existing == -1) this._bms.setItemGUID(itemId, command.data.GUID); else - this._log.warning("Can't change GUID " + command.GUID + - " to " + command.data.GUID + ": GUID already exists."); + this._log.warn("Can't change GUID " + command.GUID + + " to " + command.data.GUID + ": GUID already exists."); break; case "title": this._bms.setItemTitle(itemId, command.data.title); @@ -763,7 +780,7 @@ BookmarksSyncService.prototype = { this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); break; default: - this._log.warning("Can't change item property: " + key); + this._log.warn("Can't change item property: " + key); break; } } @@ -783,8 +800,8 @@ BookmarksSyncService.prototype = { for (let guid in unfiled) { if (guid in filed) - this._log.warning("Same bookmark (guid) in both " + - "filed and unfiled trees!"); + this._log.warn("Same bookmark (guid) in both " + + "filed and unfiled trees!"); else filed[guid] = unfiled[guid]; } @@ -857,7 +874,7 @@ BookmarksSyncService.prototype = { if (locked) this._log.info("Lock acquired"); else { - this._log.warning("Could not acquire lock, aborting sync"); + this._log.warn("Could not acquire lock, aborting sync"); return; } @@ -955,7 +972,8 @@ BookmarksSyncService.prototype = { if (diff.length != 0) { this._log.error("Commands did not apply correctly"); this._log.debug("Diff from snapshot+commands -> " + - "new snapshot after commands:\n" + uneval(diff)); + "new snapshot after commands:\n" + + this._mungeCommands(diff)); this._snapshot = newSnapshot; // FIXME: What else can we do? } @@ -968,11 +986,11 @@ BookmarksSyncService.prototype = { // current client snapshot. In the case where there are no // conflicts, it should be the same as what the resolver returned - let serverDelta = this._detectUpdates(this._snapshot, server.snapshot); + let serverDelta = this._detectUpdates(server.snapshot, this._snapshot); // Log an error if not the same if (!(serverConflicts.length || - this.deepEquals(serverChanges, serverDelta))) + this._deepEquals(serverChanges, serverDelta))) this._log.error("Predicted server changes differ from " + "actual server->client diff"); @@ -982,7 +1000,8 @@ BookmarksSyncService.prototype = { this._snapshotVersion = ++server.maxVersion; server.deltas.push(serverDelta); - this._dav.PUT("bookmarks-deltas.json", uneval(server.deltas), cont); + this._dav.PUT("bookmarks-deltas.json", + this._mungeCommands(server.deltas), cont); let deltasPut = yield; // FIXME: need to watch out for the storage format version changing, @@ -1153,7 +1172,7 @@ BookmarksSyncService.prototype = { this._snapshotGUID = null; // in case there are other snapshots out there this._dav.PUT("bookmarks-snapshot.json", - uneval(this._snapshot), cont); + this._mungeNodes(this._snapshot), cont); let snapPut = yield; if (snapPut.target.status < 200 || snapPut.target.status >= 300) { this._log.error("Could not upload snapshot to server, error code: " + @@ -1589,10 +1608,13 @@ DAVCollection.prototype = { if (!this._getActiveLock.async(this, cont)) return; this._token = yield; - - if (!this.unlock.async(this, cont)) - return; - let unlocked = yield; + + let unlocked = true; + if (this._token) { + if (!this.unlock.async(this, cont)) + return; + unlocked = yield; + } if (unlocked && this.lock.async(this, cont)) stolen = yield; diff --git a/services/sync/Log4MozService.js b/services/sync/Log4MozService.js index 0b110817bd20..3cb021cc443d 100644 --- a/services/sync/Log4MozService.js +++ b/services/sync/Log4MozService.js @@ -358,9 +358,6 @@ FileAppender.prototype = { }, openStream: function FApp_openStream() { - dump("OPENING STREAM\n"); - if (!this._file) - dump("THERE IS NO FILE\n"); this.__fos = Cc["@mozilla.org/network/file-output-stream;1"]. createInstance(Ci.nsIFileOutputStream); let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; From 0ec95e353e0435892818178c3582a32609fb1cfc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Oct 2007 16:10:42 -0700 Subject: [PATCH 0040/1860] minor cleanup --- services/sync/BookmarksSyncService.js | 35 ++++----------------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index ec0df63fb356..4e7dda80480d 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -384,18 +384,6 @@ BookmarksSyncService.prototype = { return cmds; }, - _conflicts: function BSS__conflicts(a, b) { - /* - if ((a.depth < b.depth) && - (b.parents.indexOf(a.GUID) >= 0) && - a.action == "remove") - return true; - */ - if ((a.GUID == b.GUID) && !this._deepEquals(a, b)) - return true; - return false; - }, - // Bookmarks are allowed to be in a different index as long as they // are in the same folder. Folders and separators must be at the // same index to qualify for 'likeness'. @@ -484,20 +472,11 @@ BookmarksSyncService.prototype = { } }, - // FIXME: todo: change the conflicts data structure to hold more - // information about which specific command pars conflict; an what - // commands would apply once those are resolved. Perhaps something - // like: - - // [[direct-conflictA, 2nd-degree-conflictA1, ...], - // [direct-conflictB, 2nd-degree-conflict-B1, ...], ...] - - // possible problem: a 2nd-degree conflict could show up in multiple - // lists (that is, there are multiple direct conflicts that prevent - // another command pair from cleanly applying). maybe: - - // [[[dcA1, dcB1], [dcA2. dcB2], ...], - // [2dcA1, 2dcA2, ...], [2dcB1, 2dcB2, ...]] + _conflicts: function BSS__conflicts(a, b) { + if ((a.GUID == b.GUID) && !this._deepEquals(a, b)) + return true; + return false; + }, _reconcile: function BSS__reconcile(onComplete, listA, listB) { let cont = yield; @@ -508,7 +487,6 @@ BookmarksSyncService.prototype = { let conflicts = [[], []]; for (let i = 0; i < listA.length; i++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); yield; // Yield to main loop @@ -533,7 +511,6 @@ BookmarksSyncService.prototype = { listB = listB.filter(function(elt) { return elt }); for (let i = 0; i < listA.length; i++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); yield; // Yield to main loop @@ -551,7 +528,6 @@ BookmarksSyncService.prototype = { } for (let i = 0; i < listA.length; i++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); yield; // Yield to main loop @@ -568,7 +544,6 @@ BookmarksSyncService.prototype = { propagations[1].push(listA[i]); } for (let j = 0; j < listB.length; j++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); yield; // Yield to main loop From bda2e2e04f7c98b903d5f5f1b6d8e4cff334ddc1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Oct 2007 18:30:26 -0700 Subject: [PATCH 0041/1860] reset guids on initial sync; prune out commands with guids that already exist on the system --- services/sync/BookmarksSyncService.js | 35 ++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 4e7dda80480d..58d2af5ca972 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -495,15 +495,20 @@ BookmarksSyncService.prototype = { delete listA[i]; delete listB[j]; } else if (this._commandLike(listA[i], listB[j])) { - // Disregard likeness if the target GUID already exists locally - if (this._bms.getItemIdForGUID(listB[j].GUID) >= 0) - continue; this._fixParents(listA, listA[i].GUID, listB[j].GUID); listB[j].data = {GUID: listB[j].GUID}; listB[j].GUID = listA[i].GUID; listB[j].action = "edit"; delete listA[i]; } + + // watch out for create commands with GUIDs that already exist + if (listB[j] && listB[j].action == "create" && + this._bms.getItemForGUID(listB[j].GUID) >= 0) { + this._log.error("Remote command has GUID that already exists " + + "locally. Dropping command."); + delete listB[j]; + } } } @@ -857,7 +862,7 @@ BookmarksSyncService.prototype = { this._log.debug("local json:\n" + this._mungeNodes(localJson)); // 1) Fetch server deltas - if (!this._getServerData.async(this, cont, localJson)) + if (!this._getServerData.async(this, cont)) return let server = yield; @@ -1011,6 +1016,23 @@ BookmarksSyncService.prototype = { } }, + _resetGUIDs: function BSS__resetGUIDs(node) { + if (!node) + node = this._getBookmarks(); + + if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) + this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); + + if (node.type == node.RESULT_TYPE_FOLDER && + !this._ls.isLivemark(node.itemId)) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._resetGUIDs(node.getChild(i)); + } + } + }, + /* Get the deltas/combined updates from the server * Returns: * status: @@ -1031,7 +1053,7 @@ BookmarksSyncService.prototype = { * the relevant deltas (from our snapshot version to current), * combined into a single set. */ - _getServerData: function BSS__getServerData(onComplete, localJson) { + _getServerData: function BSS__getServerData(onComplete) { let cont = yield; let ret = {status: -1, @@ -1060,6 +1082,7 @@ BookmarksSyncService.prototype = { if (status.GUID != this._snapshotGUID) { this._log.info("Remote/local sync GUIDs do not match. " + "Forcing initial sync."); + this._resetGUIDs(); this._snapshot = {}; this._snapshotVersion = -1; this._snapshotGUID = status.GUID; @@ -1142,7 +1165,7 @@ BookmarksSyncService.prototype = { case 404: this._log.info("Server has no status file, Initial upload to server"); - this._snapshot = localJson; + this._snapshot = this._getBookmarks(); this._snapshotVersion = 0; this._snapshotGUID = null; // in case there are other snapshots out there From 4d693092649ee3ff16158d546af63362fd548b56 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Oct 2007 18:44:58 -0700 Subject: [PATCH 0042/1860] fix guids reset method --- services/sync/BookmarksSyncService.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 58d2af5ca972..e7d3ed0f3346 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1016,10 +1016,18 @@ BookmarksSyncService.prototype = { } }, - _resetGUIDs: function BSS__resetGUIDs(node) { - if (!node) - node = this._getBookmarks(); + _getFolderNodes: function BSS__getFolderNodes(folder) { + let query = this._hsvc.getNewQuery(); + query.setFolders([folder], 1); + return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + }, + _resetGUIDs: function BSS__resetGUIDs() { + this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksRoot)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledRoot)); + }, + + _resetGUIDsInt: function BSS__resetGUIDsInt(node) { if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); From dab100fdda418cb9bc9f7c7d242418cfe9fb4df2 Mon Sep 17 00:00:00 2001 From: Date: Fri, 12 Oct 2007 22:58:38 -0700 Subject: [PATCH 0043/1860] fix recursive function bug --- services/sync/BookmarksSyncService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index e7d3ed0f3346..8b74519e6c9f 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1036,7 +1036,7 @@ BookmarksSyncService.prototype = { node.QueryInterface(Ci.nsINavHistoryQueryResultNode); node.containerOpen = true; for (var i = 0; i < node.childCount; i++) { - this._resetGUIDs(node.getChild(i)); + this._resetGUIDsInt(node.getChild(i)); } } }, From 488d49632573f860d6927cfbd2354ab0df95a841 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 13:31:25 -0700 Subject: [PATCH 0044/1860] add resetLock() method to service interface, it resets any server-side locks there may be. not hooked up to any ui. --- services/sync/BookmarksSyncService.js | 33 +++++++++++++++++++++--- services/sync/IBookmarksSyncService.idl | 6 +++++ services/sync/IBookmarksSyncService.xpt | Bin 194 -> 212 bytes 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 8b74519e6c9f..2fff176c53b0 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1240,6 +1240,16 @@ BookmarksSyncService.prototype = { } }, + _onResetLock: function BSS__resetLock(success) { + if (success) { + this._log.info("Lock reset"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset", ""); + } else { + this._log.warn("Lock reset error"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); + } + }, + // XPCOM registration classDescription: "Bookmarks Sync Service", contractID: "@mozilla.org/places/sync-service;1", @@ -1268,6 +1278,11 @@ BookmarksSyncService.prototype = { this._log.info("Logging out"); this._dav.logout(); this._os.notifyObservers(null, "bookmarks-sync:logout", ""); + }, + + resetLock: function BSS_resetLock() { + this._log.info("Resetting lock"); + this._dav.forceUnlock.async(this._dav, bind2(this, this._onResetLock)); } }; @@ -1606,21 +1621,33 @@ DAVCollection.prototype = { } }, - stealLock: function DC_stealLock(onComplete) { + forceUnlock: function DC_forceUnlock(onComplete) { let cont = yield; - let stolen = null; + let unlocked = true; try { if (!this._getActiveLock.async(this, cont)) return; this._token = yield; - let unlocked = true; if (this._token) { if (!this.unlock.async(this, cont)) return; unlocked = yield; } + } finally { + generatorDone(this, onComplete, unlocked); + } + }, + + stealLock: function DC_stealLock(onComplete) { + let cont = yield; + let stolen = null; + + try { + if (!this.forceUnlock.async(this, cont)) + return; + let unlocked = yield; if (unlocked && this.lock.async(this, cont)) stolen = yield; diff --git a/services/sync/IBookmarksSyncService.idl b/services/sync/IBookmarksSyncService.idl index 1ab991512b2a..287a1a1c6b19 100644 --- a/services/sync/IBookmarksSyncService.idl +++ b/services/sync/IBookmarksSyncService.idl @@ -60,4 +60,10 @@ interface IBookmarksSyncService : nsISupports * Initiate a sync operation. */ void sync(); + + /** + * Reset a server lock + * (for stale locks, useful for debugging after a crash) + */ + void resetLock(); }; diff --git a/services/sync/IBookmarksSyncService.xpt b/services/sync/IBookmarksSyncService.xpt index ae6d3d02afe39d59a601c653ba4be53aca015f0e..97238be8d71c88d0c909c97eb7abb257b84ac753 100644 GIT binary patch delta 66 zcmX@ac!hC-IOCOx5@{N&4GatnA&e__GOz&|3<(SnIt@zaLg}K3^HjKsQj1edeDagC F834%|4NU+5 delta 47 scmcb@c!+U=IOCy-5@`x74GatnK8!1NGOz&|3=s?vIu1&wPF$t}00Nf?>;M1& From cb5af2787e2cbdc7099e5b79e7bbd16f5552845c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 15:31:15 -0700 Subject: [PATCH 0045/1860] go go synchotron! --- services/sync/BookmarksSyncService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 2fff176c53b0..a8fa7599fa54 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -934,7 +934,7 @@ BookmarksSyncService.prototype = { } if (clientConflicts.length || serverConflicts.length) { - this._log.error("\nWARNING: Conflicts found, but we don't resolve conflicts yet!\n"); + this._log.warn("Conflicts found! Discarding server changes"); } // 3.1) Apply server changes to local store From 5a3016b24ee538508a7def18b3b2dda49ef66247 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 16:52:43 -0700 Subject: [PATCH 0046/1860] various backendy fixes --- services/sync/BookmarksSyncService.js | 65 ++++++++++++--------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index a8fa7599fa54..0ed1e6ff99a5 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -478,6 +478,23 @@ BookmarksSyncService.prototype = { return false; }, + _getPropagations: function BSS__getPropagations(commands, conflicts, propagations) { + for (let i = 0; i < commands.length; i++) { + let alsoConflicts = function(elt) { + return elt.action == "remove" && + commands[i].parents.indexOf(elt.GUID) >= 0; + }; + if (conflicts.some(alsoConflicts)) + conflicts.push(commands[i]); + + let cmdConflicts = function(elt) { + return elt.GUID == commands[i].GUID; + }; + if (!conflicts.some(cmdConflicts)) + propagations.push(commands[i]); + } + }, + _reconcile: function BSS__reconcile(onComplete, listA, listB) { let cont = yield; let listener = new EventListener(cont); @@ -504,7 +521,7 @@ BookmarksSyncService.prototype = { // watch out for create commands with GUIDs that already exist if (listB[j] && listB[j].action == "create" && - this._bms.getItemForGUID(listB[j].GUID) >= 0) { + this._bms.getItemIdForGUID(listB[j].GUID) >= 0) { this._log.error("Remote command has GUID that already exists " + "locally. Dropping command."); delete listB[j]; @@ -532,38 +549,12 @@ BookmarksSyncService.prototype = { } } - for (let i = 0; i < listA.length; i++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; // Yield to main loop + this._getPropagations(listA, conflicts[0], propagations[1]); - let alsoConflictsA = function(elt) { - return listA[i].parents.indexOf(elt.GUID) >= 0; - }; - if (conflicts[0].some(alsoConflictsA)) - conflicts[0].push(listA[i]); + this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + yield; // Yield to main loop - let conflictsA = function(elt) { - return elt.GUID == listA[i].GUID; - }; - if (!conflicts[0].some(conflictsA)) - propagations[1].push(listA[i]); - } - for (let j = 0; j < listB.length; j++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - let alsoConflictsB = function(elt) { - return listB[j].parents.indexOf(elt.GUID) >= 0; - }; - if (conflicts[1].some(alsoConflictsB)) - conflicts[1].push(listB[j]); - - let conflictsB = function(elt) { - return elt.GUID == listB[j].GUID; - }; - if (!conflicts[1].some(conflictsB)) - propagations[0].push(listB[j]); - } + this._getPropagations(listB, conflicts[1], propagations[0]); this._timer = null; let ret = {propagations: propagations, conflicts: conflicts}; @@ -631,7 +622,7 @@ BookmarksSyncService.prototype = { let newId; let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); - if (parentId <= 0) { + if (parentId < 0) { this._log.warn("Creating node with unknown parent -> reparenting to root"); parentId = this._bms.bookmarksRoot; } @@ -707,7 +698,7 @@ BookmarksSyncService.prototype = { _editCommand: function BSS__editCommand(command) { var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId == -1) { + if (itemId < 0) { this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); return; } @@ -716,7 +707,7 @@ BookmarksSyncService.prototype = { switch (key) { case "GUID": var existing = this._bms.getItemIdForGUID(command.data.GUID); - if (existing == -1) + if (existing < 0) this._bms.setItemGUID(itemId, command.data.GUID); else this._log.warn("Can't change GUID " + command.GUID + @@ -858,14 +849,14 @@ BookmarksSyncService.prototype = { return; } - var localJson = this._getBookmarks(); - this._log.debug("local json:\n" + this._mungeNodes(localJson)); - // 1) Fetch server deltas if (!this._getServerData.async(this, cont)) return let server = yield; + var localJson = this._getBookmarks(); + + this._log.debug("local json:\n" + this._mungeNodes(localJson)); this._log.info("Local snapshot version: " + this._snapshotVersion); this._log.info("Server status: " + server.status); From b0eb40baf4c576be710333f5dbe65c227b3f6a22 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 17:06:13 -0700 Subject: [PATCH 0047/1860] one more conflict detection fix --- services/sync/BookmarksSyncService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 0ed1e6ff99a5..e9e04e158988 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -481,7 +481,7 @@ BookmarksSyncService.prototype = { _getPropagations: function BSS__getPropagations(commands, conflicts, propagations) { for (let i = 0; i < commands.length; i++) { let alsoConflicts = function(elt) { - return elt.action == "remove" && + return (elt.action == "create" || elt.action == "remove") && commands[i].parents.indexOf(elt.GUID) >= 0; }; if (conflicts.some(alsoConflicts)) From 87a5e0dad80dd89eb5d9f5553f09dc66986d4c3a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 18:14:44 -0700 Subject: [PATCH 0048/1860] add timer support, needs cbeard's new prefs --- services/sync/BookmarksSyncService.js | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index e9e04e158988..0cd3c65d554a 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -129,6 +129,9 @@ BookmarksSyncService.prototype = { // Timer object for yielding to the main loop _timer: null, + // Timer object for automagically syncing + _scheduleTimer: null, + // DAVCollection object _dav: null, @@ -159,16 +162,58 @@ BookmarksSyncService.prototype = { this._log.info("Bookmarks Sync Service Initializing"); let serverURL = 'https://dotmoz.mozilla.org/'; + let enabled = false; + let schedule = 0; try { let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); serverURL = branch.getCharPref("browser.places.sync.serverURL"); + enabled = branch.getBoolPref("browser.places.sync.enabled"); + schedule = branch.getIntPref("browser.places.sync.schedule"); + + branch.addObserver("browser.places.sync", this, false); } catch (ex) { /* use defaults */ } this._log.info("Bookmarks login server: " + serverURL); this._dav = new DAVCollection(serverURL); this._readSnapshot(); + + if (!enabled) { + this._log.info("Bookmarks sync disabled"); + return; + } + + switch (schedule) { + case 0: + this._log.info("Bookmarks sync enabled, manual mode"); + break; + case 1: + this._log.info("Bookmarks sync enabled, automagic mode"); + this._enableSchedule(); + break; + default: + this._log.info("Bookmarks sync enabled"); + this._log.info("Invalid schedule setting: " + schedule); + break; + } + }, + + _enableSchedule: function BSS__enableSchedule() { + this._scheduleTimer = Cc["@mozilla.org/timer;1"]. + createInstance(Ci.nsITimer); + let listener = new EventListener(bind2(this, this._onSchedule)); + this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min + this._scheduleTimer.TYPE_REPEATING_SLACK); + }, + + _disableSchedule: function BSS__disableSchedule() { + this._scheduleTimer = null; + }, + + _onSchedule: function BSS__onSchedule() { + this._log.info("Running scheduled sync"); + this.sync(); }, _initLogs: function BSS__initLogs() { @@ -1246,10 +1291,35 @@ BookmarksSyncService.prototype = { contractID: "@mozilla.org/places/sync-service;1", classID: Components.ID("{efb3ba58-69bc-42d5-a430-0746fa4b1a7f}"), QueryInterface: XPCOMUtils.generateQI([Ci.IBookmarksSyncService, + Ci.nsIObserver, Ci.nsISupports]), // nsISupports + // nsIObserver + + observe: function BSS__observe(subject, topic, data) { + switch (topic) { + case "browser.places.sync.schedule": + switch (data) { + case 0: + this._log.info("Disabling automagic bookmarks sync"); + this._disableSchedule(); + break; + case 1: + this._log.info("Enabling automagic bookmarks sync"); + this._enableSchedule(); + break; + default: + this._log.warn("Unknown schedule value set"); + break; + } + break; + default: + // ignore, there are prefs we observe but don't care about + } + }, + // IBookmarksSyncService sync: function BSS_sync() { From af10d9f5a1b78b985d3d086becf42b9b80be9276 Mon Sep 17 00:00:00 2001 From: Date: Mon, 15 Oct 2007 18:26:39 -0700 Subject: [PATCH 0049/1860] revised prefpane and workflows to get us to a more public test of sync --- services/sync/BookmarksSyncService.js | 2 +- services/sync/services-sync.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 0cd3c65d554a..132453d25ab4 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1482,7 +1482,7 @@ DAVCollection.prototype = { // fixme: make a request and get the realm let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); let logins = lm.findLogins({}, URI.hostPort, null, - 'Use your ldap username/password - dotmoz'); + 'services.mozilla.com'); for (let i = 0; i < logins.length; i++) { if (logins[i].username == username) { diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 2936e2da08ff..9c0d3684ab1e 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,8 @@ pref("browser.places.sync.serverURL", "https://services.mozilla.com/user/"); pref("browser.places.sync.username", "nobody@mozilla.com"); -pref("browser.places.sync.autoconnect", false); +pref("browser.places.sync.rememberpassword", true); +pref("browser.places.sync.autoconnect", true); +pref("browser.places.sync.enabled", true); +pref("browser.places.sync.bookmarks", true); +pref("browser.places.sync.schedule", 1) pref("browser.places.sync.lastsync", "0"); From 03452b4c99cd9aca1bdf679549bce7dc33c7b5f4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 18:45:02 -0700 Subject: [PATCH 0050/1860] listen to sync.enabled pref and enable/disable auto sync --- services/sync/BookmarksSyncService.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 132453d25ab4..51993b8d0b94 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1300,6 +1300,18 @@ BookmarksSyncService.prototype = { observe: function BSS__observe(subject, topic, data) { switch (topic) { + case "browser.places.sync.enabled": + switch (data) { + case false: + this._log.info("Disabling automagic bookmarks sync"); + this._disableSchedule(); + break; + case true: + this._log.info("Enabling automagic bookmarks sync"); + this._enableSchedule(); + break; + } + break; case "browser.places.sync.schedule": switch (data) { case 0: From 22b53dfe57e6d197a4d9cd91638a675d2572d7dc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 19:59:54 -0700 Subject: [PATCH 0051/1860] add resetServer, fix schedule pref --- services/sync/BookmarksSyncService.js | 107 +++++++++++++++++++++++--- services/sync/services-sync.js | 2 +- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 51993b8d0b94..c1fc7f707dd6 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -882,7 +882,11 @@ BookmarksSyncService.prototype = { _doSync: function BSS__doSync() { let cont = yield; + let synced = false; + try { + this._os.notifyObservers(null, "bookmarks-sync:start", ""); + if (!this._dav.lock.async(this._dav, cont)) return; let locked = yield; @@ -896,7 +900,7 @@ BookmarksSyncService.prototype = { // 1) Fetch server deltas if (!this._getServerData.async(this, cont)) - return + return; let server = yield; var localJson = this._getBookmarks(); @@ -923,6 +927,7 @@ BookmarksSyncService.prototype = { if (server.updates.length == 0 && localUpdates.length == 0) { this._snapshotVersion = server.maxVersion; this._log.info("Sync complete (1): no changes needed on client or server"); + synced = true; return; } @@ -930,7 +935,7 @@ BookmarksSyncService.prototype = { this._log.info("Reconciling client/server updates"); if (!this._reconcile.async(this, cont, localUpdates, server.updates)) - return + return; let ret = yield; // FIXME: Need to come up with a closing protocol for generators @@ -966,6 +971,7 @@ BookmarksSyncService.prototype = { this._snapshot = localJson; this._snapshotVersion = server.maxVersion; this._saveSnapshot(); + synced = true; return; } @@ -1040,15 +1046,18 @@ BookmarksSyncService.prototype = { } } this._log.info("Sync complete"); + synced = true; + } finally { + if (this._dav.unlock.async(this._dav, cont)) { + let unlocked = yield; + if (unlocked) + this._log.info("Lock released"); + else + this._log.error("Could not unlock DAV collection"); + } this._os.notifyObservers(null, "bookmarks-sync:end", ""); - if (!this._dav.unlock.async(this._dav, cont)) - return; - let unlocked = yield; - if (unlocked) - this._log.info("Lock released"); - else - this._log.error("Could not unlock DAV collection"); + generatorDone(this, onComplete, synced); } }, @@ -1286,6 +1295,66 @@ BookmarksSyncService.prototype = { } }, + _resetServer: function BSS__resetServer() { + let cont = yield; + let done = false; + + try { + this._os.notifyObservers(null, "bookmarks-sync:reset-server:start", ""); + + if (!this._dav.lock.async(this._dav, cont)) + return; + let locked = yield; + + if (locked) + this._log.info("Lock acquired"); + else { + this._log.warn("Could not acquire lock, aborting sync"); + return; + } + + this._dav.DELETE("bookmarks-status.json", cont); + let statusResp = yield; + this._dav.DELETE("bookmarks-snapshot.json", cont); + let snapshotResp = yield; + this._dav.DELETE("bookmarks-deltas.json", cont); + let deltasResp = yield; + + if (statusResp.target.status < 200 || statusResp.target.status >= 300 || + snapshotResp.target.status < 200 || snapshotResp.target.status >= 300 || + deltasResp.target.status < 200 || deltasResp.target.status >= 300) { + this._log.error("Could delete server data, response codes " + + statusResp.target.status + ", " + + snapshotResp.target.status + ", " + + deltasResp.target.status); + return; + } + + this._log.info("Server files deleted, starting sync"); + this._os.notifyObservers(null, "bookmarks-sync:start", ""); + if (!this._doSync.async(this, cont)) + return; + done = yield; + + } finally { + if (this._dav.unlock.async(this._dav, cont)) { + let unlocked = yield; + if (unlocked) + this._log.info("Lock released"); + else + this._log.error("Could not unlock DAV collection"); + } + + if (done) + this._info("Server reset completed successfully"); + else + this._info("Server reset failed: could not sync bookmarks"); + + this._os.notifyObservers(null, "bookmarks-sync:reset-server:end", ""); + generatorDone(this, onComplete, done) + } + }, + // XPCOM registration classDescription: "Bookmarks Sync Service", contractID: "@mozilla.org/places/sync-service;1", @@ -1336,8 +1405,8 @@ BookmarksSyncService.prototype = { sync: function BSS_sync() { this._log.info("Beginning sync"); - this._os.notifyObservers(null, "bookmarks-sync:start", ""); - this._doSync.async(this); + if (!this._doSync.async(this)) + this._os.notifyObservers(null, "bookmarks-sync:end", false); }, login: function BSS_login() { @@ -1356,6 +1425,11 @@ BookmarksSyncService.prototype = { resetLock: function BSS_resetLock() { this._log.info("Resetting lock"); this._dav.forceUnlock.async(this._dav, bind2(this, this._onResetLock)); + }, + + resetServer: function BSS_resetServer() { + this._log.info("Resetting server data"); + this._resetServer.async(this); } }; @@ -1568,6 +1642,17 @@ DAVCollection.prototype = { request.send(data); }, + DELETE: function DC_DELETE(path, onComplete, headers) { + if (!headers) + headers = {'Content-type': 'text/plain'}; + if (this._auth) + headers['Authorization'] = this._auth; + if (this._token) + headers['If'] = "<" + this._bashURL + "> (<" + this._token + ">)"; + let request = this._makeRequest("DELETE", path, onComplete, headers); + request.send(null); + }, + // Login / Logout login: function DC_login(onComplete) { diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9c0d3684ab1e..851b7657c660 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -4,5 +4,5 @@ pref("browser.places.sync.rememberpassword", true); pref("browser.places.sync.autoconnect", true); pref("browser.places.sync.enabled", true); pref("browser.places.sync.bookmarks", true); -pref("browser.places.sync.schedule", 1) +pref("browser.places.sync.schedule", 1); pref("browser.places.sync.lastsync", "0"); From a04d22599186b381d9b277b97a413ac1c37396aa Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 20:00:42 -0700 Subject: [PATCH 0052/1860] add resetServer to idl --- services/sync/IBookmarksSyncService.idl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/sync/IBookmarksSyncService.idl b/services/sync/IBookmarksSyncService.idl index 287a1a1c6b19..c32a395ae2bb 100644 --- a/services/sync/IBookmarksSyncService.idl +++ b/services/sync/IBookmarksSyncService.idl @@ -66,4 +66,9 @@ interface IBookmarksSyncService : nsISupports * (for stale locks, useful for debugging after a crash) */ void resetLock(); + + /** + * Delete json on server and re-sync + */ + void resetServer(); }; From 9af6caf938dd087ba7e87f273bc73051b0fade5d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 20:01:56 -0700 Subject: [PATCH 0053/1860] new xpt from resetServer idl changes --- services/sync/IBookmarksSyncService.xpt | Bin 212 -> 232 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/services/sync/IBookmarksSyncService.xpt b/services/sync/IBookmarksSyncService.xpt index 97238be8d71c88d0c909c97eb7abb257b84ac753..b3c4f23be2d8a0a6599eef6957e0638d368b01b5 100644 GIT binary patch delta 76 zcmcb@_=0hQIOB_n5@`l(4GatnF^nsAGOz&|3>gd%IuA;hLg^|f-7;~R9#2tfacW6$ KYEfBg5d#2B3=c#A delta 55 ucmaFCc!hC-IOCOx5@{N&4GatnA&e__GOz&|3<(SnIt@zaLg}K3>+}FoI0@GP From f99d47cb1557bc8ca3a3acea134e722cffa0516d Mon Sep 17 00:00:00 2001 From: Date: Mon, 15 Oct 2007 20:04:00 -0700 Subject: [PATCH 0054/1860] more cleanup of preferences, wizard and temporary privacy text for internal testing --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9c0d3684ab1e..851b7657c660 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -4,5 +4,5 @@ pref("browser.places.sync.rememberpassword", true); pref("browser.places.sync.autoconnect", true); pref("browser.places.sync.enabled", true); pref("browser.places.sync.bookmarks", true); -pref("browser.places.sync.schedule", 1) +pref("browser.places.sync.schedule", 1); pref("browser.places.sync.lastsync", "0"); From 1ede88b28dfb9901768480089ec184c4eb28d307 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 20:04:02 -0700 Subject: [PATCH 0055/1860] only run generatorDone if there's an onComplete handler --- services/sync/BookmarksSyncService.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index c1fc7f707dd6..85c83316fc3c 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -880,7 +880,7 @@ BookmarksSyncService.prototype = { // 3.1) Apply local delta with server changes ("D") // 3.2) Append server delta to the delta file and upload ("C") - _doSync: function BSS__doSync() { + _doSync: function BSS__doSync(onComplete) { let cont = yield; let synced = false; @@ -1057,7 +1057,8 @@ BookmarksSyncService.prototype = { this._log.error("Could not unlock DAV collection"); } this._os.notifyObservers(null, "bookmarks-sync:end", ""); - generatorDone(this, onComplete, synced); + if (onComplete) + generatorDone(this, onComplete, synced); } }, @@ -1295,7 +1296,7 @@ BookmarksSyncService.prototype = { } }, - _resetServer: function BSS__resetServer() { + _resetServer: function BSS__resetServer(onComplete) { let cont = yield; let done = false; @@ -1351,7 +1352,8 @@ BookmarksSyncService.prototype = { this._info("Server reset failed: could not sync bookmarks"); this._os.notifyObservers(null, "bookmarks-sync:reset-server:end", ""); - generatorDone(this, onComplete, done) + if (onComplete) + generatorDone(this, onComplete, done) } }, From e083fcd65bf186329fa3105f6ebbdce179801763 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 20:07:00 -0700 Subject: [PATCH 0056/1860] hook up resetServer to UI --- services/sync/BookmarksSyncService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 85c83316fc3c..0c8e5ba67045 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1347,9 +1347,9 @@ BookmarksSyncService.prototype = { } if (done) - this._info("Server reset completed successfully"); + this._log.info("Server reset completed successfully"); else - this._info("Server reset failed: could not sync bookmarks"); + this._log.info("Server reset failed: could not sync bookmarks"); this._os.notifyObservers(null, "bookmarks-sync:reset-server:end", ""); if (onComplete) From 5f57fd3bf349fd2ae6f9f3290211b411ce6606be Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 15 Oct 2007 20:16:11 -0700 Subject: [PATCH 0057/1860] accept only 200 and 404 as good return codes from a DELETE during resetServer(); release lock before calling doSync --- services/sync/BookmarksSyncService.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 0c8e5ba67045..ff887032e86a 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1321,9 +1321,17 @@ BookmarksSyncService.prototype = { this._dav.DELETE("bookmarks-deltas.json", cont); let deltasResp = yield; - if (statusResp.target.status < 200 || statusResp.target.status >= 300 || - snapshotResp.target.status < 200 || snapshotResp.target.status >= 300 || - deltasResp.target.status < 200 || deltasResp.target.status >= 300) { + if (this._dav.unlock.async(this._dav, cont)) { + let unlocked = yield; + if (unlocked) + this._log.info("Lock released"); + else + this._log.error("Could not unlock DAV collection"); + } + + if (!(statusResp.target.status == 200 || statusResp.target.status == 404 || + snapshotResp.target.status == 200 || snapshotResp.target.status == 404 || + deltasResp.target.status == 200 || deltasResp.target.status == 404)) { this._log.error("Could delete server data, response codes " + statusResp.target.status + ", " + snapshotResp.target.status + ", " + @@ -1338,14 +1346,6 @@ BookmarksSyncService.prototype = { done = yield; } finally { - if (this._dav.unlock.async(this._dav, cont)) { - let unlocked = yield; - if (unlocked) - this._log.info("Lock released"); - else - this._log.error("Could not unlock DAV collection"); - } - if (done) this._log.info("Server reset completed successfully"); else From e77f8673b13a4abcab46f6ea3dd459da903f99cc Mon Sep 17 00:00:00 2001 From: Date: Tue, 16 Oct 2007 01:45:57 -0700 Subject: [PATCH 0058/1860] move account 'creation' into the service --- services/sync/BookmarksSyncService.js | 9 +++++---- services/sync/services-sync.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index ff887032e86a..c83b46aad7e6 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1668,22 +1668,23 @@ DAVCollection.prototype = { this._authProvider._authFailed = false; - let request = this._makeRequest("GET", "", cont, headers); + // This ensures the auth header is correct, and it doubles as an + // account creation request + let request = this._makeRequest("GET", "createAcct.php", cont, headers); request.send(null); let event = yield; if (this._authProvider._authFailed || event.target.status >= 400) return; - // XXX we need to refine some server response codes let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); this._currentUser = branch.getCharPref("browser.places.sync.username"); - // XXX + // FIXME: hack let path = this._currentUser.split("@"); this._currentUserPath = path[0]; - this._baseURL = this._baseURL + this._currentUserPath + "/"; + this._baseURL = this._baseURL + "user/" + this._currentUserPath + "/"; this._loggedIn = true; } finally { diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 851b7657c660..9a7f3cb5dac7 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,4 @@ -pref("browser.places.sync.serverURL", "https://services.mozilla.com/user/"); +pref("browser.places.sync.serverURL", "https://services.mozilla.com/"); pref("browser.places.sync.username", "nobody@mozilla.com"); pref("browser.places.sync.rememberpassword", true); pref("browser.places.sync.autoconnect", true); From 7d46a4e7b317e4abac2d0b02c8232b618cbf87f1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 16 Oct 2007 15:22:40 -0700 Subject: [PATCH 0059/1860] add a sync error event; make event names slightly more uniform; fix some resolver bugs --- services/sync/BookmarksSyncService.js | 63 +++++++++++++++++++-------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index c83b46aad7e6..11907f51fb51 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -883,13 +883,14 @@ BookmarksSyncService.prototype = { _doSync: function BSS__doSync(onComplete) { let cont = yield; let synced = false; + let locked = null; try { - this._os.notifyObservers(null, "bookmarks-sync:start", ""); + this._os.notifyObservers(null, "bookmarks-sync:sync-start", ""); if (!this._dav.lock.async(this._dav, cont)) return; - let locked = yield; + locked = yield; if (locked) this._log.info("Lock acquired"); @@ -957,11 +958,13 @@ BookmarksSyncService.prototype = { serverConflicts = ret.conflicts[1]; this._log.info("Changes for client: " + clientChanges.length); - this._log.info("Changes for server: " + serverChanges.length); + this._log.info("Predicted changes for server: " + + serverChanges.length); this._log.info("Client conflicts: " + clientConflicts.length); this._log.info("Server conflicts: " + serverConflicts.length); this._log.debug("Changes for client: " + this._mungeCommands(clientChanges)); - this._log.debug("Changes for server: " + this._mungeCommands(serverChanges)); + this._log.debug("Predicted changes for server: " + + this._mungeCommands(serverChanges)); this._log.debug("Client conflicts: " + this._mungeConflicts(clientConflicts)); this._log.debug("Server conflicts: " + this._mungeConflicts(serverConflicts)); @@ -979,6 +982,10 @@ BookmarksSyncService.prototype = { this._log.warn("Conflicts found! Discarding server changes"); } + let savedSnap = eval(uneval(this._snapshot)); + let savedVersion = this._snapshotVersion; + let newSnapshot; + // 3.1) Apply server changes to local store if (clientChanges.length) { this._log.info("Applying changes locally"); @@ -988,17 +995,19 @@ BookmarksSyncService.prototype = { this._snapshot = this._applyCommandsToObj(clientChanges, localJson); this._snapshotVersion = server.maxVersion; this._applyCommands(clientChanges); + newSnapshot = this._getBookmarks(); - let newSnapshot = this._getBookmarks(); let diff = this._detectUpdates(this._snapshot, newSnapshot); if (diff.length != 0) { this._log.error("Commands did not apply correctly"); this._log.debug("Diff from snapshot+commands -> " + "new snapshot after commands:\n" + this._mungeCommands(diff)); - this._snapshot = newSnapshot; - // FIXME: What else can we do? + // FIXME: do we really want to revert the snapshot here? + this._snapshot = eval(uneval(savedSnap)); + this._snapshotVersion = savedVersion; } + this._saveSnapshot(); } @@ -1008,7 +1017,8 @@ BookmarksSyncService.prototype = { // current client snapshot. In the case where there are no // conflicts, it should be the same as what the resolver returned - let serverDelta = this._detectUpdates(server.snapshot, this._snapshot); + newSnapshot = this._getBookmarks(); + let serverDelta = this._detectUpdates(server.snapshot, newSnapshot); // Log an error if not the same if (!(serverConflicts.length || @@ -1016,10 +1026,16 @@ BookmarksSyncService.prototype = { this._log.error("Predicted server changes differ from " + "actual server->client diff"); + this._log.info("Actual changes for server: " + serverDelta.length); + this._log.debug("Actual changes for server: " + + this._mungeCommands(serverDelta)); + if (serverDelta.length) { this._log.info("Uploading changes to server"); - this._snapshot = this._getBookmarks(); + + this._snapshot = newSnapshot; this._snapshotVersion = ++server.maxVersion; + server.deltas.push(serverDelta); this._dav.PUT("bookmarks-deltas.json", @@ -1045,18 +1061,24 @@ BookmarksSyncService.prototype = { this._log.error("Could not update deltas on server"); } } + this._log.info("Sync complete"); synced = true; } finally { - if (this._dav.unlock.async(this._dav, cont)) { + if (locked && this._dav.unlock.async(this._dav, cont)) { let unlocked = yield; - if (unlocked) + if (unlocked) { + locked = null; this._log.info("Lock released"); - else + this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); + } else { this._log.error("Could not unlock DAV collection"); + this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); + } + } else { + this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); } - this._os.notifyObservers(null, "bookmarks-sync:end", ""); if (onComplete) generatorDone(this, onComplete, synced); } @@ -1301,7 +1323,7 @@ BookmarksSyncService.prototype = { let done = false; try { - this._os.notifyObservers(null, "bookmarks-sync:reset-server:start", ""); + this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", ""); if (!this._dav.lock.async(this._dav, cont)) return; @@ -1340,7 +1362,6 @@ BookmarksSyncService.prototype = { } this._log.info("Server files deleted, starting sync"); - this._os.notifyObservers(null, "bookmarks-sync:start", ""); if (!this._doSync.async(this, cont)) return; done = yield; @@ -1351,7 +1372,7 @@ BookmarksSyncService.prototype = { else this._log.info("Server reset failed: could not sync bookmarks"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server:end", ""); + this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); if (onComplete) generatorDone(this, onComplete, done) } @@ -1407,15 +1428,19 @@ BookmarksSyncService.prototype = { sync: function BSS_sync() { this._log.info("Beginning sync"); - if (!this._doSync.async(this)) - this._os.notifyObservers(null, "bookmarks-sync:end", false); + if (!this._doSync.async(this)) { + this._log.fatal("Could not start sync operation"); + this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); + } }, login: function BSS_login() { this._log.info("Logging in"); let callback = bind2(this, this._onLogin); - if (!this._dav.login.async(this._dav, callback)) + if (!this._dav.login.async(this._dav, callback)) { + this._log.fatal("Could not start login operation"); this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + } }, logout: function BSS_logout() { From 6a07a29ad8860888a0b59a9c9b188d7667385c9e Mon Sep 17 00:00:00 2001 From: Date: Tue, 16 Oct 2007 15:29:02 -0700 Subject: [PATCH 0060/1860] myk's patch to catch and ignore a microsummary exception --- services/sync/BookmarksSyncService.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 11907f51fb51..3536df2cc1dd 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -688,8 +688,11 @@ BookmarksSyncService.prototype = { if (command.data.type == "microsummary") { this._log.info(" \-> is a microsummary"); let genURI = makeURI(command.data.generatorURI); - let micsum = this._ms.createMicrosummary(URI, genURI); - this._ms.setMicrosummary(newId, micsum); + try { + let micsum = this._ms.createMicrosummary(URI, genURI); + this._ms.setMicrosummary(newId, micsum); + } + catch(ex) { /* ignore "missing local generator" exceptions */ } } break; case "folder": From 39e76c0f66c1226f219ba782646a5dd983bce7e7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 16 Oct 2007 15:29:31 -0700 Subject: [PATCH 0061/1860] fix logout/re-login --- services/sync/BookmarksSyncService.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 11907f51fb51..73e888a534ad 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1551,6 +1551,7 @@ function xpath(xmlDoc, xpathString) { function DAVCollection(baseURL) { this._baseURL = baseURL; + this._userURL = baseURL; this._authProvider = new DummyAuthProvider(); } DAVCollection.prototype = { @@ -1619,6 +1620,10 @@ DAVCollection.prototype = { return this._baseURL; }, + get userURL() { + return this._userURL; + }, + _loggedIn: false, _currentUserPath: "nobody", @@ -1634,7 +1639,7 @@ DAVCollection.prototype = { request.addEventListener("load", new EventListener(onComplete), false); request.addEventListener("error", new EventListener(onComplete), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); - request.open(op, this._baseURL + path, true); + request.open(op, this._userURL + path, true); if (headers) { for (var key in headers) { @@ -1709,7 +1714,7 @@ DAVCollection.prototype = { // FIXME: hack let path = this._currentUser.split("@"); this._currentUserPath = path[0]; - this._baseURL = this._baseURL + "user/" + this._currentUserPath + "/"; + this._userURL = this._baseURL + "user/" + this._currentUserPath + "/"; this._loggedIn = true; } finally { From 50d1742a19f9a3be40958b63a0029e5fbe3db4e5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 16 Oct 2007 15:36:26 -0700 Subject: [PATCH 0062/1860] baseURL -> userURL --- services/sync/BookmarksSyncService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 7281abc4add6..9cc4ca5e4024 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1585,7 +1585,7 @@ DAVCollection.prototype = { return this.__auth; try { - let URI = makeURI(this._baseURL); + let URI = makeURI(this._userURL); let username = 'nobody@mozilla.com'; let password; From 46449a5148cf7ad78b904266a2b3e057b9b492b7 Mon Sep 17 00:00:00 2001 From: Date: Tue, 16 Oct 2007 15:47:54 -0700 Subject: [PATCH 0063/1860] fix to allow subsequent logins --- services/sync/BookmarksSyncService.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 9cc4ca5e4024..8a77757ddc10 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -161,7 +161,7 @@ BookmarksSyncService.prototype = { this._initLogs(); this._log.info("Bookmarks Sync Service Initializing"); - let serverURL = 'https://dotmoz.mozilla.org/'; + let serverURL = 'https://services.mozilla.com/'; let enabled = false; let schedule = 0; try { @@ -1557,6 +1557,7 @@ function DAVCollection(baseURL) { this._userURL = baseURL; this._authProvider = new DummyAuthProvider(); } + DAVCollection.prototype = { __dp: null, get _dp() { @@ -1726,6 +1727,8 @@ DAVCollection.prototype = { }, logout: function DC_logout() { + this._userURL = this._baseURL; // XXX this shouldn't be necessary, suggests that we're not invalidating/caching auth correctly + this._loggedIn = false; this.__auth = null; }, From 4fe09bc21a346ccc2d4ede9ac7e7de171e003bfb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 16 Oct 2007 16:49:46 -0700 Subject: [PATCH 0064/1860] fix same-parent requirement for command likeness --- services/sync/BookmarksSyncService.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 8a77757ddc10..0ed09648e977 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -445,10 +445,14 @@ BookmarksSyncService.prototype = { if (!a || !b || a.action != b.action || a.data.type != b.data.type || - a.parentGUID != b.parentGUID || + a.data.parentGUID != b.data.parentGUID || a.GUID == b.GUID) return false; + this._log.debug("deciding on likeness of " + a.GUID + " vs " + b.GUID); + this._log.debug("they have the same action, type, and parent"); + this._log.debug("parent is " + a.data.parentGUID); + switch (a.data.type) { case "bookmark": if (a.data.URI == b.data.URI && @@ -508,8 +512,8 @@ BookmarksSyncService.prototype = { for (let i = 0; i < list.length; i++) { if (!list[i]) continue; - if (list[i].parentGUID == oldGUID) - list[i].parentGUID = newGUID; + if (list[i].data.parentGUID == oldGUID) + list[i].data.parentGUID = newGUID; for (let j = 0; j < list[i].parents.length; j++) { if (list[i].parents[j] == oldGUID) list[i].parents[j] = newGUID; From f08447c36ae266de3b035d8a3c3ef03088e0dc5e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 16 Oct 2007 16:51:44 -0700 Subject: [PATCH 0065/1860] downgrade server changes error to a warning, it will be triggered often by changing indeces --- services/sync/BookmarksSyncService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 0ed09648e977..3cbe62da9b4b 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1030,8 +1030,8 @@ BookmarksSyncService.prototype = { // Log an error if not the same if (!(serverConflicts.length || this._deepEquals(serverChanges, serverDelta))) - this._log.error("Predicted server changes differ from " + - "actual server->client diff"); + this._log.warn("Predicted server changes differ from " + + "actual server->client diff (can be ignored in many cases)"); this._log.info("Actual changes for server: " + serverDelta.length); this._log.debug("Actual changes for server: " + From a9e174c4c1f095fa7ef43a362f35925357d03456 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 16 Oct 2007 20:11:35 -0700 Subject: [PATCH 0066/1860] login/logout fixes; (dav obj) just return from login call right away if we're already logged in --- services/sync/BookmarksSyncService.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 3cbe62da9b4b..d46597ece478 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1307,7 +1307,7 @@ BookmarksSyncService.prototype = { _onLogin: function BSS__onLogin(success) { if (success) { this._log.info("Logged in"); - this._log.info("Bookmarks sync server: " + this._dav.baseURL); + this._log.info("Bookmarks sync server: " + this._dav.userURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); } else { this._log.info("Could not log in"); @@ -1584,12 +1584,14 @@ DAVCollection.prototype = { }, // FIXME: should we regen this each time to prevent it from staying in memory? + __authURI: null, __auth: null, get _auth() { - if (this.__auth) + if (this.__auth && this._userURL == this.__authURI) return this.__auth; try { + this.__authURI = this._userURL; let URI = makeURI(this._userURL); let username = 'nobody@mozilla.com'; let password; @@ -1697,9 +1699,11 @@ DAVCollection.prototype = { login: function DC_login(onComplete) { let cont = yield; - this._loggedIn = false; try { + if (this._loggedIn) + return; + let headers = {}; if (this._auth) headers['Authorization'] = this._auth; From 90d67ff84fb9f9725f2cd871c8706d95ba5a3064 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 17 Oct 2007 13:49:51 -0700 Subject: [PATCH 0067/1860] add lots more logging output to DAVCollection --- services/sync/BookmarksSyncService.js | 76 +++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index d46597ece478..c5f333f832a2 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1560,6 +1560,9 @@ function DAVCollection(baseURL) { this._baseURL = baseURL; this._userURL = baseURL; this._authProvider = new DummyAuthProvider(); + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service.DAV"); } DAVCollection.prototype = { @@ -1590,6 +1593,8 @@ DAVCollection.prototype = { if (this.__auth && this._userURL == this.__authURI) return this.__auth; + this._log.debug("Generating new authentication header"); + try { this.__authURI = this._userURL; let URI = makeURI(this._userURL); @@ -1600,8 +1605,10 @@ DAVCollection.prototype = { getService(Ci.nsIPrefBranch); username = branch.getCharPref("browser.places.sync.username"); - if (!username) + if (!username) { + this._log.info("No username found in password mgr, can't generate auth header"); return null; + } // fixme: make a request and get the realm let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); @@ -1615,8 +1622,10 @@ DAVCollection.prototype = { } } - if (!password) + if (!password) { + this._log.info("No password found in password mgr, can't generate auth header"); return null; + } this.__auth = "Basic " + this._base64.Base64.encode(username + ":" + password); @@ -1643,7 +1652,9 @@ DAVCollection.prototype = { }, _makeRequest: function DC__makeRequest(op, path, onComplete, headers) { - var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); + this._log.debug("Creating request, url=" + this._userURL + path); + + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); request.addEventListener("load", new EventListener(onComplete), false); @@ -1652,7 +1663,8 @@ DAVCollection.prototype = { request.open(op, this._userURL + path, true); if (headers) { - for (var key in headers) { + let key; + for (key in headers) { request.setRequestHeader(key, headers[key]); } } @@ -1701,8 +1713,10 @@ DAVCollection.prototype = { let cont = yield; try { - if (this._loggedIn) + if (this._loggedIn) { + this._log.debug("Login requested, but already logged in"); return; + } let headers = {}; if (this._auth) @@ -1716,8 +1730,14 @@ DAVCollection.prototype = { request.send(null); let event = yield; - if (this._authProvider._authFailed || event.target.status >= 400) + if (this._authProvider._authFailed) { + this._log.warn("Login authentication failed"); return; + } + if (event.target.status >= 400) { + this._log.warn("Login request failed, status " + event.target.status); + return; + } let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); @@ -1735,8 +1755,7 @@ DAVCollection.prototype = { }, logout: function DC_logout() { - this._userURL = this._baseURL; // XXX this shouldn't be necessary, suggests that we're not invalidating/caching auth correctly - + this._log.debug("Logging out (forgetting auth header)"); this._loggedIn = false; this.__auth = null; }, @@ -1747,6 +1766,8 @@ DAVCollection.prototype = { let cont = yield; let ret = null; + this._log.info("Getting active lock token"); + try { let headers = {'Content-Type': 'text/xml; charset="utf-8"', 'Depth': '0'}; @@ -1759,6 +1780,16 @@ DAVCollection.prototype = { " " + ""); let event = yield; + + if (this._authProvider._authFailed) { + this._log.warn("_getActiveLock: authentication failed"); + return; + } + if (event.target.status >= 400) { + this._log.warn("_getActiveLock: got status " + event.target.status); + return; + } + let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); ret = token.textContent; @@ -1771,7 +1802,14 @@ DAVCollection.prototype = { let cont = yield; this._token = null; + this._log.info("Acquiring lock"); + try { + if (this._token) { + this._log.debug("Lock called, but we already hold a token"); + return; + } + headers = {'Content-Type': 'text/xml; charset="utf-8"', 'Timeout': 'Second-600', 'Depth': 'infinity'}; @@ -1786,8 +1824,14 @@ DAVCollection.prototype = { ""); let event = yield; - if (event.target.status < 200 || event.target.status >= 300) + if (this._authProvider._authFailed) { + this._log.warn("lock: authentication failed"); return; + } + if (event.target.status < 200 || event.target.status >= 300) { + this._log.warn("lock: got status " + event.target.status); + return; + } let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); @@ -1802,9 +1846,13 @@ DAVCollection.prototype = { unlock: function DC_unlock(onComplete) { let cont = yield; + this._log.info("Releasing lock"); + try { - if (!this._token) + if (!this._token) { + this._log.debug("Unlock called, but we don't hold a token right now"); return; + } let headers = {'Lock-Token': "<" + this._token + ">"}; if (this._auth) @@ -1814,8 +1862,14 @@ DAVCollection.prototype = { request.send(null); let event = yield; - if (event.target.status < 200 || event.target.status >= 300) + if (this._authProvider._authFailed) { + this._log.warn("unlock: authentication failed"); return; + } + if (event.target.status < 200 || event.target.status >= 300) { + this._log.warn("unlock: got status " + event.target.status); + return; + } this._token = null; } finally { From 46f7c4b04a985b322b8fa8be87b66a3f6639088b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 17 Oct 2007 22:03:55 -0700 Subject: [PATCH 0068/1860] change all generators to expect to be closed by their onComplete handler; make sure they all do the bulk of their work within try blocks and always call the onComplete handler --- services/sync/BookmarksSyncService.js | 703 ++++++++++++++------------ 1 file changed, 377 insertions(+), 326 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index c5f333f832a2..382794538b2a 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -545,69 +545,77 @@ BookmarksSyncService.prototype = { }, _reconcile: function BSS__reconcile(onComplete, listA, listB) { - let cont = yield; - let listener = new EventListener(cont); - this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let propagations = [[], []]; let conflicts = [[], []]; - - for (let i = 0; i < listA.length; i++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - for (let j = 0; j < listB.length; j++) { - if (this._deepEquals(listA[i], listB[j])) { - delete listA[i]; - delete listB[j]; - } else if (this._commandLike(listA[i], listB[j])) { - this._fixParents(listA, listA[i].GUID, listB[j].GUID); - listB[j].data = {GUID: listB[j].GUID}; - listB[j].GUID = listA[i].GUID; - listB[j].action = "edit"; - delete listA[i]; - } - - // watch out for create commands with GUIDs that already exist - if (listB[j] && listB[j].action == "create" && - this._bms.getItemIdForGUID(listB[j].GUID) >= 0) { - this._log.error("Remote command has GUID that already exists " + - "locally. Dropping command."); - delete listB[j]; - } - } - } - - listA = listA.filter(function(elt) { return elt }); - listB = listB.filter(function(elt) { return elt }); - - for (let i = 0; i < listA.length; i++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - for (let j = 0; j < listB.length; j++) { - if (this._conflicts(listA[i], listB[j]) || - this._conflicts(listB[j], listA[i])) { - if (!conflicts[0].some( - function(elt) { return elt.GUID == listA[i].GUID })) - conflicts[0].push(listA[i]); - if (!conflicts[1].some( - function(elt) { return elt.GUID == listB[j].GUID })) - conflicts[1].push(listB[j]); - } - } - } - - this._getPropagations(listA, conflicts[0], propagations[1]); - - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - this._getPropagations(listB, conflicts[1], propagations[0]); - - this._timer = null; let ret = {propagations: propagations, conflicts: conflicts}; - generatorDone(this, onComplete, ret); + + try { + let cont = yield; + let listener = new EventListener(cont); + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + for (let i = 0; i < listA.length; i++) { + this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + for (let j = 0; j < listB.length; j++) { + if (this._deepEquals(listA[i], listB[j])) { + delete listA[i]; + delete listB[j]; + } else if (this._commandLike(listA[i], listB[j])) { + this._fixParents(listA, listA[i].GUID, listB[j].GUID); + listB[j].data = {GUID: listB[j].GUID}; + listB[j].GUID = listA[i].GUID; + listB[j].action = "edit"; + delete listA[i]; + } + + // watch out for create commands with GUIDs that already exist + if (listB[j] && listB[j].action == "create" && + this._bms.getItemIdForGUID(listB[j].GUID) >= 0) { + this._log.error("Remote command has GUID that already exists " + + "locally. Dropping command."); + delete listB[j]; + } + } + } + + listA = listA.filter(function(elt) { return elt }); + listB = listB.filter(function(elt) { return elt }); + + for (let i = 0; i < listA.length; i++) { + this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + for (let j = 0; j < listB.length; j++) { + if (this._conflicts(listA[i], listB[j]) || + this._conflicts(listB[j], listA[i])) { + if (!conflicts[0].some( + function(elt) { return elt.GUID == listA[i].GUID })) + conflicts[0].push(listA[i]); + if (!conflicts[1].some( + function(elt) { return elt.GUID == listB[j].GUID })) + conflicts[1].push(listB[j]); + } + } + } + + this._getPropagations(listA, conflicts[0], propagations[1]); + + this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + this._getPropagations(listB, conflicts[1], propagations[0]); + ret = {propagations: propagations, conflicts: conflicts}; + } catch (e) { + if (e != 'close generator') + throw e; + } finally { + this._timer = null; + generatorDone(this, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); }, _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { @@ -891,87 +899,71 @@ BookmarksSyncService.prototype = { let cont = yield; let synced = false; let locked = null; + let gen; try { this._os.notifyObservers(null, "bookmarks-sync:sync-start", ""); - if (!this._dav.lock.async(this._dav, cont)) - return; + gen = this._dav.lock.async(this._dav, cont); locked = yield; + gen.close(); if (locked) this._log.info("Lock acquired"); else { this._log.warn("Could not acquire lock, aborting sync"); - return; + throw 'close generator'; } // 1) Fetch server deltas - if (!this._getServerData.async(this, cont)) - return; + gen = this._getServerData.async(this, cont); let server = yield; + gen.close(); - var localJson = this._getBookmarks(); - - this._log.debug("local json:\n" + this._mungeNodes(localJson)); this._log.info("Local snapshot version: " + this._snapshotVersion); this._log.info("Server status: " + server.status); + this._log.info("Server maxVersion: " + server.maxVersion); + this._log.info("Server snapVersion: " + server.snapVersion); if (server.status != 0) { this._log.fatal("Sync error: could not get server status, " + "or initial upload failed. Aborting sync."); - return; + throw 'close generator'; } - this._log.info("Server maxVersion: " + server.maxVersion); - this._log.info("Server snapVersion: " + server.snapVersion); - this._log.debug("Server updates: " + this._mungeCommands(server.updates)); - // 2) Generate local deltas from snapshot -> current client status + let localJson = this._getBookmarks(); let localUpdates = this._detectUpdates(this._snapshot, localJson); + this._log.debug("local json:\n" + this._mungeNodes(localJson)); this._log.debug("Local updates: " + this._mungeCommands(localUpdates)); + this._log.debug("Server updates: " + this._mungeCommands(server.updates)); if (server.updates.length == 0 && localUpdates.length == 0) { this._snapshotVersion = server.maxVersion; this._log.info("Sync complete (1): no changes needed on client or server"); synced = true; - return; + throw 'close generator'; } // 3) Reconcile client/server deltas and generate new deltas for them. this._log.info("Reconciling client/server updates"); - if (!this._reconcile.async(this, cont, localUpdates, server.updates)) - return; + gen = this._reconcile.async(this, cont, localUpdates, server.updates); let ret = yield; + gen.close(); - // FIXME: Need to come up with a closing protocol for generators - //rec_gen.close(); - - let clientChanges = []; - let serverChanges = []; - let clientConflicts = []; - let serverConflicts = []; - - if (ret.propagations[0]) - clientChanges = ret.propagations[0]; - if (ret.propagations[1]) - serverChanges = ret.propagations[1]; - - if (ret.conflicts && ret.conflicts[0]) - clientConflicts = ret.conflicts[0]; - if (ret.conflicts && ret.conflicts[1]) - serverConflicts = ret.conflicts[1]; + let clientChanges = ret.propagations[0]; + let serverChanges = ret.propagations[1]; + let clientConflicts = ret.conflicts[0]; + let serverConflicts = ret.conflicts[1]; this._log.info("Changes for client: " + clientChanges.length); - this._log.info("Predicted changes for server: " + - serverChanges.length); + this._log.info("Predicted changes for server: " + serverChanges.length); this._log.info("Client conflicts: " + clientConflicts.length); this._log.info("Server conflicts: " + serverConflicts.length); this._log.debug("Changes for client: " + this._mungeCommands(clientChanges)); - this._log.debug("Predicted changes for server: " + - this._mungeCommands(serverChanges)); + this._log.debug("Predicted changes for server: " + this._mungeCommands(serverChanges)); this._log.debug("Client conflicts: " + this._mungeConflicts(clientConflicts)); this._log.debug("Server conflicts: " + this._mungeConflicts(serverConflicts)); @@ -982,7 +974,7 @@ BookmarksSyncService.prototype = { this._snapshotVersion = server.maxVersion; this._saveSnapshot(); synced = true; - return; + throw 'close generator'; } if (clientConflicts.length || serverConflicts.length) { @@ -1072,23 +1064,32 @@ BookmarksSyncService.prototype = { this._log.info("Sync complete"); synced = true; + } catch (e) { + if (e != 'close generator') + throw e; + } finally { - if (locked && this._dav.unlock.async(this._dav, cont)) { - let unlocked = yield; - if (unlocked) { - locked = null; - this._log.info("Lock released"); - this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); - } else { - this._log.error("Could not unlock DAV collection"); - this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); - } + let ok = false; + if (locked) { + gen = this._dav.unlock.async(this._dav, cont); + ok = yield; + gen.close(); + } + if (ok && synced) { + this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); + generatorDone(this, onComplete, true); } else { this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); + generatorDone(this, onComplete, false); } - if (onComplete) - generatorDone(this, onComplete, synced); + yield; // onComplete is responsible for closing the generator } + this._log.warn("generator not properly closed"); + }, + + _onSyncFinished: function BSS__onSyncFinished() { + this._syncGen.close(); + this._syncGen = null; }, _getFolderNodes: function BSS__getFolderNodes(folder) { @@ -1138,173 +1139,182 @@ BookmarksSyncService.prototype = { */ _getServerData: function BSS__getServerData(onComplete) { let cont = yield; - let ret = {status: -1, formatVersion: null, maxVersion: null, snapVersion: null, snapshot: null, deltas: null, updates: null}; - this._log.info("Getting bookmarks status from server"); - this._dav.GET("bookmarks-status.json", cont); - let statusResp = yield; - - switch (statusResp.target.status) { - case 200: - this._log.info("Got bookmarks status from server"); - - let status = eval(statusResp.target.responseText); - let snap, deltas, allDeltas; - - // Bail out if the server has a newer format version than we can parse - if (status.formatVersion > STORAGE_FORMAT_VERSION) { - this._log.error("Server uses storage format v" + status.formatVersion + - ", this client understands up to v" + STORAGE_FORMAT_VERSION); - generatorDone(this, onComplete, ret) - return; - } - - if (status.GUID != this._snapshotGUID) { - this._log.info("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._resetGUIDs(); - this._snapshot = {}; - this._snapshotVersion = -1; - this._snapshotGUID = status.GUID; - } - - if (this._snapshotVersion < status.snapVersion) { - if (this._snapshotVersion >= 0) - this._log.info("Local snapshot is out of date"); - - this._log.info("Downloading server snapshot"); - this._dav.GET("bookmarks-snapshot.json", cont); - let snapResp = yield; - if (snapResp.target.status < 200 || snapResp.target.status >= 300) { - this._log.error("Could not download server snapshot"); + try { + this._log.info("Getting bookmarks status from server"); + this._dav.GET("bookmarks-status.json", cont); + let statusResp = yield; + + switch (statusResp.target.status) { + case 200: + this._log.info("Got bookmarks status from server"); + + let status = eval(statusResp.target.responseText); + let snap, deltas, allDeltas; + + // Bail out if the server has a newer format version than we can parse + if (status.formatVersion > STORAGE_FORMAT_VERSION) { + this._log.error("Server uses storage format v" + status.formatVersion + + ", this client understands up to v" + STORAGE_FORMAT_VERSION); generatorDone(this, onComplete, ret) - return; + throw 'close generator'; } - snap = eval(snapResp.target.responseText); - - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - let deltasResp = yield; - if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { - this._log.error("Could not download server deltas"); + + if (status.GUID != this._snapshotGUID) { + this._log.info("Remote/local sync GUIDs do not match. " + + "Forcing initial sync."); + this._resetGUIDs(); + this._snapshot = {}; + this._snapshotVersion = -1; + this._snapshotGUID = status.GUID; + } + + if (this._snapshotVersion < status.snapVersion) { + if (this._snapshotVersion >= 0) + this._log.info("Local snapshot is out of date"); + + this._log.info("Downloading server snapshot"); + this._dav.GET("bookmarks-snapshot.json", cont); + let snapResp = yield; + if (snapResp.target.status < 200 || snapResp.target.status >= 300) { + this._log.error("Could not download server snapshot"); + generatorDone(this, onComplete, ret) + throw 'close generator'; + } + snap = eval(snapResp.target.responseText); + + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + let deltasResp = yield; + if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { + this._log.error("Could not download server deltas"); + generatorDone(this, onComplete, ret) + throw 'close generator'; + } + allDeltas = eval(deltasResp.target.responseText); + deltas = eval(uneval(allDeltas)); + + } else if (this._snapshotVersion >= status.snapVersion && + this._snapshotVersion < status.maxVersion) { + snap = eval(uneval(this._snapshot)); + + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + let deltasResp = yield; + if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { + this._log.error("Could not download server deltas"); + generatorDone(this, onComplete, ret) + throw 'close generator'; + } + allDeltas = eval(deltasResp.target.responseText); + deltas = allDeltas.slice(this._snapshotVersion - status.snapVersion); + + } else if (this._snapshotVersion == status.maxVersion) { + snap = eval(uneval(this._snapshot)); + + // FIXME: could optimize this case by caching deltas file + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + let deltasResp = yield; + if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { + this._log.error("Could not download server deltas"); + generatorDone(this, onComplete, ret) + throw 'close generator'; + } + allDeltas = eval(deltasResp.target.responseText); + deltas = []; + + } else { // this._snapshotVersion > status.maxVersion + this._log.error("Server snapshot is older than local snapshot"); generatorDone(this, onComplete, ret) - return; + throw 'close generator'; } - allDeltas = eval(deltasResp.target.responseText); - deltas = eval(uneval(allDeltas)); - - } else if (this._snapshotVersion >= status.snapVersion && - this._snapshotVersion < status.maxVersion) { - snap = eval(uneval(this._snapshot)); - - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - let deltasResp = yield; - if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { - this._log.error("Could not download server deltas"); + + for (var i = 0; i < deltas.length; i++) { + snap = this._applyCommandsToObj(deltas[i], snap); + } + + ret.status = 0; + ret.formatVersion = status.formatVersion; + ret.maxVersion = status.maxVersion; + ret.snapVersion = status.snapVersion; + ret.snapshot = snap; + ret.deltas = allDeltas; + ret.updates = this._detectUpdates(this._snapshot, snap); + break; + + case 404: + this._log.info("Server has no status file, Initial upload to server"); + + this._snapshot = this._getBookmarks(); + this._snapshotVersion = 0; + this._snapshotGUID = null; // in case there are other snapshots out there + + this._dav.PUT("bookmarks-snapshot.json", + this._mungeNodes(this._snapshot), cont); + let snapPut = yield; + if (snapPut.target.status < 200 || snapPut.target.status >= 300) { + this._log.error("Could not upload snapshot to server, error code: " + + snapPut.target.status); generatorDone(this, onComplete, ret) - return; + throw 'close generator'; } - allDeltas = eval(deltasResp.target.responseText); - deltas = allDeltas.slice(this._snapshotVersion - status.snapVersion); - - } else if (this._snapshotVersion == status.maxVersion) { - snap = eval(uneval(this._snapshot)); - - // FIXME: could optimize this case by caching deltas file - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - let deltasResp = yield; - if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { - this._log.error("Could not download server deltas"); + + this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); + let deltasPut = yield; + if (deltasPut.target.status < 200 || deltasPut.target.status >= 300) { + this._log.error("Could not upload deltas to server, error code: " + + deltasPut.target.status); generatorDone(this, onComplete, ret) - return; + throw 'close generator'; } - allDeltas = eval(deltasResp.target.responseText); - deltas = []; - - } else { // this._snapshotVersion > status.maxVersion - this._log.error("Server snapshot is older than local snapshot"); - generatorDone(this, onComplete, ret) - return; + + this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshotGUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshotVersion, + maxVersion: this._snapshotVersion}), cont); + let statusPut = yield; + if (statusPut.target.status < 200 || statusPut.target.status >= 300) { + this._log.error("Could not upload status file to server, error code: " + + statusPut.target.status); + generatorDone(this, onComplete, ret) + throw 'close generator'; + } + + this._log.info("Initial upload to server successful"); + this._saveSnapshot(); + + ret.status = 0; + ret.formatVersion = STORAGE_FORMAT_VERSION; + ret.maxVersion = this._snapshotVersion; + ret.snapVersion = this._snapshotVersion; + ret.snapshot = eval(uneval(this._snapshot)); + ret.deltas = []; + ret.updates = []; + break; + + default: + this._log.error("Could not get bookmarks.status: unknown HTTP status code " + + statusResp.target.status); + break; } - - for (var i = 0; i < deltas.length; i++) { - snap = this._applyCommandsToObj(deltas[i], snap); - } - - ret.status = 0; - ret.formatVersion = status.formatVersion; - ret.maxVersion = status.maxVersion; - ret.snapVersion = status.snapVersion; - ret.snapshot = snap; - ret.deltas = allDeltas; - ret.updates = this._detectUpdates(this._snapshot, snap); - break; - - case 404: - this._log.info("Server has no status file, Initial upload to server"); - - this._snapshot = this._getBookmarks(); - this._snapshotVersion = 0; - this._snapshotGUID = null; // in case there are other snapshots out there - - this._dav.PUT("bookmarks-snapshot.json", - this._mungeNodes(this._snapshot), cont); - let snapPut = yield; - if (snapPut.target.status < 200 || snapPut.target.status >= 300) { - this._log.error("Could not upload snapshot to server, error code: " + - snapPut.target.status); - generatorDone(this, onComplete, ret) - return; - } - - this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); - let deltasPut = yield; - if (deltasPut.target.status < 200 || deltasPut.target.status >= 300) { - this._log.error("Could not upload deltas to server, error code: " + - deltasPut.target.status); - generatorDone(this, onComplete, ret) - return; - } - - this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshotVersion, - maxVersion: this._snapshotVersion}), cont); - let statusPut = yield; - if (statusPut.target.status < 200 || statusPut.target.status >= 300) { - this._log.error("Could not upload status file to server, error code: " + - statusPut.target.status); - generatorDone(this, onComplete, ret) - return; - } - - this._log.info("Initial upload to server successful"); - this._saveSnapshot(); - - ret.status = 0; - ret.formatVersion = STORAGE_FORMAT_VERSION; - ret.maxVersion = this._snapshotVersion; - ret.snapVersion = this._snapshotVersion; - ret.snapshot = eval(uneval(this._snapshot)); - ret.deltas = []; - ret.updates = []; - break; - - default: - this._log.error("Could not get bookmarks.status: unknown HTTP status code " + - statusResp.target.status); - break; + } catch (e) { + if (e != 'close generator') + throw e; + } finally { + generatorDone(this, onComplete, ret) + yield; // onComplete is responsible for closing the generator } - generatorDone(this, onComplete, ret) + this._log.warn("generator not properly closed"); }, _onLogin: function BSS__onLogin(success) { + this._loginGen.close(); + this._loginGen = null; if (success) { this._log.info("Logged in"); this._log.info("Bookmarks sync server: " + this._dav.userURL); @@ -1316,6 +1326,8 @@ BookmarksSyncService.prototype = { }, _onResetLock: function BSS__resetLock(success) { + this._forceUnlockGen.close(); + this._forceUnlockGen = null; if (success) { this._log.info("Lock reset"); this._os.notifyObservers(null, "bookmarks-sync:lock-reset", ""); @@ -1325,6 +1337,11 @@ BookmarksSyncService.prototype = { } }, + _onResetServer: function BSS__resetServer(success) { + this._resetServerGen.close(); + this._resetServerGen = null; + }, + _resetServer: function BSS__resetServer(onComplete) { let cont = yield; let done = false; @@ -1332,9 +1349,9 @@ BookmarksSyncService.prototype = { try { this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", ""); - if (!this._dav.lock.async(this._dav, cont)) - return; + let gen = this._dav.lock.async(this._dav, cont); let locked = yield; + gen.close(); if (locked) this._log.info("Lock acquired"); @@ -1350,13 +1367,9 @@ BookmarksSyncService.prototype = { this._dav.DELETE("bookmarks-deltas.json", cont); let deltasResp = yield; - if (this._dav.unlock.async(this._dav, cont)) { - let unlocked = yield; - if (unlocked) - this._log.info("Lock released"); - else - this._log.error("Could not unlock DAV collection"); - } + gen = this._dav.unlock.async(this._dav, cont); + let unlocked = yield; + gen.close(); if (!(statusResp.target.status == 200 || statusResp.target.status == 404 || snapshotResp.target.status == 200 || snapshotResp.target.status == 404 || @@ -1369,20 +1382,25 @@ BookmarksSyncService.prototype = { } this._log.info("Server files deleted, starting sync"); - if (!this._doSync.async(this, cont)) - return; + gen = this._doSync.async(this, cont); done = yield; + gen.close(); + } catch (e) { + if (e != 'close generator') + throw e; } finally { - if (done) + if (done) { this._log.info("Server reset completed successfully"); - else + this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); + } else { this._log.info("Server reset failed: could not sync bookmarks"); - - this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); - if (onComplete) - generatorDone(this, onComplete, done) + this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", ""); + } + generatorDone(this, onComplete, done) + yield; // onComplete is responsible for closing the generator } + this._log.warn("generator not properly closed"); }, // XPCOM registration @@ -1435,19 +1453,14 @@ BookmarksSyncService.prototype = { sync: function BSS_sync() { this._log.info("Beginning sync"); - if (!this._doSync.async(this)) { - this._log.fatal("Could not start sync operation"); - this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); - } + let callback = bind2(this, this._onSyncFinished); + this._syncGen = this._doSync.async(this, callback); }, login: function BSS_login() { this._log.info("Logging in"); let callback = bind2(this, this._onLogin); - if (!this._dav.login.async(this._dav, callback)) { - this._log.fatal("Could not start login operation"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - } + this._loginGen = this._dav.login.async(this._dav, callback); }, logout: function BSS_logout() { @@ -1458,12 +1471,14 @@ BookmarksSyncService.prototype = { resetLock: function BSS_resetLock() { this._log.info("Resetting lock"); - this._dav.forceUnlock.async(this._dav, bind2(this, this._onResetLock)); + let callback = bind2(this, this._onResetLock); + this._forceUnlockGen = this._dav.forceUnlock.async(this._dav, callback); }, resetServer: function BSS_resetServer() { this._log.info("Resetting server data"); - this._resetServer.async(this); + let callback = bind2(this, this._onResetServer); + this._resetServerGen = this._resetServer.async(this, callback); } }; @@ -1484,27 +1499,27 @@ function bind2(object, method) { } Function.prototype.async = function(self, extra_args) { - let args = Array.prototype.slice.call(arguments, 1); - let ret = false; try { + let args = Array.prototype.slice.call(arguments, 1); let gen = this.apply(self, args); gen.next(); // must initialize before sending gen.send(function(data) { continueGenerator(gen, data); }); - ret = true; + return gen; } catch (e) { - if (e instanceof StopIteration) - dump("Warning: generator stopped unexpectedly"); - else + if (e instanceof StopIteration) { + dump("async warning: generator stopped unexpectedly"); + return null; + } else { throw e; + } } - return ret; } function continueGenerator(generator, data) { try { generator.send(data); } catch (e) { if (e instanceof StopIteration) - generator = null; + dump("continueGenerator warning: generator stopped unexpectedly"); else throw e; } @@ -1652,7 +1667,7 @@ DAVCollection.prototype = { }, _makeRequest: function DC__makeRequest(op, path, onComplete, headers) { - this._log.debug("Creating request, url=" + this._userURL + path); + this._log.debug("Creating " + op + " request for" + this._userURL + path); let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); @@ -1711,11 +1726,10 @@ DAVCollection.prototype = { login: function DC_login(onComplete) { let cont = yield; - try { if (this._loggedIn) { this._log.debug("Login requested, but already logged in"); - return; + throw 'close generator'; } let headers = {}; @@ -1732,11 +1746,11 @@ DAVCollection.prototype = { if (this._authProvider._authFailed) { this._log.warn("Login authentication failed"); - return; + throw 'close generator'; } - if (event.target.status >= 400) { + if (event.target.status < 200 || event.target.status >= 400) { this._log.warn("Login request failed, status " + event.target.status); - return; + throw 'close generator'; } let branch = Cc["@mozilla.org/preferences-service;1"]. @@ -1749,9 +1763,14 @@ DAVCollection.prototype = { this._userURL = this._baseURL + "user/" + this._currentUserPath + "/"; this._loggedIn = true; + } catch (e) { + if (e != 'close generator') + throw e; } finally { generatorDone(this, onComplete, this._loggedIn); + yield; // onComplete is responsible for closing the generator } + this._log.warn("generator not properly closed"); }, logout: function DC_logout() { @@ -1766,9 +1785,9 @@ DAVCollection.prototype = { let cont = yield; let ret = null; - this._log.info("Getting active lock token"); - try { + this._log.info("Getting active lock token"); + let headers = {'Content-Type': 'text/xml; charset="utf-8"', 'Depth': '0'}; if (this._auth) @@ -1783,31 +1802,36 @@ DAVCollection.prototype = { if (this._authProvider._authFailed) { this._log.warn("_getActiveLock: authentication failed"); - return; + throw 'close generator'; } if (event.target.status >= 400) { this._log.warn("_getActiveLock: got status " + event.target.status); - return; + throw 'close generator'; } let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); ret = token.textContent; + } catch (e) { + if (e != 'close generator') + throw e; } finally { generatorDone(this, onComplete, ret); + yield; // onComplete is responsible for closing the generator } + this._log.warn("generator not properly closed"); }, lock: function DC_lock(onComplete) { let cont = yield; this._token = null; - this._log.info("Acquiring lock"); - try { + this._log.info("Acquiring lock"); + if (this._token) { this._log.debug("Lock called, but we already hold a token"); - return; + throw 'close generator'; } headers = {'Content-Type': 'text/xml; charset="utf-8"', @@ -1826,11 +1850,11 @@ DAVCollection.prototype = { if (this._authProvider._authFailed) { this._log.warn("lock: authentication failed"); - return; + throw 'close generator'; } if (event.target.status < 200 || event.target.status >= 300) { this._log.warn("lock: got status " + event.target.status); - return; + throw 'close generator'; } let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); @@ -1838,20 +1862,24 @@ DAVCollection.prototype = { if (token) this._token = token.textContent; + } catch (e){ + if (e != 'close generator') + throw e; } finally { generatorDone(this, onComplete, this._token); + yield; // onComplete is responsible for closing the generator } + this._log.warn("generator not properly closed"); }, unlock: function DC_unlock(onComplete) { let cont = yield; - - this._log.info("Releasing lock"); - try { + this._log.info("Releasing lock"); + if (!this._token) { this._log.debug("Unlock called, but we don't hold a token right now"); - return; + throw 'close generator'; } let headers = {'Lock-Token': "<" + this._token + ">"}; @@ -1864,20 +1892,28 @@ DAVCollection.prototype = { if (this._authProvider._authFailed) { this._log.warn("unlock: authentication failed"); - return; + throw 'close generator'; } if (event.target.status < 200 || event.target.status >= 300) { this._log.warn("unlock: got status " + event.target.status); - return; + throw 'close generator'; } this._token = null; + } catch (e){ + if (e != 'close generator') + throw e; } finally { - if (this._token) + if (this._token) { + this._log.info("Could not release lock"); generatorDone(this, onComplete, false); - else + } else { + this._log.info("Lock released"); generatorDone(this, onComplete, true); + } + yield; // onComplete is responsible for closing the generator } + this._log.warn("generator not properly closed"); }, forceUnlock: function DC_forceUnlock(onComplete) { @@ -1885,18 +1921,24 @@ DAVCollection.prototype = { let unlocked = true; try { - if (!this._getActiveLock.async(this, cont)) - return; + + let gen = this._getActiveLock.async(this, cont); this._token = yield; + gen.close(); if (this._token) { - if (!this.unlock.async(this, cont)) - return; + gen = this.unlock.async(this, cont); unlocked = yield; + gen.close(); } + } catch (e){ + if (e != 'close generator') + throw e; } finally { generatorDone(this, onComplete, unlocked); + yield; // onComplete is responsible for closing the generator } + this._log.warn("generator not properly closed"); }, stealLock: function DC_stealLock(onComplete) { @@ -1904,15 +1946,24 @@ DAVCollection.prototype = { let stolen = null; try { - if (!this.forceUnlock.async(this, cont)) - return; - let unlocked = yield; - if (unlocked && this.lock.async(this, cont)) + let gen = this.forceUnlock.async(this, cont); + let unlocked = yield; + gen.close(); + + if (unlocked) { + gen = this.lock.async(this, cont); stolen = yield; + gen.close(); + } + } catch (e){ + if (e != 'close generator') + throw e; } finally { generatorDone(this, onComplete, stolen); + yield; // onComplete is responsible for closing the generator } + this._log.warn("generator not properly closed"); } }; From b4c406fdc78881b0cf92189842631cbbb5d01da8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 18 Oct 2007 03:13:35 -0700 Subject: [PATCH 0069/1860] DAVCollection refactoring --- services/sync/BookmarksSyncService.js | 389 ++++++++++++++------------ 1 file changed, 215 insertions(+), 174 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 382794538b2a..fb6b422ab0c9 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1037,21 +1037,23 @@ BookmarksSyncService.prototype = { server.deltas.push(serverDelta); - this._dav.PUT("bookmarks-deltas.json", - this._mungeCommands(server.deltas), cont); + gen = this._dav.PUT("bookmarks-deltas.json", + this._mungeCommands(server.deltas), cont); let deltasPut = yield; + gen.close(); // FIXME: need to watch out for the storage format version changing, // in that case we'll have to re-upload all the files, not just these - this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshotVersion}), cont); + gen = this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshotGUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshotVersion}), cont); let statusPut = yield; + gen.close(); - if (deltasPut.target.status >= 200 && deltasPut.target.status < 300 && - statusPut.target.status >= 200 && statusPut.target.status < 300) { + if (deltasPut.status >= 200 && deltasPut.status < 300 && + statusPut.status >= 200 && statusPut.status < 300) { this._log.info("Successfully updated deltas and status on server"); this._saveSnapshot(); } else { @@ -1145,14 +1147,16 @@ BookmarksSyncService.prototype = { try { this._log.info("Getting bookmarks status from server"); - this._dav.GET("bookmarks-status.json", cont); - let statusResp = yield; + let gen = this._dav.GET("bookmarks-status.json", cont); + let resp = yield; + let status = resp.status; + gen.close(); - switch (statusResp.target.status) { + switch (status) { case 200: this._log.info("Got bookmarks status from server"); - let status = eval(statusResp.target.responseText); + let status = eval(resp.responseText); let snap, deltas, allDeltas; // Bail out if the server has a newer format version than we can parse @@ -1177,24 +1181,28 @@ BookmarksSyncService.prototype = { this._log.info("Local snapshot is out of date"); this._log.info("Downloading server snapshot"); - this._dav.GET("bookmarks-snapshot.json", cont); - let snapResp = yield; - if (snapResp.target.status < 200 || snapResp.target.status >= 300) { + gen = this._dav.GET("bookmarks-snapshot.json", cont); + resp = yield; + gen.close() + + if (resp.status < 200 || resp.status >= 300) { this._log.error("Could not download server snapshot"); generatorDone(this, onComplete, ret) throw 'close generator'; } - snap = eval(snapResp.target.responseText); + snap = eval(resp.responseText); this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - let deltasResp = yield; - if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { + gen = this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + gen.close(); + + if (resp.status < 200 || resp.status >= 300) { this._log.error("Could not download server deltas"); generatorDone(this, onComplete, ret) throw 'close generator'; } - allDeltas = eval(deltasResp.target.responseText); + allDeltas = eval(resp.responseText); deltas = eval(uneval(allDeltas)); } else if (this._snapshotVersion >= status.snapVersion && @@ -1202,14 +1210,16 @@ BookmarksSyncService.prototype = { snap = eval(uneval(this._snapshot)); this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - let deltasResp = yield; - if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { + gen = this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + gen.close(); + + if (resp.status < 200 || resp.status >= 300) { this._log.error("Could not download server deltas"); generatorDone(this, onComplete, ret) throw 'close generator'; } - allDeltas = eval(deltasResp.target.responseText); + allDeltas = eval(resp.responseText); deltas = allDeltas.slice(this._snapshotVersion - status.snapVersion); } else if (this._snapshotVersion == status.maxVersion) { @@ -1217,14 +1227,16 @@ BookmarksSyncService.prototype = { // FIXME: could optimize this case by caching deltas file this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - let deltasResp = yield; - if (deltasResp.target.status < 200 || deltasResp.target.status >= 300) { + gen = this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + gen.close(); + + if (resp.status < 200 || resp.status >= 300) { this._log.error("Could not download server deltas"); generatorDone(this, onComplete, ret) throw 'close generator'; } - allDeltas = eval(deltasResp.target.responseText); + allDeltas = eval(resp.responseText); deltas = []; } else { // this._snapshotVersion > status.maxVersion @@ -1253,34 +1265,40 @@ BookmarksSyncService.prototype = { this._snapshotVersion = 0; this._snapshotGUID = null; // in case there are other snapshots out there - this._dav.PUT("bookmarks-snapshot.json", - this._mungeNodes(this._snapshot), cont); - let snapPut = yield; - if (snapPut.target.status < 200 || snapPut.target.status >= 300) { + gen = this._dav.PUT("bookmarks-snapshot.json", + this._mungeNodes(this._snapshot), cont); + resp = yield; + gen.close(); + + if (resp.status < 200 || resp.status >= 300) { this._log.error("Could not upload snapshot to server, error code: " + - snapPut.target.status); + resp.status); generatorDone(this, onComplete, ret) throw 'close generator'; } - this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); - let deltasPut = yield; - if (deltasPut.target.status < 200 || deltasPut.target.status >= 300) { + gen = this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); + resp = yield; + gen.close(); + + if (resp.status < 200 || resp.status >= 300) { this._log.error("Could not upload deltas to server, error code: " + - deltasPut.target.status); + resp.status); generatorDone(this, onComplete, ret) throw 'close generator'; } - this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshotVersion, - maxVersion: this._snapshotVersion}), cont); - let statusPut = yield; - if (statusPut.target.status < 200 || statusPut.target.status >= 300) { + gen = this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshotGUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshotVersion, + maxVersion: this._snapshotVersion}), cont); + resp = yield; + gen.close(); + + if (resp.status < 200 || resp.status >= 300) { this._log.error("Could not upload status file to server, error code: " + - statusPut.target.status); + resp.status); generatorDone(this, onComplete, ret) throw 'close generator'; } @@ -1299,7 +1317,7 @@ BookmarksSyncService.prototype = { default: this._log.error("Could not get bookmarks.status: unknown HTTP status code " + - statusResp.target.status); + status); break; } } catch (e) { @@ -1360,24 +1378,26 @@ BookmarksSyncService.prototype = { return; } - this._dav.DELETE("bookmarks-status.json", cont); + gen = this._dav.DELETE("bookmarks-status.json", cont); let statusResp = yield; - this._dav.DELETE("bookmarks-snapshot.json", cont); + gen.close(); + gen = this._dav.DELETE("bookmarks-snapshot.json", cont); let snapshotResp = yield; - this._dav.DELETE("bookmarks-deltas.json", cont); + gen.close(); + gen = this._dav.DELETE("bookmarks-deltas.json", cont); let deltasResp = yield; + gen.close(); gen = this._dav.unlock.async(this._dav, cont); let unlocked = yield; gen.close(); - if (!(statusResp.target.status == 200 || statusResp.target.status == 404 || - snapshotResp.target.status == 200 || snapshotResp.target.status == 404 || - deltasResp.target.status == 200 || deltasResp.target.status == 404)) { + if (!(statusResp.status == 200 || statusResp.status == 404 || + snapshotResp.status == 200 || snapshotResp.status == 404 || + deltasResp.status == 200 || deltasResp.status == 404)) { this._log.error("Could delete server data, response codes " + - statusResp.target.status + ", " + - snapshotResp.target.status + ", " + - deltasResp.target.status); + statusResp.status + ", " + snapshotResp.status + ", " + + deltasResp.status); return; } @@ -1608,9 +1628,9 @@ DAVCollection.prototype = { if (this.__auth && this._userURL == this.__authURI) return this.__auth; - this._log.debug("Generating new authentication header"); - try { + this._log.debug("Generating new authentication header"); + this.__authURI = this._userURL; let URI = makeURI(this._userURL); let username = 'nobody@mozilla.com'; @@ -1666,92 +1686,115 @@ DAVCollection.prototype = { return this._currentUser; }, - _makeRequest: function DC__makeRequest(op, path, onComplete, headers) { - this._log.debug("Creating " + op + " request for" + this._userURL + path); + _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { + let cont = yield; + let ret; - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); - request = request.QueryInterface(Ci.nsIDOMEventTarget); + try { + this._log.debug("Creating " + op + " request for " + this._userURL + path); - request.addEventListener("load", new EventListener(onComplete), false); - request.addEventListener("error", new EventListener(onComplete), false); - request = request.QueryInterface(Ci.nsIXMLHttpRequest); - request.open(op, this._userURL + path, true); - - if (headers) { + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); + request = request.QueryInterface(Ci.nsIDOMEventTarget); + + request.addEventListener("load", new EventListener(cont), false); + request.addEventListener("error", new EventListener(cont), false); + request = request.QueryInterface(Ci.nsIXMLHttpRequest); + request.open(op, this._userURL + path, true); + let key; for (key in headers) { + this._log.debug("HTTP Header " + key + ": " + headers[key]); request.setRequestHeader(key, headers[key]); } + + this._authProvider._authFailed = false; + request.channel.notificationCallbacks = this._authProvider; + + request.send(data); + let event = yield; + ret = event.target; + + if (this._authProvider._authFailed) + this._log.warn("_makeRequest: authentication failed"); + if (ret.status < 200 || ret.status >= 300) + this._log.warn("_makeRequest: got status " + ret.status); + + } catch (e) { + if (e != 'close generator') + throw e; + + } finally { + generatorDone(this, onComplete, ret); + yield; // onComplete is responsible for closing the generator } - - request.channel.notificationCallbacks = this._authProvider; - - return request; + this._log.warn("generator not properly closed"); }, - GET: function DC_GET(path, onComplete, headers) { - if (!headers) - headers = {'Content-type': 'text/plain'}; - if (this._auth) - headers['Authorization'] = this._auth; - if (this._token) - headers['If'] = "<" + this._bashURL + "> (<" + this._token + ">)"; - let request = this._makeRequest("GET", path, onComplete, headers); - request.send(null); + get _defaultHeaders() { + return {'Authorization': this._auth? this._auth : '', + 'Content-type': 'text/plain', + 'If': this._token? + "<" + this._userURL + "> (<" + this._token + ">)" : ''}; }, - PUT: function DC_PUT(path, data, onComplete, headers) { - if (!headers) - headers = {'Content-type': 'text/plain'}; - if (this._auth) - headers['Authorization'] = this._auth; - if (this._token) - headers['If'] = "<" + this._bashURL + "> (<" + this._token + ">)"; - let request = this._makeRequest("PUT", path, onComplete, headers); - request.send(data); + GET: function DC_GET(path, onComplete) { + return this._makeRequest.async(this, onComplete, "GET", path, + this._defaultHeaders); }, - DELETE: function DC_DELETE(path, onComplete, headers) { - if (!headers) - headers = {'Content-type': 'text/plain'}; - if (this._auth) - headers['Authorization'] = this._auth; - if (this._token) - headers['If'] = "<" + this._bashURL + "> (<" + this._token + ">)"; - let request = this._makeRequest("DELETE", path, onComplete, headers); - request.send(null); + PUT: function DC_PUT(path, data, onComplete) { + return this._makeRequest.async(this, onComplete, "PUT", path, + this._defaultHeaders, data); + }, + + DELETE: function DC_DELETE(path, onComplete) { + return this._makeRequest.async(this, onComplete, "DELETE", path, + this._defaultHeaders); + }, + + PROPFIND: function DC_PROPFIND(path, data, onComplete) { + let headers = {'Content-type': 'text/xml; charset="utf-8"', + 'Depth': '0'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "PROPFIND", path, + headers, data); + }, + + LOCK: function DC_LOCK(path, data, onComplete) { + let headers = {'Content-type': 'text/xml; charset="utf-8"', + 'Depth': 'infinity', + 'Timeout': 'Second-600'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "LOCK", path, headers, data); + }, + + UNLOCK: function DC_UNLOCK(path, onComplete) { + let headers = {'Lock-Token': '<' + this._token + '>'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers); }, // Login / Logout login: function DC_login(onComplete) { let cont = yield; + try { if (this._loggedIn) { this._log.debug("Login requested, but already logged in"); throw 'close generator'; } - - let headers = {}; - if (this._auth) - headers['Authorization'] = this._auth; - this._authProvider._authFailed = false; + this._log.info("Logging in"); // This ensures the auth header is correct, and it doubles as an // account creation request - let request = this._makeRequest("GET", "createAcct.php", cont, headers); - request.send(null); - let event = yield; + let gen = this.GET("createAcct.php", cont); + let resp = yield; + gen.close(); - if (this._authProvider._authFailed) { - this._log.warn("Login authentication failed"); + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) throw 'close generator'; - } - if (event.target.status < 200 || event.target.status >= 400) { - this._log.warn("Login request failed, status " + event.target.status); - throw 'close generator'; - } let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); @@ -1766,7 +1809,12 @@ DAVCollection.prototype = { } catch (e) { if (e != 'close generator') throw e; + } finally { + if (this._loggedIn) + this._log.info("Logged in"); + else + this._log.warn("Could not log in"); generatorDone(this, onComplete, this._loggedIn); yield; // onComplete is responsible for closing the generator } @@ -1787,35 +1835,30 @@ DAVCollection.prototype = { try { this._log.info("Getting active lock token"); + let gen = this.PROPFIND("", + "" + + "" + + " " + + "", cont); + let resp = yield; + gen.close(); - let headers = {'Content-Type': 'text/xml; charset="utf-8"', - 'Depth': '0'}; - if (this._auth) - headers['Authorization'] = this._auth; - - let request = this._makeRequest("PROPFIND", "", cont, headers); - request.send("" + - "" + - " " + - ""); - let event = yield; - - if (this._authProvider._authFailed) { - this._log.warn("_getActiveLock: authentication failed"); + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) throw 'close generator'; - } - if (event.target.status >= 400) { - this._log.warn("_getActiveLock: got status " + event.target.status); - throw 'close generator'; - } - let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); + let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); ret = token.textContent; + } catch (e) { if (e != 'close generator') throw e; + } finally { + if (ret) + this._log.debug("Found an active lock token"); + else + this._log.debug("No active lock token found"); generatorDone(this, onComplete, ret); yield; // onComplete is responsible for closing the generator } @@ -1834,30 +1877,19 @@ DAVCollection.prototype = { throw 'close generator'; } - headers = {'Content-Type': 'text/xml; charset="utf-8"', - 'Timeout': 'Second-600', - 'Depth': 'infinity'}; - if (this._auth) - headers['Authorization'] = this._auth; + let gen = this.LOCK("", + "\n" + + "\n" + + " \n" + + " \n" + + "", cont); + let resp = yield; + gen.close(); - let request = this._makeRequest("LOCK", "", cont, headers); - request.send("\n" + - "\n" + - " \n" + - " \n" + - ""); - let event = yield; - - if (this._authProvider._authFailed) { - this._log.warn("lock: authentication failed"); + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) throw 'close generator'; - } - if (event.target.status < 200 || event.target.status >= 300) { - this._log.warn("lock: got status " + event.target.status); - throw 'close generator'; - } - let tokens = xpath(event.target.responseXML, '//D:locktoken/D:href'); + let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); if (token) this._token = token.textContent; @@ -1865,7 +1897,12 @@ DAVCollection.prototype = { } catch (e){ if (e != 'close generator') throw e; + } finally { + if (this._token) + this._log.info("Lock acquired"); + else + this._log.warn("Could not acquire lock"); generatorDone(this, onComplete, this._token); yield; // onComplete is responsible for closing the generator } @@ -1882,27 +1919,19 @@ DAVCollection.prototype = { throw 'close generator'; } - let headers = {'Lock-Token': "<" + this._token + ">"}; - if (this._auth) - headers['Authorization'] = this._auth; + let gen = this.UNLOCK("", cont); + let resp = yield; + gen.close(); - let request = this._makeRequest("UNLOCK", "", cont, headers); - request.send(null); - let event = yield; - - if (this._authProvider._authFailed) { - this._log.warn("unlock: authentication failed"); + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) throw 'close generator'; - } - if (event.target.status < 200 || event.target.status >= 300) { - this._log.warn("unlock: got status " + event.target.status); - throw 'close generator'; - } this._token = null; + } catch (e){ if (e != 'close generator') throw e; + } finally { if (this._token) { this._log.info("Could not release lock"); @@ -1921,20 +1950,31 @@ DAVCollection.prototype = { let unlocked = true; try { + this._log.info("Forcibly releasing any server locks"); let gen = this._getActiveLock.async(this, cont); this._token = yield; gen.close(); - if (this._token) { - gen = this.unlock.async(this, cont); - unlocked = yield; - gen.close(); + if (!this._token) { + this._log.info("No server lock found"); + throw 'close generator'; } + + this._log.info("Server lock found, unlocking"); + gen = this.unlock.async(this, cont); + unlocked = yield; + gen.close(); + } catch (e){ if (e != 'close generator') throw e; + } finally { + if (unlocked) + this._log.debug("Lock released"); + else + this._log.debug("No lock released"); generatorDone(this, onComplete, unlocked); yield; // onComplete is responsible for closing the generator } @@ -1946,7 +1986,6 @@ DAVCollection.prototype = { let stolen = null; try { - let gen = this.forceUnlock.async(this, cont); let unlocked = yield; gen.close(); @@ -1956,9 +1995,11 @@ DAVCollection.prototype = { stolen = yield; gen.close(); } + } catch (e){ if (e != 'close generator') throw e; + } finally { generatorDone(this, onComplete, stolen); yield; // onComplete is responsible for closing the generator From a49f8d03f9f6ff7fc853dbbe8764c791b7ac1374 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 18 Oct 2007 10:00:09 -0700 Subject: [PATCH 0070/1860] Fix for logout/re-login (no, really!) --- services/sync/BookmarksSyncService.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index fb6b422ab0c9..b8aff31de566 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1622,10 +1622,9 @@ DAVCollection.prototype = { }, // FIXME: should we regen this each time to prevent it from staying in memory? - __authURI: null, __auth: null, get _auth() { - if (this.__auth && this._userURL == this.__authURI) + if (this.__auth) return this.__auth; try { @@ -1787,6 +1786,8 @@ DAVCollection.prototype = { this._log.info("Logging in"); + this._userURL = this._baseURL; // for createAcct.php + // This ensures the auth header is correct, and it doubles as an // account creation request let gen = this.GET("createAcct.php", cont); From c337be7d6c9d5808fae71b319b765d54f7d7dc66 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 18 Oct 2007 20:29:17 -0700 Subject: [PATCH 0071/1860] better check for whether we have a token; log event handler events --- services/sync/BookmarksSyncService.js | 38 ++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index b8aff31de566..01b7dbac9cd2 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1334,11 +1334,9 @@ BookmarksSyncService.prototype = { this._loginGen.close(); this._loginGen = null; if (success) { - this._log.info("Logged in"); this._log.info("Bookmarks sync server: " + this._dav.userURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); } else { - this._log.info("Could not log in"); this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); } }, @@ -1423,6 +1421,20 @@ BookmarksSyncService.prototype = { this._log.warn("generator not properly closed"); }, + _resetClient: function BSS__resetClient(onComplete) { + let cont = yield; + + try { + + } catch (e) { + if (e != 'close generator') + throw e; + + } finally { + generatorDone(this, onComplete, null); + } + }, + // XPCOM registration classDescription: "Bookmarks Sync Service", contractID: "@mozilla.org/places/sync-service;1", @@ -1499,6 +1511,12 @@ BookmarksSyncService.prototype = { this._log.info("Resetting server data"); let callback = bind2(this, this._onResetServer); this._resetServerGen = this._resetServer.async(this, callback); + }, + + resetClient: function BSS_resetClient() { + this._log.info("Resetting client data"); + let callback = bind2(this, this._onResetClient); + this._resetClientGen = this._resetClient.async(this, callback); } }; @@ -1565,19 +1583,25 @@ function generatorDone(object, callback, retval) { 0, object._timer.TYPE_ONE_SHOT); } -function EventListener(handler) { +function EventListener(handler, eventName) { this._handler = handler; + this._eventName = eventName; + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service.EventHandler"); } EventListener.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), // DOM event listener handleEvent: function EL_handleEvent(event) { + this._log.debug("Handling event " + this._eventName); this._handler(event); }, // nsITimerCallback notify: function EL_notify(timer) { + this._log.trace("Timer fired"); this._handler(timer); } }; @@ -1695,8 +1719,8 @@ DAVCollection.prototype = { let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); - request.addEventListener("load", new EventListener(cont), false); - request.addEventListener("error", new EventListener(cont), false); + request.addEventListener("load", new EventListener(cont, "load"), false); + request.addEventListener("error", new EventListener(cont, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); request.open(op, this._userURL + path, true); @@ -1915,7 +1939,7 @@ DAVCollection.prototype = { try { this._log.info("Releasing lock"); - if (!this._token) { + if (this._token === null) { this._log.debug("Unlock called, but we don't hold a token right now"); throw 'close generator'; } @@ -1938,7 +1962,7 @@ DAVCollection.prototype = { this._log.info("Could not release lock"); generatorDone(this, onComplete, false); } else { - this._log.info("Lock released"); + this._log.info("Lock released (or we didn't have one)"); generatorDone(this, onComplete, true); } yield; // onComplete is responsible for closing the generator From 6be8eb8a50baff7fa3a6e97e3154f28ac7da1baf Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 19 Oct 2007 16:33:03 -0700 Subject: [PATCH 0072/1860] Add reset client button to prefs window; add support for queries (e.g. the new 'Places' folder); display errors for all exceptions caught; reset server/client no longer trigger an atomatic re-sync --- services/sync/BookmarksSyncService.js | 62 +++++++++++++++--------- services/sync/IBookmarksSyncService.idl | 7 ++- services/sync/IBookmarksSyncService.xpt | Bin 232 -> 252 bytes 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 01b7dbac9cd2..5cda2d51334e 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -334,11 +334,15 @@ BookmarksSyncService.prototype = { } } item.title = node.title; - } else if (node.type == node.RESULT_TYPE_URI) { + } else if (node.type == node.RESULT_TYPE_URI || + node.type == node.RESULT_TYPE_QUERY) { if (this._ms.hasMicrosummary(node.itemId)) { item.type = "microsummary"; let micsum = this._ms.getMicrosummary(node.itemId); - item.generatorURI = micsum.generator.uri.spec; // FIXME: might not be a remote generator! + item.generatorURI = micsum.generator.uri.spec; // breaks local generators + } else if (node.type == node.RESULT_TYPE_QUERY) { + item.type = "query"; + item.title = node.title; } else { item.type = "bookmark"; item.title = node.title; @@ -348,8 +352,6 @@ BookmarksSyncService.prototype = { item.keyword = this._bms.getKeywordForBookmark(node.itemId); } else if (node.type == node.RESULT_TYPE_SEPARATOR) { item.type = "separator"; - } else if (node.type == node.RESULT_TYPE_QUERY) { - item.type = "query"; } else { this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); return; @@ -607,9 +609,11 @@ BookmarksSyncService.prototype = { this._getPropagations(listB, conflicts[1], propagations[0]); ret = {propagations: propagations, conflicts: conflicts}; + } catch (e) { if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); + } finally { this._timer = null; generatorDone(this, onComplete, ret); @@ -685,6 +689,7 @@ BookmarksSyncService.prototype = { } switch (command.data.type) { + case "query": case "bookmark": case "microsummary": this._log.info(" -> creating bookmark \"" + command.data.title + "\""); @@ -1068,7 +1073,7 @@ BookmarksSyncService.prototype = { } catch (e) { if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { let ok = false; @@ -1320,9 +1325,11 @@ BookmarksSyncService.prototype = { status); break; } + } catch (e) { if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); + } finally { generatorDone(this, onComplete, ret) yield; // onComplete is responsible for closing the generator @@ -1399,20 +1406,19 @@ BookmarksSyncService.prototype = { return; } - this._log.info("Server files deleted, starting sync"); - gen = this._doSync.async(this, cont); - done = yield; - gen.close(); + this._log.info("Server files deleted"); + done = true; } catch (e) { if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); + } finally { if (done) { this._log.info("Server reset completed successfully"); this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); } else { - this._log.info("Server reset failed: could not sync bookmarks"); + this._log.info("Server reset failed"); this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", ""); } generatorDone(this, onComplete, done) @@ -1423,15 +1429,23 @@ BookmarksSyncService.prototype = { _resetClient: function BSS__resetClient(onComplete) { let cont = yield; + let done = false; try { + this._log.info("Resetting client state"); + this._snapshot = {}; + this._snapshotVersion = -1; + this._saveSnapshot(); + + this._log.info("Client snapshot cleared"); + done = true; } catch (e) { if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { - generatorDone(this, onComplete, null); + generatorDone(this, onComplete, done); } }, @@ -1548,7 +1562,7 @@ Function.prototype.async = function(self, extra_args) { dump("async warning: generator stopped unexpectedly"); return null; } else { - throw e; + this._log.error("Exception caught: " + e.message); } } } @@ -1559,7 +1573,7 @@ function continueGenerator(generator, data) { if (e instanceof StopIteration) dump("continueGenerator warning: generator stopped unexpectedly"); else - throw e; + this._log.error("Exception caught: " + e.message); } } @@ -1744,7 +1758,7 @@ DAVCollection.prototype = { } catch (e) { if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { generatorDone(this, onComplete, ret); @@ -1833,7 +1847,7 @@ DAVCollection.prototype = { } catch (e) { if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { if (this._loggedIn) @@ -1877,7 +1891,7 @@ DAVCollection.prototype = { } catch (e) { if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { if (ret) @@ -1921,7 +1935,7 @@ DAVCollection.prototype = { } catch (e){ if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { if (this._token) @@ -1955,7 +1969,7 @@ DAVCollection.prototype = { } catch (e){ if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { if (this._token) { @@ -1993,7 +2007,7 @@ DAVCollection.prototype = { } catch (e){ if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { if (unlocked) @@ -2023,7 +2037,7 @@ DAVCollection.prototype = { } catch (e){ if (e != 'close generator') - throw e; + this._log.error("Exception caught: " + e.message); } finally { generatorDone(this, onComplete, stolen); diff --git a/services/sync/IBookmarksSyncService.idl b/services/sync/IBookmarksSyncService.idl index c32a395ae2bb..935033c0932d 100644 --- a/services/sync/IBookmarksSyncService.idl +++ b/services/sync/IBookmarksSyncService.idl @@ -68,7 +68,12 @@ interface IBookmarksSyncService : nsISupports void resetLock(); /** - * Delete json on server and re-sync + * Delete json files on server */ void resetServer(); + + /** + * Clear local snapshot + */ + void resetClient(); }; diff --git a/services/sync/IBookmarksSyncService.xpt b/services/sync/IBookmarksSyncService.xpt index b3c4f23be2d8a0a6599eef6957e0638d368b01b5..8ef4f0fdc78a700b71b95a6c07d25e651e91fb78 100644 GIT binary patch delta 84 zcmaFC_=j6OnVMI^006%P5K{mE delta 63 xcmeyv_=0hQIOB_n5@`l(4GatnF^nsAGOz&|3>gd%IuA;hLg^|f-7;~T6#%O}3gQ3& From 91845936d8fff9494ee8e9b6a31f56b282d4cc45 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 19 Oct 2007 16:56:36 -0700 Subject: [PATCH 0073/1860] don't die when livemarks don't have a site/feed uri set --- services/sync/BookmarksSyncService.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 5cda2d51334e..fc41d5b6d4cb 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -323,8 +323,10 @@ BookmarksSyncService.prototype = { if (node.type == node.RESULT_TYPE_FOLDER) { if (this._ls.isLivemark(node.itemId)) { item.type = "livemark"; - item.siteURI = this._ls.getSiteURI(node.itemId).spec; - item.feedURI = this._ls.getFeedURI(node.itemId).spec; + let siteURI = this._ls.getSiteURI(node.itemId); + let feedURI = this._ls.getFeedURI(node.itemId); + item.siteURI = siteURI? siteURI.spec : ""; + item.feedURI = feedURI? feedURI.spec : ""; } else { item.type = "folder"; node.QueryInterface(Ci.nsINavHistoryQueryResultNode); From 6270e9b6b7bd29e6e17958db97bcab7136d1dfa7 Mon Sep 17 00:00:00 2001 From: Date: Fri, 19 Oct 2007 19:57:39 -0700 Subject: [PATCH 0074/1860] testing possible fix for deserializing feeds with empty site urls --- services/sync/BookmarksSyncService.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index fc41d5b6d4cb..999b29c70582 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1543,8 +1543,10 @@ function makeFile(path) { } function makeURI(URIString) { - var ioservice = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); + if (URIString === null || URIString == "") + return null; + let ioservice = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); return ioservice.newURI(URIString, null, null); } From 8be4ca482ede002e76480a834851ec97adbda088 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 22 Oct 2007 11:27:55 -0700 Subject: [PATCH 0075/1860] partial fix for deleting bookmark trees --- services/sync/BookmarksSyncService.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 999b29c70582..961a0176b7de 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -742,6 +742,11 @@ BookmarksSyncService.prototype = { _removeCommand: function BSS__removeCommand(command) { var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Attempted to remove item " + command.GUID + + ", but it does not exist. Skipping."); + return; + } var type = this._bms.getItemType(itemId); switch (type) { @@ -1339,6 +1344,21 @@ BookmarksSyncService.prototype = { this._log.warn("generator not properly closed"); }, + _encrypt: function BSS__encrypt(string, passphrase) { + let koFactory = Cc["@mozilla.org/security/keyobjectfactory;1"]. + getService(Ci.nsIKeyObjectFactory); + let ko = koFactory.keyFromString(2, passphrase); // 2 is AES + let streamCipher = Cc["@mozilla.org/security/streamcipher;1"]. + getService(Ci.nsIStreamCipher); + streamCipher.init(ko); + streamCipher.updateFromString(string); + return streamCipher.finish(true); + }, + + _decrypt: function BSS__decrypt(string, passphrase) { + // FIXME + }, + _onLogin: function BSS__onLogin(success) { this._loginGen.close(); this._loginGen = null; From 204936a81b9fa987cd1c2c97b0004f4ac92746a5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 29 Oct 2007 17:45:02 -0700 Subject: [PATCH 0076/1860] minor changes, version bump --- services/sync/BookmarksSyncService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 961a0176b7de..4a3c04321791 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1401,7 +1401,7 @@ BookmarksSyncService.prototype = { if (locked) this._log.info("Lock acquired"); else { - this._log.warn("Could not acquire lock, aborting sync"); + this._log.warn("Could not acquire lock, aborting server reset"); return; } From 31ea1374ffedc02d546768d2098c42a600075614 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 31 Oct 2007 18:23:13 -0700 Subject: [PATCH 0077/1860] Tweak logging output; make detectUpdates asynchronous (makes the spinny much smoother); avoid setting timers in our service object; add missing piece to support query items; force cache verification when making requests (to avoid using stale data) --- services/sync/BookmarksSyncService.js | 159 +++++++++++++++++--------- 1 file changed, 102 insertions(+), 57 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 4a3c04321791..6855963256cd 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -126,9 +126,6 @@ BookmarksSyncService.prototype = { // Logger object _log: null, - // Timer object for yielding to the main loop - _timer: null, - // Timer object for automagically syncing _scheduleTimer: null, @@ -224,14 +221,14 @@ BookmarksSyncService.prototype = { let formatter = logSvc.newFormatter("basic"); let root = logSvc.rootLogger; - root.level = root.LEVEL_ALL; + root.level = root.LEVEL_DEBUG; let capp = logSvc.newAppender("console", formatter); capp.level = root.LEVEL_WARN; root.addAppender(capp); let dapp = logSvc.newAppender("dump", formatter); - dapp.level = root.LEVEL_WARN; + dapp.level = root.LEVEL_ALL; root.addAppender(dapp); let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. @@ -247,7 +244,7 @@ BookmarksSyncService.prototype = { fapp.level = root.LEVEL_INFO; root.addAppender(fapp); let vapp = logSvc.newFileAppender("rotating", verboseFile, formatter); - vapp.level = root.LEVEL_ALL; + vapp.level = root.LEVEL_DEBUG; root.addAppender(vapp); }, @@ -392,45 +389,69 @@ BookmarksSyncService.prototype = { return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); }, - _detectUpdates: function BSS__detectUpdates(a, b) { + _detectUpdates: function BSS__detectUpdates(onComplete, a, b) { + let cont = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let cmds = []; - for (let GUID in a) { - if (GUID in b) { - let edits = this._getEdits(a[GUID], b[GUID]); - if (edits == -1) // something went very wrong -- FIXME - continue; - if (edits.numProps == 0) // no changes - skip + + try { + for (let GUID in a) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (GUID in b) { + let edits = this._getEdits(a[GUID], b[GUID]); + if (edits == -1) // something went very wrong -- FIXME + continue; + if (edits.numProps == 0) // no changes - skip + continue; + let parents = this._nodeParents(GUID, b); + cmds.push({action: "edit", GUID: GUID, + depth: parents.length, parents: parents, + data: edits.props}); + } else { + let parents = this._nodeParents(GUID, a); // ??? + cmds.push({action: "remove", GUID: GUID, + depth: parents.length, parents: parents}); + } + } + for (let GUID in b) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (GUID in a) continue; let parents = this._nodeParents(GUID, b); - cmds.push({action: "edit", GUID: GUID, + cmds.push({action: "create", GUID: GUID, depth: parents.length, parents: parents, - data: edits.props}); - } else { - let parents = this._nodeParents(GUID, a); // ??? - cmds.push({action: "remove", GUID: GUID, - depth: parents.length, parents: parents}); + data: b[GUID]}); } + cmds.sort(function(a, b) { + if (a.depth > b.depth) + return 1; + if (a.depth < b.depth) + return -1; + if (a.index > b.index) + return -1; + if (a.index < b.index) + return 1; + return 0; // should never happen, but not a big deal if it does + }); + + } catch (e) { + if (e != 'close generator') + this._log.error("Exception caught: " + e.message); + + } finally { + timer = null; + generatorDone(this, onComplete, cmds); + yield; // onComplete is responsible for closing the generator } - for (let GUID in b) { - if (GUID in a) - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "create", GUID: GUID, - depth: parents.length, parents: parents, - data: b[GUID]}); - } - cmds.sort(function(a, b) { - if (a.depth > b.depth) - return 1; - if (a.depth < b.depth) - return -1; - if (a.index > b.index) - return -1; - if (a.index < b.index) - return 1; - return 0; // should never happen, but not a big deal if it does - }); - return cmds; + this._log.warn("generator not properly closed"); }, // Bookmarks are allowed to be in a different index as long as they @@ -463,6 +484,11 @@ BookmarksSyncService.prototype = { a.data.title == b.data.title) return true; return false; + case "query": + if (a.data.URI == b.data.URI && + a.data.title == b.data.title) + return true; + return false; case "microsummary": if (a.data.URI == b.data.URI && a.data.generatorURI == b.data.generatorURI) @@ -549,20 +575,21 @@ BookmarksSyncService.prototype = { }, _reconcile: function BSS__reconcile(onComplete, listA, listB) { + let cont = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let propagations = [[], []]; let conflicts = [[], []]; let ret = {propagations: propagations, conflicts: conflicts}; try { - let cont = yield; - let listener = new EventListener(cont); - this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - for (let i = 0; i < listA.length; i++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - for (let j = 0; j < listB.length; j++) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + if (this._deepEquals(listA[i], listB[j])) { delete listA[i]; delete listB[j]; @@ -588,10 +615,11 @@ BookmarksSyncService.prototype = { listB = listB.filter(function(elt) { return elt }); for (let i = 0; i < listA.length; i++) { - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - for (let j = 0; j < listB.length; j++) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + if (this._conflicts(listA[i], listB[j]) || this._conflicts(listB[j], listA[i])) { if (!conflicts[0].some( @@ -606,7 +634,7 @@ BookmarksSyncService.prototype = { this._getPropagations(listA, conflicts[0], propagations[1]); - this._timer.initWithCallback(listener, 0, this._timer.TYPE_ONE_SHOT); + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); yield; // Yield to main loop this._getPropagations(listB, conflicts[1], propagations[0]); @@ -617,7 +645,7 @@ BookmarksSyncService.prototype = { this._log.error("Exception caught: " + e.message); } finally { - this._timer = null; + timer = null; generatorDone(this, onComplete, ret); yield; // onComplete is responsible for closing the generator } @@ -946,7 +974,10 @@ BookmarksSyncService.prototype = { // 2) Generate local deltas from snapshot -> current client status let localJson = this._getBookmarks(); - let localUpdates = this._detectUpdates(this._snapshot, localJson); + gen = this._detectUpdates.async(this, cont, this._snapshot, localJson); + let localUpdates = yield; + gen.close(); + this._log.debug("local json:\n" + this._mungeNodes(localJson)); this._log.debug("Local updates: " + this._mungeCommands(localUpdates)); this._log.debug("Server updates: " + this._mungeCommands(server.updates)); @@ -1008,9 +1039,11 @@ BookmarksSyncService.prototype = { this._applyCommands(clientChanges); newSnapshot = this._getBookmarks(); - let diff = this._detectUpdates(this._snapshot, newSnapshot); + gen = this._detectUpdates.async(this, cont, this._snapshot, newSnapshot); + let diff = yield; + gen.close(); if (diff.length != 0) { - this._log.error("Commands did not apply correctly"); + this._log.warn("Commands did not apply correctly"); this._log.debug("Diff from snapshot+commands -> " + "new snapshot after commands:\n" + this._mungeCommands(diff)); @@ -1029,7 +1062,9 @@ BookmarksSyncService.prototype = { // conflicts, it should be the same as what the resolver returned newSnapshot = this._getBookmarks(); - let serverDelta = this._detectUpdates(server.snapshot, newSnapshot); + gen = this._detectUpdates.async(this, cont, server.snapshot, newSnapshot); + let serverDelta = yield; + gen.close(); // Log an error if not the same if (!(serverConflicts.length || @@ -1267,7 +1302,9 @@ BookmarksSyncService.prototype = { ret.snapVersion = status.snapVersion; ret.snapshot = snap; ret.deltas = allDeltas; - ret.updates = this._detectUpdates(this._snapshot, snap); + gen = this._detectUpdates.async(this, cont, this._snapshot, snap); + ret.updates = yield; + gen.close(); break; case 404: @@ -1761,7 +1798,15 @@ DAVCollection.prototype = { request.addEventListener("error", new EventListener(cont, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); request.open(op, this._userURL + path, true); - + + + // Force cache validation + let channel = request.channel; + channel = channel.QueryInterface(Ci.nsIRequest); + let loadFlags = channel.loadFlags; + loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; + channel.loadFlags = loadFlags; + let key; for (key in headers) { this._log.debug("HTTP Header " + key + ": " + headers[key]); From aa9d251fe7ae6ec0a37b237343851617bcc93b98 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 6 Nov 2007 14:35:23 -0800 Subject: [PATCH 0078/1860] don't sync/reset/login if already doing so; improve some http status code checks --- services/sync/BookmarksSyncService.js | 62 +++++++++++++++++++++------ 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 6855963256cd..0b623cee23e4 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -474,10 +474,6 @@ BookmarksSyncService.prototype = { a.GUID == b.GUID) return false; - this._log.debug("deciding on likeness of " + a.GUID + " vs " + b.GUID); - this._log.debug("they have the same action, type, and parent"); - this._log.debug("parent is " + a.data.parentGUID); - switch (a.data.type) { case "bookmark": if (a.data.URI == b.data.URI && @@ -1139,6 +1135,7 @@ BookmarksSyncService.prototype = { _onSyncFinished: function BSS__onSyncFinished() { this._syncGen.close(); this._syncGen = null; + this._syncing = false; }, _getFolderNodes: function BSS__getFolderNodes(folder) { @@ -1405,6 +1402,7 @@ BookmarksSyncService.prototype = { } else { this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); } + this._loggingIn = false; }, _onResetLock: function BSS__resetLock(success) { @@ -1417,11 +1415,7 @@ BookmarksSyncService.prototype = { this._log.warn("Lock reset error"); this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); } - }, - - _onResetServer: function BSS__resetServer(success) { - this._resetServerGen.close(); - this._resetServerGen = null; + this._resettingLock = false; }, _resetServer: function BSS__resetServer(onComplete) { @@ -1456,9 +1450,16 @@ BookmarksSyncService.prototype = { let unlocked = yield; gen.close(); - if (!(statusResp.status == 200 || statusResp.status == 404 || - snapshotResp.status == 200 || snapshotResp.status == 404 || - deltasResp.status == 200 || deltasResp.status == 404)) { + function ok(code) { + if (code >= 200 && code < 300) + return true; + if (code == 404) + return true; + return false; + } + + if (!(ok(statusResp.status) && ok(snapshotResp.status) && + ok(deltasResp.status))) { this._log.error("Could delete server data, response codes " + statusResp.status + ", " + snapshotResp.status + ", " + deltasResp.status); @@ -1486,6 +1487,12 @@ BookmarksSyncService.prototype = { this._log.warn("generator not properly closed"); }, + _onResetServer: function BSS__resetServer(success) { + this._resetServerGen.close(); + this._resetServerGen = null; + this._resettingServer = false; + }, + _resetClient: function BSS__resetClient(onComplete) { let cont = yield; let done = false; @@ -1508,6 +1515,12 @@ BookmarksSyncService.prototype = { } }, + _onResetClient: function BSS__resetClient(success) { + this._resetClientGen.close(); + this._resetClientGen = null; + this._resettingClient = false; + }, + // XPCOM registration classDescription: "Bookmarks Sync Service", contractID: "@mozilla.org/places/sync-service;1", @@ -1557,12 +1570,22 @@ BookmarksSyncService.prototype = { // IBookmarksSyncService sync: function BSS_sync() { + if (this._syncing) { + this._log.warn("Sync requested, but already syncing"); + return; + } + this._syncing = true; this._log.info("Beginning sync"); let callback = bind2(this, this._onSyncFinished); this._syncGen = this._doSync.async(this, callback); }, login: function BSS_login() { + if (this._loggingIn) { + this._log.warn("Login requested, but already logging in"); + return; + } + this._loggingIn = true; this._log.info("Logging in"); let callback = bind2(this, this._onLogin); this._loginGen = this._dav.login.async(this._dav, callback); @@ -1575,18 +1598,33 @@ BookmarksSyncService.prototype = { }, resetLock: function BSS_resetLock() { + if (this._resettingLock) { + this._log.warn("Reset lock requested, but already resetting lock"); + return; + } + this._resettingLock = true; this._log.info("Resetting lock"); let callback = bind2(this, this._onResetLock); this._forceUnlockGen = this._dav.forceUnlock.async(this._dav, callback); }, resetServer: function BSS_resetServer() { + if (this._resettingServer) { + this._log.warn("Reset server requested, but already resetting server"); + return; + } + this._resettingServer = true; this._log.info("Resetting server data"); let callback = bind2(this, this._onResetServer); this._resetServerGen = this._resetServer.async(this, callback); }, resetClient: function BSS_resetClient() { + if (this._resettingClient) { + this._log.warn("Reset client requested, but already resetting client"); + return; + } + this._resettingClient = true; this._log.info("Resetting client data"); let callback = bind2(this, this._onResetClient); this._resetClientGen = this._resetClient.async(this, callback); From cec1afa5843f0f57b67897e6896342a0b987a584 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 6 Nov 2007 17:09:22 -0800 Subject: [PATCH 0079/1860] fix deepEquals to correctly compare non-objects --- services/sync/BookmarksSyncService.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 0b623cee23e4..5d8b90d9fa8f 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -517,6 +517,11 @@ BookmarksSyncService.prototype = { if (!a || !b) return false; + if (typeof(a) != "object" && typeof(b) != "object") + return a == b; + if (typeof(a) != "object" || typeof(b) != "object") + return false; + for (let key in a) { if (typeof(a[key]) == "object") { if (!typeof(b[key]) == "object") From 5a45bcf459d525143dbcfec2c76e01b0eb3cfd7f Mon Sep 17 00:00:00 2001 From: Date: Mon, 12 Nov 2007 21:23:07 -0800 Subject: [PATCH 0080/1860] support new services service (rework auth dance) --- services/sync/BookmarksSyncService.js | 200 +++++++++++++------------- 1 file changed, 102 insertions(+), 98 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 5d8b90d9fa8f..1aa22bb0f6fd 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -132,6 +132,18 @@ BookmarksSyncService.prototype = { // DAVCollection object _dav: null, + __encrypter: {}, + __encrypterLoaded: false, + get _encrypter() { + if (!this.__encrypterLoaded) { + let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + jsLoader.loadSubScript("chrome://sync/content/encrypt.js", this.__encrypter); + this.__encrypterLoaded = true; + } + return this.__encrypter; + }, + // Last synced tree, version, and GUID (to detect if the store has // been completely replaced and invalidate the snapshot) _snapshot: {}, @@ -150,21 +162,74 @@ BookmarksSyncService.prototype = { this.__snapshotGUID = GUID; }, + get username() { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + return branch.getCharPref("browser.places.sync.username"); + }, + set username(value) { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + return branch.setCharPref("browser.places.sync.username", value); + }, + + get password() { + // fixme: make a request and get the realm + let password; + let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, makeURI(this._serverURL).hostPort, null, + 'services.mozilla.com - proxy'); + + for (let i = 0; i < logins.length; i++) { + if (logins[i].username == this.username) { + password = logins[i].password; + break; + } + } + return password; + }, + + get userPath() { + this._log.info("Hashing username " + this.username); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let data = converter.convertToByteArray(this.username, {}); + hasher.update(data, data.length); + let rawHash = hasher.finish(false); + + // return the two-digit hexadecimal code for a byte + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); + this._log.debug("Username hashes to " + hash); + return hash; + }, + get currentUser() { - return this._dav.currentUser; + return this.username; }, _init: function BSS__init() { this._initLogs(); this._log.info("Bookmarks Sync Service Initializing"); - let serverURL = 'https://services.mozilla.com/'; + this._serverURL = 'https://services.mozilla.com/'; + this._user = ''; let enabled = false; let schedule = 0; try { let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); - serverURL = branch.getCharPref("browser.places.sync.serverURL"); + this._serverURL = branch.getCharPref("browser.places.sync.serverURL"); enabled = branch.getBoolPref("browser.places.sync.enabled"); schedule = branch.getIntPref("browser.places.sync.schedule"); @@ -172,8 +237,7 @@ BookmarksSyncService.prototype = { } catch (ex) { /* use defaults */ } - this._log.info("Bookmarks login server: " + serverURL); - this._dav = new DAVCollection(serverURL); + this._dav = new DAVCollection(); this._readSnapshot(); if (!enabled) { @@ -1383,26 +1447,10 @@ BookmarksSyncService.prototype = { this._log.warn("generator not properly closed"); }, - _encrypt: function BSS__encrypt(string, passphrase) { - let koFactory = Cc["@mozilla.org/security/keyobjectfactory;1"]. - getService(Ci.nsIKeyObjectFactory); - let ko = koFactory.keyFromString(2, passphrase); // 2 is AES - let streamCipher = Cc["@mozilla.org/security/streamcipher;1"]. - getService(Ci.nsIStreamCipher); - streamCipher.init(ko); - streamCipher.updateFromString(string); - return streamCipher.finish(true); - }, - - _decrypt: function BSS__decrypt(string, passphrase) { - // FIXME - }, - _onLogin: function BSS__onLogin(success) { this._loginGen.close(); this._loginGen = null; if (success) { - this._log.info("Bookmarks sync server: " + this._dav.userURL); this._os.notifyObservers(null, "bookmarks-sync:login", ""); } else { this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); @@ -1590,10 +1638,27 @@ BookmarksSyncService.prototype = { this._log.warn("Login requested, but already logging in"); return; } - this._loggingIn = true; + this._log.info("Logging in"); + + if (!this.username) { + this._log.warn("No username set, login failed"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + return; + } + if (!this.password) { + this._log.warn("No password found in password manager"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + return; + } + + this._loggingIn = true; + + this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; + this._log.info("Using server URL: " + this._dav.baseURL); let callback = bind2(this, this._onLogin); - this._loginGen = this._dav.login.async(this._dav, callback); + this._loginGen = this._dav.login.async(this._dav, callback, + this.username, this.password); }, logout: function BSS_logout() { @@ -1677,7 +1742,7 @@ function continueGenerator(generator, data) { if (e instanceof StopIteration) dump("continueGenerator warning: generator stopped unexpectedly"); else - this._log.error("Exception caught: " + e.message); + dump("Exception caught: " + e.message); } } @@ -1735,7 +1800,6 @@ function xpath(xmlDoc, xpathString) { function DAVCollection(baseURL) { this._baseURL = baseURL; - this._userURL = baseURL; this._authProvider = new DummyAuthProvider(); let logSvc = Cc["@mozilla.org/log4moz/service;1"]. getService(Ci.ILog4MozService); @@ -1752,7 +1816,7 @@ DAVCollection.prototype = { }, __base64: {}, - __vase64loaded: false, + __base64loaded: false, get _base64() { if (!this.__base64loaded) { let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. @@ -1763,76 +1827,23 @@ DAVCollection.prototype = { return this.__base64; }, - // FIXME: should we regen this each time to prevent it from staying in memory? - __auth: null, - get _auth() { - if (this.__auth) - return this.__auth; - - try { - this._log.debug("Generating new authentication header"); - - this.__authURI = this._userURL; - let URI = makeURI(this._userURL); - let username = 'nobody@mozilla.com'; - let password; - - let branch = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch); - username = branch.getCharPref("browser.places.sync.username"); - - if (!username) { - this._log.info("No username found in password mgr, can't generate auth header"); - return null; - } - - // fixme: make a request and get the realm - let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, URI.hostPort, null, - 'services.mozilla.com'); - - for (let i = 0; i < logins.length; i++) { - if (logins[i].username == username) { - password = logins[i].password; - break; - } - } - - if (!password) { - this._log.info("No password found in password mgr, can't generate auth header"); - return null; - } - - this.__auth = "Basic " + - this._base64.Base64.encode(username + ":" + password); - - } catch (e) {} - - return this.__auth; - }, + _auth: null, get baseURL() { return this._baseURL; }, - - get userURL() { - return this._userURL; + set baseURL(value) { + this._baseURL = value; }, _loggedIn: false, - _currentUserPath: "nobody", - - _currentUser: "nobody@mozilla.com", - get currentUser() { - return this._currentUser; - }, _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { let cont = yield; let ret; try { - this._log.debug("Creating " + op + " request for " + this._userURL + path); + this._log.debug("Creating " + op + " request for " + this._baseURL + path); let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); @@ -1840,7 +1851,7 @@ DAVCollection.prototype = { request.addEventListener("load", new EventListener(cont, "load"), false); request.addEventListener("error", new EventListener(cont, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); - request.open(op, this._userURL + path, true); + request.open(op, this._baseURL + path, true); // Force cache validation @@ -1883,7 +1894,7 @@ DAVCollection.prototype = { return {'Authorization': this._auth? this._auth : '', 'Content-type': 'text/plain', 'If': this._token? - "<" + this._userURL + "> (<" + this._token + ">)" : ''}; + "<" + this._baseURL + "> (<" + this._token + ">)" : ''}; }, GET: function DC_GET(path, onComplete) { @@ -1925,7 +1936,7 @@ DAVCollection.prototype = { // Login / Logout - login: function DC_login(onComplete) { + login: function DC_login(onComplete, username, password) { let cont = yield; try { @@ -1936,25 +1947,18 @@ DAVCollection.prototype = { this._log.info("Logging in"); - this._userURL = this._baseURL; // for createAcct.php + let URI = makeURI(this._baseURL); + this._auth = "Basic " + + this._base64.Base64.encode(username + ":" + password); - // This ensures the auth header is correct, and it doubles as an - // account creation request - let gen = this.GET("createAcct.php", cont); + // Make a call to make sure it's working + let gen = this.GET("", cont); let resp = yield; gen.close(); if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) throw 'close generator'; - let branch = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch); - this._currentUser = branch.getCharPref("browser.places.sync.username"); - - // FIXME: hack - let path = this._currentUser.split("@"); - this._currentUserPath = path[0]; - this._userURL = this._baseURL + "user/" + this._currentUserPath + "/"; this._loggedIn = true; } catch (e) { From 2f0c56fa9cac04e431eccc78a664fc2e15a331d8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 13 Nov 2007 15:04:55 -0800 Subject: [PATCH 0081/1860] add encryption library; add login dialog --- services/sync/BookmarksSyncService.js | 38 ++++++++++++++++++++---- services/sync/IBookmarksSyncService.idl | 11 +++++-- services/sync/IBookmarksSyncService.xpt | Bin 252 -> 330 bytes 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 1aa22bb0f6fd..05dbeaae48ff 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -188,6 +188,26 @@ BookmarksSyncService.prototype = { } return password; }, + set password(value) { + // cleanup any existing passwords + let uri = makeURI(this._serverURL); + let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, uri.hostPort, null, + 'services.mozilla.com - proxy'); + for(let i = 0; i < logins.length; i++) { + lm.removeLogin(logins[i]); + } + + if (!value) + return; + + // save the new one + let nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo(uri.hostPort, null, 'services.mozilla.com - proxy', + this.username, value, null, null); + lm.addLogin(login); + }, get userPath() { this._log.info("Hashing username " + this.username); @@ -215,7 +235,9 @@ BookmarksSyncService.prototype = { }, get currentUser() { - return this.username; + if (this._dav.loggedIn) + return this.username; + return null; }, _init: function BSS__init() { @@ -1633,7 +1655,7 @@ BookmarksSyncService.prototype = { this._syncGen = this._doSync.async(this, callback); }, - login: function BSS_login() { + login: function BSS_login(password) { if (this._loggingIn) { this._log.warn("Login requested, but already logging in"); return; @@ -1646,19 +1668,20 @@ BookmarksSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); return; } - if (!this.password) { - this._log.warn("No password found in password manager"); + if (!password) + password = this.password; + if (!password) { + this._log.warn("No password given or found in password manager"); this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); return; } - this._loggingIn = true; this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; this._log.info("Using server URL: " + this._dav.baseURL); let callback = bind2(this, this._onLogin); this._loginGen = this._dav.login.async(this._dav, callback, - this.username, this.password); + this.username, password); }, logout: function BSS_logout() { @@ -1837,6 +1860,9 @@ DAVCollection.prototype = { }, _loggedIn: false, + get loggedIn() { + return this._loggedIn; + }, _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { let cont = yield; diff --git a/services/sync/IBookmarksSyncService.idl b/services/sync/IBookmarksSyncService.idl index 935033c0932d..1502b04ad857 100644 --- a/services/sync/IBookmarksSyncService.idl +++ b/services/sync/IBookmarksSyncService.idl @@ -42,14 +42,21 @@ interface IBookmarksSyncService : nsISupports { /** - * The currently logged-in user + * The stored username and password + */ + attribute AString username; + attribute AString password; + + /** + * The currently logged-in user. Null if not logged in. */ readonly attribute AString currentUser; /** * Log into the server. Pre-requisite for sync(). + * password is optional when the password is saved in the password manager */ - void login(); + void login(in AString password); /** * Log out of the server. diff --git a/services/sync/IBookmarksSyncService.xpt b/services/sync/IBookmarksSyncService.xpt index 8ef4f0fdc78a700b71b95a6c07d25e651e91fb78..29e859c42188e9c5fa605c7145015f86a9e16aaf 100644 GIT binary patch delta 157 zcmeyvc#3I)7!#w{M2WO|?gj=1hCap>I~mv%U<1*oq4WhPeG5uI1Jezq#i>PkiMgo^a7IC5adA0seo+bom<3`?+~WWM38^M2 delta 79 zcmX@b^oMbR7!$*vi4tjc> Date: Tue, 13 Nov 2007 21:37:20 -0800 Subject: [PATCH 0082/1860] encryption support, woo\! --- services/sync/BookmarksSyncService.js | 304 ++++++++++++++++-------- services/sync/IBookmarksSyncService.idl | 18 +- services/sync/IBookmarksSyncService.xpt | Bin 330 -> 414 bytes services/sync/services-sync.js | 1 + 4 files changed, 222 insertions(+), 101 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 05dbeaae48ff..1e6ab95aaad1 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -48,7 +48,7 @@ const MODE_TRUNCATE = 0x20; const PERMS_FILE = 0644; const PERMS_DIRECTORY = 0755; -const STORAGE_FORMAT_VERSION = 0; +const STORAGE_FORMAT_VERSION = 1; const ONE_BYTE = 1; const ONE_KILOBYTE = 1024 * ONE_BYTE; @@ -173,12 +173,11 @@ BookmarksSyncService.prototype = { return branch.setCharPref("browser.places.sync.username", value); }, - get password() { + _lmGet: function BSS__lmGet(realm) { // fixme: make a request and get the realm let password; let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, makeURI(this._serverURL).hostPort, null, - 'services.mozilla.com - proxy'); + let logins = lm.findLogins({}, 'chrome://sync', null, realm); for (let i = 0; i < logins.length; i++) { if (logins[i].username == this.username) { @@ -188,27 +187,43 @@ BookmarksSyncService.prototype = { } return password; }, - set password(value) { + + _lmSet: function BSS__lmSet(realm, password) { // cleanup any existing passwords - let uri = makeURI(this._serverURL); let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, uri.hostPort, null, - 'services.mozilla.com - proxy'); + let logins = lm.findLogins({}, 'chrome://sync', null, realm); for(let i = 0; i < logins.length; i++) { lm.removeLogin(logins[i]); } - if (!value) + if (!password) return; // save the new one let nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new nsLoginInfo(uri.hostPort, null, 'services.mozilla.com - proxy', - this.username, value, null, null); + let login = new nsLoginInfo('chrome://sync', null, realm, + this.username, password, null, null); lm.addLogin(login); }, + get password() { + return this._lmGet('Mozilla Services Password'); + }, + set password(value) { + this._lmSet('Mozilla Services Password', value); + }, + + _passphrase: null, + get passphrase() { + if (this._passphrase === null) + return this._lmGet('Mozilla Services Encryption Passphrase'); + return this._passphrase; + }, + set passphrase(value) { + this._lmSet('Mozilla Services Encryption Passphrase', value); + }, + get userPath() { this._log.info("Hashing username " + this.username); @@ -240,6 +255,29 @@ BookmarksSyncService.prototype = { return null; }, + _encryptionChanged: false, + get encryption() { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + return branch.getCharPref("browser.places.sync.encryption"); + }, + set encryption(value) { + switch (value) { + case "XXXTEA": + case "none": + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + let cur = branch.getCharPref("browser.places.sync.encryption"); + if (value != cur) { + this._encryptionChanged = true; + branch.setCharPref("browser.places.sync.encryption", value); + } + break; + default: + throw "Invalid encryption value: " + value; + } + }, + _init: function BSS__init() { this._initLogs(); this._log.info("Bookmarks Sync Service Initializing"); @@ -1171,29 +1209,49 @@ BookmarksSyncService.prototype = { server.deltas.push(serverDelta); - gen = this._dav.PUT("bookmarks-deltas.json", - this._mungeCommands(server.deltas), cont); - let deltasPut = yield; - gen.close(); + if (server.formatVersion != STORAGE_FORMAT_VERSION || + this._encryptionChanged) { + gen = this._fullUpload.async(this, cont); + let status = yield; + gen.close(); + if (!status) + this._log.error("Could not upload files to server"); // eep? - // FIXME: need to watch out for the storage format version changing, - // in that case we'll have to re-upload all the files, not just these - gen = this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshotVersion}), cont); - let statusPut = yield; - gen.close(); - - if (deltasPut.status >= 200 && deltasPut.status < 300 && - statusPut.status >= 200 && statusPut.status < 300) { - this._log.info("Successfully updated deltas and status on server"); - this._saveSnapshot(); } else { - // FIXME: revert snapshot here? - can't, we already applied - // updates locally! - need to save and retry - this._log.error("Could not update deltas on server"); + let data; + if (this.encryption == "none") { + data = this._mungeCommands(server.deltas); + } else if (this.encryption == "XXXTEA") { + this._log.debug("Encrypting snapshot"); + data = this._encrypter.encrypt(uneval(server.deltas), this.passphrase); + this._log.debug("Done encrypting snapshot"); + } else { + this._log.error("Unknown encryption scheme: " + this.encryption); + throw 'close generator'; + } + gen = this._dav.PUT("bookmarks-deltas.json", data, cont); + let deltasPut = yield; + gen.close(); + + gen = this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshotGUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshotVersion, + snapEncryption: server.snapEncryption, + deltasEncryption: this.encryption}), cont); + let statusPut = yield; + gen.close(); + + if (deltasPut.status >= 200 && deltasPut.status < 300 && + statusPut.status >= 200 && statusPut.status < 300) { + this._log.info("Successfully updated deltas and status on server"); + this._saveSnapshot(); + } else { + // FIXME: revert snapshot here? - can't, we already applied + // updates locally! - need to save and retry + this._log.error("Could not update deltas on server"); + } } } @@ -1254,6 +1312,38 @@ BookmarksSyncService.prototype = { } }, + // NOTE: this method can throw 'close generator' + _checkStatus: function BSS__checkStatus(code, msg) { + if (code >= 200 && code < 300) + return; + this._log.error(msg + " Error code: " + code); + throw 'close generator'; + }, + + // NOTE: this method can throw 'close generator' + _decrypt: function BSS__decrypt(alg, data) { + let out; + switch (alg) { + case "XXXTEA": + try { + this._log.debug("Decrypting data"); + out = eval(this._encrypter.decrypt(data, this.passphrase)); + this._log.debug("Done decrypting data"); + } catch (e) { + this._log.error("Could not decrypt server snapshot"); + throw 'close generator'; + } + break; + case "none": + out = eval(data); + break; + default: + this._log.error("Unknown encryption algorithm: " + alg); + throw 'close generator'; + } + return out; + }, + /* Get the deltas/combined updates from the server * Returns: * status: @@ -1266,6 +1356,10 @@ BookmarksSyncService.prototype = { * the latest version on the server * snapVersion: * the version of the current snapshot on the server (deltas not applied) + * snapEncryption: + * encryption algorithm currently used on the server-stored snapshot + * deltasEncryption: + * encryption algorithm currently used on the server-stored deltas * snapshot: * full snapshot of the latest server version (deltas applied) * deltas: @@ -1278,6 +1372,7 @@ BookmarksSyncService.prototype = { let cont = yield; let ret = {status: -1, formatVersion: null, maxVersion: null, snapVersion: null, + snapEncryption: null, deltasEncryption: null, snapshot: null, deltas: null, updates: null}; try { @@ -1301,6 +1396,11 @@ BookmarksSyncService.prototype = { generatorDone(this, onComplete, ret) throw 'close generator'; } + + if (status.formatVersion == 0) { + ret.snapEncryption = status.snapEncryption = "none"; + ret.deltasEncryption = status.deltasEncryption = "none"; + } if (status.GUID != this._snapshotGUID) { this._log.info("Remote/local sync GUIDs do not match. " + @@ -1319,25 +1419,15 @@ BookmarksSyncService.prototype = { gen = this._dav.GET("bookmarks-snapshot.json", cont); resp = yield; gen.close() + this._checkStatus(resp.status, "Could not download snapshot."); + snap = this._decrypt(status.snapEncryption, resp.responseText); - if (resp.status < 200 || resp.status >= 300) { - this._log.error("Could not download server snapshot"); - generatorDone(this, onComplete, ret) - throw 'close generator'; - } - snap = eval(resp.responseText); - this._log.info("Downloading server deltas"); gen = this._dav.GET("bookmarks-deltas.json", cont); resp = yield; gen.close(); - - if (resp.status < 200 || resp.status >= 300) { - this._log.error("Could not download server deltas"); - generatorDone(this, onComplete, ret) - throw 'close generator'; - } - allDeltas = eval(resp.responseText); + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); deltas = eval(uneval(allDeltas)); } else if (this._snapshotVersion >= status.snapVersion && @@ -1348,13 +1438,8 @@ BookmarksSyncService.prototype = { gen = this._dav.GET("bookmarks-deltas.json", cont); resp = yield; gen.close(); - - if (resp.status < 200 || resp.status >= 300) { - this._log.error("Could not download server deltas"); - generatorDone(this, onComplete, ret) - throw 'close generator'; - } - allDeltas = eval(resp.responseText); + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); deltas = allDeltas.slice(this._snapshotVersion - status.snapVersion); } else if (this._snapshotVersion == status.maxVersion) { @@ -1365,18 +1450,12 @@ BookmarksSyncService.prototype = { gen = this._dav.GET("bookmarks-deltas.json", cont); resp = yield; gen.close(); - - if (resp.status < 200 || resp.status >= 300) { - this._log.error("Could not download server deltas"); - generatorDone(this, onComplete, ret) - throw 'close generator'; - } - allDeltas = eval(resp.responseText); + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); deltas = []; } else { // this._snapshotVersion > status.maxVersion this._log.error("Server snapshot is older than local snapshot"); - generatorDone(this, onComplete, ret) throw 'close generator'; } @@ -1388,6 +1467,8 @@ BookmarksSyncService.prototype = { ret.formatVersion = status.formatVersion; ret.maxVersion = status.maxVersion; ret.snapVersion = status.snapVersion; + ret.snapEncryption = status.snapEncryption; + ret.deltasEncryption = status.deltasEncryption; ret.snapshot = snap; ret.deltas = allDeltas; gen = this._detectUpdates.async(this, cont, this._snapshot, snap); @@ -1401,44 +1482,12 @@ BookmarksSyncService.prototype = { this._snapshot = this._getBookmarks(); this._snapshotVersion = 0; this._snapshotGUID = null; // in case there are other snapshots out there - - gen = this._dav.PUT("bookmarks-snapshot.json", - this._mungeNodes(this._snapshot), cont); - resp = yield; - gen.close(); - if (resp.status < 200 || resp.status >= 300) { - this._log.error("Could not upload snapshot to server, error code: " + - resp.status); - generatorDone(this, onComplete, ret) - throw 'close generator'; - } - - gen = this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); - resp = yield; + gen = this._fullUpload.async(this, cont); + let uploadStatus = yield; gen.close(); - - if (resp.status < 200 || resp.status >= 300) { - this._log.error("Could not upload deltas to server, error code: " + - resp.status); - generatorDone(this, onComplete, ret) + if (!uploadStatus) throw 'close generator'; - } - - gen = this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshotVersion, - maxVersion: this._snapshotVersion}), cont); - resp = yield; - gen.close(); - - if (resp.status < 200 || resp.status >= 300) { - this._log.error("Could not upload status file to server, error code: " + - resp.status); - generatorDone(this, onComplete, ret) - throw 'close generator'; - } this._log.info("Initial upload to server successful"); this._saveSnapshot(); @@ -1447,6 +1496,8 @@ BookmarksSyncService.prototype = { ret.formatVersion = STORAGE_FORMAT_VERSION; ret.maxVersion = this._snapshotVersion; ret.snapVersion = this._snapshotVersion; + ret.snapEncryption = this.encryption; + ret.deltasEncryption = this.encryption; ret.snapshot = eval(uneval(this._snapshot)); ret.deltas = []; ret.updates = []; @@ -1469,6 +1520,57 @@ BookmarksSyncService.prototype = { this._log.warn("generator not properly closed"); }, + _fullUpload: function BSS__fullUpload(onComplete) { + let cont = yield; + let ret = false; + + try { + let data; + if (this.encryption == "none") { + data = this._mungeNodes(this._snapshot); + } else if (this.encryption == "XXXTEA") { + this._log.debug("Encrypting snapshot"); + data = this._encrypter.encrypt(uneval(this._snapshot), this.passphrase); + this._log.debug("Done encrypting snapshot"); + } else { + this._log.error("Unknown encryption scheme: " + this.encryption); + throw 'close generator'; + } + let gen = this._dav.PUT("bookmarks-snapshot.json", data, cont); + resp = yield; + gen.close(); + this._checkStatus(resp.status, "Could not upload snapshot."); + + gen = this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); + resp = yield; + gen.close(); + this._checkStatus(resp.status, "Could not upload deltas."); + + gen = this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshotGUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshotVersion, + maxVersion: this._snapshotVersion, + snapEncryption: this.encryption, + deltasEncryption: "none"}), cont); + resp = yield; + gen.close(); + this._checkStatus(resp.status, "Could not upload status file."); + + this._log.info("Full upload to server successful"); + ret = true; + + } catch (e) { + if (e != 'close generator') + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, onComplete, ret) + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + _onLogin: function BSS__onLogin(success) { this._loginGen.close(); this._loginGen = null; @@ -1477,6 +1579,7 @@ BookmarksSyncService.prototype = { } else { this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); } + this._passphrase = null; this._loggingIn = false; }, @@ -1655,7 +1758,7 @@ BookmarksSyncService.prototype = { this._syncGen = this._doSync.async(this, callback); }, - login: function BSS_login(password) { + login: function BSS_login(password, passphrase) { if (this._loggingIn) { this._log.warn("Login requested, but already logging in"); return; @@ -1675,10 +1778,14 @@ BookmarksSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); return; } - this._loggingIn = true; + + // cache passphrase. if null, we'll try to get it from the pw manager + this._passphrase = passphrase; this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; this._log.info("Using server URL: " + this._dav.baseURL); + + this._loggingIn = true; let callback = bind2(this, this._onLogin); this._loginGen = this._dav.login.async(this._dav, callback, this.username, password); @@ -1687,6 +1794,7 @@ BookmarksSyncService.prototype = { logout: function BSS_logout() { this._log.info("Logging out"); this._dav.logout(); + this._passphrase = null; this._os.notifyObservers(null, "bookmarks-sync:logout", ""); }, diff --git a/services/sync/IBookmarksSyncService.idl b/services/sync/IBookmarksSyncService.idl index 1502b04ad857..5a41eed3bc33 100644 --- a/services/sync/IBookmarksSyncService.idl +++ b/services/sync/IBookmarksSyncService.idl @@ -53,10 +53,22 @@ interface IBookmarksSyncService : nsISupports readonly attribute AString currentUser; /** - * Log into the server. Pre-requisite for sync(). - * password is optional when the password is saved in the password manager + * Encryption algorithm to use. May be 'xxxtea' or 'none'. */ - void login(in AString password); + attribute AString enryption; + + /** + * The stored encryption passphrase + */ + attribute AString passphrase; + + /** + * Log into the server. Pre-requisite for sync(). + * password and/or passphrase may be null, and then they will be + * fetched from the password manager. If they are not given or + * found, login will fail. + */ + void login(in AString password, in AString passphrase); /** * Log out of the server. diff --git a/services/sync/IBookmarksSyncService.xpt b/services/sync/IBookmarksSyncService.xpt index 29e859c42188e9c5fa605c7145015f86a9e16aaf..6c62468aa6e49c6cf850b4a73192657b89d1cd15 100644 GIT binary patch delta 202 zcmX@bG>>_LIODvD5^44P4GatnI~Z5&WMFduvJNsf09hchQ*g1%aIrgJF_7#Fxa=pm z>>rpIBRg0OsEkpFsbMF`L=ano0Yb||X%!&N3zE=-3Rq2iV5XUxS5#S0l9`{!fM69Q T78e&}6eSj?G9Z|fMHx*27VI$< delta 118 zcmbQoe2Qs;IHT7@iL@l{1_lO(KE@S08Q2_vtm%vmKo&@B0bFb)Tx=6q3@FL44=e^^ c9bsT#1JS3U^aUt=3ratm%*ALnIg8N*063!;?*IS* diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9a7f3cb5dac7..c043e5a3de03 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -6,3 +6,4 @@ pref("browser.places.sync.enabled", true); pref("browser.places.sync.bookmarks", true); pref("browser.places.sync.schedule", 1); pref("browser.places.sync.lastsync", "0"); +pref("browser.places.sync.encryption", "XXXTEA"); From 2829b5fff22e410f523f313c4b06f6e6e9d03a0e Mon Sep 17 00:00:00 2001 From: Date: Wed, 14 Nov 2007 16:20:16 -0800 Subject: [PATCH 0083/1860] add not-yet-working ui for the encryption prefs; add bookmarks count to the status file --- services/sync/BookmarksSyncService.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 1e6ab95aaad1..86d55b0ced92 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1233,13 +1233,18 @@ BookmarksSyncService.prototype = { let deltasPut = yield; gen.close(); + let c = 0; + for (GUID in this._snapshot) + c++; + gen = this._dav.PUT("bookmarks-status.json", uneval({GUID: this._snapshotGUID, formatVersion: STORAGE_FORMAT_VERSION, snapVersion: server.snapVersion, maxVersion: this._snapshotVersion, snapEncryption: server.snapEncryption, - deltasEncryption: this.encryption}), cont); + deltasEncryption: this.encryption, + bookmarksCount: c}), cont); let statusPut = yield; gen.close(); @@ -1546,13 +1551,18 @@ BookmarksSyncService.prototype = { gen.close(); this._checkStatus(resp.status, "Could not upload deltas."); + let c = 0; + for (GUID in this._snapshot) + c++; + gen = this._dav.PUT("bookmarks-status.json", uneval({GUID: this._snapshotGUID, formatVersion: STORAGE_FORMAT_VERSION, snapVersion: this._snapshotVersion, maxVersion: this._snapshotVersion, snapEncryption: this.encryption, - deltasEncryption: "none"}), cont); + deltasEncryption: "none", + bookmarksCount: c}), cont); resp = yield; gen.close(); this._checkStatus(resp.status, "Could not upload status file."); From 049f600fef6bd400a53bae9d5ebde0f57c69a9bb Mon Sep 17 00:00:00 2001 From: Date: Wed, 14 Nov 2007 17:09:09 -0800 Subject: [PATCH 0084/1860] use btoa instead of external base64 library --- services/sync/BookmarksSyncService.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 86d55b0ced92..23ee75599633 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -1956,18 +1956,6 @@ DAVCollection.prototype = { return this.__dp; }, - __base64: {}, - __base64loaded: false, - get _base64() { - if (!this.__base64loaded) { - let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. - getService(Ci.mozIJSSubScriptLoader); - jsLoader.loadSubScript("chrome://sync/content/base64.js", this.__base64); - this.__base64loaded = true; - } - return this.__base64; - }, - _auth: null, get baseURL() { @@ -2092,8 +2080,7 @@ DAVCollection.prototype = { this._log.info("Logging in"); let URI = makeURI(this._baseURL); - this._auth = "Basic " + - this._base64.Base64.encode(username + ":" + password); + this._auth = "Basic " + btoa(username + ":" + password); // Make a call to make sure it's working let gen = this.GET("", cont); From 4c249d0c4ee50b42f8d23049ea0556205e903f52 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 20 Nov 2007 21:48:41 -0800 Subject: [PATCH 0085/1860] fix maxVersion; fix bookmark root names; get new bookmarks toolbar root --- services/sync/BookmarksSyncService.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 23ee75599633..bd7c56e24697 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -991,17 +991,20 @@ BookmarksSyncService.prototype = { }, _getBookmarks: function BSS__getBookmarks() { - let filed = this._getWrappedBookmarks(this._bms.bookmarksRoot); - let unfiled = this._getWrappedBookmarks(this._bms.unfiledRoot); + let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder); + let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder); + let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder); for (let guid in unfiled) { - if (guid in filed) - this._log.warn("Same bookmark (guid) in both " + - "filed and unfiled trees!"); - else + if (!(guid in filed)) filed[guid] = unfiled[guid]; } + for (let guid in toolbar) { + if (!(guid in filed)) + filed[guid] = toolbar[guid]; + } + return filed; // (combined) }, From d337d11b304a2b4b5c070f1ee6ab64ddb941cb13 Mon Sep 17 00:00:00 2001 From: Date: Mon, 26 Nov 2007 13:45:46 -0800 Subject: [PATCH 0086/1860] sync -> weave in chrome URIs; fix broken bookmarks service attribute calls --- services/sync/BookmarksSyncService.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index bd7c56e24697..97460870b434 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -48,7 +48,7 @@ const MODE_TRUNCATE = 0x20; const PERMS_FILE = 0644; const PERMS_DIRECTORY = 0755; -const STORAGE_FORMAT_VERSION = 1; +const STORAGE_FORMAT_VERSION = 2; const ONE_BYTE = 1; const ONE_KILOBYTE = 1024 * ONE_BYTE; @@ -138,7 +138,7 @@ BookmarksSyncService.prototype = { if (!this.__encrypterLoaded) { let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. getService(Ci.mozIJSSubScriptLoader); - jsLoader.loadSubScript("chrome://sync/content/encrypt.js", this.__encrypter); + jsLoader.loadSubScript("chrome://weave/content/encrypt.js", this.__encrypter); this.__encrypterLoaded = true; } return this.__encrypter; @@ -840,7 +840,7 @@ BookmarksSyncService.prototype = { if (parentId < 0) { this._log.warn("Creating node with unknown parent -> reparenting to root"); - parentId = this._bms.bookmarksRoot; + parentId = this._bms.bookmarksMenuFolder; } switch (command.data.type) { @@ -1302,8 +1302,9 @@ BookmarksSyncService.prototype = { }, _resetGUIDs: function BSS__resetGUIDs() { - this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksRoot)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledRoot)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); }, _resetGUIDsInt: function BSS__resetGUIDsInt(node) { From 8ff78c363c1a6931f7ca2c83f83ac18cb877576d Mon Sep 17 00:00:00 2001 From: Date: Tue, 27 Nov 2007 18:26:21 -0800 Subject: [PATCH 0087/1860] refactoring and cleanup --- services/sync/BookmarksSyncService.js | 1433 +++++++++++++------------ 1 file changed, 757 insertions(+), 676 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 97460870b434..5a9d68117b5c 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -56,6 +56,11 @@ const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +/* + * Service object + * Implements IBookmarksSyncService, main entry point + */ + function BookmarksSyncService() { this._init(); } BookmarksSyncService.prototype = { @@ -123,15 +128,26 @@ BookmarksSyncService.prototype = { return this.__dirSvc; }, + __dav: null, + get _dav() { + if (!this.__dav) + this.__dav = new DAVCollection(); + return this.__dav; + }, + + __se: null, + get _se() { + if (!this.__se) + this.__se = new BookmarksSyncEngine(); + return this.__se; + }, + // Logger object _log: null, // Timer object for automagically syncing _scheduleTimer: null, - // DAVCollection object - _dav: null, - __encrypter: {}, __encrypterLoaded: false, get _encrypter() { @@ -207,11 +223,14 @@ BookmarksSyncService.prototype = { lm.addLogin(login); }, + _password: null, get password() { return this._lmGet('Mozilla Services Password'); }, set password(value) { - this._lmSet('Mozilla Services Password', value); + if (this._password === null) + return this._lmSet('Mozilla Services Password', value); + return this._password; }, _passphrase: null, @@ -255,6 +274,7 @@ BookmarksSyncService.prototype = { return null; }, + // FIXME: listen for pref changes _encryptionChanged: false, get encryption() { let branch = Cc["@mozilla.org/preferences-service;1"] @@ -297,7 +317,6 @@ BookmarksSyncService.prototype = { } catch (ex) { /* use defaults */ } - this._dav = new DAVCollection(); this._readSnapshot(); if (!enabled) { @@ -483,300 +502,6 @@ BookmarksSyncService.prototype = { items[GUID] = item; }, - // FIXME: this won't work for deep objects, or objects with optional properties - _getEdits: function BSS__getEdits(a, b) { - // check the type separately, just in case - if (a.type != b.type) - return -1; - - let ret = {numProps: 0, props: {}}; - for (prop in a) { - if (!this._deepEquals(a[prop], b[prop])) { - ret.numProps++; - ret.props[prop] = b[prop]; - } - } - - // FIXME: prune out properties we don't care about - - return ret; - }, - - _nodeParents: function BSS__nodeParents(GUID, tree) { - return this._nodeParentsInt(GUID, tree, []); - }, - - _nodeParentsInt: function BSS__nodeParentsInt(GUID, tree, parents) { - if (!tree[GUID] || !tree[GUID].parentGUID) - return parents; - parents.push(tree[GUID].parentGUID); - return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); - }, - - _detectUpdates: function BSS__detectUpdates(onComplete, a, b) { - let cont = yield; - let listener = new EventListener(cont); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - - let cmds = []; - - try { - for (let GUID in a) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (GUID in b) { - let edits = this._getEdits(a[GUID], b[GUID]); - if (edits == -1) // something went very wrong -- FIXME - continue; - if (edits.numProps == 0) // no changes - skip - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "edit", GUID: GUID, - depth: parents.length, parents: parents, - data: edits.props}); - } else { - let parents = this._nodeParents(GUID, a); // ??? - cmds.push({action: "remove", GUID: GUID, - depth: parents.length, parents: parents}); - } - } - for (let GUID in b) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (GUID in a) - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "create", GUID: GUID, - depth: parents.length, parents: parents, - data: b[GUID]}); - } - cmds.sort(function(a, b) { - if (a.depth > b.depth) - return 1; - if (a.depth < b.depth) - return -1; - if (a.index > b.index) - return -1; - if (a.index < b.index) - return 1; - return 0; // should never happen, but not a big deal if it does - }); - - } catch (e) { - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); - - } finally { - timer = null; - generatorDone(this, onComplete, cmds); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - // Bookmarks are allowed to be in a different index as long as they - // are in the same folder. Folders and separators must be at the - // same index to qualify for 'likeness'. - _commandLike: function BSS__commandLike(a, b) { - - // Check that neither command is null, that their actions, types, - // and parents are the same, and that they don't have the same - // GUID. - // Items with the same GUID do not qualify because they need to be - // processed for edits. - // The parent GUID check works because reconcile() fixes up the - // parent GUIDs as it runs, and the command list is sorted by - // depth - if (!a || !b || - a.action != b.action || - a.data.type != b.data.type || - a.data.parentGUID != b.data.parentGUID || - a.GUID == b.GUID) - return false; - - switch (a.data.type) { - case "bookmark": - if (a.data.URI == b.data.URI && - a.data.title == b.data.title) - return true; - return false; - case "query": - if (a.data.URI == b.data.URI && - a.data.title == b.data.title) - return true; - return false; - case "microsummary": - if (a.data.URI == b.data.URI && - a.data.generatorURI == b.data.generatorURI) - return true; - return false; - case "folder": - if (a.index == b.index && - a.data.title == b.data.title) - return true; - return false; - case "livemark": - if (a.data.title == b.data.title && - a.data.siteURI == b.data.siteURI && - a.data.feedURI == b.data.feedURI) - return true; - return false; - case "separator": - if (a.index == b.index) - return true; - return false; - default: - this._log.error("_commandLike: Unknown item type: " + uneval(a)); - return false; - } - }, - - _deepEquals: function BSS__commandEquals(a, b) { - if (!a && !b) - return true; - if (!a || !b) - return false; - - if (typeof(a) != "object" && typeof(b) != "object") - return a == b; - if (typeof(a) != "object" || typeof(b) != "object") - return false; - - for (let key in a) { - if (typeof(a[key]) == "object") { - if (!typeof(b[key]) == "object") - return false; - if (!this._deepEquals(a[key], b[key])) - return false; - } else { - if (a[key] != b[key]) - return false; - } - } - return true; - }, - - // When we change the GUID of a local item (because we detect it as - // being the same item as a remote one), we need to fix any other - // local items that have it as their parent - _fixParents: function BSS__fixParents(list, oldGUID, newGUID) { - for (let i = 0; i < list.length; i++) { - if (!list[i]) - continue; - if (list[i].data.parentGUID == oldGUID) - list[i].data.parentGUID = newGUID; - for (let j = 0; j < list[i].parents.length; j++) { - if (list[i].parents[j] == oldGUID) - list[i].parents[j] = newGUID; - } - } - }, - - _conflicts: function BSS__conflicts(a, b) { - if ((a.GUID == b.GUID) && !this._deepEquals(a, b)) - return true; - return false; - }, - - _getPropagations: function BSS__getPropagations(commands, conflicts, propagations) { - for (let i = 0; i < commands.length; i++) { - let alsoConflicts = function(elt) { - return (elt.action == "create" || elt.action == "remove") && - commands[i].parents.indexOf(elt.GUID) >= 0; - }; - if (conflicts.some(alsoConflicts)) - conflicts.push(commands[i]); - - let cmdConflicts = function(elt) { - return elt.GUID == commands[i].GUID; - }; - if (!conflicts.some(cmdConflicts)) - propagations.push(commands[i]); - } - }, - - _reconcile: function BSS__reconcile(onComplete, listA, listB) { - let cont = yield; - let listener = new EventListener(cont); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - - let propagations = [[], []]; - let conflicts = [[], []]; - let ret = {propagations: propagations, conflicts: conflicts}; - - try { - for (let i = 0; i < listA.length; i++) { - for (let j = 0; j < listB.length; j++) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (this._deepEquals(listA[i], listB[j])) { - delete listA[i]; - delete listB[j]; - } else if (this._commandLike(listA[i], listB[j])) { - this._fixParents(listA, listA[i].GUID, listB[j].GUID); - listB[j].data = {GUID: listB[j].GUID}; - listB[j].GUID = listA[i].GUID; - listB[j].action = "edit"; - delete listA[i]; - } - - // watch out for create commands with GUIDs that already exist - if (listB[j] && listB[j].action == "create" && - this._bms.getItemIdForGUID(listB[j].GUID) >= 0) { - this._log.error("Remote command has GUID that already exists " + - "locally. Dropping command."); - delete listB[j]; - } - } - } - - listA = listA.filter(function(elt) { return elt }); - listB = listB.filter(function(elt) { return elt }); - - for (let i = 0; i < listA.length; i++) { - for (let j = 0; j < listB.length; j++) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (this._conflicts(listA[i], listB[j]) || - this._conflicts(listB[j], listA[i])) { - if (!conflicts[0].some( - function(elt) { return elt.GUID == listA[i].GUID })) - conflicts[0].push(listA[i]); - if (!conflicts[1].some( - function(elt) { return elt.GUID == listB[j].GUID })) - conflicts[1].push(listB[j]); - } - } - } - - this._getPropagations(listA, conflicts[0], propagations[1]); - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - this._getPropagations(listB, conflicts[1], propagations[0]); - ret = {propagations: propagations, conflicts: conflicts}; - - } catch (e) { - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); - - } finally { - timer = null; - generatorDone(this, onComplete, ret); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { for (let i = 0; i < commands.length; i++) { this._log.debug("Applying cmd to obj: " + uneval(commands[i])); @@ -982,12 +707,14 @@ BookmarksSyncService.prototype = { } }, - _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { + _getFolderNodes: function BSS__getFolderNodes(folder) { let query = this._hsvc.getNewQuery(); query.setFolders([folder], 1); - let root = this._hsvc.executeQuery(query, - this._hsvc.getNewQueryOptions()).root; - return this._wrapNode(root); + return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + }, + + _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { + return this._wrapNode(this._getFolderNodes(folder)); }, _getBookmarks: function BSS__getBookmarks() { @@ -1035,6 +762,71 @@ BookmarksSyncService.prototype = { return json; }, + // IBookmarksSyncService internal implementation + + _lock: function BSS__lock() { + if (this._locked) { + this._log.warn("Service lock failed: already locked"); + return false; + } + this._locked = true; + this._log.debug("Service lock acquired"); + return true; + }, + + _unlock: function BSS__unlock() { + this._locked = false; + this._log.debug("Service lock released"); + }, + + _login: function BSS__login(onComplete) { + let [self, cont] = yield; + let success = false; + let svcLock = this._lock(); + + try { + if (!svcLock) + return; + this._log.debug("Logging in"); + this._os.notifyObservers(null, "bookmarks-sync:login-start", ""); + + if (!this.username) { + this._log.warn("No username set, login failed"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + return; + } + if (!this.password) { + this._log.warn("No password given or found in password manager"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + return; + } + + this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; + this._log.info("Using server URL: " + this._dav.baseURL); + + this._dav.login.async(this._dav, cont, this.username, this.password); + success = yield; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + this._passphrase = null; + if (success) { + this._log.debug("Login successful"); + this._os.notifyObservers(null, "bookmarks-sync:login-end", ""); + } else { + this._log.debug("Login error"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + } + if (svcLock) + this._unlock(); + generatorDone(this, self, onComplete, success); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + // original // / \ // A / \ B @@ -1064,29 +856,30 @@ BookmarksSyncService.prototype = { // 3.2) Append server delta to the delta file and upload ("C") _doSync: function BSS__doSync(onComplete) { - let cont = yield; + let [self, cont] = yield; let synced = false; let locked = null; - let gen; + let svcLock = this._lock(); try { + if (!svcLock) + return; + this._log.info("Beginning sync"); this._os.notifyObservers(null, "bookmarks-sync:sync-start", ""); - gen = this._dav.lock.async(this._dav, cont); + this._dav.lock.async(this._dav, cont); locked = yield; - gen.close(); if (locked) this._log.info("Lock acquired"); else { this._log.warn("Could not acquire lock, aborting sync"); - throw 'close generator'; + return; } // 1) Fetch server deltas - gen = this._getServerData.async(this, cont); + this._getServerData.async(this, cont); let server = yield; - gen.close(); this._log.info("Local snapshot version: " + this._snapshotVersion); this._log.info("Server status: " + server.status); @@ -1096,15 +889,14 @@ BookmarksSyncService.prototype = { if (server.status != 0) { this._log.fatal("Sync error: could not get server status, " + "or initial upload failed. Aborting sync."); - throw 'close generator'; + return; } // 2) Generate local deltas from snapshot -> current client status let localJson = this._getBookmarks(); - gen = this._detectUpdates.async(this, cont, this._snapshot, localJson); + this._se.detectUpdates(cont, this._snapshot, localJson); let localUpdates = yield; - gen.close(); this._log.debug("local json:\n" + this._mungeNodes(localJson)); this._log.debug("Local updates: " + this._mungeCommands(localUpdates)); @@ -1114,15 +906,14 @@ BookmarksSyncService.prototype = { this._snapshotVersion = server.maxVersion; this._log.info("Sync complete (1): no changes needed on client or server"); synced = true; - throw 'close generator'; + return; } // 3) Reconcile client/server deltas and generate new deltas for them. this._log.info("Reconciling client/server updates"); - gen = this._reconcile.async(this, cont, localUpdates, server.updates); + this._se.reconcile(cont, localUpdates, server.updates); let ret = yield; - gen.close(); let clientChanges = ret.propagations[0]; let serverChanges = ret.propagations[1]; @@ -1145,7 +936,7 @@ BookmarksSyncService.prototype = { this._snapshotVersion = server.maxVersion; this._saveSnapshot(); synced = true; - throw 'close generator'; + return; } if (clientConflicts.length || serverConflicts.length) { @@ -1167,9 +958,8 @@ BookmarksSyncService.prototype = { this._applyCommands(clientChanges); newSnapshot = this._getBookmarks(); - gen = this._detectUpdates.async(this, cont, this._snapshot, newSnapshot); + this._se.detectUpdates(cont, this._snapshot, newSnapshot); let diff = yield; - gen.close(); if (diff.length != 0) { this._log.warn("Commands did not apply correctly"); this._log.debug("Diff from snapshot+commands -> " + @@ -1190,13 +980,12 @@ BookmarksSyncService.prototype = { // conflicts, it should be the same as what the resolver returned newSnapshot = this._getBookmarks(); - gen = this._detectUpdates.async(this, cont, server.snapshot, newSnapshot); + this._se.detectUpdates(cont, server.snapshot, newSnapshot); let serverDelta = yield; - gen.close(); // Log an error if not the same if (!(serverConflicts.length || - this._deepEquals(serverChanges, serverDelta))) + deepEquals(serverChanges, serverDelta))) this._log.warn("Predicted server changes differ from " + "actual server->client diff (can be ignored in many cases)"); @@ -1214,9 +1003,8 @@ BookmarksSyncService.prototype = { if (server.formatVersion != STORAGE_FORMAT_VERSION || this._encryptionChanged) { - gen = this._fullUpload.async(this, cont); + this._fullUpload.async(this, cont); let status = yield; - gen.close(); if (!status) this._log.error("Could not upload files to server"); // eep? @@ -1230,26 +1018,24 @@ BookmarksSyncService.prototype = { this._log.debug("Done encrypting snapshot"); } else { this._log.error("Unknown encryption scheme: " + this.encryption); - throw 'close generator'; + return; } - gen = this._dav.PUT("bookmarks-deltas.json", data, cont); + this._dav.PUT("bookmarks-deltas.json", data, cont); let deltasPut = yield; - gen.close(); let c = 0; for (GUID in this._snapshot) c++; - gen = this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshotVersion, - snapEncryption: server.snapEncryption, - deltasEncryption: this.encryption, - bookmarksCount: c}), cont); + this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshotGUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshotVersion, + snapEncryption: server.snapEncryption, + deltasEncryption: this.encryption, + bookmarksCount: c}), cont); let statusPut = yield; - gen.close(); if (deltasPut.status >= 200 && deltasPut.status < 300 && statusPut.status >= 200 && statusPut.status < 300) { @@ -1267,40 +1053,28 @@ BookmarksSyncService.prototype = { synced = true; } catch (e) { - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { let ok = false; if (locked) { - gen = this._dav.unlock.async(this._dav, cont); + this._dav.unlock.async(this._dav, cont); ok = yield; - gen.close(); } if (ok && synced) { this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); - generatorDone(this, onComplete, true); + generatorDone(this, self, onComplete, true); } else { this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); - generatorDone(this, onComplete, false); + generatorDone(this, self, onComplete, false); } + if (svcLock) + this._unlock(); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, - _onSyncFinished: function BSS__onSyncFinished() { - this._syncGen.close(); - this._syncGen = null; - this._syncing = false; - }, - - _getFolderNodes: function BSS__getFolderNodes(folder) { - let query = this._hsvc.getNewQuery(); - query.setFolders([folder], 1); - return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; - }, - _resetGUIDs: function BSS__resetGUIDs() { this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); @@ -1321,15 +1095,13 @@ BookmarksSyncService.prototype = { } }, - // NOTE: this method can throw 'close generator' _checkStatus: function BSS__checkStatus(code, msg) { if (code >= 200 && code < 300) return; this._log.error(msg + " Error code: " + code); - throw 'close generator'; + throw 'checkStatus failed'; }, - // NOTE: this method can throw 'close generator' _decrypt: function BSS__decrypt(alg, data) { let out; switch (alg) { @@ -1340,7 +1112,7 @@ BookmarksSyncService.prototype = { this._log.debug("Done decrypting data"); } catch (e) { this._log.error("Could not decrypt server snapshot"); - throw 'close generator'; + throw 'decrypt failed'; } break; case "none": @@ -1348,7 +1120,7 @@ BookmarksSyncService.prototype = { break; default: this._log.error("Unknown encryption algorithm: " + alg); - throw 'close generator'; + throw 'decrypt failed'; } return out; }, @@ -1378,7 +1150,7 @@ BookmarksSyncService.prototype = { * combined into a single set. */ _getServerData: function BSS__getServerData(onComplete) { - let cont = yield; + let [self, cont] = yield; let ret = {status: -1, formatVersion: null, maxVersion: null, snapVersion: null, snapEncryption: null, deltasEncryption: null, @@ -1386,10 +1158,9 @@ BookmarksSyncService.prototype = { try { this._log.info("Getting bookmarks status from server"); - let gen = this._dav.GET("bookmarks-status.json", cont); + this._dav.GET("bookmarks-status.json", cont); let resp = yield; let status = resp.status; - gen.close(); switch (status) { case 200: @@ -1402,8 +1173,8 @@ BookmarksSyncService.prototype = { if (status.formatVersion > STORAGE_FORMAT_VERSION) { this._log.error("Server uses storage format v" + status.formatVersion + ", this client understands up to v" + STORAGE_FORMAT_VERSION); - generatorDone(this, onComplete, ret) - throw 'close generator'; + generatorDone(this, self, onComplete, ret) + return; } if (status.formatVersion == 0) { @@ -1425,16 +1196,14 @@ BookmarksSyncService.prototype = { this._log.info("Local snapshot is out of date"); this._log.info("Downloading server snapshot"); - gen = this._dav.GET("bookmarks-snapshot.json", cont); + this._dav.GET("bookmarks-snapshot.json", cont); resp = yield; - gen.close() this._checkStatus(resp.status, "Could not download snapshot."); snap = this._decrypt(status.snapEncryption, resp.responseText); this._log.info("Downloading server deltas"); - gen = this._dav.GET("bookmarks-deltas.json", cont); + this._dav.GET("bookmarks-deltas.json", cont); resp = yield; - gen.close(); this._checkStatus(resp.status, "Could not download deltas."); allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); deltas = eval(uneval(allDeltas)); @@ -1444,9 +1213,8 @@ BookmarksSyncService.prototype = { snap = eval(uneval(this._snapshot)); this._log.info("Downloading server deltas"); - gen = this._dav.GET("bookmarks-deltas.json", cont); + this._dav.GET("bookmarks-deltas.json", cont); resp = yield; - gen.close(); this._checkStatus(resp.status, "Could not download deltas."); allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); deltas = allDeltas.slice(this._snapshotVersion - status.snapVersion); @@ -1456,16 +1224,15 @@ BookmarksSyncService.prototype = { // FIXME: could optimize this case by caching deltas file this._log.info("Downloading server deltas"); - gen = this._dav.GET("bookmarks-deltas.json", cont); + this._dav.GET("bookmarks-deltas.json", cont); resp = yield; - gen.close(); this._checkStatus(resp.status, "Could not download deltas."); allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); deltas = []; } else { // this._snapshotVersion > status.maxVersion this._log.error("Server snapshot is older than local snapshot"); - throw 'close generator'; + return; } for (var i = 0; i < deltas.length; i++) { @@ -1480,9 +1247,8 @@ BookmarksSyncService.prototype = { ret.deltasEncryption = status.deltasEncryption; ret.snapshot = snap; ret.deltas = allDeltas; - gen = this._detectUpdates.async(this, cont, this._snapshot, snap); + this._se.detectUpdates(cont, this._snapshot, snap); ret.updates = yield; - gen.close(); break; case 404: @@ -1492,11 +1258,10 @@ BookmarksSyncService.prototype = { this._snapshotVersion = 0; this._snapshotGUID = null; // in case there are other snapshots out there - gen = this._fullUpload.async(this, cont); + this._fullUpload.async(this, cont); let uploadStatus = yield; - gen.close(); if (!uploadStatus) - throw 'close generator'; + return; this._log.info("Initial upload to server successful"); this._saveSnapshot(); @@ -1519,18 +1284,19 @@ BookmarksSyncService.prototype = { } } catch (e) { - if (e != 'close generator') + if (e != 'checkStatus failed' && + e != 'decrypt failed') this._log.error("Exception caught: " + e.message); } finally { - generatorDone(this, onComplete, ret) + generatorDone(this, self, onComplete, ret) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, _fullUpload: function BSS__fullUpload(onComplete) { - let cont = yield; + let [self, cont] = yield; let ret = false; try { @@ -1543,104 +1309,107 @@ BookmarksSyncService.prototype = { this._log.debug("Done encrypting snapshot"); } else { this._log.error("Unknown encryption scheme: " + this.encryption); - throw 'close generator'; + return; } - let gen = this._dav.PUT("bookmarks-snapshot.json", data, cont); + this._dav.PUT("bookmarks-snapshot.json", data, cont); resp = yield; - gen.close(); this._checkStatus(resp.status, "Could not upload snapshot."); - gen = this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); + this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); resp = yield; - gen.close(); this._checkStatus(resp.status, "Could not upload deltas."); let c = 0; for (GUID in this._snapshot) c++; - gen = this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshotVersion, - maxVersion: this._snapshotVersion, - snapEncryption: this.encryption, - deltasEncryption: "none", - bookmarksCount: c}), cont); + this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshotGUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshotVersion, + maxVersion: this._snapshotVersion, + snapEncryption: this.encryption, + deltasEncryption: "none", + bookmarksCount: c}), cont); resp = yield; - gen.close(); this._checkStatus(resp.status, "Could not upload status file."); this._log.info("Full upload to server successful"); ret = true; } catch (e) { - if (e != 'close generator') + if (e != 'checkStatus failed') this._log.error("Exception caught: " + e.message); } finally { - generatorDone(this, onComplete, ret) + generatorDone(this, self, onComplete, ret) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, - _onLogin: function BSS__onLogin(success) { - this._loginGen.close(); - this._loginGen = null; - if (success) { - this._os.notifyObservers(null, "bookmarks-sync:login", ""); - } else { - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - } - this._passphrase = null; - this._loggingIn = false; - }, + _resetLock: function BSS__resetLock(onComplete) { + let [self, cont] = yield; + let success = false; + let svcLock = this._lock(); - _onResetLock: function BSS__resetLock(success) { - this._forceUnlockGen.close(); - this._forceUnlockGen = null; - if (success) { - this._log.info("Lock reset"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset", ""); - } else { - this._log.warn("Lock reset error"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); + try { + if (!svcLock) + return; + this._log.debug("Resetting server lock"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-start", ""); + + this._dav.forceUnlock.async(this._dav, cont); + success = yield; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (success) { + this._log.debug("Server lock reset successful"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-end", ""); + } else { + this._log.debug("Server lock reset failed"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); + } + if (svcLock) + this._unlock(); + generatorDone(this, self, onComplete, success); + yield; // onComplete is responsible for closing the generator } - this._resettingLock = false; + this._log.warn("generator not properly closed"); }, _resetServer: function BSS__resetServer(onComplete) { - let cont = yield; + let [self, cont] = yield; let done = false; + let svcLock = this._lock(); try { + if (!svcLock) + return; + this._log.debug("Resetting server data"); this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", ""); - let gen = this._dav.lock.async(this._dav, cont); + this._dav.lock.async(this._dav, cont); let locked = yield; - gen.close(); - if (locked) - this._log.info("Lock acquired"); + this._log.debug("Lock acquired"); else { this._log.warn("Could not acquire lock, aborting server reset"); - return; + return; } - gen = this._dav.DELETE("bookmarks-status.json", cont); + this._dav.DELETE("bookmarks-status.json", cont); let statusResp = yield; - gen.close(); - gen = this._dav.DELETE("bookmarks-snapshot.json", cont); + this._dav.DELETE("bookmarks-snapshot.json", cont); let snapshotResp = yield; - gen.close(); - gen = this._dav.DELETE("bookmarks-deltas.json", cont); + this._dav.DELETE("bookmarks-deltas.json", cont); let deltasResp = yield; - gen.close(); - gen = this._dav.unlock.async(this._dav, cont); + this._dav.unlock.async(this._dav, cont); let unlocked = yield; - gen.close(); function ok(code) { if (code >= 200 && code < 300) @@ -1658,59 +1427,61 @@ BookmarksSyncService.prototype = { return; } - this._log.info("Server files deleted"); + this._log.debug("Server files deleted"); done = true; } catch (e) { - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { if (done) { - this._log.info("Server reset completed successfully"); + this._log.debug("Server reset completed successfully"); this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); } else { - this._log.info("Server reset failed"); + this._log.debug("Server reset failed"); this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", ""); } - generatorDone(this, onComplete, done) + if (svcLock) + this._unlock(); + generatorDone(this, self, onComplete, done) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, - _onResetServer: function BSS__resetServer(success) { - this._resetServerGen.close(); - this._resetServerGen = null; - this._resettingServer = false; - }, - _resetClient: function BSS__resetClient(onComplete) { - let cont = yield; + let [self, cont] = yield; let done = false; + let svcLock = this._lock(); try { - this._log.info("Resetting client state"); + if (!svcLock) + return; + this._log.debug("Resetting client state"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-start", ""); + this._snapshot = {}; this._snapshotVersion = -1; this._saveSnapshot(); - - this._log.info("Client snapshot cleared"); done = true; } catch (e) { - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { - generatorDone(this, onComplete, done); + if (done) { + this._log.debug("Client reset completed successfully"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-end", ""); + } else { + this._log.debug("Client reset failed"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-error", ""); + } + if (svcLock) + this._unlock(); + generatorDone(this, self, onComplete, done); + yield; // onComplete is responsible for closing the generator } - }, - - _onResetClient: function BSS__resetClient(success) { - this._resetClientGen.close(); - this._resetClientGen = null; - this._resettingClient = false; + this._log.warn("generator not properly closed"); }, // XPCOM registration @@ -1721,8 +1492,6 @@ BookmarksSyncService.prototype = { Ci.nsIObserver, Ci.nsISupports]), - // nsISupports - // nsIObserver observe: function BSS__observe(subject, topic, data) { @@ -1759,50 +1528,14 @@ BookmarksSyncService.prototype = { } }, - // IBookmarksSyncService - - sync: function BSS_sync() { - if (this._syncing) { - this._log.warn("Sync requested, but already syncing"); - return; - } - this._syncing = true; - this._log.info("Beginning sync"); - let callback = bind2(this, this._onSyncFinished); - this._syncGen = this._doSync.async(this, callback); - }, + // IBookmarksSyncService public methods login: function BSS_login(password, passphrase) { - if (this._loggingIn) { - this._log.warn("Login requested, but already logging in"); - return; - } - - this._log.info("Logging in"); - - if (!this.username) { - this._log.warn("No username set, login failed"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - return; - } - if (!password) - password = this.password; - if (!password) { - this._log.warn("No password given or found in password manager"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - return; - } - - // cache passphrase. if null, we'll try to get it from the pw manager + // cache password & passphrase + // if null, _login() will try to get them from the pw manager + this._password = password; this._passphrase = passphrase; - - this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; - this._log.info("Using server URL: " + this._dav.baseURL); - - this._loggingIn = true; - let callback = bind2(this, this._onLogin); - this._loginGen = this._dav.login.async(this._dav, callback, - this.username, password); + this._login.async(this); }, logout: function BSS_logout() { @@ -1812,136 +1545,361 @@ BookmarksSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:logout", ""); }, + sync: function BSS_sync() { + this._doSync.async(this); + }, + resetLock: function BSS_resetLock() { - if (this._resettingLock) { - this._log.warn("Reset lock requested, but already resetting lock"); - return; - } - this._resettingLock = true; - this._log.info("Resetting lock"); - let callback = bind2(this, this._onResetLock); - this._forceUnlockGen = this._dav.forceUnlock.async(this._dav, callback); + this._resetLock.async(this); }, resetServer: function BSS_resetServer() { - if (this._resettingServer) { - this._log.warn("Reset server requested, but already resetting server"); - return; - } - this._resettingServer = true; - this._log.info("Resetting server data"); - let callback = bind2(this, this._onResetServer); - this._resetServerGen = this._resetServer.async(this, callback); + this._resetServer.async(this); }, resetClient: function BSS_resetClient() { - if (this._resettingClient) { - this._log.warn("Reset client requested, but already resetting client"); - return; - } - this._resettingClient = true; - this._log.info("Resetting client data"); - let callback = bind2(this, this._onResetClient); - this._resetClientGen = this._resetClient.async(this, callback); + this._resetClient.async(this); } }; -function makeFile(path) { - var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - file.initWithPath(path); - return file; +/* + * SyncEngine objects + * Sync engines deal with diff creation and conflict resolution. + * Tree data structures where all nodes have GUIDs only need to be + * subclassed for each data type to implement commandLike and + * itemExists. + */ + +function SyncEngine() { + this._init(); } +SyncEngine.prototype = { + _logName: "Sync", -function makeURI(URIString) { - if (URIString === null || URIString == "") - return null; - let ioservice = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - return ioservice.newURI(URIString, null, null); -} - -function bind2(object, method) { - return function innerBind() { return method.apply(object, arguments); } -} - -Function.prototype.async = function(self, extra_args) { - try { - let args = Array.prototype.slice.call(arguments, 1); - let gen = this.apply(self, args); - gen.next(); // must initialize before sending - gen.send(function(data) { continueGenerator(gen, data); }); - return gen; - } catch (e) { - if (e instanceof StopIteration) { - dump("async warning: generator stopped unexpectedly"); - return null; - } else { - this._log.error("Exception caught: " + e.message); - } - } -} - -function continueGenerator(generator, data) { - try { generator.send(data); } - catch (e) { - if (e instanceof StopIteration) - dump("continueGenerator warning: generator stopped unexpectedly"); - else - dump("Exception caught: " + e.message); - } -} - -// generators called using Function.async can't simply call the -// callback with the return value, since that would cause the calling -// function to end up running (after the yield) from inside the -// generator. Instead, generators can call this method which sets up -// a timer to call the callback from a timer (and cleans up the timer -// to avoid leaks). -function generatorDone(object, callback, retval) { - if (object._timer) - throw "Called generatorDone when there is a timer already set." - - let cb = bind2(object, function(event) { - object._timer = null; - callback(retval); - }); - - object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - object._timer.initWithCallback(new EventListener(cb), - 0, object._timer.TYPE_ONE_SHOT); -} - -function EventListener(handler, eventName) { - this._handler = handler; - this._eventName = eventName; - let logSvc = Cc["@mozilla.org/log4moz/service;1"]. - getService(Ci.ILog4MozService); - this._log = logSvc.getLogger("Service.EventHandler"); -} -EventListener.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), - - // DOM event listener - handleEvent: function EL_handleEvent(event) { - this._log.debug("Handling event " + this._eventName); - this._handler(event); + _init: function SE__init() { + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service." + this._logName); }, - // nsITimerCallback - notify: function EL_notify(timer) { - this._log.trace("Timer fired"); - this._handler(timer); + // FIXME: this won't work for deep objects, or objects with optional properties + _getEdits: function SE__getEdits(a, b) { + let ret = {numProps: 0, props: {}}; + for (prop in a) { + if (!deepEquals(a[prop], b[prop])) { + ret.numProps++; + ret.props[prop] = b[prop]; + } + } + return ret; + }, + + _nodeParents: function SE__nodeParents(GUID, tree) { + return this._nodeParentsInt(GUID, tree, []); + }, + + _nodeParentsInt: function SE__nodeParentsInt(GUID, tree, parents) { + if (!tree[GUID] || !tree[GUID].parentGUID) + return parents; + parents.push(tree[GUID].parentGUID); + return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); + }, + + _detectUpdates: function SE__detectUpdates(onComplete, a, b) { + let [self, cont] = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + let cmds = []; + + try { + for (let GUID in a) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (GUID in b) { + let edits = this._getEdits(a[GUID], b[GUID]); + if (edits.numProps == 0) // no changes - skip + continue; + let parents = this._nodeParents(GUID, b); + cmds.push({action: "edit", GUID: GUID, + depth: parents.length, parents: parents, + data: edits.props}); + } else { + let parents = this._nodeParents(GUID, a); // ??? + cmds.push({action: "remove", GUID: GUID, + depth: parents.length, parents: parents}); + } + } + for (let GUID in b) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (GUID in a) + continue; + let parents = this._nodeParents(GUID, b); + cmds.push({action: "create", GUID: GUID, + depth: parents.length, parents: parents, + data: b[GUID]}); + } + cmds.sort(function(a, b) { + if (a.depth > b.depth) + return 1; + if (a.depth < b.depth) + return -1; + if (a.index > b.index) + return -1; + if (a.index < b.index) + return 1; + return 0; // should never happen, but not a big deal if it does + }); + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + timer = null; + generatorDone(this, self, onComplete, cmds); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _commandLike: function SE__commandLike(a, b) { + // Check that neither command is null, and verify that the GUIDs + // are different (otherwise we need to check for edits) + if (!a || !b || a.GUID == b.GUID) + return false; + + // Check that all other properties are the same + // FIXME: could be optimized... + for (let key in a) { + if (key != "GUID" && !deepEquals(a[key], b[key])) + return false; + } + for (let key in b) { + if (key != "GUID" && !deepEquals(a[key], b[key])) + return false; + } + return true; + }, + + // When we change the GUID of a local item (because we detect it as + // being the same item as a remote one), we need to fix any other + // local items that have it as their parent + _fixParents: function SE__fixParents(list, oldGUID, newGUID) { + for (let i = 0; i < list.length; i++) { + if (!list[i]) + continue; + if (list[i].data.parentGUID == oldGUID) + list[i].data.parentGUID = newGUID; + for (let j = 0; j < list[i].parents.length; j++) { + if (list[i].parents[j] == oldGUID) + list[i].parents[j] = newGUID; + } + } + }, + + _conflicts: function SE__conflicts(a, b) { + if ((a.GUID == b.GUID) && !deepEquals(a, b)) + return true; + return false; + }, + + _getPropagations: function SE__getPropagations(commands, conflicts, propagations) { + for (let i = 0; i < commands.length; i++) { + let alsoConflicts = function(elt) { + return (elt.action == "create" || elt.action == "remove") && + commands[i].parents.indexOf(elt.GUID) >= 0; + }; + if (conflicts.some(alsoConflicts)) + conflicts.push(commands[i]); + + let cmdConflicts = function(elt) { + return elt.GUID == commands[i].GUID; + }; + if (!conflicts.some(cmdConflicts)) + propagations.push(commands[i]); + } + }, + + _itemExists: function SE__itemExists(GUID) { + this._log.error("itemExists needs to be subclassed"); + return false; + }, + + _reconcile: function SE__reconcile(onComplete, listA, listB) { + let [self, cont] = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + let propagations = [[], []]; + let conflicts = [[], []]; + let ret = {propagations: propagations, conflicts: conflicts}; + + try { + for (let i = 0; i < listA.length; i++) { + for (let j = 0; j < listB.length; j++) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (deepEquals(listA[i], listB[j])) { + delete listA[i]; + delete listB[j]; + } else if (this._commandLike(listA[i], listB[j])) { + this._fixParents(listA, listA[i].GUID, listB[j].GUID); + listB[j].data = {GUID: listB[j].GUID}; + listB[j].GUID = listA[i].GUID; + listB[j].action = "edit"; + delete listA[i]; + } + + // watch out for create commands with GUIDs that already exist + if (listB[j] && listB[j].action == "create" && + this._itemExists(listB[j].GUID)) { + this._log.error("Remote command has GUID that already exists " + + "locally. Dropping command."); + delete listB[j]; + } + } + } + + listA = listA.filter(function(elt) { return elt }); + listB = listB.filter(function(elt) { return elt }); + + for (let i = 0; i < listA.length; i++) { + for (let j = 0; j < listB.length; j++) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (this._conflicts(listA[i], listB[j]) || + this._conflicts(listB[j], listA[i])) { + if (!conflicts[0].some( + function(elt) { return elt.GUID == listA[i].GUID })) + conflicts[0].push(listA[i]); + if (!conflicts[1].some( + function(elt) { return elt.GUID == listB[j].GUID })) + conflicts[1].push(listB[j]); + } + } + } + + this._getPropagations(listA, conflicts[0], propagations[1]); + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + this._getPropagations(listB, conflicts[1], propagations[0]); + ret = {propagations: propagations, conflicts: conflicts}; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + timer = null; + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + // Public methods + + detectUpdates: function SE_detectUpdates(onComplete, a, b) { + return this._detectUpdates.async(this, onComplete, a, b); + }, + + reconcile: function SE_reconcile(onComplete, listA, listB) { + return this._reconcile.async(this, onComplete, listA, listB); } }; -function xpath(xmlDoc, xpathString) { - let root = xmlDoc.ownerDocument == null ? - xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement - let nsResolver = xmlDoc.createNSResolver(root); - - return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, - Ci.nsIDOMXPathResult.ANY_TYPE, null); +function BookmarksSyncEngine() { + this._init(); } +BookmarksSyncEngine.prototype = { + _logName: "BMSync", + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + // NOTE: Needs to be subclassed + _itemExists: function BSE__itemExists(GUID) { + return this._bms.getItemIdForGUID(GUID) >= 0; + }, + + commandLike: function BCC_commandLike(a, b) { + // Check that neither command is null, that their actions, types, + // and parents are the same, and that they don't have the same + // GUID. + // Items with the same GUID do not qualify for 'likeness' because + // we already consider them to be the same object, and therefore + // we need to process any edits. + // The parent GUID check works because reconcile() fixes up the + // parent GUIDs as it runs, and the command list is sorted by + // depth + if (!a || !b || + a.action != b.action || + a.data.type != b.data.type || + a.data.parentGUID != b.data.parentGUID || + a.GUID == b.GUID) + return false; + + // Bookmarks are allowed to be in a different index as long as + // they are in the same folder. Folders and separators must be at + // the same index to qualify for 'likeness'. + switch (a.data.type) { + case "bookmark": + if (a.data.URI == b.data.URI && + a.data.title == b.data.title) + return true; + return false; + case "query": + if (a.data.URI == b.data.URI && + a.data.title == b.data.title) + return true; + return false; + case "microsummary": + if (a.data.URI == b.data.URI && + a.data.generatorURI == b.data.generatorURI) + return true; + return false; + case "folder": + if (a.index == b.index && + a.data.title == b.data.title) + return true; + return false; + case "livemark": + if (a.data.title == b.data.title && + a.data.siteURI == b.data.siteURI && + a.data.feedURI == b.data.feedURI) + return true; + return false; + case "separator": + if (a.index == b.index) + return true; + return false; + default: + this._log.error("commandLike: Unknown item type: " + uneval(a)); + return false; + } + } +}; +BookmarksSyncEngine.prototype.__proto__ = new SyncEngine(); + +/* + * DAV object + * Abstracts the raw DAV commands + */ function DAVCollection(baseURL) { this._baseURL = baseURL; @@ -1950,7 +1908,6 @@ function DAVCollection(baseURL) { getService(Ci.ILog4MozService); this._log = logSvc.getLogger("Service.DAV"); } - DAVCollection.prototype = { __dp: null, get _dp() { @@ -1975,7 +1932,7 @@ DAVCollection.prototype = { }, _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { - let cont = yield; + let [self, cont] = yield; let ret; try { @@ -2016,11 +1973,10 @@ DAVCollection.prototype = { this._log.warn("_makeRequest: got status " + ret.status); } catch (e) { - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { - generatorDone(this, onComplete, ret); + generatorDone(this, self, onComplete, ret); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -2073,12 +2029,12 @@ DAVCollection.prototype = { // Login / Logout login: function DC_login(onComplete, username, password) { - let cont = yield; + let [self, cont] = yield; try { if (this._loggedIn) { this._log.debug("Login requested, but already logged in"); - throw 'close generator'; + return; } this._log.info("Logging in"); @@ -2087,25 +2043,23 @@ DAVCollection.prototype = { this._auth = "Basic " + btoa(username + ":" + password); // Make a call to make sure it's working - let gen = this.GET("", cont); + this.GET("", cont); let resp = yield; - gen.close(); if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - throw 'close generator'; + return; this._loggedIn = true; } catch (e) { - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { if (this._loggedIn) this._log.info("Logged in"); else this._log.warn("Could not log in"); - generatorDone(this, onComplete, this._loggedIn); + generatorDone(this, self, onComplete, this._loggedIn); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -2120,43 +2074,41 @@ DAVCollection.prototype = { // Locking _getActiveLock: function DC__getActiveLock(onComplete) { - let cont = yield; + let [self, cont] = yield; let ret = null; try { this._log.info("Getting active lock token"); - let gen = this.PROPFIND("", - "" + - "" + - " " + - "", cont); + this.PROPFIND("", + "" + + "" + + " " + + "", cont); let resp = yield; - gen.close(); if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - throw 'close generator'; + return; let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); ret = token.textContent; } catch (e) { - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { if (ret) this._log.debug("Found an active lock token"); else this._log.debug("No active lock token found"); - generatorDone(this, onComplete, ret); + generatorDone(this, self, onComplete, ret); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, lock: function DC_lock(onComplete) { - let cont = yield; + let [self, cont] = yield; this._token = null; try { @@ -2164,20 +2116,19 @@ DAVCollection.prototype = { if (this._token) { this._log.debug("Lock called, but we already hold a token"); - throw 'close generator'; + return; } - let gen = this.LOCK("", - "\n" + - "\n" + - " \n" + - " \n" + - "", cont); + this.LOCK("", + "\n" + + "\n" + + " \n" + + " \n" + + "", cont); let resp = yield; - gen.close(); if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - throw 'close generator'; + return; let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); @@ -2185,50 +2136,47 @@ DAVCollection.prototype = { this._token = token.textContent; } catch (e){ - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { if (this._token) this._log.info("Lock acquired"); else this._log.warn("Could not acquire lock"); - generatorDone(this, onComplete, this._token); + generatorDone(this, self, onComplete, this._token); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, unlock: function DC_unlock(onComplete) { - let cont = yield; + let [self, cont] = yield; try { this._log.info("Releasing lock"); if (this._token === null) { this._log.debug("Unlock called, but we don't hold a token right now"); - throw 'close generator'; + return; } - let gen = this.UNLOCK("", cont); + this.UNLOCK("", cont); let resp = yield; - gen.close(); if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - throw 'close generator'; + return; this._token = null; } catch (e){ - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { if (this._token) { this._log.info("Could not release lock"); - generatorDone(this, onComplete, false); + generatorDone(this, self, onComplete, false); } else { this._log.info("Lock released (or we didn't have one)"); - generatorDone(this, onComplete, true); + generatorDone(this, self, onComplete, true); } yield; // onComplete is responsible for closing the generator } @@ -2236,62 +2184,56 @@ DAVCollection.prototype = { }, forceUnlock: function DC_forceUnlock(onComplete) { - let cont = yield; + let [self, cont] = yield; let unlocked = true; try { this._log.info("Forcibly releasing any server locks"); - let gen = this._getActiveLock.async(this, cont); + this._getActiveLock.async(this, cont); this._token = yield; - gen.close(); if (!this._token) { this._log.info("No server lock found"); - throw 'close generator'; + return; } this._log.info("Server lock found, unlocking"); - gen = this.unlock.async(this, cont); + this.unlock.async(this, cont); unlocked = yield; - gen.close(); } catch (e){ - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { if (unlocked) this._log.debug("Lock released"); else this._log.debug("No lock released"); - generatorDone(this, onComplete, unlocked); + generatorDone(this, self, onComplete, unlocked); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, stealLock: function DC_stealLock(onComplete) { - let cont = yield; + let [self, cont] = yield; let stolen = null; try { - let gen = this.forceUnlock.async(this, cont); + this.forceUnlock.async(this, cont); let unlocked = yield; - gen.close(); if (unlocked) { - gen = this.lock.async(this, cont); + this.lock.async(this, cont); stolen = yield; - gen.close(); } } catch (e){ - if (e != 'close generator') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + e.message); } finally { - generatorDone(this, onComplete, stolen); + generatorDone(this, self, onComplete, stolen); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -2299,7 +2241,11 @@ DAVCollection.prototype = { }; -// Taken from nsMicrosummaryService.js and massaged slightly +/* + * Auth provider object + * Taken from nsMicrosummaryService.js and massaged slightly + */ + function DummyAuthProvider() {} DummyAuthProvider.prototype = { // Implement notification callback interfaces so we can suppress UI @@ -2445,6 +2391,141 @@ DummyAuthProvider.prototype = { } }; +/* + * Event listener object + * Used to handle XMLHttpRequest and nsITimer callbacks + */ + +function EventListener(handler, eventName) { + this._handler = handler; + this._eventName = eventName; + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service.EventHandler"); +} +EventListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), + + // DOM event listener + handleEvent: function EL_handleEvent(event) { + this._log.debug("Handling event " + this._eventName); + this._handler(event); + }, + + // nsITimerCallback + notify: function EL_notify(timer) { + this._log.trace("Timer fired"); + this._handler(timer); + } +}; + +/* + * Utility functions + */ + +function deepEquals(a, b) { + if (!a && !b) + return true; + if (!a || !b) + return false; + + if (typeof(a) != "object" && typeof(b) != "object") + return a == b; + if (typeof(a) != "object" || typeof(b) != "object") + return false; + + for (let key in a) { + if (typeof(a[key]) == "object") { + if (!typeof(b[key]) == "object") + return false; + if (!deepEquals(a[key], b[key])) + return false; + } else { + if (a[key] != b[key]) + return false; + } + } + return true; +} + +function makeFile(path) { + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return file; +} + +function makeURI(URIString) { + if (URIString === null || URIString == "") + return null; + let ioservice = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return ioservice.newURI(URIString, null, null); +} + +function xpath(xmlDoc, xpathString) { + let root = xmlDoc.ownerDocument == null ? + xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement + let nsResolver = xmlDoc.createNSResolver(root); + + return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, + Ci.nsIDOMXPathResult.ANY_TYPE, null); +} + +function bind2(object, method) { + return function innerBind() { return method.apply(object, arguments); } +} + +Function.prototype.async = function(self, extra_args) { + try { + let args = Array.prototype.slice.call(arguments, 1); + let gen = this.apply(self, args); + gen.next(); // must initialize before sending + gen.send([gen, function(data) {continueGenerator(gen, data);}]); + return gen; + } catch (e) { + if (e instanceof StopIteration) { + dump("async warning: generator stopped unexpectedly"); + return null; + } else { + this._log.error("Exception caught: " + e.message); + } + } +} + +function continueGenerator(generator, data) { + try { generator.send(data); } + catch (e) { + if (e instanceof StopIteration) + dump("continueGenerator warning: generator stopped unexpectedly"); + else + dump("Exception caught: " + e.message); + } +} + +// generators created using Function.async can't simply call the +// callback with the return value, since that would cause the calling +// function to end up running (after the yield) from inside the +// generator. Instead, generators can call this method which sets up +// a timer to call the callback from a timer (and cleans up the timer +// to avoid leaks). It also closes generators after the timeout, to +// keep things clean. +function generatorDone(object, generator, callback, retval) { + if (object._timer) + throw "Called generatorDone when there is a timer already set." + + let cb = bind2(object, function(event) { + generator.close(); + generator = null; + object._timer = null; + if (callback) + callback(retval); + }); + + object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + object._timer.initWithCallback(new EventListener(cb), + 0, object._timer.TYPE_ONE_SHOT); +} + function NSGetModule(compMgr, fileSpec) { return XPCOMUtils.generateModule([BookmarksSyncService]); } From f3107bf16233ff6363f37d6b67ec77726cb03460 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 28 Nov 2007 14:47:40 -0800 Subject: [PATCH 0088/1860] more refactoring --- services/sync/BookmarksSyncService.js | 724 ++++++++++++++------------ 1 file changed, 397 insertions(+), 327 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 5a9d68117b5c..49ce87a6ec47 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -72,22 +72,6 @@ BookmarksSyncService.prototype = { return this.__bms; }, - __hsvc: null, - get _hsvc() { - if (!this.__hsvc) - this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - return this.__hsvc; - }, - - __ans: null, - get _ans() { - if (!this.__ans) - this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - return this.__ans; - }, - __ts: null, get _ts() { if (!this.__ts) @@ -135,11 +119,18 @@ BookmarksSyncService.prototype = { return this.__dav; }, - __se: null, - get _se() { - if (!this.__se) - this.__se = new BookmarksSyncEngine(); - return this.__se; + __sync: null, + get _sync() { + if (!this.__sync) + this.__sync = new BookmarksSyncCore(); + return this.__sync; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new BookmarksStore(); + return this.__store; }, // Logger object @@ -449,59 +440,6 @@ BookmarksSyncService.prototype = { } }, - _wrapNode: function BSS__wrapNode(node) { - var items = {}; - this._wrapNodeInternal(node, items, null, null); - return items; - }, - - _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) { - let GUID = this._bms.getItemGUID(node.itemId); - let item = {parentGUID: parentGUID, - index: index}; - - if (node.type == node.RESULT_TYPE_FOLDER) { - if (this._ls.isLivemark(node.itemId)) { - item.type = "livemark"; - let siteURI = this._ls.getSiteURI(node.itemId); - let feedURI = this._ls.getFeedURI(node.itemId); - item.siteURI = siteURI? siteURI.spec : ""; - item.feedURI = feedURI? feedURI.spec : ""; - } else { - item.type = "folder"; - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._wrapNodeInternal(node.getChild(i), items, GUID, i); - } - } - item.title = node.title; - } else if (node.type == node.RESULT_TYPE_URI || - node.type == node.RESULT_TYPE_QUERY) { - if (this._ms.hasMicrosummary(node.itemId)) { - item.type = "microsummary"; - let micsum = this._ms.getMicrosummary(node.itemId); - item.generatorURI = micsum.generator.uri.spec; // breaks local generators - } else if (node.type == node.RESULT_TYPE_QUERY) { - item.type = "query"; - item.title = node.title; - } else { - item.type = "bookmark"; - item.title = node.title; - } - item.URI = node.uri; - item.tags = this._ts.getTagsForURI(makeURI(node.uri)); - item.keyword = this._bms.getKeywordForBookmark(node.itemId); - } else if (node.type == node.RESULT_TYPE_SEPARATOR) { - item.type = "separator"; - } else { - this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); - return; - } - - items[GUID] = item; - }, - _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { for (let i = 0; i < commands.length; i++) { this._log.debug("Applying cmd to obj: " + uneval(commands[i])); @@ -537,204 +475,6 @@ BookmarksSyncService.prototype = { return obj; }, - // Applies commands to the places db - _applyCommands: function BSS__applyCommands(commandList) { - for (var i = 0; i < commandList.length; i++) { - var command = commandList[i]; - this._log.debug("Processing command: " + uneval(command)); - switch (command["action"]) { - case "create": - this._createCommand(command); - break; - case "remove": - this._removeCommand(command); - break; - case "edit": - this._editCommand(command); - break; - default: - this._log.error("unknown action in command: " + command["action"]); - break; - } - } - }, - - _createCommand: function BSS__createCommand(command) { - let newId; - let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); - - if (parentId < 0) { - this._log.warn("Creating node with unknown parent -> reparenting to root"); - parentId = this._bms.bookmarksMenuFolder; - } - - switch (command.data.type) { - case "query": - case "bookmark": - case "microsummary": - this._log.info(" -> creating bookmark \"" + command.data.title + "\""); - let URI = makeURI(command.data.URI); - newId = this._bms.insertBookmark(parentId, - URI, - command.data.index, - command.data.title); - this._ts.untagURI(URI, null); - this._ts.tagURI(URI, command.data.tags); - this._bms.setKeywordForBookmark(newId, command.data.keyword); - - if (command.data.type == "microsummary") { - this._log.info(" \-> is a microsummary"); - let genURI = makeURI(command.data.generatorURI); - try { - let micsum = this._ms.createMicrosummary(URI, genURI); - this._ms.setMicrosummary(newId, micsum); - } - catch(ex) { /* ignore "missing local generator" exceptions */ } - } - break; - case "folder": - this._log.info(" -> creating folder \"" + command.data.title + "\""); - newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); - break; - case "livemark": - this._log.info(" -> creating livemark \"" + command.data.title + "\""); - newId = this._ls.createLivemark(parentId, - command.data.title, - makeURI(command.data.siteURI), - makeURI(command.data.feedURI), - command.data.index); - break; - case "separator": - this._log.info(" -> creating separator"); - newId = this._bms.insertSeparator(parentId, command.data.index); - break; - default: - this._log.error("_createCommand: Unknown item type: " + command.data.type); - break; - } - if (newId) - this._bms.setItemGUID(newId, command.GUID); - }, - - _removeCommand: function BSS__removeCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId < 0) { - this._log.warn("Attempted to remove item " + command.GUID + - ", but it does not exist. Skipping."); - return; - } - var type = this._bms.getItemType(itemId); - - switch (type) { - case this._bms.TYPE_BOOKMARK: - this._log.info(" -> removing bookmark " + command.GUID); - this._bms.removeItem(itemId); - break; - case this._bms.TYPE_FOLDER: - this._log.info(" -> removing folder " + command.GUID); - this._bms.removeFolder(itemId); - break; - case this._bms.TYPE_SEPARATOR: - this._log.info(" -> removing separator " + command.GUID); - this._bms.removeItem(itemId); - break; - default: - this._log.error("removeCommand: Unknown item type: " + type); - break; - } - }, - - _editCommand: function BSS__editCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId < 0) { - this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); - return; - } - - for (let key in command.data) { - switch (key) { - case "GUID": - var existing = this._bms.getItemIdForGUID(command.data.GUID); - if (existing < 0) - this._bms.setItemGUID(itemId, command.data.GUID); - else - this._log.warn("Can't change GUID " + command.GUID + - " to " + command.data.GUID + ": GUID already exists."); - break; - case "title": - this._bms.setItemTitle(itemId, command.data.title); - break; - case "URI": - this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI)); - break; - case "index": - this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), - command.data.index); - break; - case "parentGUID": - let index = -1; - if (command.data.index && command.data.index >= 0) - index = command.data.index; - this._bms.moveItem( - itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); - break; - case "tags": - let tagsURI = this._bms.getBookmarkURI(itemId); - this._ts.untagURI(URI, null); - this._ts.tagURI(tagsURI, command.data.tags); - break; - case "keyword": - this._bms.setKeywordForBookmark(itemId, command.data.keyword); - break; - case "generatorURI": - let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); - let genURI = makeURI(command.data.generatorURI); - let micsum = this._ms.createMicrosummary(micsumURI, genURI); - this._ms.setMicrosummary(itemId, micsum); - break; - case "siteURI": - this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); - break; - case "feedURI": - this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); - break; - default: - this._log.warn("Can't change item property: " + key); - break; - } - } - }, - - _getFolderNodes: function BSS__getFolderNodes(folder) { - let query = this._hsvc.getNewQuery(); - query.setFolders([folder], 1); - return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; - }, - - _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { - return this._wrapNode(this._getFolderNodes(folder)); - }, - - _getBookmarks: function BSS__getBookmarks() { - let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder); - let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder); - let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder); - - for (let guid in unfiled) { - if (!(guid in filed)) - filed[guid] = unfiled[guid]; - } - - for (let guid in toolbar) { - if (!(guid in filed)) - filed[guid] = toolbar[guid]; - } - - return filed; // (combined) - }, - _mungeNodes: function BSS__mungeNodes(nodes) { let json = uneval(nodes); json = json.replace(/:{type/g, ":\n\t{type"); @@ -762,8 +502,6 @@ BookmarksSyncService.prototype = { return json; }, - // IBookmarksSyncService internal implementation - _lock: function BSS__lock() { if (this._locked) { this._log.warn("Service lock failed: already locked"); @@ -779,6 +517,8 @@ BookmarksSyncService.prototype = { this._log.debug("Service lock released"); }, + // IBookmarksSyncService internal implementation + _login: function BSS__login(onComplete) { let [self, cont] = yield; let success = false; @@ -894,8 +634,8 @@ BookmarksSyncService.prototype = { // 2) Generate local deltas from snapshot -> current client status - let localJson = this._getBookmarks(); - this._se.detectUpdates(cont, this._snapshot, localJson); + let localJson = this._store.wrap(); + this._sync.detectUpdates(cont, this._snapshot, localJson); let localUpdates = yield; this._log.debug("local json:\n" + this._mungeNodes(localJson)); @@ -912,7 +652,7 @@ BookmarksSyncService.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. this._log.info("Reconciling client/server updates"); - this._se.reconcile(cont, localUpdates, server.updates); + this._sync.reconcile(cont, localUpdates, server.updates); let ret = yield; let clientChanges = ret.propagations[0]; @@ -955,10 +695,10 @@ BookmarksSyncService.prototype = { this._snapshot = this._applyCommandsToObj(clientChanges, localJson); this._snapshotVersion = server.maxVersion; - this._applyCommands(clientChanges); - newSnapshot = this._getBookmarks(); + this._store.applyCommands(clientChanges); + newSnapshot = this._store.wrap(); - this._se.detectUpdates(cont, this._snapshot, newSnapshot); + this._sync.detectUpdates(cont, this._snapshot, newSnapshot); let diff = yield; if (diff.length != 0) { this._log.warn("Commands did not apply correctly"); @@ -979,8 +719,8 @@ BookmarksSyncService.prototype = { // current client snapshot. In the case where there are no // conflicts, it should be the same as what the resolver returned - newSnapshot = this._getBookmarks(); - this._se.detectUpdates(cont, server.snapshot, newSnapshot); + newSnapshot = this._store.wrap(); + this._sync.detectUpdates(cont, server.snapshot, newSnapshot); let serverDelta = yield; // Log an error if not the same @@ -1075,26 +815,6 @@ BookmarksSyncService.prototype = { this._log.warn("generator not properly closed"); }, - _resetGUIDs: function BSS__resetGUIDs() { - this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); - }, - - _resetGUIDsInt: function BSS__resetGUIDsInt(node) { - if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) - this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); - - if (node.type == node.RESULT_TYPE_FOLDER && - !this._ls.isLivemark(node.itemId)) { - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._resetGUIDsInt(node.getChild(i)); - } - } - }, - _checkStatus: function BSS__checkStatus(code, msg) { if (code >= 200 && code < 300) return; @@ -1185,7 +905,7 @@ BookmarksSyncService.prototype = { if (status.GUID != this._snapshotGUID) { this._log.info("Remote/local sync GUIDs do not match. " + "Forcing initial sync."); - this._resetGUIDs(); + this._store.resetGUIDs(); this._snapshot = {}; this._snapshotVersion = -1; this._snapshotGUID = status.GUID; @@ -1247,14 +967,14 @@ BookmarksSyncService.prototype = { ret.deltasEncryption = status.deltasEncryption; ret.snapshot = snap; ret.deltas = allDeltas; - this._se.detectUpdates(cont, this._snapshot, snap); + this._sync.detectUpdates(cont, this._snapshot, snap); ret.updates = yield; break; case 404: this._log.info("Server has no status file, Initial upload to server"); - this._snapshot = this._getBookmarks(); + this._snapshot = this._store.wrap(); this._snapshotVersion = 0; this._snapshotGUID = null; // in case there are other snapshots out there @@ -1563,27 +1283,27 @@ BookmarksSyncService.prototype = { }; /* - * SyncEngine objects - * Sync engines deal with diff creation and conflict resolution. + * SyncCore objects + * Sync cores deal with diff creation and conflict resolution. * Tree data structures where all nodes have GUIDs only need to be * subclassed for each data type to implement commandLike and * itemExists. */ -function SyncEngine() { +function SyncCore() { this._init(); } -SyncEngine.prototype = { +SyncCore.prototype = { _logName: "Sync", - _init: function SE__init() { + _init: function SC__init() { let logSvc = Cc["@mozilla.org/log4moz/service;1"]. getService(Ci.ILog4MozService); this._log = logSvc.getLogger("Service." + this._logName); }, // FIXME: this won't work for deep objects, or objects with optional properties - _getEdits: function SE__getEdits(a, b) { + _getEdits: function SC__getEdits(a, b) { let ret = {numProps: 0, props: {}}; for (prop in a) { if (!deepEquals(a[prop], b[prop])) { @@ -1594,18 +1314,18 @@ SyncEngine.prototype = { return ret; }, - _nodeParents: function SE__nodeParents(GUID, tree) { + _nodeParents: function SC__nodeParents(GUID, tree) { return this._nodeParentsInt(GUID, tree, []); }, - _nodeParentsInt: function SE__nodeParentsInt(GUID, tree, parents) { + _nodeParentsInt: function SC__nodeParentsInt(GUID, tree, parents) { if (!tree[GUID] || !tree[GUID].parentGUID) return parents; parents.push(tree[GUID].parentGUID); return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); }, - _detectUpdates: function SE__detectUpdates(onComplete, a, b) { + _detectUpdates: function SC__detectUpdates(onComplete, a, b) { let [self, cont] = yield; let listener = new EventListener(cont); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); @@ -1667,7 +1387,7 @@ SyncEngine.prototype = { this._log.warn("generator not properly closed"); }, - _commandLike: function SE__commandLike(a, b) { + _commandLike: function SC__commandLike(a, b) { // Check that neither command is null, and verify that the GUIDs // are different (otherwise we need to check for edits) if (!a || !b || a.GUID == b.GUID) @@ -1689,7 +1409,7 @@ SyncEngine.prototype = { // When we change the GUID of a local item (because we detect it as // being the same item as a remote one), we need to fix any other // local items that have it as their parent - _fixParents: function SE__fixParents(list, oldGUID, newGUID) { + _fixParents: function SC__fixParents(list, oldGUID, newGUID) { for (let i = 0; i < list.length; i++) { if (!list[i]) continue; @@ -1702,13 +1422,13 @@ SyncEngine.prototype = { } }, - _conflicts: function SE__conflicts(a, b) { + _conflicts: function SC__conflicts(a, b) { if ((a.GUID == b.GUID) && !deepEquals(a, b)) return true; return false; }, - _getPropagations: function SE__getPropagations(commands, conflicts, propagations) { + _getPropagations: function SC__getPropagations(commands, conflicts, propagations) { for (let i = 0; i < commands.length; i++) { let alsoConflicts = function(elt) { return (elt.action == "create" || elt.action == "remove") && @@ -1725,12 +1445,12 @@ SyncEngine.prototype = { } }, - _itemExists: function SE__itemExists(GUID) { + _itemExists: function SC__itemExists(GUID) { this._log.error("itemExists needs to be subclassed"); return false; }, - _reconcile: function SE__reconcile(onComplete, listA, listB) { + _reconcile: function SC__reconcile(onComplete, listA, listB) { let [self, cont] = yield; let listener = new EventListener(cont); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); @@ -1809,19 +1529,19 @@ SyncEngine.prototype = { // Public methods - detectUpdates: function SE_detectUpdates(onComplete, a, b) { + detectUpdates: function SC_detectUpdates(onComplete, a, b) { return this._detectUpdates.async(this, onComplete, a, b); }, - reconcile: function SE_reconcile(onComplete, listA, listB) { + reconcile: function SC_reconcile(onComplete, listA, listB) { return this._reconcile.async(this, onComplete, listA, listB); } }; -function BookmarksSyncEngine() { +function BookmarksSyncCore() { this._init(); } -BookmarksSyncEngine.prototype = { +BookmarksSyncCore.prototype = { _logName: "BMSync", __bms: null, @@ -1833,11 +1553,11 @@ BookmarksSyncEngine.prototype = { }, // NOTE: Needs to be subclassed - _itemExists: function BSE__itemExists(GUID) { + _itemExists: function BSC__itemExists(GUID) { return this._bms.getItemIdForGUID(GUID) >= 0; }, - commandLike: function BCC_commandLike(a, b) { + commandLike: function BSC_commandLike(a, b) { // Check that neither command is null, that their actions, types, // and parents are the same, and that they don't have the same // GUID. @@ -1894,7 +1614,357 @@ BookmarksSyncEngine.prototype = { } } }; -BookmarksSyncEngine.prototype.__proto__ = new SyncEngine(); +BookmarksSyncCore.prototype.__proto__ = new SyncCore(); + +/* + * Data Stores + * These can wrap, serialize items and apply commands + */ + +function Store() { + this._init(); +} +Store.prototype = { + _logName: "Store", + + _init: function Store__init() { + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service." + this._logName); + }, + + wrap: function Store_wrap() { + }, + + applyCommands: function Store_applyCommands(commandList) { + } +}; + +function BookmarksStore() { + this._init(); +} +BookmarksStore.prototype = { + _logName: "BStore", + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + __hsvc: null, + get _hsvc() { + if (!this.__hsvc) + this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + return this.__hsvc; + }, + + __ls: null, + get _ls() { + if (!this.__ls) + this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. + getService(Ci.nsILivemarkService); + return this.__ls; + }, + + __ms: null, + get _ms() { + if (!this.__ms) + this.__ms = Cc["@mozilla.org/microsummary/service;1"]. + getService(Ci.nsIMicrosummaryService); + return this.__ms; + }, + + __ts: null, + get _ts() { + if (!this.__ts) + this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + return this.__ts; + }, + + __ans: null, + get _ans() { + if (!this.__ans) + this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + return this.__ans; + }, + + _getFolderNodes: function BSS__getFolderNodes(folder) { + let query = this._hsvc.getNewQuery(); + query.setFolders([folder], 1); + return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + }, + + _wrapNode: function BSS__wrapNode(node) { + var items = {}; + this._wrapNodeInternal(node, items, null, null); + return items; + }, + + _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) { + let GUID = this._bms.getItemGUID(node.itemId); + let item = {parentGUID: parentGUID, + index: index}; + + if (node.type == node.RESULT_TYPE_FOLDER) { + if (this._ls.isLivemark(node.itemId)) { + item.type = "livemark"; + let siteURI = this._ls.getSiteURI(node.itemId); + let feedURI = this._ls.getFeedURI(node.itemId); + item.siteURI = siteURI? siteURI.spec : ""; + item.feedURI = feedURI? feedURI.spec : ""; + } else { + item.type = "folder"; + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._wrapNodeInternal(node.getChild(i), items, GUID, i); + } + } + item.title = node.title; + } else if (node.type == node.RESULT_TYPE_URI || + node.type == node.RESULT_TYPE_QUERY) { + if (this._ms.hasMicrosummary(node.itemId)) { + item.type = "microsummary"; + let micsum = this._ms.getMicrosummary(node.itemId); + item.generatorURI = micsum.generator.uri.spec; // breaks local generators + } else if (node.type == node.RESULT_TYPE_QUERY) { + item.type = "query"; + item.title = node.title; + } else { + item.type = "bookmark"; + item.title = node.title; + } + item.URI = node.uri; + item.tags = this._ts.getTagsForURI(makeURI(node.uri)); + item.keyword = this._bms.getKeywordForBookmark(node.itemId); + } else if (node.type == node.RESULT_TYPE_SEPARATOR) { + item.type = "separator"; + } else { + this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); + return; + } + + items[GUID] = item; + }, + + _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { + return this._wrapNode(this._getFolderNodes(folder)); + }, + + _resetGUIDsInt: function BSS__resetGUIDsInt(node) { + if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) + this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); + + if (node.type == node.RESULT_TYPE_FOLDER && + !this._ls.isLivemark(node.itemId)) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._resetGUIDsInt(node.getChild(i)); + } + } + }, + + _createCommand: function BStore__createCommand(command) { + let newId; + let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); + + if (parentId < 0) { + this._log.warn("Creating node with unknown parent -> reparenting to root"); + parentId = this._bms.bookmarksMenuFolder; + } + + switch (command.data.type) { + case "query": + case "bookmark": + case "microsummary": + this._log.info(" -> creating bookmark \"" + command.data.title + "\""); + let URI = makeURI(command.data.URI); + newId = this._bms.insertBookmark(parentId, + URI, + command.data.index, + command.data.title); + this._ts.untagURI(URI, null); + this._ts.tagURI(URI, command.data.tags); + this._bms.setKeywordForBookmark(newId, command.data.keyword); + + if (command.data.type == "microsummary") { + this._log.info(" \-> is a microsummary"); + let genURI = makeURI(command.data.generatorURI); + try { + let micsum = this._ms.createMicrosummary(URI, genURI); + this._ms.setMicrosummary(newId, micsum); + } + catch(ex) { /* ignore "missing local generator" exceptions */ } + } + break; + case "folder": + this._log.info(" -> creating folder \"" + command.data.title + "\""); + newId = this._bms.createFolder(parentId, + command.data.title, + command.data.index); + break; + case "livemark": + this._log.info(" -> creating livemark \"" + command.data.title + "\""); + newId = this._ls.createLivemark(parentId, + command.data.title, + makeURI(command.data.siteURI), + makeURI(command.data.feedURI), + command.data.index); + break; + case "separator": + this._log.info(" -> creating separator"); + newId = this._bms.insertSeparator(parentId, command.data.index); + break; + default: + this._log.error("_createCommand: Unknown item type: " + command.data.type); + break; + } + if (newId) + this._bms.setItemGUID(newId, command.GUID); + }, + + _removeCommand: function BStore__removeCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Attempted to remove item " + command.GUID + + ", but it does not exist. Skipping."); + return; + } + var type = this._bms.getItemType(itemId); + + switch (type) { + case this._bms.TYPE_BOOKMARK: + this._log.info(" -> removing bookmark " + command.GUID); + this._bms.removeItem(itemId); + break; + case this._bms.TYPE_FOLDER: + this._log.info(" -> removing folder " + command.GUID); + this._bms.removeFolder(itemId); + break; + case this._bms.TYPE_SEPARATOR: + this._log.info(" -> removing separator " + command.GUID); + this._bms.removeItem(itemId); + break; + default: + this._log.error("removeCommand: Unknown item type: " + type); + break; + } + }, + + _editCommand: function BStore__editCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); + return; + } + + for (let key in command.data) { + switch (key) { + case "GUID": + var existing = this._bms.getItemIdForGUID(command.data.GUID); + if (existing < 0) + this._bms.setItemGUID(itemId, command.data.GUID); + else + this._log.warn("Can't change GUID " + command.GUID + + " to " + command.data.GUID + ": GUID already exists."); + break; + case "title": + this._bms.setItemTitle(itemId, command.data.title); + break; + case "URI": + this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI)); + break; + case "index": + this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), + command.data.index); + break; + case "parentGUID": + let index = -1; + if (command.data.index && command.data.index >= 0) + index = command.data.index; + this._bms.moveItem( + itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); + break; + case "tags": + let tagsURI = this._bms.getBookmarkURI(itemId); + this._ts.untagURI(URI, null); + this._ts.tagURI(tagsURI, command.data.tags); + break; + case "keyword": + this._bms.setKeywordForBookmark(itemId, command.data.keyword); + break; + case "generatorURI": + let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); + let genURI = makeURI(command.data.generatorURI); + let micsum = this._ms.createMicrosummary(micsumURI, genURI); + this._ms.setMicrosummary(itemId, micsum); + break; + case "siteURI": + this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); + break; + case "feedURI": + this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); + break; + default: + this._log.warn("Can't change item property: " + key); + break; + } + } + }, + + wrap: function BStore_wrap() { + let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder); + let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder); + let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder); + + for (let guid in unfiled) { + if (!(guid in filed)) + filed[guid] = unfiled[guid]; + } + + for (let guid in toolbar) { + if (!(guid in filed)) + filed[guid] = toolbar[guid]; + } + + return filed; // (combined) + }, + + resetGUIDs: function BStore_resetGUIDs() { + this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); + }, + + applyCommands: function BStore_applyCommands(commandList) { + for (var i = 0; i < commandList.length; i++) { + var command = commandList[i]; + this._log.debug("Processing command: " + uneval(command)); + switch (command["action"]) { + case "create": + this._createCommand(command); + break; + case "remove": + this._removeCommand(command); + break; + case "edit": + this._editCommand(command); + break; + default: + this._log.error("unknown action in command: " + command["action"]); + break; + } + } + } +}; +BookmarksStore.prototype.__proto__ = new Store(); /* * DAV object From ea163c20e0c79edd882b6b7b412873a30bef8d85 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 29 Nov 2007 17:14:10 -0800 Subject: [PATCH 0089/1860] fix bug 406067; more refactoring; speed up reconciliation --- services/sync/BookmarksSyncService.js | 353 ++++++++++++++------------ 1 file changed, 184 insertions(+), 169 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index 49ce87a6ec47..dfbbe6e21a4b 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -64,38 +64,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function BookmarksSyncService() { this._init(); } BookmarksSyncService.prototype = { - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - __ts: null, - get _ts() { - if (!this.__ts) - this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. - getService(Ci.nsITaggingService); - return this.__ts; - }, - - __ls: null, - get _ls() { - if (!this.__ls) - this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. - getService(Ci.nsILivemarkService); - return this.__ls; - }, - - __ms: null, - get _ms() { - if (!this.__ms) - this.__ms = Cc["@mozilla.org/microsummary/service;1"]. - getService(Ci.nsIMicrosummaryService); - return this.__ms; - }, - __os: null, get _os() { if (!this.__os) @@ -133,6 +101,16 @@ BookmarksSyncService.prototype = { return this.__store; }, + __snapshot: null, + get _snapshot() { + if (!this.__snapshot) + this.__snapshot = new SnapshotStore(); + return this.__snapshot; + }, + set _snapshot(value) { + this.__snapshot = value; + }, + // Logger object _log: null, @@ -151,23 +129,6 @@ BookmarksSyncService.prototype = { return this.__encrypter; }, - // Last synced tree, version, and GUID (to detect if the store has - // been completely replaced and invalidate the snapshot) - _snapshot: {}, - _snapshotVersion: 0, - - __snapshotGUID: null, - get _snapshotGUID() { - if (!this.__snapshotGUID) { - let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. - getService(Ci.nsIUUIDGenerator); - this.__snapshotGUID = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); - } - return this.__snapshotGUID; - }, - set _snapshotGUID(GUID) { - this.__snapshotGUID = GUID; - }, get username() { let branch = Cc["@mozilla.org/preferences-service;1"] @@ -365,9 +326,7 @@ BookmarksSyncService.prototype = { dapp.level = root.LEVEL_ALL; root.addAppender(dapp); - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - let logFile = dirSvc.get("ProfD", Ci.nsIFile); + let logFile = this._dirSvc.get("ProfD", Ci.nsIFile); let verboseFile = logFile.clone(); logFile.append("bm-sync.log"); logFile.QueryInterface(Ci.nsILocalFile); @@ -385,10 +344,7 @@ BookmarksSyncService.prototype = { _saveSnapshot: function BSS__saveSnapshot() { this._log.info("Saving snapshot to disk"); - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - - let file = dirSvc.get("ProfD", Ci.nsIFile); + let file = this._dirSvc.get("ProfD", Ci.nsIFile); file.append("bm-sync-snapshot.json"); file.QueryInterface(Ci.nsILocalFile); @@ -400,8 +356,8 @@ BookmarksSyncService.prototype = { let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; fos.init(file, flags, PERMS_FILE, 0); - let out = {version: this._snapshotVersion, - GUID: this._snapshotGUID, + let out = {version: this._snapshot.version, + GUID: this._snapshot.GUID, snapshot: this._snapshot}; out = uneval(out); fos.write(out, out.length); @@ -409,10 +365,7 @@ BookmarksSyncService.prototype = { }, _readSnapshot: function BSS__readSnapshot() { - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - - let file = dirSvc.get("ProfD", Ci.nsIFile); + let file = this._dirSvc.get("ProfD", Ci.nsIFile); file.append("bm-sync-snapshot.json"); if (!file.exists()) @@ -435,46 +388,11 @@ BookmarksSyncService.prototype = { if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { this._log.info("Read saved snapshot from disk"); this._snapshot = json.snapshot; - this._snapshotVersion = json.version; - this._snapshotGUID = json.GUID; + this._snapshot.version = json.version; + this._snapshot.GUID = json.GUID; } }, - _applyCommandsToObj: function BSS__applyCommandsToObj(commands, obj) { - for (let i = 0; i < commands.length; i++) { - this._log.debug("Applying cmd to obj: " + uneval(commands[i])); - switch (commands[i].action) { - case "create": - obj[commands[i].GUID] = eval(uneval(commands[i].data)); - break; - case "edit": - if ("GUID" in commands[i].data) { - // special-case guid changes - let newGUID = commands[i].data.GUID, - oldGUID = commands[i].GUID; - - obj[newGUID] = obj[oldGUID]; - delete obj[oldGUID] - - for (let GUID in obj) { - if (obj[GUID].parentGUID == oldGUID) - obj[GUID].parentGUID = newGUID; - } - } - for (let prop in commands[i].data) { - if (prop == "GUID") - continue; - obj[commands[i].GUID][prop] = commands[i].data[prop]; - } - break; - case "remove": - delete obj[commands[i].GUID]; - break; - } - } - return obj; - }, - _mungeNodes: function BSS__mungeNodes(nodes) { let json = uneval(nodes); json = json.replace(/:{type/g, ":\n\t{type"); @@ -621,7 +539,7 @@ BookmarksSyncService.prototype = { this._getServerData.async(this, cont); let server = yield; - this._log.info("Local snapshot version: " + this._snapshotVersion); + this._log.info("Local snapshot version: " + this._snapshot.version); this._log.info("Server status: " + server.status); this._log.info("Server maxVersion: " + server.maxVersion); this._log.info("Server snapVersion: " + server.snapVersion); @@ -634,16 +552,17 @@ BookmarksSyncService.prototype = { // 2) Generate local deltas from snapshot -> current client status - let localJson = this._store.wrap(); - this._sync.detectUpdates(cont, this._snapshot, localJson); + let localJson = new SnapshotStore(); + localJson.data = this._store.wrap(); + this._sync.detectUpdates(cont, this._snapshot.data, localJson.data); let localUpdates = yield; - this._log.debug("local json:\n" + this._mungeNodes(localJson)); + this._log.debug("local json:\n" + this._mungeNodes(localJson.data)); this._log.debug("Local updates: " + this._mungeCommands(localUpdates)); this._log.debug("Server updates: " + this._mungeCommands(server.updates)); if (server.updates.length == 0 && localUpdates.length == 0) { - this._snapshotVersion = server.maxVersion; + this._snapshot.version = server.maxVersion; this._log.info("Sync complete (1): no changes needed on client or server"); synced = true; return; @@ -672,8 +591,8 @@ BookmarksSyncService.prototype = { if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { this._log.info("Sync complete (2): no changes needed on client or server"); - this._snapshot = localJson; - this._snapshotVersion = server.maxVersion; + this._snapshot.data = localJson.data; + this._snapshot.version = server.maxVersion; this._saveSnapshot(); synced = true; return; @@ -683,8 +602,8 @@ BookmarksSyncService.prototype = { this._log.warn("Conflicts found! Discarding server changes"); } - let savedSnap = eval(uneval(this._snapshot)); - let savedVersion = this._snapshotVersion; + let savedSnap = eval(uneval(this._snapshot.data)); + let savedVersion = this._snapshot.version; let newSnapshot; // 3.1) Apply server changes to local store @@ -693,12 +612,13 @@ BookmarksSyncService.prototype = { // Note that we need to need to apply client changes to the // current tree, not the saved snapshot - this._snapshot = this._applyCommandsToObj(clientChanges, localJson); - this._snapshotVersion = server.maxVersion; + localJson.applyCommands(clientChanges); + this._snapshot.data = localJson.data; + this._snapshot.version = server.maxVersion; this._store.applyCommands(clientChanges); newSnapshot = this._store.wrap(); - this._sync.detectUpdates(cont, this._snapshot, newSnapshot); + this._sync.detectUpdates(cont, this._snapshot.data, newSnapshot); let diff = yield; if (diff.length != 0) { this._log.warn("Commands did not apply correctly"); @@ -706,8 +626,8 @@ BookmarksSyncService.prototype = { "new snapshot after commands:\n" + this._mungeCommands(diff)); // FIXME: do we really want to revert the snapshot here? - this._snapshot = eval(uneval(savedSnap)); - this._snapshotVersion = savedVersion; + this._snapshot.data = eval(uneval(savedSnap)); + this._snapshot.version = savedVersion; } this._saveSnapshot(); @@ -736,8 +656,8 @@ BookmarksSyncService.prototype = { if (serverDelta.length) { this._log.info("Uploading changes to server"); - this._snapshot = newSnapshot; - this._snapshotVersion = ++server.maxVersion; + this._snapshot.data = newSnapshot; + this._snapshot.version = ++server.maxVersion; server.deltas.push(serverDelta); @@ -764,14 +684,14 @@ BookmarksSyncService.prototype = { let deltasPut = yield; let c = 0; - for (GUID in this._snapshot) + for (GUID in this._snapshot.data) c++; this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, + uneval({GUID: this._snapshot.GUID, formatVersion: STORAGE_FORMAT_VERSION, snapVersion: server.snapVersion, - maxVersion: this._snapshotVersion, + maxVersion: this._snapshot.version, snapEncryption: server.snapEncryption, deltasEncryption: this.encryption, bookmarksCount: c}), cont); @@ -887,7 +807,8 @@ BookmarksSyncService.prototype = { this._log.info("Got bookmarks status from server"); let status = eval(resp.responseText); - let snap, deltas, allDeltas; + let deltas, allDeltas; + let snap = new SnapshotStore(); // Bail out if the server has a newer format version than we can parse if (status.formatVersion > STORAGE_FORMAT_VERSION) { @@ -902,24 +823,24 @@ BookmarksSyncService.prototype = { ret.deltasEncryption = status.deltasEncryption = "none"; } - if (status.GUID != this._snapshotGUID) { + if (status.GUID != this._snapshot.GUID) { this._log.info("Remote/local sync GUIDs do not match. " + "Forcing initial sync."); this._store.resetGUIDs(); - this._snapshot = {}; - this._snapshotVersion = -1; - this._snapshotGUID = status.GUID; + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.GUID = status.GUID; } - if (this._snapshotVersion < status.snapVersion) { - if (this._snapshotVersion >= 0) + if (this._snapshot.version < status.snapVersion) { + if (this._snapshot.version >= 0) this._log.info("Local snapshot is out of date"); this._log.info("Downloading server snapshot"); this._dav.GET("bookmarks-snapshot.json", cont); resp = yield; this._checkStatus(resp.status, "Could not download snapshot."); - snap = this._decrypt(status.snapEncryption, resp.responseText); + snap.data = this._decrypt(status.snapEncryption, resp.responseText); this._log.info("Downloading server deltas"); this._dav.GET("bookmarks-deltas.json", cont); @@ -928,19 +849,19 @@ BookmarksSyncService.prototype = { allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); deltas = eval(uneval(allDeltas)); - } else if (this._snapshotVersion >= status.snapVersion && - this._snapshotVersion < status.maxVersion) { - snap = eval(uneval(this._snapshot)); + } else if (this._snapshot.version >= status.snapVersion && + this._snapshot.version < status.maxVersion) { + snap.data = eval(uneval(this._snapshot.data)); this._log.info("Downloading server deltas"); this._dav.GET("bookmarks-deltas.json", cont); resp = yield; this._checkStatus(resp.status, "Could not download deltas."); allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); - deltas = allDeltas.slice(this._snapshotVersion - status.snapVersion); + deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); - } else if (this._snapshotVersion == status.maxVersion) { - snap = eval(uneval(this._snapshot)); + } else if (this._snapshot.version == status.maxVersion) { + snap.data = eval(uneval(this._snapshot.data)); // FIXME: could optimize this case by caching deltas file this._log.info("Downloading server deltas"); @@ -950,13 +871,13 @@ BookmarksSyncService.prototype = { allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); deltas = []; - } else { // this._snapshotVersion > status.maxVersion + } else { // this._snapshot.version > status.maxVersion this._log.error("Server snapshot is older than local snapshot"); return; } for (var i = 0; i < deltas.length; i++) { - snap = this._applyCommandsToObj(deltas[i], snap); + snap.applyCommands(deltas[i]); } ret.status = 0; @@ -965,18 +886,18 @@ BookmarksSyncService.prototype = { ret.snapVersion = status.snapVersion; ret.snapEncryption = status.snapEncryption; ret.deltasEncryption = status.deltasEncryption; - ret.snapshot = snap; + ret.snapshot = snap.data; ret.deltas = allDeltas; - this._sync.detectUpdates(cont, this._snapshot, snap); + this._sync.detectUpdates(cont, this._snapshot.data, snap.data); ret.updates = yield; break; case 404: this._log.info("Server has no status file, Initial upload to server"); - this._snapshot = this._store.wrap(); - this._snapshotVersion = 0; - this._snapshotGUID = null; // in case there are other snapshots out there + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there this._fullUpload.async(this, cont); let uploadStatus = yield; @@ -988,11 +909,11 @@ BookmarksSyncService.prototype = { ret.status = 0; ret.formatVersion = STORAGE_FORMAT_VERSION; - ret.maxVersion = this._snapshotVersion; - ret.snapVersion = this._snapshotVersion; + ret.maxVersion = this._snapshot.version; + ret.snapVersion = this._snapshot.version; ret.snapEncryption = this.encryption; ret.deltasEncryption = this.encryption; - ret.snapshot = eval(uneval(this._snapshot)); + ret.snapshot = eval(uneval(this._snapshot.data)); ret.deltas = []; ret.updates = []; break; @@ -1022,10 +943,10 @@ BookmarksSyncService.prototype = { try { let data; if (this.encryption == "none") { - data = this._mungeNodes(this._snapshot); + data = this._mungeNodes(this._snapshot.data); } else if (this.encryption == "XXXTEA") { this._log.debug("Encrypting snapshot"); - data = this._encrypter.encrypt(uneval(this._snapshot), this.passphrase); + data = this._encrypter.encrypt(uneval(this._snapshot.data), this.passphrase); this._log.debug("Done encrypting snapshot"); } else { this._log.error("Unknown encryption scheme: " + this.encryption); @@ -1040,14 +961,14 @@ BookmarksSyncService.prototype = { this._checkStatus(resp.status, "Could not upload deltas."); let c = 0; - for (GUID in this._snapshot) + for (GUID in this._snapshot.data) c++; this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshotGUID, + uneval({GUID: this._snapshot.GUID, formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshotVersion, - maxVersion: this._snapshotVersion, + snapVersion: this._snapshot.version, + maxVersion: this._snapshot.version, snapEncryption: this.encryption, deltasEncryption: "none", bookmarksCount: c}), cont); @@ -1180,8 +1101,8 @@ BookmarksSyncService.prototype = { this._log.debug("Resetting client state"); this._os.notifyObservers(null, "bookmarks-sync:reset-client-start", ""); - this._snapshot = {}; - this._snapshotVersion = -1; + this._snapshot.data = {}; + this._snapshot.version = -1; this._saveSnapshot(); done = true; @@ -1388,6 +1309,8 @@ SyncCore.prototype = { }, _commandLike: function SC__commandLike(a, b) { + this._log.error("commandLike needs to be subclassed"); + // Check that neither command is null, and verify that the GUIDs // are different (otherwise we need to check for edits) if (!a || !b || a.GUID == b.GUID) @@ -1458,37 +1381,51 @@ SyncCore.prototype = { let propagations = [[], []]; let conflicts = [[], []]; let ret = {propagations: propagations, conflicts: conflicts}; + this._log.debug("Reconciling " + listA.length + + " against " + listB.length + "commands"); try { + let guidChanges = []; for (let i = 0; i < listA.length; i++) { - for (let j = 0; j < listB.length; j++) { + let a = listA[i]; + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (deepEquals(listA[i], listB[j])) { - delete listA[i]; - delete listB[j]; - } else if (this._commandLike(listA[i], listB[j])) { - this._fixParents(listA, listA[i].GUID, listB[j].GUID); - listB[j].data = {GUID: listB[j].GUID}; - listB[j].GUID = listA[i].GUID; - listB[j].action = "edit"; - delete listA[i]; + this._log.debug("comparing " + i + ", listB length: " + listB.length); + + let skip = false; + listB = listB.filter(function(b) { + // fast path for when we already found a matching command + if (skip) + return true; + + if (deepEquals(a, b)) { + delete listA[i]; // a + skip = true; + return false; // b + + } else if (this._commandLike(a, b)) { + this._fixParents(listA, a.GUID, b.GUID); + guidChanges.push({action: "edit", + GUID: a.GUID, + data: {GUID: b.GUID}}); + delete listA[i]; // a + skip = true; + return false; // b, but we add it back from guidChanges } // watch out for create commands with GUIDs that already exist - if (listB[j] && listB[j].action == "create" && - this._itemExists(listB[j].GUID)) { + if (b.action == "create" && this._itemExists(b.GUID)) { this._log.error("Remote command has GUID that already exists " + "locally. Dropping command."); - delete listB[j]; + return false; // delete b } - } + return true; // keep b + }, this); } listA = listA.filter(function(elt) { return elt }); - listB = listB.filter(function(elt) { return elt }); + listB = listB.concat(guidChanges); for (let i = 0; i < listA.length; i++) { for (let j = 0; j < listB.length; j++) { @@ -1557,7 +1494,7 @@ BookmarksSyncCore.prototype = { return this._bms.getItemIdForGUID(GUID) >= 0; }, - commandLike: function BSC_commandLike(a, b) { + _commandLike: function BSC_commandLike(a, b) { // Check that neither command is null, that their actions, types, // and parents are the same, and that they don't have the same // GUID. @@ -1640,6 +1577,84 @@ Store.prototype = { } }; +function SnapshotStore() { + this._init(); +} +SnapshotStore.prototype = { + _logName: "SStore", + + // Last synced tree, version, and GUID (to detect if the store has + // been completely replaced and invalidate the snapshot) + + _data: {}, + get data() { + return this._data; + }, + set data(value) { + this._data = value; + }, + + _version: 0, + get version() { + return this._version; + }, + set version(value) { + this._version = value; + }, + + _GUID: null, + get GUID() { + if (!this._GUID) { + let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator); + this._GUID = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); + } + return this._GUID; + }, + set GUID(GUID) { + this._GUID = GUID; + }, + + wrap: function SStore_wrap() { + }, + + applyCommands: function SStore_applyCommands(commands) { + for (let i = 0; i < commands.length; i++) { + this._log.debug("Applying cmd to obj: " + uneval(commands[i])); + switch (commands[i].action) { + case "create": + this._data[commands[i].GUID] = eval(uneval(commands[i].data)); + break; + case "edit": + if ("GUID" in commands[i].data) { + // special-case guid changes + let newGUID = commands[i].data.GUID, + oldGUID = commands[i].GUID; + + this._data[newGUID] = this._data[oldGUID]; + delete this._data[oldGUID] + + for (let GUID in this._data) { + if (this._data[GUID].parentGUID == oldGUID) + this._data[GUID].parentGUID = newGUID; + } + } + for (let prop in commands[i].data) { + if (prop == "GUID") + continue; + this._data[commands[i].GUID][prop] = commands[i].data[prop]; + } + break; + case "remove": + delete this._data[commands[i].GUID]; + break; + } + } + return this._data; + } +}; +SnapshotStore.prototype.__proto__ = new Store(); + function BookmarksStore() { this._init(); } From 9919239851bc4b8994d22fbe21792e2aaa657bbc Mon Sep 17 00:00:00 2001 From: Date: Fri, 30 Nov 2007 10:08:05 -0800 Subject: [PATCH 0090/1860] fix incorrect manifest line for locale; move idl files and xpt generation script to a public directory --- services/sync/IBookmarksSyncService.idl | 98 ----------------- services/sync/ILog4MozService.idl | 137 ------------------------ services/sync/locales/en-US/sync.dtd | 0 services/sync/xptgen | 13 --- 4 files changed, 248 deletions(-) delete mode 100644 services/sync/IBookmarksSyncService.idl delete mode 100644 services/sync/ILog4MozService.idl create mode 100644 services/sync/locales/en-US/sync.dtd delete mode 100755 services/sync/xptgen diff --git a/services/sync/IBookmarksSyncService.idl b/services/sync/IBookmarksSyncService.idl deleted file mode 100644 index 5a41eed3bc33..000000000000 --- a/services/sync/IBookmarksSyncService.idl +++ /dev/null @@ -1,98 +0,0 @@ -/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Places. - * - * The Initial Developer of the Original Code is - * Mozilla Corporation - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -#include "nsISupports.idl" - -[scriptable, uuid(efb3ba58-69bc-42d5-a430-0746fa4b1a7f)] -interface IBookmarksSyncService : nsISupports -{ - /** - * The stored username and password - */ - attribute AString username; - attribute AString password; - - /** - * The currently logged-in user. Null if not logged in. - */ - readonly attribute AString currentUser; - - /** - * Encryption algorithm to use. May be 'xxxtea' or 'none'. - */ - attribute AString enryption; - - /** - * The stored encryption passphrase - */ - attribute AString passphrase; - - /** - * Log into the server. Pre-requisite for sync(). - * password and/or passphrase may be null, and then they will be - * fetched from the password manager. If they are not given or - * found, login will fail. - */ - void login(in AString password, in AString passphrase); - - /** - * Log out of the server. - */ - void logout(); - - /** - * Initiate a sync operation. - */ - void sync(); - - /** - * Reset a server lock - * (for stale locks, useful for debugging after a crash) - */ - void resetLock(); - - /** - * Delete json files on server - */ - void resetServer(); - - /** - * Clear local snapshot - */ - void resetClient(); -}; diff --git a/services/sync/ILog4MozService.idl b/services/sync/ILog4MozService.idl deleted file mode 100644 index f82595f0c59e..000000000000 --- a/services/sync/ILog4MozService.idl +++ /dev/null @@ -1,137 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is log4moz - * - * The Initial Developer of the Original Code is - * Michael Johnston - * Portions created by the Initial Developer are Copyright (C) 2006 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -#include "nsISupports.idl" - -interface nsIFile; - -[scriptable, uuid(eeb0adbc-afa3-4326-9a52-040ce4855e9d)] -interface ILogMessage : nsISupports -{ - attribute AString loggerName; - attribute PRInt32 level; - attribute AString levelDesc; - attribute AString message; - attribute PRTime time; -}; - -[scriptable, uuid(ac7f40df-49e7-4270-86ec-9c905047e70e)] -interface IFormatter : nsISupports -{ - AString format(in ILogMessage message); -}; - -[scriptable, uuid(abe56c32-79f4-465e-ac20-8e39d33cba0b)] -interface IAppender : nsISupports -{ - attribute PRInt32 level; - void append(in ILogMessage message); -}; - -[scriptable, uuid(2c1a0f87-f544-479c-80e0-acd11ae52ebf)] -interface ILogger : nsISupports -{ - /* - * Level constants - */ - const PRInt32 LEVEL_FATAL = 70; - const PRInt32 LEVEL_ERROR = 60; - const PRInt32 LEVEL_WARN = 50; - const PRInt32 LEVEL_INFO = 40; - const PRInt32 LEVEL_CONFIG = 30; - const PRInt32 LEVEL_DEBUG = 20; - const PRInt32 LEVEL_TRACE = 10; - const PRInt32 LEVEL_ALL = 0; - - /* - * The assigned level for this logger. Applies also to descendant - * loggers unless overridden. - */ - attribute PRInt32 level; - - /* - * Add an appender to this logger's list. Anything logged via this - * logger will be sent to the list of appenders (including its - * parent logger's appenders). - */ - void addAppender(in IAppender appender); - - /* - * Log messages at various levels - */ - - void fatal(in AString message); - void error(in AString message); - void warn(in AString message); - void info(in AString message); - void config(in AString message); - void debug(in AString message); - void trace(in AString message); -}; - -[scriptable, uuid(cbfb3abe-4ca9-498a-a692-581f8da52323)] -interface ILoggerRepository : nsISupports -{ -}; - -[scriptable, uuid(a60e50d7-90b8-4a12-ad0c-79e6a1896978)] -interface ILog4MozService : nsISupports -{ - /* - * The root logger. Appenders added to this logger will be - * inherited by all other loggers - */ - readonly attribute ILogger rootLogger; - - /* - * Get a Logger object that can be used to print debug/info/error - * messages to various places. - * - * @param name - * The name of the logger. A new logger will be created - * with that name if it doesn't already exist. - * @returns The ILogger object that can be used to log messages. - */ - - ILogger getLogger(in AString name); - - IAppender newAppender(in AString kind, in IFormatter formatter); - IAppender newFileAppender(in AString kind, in nsIFile file, - in IFormatter formatter); - - IFormatter newFormatter(in AString kind); -}; diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/services/sync/xptgen b/services/sync/xptgen deleted file mode 100755 index d38f3b166a94..000000000000 --- a/services/sync/xptgen +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -x - -if [[ ! -d $MOZOBJDIR ]]; then - echo "MOZOBJDIR environment variable must point to a Mozilla build obj tree (with xptgen)" - exit 1 -fi -if [[ ! -d $MOZSRCDIR ]]; then - echo "MOZSRCDIR environment variable must point to a Mozilla source tree" - exit 1 -fi - -$MOZOBJDIR/dist/bin/xpidl -m typelib -I $MOZSRCDIR/xpcom/base/ -o IBookmarksSyncService IBookmarksSyncService.idl -$MOZOBJDIR/dist/bin/xpidl -m typelib -I $MOZSRCDIR/xpcom/base/ -o ILog4MozService ILog4MozService.idl From 36a4820f1c5a74d80542774bee711a8edb396779 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 1 Dec 2007 22:57:57 -0800 Subject: [PATCH 0091/1860] refactoring more stuff out of the service --- services/sync/BookmarksSyncService.js | 190 +++++++++++++------------- 1 file changed, 94 insertions(+), 96 deletions(-) diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js index dfbbe6e21a4b..cad33d41ec85 100644 --- a/services/sync/BookmarksSyncService.js +++ b/services/sync/BookmarksSyncService.js @@ -269,7 +269,7 @@ BookmarksSyncService.prototype = { } catch (ex) { /* use defaults */ } - this._readSnapshot(); + this._snapshot.load(); if (!enabled) { this._log.info("Bookmarks sync disabled"); @@ -341,85 +341,6 @@ BookmarksSyncService.prototype = { root.addAppender(vapp); }, - _saveSnapshot: function BSS__saveSnapshot() { - this._log.info("Saving snapshot to disk"); - - let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync-snapshot.json"); - file.QueryInterface(Ci.nsILocalFile); - - if (!file.exists()) - file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); - - let fos = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; - fos.init(file, flags, PERMS_FILE, 0); - - let out = {version: this._snapshot.version, - GUID: this._snapshot.GUID, - snapshot: this._snapshot}; - out = uneval(out); - fos.write(out, out.length); - fos.close(); - }, - - _readSnapshot: function BSS__readSnapshot() { - let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync-snapshot.json"); - - if (!file.exists()) - return; - - let fis = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - fis.init(file, MODE_RDONLY, PERMS_FILE, 0); - fis.QueryInterface(Ci.nsILineInputStream); - - let json = ""; - while (fis.available()) { - let ret = {}; - fis.readLine(ret); - json += ret.value; - } - fis.close(); - json = eval(json); - - if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { - this._log.info("Read saved snapshot from disk"); - this._snapshot = json.snapshot; - this._snapshot.version = json.version; - this._snapshot.GUID = json.GUID; - } - }, - - _mungeNodes: function BSS__mungeNodes(nodes) { - let json = uneval(nodes); - json = json.replace(/:{type/g, ":\n\t{type"); - json = json.replace(/}, /g, "},\n "); - json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); - json = json.replace(/, index/g, ",\n\t index"); - json = json.replace(/, title/g, ",\n\t title"); - json = json.replace(/, URI/g, ",\n\t URI"); - json = json.replace(/, tags/g, ",\n\t tags"); - json = json.replace(/, keyword/g, ",\n\t keyword"); - return json; - }, - - _mungeCommands: function BSS__mungeCommands(commands) { - let json = uneval(commands); - json = json.replace(/ {action/g, "\n {action"); - //json = json.replace(/, data/g, ",\n data"); - return json; - }, - - _mungeConflicts: function BSS__mungeConflicts(conflicts) { - let json = uneval(conflicts); - json = json.replace(/ {action/g, "\n {action"); - //json = json.replace(/, data/g, ",\n data"); - return json; - }, - _lock: function BSS__lock() { if (this._locked) { this._log.warn("Service lock failed: already locked"); @@ -557,9 +478,9 @@ BookmarksSyncService.prototype = { this._sync.detectUpdates(cont, this._snapshot.data, localJson.data); let localUpdates = yield; - this._log.debug("local json:\n" + this._mungeNodes(localJson.data)); - this._log.debug("Local updates: " + this._mungeCommands(localUpdates)); - this._log.debug("Server updates: " + this._mungeCommands(server.updates)); + this._log.debug("local json:\n" + localJson.serialize()); + this._log.debug("Local updates: " + serializeCommands(localUpdates)); + this._log.debug("Server updates: " + serializeCommands(server.updates)); if (server.updates.length == 0 && localUpdates.length == 0) { this._snapshot.version = server.maxVersion; @@ -583,17 +504,17 @@ BookmarksSyncService.prototype = { this._log.info("Predicted changes for server: " + serverChanges.length); this._log.info("Client conflicts: " + clientConflicts.length); this._log.info("Server conflicts: " + serverConflicts.length); - this._log.debug("Changes for client: " + this._mungeCommands(clientChanges)); - this._log.debug("Predicted changes for server: " + this._mungeCommands(serverChanges)); - this._log.debug("Client conflicts: " + this._mungeConflicts(clientConflicts)); - this._log.debug("Server conflicts: " + this._mungeConflicts(serverConflicts)); + this._log.debug("Changes for client: " + serializeCommands(clientChanges)); + this._log.debug("Predicted changes for server: " + serializeCommands(serverChanges)); + this._log.debug("Client conflicts: " + serializeConflicts(clientConflicts)); + this._log.debug("Server conflicts: " + serializeConflicts(serverConflicts)); if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { this._log.info("Sync complete (2): no changes needed on client or server"); this._snapshot.data = localJson.data; this._snapshot.version = server.maxVersion; - this._saveSnapshot(); + this._snapshot.save(); synced = true; return; } @@ -624,13 +545,13 @@ BookmarksSyncService.prototype = { this._log.warn("Commands did not apply correctly"); this._log.debug("Diff from snapshot+commands -> " + "new snapshot after commands:\n" + - this._mungeCommands(diff)); + serializeCommands(diff)); // FIXME: do we really want to revert the snapshot here? this._snapshot.data = eval(uneval(savedSnap)); this._snapshot.version = savedVersion; } - this._saveSnapshot(); + this._snapshot.save(); } // 3.2) Append server delta to the delta file and upload @@ -651,7 +572,7 @@ BookmarksSyncService.prototype = { this._log.info("Actual changes for server: " + serverDelta.length); this._log.debug("Actual changes for server: " + - this._mungeCommands(serverDelta)); + serializeCommands(serverDelta)); if (serverDelta.length) { this._log.info("Uploading changes to server"); @@ -671,7 +592,7 @@ BookmarksSyncService.prototype = { } else { let data; if (this.encryption == "none") { - data = this._mungeCommands(server.deltas); + data = serializeCommands(server.deltas); } else if (this.encryption == "XXXTEA") { this._log.debug("Encrypting snapshot"); data = this._encrypter.encrypt(uneval(server.deltas), this.passphrase); @@ -700,7 +621,7 @@ BookmarksSyncService.prototype = { if (deltasPut.status >= 200 && deltasPut.status < 300 && statusPut.status >= 200 && statusPut.status < 300) { this._log.info("Successfully updated deltas and status on server"); - this._saveSnapshot(); + this.snapshot.save(); } else { // FIXME: revert snapshot here? - can't, we already applied // updates locally! - need to save and retry @@ -905,7 +826,7 @@ BookmarksSyncService.prototype = { return; this._log.info("Initial upload to server successful"); - this._saveSnapshot(); + this.snapshot.save(); ret.status = 0; ret.formatVersion = STORAGE_FORMAT_VERSION; @@ -943,7 +864,7 @@ BookmarksSyncService.prototype = { try { let data; if (this.encryption == "none") { - data = this._mungeNodes(this._snapshot.data); + data = this._snapshot.serialize(); } else if (this.encryption == "XXXTEA") { this._log.debug("Encrypting snapshot"); data = this._encrypter.encrypt(uneval(this._snapshot.data), this.passphrase); @@ -1103,7 +1024,7 @@ BookmarksSyncService.prototype = { this._snapshot.data = {}; this._snapshot.version = -1; - this._saveSnapshot(); + this.snapshot.save(); done = true; } catch (e) { @@ -1203,6 +1124,18 @@ BookmarksSyncService.prototype = { } }; +serializeCommands: function serializeCommands(commands) { + let json = uneval(commands); + json = json.replace(/ {action/g, "\n {action"); + return json; +} + +serializeConflicts: function serializeConflicts(conflicts) { + let json = uneval(conflicts); + json = json.replace(/ {action/g, "\n {action"); + return json; +} + /* * SyncCore objects * Sync cores deal with diff creation and conflict resolution. @@ -1615,6 +1548,71 @@ SnapshotStore.prototype = { this._GUID = GUID; }, + save: function SStore_save() { + this._log.info("Saving snapshot to disk"); + + let file = this._dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync-snapshot.json"); + file.QueryInterface(Ci.nsILocalFile); + + if (!file.exists()) + file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); + + let fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; + fos.init(file, flags, PERMS_FILE, 0); + + let out = {version: this._snapshot.version, + GUID: this._snapshot.GUID, + snapshot: this._snapshot}; + out = uneval(out); + fos.write(out, out.length); + fos.close(); + }, + + load: function SStore_load() { + let file = this._dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync-snapshot.json"); + + if (!file.exists()) + return; + + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(file, MODE_RDONLY, PERMS_FILE, 0); + fis.QueryInterface(Ci.nsILineInputStream); + + let json = ""; + while (fis.available()) { + let ret = {}; + fis.readLine(ret); + json += ret.value; + } + fis.close(); + json = eval(json); + + if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { + this._log.info("Read saved snapshot from disk"); + this._snapshot = json.snapshot; + this._snapshot.version = json.version; + this._snapshot.GUID = json.GUID; + } + }, + + serialize: function SStore_serialize() { + let json = uneval(this.data); + json = json.replace(/:{type/g, ":\n\t{type"); + json = json.replace(/}, /g, "},\n "); + json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); + json = json.replace(/, index/g, ",\n\t index"); + json = json.replace(/, title/g, ",\n\t title"); + json = json.replace(/, URI/g, ",\n\t URI"); + json = json.replace(/, tags/g, ",\n\t tags"); + json = json.replace(/, keyword/g, ",\n\t keyword"); + return json; + }, + wrap: function SStore_wrap() { }, From b6976d395d324197fc6fbb2b492e7690c5b55a2c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 10 Dec 2007 16:42:10 -0800 Subject: [PATCH 0092/1860] more refactoring: move bookmarks-specific code into an 'engine' object; get rid of sync xpcom component and replace it with Components.utils.import hotness --- services/sync/IBookmarksSyncService.xpt | Bin 414 -> 0 bytes services/sync/modules/weave.js | 2742 +++++++++++++++++++++++ 2 files changed, 2742 insertions(+) delete mode 100644 services/sync/IBookmarksSyncService.xpt create mode 100644 services/sync/modules/weave.js diff --git a/services/sync/IBookmarksSyncService.xpt b/services/sync/IBookmarksSyncService.xpt deleted file mode 100644 index 6c62468aa6e49c6cf850b4a73192657b89d1cd15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 414 zcmazDaQ64*3aKne^~p@)<&t7#VqjumV4TOmz@P-gkqtls3=TjjHz36bVKOkh-@Gd# zbC1*2B?jznzr3aDf#SSSIpw@!&*0L6g8ZVAVg^sA{QT_P#G>rt;L5z@;MAhB%;Z#% zIs6Sk||hb0J07;HUL>5u~Trd%W$zfU@?&F3%Kkjxa=R87$ZAa45*Azh^b*G z$V3oZf&oIyLunN#tp}y8z;r`tacWUsVs0t}oKcWiTwI=Cl)?aKB$pNyrRJ4{0+lkP z<`q>Ilw{`TA+bOT3o?oli&MdRpv;{7^vpa4AemoU!cYth2Zo~5;?xqK{N!v13luIu N6Tza+IhjBk7y$RPY+3*S diff --git a/services/sync/modules/weave.js b/services/sync/modules/weave.js new file mode 100644 index 000000000000..53d444c2a48a --- /dev/null +++ b/services/sync/modules/weave.js @@ -0,0 +1,2742 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = ['Weave']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +const MODE_RDONLY = 0x01; +const MODE_WRONLY = 0x02; +const MODE_CREATE = 0x08; +const MODE_APPEND = 0x10; +const MODE_TRUNCATE = 0x20; + +const PERMS_FILE = 0644; +const PERMS_DIRECTORY = 0755; + +const STORAGE_FORMAT_VERSION = 2; + +const ONE_BYTE = 1; +const ONE_KILOBYTE = 1024 * ONE_BYTE; +const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +/* + * Service object + * Implements IBookmarksSyncService, main entry point + */ + +function BookmarksSyncService() { this._init(); } +BookmarksSyncService.prototype = { + + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + return this.__dirSvc; + }, + + __dav: null, + get _dav() { + if (!this.__dav) + this.__dav = new DAVCollection(); + return this.__dav; + }, + + __bmkEngine: null, + get _bmkEngine() { + if (!this.__bmkEngine) + this.__bmkEngine = new BookmarksEngine(this._dav, this._cryptoId); + return this.__bmkEngine; + }, + + // Logger object + _log: null, + + // Timer object for automagically syncing + _scheduleTimer: null, + + __mozId: null, + get _mozId() { + if (this.__mozId === null) + this.__mozId = new Identity('Mozilla Services Password', this.username); + return this.__mozId; + }, + + __cryptoId: null, + get _cryptoId() { + if (this.__cryptoId === null) + this.__cryptoId = new Identity('Mozilla Services Encryption Passphrase', + this.username); + return this.__cryptoId; + }, + + get username() { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + return branch.getCharPref("browser.places.sync.username"); + }, + set username(value) { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + branch.setCharPref("browser.places.sync.username", value); + // fixme - need to loop over all Identity objects - needs some rethinking... + this._mozId.username = value; + this._cryptoId.username = value; + }, + + get password() { return this._mozId.password; }, + set password(value) { this._mozId.password = value; }, + + get passphrase() { return this._cryptoId.password; }, + set passphrase(value) { this._cryptoId.password = value; }, + + get userPath() { + this._log.info("Hashing username " + this.username); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let data = converter.convertToByteArray(this.username, {}); + hasher.update(data, data.length); + let rawHash = hasher.finish(false); + + // return the two-digit hexadecimal code for a byte + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); + this._log.debug("Username hashes to " + hash); + return hash; + }, + + get currentUser() { + if (this._dav.loggedIn) + return this.username; + return null; + }, + + _init: function BSS__init() { + this._initLogs(); + this._log.info("Weave Sync Service Initializing"); + + this._serverURL = 'https://services.mozilla.com/'; + this._user = ''; + let enabled = false; + let schedule = 0; + try { + let branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch2); + this._serverURL = branch.getCharPref("browser.places.sync.serverURL"); + enabled = branch.getBoolPref("browser.places.sync.enabled"); + schedule = branch.getIntPref("browser.places.sync.schedule"); + + branch.addObserver("browser.places.sync", this, false); + } + catch (ex) { /* use defaults */ } + + if (!enabled) { + this._log.info("Bookmarks sync disabled"); + return; + } + + switch (schedule) { + case 0: + this._log.info("Bookmarks sync enabled, manual mode"); + break; + case 1: + this._log.info("Bookmarks sync enabled, automagic mode"); + this._enableSchedule(); + break; + default: + this._log.info("Bookmarks sync enabled"); + this._log.info("Invalid schedule setting: " + schedule); + break; + } + }, + + _enableSchedule: function BSS__enableSchedule() { + this._scheduleTimer = Cc["@mozilla.org/timer;1"]. + createInstance(Ci.nsITimer); + let listener = new EventListener(bind2(this, this._onSchedule)); + this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min + this._scheduleTimer.TYPE_REPEATING_SLACK); + }, + + _disableSchedule: function BSS__disableSchedule() { + this._scheduleTimer = null; + }, + + _onSchedule: function BSS__onSchedule() { + this._log.info("Running scheduled sync"); + this.sync(); + }, + + _initLogs: function BSS__initLogs() { + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + + this._log = logSvc.getLogger("Service.Main"); + + let formatter = logSvc.newFormatter("basic"); + let root = logSvc.rootLogger; + root.level = root.LEVEL_DEBUG; + + let capp = logSvc.newAppender("console", formatter); + capp.level = root.LEVEL_WARN; + root.addAppender(capp); + + let dapp = logSvc.newAppender("dump", formatter); + dapp.level = root.LEVEL_ALL; + root.addAppender(dapp); + + let logFile = this._dirSvc.get("ProfD", Ci.nsIFile); + let verboseFile = logFile.clone(); + logFile.append("bm-sync.log"); + logFile.QueryInterface(Ci.nsILocalFile); + verboseFile.append("bm-sync-verbose.log"); + verboseFile.QueryInterface(Ci.nsILocalFile); + + let fapp = logSvc.newFileAppender("rotating", logFile, formatter); + fapp.level = root.LEVEL_INFO; + root.addAppender(fapp); + let vapp = logSvc.newFileAppender("rotating", verboseFile, formatter); + vapp.level = root.LEVEL_DEBUG; + root.addAppender(vapp); + }, + + _lock: function BSS__lock() { + if (this._locked) { + this._log.warn("Service lock failed: already locked"); + return false; + } + this._locked = true; + this._log.debug("Service lock acquired"); + return true; + }, + + _unlock: function BSS__unlock() { + this._locked = false; + this._log.debug("Service lock released"); + }, + + // IBookmarksSyncService internal implementation + + _login: function BSS__login(onComplete) { + let [self, cont] = yield; + let success = false; + + try { + this._log.debug("Logging in"); + this._os.notifyObservers(null, "bookmarks-sync:login-start", ""); + + if (!this.username) { + this._log.warn("No username set, login failed"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + return; + } + if (!this.password) { + this._log.warn("No password given or found in password manager"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + return; + } + + this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; + this._log.info("Using server URL: " + this._dav.baseURL); + + this._dav.login.async(this._dav, cont, this.username, this.password); + success = yield; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + this._passphrase = null; + if (success) { + this._log.debug("Login successful"); + this._os.notifyObservers(null, "bookmarks-sync:login-end", ""); + } else { + this._log.debug("Login error"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + } + generatorDone(this, self, onComplete, success); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _resetLock: function BSS__resetLock(onComplete) { + let [self, cont] = yield; + let success = false; + + try { + this._log.debug("Resetting server lock"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-start", ""); + + this._dav.forceUnlock.async(this._dav, cont); + success = yield; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (success) { + this._log.debug("Server lock reset successful"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-end", ""); + } else { + this._log.debug("Server lock reset failed"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); + } + generatorDone(this, self, onComplete, success); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), + + // nsIObserver + + observe: function BSS__observe(subject, topic, data) { + switch (topic) { + case "browser.places.sync.enabled": + switch (data) { + case false: + this._log.info("Disabling automagic bookmarks sync"); + this._disableSchedule(); + break; + case true: + this._log.info("Enabling automagic bookmarks sync"); + this._enableSchedule(); + break; + } + break; + case "browser.places.sync.schedule": + switch (data) { + case 0: + this._log.info("Disabling automagic bookmarks sync"); + this._disableSchedule(); + break; + case 1: + this._log.info("Enabling automagic bookmarks sync"); + this._enableSchedule(); + break; + default: + this._log.warn("Unknown schedule value set"); + break; + } + break; + default: + // ignore, there are prefs we observe but don't care about + } + }, + + // IBookmarksSyncService public methods + + // These are global (for all engines) + + login: function BSS_login(password, passphrase) { + if (!this._lock()) + return; + // cache password & passphrase + // if null, _login() will try to get them from the pw manager + this._mozId.setTempPassword(password); + this._cryptoId.setTempPassword(passphrase); + let self = this; + this._login.async(this, function() {self._unlock()}); + }, + + logout: function BSS_logout() { + this._log.info("Logging out"); + this._dav.logout(); + this._mozId.setTempPassword(null); // clear cached password + this._cryptoId.setTempPassword(null); // and passphrase + this._os.notifyObservers(null, "bookmarks-sync:logout", ""); + }, + + resetLock: function BSS_resetLock() { + if (!this._lock()) + return; + let self = this; + this._resetLock.async(this, function() {self._unlock()}); + }, + + // These are per-engine + + sync: function BSS_sync() { + if (!this._lock()) + return; + let self = this; + this._bmkEngine.sync(function() {self._unlock()}); + }, + + resetServer: function BSS_resetServer() { + if (!this._lock()) + return; + let self = this; + this._bmkEngine.resetServer(function() {self._unlock()}); + }, + + resetClient: function BSS_resetClient() { + if (!this._lock()) + return; + let self = this; + this._bmkEngine.resetClient(function() {self._unlock()}); + } +}; + +function BookmarksEngine(davCollection, cryptoId) { + this._init(davCollection, cryptoId); +} +BookmarksEngine.prototype = { + _logName: "BmkEngine", + + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new BookmarksStore(); + return this.__store; + }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new BookmarksSyncCore(); + return this.__core; + }, + + __snapshot: null, + get _snapshot() { + if (!this.__snapshot) + this.__snapshot = new SnapshotStore(); + return this.__snapshot; + }, + set _snapshot(value) { + this.__snapshot = value; + }, + + _init: function BmkEngine__init(davCollection, cryptoId) { + this._dav = davCollection; + this._cryptoId = cryptoId; + + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service." + this._logName); + + this._snapshot.load(); + }, + + _checkStatus: function BmkEngine__checkStatus(code, msg) { + if (code >= 200 && code < 300) + return; + this._log.error(msg + " Error code: " + code); + throw 'checkStatus failed'; + }, + + /* Get the deltas/combined updates from the server + * Returns: + * status: + * -1: error + * 0: ok + * These fields may be null when status is -1: + * formatVersion: + * version of the data format itself. For compatibility checks. + * maxVersion: + * the latest version on the server + * snapVersion: + * the version of the current snapshot on the server (deltas not applied) + * snapEncryption: + * encryption algorithm currently used on the server-stored snapshot + * deltasEncryption: + * encryption algorithm currently used on the server-stored deltas + * snapshot: + * full snapshot of the latest server version (deltas applied) + * deltas: + * all of the individual deltas on the server + * updates: + * the relevant deltas (from our snapshot version to current), + * combined into a single set. + */ + _getServerData: function BmkEngine__getServerData(onComplete) { + let [self, cont] = yield; + let ret = {status: -1, + formatVersion: null, maxVersion: null, snapVersion: null, + snapEncryption: null, deltasEncryption: null, + snapshot: null, deltas: null, updates: null}; + + try { + this._log.info("Getting bookmarks status from server"); + this._dav.GET("bookmarks-status.json", cont); + let resp = yield; + let status = resp.status; + + switch (status) { + case 200: + this._log.info("Got bookmarks status from server"); + + let status = eval(resp.responseText); + let deltas, allDeltas; + let snap = new SnapshotStore(); + + // Bail out if the server has a newer format version than we can parse + if (status.formatVersion > STORAGE_FORMAT_VERSION) { + this._log.error("Server uses storage format v" + status.formatVersion + + ", this client understands up to v" + STORAGE_FORMAT_VERSION); + generatorDone(this, self, onComplete, ret) + return; + } + + if (status.formatVersion == 0) { + ret.snapEncryption = status.snapEncryption = "none"; + ret.deltasEncryption = status.deltasEncryption = "none"; + } + + if (status.GUID != this._snapshot.GUID) { + this._log.info("Remote/local sync GUIDs do not match. " + + "Forcing initial sync."); + this._store.resetGUIDs(); + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.GUID = status.GUID; + } + + if (this._snapshot.version < status.snapVersion) { + if (this._snapshot.version >= 0) + this._log.info("Local snapshot is out of date"); + + this._log.info("Downloading server snapshot"); + this._dav.GET("bookmarks-snapshot.json", cont); + resp = yield; + this._checkStatus(resp.status, "Could not download snapshot."); + snap.data = Weave.Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.snapEncryption); + + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Weave.Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = eval(uneval(allDeltas)); + + } else if (this._snapshot.version >= status.snapVersion && + this._snapshot.version < status.maxVersion) { + snap.data = eval(uneval(this._snapshot.data)); + + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Weave.Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); + + } else if (this._snapshot.version == status.maxVersion) { + snap.data = eval(uneval(this._snapshot.data)); + + // FIXME: could optimize this case by caching deltas file + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Weave.Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = []; + + } else { // this._snapshot.version > status.maxVersion + this._log.error("Server snapshot is older than local snapshot"); + return; + } + + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands(deltas[i]); + } + + ret.status = 0; + ret.formatVersion = status.formatVersion; + ret.maxVersion = status.maxVersion; + ret.snapVersion = status.snapVersion; + ret.snapEncryption = status.snapEncryption; + ret.deltasEncryption = status.deltasEncryption; + ret.snapshot = snap.data; + ret.deltas = allDeltas; + this._core.detectUpdates(cont, this._snapshot.data, snap.data); + ret.updates = yield; + break; + + case 404: + this._log.info("Server has no status file, Initial upload to server"); + + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + + this._fullUpload.async(this, cont); + let uploadStatus = yield; + if (!uploadStatus) + return; + + this._log.info("Initial upload to server successful"); + this.snapshot.save(); + + ret.status = 0; + ret.formatVersion = STORAGE_FORMAT_VERSION; + ret.maxVersion = this._snapshot.version; + ret.snapVersion = this._snapshot.version; + ret.snapEncryption = Weave.Crypto.defaultAlgorithm; + ret.deltasEncryption = Weave.Crypto.defaultAlgorithm; + ret.snapshot = eval(uneval(this._snapshot.data)); + ret.deltas = []; + ret.updates = []; + break; + + default: + this._log.error("Could not get bookmarks.status: unknown HTTP status code " + + status); + break; + } + + } catch (e) { + if (e != 'checkStatus failed' && + e != 'decrypt failed') + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, self, onComplete, ret) + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _fullUpload: function BmkEngine__fullUpload(onComplete) { + let [self, cont] = yield; + let ret = false; + + try { + let data = Weave.Crypto.PBEencrypt(this._snapshot.serialize(), + this._cryptoId); + this._dav.PUT("bookmarks-snapshot.json", data, cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload snapshot."); + + this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload deltas."); + + let c = 0; + for (GUID in this._snapshot.data) + c++; + + this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshot.version, + maxVersion: this._snapshot.version, + snapEncryption: Weave.Crypto.defaultAlgorithm, + deltasEncryption: "none", + bookmarksCount: c}), cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload status file."); + + this._log.info("Full upload to server successful"); + ret = true; + + } catch (e) { + if (e != 'checkStatus failed') + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, self, onComplete, ret) + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + // original + // / \ + // A / \ B + // / \ + // client --C-> server + // \ / + // D \ / C + // \ / + // final + + // If we have a saved snapshot, original == snapshot. Otherwise, + // it's the empty set {}. + + // C is really the diff between server -> final, so if we determine + // D we can calculate C from that. In the case where A and B have + // no conflicts, C == A and D == B. + + // Sync flow: + // 1) Fetch server deltas + // 1.1) Construct current server status from snapshot + server deltas + // 1.2) Generate single delta from snapshot -> current server status ("B") + // 2) Generate local deltas from snapshot -> current client status ("A") + // 3) Reconcile client/server deltas and generate new deltas for them. + // Reconciliation won't generate C directly, we will simply diff + // server->final after step 3.1. + // 3.1) Apply local delta with server changes ("D") + // 3.2) Append server delta to the delta file and upload ("C") + + _sync: function BmkEngine__sync(onComplete) { + let [self, cont] = yield; + let synced = false, locked = null; + + try { + this._log.info("Beginning sync"); + this._os.notifyObservers(null, "bookmarks-sync:sync-start", ""); + + this._dav.lock.async(this._dav, cont); + locked = yield; + + if (locked) + this._log.info("Lock acquired"); + else { + this._log.warn("Could not acquire lock, aborting sync"); + return; + } + + // 1) Fetch server deltas + this._getServerData.async(this, cont); + let server = yield; + + this._log.info("Local snapshot version: " + this._snapshot.version); + this._log.info("Server status: " + server.status); + this._log.info("Server maxVersion: " + server.maxVersion); + this._log.info("Server snapVersion: " + server.snapVersion); + + if (server.status != 0) { + this._log.fatal("Sync error: could not get server status, " + + "or initial upload failed. Aborting sync."); + return; + } + + // 2) Generate local deltas from snapshot -> current client status + + let localJson = new SnapshotStore(); + localJson.data = this._store.wrap(); + this._core.detectUpdates(cont, this._snapshot.data, localJson.data); + let localUpdates = yield; + + this._log.debug("local json:\n" + localJson.serialize()); + this._log.debug("Local updates: " + serializeCommands(localUpdates)); + this._log.debug("Server updates: " + serializeCommands(server.updates)); + + if (server.updates.length == 0 && localUpdates.length == 0) { + this._snapshot.version = server.maxVersion; + this._log.info("Sync complete (1): no changes needed on client or server"); + synced = true; + return; + } + + // 3) Reconcile client/server deltas and generate new deltas for them. + + this._log.info("Reconciling client/server updates"); + this._core.reconcile(cont, localUpdates, server.updates); + let ret = yield; + + let clientChanges = ret.propagations[0]; + let serverChanges = ret.propagations[1]; + let clientConflicts = ret.conflicts[0]; + let serverConflicts = ret.conflicts[1]; + + this._log.info("Changes for client: " + clientChanges.length); + this._log.info("Predicted changes for server: " + serverChanges.length); + this._log.info("Client conflicts: " + clientConflicts.length); + this._log.info("Server conflicts: " + serverConflicts.length); + this._log.debug("Changes for client: " + serializeCommands(clientChanges)); + this._log.debug("Predicted changes for server: " + serializeCommands(serverChanges)); + this._log.debug("Client conflicts: " + serializeConflicts(clientConflicts)); + this._log.debug("Server conflicts: " + serializeConflicts(serverConflicts)); + + if (!(clientChanges.length || serverChanges.length || + clientConflicts.length || serverConflicts.length)) { + this._log.info("Sync complete (2): no changes needed on client or server"); + this._snapshot.data = localJson.data; + this._snapshot.version = server.maxVersion; + this._snapshot.save(); + synced = true; + return; + } + + if (clientConflicts.length || serverConflicts.length) { + this._log.warn("Conflicts found! Discarding server changes"); + } + + let savedSnap = eval(uneval(this._snapshot.data)); + let savedVersion = this._snapshot.version; + let newSnapshot; + + // 3.1) Apply server changes to local store + if (clientChanges.length) { + this._log.info("Applying changes locally"); + // Note that we need to need to apply client changes to the + // current tree, not the saved snapshot + + localJson.applyCommands(clientChanges); + this._snapshot.data = localJson.data; + this._snapshot.version = server.maxVersion; + this._store.applyCommands(clientChanges); + newSnapshot = this._store.wrap(); + + this._core.detectUpdates(cont, this._snapshot.data, newSnapshot); + let diff = yield; + if (diff.length != 0) { + this._log.warn("Commands did not apply correctly"); + this._log.debug("Diff from snapshot+commands -> " + + "new snapshot after commands:\n" + + serializeCommands(diff)); + // FIXME: do we really want to revert the snapshot here? + this._snapshot.data = eval(uneval(savedSnap)); + this._snapshot.version = savedVersion; + } + + this._snapshot.save(); + } + + // 3.2) Append server delta to the delta file and upload + + // Generate a new diff, from the current server snapshot to the + // current client snapshot. In the case where there are no + // conflicts, it should be the same as what the resolver returned + + newSnapshot = this._store.wrap(); + this._core.detectUpdates(cont, server.snapshot, newSnapshot); + let serverDelta = yield; + + // Log an error if not the same + if (!(serverConflicts.length || + deepEquals(serverChanges, serverDelta))) + this._log.warn("Predicted server changes differ from " + + "actual server->client diff (can be ignored in many cases)"); + + this._log.info("Actual changes for server: " + serverDelta.length); + this._log.debug("Actual changes for server: " + + serializeCommands(serverDelta)); + + if (serverDelta.length) { + this._log.info("Uploading changes to server"); + + this._snapshot.data = newSnapshot; + this._snapshot.version = ++server.maxVersion; + + server.deltas.push(serverDelta); + + if (server.formatVersion != STORAGE_FORMAT_VERSION || + this._encryptionChanged) { + this._fullUpload.async(this, cont); + let status = yield; + if (!status) + this._log.error("Could not upload files to server"); // eep? + + } else { + let data = Weave.Crypto.PBEencrypt(serializeCommands(server.deltas), + this._cryptoId); + this._dav.PUT("bookmarks-deltas.json", data, cont); + let deltasPut = yield; + + let c = 0; + for (GUID in this._snapshot.data) + c++; + + this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshot.version, + snapEncryption: server.snapEncryption, + deltasEncryption: Weave.Crypto.defaultAlgorithm, + Bookmarkscount: c}), cont); + let statusPut = yield; + + if (deltasPut.status >= 200 && deltasPut.status < 300 && + statusPut.status >= 200 && statusPut.status < 300) { + this._log.info("Successfully updated deltas and status on server"); + this._snapshot.save(); + } else { + // FIXME: revert snapshot here? - can't, we already applied + // updates locally! - need to save and retry + this._log.error("Could not update deltas on server"); + } + } + } + + this._log.info("Sync complete"); + synced = true; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + let ok = false; + if (locked) { + this._dav.unlock.async(this._dav, cont); + ok = yield; + } + if (ok && synced) { + this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); + generatorDone(this, self, onComplete, true); + } else { + this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); + generatorDone(this, self, onComplete, false); + } + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _resetServer: function BmkEngine__resetServer(onComplete) { + let [self, cont] = yield; + let done = false; + + try { + this._log.debug("Resetting server data"); + this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", ""); + + this._dav.lock.async(this._dav, cont); + let locked = yield; + if (locked) + this._log.debug("Lock acquired"); + else { + this._log.warn("Could not acquire lock, aborting server reset"); + return; + } + + this._dav.DELETE("bookmarks-status.json", cont); + let statusResp = yield; + this._dav.DELETE("bookmarks-snapshot.json", cont); + let snapshotResp = yield; + this._dav.DELETE("bookmarks-deltas.json", cont); + let deltasResp = yield; + + this._dav.unlock.async(this._dav, cont); + let unlocked = yield; + + function ok(code) { + if (code >= 200 && code < 300) + return true; + if (code == 404) + return true; + return false; + } + + if (!(ok(statusResp.status) && ok(snapshotResp.status) && + ok(deltasResp.status))) { + this._log.error("Could delete server data, response codes " + + statusResp.status + ", " + snapshotResp.status + ", " + + deltasResp.status); + return; + } + + this._log.debug("Server files deleted"); + done = true; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (done) { + this._log.debug("Server reset completed successfully"); + this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); + } else { + this._log.debug("Server reset failed"); + this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", ""); + } + generatorDone(this, self, onComplete, done) + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _resetClient: function BmkEngine__resetClient(onComplete) { + let [self, cont] = yield; + let done = false; + + try { + this._log.debug("Resetting client state"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-start", ""); + + this._snapshot.data = {}; + this._snapshot.version = -1; + this.snapshot.save(); + done = true; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (done) { + this._log.debug("Client reset completed successfully"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-end", ""); + } else { + this._log.debug("Client reset failed"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-error", ""); + } + generatorDone(this, self, onComplete, done); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + sync: function BmkEngine_sync(onComplete) { + return this._sync.async(this, onComplete); + }, + + resetServer: function BmkEngine_resetServer(onComplete) { + return this._resetServer.async(this, onComplete); + }, + + resetClient: function BmkEngine_resetClient(onComplete) { + return this._resetClient.async(this, onComplete); + } +}; + +function Identity(realm, username, password) { + this._realm = realm; + this._username = username; + this._password = password; +} +Identity.prototype = { + get realm() { return this._realm; }, + set realm(value) { this._realm = value; }, + + get username() { return this._username; }, + set username(value) { this._username = value; }, + + _password: null, + get password() { + if (this._password === null) + return findPassword(this.realm, this.username); + return this._password; + }, + set password(value) { + setPassword(this.realm, this.username, value); + }, + + setTempPassword: function Id_setTempPassword(value) { + this._password = value; + } +}; + +function Crypto() { + this._init(); +} +Crypto.prototype = { + _logName: "Crypto", + + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + + __xxxtea: {}, + __xxxteaLoaded: false, + get _xxxtea() { + if (!this.__xxxteaLoaded) { + let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + jsLoader.loadSubScript("chrome://weave/content/encrypt.js", this.__xxxtea); + this.__xxxteaLoaded = true; + } + return this.__xxxtea; + }, + + get defaultAlgorithm() { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + return branch.getCharPref("browser.places.sync.encryption"); + }, + set defaultAlgorithm(value) { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + let cur = branch.getCharPref("browser.places.sync.encryption"); + if (value != cur) + branch.setCharPref("browser.places.sync.encryption", value); + }, + + _init: function Crypto__init() { + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service." + this._logName); + + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch2); + branch.addObserver("browser.places.sync.encryption", this, false); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), + + // nsIObserver + + observe: function Sync_observe(subject, topic, data) { + switch (topic) { + case "browser.places.sync.encryption": + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + + let cur = branch.getCharPref("browser.places.sync.encryption"); + if (cur == data) + return; + + switch (data) { + case "none": + this._log.info("Encryption disabled"); + break; + case "XXXTEA": + this._log.info("Using encryption algorithm: " + data); + break; + default: + this._log.warn("Unknown encryption algorithm, resetting"); + branch.setCharPref("browser.places.sync.encryption", "XXXTEA"); + return; // otherwise we'll send the alg changed event twice + } + // FIXME: listen to this bad boy somewhere + this._os.notifyObservers(null, "weave:encryption:algorithm-changed", ""); + break; + default: + this._log.warn("Unknown encryption preference changed - ignoring"); + } + }, + + // Crypto + + PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) { + let out; + if (!algorithm) + algorithm = this.defaultAlgorithm; + switch (algorithm) { + case "none": + out = data; + break; + case "XXXTEA": + try { + this._log.debug("Encrypting data"); + out = this._xxxtea.encrypt(data, identity.password); + this._log.debug("Done encrypting data"); + } catch (e) { + this._log.error("Data encryption failed: " + e); + throw 'encrypt failed'; + } + break; + default: + this._log.error("Unknown encryption algorithm: " + algorithm); + throw 'encrypt failed'; + } + return out; + }, + + PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) { + let out; + switch (algorithm) { + case "none": + out = eval(data); + break; + case "XXXTEA": + try { + this._log.debug("Decrypting data"); + out = eval(this._xxxtea.decrypt(data, identity.password)); + this._log.debug("Done decrypting data"); + } catch (e) { + this._log.error("Data decryption failed: " + e); + throw 'decrypt failed'; + } + break; + default: + this._log.error("Unknown encryption algorithm: " + algorithm); + throw 'decrypt failed'; + } + return out; + } +}; + +let Weave = {}; +Weave.Crypto = new Crypto(); +Weave.Service = new BookmarksSyncService(); + +function findPassword(realm, username) { + // fixme: make a request and get the realm ? + let password; + let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, 'chrome://sync', null, realm); + + for (let i = 0; i < logins.length; i++) { + if (logins[i].username == username) { + password = logins[i].password; + break; + } + } + return password; +} + +function setPassword(realm, username, password) { + // cleanup any existing passwords + let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, 'chrome://sync', null, realm); + for(let i = 0; i < logins.length; i++) { + lm.removeLogin(logins[i]); + } + + if (!password) + return; + + // save the new one + let nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo('chrome://sync', null, realm, + username, password, null, null); + lm.addLogin(login); +} + +serializeCommands: function serializeCommands(commands) { + let json = uneval(commands); + json = json.replace(/ {action/g, "\n {action"); + return json; +} + +serializeConflicts: function serializeConflicts(conflicts) { + let json = uneval(conflicts); + json = json.replace(/ {action/g, "\n {action"); + return json; +} + +/* + * SyncCore objects + * Sync cores deal with diff creation and conflict resolution. + * Tree data structures where all nodes have GUIDs only need to be + * subclassed for each data type to implement commandLike and + * itemExists. + */ + +function SyncCore() { + this._init(); +} +SyncCore.prototype = { + _logName: "Sync", + + _init: function SC__init() { + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service." + this._logName); + }, + + // FIXME: this won't work for deep objects, or objects with optional properties + _getEdits: function SC__getEdits(a, b) { + let ret = {numProps: 0, props: {}}; + for (prop in a) { + if (!deepEquals(a[prop], b[prop])) { + ret.numProps++; + ret.props[prop] = b[prop]; + } + } + return ret; + }, + + _nodeParents: function SC__nodeParents(GUID, tree) { + return this._nodeParentsInt(GUID, tree, []); + }, + + _nodeParentsInt: function SC__nodeParentsInt(GUID, tree, parents) { + if (!tree[GUID] || !tree[GUID].parentGUID) + return parents; + parents.push(tree[GUID].parentGUID); + return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); + }, + + _detectUpdates: function SC__detectUpdates(onComplete, a, b) { + let [self, cont] = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + let cmds = []; + + try { + for (let GUID in a) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (GUID in b) { + let edits = this._getEdits(a[GUID], b[GUID]); + if (edits.numProps == 0) // no changes - skip + continue; + let parents = this._nodeParents(GUID, b); + cmds.push({action: "edit", GUID: GUID, + depth: parents.length, parents: parents, + data: edits.props}); + } else { + let parents = this._nodeParents(GUID, a); // ??? + cmds.push({action: "remove", GUID: GUID, + depth: parents.length, parents: parents}); + } + } + for (let GUID in b) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (GUID in a) + continue; + let parents = this._nodeParents(GUID, b); + cmds.push({action: "create", GUID: GUID, + depth: parents.length, parents: parents, + data: b[GUID]}); + } + cmds.sort(function(a, b) { + if (a.depth > b.depth) + return 1; + if (a.depth < b.depth) + return -1; + if (a.index > b.index) + return -1; + if (a.index < b.index) + return 1; + return 0; // should never happen, but not a big deal if it does + }); + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + timer = null; + generatorDone(this, self, onComplete, cmds); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _commandLike: function SC__commandLike(a, b) { + this._log.error("commandLike needs to be subclassed"); + + // Check that neither command is null, and verify that the GUIDs + // are different (otherwise we need to check for edits) + if (!a || !b || a.GUID == b.GUID) + return false; + + // Check that all other properties are the same + // FIXME: could be optimized... + for (let key in a) { + if (key != "GUID" && !deepEquals(a[key], b[key])) + return false; + } + for (let key in b) { + if (key != "GUID" && !deepEquals(a[key], b[key])) + return false; + } + return true; + }, + + // When we change the GUID of a local item (because we detect it as + // being the same item as a remote one), we need to fix any other + // local items that have it as their parent + _fixParents: function SC__fixParents(list, oldGUID, newGUID) { + for (let i = 0; i < list.length; i++) { + if (!list[i]) + continue; + if (list[i].data.parentGUID == oldGUID) + list[i].data.parentGUID = newGUID; + for (let j = 0; j < list[i].parents.length; j++) { + if (list[i].parents[j] == oldGUID) + list[i].parents[j] = newGUID; + } + } + }, + + _conflicts: function SC__conflicts(a, b) { + if ((a.GUID == b.GUID) && !deepEquals(a, b)) + return true; + return false; + }, + + _getPropagations: function SC__getPropagations(commands, conflicts, propagations) { + for (let i = 0; i < commands.length; i++) { + let alsoConflicts = function(elt) { + return (elt.action == "create" || elt.action == "remove") && + commands[i].parents.indexOf(elt.GUID) >= 0; + }; + if (conflicts.some(alsoConflicts)) + conflicts.push(commands[i]); + + let cmdConflicts = function(elt) { + return elt.GUID == commands[i].GUID; + }; + if (!conflicts.some(cmdConflicts)) + propagations.push(commands[i]); + } + }, + + _itemExists: function SC__itemExists(GUID) { + this._log.error("itemExists needs to be subclassed"); + return false; + }, + + _reconcile: function SC__reconcile(onComplete, listA, listB) { + let [self, cont] = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + let propagations = [[], []]; + let conflicts = [[], []]; + let ret = {propagations: propagations, conflicts: conflicts}; + this._log.debug("Reconciling " + listA.length + + " against " + listB.length + "commands"); + + try { + let guidChanges = []; + for (let i = 0; i < listA.length; i++) { + let a = listA[i]; + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + //this._log.debug("comparing " + i + ", listB length: " + listB.length); + + let skip = false; + listB = listB.filter(function(b) { + // fast path for when we already found a matching command + if (skip) + return true; + + if (deepEquals(a, b)) { + delete listA[i]; // a + skip = true; + return false; // b + + } else if (this._commandLike(a, b)) { + this._fixParents(listA, a.GUID, b.GUID); + guidChanges.push({action: "edit", + GUID: a.GUID, + data: {GUID: b.GUID}}); + delete listA[i]; // a + skip = true; + return false; // b, but we add it back from guidChanges + } + + // watch out for create commands with GUIDs that already exist + if (b.action == "create" && this._itemExists(b.GUID)) { + this._log.error("Remote command has GUID that already exists " + + "locally. Dropping command."); + return false; // delete b + } + return true; // keep b + }, this); + } + + listA = listA.filter(function(elt) { return elt }); + listB = listB.concat(guidChanges); + + for (let i = 0; i < listA.length; i++) { + for (let j = 0; j < listB.length; j++) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (this._conflicts(listA[i], listB[j]) || + this._conflicts(listB[j], listA[i])) { + if (!conflicts[0].some( + function(elt) { return elt.GUID == listA[i].GUID })) + conflicts[0].push(listA[i]); + if (!conflicts[1].some( + function(elt) { return elt.GUID == listB[j].GUID })) + conflicts[1].push(listB[j]); + } + } + } + + this._getPropagations(listA, conflicts[0], propagations[1]); + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + this._getPropagations(listB, conflicts[1], propagations[0]); + ret = {propagations: propagations, conflicts: conflicts}; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + timer = null; + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + // Public methods + + detectUpdates: function SC_detectUpdates(onComplete, a, b) { + return this._detectUpdates.async(this, onComplete, a, b); + }, + + reconcile: function SC_reconcile(onComplete, listA, listB) { + return this._reconcile.async(this, onComplete, listA, listB); + } +}; + +function BookmarksSyncCore() { + this._init(); +} +BookmarksSyncCore.prototype = { + _logName: "BMSync", + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + // NOTE: Needs to be subclassed + _itemExists: function BSC__itemExists(GUID) { + return this._bms.getItemIdForGUID(GUID) >= 0; + }, + + _commandLike: function BSC_commandLike(a, b) { + // Check that neither command is null, that their actions, types, + // and parents are the same, and that they don't have the same + // GUID. + // Items with the same GUID do not qualify for 'likeness' because + // we already consider them to be the same object, and therefore + // we need to process any edits. + // The parent GUID check works because reconcile() fixes up the + // parent GUIDs as it runs, and the command list is sorted by + // depth + if (!a || !b || + a.action != b.action || + a.data.type != b.data.type || + a.data.parentGUID != b.data.parentGUID || + a.GUID == b.GUID) + return false; + + // Bookmarks are allowed to be in a different index as long as + // they are in the same folder. Folders and separators must be at + // the same index to qualify for 'likeness'. + switch (a.data.type) { + case "bookmark": + if (a.data.URI == b.data.URI && + a.data.title == b.data.title) + return true; + return false; + case "query": + if (a.data.URI == b.data.URI && + a.data.title == b.data.title) + return true; + return false; + case "microsummary": + if (a.data.URI == b.data.URI && + a.data.generatorURI == b.data.generatorURI) + return true; + return false; + case "folder": + if (a.index == b.index && + a.data.title == b.data.title) + return true; + return false; + case "livemark": + if (a.data.title == b.data.title && + a.data.siteURI == b.data.siteURI && + a.data.feedURI == b.data.feedURI) + return true; + return false; + case "separator": + if (a.index == b.index) + return true; + return false; + default: + this._log.error("commandLike: Unknown item type: " + uneval(a)); + return false; + } + } +}; +BookmarksSyncCore.prototype.__proto__ = new SyncCore(); + +/* + * Data Stores + * These can wrap, serialize items and apply commands + */ + +function Store() { + this._init(); +} +Store.prototype = { + _logName: "Store", + + _init: function Store__init() { + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service." + this._logName); + }, + + wrap: function Store_wrap() { + }, + + applyCommands: function Store_applyCommands(commandList) { + } +}; + +function SnapshotStore() { + this._init(); +} +SnapshotStore.prototype = { + _logName: "SStore", + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + return this.__dirSvc; + }, + + // Last synced tree, version, and GUID (to detect if the store has + // been completely replaced and invalidate the snapshot) + + _data: {}, + get data() { return this._data; }, + set data(value) { this._data = value; }, + + _version: 0, + get version() { return this._version; }, + set version(value) { this._version = value; }, + + _GUID: null, + get GUID() { + if (!this._GUID) { + let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator); + this._GUID = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); + } + return this._GUID; + }, + set GUID(GUID) { + this._GUID = GUID; + }, + + save: function SStore_save() { + this._log.info("Saving snapshot to disk"); + + let file = this._dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync-snapshot.json"); + file.QueryInterface(Ci.nsILocalFile); + + if (!file.exists()) + file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); + + let fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; + fos.init(file, flags, PERMS_FILE, 0); + + let out = {version: this.version, + GUID: this.GUID, + snapshot: this.data}; + out = uneval(out); + fos.write(out, out.length); + fos.close(); + }, + + load: function SStore_load() { + let file = this._dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync-snapshot.json"); + + if (!file.exists()) + return; + + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(file, MODE_RDONLY, PERMS_FILE, 0); + fis.QueryInterface(Ci.nsILineInputStream); + + let json = ""; + while (fis.available()) { + let ret = {}; + fis.readLine(ret); + json += ret.value; + } + fis.close(); + json = eval(json); + + if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { + this._log.info("Read saved snapshot from disk"); + this.data = json.snapshot; + this.version = json.version; + this.GUID = json.GUID; + } + }, + + serialize: function SStore_serialize() { + let json = uneval(this.data); + json = json.replace(/:{type/g, ":\n\t{type"); + json = json.replace(/}, /g, "},\n "); + json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); + json = json.replace(/, index/g, ",\n\t index"); + json = json.replace(/, title/g, ",\n\t title"); + json = json.replace(/, URI/g, ",\n\t URI"); + json = json.replace(/, tags/g, ",\n\t tags"); + json = json.replace(/, keyword/g, ",\n\t keyword"); + return json; + }, + + wrap: function SStore_wrap() { + }, + + applyCommands: function SStore_applyCommands(commands) { + for (let i = 0; i < commands.length; i++) { + // this._log.debug("Applying cmd to obj: " + uneval(commands[i])); + switch (commands[i].action) { + case "create": + this._data[commands[i].GUID] = eval(uneval(commands[i].data)); + break; + case "edit": + if ("GUID" in commands[i].data) { + // special-case guid changes + let newGUID = commands[i].data.GUID, + oldGUID = commands[i].GUID; + + this._data[newGUID] = this._data[oldGUID]; + delete this._data[oldGUID] + + for (let GUID in this._data) { + if (this._data[GUID].parentGUID == oldGUID) + this._data[GUID].parentGUID = newGUID; + } + } + for (let prop in commands[i].data) { + if (prop == "GUID") + continue; + this._data[commands[i].GUID][prop] = commands[i].data[prop]; + } + break; + case "remove": + delete this._data[commands[i].GUID]; + break; + } + } + return this._data; + } +}; +SnapshotStore.prototype.__proto__ = new Store(); + +function BookmarksStore() { + this._init(); +} +BookmarksStore.prototype = { + _logName: "BStore", + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + __hsvc: null, + get _hsvc() { + if (!this.__hsvc) + this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + return this.__hsvc; + }, + + __ls: null, + get _ls() { + if (!this.__ls) + this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. + getService(Ci.nsILivemarkService); + return this.__ls; + }, + + __ms: null, + get _ms() { + if (!this.__ms) + this.__ms = Cc["@mozilla.org/microsummary/service;1"]. + getService(Ci.nsIMicrosummaryService); + return this.__ms; + }, + + __ts: null, + get _ts() { + if (!this.__ts) + this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + return this.__ts; + }, + + __ans: null, + get _ans() { + if (!this.__ans) + this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + return this.__ans; + }, + + _getFolderNodes: function BSS__getFolderNodes(folder) { + let query = this._hsvc.getNewQuery(); + query.setFolders([folder], 1); + return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + }, + + _wrapNode: function BSS__wrapNode(node) { + var items = {}; + this._wrapNodeInternal(node, items, null, null); + return items; + }, + + _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) { + let GUID = this._bms.getItemGUID(node.itemId); + let item = {parentGUID: parentGUID, + index: index}; + + if (node.type == node.RESULT_TYPE_FOLDER) { + if (this._ls.isLivemark(node.itemId)) { + item.type = "livemark"; + let siteURI = this._ls.getSiteURI(node.itemId); + let feedURI = this._ls.getFeedURI(node.itemId); + item.siteURI = siteURI? siteURI.spec : ""; + item.feedURI = feedURI? feedURI.spec : ""; + } else { + item.type = "folder"; + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._wrapNodeInternal(node.getChild(i), items, GUID, i); + } + } + item.title = node.title; + } else if (node.type == node.RESULT_TYPE_URI || + node.type == node.RESULT_TYPE_QUERY) { + if (this._ms.hasMicrosummary(node.itemId)) { + item.type = "microsummary"; + let micsum = this._ms.getMicrosummary(node.itemId); + item.generatorURI = micsum.generator.uri.spec; // breaks local generators + } else if (node.type == node.RESULT_TYPE_QUERY) { + item.type = "query"; + item.title = node.title; + } else { + item.type = "bookmark"; + item.title = node.title; + } + item.URI = node.uri; + item.tags = this._ts.getTagsForURI(makeURI(node.uri)); + item.keyword = this._bms.getKeywordForBookmark(node.itemId); + } else if (node.type == node.RESULT_TYPE_SEPARATOR) { + item.type = "separator"; + } else { + this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); + return; + } + + items[GUID] = item; + }, + + _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { + return this._wrapNode(this._getFolderNodes(folder)); + }, + + _resetGUIDsInt: function BSS__resetGUIDsInt(node) { + if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) + this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); + + if (node.type == node.RESULT_TYPE_FOLDER && + !this._ls.isLivemark(node.itemId)) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._resetGUIDsInt(node.getChild(i)); + } + } + }, + + _createCommand: function BStore__createCommand(command) { + let newId; + let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); + + if (parentId < 0) { + this._log.warn("Creating node with unknown parent -> reparenting to root"); + parentId = this._bms.bookmarksMenuFolder; + } + + switch (command.data.type) { + case "query": + case "bookmark": + case "microsummary": + this._log.info(" -> creating bookmark \"" + command.data.title + "\""); + let URI = makeURI(command.data.URI); + newId = this._bms.insertBookmark(parentId, + URI, + command.data.index, + command.data.title); + this._ts.untagURI(URI, null); + this._ts.tagURI(URI, command.data.tags); + this._bms.setKeywordForBookmark(newId, command.data.keyword); + + if (command.data.type == "microsummary") { + this._log.info(" \-> is a microsummary"); + let genURI = makeURI(command.data.generatorURI); + try { + let micsum = this._ms.createMicrosummary(URI, genURI); + this._ms.setMicrosummary(newId, micsum); + } + catch(ex) { /* ignore "missing local generator" exceptions */ } + } + break; + case "folder": + this._log.info(" -> creating folder \"" + command.data.title + "\""); + newId = this._bms.createFolder(parentId, + command.data.title, + command.data.index); + break; + case "livemark": + this._log.info(" -> creating livemark \"" + command.data.title + "\""); + newId = this._ls.createLivemark(parentId, + command.data.title, + makeURI(command.data.siteURI), + makeURI(command.data.feedURI), + command.data.index); + break; + case "separator": + this._log.info(" -> creating separator"); + newId = this._bms.insertSeparator(parentId, command.data.index); + break; + default: + this._log.error("_createCommand: Unknown item type: " + command.data.type); + break; + } + if (newId) + this._bms.setItemGUID(newId, command.GUID); + }, + + _removeCommand: function BStore__removeCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Attempted to remove item " + command.GUID + + ", but it does not exist. Skipping."); + return; + } + var type = this._bms.getItemType(itemId); + + switch (type) { + case this._bms.TYPE_BOOKMARK: + this._log.info(" -> removing bookmark " + command.GUID); + this._bms.removeItem(itemId); + break; + case this._bms.TYPE_FOLDER: + this._log.info(" -> removing folder " + command.GUID); + this._bms.removeFolder(itemId); + break; + case this._bms.TYPE_SEPARATOR: + this._log.info(" -> removing separator " + command.GUID); + this._bms.removeItem(itemId); + break; + default: + this._log.error("removeCommand: Unknown item type: " + type); + break; + } + }, + + _editCommand: function BStore__editCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); + return; + } + + for (let key in command.data) { + switch (key) { + case "GUID": + var existing = this._bms.getItemIdForGUID(command.data.GUID); + if (existing < 0) + this._bms.setItemGUID(itemId, command.data.GUID); + else + this._log.warn("Can't change GUID " + command.GUID + + " to " + command.data.GUID + ": GUID already exists."); + break; + case "title": + this._bms.setItemTitle(itemId, command.data.title); + break; + case "URI": + this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI)); + break; + case "index": + this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), + command.data.index); + break; + case "parentGUID": + let index = -1; + if (command.data.index && command.data.index >= 0) + index = command.data.index; + this._bms.moveItem( + itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); + break; + case "tags": + let tagsURI = this._bms.getBookmarkURI(itemId); + this._ts.untagURI(URI, null); + this._ts.tagURI(tagsURI, command.data.tags); + break; + case "keyword": + this._bms.setKeywordForBookmark(itemId, command.data.keyword); + break; + case "generatorURI": + let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); + let genURI = makeURI(command.data.generatorURI); + let micsum = this._ms.createMicrosummary(micsumURI, genURI); + this._ms.setMicrosummary(itemId, micsum); + break; + case "siteURI": + this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); + break; + case "feedURI": + this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); + break; + default: + this._log.warn("Can't change item property: " + key); + break; + } + } + }, + + wrap: function BStore_wrap() { + let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder); + let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder); + let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder); + + for (let guid in unfiled) { + if (!(guid in filed)) + filed[guid] = unfiled[guid]; + } + + for (let guid in toolbar) { + if (!(guid in filed)) + filed[guid] = toolbar[guid]; + } + + return filed; // (combined) + }, + + resetGUIDs: function BStore_resetGUIDs() { + this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); + }, + + applyCommands: function BStore_applyCommands(commandList) { + for (var i = 0; i < commandList.length; i++) { + var command = commandList[i]; + this._log.debug("Processing command: " + uneval(command)); + switch (command["action"]) { + case "create": + this._createCommand(command); + break; + case "remove": + this._removeCommand(command); + break; + case "edit": + this._editCommand(command); + break; + default: + this._log.error("unknown action in command: " + command["action"]); + break; + } + } + } +}; +BookmarksStore.prototype.__proto__ = new Store(); + +/* + * DAV object + * Abstracts the raw DAV commands + */ + +function DAVCollection(baseURL) { + this._baseURL = baseURL; + this._authProvider = new DummyAuthProvider(); + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service.DAV"); +} +DAVCollection.prototype = { + __dp: null, + get _dp() { + if (!this.__dp) + this.__dp = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + return this.__dp; + }, + + _auth: null, + + get baseURL() { + return this._baseURL; + }, + set baseURL(value) { + this._baseURL = value; + }, + + _loggedIn: false, + get loggedIn() { + return this._loggedIn; + }, + + _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { + let [self, cont] = yield; + let ret; + + try { + this._log.debug("Creating " + op + " request for " + this._baseURL + path); + + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); + request = request.QueryInterface(Ci.nsIDOMEventTarget); + + request.addEventListener("load", new EventListener(cont, "load"), false); + request.addEventListener("error", new EventListener(cont, "error"), false); + request = request.QueryInterface(Ci.nsIXMLHttpRequest); + request.open(op, this._baseURL + path, true); + + + // Force cache validation + let channel = request.channel; + channel = channel.QueryInterface(Ci.nsIRequest); + let loadFlags = channel.loadFlags; + loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; + channel.loadFlags = loadFlags; + + let key; + for (key in headers) { + this._log.debug("HTTP Header " + key + ": " + headers[key]); + request.setRequestHeader(key, headers[key]); + } + + this._authProvider._authFailed = false; + request.channel.notificationCallbacks = this._authProvider; + + request.send(data); + let event = yield; + ret = event.target; + + if (this._authProvider._authFailed) + this._log.warn("_makeRequest: authentication failed"); + if (ret.status < 200 || ret.status >= 300) + this._log.warn("_makeRequest: got status " + ret.status); + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + get _defaultHeaders() { + return {'Authorization': this._auth? this._auth : '', + 'Content-type': 'text/plain', + 'If': this._token? + "<" + this._baseURL + "> (<" + this._token + ">)" : ''}; + }, + + GET: function DC_GET(path, onComplete) { + return this._makeRequest.async(this, onComplete, "GET", path, + this._defaultHeaders); + }, + + PUT: function DC_PUT(path, data, onComplete) { + return this._makeRequest.async(this, onComplete, "PUT", path, + this._defaultHeaders, data); + }, + + DELETE: function DC_DELETE(path, onComplete) { + return this._makeRequest.async(this, onComplete, "DELETE", path, + this._defaultHeaders); + }, + + PROPFIND: function DC_PROPFIND(path, data, onComplete) { + let headers = {'Content-type': 'text/xml; charset="utf-8"', + 'Depth': '0'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "PROPFIND", path, + headers, data); + }, + + LOCK: function DC_LOCK(path, data, onComplete) { + let headers = {'Content-type': 'text/xml; charset="utf-8"', + 'Depth': 'infinity', + 'Timeout': 'Second-600'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "LOCK", path, headers, data); + }, + + UNLOCK: function DC_UNLOCK(path, onComplete) { + let headers = {'Lock-Token': '<' + this._token + '>'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers); + }, + + // Login / Logout + + login: function DC_login(onComplete, username, password) { + let [self, cont] = yield; + + try { + if (this._loggedIn) { + this._log.debug("Login requested, but already logged in"); + return; + } + + this._log.info("Logging in"); + + let URI = makeURI(this._baseURL); + this._auth = "Basic " + btoa(username + ":" + password); + + // Make a call to make sure it's working + this.GET("", cont); + let resp = yield; + + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) + return; + + this._loggedIn = true; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (this._loggedIn) + this._log.info("Logged in"); + else + this._log.warn("Could not log in"); + generatorDone(this, self, onComplete, this._loggedIn); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + logout: function DC_logout() { + this._log.debug("Logging out (forgetting auth header)"); + this._loggedIn = false; + this.__auth = null; + }, + + // Locking + + _getActiveLock: function DC__getActiveLock(onComplete) { + let [self, cont] = yield; + let ret = null; + + try { + this._log.info("Getting active lock token"); + this.PROPFIND("", + "" + + "" + + " " + + "", cont); + let resp = yield; + + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) + return; + + let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + ret = token.textContent; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (ret) + this._log.debug("Found an active lock token"); + else + this._log.debug("No active lock token found"); + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + lock: function DC_lock(onComplete) { + let [self, cont] = yield; + this._token = null; + + try { + this._log.info("Acquiring lock"); + + if (this._token) { + this._log.debug("Lock called, but we already hold a token"); + return; + } + + this.LOCK("", + "\n" + + "\n" + + " \n" + + " \n" + + "", cont); + let resp = yield; + + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) + return; + + let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + if (token) + this._token = token.textContent; + + } catch (e){ + this._log.error("Exception caught: " + e.message); + + } finally { + if (this._token) + this._log.info("Lock acquired"); + else + this._log.warn("Could not acquire lock"); + generatorDone(this, self, onComplete, this._token); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + unlock: function DC_unlock(onComplete) { + let [self, cont] = yield; + try { + this._log.info("Releasing lock"); + + if (this._token === null) { + this._log.debug("Unlock called, but we don't hold a token right now"); + return; + } + + this.UNLOCK("", cont); + let resp = yield; + + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) + return; + + this._token = null; + + } catch (e){ + this._log.error("Exception caught: " + e.message); + + } finally { + if (this._token) { + this._log.info("Could not release lock"); + generatorDone(this, self, onComplete, false); + } else { + this._log.info("Lock released (or we didn't have one)"); + generatorDone(this, self, onComplete, true); + } + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + forceUnlock: function DC_forceUnlock(onComplete) { + let [self, cont] = yield; + let unlocked = true; + + try { + this._log.info("Forcibly releasing any server locks"); + + this._getActiveLock.async(this, cont); + this._token = yield; + + if (!this._token) { + this._log.info("No server lock found"); + return; + } + + this._log.info("Server lock found, unlocking"); + this.unlock.async(this, cont); + unlocked = yield; + + } catch (e){ + this._log.error("Exception caught: " + e.message); + + } finally { + if (unlocked) + this._log.debug("Lock released"); + else + this._log.debug("No lock released"); + generatorDone(this, self, onComplete, unlocked); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + stealLock: function DC_stealLock(onComplete) { + let [self, cont] = yield; + let stolen = null; + + try { + this.forceUnlock.async(this, cont); + let unlocked = yield; + + if (unlocked) { + this.lock.async(this, cont); + stolen = yield; + } + + } catch (e){ + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, self, onComplete, stolen); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + } +}; + + +/* + * Auth provider object + * Taken from nsMicrosummaryService.js and massaged slightly + */ + +function DummyAuthProvider() {} +DummyAuthProvider.prototype = { + // Implement notification callback interfaces so we can suppress UI + // and abort loads for bad SSL certs and HTTP authorization requests. + + // Interfaces this component implements. + interfaces: [Ci.nsIBadCertListener, + Ci.nsIAuthPromptProvider, + Ci.nsIAuthPrompt, + Ci.nsIPrompt, + Ci.nsIProgressEventSink, + Ci.nsIInterfaceRequestor, + Ci.nsISupports], + + // Auth requests appear to succeed when we cancel them (since the server + // redirects us to a "you're not authorized" page), so we have to set a flag + // to let the load handler know to treat the load as a failure. + get _authFailed() { return this.__authFailed; }, + set _authFailed(newValue) { return this.__authFailed = newValue }, + + // nsISupports + + QueryInterface: function DAP_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + + // nsIAuthPrompt and nsIPrompt need separate implementations because + // their method signatures conflict. The other interfaces we implement + // within DummyAuthProvider itself. + switch(iid) { + case Ci.nsIAuthPrompt: + return this.authPrompt; + case Ci.nsIPrompt: + return this.prompt; + default: + return this; + } + }, + + // nsIInterfaceRequestor + + getInterface: function DAP_getInterface(iid) { + return this.QueryInterface(iid); + }, + + // nsIBadCertListener + + // Suppress UI and abort secure loads from servers with bad SSL certificates. + + confirmUnknownIssuer: function DAP_confirmUnknownIssuer(socketInfo, cert, certAddType) { + return false; + }, + + confirmMismatchDomain: function DAP_confirmMismatchDomain(socketInfo, targetURL, cert) { + return false; + }, + + confirmCertExpired: function DAP_confirmCertExpired(socketInfo, cert) { + return false; + }, + + notifyCrlNextupdate: function DAP_notifyCrlNextupdate(socketInfo, targetURL, cert) { + }, + + // nsIAuthPromptProvider + + getAuthPrompt: function(aPromptReason, aIID) { + this._authFailed = true; + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + // HTTP always requests nsIAuthPromptProvider first, so it never needs + // nsIAuthPrompt, but not all channels use nsIAuthPromptProvider, so we + // implement nsIAuthPrompt too. + + // nsIAuthPrompt + + get authPrompt() { + var resource = this; + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) { + resource._authFailed = true; + return false; + }, + promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) { + resource._authFailed = true; + return false; + }, + promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) { + resource._authFailed = true; + return false; + } + }; + }, + + // nsIPrompt + + get prompt() { + var resource = this; + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + alert: function(dialogTitle, text) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + alertCheck: function(dialogTitle, text, checkMessage, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirm: function(dialogTitle, text) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirmCheck: function(dialogTitle, text, checkMessage, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirmEx: function(dialogTitle, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + prompt: function(dialogTitle, text, value, checkMsg, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + promptPassword: function(dialogTitle, text, password, checkMsg, checkValue) { + resource._authFailed = true; + return false; + }, + promptUsernameAndPassword: function(dialogTitle, text, username, password, checkMsg, checkValue) { + resource._authFailed = true; + return false; + }, + select: function(dialogTitle, text, count, selectList, outSelection) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + }; + }, + + // nsIProgressEventSink + + onProgress: function DAP_onProgress(aRequest, aContext, + aProgress, aProgressMax) { + }, + + onStatus: function DAP_onStatus(aRequest, aContext, + aStatus, aStatusArg) { + } +}; + +/* + * Event listener object + * Used to handle XMLHttpRequest and nsITimer callbacks + */ + +function EventListener(handler, eventName) { + this._handler = handler; + this._eventName = eventName; + let logSvc = Cc["@mozilla.org/log4moz/service;1"]. + getService(Ci.ILog4MozService); + this._log = logSvc.getLogger("Service.EventHandler"); +} +EventListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), + + // DOM event listener + handleEvent: function EL_handleEvent(event) { + this._log.debug("Handling event " + this._eventName); + this._handler(event); + }, + + // nsITimerCallback + notify: function EL_notify(timer) { + this._log.trace("Timer fired"); + this._handler(timer); + } +}; + +/* + * Utility functions + */ + +function deepEquals(a, b) { + if (!a && !b) + return true; + if (!a || !b) + return false; + + if (typeof(a) != "object" && typeof(b) != "object") + return a == b; + if (typeof(a) != "object" || typeof(b) != "object") + return false; + + for (let key in a) { + if (typeof(a[key]) == "object") { + if (!typeof(b[key]) == "object") + return false; + if (!deepEquals(a[key], b[key])) + return false; + } else { + if (a[key] != b[key]) + return false; + } + } + return true; +} + +function makeFile(path) { + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return file; +} + +function makeURI(URIString) { + if (URIString === null || URIString == "") + return null; + let ioservice = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return ioservice.newURI(URIString, null, null); +} + +function xpath(xmlDoc, xpathString) { + let root = xmlDoc.ownerDocument == null ? + xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement + let nsResolver = xmlDoc.createNSResolver(root); + + return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, + Ci.nsIDOMXPathResult.ANY_TYPE, null); +} + +function bind2(object, method) { + return function innerBind() { return method.apply(object, arguments); } +} + +Function.prototype.async = function(self, extra_args) { + try { + let args = Array.prototype.slice.call(arguments, 1); + let gen = this.apply(self, args); + gen.next(); // must initialize before sending + gen.send([gen, function(data) {continueGenerator(gen, data);}]); + return gen; + } catch (e) { + if (e instanceof StopIteration) { + dump("async warning: generator stopped unexpectedly"); + return null; + } else { + dump("Exception caught: " + e.message); + } + } +} + +function continueGenerator(generator, data) { + try { generator.send(data); } + catch (e) { + if (e instanceof StopIteration) + dump("continueGenerator warning: generator stopped unexpectedly"); + else + dump("Exception caught: " + e.message); + } +} + +// generators created using Function.async can't simply call the +// callback with the return value, since that would cause the calling +// function to end up running (after the yield) from inside the +// generator. Instead, generators can call this method which sets up +// a timer to call the callback from a timer (and cleans up the timer +// to avoid leaks). It also closes generators after the timeout, to +// keep things clean. +function generatorDone(object, generator, callback, retval) { + if (object._timer) + throw "Called generatorDone when there is a timer already set." + + let cb = bind2(object, function(event) { + generator.close(); + generator = null; + object._timer = null; + if (callback) + callback(retval); + }); + + object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + object._timer.initWithCallback(new EventListener(cb), + 0, object._timer.TYPE_ONE_SHOT); +} From b16ce205a25501735872023020e4aa932f6cf01a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 10 Dec 2007 19:47:11 -0800 Subject: [PATCH 0093/1860] refactoring: remove log4moz component, use it as a js module instead --- services/sync/ILog4MozService.xpt | Bin 1120 -> 0 bytes .../{Log4MozService.js => modules/log4moz.js} | 344 +++++++++--------- services/sync/modules/weave.js | 69 ++-- 3 files changed, 211 insertions(+), 202 deletions(-) delete mode 100644 services/sync/ILog4MozService.xpt rename services/sync/{Log4MozService.js => modules/log4moz.js} (91%) diff --git a/services/sync/ILog4MozService.xpt b/services/sync/ILog4MozService.xpt deleted file mode 100644 index 0b632e5397b2d4c6ff5705bab8af350ad0f721a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1120 zcmZ9LO=uHA6vy98(hp->#g9uxD;A1i@lZridgwOIhLvqnwzXD;nr$-)3u$)QY+K{S zxu_^8B{!uWL|PPE^k_xYyC?NfPby+9ND&VzLNC6V{piOYc7F4J^Jd=u=k3TqGTWaR zsya-!#+nnYghC@gJO=EYCbYvuClX`u)L8>oOkX_%XtttdzQ{ztz!&!*@=7=zg&ifChQAZQ*LqXKl6D zxxO|#Gmu_w!Pt2}kLKU5pMIr3)^5$$M%r%AwYOu82eI1EXNxbNK1_DpGvZC(u8rQu z*xevDSFUKPRbqhS;H2Z&u2%s~w~IyQ;xb5xP2Q6nh58<(kAx#3z$e0MQ0Max!k2vd z717^_)_fO9Ac0U20KCMe zSPA^Mna?YZ!^(O5n1bn=yD)9KWw6Qx8^-N&!74(YjZGH8bIoxEdTKbOUs9DJ zMGrV-7+E9WbBd7(P|K*<0F&8_s-=TTU#j(bg2{q~hX=1y677+PG% zvG`?@t3gbfNWN0Ov@u(Y@g=mn9u?ni@-HqGaRD1DZS=G1#U9+XZ7;aQA`7X^rXx3y z#kFsl+wO$vd1#du$yiCN zhQn4fm}6Hg&vvWA5q!Al!G2b$m_=!l=)>4 Date: Mon, 10 Dec 2007 21:38:53 -0800 Subject: [PATCH 0094/1860] more and more refactoring: split weave module into multiple files (finally!) --- services/sync/modules/constants.js | 63 + services/sync/modules/crypto.js | 196 ++ services/sync/modules/dav.js | 559 ++++++ services/sync/modules/engines.js | 697 +++++++ services/sync/modules/identity.js | 133 ++ services/sync/modules/service.js | 448 +++++ services/sync/modules/stores.js | 556 ++++++ services/sync/modules/syncCores.js | 413 +++++ services/sync/modules/util.js | 210 +++ services/sync/modules/weave.js | 2712 +--------------------------- 10 files changed, 3294 insertions(+), 2693 deletions(-) create mode 100644 services/sync/modules/constants.js create mode 100644 services/sync/modules/crypto.js create mode 100644 services/sync/modules/dav.js create mode 100644 services/sync/modules/engines.js create mode 100644 services/sync/modules/identity.js create mode 100644 services/sync/modules/service.js create mode 100644 services/sync/modules/stores.js create mode 100644 services/sync/modules/syncCores.js create mode 100644 services/sync/modules/util.js diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js new file mode 100644 index 000000000000..435c130f6cb5 --- /dev/null +++ b/services/sync/modules/constants.js @@ -0,0 +1,63 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Cc', 'Ci', 'Cr', 'Cu', + 'MODE_RDONLY', 'MODE_WRONLY', + 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', + 'PERMS_FILE', 'PERMS_DIRECTORY', + 'STORAGE_FORMAT_VERSION', + 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +const MODE_RDONLY = 0x01; +const MODE_WRONLY = 0x02; +const MODE_CREATE = 0x08; +const MODE_APPEND = 0x10; +const MODE_TRUNCATE = 0x20; + +const PERMS_FILE = 0644; +const PERMS_DIRECTORY = 0755; + +const STORAGE_FORMAT_VERSION = 2; + +const ONE_BYTE = 1; +const ONE_KILOBYTE = 1024 * ONE_BYTE; +const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; + diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js new file mode 100644 index 000000000000..06f416bc53d6 --- /dev/null +++ b/services/sync/modules/crypto.js @@ -0,0 +1,196 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['WeaveCrypto']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function WeaveCrypto() { + this._init(); +} +WeaveCrypto.prototype = { + _logName: "Crypto", + + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + + __xxxtea: {}, + __xxxteaLoaded: false, + get _xxxtea() { + if (!this.__xxxteaLoaded) { + let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + jsLoader.loadSubScript("chrome://weave/content/encrypt.js", this.__xxxtea); + this.__xxxteaLoaded = true; + } + return this.__xxxtea; + }, + + get defaultAlgorithm() { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + return branch.getCharPref("browser.places.sync.encryption"); + }, + set defaultAlgorithm(value) { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + let cur = branch.getCharPref("browser.places.sync.encryption"); + if (value != cur) + branch.setCharPref("browser.places.sync.encryption", value); + }, + + _init: function Crypto__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch2); + branch.addObserver("browser.places.sync.encryption", this, false); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), + + // nsIObserver + + observe: function Sync_observe(subject, topic, data) { + switch (topic) { + case "browser.places.sync.encryption": + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + + let cur = branch.getCharPref("browser.places.sync.encryption"); + if (cur == data) + return; + + switch (data) { + case "none": + this._log.info("Encryption disabled"); + break; + case "XXXTEA": + this._log.info("Using encryption algorithm: " + data); + break; + default: + this._log.warn("Unknown encryption algorithm, resetting"); + branch.setCharPref("browser.places.sync.encryption", "XXXTEA"); + return; // otherwise we'll send the alg changed event twice + } + // FIXME: listen to this bad boy somewhere + this._os.notifyObservers(null, "weave:encryption:algorithm-changed", ""); + break; + default: + this._log.warn("Unknown encryption preference changed - ignoring"); + } + }, + + // Crypto + + PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) { + let out; + if (!algorithm) + algorithm = this.defaultAlgorithm; + switch (algorithm) { + case "none": + out = data; + break; + case "XXXTEA": + try { + this._log.debug("Encrypting data"); + out = this._xxxtea.encrypt(data, identity.password); + this._log.debug("Done encrypting data"); + } catch (e) { + this._log.error("Data encryption failed: " + e); + throw 'encrypt failed'; + } + break; + default: + this._log.error("Unknown encryption algorithm: " + algorithm); + throw 'encrypt failed'; + } + return out; + }, + + PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) { + let out; + switch (algorithm) { + case "none": + out = eval(data); + break; + case "XXXTEA": + try { + this._log.debug("Decrypting data"); + out = eval(this._xxxtea.decrypt(data, identity.password)); + this._log.debug("Done decrypting data"); + } catch (e) { + this._log.error("Data decryption failed: " + e); + throw 'decrypt failed'; + } + break; + default: + this._log.error("Unknown encryption algorithm: " + algorithm); + throw 'decrypt failed'; + } + return out; + } +}; + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); + } +} diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js new file mode 100644 index 000000000000..8f446ca51f50 --- /dev/null +++ b/services/sync/modules/dav.js @@ -0,0 +1,559 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['DAVCollection']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +Function.prototype.async = generatorAsync; + +/* + * DAV object + * Abstracts the raw DAV commands + */ + +function DAVCollection(baseURL) { + this._baseURL = baseURL; + this._authProvider = new DummyAuthProvider(); + this._log = Log4Moz.Service.getLogger("Service.DAV"); +} +DAVCollection.prototype = { + __dp: null, + get _dp() { + if (!this.__dp) + this.__dp = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + return this.__dp; + }, + + _auth: null, + + get baseURL() { + return this._baseURL; + }, + set baseURL(value) { + this._baseURL = value; + }, + + _loggedIn: false, + get loggedIn() { + return this._loggedIn; + }, + + _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { + let [self, cont] = yield; + let ret; + + try { + this._log.debug("Creating " + op + " request for " + this._baseURL + path); + + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); + request = request.QueryInterface(Ci.nsIDOMEventTarget); + + request.addEventListener("load", new EventListener(cont, "load"), false); + request.addEventListener("error", new EventListener(cont, "error"), false); + request = request.QueryInterface(Ci.nsIXMLHttpRequest); + request.open(op, this._baseURL + path, true); + + + // Force cache validation + let channel = request.channel; + channel = channel.QueryInterface(Ci.nsIRequest); + let loadFlags = channel.loadFlags; + loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; + channel.loadFlags = loadFlags; + + let key; + for (key in headers) { + this._log.debug("HTTP Header " + key + ": " + headers[key]); + request.setRequestHeader(key, headers[key]); + } + + this._authProvider._authFailed = false; + request.channel.notificationCallbacks = this._authProvider; + + request.send(data); + let event = yield; + ret = event.target; + + if (this._authProvider._authFailed) + this._log.warn("_makeRequest: authentication failed"); + if (ret.status < 200 || ret.status >= 300) + this._log.warn("_makeRequest: got status " + ret.status); + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + get _defaultHeaders() { + return {'Authorization': this._auth? this._auth : '', + 'Content-type': 'text/plain', + 'If': this._token? + "<" + this._baseURL + "> (<" + this._token + ">)" : ''}; + }, + + GET: function DC_GET(path, onComplete) { + return this._makeRequest.async(this, onComplete, "GET", path, + this._defaultHeaders); + }, + + PUT: function DC_PUT(path, data, onComplete) { + return this._makeRequest.async(this, onComplete, "PUT", path, + this._defaultHeaders, data); + }, + + DELETE: function DC_DELETE(path, onComplete) { + return this._makeRequest.async(this, onComplete, "DELETE", path, + this._defaultHeaders); + }, + + PROPFIND: function DC_PROPFIND(path, data, onComplete) { + let headers = {'Content-type': 'text/xml; charset="utf-8"', + 'Depth': '0'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "PROPFIND", path, + headers, data); + }, + + LOCK: function DC_LOCK(path, data, onComplete) { + let headers = {'Content-type': 'text/xml; charset="utf-8"', + 'Depth': 'infinity', + 'Timeout': 'Second-600'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "LOCK", path, headers, data); + }, + + UNLOCK: function DC_UNLOCK(path, onComplete) { + let headers = {'Lock-Token': '<' + this._token + '>'}; + headers.__proto__ = this._defaultHeaders; + return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers); + }, + + // Login / Logout + + login: function DC_login(onComplete, username, password) { + let [self, cont] = yield; + + try { + if (this._loggedIn) { + this._log.debug("Login requested, but already logged in"); + return; + } + + this._log.info("Logging in"); + + let URI = makeURI(this._baseURL); + this._auth = "Basic " + btoa(username + ":" + password); + + // Make a call to make sure it's working + this.GET("", cont); + let resp = yield; + + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) + return; + + this._loggedIn = true; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (this._loggedIn) + this._log.info("Logged in"); + else + this._log.warn("Could not log in"); + generatorDone(this, self, onComplete, this._loggedIn); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + logout: function DC_logout() { + this._log.debug("Logging out (forgetting auth header)"); + this._loggedIn = false; + this.__auth = null; + }, + + // Locking + + _getActiveLock: function DC__getActiveLock(onComplete) { + let [self, cont] = yield; + let ret = null; + + try { + this._log.info("Getting active lock token"); + this.PROPFIND("", + "" + + "" + + " " + + "", cont); + let resp = yield; + + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) + return; + + let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + ret = token.textContent; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (ret) + this._log.debug("Found an active lock token"); + else + this._log.debug("No active lock token found"); + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + lock: function DC_lock(onComplete) { + let [self, cont] = yield; + this._token = null; + + try { + this._log.info("Acquiring lock"); + + if (this._token) { + this._log.debug("Lock called, but we already hold a token"); + return; + } + + this.LOCK("", + "\n" + + "\n" + + " \n" + + " \n" + + "", cont); + let resp = yield; + + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) + return; + + let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + if (token) + this._token = token.textContent; + + } catch (e){ + this._log.error("Exception caught: " + e.message); + + } finally { + if (this._token) + this._log.info("Lock acquired"); + else + this._log.warn("Could not acquire lock"); + generatorDone(this, self, onComplete, this._token); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + unlock: function DC_unlock(onComplete) { + let [self, cont] = yield; + try { + this._log.info("Releasing lock"); + + if (this._token === null) { + this._log.debug("Unlock called, but we don't hold a token right now"); + return; + } + + this.UNLOCK("", cont); + let resp = yield; + + if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) + return; + + this._token = null; + + } catch (e){ + this._log.error("Exception caught: " + e.message); + + } finally { + if (this._token) { + this._log.info("Could not release lock"); + generatorDone(this, self, onComplete, false); + } else { + this._log.info("Lock released (or we didn't have one)"); + generatorDone(this, self, onComplete, true); + } + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + forceUnlock: function DC_forceUnlock(onComplete) { + let [self, cont] = yield; + let unlocked = true; + + try { + this._log.info("Forcibly releasing any server locks"); + + this._getActiveLock.async(this, cont); + this._token = yield; + + if (!this._token) { + this._log.info("No server lock found"); + return; + } + + this._log.info("Server lock found, unlocking"); + this.unlock.async(this, cont); + unlocked = yield; + + } catch (e){ + this._log.error("Exception caught: " + e.message); + + } finally { + if (unlocked) + this._log.debug("Lock released"); + else + this._log.debug("No lock released"); + generatorDone(this, self, onComplete, unlocked); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + stealLock: function DC_stealLock(onComplete) { + let [self, cont] = yield; + let stolen = null; + + try { + this.forceUnlock.async(this, cont); + let unlocked = yield; + + if (unlocked) { + this.lock.async(this, cont); + stolen = yield; + } + + } catch (e){ + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, self, onComplete, stolen); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + } +}; + + +/* + * Auth provider object + * Taken from nsMicrosummaryService.js and massaged slightly + */ + +function DummyAuthProvider() {} +DummyAuthProvider.prototype = { + // Implement notification callback interfaces so we can suppress UI + // and abort loads for bad SSL certs and HTTP authorization requests. + + // Interfaces this component implements. + interfaces: [Ci.nsIBadCertListener, + Ci.nsIAuthPromptProvider, + Ci.nsIAuthPrompt, + Ci.nsIPrompt, + Ci.nsIProgressEventSink, + Ci.nsIInterfaceRequestor, + Ci.nsISupports], + + // Auth requests appear to succeed when we cancel them (since the server + // redirects us to a "you're not authorized" page), so we have to set a flag + // to let the load handler know to treat the load as a failure. + get _authFailed() { return this.__authFailed; }, + set _authFailed(newValue) { return this.__authFailed = newValue }, + + // nsISupports + + QueryInterface: function DAP_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + + // nsIAuthPrompt and nsIPrompt need separate implementations because + // their method signatures conflict. The other interfaces we implement + // within DummyAuthProvider itself. + switch(iid) { + case Ci.nsIAuthPrompt: + return this.authPrompt; + case Ci.nsIPrompt: + return this.prompt; + default: + return this; + } + }, + + // nsIInterfaceRequestor + + getInterface: function DAP_getInterface(iid) { + return this.QueryInterface(iid); + }, + + // nsIBadCertListener + + // Suppress UI and abort secure loads from servers with bad SSL certificates. + + confirmUnknownIssuer: function DAP_confirmUnknownIssuer(socketInfo, cert, certAddType) { + return false; + }, + + confirmMismatchDomain: function DAP_confirmMismatchDomain(socketInfo, targetURL, cert) { + return false; + }, + + confirmCertExpired: function DAP_confirmCertExpired(socketInfo, cert) { + return false; + }, + + notifyCrlNextupdate: function DAP_notifyCrlNextupdate(socketInfo, targetURL, cert) { + }, + + // nsIAuthPromptProvider + + getAuthPrompt: function(aPromptReason, aIID) { + this._authFailed = true; + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + // HTTP always requests nsIAuthPromptProvider first, so it never needs + // nsIAuthPrompt, but not all channels use nsIAuthPromptProvider, so we + // implement nsIAuthPrompt too. + + // nsIAuthPrompt + + get authPrompt() { + var resource = this; + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) { + resource._authFailed = true; + return false; + }, + promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) { + resource._authFailed = true; + return false; + }, + promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) { + resource._authFailed = true; + return false; + } + }; + }, + + // nsIPrompt + + get prompt() { + var resource = this; + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + alert: function(dialogTitle, text) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + alertCheck: function(dialogTitle, text, checkMessage, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirm: function(dialogTitle, text) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirmCheck: function(dialogTitle, text, checkMessage, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + confirmEx: function(dialogTitle, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + prompt: function(dialogTitle, text, value, checkMsg, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + promptPassword: function(dialogTitle, text, password, checkMsg, checkValue) { + resource._authFailed = true; + return false; + }, + promptUsernameAndPassword: function(dialogTitle, text, username, password, checkMsg, checkValue) { + resource._authFailed = true; + return false; + }, + select: function(dialogTitle, text, count, selectList, outSelection) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + }; + }, + + // nsIProgressEventSink + + onProgress: function DAP_onProgress(aRequest, aContext, + aProgress, aProgressMax) { + }, + + onStatus: function DAP_onStatus(aRequest, aContext, + aStatus, aStatusArg) { + } +}; + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); + } +} diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js new file mode 100644 index 000000000000..5a0f63c25046 --- /dev/null +++ b/services/sync/modules/engines.js @@ -0,0 +1,697 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['BookmarksEngine']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/syncCores.js"); + +Function.prototype.async = generatorAsync; +let Crypto = new WeaveCrypto(); + +function BookmarksEngine(davCollection, cryptoId) { + this._init(davCollection, cryptoId); +} +BookmarksEngine.prototype = { + _logName: "BmkEngine", + + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new BookmarksStore(); + return this.__store; + }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new BookmarksSyncCore(); + return this.__core; + }, + + __snapshot: null, + get _snapshot() { + if (!this.__snapshot) + this.__snapshot = new SnapshotStore(); + return this.__snapshot; + }, + set _snapshot(value) { + this.__snapshot = value; + }, + + _init: function BmkEngine__init(davCollection, cryptoId) { + this._dav = davCollection; + this._cryptoId = cryptoId; + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._snapshot.load(); + }, + + _checkStatus: function BmkEngine__checkStatus(code, msg) { + if (code >= 200 && code < 300) + return; + this._log.error(msg + " Error code: " + code); + throw 'checkStatus failed'; + }, + + /* Get the deltas/combined updates from the server + * Returns: + * status: + * -1: error + * 0: ok + * These fields may be null when status is -1: + * formatVersion: + * version of the data format itself. For compatibility checks. + * maxVersion: + * the latest version on the server + * snapVersion: + * the version of the current snapshot on the server (deltas not applied) + * snapEncryption: + * encryption algorithm currently used on the server-stored snapshot + * deltasEncryption: + * encryption algorithm currently used on the server-stored deltas + * snapshot: + * full snapshot of the latest server version (deltas applied) + * deltas: + * all of the individual deltas on the server + * updates: + * the relevant deltas (from our snapshot version to current), + * combined into a single set. + */ + _getServerData: function BmkEngine__getServerData(onComplete) { + let [self, cont] = yield; + let ret = {status: -1, + formatVersion: null, maxVersion: null, snapVersion: null, + snapEncryption: null, deltasEncryption: null, + snapshot: null, deltas: null, updates: null}; + + try { + this._log.info("Getting bookmarks status from server"); + this._dav.GET("bookmarks-status.json", cont); + let resp = yield; + let status = resp.status; + + switch (status) { + case 200: + this._log.info("Got bookmarks status from server"); + + let status = eval(resp.responseText); + let deltas, allDeltas; + let snap = new SnapshotStore(); + + // Bail out if the server has a newer format version than we can parse + if (status.formatVersion > STORAGE_FORMAT_VERSION) { + this._log.error("Server uses storage format v" + status.formatVersion + + ", this client understands up to v" + STORAGE_FORMAT_VERSION); + generatorDone(this, self, onComplete, ret) + return; + } + + if (status.formatVersion == 0) { + ret.snapEncryption = status.snapEncryption = "none"; + ret.deltasEncryption = status.deltasEncryption = "none"; + } + + if (status.GUID != this._snapshot.GUID) { + this._log.info("Remote/local sync GUIDs do not match. " + + "Forcing initial sync."); + this._store.resetGUIDs(); + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.GUID = status.GUID; + } + + if (this._snapshot.version < status.snapVersion) { + if (this._snapshot.version >= 0) + this._log.info("Local snapshot is out of date"); + + this._log.info("Downloading server snapshot"); + this._dav.GET("bookmarks-snapshot.json", cont); + resp = yield; + this._checkStatus(resp.status, "Could not download snapshot."); + snap.data = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.snapEncryption); + + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = eval(uneval(allDeltas)); + + } else if (this._snapshot.version >= status.snapVersion && + this._snapshot.version < status.maxVersion) { + snap.data = eval(uneval(this._snapshot.data)); + + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); + + } else if (this._snapshot.version == status.maxVersion) { + snap.data = eval(uneval(this._snapshot.data)); + + // FIXME: could optimize this case by caching deltas file + this._log.info("Downloading server deltas"); + this._dav.GET("bookmarks-deltas.json", cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = []; + + } else { // this._snapshot.version > status.maxVersion + this._log.error("Server snapshot is older than local snapshot"); + return; + } + + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands(deltas[i]); + } + + ret.status = 0; + ret.formatVersion = status.formatVersion; + ret.maxVersion = status.maxVersion; + ret.snapVersion = status.snapVersion; + ret.snapEncryption = status.snapEncryption; + ret.deltasEncryption = status.deltasEncryption; + ret.snapshot = snap.data; + ret.deltas = allDeltas; + this._core.detectUpdates(cont, this._snapshot.data, snap.data); + ret.updates = yield; + break; + + case 404: + this._log.info("Server has no status file, Initial upload to server"); + + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + + this._fullUpload.async(this, cont); + let uploadStatus = yield; + if (!uploadStatus) + return; + + this._log.info("Initial upload to server successful"); + this.snapshot.save(); + + ret.status = 0; + ret.formatVersion = STORAGE_FORMAT_VERSION; + ret.maxVersion = this._snapshot.version; + ret.snapVersion = this._snapshot.version; + ret.snapEncryption = Crypto.defaultAlgorithm; + ret.deltasEncryption = Crypto.defaultAlgorithm; + ret.snapshot = eval(uneval(this._snapshot.data)); + ret.deltas = []; + ret.updates = []; + break; + + default: + this._log.error("Could not get bookmarks.status: unknown HTTP status code " + + status); + break; + } + + } catch (e) { + if (e != 'checkStatus failed' && + e != 'decrypt failed') + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, self, onComplete, ret) + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _fullUpload: function BmkEngine__fullUpload(onComplete) { + let [self, cont] = yield; + let ret = false; + + try { + let data = Crypto.PBEencrypt(this._snapshot.serialize(), + this._cryptoId); + this._dav.PUT("bookmarks-snapshot.json", data, cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload snapshot."); + + this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload deltas."); + + let c = 0; + for (GUID in this._snapshot.data) + c++; + + this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshot.version, + maxVersion: this._snapshot.version, + snapEncryption: Crypto.defaultAlgorithm, + deltasEncryption: "none", + bookmarksCount: c}), cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload status file."); + + this._log.info("Full upload to server successful"); + ret = true; + + } catch (e) { + if (e != 'checkStatus failed') + this._log.error("Exception caught: " + e.message); + + } finally { + generatorDone(this, self, onComplete, ret) + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + // original + // / \ + // A / \ B + // / \ + // client --C-> server + // \ / + // D \ / C + // \ / + // final + + // If we have a saved snapshot, original == snapshot. Otherwise, + // it's the empty set {}. + + // C is really the diff between server -> final, so if we determine + // D we can calculate C from that. In the case where A and B have + // no conflicts, C == A and D == B. + + // Sync flow: + // 1) Fetch server deltas + // 1.1) Construct current server status from snapshot + server deltas + // 1.2) Generate single delta from snapshot -> current server status ("B") + // 2) Generate local deltas from snapshot -> current client status ("A") + // 3) Reconcile client/server deltas and generate new deltas for them. + // Reconciliation won't generate C directly, we will simply diff + // server->final after step 3.1. + // 3.1) Apply local delta with server changes ("D") + // 3.2) Append server delta to the delta file and upload ("C") + + _sync: function BmkEngine__sync(onComplete) { + let [self, cont] = yield; + let synced = false, locked = null; + + try { + this._log.info("Beginning sync"); + this._os.notifyObservers(null, "bookmarks-sync:sync-start", ""); + + this._dav.lock.async(this._dav, cont); + locked = yield; + + if (locked) + this._log.info("Lock acquired"); + else { + this._log.warn("Could not acquire lock, aborting sync"); + return; + } + + // 1) Fetch server deltas + this._getServerData.async(this, cont); + let server = yield; + + this._log.info("Local snapshot version: " + this._snapshot.version); + this._log.info("Server status: " + server.status); + this._log.info("Server maxVersion: " + server.maxVersion); + this._log.info("Server snapVersion: " + server.snapVersion); + + if (server.status != 0) { + this._log.fatal("Sync error: could not get server status, " + + "or initial upload failed. Aborting sync."); + return; + } + + // 2) Generate local deltas from snapshot -> current client status + + let localJson = new SnapshotStore(); + localJson.data = this._store.wrap(); + this._core.detectUpdates(cont, this._snapshot.data, localJson.data); + let localUpdates = yield; + + this._log.debug("local json:\n" + localJson.serialize()); + this._log.debug("Local updates: " + serializeCommands(localUpdates)); + this._log.debug("Server updates: " + serializeCommands(server.updates)); + + if (server.updates.length == 0 && localUpdates.length == 0) { + this._snapshot.version = server.maxVersion; + this._log.info("Sync complete (1): no changes needed on client or server"); + synced = true; + return; + } + + // 3) Reconcile client/server deltas and generate new deltas for them. + + this._log.info("Reconciling client/server updates"); + this._core.reconcile(cont, localUpdates, server.updates); + let ret = yield; + + let clientChanges = ret.propagations[0]; + let serverChanges = ret.propagations[1]; + let clientConflicts = ret.conflicts[0]; + let serverConflicts = ret.conflicts[1]; + + this._log.info("Changes for client: " + clientChanges.length); + this._log.info("Predicted changes for server: " + serverChanges.length); + this._log.info("Client conflicts: " + clientConflicts.length); + this._log.info("Server conflicts: " + serverConflicts.length); + this._log.debug("Changes for client: " + serializeCommands(clientChanges)); + this._log.debug("Predicted changes for server: " + serializeCommands(serverChanges)); + this._log.debug("Client conflicts: " + serializeConflicts(clientConflicts)); + this._log.debug("Server conflicts: " + serializeConflicts(serverConflicts)); + + if (!(clientChanges.length || serverChanges.length || + clientConflicts.length || serverConflicts.length)) { + this._log.info("Sync complete (2): no changes needed on client or server"); + this._snapshot.data = localJson.data; + this._snapshot.version = server.maxVersion; + this._snapshot.save(); + synced = true; + return; + } + + if (clientConflicts.length || serverConflicts.length) { + this._log.warn("Conflicts found! Discarding server changes"); + } + + let savedSnap = eval(uneval(this._snapshot.data)); + let savedVersion = this._snapshot.version; + let newSnapshot; + + // 3.1) Apply server changes to local store + if (clientChanges.length) { + this._log.info("Applying changes locally"); + // Note that we need to need to apply client changes to the + // current tree, not the saved snapshot + + localJson.applyCommands(clientChanges); + this._snapshot.data = localJson.data; + this._snapshot.version = server.maxVersion; + this._store.applyCommands(clientChanges); + newSnapshot = this._store.wrap(); + + this._core.detectUpdates(cont, this._snapshot.data, newSnapshot); + let diff = yield; + if (diff.length != 0) { + this._log.warn("Commands did not apply correctly"); + this._log.debug("Diff from snapshot+commands -> " + + "new snapshot after commands:\n" + + serializeCommands(diff)); + // FIXME: do we really want to revert the snapshot here? + this._snapshot.data = eval(uneval(savedSnap)); + this._snapshot.version = savedVersion; + } + + this._snapshot.save(); + } + + // 3.2) Append server delta to the delta file and upload + + // Generate a new diff, from the current server snapshot to the + // current client snapshot. In the case where there are no + // conflicts, it should be the same as what the resolver returned + + newSnapshot = this._store.wrap(); + this._core.detectUpdates(cont, server.snapshot, newSnapshot); + let serverDelta = yield; + + // Log an error if not the same + if (!(serverConflicts.length || + deepEquals(serverChanges, serverDelta))) + this._log.warn("Predicted server changes differ from " + + "actual server->client diff (can be ignored in many cases)"); + + this._log.info("Actual changes for server: " + serverDelta.length); + this._log.debug("Actual changes for server: " + + serializeCommands(serverDelta)); + + if (serverDelta.length) { + this._log.info("Uploading changes to server"); + + this._snapshot.data = newSnapshot; + this._snapshot.version = ++server.maxVersion; + + server.deltas.push(serverDelta); + + if (server.formatVersion != STORAGE_FORMAT_VERSION || + this._encryptionChanged) { + this._fullUpload.async(this, cont); + let status = yield; + if (!status) + this._log.error("Could not upload files to server"); // eep? + + } else { + let data = Crypto.PBEencrypt(serializeCommands(server.deltas), + this._cryptoId); + this._dav.PUT("bookmarks-deltas.json", data, cont); + let deltasPut = yield; + + let c = 0; + for (GUID in this._snapshot.data) + c++; + + this._dav.PUT("bookmarks-status.json", + uneval({GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshot.version, + snapEncryption: server.snapEncryption, + deltasEncryption: Crypto.defaultAlgorithm, + Bookmarkscount: c}), cont); + let statusPut = yield; + + if (deltasPut.status >= 200 && deltasPut.status < 300 && + statusPut.status >= 200 && statusPut.status < 300) { + this._log.info("Successfully updated deltas and status on server"); + this._snapshot.save(); + } else { + // FIXME: revert snapshot here? - can't, we already applied + // updates locally! - need to save and retry + this._log.error("Could not update deltas on server"); + } + } + } + + this._log.info("Sync complete"); + synced = true; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + let ok = false; + if (locked) { + this._dav.unlock.async(this._dav, cont); + ok = yield; + } + if (ok && synced) { + this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); + generatorDone(this, self, onComplete, true); + } else { + this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); + generatorDone(this, self, onComplete, false); + } + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _resetServer: function BmkEngine__resetServer(onComplete) { + let [self, cont] = yield; + let done = false; + + try { + this._log.debug("Resetting server data"); + this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", ""); + + this._dav.lock.async(this._dav, cont); + let locked = yield; + if (locked) + this._log.debug("Lock acquired"); + else { + this._log.warn("Could not acquire lock, aborting server reset"); + return; + } + + this._dav.DELETE("bookmarks-status.json", cont); + let statusResp = yield; + this._dav.DELETE("bookmarks-snapshot.json", cont); + let snapshotResp = yield; + this._dav.DELETE("bookmarks-deltas.json", cont); + let deltasResp = yield; + + this._dav.unlock.async(this._dav, cont); + let unlocked = yield; + + function ok(code) { + if (code >= 200 && code < 300) + return true; + if (code == 404) + return true; + return false; + } + + if (!(ok(statusResp.status) && ok(snapshotResp.status) && + ok(deltasResp.status))) { + this._log.error("Could delete server data, response codes " + + statusResp.status + ", " + snapshotResp.status + ", " + + deltasResp.status); + return; + } + + this._log.debug("Server files deleted"); + done = true; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (done) { + this._log.debug("Server reset completed successfully"); + this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); + } else { + this._log.debug("Server reset failed"); + this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", ""); + } + generatorDone(this, self, onComplete, done) + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _resetClient: function BmkEngine__resetClient(onComplete) { + let [self, cont] = yield; + let done = false; + + try { + this._log.debug("Resetting client state"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-start", ""); + + this._snapshot.data = {}; + this._snapshot.version = -1; + this.snapshot.save(); + done = true; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (done) { + this._log.debug("Client reset completed successfully"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-end", ""); + } else { + this._log.debug("Client reset failed"); + this._os.notifyObservers(null, "bookmarks-sync:reset-client-error", ""); + } + generatorDone(this, self, onComplete, done); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + sync: function BmkEngine_sync(onComplete) { + return this._sync.async(this, onComplete); + }, + + resetServer: function BmkEngine_resetServer(onComplete) { + return this._resetServer.async(this, onComplete); + }, + + resetClient: function BmkEngine_resetClient(onComplete) { + return this._resetClient.async(this, onComplete); + } +}; + +serializeCommands: function serializeCommands(commands) { + let json = uneval(commands); + json = json.replace(/ {action/g, "\n {action"); + return json; +} + +serializeConflicts: function serializeConflicts(conflicts) { + let json = uneval(conflicts); + json = json.replace(/ {action/g, "\n {action"); + return json; +} + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); + } +} diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js new file mode 100644 index 000000000000..0230e3daab78 --- /dev/null +++ b/services/sync/modules/identity.js @@ -0,0 +1,133 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Identity']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); + +/* + * Identity + * These objects hold a realm, username, and password + * They can hold a password in memory, but will try to fetch it from + * the password manager if it's not set. + * FIXME: need to rethink this stuff as part of a bigger identity mgmt framework + */ + +function Identity(realm, username, password) { + this._realm = realm; + this._username = username; + this._password = password; +} +Identity.prototype = { + get realm() { return this._realm; }, + set realm(value) { this._realm = value; }, + + get username() { return this._username; }, + set username(value) { this._username = value; }, + + _password: null, + get password() { + if (this._password === null) + return findPassword(this.realm, this.username); + return this._password; + }, + set password(value) { + setPassword(this.realm, this.username, value); + }, + + setTempPassword: function Id_setTempPassword(value) { + this._password = value; + } +}; + +// fixme: move these to util.js? +function findPassword(realm, username) { + // fixme: make a request and get the realm ? + let password; + let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, 'chrome://sync', null, realm); + + for (let i = 0; i < logins.length; i++) { + if (logins[i].username == username) { + password = logins[i].password; + break; + } + } + return password; +} + +function setPassword(realm, username, password) { + // cleanup any existing passwords + let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, 'chrome://sync', null, realm); + for(let i = 0; i < logins.length; i++) { + lm.removeLogin(logins[i]); + } + + if (!password) + return; + + // save the new one + let nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo('chrome://sync', null, realm, + username, password, null, null); + lm.addLogin(login); +} + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); + } +} diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js new file mode 100644 index 000000000000..a9daaefb280b --- /dev/null +++ b/services/sync/modules/service.js @@ -0,0 +1,448 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['WeaveSyncService']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/dav.js"); +Cu.import("resource://weave/identity.js"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +Function.prototype.async = generatorAsync; + +/* + * Service singleton + * Main entry point into Weave's sync framework + */ + +function WeaveSyncService() { this._init(); } +WeaveSyncService.prototype = { + + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + return this.__dirSvc; + }, + + __dav: null, + get _dav() { + if (!this.__dav) + this.__dav = new DAVCollection(); + return this.__dav; + }, + + __bmkEngine: null, + get _bmkEngine() { + if (!this.__bmkEngine) + this.__bmkEngine = new BookmarksEngine(this._dav, this._cryptoId); + return this.__bmkEngine; + }, + + // Logger object + _log: null, + + // Timer object for automagically syncing + _scheduleTimer: null, + + __mozId: null, + get _mozId() { + if (this.__mozId === null) + this.__mozId = new Identity('Mozilla Services Password', this.username); + return this.__mozId; + }, + + __cryptoId: null, + get _cryptoId() { + if (this.__cryptoId === null) + this.__cryptoId = new Identity('Mozilla Services Encryption Passphrase', + this.username); + return this.__cryptoId; + }, + + get username() { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + return branch.getCharPref("browser.places.sync.username"); + }, + set username(value) { + let branch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + branch.setCharPref("browser.places.sync.username", value); + // fixme - need to loop over all Identity objects - needs some rethinking... + this._mozId.username = value; + this._cryptoId.username = value; + }, + + get password() { return this._mozId.password; }, + set password(value) { this._mozId.password = value; }, + + get passphrase() { return this._cryptoId.password; }, + set passphrase(value) { this._cryptoId.password = value; }, + + get userPath() { + this._log.info("Hashing username " + this.username); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let data = converter.convertToByteArray(this.username, {}); + hasher.update(data, data.length); + let rawHash = hasher.finish(false); + + // return the two-digit hexadecimal code for a byte + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); + this._log.debug("Username hashes to " + hash); + return hash; + }, + + get currentUser() { + if (this._dav.loggedIn) + return this.username; + return null; + }, + + _init: function BSS__init() { + this._initLogs(); + this._log.info("Weave Sync Service Initializing"); + + this._serverURL = 'https://services.mozilla.com/'; + this._user = ''; + let enabled = false; + let schedule = 0; + try { + let branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch2); + this._serverURL = branch.getCharPref("browser.places.sync.serverURL"); + enabled = branch.getBoolPref("browser.places.sync.enabled"); + schedule = branch.getIntPref("browser.places.sync.schedule"); + + branch.addObserver("browser.places.sync", this, false); + } + catch (ex) { /* use defaults */ } + + if (!enabled) { + this._log.info("Bookmarks sync disabled"); + return; + } + + switch (schedule) { + case 0: + this._log.info("Bookmarks sync enabled, manual mode"); + break; + case 1: + this._log.info("Bookmarks sync enabled, automagic mode"); + this._enableSchedule(); + break; + default: + this._log.info("Bookmarks sync enabled"); + this._log.info("Invalid schedule setting: " + schedule); + break; + } + }, + + _enableSchedule: function BSS__enableSchedule() { + this._scheduleTimer = Cc["@mozilla.org/timer;1"]. + createInstance(Ci.nsITimer); + let listener = new EventListener(bind2(this, this._onSchedule)); + this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min + this._scheduleTimer.TYPE_REPEATING_SLACK); + }, + + _disableSchedule: function BSS__disableSchedule() { + this._scheduleTimer = null; + }, + + _onSchedule: function BSS__onSchedule() { + this._log.info("Running scheduled sync"); + this.sync(); + }, + + _initLogs: function BSS__initLogs() { + this._log = Log4Moz.Service.getLogger("Service.Main"); + + let formatter = Log4Moz.Service.newFormatter("basic"); + let root = Log4Moz.Service.rootLogger; + root.level = Log4Moz.Level.Debug; + + let capp = Log4Moz.Service.newAppender("console", formatter); + capp.level = Log4Moz.Level.Warn; + root.addAppender(capp); + + let dapp = Log4Moz.Service.newAppender("dump", formatter); + dapp.level = Log4Moz.Level.All; + root.addAppender(dapp); + + let logFile = this._dirSvc.get("ProfD", Ci.nsIFile); + let verboseFile = logFile.clone(); + logFile.append("bm-sync.log"); + logFile.QueryInterface(Ci.nsILocalFile); + verboseFile.append("bm-sync-verbose.log"); + verboseFile.QueryInterface(Ci.nsILocalFile); + + let fapp = Log4Moz.Service.newFileAppender("rotating", logFile, formatter); + fapp.level = Log4Moz.Level.Info; + root.addAppender(fapp); + let vapp = Log4Moz.Service.newFileAppender("rotating", verboseFile, formatter); + vapp.level = Log4Moz.Level.Debug; + root.addAppender(vapp); + }, + + _lock: function BSS__lock() { + if (this._locked) { + this._log.warn("Service lock failed: already locked"); + return false; + } + this._locked = true; + this._log.debug("Service lock acquired"); + return true; + }, + + _unlock: function BSS__unlock() { + this._locked = false; + this._log.debug("Service lock released"); + }, + + // IBookmarksSyncService internal implementation + + _login: function BSS__login(onComplete) { + let [self, cont] = yield; + let success = false; + + try { + this._log.debug("Logging in"); + this._os.notifyObservers(null, "bookmarks-sync:login-start", ""); + + if (!this.username) { + this._log.warn("No username set, login failed"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + return; + } + if (!this.password) { + this._log.warn("No password given or found in password manager"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + return; + } + + this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; + this._log.info("Using server URL: " + this._dav.baseURL); + + this._dav.login.async(this._dav, cont, this.username, this.password); + success = yield; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + this._passphrase = null; + if (success) { + this._log.debug("Login successful"); + this._os.notifyObservers(null, "bookmarks-sync:login-end", ""); + } else { + this._log.debug("Login error"); + this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + } + generatorDone(this, self, onComplete, success); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _resetLock: function BSS__resetLock(onComplete) { + let [self, cont] = yield; + let success = false; + + try { + this._log.debug("Resetting server lock"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-start", ""); + + this._dav.forceUnlock.async(this._dav, cont); + success = yield; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + if (success) { + this._log.debug("Server lock reset successful"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-end", ""); + } else { + this._log.debug("Server lock reset failed"); + this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); + } + generatorDone(this, self, onComplete, success); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), + + // nsIObserver + + observe: function BSS__observe(subject, topic, data) { + switch (topic) { + case "browser.places.sync.enabled": + switch (data) { + case false: + this._log.info("Disabling automagic bookmarks sync"); + this._disableSchedule(); + break; + case true: + this._log.info("Enabling automagic bookmarks sync"); + this._enableSchedule(); + break; + } + break; + case "browser.places.sync.schedule": + switch (data) { + case 0: + this._log.info("Disabling automagic bookmarks sync"); + this._disableSchedule(); + break; + case 1: + this._log.info("Enabling automagic bookmarks sync"); + this._enableSchedule(); + break; + default: + this._log.warn("Unknown schedule value set"); + break; + } + break; + default: + // ignore, there are prefs we observe but don't care about + } + }, + + // IBookmarksSyncService public methods + + // These are global (for all engines) + + login: function BSS_login(password, passphrase) { + if (!this._lock()) + return; + // cache password & passphrase + // if null, _login() will try to get them from the pw manager + this._mozId.setTempPassword(password); + this._cryptoId.setTempPassword(passphrase); + let self = this; + this._login.async(this, function() {self._unlock()}); + }, + + logout: function BSS_logout() { + this._log.info("Logging out"); + this._dav.logout(); + this._mozId.setTempPassword(null); // clear cached password + this._cryptoId.setTempPassword(null); // and passphrase + this._os.notifyObservers(null, "bookmarks-sync:logout", ""); + }, + + resetLock: function BSS_resetLock() { + if (!this._lock()) + return; + let self = this; + this._resetLock.async(this, function() {self._unlock()}); + }, + + // These are per-engine + + sync: function BSS_sync() { + if (!this._lock()) + return; + let self = this; + this._bmkEngine.sync(function() {self._unlock()}); + }, + + resetServer: function BSS_resetServer() { + if (!this._lock()) + return; + let self = this; + this._bmkEngine.resetServer(function() {self._unlock()}); + }, + + resetClient: function BSS_resetClient() { + if (!this._lock()) + return; + let self = this; + this._bmkEngine.resetClient(function() {self._unlock()}); + } +}; + + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); + } +} diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js new file mode 100644 index 000000000000..ffb99e518b2f --- /dev/null +++ b/services/sync/modules/stores.js @@ -0,0 +1,556 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +/* + * Data Stores + * These can wrap, serialize items and apply commands + */ + +function Store() { + this._init(); +} +Store.prototype = { + _logName: "Store", + + _init: function Store__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + }, + + wrap: function Store_wrap() { + }, + + applyCommands: function Store_applyCommands(commandList) { + } +}; + +function SnapshotStore() { + this._init(); +} +SnapshotStore.prototype = { + _logName: "SStore", + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + return this.__dirSvc; + }, + + // Last synced tree, version, and GUID (to detect if the store has + // been completely replaced and invalidate the snapshot) + + _data: {}, + get data() { return this._data; }, + set data(value) { this._data = value; }, + + _version: 0, + get version() { return this._version; }, + set version(value) { this._version = value; }, + + _GUID: null, + get GUID() { + if (!this._GUID) { + let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator); + this._GUID = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); + } + return this._GUID; + }, + set GUID(GUID) { + this._GUID = GUID; + }, + + save: function SStore_save() { + this._log.info("Saving snapshot to disk"); + + let file = this._dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync-snapshot.json"); + file.QueryInterface(Ci.nsILocalFile); + + if (!file.exists()) + file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); + + let fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; + fos.init(file, flags, PERMS_FILE, 0); + + let out = {version: this.version, + GUID: this.GUID, + snapshot: this.data}; + out = uneval(out); + fos.write(out, out.length); + fos.close(); + }, + + load: function SStore_load() { + let file = this._dirSvc.get("ProfD", Ci.nsIFile); + file.append("bm-sync-snapshot.json"); + + if (!file.exists()) + return; + + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(file, MODE_RDONLY, PERMS_FILE, 0); + fis.QueryInterface(Ci.nsILineInputStream); + + let json = ""; + while (fis.available()) { + let ret = {}; + fis.readLine(ret); + json += ret.value; + } + fis.close(); + json = eval(json); + + if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { + this._log.info("Read saved snapshot from disk"); + this.data = json.snapshot; + this.version = json.version; + this.GUID = json.GUID; + } + }, + + serialize: function SStore_serialize() { + let json = uneval(this.data); + json = json.replace(/:{type/g, ":\n\t{type"); + json = json.replace(/}, /g, "},\n "); + json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); + json = json.replace(/, index/g, ",\n\t index"); + json = json.replace(/, title/g, ",\n\t title"); + json = json.replace(/, URI/g, ",\n\t URI"); + json = json.replace(/, tags/g, ",\n\t tags"); + json = json.replace(/, keyword/g, ",\n\t keyword"); + return json; + }, + + wrap: function SStore_wrap() { + }, + + applyCommands: function SStore_applyCommands(commands) { + for (let i = 0; i < commands.length; i++) { + // this._log.debug("Applying cmd to obj: " + uneval(commands[i])); + switch (commands[i].action) { + case "create": + this._data[commands[i].GUID] = eval(uneval(commands[i].data)); + break; + case "edit": + if ("GUID" in commands[i].data) { + // special-case guid changes + let newGUID = commands[i].data.GUID, + oldGUID = commands[i].GUID; + + this._data[newGUID] = this._data[oldGUID]; + delete this._data[oldGUID] + + for (let GUID in this._data) { + if (this._data[GUID].parentGUID == oldGUID) + this._data[GUID].parentGUID = newGUID; + } + } + for (let prop in commands[i].data) { + if (prop == "GUID") + continue; + this._data[commands[i].GUID][prop] = commands[i].data[prop]; + } + break; + case "remove": + delete this._data[commands[i].GUID]; + break; + } + } + return this._data; + } +}; +SnapshotStore.prototype.__proto__ = new Store(); + +function BookmarksStore() { + this._init(); +} +BookmarksStore.prototype = { + _logName: "BStore", + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + __hsvc: null, + get _hsvc() { + if (!this.__hsvc) + this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + return this.__hsvc; + }, + + __ls: null, + get _ls() { + if (!this.__ls) + this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. + getService(Ci.nsILivemarkService); + return this.__ls; + }, + + __ms: null, + get _ms() { + if (!this.__ms) + this.__ms = Cc["@mozilla.org/microsummary/service;1"]. + getService(Ci.nsIMicrosummaryService); + return this.__ms; + }, + + __ts: null, + get _ts() { + if (!this.__ts) + this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + return this.__ts; + }, + + __ans: null, + get _ans() { + if (!this.__ans) + this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + return this.__ans; + }, + + _getFolderNodes: function BSS__getFolderNodes(folder) { + let query = this._hsvc.getNewQuery(); + query.setFolders([folder], 1); + return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + }, + + _wrapNode: function BSS__wrapNode(node) { + var items = {}; + this._wrapNodeInternal(node, items, null, null); + return items; + }, + + _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) { + let GUID = this._bms.getItemGUID(node.itemId); + let item = {parentGUID: parentGUID, + index: index}; + + if (node.type == node.RESULT_TYPE_FOLDER) { + if (this._ls.isLivemark(node.itemId)) { + item.type = "livemark"; + let siteURI = this._ls.getSiteURI(node.itemId); + let feedURI = this._ls.getFeedURI(node.itemId); + item.siteURI = siteURI? siteURI.spec : ""; + item.feedURI = feedURI? feedURI.spec : ""; + } else { + item.type = "folder"; + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._wrapNodeInternal(node.getChild(i), items, GUID, i); + } + } + item.title = node.title; + } else if (node.type == node.RESULT_TYPE_URI || + node.type == node.RESULT_TYPE_QUERY) { + if (this._ms.hasMicrosummary(node.itemId)) { + item.type = "microsummary"; + let micsum = this._ms.getMicrosummary(node.itemId); + item.generatorURI = micsum.generator.uri.spec; // breaks local generators + } else if (node.type == node.RESULT_TYPE_QUERY) { + item.type = "query"; + item.title = node.title; + } else { + item.type = "bookmark"; + item.title = node.title; + } + item.URI = node.uri; + item.tags = this._ts.getTagsForURI(makeURI(node.uri)); + item.keyword = this._bms.getKeywordForBookmark(node.itemId); + } else if (node.type == node.RESULT_TYPE_SEPARATOR) { + item.type = "separator"; + } else { + this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); + return; + } + + items[GUID] = item; + }, + + _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { + return this._wrapNode(this._getFolderNodes(folder)); + }, + + _resetGUIDsInt: function BSS__resetGUIDsInt(node) { + if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) + this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); + + if (node.type == node.RESULT_TYPE_FOLDER && + !this._ls.isLivemark(node.itemId)) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._resetGUIDsInt(node.getChild(i)); + } + } + }, + + _createCommand: function BStore__createCommand(command) { + let newId; + let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); + + if (parentId < 0) { + this._log.warn("Creating node with unknown parent -> reparenting to root"); + parentId = this._bms.bookmarksMenuFolder; + } + + switch (command.data.type) { + case "query": + case "bookmark": + case "microsummary": + this._log.info(" -> creating bookmark \"" + command.data.title + "\""); + let URI = makeURI(command.data.URI); + newId = this._bms.insertBookmark(parentId, + URI, + command.data.index, + command.data.title); + this._ts.untagURI(URI, null); + this._ts.tagURI(URI, command.data.tags); + this._bms.setKeywordForBookmark(newId, command.data.keyword); + + if (command.data.type == "microsummary") { + this._log.info(" \-> is a microsummary"); + let genURI = makeURI(command.data.generatorURI); + try { + let micsum = this._ms.createMicrosummary(URI, genURI); + this._ms.setMicrosummary(newId, micsum); + } + catch(ex) { /* ignore "missing local generator" exceptions */ } + } + break; + case "folder": + this._log.info(" -> creating folder \"" + command.data.title + "\""); + newId = this._bms.createFolder(parentId, + command.data.title, + command.data.index); + break; + case "livemark": + this._log.info(" -> creating livemark \"" + command.data.title + "\""); + newId = this._ls.createLivemark(parentId, + command.data.title, + makeURI(command.data.siteURI), + makeURI(command.data.feedURI), + command.data.index); + break; + case "separator": + this._log.info(" -> creating separator"); + newId = this._bms.insertSeparator(parentId, command.data.index); + break; + default: + this._log.error("_createCommand: Unknown item type: " + command.data.type); + break; + } + if (newId) + this._bms.setItemGUID(newId, command.GUID); + }, + + _removeCommand: function BStore__removeCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Attempted to remove item " + command.GUID + + ", but it does not exist. Skipping."); + return; + } + var type = this._bms.getItemType(itemId); + + switch (type) { + case this._bms.TYPE_BOOKMARK: + this._log.info(" -> removing bookmark " + command.GUID); + this._bms.removeItem(itemId); + break; + case this._bms.TYPE_FOLDER: + this._log.info(" -> removing folder " + command.GUID); + this._bms.removeFolder(itemId); + break; + case this._bms.TYPE_SEPARATOR: + this._log.info(" -> removing separator " + command.GUID); + this._bms.removeItem(itemId); + break; + default: + this._log.error("removeCommand: Unknown item type: " + type); + break; + } + }, + + _editCommand: function BStore__editCommand(command) { + var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); + return; + } + + for (let key in command.data) { + switch (key) { + case "GUID": + var existing = this._bms.getItemIdForGUID(command.data.GUID); + if (existing < 0) + this._bms.setItemGUID(itemId, command.data.GUID); + else + this._log.warn("Can't change GUID " + command.GUID + + " to " + command.data.GUID + ": GUID already exists."); + break; + case "title": + this._bms.setItemTitle(itemId, command.data.title); + break; + case "URI": + this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI)); + break; + case "index": + this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), + command.data.index); + break; + case "parentGUID": + let index = -1; + if (command.data.index && command.data.index >= 0) + index = command.data.index; + this._bms.moveItem( + itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); + break; + case "tags": + let tagsURI = this._bms.getBookmarkURI(itemId); + this._ts.untagURI(URI, null); + this._ts.tagURI(tagsURI, command.data.tags); + break; + case "keyword": + this._bms.setKeywordForBookmark(itemId, command.data.keyword); + break; + case "generatorURI": + let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); + let genURI = makeURI(command.data.generatorURI); + let micsum = this._ms.createMicrosummary(micsumURI, genURI); + this._ms.setMicrosummary(itemId, micsum); + break; + case "siteURI": + this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); + break; + case "feedURI": + this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); + break; + default: + this._log.warn("Can't change item property: " + key); + break; + } + } + }, + + wrap: function BStore_wrap() { + let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder); + let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder); + let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder); + + for (let guid in unfiled) { + if (!(guid in filed)) + filed[guid] = unfiled[guid]; + } + + for (let guid in toolbar) { + if (!(guid in filed)) + filed[guid] = toolbar[guid]; + } + + return filed; // (combined) + }, + + resetGUIDs: function BStore_resetGUIDs() { + this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); + this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); + }, + + applyCommands: function BStore_applyCommands(commandList) { + for (var i = 0; i < commandList.length; i++) { + var command = commandList[i]; + this._log.debug("Processing command: " + uneval(command)); + switch (command["action"]) { + case "create": + this._createCommand(command); + break; + case "remove": + this._removeCommand(command); + break; + case "edit": + this._editCommand(command); + break; + default: + this._log.error("unknown action in command: " + command["action"]); + break; + } + } + } +}; +BookmarksStore.prototype.__proto__ = new Store(); + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); + } +} diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js new file mode 100644 index 000000000000..39bbd3203cd6 --- /dev/null +++ b/services/sync/modules/syncCores.js @@ -0,0 +1,413 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); + +Function.prototype.async = generatorAsync; + +/* + * SyncCore objects + * Sync cores deal with diff creation and conflict resolution. + * Tree data structures where all nodes have GUIDs only need to be + * subclassed for each data type to implement commandLike and + * itemExists. + */ + +function SyncCore() { + this._init(); +} +SyncCore.prototype = { + _logName: "Sync", + + _init: function SC__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + }, + + // FIXME: this won't work for deep objects, or objects with optional properties + _getEdits: function SC__getEdits(a, b) { + let ret = {numProps: 0, props: {}}; + for (prop in a) { + if (!deepEquals(a[prop], b[prop])) { + ret.numProps++; + ret.props[prop] = b[prop]; + } + } + return ret; + }, + + _nodeParents: function SC__nodeParents(GUID, tree) { + return this._nodeParentsInt(GUID, tree, []); + }, + + _nodeParentsInt: function SC__nodeParentsInt(GUID, tree, parents) { + if (!tree[GUID] || !tree[GUID].parentGUID) + return parents; + parents.push(tree[GUID].parentGUID); + return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); + }, + + _detectUpdates: function SC__detectUpdates(onComplete, a, b) { + let [self, cont] = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + let cmds = []; + + try { + for (let GUID in a) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (GUID in b) { + let edits = this._getEdits(a[GUID], b[GUID]); + if (edits.numProps == 0) // no changes - skip + continue; + let parents = this._nodeParents(GUID, b); + cmds.push({action: "edit", GUID: GUID, + depth: parents.length, parents: parents, + data: edits.props}); + } else { + let parents = this._nodeParents(GUID, a); // ??? + cmds.push({action: "remove", GUID: GUID, + depth: parents.length, parents: parents}); + } + } + for (let GUID in b) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (GUID in a) + continue; + let parents = this._nodeParents(GUID, b); + cmds.push({action: "create", GUID: GUID, + depth: parents.length, parents: parents, + data: b[GUID]}); + } + cmds.sort(function(a, b) { + if (a.depth > b.depth) + return 1; + if (a.depth < b.depth) + return -1; + if (a.index > b.index) + return -1; + if (a.index < b.index) + return 1; + return 0; // should never happen, but not a big deal if it does + }); + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + timer = null; + generatorDone(this, self, onComplete, cmds); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _commandLike: function SC__commandLike(a, b) { + this._log.error("commandLike needs to be subclassed"); + + // Check that neither command is null, and verify that the GUIDs + // are different (otherwise we need to check for edits) + if (!a || !b || a.GUID == b.GUID) + return false; + + // Check that all other properties are the same + // FIXME: could be optimized... + for (let key in a) { + if (key != "GUID" && !deepEquals(a[key], b[key])) + return false; + } + for (let key in b) { + if (key != "GUID" && !deepEquals(a[key], b[key])) + return false; + } + return true; + }, + + // When we change the GUID of a local item (because we detect it as + // being the same item as a remote one), we need to fix any other + // local items that have it as their parent + _fixParents: function SC__fixParents(list, oldGUID, newGUID) { + for (let i = 0; i < list.length; i++) { + if (!list[i]) + continue; + if (list[i].data.parentGUID == oldGUID) + list[i].data.parentGUID = newGUID; + for (let j = 0; j < list[i].parents.length; j++) { + if (list[i].parents[j] == oldGUID) + list[i].parents[j] = newGUID; + } + } + }, + + _conflicts: function SC__conflicts(a, b) { + if ((a.GUID == b.GUID) && !deepEquals(a, b)) + return true; + return false; + }, + + _getPropagations: function SC__getPropagations(commands, conflicts, propagations) { + for (let i = 0; i < commands.length; i++) { + let alsoConflicts = function(elt) { + return (elt.action == "create" || elt.action == "remove") && + commands[i].parents.indexOf(elt.GUID) >= 0; + }; + if (conflicts.some(alsoConflicts)) + conflicts.push(commands[i]); + + let cmdConflicts = function(elt) { + return elt.GUID == commands[i].GUID; + }; + if (!conflicts.some(cmdConflicts)) + propagations.push(commands[i]); + } + }, + + _itemExists: function SC__itemExists(GUID) { + this._log.error("itemExists needs to be subclassed"); + return false; + }, + + _reconcile: function SC__reconcile(onComplete, listA, listB) { + let [self, cont] = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + let propagations = [[], []]; + let conflicts = [[], []]; + let ret = {propagations: propagations, conflicts: conflicts}; + this._log.debug("Reconciling " + listA.length + + " against " + listB.length + "commands"); + + try { + let guidChanges = []; + for (let i = 0; i < listA.length; i++) { + let a = listA[i]; + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + //this._log.debug("comparing " + i + ", listB length: " + listB.length); + + let skip = false; + listB = listB.filter(function(b) { + // fast path for when we already found a matching command + if (skip) + return true; + + if (deepEquals(a, b)) { + delete listA[i]; // a + skip = true; + return false; // b + + } else if (this._commandLike(a, b)) { + this._fixParents(listA, a.GUID, b.GUID); + guidChanges.push({action: "edit", + GUID: a.GUID, + data: {GUID: b.GUID}}); + delete listA[i]; // a + skip = true; + return false; // b, but we add it back from guidChanges + } + + // watch out for create commands with GUIDs that already exist + if (b.action == "create" && this._itemExists(b.GUID)) { + this._log.error("Remote command has GUID that already exists " + + "locally. Dropping command."); + return false; // delete b + } + return true; // keep b + }, this); + } + + listA = listA.filter(function(elt) { return elt }); + listB = listB.concat(guidChanges); + + for (let i = 0; i < listA.length; i++) { + for (let j = 0; j < listB.length; j++) { + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + if (this._conflicts(listA[i], listB[j]) || + this._conflicts(listB[j], listA[i])) { + if (!conflicts[0].some( + function(elt) { return elt.GUID == listA[i].GUID })) + conflicts[0].push(listA[i]); + if (!conflicts[1].some( + function(elt) { return elt.GUID == listB[j].GUID })) + conflicts[1].push(listB[j]); + } + } + } + + this._getPropagations(listA, conflicts[0], propagations[1]); + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + this._getPropagations(listB, conflicts[1], propagations[0]); + ret = {propagations: propagations, conflicts: conflicts}; + + } catch (e) { + this._log.error("Exception caught: " + e.message); + + } finally { + timer = null; + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + // Public methods + + detectUpdates: function SC_detectUpdates(onComplete, a, b) { + return this._detectUpdates.async(this, onComplete, a, b); + }, + + reconcile: function SC_reconcile(onComplete, listA, listB) { + return this._reconcile.async(this, onComplete, listA, listB); + } +}; + +function BookmarksSyncCore() { + this._init(); +} +BookmarksSyncCore.prototype = { + _logName: "BMSync", + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + // NOTE: Needs to be subclassed + _itemExists: function BSC__itemExists(GUID) { + return this._bms.getItemIdForGUID(GUID) >= 0; + }, + + _commandLike: function BSC_commandLike(a, b) { + // Check that neither command is null, that their actions, types, + // and parents are the same, and that they don't have the same + // GUID. + // Items with the same GUID do not qualify for 'likeness' because + // we already consider them to be the same object, and therefore + // we need to process any edits. + // The parent GUID check works because reconcile() fixes up the + // parent GUIDs as it runs, and the command list is sorted by + // depth + if (!a || !b || + a.action != b.action || + a.data.type != b.data.type || + a.data.parentGUID != b.data.parentGUID || + a.GUID == b.GUID) + return false; + + // Bookmarks are allowed to be in a different index as long as + // they are in the same folder. Folders and separators must be at + // the same index to qualify for 'likeness'. + switch (a.data.type) { + case "bookmark": + if (a.data.URI == b.data.URI && + a.data.title == b.data.title) + return true; + return false; + case "query": + if (a.data.URI == b.data.URI && + a.data.title == b.data.title) + return true; + return false; + case "microsummary": + if (a.data.URI == b.data.URI && + a.data.generatorURI == b.data.generatorURI) + return true; + return false; + case "folder": + if (a.index == b.index && + a.data.title == b.data.title) + return true; + return false; + case "livemark": + if (a.data.title == b.data.title && + a.data.siteURI == b.data.siteURI && + a.data.feedURI == b.data.feedURI) + return true; + return false; + case "separator": + if (a.index == b.index) + return true; + return false; + default: + this._log.error("commandLike: Unknown item type: " + uneval(a)); + return false; + } + } +}; +BookmarksSyncCore.prototype.__proto__ = new SyncCore(); + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); + } +} diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js new file mode 100644 index 000000000000..64b2714a21b3 --- /dev/null +++ b/services/sync/modules/util.js @@ -0,0 +1,210 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['deepEquals', 'makeFile', 'makeURI', 'xpath', + 'bind2', 'generatorAsync', 'generatorDone', 'EventListener']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/log4moz.js"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +/* + * Utility functions + */ + +function deepEquals(a, b) { + if (!a && !b) + return true; + if (!a || !b) + return false; + + if (typeof(a) != "object" && typeof(b) != "object") + return a == b; + if (typeof(a) != "object" || typeof(b) != "object") + return false; + + for (let key in a) { + if (typeof(a[key]) == "object") { + if (!typeof(b[key]) == "object") + return false; + if (!deepEquals(a[key], b[key])) + return false; + } else { + if (a[key] != b[key]) + return false; + } + } + return true; +} + +function makeFile(path) { + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return file; +} + +function makeURI(URIString) { + if (URIString === null || URIString == "") + return null; + let ioservice = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return ioservice.newURI(URIString, null, null); +} + +function xpath(xmlDoc, xpathString) { + let root = xmlDoc.ownerDocument == null ? + xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement + let nsResolver = xmlDoc.createNSResolver(root); + + return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, + Ci.nsIDOMXPathResult.ANY_TYPE, null); +} + +function bind2(object, method) { + return function innerBind() { return method.apply(object, arguments); } +} + +// Meant to be used like this in code that imports this file: +// +// Function.prototype.async = generatorAsync; +// +// So that you can do: +// +// gen = fooGen.async(...); +// ret = yield; +// +// where fooGen is a generator function, and gen is the running generator. +// ret is whatever the generator 'returns' via generatorDone(). + +function generatorAsync(self, extra_args) { + try { + let args = Array.prototype.slice.call(arguments, 1); + let gen = this.apply(self, args); + gen.next(); // must initialize before sending + gen.send([gen, function(data) {continueGenerator(gen, data);}]); + return gen; + } catch (e) { + if (e instanceof StopIteration) { + dump("async warning: generator stopped unexpectedly"); + return null; + } else { + dump("Exception caught: " + e.message); + } + } +} + +function continueGenerator(generator, data) { + try { generator.send(data); } + catch (e) { + if (e instanceof StopIteration) + dump("continueGenerator warning: generator stopped unexpectedly"); + else + dump("Exception caught: " + e.message); + } +} + +// generators created using Function.async can't simply call the +// callback with the return value, since that would cause the calling +// function to end up running (after the yield) from inside the +// generator. Instead, generators can call this method which sets up +// a timer to call the callback from a timer (and cleans up the timer +// to avoid leaks). It also closes generators after the timeout, to +// keep things clean. +function generatorDone(object, generator, callback, retval) { + if (object._timer) + throw "Called generatorDone when there is a timer already set." + + let cb = bind2(object, function(event) { + generator.close(); + generator = null; + object._timer = null; + if (callback) + callback(retval); + }); + + object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + object._timer.initWithCallback(new EventListener(cb), + 0, object._timer.TYPE_ONE_SHOT); +} + +/* + * Event listener object + * Used to handle XMLHttpRequest and nsITimer callbacks + */ + +function EventListener(handler, eventName) { + this._handler = handler; + this._eventName = eventName; + this._log = Log4Moz.Service.getLogger("Service.EventHandler"); +} +EventListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), + + // DOM event listener + handleEvent: function EL_handleEvent(event) { + this._log.debug("Handling event " + this._eventName); + this._handler(event); + }, + + // nsITimerCallback + notify: function EL_notify(timer) { + this._log.trace("Timer fired"); + this._handler(timer); + } +}; + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); + } +} diff --git a/services/sync/modules/weave.js b/services/sync/modules/weave.js index 87ba09e831aa..841c03b03b4f 100644 --- a/services/sync/modules/weave.js +++ b/services/sync/modules/weave.js @@ -34,2706 +34,32 @@ * * ***** END LICENSE BLOCK ***** */ -EXPORTED_SYMBOLS = ['Weave']; +const EXPORTED_SYMBOLS = ['Weave']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -const MODE_RDONLY = 0x01; -const MODE_WRONLY = 0x02; -const MODE_CREATE = 0x08; -const MODE_APPEND = 0x10; -const MODE_TRUNCATE = 0x20; - -const PERMS_FILE = 0644; -const PERMS_DIRECTORY = 0755; - -const STORAGE_FORMAT_VERSION = 2; - -const ONE_BYTE = 1; -const ONE_KILOBYTE = 1024 * ONE_BYTE; -const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -let ioSvc = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); -let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Components.interfaces.nsIResProtocolHandler); - -if (!resProt.hasSubstitution("weave")) { - let extMgr = Components.classes["@mozilla.org/extensions/manager;1"] - .getService(Components.interfaces.nsIExtensionManager); - let loc = extMgr.getInstallLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); - let extD = loc.getItemLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); - extD.append("modules"); - resProt.setSubstitution("weave", ioSvc.newFileURI(extD)); -} -Cu.import("resource://weave/log4moz.js"); - -/* - * Service object - * Implements IBookmarksSyncService, main entry point - */ - -function BookmarksSyncService() { this._init(); } -BookmarksSyncService.prototype = { - - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - __dirSvc: null, - get _dirSvc() { - if (!this.__dirSvc) - this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - return this.__dirSvc; - }, - - __dav: null, - get _dav() { - if (!this.__dav) - this.__dav = new DAVCollection(); - return this.__dav; - }, - - __bmkEngine: null, - get _bmkEngine() { - if (!this.__bmkEngine) - this.__bmkEngine = new BookmarksEngine(this._dav, this._cryptoId); - return this.__bmkEngine; - }, - - // Logger object - _log: null, - - // Timer object for automagically syncing - _scheduleTimer: null, - - __mozId: null, - get _mozId() { - if (this.__mozId === null) - this.__mozId = new Identity('Mozilla Services Password', this.username); - return this.__mozId; - }, - - __cryptoId: null, - get _cryptoId() { - if (this.__cryptoId === null) - this.__cryptoId = new Identity('Mozilla Services Encryption Passphrase', - this.username); - return this.__cryptoId; - }, - - get username() { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - return branch.getCharPref("browser.places.sync.username"); - }, - set username(value) { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - branch.setCharPref("browser.places.sync.username", value); - // fixme - need to loop over all Identity objects - needs some rethinking... - this._mozId.username = value; - this._cryptoId.username = value; - }, - - get password() { return this._mozId.password; }, - set password(value) { this._mozId.password = value; }, - - get passphrase() { return this._cryptoId.password; }, - set passphrase(value) { this._cryptoId.password = value; }, - - get userPath() { - this._log.info("Hashing username " + this.username); - - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA1); - - let data = converter.convertToByteArray(this.username, {}); - hasher.update(data, data.length); - let rawHash = hasher.finish(false); - - // return the two-digit hexadecimal code for a byte - function toHexString(charCode) { - return ("0" + charCode.toString(16)).slice(-2); - } - - let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); - this._log.debug("Username hashes to " + hash); - return hash; - }, - - get currentUser() { - if (this._dav.loggedIn) - return this.username; - return null; - }, - - _init: function BSS__init() { - this._initLogs(); - this._log.info("Weave Sync Service Initializing"); - - this._serverURL = 'https://services.mozilla.com/'; - this._user = ''; - let enabled = false; - let schedule = 0; - try { - let branch = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch2); - this._serverURL = branch.getCharPref("browser.places.sync.serverURL"); - enabled = branch.getBoolPref("browser.places.sync.enabled"); - schedule = branch.getIntPref("browser.places.sync.schedule"); - - branch.addObserver("browser.places.sync", this, false); - } - catch (ex) { /* use defaults */ } - - if (!enabled) { - this._log.info("Bookmarks sync disabled"); - return; - } - - switch (schedule) { - case 0: - this._log.info("Bookmarks sync enabled, manual mode"); - break; - case 1: - this._log.info("Bookmarks sync enabled, automagic mode"); - this._enableSchedule(); - break; - default: - this._log.info("Bookmarks sync enabled"); - this._log.info("Invalid schedule setting: " + schedule); - break; - } - }, - - _enableSchedule: function BSS__enableSchedule() { - this._scheduleTimer = Cc["@mozilla.org/timer;1"]. - createInstance(Ci.nsITimer); - let listener = new EventListener(bind2(this, this._onSchedule)); - this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min - this._scheduleTimer.TYPE_REPEATING_SLACK); - }, - - _disableSchedule: function BSS__disableSchedule() { - this._scheduleTimer = null; - }, - - _onSchedule: function BSS__onSchedule() { - this._log.info("Running scheduled sync"); - this.sync(); - }, - - _initLogs: function BSS__initLogs() { - this._log = Log4Moz.Service.getLogger("Service.Main"); - - let formatter = Log4Moz.Service.newFormatter("basic"); - let root = Log4Moz.Service.rootLogger; - root.level = Log4Moz.Level.Debug; - - let capp = Log4Moz.Service.newAppender("console", formatter); - capp.level = Log4Moz.Level.Warn; - root.addAppender(capp); - - let dapp = Log4Moz.Service.newAppender("dump", formatter); - dapp.level = Log4Moz.Level.All; - root.addAppender(dapp); - - let logFile = this._dirSvc.get("ProfD", Ci.nsIFile); - let verboseFile = logFile.clone(); - logFile.append("bm-sync.log"); - logFile.QueryInterface(Ci.nsILocalFile); - verboseFile.append("bm-sync-verbose.log"); - verboseFile.QueryInterface(Ci.nsILocalFile); - - let fapp = Log4Moz.Service.newFileAppender("rotating", logFile, formatter); - fapp.level = Log4Moz.Level.Info; - root.addAppender(fapp); - let vapp = Log4Moz.Service.newFileAppender("rotating", verboseFile, formatter); - vapp.level = Log4Moz.Level.Debug; - root.addAppender(vapp); - }, - - _lock: function BSS__lock() { - if (this._locked) { - this._log.warn("Service lock failed: already locked"); - return false; - } - this._locked = true; - this._log.debug("Service lock acquired"); - return true; - }, - - _unlock: function BSS__unlock() { - this._locked = false; - this._log.debug("Service lock released"); - }, - - // IBookmarksSyncService internal implementation - - _login: function BSS__login(onComplete) { - let [self, cont] = yield; - let success = false; - - try { - this._log.debug("Logging in"); - this._os.notifyObservers(null, "bookmarks-sync:login-start", ""); - - if (!this.username) { - this._log.warn("No username set, login failed"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - return; - } - if (!this.password) { - this._log.warn("No password given or found in password manager"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - return; - } - - this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; - this._log.info("Using server URL: " + this._dav.baseURL); - - this._dav.login.async(this._dav, cont, this.username, this.password); - success = yield; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - this._passphrase = null; - if (success) { - this._log.debug("Login successful"); - this._os.notifyObservers(null, "bookmarks-sync:login-end", ""); - } else { - this._log.debug("Login error"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - } - generatorDone(this, self, onComplete, success); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _resetLock: function BSS__resetLock(onComplete) { - let [self, cont] = yield; - let success = false; - - try { - this._log.debug("Resetting server lock"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-start", ""); - - this._dav.forceUnlock.async(this._dav, cont); - success = yield; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (success) { - this._log.debug("Server lock reset successful"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-end", ""); - } else { - this._log.debug("Server lock reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); - } - generatorDone(this, self, onComplete, success); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), - - // nsIObserver - - observe: function BSS__observe(subject, topic, data) { - switch (topic) { - case "browser.places.sync.enabled": - switch (data) { - case false: - this._log.info("Disabling automagic bookmarks sync"); - this._disableSchedule(); - break; - case true: - this._log.info("Enabling automagic bookmarks sync"); - this._enableSchedule(); - break; - } - break; - case "browser.places.sync.schedule": - switch (data) { - case 0: - this._log.info("Disabling automagic bookmarks sync"); - this._disableSchedule(); - break; - case 1: - this._log.info("Enabling automagic bookmarks sync"); - this._enableSchedule(); - break; - default: - this._log.warn("Unknown schedule value set"); - break; - } - break; - default: - // ignore, there are prefs we observe but don't care about - } - }, - - // IBookmarksSyncService public methods - - // These are global (for all engines) - - login: function BSS_login(password, passphrase) { - if (!this._lock()) - return; - // cache password & passphrase - // if null, _login() will try to get them from the pw manager - this._mozId.setTempPassword(password); - this._cryptoId.setTempPassword(passphrase); - let self = this; - this._login.async(this, function() {self._unlock()}); - }, - - logout: function BSS_logout() { - this._log.info("Logging out"); - this._dav.logout(); - this._mozId.setTempPassword(null); // clear cached password - this._cryptoId.setTempPassword(null); // and passphrase - this._os.notifyObservers(null, "bookmarks-sync:logout", ""); - }, - - resetLock: function BSS_resetLock() { - if (!this._lock()) - return; - let self = this; - this._resetLock.async(this, function() {self._unlock()}); - }, - - // These are per-engine - - sync: function BSS_sync() { - if (!this._lock()) - return; - let self = this; - this._bmkEngine.sync(function() {self._unlock()}); - }, - - resetServer: function BSS_resetServer() { - if (!this._lock()) - return; - let self = this; - this._bmkEngine.resetServer(function() {self._unlock()}); - }, - - resetClient: function BSS_resetClient() { - if (!this._lock()) - return; - let self = this; - this._bmkEngine.resetClient(function() {self._unlock()}); - } -}; - -function BookmarksEngine(davCollection, cryptoId) { - this._init(davCollection, cryptoId); -} -BookmarksEngine.prototype = { - _logName: "BmkEngine", - - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new BookmarksStore(); - return this.__store; - }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new BookmarksSyncCore(); - return this.__core; - }, - - __snapshot: null, - get _snapshot() { - if (!this.__snapshot) - this.__snapshot = new SnapshotStore(); - return this.__snapshot; - }, - set _snapshot(value) { - this.__snapshot = value; - }, - - _init: function BmkEngine__init(davCollection, cryptoId) { - this._dav = davCollection; - this._cryptoId = cryptoId; - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._snapshot.load(); - }, - - _checkStatus: function BmkEngine__checkStatus(code, msg) { - if (code >= 200 && code < 300) - return; - this._log.error(msg + " Error code: " + code); - throw 'checkStatus failed'; - }, - - /* Get the deltas/combined updates from the server - * Returns: - * status: - * -1: error - * 0: ok - * These fields may be null when status is -1: - * formatVersion: - * version of the data format itself. For compatibility checks. - * maxVersion: - * the latest version on the server - * snapVersion: - * the version of the current snapshot on the server (deltas not applied) - * snapEncryption: - * encryption algorithm currently used on the server-stored snapshot - * deltasEncryption: - * encryption algorithm currently used on the server-stored deltas - * snapshot: - * full snapshot of the latest server version (deltas applied) - * deltas: - * all of the individual deltas on the server - * updates: - * the relevant deltas (from our snapshot version to current), - * combined into a single set. - */ - _getServerData: function BmkEngine__getServerData(onComplete) { - let [self, cont] = yield; - let ret = {status: -1, - formatVersion: null, maxVersion: null, snapVersion: null, - snapEncryption: null, deltasEncryption: null, - snapshot: null, deltas: null, updates: null}; - - try { - this._log.info("Getting bookmarks status from server"); - this._dav.GET("bookmarks-status.json", cont); - let resp = yield; - let status = resp.status; - - switch (status) { - case 200: - this._log.info("Got bookmarks status from server"); - - let status = eval(resp.responseText); - let deltas, allDeltas; - let snap = new SnapshotStore(); - - // Bail out if the server has a newer format version than we can parse - if (status.formatVersion > STORAGE_FORMAT_VERSION) { - this._log.error("Server uses storage format v" + status.formatVersion + - ", this client understands up to v" + STORAGE_FORMAT_VERSION); - generatorDone(this, self, onComplete, ret) - return; - } - - if (status.formatVersion == 0) { - ret.snapEncryption = status.snapEncryption = "none"; - ret.deltasEncryption = status.deltasEncryption = "none"; - } - - if (status.GUID != this._snapshot.GUID) { - this._log.info("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._store.resetGUIDs(); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.GUID = status.GUID; - } - - if (this._snapshot.version < status.snapVersion) { - if (this._snapshot.version >= 0) - this._log.info("Local snapshot is out of date"); - - this._log.info("Downloading server snapshot"); - this._dav.GET("bookmarks-snapshot.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download snapshot."); - snap.data = Weave.Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.snapEncryption); - - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Weave.Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); - deltas = eval(uneval(allDeltas)); - - } else if (this._snapshot.version >= status.snapVersion && - this._snapshot.version < status.maxVersion) { - snap.data = eval(uneval(this._snapshot.data)); - - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Weave.Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); - deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); - - } else if (this._snapshot.version == status.maxVersion) { - snap.data = eval(uneval(this._snapshot.data)); - - // FIXME: could optimize this case by caching deltas file - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Weave.Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); - deltas = []; - - } else { // this._snapshot.version > status.maxVersion - this._log.error("Server snapshot is older than local snapshot"); - return; - } - - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands(deltas[i]); - } - - ret.status = 0; - ret.formatVersion = status.formatVersion; - ret.maxVersion = status.maxVersion; - ret.snapVersion = status.snapVersion; - ret.snapEncryption = status.snapEncryption; - ret.deltasEncryption = status.deltasEncryption; - ret.snapshot = snap.data; - ret.deltas = allDeltas; - this._core.detectUpdates(cont, this._snapshot.data, snap.data); - ret.updates = yield; - break; - - case 404: - this._log.info("Server has no status file, Initial upload to server"); - - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - - this._fullUpload.async(this, cont); - let uploadStatus = yield; - if (!uploadStatus) - return; - - this._log.info("Initial upload to server successful"); - this.snapshot.save(); - - ret.status = 0; - ret.formatVersion = STORAGE_FORMAT_VERSION; - ret.maxVersion = this._snapshot.version; - ret.snapVersion = this._snapshot.version; - ret.snapEncryption = Weave.Crypto.defaultAlgorithm; - ret.deltasEncryption = Weave.Crypto.defaultAlgorithm; - ret.snapshot = eval(uneval(this._snapshot.data)); - ret.deltas = []; - ret.updates = []; - break; - - default: - this._log.error("Could not get bookmarks.status: unknown HTTP status code " + - status); - break; - } - - } catch (e) { - if (e != 'checkStatus failed' && - e != 'decrypt failed') - this._log.error("Exception caught: " + e.message); - - } finally { - generatorDone(this, self, onComplete, ret) - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _fullUpload: function BmkEngine__fullUpload(onComplete) { - let [self, cont] = yield; - let ret = false; - - try { - let data = Weave.Crypto.PBEencrypt(this._snapshot.serialize(), - this._cryptoId); - this._dav.PUT("bookmarks-snapshot.json", data, cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload snapshot."); - - this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload deltas."); - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshot.version, - maxVersion: this._snapshot.version, - snapEncryption: Weave.Crypto.defaultAlgorithm, - deltasEncryption: "none", - bookmarksCount: c}), cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload status file."); - - this._log.info("Full upload to server successful"); - ret = true; - - } catch (e) { - if (e != 'checkStatus failed') - this._log.error("Exception caught: " + e.message); - - } finally { - generatorDone(this, self, onComplete, ret) - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - // original - // / \ - // A / \ B - // / \ - // client --C-> server - // \ / - // D \ / C - // \ / - // final - - // If we have a saved snapshot, original == snapshot. Otherwise, - // it's the empty set {}. - - // C is really the diff between server -> final, so if we determine - // D we can calculate C from that. In the case where A and B have - // no conflicts, C == A and D == B. - - // Sync flow: - // 1) Fetch server deltas - // 1.1) Construct current server status from snapshot + server deltas - // 1.2) Generate single delta from snapshot -> current server status ("B") - // 2) Generate local deltas from snapshot -> current client status ("A") - // 3) Reconcile client/server deltas and generate new deltas for them. - // Reconciliation won't generate C directly, we will simply diff - // server->final after step 3.1. - // 3.1) Apply local delta with server changes ("D") - // 3.2) Append server delta to the delta file and upload ("C") - - _sync: function BmkEngine__sync(onComplete) { - let [self, cont] = yield; - let synced = false, locked = null; - - try { - this._log.info("Beginning sync"); - this._os.notifyObservers(null, "bookmarks-sync:sync-start", ""); - - this._dav.lock.async(this._dav, cont); - locked = yield; - - if (locked) - this._log.info("Lock acquired"); - else { - this._log.warn("Could not acquire lock, aborting sync"); - return; - } - - // 1) Fetch server deltas - this._getServerData.async(this, cont); - let server = yield; - - this._log.info("Local snapshot version: " + this._snapshot.version); - this._log.info("Server status: " + server.status); - this._log.info("Server maxVersion: " + server.maxVersion); - this._log.info("Server snapVersion: " + server.snapVersion); - - if (server.status != 0) { - this._log.fatal("Sync error: could not get server status, " + - "or initial upload failed. Aborting sync."); - return; - } - - // 2) Generate local deltas from snapshot -> current client status - - let localJson = new SnapshotStore(); - localJson.data = this._store.wrap(); - this._core.detectUpdates(cont, this._snapshot.data, localJson.data); - let localUpdates = yield; - - this._log.debug("local json:\n" + localJson.serialize()); - this._log.debug("Local updates: " + serializeCommands(localUpdates)); - this._log.debug("Server updates: " + serializeCommands(server.updates)); - - if (server.updates.length == 0 && localUpdates.length == 0) { - this._snapshot.version = server.maxVersion; - this._log.info("Sync complete (1): no changes needed on client or server"); - synced = true; - return; - } - - // 3) Reconcile client/server deltas and generate new deltas for them. - - this._log.info("Reconciling client/server updates"); - this._core.reconcile(cont, localUpdates, server.updates); - let ret = yield; - - let clientChanges = ret.propagations[0]; - let serverChanges = ret.propagations[1]; - let clientConflicts = ret.conflicts[0]; - let serverConflicts = ret.conflicts[1]; - - this._log.info("Changes for client: " + clientChanges.length); - this._log.info("Predicted changes for server: " + serverChanges.length); - this._log.info("Client conflicts: " + clientConflicts.length); - this._log.info("Server conflicts: " + serverConflicts.length); - this._log.debug("Changes for client: " + serializeCommands(clientChanges)); - this._log.debug("Predicted changes for server: " + serializeCommands(serverChanges)); - this._log.debug("Client conflicts: " + serializeConflicts(clientConflicts)); - this._log.debug("Server conflicts: " + serializeConflicts(serverConflicts)); - - if (!(clientChanges.length || serverChanges.length || - clientConflicts.length || serverConflicts.length)) { - this._log.info("Sync complete (2): no changes needed on client or server"); - this._snapshot.data = localJson.data; - this._snapshot.version = server.maxVersion; - this._snapshot.save(); - synced = true; - return; - } - - if (clientConflicts.length || serverConflicts.length) { - this._log.warn("Conflicts found! Discarding server changes"); - } - - let savedSnap = eval(uneval(this._snapshot.data)); - let savedVersion = this._snapshot.version; - let newSnapshot; - - // 3.1) Apply server changes to local store - if (clientChanges.length) { - this._log.info("Applying changes locally"); - // Note that we need to need to apply client changes to the - // current tree, not the saved snapshot - - localJson.applyCommands(clientChanges); - this._snapshot.data = localJson.data; - this._snapshot.version = server.maxVersion; - this._store.applyCommands(clientChanges); - newSnapshot = this._store.wrap(); - - this._core.detectUpdates(cont, this._snapshot.data, newSnapshot); - let diff = yield; - if (diff.length != 0) { - this._log.warn("Commands did not apply correctly"); - this._log.debug("Diff from snapshot+commands -> " + - "new snapshot after commands:\n" + - serializeCommands(diff)); - // FIXME: do we really want to revert the snapshot here? - this._snapshot.data = eval(uneval(savedSnap)); - this._snapshot.version = savedVersion; - } - - this._snapshot.save(); - } - - // 3.2) Append server delta to the delta file and upload - - // Generate a new diff, from the current server snapshot to the - // current client snapshot. In the case where there are no - // conflicts, it should be the same as what the resolver returned - - newSnapshot = this._store.wrap(); - this._core.detectUpdates(cont, server.snapshot, newSnapshot); - let serverDelta = yield; - - // Log an error if not the same - if (!(serverConflicts.length || - deepEquals(serverChanges, serverDelta))) - this._log.warn("Predicted server changes differ from " + - "actual server->client diff (can be ignored in many cases)"); - - this._log.info("Actual changes for server: " + serverDelta.length); - this._log.debug("Actual changes for server: " + - serializeCommands(serverDelta)); - - if (serverDelta.length) { - this._log.info("Uploading changes to server"); - - this._snapshot.data = newSnapshot; - this._snapshot.version = ++server.maxVersion; - - server.deltas.push(serverDelta); - - if (server.formatVersion != STORAGE_FORMAT_VERSION || - this._encryptionChanged) { - this._fullUpload.async(this, cont); - let status = yield; - if (!status) - this._log.error("Could not upload files to server"); // eep? - - } else { - let data = Weave.Crypto.PBEencrypt(serializeCommands(server.deltas), - this._cryptoId); - this._dav.PUT("bookmarks-deltas.json", data, cont); - let deltasPut = yield; - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshot.version, - snapEncryption: server.snapEncryption, - deltasEncryption: Weave.Crypto.defaultAlgorithm, - Bookmarkscount: c}), cont); - let statusPut = yield; - - if (deltasPut.status >= 200 && deltasPut.status < 300 && - statusPut.status >= 200 && statusPut.status < 300) { - this._log.info("Successfully updated deltas and status on server"); - this._snapshot.save(); - } else { - // FIXME: revert snapshot here? - can't, we already applied - // updates locally! - need to save and retry - this._log.error("Could not update deltas on server"); - } - } - } - - this._log.info("Sync complete"); - synced = true; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - let ok = false; - if (locked) { - this._dav.unlock.async(this._dav, cont); - ok = yield; - } - if (ok && synced) { - this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); - generatorDone(this, self, onComplete, true); - } else { - this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); - generatorDone(this, self, onComplete, false); - } - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _resetServer: function BmkEngine__resetServer(onComplete) { - let [self, cont] = yield; - let done = false; - - try { - this._log.debug("Resetting server data"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", ""); - - this._dav.lock.async(this._dav, cont); - let locked = yield; - if (locked) - this._log.debug("Lock acquired"); - else { - this._log.warn("Could not acquire lock, aborting server reset"); - return; - } - - this._dav.DELETE("bookmarks-status.json", cont); - let statusResp = yield; - this._dav.DELETE("bookmarks-snapshot.json", cont); - let snapshotResp = yield; - this._dav.DELETE("bookmarks-deltas.json", cont); - let deltasResp = yield; - - this._dav.unlock.async(this._dav, cont); - let unlocked = yield; - - function ok(code) { - if (code >= 200 && code < 300) - return true; - if (code == 404) - return true; - return false; - } - - if (!(ok(statusResp.status) && ok(snapshotResp.status) && - ok(deltasResp.status))) { - this._log.error("Could delete server data, response codes " + - statusResp.status + ", " + snapshotResp.status + ", " + - deltasResp.status); - return; - } - - this._log.debug("Server files deleted"); - done = true; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (done) { - this._log.debug("Server reset completed successfully"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); - } else { - this._log.debug("Server reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", ""); - } - generatorDone(this, self, onComplete, done) - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _resetClient: function BmkEngine__resetClient(onComplete) { - let [self, cont] = yield; - let done = false; - - try { - this._log.debug("Resetting client state"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-start", ""); - - this._snapshot.data = {}; - this._snapshot.version = -1; - this.snapshot.save(); - done = true; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (done) { - this._log.debug("Client reset completed successfully"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-end", ""); - } else { - this._log.debug("Client reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-error", ""); - } - generatorDone(this, self, onComplete, done); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - sync: function BmkEngine_sync(onComplete) { - return this._sync.async(this, onComplete); - }, - - resetServer: function BmkEngine_resetServer(onComplete) { - return this._resetServer.async(this, onComplete); - }, - - resetClient: function BmkEngine_resetClient(onComplete) { - return this._resetClient.async(this, onComplete); - } -}; - -function Identity(realm, username, password) { - this._realm = realm; - this._username = username; - this._password = password; -} -Identity.prototype = { - get realm() { return this._realm; }, - set realm(value) { this._realm = value; }, - - get username() { return this._username; }, - set username(value) { this._username = value; }, - - _password: null, - get password() { - if (this._password === null) - return findPassword(this.realm, this.username); - return this._password; - }, - set password(value) { - setPassword(this.realm, this.username, value); - }, - - setTempPassword: function Id_setTempPassword(value) { - this._password = value; - } -}; - -function Crypto() { - this._init(); -} -Crypto.prototype = { - _logName: "Crypto", - - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - __xxxtea: {}, - __xxxteaLoaded: false, - get _xxxtea() { - if (!this.__xxxteaLoaded) { - let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. - getService(Ci.mozIJSSubScriptLoader); - jsLoader.loadSubScript("chrome://weave/content/encrypt.js", this.__xxxtea); - this.__xxxteaLoaded = true; - } - return this.__xxxtea; - }, - - get defaultAlgorithm() { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - return branch.getCharPref("browser.places.sync.encryption"); - }, - set defaultAlgorithm(value) { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - let cur = branch.getCharPref("browser.places.sync.encryption"); - if (value != cur) - branch.setCharPref("browser.places.sync.encryption", value); - }, - - _init: function Crypto__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch2); - branch.addObserver("browser.places.sync.encryption", this, false); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), - - // nsIObserver - - observe: function Sync_observe(subject, topic, data) { - switch (topic) { - case "browser.places.sync.encryption": - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - - let cur = branch.getCharPref("browser.places.sync.encryption"); - if (cur == data) - return; - - switch (data) { - case "none": - this._log.info("Encryption disabled"); - break; - case "XXXTEA": - this._log.info("Using encryption algorithm: " + data); - break; - default: - this._log.warn("Unknown encryption algorithm, resetting"); - branch.setCharPref("browser.places.sync.encryption", "XXXTEA"); - return; // otherwise we'll send the alg changed event twice - } - // FIXME: listen to this bad boy somewhere - this._os.notifyObservers(null, "weave:encryption:algorithm-changed", ""); - break; - default: - this._log.warn("Unknown encryption preference changed - ignoring"); - } - }, - - // Crypto - - PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) { - let out; - if (!algorithm) - algorithm = this.defaultAlgorithm; - switch (algorithm) { - case "none": - out = data; - break; - case "XXXTEA": - try { - this._log.debug("Encrypting data"); - out = this._xxxtea.encrypt(data, identity.password); - this._log.debug("Done encrypting data"); - } catch (e) { - this._log.error("Data encryption failed: " + e); - throw 'encrypt failed'; - } - break; - default: - this._log.error("Unknown encryption algorithm: " + algorithm); - throw 'encrypt failed'; - } - return out; - }, - - PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) { - let out; - switch (algorithm) { - case "none": - out = eval(data); - break; - case "XXXTEA": - try { - this._log.debug("Decrypting data"); - out = eval(this._xxxtea.decrypt(data, identity.password)); - this._log.debug("Done decrypting data"); - } catch (e) { - this._log.error("Data decryption failed: " + e); - throw 'decrypt failed'; - } - break; - default: - this._log.error("Unknown encryption algorithm: " + algorithm); - throw 'decrypt failed'; - } - return out; - } -}; +addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://weave/service.js"); let Weave = {}; -Weave.Crypto = new Crypto(); -Weave.Service = new BookmarksSyncService(); - -function findPassword(realm, username) { - // fixme: make a request and get the realm ? - let password; - let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://sync', null, realm); - - for (let i = 0; i < logins.length; i++) { - if (logins[i].username == username) { - password = logins[i].password; - break; - } - } - return password; -} - -function setPassword(realm, username, password) { - // cleanup any existing passwords - let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://sync', null, realm); - for(let i = 0; i < logins.length; i++) { - lm.removeLogin(logins[i]); - } - - if (!password) - return; - - // save the new one - let nsLoginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new nsLoginInfo('chrome://sync', null, realm, - username, password, null, null); - lm.addLogin(login); -} - -serializeCommands: function serializeCommands(commands) { - let json = uneval(commands); - json = json.replace(/ {action/g, "\n {action"); - return json; -} - -serializeConflicts: function serializeConflicts(conflicts) { - let json = uneval(conflicts); - json = json.replace(/ {action/g, "\n {action"); - return json; -} - -/* - * SyncCore objects - * Sync cores deal with diff creation and conflict resolution. - * Tree data structures where all nodes have GUIDs only need to be - * subclassed for each data type to implement commandLike and - * itemExists. - */ - -function SyncCore() { - this._init(); -} -SyncCore.prototype = { - _logName: "Sync", - - _init: function SC__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - }, - - // FIXME: this won't work for deep objects, or objects with optional properties - _getEdits: function SC__getEdits(a, b) { - let ret = {numProps: 0, props: {}}; - for (prop in a) { - if (!deepEquals(a[prop], b[prop])) { - ret.numProps++; - ret.props[prop] = b[prop]; - } - } - return ret; - }, - - _nodeParents: function SC__nodeParents(GUID, tree) { - return this._nodeParentsInt(GUID, tree, []); - }, - - _nodeParentsInt: function SC__nodeParentsInt(GUID, tree, parents) { - if (!tree[GUID] || !tree[GUID].parentGUID) - return parents; - parents.push(tree[GUID].parentGUID); - return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); - }, - - _detectUpdates: function SC__detectUpdates(onComplete, a, b) { - let [self, cont] = yield; - let listener = new EventListener(cont); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - - let cmds = []; - - try { - for (let GUID in a) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (GUID in b) { - let edits = this._getEdits(a[GUID], b[GUID]); - if (edits.numProps == 0) // no changes - skip - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "edit", GUID: GUID, - depth: parents.length, parents: parents, - data: edits.props}); - } else { - let parents = this._nodeParents(GUID, a); // ??? - cmds.push({action: "remove", GUID: GUID, - depth: parents.length, parents: parents}); - } - } - for (let GUID in b) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (GUID in a) - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "create", GUID: GUID, - depth: parents.length, parents: parents, - data: b[GUID]}); - } - cmds.sort(function(a, b) { - if (a.depth > b.depth) - return 1; - if (a.depth < b.depth) - return -1; - if (a.index > b.index) - return -1; - if (a.index < b.index) - return 1; - return 0; // should never happen, but not a big deal if it does - }); - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - timer = null; - generatorDone(this, self, onComplete, cmds); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _commandLike: function SC__commandLike(a, b) { - this._log.error("commandLike needs to be subclassed"); - - // Check that neither command is null, and verify that the GUIDs - // are different (otherwise we need to check for edits) - if (!a || !b || a.GUID == b.GUID) - return false; - - // Check that all other properties are the same - // FIXME: could be optimized... - for (let key in a) { - if (key != "GUID" && !deepEquals(a[key], b[key])) - return false; - } - for (let key in b) { - if (key != "GUID" && !deepEquals(a[key], b[key])) - return false; - } - return true; - }, - - // When we change the GUID of a local item (because we detect it as - // being the same item as a remote one), we need to fix any other - // local items that have it as their parent - _fixParents: function SC__fixParents(list, oldGUID, newGUID) { - for (let i = 0; i < list.length; i++) { - if (!list[i]) - continue; - if (list[i].data.parentGUID == oldGUID) - list[i].data.parentGUID = newGUID; - for (let j = 0; j < list[i].parents.length; j++) { - if (list[i].parents[j] == oldGUID) - list[i].parents[j] = newGUID; - } - } - }, - - _conflicts: function SC__conflicts(a, b) { - if ((a.GUID == b.GUID) && !deepEquals(a, b)) - return true; - return false; - }, - - _getPropagations: function SC__getPropagations(commands, conflicts, propagations) { - for (let i = 0; i < commands.length; i++) { - let alsoConflicts = function(elt) { - return (elt.action == "create" || elt.action == "remove") && - commands[i].parents.indexOf(elt.GUID) >= 0; - }; - if (conflicts.some(alsoConflicts)) - conflicts.push(commands[i]); - - let cmdConflicts = function(elt) { - return elt.GUID == commands[i].GUID; - }; - if (!conflicts.some(cmdConflicts)) - propagations.push(commands[i]); - } - }, - - _itemExists: function SC__itemExists(GUID) { - this._log.error("itemExists needs to be subclassed"); - return false; - }, - - _reconcile: function SC__reconcile(onComplete, listA, listB) { - let [self, cont] = yield; - let listener = new EventListener(cont); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - - let propagations = [[], []]; - let conflicts = [[], []]; - let ret = {propagations: propagations, conflicts: conflicts}; - this._log.debug("Reconciling " + listA.length + - " against " + listB.length + "commands"); - - try { - let guidChanges = []; - for (let i = 0; i < listA.length; i++) { - let a = listA[i]; - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - //this._log.debug("comparing " + i + ", listB length: " + listB.length); - - let skip = false; - listB = listB.filter(function(b) { - // fast path for when we already found a matching command - if (skip) - return true; - - if (deepEquals(a, b)) { - delete listA[i]; // a - skip = true; - return false; // b - - } else if (this._commandLike(a, b)) { - this._fixParents(listA, a.GUID, b.GUID); - guidChanges.push({action: "edit", - GUID: a.GUID, - data: {GUID: b.GUID}}); - delete listA[i]; // a - skip = true; - return false; // b, but we add it back from guidChanges - } - - // watch out for create commands with GUIDs that already exist - if (b.action == "create" && this._itemExists(b.GUID)) { - this._log.error("Remote command has GUID that already exists " + - "locally. Dropping command."); - return false; // delete b - } - return true; // keep b - }, this); - } - - listA = listA.filter(function(elt) { return elt }); - listB = listB.concat(guidChanges); - - for (let i = 0; i < listA.length; i++) { - for (let j = 0; j < listB.length; j++) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (this._conflicts(listA[i], listB[j]) || - this._conflicts(listB[j], listA[i])) { - if (!conflicts[0].some( - function(elt) { return elt.GUID == listA[i].GUID })) - conflicts[0].push(listA[i]); - if (!conflicts[1].some( - function(elt) { return elt.GUID == listB[j].GUID })) - conflicts[1].push(listB[j]); - } - } - } - - this._getPropagations(listA, conflicts[0], propagations[1]); - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - this._getPropagations(listB, conflicts[1], propagations[0]); - ret = {propagations: propagations, conflicts: conflicts}; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - timer = null; - generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - // Public methods - - detectUpdates: function SC_detectUpdates(onComplete, a, b) { - return this._detectUpdates.async(this, onComplete, a, b); - }, - - reconcile: function SC_reconcile(onComplete, listA, listB) { - return this._reconcile.async(this, onComplete, listA, listB); - } -}; - -function BookmarksSyncCore() { - this._init(); -} -BookmarksSyncCore.prototype = { - _logName: "BMSync", - - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - // NOTE: Needs to be subclassed - _itemExists: function BSC__itemExists(GUID) { - return this._bms.getItemIdForGUID(GUID) >= 0; - }, - - _commandLike: function BSC_commandLike(a, b) { - // Check that neither command is null, that their actions, types, - // and parents are the same, and that they don't have the same - // GUID. - // Items with the same GUID do not qualify for 'likeness' because - // we already consider them to be the same object, and therefore - // we need to process any edits. - // The parent GUID check works because reconcile() fixes up the - // parent GUIDs as it runs, and the command list is sorted by - // depth - if (!a || !b || - a.action != b.action || - a.data.type != b.data.type || - a.data.parentGUID != b.data.parentGUID || - a.GUID == b.GUID) - return false; - - // Bookmarks are allowed to be in a different index as long as - // they are in the same folder. Folders and separators must be at - // the same index to qualify for 'likeness'. - switch (a.data.type) { - case "bookmark": - if (a.data.URI == b.data.URI && - a.data.title == b.data.title) - return true; - return false; - case "query": - if (a.data.URI == b.data.URI && - a.data.title == b.data.title) - return true; - return false; - case "microsummary": - if (a.data.URI == b.data.URI && - a.data.generatorURI == b.data.generatorURI) - return true; - return false; - case "folder": - if (a.index == b.index && - a.data.title == b.data.title) - return true; - return false; - case "livemark": - if (a.data.title == b.data.title && - a.data.siteURI == b.data.siteURI && - a.data.feedURI == b.data.feedURI) - return true; - return false; - case "separator": - if (a.index == b.index) - return true; - return false; - default: - this._log.error("commandLike: Unknown item type: " + uneval(a)); - return false; - } - } -}; -BookmarksSyncCore.prototype.__proto__ = new SyncCore(); - -/* - * Data Stores - * These can wrap, serialize items and apply commands - */ - -function Store() { - this._init(); -} -Store.prototype = { - _logName: "Store", - - _init: function Store__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - }, - - wrap: function Store_wrap() { - }, - - applyCommands: function Store_applyCommands(commandList) { - } -}; - -function SnapshotStore() { - this._init(); -} -SnapshotStore.prototype = { - _logName: "SStore", - - __dirSvc: null, - get _dirSvc() { - if (!this.__dirSvc) - this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - return this.__dirSvc; - }, - - // Last synced tree, version, and GUID (to detect if the store has - // been completely replaced and invalidate the snapshot) - - _data: {}, - get data() { return this._data; }, - set data(value) { this._data = value; }, - - _version: 0, - get version() { return this._version; }, - set version(value) { this._version = value; }, - - _GUID: null, - get GUID() { - if (!this._GUID) { - let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. - getService(Ci.nsIUUIDGenerator); - this._GUID = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); - } - return this._GUID; - }, - set GUID(GUID) { - this._GUID = GUID; - }, - - save: function SStore_save() { - this._log.info("Saving snapshot to disk"); - - let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync-snapshot.json"); - file.QueryInterface(Ci.nsILocalFile); - - if (!file.exists()) - file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); - - let fos = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; - fos.init(file, flags, PERMS_FILE, 0); - - let out = {version: this.version, - GUID: this.GUID, - snapshot: this.data}; - out = uneval(out); - fos.write(out, out.length); - fos.close(); - }, - - load: function SStore_load() { - let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync-snapshot.json"); - - if (!file.exists()) - return; - - let fis = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - fis.init(file, MODE_RDONLY, PERMS_FILE, 0); - fis.QueryInterface(Ci.nsILineInputStream); - - let json = ""; - while (fis.available()) { - let ret = {}; - fis.readLine(ret); - json += ret.value; - } - fis.close(); - json = eval(json); - - if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { - this._log.info("Read saved snapshot from disk"); - this.data = json.snapshot; - this.version = json.version; - this.GUID = json.GUID; - } - }, - - serialize: function SStore_serialize() { - let json = uneval(this.data); - json = json.replace(/:{type/g, ":\n\t{type"); - json = json.replace(/}, /g, "},\n "); - json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); - json = json.replace(/, index/g, ",\n\t index"); - json = json.replace(/, title/g, ",\n\t title"); - json = json.replace(/, URI/g, ",\n\t URI"); - json = json.replace(/, tags/g, ",\n\t tags"); - json = json.replace(/, keyword/g, ",\n\t keyword"); - return json; - }, - - wrap: function SStore_wrap() { - }, - - applyCommands: function SStore_applyCommands(commands) { - for (let i = 0; i < commands.length; i++) { - // this._log.debug("Applying cmd to obj: " + uneval(commands[i])); - switch (commands[i].action) { - case "create": - this._data[commands[i].GUID] = eval(uneval(commands[i].data)); - break; - case "edit": - if ("GUID" in commands[i].data) { - // special-case guid changes - let newGUID = commands[i].data.GUID, - oldGUID = commands[i].GUID; - - this._data[newGUID] = this._data[oldGUID]; - delete this._data[oldGUID] - - for (let GUID in this._data) { - if (this._data[GUID].parentGUID == oldGUID) - this._data[GUID].parentGUID = newGUID; - } - } - for (let prop in commands[i].data) { - if (prop == "GUID") - continue; - this._data[commands[i].GUID][prop] = commands[i].data[prop]; - } - break; - case "remove": - delete this._data[commands[i].GUID]; - break; - } - } - return this._data; - } -}; -SnapshotStore.prototype.__proto__ = new Store(); - -function BookmarksStore() { - this._init(); -} -BookmarksStore.prototype = { - _logName: "BStore", - - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - __hsvc: null, - get _hsvc() { - if (!this.__hsvc) - this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - return this.__hsvc; - }, - - __ls: null, - get _ls() { - if (!this.__ls) - this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. - getService(Ci.nsILivemarkService); - return this.__ls; - }, - - __ms: null, - get _ms() { - if (!this.__ms) - this.__ms = Cc["@mozilla.org/microsummary/service;1"]. - getService(Ci.nsIMicrosummaryService); - return this.__ms; - }, - - __ts: null, - get _ts() { - if (!this.__ts) - this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. - getService(Ci.nsITaggingService); - return this.__ts; - }, - - __ans: null, - get _ans() { - if (!this.__ans) - this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - return this.__ans; - }, - - _getFolderNodes: function BSS__getFolderNodes(folder) { - let query = this._hsvc.getNewQuery(); - query.setFolders([folder], 1); - return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; - }, - - _wrapNode: function BSS__wrapNode(node) { - var items = {}; - this._wrapNodeInternal(node, items, null, null); - return items; - }, - - _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) { - let GUID = this._bms.getItemGUID(node.itemId); - let item = {parentGUID: parentGUID, - index: index}; - - if (node.type == node.RESULT_TYPE_FOLDER) { - if (this._ls.isLivemark(node.itemId)) { - item.type = "livemark"; - let siteURI = this._ls.getSiteURI(node.itemId); - let feedURI = this._ls.getFeedURI(node.itemId); - item.siteURI = siteURI? siteURI.spec : ""; - item.feedURI = feedURI? feedURI.spec : ""; - } else { - item.type = "folder"; - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._wrapNodeInternal(node.getChild(i), items, GUID, i); - } - } - item.title = node.title; - } else if (node.type == node.RESULT_TYPE_URI || - node.type == node.RESULT_TYPE_QUERY) { - if (this._ms.hasMicrosummary(node.itemId)) { - item.type = "microsummary"; - let micsum = this._ms.getMicrosummary(node.itemId); - item.generatorURI = micsum.generator.uri.spec; // breaks local generators - } else if (node.type == node.RESULT_TYPE_QUERY) { - item.type = "query"; - item.title = node.title; - } else { - item.type = "bookmark"; - item.title = node.title; - } - item.URI = node.uri; - item.tags = this._ts.getTagsForURI(makeURI(node.uri)); - item.keyword = this._bms.getKeywordForBookmark(node.itemId); - } else if (node.type == node.RESULT_TYPE_SEPARATOR) { - item.type = "separator"; - } else { - this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); - return; - } - - items[GUID] = item; - }, - - _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { - return this._wrapNode(this._getFolderNodes(folder)); - }, - - _resetGUIDsInt: function BSS__resetGUIDsInt(node) { - if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) - this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); - - if (node.type == node.RESULT_TYPE_FOLDER && - !this._ls.isLivemark(node.itemId)) { - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._resetGUIDsInt(node.getChild(i)); - } - } - }, - - _createCommand: function BStore__createCommand(command) { - let newId; - let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); - - if (parentId < 0) { - this._log.warn("Creating node with unknown parent -> reparenting to root"); - parentId = this._bms.bookmarksMenuFolder; - } - - switch (command.data.type) { - case "query": - case "bookmark": - case "microsummary": - this._log.info(" -> creating bookmark \"" + command.data.title + "\""); - let URI = makeURI(command.data.URI); - newId = this._bms.insertBookmark(parentId, - URI, - command.data.index, - command.data.title); - this._ts.untagURI(URI, null); - this._ts.tagURI(URI, command.data.tags); - this._bms.setKeywordForBookmark(newId, command.data.keyword); - - if (command.data.type == "microsummary") { - this._log.info(" \-> is a microsummary"); - let genURI = makeURI(command.data.generatorURI); - try { - let micsum = this._ms.createMicrosummary(URI, genURI); - this._ms.setMicrosummary(newId, micsum); - } - catch(ex) { /* ignore "missing local generator" exceptions */ } - } - break; - case "folder": - this._log.info(" -> creating folder \"" + command.data.title + "\""); - newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); - break; - case "livemark": - this._log.info(" -> creating livemark \"" + command.data.title + "\""); - newId = this._ls.createLivemark(parentId, - command.data.title, - makeURI(command.data.siteURI), - makeURI(command.data.feedURI), - command.data.index); - break; - case "separator": - this._log.info(" -> creating separator"); - newId = this._bms.insertSeparator(parentId, command.data.index); - break; - default: - this._log.error("_createCommand: Unknown item type: " + command.data.type); - break; - } - if (newId) - this._bms.setItemGUID(newId, command.GUID); - }, - - _removeCommand: function BStore__removeCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId < 0) { - this._log.warn("Attempted to remove item " + command.GUID + - ", but it does not exist. Skipping."); - return; - } - var type = this._bms.getItemType(itemId); - - switch (type) { - case this._bms.TYPE_BOOKMARK: - this._log.info(" -> removing bookmark " + command.GUID); - this._bms.removeItem(itemId); - break; - case this._bms.TYPE_FOLDER: - this._log.info(" -> removing folder " + command.GUID); - this._bms.removeFolder(itemId); - break; - case this._bms.TYPE_SEPARATOR: - this._log.info(" -> removing separator " + command.GUID); - this._bms.removeItem(itemId); - break; - default: - this._log.error("removeCommand: Unknown item type: " + type); - break; - } - }, - - _editCommand: function BStore__editCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId < 0) { - this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); - return; - } - - for (let key in command.data) { - switch (key) { - case "GUID": - var existing = this._bms.getItemIdForGUID(command.data.GUID); - if (existing < 0) - this._bms.setItemGUID(itemId, command.data.GUID); - else - this._log.warn("Can't change GUID " + command.GUID + - " to " + command.data.GUID + ": GUID already exists."); - break; - case "title": - this._bms.setItemTitle(itemId, command.data.title); - break; - case "URI": - this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI)); - break; - case "index": - this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), - command.data.index); - break; - case "parentGUID": - let index = -1; - if (command.data.index && command.data.index >= 0) - index = command.data.index; - this._bms.moveItem( - itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); - break; - case "tags": - let tagsURI = this._bms.getBookmarkURI(itemId); - this._ts.untagURI(URI, null); - this._ts.tagURI(tagsURI, command.data.tags); - break; - case "keyword": - this._bms.setKeywordForBookmark(itemId, command.data.keyword); - break; - case "generatorURI": - let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); - let genURI = makeURI(command.data.generatorURI); - let micsum = this._ms.createMicrosummary(micsumURI, genURI); - this._ms.setMicrosummary(itemId, micsum); - break; - case "siteURI": - this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); - break; - case "feedURI": - this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); - break; - default: - this._log.warn("Can't change item property: " + key); - break; - } - } - }, - - wrap: function BStore_wrap() { - let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder); - let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder); - let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder); - - for (let guid in unfiled) { - if (!(guid in filed)) - filed[guid] = unfiled[guid]; - } - - for (let guid in toolbar) { - if (!(guid in filed)) - filed[guid] = toolbar[guid]; - } - - return filed; // (combined) - }, - - resetGUIDs: function BStore_resetGUIDs() { - this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); - }, - - applyCommands: function BStore_applyCommands(commandList) { - for (var i = 0; i < commandList.length; i++) { - var command = commandList[i]; - this._log.debug("Processing command: " + uneval(command)); - switch (command["action"]) { - case "create": - this._createCommand(command); - break; - case "remove": - this._removeCommand(command); - break; - case "edit": - this._editCommand(command); - break; - default: - this._log.error("unknown action in command: " + command["action"]); - break; - } - } - } -}; -BookmarksStore.prototype.__proto__ = new Store(); - -/* - * DAV object - * Abstracts the raw DAV commands - */ - -function DAVCollection(baseURL) { - this._baseURL = baseURL; - this._authProvider = new DummyAuthProvider(); - this._log = Log4Moz.Service.getLogger("Service.DAV"); -} -DAVCollection.prototype = { - __dp: null, - get _dp() { - if (!this.__dp) - this.__dp = Cc["@mozilla.org/xmlextras/domparser;1"]. - createInstance(Ci.nsIDOMParser); - return this.__dp; - }, - - _auth: null, - - get baseURL() { - return this._baseURL; - }, - set baseURL(value) { - this._baseURL = value; - }, - - _loggedIn: false, - get loggedIn() { - return this._loggedIn; - }, - - _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { - let [self, cont] = yield; - let ret; - - try { - this._log.debug("Creating " + op + " request for " + this._baseURL + path); - - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); - request = request.QueryInterface(Ci.nsIDOMEventTarget); - - request.addEventListener("load", new EventListener(cont, "load"), false); - request.addEventListener("error", new EventListener(cont, "error"), false); - request = request.QueryInterface(Ci.nsIXMLHttpRequest); - request.open(op, this._baseURL + path, true); - - - // Force cache validation - let channel = request.channel; - channel = channel.QueryInterface(Ci.nsIRequest); - let loadFlags = channel.loadFlags; - loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; - channel.loadFlags = loadFlags; - - let key; - for (key in headers) { - this._log.debug("HTTP Header " + key + ": " + headers[key]); - request.setRequestHeader(key, headers[key]); - } - - this._authProvider._authFailed = false; - request.channel.notificationCallbacks = this._authProvider; - - request.send(data); - let event = yield; - ret = event.target; - - if (this._authProvider._authFailed) - this._log.warn("_makeRequest: authentication failed"); - if (ret.status < 200 || ret.status >= 300) - this._log.warn("_makeRequest: got status " + ret.status); - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - get _defaultHeaders() { - return {'Authorization': this._auth? this._auth : '', - 'Content-type': 'text/plain', - 'If': this._token? - "<" + this._baseURL + "> (<" + this._token + ">)" : ''}; - }, - - GET: function DC_GET(path, onComplete) { - return this._makeRequest.async(this, onComplete, "GET", path, - this._defaultHeaders); - }, - - PUT: function DC_PUT(path, data, onComplete) { - return this._makeRequest.async(this, onComplete, "PUT", path, - this._defaultHeaders, data); - }, - - DELETE: function DC_DELETE(path, onComplete) { - return this._makeRequest.async(this, onComplete, "DELETE", path, - this._defaultHeaders); - }, - - PROPFIND: function DC_PROPFIND(path, data, onComplete) { - let headers = {'Content-type': 'text/xml; charset="utf-8"', - 'Depth': '0'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "PROPFIND", path, - headers, data); - }, - - LOCK: function DC_LOCK(path, data, onComplete) { - let headers = {'Content-type': 'text/xml; charset="utf-8"', - 'Depth': 'infinity', - 'Timeout': 'Second-600'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "LOCK", path, headers, data); - }, - - UNLOCK: function DC_UNLOCK(path, onComplete) { - let headers = {'Lock-Token': '<' + this._token + '>'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers); - }, - - // Login / Logout - - login: function DC_login(onComplete, username, password) { - let [self, cont] = yield; - - try { - if (this._loggedIn) { - this._log.debug("Login requested, but already logged in"); - return; - } - - this._log.info("Logging in"); - - let URI = makeURI(this._baseURL); - this._auth = "Basic " + btoa(username + ":" + password); - - // Make a call to make sure it's working - this.GET("", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - this._loggedIn = true; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (this._loggedIn) - this._log.info("Logged in"); - else - this._log.warn("Could not log in"); - generatorDone(this, self, onComplete, this._loggedIn); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - logout: function DC_logout() { - this._log.debug("Logging out (forgetting auth header)"); - this._loggedIn = false; - this.__auth = null; - }, - - // Locking - - _getActiveLock: function DC__getActiveLock(onComplete) { - let [self, cont] = yield; - let ret = null; - - try { - this._log.info("Getting active lock token"); - this.PROPFIND("", - "" + - "" + - " " + - "", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - ret = token.textContent; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (ret) - this._log.debug("Found an active lock token"); - else - this._log.debug("No active lock token found"); - generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - lock: function DC_lock(onComplete) { - let [self, cont] = yield; - this._token = null; - - try { - this._log.info("Acquiring lock"); - - if (this._token) { - this._log.debug("Lock called, but we already hold a token"); - return; - } - - this.LOCK("", - "\n" + - "\n" + - " \n" + - " \n" + - "", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - if (token) - this._token = token.textContent; - - } catch (e){ - this._log.error("Exception caught: " + e.message); - - } finally { - if (this._token) - this._log.info("Lock acquired"); - else - this._log.warn("Could not acquire lock"); - generatorDone(this, self, onComplete, this._token); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - unlock: function DC_unlock(onComplete) { - let [self, cont] = yield; - try { - this._log.info("Releasing lock"); - - if (this._token === null) { - this._log.debug("Unlock called, but we don't hold a token right now"); - return; - } - - this.UNLOCK("", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - this._token = null; - - } catch (e){ - this._log.error("Exception caught: " + e.message); - - } finally { - if (this._token) { - this._log.info("Could not release lock"); - generatorDone(this, self, onComplete, false); - } else { - this._log.info("Lock released (or we didn't have one)"); - generatorDone(this, self, onComplete, true); - } - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - forceUnlock: function DC_forceUnlock(onComplete) { - let [self, cont] = yield; - let unlocked = true; - - try { - this._log.info("Forcibly releasing any server locks"); - - this._getActiveLock.async(this, cont); - this._token = yield; - - if (!this._token) { - this._log.info("No server lock found"); - return; - } - - this._log.info("Server lock found, unlocking"); - this.unlock.async(this, cont); - unlocked = yield; - - } catch (e){ - this._log.error("Exception caught: " + e.message); - - } finally { - if (unlocked) - this._log.debug("Lock released"); - else - this._log.debug("No lock released"); - generatorDone(this, self, onComplete, unlocked); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - stealLock: function DC_stealLock(onComplete) { - let [self, cont] = yield; - let stolen = null; - - try { - this.forceUnlock.async(this, cont); - let unlocked = yield; - - if (unlocked) { - this.lock.async(this, cont); - stolen = yield; - } - - } catch (e){ - this._log.error("Exception caught: " + e.message); - - } finally { - generatorDone(this, self, onComplete, stolen); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - } -}; - - -/* - * Auth provider object - * Taken from nsMicrosummaryService.js and massaged slightly - */ - -function DummyAuthProvider() {} -DummyAuthProvider.prototype = { - // Implement notification callback interfaces so we can suppress UI - // and abort loads for bad SSL certs and HTTP authorization requests. - - // Interfaces this component implements. - interfaces: [Ci.nsIBadCertListener, - Ci.nsIAuthPromptProvider, - Ci.nsIAuthPrompt, - Ci.nsIPrompt, - Ci.nsIProgressEventSink, - Ci.nsIInterfaceRequestor, - Ci.nsISupports], - - // Auth requests appear to succeed when we cancel them (since the server - // redirects us to a "you're not authorized" page), so we have to set a flag - // to let the load handler know to treat the load as a failure. - get _authFailed() { return this.__authFailed; }, - set _authFailed(newValue) { return this.__authFailed = newValue }, - - // nsISupports - - QueryInterface: function DAP_QueryInterface(iid) { - if (!this.interfaces.some( function(v) { return iid.equals(v) } )) - throw Cr.NS_ERROR_NO_INTERFACE; - - // nsIAuthPrompt and nsIPrompt need separate implementations because - // their method signatures conflict. The other interfaces we implement - // within DummyAuthProvider itself. - switch(iid) { - case Ci.nsIAuthPrompt: - return this.authPrompt; - case Ci.nsIPrompt: - return this.prompt; - default: - return this; - } - }, - - // nsIInterfaceRequestor - - getInterface: function DAP_getInterface(iid) { - return this.QueryInterface(iid); - }, - - // nsIBadCertListener - - // Suppress UI and abort secure loads from servers with bad SSL certificates. - - confirmUnknownIssuer: function DAP_confirmUnknownIssuer(socketInfo, cert, certAddType) { - return false; - }, - - confirmMismatchDomain: function DAP_confirmMismatchDomain(socketInfo, targetURL, cert) { - return false; - }, - - confirmCertExpired: function DAP_confirmCertExpired(socketInfo, cert) { - return false; - }, - - notifyCrlNextupdate: function DAP_notifyCrlNextupdate(socketInfo, targetURL, cert) { - }, - - // nsIAuthPromptProvider - - getAuthPrompt: function(aPromptReason, aIID) { - this._authFailed = true; - throw Cr.NS_ERROR_NOT_AVAILABLE; - }, - - // HTTP always requests nsIAuthPromptProvider first, so it never needs - // nsIAuthPrompt, but not all channels use nsIAuthPromptProvider, so we - // implement nsIAuthPrompt too. - - // nsIAuthPrompt - - get authPrompt() { - var resource = this; - return { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), - prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) { - resource._authFailed = true; - return false; - }, - promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) { - resource._authFailed = true; - return false; - }, - promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) { - resource._authFailed = true; - return false; - } - }; - }, - - // nsIPrompt - - get prompt() { - var resource = this; - return { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), - alert: function(dialogTitle, text) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - alertCheck: function(dialogTitle, text, checkMessage, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirm: function(dialogTitle, text) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirmCheck: function(dialogTitle, text, checkMessage, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirmEx: function(dialogTitle, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - prompt: function(dialogTitle, text, value, checkMsg, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - promptPassword: function(dialogTitle, text, password, checkMsg, checkValue) { - resource._authFailed = true; - return false; - }, - promptUsernameAndPassword: function(dialogTitle, text, username, password, checkMsg, checkValue) { - resource._authFailed = true; - return false; - }, - select: function(dialogTitle, text, count, selectList, outSelection) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - } - }; - }, - - // nsIProgressEventSink - - onProgress: function DAP_onProgress(aRequest, aContext, - aProgress, aProgressMax) { - }, - - onStatus: function DAP_onStatus(aRequest, aContext, - aStatus, aStatusArg) { - } -}; - -/* - * Event listener object - * Used to handle XMLHttpRequest and nsITimer callbacks - */ - -function EventListener(handler, eventName) { - this._handler = handler; - this._eventName = eventName; - this._log = Log4Moz.Service.getLogger("Service.EventHandler"); -} -EventListener.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), - - // DOM event listener - handleEvent: function EL_handleEvent(event) { - this._log.debug("Handling event " + this._eventName); - this._handler(event); - }, - - // nsITimerCallback - notify: function EL_notify(timer) { - this._log.trace("Timer fired"); - this._handler(timer); - } -}; - -/* - * Utility functions - */ - -function deepEquals(a, b) { - if (!a && !b) - return true; - if (!a || !b) - return false; - - if (typeof(a) != "object" && typeof(b) != "object") - return a == b; - if (typeof(a) != "object" || typeof(b) != "object") - return false; - - for (let key in a) { - if (typeof(a[key]) == "object") { - if (!typeof(b[key]) == "object") - return false; - if (!deepEquals(a[key], b[key])) - return false; - } else { - if (a[key] != b[key]) - return false; - } - } - return true; -} - -function makeFile(path) { - var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - file.initWithPath(path); - return file; -} - -function makeURI(URIString) { - if (URIString === null || URIString == "") - return null; - let ioservice = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - return ioservice.newURI(URIString, null, null); -} - -function xpath(xmlDoc, xpathString) { - let root = xmlDoc.ownerDocument == null ? - xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement - let nsResolver = xmlDoc.createNSResolver(root); - - return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, - Ci.nsIDOMXPathResult.ANY_TYPE, null); -} - -function bind2(object, method) { - return function innerBind() { return method.apply(object, arguments); } -} - -Function.prototype.async = function(self, extra_args) { - try { - let args = Array.prototype.slice.call(arguments, 1); - let gen = this.apply(self, args); - gen.next(); // must initialize before sending - gen.send([gen, function(data) {continueGenerator(gen, data);}]); - return gen; - } catch (e) { - if (e instanceof StopIteration) { - dump("async warning: generator stopped unexpectedly"); - return null; - } else { - dump("Exception caught: " + e.message); - } +//Weave.Crypto = new WeaveCrypto(); +Weave.Service = new WeaveSyncService(); + +function addModuleAlias(alias, extensionId) { + let ioSvc = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioSvc.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + if (!resProt.hasSubstitution(alias)) { + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); + let loc = extMgr.getInstallLocation(extensionId); + let extD = loc.getItemLocation(extensionId); + extD.append("modules"); + resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); } } - -function continueGenerator(generator, data) { - try { generator.send(data); } - catch (e) { - if (e instanceof StopIteration) - dump("continueGenerator warning: generator stopped unexpectedly"); - else - dump("Exception caught: " + e.message); - } -} - -// generators created using Function.async can't simply call the -// callback with the return value, since that would cause the calling -// function to end up running (after the yield) from inside the -// generator. Instead, generators can call this method which sets up -// a timer to call the callback from a timer (and cleans up the timer -// to avoid leaks). It also closes generators after the timeout, to -// keep things clean. -function generatorDone(object, generator, callback, retval) { - if (object._timer) - throw "Called generatorDone when there is a timer already set." - - let cb = bind2(object, function(event) { - generator.close(); - generator = null; - object._timer = null; - if (callback) - callback(retval); - }); - - object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - object._timer.initWithCallback(new EventListener(cb), - 0, object._timer.TYPE_ONE_SHOT); -} From 59d63054276b251d993f134999a9650487eec8d4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 11 Dec 2007 11:57:13 -0800 Subject: [PATCH 0095/1860] assume in loaded modules that the resource:// alias has already been added - remove all the templatey code to do that --- services/sync/modules/crypto.js | 17 ----------------- services/sync/modules/dav.js | 17 ----------------- services/sync/modules/engines.js | 17 ----------------- services/sync/modules/identity.js | 17 ----------------- services/sync/modules/service.js | 18 ------------------ services/sync/modules/util.js | 17 ----------------- services/sync/modules/weave.js | 17 ----------------- 7 files changed, 120 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 06f416bc53d6..782a1510c9f1 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -178,19 +177,3 @@ WeaveCrypto.prototype = { return out; } }; - -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } -} diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 8f446ca51f50..1f4ac4d42457 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); @@ -541,19 +540,3 @@ DummyAuthProvider.prototype = { aStatus, aStatusArg) { } }; - -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } -} diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5a0f63c25046..95b63756bd5b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -679,19 +678,3 @@ serializeConflicts: function serializeConflicts(conflicts) { json = json.replace(/ {action/g, "\n {action"); return json; } - -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } -} diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 0230e3daab78..e74afddb0cd7 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -115,19 +114,3 @@ function setPassword(realm, username, password) { username, password, null, null); lm.addLogin(login); } - -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } -} diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a9daaefb280b..20e1a2cf82d6 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -429,20 +428,3 @@ WeaveSyncService.prototype = { this._bmkEngine.resetClient(function() {self._unlock()}); } }; - - -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } -} diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 64b2714a21b3..4036ef30bbf5 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -42,7 +42,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/log4moz.js"); @@ -192,19 +191,3 @@ EventListener.prototype = { this._handler(timer); } }; - -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } -} diff --git a/services/sync/modules/weave.js b/services/sync/modules/weave.js index 841c03b03b4f..b55243e7a33e 100644 --- a/services/sync/modules/weave.js +++ b/services/sync/modules/weave.js @@ -41,25 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); Cu.import("resource://weave/service.js"); let Weave = {}; //Weave.Crypto = new WeaveCrypto(); Weave.Service = new WeaveSyncService(); - -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } -} From a3b6ed86d990ff3b2218f0d42dd72dfd1cede608 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 14 Dec 2007 18:07:25 -0800 Subject: [PATCH 0096/1860] some more cleanup/refactoring; add history engine/core/store (history sync\!) --- services/sync/modules/constants.js | 8 +- services/sync/modules/crypto.js | 3 +- services/sync/modules/dav.js | 72 +++- services/sync/modules/engines.js | 640 ++++++++++++++++------------- services/sync/modules/identity.js | 1 + services/sync/modules/log4moz.js | 92 ++--- services/sync/modules/service.js | 132 ++++-- services/sync/modules/stores.js | 128 ++++-- services/sync/modules/syncCores.js | 38 +- services/sync/modules/util.js | 3 +- services/sync/modules/weave.js | 1 + 11 files changed, 673 insertions(+), 445 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 435c130f6cb5..e6c5b0f3d04f 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -34,18 +34,12 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Cc', 'Ci', 'Cr', 'Cu', - 'MODE_RDONLY', 'MODE_WRONLY', +const EXPORTED_SYMBOLS = ['MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', 'PERMS_FILE', 'PERMS_DIRECTORY', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - const MODE_RDONLY = 0x01; const MODE_WRONLY = 0x02; const MODE_CREATE = 0x08; diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 782a1510c9f1..20bb59fb8975 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -41,12 +41,11 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - function WeaveCrypto() { this._init(); } diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 1f4ac4d42457..72b6c0452b19 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -41,11 +41,10 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - Function.prototype.async = generatorAsync; /* @@ -123,7 +122,7 @@ DAVCollection.prototype = { this._log.warn("_makeRequest: got status " + ret.status); } catch (e) { - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { generatorDone(this, self, onComplete, ret); @@ -136,7 +135,56 @@ DAVCollection.prototype = { return {'Authorization': this._auth? this._auth : '', 'Content-type': 'text/plain', 'If': this._token? - "<" + this._baseURL + "> (<" + this._token + ">)" : ''}; + "<" + this._baseURL + "> (<" + this._token + ">)" : ''}; + }, + + // mkdir -p + _mkcol: function DC__mkcol(path, onComplete) { + let [self, cont] = yield; + let ret; + + try { + let components = path.split('/'); + let path2 = ''; + + for (let i = 0; i < components.length; i++) { + + // trailing slashes will cause an empty path component at the end + if (components[i] == '') + break; + + path2 = path2 + components[i]; + + // check if it exists first + this._makeRequest.async(this, cont, "GET", path2 + "/", this._defaultHeaders); + ret = yield; + if (!(ret.status == 404 || ret.status == 500)) { // FIXME: 500 is a services.m.c oddity + this._log.debug("Skipping creation of path " + path2 + + " (got status " + ret.status + ")"); + } else { + this._log.debug("Creating path: " + path2); + let gen = this._makeRequest.async(this, cont, "MKCOL", path2, + this._defaultHeaders); + ret = yield; + + if (ret.status != 201) { + this._log.debug(ret.responseText); + throw 'request failed: ' + ret.status; + } + } + + // add slash *after* the request, trailing slashes cause a 412! + path2 = path2 + "/"; + } + + } catch (e) { + this._log.error("Exception caught: " + (e.message? e.message : e)); + + } finally { + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator + } + this._log.warn("generator not properly closed"); }, GET: function DC_GET(path, onComplete) { @@ -154,6 +202,10 @@ DAVCollection.prototype = { this._defaultHeaders); }, + MKCOL: function DC_MKCOL(path, onComplete) { + return this._mkcol.async(this, path, onComplete); + }, + PROPFIND: function DC_PROPFIND(path, data, onComplete) { let headers = {'Content-type': 'text/xml; charset="utf-8"', 'Depth': '0'}; @@ -202,7 +254,7 @@ DAVCollection.prototype = { this._loggedIn = true; } catch (e) { - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { if (this._loggedIn) @@ -244,7 +296,7 @@ DAVCollection.prototype = { ret = token.textContent; } catch (e) { - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { if (ret) @@ -286,7 +338,7 @@ DAVCollection.prototype = { this._token = token.textContent; } catch (e){ - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { if (this._token) @@ -318,7 +370,7 @@ DAVCollection.prototype = { this._token = null; } catch (e){ - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { if (this._token) { @@ -353,7 +405,7 @@ DAVCollection.prototype = { unlocked = yield; } catch (e){ - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { if (unlocked) @@ -380,7 +432,7 @@ DAVCollection.prototype = { } } catch (e){ - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { generatorDone(this, self, onComplete, stolen); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 95b63756bd5b..86bc1dd14e1e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -34,13 +34,14 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['BookmarksEngine']; +const EXPORTED_SYMBOLS = ['Engine', 'BookmarksEngine', 'HistoryEngine']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -51,11 +52,24 @@ Cu.import("resource://weave/syncCores.js"); Function.prototype.async = generatorAsync; let Crypto = new WeaveCrypto(); -function BookmarksEngine(davCollection, cryptoId) { - this._init(davCollection, cryptoId); +function Engine(davCollection, cryptoId) { + //this._init(davCollection, cryptoId); } -BookmarksEngine.prototype = { - _logName: "BmkEngine", +Engine.prototype = { + // "default-engine"; + get name() { throw "name property must be overridden in subclasses"; }, + + // "DefaultEngine"; + get logName() { throw "logName property must be overridden in subclasses"; }, + + // "user-data/default-engine/"; + get serverPrefix() { throw "serverPrefix property must be overridden in subclasses"; }, + + // These can be overridden in subclasses, but don't need to be (assuming + // serverPrefix is not shared with anything else) + get statusFile() { return this.serverPrefix + "status.json"; }, + get snapshotFile() { return this.serverPrefix + "snapshot.json"; }, + get deltasFile() { return this.serverPrefix + "deltas.json"; }, __os: null, get _os() { @@ -65,24 +79,25 @@ BookmarksEngine.prototype = { return this.__os; }, - __store: null, - get _store() { - if (!this.__store) - this.__store = new BookmarksStore(); - return this.__store; - }, - + // _core, and _store need to be overridden in subclasses __core: null, get _core() { if (!this.__core) - this.__core = new BookmarksSyncCore(); + this.__core = new SyncCore(); return this.__core; }, + __store: null, + get _store() { + if (!this.__store) + this.__store = new Store(); + return this.__store; + }, + __snapshot: null, get _snapshot() { if (!this.__snapshot) - this.__snapshot = new SnapshotStore(); + this.__snapshot = new SnapshotStore(this.name); return this.__snapshot; }, set _snapshot(value) { @@ -92,235 +107,98 @@ BookmarksEngine.prototype = { _init: function BmkEngine__init(davCollection, cryptoId) { this._dav = davCollection; this._cryptoId = cryptoId; - this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log = Log4Moz.Service.getLogger("Service." + this.logName); + this._osPrefix = "weave:" + this.name + ":"; this._snapshot.load(); }, - _checkStatus: function BmkEngine__checkStatus(code, msg) { + _checkStatus: function BmkEngine__checkStatus(code, msg, ok404) { if (code >= 200 && code < 300) return; + if (ok404 && code == 404) + return; this._log.error(msg + " Error code: " + code); throw 'checkStatus failed'; }, - /* Get the deltas/combined updates from the server - * Returns: - * status: - * -1: error - * 0: ok - * These fields may be null when status is -1: - * formatVersion: - * version of the data format itself. For compatibility checks. - * maxVersion: - * the latest version on the server - * snapVersion: - * the version of the current snapshot on the server (deltas not applied) - * snapEncryption: - * encryption algorithm currently used on the server-stored snapshot - * deltasEncryption: - * encryption algorithm currently used on the server-stored deltas - * snapshot: - * full snapshot of the latest server version (deltas applied) - * deltas: - * all of the individual deltas on the server - * updates: - * the relevant deltas (from our snapshot version to current), - * combined into a single set. - */ - _getServerData: function BmkEngine__getServerData(onComplete) { + _resetServer: function Engine__resetServer(onComplete) { let [self, cont] = yield; - let ret = {status: -1, - formatVersion: null, maxVersion: null, snapVersion: null, - snapEncryption: null, deltasEncryption: null, - snapshot: null, deltas: null, updates: null}; + let done = false; try { - this._log.info("Getting bookmarks status from server"); - this._dav.GET("bookmarks-status.json", cont); - let resp = yield; - let status = resp.status; - - switch (status) { - case 200: - this._log.info("Got bookmarks status from server"); - - let status = eval(resp.responseText); - let deltas, allDeltas; - let snap = new SnapshotStore(); - - // Bail out if the server has a newer format version than we can parse - if (status.formatVersion > STORAGE_FORMAT_VERSION) { - this._log.error("Server uses storage format v" + status.formatVersion + - ", this client understands up to v" + STORAGE_FORMAT_VERSION); - generatorDone(this, self, onComplete, ret) - return; - } + this._log.debug("Resetting server data"); + this._os.notifyObservers(null, this._osPrefix + "reset-server:start", ""); - if (status.formatVersion == 0) { - ret.snapEncryption = status.snapEncryption = "none"; - ret.deltasEncryption = status.deltasEncryption = "none"; - } - - if (status.GUID != this._snapshot.GUID) { - this._log.info("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._store.resetGUIDs(); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.GUID = status.GUID; - } - - if (this._snapshot.version < status.snapVersion) { - if (this._snapshot.version >= 0) - this._log.info("Local snapshot is out of date"); - - this._log.info("Downloading server snapshot"); - this._dav.GET("bookmarks-snapshot.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download snapshot."); - snap.data = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.snapEncryption); - - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); - deltas = eval(uneval(allDeltas)); - - } else if (this._snapshot.version >= status.snapVersion && - this._snapshot.version < status.maxVersion) { - snap.data = eval(uneval(this._snapshot.data)); - - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); - deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); - - } else if (this._snapshot.version == status.maxVersion) { - snap.data = eval(uneval(this._snapshot.data)); - - // FIXME: could optimize this case by caching deltas file - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); - deltas = []; - - } else { // this._snapshot.version > status.maxVersion - this._log.error("Server snapshot is older than local snapshot"); - return; - } - - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands(deltas[i]); - } - - ret.status = 0; - ret.formatVersion = status.formatVersion; - ret.maxVersion = status.maxVersion; - ret.snapVersion = status.snapVersion; - ret.snapEncryption = status.snapEncryption; - ret.deltasEncryption = status.deltasEncryption; - ret.snapshot = snap.data; - ret.deltas = allDeltas; - this._core.detectUpdates(cont, this._snapshot.data, snap.data); - ret.updates = yield; - break; - - case 404: - this._log.info("Server has no status file, Initial upload to server"); - - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - - this._fullUpload.async(this, cont); - let uploadStatus = yield; - if (!uploadStatus) - return; - - this._log.info("Initial upload to server successful"); - this.snapshot.save(); - - ret.status = 0; - ret.formatVersion = STORAGE_FORMAT_VERSION; - ret.maxVersion = this._snapshot.version; - ret.snapVersion = this._snapshot.version; - ret.snapEncryption = Crypto.defaultAlgorithm; - ret.deltasEncryption = Crypto.defaultAlgorithm; - ret.snapshot = eval(uneval(this._snapshot.data)); - ret.deltas = []; - ret.updates = []; - break; - - default: - this._log.error("Could not get bookmarks.status: unknown HTTP status code " + - status); - break; + this._dav.lock.async(this._dav, cont); + let locked = yield; + if (locked) + this._log.debug("Lock acquired"); + else { + this._log.warn("Could not acquire lock, aborting server reset"); + return; } + // try to delete all 3, check status after + this._dav.DELETE(this.statusFile, cont); + let statusResp = yield; + this._dav.DELETE(this.snapshotFile, cont); + let snapshotResp = yield; + this._dav.DELETE(this.deltasFile, cont); + let deltasResp = yield; + + this._dav.unlock.async(this._dav, cont); + let unlocked = yield; + + checkStatus(statusResp.status, "Could not delete status file.", true); + checkStatus(snapshotResp.status, "Could not delete snapshot file.", true); + checkStatus(deltasResp.status, "Could not delete deltas file.", true); + + this._log.debug("Server files deleted"); + done = true; + } catch (e) { - if (e != 'checkStatus failed' && - e != 'decrypt failed') - this._log.error("Exception caught: " + e.message); + if (e != 'checkStatus failed') + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self, onComplete, ret) + if (done) { + this._log.debug("Server reset completed successfully"); + this._os.notifyObservers(null, this._osPrefix + "reset-server:end", ""); + } else { + this._log.debug("Server reset failed"); + this._os.notifyObservers(null, this._osPrefix + "reset-server:error", ""); + } + generatorDone(this, self, onComplete, done) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, - _fullUpload: function BmkEngine__fullUpload(onComplete) { + _resetClient: function Engine__resetClient(onComplete) { let [self, cont] = yield; - let ret = false; + let done = false; try { - let data = Crypto.PBEencrypt(this._snapshot.serialize(), - this._cryptoId); - this._dav.PUT("bookmarks-snapshot.json", data, cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload snapshot."); + this._log.debug("Resetting client state"); + this._os.notifyObservers(null, this._osPrefix + "reset-client:start", ""); - this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload deltas."); - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshot.version, - maxVersion: this._snapshot.version, - snapEncryption: Crypto.defaultAlgorithm, - deltasEncryption: "none", - bookmarksCount: c}), cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload status file."); - - this._log.info("Full upload to server successful"); - ret = true; + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.save(); + done = true; } catch (e) { - if (e != 'checkStatus failed') - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self, onComplete, ret) + if (done) { + this._log.debug("Client reset completed successfully"); + this._os.notifyObservers(null, this._osPrefix + "reset-client:end", ""); + } else { + this._log.debug("Client reset failed"); + this._os.notifyObservers(null, this._osPrefix + "reset-client:error", ""); + } + generatorDone(this, self, onComplete, done); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -360,7 +238,7 @@ BookmarksEngine.prototype = { try { this._log.info("Beginning sync"); - this._os.notifyObservers(null, "bookmarks-sync:sync-start", ""); + this._os.notifyObservers(null, this._osPrefix + "sync:start", ""); this._dav.lock.async(this._dav, cont); locked = yield; @@ -372,6 +250,11 @@ BookmarksEngine.prototype = { return; } + // Before we get started, make sure we have a remote directory to play in + this._dav.MKCOL(this.serverPrefix, cont); + let ret = yield; + this._checkStatus(ret.status, "Could not create remote folder."); + // 1) Fetch server deltas this._getServerData.async(this, cont); let server = yield; @@ -409,7 +292,7 @@ BookmarksEngine.prototype = { this._log.info("Reconciling client/server updates"); this._core.reconcile(cont, localUpdates, server.updates); - let ret = yield; + ret = yield; let clientChanges = ret.propagations[0]; let serverChanges = ret.propagations[1]; @@ -466,7 +349,6 @@ BookmarksEngine.prototype = { this._snapshot.data = eval(uneval(savedSnap)); this._snapshot.version = savedVersion; } - this._snapshot.save(); } @@ -508,21 +390,21 @@ BookmarksEngine.prototype = { } else { let data = Crypto.PBEencrypt(serializeCommands(server.deltas), this._cryptoId); - this._dav.PUT("bookmarks-deltas.json", data, cont); + this._dav.PUT(this.deltasFile, data, cont); let deltasPut = yield; let c = 0; for (GUID in this._snapshot.data) c++; - this._dav.PUT("bookmarks-status.json", + this._dav.PUT(this.statusFile, uneval({GUID: this._snapshot.GUID, formatVersion: STORAGE_FORMAT_VERSION, snapVersion: server.snapVersion, maxVersion: this._snapshot.version, snapEncryption: server.snapEncryption, deltasEncryption: Crypto.defaultAlgorithm, - Bookmarkscount: c}), cont); + itemCount: c}), cont); let statusPut = yield; if (deltasPut.status >= 200 && deltasPut.status < 300 && @@ -541,7 +423,7 @@ BookmarksEngine.prototype = { synced = true; } catch (e) { - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { let ok = false; @@ -550,10 +432,10 @@ BookmarksEngine.prototype = { ok = yield; } if (ok && synced) { - this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); + this._os.notifyObservers(null, this._osPrefix + "sync:end", ""); generatorDone(this, self, onComplete, true); } else { - this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); + this._os.notifyObservers(null, this._osPrefix + "sync:error", ""); generatorDone(this, self, onComplete, false); } yield; // onComplete is responsible for closing the generator @@ -561,112 +443,290 @@ BookmarksEngine.prototype = { this._log.warn("generator not properly closed"); }, - _resetServer: function BmkEngine__resetServer(onComplete) { + /* Get the deltas/combined updates from the server + * Returns: + * status: + * -1: error + * 0: ok + * These fields may be null when status is -1: + * formatVersion: + * version of the data format itself. For compatibility checks. + * maxVersion: + * the latest version on the server + * snapVersion: + * the version of the current snapshot on the server (deltas not applied) + * snapEncryption: + * encryption algorithm currently used on the server-stored snapshot + * deltasEncryption: + * encryption algorithm currently used on the server-stored deltas + * snapshot: + * full snapshot of the latest server version (deltas applied) + * deltas: + * all of the individual deltas on the server + * updates: + * the relevant deltas (from our snapshot version to current), + * combined into a single set. + */ + _getServerData: function BmkEngine__getServerData(onComplete) { let [self, cont] = yield; - let done = false; + let ret = {status: -1, + formatVersion: null, maxVersion: null, snapVersion: null, + snapEncryption: null, deltasEncryption: null, + snapshot: null, deltas: null, updates: null}; try { - this._log.debug("Resetting server data"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", ""); + this._log.debug("Getting status file from server"); + this._dav.GET(this.statusFile, cont); + let resp = yield; + let status = resp.status; + + switch (status) { + case 200: + this._log.info("Got status file from server"); + + let status = eval(resp.responseText); + let deltas, allDeltas; + let snap = new SnapshotStore(); + + // Bail out if the server has a newer format version than we can parse + if (status.formatVersion > STORAGE_FORMAT_VERSION) { + this._log.error("Server uses storage format v" + status.formatVersion + + ", this client understands up to v" + STORAGE_FORMAT_VERSION); + generatorDone(this, self, onComplete, ret) + return; + } - this._dav.lock.async(this._dav, cont); - let locked = yield; - if (locked) - this._log.debug("Lock acquired"); - else { - this._log.warn("Could not acquire lock, aborting server reset"); - return; + if (status.formatVersion == 0) { + ret.snapEncryption = status.snapEncryption = "none"; + ret.deltasEncryption = status.deltasEncryption = "none"; + } + + if (status.GUID != this._snapshot.GUID) { + this._log.info("Remote/local sync GUIDs do not match. " + + "Forcing initial sync."); + this._store.resetGUIDs(); + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.GUID = status.GUID; + } + + if (this._snapshot.version < status.snapVersion) { + if (this._snapshot.version >= 0) + this._log.info("Local snapshot is out of date"); + + this._log.info("Downloading server snapshot"); + this._dav.GET(this.snapshotFile, cont); + resp = yield; + this._checkStatus(resp.status, "Could not download snapshot."); + snap.data = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.snapEncryption); + + this._log.info("Downloading server deltas"); + this._dav.GET(this.deltasFile, cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = eval(uneval(allDeltas)); + + } else if (this._snapshot.version >= status.snapVersion && + this._snapshot.version < status.maxVersion) { + snap.data = eval(uneval(this._snapshot.data)); + + this._log.info("Downloading server deltas"); + this._dav.GET(this.deltasFile, cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); + + } else if (this._snapshot.version == status.maxVersion) { + snap.data = eval(uneval(this._snapshot.data)); + + // FIXME: could optimize this case by caching deltas file + this._log.info("Downloading server deltas"); + this._dav.GET(this.deltasFile, cont); + resp = yield; + this._checkStatus(resp.status, "Could not download deltas."); + allDeltas = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + deltas = []; + + } else { // this._snapshot.version > status.maxVersion + this._log.error("Server snapshot is older than local snapshot"); + return; + } + + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands(deltas[i]); + } + + ret.status = 0; + ret.formatVersion = status.formatVersion; + ret.maxVersion = status.maxVersion; + ret.snapVersion = status.snapVersion; + ret.snapEncryption = status.snapEncryption; + ret.deltasEncryption = status.deltasEncryption; + ret.snapshot = snap.data; + ret.deltas = allDeltas; + this._core.detectUpdates(cont, this._snapshot.data, snap.data); + ret.updates = yield; + break; + + case 404: + this._log.info("Server has no status file, Initial upload to server"); + + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + + this._fullUpload.async(this, cont); + let uploadStatus = yield; + if (!uploadStatus) + return; + + this._log.info("Initial upload to server successful"); + this._snapshot.save(); + + ret.status = 0; + ret.formatVersion = STORAGE_FORMAT_VERSION; + ret.maxVersion = this._snapshot.version; + ret.snapVersion = this._snapshot.version; + ret.snapEncryption = Crypto.defaultAlgorithm; + ret.deltasEncryption = Crypto.defaultAlgorithm; + ret.snapshot = eval(uneval(this._snapshot.data)); + ret.deltas = []; + ret.updates = []; + break; + + default: + this._log.error("Could not get status file: unknown HTTP status code " + + status); + break; } - this._dav.DELETE("bookmarks-status.json", cont); - let statusResp = yield; - this._dav.DELETE("bookmarks-snapshot.json", cont); - let snapshotResp = yield; - this._dav.DELETE("bookmarks-deltas.json", cont); - let deltasResp = yield; - - this._dav.unlock.async(this._dav, cont); - let unlocked = yield; - - function ok(code) { - if (code >= 200 && code < 300) - return true; - if (code == 404) - return true; - return false; - } - - if (!(ok(statusResp.status) && ok(snapshotResp.status) && - ok(deltasResp.status))) { - this._log.error("Could delete server data, response codes " + - statusResp.status + ", " + snapshotResp.status + ", " + - deltasResp.status); - return; - } - - this._log.debug("Server files deleted"); - done = true; - } catch (e) { - this._log.error("Exception caught: " + e.message); + if (e != 'checkStatus failed' && + e != 'decrypt failed') + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - if (done) { - this._log.debug("Server reset completed successfully"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); - } else { - this._log.debug("Server reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", ""); - } - generatorDone(this, self, onComplete, done) + generatorDone(this, self, onComplete, ret) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, - _resetClient: function BmkEngine__resetClient(onComplete) { + _fullUpload: function Engine__fullUpload(onComplete) { let [self, cont] = yield; - let done = false; + let ret = false; try { - this._log.debug("Resetting client state"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-start", ""); + let data = Crypto.PBEencrypt(this._snapshot.serialize(), + this._cryptoId); + this._dav.PUT(this.snapshotFile, data, cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload snapshot."); - this._snapshot.data = {}; - this._snapshot.version = -1; - this.snapshot.save(); - done = true; + this._dav.PUT(this.deltasFile, uneval([]), cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload deltas."); + + let c = 0; + for (GUID in this._snapshot.data) + c++; + + this._dav.PUT(this.statusFile, + uneval({GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshot.version, + maxVersion: this._snapshot.version, + snapEncryption: Crypto.defaultAlgorithm, + deltasEncryption: "none", + itemCount: c}), cont); + resp = yield; + this._checkStatus(resp.status, "Could not upload status file."); + + this._log.info("Full upload to server successful"); + ret = true; } catch (e) { - this._log.error("Exception caught: " + e.message); + if (e != 'checkStatus failed') + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - if (done) { - this._log.debug("Client reset completed successfully"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-end", ""); - } else { - this._log.debug("Client reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-error", ""); - } - generatorDone(this, self, onComplete, done); + generatorDone(this, self, onComplete, ret) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); }, - sync: function BmkEngine_sync(onComplete) { + sync: function Engine_sync(onComplete) { return this._sync.async(this, onComplete); }, - resetServer: function BmkEngine_resetServer(onComplete) { + resetServer: function Engine_resetServer(onComplete) { return this._resetServer.async(this, onComplete); }, - resetClient: function BmkEngine_resetClient(onComplete) { + resetClient: function Engine_resetClient(onComplete) { return this._resetClient.async(this, onComplete); } }; +function BookmarksEngine(davCollection, cryptoId) { + this._init(davCollection, cryptoId); +} +BookmarksEngine.prototype = { + get name() { return "bookmarks-engine"; }, + get logName() { return "BmkEngine"; }, + get serverPrefix() { return "user-data/bookmarks/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new BookmarksSyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new BookmarksStore(); + return this.__store; + } +}; +BookmarksEngine.prototype.__proto__ = new Engine(); + +function HistoryEngine(davCollection, cryptoId) { + this._init(davCollection, cryptoId); +} +HistoryEngine.prototype = { + get name() { return "history-engine"; }, + get logName() { return "HistEngine"; }, + get serverPrefix() { return "user-data/history/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new HistorySyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new HistoryStore(); + return this.__store; + } +}; +HistoryEngine.prototype.__proto__ = new Engine(); + serializeCommands: function serializeCommands(commands) { let json = uneval(commands); json = json.replace(/ {action/g, "\n {action"); diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index e74afddb0cd7..d309e0c882d8 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -41,6 +41,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 3b87714085a1..64b0c00b18b7 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -36,30 +36,28 @@ * * ***** END LICENSE BLOCK ***** */ +const EXPORTED_SYMBOLS = ['Log4Moz']; + const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -const MODE_RDONLY = 0x01; -const MODE_WRONLY = 0x02; -const MODE_CREATE = 0x08; -const MODE_APPEND = 0x10; -const MODE_TRUNCATE = 0x20; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/constants.js"); -const PERMS_FILE = 0644; -const PERMS_DIRECTORY = 0755; +const Log4Moz = {}; +Log4Moz.Level = {}; +Log4Moz.Level.Fatal = 70; +Log4Moz.Level.Error = 60; +Log4Moz.Level.Warn = 50; +Log4Moz.Level.Info = 40; +Log4Moz.Level.Config = 30; +Log4Moz.Level.Debug = 20; +Log4Moz.Level.Trace = 10; +Log4Moz.Level.All = 0; -const LEVEL_FATAL = 70; -const LEVEL_ERROR = 60; -const LEVEL_WARN = 50; -const LEVEL_INFO = 40; -const LEVEL_CONFIG = 30; -const LEVEL_DEBUG = 20; -const LEVEL_TRACE = 10; -const LEVEL_ALL = 0; - -const LEVEL_DESC = { +Log4Moz.Level.Desc = { 70: "FATAL", 60: "ERROR", 50: "WARN", @@ -70,12 +68,6 @@ const LEVEL_DESC = { 0: "ALL" }; -const ONE_BYTE = 1; -const ONE_KILOBYTE = 1024 * ONE_BYTE; -const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - /* * LogMessage * Encapsulates a single log event's data @@ -87,11 +79,11 @@ function LogMessage(loggerName, level, message){ this.time = Date.now(); } LogMessage.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), // Ci.ILogMessage, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), get levelDesc() { - if (this.level in LEVEL_DESC) - return LEVEL_DESC[this.level]; + if (this.level in Log4Moz.Level.Desc) + return Log4Moz.Level.Desc[this.level]; return "UNKNOWN"; }, @@ -112,7 +104,7 @@ function Logger(name, repository) { this._appenders = []; } Logger.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), // Ci.ILogger, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), parent: null, @@ -123,7 +115,7 @@ Logger.prototype = { if (this.parent) return this.parent.level; dump("log4moz warning: root logger configuration error: no level defined\n"); - return LEVEL_ALL; + return Log4Moz.Level.All; }, set level(level) { this._level = level; @@ -154,25 +146,25 @@ Logger.prototype = { }, fatal: function Logger_fatal(string) { - this.log(new LogMessage(this._name, LEVEL_FATAL, string)); + this.log(new LogMessage(this._name, Log4Moz.Level.Fatal, string)); }, error: function Logger_error(string) { - this.log(new LogMessage(this._name, LEVEL_ERROR, string)); + this.log(new LogMessage(this._name, Log4Moz.Level.Error, string)); }, warn: function Logger_warn(string) { - this.log(new LogMessage(this._name, LEVEL_WARN, string)); + this.log(new LogMessage(this._name, Log4Moz.Level.Warn, string)); }, info: function Logger_info(string) { - this.log(new LogMessage(this._name, LEVEL_INFO, string)); + this.log(new LogMessage(this._name, Log4Moz.Level.Info, string)); }, config: function Logger_config(string) { - this.log(new LogMessage(this._name, LEVEL_CONFIG, string)); + this.log(new LogMessage(this._name, Log4Moz.Level.Config, string)); }, debug: function Logger_debug(string) { - this.log(new LogMessage(this._name, LEVEL_DEBUG, string)); + this.log(new LogMessage(this._name, Log4Moz.Level.Debug, string)); }, trace: function Logger_trace(string) { - this.log(new LogMessage(this._name, LEVEL_TRACE, string)); + this.log(new LogMessage(this._name, Log4Moz.Level.Trace, string)); } }; @@ -183,7 +175,7 @@ Logger.prototype = { function LoggerRepository() {} LoggerRepository.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), // Ci.ILoggerRepository, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), _loggers: {}, @@ -191,7 +183,7 @@ LoggerRepository.prototype = { get rootLogger() { if (!this._rootLogger) { this._rootLogger = new Logger("root", this); - this._rootLogger.level = LEVEL_ALL; + this._rootLogger.level = Log4Moz.Level.All; } return this._rootLogger; }, @@ -245,7 +237,7 @@ LoggerRepository.prototype = { // Abstract formatter function Formatter() {} Formatter.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), // Ci.IFormatter, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), format: function Formatter_format(message) {} }; @@ -287,9 +279,9 @@ function Appender(formatter) { this._formatter = formatter? formatter : new BasicFormatter(); } Appender.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), // Ci.IAppender, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), - _level: LEVEL_ALL, + _level: Log4Moz.Level.All, get level() { return this._level; }, set level(level) { this._level = level; }, @@ -331,7 +323,7 @@ function ConsoleAppender(formatter) { } ConsoleAppender.prototype = { doAppend: function CApp_doAppend(message) { - if (message.level > LEVEL_WARN) { + if (message.level > Log4Moz.Level.Warn) { Cu.reportError(message); return; } @@ -446,7 +438,7 @@ Log4MozService.prototype = { //classDescription: "Log4moz Logging Service", //contractID: "@mozilla.org/log4moz/service;1", //classID: Components.ID("{a60e50d7-90b8-4a12-ad0c-79e6a1896978}"), - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), //Ci.ILog4MozService, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), get rootLogger() { return this._repository.rootLogger; @@ -492,20 +484,4 @@ Log4MozService.prototype = { } }; -const EXPORTED_SYMBOLS = ['Log4Moz']; - -const Log4Moz = {}; - -Log4Moz.Level = {}; -Log4Moz.Level.Fatal = LEVEL_FATAL; -Log4Moz.Level.Error = LEVEL_ERROR; -Log4Moz.Level.Warn = LEVEL_WARN; -Log4Moz.Level.Info = LEVEL_INFO; -Log4Moz.Level.Config = LEVEL_CONFIG; -Log4Moz.Level.Debug = LEVEL_DEBUG; -Log4Moz.Level.Trace = LEVEL_TRACE; -Log4Moz.Level.All = LEVEL_ALL; - -Log4Moz.Level.Desc = LEVEL_DESC; - Log4Moz.Service = new Log4MozService(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 20e1a2cf82d6..d265ccc988a2 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -41,6 +41,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -49,7 +50,6 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Function.prototype.async = generatorAsync; @@ -84,6 +84,8 @@ WeaveSyncService.prototype = { return this.__dav; }, + // FIXME: engines should be loaded dynamically somehow / need API to register + __bmkEngine: null, get _bmkEngine() { if (!this.__bmkEngine) @@ -91,6 +93,13 @@ WeaveSyncService.prototype = { return this.__bmkEngine; }, + __histEngine: null, + get _histEngine() { + if (!this.__histEngine) + this.__histEngine = new HistoryEngine(this._dav, this._cryptoId); + return this.__histEngine; + }, + // Logger object _log: null, @@ -163,7 +172,7 @@ WeaveSyncService.prototype = { return null; }, - _init: function BSS__init() { + _init: function WeaveSync__init() { this._initLogs(); this._log.info("Weave Sync Service Initializing"); @@ -202,7 +211,7 @@ WeaveSyncService.prototype = { } }, - _enableSchedule: function BSS__enableSchedule() { + _enableSchedule: function WeaveSync__enableSchedule() { this._scheduleTimer = Cc["@mozilla.org/timer;1"]. createInstance(Ci.nsITimer); let listener = new EventListener(bind2(this, this._onSchedule)); @@ -210,16 +219,16 @@ WeaveSyncService.prototype = { this._scheduleTimer.TYPE_REPEATING_SLACK); }, - _disableSchedule: function BSS__disableSchedule() { + _disableSchedule: function WeaveSync__disableSchedule() { this._scheduleTimer = null; }, - _onSchedule: function BSS__onSchedule() { + _onSchedule: function WeaveSync__onSchedule() { this._log.info("Running scheduled sync"); this.sync(); }, - _initLogs: function BSS__initLogs() { + _initLogs: function WeaveSync__initLogs() { this._log = Log4Moz.Service.getLogger("Service.Main"); let formatter = Log4Moz.Service.newFormatter("basic"); @@ -249,7 +258,7 @@ WeaveSyncService.prototype = { root.addAppender(vapp); }, - _lock: function BSS__lock() { + _lock: function weaveSync__lock() { if (this._locked) { this._log.warn("Service lock failed: already locked"); return false; @@ -259,14 +268,14 @@ WeaveSyncService.prototype = { return true; }, - _unlock: function BSS__unlock() { + _unlock: function WeaveSync__unlock() { this._locked = false; this._log.debug("Service lock released"); }, // IBookmarksSyncService internal implementation - _login: function BSS__login(onComplete) { + _login: function WeaveSync__login(onComplete) { let [self, cont] = yield; let success = false; @@ -309,7 +318,7 @@ WeaveSyncService.prototype = { this._log.warn("generator not properly closed"); }, - _resetLock: function BSS__resetLock(onComplete) { + _resetLock: function WeaveSync__resetLock(onComplete) { let [self, cont] = yield; let success = false; @@ -321,7 +330,7 @@ WeaveSyncService.prototype = { success = yield; } catch (e) { - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { if (success) { @@ -332,7 +341,75 @@ WeaveSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); } generatorDone(this, self, onComplete, success); - yield; // onComplete is responsible for closing the generator + yield; // generatorDone is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _sync: function WeaveSync__sync() { + let [self, cont] = yield; + + try { + if (!this._lock()) + return; + + this._bmkEngine.sync(cont); + yield; + this._histEngine.sync(cont); + yield; + + this._unlock(); + + } catch (e) { + this._log.error("Exception caught: " + (e.message? e.message : e)); + + } finally { + generatorDone(this, self); + yield; // generatorDone is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _resetServer: function WeaveSync__resetServer() { + let [self, cont] = yield; + + try { + if (!this._lock()) + return; + + this._bmkEngine.resetServer(cont); + this._histEngine.resetServer(cont); + + this._unlock(); + + } catch (e) { + this._log.error("Exception caught: " + (e.message? e.message : e)); + + } finally { + generatorDone(this, self); + yield; // generatorDone is responsible for closing the generator + } + this._log.warn("generator not properly closed"); + }, + + _resetClient: function WeaveSync__resetClient() { + let [self, cont] = yield; + + try { + if (!this._lock()) + return; + + this._bmkEngine.resetClient(cont); + this._histEngine.resetClient(cont); + + this._unlock(); + + } catch (e) { + this._log.error("Exception caught: " + (e.message? e.message : e)); + + } finally { + generatorDone(this, self); + yield; // generatorDone is responsible for closing the generator } this._log.warn("generator not properly closed"); }, @@ -341,7 +418,7 @@ WeaveSyncService.prototype = { // nsIObserver - observe: function BSS__observe(subject, topic, data) { + observe: function WeaveSync__observe(subject, topic, data) { switch (topic) { case "browser.places.sync.enabled": switch (data) { @@ -379,7 +456,7 @@ WeaveSyncService.prototype = { // These are global (for all engines) - login: function BSS_login(password, passphrase) { + login: function WeaveSync_login(password, passphrase) { if (!this._lock()) return; // cache password & passphrase @@ -390,7 +467,7 @@ WeaveSyncService.prototype = { this._login.async(this, function() {self._unlock()}); }, - logout: function BSS_logout() { + logout: function WeaveSync_logout() { this._log.info("Logging out"); this._dav.logout(); this._mozId.setTempPassword(null); // clear cached password @@ -398,7 +475,7 @@ WeaveSyncService.prototype = { this._os.notifyObservers(null, "bookmarks-sync:logout", ""); }, - resetLock: function BSS_resetLock() { + resetLock: function WeaveSync_resetLock() { if (!this._lock()) return; let self = this; @@ -407,24 +484,7 @@ WeaveSyncService.prototype = { // These are per-engine - sync: function BSS_sync() { - if (!this._lock()) - return; - let self = this; - this._bmkEngine.sync(function() {self._unlock()}); - }, - - resetServer: function BSS_resetServer() { - if (!this._lock()) - return; - let self = this; - this._bmkEngine.resetServer(function() {self._unlock()}); - }, - - resetClient: function BSS_resetClient() { - if (!this._lock()) - return; - let self = this; - this._bmkEngine.resetClient(function() {self._unlock()}); - } + sync: function WeaveSync_sync() { this._sync.async(this); }, + resetServer: function WeaveSync_resetServer() { this._resetServer.async(this); }, + resetClient: function WeaveSync_resetClient() { this._resetClient.async(this); } }; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index ffb99e518b2f..8a2cf81f61fe 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -34,20 +34,19 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore']; +const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', + 'HistoryStore']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - /* * Data Stores * These can wrap, serialize items and apply commands @@ -67,15 +66,46 @@ Store.prototype = { }, applyCommands: function Store_applyCommands(commandList) { + for (var i = 0; i < commandList.length; i++) { + var command = commandList[i]; + this._log.debug("Processing command: " + uneval(command)); + switch (command["action"]) { + case "create": + this._createCommand(command); + break; + case "remove": + this._removeCommand(command); + break; + case "edit": + this._editCommand(command); + break; + default: + this._log.error("unknown action in command: " + command["action"]); + break; + } + } + }, + + resetGUIDs: function Store_resetGUIDs() { } }; -function SnapshotStore() { - this._init(); +function SnapshotStore(name) { + this._init(name); } SnapshotStore.prototype = { _logName: "SStore", + _filename: null, + get filename() { + if (this._filename === null) + throw "filename is null"; + return this._filename; + }, + set filename(value) { + this._filename = value + ".json"; + }, + __dirSvc: null, get _dirSvc() { if (!this.__dirSvc) @@ -108,13 +138,25 @@ SnapshotStore.prototype = { this._GUID = GUID; }, + _init: function SStore__init(name) { + this.filename = name; + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + }, + save: function SStore_save() { this._log.info("Saving snapshot to disk"); let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync-snapshot.json"); - file.QueryInterface(Ci.nsILocalFile); + file.append("weave-snapshots"); + file.QueryInterface(Ci.nsILocalFile); + if (!file.exists()) + file.create(file.DIRECTORY_TYPE, PERMS_DIRECTORY); + file.QueryInterface(Ci.nsIFile); + + file.append(this.filename); + + file.QueryInterface(Ci.nsILocalFile); if (!file.exists()) file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); @@ -133,7 +175,8 @@ SnapshotStore.prototype = { load: function SStore_load() { let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync-snapshot.json"); + file.append("weave-snapshots"); + file.append(this.filename); if (!file.exists()) return; @@ -539,18 +582,57 @@ BookmarksStore.prototype = { }; BookmarksStore.prototype.__proto__ = new Store(); -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } +function HistoryStore() { + this._init(); } +HistoryStore.prototype = { + _logName: "HistStore", + + __hsvc: null, + get _hsvc() { + if (!this.__hsvc) + this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + return this.__hsvc; + }, + + _createCommand: function HistStore__createCommand(command) { + this._log.info(" -> creating history entry: " + command.GUID); + this._hsvc.addVisit(makeURI(command.data.URI), command.data.time, + 0, this._hsvc.TRANSITION_LINK, false, 0); + this._hsvc.addVisit(makeURI(command.data.URI), command.data.title, + command.data.accessCount, false, false); + }, + + _removeCommand: function HistStore__removeCommand(command) { + this._log.info(" -> NOT removing history entry: " + command.GUID); + // skip removals + }, + + _editCommand: function HistStore__editCommand(command) { + this._log.info(" -> FIXME: NOT editing history entry: " + command.GUID); + // FIXME: implement! + }, + + wrap: function HistStore_wrap() { + let query = this._hsvc.getNewQuery(); + query.minVisits = 1; + let root = this._hsvc.executeQuery(query, + this._hsvc.getNewQueryOptions()).root; + root.QueryInterface(Ci.nsINavHistoryQueryResultNode); + root.containerOpen = true; + let items = {}; + for (let i = 0; i < root.childCount; i++) { + let item = root.getChild(i); + items[item.uri] = {parentGUID: '', + title: item.title, + URI: item.uri, + time: item.time, + accessCount: item.accessCount, + dateAdded: item.dateAdded, + }; + } + return items; + } +}; +HistoryStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 39bbd3203cd6..7edf29ce524c 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,14 +34,14 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore']; +const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -addModuleAlias("weave", "{340c2bbc-ce74-4362-90b5-7c26312808ef}"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -332,7 +332,6 @@ BookmarksSyncCore.prototype = { return this.__bms; }, - // NOTE: Needs to be subclassed _itemExists: function BSC__itemExists(GUID) { return this._bms.getItemIdForGUID(GUID) >= 0; }, @@ -396,18 +395,23 @@ BookmarksSyncCore.prototype = { }; BookmarksSyncCore.prototype.__proto__ = new SyncCore(); -function addModuleAlias(alias, extensionId) { - let ioSvc = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioSvc.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - if (!resProt.hasSubstitution(alias)) { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation(extensionId); - let extD = loc.getItemLocation(extensionId); - extD.append("modules"); - resProt.setSubstitution(alias, ioSvc.newFileURI(extD)); - } +function HistorySyncCore() { + this._init(); } +HistorySyncCore.prototype = { + _logName: "HistSync", + + _itemExists: function BSC__itemExists(GUID) { + // we don't care about already-existing items; just try to re-add them + return false; + }, + + _commandLike: function BSC_commandLike(a, b) { + // History commands never qualify for likeness. We will always + // take the union of all client/server items. We use the URL as + // the GUID, so the same sites will map to the same item (same + // GUID), without our intervention. + return false; + } +}; +HistorySyncCore.prototype.__proto__ = new SyncCore(); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 4036ef30bbf5..3f88eb0aab62 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -42,11 +42,10 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - /* * Utility functions */ diff --git a/services/sync/modules/weave.js b/services/sync/modules/weave.js index b55243e7a33e..b6c5080fe72b 100644 --- a/services/sync/modules/weave.js +++ b/services/sync/modules/weave.js @@ -41,6 +41,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/service.js"); let Weave = {}; From da64344e134e6eef77ffd33d554725e4d449552a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Dec 2007 13:24:31 -0800 Subject: [PATCH 0097/1860] login fixes; history sync fixes; make reset client actually delete all client data (useful for testing and for syncing down server data - e.g. the restore case) --- services/sync/modules/dav.js | 4 - services/sync/modules/engines.js | 5 +- services/sync/modules/service.js | 13 +-- services/sync/modules/stores.js | 151 +++++++++++++++++-------------- 4 files changed, 90 insertions(+), 83 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 72b6c0452b19..c57922bc1e20 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -257,10 +257,6 @@ DAVCollection.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - if (this._loggedIn) - this._log.info("Logged in"); - else - this._log.warn("Could not log in"); generatorDone(this, self, onComplete, this._loggedIn); yield; // onComplete is responsible for closing the generator } diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 86bc1dd14e1e..3084ad4ea25e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -182,9 +182,8 @@ Engine.prototype = { this._log.debug("Resetting client state"); this._os.notifyObservers(null, this._osPrefix + "reset-client:start", ""); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.save(); + this._snapshot.wipe(); + this._store.wipe(); done = true; } catch (e) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d265ccc988a2..436311e7e610 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -261,16 +261,19 @@ WeaveSyncService.prototype = { _lock: function weaveSync__lock() { if (this._locked) { this._log.warn("Service lock failed: already locked"); + this._os.notifyObservers(null, "weave:service-lock:error", ""); return false; } this._locked = true; this._log.debug("Service lock acquired"); + this._os.notifyObservers(null, "weave:service-lock:success", ""); return true; }, _unlock: function WeaveSync__unlock() { this._locked = false; this._log.debug("Service lock released"); + this._os.notifyObservers(null, "weave:service-unlock:success", ""); }, // IBookmarksSyncService internal implementation @@ -285,13 +288,11 @@ WeaveSyncService.prototype = { if (!this.username) { this._log.warn("No username set, login failed"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - return; + return; } if (!this.password) { this._log.warn("No password given or found in password manager"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - return; + return; } this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; @@ -306,10 +307,10 @@ WeaveSyncService.prototype = { } finally { this._passphrase = null; if (success) { - this._log.debug("Login successful"); + //this._log.debug("Login successful"); // chrome prints this too, hm this._os.notifyObservers(null, "bookmarks-sync:login-end", ""); } else { - this._log.debug("Login error"); + //this._log.debug("Login error"); this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); } generatorDone(this, self, onComplete, success); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 8a2cf81f61fe..390c6ceb8174 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -62,9 +62,6 @@ Store.prototype = { this._log = Log4Moz.Service.getLogger("Service." + this._logName); }, - wrap: function Store_wrap() { - }, - applyCommands: function Store_applyCommands(commandList) { for (var i = 0; i < commandList.length; i++) { var command = commandList[i]; @@ -86,15 +83,17 @@ Store.prototype = { } }, - resetGUIDs: function Store_resetGUIDs() { - } + // override these in derived objects + wrap: function Store_wrap() {}, + wipe: function Store_wipe() {}, + resetGUIDs: function Store_resetGUIDs() {} }; function SnapshotStore(name) { this._init(name); } SnapshotStore.prototype = { - _logName: "SStore", + _logName: "SnapStore", _filename: null, get filename() { @@ -143,6 +142,35 @@ SnapshotStore.prototype = { this._log = Log4Moz.Service.getLogger("Service." + this._logName); }, + _createCommand: function SStore__createCommand(command) { + this._data[command.GUID] = eval(uneval(command.data)); + }, + + _removeCommand: function SStore__removeCommand(command) { + delete this._data[command.GUID]; + }, + + _editCommand: function SStore__editCommand(command) { + if ("GUID" in command.data) { + // special-case guid changes + let newGUID = command.data.GUID, + oldGUID = command.GUID; + + this._data[newGUID] = this._data[oldGUID]; + delete this._data[oldGUID] + + for (let GUID in this._data) { + if (this._data[GUID].parentGUID == oldGUID) + this._data[GUID].parentGUID = newGUID; + } + } + for (let prop in command.data) { + if (prop == "GUID") + continue; + this._data[command.GUID][prop] = command.data[prop]; + } + }, + save: function SStore_save() { this._log.info("Saving snapshot to disk"); @@ -217,41 +245,14 @@ SnapshotStore.prototype = { }, wrap: function SStore_wrap() { + return this.data; }, - applyCommands: function SStore_applyCommands(commands) { - for (let i = 0; i < commands.length; i++) { - // this._log.debug("Applying cmd to obj: " + uneval(commands[i])); - switch (commands[i].action) { - case "create": - this._data[commands[i].GUID] = eval(uneval(commands[i].data)); - break; - case "edit": - if ("GUID" in commands[i].data) { - // special-case guid changes - let newGUID = commands[i].data.GUID, - oldGUID = commands[i].GUID; - - this._data[newGUID] = this._data[oldGUID]; - delete this._data[oldGUID] - - for (let GUID in this._data) { - if (this._data[GUID].parentGUID == oldGUID) - this._data[GUID].parentGUID = newGUID; - } - } - for (let prop in commands[i].data) { - if (prop == "GUID") - continue; - this._data[commands[i].GUID][prop] = commands[i].data[prop]; - } - break; - case "remove": - delete this._data[commands[i].GUID]; - break; - } - } - return this._data; + wipe: function SStore_wipe() { + this.data = {}; + this.version = -1; + this.GUID = null; + this.save(); } }; SnapshotStore.prototype.__proto__ = new Store(); @@ -553,31 +554,16 @@ BookmarksStore.prototype = { return filed; // (combined) }, + wipe: function BStore_wipe() { + this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder); + this._bms.removeFolderChildren(this._bms.toolbarFolder); + this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder); + }, + resetGUIDs: function BStore_resetGUIDs() { this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); - }, - - applyCommands: function BStore_applyCommands(commandList) { - for (var i = 0; i < commandList.length; i++) { - var command = commandList[i]; - this._log.debug("Processing command: " + uneval(command)); - switch (command["action"]) { - case "create": - this._createCommand(command); - break; - case "remove": - this._removeCommand(command); - break; - case "edit": - this._editCommand(command); - break; - default: - this._log.error("unknown action in command: " + command["action"]); - break; - } - } } }; BookmarksStore.prototype.__proto__ = new Store(); @@ -596,17 +582,30 @@ HistoryStore.prototype = { return this.__hsvc; }, + __browserHist: null, + get _browserHist() { + if (!this.__browserHist) + this.__browserHist = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsIBrowserHistory); + return this.__browserHist; + }, + _createCommand: function HistStore__createCommand(command) { this._log.info(" -> creating history entry: " + command.GUID); - this._hsvc.addVisit(makeURI(command.data.URI), command.data.time, - 0, this._hsvc.TRANSITION_LINK, false, 0); - this._hsvc.addVisit(makeURI(command.data.URI), command.data.title, - command.data.accessCount, false, false); + try { + this._browserHist.addPageWithDetails(makeURI(command.GUID), + command.data.title, + command.data.time); + this._hsvc.setPageDetails(makeURI(command.GUID), command.data.title, + command.data.accessCount, false, false); + } catch (e) { + this._log.error("Exception caught: " + (e.message? e.message : e)); + } }, _removeCommand: function HistStore__removeCommand(command) { - this._log.info(" -> NOT removing history entry: " + command.GUID); - // skip removals + this._log.info(" -> removing history entry: " + command.GUID); + this._browserHist.removePage(command.GUID); }, _editCommand: function HistStore__editCommand(command) { @@ -614,12 +613,20 @@ HistoryStore.prototype = { // FIXME: implement! }, - wrap: function HistStore_wrap() { - let query = this._hsvc.getNewQuery(); + _historyRoot: function HistStore__historyRoot() { + let query = this._hsvc.getNewQuery(), + options = this._hsvc.getNewQueryOptions(); + query.minVisits = 1; - let root = this._hsvc.executeQuery(query, - this._hsvc.getNewQueryOptions()).root; + options.queryType = options.QUERY_TYPE_HISTORY; + + let root = this._hsvc.executeQuery(query, options).root; root.QueryInterface(Ci.nsINavHistoryQueryResultNode); + return root; + }, + + wrap: function HistStore_wrap() { + let root = this._historyRoot(); root.containerOpen = true; let items = {}; for (let i = 0; i < root.childCount; i++) { @@ -633,6 +640,10 @@ HistoryStore.prototype = { }; } return items; + }, + + wipe: function HistStore_wipe() { + this._browserHist.removeAllPages(); } }; HistoryStore.prototype.__proto__ = new Store(); From 25d6885847e387a23938e1cd5449ee6d0995cbd2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Dec 2007 17:37:01 -0800 Subject: [PATCH 0098/1860] rename 'bookmarks' events to be weave events; add service-level events in addition to engine-specific ones (where appropriate) --- services/sync/modules/engines.js | 6 +++--- services/sync/modules/service.js | 22 +++++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3084ad4ea25e..dd0515bce55c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -163,7 +163,7 @@ Engine.prototype = { } finally { if (done) { this._log.debug("Server reset completed successfully"); - this._os.notifyObservers(null, this._osPrefix + "reset-server:end", ""); + this._os.notifyObservers(null, this._osPrefix + "reset-server:success", ""); } else { this._log.debug("Server reset failed"); this._os.notifyObservers(null, this._osPrefix + "reset-server:error", ""); @@ -192,7 +192,7 @@ Engine.prototype = { } finally { if (done) { this._log.debug("Client reset completed successfully"); - this._os.notifyObservers(null, this._osPrefix + "reset-client:end", ""); + this._os.notifyObservers(null, this._osPrefix + "reset-client:success", ""); } else { this._log.debug("Client reset failed"); this._os.notifyObservers(null, this._osPrefix + "reset-client:error", ""); @@ -431,7 +431,7 @@ Engine.prototype = { ok = yield; } if (ok && synced) { - this._os.notifyObservers(null, this._osPrefix + "sync:end", ""); + this._os.notifyObservers(null, this._osPrefix + "sync:success", ""); generatorDone(this, self, onComplete, true); } else { this._os.notifyObservers(null, this._osPrefix + "sync:error", ""); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 436311e7e610..21622cb6a4a5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -284,7 +284,7 @@ WeaveSyncService.prototype = { try { this._log.debug("Logging in"); - this._os.notifyObservers(null, "bookmarks-sync:login-start", ""); + this._os.notifyObservers(null, "weave:service-login:start", ""); if (!this.username) { this._log.warn("No username set, login failed"); @@ -308,10 +308,10 @@ WeaveSyncService.prototype = { this._passphrase = null; if (success) { //this._log.debug("Login successful"); // chrome prints this too, hm - this._os.notifyObservers(null, "bookmarks-sync:login-end", ""); + this._os.notifyObservers(null, "weave:service-login:success", ""); } else { //this._log.debug("Login error"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); + this._os.notifyObservers(null, "weave:service-login:error", ""); } generatorDone(this, self, onComplete, success); yield; // onComplete is responsible for closing the generator @@ -325,7 +325,7 @@ WeaveSyncService.prototype = { try { this._log.debug("Resetting server lock"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-start", ""); + this._os.notifyObservers(null, "weave:server-lock-reset:start", ""); this._dav.forceUnlock.async(this._dav, cont); success = yield; @@ -336,10 +336,10 @@ WeaveSyncService.prototype = { } finally { if (success) { this._log.debug("Server lock reset successful"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-end", ""); + this._os.notifyObservers(null, "weave:server-lock-reset:success", ""); } else { this._log.debug("Server lock reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); + this._os.notifyObservers(null, "weave:server-lock-reset:error", ""); } generatorDone(this, self, onComplete, success); yield; // generatorDone is responsible for closing the generator @@ -349,22 +349,30 @@ WeaveSyncService.prototype = { _sync: function WeaveSync__sync() { let [self, cont] = yield; + let success = false; try { if (!this._lock()) return; + this._os.notifyObservers(null, "weave:service:sync:start", ""); + this._bmkEngine.sync(cont); yield; this._histEngine.sync(cont); yield; + success = true; this._unlock(); } catch (e) { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { + if (success) + this._os.notifyObservers(null, "weave:service:sync:success", ""); + else + this._os.notifyObservers(null, "weave:service:sync:error", ""); generatorDone(this, self); yield; // generatorDone is responsible for closing the generator } @@ -473,7 +481,7 @@ WeaveSyncService.prototype = { this._dav.logout(); this._mozId.setTempPassword(null); // clear cached password this._cryptoId.setTempPassword(null); // and passphrase - this._os.notifyObservers(null, "bookmarks-sync:logout", ""); + this._os.notifyObservers(null, "weave:service-logout:success", ""); }, resetLock: function WeaveSync_resetLock() { From 9aa2ac860137609cd394f09ef6a9c82d5766fe4b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Dec 2007 19:49:28 -0800 Subject: [PATCH 0099/1860] move logs and snapshots into a 'weave' directory in the profile; use .txt filenames for logs to (hopefully) fix content type problems some people are seeing --- services/sync/modules/service.js | 29 ++++++++++++++++++++++------- services/sync/modules/stores.js | 11 ++++++----- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 21622cb6a4a5..2ad1c77dfe06 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -244,16 +244,31 @@ WeaveSyncService.prototype = { root.addAppender(dapp); let logFile = this._dirSvc.get("ProfD", Ci.nsIFile); - let verboseFile = logFile.clone(); - logFile.append("bm-sync.log"); - logFile.QueryInterface(Ci.nsILocalFile); - verboseFile.append("bm-sync-verbose.log"); - verboseFile.QueryInterface(Ci.nsILocalFile); - let fapp = Log4Moz.Service.newFileAppender("rotating", logFile, formatter); + let brief = this._dirSvc.get("ProfD", Ci.nsIFile); + brief.QueryInterface(Ci.nsILocalFile); + + brief.append("weave"); + if (!brief.exists()) + brief.create(brief.DIRECTORY_TYPE, PERMS_DIRECTORY); + + brief.append("logs"); + if (!brief.exists()) + brief.create(brief.DIRECTORY_TYPE, PERMS_DIRECTORY); + + brief.append("brief-log.txt"); + if (!brief.exists()) + brief.create(brief.NORMAL_FILE_TYPE, PERMS_FILE); + + let verbose = brief.parent.clone(); + verbose.append("verbose-log.txt"); + if (!verbose.exists()) + verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); + + let fapp = Log4Moz.Service.newFileAppender("rotating", brief, formatter); fapp.level = Log4Moz.Level.Info; root.addAppender(fapp); - let vapp = Log4Moz.Service.newFileAppender("rotating", verboseFile, formatter); + let vapp = Log4Moz.Service.newFileAppender("rotating", verbose, formatter); vapp.level = Log4Moz.Level.Debug; root.addAppender(vapp); }, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 390c6ceb8174..0d30114121a0 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -175,16 +175,17 @@ SnapshotStore.prototype = { this._log.info("Saving snapshot to disk"); let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("weave-snapshots"); - file.QueryInterface(Ci.nsILocalFile); + + file.append("weave"); + if (!file.exists()) + file.create(file.DIRECTORY_TYPE, PERMS_DIRECTORY); + + file.append("snapshots"); if (!file.exists()) file.create(file.DIRECTORY_TYPE, PERMS_DIRECTORY); - file.QueryInterface(Ci.nsIFile); file.append(this.filename); - - file.QueryInterface(Ci.nsILocalFile); if (!file.exists()) file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); From 1c7208805f6e8259f683803a51f8c38b9ba2f1b5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Dec 2007 12:18:41 -0800 Subject: [PATCH 0100/1860] ui / prefs fixes --- services/sync/modules/service.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2ad1c77dfe06..403536d82e99 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -129,7 +129,11 @@ WeaveSyncService.prototype = { set username(value) { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); - branch.setCharPref("browser.places.sync.username", value); + if (value) + branch.setCharPref("browser.places.sync.username", value); + else + branch.clearUserPref("browser.places.sync.username"); + // fixme - need to loop over all Identity objects - needs some rethinking... this._mozId.username = value; this._cryptoId.username = value; From d194c734c4602c7c5d51c3a20348bcbda56118b6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Dec 2007 14:46:12 -0800 Subject: [PATCH 0101/1860] change prefs to live under extensions.weave --- services/sync/modules/constants.js | 12 ++++++++---- services/sync/modules/crypto.js | 14 +++++++------- services/sync/modules/service.js | 18 +++++++++--------- services/sync/services-sync.js | 18 +++++++++--------- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index e6c5b0f3d04f..b6fb3c50ed1e 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -34,12 +34,18 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['MODE_RDONLY', 'MODE_WRONLY', +const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', + 'PREFS_PRANCH', + 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', 'PERMS_FILE', 'PERMS_DIRECTORY', - 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; +const WEAVE_VERSION = "0.1.12.7"; +const STORAGE_FORMAT_VERSION = 2; + +const PREFS_BRANCH = "extensions.weave."; + const MODE_RDONLY = 0x01; const MODE_WRONLY = 0x02; const MODE_CREATE = 0x08; @@ -49,8 +55,6 @@ const MODE_TRUNCATE = 0x20; const PERMS_FILE = 0644; const PERMS_DIRECTORY = 0755; -const STORAGE_FORMAT_VERSION = 2; - const ONE_BYTE = 1; const ONE_KILOBYTE = 1024 * ONE_BYTE; const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 20bb59fb8975..599f893d9e89 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -75,21 +75,21 @@ WeaveCrypto.prototype = { get defaultAlgorithm() { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); - return branch.getCharPref("browser.places.sync.encryption"); + return branch.getCharPref("extensions.weave.encryption"); }, set defaultAlgorithm(value) { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); - let cur = branch.getCharPref("browser.places.sync.encryption"); + let cur = branch.getCharPref("extensions.weave.encryption"); if (value != cur) - branch.setCharPref("browser.places.sync.encryption", value); + branch.setCharPref("extensions.weave.encryption", value); }, _init: function Crypto__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch2); - branch.addObserver("browser.places.sync.encryption", this, false); + branch.addObserver("extensions.weave.encryption", this, false); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), @@ -98,11 +98,11 @@ WeaveCrypto.prototype = { observe: function Sync_observe(subject, topic, data) { switch (topic) { - case "browser.places.sync.encryption": + case "extensions.weave.encryption": let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); - let cur = branch.getCharPref("browser.places.sync.encryption"); + let cur = branch.getCharPref("extensions.weave.encryption"); if (cur == data) return; @@ -115,7 +115,7 @@ WeaveCrypto.prototype = { break; default: this._log.warn("Unknown encryption algorithm, resetting"); - branch.setCharPref("browser.places.sync.encryption", "XXXTEA"); + branch.setCharPref("extensions.weave.encryption", "XXXTEA"); return; // otherwise we'll send the alg changed event twice } // FIXME: listen to this bad boy somewhere diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 403536d82e99..75067f4be60a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -124,15 +124,15 @@ WeaveSyncService.prototype = { get username() { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); - return branch.getCharPref("browser.places.sync.username"); + return branch.getCharPref("extensions.weave.username"); }, set username(value) { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); if (value) - branch.setCharPref("browser.places.sync.username", value); + branch.setCharPref("extensions.weave.username", value); else - branch.clearUserPref("browser.places.sync.username"); + branch.clearUserPref("extensions.weave.username"); // fixme - need to loop over all Identity objects - needs some rethinking... this._mozId.username = value; @@ -187,11 +187,11 @@ WeaveSyncService.prototype = { try { let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch2); - this._serverURL = branch.getCharPref("browser.places.sync.serverURL"); - enabled = branch.getBoolPref("browser.places.sync.enabled"); - schedule = branch.getIntPref("browser.places.sync.schedule"); + this._serverURL = branch.getCharPref("extensions.weave.serverURL"); + enabled = branch.getBoolPref("extensions.weave.enabled"); + schedule = branch.getIntPref("extensions.weave.schedule"); - branch.addObserver("browser.places.sync", this, false); + branch.addObserver("extensions.weave", this, false); } catch (ex) { /* use defaults */ } @@ -448,7 +448,7 @@ WeaveSyncService.prototype = { observe: function WeaveSync__observe(subject, topic, data) { switch (topic) { - case "browser.places.sync.enabled": + case "extensions.weave.enabled": switch (data) { case false: this._log.info("Disabling automagic bookmarks sync"); @@ -460,7 +460,7 @@ WeaveSyncService.prototype = { break; } break; - case "browser.places.sync.schedule": + case "extensions.weave.schedule": switch (data) { case 0: this._log.info("Disabling automagic bookmarks sync"); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index c043e5a3de03..7291bb32ee4e 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,9 +1,9 @@ -pref("browser.places.sync.serverURL", "https://services.mozilla.com/"); -pref("browser.places.sync.username", "nobody@mozilla.com"); -pref("browser.places.sync.rememberpassword", true); -pref("browser.places.sync.autoconnect", true); -pref("browser.places.sync.enabled", true); -pref("browser.places.sync.bookmarks", true); -pref("browser.places.sync.schedule", 1); -pref("browser.places.sync.lastsync", "0"); -pref("browser.places.sync.encryption", "XXXTEA"); +pref("extensions.weave.serverURL", "https://services.mozilla.com/"); +pref("extensions.weave.username", "nobody@mozilla.com"); +pref("extensions.weave.rememberpassword", true); +pref("extensions.weave.autoconnect", true); +pref("extensions.weave.enabled", true); +pref("extensions.weave.bookmarks", true); +pref("extensions.weave.schedule", 1); +pref("extensions.weave.lastsync", "0"); +pref("extensions.weave.encryption", "XXXTEA"); From b5cce34abfd5ce03ec7efbeaf94ebb9096377808 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Dec 2007 15:35:19 -0800 Subject: [PATCH 0102/1860] implement firstrun/updated pages --- services/sync/services-sync.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 7291bb32ee4e..cc17fab370da 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,5 +1,6 @@ pref("extensions.weave.serverURL", "https://services.mozilla.com/"); pref("extensions.weave.username", "nobody@mozilla.com"); +pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); From bf2d5b74aa47d74c7713a0ae9c326197825476ec Mon Sep 17 00:00:00 2001 From: Date: Thu, 20 Dec 2007 15:43:18 -0800 Subject: [PATCH 0103/1860] sorry about the tabs! lots of refactoring of the UI bits --- services/sync/services-sync.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 7291bb32ee4e..cd7ac05ef53c 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -4,6 +4,7 @@ pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); pref("extensions.weave.bookmarks", true); +pref("extensions.weave.history", true); pref("extensions.weave.schedule", 1); pref("extensions.weave.lastsync", "0"); pref("extensions.weave.encryption", "XXXTEA"); From 2b316eceba0d4659d27cd178e15d6eb425b3ba2b Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Thu, 20 Dec 2007 17:19:36 -0800 Subject: [PATCH 0104/1860] make the browser overlay (sync.xul) localized and localizable --- services/sync/locales/en-US/sync.dtd | 6 ++++++ services/sync/locales/en-US/sync.properties | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 services/sync/locales/en-US/sync.properties diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index e69de29bb2d1..e911359c4341 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -0,0 +1,6 @@ + + + + + + diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties new file mode 100644 index 000000000000..34ddf5f5e362 --- /dev/null +++ b/services/sync/locales/en-US/sync.properties @@ -0,0 +1,2 @@ +# %S is the date and time at which the last sync successfully completed +lastSync.label = Last Update: %S From 7b9b6cbe40512c6b84d2dd36d18edc15d4bfa97b Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Thu, 20 Dec 2007 23:31:29 -0800 Subject: [PATCH 0105/1860] make wizard localizable --- services/sync/locales/en-US/wizard.dtd | 32 +++++++++++++++++++ services/sync/locales/en-US/wizard.properties | 10 ++++++ 2 files changed, 42 insertions(+) create mode 100644 services/sync/locales/en-US/wizard.dtd create mode 100644 services/sync/locales/en-US/wizard.properties diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd new file mode 100644 index 000000000000..0ff03d3f5082 --- /dev/null +++ b/services/sync/locales/en-US/wizard.dtd @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties new file mode 100644 index 000000000000..f4cfb2c5808e --- /dev/null +++ b/services/sync/locales/en-US/wizard.properties @@ -0,0 +1,10 @@ +verifyStatusUnverified.label = Status: Unverified. +verifyStatusLoginVerified.label = Status: Login Verified. +verifyStatusLoginFailed.label = Status: Login Failed. + +initStatusReadyToSync.label = Status: Ready to Sync. +initStatusSyncing.label = Status: Syncing... +initStatusSyncComplete.label = Status: Sync Complete +initStatusSyncFailed.label = Status: Sync Failed + +invalidCredentials.alert = You must provide a valid user name and password to continue. From f90fbc3318d032f077ed221f4110562ced131148 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 21 Dec 2007 00:11:50 -0800 Subject: [PATCH 0106/1860] make log dialog localizable --- services/sync/locales/en-US/log.dtd | 4 ++++ services/sync/locales/en-US/log.properties | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 services/sync/locales/en-US/log.dtd create mode 100644 services/sync/locales/en-US/log.properties diff --git a/services/sync/locales/en-US/log.dtd b/services/sync/locales/en-US/log.dtd new file mode 100644 index 000000000000..992e41ee93aa --- /dev/null +++ b/services/sync/locales/en-US/log.dtd @@ -0,0 +1,4 @@ + + + + diff --git a/services/sync/locales/en-US/log.properties b/services/sync/locales/en-US/log.properties new file mode 100644 index 000000000000..de6b5673aa5a --- /dev/null +++ b/services/sync/locales/en-US/log.properties @@ -0,0 +1,2 @@ +noLogAvailable.alert = No log available +filePicker.title = Choose Destination File From 91d2a5d8d18b9da64b7f94b53954cf8501f73a6d Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 21 Dec 2007 00:29:54 -0800 Subject: [PATCH 0107/1860] make login dialog localizable --- services/sync/locales/en-US/login.dtd | 7 +++++++ services/sync/locales/en-US/login.properties | 1 + 2 files changed, 8 insertions(+) create mode 100644 services/sync/locales/en-US/login.dtd create mode 100644 services/sync/locales/en-US/login.properties diff --git a/services/sync/locales/en-US/login.dtd b/services/sync/locales/en-US/login.dtd new file mode 100644 index 000000000000..ea6df3f84f0b --- /dev/null +++ b/services/sync/locales/en-US/login.dtd @@ -0,0 +1,7 @@ + + + + + + + diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties new file mode 100644 index 000000000000..3f2e6650d769 --- /dev/null +++ b/services/sync/locales/en-US/login.properties @@ -0,0 +1 @@ +loginFailed.alert = Login failed From 53d5074a6db62e1f9c5b83401dc090c04baf7cb4 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 21 Dec 2007 01:09:08 -0800 Subject: [PATCH 0108/1860] make prefpane localizable --- services/sync/locales/en-US/preferences.dtd | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 services/sync/locales/en-US/preferences.dtd diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd new file mode 100644 index 000000000000..6dee09c085fc --- /dev/null +++ b/services/sync/locales/en-US/preferences.dtd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 53e88e322a3485039dca2a414030a0ca240aa81e Mon Sep 17 00:00:00 2001 From: Date: Fri, 21 Dec 2007 13:34:43 -0800 Subject: [PATCH 0109/1860] UI clean up and refactoring --- services/sync/locales/en-US/preferences.dtd | 31 +++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 6dee09c085fc..3666c77215cf 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -5,14 +5,16 @@ - + - + + - + - + + @@ -22,20 +24,27 @@ - + - + - + - - - + + + + + + + - + + + + From 41de63fc3d54d4b5ffdc7841508132aa6206dad2 Mon Sep 17 00:00:00 2001 From: Date: Fri, 21 Dec 2007 15:08:47 -0800 Subject: [PATCH 0110/1860] refactoring of the setup wizard, fix for opening prefs pane to weave pane regardless of previous state --- services/sync/locales/en-US/wizard.dtd | 35 ++++++++++--------- services/sync/locales/en-US/wizard.properties | 16 ++++----- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 0ff03d3f5082..97fa48455b20 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,32 +1,35 @@ - + - - - + + + + + - - - + + + - - + + + - - + + - - + + - - + + - + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index f4cfb2c5808e..a7a8abe82dcc 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,10 +1,10 @@ -verifyStatusUnverified.label = Status: Unverified. -verifyStatusLoginVerified.label = Status: Login Verified. -verifyStatusLoginFailed.label = Status: Login Failed. +verifyStatusUnverified.label = Status: Unverified +verifyStatusLoginVerified.label = Status: Account Verified +verifyStatusLoginFailed.label = Status: Authentication Failed -initStatusReadyToSync.label = Status: Ready to Sync. -initStatusSyncing.label = Status: Syncing... -initStatusSyncComplete.label = Status: Sync Complete -initStatusSyncFailed.label = Status: Sync Failed +initStatusReadyToSync.label = Status: Ready to Transfer Data +initStatusSyncing.label = Status: Transfering Data... +initStatusSyncComplete.label = Status: Transfer Complete +initStatusSyncFailed.label = Status: Transfer Failed -invalidCredentials.alert = You must provide a valid user name and password to continue. +invalidCredentials.alert = You must provide a valid Weave user name and password to continue. From f426beb29315b494588faae4bf998a11e35083d6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Dec 2007 16:07:42 -0800 Subject: [PATCH 0111/1860] limit history sync to the last 500 items; load snapshots from the right directory in the profile --- services/sync/modules/engines.js | 2 ++ services/sync/modules/stores.js | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index dd0515bce55c..23b244ff2c56 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -503,6 +503,8 @@ Engine.prototype = { if (status.GUID != this._snapshot.GUID) { this._log.info("Remote/local sync GUIDs do not match. " + "Forcing initial sync."); + this._log.debug("Remote: " + status.GUID); + this._log.debug("Local: " + this._snapshot.GUID); this._store.resetGUIDs(); this._snapshot.data = {}; this._snapshot.version = -1; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 0d30114121a0..df1c6289f6e1 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -204,7 +204,8 @@ SnapshotStore.prototype = { load: function SStore_load() { let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("weave-snapshots"); + file.append("weave"); + file.append("snapshots"); file.append(this.filename); if (!file.exists()) @@ -605,8 +606,8 @@ HistoryStore.prototype = { }, _removeCommand: function HistStore__removeCommand(command) { - this._log.info(" -> removing history entry: " + command.GUID); - this._browserHist.removePage(command.GUID); + this._log.info(" -> NOT removing history entry: " + command.GUID); + //this._browserHist.removePage(command.GUID); }, _editCommand: function HistStore__editCommand(command) { @@ -619,6 +620,8 @@ HistoryStore.prototype = { options = this._hsvc.getNewQueryOptions(); query.minVisits = 1; + options.maxResults = 500; + options.sortingMode = query.SORT_BY_LASTMODIFIED_DESCENDING; options.queryType = options.QUERY_TYPE_HISTORY; let root = this._hsvc.executeQuery(query, options).root; From 16e462965b58d65e6836e3ca281b8344e1d71628 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Dec 2007 16:50:32 -0800 Subject: [PATCH 0112/1860] process GUID changes *before* anything else --- services/sync/modules/syncCores.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 7edf29ce524c..f3dd181ecf4f 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -268,7 +268,7 @@ SyncCore.prototype = { } listA = listA.filter(function(elt) { return elt }); - listB = listB.concat(guidChanges); + listB = guidChanges.concat(listB); for (let i = 0; i < listA.length; i++) { for (let j = 0; j < listB.length; j++) { From 9fee6041cd8c2ec42390ecd38af9f208a6d4d61c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Dec 2007 16:51:14 -0800 Subject: [PATCH 0113/1860] version bump --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index b6fb3c50ed1e..9b103be38471 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.12.7"; +const WEAVE_VERSION = "0.1.12.8"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 2fd530b32caa4bd5ec2a63f9290642a8017b66ac Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 21 Dec 2007 17:41:26 -0800 Subject: [PATCH 0114/1860] clean up preferences XUL and make Create Account button work on Windows and Linux --- services/sync/locales/en-US/preferences.dtd | 6 +++--- services/sync/locales/en-US/preferences.properties | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 services/sync/locales/en-US/preferences.properties diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 3666c77215cf..b0e41483e34b 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -8,7 +8,6 @@ - @@ -19,12 +18,13 @@ + - + - + diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties new file mode 100644 index 000000000000..f3f854d05f1a --- /dev/null +++ b/services/sync/locales/en-US/preferences.properties @@ -0,0 +1,2 @@ +# %S is the username of the signed in user +signedIn.description = Signed in as %S From 6ec960de06555f17870739bd342aac81b04efceb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Dec 2007 18:05:01 -0800 Subject: [PATCH 0115/1860] alert errors when the passphrase is empty --- services/sync/locales/en-US/login.properties | 3 +++ services/sync/locales/en-US/wizard.properties | 3 +++ 2 files changed, 6 insertions(+) diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties index 3f2e6650d769..decb241f8f0d 100644 --- a/services/sync/locales/en-US/login.properties +++ b/services/sync/locales/en-US/login.properties @@ -1 +1,4 @@ loginFailed.alert = Login failed +noPassword.alert = You must enter a password +noPassphrase.alert = You must enter a passphrase +samePasswordAndPassphrase.alert = Your password and passphrase must be different diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index a7a8abe82dcc..6392dc94bb32 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -8,3 +8,6 @@ initStatusSyncComplete.label = Status: Transfer Complete initStatusSyncFailed.label = Status: Transfer Failed invalidCredentials.alert = You must provide a valid Weave user name and password to continue. + +noPassphrase.alert = You must enter a passphrase +samePasswordAndPassphrase.alert = Your password and passphrase must be different From 007029e4a591ae7b640787ec5cbb9116edf5f3ee Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Dec 2007 18:38:04 -0800 Subject: [PATCH 0116/1860] version bump --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 9b103be38471..9370f781f53d 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.12.8"; +const WEAVE_VERSION = "0.1.12.9"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 5c0ffde265fc9927cd6631f86a77d6326b0143c7 Mon Sep 17 00:00:00 2001 From: Date: Mon, 24 Dec 2007 14:47:37 -0800 Subject: [PATCH 0117/1860] fix getTagsForURI call (API changed); bump version --- services/sync/modules/stores.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index df1c6289f6e1..f8421b333b9b 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -360,7 +360,7 @@ BookmarksStore.prototype = { item.title = node.title; } item.URI = node.uri; - item.tags = this._ts.getTagsForURI(makeURI(node.uri)); + item.tags = this._ts.getTagsForURI(makeURI(node.uri), {}); item.keyword = this._bms.getKeywordForBookmark(node.itemId); } else if (node.type == node.RESULT_TYPE_SEPARATOR) { item.type = "separator"; From 9ddfacaa98eefe9ae29ff217445259a965acbdfb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Dec 2007 14:49:03 -0800 Subject: [PATCH 0118/1860] Bug 409673: Don't log the authentication header --- services/sync/modules/dav.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index c57922bc1e20..fe8745bfbda7 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -105,7 +105,10 @@ DAVCollection.prototype = { let key; for (key in headers) { - this._log.debug("HTTP Header " + key + ": " + headers[key]); + if (key == 'Authentication') + this._log.debug("HTTP Header " + key + ": (supressed)"); + else + this._log.debug("HTTP Header " + key + ": " + headers[key]); request.setRequestHeader(key, headers[key]); } From 35ade8930335f04e6bf93ae626b754add46abf97 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Dec 2007 14:51:52 -0800 Subject: [PATCH 0119/1860] Fix typo in last commit; use asterisks in place of auth header --- services/sync/modules/dav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index fe8745bfbda7..a7867dbc34a3 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -106,7 +106,7 @@ DAVCollection.prototype = { let key; for (key in headers) { if (key == 'Authentication') - this._log.debug("HTTP Header " + key + ": (supressed)"); + this._log.debug("HTTP Header " + key + ": ***** (suppressed)"); else this._log.debug("HTTP Header " + key + ": " + headers[key]); request.setRequestHeader(key, headers[key]); From 6d7dd3c59d3148b9973cc6f1f75f59f729f20a22 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Dec 2007 14:56:52 -0800 Subject: [PATCH 0120/1860] Bug 409601: Ask for an email instead of a username (since Weave usernames are email addresses) --- services/sync/locales/en-US/login.dtd | 2 +- services/sync/locales/en-US/wizard.dtd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/login.dtd b/services/sync/locales/en-US/login.dtd index ea6df3f84f0b..501b5bb33b67 100644 --- a/services/sync/locales/en-US/login.dtd +++ b/services/sync/locales/en-US/login.dtd @@ -1,6 +1,6 @@ - + diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 97fa48455b20..592339da5d9a 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -16,7 +16,7 @@ - + From 98691f679cbe5d087d28016a6817fc27c4164cd3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Dec 2007 16:10:23 -0800 Subject: [PATCH 0121/1860] Pull xxxtea code into modules/; fix passphrase bug in the login dialog --- services/sync/modules/crypto.js | 72 ++++++++-------- services/sync/modules/engines.js | 30 ++++--- services/sync/modules/service.js | 4 + services/sync/modules/xxxtea.js | 138 +++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 46 deletions(-) create mode 100644 services/sync/modules/xxxtea.js diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 599f893d9e89..d0a1a250b469 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -64,9 +64,7 @@ WeaveCrypto.prototype = { __xxxteaLoaded: false, get _xxxtea() { if (!this.__xxxteaLoaded) { - let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. - getService(Ci.mozIJSSubScriptLoader); - jsLoader.loadSubScript("chrome://weave/content/encrypt.js", this.__xxxtea); + Cu.import("resource://weave/xxxtea.js", this.__xxxtea); this.__xxxteaLoaded = true; } return this.__xxxtea; @@ -129,48 +127,56 @@ WeaveCrypto.prototype = { // Crypto PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) { - let out; if (!algorithm) algorithm = this.defaultAlgorithm; - switch (algorithm) { - case "none": - out = data; - break; - case "XXXTEA": - try { - this._log.debug("Encrypting data"); + + if (algorithm == "none") // check to skip the 'encrypting data' log msgs + return data; + + let out; + try { + this._log.debug("Encrypting data"); + + switch (algorithm) { + case "XXXTEA": out = this._xxxtea.encrypt(data, identity.password); - this._log.debug("Done encrypting data"); - } catch (e) { - this._log.error("Data encryption failed: " + e); - throw 'encrypt failed'; + break; + default: + throw "Unknown encryption algorithm: " + algorithm; } - break; - default: - this._log.error("Unknown encryption algorithm: " + algorithm); + + this._log.debug("Done encrypting data"); + + } catch (e) { + this._log.error("Data encryption failed: " + e); throw 'encrypt failed'; } return out; }, PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) { + if (!algorithm) + algorithm = this.defaultAlgorithm; + + if (algorithm == "none") // check to skip the 'decrypting data' log msgs + return data; + let out; - switch (algorithm) { - case "none": - out = eval(data); - break; - case "XXXTEA": - try { - this._log.debug("Decrypting data"); - out = eval(this._xxxtea.decrypt(data, identity.password)); - this._log.debug("Done decrypting data"); - } catch (e) { - this._log.error("Data decryption failed: " + e); - throw 'decrypt failed'; + try { + this._log.debug("Decrypting data"); + + switch (algorithm) { + case "XXXTEA": + out = this._xxxtea.decrypt(data, identity.password); + break; + default: + throw "Unknown encryption algorithm: " + algorithm; } - break; - default: - this._log.error("Unknown encryption algorithm: " + algorithm); + + this._log.debug("Done decrypting data"); + + } catch (e) { + this._log.error("Data decryption failed: " + e); throw 'decrypt failed'; } return out; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 23b244ff2c56..31e3fc3416f9 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -519,18 +519,20 @@ Engine.prototype = { this._dav.GET(this.snapshotFile, cont); resp = yield; this._checkStatus(resp.status, "Could not download snapshot."); - snap.data = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.snapEncryption); + let data = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.snapEncryption); + snap.data = eval(data); this._log.info("Downloading server deltas"); this._dav.GET(this.deltasFile, cont); resp = yield; this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); - deltas = eval(uneval(allDeltas)); + data = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + allDeltas = eval(data); + deltas = eval(data); } else if (this._snapshot.version >= status.snapVersion && this._snapshot.version < status.maxVersion) { @@ -540,9 +542,10 @@ Engine.prototype = { this._dav.GET(this.deltasFile, cont); resp = yield; this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); + let data = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + allDeltas = eval(data); deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); } else if (this._snapshot.version == status.maxVersion) { @@ -553,9 +556,10 @@ Engine.prototype = { this._dav.GET(this.deltasFile, cont); resp = yield; this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); + let data = Crypto.PBEdecrypt(resp.responseText, + this._cryptoId, + status.deltasEncryption); + allDeltas = eval(data); deltas = []; } else { // this._snapshot.version > status.maxVersion diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 75067f4be60a..3a67637568c3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -314,6 +314,10 @@ WeaveSyncService.prototype = { return; } + this._log.debug("USERNAME: " + this.username); + this._log.debug("PASSWORD: " + this.password); + this._log.debug("PASSPHRASE: " + this.passphrase); + this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; this._log.info("Using server URL: " + this._dav.baseURL); diff --git a/services/sync/modules/xxxtea.js b/services/sync/modules/xxxtea.js new file mode 100644 index 000000000000..a6c8655743bf --- /dev/null +++ b/services/sync/modules/xxxtea.js @@ -0,0 +1,138 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Corrected Block TEA. + * + * The Initial Developer of the Original Code is + * Chris Veness + * Portions created by the Initial Developer are Copyright (C) 2005 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Veness + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// Original 'Corrected Block TEA' algorithm David Wheeler & Roger Needham +// See http://en.wikipedia.org/wiki/XXTEA +// +// Javascript version by Chris Veness +// http://www.movable-type.co.uk/scripts/tea.html + +const EXPORTED_SYMBOLS = ['encrypt', 'decrypt']; + +// use (16 chars of) 'password' to encrypt 'plaintext' + +function encrypt(plaintext, password) { + var v = new Array(2), k = new Array(4), s = "", i; + + plaintext = escape(plaintext); // use escape() so only have single-byte chars to encode + + // build key directly from 1st 16 chars of password + for (var i=0; i<4; i++) k[i] = Str4ToLong(password.slice(i*4,(i+1)*4)); + + for (i=0; i>>5)+z ^ sum+k[sum & 3]; + sum += delta; + z += (y<<4 ^ y>>>5)+y ^ sum+k[sum>>>11 & 3]; + // note: unsigned right-shift '>>>' is used in place of original '>>', due to lack + // of 'unsigned' type declaration in JavaScript (thanks to Karsten Kraus for this) + } + v[0] = y; v[1] = z; +} + +function decode(v, k) { + var y = v[0], z = v[1]; + var delta = 0x9E3779B9, sum = delta*32; + + while (sum != 0) { + z -= (y<<4 ^ y>>>5)+y ^ sum+k[sum>>>11 & 3]; + sum -= delta; + y -= (z<<4 ^ z>>>5)+z ^ sum+k[sum & 3]; + } + v[0] = y; v[1] = z; +} + + +// supporting functions + +function Str4ToLong(s) { // convert 4 chars of s to a numeric long + var v = 0; + for (var i=0; i<4; i++) v |= s.charCodeAt(i) << i*8; + return isNaN(v) ? 0 : v; +} + +function LongToStr4(v) { // convert a numeric long to 4 char string + var s = String.fromCharCode(v & 0xFF, v>>8 & 0xFF, v>>16 & 0xFF, v>>24 & 0xFF); + return s; +} + +function escCtrlCh(str) { // escape control chars which might cause problems with encrypted texts + return str.replace(/[\0\t\n\v\f\r\xa0'"!]/g, function(c) { return '!' + c.charCodeAt(0) + '!'; }); +} + +function unescCtrlCh(str) { // unescape potentially problematic nulls and control characters + return str.replace(/!\d\d?\d?!/g, function(c) { return String.fromCharCode(c.slice(1,-1)); }); +} From 12122a1c152b74761f847d5077659c8632ebb6a9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Dec 2007 16:11:19 -0800 Subject: [PATCH 0122/1860] remove debug statements --- services/sync/modules/service.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3a67637568c3..75067f4be60a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -314,10 +314,6 @@ WeaveSyncService.prototype = { return; } - this._log.debug("USERNAME: " + this.username); - this._log.debug("PASSWORD: " + this.password); - this._log.debug("PASSPHRASE: " + this.passphrase); - this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; this._log.info("Using server URL: " + this._dav.baseURL); From 9f73e9a0e46da3dd0564279e31d2d81f6856d247 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Dec 2007 17:40:46 -0800 Subject: [PATCH 0123/1860] Make {en,de}cryption asynchronous so as to not block the UI; fix 'xxxtea' typos (it's xxtea); fix auth header suppression --- services/sync/modules/crypto.js | 107 ++++++++++++++++++----------- services/sync/modules/dav.js | 2 +- services/sync/modules/engines.js | 44 +++++++----- services/sync/modules/syncCores.js | 4 +- services/sync/services-sync.js | 2 +- 5 files changed, 100 insertions(+), 59 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index d0a1a250b469..155fff6b32dd 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -46,6 +46,8 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Function.prototype.async = generatorAsync; + function WeaveCrypto() { this._init(); } @@ -60,14 +62,14 @@ WeaveCrypto.prototype = { return this.__os; }, - __xxxtea: {}, - __xxxteaLoaded: false, - get _xxxtea() { - if (!this.__xxxteaLoaded) { - Cu.import("resource://weave/xxxtea.js", this.__xxxtea); - this.__xxxteaLoaded = true; + __xxtea: {}, + __xxteaLoaded: false, + get _xxtea() { + if (!this.__xxteaLoaded) { + Cu.import("resource://weave/xxtea.js", this.__xxtea); + this.__xxteaLoaded = true; } - return this.__xxxtea; + return this.__xxtea; }, get defaultAlgorithm() { @@ -108,12 +110,13 @@ WeaveCrypto.prototype = { case "none": this._log.info("Encryption disabled"); break; - case "XXXTEA": + case "XXTEA": + case "XXXTEA": // Weave 0.1 had this typo this._log.info("Using encryption algorithm: " + data); break; default: this._log.warn("Unknown encryption algorithm, resetting"); - branch.setCharPref("extensions.weave.encryption", "XXXTEA"); + branch.setCharPref("extensions.weave.encryption", "XXTEA"); return; // otherwise we'll send the alg changed event twice } // FIXME: listen to this bad boy somewhere @@ -126,59 +129,85 @@ WeaveCrypto.prototype = { // Crypto - PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) { - if (!algorithm) - algorithm = this.defaultAlgorithm; + PBEencrypt: function Crypto_PBEencrypt(onComplete, data, identity, algorithm) { + let [self, cont] = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let ret; - if (algorithm == "none") // check to skip the 'encrypting data' log msgs - return data; - - let out; try { - this._log.debug("Encrypting data"); + if (!algorithm) + algorithm = this.defaultAlgorithm; switch (algorithm) { - case "XXXTEA": - out = this._xxxtea.encrypt(data, identity.password); + case "none": + ret = data; + case "XXTEA": + case "XXXTEA": // Weave 0.1.12.10 and below had this typo + this._log.debug("Encrypting data"); + let gen = this._xxtea.encrypt(data, identity.password); + ret = gen.next(); + while (typeof(ret) == "object") { + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + ret = gen.next(); + } + gen.close(); + this._log.debug("Done encrypting data"); break; default: throw "Unknown encryption algorithm: " + algorithm; } - this._log.debug("Done encrypting data"); - } catch (e) { - this._log.error("Data encryption failed: " + e); - throw 'encrypt failed'; + this._log.error("Exception caught: " + (e.message? e.message : e)); + + } finally { + timer = null; + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator } - return out; + this._log.warn("generator not properly closed"); }, - PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) { - if (!algorithm) - algorithm = this.defaultAlgorithm; + PBEdecrypt: function Crypto_PBEdecrypt(onComplete, data, identity, algorithm) { + let [self, cont] = yield; + let listener = new EventListener(cont); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let ret; - if (algorithm == "none") // check to skip the 'decrypting data' log msgs - return data; - - let out; try { - this._log.debug("Decrypting data"); + if (!algorithm) + algorithm = this.defaultAlgorithm; switch (algorithm) { - case "XXXTEA": - out = this._xxxtea.decrypt(data, identity.password); + case "none": + ret = data; + case "XXTEA": + case "XXXTEA": // Weave 0.1.12.10 and below had this typo + this._log.debug("Decrypting data"); + let gen = this._xxtea.decrypt(data, identity.password); + ret = gen.next(); + while (typeof(ret) == "object") { + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + ret = gen.next(); + } + gen.close(); + this._log.debug("Done decrypting data"); break; default: throw "Unknown encryption algorithm: " + algorithm; } - this._log.debug("Done decrypting data"); - } catch (e) { - this._log.error("Data decryption failed: " + e); - throw 'decrypt failed'; + this._log.error("Exception caught: " + (e.message? e.message : e)); + + } finally { + timer = null; + generatorDone(this, self, onComplete, ret); + yield; // onComplete is responsible for closing the generator } - return out; + this._log.warn("generator not properly closed"); } }; diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index a7867dbc34a3..6dc3f04d4ea6 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -105,7 +105,7 @@ DAVCollection.prototype = { let key; for (key in headers) { - if (key == 'Authentication') + if (key == 'Authorization') this._log.debug("HTTP Header " + key + ": ***** (suppressed)"); else this._log.debug("HTTP Header " + key + ": " + headers[key]); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 31e3fc3416f9..a24850f8f6c1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -387,8 +387,10 @@ Engine.prototype = { this._log.error("Could not upload files to server"); // eep? } else { - let data = Crypto.PBEencrypt(serializeCommands(server.deltas), - this._cryptoId); + Crypto.PBEencrypt.async(Crypto, cont, + serializeCommands(server.deltas), + this._cryptoId); + let data = yield; this._dav.PUT(this.deltasFile, data, cont); let deltasPut = yield; @@ -519,18 +521,22 @@ Engine.prototype = { this._dav.GET(this.snapshotFile, cont); resp = yield; this._checkStatus(resp.status, "Could not download snapshot."); - let data = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.snapEncryption); + Crypto.PBEdecrypt.async(Crypto, cont, + resp.responseText, + this._cryptoId, + status.snapEncryption); + let data = yield; snap.data = eval(data); this._log.info("Downloading server deltas"); this._dav.GET(this.deltasFile, cont); resp = yield; this._checkStatus(resp.status, "Could not download deltas."); - data = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); + Crypto.PBEdecrypt.async(Crypto, cont, + resp.responseText, + this._cryptoId, + status.deltasEncryption); + data = yield; allDeltas = eval(data); deltas = eval(data); @@ -542,9 +548,11 @@ Engine.prototype = { this._dav.GET(this.deltasFile, cont); resp = yield; this._checkStatus(resp.status, "Could not download deltas."); - let data = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); + Crypto.PBEdecrypt.async(Crypto, cont, + resp.responseText, + this._cryptoId, + status.deltasEncryption); + let data = yield; allDeltas = eval(data); deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); @@ -556,9 +564,11 @@ Engine.prototype = { this._dav.GET(this.deltasFile, cont); resp = yield; this._checkStatus(resp.status, "Could not download deltas."); - let data = Crypto.PBEdecrypt(resp.responseText, - this._cryptoId, - status.deltasEncryption); + Crypto.PBEdecrypt.async(Crypto, cont, + resp.responseText, + this._cryptoId, + status.deltasEncryption); + let data = yield; allDeltas = eval(data); deltas = []; @@ -632,8 +642,10 @@ Engine.prototype = { let ret = false; try { - let data = Crypto.PBEencrypt(this._snapshot.serialize(), - this._cryptoId); + Crypto.PBEencrypt.async(Crypto, cont, + this._snapshot.serialize(), + this._cryptoId); + let data = yield; this._dav.PUT(this.snapshotFile, data, cont); resp = yield; this._checkStatus(resp.status, "Could not upload snapshot."); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index f3dd181ecf4f..307c36cd7e7a 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -141,7 +141,7 @@ SyncCore.prototype = { }); } catch (e) { - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { timer = null; @@ -297,7 +297,7 @@ SyncCore.prototype = { ret = {propagations: propagations, conflicts: conflicts}; } catch (e) { - this._log.error("Exception caught: " + e.message); + this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { timer = null; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index a642a3dcb9f7..c6634b153b3c 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -8,4 +8,4 @@ pref("extensions.weave.bookmarks", true); pref("extensions.weave.history", true); pref("extensions.weave.schedule", 1); pref("extensions.weave.lastsync", "0"); -pref("extensions.weave.encryption", "XXXTEA"); +pref("extensions.weave.encryption", "XXTEA"); From a93becbae968460e0e8a64d6723b610e46290c9c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 27 Dec 2007 12:50:51 -0800 Subject: [PATCH 0124/1860] move xxxtea.js -> xxtea.js (typo) --- services/sync/modules/{xxxtea.js => xxtea.js} | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) rename services/sync/modules/{xxxtea.js => xxtea.js} (76%) diff --git a/services/sync/modules/xxxtea.js b/services/sync/modules/xxtea.js similarity index 76% rename from services/sync/modules/xxxtea.js rename to services/sync/modules/xxtea.js index a6c8655743bf..4323c2f8283a 100644 --- a/services/sync/modules/xxxtea.js +++ b/services/sync/modules/xxtea.js @@ -43,26 +43,46 @@ const EXPORTED_SYMBOLS = ['encrypt', 'decrypt']; +function Paused() { +} +Paused.prototype = { + toString: function Paused_toString() { + return "[Generator Paused]"; + } +} + // use (16 chars of) 'password' to encrypt 'plaintext' +// +// note1:this is a generator so the caller can pause and give control +// to the UI thread +// +// note2: if plaintext or password are passed as string objects, rather +// than strings, this function will throw an 'Object doesn't support +// this property or method' error function encrypt(plaintext, password) { var v = new Array(2), k = new Array(4), s = "", i; - plaintext = escape(plaintext); // use escape() so only have single-byte chars to encode + // use escape() so only have single-byte chars to encode + plaintext = escape(plaintext); // build key directly from 1st 16 chars of password - for (var i=0; i<4; i++) k[i] = Str4ToLong(password.slice(i*4,(i+1)*4)); + for (i = 0; i < 4; i++) + k[i] = Str4ToLong(password.slice(i * 4, (i + 1) * 4)); - for (i=0; i Date: Fri, 28 Dec 2007 16:33:09 -0800 Subject: [PATCH 0125/1860] Bug 409908: don't decrypt cleartext. Also, fix broken checkStatus calls when resetting server data. --- services/sync/modules/crypto.js | 1 + services/sync/modules/engines.js | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 155fff6b32dd..c351f093833c 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -183,6 +183,7 @@ WeaveCrypto.prototype = { switch (algorithm) { case "none": ret = data; + break; case "XXTEA": case "XXXTEA": // Weave 0.1.12.10 and below had this typo this._log.debug("Decrypting data"); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a24850f8f6c1..fe1555caae98 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -149,9 +149,12 @@ Engine.prototype = { this._dav.unlock.async(this._dav, cont); let unlocked = yield; - checkStatus(statusResp.status, "Could not delete status file.", true); - checkStatus(snapshotResp.status, "Could not delete snapshot file.", true); - checkStatus(deltasResp.status, "Could not delete deltas file.", true); + this._checkStatus(statusResp.status, + "Could not delete status file.", true); + this._checkStatus(snapshotResp.status, + "Could not delete snapshot file.", true); + this._checkStatus(deltasResp.status, + "Could not delete deltas file.", true); this._log.debug("Server files deleted"); done = true; From 148dc482194a4c2056365711b04e1fa609f71322 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 28 Dec 2007 16:34:28 -0800 Subject: [PATCH 0126/1860] bump version in constants.js --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 9370f781f53d..59b07eab65b4 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.12.9"; +const WEAVE_VERSION = "0.1.13"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From a0f225dba31f5354772e32a6f25363fb96026961 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 7 Jan 2008 16:28:57 -0800 Subject: [PATCH 0127/1860] load all modules directly from load-weave.js to get around bug 408412; fix uses of |let| not directly within a block --- services/sync/modules/crypto.js | 12 ++++++------ services/sync/modules/engines.js | 12 ++++++------ services/sync/modules/stores.js | 16 ++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index c351f093833c..81eb40bb40e0 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -98,7 +98,7 @@ WeaveCrypto.prototype = { observe: function Sync_observe(subject, topic, data) { switch (topic) { - case "extensions.weave.encryption": + case "extensions.weave.encryption": { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); @@ -121,7 +121,7 @@ WeaveCrypto.prototype = { } // FIXME: listen to this bad boy somewhere this._os.notifyObservers(null, "weave:encryption:algorithm-changed", ""); - break; + } break; default: this._log.warn("Unknown encryption preference changed - ignoring"); } @@ -142,8 +142,8 @@ WeaveCrypto.prototype = { switch (algorithm) { case "none": ret = data; - case "XXTEA": case "XXXTEA": // Weave 0.1.12.10 and below had this typo + case "XXTEA": { this._log.debug("Encrypting data"); let gen = this._xxtea.encrypt(data, identity.password); ret = gen.next(); @@ -154,7 +154,7 @@ WeaveCrypto.prototype = { } gen.close(); this._log.debug("Done encrypting data"); - break; + } break; default: throw "Unknown encryption algorithm: " + algorithm; } @@ -184,8 +184,8 @@ WeaveCrypto.prototype = { case "none": ret = data; break; - case "XXTEA": case "XXXTEA": // Weave 0.1.12.10 and below had this typo + case "XXTEA": { this._log.debug("Decrypting data"); let gen = this._xxtea.decrypt(data, identity.password); ret = gen.next(); @@ -196,7 +196,7 @@ WeaveCrypto.prototype = { } gen.close(); this._log.debug("Done decrypting data"); - break; + } break; default: throw "Unknown encryption algorithm: " + algorithm; } diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index fe1555caae98..3f56abc12907 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -485,7 +485,7 @@ Engine.prototype = { let status = resp.status; switch (status) { - case 200: + case 200: { this._log.info("Got status file from server"); let status = eval(resp.responseText); @@ -594,9 +594,9 @@ Engine.prototype = { ret.deltas = allDeltas; this._core.detectUpdates(cont, this._snapshot.data, snap.data); ret.updates = yield; - break; - - case 404: + } break; + + case 404: { this._log.info("Server has no status file, Initial upload to server"); this._snapshot.data = this._store.wrap(); @@ -620,8 +620,8 @@ Engine.prototype = { ret.snapshot = eval(uneval(this._snapshot.data)); ret.deltas = []; ret.updates = []; - break; - + } break; + default: this._log.error("Could not get status file: unknown HTTP status code " + status); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index f8421b333b9b..bc09a0f4a6ee 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -402,7 +402,7 @@ BookmarksStore.prototype = { switch (command.data.type) { case "query": case "bookmark": - case "microsummary": + case "microsummary": { this._log.info(" -> creating bookmark \"" + command.data.title + "\""); let URI = makeURI(command.data.URI); newId = this._bms.insertBookmark(parentId, @@ -422,7 +422,7 @@ BookmarksStore.prototype = { } catch(ex) { /* ignore "missing local generator" exceptions */ } } - break; + } break; case "folder": this._log.info(" -> creating folder \"" + command.data.title + "\""); newId = this._bms.createFolder(parentId, @@ -504,27 +504,27 @@ BookmarksStore.prototype = { this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), command.data.index); break; - case "parentGUID": + case "parentGUID": { let index = -1; if (command.data.index && command.data.index >= 0) index = command.data.index; this._bms.moveItem( itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); - break; - case "tags": + } break; + case "tags": { let tagsURI = this._bms.getBookmarkURI(itemId); this._ts.untagURI(URI, null); this._ts.tagURI(tagsURI, command.data.tags); - break; + } break; case "keyword": this._bms.setKeywordForBookmark(itemId, command.data.keyword); break; - case "generatorURI": + case "generatorURI": { let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); let genURI = makeURI(command.data.generatorURI); let micsum = this._ms.createMicrosummary(micsumURI, genURI); this._ms.setMicrosummary(itemId, micsum); - break; + } break; case "siteURI": this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); break; From e78212c91da7c44339ecb8db606712aca11efa53 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 7 Jan 2008 16:29:30 -0800 Subject: [PATCH 0128/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 59b07eab65b4..3a76ae38d671 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.13"; +const WEAVE_VERSION = "0.1.14"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From c84e99c87a7327fe02f9311268d589af433d2014 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 7 Jan 2008 20:20:54 -0800 Subject: [PATCH 0129/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 3a76ae38d671..08f88731d411 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.14"; +const WEAVE_VERSION = "0.1.15"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 6fc4228502a7ff23df7115631afa375978465b0f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 22 Jan 2008 22:46:07 -0800 Subject: [PATCH 0130/1860] Bug 411105: nsILoginInfo no longer accepts null values for usernameField/passwordField. Send empty strings instead --- services/sync/modules/identity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index d309e0c882d8..2cec498dd883 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -112,6 +112,6 @@ function setPassword(realm, username, password) { let nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); let login = new nsLoginInfo('chrome://sync', null, realm, - username, password, null, null); + username, password, "", ""); lm.addLogin(login); } From 845b75decc1a1ffba476006c8f217eb647f80b04 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 22 Jan 2008 22:46:39 -0800 Subject: [PATCH 0131/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 08f88731d411..e193e0965ab9 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.15"; +const WEAVE_VERSION = "0.1.16"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 1315edb13bbc20243f0f3d0f94fe359cb8d8d3e6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 23 Jan 2008 15:35:11 -0800 Subject: [PATCH 0132/1860] remove obsolete xpcom component code --- services/sync/BookmarksSyncService.js | 2615 ------------------------- 1 file changed, 2615 deletions(-) delete mode 100644 services/sync/BookmarksSyncService.js diff --git a/services/sync/BookmarksSyncService.js b/services/sync/BookmarksSyncService.js deleted file mode 100644 index cad33d41ec85..000000000000 --- a/services/sync/BookmarksSyncService.js +++ /dev/null @@ -1,2615 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -const MODE_RDONLY = 0x01; -const MODE_WRONLY = 0x02; -const MODE_CREATE = 0x08; -const MODE_APPEND = 0x10; -const MODE_TRUNCATE = 0x20; - -const PERMS_FILE = 0644; -const PERMS_DIRECTORY = 0755; - -const STORAGE_FORMAT_VERSION = 2; - -const ONE_BYTE = 1; -const ONE_KILOBYTE = 1024 * ONE_BYTE; -const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -/* - * Service object - * Implements IBookmarksSyncService, main entry point - */ - -function BookmarksSyncService() { this._init(); } -BookmarksSyncService.prototype = { - - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - __dirSvc: null, - get _dirSvc() { - if (!this.__dirSvc) - this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - return this.__dirSvc; - }, - - __dav: null, - get _dav() { - if (!this.__dav) - this.__dav = new DAVCollection(); - return this.__dav; - }, - - __sync: null, - get _sync() { - if (!this.__sync) - this.__sync = new BookmarksSyncCore(); - return this.__sync; - }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new BookmarksStore(); - return this.__store; - }, - - __snapshot: null, - get _snapshot() { - if (!this.__snapshot) - this.__snapshot = new SnapshotStore(); - return this.__snapshot; - }, - set _snapshot(value) { - this.__snapshot = value; - }, - - // Logger object - _log: null, - - // Timer object for automagically syncing - _scheduleTimer: null, - - __encrypter: {}, - __encrypterLoaded: false, - get _encrypter() { - if (!this.__encrypterLoaded) { - let jsLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. - getService(Ci.mozIJSSubScriptLoader); - jsLoader.loadSubScript("chrome://weave/content/encrypt.js", this.__encrypter); - this.__encrypterLoaded = true; - } - return this.__encrypter; - }, - - - get username() { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - return branch.getCharPref("browser.places.sync.username"); - }, - set username(value) { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - return branch.setCharPref("browser.places.sync.username", value); - }, - - _lmGet: function BSS__lmGet(realm) { - // fixme: make a request and get the realm - let password; - let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://sync', null, realm); - - for (let i = 0; i < logins.length; i++) { - if (logins[i].username == this.username) { - password = logins[i].password; - break; - } - } - return password; - }, - - _lmSet: function BSS__lmSet(realm, password) { - // cleanup any existing passwords - let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://sync', null, realm); - for(let i = 0; i < logins.length; i++) { - lm.removeLogin(logins[i]); - } - - if (!password) - return; - - // save the new one - let nsLoginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new nsLoginInfo('chrome://sync', null, realm, - this.username, password, null, null); - lm.addLogin(login); - }, - - _password: null, - get password() { - return this._lmGet('Mozilla Services Password'); - }, - set password(value) { - if (this._password === null) - return this._lmSet('Mozilla Services Password', value); - return this._password; - }, - - _passphrase: null, - get passphrase() { - if (this._passphrase === null) - return this._lmGet('Mozilla Services Encryption Passphrase'); - return this._passphrase; - }, - set passphrase(value) { - this._lmSet('Mozilla Services Encryption Passphrase', value); - }, - - get userPath() { - this._log.info("Hashing username " + this.username); - - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA1); - - let data = converter.convertToByteArray(this.username, {}); - hasher.update(data, data.length); - let rawHash = hasher.finish(false); - - // return the two-digit hexadecimal code for a byte - function toHexString(charCode) { - return ("0" + charCode.toString(16)).slice(-2); - } - - let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); - this._log.debug("Username hashes to " + hash); - return hash; - }, - - get currentUser() { - if (this._dav.loggedIn) - return this.username; - return null; - }, - - // FIXME: listen for pref changes - _encryptionChanged: false, - get encryption() { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - return branch.getCharPref("browser.places.sync.encryption"); - }, - set encryption(value) { - switch (value) { - case "XXXTEA": - case "none": - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - let cur = branch.getCharPref("browser.places.sync.encryption"); - if (value != cur) { - this._encryptionChanged = true; - branch.setCharPref("browser.places.sync.encryption", value); - } - break; - default: - throw "Invalid encryption value: " + value; - } - }, - - _init: function BSS__init() { - this._initLogs(); - this._log.info("Bookmarks Sync Service Initializing"); - - this._serverURL = 'https://services.mozilla.com/'; - this._user = ''; - let enabled = false; - let schedule = 0; - try { - let branch = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch); - this._serverURL = branch.getCharPref("browser.places.sync.serverURL"); - enabled = branch.getBoolPref("browser.places.sync.enabled"); - schedule = branch.getIntPref("browser.places.sync.schedule"); - - branch.addObserver("browser.places.sync", this, false); - } - catch (ex) { /* use defaults */ } - - this._snapshot.load(); - - if (!enabled) { - this._log.info("Bookmarks sync disabled"); - return; - } - - switch (schedule) { - case 0: - this._log.info("Bookmarks sync enabled, manual mode"); - break; - case 1: - this._log.info("Bookmarks sync enabled, automagic mode"); - this._enableSchedule(); - break; - default: - this._log.info("Bookmarks sync enabled"); - this._log.info("Invalid schedule setting: " + schedule); - break; - } - }, - - _enableSchedule: function BSS__enableSchedule() { - this._scheduleTimer = Cc["@mozilla.org/timer;1"]. - createInstance(Ci.nsITimer); - let listener = new EventListener(bind2(this, this._onSchedule)); - this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min - this._scheduleTimer.TYPE_REPEATING_SLACK); - }, - - _disableSchedule: function BSS__disableSchedule() { - this._scheduleTimer = null; - }, - - _onSchedule: function BSS__onSchedule() { - this._log.info("Running scheduled sync"); - this.sync(); - }, - - _initLogs: function BSS__initLogs() { - let logSvc = Cc["@mozilla.org/log4moz/service;1"]. - getService(Ci.ILog4MozService); - - this._log = logSvc.getLogger("Service.Main"); - - let formatter = logSvc.newFormatter("basic"); - let root = logSvc.rootLogger; - root.level = root.LEVEL_DEBUG; - - let capp = logSvc.newAppender("console", formatter); - capp.level = root.LEVEL_WARN; - root.addAppender(capp); - - let dapp = logSvc.newAppender("dump", formatter); - dapp.level = root.LEVEL_ALL; - root.addAppender(dapp); - - let logFile = this._dirSvc.get("ProfD", Ci.nsIFile); - let verboseFile = logFile.clone(); - logFile.append("bm-sync.log"); - logFile.QueryInterface(Ci.nsILocalFile); - verboseFile.append("bm-sync-verbose.log"); - verboseFile.QueryInterface(Ci.nsILocalFile); - - let fapp = logSvc.newFileAppender("rotating", logFile, formatter); - fapp.level = root.LEVEL_INFO; - root.addAppender(fapp); - let vapp = logSvc.newFileAppender("rotating", verboseFile, formatter); - vapp.level = root.LEVEL_DEBUG; - root.addAppender(vapp); - }, - - _lock: function BSS__lock() { - if (this._locked) { - this._log.warn("Service lock failed: already locked"); - return false; - } - this._locked = true; - this._log.debug("Service lock acquired"); - return true; - }, - - _unlock: function BSS__unlock() { - this._locked = false; - this._log.debug("Service lock released"); - }, - - // IBookmarksSyncService internal implementation - - _login: function BSS__login(onComplete) { - let [self, cont] = yield; - let success = false; - let svcLock = this._lock(); - - try { - if (!svcLock) - return; - this._log.debug("Logging in"); - this._os.notifyObservers(null, "bookmarks-sync:login-start", ""); - - if (!this.username) { - this._log.warn("No username set, login failed"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - return; - } - if (!this.password) { - this._log.warn("No password given or found in password manager"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - return; - } - - this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; - this._log.info("Using server URL: " + this._dav.baseURL); - - this._dav.login.async(this._dav, cont, this.username, this.password); - success = yield; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - this._passphrase = null; - if (success) { - this._log.debug("Login successful"); - this._os.notifyObservers(null, "bookmarks-sync:login-end", ""); - } else { - this._log.debug("Login error"); - this._os.notifyObservers(null, "bookmarks-sync:login-error", ""); - } - if (svcLock) - this._unlock(); - generatorDone(this, self, onComplete, success); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - // original - // / \ - // A / \ B - // / \ - // client --C-> server - // \ / - // D \ / C - // \ / - // final - - // If we have a saved snapshot, original == snapshot. Otherwise, - // it's the empty set {}. - - // C is really the diff between server -> final, so if we determine - // D we can calculate C from that. In the case where A and B have - // no conflicts, C == A and D == B. - - // Sync flow: - // 1) Fetch server deltas - // 1.1) Construct current server status from snapshot + server deltas - // 1.2) Generate single delta from snapshot -> current server status ("B") - // 2) Generate local deltas from snapshot -> current client status ("A") - // 3) Reconcile client/server deltas and generate new deltas for them. - // Reconciliation won't generate C directly, we will simply diff - // server->final after step 3.1. - // 3.1) Apply local delta with server changes ("D") - // 3.2) Append server delta to the delta file and upload ("C") - - _doSync: function BSS__doSync(onComplete) { - let [self, cont] = yield; - let synced = false; - let locked = null; - let svcLock = this._lock(); - - try { - if (!svcLock) - return; - this._log.info("Beginning sync"); - this._os.notifyObservers(null, "bookmarks-sync:sync-start", ""); - - this._dav.lock.async(this._dav, cont); - locked = yield; - - if (locked) - this._log.info("Lock acquired"); - else { - this._log.warn("Could not acquire lock, aborting sync"); - return; - } - - // 1) Fetch server deltas - this._getServerData.async(this, cont); - let server = yield; - - this._log.info("Local snapshot version: " + this._snapshot.version); - this._log.info("Server status: " + server.status); - this._log.info("Server maxVersion: " + server.maxVersion); - this._log.info("Server snapVersion: " + server.snapVersion); - - if (server.status != 0) { - this._log.fatal("Sync error: could not get server status, " + - "or initial upload failed. Aborting sync."); - return; - } - - // 2) Generate local deltas from snapshot -> current client status - - let localJson = new SnapshotStore(); - localJson.data = this._store.wrap(); - this._sync.detectUpdates(cont, this._snapshot.data, localJson.data); - let localUpdates = yield; - - this._log.debug("local json:\n" + localJson.serialize()); - this._log.debug("Local updates: " + serializeCommands(localUpdates)); - this._log.debug("Server updates: " + serializeCommands(server.updates)); - - if (server.updates.length == 0 && localUpdates.length == 0) { - this._snapshot.version = server.maxVersion; - this._log.info("Sync complete (1): no changes needed on client or server"); - synced = true; - return; - } - - // 3) Reconcile client/server deltas and generate new deltas for them. - - this._log.info("Reconciling client/server updates"); - this._sync.reconcile(cont, localUpdates, server.updates); - let ret = yield; - - let clientChanges = ret.propagations[0]; - let serverChanges = ret.propagations[1]; - let clientConflicts = ret.conflicts[0]; - let serverConflicts = ret.conflicts[1]; - - this._log.info("Changes for client: " + clientChanges.length); - this._log.info("Predicted changes for server: " + serverChanges.length); - this._log.info("Client conflicts: " + clientConflicts.length); - this._log.info("Server conflicts: " + serverConflicts.length); - this._log.debug("Changes for client: " + serializeCommands(clientChanges)); - this._log.debug("Predicted changes for server: " + serializeCommands(serverChanges)); - this._log.debug("Client conflicts: " + serializeConflicts(clientConflicts)); - this._log.debug("Server conflicts: " + serializeConflicts(serverConflicts)); - - if (!(clientChanges.length || serverChanges.length || - clientConflicts.length || serverConflicts.length)) { - this._log.info("Sync complete (2): no changes needed on client or server"); - this._snapshot.data = localJson.data; - this._snapshot.version = server.maxVersion; - this._snapshot.save(); - synced = true; - return; - } - - if (clientConflicts.length || serverConflicts.length) { - this._log.warn("Conflicts found! Discarding server changes"); - } - - let savedSnap = eval(uneval(this._snapshot.data)); - let savedVersion = this._snapshot.version; - let newSnapshot; - - // 3.1) Apply server changes to local store - if (clientChanges.length) { - this._log.info("Applying changes locally"); - // Note that we need to need to apply client changes to the - // current tree, not the saved snapshot - - localJson.applyCommands(clientChanges); - this._snapshot.data = localJson.data; - this._snapshot.version = server.maxVersion; - this._store.applyCommands(clientChanges); - newSnapshot = this._store.wrap(); - - this._sync.detectUpdates(cont, this._snapshot.data, newSnapshot); - let diff = yield; - if (diff.length != 0) { - this._log.warn("Commands did not apply correctly"); - this._log.debug("Diff from snapshot+commands -> " + - "new snapshot after commands:\n" + - serializeCommands(diff)); - // FIXME: do we really want to revert the snapshot here? - this._snapshot.data = eval(uneval(savedSnap)); - this._snapshot.version = savedVersion; - } - - this._snapshot.save(); - } - - // 3.2) Append server delta to the delta file and upload - - // Generate a new diff, from the current server snapshot to the - // current client snapshot. In the case where there are no - // conflicts, it should be the same as what the resolver returned - - newSnapshot = this._store.wrap(); - this._sync.detectUpdates(cont, server.snapshot, newSnapshot); - let serverDelta = yield; - - // Log an error if not the same - if (!(serverConflicts.length || - deepEquals(serverChanges, serverDelta))) - this._log.warn("Predicted server changes differ from " + - "actual server->client diff (can be ignored in many cases)"); - - this._log.info("Actual changes for server: " + serverDelta.length); - this._log.debug("Actual changes for server: " + - serializeCommands(serverDelta)); - - if (serverDelta.length) { - this._log.info("Uploading changes to server"); - - this._snapshot.data = newSnapshot; - this._snapshot.version = ++server.maxVersion; - - server.deltas.push(serverDelta); - - if (server.formatVersion != STORAGE_FORMAT_VERSION || - this._encryptionChanged) { - this._fullUpload.async(this, cont); - let status = yield; - if (!status) - this._log.error("Could not upload files to server"); // eep? - - } else { - let data; - if (this.encryption == "none") { - data = serializeCommands(server.deltas); - } else if (this.encryption == "XXXTEA") { - this._log.debug("Encrypting snapshot"); - data = this._encrypter.encrypt(uneval(server.deltas), this.passphrase); - this._log.debug("Done encrypting snapshot"); - } else { - this._log.error("Unknown encryption scheme: " + this.encryption); - return; - } - this._dav.PUT("bookmarks-deltas.json", data, cont); - let deltasPut = yield; - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshot.version, - snapEncryption: server.snapEncryption, - deltasEncryption: this.encryption, - bookmarksCount: c}), cont); - let statusPut = yield; - - if (deltasPut.status >= 200 && deltasPut.status < 300 && - statusPut.status >= 200 && statusPut.status < 300) { - this._log.info("Successfully updated deltas and status on server"); - this.snapshot.save(); - } else { - // FIXME: revert snapshot here? - can't, we already applied - // updates locally! - need to save and retry - this._log.error("Could not update deltas on server"); - } - } - } - - this._log.info("Sync complete"); - synced = true; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - let ok = false; - if (locked) { - this._dav.unlock.async(this._dav, cont); - ok = yield; - } - if (ok && synced) { - this._os.notifyObservers(null, "bookmarks-sync:sync-end", ""); - generatorDone(this, self, onComplete, true); - } else { - this._os.notifyObservers(null, "bookmarks-sync:sync-error", ""); - generatorDone(this, self, onComplete, false); - } - if (svcLock) - this._unlock(); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _checkStatus: function BSS__checkStatus(code, msg) { - if (code >= 200 && code < 300) - return; - this._log.error(msg + " Error code: " + code); - throw 'checkStatus failed'; - }, - - _decrypt: function BSS__decrypt(alg, data) { - let out; - switch (alg) { - case "XXXTEA": - try { - this._log.debug("Decrypting data"); - out = eval(this._encrypter.decrypt(data, this.passphrase)); - this._log.debug("Done decrypting data"); - } catch (e) { - this._log.error("Could not decrypt server snapshot"); - throw 'decrypt failed'; - } - break; - case "none": - out = eval(data); - break; - default: - this._log.error("Unknown encryption algorithm: " + alg); - throw 'decrypt failed'; - } - return out; - }, - - /* Get the deltas/combined updates from the server - * Returns: - * status: - * -1: error - * 0: ok - * These fields may be null when status is -1: - * formatVersion: - * version of the data format itself. For compatibility checks. - * maxVersion: - * the latest version on the server - * snapVersion: - * the version of the current snapshot on the server (deltas not applied) - * snapEncryption: - * encryption algorithm currently used on the server-stored snapshot - * deltasEncryption: - * encryption algorithm currently used on the server-stored deltas - * snapshot: - * full snapshot of the latest server version (deltas applied) - * deltas: - * all of the individual deltas on the server - * updates: - * the relevant deltas (from our snapshot version to current), - * combined into a single set. - */ - _getServerData: function BSS__getServerData(onComplete) { - let [self, cont] = yield; - let ret = {status: -1, - formatVersion: null, maxVersion: null, snapVersion: null, - snapEncryption: null, deltasEncryption: null, - snapshot: null, deltas: null, updates: null}; - - try { - this._log.info("Getting bookmarks status from server"); - this._dav.GET("bookmarks-status.json", cont); - let resp = yield; - let status = resp.status; - - switch (status) { - case 200: - this._log.info("Got bookmarks status from server"); - - let status = eval(resp.responseText); - let deltas, allDeltas; - let snap = new SnapshotStore(); - - // Bail out if the server has a newer format version than we can parse - if (status.formatVersion > STORAGE_FORMAT_VERSION) { - this._log.error("Server uses storage format v" + status.formatVersion + - ", this client understands up to v" + STORAGE_FORMAT_VERSION); - generatorDone(this, self, onComplete, ret) - return; - } - - if (status.formatVersion == 0) { - ret.snapEncryption = status.snapEncryption = "none"; - ret.deltasEncryption = status.deltasEncryption = "none"; - } - - if (status.GUID != this._snapshot.GUID) { - this._log.info("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._store.resetGUIDs(); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.GUID = status.GUID; - } - - if (this._snapshot.version < status.snapVersion) { - if (this._snapshot.version >= 0) - this._log.info("Local snapshot is out of date"); - - this._log.info("Downloading server snapshot"); - this._dav.GET("bookmarks-snapshot.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download snapshot."); - snap.data = this._decrypt(status.snapEncryption, resp.responseText); - - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); - deltas = eval(uneval(allDeltas)); - - } else if (this._snapshot.version >= status.snapVersion && - this._snapshot.version < status.maxVersion) { - snap.data = eval(uneval(this._snapshot.data)); - - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); - deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); - - } else if (this._snapshot.version == status.maxVersion) { - snap.data = eval(uneval(this._snapshot.data)); - - // FIXME: could optimize this case by caching deltas file - this._log.info("Downloading server deltas"); - this._dav.GET("bookmarks-deltas.json", cont); - resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); - allDeltas = this._decrypt(status.deltasEncryption, resp.responseText); - deltas = []; - - } else { // this._snapshot.version > status.maxVersion - this._log.error("Server snapshot is older than local snapshot"); - return; - } - - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands(deltas[i]); - } - - ret.status = 0; - ret.formatVersion = status.formatVersion; - ret.maxVersion = status.maxVersion; - ret.snapVersion = status.snapVersion; - ret.snapEncryption = status.snapEncryption; - ret.deltasEncryption = status.deltasEncryption; - ret.snapshot = snap.data; - ret.deltas = allDeltas; - this._sync.detectUpdates(cont, this._snapshot.data, snap.data); - ret.updates = yield; - break; - - case 404: - this._log.info("Server has no status file, Initial upload to server"); - - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - - this._fullUpload.async(this, cont); - let uploadStatus = yield; - if (!uploadStatus) - return; - - this._log.info("Initial upload to server successful"); - this.snapshot.save(); - - ret.status = 0; - ret.formatVersion = STORAGE_FORMAT_VERSION; - ret.maxVersion = this._snapshot.version; - ret.snapVersion = this._snapshot.version; - ret.snapEncryption = this.encryption; - ret.deltasEncryption = this.encryption; - ret.snapshot = eval(uneval(this._snapshot.data)); - ret.deltas = []; - ret.updates = []; - break; - - default: - this._log.error("Could not get bookmarks.status: unknown HTTP status code " + - status); - break; - } - - } catch (e) { - if (e != 'checkStatus failed' && - e != 'decrypt failed') - this._log.error("Exception caught: " + e.message); - - } finally { - generatorDone(this, self, onComplete, ret) - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _fullUpload: function BSS__fullUpload(onComplete) { - let [self, cont] = yield; - let ret = false; - - try { - let data; - if (this.encryption == "none") { - data = this._snapshot.serialize(); - } else if (this.encryption == "XXXTEA") { - this._log.debug("Encrypting snapshot"); - data = this._encrypter.encrypt(uneval(this._snapshot.data), this.passphrase); - this._log.debug("Done encrypting snapshot"); - } else { - this._log.error("Unknown encryption scheme: " + this.encryption); - return; - } - this._dav.PUT("bookmarks-snapshot.json", data, cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload snapshot."); - - this._dav.PUT("bookmarks-deltas.json", uneval([]), cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload deltas."); - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._dav.PUT("bookmarks-status.json", - uneval({GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshot.version, - maxVersion: this._snapshot.version, - snapEncryption: this.encryption, - deltasEncryption: "none", - bookmarksCount: c}), cont); - resp = yield; - this._checkStatus(resp.status, "Could not upload status file."); - - this._log.info("Full upload to server successful"); - ret = true; - - } catch (e) { - if (e != 'checkStatus failed') - this._log.error("Exception caught: " + e.message); - - } finally { - generatorDone(this, self, onComplete, ret) - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _resetLock: function BSS__resetLock(onComplete) { - let [self, cont] = yield; - let success = false; - let svcLock = this._lock(); - - try { - if (!svcLock) - return; - this._log.debug("Resetting server lock"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-start", ""); - - this._dav.forceUnlock.async(this._dav, cont); - success = yield; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (success) { - this._log.debug("Server lock reset successful"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-end", ""); - } else { - this._log.debug("Server lock reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:lock-reset-error", ""); - } - if (svcLock) - this._unlock(); - generatorDone(this, self, onComplete, success); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _resetServer: function BSS__resetServer(onComplete) { - let [self, cont] = yield; - let done = false; - let svcLock = this._lock(); - - try { - if (!svcLock) - return; - this._log.debug("Resetting server data"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-start", ""); - - this._dav.lock.async(this._dav, cont); - let locked = yield; - if (locked) - this._log.debug("Lock acquired"); - else { - this._log.warn("Could not acquire lock, aborting server reset"); - return; - } - - this._dav.DELETE("bookmarks-status.json", cont); - let statusResp = yield; - this._dav.DELETE("bookmarks-snapshot.json", cont); - let snapshotResp = yield; - this._dav.DELETE("bookmarks-deltas.json", cont); - let deltasResp = yield; - - this._dav.unlock.async(this._dav, cont); - let unlocked = yield; - - function ok(code) { - if (code >= 200 && code < 300) - return true; - if (code == 404) - return true; - return false; - } - - if (!(ok(statusResp.status) && ok(snapshotResp.status) && - ok(deltasResp.status))) { - this._log.error("Could delete server data, response codes " + - statusResp.status + ", " + snapshotResp.status + ", " + - deltasResp.status); - return; - } - - this._log.debug("Server files deleted"); - done = true; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (done) { - this._log.debug("Server reset completed successfully"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-end", ""); - } else { - this._log.debug("Server reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:reset-server-error", ""); - } - if (svcLock) - this._unlock(); - generatorDone(this, self, onComplete, done) - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _resetClient: function BSS__resetClient(onComplete) { - let [self, cont] = yield; - let done = false; - let svcLock = this._lock(); - - try { - if (!svcLock) - return; - this._log.debug("Resetting client state"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-start", ""); - - this._snapshot.data = {}; - this._snapshot.version = -1; - this.snapshot.save(); - done = true; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (done) { - this._log.debug("Client reset completed successfully"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-end", ""); - } else { - this._log.debug("Client reset failed"); - this._os.notifyObservers(null, "bookmarks-sync:reset-client-error", ""); - } - if (svcLock) - this._unlock(); - generatorDone(this, self, onComplete, done); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - // XPCOM registration - classDescription: "Bookmarks Sync Service", - contractID: "@mozilla.org/places/sync-service;1", - classID: Components.ID("{efb3ba58-69bc-42d5-a430-0746fa4b1a7f}"), - QueryInterface: XPCOMUtils.generateQI([Ci.IBookmarksSyncService, - Ci.nsIObserver, - Ci.nsISupports]), - - // nsIObserver - - observe: function BSS__observe(subject, topic, data) { - switch (topic) { - case "browser.places.sync.enabled": - switch (data) { - case false: - this._log.info("Disabling automagic bookmarks sync"); - this._disableSchedule(); - break; - case true: - this._log.info("Enabling automagic bookmarks sync"); - this._enableSchedule(); - break; - } - break; - case "browser.places.sync.schedule": - switch (data) { - case 0: - this._log.info("Disabling automagic bookmarks sync"); - this._disableSchedule(); - break; - case 1: - this._log.info("Enabling automagic bookmarks sync"); - this._enableSchedule(); - break; - default: - this._log.warn("Unknown schedule value set"); - break; - } - break; - default: - // ignore, there are prefs we observe but don't care about - } - }, - - // IBookmarksSyncService public methods - - login: function BSS_login(password, passphrase) { - // cache password & passphrase - // if null, _login() will try to get them from the pw manager - this._password = password; - this._passphrase = passphrase; - this._login.async(this); - }, - - logout: function BSS_logout() { - this._log.info("Logging out"); - this._dav.logout(); - this._passphrase = null; - this._os.notifyObservers(null, "bookmarks-sync:logout", ""); - }, - - sync: function BSS_sync() { - this._doSync.async(this); - }, - - resetLock: function BSS_resetLock() { - this._resetLock.async(this); - }, - - resetServer: function BSS_resetServer() { - this._resetServer.async(this); - }, - - resetClient: function BSS_resetClient() { - this._resetClient.async(this); - } -}; - -serializeCommands: function serializeCommands(commands) { - let json = uneval(commands); - json = json.replace(/ {action/g, "\n {action"); - return json; -} - -serializeConflicts: function serializeConflicts(conflicts) { - let json = uneval(conflicts); - json = json.replace(/ {action/g, "\n {action"); - return json; -} - -/* - * SyncCore objects - * Sync cores deal with diff creation and conflict resolution. - * Tree data structures where all nodes have GUIDs only need to be - * subclassed for each data type to implement commandLike and - * itemExists. - */ - -function SyncCore() { - this._init(); -} -SyncCore.prototype = { - _logName: "Sync", - - _init: function SC__init() { - let logSvc = Cc["@mozilla.org/log4moz/service;1"]. - getService(Ci.ILog4MozService); - this._log = logSvc.getLogger("Service." + this._logName); - }, - - // FIXME: this won't work for deep objects, or objects with optional properties - _getEdits: function SC__getEdits(a, b) { - let ret = {numProps: 0, props: {}}; - for (prop in a) { - if (!deepEquals(a[prop], b[prop])) { - ret.numProps++; - ret.props[prop] = b[prop]; - } - } - return ret; - }, - - _nodeParents: function SC__nodeParents(GUID, tree) { - return this._nodeParentsInt(GUID, tree, []); - }, - - _nodeParentsInt: function SC__nodeParentsInt(GUID, tree, parents) { - if (!tree[GUID] || !tree[GUID].parentGUID) - return parents; - parents.push(tree[GUID].parentGUID); - return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); - }, - - _detectUpdates: function SC__detectUpdates(onComplete, a, b) { - let [self, cont] = yield; - let listener = new EventListener(cont); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - - let cmds = []; - - try { - for (let GUID in a) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (GUID in b) { - let edits = this._getEdits(a[GUID], b[GUID]); - if (edits.numProps == 0) // no changes - skip - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "edit", GUID: GUID, - depth: parents.length, parents: parents, - data: edits.props}); - } else { - let parents = this._nodeParents(GUID, a); // ??? - cmds.push({action: "remove", GUID: GUID, - depth: parents.length, parents: parents}); - } - } - for (let GUID in b) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (GUID in a) - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "create", GUID: GUID, - depth: parents.length, parents: parents, - data: b[GUID]}); - } - cmds.sort(function(a, b) { - if (a.depth > b.depth) - return 1; - if (a.depth < b.depth) - return -1; - if (a.index > b.index) - return -1; - if (a.index < b.index) - return 1; - return 0; // should never happen, but not a big deal if it does - }); - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - timer = null; - generatorDone(this, self, onComplete, cmds); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - _commandLike: function SC__commandLike(a, b) { - this._log.error("commandLike needs to be subclassed"); - - // Check that neither command is null, and verify that the GUIDs - // are different (otherwise we need to check for edits) - if (!a || !b || a.GUID == b.GUID) - return false; - - // Check that all other properties are the same - // FIXME: could be optimized... - for (let key in a) { - if (key != "GUID" && !deepEquals(a[key], b[key])) - return false; - } - for (let key in b) { - if (key != "GUID" && !deepEquals(a[key], b[key])) - return false; - } - return true; - }, - - // When we change the GUID of a local item (because we detect it as - // being the same item as a remote one), we need to fix any other - // local items that have it as their parent - _fixParents: function SC__fixParents(list, oldGUID, newGUID) { - for (let i = 0; i < list.length; i++) { - if (!list[i]) - continue; - if (list[i].data.parentGUID == oldGUID) - list[i].data.parentGUID = newGUID; - for (let j = 0; j < list[i].parents.length; j++) { - if (list[i].parents[j] == oldGUID) - list[i].parents[j] = newGUID; - } - } - }, - - _conflicts: function SC__conflicts(a, b) { - if ((a.GUID == b.GUID) && !deepEquals(a, b)) - return true; - return false; - }, - - _getPropagations: function SC__getPropagations(commands, conflicts, propagations) { - for (let i = 0; i < commands.length; i++) { - let alsoConflicts = function(elt) { - return (elt.action == "create" || elt.action == "remove") && - commands[i].parents.indexOf(elt.GUID) >= 0; - }; - if (conflicts.some(alsoConflicts)) - conflicts.push(commands[i]); - - let cmdConflicts = function(elt) { - return elt.GUID == commands[i].GUID; - }; - if (!conflicts.some(cmdConflicts)) - propagations.push(commands[i]); - } - }, - - _itemExists: function SC__itemExists(GUID) { - this._log.error("itemExists needs to be subclassed"); - return false; - }, - - _reconcile: function SC__reconcile(onComplete, listA, listB) { - let [self, cont] = yield; - let listener = new EventListener(cont); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - - let propagations = [[], []]; - let conflicts = [[], []]; - let ret = {propagations: propagations, conflicts: conflicts}; - this._log.debug("Reconciling " + listA.length + - " against " + listB.length + "commands"); - - try { - let guidChanges = []; - for (let i = 0; i < listA.length; i++) { - let a = listA[i]; - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - this._log.debug("comparing " + i + ", listB length: " + listB.length); - - let skip = false; - listB = listB.filter(function(b) { - // fast path for when we already found a matching command - if (skip) - return true; - - if (deepEquals(a, b)) { - delete listA[i]; // a - skip = true; - return false; // b - - } else if (this._commandLike(a, b)) { - this._fixParents(listA, a.GUID, b.GUID); - guidChanges.push({action: "edit", - GUID: a.GUID, - data: {GUID: b.GUID}}); - delete listA[i]; // a - skip = true; - return false; // b, but we add it back from guidChanges - } - - // watch out for create commands with GUIDs that already exist - if (b.action == "create" && this._itemExists(b.GUID)) { - this._log.error("Remote command has GUID that already exists " + - "locally. Dropping command."); - return false; // delete b - } - return true; // keep b - }, this); - } - - listA = listA.filter(function(elt) { return elt }); - listB = listB.concat(guidChanges); - - for (let i = 0; i < listA.length; i++) { - for (let j = 0; j < listB.length; j++) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (this._conflicts(listA[i], listB[j]) || - this._conflicts(listB[j], listA[i])) { - if (!conflicts[0].some( - function(elt) { return elt.GUID == listA[i].GUID })) - conflicts[0].push(listA[i]); - if (!conflicts[1].some( - function(elt) { return elt.GUID == listB[j].GUID })) - conflicts[1].push(listB[j]); - } - } - } - - this._getPropagations(listA, conflicts[0], propagations[1]); - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - this._getPropagations(listB, conflicts[1], propagations[0]); - ret = {propagations: propagations, conflicts: conflicts}; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - timer = null; - generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - // Public methods - - detectUpdates: function SC_detectUpdates(onComplete, a, b) { - return this._detectUpdates.async(this, onComplete, a, b); - }, - - reconcile: function SC_reconcile(onComplete, listA, listB) { - return this._reconcile.async(this, onComplete, listA, listB); - } -}; - -function BookmarksSyncCore() { - this._init(); -} -BookmarksSyncCore.prototype = { - _logName: "BMSync", - - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - // NOTE: Needs to be subclassed - _itemExists: function BSC__itemExists(GUID) { - return this._bms.getItemIdForGUID(GUID) >= 0; - }, - - _commandLike: function BSC_commandLike(a, b) { - // Check that neither command is null, that their actions, types, - // and parents are the same, and that they don't have the same - // GUID. - // Items with the same GUID do not qualify for 'likeness' because - // we already consider them to be the same object, and therefore - // we need to process any edits. - // The parent GUID check works because reconcile() fixes up the - // parent GUIDs as it runs, and the command list is sorted by - // depth - if (!a || !b || - a.action != b.action || - a.data.type != b.data.type || - a.data.parentGUID != b.data.parentGUID || - a.GUID == b.GUID) - return false; - - // Bookmarks are allowed to be in a different index as long as - // they are in the same folder. Folders and separators must be at - // the same index to qualify for 'likeness'. - switch (a.data.type) { - case "bookmark": - if (a.data.URI == b.data.URI && - a.data.title == b.data.title) - return true; - return false; - case "query": - if (a.data.URI == b.data.URI && - a.data.title == b.data.title) - return true; - return false; - case "microsummary": - if (a.data.URI == b.data.URI && - a.data.generatorURI == b.data.generatorURI) - return true; - return false; - case "folder": - if (a.index == b.index && - a.data.title == b.data.title) - return true; - return false; - case "livemark": - if (a.data.title == b.data.title && - a.data.siteURI == b.data.siteURI && - a.data.feedURI == b.data.feedURI) - return true; - return false; - case "separator": - if (a.index == b.index) - return true; - return false; - default: - this._log.error("commandLike: Unknown item type: " + uneval(a)); - return false; - } - } -}; -BookmarksSyncCore.prototype.__proto__ = new SyncCore(); - -/* - * Data Stores - * These can wrap, serialize items and apply commands - */ - -function Store() { - this._init(); -} -Store.prototype = { - _logName: "Store", - - _init: function Store__init() { - let logSvc = Cc["@mozilla.org/log4moz/service;1"]. - getService(Ci.ILog4MozService); - this._log = logSvc.getLogger("Service." + this._logName); - }, - - wrap: function Store_wrap() { - }, - - applyCommands: function Store_applyCommands(commandList) { - } -}; - -function SnapshotStore() { - this._init(); -} -SnapshotStore.prototype = { - _logName: "SStore", - - // Last synced tree, version, and GUID (to detect if the store has - // been completely replaced and invalidate the snapshot) - - _data: {}, - get data() { - return this._data; - }, - set data(value) { - this._data = value; - }, - - _version: 0, - get version() { - return this._version; - }, - set version(value) { - this._version = value; - }, - - _GUID: null, - get GUID() { - if (!this._GUID) { - let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. - getService(Ci.nsIUUIDGenerator); - this._GUID = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); - } - return this._GUID; - }, - set GUID(GUID) { - this._GUID = GUID; - }, - - save: function SStore_save() { - this._log.info("Saving snapshot to disk"); - - let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync-snapshot.json"); - file.QueryInterface(Ci.nsILocalFile); - - if (!file.exists()) - file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); - - let fos = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; - fos.init(file, flags, PERMS_FILE, 0); - - let out = {version: this._snapshot.version, - GUID: this._snapshot.GUID, - snapshot: this._snapshot}; - out = uneval(out); - fos.write(out, out.length); - fos.close(); - }, - - load: function SStore_load() { - let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("bm-sync-snapshot.json"); - - if (!file.exists()) - return; - - let fis = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - fis.init(file, MODE_RDONLY, PERMS_FILE, 0); - fis.QueryInterface(Ci.nsILineInputStream); - - let json = ""; - while (fis.available()) { - let ret = {}; - fis.readLine(ret); - json += ret.value; - } - fis.close(); - json = eval(json); - - if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { - this._log.info("Read saved snapshot from disk"); - this._snapshot = json.snapshot; - this._snapshot.version = json.version; - this._snapshot.GUID = json.GUID; - } - }, - - serialize: function SStore_serialize() { - let json = uneval(this.data); - json = json.replace(/:{type/g, ":\n\t{type"); - json = json.replace(/}, /g, "},\n "); - json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); - json = json.replace(/, index/g, ",\n\t index"); - json = json.replace(/, title/g, ",\n\t title"); - json = json.replace(/, URI/g, ",\n\t URI"); - json = json.replace(/, tags/g, ",\n\t tags"); - json = json.replace(/, keyword/g, ",\n\t keyword"); - return json; - }, - - wrap: function SStore_wrap() { - }, - - applyCommands: function SStore_applyCommands(commands) { - for (let i = 0; i < commands.length; i++) { - this._log.debug("Applying cmd to obj: " + uneval(commands[i])); - switch (commands[i].action) { - case "create": - this._data[commands[i].GUID] = eval(uneval(commands[i].data)); - break; - case "edit": - if ("GUID" in commands[i].data) { - // special-case guid changes - let newGUID = commands[i].data.GUID, - oldGUID = commands[i].GUID; - - this._data[newGUID] = this._data[oldGUID]; - delete this._data[oldGUID] - - for (let GUID in this._data) { - if (this._data[GUID].parentGUID == oldGUID) - this._data[GUID].parentGUID = newGUID; - } - } - for (let prop in commands[i].data) { - if (prop == "GUID") - continue; - this._data[commands[i].GUID][prop] = commands[i].data[prop]; - } - break; - case "remove": - delete this._data[commands[i].GUID]; - break; - } - } - return this._data; - } -}; -SnapshotStore.prototype.__proto__ = new Store(); - -function BookmarksStore() { - this._init(); -} -BookmarksStore.prototype = { - _logName: "BStore", - - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - __hsvc: null, - get _hsvc() { - if (!this.__hsvc) - this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - return this.__hsvc; - }, - - __ls: null, - get _ls() { - if (!this.__ls) - this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. - getService(Ci.nsILivemarkService); - return this.__ls; - }, - - __ms: null, - get _ms() { - if (!this.__ms) - this.__ms = Cc["@mozilla.org/microsummary/service;1"]. - getService(Ci.nsIMicrosummaryService); - return this.__ms; - }, - - __ts: null, - get _ts() { - if (!this.__ts) - this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. - getService(Ci.nsITaggingService); - return this.__ts; - }, - - __ans: null, - get _ans() { - if (!this.__ans) - this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - return this.__ans; - }, - - _getFolderNodes: function BSS__getFolderNodes(folder) { - let query = this._hsvc.getNewQuery(); - query.setFolders([folder], 1); - return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; - }, - - _wrapNode: function BSS__wrapNode(node) { - var items = {}; - this._wrapNodeInternal(node, items, null, null); - return items; - }, - - _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) { - let GUID = this._bms.getItemGUID(node.itemId); - let item = {parentGUID: parentGUID, - index: index}; - - if (node.type == node.RESULT_TYPE_FOLDER) { - if (this._ls.isLivemark(node.itemId)) { - item.type = "livemark"; - let siteURI = this._ls.getSiteURI(node.itemId); - let feedURI = this._ls.getFeedURI(node.itemId); - item.siteURI = siteURI? siteURI.spec : ""; - item.feedURI = feedURI? feedURI.spec : ""; - } else { - item.type = "folder"; - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._wrapNodeInternal(node.getChild(i), items, GUID, i); - } - } - item.title = node.title; - } else if (node.type == node.RESULT_TYPE_URI || - node.type == node.RESULT_TYPE_QUERY) { - if (this._ms.hasMicrosummary(node.itemId)) { - item.type = "microsummary"; - let micsum = this._ms.getMicrosummary(node.itemId); - item.generatorURI = micsum.generator.uri.spec; // breaks local generators - } else if (node.type == node.RESULT_TYPE_QUERY) { - item.type = "query"; - item.title = node.title; - } else { - item.type = "bookmark"; - item.title = node.title; - } - item.URI = node.uri; - item.tags = this._ts.getTagsForURI(makeURI(node.uri)); - item.keyword = this._bms.getKeywordForBookmark(node.itemId); - } else if (node.type == node.RESULT_TYPE_SEPARATOR) { - item.type = "separator"; - } else { - this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); - return; - } - - items[GUID] = item; - }, - - _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { - return this._wrapNode(this._getFolderNodes(folder)); - }, - - _resetGUIDsInt: function BSS__resetGUIDsInt(node) { - if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) - this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); - - if (node.type == node.RESULT_TYPE_FOLDER && - !this._ls.isLivemark(node.itemId)) { - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._resetGUIDsInt(node.getChild(i)); - } - } - }, - - _createCommand: function BStore__createCommand(command) { - let newId; - let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); - - if (parentId < 0) { - this._log.warn("Creating node with unknown parent -> reparenting to root"); - parentId = this._bms.bookmarksMenuFolder; - } - - switch (command.data.type) { - case "query": - case "bookmark": - case "microsummary": - this._log.info(" -> creating bookmark \"" + command.data.title + "\""); - let URI = makeURI(command.data.URI); - newId = this._bms.insertBookmark(parentId, - URI, - command.data.index, - command.data.title); - this._ts.untagURI(URI, null); - this._ts.tagURI(URI, command.data.tags); - this._bms.setKeywordForBookmark(newId, command.data.keyword); - - if (command.data.type == "microsummary") { - this._log.info(" \-> is a microsummary"); - let genURI = makeURI(command.data.generatorURI); - try { - let micsum = this._ms.createMicrosummary(URI, genURI); - this._ms.setMicrosummary(newId, micsum); - } - catch(ex) { /* ignore "missing local generator" exceptions */ } - } - break; - case "folder": - this._log.info(" -> creating folder \"" + command.data.title + "\""); - newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); - break; - case "livemark": - this._log.info(" -> creating livemark \"" + command.data.title + "\""); - newId = this._ls.createLivemark(parentId, - command.data.title, - makeURI(command.data.siteURI), - makeURI(command.data.feedURI), - command.data.index); - break; - case "separator": - this._log.info(" -> creating separator"); - newId = this._bms.insertSeparator(parentId, command.data.index); - break; - default: - this._log.error("_createCommand: Unknown item type: " + command.data.type); - break; - } - if (newId) - this._bms.setItemGUID(newId, command.GUID); - }, - - _removeCommand: function BStore__removeCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId < 0) { - this._log.warn("Attempted to remove item " + command.GUID + - ", but it does not exist. Skipping."); - return; - } - var type = this._bms.getItemType(itemId); - - switch (type) { - case this._bms.TYPE_BOOKMARK: - this._log.info(" -> removing bookmark " + command.GUID); - this._bms.removeItem(itemId); - break; - case this._bms.TYPE_FOLDER: - this._log.info(" -> removing folder " + command.GUID); - this._bms.removeFolder(itemId); - break; - case this._bms.TYPE_SEPARATOR: - this._log.info(" -> removing separator " + command.GUID); - this._bms.removeItem(itemId); - break; - default: - this._log.error("removeCommand: Unknown item type: " + type); - break; - } - }, - - _editCommand: function BStore__editCommand(command) { - var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId < 0) { - this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); - return; - } - - for (let key in command.data) { - switch (key) { - case "GUID": - var existing = this._bms.getItemIdForGUID(command.data.GUID); - if (existing < 0) - this._bms.setItemGUID(itemId, command.data.GUID); - else - this._log.warn("Can't change GUID " + command.GUID + - " to " + command.data.GUID + ": GUID already exists."); - break; - case "title": - this._bms.setItemTitle(itemId, command.data.title); - break; - case "URI": - this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI)); - break; - case "index": - this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), - command.data.index); - break; - case "parentGUID": - let index = -1; - if (command.data.index && command.data.index >= 0) - index = command.data.index; - this._bms.moveItem( - itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); - break; - case "tags": - let tagsURI = this._bms.getBookmarkURI(itemId); - this._ts.untagURI(URI, null); - this._ts.tagURI(tagsURI, command.data.tags); - break; - case "keyword": - this._bms.setKeywordForBookmark(itemId, command.data.keyword); - break; - case "generatorURI": - let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); - let genURI = makeURI(command.data.generatorURI); - let micsum = this._ms.createMicrosummary(micsumURI, genURI); - this._ms.setMicrosummary(itemId, micsum); - break; - case "siteURI": - this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); - break; - case "feedURI": - this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); - break; - default: - this._log.warn("Can't change item property: " + key); - break; - } - } - }, - - wrap: function BStore_wrap() { - let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder); - let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder); - let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder); - - for (let guid in unfiled) { - if (!(guid in filed)) - filed[guid] = unfiled[guid]; - } - - for (let guid in toolbar) { - if (!(guid in filed)) - filed[guid] = toolbar[guid]; - } - - return filed; // (combined) - }, - - resetGUIDs: function BStore_resetGUIDs() { - this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); - }, - - applyCommands: function BStore_applyCommands(commandList) { - for (var i = 0; i < commandList.length; i++) { - var command = commandList[i]; - this._log.debug("Processing command: " + uneval(command)); - switch (command["action"]) { - case "create": - this._createCommand(command); - break; - case "remove": - this._removeCommand(command); - break; - case "edit": - this._editCommand(command); - break; - default: - this._log.error("unknown action in command: " + command["action"]); - break; - } - } - } -}; -BookmarksStore.prototype.__proto__ = new Store(); - -/* - * DAV object - * Abstracts the raw DAV commands - */ - -function DAVCollection(baseURL) { - this._baseURL = baseURL; - this._authProvider = new DummyAuthProvider(); - let logSvc = Cc["@mozilla.org/log4moz/service;1"]. - getService(Ci.ILog4MozService); - this._log = logSvc.getLogger("Service.DAV"); -} -DAVCollection.prototype = { - __dp: null, - get _dp() { - if (!this.__dp) - this.__dp = Cc["@mozilla.org/xmlextras/domparser;1"]. - createInstance(Ci.nsIDOMParser); - return this.__dp; - }, - - _auth: null, - - get baseURL() { - return this._baseURL; - }, - set baseURL(value) { - this._baseURL = value; - }, - - _loggedIn: false, - get loggedIn() { - return this._loggedIn; - }, - - _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { - let [self, cont] = yield; - let ret; - - try { - this._log.debug("Creating " + op + " request for " + this._baseURL + path); - - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); - request = request.QueryInterface(Ci.nsIDOMEventTarget); - - request.addEventListener("load", new EventListener(cont, "load"), false); - request.addEventListener("error", new EventListener(cont, "error"), false); - request = request.QueryInterface(Ci.nsIXMLHttpRequest); - request.open(op, this._baseURL + path, true); - - - // Force cache validation - let channel = request.channel; - channel = channel.QueryInterface(Ci.nsIRequest); - let loadFlags = channel.loadFlags; - loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; - channel.loadFlags = loadFlags; - - let key; - for (key in headers) { - this._log.debug("HTTP Header " + key + ": " + headers[key]); - request.setRequestHeader(key, headers[key]); - } - - this._authProvider._authFailed = false; - request.channel.notificationCallbacks = this._authProvider; - - request.send(data); - let event = yield; - ret = event.target; - - if (this._authProvider._authFailed) - this._log.warn("_makeRequest: authentication failed"); - if (ret.status < 200 || ret.status >= 300) - this._log.warn("_makeRequest: got status " + ret.status); - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - get _defaultHeaders() { - return {'Authorization': this._auth? this._auth : '', - 'Content-type': 'text/plain', - 'If': this._token? - "<" + this._baseURL + "> (<" + this._token + ">)" : ''}; - }, - - GET: function DC_GET(path, onComplete) { - return this._makeRequest.async(this, onComplete, "GET", path, - this._defaultHeaders); - }, - - PUT: function DC_PUT(path, data, onComplete) { - return this._makeRequest.async(this, onComplete, "PUT", path, - this._defaultHeaders, data); - }, - - DELETE: function DC_DELETE(path, onComplete) { - return this._makeRequest.async(this, onComplete, "DELETE", path, - this._defaultHeaders); - }, - - PROPFIND: function DC_PROPFIND(path, data, onComplete) { - let headers = {'Content-type': 'text/xml; charset="utf-8"', - 'Depth': '0'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "PROPFIND", path, - headers, data); - }, - - LOCK: function DC_LOCK(path, data, onComplete) { - let headers = {'Content-type': 'text/xml; charset="utf-8"', - 'Depth': 'infinity', - 'Timeout': 'Second-600'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "LOCK", path, headers, data); - }, - - UNLOCK: function DC_UNLOCK(path, onComplete) { - let headers = {'Lock-Token': '<' + this._token + '>'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers); - }, - - // Login / Logout - - login: function DC_login(onComplete, username, password) { - let [self, cont] = yield; - - try { - if (this._loggedIn) { - this._log.debug("Login requested, but already logged in"); - return; - } - - this._log.info("Logging in"); - - let URI = makeURI(this._baseURL); - this._auth = "Basic " + btoa(username + ":" + password); - - // Make a call to make sure it's working - this.GET("", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - this._loggedIn = true; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (this._loggedIn) - this._log.info("Logged in"); - else - this._log.warn("Could not log in"); - generatorDone(this, self, onComplete, this._loggedIn); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - logout: function DC_logout() { - this._log.debug("Logging out (forgetting auth header)"); - this._loggedIn = false; - this.__auth = null; - }, - - // Locking - - _getActiveLock: function DC__getActiveLock(onComplete) { - let [self, cont] = yield; - let ret = null; - - try { - this._log.info("Getting active lock token"); - this.PROPFIND("", - "" + - "" + - " " + - "", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - ret = token.textContent; - - } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - if (ret) - this._log.debug("Found an active lock token"); - else - this._log.debug("No active lock token found"); - generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - lock: function DC_lock(onComplete) { - let [self, cont] = yield; - this._token = null; - - try { - this._log.info("Acquiring lock"); - - if (this._token) { - this._log.debug("Lock called, but we already hold a token"); - return; - } - - this.LOCK("", - "\n" + - "\n" + - " \n" + - " \n" + - "", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - if (token) - this._token = token.textContent; - - } catch (e){ - this._log.error("Exception caught: " + e.message); - - } finally { - if (this._token) - this._log.info("Lock acquired"); - else - this._log.warn("Could not acquire lock"); - generatorDone(this, self, onComplete, this._token); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - unlock: function DC_unlock(onComplete) { - let [self, cont] = yield; - try { - this._log.info("Releasing lock"); - - if (this._token === null) { - this._log.debug("Unlock called, but we don't hold a token right now"); - return; - } - - this.UNLOCK("", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - this._token = null; - - } catch (e){ - this._log.error("Exception caught: " + e.message); - - } finally { - if (this._token) { - this._log.info("Could not release lock"); - generatorDone(this, self, onComplete, false); - } else { - this._log.info("Lock released (or we didn't have one)"); - generatorDone(this, self, onComplete, true); - } - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - forceUnlock: function DC_forceUnlock(onComplete) { - let [self, cont] = yield; - let unlocked = true; - - try { - this._log.info("Forcibly releasing any server locks"); - - this._getActiveLock.async(this, cont); - this._token = yield; - - if (!this._token) { - this._log.info("No server lock found"); - return; - } - - this._log.info("Server lock found, unlocking"); - this.unlock.async(this, cont); - unlocked = yield; - - } catch (e){ - this._log.error("Exception caught: " + e.message); - - } finally { - if (unlocked) - this._log.debug("Lock released"); - else - this._log.debug("No lock released"); - generatorDone(this, self, onComplete, unlocked); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - }, - - stealLock: function DC_stealLock(onComplete) { - let [self, cont] = yield; - let stolen = null; - - try { - this.forceUnlock.async(this, cont); - let unlocked = yield; - - if (unlocked) { - this.lock.async(this, cont); - stolen = yield; - } - - } catch (e){ - this._log.error("Exception caught: " + e.message); - - } finally { - generatorDone(this, self, onComplete, stolen); - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); - } -}; - - -/* - * Auth provider object - * Taken from nsMicrosummaryService.js and massaged slightly - */ - -function DummyAuthProvider() {} -DummyAuthProvider.prototype = { - // Implement notification callback interfaces so we can suppress UI - // and abort loads for bad SSL certs and HTTP authorization requests. - - // Interfaces this component implements. - interfaces: [Ci.nsIBadCertListener, - Ci.nsIAuthPromptProvider, - Ci.nsIAuthPrompt, - Ci.nsIPrompt, - Ci.nsIProgressEventSink, - Ci.nsIInterfaceRequestor, - Ci.nsISupports], - - // Auth requests appear to succeed when we cancel them (since the server - // redirects us to a "you're not authorized" page), so we have to set a flag - // to let the load handler know to treat the load as a failure. - get _authFailed() { return this.__authFailed; }, - set _authFailed(newValue) { return this.__authFailed = newValue }, - - // nsISupports - - QueryInterface: function DAP_QueryInterface(iid) { - if (!this.interfaces.some( function(v) { return iid.equals(v) } )) - throw Cr.NS_ERROR_NO_INTERFACE; - - // nsIAuthPrompt and nsIPrompt need separate implementations because - // their method signatures conflict. The other interfaces we implement - // within DummyAuthProvider itself. - switch(iid) { - case Ci.nsIAuthPrompt: - return this.authPrompt; - case Ci.nsIPrompt: - return this.prompt; - default: - return this; - } - }, - - // nsIInterfaceRequestor - - getInterface: function DAP_getInterface(iid) { - return this.QueryInterface(iid); - }, - - // nsIBadCertListener - - // Suppress UI and abort secure loads from servers with bad SSL certificates. - - confirmUnknownIssuer: function DAP_confirmUnknownIssuer(socketInfo, cert, certAddType) { - return false; - }, - - confirmMismatchDomain: function DAP_confirmMismatchDomain(socketInfo, targetURL, cert) { - return false; - }, - - confirmCertExpired: function DAP_confirmCertExpired(socketInfo, cert) { - return false; - }, - - notifyCrlNextupdate: function DAP_notifyCrlNextupdate(socketInfo, targetURL, cert) { - }, - - // nsIAuthPromptProvider - - getAuthPrompt: function(aPromptReason, aIID) { - this._authFailed = true; - throw Cr.NS_ERROR_NOT_AVAILABLE; - }, - - // HTTP always requests nsIAuthPromptProvider first, so it never needs - // nsIAuthPrompt, but not all channels use nsIAuthPromptProvider, so we - // implement nsIAuthPrompt too. - - // nsIAuthPrompt - - get authPrompt() { - var resource = this; - return { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), - prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) { - resource._authFailed = true; - return false; - }, - promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) { - resource._authFailed = true; - return false; - }, - promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) { - resource._authFailed = true; - return false; - } - }; - }, - - // nsIPrompt - - get prompt() { - var resource = this; - return { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), - alert: function(dialogTitle, text) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - alertCheck: function(dialogTitle, text, checkMessage, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirm: function(dialogTitle, text) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirmCheck: function(dialogTitle, text, checkMessage, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirmEx: function(dialogTitle, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - prompt: function(dialogTitle, text, value, checkMsg, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - promptPassword: function(dialogTitle, text, password, checkMsg, checkValue) { - resource._authFailed = true; - return false; - }, - promptUsernameAndPassword: function(dialogTitle, text, username, password, checkMsg, checkValue) { - resource._authFailed = true; - return false; - }, - select: function(dialogTitle, text, count, selectList, outSelection) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - } - }; - }, - - // nsIProgressEventSink - - onProgress: function DAP_onProgress(aRequest, aContext, - aProgress, aProgressMax) { - }, - - onStatus: function DAP_onStatus(aRequest, aContext, - aStatus, aStatusArg) { - } -}; - -/* - * Event listener object - * Used to handle XMLHttpRequest and nsITimer callbacks - */ - -function EventListener(handler, eventName) { - this._handler = handler; - this._eventName = eventName; - let logSvc = Cc["@mozilla.org/log4moz/service;1"]. - getService(Ci.ILog4MozService); - this._log = logSvc.getLogger("Service.EventHandler"); -} -EventListener.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), - - // DOM event listener - handleEvent: function EL_handleEvent(event) { - this._log.debug("Handling event " + this._eventName); - this._handler(event); - }, - - // nsITimerCallback - notify: function EL_notify(timer) { - this._log.trace("Timer fired"); - this._handler(timer); - } -}; - -/* - * Utility functions - */ - -function deepEquals(a, b) { - if (!a && !b) - return true; - if (!a || !b) - return false; - - if (typeof(a) != "object" && typeof(b) != "object") - return a == b; - if (typeof(a) != "object" || typeof(b) != "object") - return false; - - for (let key in a) { - if (typeof(a[key]) == "object") { - if (!typeof(b[key]) == "object") - return false; - if (!deepEquals(a[key], b[key])) - return false; - } else { - if (a[key] != b[key]) - return false; - } - } - return true; -} - -function makeFile(path) { - var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - file.initWithPath(path); - return file; -} - -function makeURI(URIString) { - if (URIString === null || URIString == "") - return null; - let ioservice = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - return ioservice.newURI(URIString, null, null); -} - -function xpath(xmlDoc, xpathString) { - let root = xmlDoc.ownerDocument == null ? - xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement - let nsResolver = xmlDoc.createNSResolver(root); - - return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, - Ci.nsIDOMXPathResult.ANY_TYPE, null); -} - -function bind2(object, method) { - return function innerBind() { return method.apply(object, arguments); } -} - -Function.prototype.async = function(self, extra_args) { - try { - let args = Array.prototype.slice.call(arguments, 1); - let gen = this.apply(self, args); - gen.next(); // must initialize before sending - gen.send([gen, function(data) {continueGenerator(gen, data);}]); - return gen; - } catch (e) { - if (e instanceof StopIteration) { - dump("async warning: generator stopped unexpectedly"); - return null; - } else { - this._log.error("Exception caught: " + e.message); - } - } -} - -function continueGenerator(generator, data) { - try { generator.send(data); } - catch (e) { - if (e instanceof StopIteration) - dump("continueGenerator warning: generator stopped unexpectedly"); - else - dump("Exception caught: " + e.message); - } -} - -// generators created using Function.async can't simply call the -// callback with the return value, since that would cause the calling -// function to end up running (after the yield) from inside the -// generator. Instead, generators can call this method which sets up -// a timer to call the callback from a timer (and cleans up the timer -// to avoid leaks). It also closes generators after the timeout, to -// keep things clean. -function generatorDone(object, generator, callback, retval) { - if (object._timer) - throw "Called generatorDone when there is a timer already set." - - let cb = bind2(object, function(event) { - generator.close(); - generator = null; - object._timer = null; - if (callback) - callback(retval); - }); - - object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - object._timer.initWithCallback(new EventListener(cb), - 0, object._timer.TYPE_ONE_SHOT); -} - -function NSGetModule(compMgr, fileSpec) { - return XPCOMUtils.generateModule([BookmarksSyncService]); -} - From cc10e5c053b33a92f8dc052e345ddff2df57a8c7 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Thu, 24 Jan 2008 17:41:36 -0800 Subject: [PATCH 0133/1860] make log4moz not depend on constants.js so it's easier to reuse it in another project --- services/sync/modules/log4moz.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 64b0c00b18b7..3365d40a557c 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -44,7 +44,19 @@ const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/constants.js"); + +const MODE_RDONLY = 0x01; +const MODE_WRONLY = 0x02; +const MODE_CREATE = 0x08; +const MODE_APPEND = 0x10; +const MODE_TRUNCATE = 0x20; + +const PERMS_FILE = 0644; +const PERMS_DIRECTORY = 0755; + +const ONE_BYTE = 1; +const ONE_KILOBYTE = 1024 * ONE_BYTE; +const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; const Log4Moz = {}; Log4Moz.Level = {}; From 189f91e845df4d1a06077fd10204d135828bed63 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 5 Feb 2008 14:15:53 -0800 Subject: [PATCH 0134/1860] add openssl support --- services/sync/modules/constants.js | 3 +- services/sync/modules/crypto.js | 96 ++++++++++++++++++++++++++++-- services/sync/modules/service.js | 5 -- services/sync/modules/util.js | 96 +++++++++++++++++++++++++++++- 4 files changed, 189 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index e193e0965ab9..e95add071cad 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -38,7 +38,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PREFS_PRANCH', 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', - 'PERMS_FILE', 'PERMS_DIRECTORY', + 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; const WEAVE_VERSION = "0.1.16"; @@ -53,6 +53,7 @@ const MODE_APPEND = 0x10; const MODE_TRUNCATE = 0x20; const PERMS_FILE = 0644; +const PERMS_PASSFILE = 0600; const PERMS_DIRECTORY = 0755; const ONE_BYTE = 1; diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 81eb40bb40e0..dfaa5580be90 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -139,12 +139,16 @@ WeaveCrypto.prototype = { if (!algorithm) algorithm = this.defaultAlgorithm; + if (algorithm != "none") + this._log.debug("Encrypting data"); + switch (algorithm) { case "none": ret = data; + break; + case "XXXTEA": // Weave 0.1.12.10 and below had this typo case "XXTEA": { - this._log.debug("Encrypting data"); let gen = this._xxtea.encrypt(data, identity.password); ret = gen.next(); while (typeof(ret) == "object") { @@ -153,12 +157,23 @@ WeaveCrypto.prototype = { ret = gen.next(); } gen.close(); - this._log.debug("Done encrypting data"); } break; + + case "aes-128-cbc": + case "aes-192-cbc": + case "aes-256-cbc": + case "bf-cbc": + case "des-ede3-cbc": + ret = openssl("-e", algorithm, data, identity.password); + break; + default: throw "Unknown encryption algorithm: " + algorithm; } + if (algorithm != "none") + this._log.debug("Done encrypting data"); + } catch (e) { this._log.error("Exception caught: " + (e.message? e.message : e)); @@ -180,13 +195,16 @@ WeaveCrypto.prototype = { if (!algorithm) algorithm = this.defaultAlgorithm; + if (algorithm != "none") + this._log.debug("Decrypting data"); + switch (algorithm) { case "none": ret = data; break; + case "XXXTEA": // Weave 0.1.12.10 and below had this typo case "XXTEA": { - this._log.debug("Decrypting data"); let gen = this._xxtea.decrypt(data, identity.password); ret = gen.next(); while (typeof(ret) == "object") { @@ -195,12 +213,23 @@ WeaveCrypto.prototype = { ret = gen.next(); } gen.close(); - this._log.debug("Done decrypting data"); } break; + + case "aes-128-cbc": + case "aes-192-cbc": + case "aes-256-cbc": + case "bf-cbc": + case "des-ede3-cbc": + ret = openssl("-d", algorithm, data, identity.password); + break; + default: throw "Unknown encryption algorithm: " + algorithm; } + if (algorithm != "none") + this._log.debug("Done decrypting data"); + } catch (e) { this._log.error("Exception caught: " + (e.message? e.message : e)); @@ -212,3 +241,62 @@ WeaveCrypto.prototype = { this._log.warn("generator not properly closed"); } }; + +function openssl(op, algorithm, input, password) { + let extMgr = Components.classes["@mozilla.org/extensions/manager;1"] + .getService(Components.interfaces.nsIExtensionManager); + let loc = extMgr.getInstallLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); + + let wrap = loc.getItemLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); + wrap.append("openssl"); + let bin; + + let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; + switch(os) { + case "WINNT": + wrap.append("win32"); + wrap.append("exec.bat"); + bin = wrap.parent.path + "\openssl.exe"; + dump("FIXME TESTING path: " + bin + "\n"); + break; + case "Linux": + case "Darwin": + wrap.append("unix"); + wrap.append("exec.sh"); + bin = "openssl"; + break; + default: + throw "encryption not supported on this platform: " + os; + } + + let inputFile = getTmp("input"); + let [inputFOS] = open(inputFile, ">"); + inputFOS.write(input, input.length); + inputFOS.close(); + + let outputFile = getTmp("output"); + if (outputFile.exists()) + outputFile.remove(false); + + let passFile = getTmp("pass"); + let [passFOS] = open(passFile, ">", PERMS_PASSFILE); + passFOS.write(password, password.length); + passFOS.close(); + + try { + runCmd(wrap, getTmp().path, bin, algorithm, op, "-a", "-salt", + "-in", "input", "-out", "output", "-pass", "file:pass"); + } catch (e) { + throw e; + } finally { + passFile.remove(false); + inputFile.remove(false); + } + + let [outputFIS] = open(outputFile, "<"); + let ret = readStream(outputFIS); + outputFIS.close(); + outputFile.remove(false); + + return ret; +} diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 75067f4be60a..4d2754cd0eec 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -247,15 +247,10 @@ WeaveSyncService.prototype = { dapp.level = Log4Moz.Level.All; root.addAppender(dapp); - let logFile = this._dirSvc.get("ProfD", Ci.nsIFile); - let brief = this._dirSvc.get("ProfD", Ci.nsIFile); brief.QueryInterface(Ci.nsILocalFile); brief.append("weave"); - if (!brief.exists()) - brief.create(brief.DIRECTORY_TYPE, PERMS_DIRECTORY); - brief.append("logs"); if (!brief.exists()) brief.create(brief.DIRECTORY_TYPE, PERMS_DIRECTORY); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 3f88eb0aab62..37de2c83f01c 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -35,7 +35,9 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['deepEquals', 'makeFile', 'makeURI', 'xpath', - 'bind2', 'generatorAsync', 'generatorDone', 'EventListener']; + 'bind2', 'generatorAsync', 'generatorDone', + 'EventListener', + 'runCmd', 'getTmp', 'open', 'readStream']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -190,3 +192,95 @@ EventListener.prototype = { this._handler(timer); } }; + +function runCmd() { + var binary; + var args = []; + + for (let i = 0; i < arguments.length; ++i) { + args.push(arguments[i]); + } + + if (args[0] instanceof Ci.nsIFile) { + binary = args.shift(); + } else { + binary = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + binary.initWithPath(args.shift()); + } + + var p = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + p.init(binary); + + p.run(true, args, args.length); + return p.exitValue; +} + +function getTmp(name) { + let ds = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + + let tmp = ds.get("ProfD", Ci.nsIFile); + tmp.QueryInterface(Ci.nsILocalFile); + + tmp.append("weave"); + tmp.append("tmp"); + if (!tmp.exists()) + tmp.create(tmp.DIRECTORY_TYPE, PERMS_DIRECTORY); + + if (name) + tmp.append(name); + + return tmp; +} + +function open(pathOrFile, mode, perms) { + let stream, file; + + if (pathOrFile instanceof Ci.nsIFile) { + file = pathOrFile; + } else { + file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(pathOrFile); + } + + if (!perms) + perms = PERMS_FILE; + + switch(mode) { + case "<": { + if (!file.exists()) + throw "Cannot open file for reading, file does not exist"; + stream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + stream.init(file, MODE_RDONLY, perms, 0); + stream.QueryInterface(Ci.nsILineInputStream); + } break; + + case ">": { + stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(file, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, perms, 0); + } break; + + case ">>": { + stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(file, MODE_WRONLY | MODE_CREATE | MODE_APPEND, perms, 0); + } break; + + default: + throw "Illegal mode to open(): " + mode; + } + + return [stream, file]; +} + +function readStream(fis) { + let data = ""; + while (fis.available()) { + let ret = {}; + fis.readLine(ret); + data += ret.value; + } + return data; +} From 32e55f1bb550c49950dbde2cc7c01b4c31b8217e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 5 Feb 2008 16:15:43 -0800 Subject: [PATCH 0135/1860] fix openssl binary path on windows --- services/sync/modules/crypto.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index dfaa5580be90..95e33f0cc413 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -256,8 +256,7 @@ function openssl(op, algorithm, input, password) { case "WINNT": wrap.append("win32"); wrap.append("exec.bat"); - bin = wrap.parent.path + "\openssl.exe"; - dump("FIXME TESTING path: " + bin + "\n"); + bin = wrap.parent.path + "\\openssl.exe"; break; case "Linux": case "Darwin": From 6984da81f5082ecbff6ed89fbba89e2544fbc2c0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 8 Feb 2008 01:03:45 -0800 Subject: [PATCH 0136/1860] create user directories when they don't exist - good for personal webdav servers --- services/sync/modules/crypto.js | 1 + services/sync/modules/service.js | 18 ++++++++++++++++++ services/sync/modules/util.js | 2 +- services/sync/services-sync.js | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 95e33f0cc413..1899a08a2b39 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -277,6 +277,7 @@ function openssl(op, algorithm, input, password) { if (outputFile.exists()) outputFile.remove(false); + // nsIProcess doesn't support stdin, so we write a file instead let passFile = getTmp("pass"); let [passFOS] = open(passFile, ">", PERMS_PASSFILE); passFOS.write(password, password.length); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4d2754cd0eec..54679390faae 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -315,6 +315,24 @@ WeaveSyncService.prototype = { this._dav.login.async(this._dav, cont, this.username, this.password); success = yield; + // FIXME: we want to limit this to when we get a 404! + if (!success) { + this._log.debug("Attempting to create user directory"); + + this._dav.baseURL = this._serverURL; + this._dav.MKCOL("user/" + this.userPath, cont); + let ret = yield; + + if (ret.status == 201) { + this._log.debug("Successfully created user directory. Re-attempting login."); + this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; + this._dav.login.async(this._dav, cont, this.username, this.password); + success = yield; + } else { + this._log.debug("Could not create user directory. Got status: " + ret.status); + } + } + } catch (e) { this._log.error("Exception caught: " + e.message); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 37de2c83f01c..e44f911bc32b 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -182,7 +182,7 @@ EventListener.prototype = { // DOM event listener handleEvent: function EL_handleEvent(event) { - this._log.debug("Handling event " + this._eventName); + this._log.trace("Handling event " + this._eventName); this._handler(event); }, diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index c6634b153b3c..3170f0a9981d 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -8,4 +8,4 @@ pref("extensions.weave.bookmarks", true); pref("extensions.weave.history", true); pref("extensions.weave.schedule", 1); pref("extensions.weave.lastsync", "0"); -pref("extensions.weave.encryption", "XXTEA"); +pref("extensions.weave.encryption", "aes-256-cbc"); From a76aaa17d94817b37bf59a3f74b47628f2e1d3b6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 13 Feb 2008 14:30:44 -0800 Subject: [PATCH 0137/1860] put utility functions in an object to avoid namespace pollution --- services/sync/modules/crypto.js | 28 +- services/sync/modules/dav.js | 30 +- services/sync/modules/engines.js | 18 +- services/sync/modules/service.js | 45 +-- services/sync/modules/stores.js | 24 +- services/sync/modules/syncCores.js | 20 +- services/sync/modules/util.js | 428 ++++++++++++++--------------- 7 files changed, 297 insertions(+), 296 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 1899a08a2b39..9fc2b114da5d 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -46,7 +46,7 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Function.prototype.async = generatorAsync; +Function.prototype.async = Utils.generatorAsync; function WeaveCrypto() { this._init(); @@ -131,7 +131,7 @@ WeaveCrypto.prototype = { PBEencrypt: function Crypto_PBEencrypt(onComplete, data, identity, algorithm) { let [self, cont] = yield; - let listener = new EventListener(cont); + let listener = new Utils.EventListener(cont); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let ret; @@ -179,7 +179,7 @@ WeaveCrypto.prototype = { } finally { timer = null; - generatorDone(this, self, onComplete, ret); + Utils.generatorDone(this, self, onComplete, ret); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -187,7 +187,7 @@ WeaveCrypto.prototype = { PBEdecrypt: function Crypto_PBEdecrypt(onComplete, data, identity, algorithm) { let [self, cont] = yield; - let listener = new EventListener(cont); + let listener = new Utils.EventListener(cont); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let ret; @@ -235,7 +235,7 @@ WeaveCrypto.prototype = { } finally { timer = null; - generatorDone(this, self, onComplete, ret); + Utils.generatorDone(this, self, onComplete, ret); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -268,24 +268,24 @@ function openssl(op, algorithm, input, password) { throw "encryption not supported on this platform: " + os; } - let inputFile = getTmp("input"); - let [inputFOS] = open(inputFile, ">"); + let inputFile = Utils.getTmp("input"); + let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.write(input, input.length); inputFOS.close(); - let outputFile = getTmp("output"); + let outputFile = Utils.getTmp("output"); if (outputFile.exists()) outputFile.remove(false); // nsIProcess doesn't support stdin, so we write a file instead - let passFile = getTmp("pass"); - let [passFOS] = open(passFile, ">", PERMS_PASSFILE); + let passFile = Utils.getTmp("pass"); + let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); passFOS.write(password, password.length); passFOS.close(); try { - runCmd(wrap, getTmp().path, bin, algorithm, op, "-a", "-salt", - "-in", "input", "-out", "output", "-pass", "file:pass"); + Utils.runCmd(wrap, Utils.getTmp().path, bin, algorithm, op, "-a", "-salt", + "-in", "input", "-out", "output", "-pass", "file:pass"); } catch (e) { throw e; } finally { @@ -293,8 +293,8 @@ function openssl(op, algorithm, input, password) { inputFile.remove(false); } - let [outputFIS] = open(outputFile, "<"); - let ret = readStream(outputFIS); + let [outputFIS] = Utils.open(outputFile, "<"); + let ret = Utils.readStream(outputFIS); outputFIS.close(); outputFile.remove(false); diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 6dc3f04d4ea6..59a37465825a 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -45,7 +45,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Function.prototype.async = generatorAsync; +Function.prototype.async = Utils.generatorAsync; /* * DAV object @@ -90,8 +90,8 @@ DAVCollection.prototype = { let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); - request.addEventListener("load", new EventListener(cont, "load"), false); - request.addEventListener("error", new EventListener(cont, "error"), false); + request.addEventListener("load", new Utils.EventListener(cont, "load"), false); + request.addEventListener("error", new Utils.EventListener(cont, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); request.open(op, this._baseURL + path, true); @@ -128,7 +128,7 @@ DAVCollection.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self, onComplete, ret); + Utils.generatorDone(this, self, onComplete, ret); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -184,7 +184,7 @@ DAVCollection.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self, onComplete, ret); + Utils.generatorDone(this, self, onComplete, ret); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -244,7 +244,7 @@ DAVCollection.prototype = { this._log.info("Logging in"); - let URI = makeURI(this._baseURL); + let URI = Utils.makeURI(this._baseURL); this._auth = "Basic " + btoa(username + ":" + password); // Make a call to make sure it's working @@ -260,7 +260,7 @@ DAVCollection.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self, onComplete, this._loggedIn); + Utils.generatorDone(this, self, onComplete, this._loggedIn); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -290,7 +290,7 @@ DAVCollection.prototype = { if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) return; - let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); + let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); ret = token.textContent; @@ -302,7 +302,7 @@ DAVCollection.prototype = { this._log.debug("Found an active lock token"); else this._log.debug("No active lock token found"); - generatorDone(this, self, onComplete, ret); + Utils.generatorDone(this, self, onComplete, ret); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -331,7 +331,7 @@ DAVCollection.prototype = { if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) return; - let tokens = xpath(resp.responseXML, '//D:locktoken/D:href'); + let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); if (token) this._token = token.textContent; @@ -344,7 +344,7 @@ DAVCollection.prototype = { this._log.info("Lock acquired"); else this._log.warn("Could not acquire lock"); - generatorDone(this, self, onComplete, this._token); + Utils.generatorDone(this, self, onComplete, this._token); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -374,10 +374,10 @@ DAVCollection.prototype = { } finally { if (this._token) { this._log.info("Could not release lock"); - generatorDone(this, self, onComplete, false); + Utils.generatorDone(this, self, onComplete, false); } else { this._log.info("Lock released (or we didn't have one)"); - generatorDone(this, self, onComplete, true); + Utils.generatorDone(this, self, onComplete, true); } yield; // onComplete is responsible for closing the generator } @@ -411,7 +411,7 @@ DAVCollection.prototype = { this._log.debug("Lock released"); else this._log.debug("No lock released"); - generatorDone(this, self, onComplete, unlocked); + Utils.generatorDone(this, self, onComplete, unlocked); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -434,7 +434,7 @@ DAVCollection.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self, onComplete, stolen); + Utils.generatorDone(this, self, onComplete, stolen); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3f56abc12907..987caaad2a42 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -49,7 +49,7 @@ Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/syncCores.js"); -Function.prototype.async = generatorAsync; +Function.prototype.async = Utils.generatorAsync; let Crypto = new WeaveCrypto(); function Engine(davCollection, cryptoId) { @@ -171,7 +171,7 @@ Engine.prototype = { this._log.debug("Server reset failed"); this._os.notifyObservers(null, this._osPrefix + "reset-server:error", ""); } - generatorDone(this, self, onComplete, done) + Utils.generatorDone(this, self, onComplete, done) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -200,7 +200,7 @@ Engine.prototype = { this._log.debug("Client reset failed"); this._os.notifyObservers(null, this._osPrefix + "reset-client:error", ""); } - generatorDone(this, self, onComplete, done); + Utils.generatorDone(this, self, onComplete, done); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -366,7 +366,7 @@ Engine.prototype = { // Log an error if not the same if (!(serverConflicts.length || - deepEquals(serverChanges, serverDelta))) + Utils.deepEquals(serverChanges, serverDelta))) this._log.warn("Predicted server changes differ from " + "actual server->client diff (can be ignored in many cases)"); @@ -437,10 +437,10 @@ Engine.prototype = { } if (ok && synced) { this._os.notifyObservers(null, this._osPrefix + "sync:success", ""); - generatorDone(this, self, onComplete, true); + Utils.generatorDone(this, self, onComplete, true); } else { this._os.notifyObservers(null, this._osPrefix + "sync:error", ""); - generatorDone(this, self, onComplete, false); + Utils.generatorDone(this, self, onComplete, false); } yield; // onComplete is responsible for closing the generator } @@ -496,7 +496,7 @@ Engine.prototype = { if (status.formatVersion > STORAGE_FORMAT_VERSION) { this._log.error("Server uses storage format v" + status.formatVersion + ", this client understands up to v" + STORAGE_FORMAT_VERSION); - generatorDone(this, self, onComplete, ret) + Utils.generatorDone(this, self, onComplete, ret) return; } @@ -634,7 +634,7 @@ Engine.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self, onComplete, ret) + Utils.generatorDone(this, self, onComplete, ret) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -680,7 +680,7 @@ Engine.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self, onComplete, ret) + Utils.generatorDone(this, self, onComplete, ret) yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 54679390faae..5fa5851b4687 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -51,7 +51,7 @@ Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); -Function.prototype.async = generatorAsync; +Function.prototype.async = Utils.generatorAsync; /* * Service singleton @@ -61,6 +61,14 @@ Function.prototype.async = generatorAsync; function WeaveSyncService() { this._init(); } WeaveSyncService.prototype = { + __prefs: null, + get _prefs() { + if (!this.__prefs) + this.__prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + return this.__prefs; + }, + __os: null, get _os() { if (!this.__os) @@ -122,17 +130,13 @@ WeaveSyncService.prototype = { }, get username() { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - return branch.getCharPref("extensions.weave.username"); + return this._prefs.getCharPref("extensions.weave.username"); }, set username(value) { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); if (value) - branch.setCharPref("extensions.weave.username", value); + this._prefs.setCharPref("extensions.weave.username", value); else - branch.clearUserPref("extensions.weave.username"); + this._prefs.clearUserPref("extensions.weave.username"); // fixme - need to loop over all Identity objects - needs some rethinking... this._mozId.username = value; @@ -185,13 +189,11 @@ WeaveSyncService.prototype = { let enabled = false; let schedule = 0; try { - let branch = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch2); - this._serverURL = branch.getCharPref("extensions.weave.serverURL"); - enabled = branch.getBoolPref("extensions.weave.enabled"); - schedule = branch.getIntPref("extensions.weave.schedule"); + this._serverURL = this._prefs.getCharPref("extensions.weave.serverURL"); + enabled = this._prefs.getBoolPref("extensions.weave.enabled"); + schedule = this._prefs.getIntPref("extensions.weave.schedule"); - branch.addObserver("extensions.weave", this, false); + this._prefs.addObserver("extensions.weave", this, false); } catch (ex) { /* use defaults */ } @@ -215,10 +217,13 @@ WeaveSyncService.prototype = { } }, + _scheduleChanged: function WeaveSync__scheduleChanged() { + }, + _enableSchedule: function WeaveSync__enableSchedule() { this._scheduleTimer = Cc["@mozilla.org/timer;1"]. createInstance(Ci.nsITimer); - let listener = new EventListener(bind2(this, this._onSchedule)); + let listener = new Utils.EventListener(Utils.bind2(this, this._onSchedule)); this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min this._scheduleTimer.TYPE_REPEATING_SLACK); }, @@ -345,7 +350,7 @@ WeaveSyncService.prototype = { //this._log.debug("Login error"); this._os.notifyObservers(null, "weave:service-login:error", ""); } - generatorDone(this, self, onComplete, success); + Utils.generatorDone(this, self, onComplete, success); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -373,7 +378,7 @@ WeaveSyncService.prototype = { this._log.debug("Server lock reset failed"); this._os.notifyObservers(null, "weave:server-lock-reset:error", ""); } - generatorDone(this, self, onComplete, success); + Utils.generatorDone(this, self, onComplete, success); yield; // generatorDone is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -405,7 +410,7 @@ WeaveSyncService.prototype = { this._os.notifyObservers(null, "weave:service:sync:success", ""); else this._os.notifyObservers(null, "weave:service:sync:error", ""); - generatorDone(this, self); + Utils.generatorDone(this, self); yield; // generatorDone is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -427,7 +432,7 @@ WeaveSyncService.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self); + Utils.generatorDone(this, self); yield; // generatorDone is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -449,7 +454,7 @@ WeaveSyncService.prototype = { this._log.error("Exception caught: " + (e.message? e.message : e)); } finally { - generatorDone(this, self); + Utils.generatorDone(this, self); yield; // generatorDone is responsible for closing the generator } this._log.warn("generator not properly closed"); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index bc09a0f4a6ee..692bfd5a11e0 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -360,7 +360,7 @@ BookmarksStore.prototype = { item.title = node.title; } item.URI = node.uri; - item.tags = this._ts.getTagsForURI(makeURI(node.uri), {}); + item.tags = this._ts.getTagsForURI(Utils.makeURI(node.uri), {}); item.keyword = this._bms.getKeywordForBookmark(node.itemId); } else if (node.type == node.RESULT_TYPE_SEPARATOR) { item.type = "separator"; @@ -404,7 +404,7 @@ BookmarksStore.prototype = { case "bookmark": case "microsummary": { this._log.info(" -> creating bookmark \"" + command.data.title + "\""); - let URI = makeURI(command.data.URI); + let URI = Utils.makeURI(command.data.URI); newId = this._bms.insertBookmark(parentId, URI, command.data.index, @@ -415,7 +415,7 @@ BookmarksStore.prototype = { if (command.data.type == "microsummary") { this._log.info(" \-> is a microsummary"); - let genURI = makeURI(command.data.generatorURI); + let genURI = Utils.makeURI(command.data.generatorURI); try { let micsum = this._ms.createMicrosummary(URI, genURI); this._ms.setMicrosummary(newId, micsum); @@ -433,8 +433,8 @@ BookmarksStore.prototype = { this._log.info(" -> creating livemark \"" + command.data.title + "\""); newId = this._ls.createLivemark(parentId, command.data.title, - makeURI(command.data.siteURI), - makeURI(command.data.feedURI), + Utils.makeURI(command.data.siteURI), + Utils.makeURI(command.data.feedURI), command.data.index); break; case "separator": @@ -498,7 +498,7 @@ BookmarksStore.prototype = { this._bms.setItemTitle(itemId, command.data.title); break; case "URI": - this._bms.changeBookmarkURI(itemId, makeURI(command.data.URI)); + this._bms.changeBookmarkURI(itemId, Utils.makeURI(command.data.URI)); break; case "index": this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), @@ -520,16 +520,16 @@ BookmarksStore.prototype = { this._bms.setKeywordForBookmark(itemId, command.data.keyword); break; case "generatorURI": { - let micsumURI = makeURI(this._bms.getBookmarkURI(itemId)); - let genURI = makeURI(command.data.generatorURI); + let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); + let genURI = Utils.makeURI(command.data.generatorURI); let micsum = this._ms.createMicrosummary(micsumURI, genURI); this._ms.setMicrosummary(itemId, micsum); } break; case "siteURI": - this._ls.setSiteURI(itemId, makeURI(command.data.siteURI)); + this._ls.setSiteURI(itemId, Utils.makeURI(command.data.siteURI)); break; case "feedURI": - this._ls.setFeedURI(itemId, makeURI(command.data.feedURI)); + this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI)); break; default: this._log.warn("Can't change item property: " + key); @@ -595,10 +595,10 @@ HistoryStore.prototype = { _createCommand: function HistStore__createCommand(command) { this._log.info(" -> creating history entry: " + command.GUID); try { - this._browserHist.addPageWithDetails(makeURI(command.GUID), + this._browserHist.addPageWithDetails(Utils.makeURI(command.GUID), command.data.title, command.data.time); - this._hsvc.setPageDetails(makeURI(command.GUID), command.data.title, + this._hsvc.setPageDetails(Utils.makeURI(command.GUID), command.data.title, command.data.accessCount, false, false); } catch (e) { this._log.error("Exception caught: " + (e.message? e.message : e)); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 307c36cd7e7a..821ee9774d40 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -46,7 +46,7 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Function.prototype.async = generatorAsync; +Function.prototype.async = Utils.generatorAsync; /* * SyncCore objects @@ -70,7 +70,7 @@ SyncCore.prototype = { _getEdits: function SC__getEdits(a, b) { let ret = {numProps: 0, props: {}}; for (prop in a) { - if (!deepEquals(a[prop], b[prop])) { + if (!Utils.deepEquals(a[prop], b[prop])) { ret.numProps++; ret.props[prop] = b[prop]; } @@ -91,7 +91,7 @@ SyncCore.prototype = { _detectUpdates: function SC__detectUpdates(onComplete, a, b) { let [self, cont] = yield; - let listener = new EventListener(cont); + let listener = new Utils.EventListener(cont); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let cmds = []; @@ -145,7 +145,7 @@ SyncCore.prototype = { } finally { timer = null; - generatorDone(this, self, onComplete, cmds); + Utils.generatorDone(this, self, onComplete, cmds); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); @@ -162,11 +162,11 @@ SyncCore.prototype = { // Check that all other properties are the same // FIXME: could be optimized... for (let key in a) { - if (key != "GUID" && !deepEquals(a[key], b[key])) + if (key != "GUID" && !Utils.deepEquals(a[key], b[key])) return false; } for (let key in b) { - if (key != "GUID" && !deepEquals(a[key], b[key])) + if (key != "GUID" && !Utils.deepEquals(a[key], b[key])) return false; } return true; @@ -189,7 +189,7 @@ SyncCore.prototype = { }, _conflicts: function SC__conflicts(a, b) { - if ((a.GUID == b.GUID) && !deepEquals(a, b)) + if ((a.GUID == b.GUID) && !Utils.deepEquals(a, b)) return true; return false; }, @@ -218,7 +218,7 @@ SyncCore.prototype = { _reconcile: function SC__reconcile(onComplete, listA, listB) { let [self, cont] = yield; - let listener = new EventListener(cont); + let listener = new Utils.EventListener(cont); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let propagations = [[], []]; @@ -242,7 +242,7 @@ SyncCore.prototype = { if (skip) return true; - if (deepEquals(a, b)) { + if (Utils.deepEquals(a, b)) { delete listA[i]; // a skip = true; return false; // b @@ -301,7 +301,7 @@ SyncCore.prototype = { } finally { timer = null; - generatorDone(this, self, onComplete, ret); + Utils.generatorDone(this, self, onComplete, ret); yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index e44f911bc32b..f731671b4a66 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -34,10 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['deepEquals', 'makeFile', 'makeURI', 'xpath', - 'bind2', 'generatorAsync', 'generatorDone', - 'EventListener', - 'runCmd', 'getTmp', 'open', 'readStream']; +const EXPORTED_SYMBOLS = ['Utils']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -52,132 +49,223 @@ Cu.import("resource://weave/log4moz.js"); * Utility functions */ -function deepEquals(a, b) { - if (!a && !b) +let Utils = { + + deepEquals: function Weave_deepEquals(a, b) { + if (!a && !b) + return true; + if (!a || !b) + return false; + + if (typeof(a) != "object" && typeof(b) != "object") + return a == b; + if (typeof(a) != "object" || typeof(b) != "object") + return false; + + for (let key in a) { + if (typeof(a[key]) == "object") { + if (!typeof(b[key]) == "object") + return false; + if (!Utils.deepEquals(a[key], b[key])) + return false; + } else { + if (a[key] != b[key]) + return false; + } + } return true; - if (!a || !b) - return false; + }, - if (typeof(a) != "object" && typeof(b) != "object") - return a == b; - if (typeof(a) != "object" || typeof(b) != "object") - return false; - - for (let key in a) { - if (typeof(a[key]) == "object") { - if (!typeof(b[key]) == "object") - return false; - if (!deepEquals(a[key], b[key])) - return false; - } else { - if (a[key] != b[key]) - return false; - } - } - return true; -} - -function makeFile(path) { - var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - file.initWithPath(path); - return file; -} - -function makeURI(URIString) { - if (URIString === null || URIString == "") - return null; - let ioservice = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - return ioservice.newURI(URIString, null, null); -} - -function xpath(xmlDoc, xpathString) { - let root = xmlDoc.ownerDocument == null ? - xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement - let nsResolver = xmlDoc.createNSResolver(root); - - return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, - Ci.nsIDOMXPathResult.ANY_TYPE, null); -} - -function bind2(object, method) { - return function innerBind() { return method.apply(object, arguments); } -} - -// Meant to be used like this in code that imports this file: -// -// Function.prototype.async = generatorAsync; -// -// So that you can do: -// -// gen = fooGen.async(...); -// ret = yield; -// -// where fooGen is a generator function, and gen is the running generator. -// ret is whatever the generator 'returns' via generatorDone(). - -function generatorAsync(self, extra_args) { - try { - let args = Array.prototype.slice.call(arguments, 1); - let gen = this.apply(self, args); - gen.next(); // must initialize before sending - gen.send([gen, function(data) {continueGenerator(gen, data);}]); - return gen; - } catch (e) { - if (e instanceof StopIteration) { - dump("async warning: generator stopped unexpectedly"); + makeURI: function Weave_makeURI(URIString) { + if (URIString === null || URIString == "") return null; - } else { - dump("Exception caught: " + e.message); + let ioservice = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return ioservice.newURI(URIString, null, null); + }, + + xpath: function Weave_xpath(xmlDoc, xpathString) { + let root = xmlDoc.ownerDocument == null ? + xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement + let nsResolver = xmlDoc.createNSResolver(root); + + return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, + Ci.nsIDOMXPathResult.ANY_TYPE, null); + }, + + bind2: function Weave_bind2(object, method) { + return function innerBind() { return method.apply(object, arguments); } + }, + + // Meant to be used like this in code that imports this file: + // + // Function.prototype.async = generatorAsync; + // + // So that you can do: + // + // gen = fooGen.async(...); + // ret = yield; + // + // where fooGen is a generator function, and gen is the running generator. + // ret is whatever the generator 'returns' via generatorDone(). + + generatorAsync: function Weave_generatorAsync(self, extra_args) { + try { + let args = Array.prototype.slice.call(arguments, 1); + let gen = this.apply(self, args); + gen.next(); // must initialize before sending + gen.send([gen, function(data) {Utils.continueGenerator(gen, data);}]); + return gen; + } catch (e) { + if (e instanceof StopIteration) { + dump("async warning: generator stopped unexpectedly"); + return null; + } else { + dump("Exception caught: " + e.message); + } } + }, + + continueGenerator: function Weave_continueGenerator(generator, data) { + try { generator.send(data); } + catch (e) { + if (e instanceof StopIteration) + dump("continueGenerator warning: generator stopped unexpectedly"); + else + dump("Exception caught: " + e.message); + } + }, + + // generators created using Function.async can't simply call the + // callback with the return value, since that would cause the calling + // function to end up running (after the yield) from inside the + // generator. Instead, generators can call this method which sets up + // a timer to call the callback from a timer (and cleans up the timer + // to avoid leaks). It also closes generators after the timeout, to + // keep things clean. + generatorDone: function Weave_generatorDone(object, generator, callback, retval) { + if (object._timer) + throw "Called generatorDone when there is a timer already set." + + let cb = Utils.bind2(object, function(event) { + generator.close(); + generator = null; + object._timer = null; + if (callback) + callback(retval); + }); + + object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + object._timer.initWithCallback(new Utils.EventListener(cb), + 0, object._timer.TYPE_ONE_SHOT); + }, + + runCmd: function Weave_runCmd() { + var binary; + var args = []; + + for (let i = 0; i < arguments.length; ++i) { + args.push(arguments[i]); + } + + if (args[0] instanceof Ci.nsIFile) { + binary = args.shift(); + } else { + binary = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + binary.initWithPath(args.shift()); + } + + var p = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + p.init(binary); + + p.run(true, args, args.length); + return p.exitValue; + }, + + getTmp: function Weave_getTmp(name) { + let ds = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + + let tmp = ds.get("ProfD", Ci.nsIFile); + tmp.QueryInterface(Ci.nsILocalFile); + + tmp.append("weave"); + tmp.append("tmp"); + if (!tmp.exists()) + tmp.create(tmp.DIRECTORY_TYPE, PERMS_DIRECTORY); + + if (name) + tmp.append(name); + + return tmp; + }, + + open: function open(pathOrFile, mode, perms) { + let stream, file; + + if (pathOrFile instanceof Ci.nsIFile) { + file = pathOrFile; + } else { + file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + dump("PATH IS" + pathOrFile + "\n"); + file.initWithPath(pathOrFile); + } + + if (!perms) + perms = PERMS_FILE; + + switch(mode) { + case "<": { + if (!file.exists()) + throw "Cannot open file for reading, file does not exist"; + stream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + stream.init(file, MODE_RDONLY, perms, 0); + stream.QueryInterface(Ci.nsILineInputStream); + } break; + + case ">": { + stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(file, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, perms, 0); + } break; + + case ">>": { + stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(file, MODE_WRONLY | MODE_CREATE | MODE_APPEND, perms, 0); + } break; + + default: + throw "Illegal mode to open(): " + mode; + } + + return [stream, file]; + }, + + readStream: function Weave_readStream(fis) { + let data = ""; + while (fis.available()) { + let ret = {}; + fis.readLine(ret); + data += ret.value; + } + return data; + }, + + /* + * Event listener object + * Used to handle XMLHttpRequest and nsITimer callbacks + */ + + EventListener: function Weave_EventListener(handler, eventName) { + this._handler = handler; + this._eventName = eventName; + this._log = Log4Moz.Service.getLogger("Service.EventHandler"); } -} +}; -function continueGenerator(generator, data) { - try { generator.send(data); } - catch (e) { - if (e instanceof StopIteration) - dump("continueGenerator warning: generator stopped unexpectedly"); - else - dump("Exception caught: " + e.message); - } -} - -// generators created using Function.async can't simply call the -// callback with the return value, since that would cause the calling -// function to end up running (after the yield) from inside the -// generator. Instead, generators can call this method which sets up -// a timer to call the callback from a timer (and cleans up the timer -// to avoid leaks). It also closes generators after the timeout, to -// keep things clean. -function generatorDone(object, generator, callback, retval) { - if (object._timer) - throw "Called generatorDone when there is a timer already set." - - let cb = bind2(object, function(event) { - generator.close(); - generator = null; - object._timer = null; - if (callback) - callback(retval); - }); - - object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - object._timer.initWithCallback(new EventListener(cb), - 0, object._timer.TYPE_ONE_SHOT); -} - -/* - * Event listener object - * Used to handle XMLHttpRequest and nsITimer callbacks - */ - -function EventListener(handler, eventName) { - this._handler = handler; - this._eventName = eventName; - this._log = Log4Moz.Service.getLogger("Service.EventHandler"); -} -EventListener.prototype = { +Utils.EventListener.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), // DOM event listener @@ -191,96 +279,4 @@ EventListener.prototype = { this._log.trace("Timer fired"); this._handler(timer); } -}; - -function runCmd() { - var binary; - var args = []; - - for (let i = 0; i < arguments.length; ++i) { - args.push(arguments[i]); - } - - if (args[0] instanceof Ci.nsIFile) { - binary = args.shift(); - } else { - binary = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - binary.initWithPath(args.shift()); - } - - var p = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); - p.init(binary); - - p.run(true, args, args.length); - return p.exitValue; -} - -function getTmp(name) { - let ds = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - - let tmp = ds.get("ProfD", Ci.nsIFile); - tmp.QueryInterface(Ci.nsILocalFile); - - tmp.append("weave"); - tmp.append("tmp"); - if (!tmp.exists()) - tmp.create(tmp.DIRECTORY_TYPE, PERMS_DIRECTORY); - - if (name) - tmp.append(name); - - return tmp; -} - -function open(pathOrFile, mode, perms) { - let stream, file; - - if (pathOrFile instanceof Ci.nsIFile) { - file = pathOrFile; - } else { - file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - file.initWithPath(pathOrFile); - } - - if (!perms) - perms = PERMS_FILE; - - switch(mode) { - case "<": { - if (!file.exists()) - throw "Cannot open file for reading, file does not exist"; - stream = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - stream.init(file, MODE_RDONLY, perms, 0); - stream.QueryInterface(Ci.nsILineInputStream); - } break; - - case ">": { - stream = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - stream.init(file, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, perms, 0); - } break; - - case ">>": { - stream = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - stream.init(file, MODE_WRONLY | MODE_CREATE | MODE_APPEND, perms, 0); - } break; - - default: - throw "Illegal mode to open(): " + mode; - } - - return [stream, file]; -} - -function readStream(fis) { - let data = ""; - while (fis.available()) { - let ret = {}; - fis.readLine(ret); - data += ret.value; - } - return data; } From 7d712ce15a826ac09cdc1b02316e03be8dc136fb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 13 Feb 2008 16:07:11 -0800 Subject: [PATCH 0138/1860] clean up prefs; add logging prefs; check weave is enabled on scheduled sync --- services/sync/modules/constants.js | 2 +- services/sync/modules/service.js | 129 +++++++++++++---------------- services/sync/services-sync.js | 14 +++- 3 files changed, 71 insertions(+), 74 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index e95add071cad..ce4ed9d38e8a 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', - 'PREFS_PRANCH', + 'PREFS_BRANCH', 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5fa5851b4687..7d3279512c4f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -63,9 +63,12 @@ WeaveSyncService.prototype = { __prefs: null, get _prefs() { - if (!this.__prefs) + if (!this.__prefs) { this.__prefs = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); + .getService(Ci.nsIPrefService); + this.__prefs = this.__prefs.getBranch(PREFS_BRANCH); + this.__prefs.QueryInterface(Ci.nsIPrefBranch2); + } return this.__prefs; }, @@ -130,13 +133,13 @@ WeaveSyncService.prototype = { }, get username() { - return this._prefs.getCharPref("extensions.weave.username"); + return this._prefs.getCharPref("username"); }, set username(value) { if (value) - this._prefs.setCharPref("extensions.weave.username", value); + this._prefs.setCharPref("username", value); else - this._prefs.clearUserPref("extensions.weave.username"); + this._prefs.clearUserPref("username"); // fixme - need to loop over all Identity objects - needs some rethinking... this._mozId.username = value; @@ -180,61 +183,70 @@ WeaveSyncService.prototype = { return null; }, + get enabled() { + return this._prefs.getBoolPref("enabled"); + }, + + get schedule() { + if (!this.enabled) + return 0; // manual/off + return this._prefs.getIntPref("schedule"); + }, + _init: function WeaveSync__init() { this._initLogs(); this._log.info("Weave Sync Service Initializing"); - this._serverURL = 'https://services.mozilla.com/'; - this._user = ''; - let enabled = false; - let schedule = 0; - try { - this._serverURL = this._prefs.getCharPref("extensions.weave.serverURL"); - enabled = this._prefs.getBoolPref("extensions.weave.enabled"); - schedule = this._prefs.getIntPref("extensions.weave.schedule"); + this._prefs.addObserver("", this, false); - this._prefs.addObserver("extensions.weave", this, false); - } - catch (ex) { /* use defaults */ } - - if (!enabled) { - this._log.info("Bookmarks sync disabled"); + if (!this.enabled) { + this._log.info("Weave Sync disabled"); return; } - switch (schedule) { + this._setSchedule(this.schedule); + }, + + _setSchedule: function Weave__setSchedule(schedule) { + switch (this.schedule) { case 0: - this._log.info("Bookmarks sync enabled, manual mode"); + this._disableSchedule(); break; case 1: - this._log.info("Bookmarks sync enabled, automagic mode"); this._enableSchedule(); break; default: - this._log.info("Bookmarks sync enabled"); - this._log.info("Invalid schedule setting: " + schedule); + this._log.info("Invalid Weave scheduler setting: " + schedule); break; } }, - _scheduleChanged: function WeaveSync__scheduleChanged() { - }, - _enableSchedule: function WeaveSync__enableSchedule() { + if (this._scheduleTimer) { + this._scheduleTimer.cancel(); + this._scheduleTimer = null; + } this._scheduleTimer = Cc["@mozilla.org/timer;1"]. createInstance(Ci.nsITimer); let listener = new Utils.EventListener(Utils.bind2(this, this._onSchedule)); this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min this._scheduleTimer.TYPE_REPEATING_SLACK); + this._log.info("Weave scheduler enabled"); }, _disableSchedule: function WeaveSync__disableSchedule() { - this._scheduleTimer = null; + if (this._scheduleTimer) { + this._scheduleTimer.cancel(); + this._scheduleTimer = null; + } + this._log.info("Weave scheduler disabled"); }, _onSchedule: function WeaveSync__onSchedule() { - this._log.info("Running scheduled sync"); - this.sync(); + if (this.enabled) { + this._log.info("Running scheduled sync"); + this.sync(); + } }, _initLogs: function WeaveSync__initLogs() { @@ -242,14 +254,14 @@ WeaveSyncService.prototype = { let formatter = Log4Moz.Service.newFormatter("basic"); let root = Log4Moz.Service.rootLogger; - root.level = Log4Moz.Level.Debug; + root.level = Log4Moz.Level[this._prefs.getCharPref("log.rootLogger")]; let capp = Log4Moz.Service.newAppender("console", formatter); - capp.level = Log4Moz.Level.Warn; + capp.level = Log4Moz.Level[this._prefs.getCharPref("log.appender.console")]; root.addAppender(capp); let dapp = Log4Moz.Service.newAppender("dump", formatter); - dapp.level = Log4Moz.Level.All; + dapp.level = Log4Moz.Level[this._prefs.getCharPref("log.appender.dump")]; root.addAppender(dapp); let brief = this._dirSvc.get("ProfD", Ci.nsIFile); @@ -270,10 +282,10 @@ WeaveSyncService.prototype = { verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); let fapp = Log4Moz.Service.newFileAppender("rotating", brief, formatter); - fapp.level = Log4Moz.Level.Info; + fapp.level = Log4Moz.Level[this._prefs.getCharPref("log.appender.briefLog")]; root.addAppender(fapp); let vapp = Log4Moz.Service.newFileAppender("rotating", verbose, formatter); - vapp.level = Log4Moz.Level.Debug; + vapp.level = Log4Moz.Level[this._prefs.getCharPref("log.appender.debugLog")]; root.addAppender(vapp); }, @@ -295,8 +307,6 @@ WeaveSyncService.prototype = { this._os.notifyObservers(null, "weave:service-unlock:success", ""); }, - // IBookmarksSyncService internal implementation - _login: function WeaveSync__login(onComplete) { let [self, cont] = yield; let success = false; @@ -314,7 +324,8 @@ WeaveSyncService.prototype = { return; } - this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; + let serverURL = this._prefs.getCharPref("serverURL"); + this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; this._log.info("Using server URL: " + this._dav.baseURL); this._dav.login.async(this._dav, cont, this.username, this.password); @@ -324,13 +335,13 @@ WeaveSyncService.prototype = { if (!success) { this._log.debug("Attempting to create user directory"); - this._dav.baseURL = this._serverURL; + this._dav.baseURL = serverURL; this._dav.MKCOL("user/" + this.userPath, cont); let ret = yield; if (ret.status == 201) { this._log.debug("Successfully created user directory. Re-attempting login."); - this._dav.baseURL = this._serverURL + "user/" + this.userPath + "/"; + this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; this._dav.login.async(this._dav, cont, this.username, this.password); success = yield; } else { @@ -465,41 +476,17 @@ WeaveSyncService.prototype = { // nsIObserver observe: function WeaveSync__observe(subject, topic, data) { - switch (topic) { - case "extensions.weave.enabled": - switch (data) { - case false: - this._log.info("Disabling automagic bookmarks sync"); - this._disableSchedule(); - break; - case true: - this._log.info("Enabling automagic bookmarks sync"); - this._enableSchedule(); - break; - } + if (topic != "nsPref:changed") + return; + + switch (data) { + case "enabled": // this works because this.schedule is 0 when disabled + case "schedule": + this._setSchedule(this.schedule); break; - case "extensions.weave.schedule": - switch (data) { - case 0: - this._log.info("Disabling automagic bookmarks sync"); - this._disableSchedule(); - break; - case 1: - this._log.info("Enabling automagic bookmarks sync"); - this._enableSchedule(); - break; - default: - this._log.warn("Unknown schedule value set"); - break; - } - break; - default: - // ignore, there are prefs we observe but don't care about } }, - // IBookmarksSyncService public methods - // These are global (for all engines) login: function WeaveSync_login(password, passphrase) { diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 3170f0a9981d..4ff2bc74417d 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,11 +1,21 @@ pref("extensions.weave.serverURL", "https://services.mozilla.com/"); pref("extensions.weave.username", "nobody@mozilla.com"); + +pref("extensions.weave.encryption", "aes-256-cbc"); + pref("extensions.weave.lastversion", "firstrun"); +pref("extensions.weave.lastsync", "0"); + pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); pref("extensions.weave.bookmarks", true); pref("extensions.weave.history", true); pref("extensions.weave.schedule", 1); -pref("extensions.weave.lastsync", "0"); -pref("extensions.weave.encryption", "aes-256-cbc"); + + +pref("extensions.weave.log.rootLogger", "Config"); +pref("extensions.weave.log.appender.console", "Warn"); +pref("extensions.weave.log.appender.dump", "Error"); +pref("extensions.weave.log.appender.briefLog", "Info"); +pref("extensions.weave.log.appender.debugLog", "Config"); From 5405db92d6963f06502103066468e220ad0de5e3 Mon Sep 17 00:00:00 2001 From: Date: Mon, 18 Feb 2008 11:18:04 -0800 Subject: [PATCH 0139/1860] move openssl() into crypto object so it can use the logger there. up version --- services/sync/modules/crypto.js | 125 ++++++++++++++++---------------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 9fc2b114da5d..70f04d3e87c8 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -92,6 +92,68 @@ WeaveCrypto.prototype = { branch.addObserver("extensions.weave.encryption", this, false); }, + _openssl: function Crypto__openssl(op, algorithm, input, password) { + let extMgr = Components.classes["@mozilla.org/extensions/manager;1"] + .getService(Components.interfaces.nsIExtensionManager); + let loc = extMgr.getInstallLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); + + let wrap = loc.getItemLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); + wrap.append("openssl"); + let bin; + + let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; + switch(os) { + case "WINNT": + wrap.append("win32"); + wrap.append("exec.bat"); + bin = wrap.parent.path + "\\openssl.exe"; + break; + case "Linux": + case "Darwin": + wrap.append("unix"); + wrap.append("exec.sh"); + bin = "openssl"; + break; + default: + throw "encryption not supported on this platform: " + os; + } + + let inputFile = Utils.getTmp("input"); + let [inputFOS] = Utils.open(inputFile, ">"); + inputFOS.write(input, input.length); + inputFOS.close(); + + let outputFile = Utils.getTmp("output"); + if (outputFile.exists()) + outputFile.remove(false); + + // nsIProcess doesn't support stdin, so we write a file instead + let passFile = Utils.getTmp("pass"); + let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); + passFOS.write(password, password.length); + passFOS.close(); + + try { + this._log.debug("Running command: " + wrap.path + " " + + Utils.getTmp().path + " " + bin + " " + algorithm + " " + + op + "-a -salt -in input -out output -pass file:pass"); + Utils.runCmd(wrap, Utils.getTmp().path, bin, algorithm, op, "-a", "-salt", + "-in", "input", "-out", "output", "-pass", "file:pass"); + } catch (e) { + throw e; + } finally { + //passFile.remove(false); + //inputFile.remove(false); + } + + let [outputFIS] = Utils.open(outputFile, "<"); + let ret = Utils.readStream(outputFIS); + outputFIS.close(); + //outputFile.remove(false); + + return ret; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), // nsIObserver @@ -164,7 +226,7 @@ WeaveCrypto.prototype = { case "aes-256-cbc": case "bf-cbc": case "des-ede3-cbc": - ret = openssl("-e", algorithm, data, identity.password); + ret = this._openssl("-e", algorithm, data, identity.password); break; default: @@ -220,7 +282,7 @@ WeaveCrypto.prototype = { case "aes-256-cbc": case "bf-cbc": case "des-ede3-cbc": - ret = openssl("-d", algorithm, data, identity.password); + ret = this._openssl("-d", algorithm, data, identity.password); break; default: @@ -241,62 +303,3 @@ WeaveCrypto.prototype = { this._log.warn("generator not properly closed"); } }; - -function openssl(op, algorithm, input, password) { - let extMgr = Components.classes["@mozilla.org/extensions/manager;1"] - .getService(Components.interfaces.nsIExtensionManager); - let loc = extMgr.getInstallLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); - - let wrap = loc.getItemLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); - wrap.append("openssl"); - let bin; - - let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; - switch(os) { - case "WINNT": - wrap.append("win32"); - wrap.append("exec.bat"); - bin = wrap.parent.path + "\\openssl.exe"; - break; - case "Linux": - case "Darwin": - wrap.append("unix"); - wrap.append("exec.sh"); - bin = "openssl"; - break; - default: - throw "encryption not supported on this platform: " + os; - } - - let inputFile = Utils.getTmp("input"); - let [inputFOS] = Utils.open(inputFile, ">"); - inputFOS.write(input, input.length); - inputFOS.close(); - - let outputFile = Utils.getTmp("output"); - if (outputFile.exists()) - outputFile.remove(false); - - // nsIProcess doesn't support stdin, so we write a file instead - let passFile = Utils.getTmp("pass"); - let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.write(password, password.length); - passFOS.close(); - - try { - Utils.runCmd(wrap, Utils.getTmp().path, bin, algorithm, op, "-a", "-salt", - "-in", "input", "-out", "output", "-pass", "file:pass"); - } catch (e) { - throw e; - } finally { - passFile.remove(false); - inputFile.remove(false); - } - - let [outputFIS] = Utils.open(outputFile, "<"); - let ret = Utils.readStream(outputFIS); - outputFIS.close(); - outputFile.remove(false); - - return ret; -} From 1643505424c03f07e8e1ca9d7a427e68f860fd1f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 19 Feb 2008 11:39:39 -0800 Subject: [PATCH 0140/1860] flesh out openssl code; still needs more work --- services/sync/modules/crypto.js | 188 ++++++++++++++++++++++++++++++-- services/sync/modules/util.js | 3 + 2 files changed, 180 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 70f04d3e87c8..e4fabf1dbe51 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -92,7 +92,7 @@ WeaveCrypto.prototype = { branch.addObserver("extensions.weave.encryption", this, false); }, - _openssl: function Crypto__openssl(op, algorithm, input, password) { + _openssl: function Crypto__openssl() { let extMgr = Components.classes["@mozilla.org/extensions/manager;1"] .getService(Components.interfaces.nsIExtensionManager); let loc = extMgr.getInstallLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); @@ -118,6 +118,12 @@ WeaveCrypto.prototype = { throw "encryption not supported on this platform: " + os; } + let args = [wrap, Utils.getTmp().path, bin]; + args = args.concat(arguments); + return Utils.runCmd.apply(null, args); + }, + + _opensslPBE: function Crypto__openssl(op, algorithm, input, password) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.write(input, input.length); @@ -134,26 +140,170 @@ WeaveCrypto.prototype = { passFOS.close(); try { - this._log.debug("Running command: " + wrap.path + " " + - Utils.getTmp().path + " " + bin + " " + algorithm + " " + - op + "-a -salt -in input -out output -pass file:pass"); - Utils.runCmd(wrap, Utils.getTmp().path, bin, algorithm, op, "-a", "-salt", - "-in", "input", "-out", "output", "-pass", "file:pass"); + this._openssl(algorithm, op, "-a", "-salt", "-in", "input", + "-out", "output", "-pass", "file:pass"); + // FIXME: check rv + } catch (e) { throw e; + } finally { - //passFile.remove(false); - //inputFile.remove(false); + passFile.remove(false); + inputFile.remove(false); } let [outputFIS] = Utils.open(outputFile, "<"); let ret = Utils.readStream(outputFIS); outputFIS.close(); - //outputFile.remove(false); + outputFile.remove(false); return ret; }, + // generates a random string that can be used as a passphrase + _opensslRand: function Crypto__opensslRand(length) { + if (!length) + length = 256; + + let outputFile = Utils.getTmp("output"); + if (outputFile.exists()) + outputFile.remove(false); + + let rv = this._openssl("rand", "-base64", "-out", "output", length); + // FIXME: check rv + + let [outputFIS] = Utils.open(outputFile, "<"); + let ret = Utils.readStream(outputFIS); + outputFIS.close(); + outputFile.remove(false); + + return ret; + }, + + // generates an rsa public/private key pair, with the private key encrypted + _opensslRSAKeyGen: function Crypto__opensslRSAKeyGen(password, algorithm, bits) { + if (!algorithm) + algorithm = "aes-256-cbc"; + if (!bits) + bits = "2048"; + + let privKeyF = Utils.getTmp("privkey.pem"); + if (privKeyF.exists()) + privKeyF.remove(false); + + let rv = this._openssl("genrsa", "-out", "privkey.pem", bits); + // FIXME: check rv + + let pubKeyF = Utils.getTmp("pubkey.pem"); + if (pubKeyF.exists()) + pubKeyF.remove(false); + + rv = this._openssl("rsa", "-in", "privkey.pem", "-out", "pubkey.pem", + "-outform", "PEM", "-pubout"); + // FIXME: check rv + + let cryptedKeyF = Utils.getTmp("enckey.pem"); + if (cryptedKeyF.exists()) + cryptedKeyF.remove(false); + + // nsIProcess doesn't support stdin, so we write a file instead + let passFile = Utils.getTmp("pass"); + let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); + passFOS.write(password, password.length); + passFOS.close(); + + try { + rv = this._openssl("pkcs8", "-in", "privkey.pem", "-out", "enckey.pem", + "-topk8", "-v2", algorithm, "-pass", "file:pass"); + // FIXME: check rv + } catch (e) { + throw e; + } finally { + passFile.remove(false); + privKeyF.remove(false); + } + + let [cryptedKeyFIS] = Utils.open(cryptedKeyF, "<"); + let cryptedKey = Utils.readStream(cryptedKeyFIS); + cryptedKeyFIS.close(); + cryptedKey.remove(false); + + let [pubKeyFIS] = Utils.open(pubKeyF, "<"); + let pubKey = Utils.readStream(pubKeyFIS); + pubKeyFIS.close(); + pubKeyF.remove(false); + + return [cryptedKey, pubKey]; + }, + + // returns 'input' encrypted with the 'pubkey' public RSA key + _opensslRSAEncrypt: function Crypto__opensslRSAEncrypt(input, pubkey) { + let inputFile = Utils.getTmp("input"); + let [inputFOS] = Utils.open(inputFile, ">"); + inputFOS.write(input, input.length); + inputFOS.close(); + + let keyFile = Utils.getTmp("key"); + let [keyFOS] = Utils.open(keyFile, ">"); + keyFOS.write(pubkey, pubkey.length); + keyFOS.close(); + + let outputFile = Utils.getTmp("output"); + if (outputFile.exists()) + outputFile.remove(false); + + let rv = this._openssl("rsautl", "-encrypt", "-pubin", "-inkey", "key", + "-in", "input", "-out", "output"); + // FIXME: check rv + + let [outputFIS] = Utils.open(outputFile, "<"); + let output = Utils.readStream(outpusFIS); + outputFIS.close(); + outputFile.remove(false); + + return output; + }, + + // returns 'input' decrypted with the 'privkey' private RSA key and password + _opensslRSADecrypt: function Crypto__opensslRSADecrypt(input, privkey, password) { + let inputFile = Utils.getTmp("input"); + let [inputFOS] = Utils.open(inputFile, ">"); + inputFOS.write(input, input.length); + inputFOS.close(); + + let keyFile = Utils.getTmp("key"); + let [keyFOS] = Utils.open(keyFile, ">"); + keyFOS.write(privkey, privkey.length); + keyFOS.close(); + + let outputFile = Utils.getTmp("output"); + if (outputFile.exists()) + outputFile.remove(false); + + // nsIProcess doesn't support stdin, so we write a file instead + let passFile = Utils.getTmp("pass"); + let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); + passFOS.write(password, password.length); + passFOS.close(); + + try { + let rv = this._openssl("rsautl", "-decrypt", "-inkey", "key", "-pass", + "file:pass", "-in", "input", "-out", "output"); + // FIXME: check rv + } catch(e) { + throw e; + } finally { + passFile.remove(false); + } + + let [outputFIS] = Utils.open(outputFile, "<"); + let output = Utils.readStream(outpusFIS); + outputFIS.close(); + outputFile.remove(false); + + return output; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), // nsIObserver @@ -226,7 +376,7 @@ WeaveCrypto.prototype = { case "aes-256-cbc": case "bf-cbc": case "des-ede3-cbc": - ret = this._openssl("-e", algorithm, data, identity.password); + ret = this._opensslPBE("-e", algorithm, data, identity.password); break; default: @@ -282,7 +432,7 @@ WeaveCrypto.prototype = { case "aes-256-cbc": case "bf-cbc": case "des-ede3-cbc": - ret = this._openssl("-d", algorithm, data, identity.password); + ret = this._opensslPBE("-d", algorithm, data, identity.password); break; default: @@ -301,5 +451,21 @@ WeaveCrypto.prototype = { yield; // onComplete is responsible for closing the generator } this._log.warn("generator not properly closed"); + }, + + PBEkeygen: function Crypto_PBEkeygen() { + return this._opensslRand(); + }, + + RSAkeygen: function Crypto_RSAkeygen(password) { + return this._opensslRSAKeyGen(password); + }, + + RSAencrypt: function Crypto_RSAencrypt(data, key) { + return this._opensslRSAEncrypt(data, key); + }, + + RSAdecrypt: function Crypto_RSAdecrypt(data, key, password) { + return this._opensslRSADecrypt(data, key, password); } }; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index f731671b4a66..58ed81a7e7a3 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -178,6 +178,9 @@ let Utils = { var p = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); p.init(binary); + let log = Log4Moz.Service.getLogger("Service.Util"); + log.debug("Running command: " + binary.path + " " + args.join(" ")); + p.run(true, args, args.length); return p.exitValue; }, From d458dc549543070af1c34d1b9c17006a4b94c38d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 19 Feb 2008 20:53:01 -0800 Subject: [PATCH 0141/1860] use a scriptableinputstream to correctly read openssl output --- services/sync/modules/crypto.js | 41 +++++++++++++++------------------ services/sync/modules/util.js | 23 ++++++++++-------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index e4fabf1dbe51..c6d76bd2212f 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -120,7 +120,10 @@ WeaveCrypto.prototype = { let args = [wrap, Utils.getTmp().path, bin]; args = args.concat(arguments); - return Utils.runCmd.apply(null, args); + + let rv = Utils.runCmd.apply(null, args); + if (rv != 0) + throw "openssl did not run successfully, error code " + rv; }, _opensslPBE: function Crypto__openssl(op, algorithm, input, password) { @@ -129,10 +132,6 @@ WeaveCrypto.prototype = { inputFOS.write(input, input.length); inputFOS.close(); - let outputFile = Utils.getTmp("output"); - if (outputFile.exists()) - outputFile.remove(false); - // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); @@ -142,7 +141,6 @@ WeaveCrypto.prototype = { try { this._openssl(algorithm, op, "-a", "-salt", "-in", "input", "-out", "output", "-pass", "file:pass"); - // FIXME: check rv } catch (e) { throw e; @@ -152,6 +150,7 @@ WeaveCrypto.prototype = { inputFile.remove(false); } + let outputFile = Utils.getTmp("output"); let [outputFIS] = Utils.open(outputFile, "<"); let ret = Utils.readStream(outputFIS); outputFIS.close(); @@ -169,8 +168,7 @@ WeaveCrypto.prototype = { if (outputFile.exists()) outputFile.remove(false); - let rv = this._openssl("rand", "-base64", "-out", "output", length); - // FIXME: check rv + this._openssl("rand", "-base64", "-out", "output", length); let [outputFIS] = Utils.open(outputFile, "<"); let ret = Utils.readStream(outputFIS); @@ -191,16 +189,14 @@ WeaveCrypto.prototype = { if (privKeyF.exists()) privKeyF.remove(false); - let rv = this._openssl("genrsa", "-out", "privkey.pem", bits); - // FIXME: check rv + this._openssl("genrsa", "-out", "privkey.pem", bits); let pubKeyF = Utils.getTmp("pubkey.pem"); if (pubKeyF.exists()) pubKeyF.remove(false); - rv = this._openssl("rsa", "-in", "privkey.pem", "-out", "pubkey.pem", - "-outform", "PEM", "-pubout"); - // FIXME: check rv + this._openssl("rsa", "-in", "privkey.pem", "-out", "pubkey.pem", + "-outform", "PEM", "-pubout"); let cryptedKeyF = Utils.getTmp("enckey.pem"); if (cryptedKeyF.exists()) @@ -213,11 +209,12 @@ WeaveCrypto.prototype = { passFOS.close(); try { - rv = this._openssl("pkcs8", "-in", "privkey.pem", "-out", "enckey.pem", - "-topk8", "-v2", algorithm, "-pass", "file:pass"); - // FIXME: check rv + this._openssl("pkcs8", "-in", "privkey.pem", "-out", "enckey.pem", + "-topk8", "-v2", algorithm, "-pass", "file:pass"); + } catch (e) { throw e; + } finally { passFile.remove(false); privKeyF.remove(false); @@ -252,9 +249,8 @@ WeaveCrypto.prototype = { if (outputFile.exists()) outputFile.remove(false); - let rv = this._openssl("rsautl", "-encrypt", "-pubin", "-inkey", "key", - "-in", "input", "-out", "output"); - // FIXME: check rv + this._openssl("rsautl", "-encrypt", "-pubin", "-inkey", "key", + "-in", "input", "-out", "output"); let [outputFIS] = Utils.open(outputFile, "<"); let output = Utils.readStream(outpusFIS); @@ -287,11 +283,12 @@ WeaveCrypto.prototype = { passFOS.close(); try { - let rv = this._openssl("rsautl", "-decrypt", "-inkey", "key", "-pass", - "file:pass", "-in", "input", "-out", "output"); - // FIXME: check rv + this._openssl("rsautl", "-decrypt", "-inkey", "key", "-pass", + "file:pass", "-in", "input", "-out", "output"); + } catch(e) { throw e; + } finally { passFile.remove(false); } diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 58ed81a7e7a3..ef119b1a221e 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -221,10 +221,12 @@ let Utils = { case "<": { if (!file.exists()) throw "Cannot open file for reading, file does not exist"; - stream = Cc["@mozilla.org/network/file-input-stream;1"]. + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. createInstance(Ci.nsIFileInputStream); - stream.init(file, MODE_RDONLY, perms, 0); - stream.QueryInterface(Ci.nsILineInputStream); + fis.init(file, MODE_RDONLY, perms, 0); + stream = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + stream.init(fis); } break; case ">": { @@ -246,14 +248,15 @@ let Utils = { return [stream, file]; }, - readStream: function Weave_readStream(fis) { - let data = ""; - while (fis.available()) { - let ret = {}; - fis.readLine(ret); - data += ret.value; + // assumes an nsIScriptableInputStream + readStream: function Weave_readStream(is) { + let ret = ""; + let chunk = is.read(4096); + while (chunk.length > 0) { + ret += chunk; + chunk = is.read(4096); } - return data; + return ret; }, /* From d7c8c1a05ad1c782c4117160433ecd0586df738c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 19 Feb 2008 20:53:45 -0800 Subject: [PATCH 0142/1860] version bump --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index ce4ed9d38e8a..56511a6cbcb0 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.16"; +const WEAVE_VERSION = "0.1.18"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 3a708e570d440cdebb0b839ac701558341a285eb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 19 Feb 2008 21:01:11 -0800 Subject: [PATCH 0143/1860] fix openssl args mangling --- services/sync/modules/crypto.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index c6d76bd2212f..3bafe7ae643a 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -118,8 +118,8 @@ WeaveCrypto.prototype = { throw "encryption not supported on this platform: " + os; } - let args = [wrap, Utils.getTmp().path, bin]; - args = args.concat(arguments); + let args = Array.prototype.slice.call(arguments); + args.unshift(wrap, Utils.getTmp().path, bin); let rv = Utils.runCmd.apply(null, args); if (rv != 0) From 370be1ba821aeef9ea68f9295b25d4bafba30b18 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 5 Mar 2008 00:00:56 -0800 Subject: [PATCH 0144/1860] switch to nsIJSON for JSON parsing and output. add a deepCopy function instead of using eval(uneval()). make *sure* to read and write UTF-8 to files. bump version --- services/sync/modules/constants.js | 2 +- services/sync/modules/crypto.js | 16 ++-- services/sync/modules/dav.js | 2 +- services/sync/modules/engines.js | 143 +++++++++++++++-------------- services/sync/modules/stores.js | 48 ++++------ services/sync/modules/syncCores.js | 3 +- services/sync/modules/util.js | 97 +++++++++++++++---- 7 files changed, 181 insertions(+), 130 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 56511a6cbcb0..d14217c6f967 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.18"; +const WEAVE_VERSION = "0.1.19"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 3bafe7ae643a..5c12df48af79 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -129,13 +129,13 @@ WeaveCrypto.prototype = { _opensslPBE: function Crypto__openssl(op, algorithm, input, password) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); - inputFOS.write(input, input.length); + inputFOS.writeString(input); inputFOS.close(); // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.write(password, password.length); + passFOS.writeString(password); passFOS.close(); try { @@ -205,7 +205,7 @@ WeaveCrypto.prototype = { // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.write(password, password.length); + passFOS.writeString(password); passFOS.close(); try { @@ -237,12 +237,12 @@ WeaveCrypto.prototype = { _opensslRSAEncrypt: function Crypto__opensslRSAEncrypt(input, pubkey) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); - inputFOS.write(input, input.length); + inputFOS.writeString(input); inputFOS.close(); let keyFile = Utils.getTmp("key"); let [keyFOS] = Utils.open(keyFile, ">"); - keyFOS.write(pubkey, pubkey.length); + keyFOS.writeString(pubkey); keyFOS.close(); let outputFile = Utils.getTmp("output"); @@ -264,12 +264,12 @@ WeaveCrypto.prototype = { _opensslRSADecrypt: function Crypto__opensslRSADecrypt(input, privkey, password) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); - inputFOS.write(input, input.length); + inputFOS.writeString(input); inputFOS.close(); let keyFile = Utils.getTmp("key"); let [keyFOS] = Utils.open(keyFile, ">"); - keyFOS.write(privkey, privkey.length); + keyFOS.writeString(privkey); keyFOS.close(); let outputFile = Utils.getTmp("output"); @@ -279,7 +279,7 @@ WeaveCrypto.prototype = { // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.write(password, password.length); + passFOS.writeString(password); passFOS.close(); try { diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 59a37465825a..4d96ed5f19de 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -114,7 +114,7 @@ DAVCollection.prototype = { this._authProvider._authFailed = false; request.channel.notificationCallbacks = this._authProvider; - + request.send(data); let event = yield; ret = event.target; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 987caaad2a42..a695d0d45753 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -79,6 +79,14 @@ Engine.prototype = { return this.__os; }, + __json: null, + get _json() { + if (!this.__json) + this.__json = Cc["@mozilla.org/dom/json;1"]. + createInstance(Ci.nsIJSON); + return this.__json; + }, + // _core, and _store need to be overridden in subclasses __core: null, get _core() { @@ -104,7 +112,7 @@ Engine.prototype = { this.__snapshot = value; }, - _init: function BmkEngine__init(davCollection, cryptoId) { + _init: function Engine__init(davCollection, cryptoId) { this._dav = davCollection; this._cryptoId = cryptoId; this._log = Log4Moz.Service.getLogger("Service." + this.logName); @@ -112,13 +120,16 @@ Engine.prototype = { this._snapshot.load(); }, - _checkStatus: function BmkEngine__checkStatus(code, msg, ok404) { - if (code >= 200 && code < 300) - return; - if (ok404 && code == 404) - return; - this._log.error(msg + " Error code: " + code); - throw 'checkStatus failed'; + _serializeCommands: function Engine__serializeCommands(commands) { + let json = this._json.encode(commands); + //json = json.replace(/ {action/g, "\n {action"); + return json; + }, + + _serializeConflicts: function Engine__serializeConflicts(conflicts) { + let json = this._json.encode(conflicts); + //json = json.replace(/ {action/g, "\n {action"); + return json; }, _resetServer: function Engine__resetServer(onComplete) { @@ -149,12 +160,12 @@ Engine.prototype = { this._dav.unlock.async(this._dav, cont); let unlocked = yield; - this._checkStatus(statusResp.status, - "Could not delete status file.", true); - this._checkStatus(snapshotResp.status, - "Could not delete snapshot file.", true); - this._checkStatus(deltasResp.status, - "Could not delete deltas file.", true); + Utils.checkStatus(statusResp.status, + "Could not delete status file.", [[200,300],404]); + Utils.checkStatus(snapshotResp.status, + "Could not delete snapshot file.", [[200,300],404]); + Utils.checkStatus(deltasResp.status, + "Could not delete deltas file.", [[200,300],404]); this._log.debug("Server files deleted"); done = true; @@ -255,7 +266,7 @@ Engine.prototype = { // Before we get started, make sure we have a remote directory to play in this._dav.MKCOL(this.serverPrefix, cont); let ret = yield; - this._checkStatus(ret.status, "Could not create remote folder."); + Utils.checkStatus(ret.status, "Could not create remote folder."); // 1) Fetch server deltas this._getServerData.async(this, cont); @@ -279,9 +290,9 @@ Engine.prototype = { this._core.detectUpdates(cont, this._snapshot.data, localJson.data); let localUpdates = yield; - this._log.debug("local json:\n" + localJson.serialize()); - this._log.debug("Local updates: " + serializeCommands(localUpdates)); - this._log.debug("Server updates: " + serializeCommands(server.updates)); + this._log.trace("local json:\n" + localJson.serialize()); + this._log.trace("Local updates: " + this._serializeCommands(localUpdates)); + this._log.trace("Server updates: " + this._serializeCommands(server.updates)); if (server.updates.length == 0 && localUpdates.length == 0) { this._snapshot.version = server.maxVersion; @@ -305,10 +316,10 @@ Engine.prototype = { this._log.info("Predicted changes for server: " + serverChanges.length); this._log.info("Client conflicts: " + clientConflicts.length); this._log.info("Server conflicts: " + serverConflicts.length); - this._log.debug("Changes for client: " + serializeCommands(clientChanges)); - this._log.debug("Predicted changes for server: " + serializeCommands(serverChanges)); - this._log.debug("Client conflicts: " + serializeConflicts(clientConflicts)); - this._log.debug("Server conflicts: " + serializeConflicts(serverConflicts)); + this._log.trace("Changes for client: " + this._serializeCommands(clientChanges)); + this._log.trace("Predicted changes for server: " + this._serializeCommands(serverChanges)); + this._log.trace("Client conflicts: " + this._serializeConflicts(clientConflicts)); + this._log.trace("Server conflicts: " + this._serializeConflicts(serverConflicts)); if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { @@ -324,7 +335,7 @@ Engine.prototype = { this._log.warn("Conflicts found! Discarding server changes"); } - let savedSnap = eval(uneval(this._snapshot.data)); + let savedSnap = Utils.deepCopy(this._snapshot.data); let savedVersion = this._snapshot.version; let newSnapshot; @@ -346,9 +357,9 @@ Engine.prototype = { this._log.warn("Commands did not apply correctly"); this._log.debug("Diff from snapshot+commands -> " + "new snapshot after commands:\n" + - serializeCommands(diff)); + this._serializeCommands(diff)); // FIXME: do we really want to revert the snapshot here? - this._snapshot.data = eval(uneval(savedSnap)); + this._snapshot.data = Utils.deepCopy(savedSnap); this._snapshot.version = savedVersion; } this._snapshot.save(); @@ -372,7 +383,7 @@ Engine.prototype = { this._log.info("Actual changes for server: " + serverDelta.length); this._log.debug("Actual changes for server: " + - serializeCommands(serverDelta)); + this._serializeCommands(serverDelta)); if (serverDelta.length) { this._log.info("Uploading changes to server"); @@ -391,7 +402,7 @@ Engine.prototype = { } else { Crypto.PBEencrypt.async(Crypto, cont, - serializeCommands(server.deltas), + this._serializeCommands(server.deltas), this._cryptoId); let data = yield; this._dav.PUT(this.deltasFile, data, cont); @@ -402,13 +413,14 @@ Engine.prototype = { c++; this._dav.PUT(this.statusFile, - uneval({GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshot.version, - snapEncryption: server.snapEncryption, - deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c}), cont); + this._json.encode( + {GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshot.version, + snapEncryption: server.snapEncryption, + deltasEncryption: Crypto.defaultAlgorithm, + itemCount: c}), cont); let statusPut = yield; if (deltasPut.status >= 200 && deltasPut.status < 300 && @@ -488,7 +500,7 @@ Engine.prototype = { case 200: { this._log.info("Got status file from server"); - let status = eval(resp.responseText); + let status = this._json.decode(resp.responseText); let deltas, allDeltas; let snap = new SnapshotStore(); @@ -523,56 +535,56 @@ Engine.prototype = { this._log.info("Downloading server snapshot"); this._dav.GET(this.snapshotFile, cont); resp = yield; - this._checkStatus(resp.status, "Could not download snapshot."); + Utils.checkStatus(resp.status, "Could not download snapshot."); Crypto.PBEdecrypt.async(Crypto, cont, resp.responseText, this._cryptoId, status.snapEncryption); let data = yield; - snap.data = eval(data); + snap.data = this._json.decode(data); this._log.info("Downloading server deltas"); this._dav.GET(this.deltasFile, cont); resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); + Utils.checkStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, cont, resp.responseText, this._cryptoId, status.deltasEncryption); data = yield; - allDeltas = eval(data); - deltas = eval(data); + allDeltas = this._json.decode(data); + deltas = this._json.decode(data); } else if (this._snapshot.version >= status.snapVersion && this._snapshot.version < status.maxVersion) { - snap.data = eval(uneval(this._snapshot.data)); + snap.data = Utils.deepCopy(this._snapshot.data); this._log.info("Downloading server deltas"); this._dav.GET(this.deltasFile, cont); resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); + Utils.checkStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, cont, resp.responseText, this._cryptoId, status.deltasEncryption); let data = yield; - allDeltas = eval(data); + allDeltas = this._json.decode(data); deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); } else if (this._snapshot.version == status.maxVersion) { - snap.data = eval(uneval(this._snapshot.data)); - + snap.data = Utils.deepCopy(this._snapshot.data); + // FIXME: could optimize this case by caching deltas file this._log.info("Downloading server deltas"); this._dav.GET(this.deltasFile, cont); resp = yield; - this._checkStatus(resp.status, "Could not download deltas."); + Utils.checkStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, cont, resp.responseText, this._cryptoId, status.deltasEncryption); let data = yield; - allDeltas = eval(data); + allDeltas = this._json.decode(data); deltas = []; } else { // this._snapshot.version > status.maxVersion @@ -617,7 +629,7 @@ Engine.prototype = { ret.snapVersion = this._snapshot.version; ret.snapEncryption = Crypto.defaultAlgorithm; ret.deltasEncryption = Crypto.defaultAlgorithm; - ret.snapshot = eval(uneval(this._snapshot.data)); + ret.snapshot = Utils.deepCopy(this._snapshot.data); ret.deltas = []; ret.updates = []; } break; @@ -651,26 +663,27 @@ Engine.prototype = { let data = yield; this._dav.PUT(this.snapshotFile, data, cont); resp = yield; - this._checkStatus(resp.status, "Could not upload snapshot."); + Utils.checkStatus(resp.status, "Could not upload snapshot."); - this._dav.PUT(this.deltasFile, uneval([]), cont); + this._dav.PUT(this.deltasFile, "[]", cont); resp = yield; - this._checkStatus(resp.status, "Could not upload deltas."); + Utils.checkStatus(resp.status, "Could not upload deltas."); let c = 0; for (GUID in this._snapshot.data) c++; this._dav.PUT(this.statusFile, - uneval({GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshot.version, - maxVersion: this._snapshot.version, - snapEncryption: Crypto.defaultAlgorithm, - deltasEncryption: "none", - itemCount: c}), cont); + this._json.encode( + {GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshot.version, + maxVersion: this._snapshot.version, + snapEncryption: Crypto.defaultAlgorithm, + deltasEncryption: "none", + itemCount: c}), cont); resp = yield; - this._checkStatus(resp.status, "Could not upload status file."); + Utils.checkStatus(resp.status, "Could not upload status file."); this._log.info("Full upload to server successful"); ret = true; @@ -746,15 +759,3 @@ HistoryEngine.prototype = { } }; HistoryEngine.prototype.__proto__ = new Engine(); - -serializeCommands: function serializeCommands(commands) { - let json = uneval(commands); - json = json.replace(/ {action/g, "\n {action"); - return json; -} - -serializeConflicts: function serializeConflicts(conflicts) { - let json = uneval(conflicts); - json = json.replace(/ {action/g, "\n {action"); - return json; -} diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 692bfd5a11e0..9d9e2505ff43 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -58,6 +58,14 @@ function Store() { Store.prototype = { _logName: "Store", + __json: null, + get _json() { + if (!this.__json) + this.__json = Cc["@mozilla.org/dom/json;1"]. + createInstance(Ci.nsIJSON); + return this.__json; + }, + _init: function Store__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); }, @@ -65,7 +73,7 @@ Store.prototype = { applyCommands: function Store_applyCommands(commandList) { for (var i = 0; i < commandList.length; i++) { var command = commandList[i]; - this._log.debug("Processing command: " + uneval(command)); + this._log.debug("Processing command: " + this._json.encode(command)); switch (command["action"]) { case "create": this._createCommand(command); @@ -143,7 +151,7 @@ SnapshotStore.prototype = { }, _createCommand: function SStore__createCommand(command) { - this._data[command.GUID] = eval(uneval(command.data)); + this._data[command.GUID] = Utils.deepCopy(command.data); }, _removeCommand: function SStore__removeCommand(command) { @@ -178,27 +186,18 @@ SnapshotStore.prototype = { file.QueryInterface(Ci.nsILocalFile); file.append("weave"); - if (!file.exists()) - file.create(file.DIRECTORY_TYPE, PERMS_DIRECTORY); - file.append("snapshots"); - if (!file.exists()) - file.create(file.DIRECTORY_TYPE, PERMS_DIRECTORY); - file.append(this.filename); if (!file.exists()) file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); - let fos = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE; - fos.init(file, flags, PERMS_FILE, 0); - let out = {version: this.version, GUID: this.GUID, snapshot: this.data}; - out = uneval(out); - fos.write(out, out.length); + out = this._json.encode(out); + + let [fos] = Utils.open(file, ">"); + fos.writeString(out); fos.close(); }, @@ -211,19 +210,10 @@ SnapshotStore.prototype = { if (!file.exists()) return; - let fis = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - fis.init(file, MODE_RDONLY, PERMS_FILE, 0); - fis.QueryInterface(Ci.nsILineInputStream); - - let json = ""; - while (fis.available()) { - let ret = {}; - fis.readLine(ret); - json += ret.value; - } - fis.close(); - json = eval(json); + let [is] = Utils.open(file, "<"); + let json = Utils.readStream(is); + is.close(); + json = this._json.decode(json); if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { this._log.info("Read saved snapshot from disk"); @@ -234,7 +224,7 @@ SnapshotStore.prototype = { }, serialize: function SStore_serialize() { - let json = uneval(this.data); + let json = this._json.encode(this.data); json = json.replace(/:{type/g, ":\n\t{type"); json = json.replace(/}, /g, "},\n "); json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 821ee9774d40..87a0415b080e 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -388,7 +388,8 @@ BookmarksSyncCore.prototype = { return true; return false; default: - this._log.error("commandLike: Unknown item type: " + uneval(a)); + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this._log.error("commandLike: Unknown item type: " + json.encode(a)); return false; } } diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index ef119b1a221e..fe8d6682f8ca 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -57,25 +57,79 @@ let Utils = { if (!a || !b) return false; + // if neither is an object, just use == if (typeof(a) != "object" && typeof(b) != "object") return a == b; + + // check if only one of them is an object if (typeof(a) != "object" || typeof(b) != "object") return false; - for (let key in a) { - if (typeof(a[key]) == "object") { - if (!typeof(b[key]) == "object") + if (a instanceof Array) + dump("a is an array\n"); + if (b instanceof Array) + dump("b is an array\n"); + + if (a instanceof Array && b instanceof Array) { + if (a.length != b.length) + return false; + + for (let i = 0; i < a.length; i++) { + if (!Utils.deepEquals(a[i], b[i])) return false; + } + + } else { + // check if only one of them is an array + if (a instanceof Array || b instanceof Array) + return false; + + for (let key in a) { if (!Utils.deepEquals(a[key], b[key])) return false; - } else { - if (a[key] != b[key]) - return false; } } + return true; }, + deepCopy: function Weave_deepCopy(thing) { + if (typeof(thing) != "object" || thing == null) + return thing; + let ret; + + if (thing instanceof Array) { + dump("making a cipy of an array!\n\n"); + ret = []; + for (let i = 0; i < thing.length; i++) + ret.push(Utils.deepCopy(thing[i])); + + } else { + ret = {}; + for (let key in thing) + ret[key] = Utils.deepCopy(thing[key]); + } + + return ret; + }, + + checkStatus: function Weave_checkStatus(code, msg, ranges) { + if (!ranges) + ranges = [[200,300]]; + + for (let i = 0; i < ranges.length; i++) { + rng = ranges[i]; + if (typeof(rng) == "object" && code >= rng[0] && code < rng[1]) + return; + else if (typeof(rng) == "integer" && code == rng) + return; + } + + let log = Log4Moz.Service.getLogger("Service.Util"); + log.error(msg + " Error code: " + code); + throw 'checkStatus failed'; + }, + makeURI: function Weave_makeURI(URIString) { if (URIString === null || URIString == "") return null; @@ -224,21 +278,28 @@ let Utils = { let fis = Cc["@mozilla.org/network/file-input-stream;1"]. createInstance(Ci.nsIFileInputStream); fis.init(file, MODE_RDONLY, perms, 0); - stream = Cc["@mozilla.org/scriptableinputstream;1"]. - createInstance(Ci.nsIScriptableInputStream); - stream.init(fis); + stream = Cc["@mozilla.org/intl/converter-input-stream;1"]. + createInstance(Ci.nsIConverterInputStream); + stream.init(fis, "UTF-8", 4096, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); } break; case ">": { - stream = Cc["@mozilla.org/network/file-output-stream;1"]. + let fos = Cc["@mozilla.org/network/file-output-stream;1"]. createInstance(Ci.nsIFileOutputStream); - stream.init(file, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, perms, 0); + fos.init(file, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, perms, 0); + stream = Cc["@mozilla.org/intl/converter-output-stream;1"] + .createInstance(Ci.nsIConverterOutputStream); + stream.init(fos, "UTF-8", 4096, 0x0000); } break; case ">>": { - stream = Cc["@mozilla.org/network/file-output-stream;1"]. + let fos = Cc["@mozilla.org/network/file-output-stream;1"]. createInstance(Ci.nsIFileOutputStream); - stream.init(file, MODE_WRONLY | MODE_CREATE | MODE_APPEND, perms, 0); + fos.init(file, MODE_WRONLY | MODE_CREATE | MODE_APPEND, perms, 0); + stream = Cc["@mozilla.org/intl/converter-output-stream;1"] + .createInstance(Ci.nsIConverterOutputStream); + stream.init(fos, "UTF-8", 4096, 0x0000); } break; default: @@ -248,13 +309,11 @@ let Utils = { return [stream, file]; }, - // assumes an nsIScriptableInputStream + // assumes an nsIConverterInputStream readStream: function Weave_readStream(is) { - let ret = ""; - let chunk = is.read(4096); - while (chunk.length > 0) { - ret += chunk; - chunk = is.read(4096); + let ret = "", str = {}; + while (is.readString(4096, str) != 0) { + ret += str.value; } return ret; }, From 208a405857b4510d95a28598caf521319f549680 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 5 Mar 2008 17:11:08 -0800 Subject: [PATCH 0145/1860] ignore broken local snapshots if they can't be parsed; trigger an initial sync instead --- services/sync/modules/stores.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 9d9e2505ff43..ca77ce30a7a7 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -210,16 +210,21 @@ SnapshotStore.prototype = { if (!file.exists()) return; - let [is] = Utils.open(file, "<"); - let json = Utils.readStream(is); - is.close(); - json = this._json.decode(json); + try { + let [is] = Utils.open(file, "<"); + let json = Utils.readStream(is); + is.close(); + json = this._json.decode(json); - if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { - this._log.info("Read saved snapshot from disk"); - this.data = json.snapshot; - this.version = json.version; - this.GUID = json.GUID; + if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { + this._log.info("Read saved snapshot from disk"); + this.data = json.snapshot; + this.version = json.version; + this.GUID = json.GUID; + } + } catch (e) { + this._log.warn("Could not parse saved snapshot; reverting to initial-sync state"); + this._log.debug("Exception: " + e); } }, From 839cd9ca3b76835ba4817f0c6efef3283586c8a6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 5 Mar 2008 18:36:58 -0800 Subject: [PATCH 0146/1860] don't sync bookmarks/history unless they have been enabled --- services/sync/modules/service.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7d3279512c4f..2850ec0b8ddf 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -266,12 +266,8 @@ WeaveSyncService.prototype = { let brief = this._dirSvc.get("ProfD", Ci.nsIFile); brief.QueryInterface(Ci.nsILocalFile); - brief.append("weave"); brief.append("logs"); - if (!brief.exists()) - brief.create(brief.DIRECTORY_TYPE, PERMS_DIRECTORY); - brief.append("brief-log.txt"); if (!brief.exists()) brief.create(brief.NORMAL_FILE_TYPE, PERMS_FILE); @@ -405,10 +401,14 @@ WeaveSyncService.prototype = { this._os.notifyObservers(null, "weave:service:sync:start", ""); - this._bmkEngine.sync(cont); - yield; - this._histEngine.sync(cont); - yield; + if (this._prefs.getBoolPref("bookmarks")) { + this._bmkEngine.sync(cont); + yield; + } + if (this._prefs.getBoolPref("history")) { + this._histEngine.sync(cont); + yield; + } success = true; this._unlock(); From ad7c98fe2e5118c27da67f4572b9a2aa4a4081df Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 7 Mar 2008 01:56:36 -0800 Subject: [PATCH 0147/1860] Asynchronous generator helpers rework + PKI work * Async helpers are in a module of their own now * Async routines have simpler semantics now. onComplete handlers are taken care of by the helpers. Exceptions are bubbled up across nested asynchronous generators * Stack traces are automatically logged for unhandled exceptions * Async generators are now allowed to 'bottom out' (StopIteration is ignored) - this is configurable. * RSA key generation fixes * On login we now create an RSA keypair, encrypt the private one with PBE, and upload them to the server * Log files are now limited to 2MB (down from 5) --- services/sync/modules/crypto.js | 175 +++++----- services/sync/modules/dav.js | 460 ++++++++++++-------------- services/sync/modules/engines.js | 501 ++++++++++++++--------------- services/sync/modules/identity.js | 4 + services/sync/modules/log4moz.js | 2 +- services/sync/modules/service.js | 171 +++++----- services/sync/modules/syncCores.js | 29 +- services/sync/modules/util.js | 101 ++---- 8 files changed, 681 insertions(+), 762 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 5c12df48af79..185e9079cba9 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -45,8 +45,9 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); -Function.prototype.async = Utils.generatorAsync; +Function.prototype.async = Async.sugar; function WeaveCrypto() { this._init(); @@ -210,7 +211,7 @@ WeaveCrypto.prototype = { try { this._openssl("pkcs8", "-in", "privkey.pem", "-out", "enckey.pem", - "-topk8", "-v2", algorithm, "-pass", "file:pass"); + "-topk8", "-v2", algorithm, "-passout", "file:pass"); } catch (e) { throw e; @@ -223,7 +224,7 @@ WeaveCrypto.prototype = { let [cryptedKeyFIS] = Utils.open(cryptedKeyF, "<"); let cryptedKey = Utils.readStream(cryptedKeyFIS); cryptedKeyFIS.close(); - cryptedKey.remove(false); + cryptedKeyF.remove(false); let [pubKeyFIS] = Utils.open(pubKeyF, "<"); let pubKey = Utils.readStream(pubKeyFIS); @@ -234,7 +235,7 @@ WeaveCrypto.prototype = { }, // returns 'input' encrypted with the 'pubkey' public RSA key - _opensslRSAEncrypt: function Crypto__opensslRSAEncrypt(input, pubkey) { + _opensslRSAencrypt: function Crypto__opensslRSAencrypt(input, pubkey) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.writeString(input); @@ -261,7 +262,7 @@ WeaveCrypto.prototype = { }, // returns 'input' decrypted with the 'privkey' private RSA key and password - _opensslRSADecrypt: function Crypto__opensslRSADecrypt(input, privkey, password) { + _opensslRSAdecrypt: function Crypto__opensslRSAdecrypt(input, privkey, password) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.writeString(input); @@ -338,131 +339,127 @@ WeaveCrypto.prototype = { // Crypto - PBEencrypt: function Crypto_PBEencrypt(onComplete, data, identity, algorithm) { - let [self, cont] = yield; - let listener = new Utils.EventListener(cont); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) { + let self = yield; let ret; - try { - if (!algorithm) - algorithm = this.defaultAlgorithm; + if (!algorithm) + algorithm = this.defaultAlgorithm; - if (algorithm != "none") - this._log.debug("Encrypting data"); + if (algorithm != "none") + this._log.debug("Encrypting data"); - switch (algorithm) { - case "none": - ret = data; - break; + switch (algorithm) { + case "none": + ret = data; + break; - case "XXXTEA": // Weave 0.1.12.10 and below had this typo - case "XXTEA": { - let gen = this._xxtea.encrypt(data, identity.password); - ret = gen.next(); + case "XXXTEA": // Weave 0.1.12.10 and below had this typo + case "XXTEA": { + let gen = this._xxtea.encrypt(data, identity.password); + ret = gen.next(); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + try { while (typeof(ret) == "object") { - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + timer.initWithCallback(self.listener, 0, timer.TYPE_ONE_SHOT); yield; // Yield to main loop ret = gen.next(); } gen.close(); - } break; - - case "aes-128-cbc": - case "aes-192-cbc": - case "aes-256-cbc": - case "bf-cbc": - case "des-ede3-cbc": - ret = this._opensslPBE("-e", algorithm, data, identity.password); - break; - - default: - throw "Unknown encryption algorithm: " + algorithm; + } finally { + timer = null; } + } break; - if (algorithm != "none") - this._log.debug("Done encrypting data"); + case "aes-128-cbc": + case "aes-192-cbc": + case "aes-256-cbc": + case "bf-cbc": + case "des-ede3-cbc": + ret = this._opensslPBE("-e", algorithm, data, identity.password); + break; - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - timer = null; - Utils.generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator + default: + throw "Unknown encryption algorithm: " + algorithm; } - this._log.warn("generator not properly closed"); + + if (algorithm != "none") + this._log.debug("Done encrypting data"); + + self.done(ret); }, - PBEdecrypt: function Crypto_PBEdecrypt(onComplete, data, identity, algorithm) { - let [self, cont] = yield; - let listener = new Utils.EventListener(cont); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) { + let self = yield; let ret; - try { - if (!algorithm) - algorithm = this.defaultAlgorithm; + if (!algorithm) + algorithm = this.defaultAlgorithm; - if (algorithm != "none") - this._log.debug("Decrypting data"); + if (algorithm != "none") + this._log.debug("Decrypting data"); - switch (algorithm) { - case "none": - ret = data; - break; + switch (algorithm) { + case "none": + ret = data; + break; - case "XXXTEA": // Weave 0.1.12.10 and below had this typo - case "XXTEA": { - let gen = this._xxtea.decrypt(data, identity.password); - ret = gen.next(); + case "XXXTEA": // Weave 0.1.12.10 and below had this typo + case "XXTEA": { + let gen = this._xxtea.decrypt(data, identity.password); + ret = gen.next(); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + try { while (typeof(ret) == "object") { - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + timer.initWithCallback(self.listener, 0, timer.TYPE_ONE_SHOT); yield; // Yield to main loop ret = gen.next(); } gen.close(); - } break; - - case "aes-128-cbc": - case "aes-192-cbc": - case "aes-256-cbc": - case "bf-cbc": - case "des-ede3-cbc": - ret = this._opensslPBE("-d", algorithm, data, identity.password); - break; - - default: - throw "Unknown encryption algorithm: " + algorithm; + } finally { + timer = null; } + } break; - if (algorithm != "none") - this._log.debug("Done decrypting data"); + case "aes-128-cbc": + case "aes-192-cbc": + case "aes-256-cbc": + case "bf-cbc": + case "des-ede3-cbc": + ret = this._opensslPBE("-d", algorithm, data, identity.password); + break; - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - timer = null; - Utils.generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator + default: + throw "Unknown encryption algorithm: " + algorithm; } - this._log.warn("generator not properly closed"); + + if (algorithm != "none") + this._log.debug("Done decrypting data"); + + self.done(ret); }, PBEkeygen: function Crypto_PBEkeygen() { - return this._opensslRand(); + let self = yield; + let ret = this._opensslRand(); + self.done(ret); }, RSAkeygen: function Crypto_RSAkeygen(password) { - return this._opensslRSAKeyGen(password); + let self = yield; + let ret = this._opensslRSAKeyGen(password); + self.done(ret); }, RSAencrypt: function Crypto_RSAencrypt(data, key) { - return this._opensslRSAEncrypt(data, key); + let self = yield; + let ret = this._opensslRSAencrypt(data, key); + self.done(ret); }, RSAdecrypt: function Crypto_RSAdecrypt(data, key, password) { - return this._opensslRSADecrypt(data, key, password); + let self = yield; + let ret = this._opensslRSAdecrypt(data, key, password); + self.done(ret); } }; diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 4d96ed5f19de..89beb84bbe53 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -44,8 +44,9 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); -Function.prototype.async = Utils.generatorAsync; +Function.prototype.async = Async.sugar; /* * DAV object @@ -80,58 +81,50 @@ DAVCollection.prototype = { return this._loggedIn; }, - _makeRequest: function DC__makeRequest(onComplete, op, path, headers, data) { - let [self, cont] = yield; + _makeRequest: function DC__makeRequest(op, path, headers, data) { + let self = yield; let ret; - try { - this._log.debug("Creating " + op + " request for " + this._baseURL + path); + this._log.debug("Creating " + op + " request for " + this._baseURL + path); + + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); + request = request.QueryInterface(Ci.nsIDOMEventTarget); - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); - request = request.QueryInterface(Ci.nsIDOMEventTarget); - - request.addEventListener("load", new Utils.EventListener(cont, "load"), false); - request.addEventListener("error", new Utils.EventListener(cont, "error"), false); - request = request.QueryInterface(Ci.nsIXMLHttpRequest); - request.open(op, this._baseURL + path, true); + request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); + request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); + request = request.QueryInterface(Ci.nsIXMLHttpRequest); + request.open(op, this._baseURL + path, true); - // Force cache validation - let channel = request.channel; - channel = channel.QueryInterface(Ci.nsIRequest); - let loadFlags = channel.loadFlags; - loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; - channel.loadFlags = loadFlags; + // Force cache validation + let channel = request.channel; + channel = channel.QueryInterface(Ci.nsIRequest); + let loadFlags = channel.loadFlags; + loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; + channel.loadFlags = loadFlags; - let key; - for (key in headers) { - if (key == 'Authorization') - this._log.debug("HTTP Header " + key + ": ***** (suppressed)"); - else - this._log.debug("HTTP Header " + key + ": " + headers[key]); - request.setRequestHeader(key, headers[key]); - } - - this._authProvider._authFailed = false; - request.channel.notificationCallbacks = this._authProvider; - - request.send(data); - let event = yield; - ret = event.target; - - if (this._authProvider._authFailed) - this._log.warn("_makeRequest: authentication failed"); - if (ret.status < 200 || ret.status >= 300) - this._log.warn("_makeRequest: got status " + ret.status); - - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - Utils.generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator + let key; + for (key in headers) { + if (key == 'Authorization') + this._log.debug("HTTP Header " + key + ": ***** (suppressed)"); + else + this._log.debug("HTTP Header " + key + ": " + headers[key]); + request.setRequestHeader(key, headers[key]); } - this._log.warn("generator not properly closed"); + + this._authProvider._authFailed = false; + request.channel.notificationCallbacks = this._authProvider; + + request.send(data); + let event = yield; + ret = event.target; + + if (this._authProvider._authFailed) + this._log.warn("_makeRequest: authentication failed"); + if (ret.status < 200 || ret.status >= 300) + this._log.warn("_makeRequest: got status " + ret.status); + + self.done(ret); }, get _defaultHeaders() { @@ -142,52 +135,52 @@ DAVCollection.prototype = { }, // mkdir -p - _mkcol: function DC__mkcol(path, onComplete) { - let [self, cont] = yield; - let ret; + _mkcol: function DC__mkcol(path) { + let self = yield; + let ok = true; try { let components = path.split('/'); let path2 = ''; - + for (let i = 0; i < components.length; i++) { - - // trailing slashes will cause an empty path component at the end - if (components[i] == '') - break; - - path2 = path2 + components[i]; - - // check if it exists first - this._makeRequest.async(this, cont, "GET", path2 + "/", this._defaultHeaders); - ret = yield; - if (!(ret.status == 404 || ret.status == 500)) { // FIXME: 500 is a services.m.c oddity - this._log.debug("Skipping creation of path " + path2 + - " (got status " + ret.status + ")"); - } else { - this._log.debug("Creating path: " + path2); - let gen = this._makeRequest.async(this, cont, "MKCOL", path2, - this._defaultHeaders); - ret = yield; - - if (ret.status != 201) { - this._log.debug(ret.responseText); - throw 'request failed: ' + ret.status; - } - } - - // add slash *after* the request, trailing slashes cause a 412! - path2 = path2 + "/"; + + // trailing slashes will cause an empty path component at the end + if (components[i] == '') + break; + + path2 = path2 + components[i]; + + // check if it exists first + this._makeRequest.async(this, self.cb, "GET", path2 + "/", this._defaultHeaders); + let ret = yield; + if (!(ret.status == 404 || ret.status == 500)) { // FIXME: 500 is a services.m.c oddity + this._log.debug("Skipping creation of path " + path2 + + " (got status " + ret.status + ")"); + } else { + this._log.debug("Creating path: " + path2); + this._makeRequest.async(this, self.cb, "MKCOL", path2, + this._defaultHeaders); + ret = yield; + + if (ret.status != 201) { + this._log.debug(ret.responseText); + throw 'request failed: ' + ret.status; + } + } + + // add slash *after* the request, trailing slashes cause a 412! + path2 = path2 + "/"; } } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - Utils.generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator + this._log.error("Could not create directory on server"); + this._log.error("Exception caught: " + (e.message? e.message : e) + + " - " + (e.location? e.location : "")); + ok = false; } - this._log.warn("generator not properly closed"); + + self.done(ok); }, GET: function DC_GET(path, onComplete) { @@ -206,7 +199,7 @@ DAVCollection.prototype = { }, MKCOL: function DC_MKCOL(path, onComplete) { - return this._mkcol.async(this, path, onComplete); + return this._mkcol.async(this, onComplete, path); }, PROPFIND: function DC_PROPFIND(path, data, onComplete) { @@ -233,37 +226,33 @@ DAVCollection.prototype = { // Login / Logout - login: function DC_login(onComplete, username, password) { - let [self, cont] = yield; + login: function DC_login(username, password) { + let self = yield; - try { - if (this._loggedIn) { - this._log.debug("Login requested, but already logged in"); - return; - } - - this._log.info("Logging in"); - - let URI = Utils.makeURI(this._baseURL); - this._auth = "Basic " + btoa(username + ":" + password); - - // Make a call to make sure it's working - this.GET("", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - this._loggedIn = true; - - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - Utils.generatorDone(this, self, onComplete, this._loggedIn); - yield; // onComplete is responsible for closing the generator + if (this._loggedIn) { + this._log.debug("Login requested, but already logged in"); + self.done(true); + yield; } - this._log.warn("generator not properly closed"); + + this._log.info("Logging in"); + + let URI = Utils.makeURI(this._baseURL); + this._auth = "Basic " + btoa(username + ":" + password); + + // Make a call to make sure it's working + this.GET("", self.cb); + let resp = yield; + + if (this._authProvider._authFailed || + resp.status < 200 || resp.status >= 300) { + self.done(false); + yield; + } + + this._loggedIn = true; + + self.done(true); }, logout: function DC_logout() { @@ -274,170 +263,143 @@ DAVCollection.prototype = { // Locking - _getActiveLock: function DC__getActiveLock(onComplete) { - let [self, cont] = yield; + _getActiveLock: function DC__getActiveLock() { + let self = yield; let ret = null; - try { - this._log.info("Getting active lock token"); - this.PROPFIND("", - "" + - "" + - " " + - "", cont); - let resp = yield; + this._log.info("Getting active lock token"); + this.PROPFIND("", + "" + + "" + + " " + + "", self.cb); + let resp = yield; - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - ret = token.textContent; - - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - if (ret) - this._log.debug("Found an active lock token"); - else - this._log.debug("No active lock token found"); - Utils.generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator + if (this._authProvider._authFailed || + resp.status < 200 || resp.status >= 300) { + self.done(false); + yield; } - this._log.warn("generator not properly closed"); + + let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + ret = token.textContent; + + if (ret) + this._log.debug("Found an active lock token"); + else + this._log.debug("No active lock token found"); + self.done(ret); }, - lock: function DC_lock(onComplete) { - let [self, cont] = yield; + lock: function DC_lock() { + let self = yield; this._token = null; - try { - this._log.info("Acquiring lock"); + this._log.info("Acquiring lock"); - if (this._token) { - this._log.debug("Lock called, but we already hold a token"); - return; - } - - this.LOCK("", - "\n" + - "\n" + - " \n" + - " \n" + - "", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - if (token) - this._token = token.textContent; - - } catch (e){ - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - if (this._token) - this._log.info("Lock acquired"); - else - this._log.warn("Could not acquire lock"); - Utils.generatorDone(this, self, onComplete, this._token); - yield; // onComplete is responsible for closing the generator + if (this._token) { + this._log.debug("Lock called, but we already hold a token"); + self.done(this._token); + yield; } - this._log.warn("generator not properly closed"); + + this.LOCK("", + "\n" + + "\n" + + " \n" + + " \n" + + "", self.cb); + let resp = yield; + + if (this._authProvider._authFailed || + resp.status < 200 || resp.status >= 300) { + self.done(this._token); + yield; + } + + let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + if (token) + this._token = token.textContent; + + if (this._token) + this._log.debug("Lock acquired"); + else + this._log.warn("Could not acquire lock"); + + self.done(this._token); }, - unlock: function DC_unlock(onComplete) { - let [self, cont] = yield; - try { - this._log.info("Releasing lock"); + unlock: function DC_unlock() { + let self = yield; - if (this._token === null) { - this._log.debug("Unlock called, but we don't hold a token right now"); - return; - } + this._log.info("Releasing lock"); - this.UNLOCK("", cont); - let resp = yield; - - if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) - return; - - this._token = null; - - } catch (e){ - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - if (this._token) { - this._log.info("Could not release lock"); - Utils.generatorDone(this, self, onComplete, false); - } else { - this._log.info("Lock released (or we didn't have one)"); - Utils.generatorDone(this, self, onComplete, true); - } - yield; // onComplete is responsible for closing the generator + if (this._token === null) { + this._log.debug("Unlock called, but we don't hold a token right now"); + self.done(true); + yield; } - this._log.warn("generator not properly closed"); + + this.UNLOCK("", self.cb); + let resp = yield; + + if (this._authProvider._authFailed || + resp.status < 200 || resp.status >= 300) { + self.done(false); + yield; + } + + this._token = null; + + if (this._token) + this._log.info("Could not release lock"); + else + this._log.info("Lock released (or we didn't have one)"); + + self.done(!this._token); }, - forceUnlock: function DC_forceUnlock(onComplete) { - let [self, cont] = yield; + forceUnlock: function DC_forceUnlock() { + let self = yield; let unlocked = true; - try { - this._log.info("Forcibly releasing any server locks"); + this._log.info("Forcibly releasing any server locks"); - this._getActiveLock.async(this, cont); - this._token = yield; + this._getActiveLock.async(this, self.cb); + this._token = yield; - if (!this._token) { - this._log.info("No server lock found"); - return; - } - - this._log.info("Server lock found, unlocking"); - this.unlock.async(this, cont); - unlocked = yield; - - } catch (e){ - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - if (unlocked) - this._log.debug("Lock released"); - else - this._log.debug("No lock released"); - Utils.generatorDone(this, self, onComplete, unlocked); - yield; // onComplete is responsible for closing the generator + if (!this._token) { + this._log.info("No server lock found"); + self.done(true); + yield; } - this._log.warn("generator not properly closed"); + + this._log.info("Server lock found, unlocking"); + this.unlock.async(this, self.cb); + unlocked = yield; + + if (unlocked) + this._log.debug("Lock released"); + else + this._log.debug("No lock released"); + self.done(unlocked); }, - stealLock: function DC_stealLock(onComplete) { - let [self, cont] = yield; + stealLock: function DC_stealLock() { + let self = yield; let stolen = null; - try { - this.forceUnlock.async(this, cont); - let unlocked = yield; + this.forceUnlock.async(this, self.cb); + let unlocked = yield; - if (unlocked) { - this.lock.async(this, cont); - stolen = yield; - } - - } catch (e){ - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - Utils.generatorDone(this, self, onComplete, stolen); - yield; // onComplete is responsible for closing the generator + if (unlocked) { + this.lock.async(this, self.cb); + stolen = yield; } - this._log.warn("generator not properly closed"); + + self.done(stolen); } }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a695d0d45753..983d2c40b049 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -48,8 +48,9 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/async.js"); -Function.prototype.async = Utils.generatorAsync; +Function.prototype.async = Async.sugar; let Crypto = new WeaveCrypto(); function Engine(davCollection, cryptoId) { @@ -68,6 +69,7 @@ Engine.prototype = { // These can be overridden in subclasses, but don't need to be (assuming // serverPrefix is not shared with anything else) get statusFile() { return this.serverPrefix + "status.json"; }, + get keysFile() { return this.serverPrefix + "keys.json"; }, get snapshotFile() { return this.serverPrefix + "snapshot.json"; }, get deltasFile() { return this.serverPrefix + "deltas.json"; }, @@ -120,6 +122,29 @@ Engine.prototype = { this._snapshot.load(); }, + _getSymKey: function Engine__getCryptoId(cryptoId) { + let self = yield; + let done = false; + + this._dav.GET(this.keysFile, self.cb); + let keysResp = yield; + Utils.ensureStatus(keysResp.status, + "Could not get keys file.", [[200,300]]); + let keys = keysResp.responseText; + + if (!keys[this._userHash]) { + this._log.error("Keyring does not contain a key for this user"); + return; + } + + //Crypto.RSAdecrypt.async(Crypto, self.cb, + // keys[this._userHash], + + self.done(done); + yield; + this._log.warn("_getSymKey generator not properly closed"); + }, + _serializeCommands: function Engine__serializeCommands(commands) { let json = this._json.encode(commands); //json = json.replace(/ {action/g, "\n {action"); @@ -132,64 +157,52 @@ Engine.prototype = { return json; }, - _resetServer: function Engine__resetServer(onComplete) { - let [self, cont] = yield; + _resetServer: function Engine__resetServer() { + let self = yield; let done = false; try { this._log.debug("Resetting server data"); this._os.notifyObservers(null, this._osPrefix + "reset-server:start", ""); - this._dav.lock.async(this._dav, cont); + this._dav.lock.async(this._dav, self.cb); let locked = yield; - if (locked) - this._log.debug("Lock acquired"); - else { - this._log.warn("Could not acquire lock, aborting server reset"); - return; - } + if (!locked) + throw "Could not acquire lock, aborting server reset"; // try to delete all 3, check status after - this._dav.DELETE(this.statusFile, cont); + this._dav.DELETE(this.statusFile, self.cb); let statusResp = yield; - this._dav.DELETE(this.snapshotFile, cont); + this._dav.DELETE(this.snapshotFile, self.cb); let snapshotResp = yield; - this._dav.DELETE(this.deltasFile, cont); + this._dav.DELETE(this.deltasFile, self.cb); let deltasResp = yield; - this._dav.unlock.async(this._dav, cont); + this._dav.unlock.async(this._dav, self.cb); let unlocked = yield; - Utils.checkStatus(statusResp.status, - "Could not delete status file.", [[200,300],404]); - Utils.checkStatus(snapshotResp.status, - "Could not delete snapshot file.", [[200,300],404]); - Utils.checkStatus(deltasResp.status, - "Could not delete deltas file.", [[200,300],404]); + Utils.ensureStatus(statusResp.status, + "Could not delete status file.", [[200,300],404]); + Utils.ensureStatus(snapshotResp.status, + "Could not delete snapshot file.", [[200,300],404]); + Utils.ensureStatus(deltasResp.status, + "Could not delete deltas file.", [[200,300],404]); this._log.debug("Server files deleted"); done = true; + this._os.notifyObservers(null, this._osPrefix + "reset-server:success", ""); } catch (e) { - if (e != 'checkStatus failed') - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - if (done) { - this._log.debug("Server reset completed successfully"); - this._os.notifyObservers(null, this._osPrefix + "reset-server:success", ""); - } else { - this._log.debug("Server reset failed"); - this._os.notifyObservers(null, this._osPrefix + "reset-server:error", ""); - } - Utils.generatorDone(this, self, onComplete, done) - yield; // onComplete is responsible for closing the generator + this._log.error("Could not delete server files"); + this._os.notifyObservers(null, this._osPrefix + "reset-server:error", ""); + throw e; } - this._log.warn("generator not properly closed"); + + self.done(done) }, - _resetClient: function Engine__resetClient(onComplete) { - let [self, cont] = yield; + _resetClient: function Engine__resetClient() { + let self = yield; let done = false; try { @@ -201,7 +214,7 @@ Engine.prototype = { done = true; } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); + throw e; } finally { if (done) { @@ -211,10 +224,8 @@ Engine.prototype = { this._log.debug("Client reset failed"); this._os.notifyObservers(null, this._osPrefix + "reset-client:error", ""); } - Utils.generatorDone(this, self, onComplete, done); - yield; // onComplete is responsible for closing the generator + self.done(done); } - this._log.warn("generator not properly closed"); }, // original @@ -245,15 +256,15 @@ Engine.prototype = { // 3.1) Apply local delta with server changes ("D") // 3.2) Append server delta to the delta file and upload ("C") - _sync: function BmkEngine__sync(onComplete) { - let [self, cont] = yield; + _sync: function BmkEngine__sync() { + let self = yield; let synced = false, locked = null; try { this._log.info("Beginning sync"); this._os.notifyObservers(null, this._osPrefix + "sync:start", ""); - this._dav.lock.async(this._dav, cont); + this._dav.lock.async(this._dav, self.cb); locked = yield; if (locked) @@ -264,12 +275,13 @@ Engine.prototype = { } // Before we get started, make sure we have a remote directory to play in - this._dav.MKCOL(this.serverPrefix, cont); + this._dav.MKCOL(this.serverPrefix, self.cb); let ret = yield; - Utils.checkStatus(ret.status, "Could not create remote folder."); + if (!ret) + throw "Could not create remote folder"; // 1) Fetch server deltas - this._getServerData.async(this, cont); + this._getServerData.async(this, self.cb); let server = yield; this._log.info("Local snapshot version: " + this._snapshot.version); @@ -287,7 +299,7 @@ Engine.prototype = { let localJson = new SnapshotStore(); localJson.data = this._store.wrap(); - this._core.detectUpdates(cont, this._snapshot.data, localJson.data); + this._core.detectUpdates(self.cb, this._snapshot.data, localJson.data); let localUpdates = yield; this._log.trace("local json:\n" + localJson.serialize()); @@ -304,7 +316,7 @@ Engine.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. this._log.info("Reconciling client/server updates"); - this._core.reconcile(cont, localUpdates, server.updates); + this._core.reconcile(self.cb, localUpdates, server.updates); ret = yield; let clientChanges = ret.propagations[0]; @@ -351,7 +363,7 @@ Engine.prototype = { this._store.applyCommands(clientChanges); newSnapshot = this._store.wrap(); - this._core.detectUpdates(cont, this._snapshot.data, newSnapshot); + this._core.detectUpdates(self.cb, this._snapshot.data, newSnapshot); let diff = yield; if (diff.length != 0) { this._log.warn("Commands did not apply correctly"); @@ -372,7 +384,7 @@ Engine.prototype = { // conflicts, it should be the same as what the resolver returned newSnapshot = this._store.wrap(); - this._core.detectUpdates(cont, server.snapshot, newSnapshot); + this._core.detectUpdates(self.cb, server.snapshot, newSnapshot); let serverDelta = yield; // Log an error if not the same @@ -395,17 +407,17 @@ Engine.prototype = { if (server.formatVersion != STORAGE_FORMAT_VERSION || this._encryptionChanged) { - this._fullUpload.async(this, cont); + this._fullUpload.async(this, self.cb); let status = yield; if (!status) this._log.error("Could not upload files to server"); // eep? } else { - Crypto.PBEencrypt.async(Crypto, cont, + Crypto.PBEencrypt.async(Crypto, self.cb, this._serializeCommands(server.deltas), this._cryptoId); let data = yield; - this._dav.PUT(this.deltasFile, data, cont); + this._dav.PUT(this.deltasFile, data, self.cb); let deltasPut = yield; let c = 0; @@ -420,7 +432,7 @@ Engine.prototype = { maxVersion: this._snapshot.version, snapEncryption: server.snapEncryption, deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c}), cont); + itemCount: c}), self.cb); let statusPut = yield; if (deltasPut.status >= 200 && deltasPut.status < 300 && @@ -439,24 +451,22 @@ Engine.prototype = { synced = true; } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); + throw e; } finally { let ok = false; if (locked) { - this._dav.unlock.async(this._dav, cont); + this._dav.unlock.async(this._dav, self.cb); ok = yield; } if (ok && synced) { this._os.notifyObservers(null, this._osPrefix + "sync:success", ""); - Utils.generatorDone(this, self, onComplete, true); + self.done(true); } else { this._os.notifyObservers(null, this._osPrefix + "sync:error", ""); - Utils.generatorDone(this, self, onComplete, false); + self.done(false); } - yield; // onComplete is responsible for closing the generator } - this._log.warn("generator not properly closed"); }, /* Get the deltas/combined updates from the server @@ -483,220 +493,205 @@ Engine.prototype = { * the relevant deltas (from our snapshot version to current), * combined into a single set. */ - _getServerData: function BmkEngine__getServerData(onComplete) { - let [self, cont] = yield; + _getServerData: function BmkEngine__getServerData() { + let self = yield; let ret = {status: -1, formatVersion: null, maxVersion: null, snapVersion: null, snapEncryption: null, deltasEncryption: null, snapshot: null, deltas: null, updates: null}; - try { - this._log.debug("Getting status file from server"); - this._dav.GET(this.statusFile, cont); - let resp = yield; - let status = resp.status; - - switch (status) { - case 200: { - this._log.info("Got status file from server"); - - let status = this._json.decode(resp.responseText); - let deltas, allDeltas; - let snap = new SnapshotStore(); - - // Bail out if the server has a newer format version than we can parse - if (status.formatVersion > STORAGE_FORMAT_VERSION) { - this._log.error("Server uses storage format v" + status.formatVersion + - ", this client understands up to v" + STORAGE_FORMAT_VERSION); - Utils.generatorDone(this, self, onComplete, ret) - return; - } + this._log.debug("Getting status file from server"); + this._dav.GET(this.statusFile, self.cb); + let resp = yield; + let status = resp.status; - if (status.formatVersion == 0) { - ret.snapEncryption = status.snapEncryption = "none"; - ret.deltasEncryption = status.deltasEncryption = "none"; - } - - if (status.GUID != this._snapshot.GUID) { - this._log.info("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._log.debug("Remote: " + status.GUID); - this._log.debug("Local: " + this._snapshot.GUID); - this._store.resetGUIDs(); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.GUID = status.GUID; - } - - if (this._snapshot.version < status.snapVersion) { - if (this._snapshot.version >= 0) - this._log.info("Local snapshot is out of date"); - - this._log.info("Downloading server snapshot"); - this._dav.GET(this.snapshotFile, cont); - resp = yield; - Utils.checkStatus(resp.status, "Could not download snapshot."); - Crypto.PBEdecrypt.async(Crypto, cont, - resp.responseText, - this._cryptoId, - status.snapEncryption); - let data = yield; - snap.data = this._json.decode(data); + switch (status) { + case 200: { + this._log.info("Got status file from server"); - this._log.info("Downloading server deltas"); - this._dav.GET(this.deltasFile, cont); - resp = yield; - Utils.checkStatus(resp.status, "Could not download deltas."); - Crypto.PBEdecrypt.async(Crypto, cont, - resp.responseText, - this._cryptoId, - status.deltasEncryption); - data = yield; - allDeltas = this._json.decode(data); - deltas = this._json.decode(data); - - } else if (this._snapshot.version >= status.snapVersion && - this._snapshot.version < status.maxVersion) { - snap.data = Utils.deepCopy(this._snapshot.data); - - this._log.info("Downloading server deltas"); - this._dav.GET(this.deltasFile, cont); - resp = yield; - Utils.checkStatus(resp.status, "Could not download deltas."); - Crypto.PBEdecrypt.async(Crypto, cont, - resp.responseText, - this._cryptoId, - status.deltasEncryption); - let data = yield; - allDeltas = this._json.decode(data); - deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); - - } else if (this._snapshot.version == status.maxVersion) { - snap.data = Utils.deepCopy(this._snapshot.data); + let status = this._json.decode(resp.responseText); + let deltas, allDeltas; + let snap = new SnapshotStore(); - // FIXME: could optimize this case by caching deltas file - this._log.info("Downloading server deltas"); - this._dav.GET(this.deltasFile, cont); - resp = yield; - Utils.checkStatus(resp.status, "Could not download deltas."); - Crypto.PBEdecrypt.async(Crypto, cont, - resp.responseText, - this._cryptoId, - status.deltasEncryption); - let data = yield; - allDeltas = this._json.decode(data); - deltas = []; - - } else { // this._snapshot.version > status.maxVersion - this._log.error("Server snapshot is older than local snapshot"); - return; - } - - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands(deltas[i]); - } - - ret.status = 0; - ret.formatVersion = status.formatVersion; - ret.maxVersion = status.maxVersion; - ret.snapVersion = status.snapVersion; - ret.snapEncryption = status.snapEncryption; - ret.deltasEncryption = status.deltasEncryption; - ret.snapshot = snap.data; - ret.deltas = allDeltas; - this._core.detectUpdates(cont, this._snapshot.data, snap.data); - ret.updates = yield; - } break; - - case 404: { - this._log.info("Server has no status file, Initial upload to server"); - - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - - this._fullUpload.async(this, cont); - let uploadStatus = yield; - if (!uploadStatus) - return; - - this._log.info("Initial upload to server successful"); - this._snapshot.save(); - - ret.status = 0; - ret.formatVersion = STORAGE_FORMAT_VERSION; - ret.maxVersion = this._snapshot.version; - ret.snapVersion = this._snapshot.version; - ret.snapEncryption = Crypto.defaultAlgorithm; - ret.deltasEncryption = Crypto.defaultAlgorithm; - ret.snapshot = Utils.deepCopy(this._snapshot.data); - ret.deltas = []; - ret.updates = []; - } break; - - default: - this._log.error("Could not get status file: unknown HTTP status code " + - status); + // Bail out if the server has a newer format version than we can parse + if (status.formatVersion > STORAGE_FORMAT_VERSION) { + this._log.error("Server uses storage format v" + status.formatVersion + + ", this client understands up to v" + STORAGE_FORMAT_VERSION); break; } - } catch (e) { - if (e != 'checkStatus failed' && - e != 'decrypt failed') - this._log.error("Exception caught: " + (e.message? e.message : e)); + if (status.formatVersion == 0) { + ret.snapEncryption = status.snapEncryption = "none"; + ret.deltasEncryption = status.deltasEncryption = "none"; + } - } finally { - Utils.generatorDone(this, self, onComplete, ret) - yield; // onComplete is responsible for closing the generator + if (status.GUID != this._snapshot.GUID) { + this._log.info("Remote/local sync GUIDs do not match. " + + "Forcing initial sync."); + this._log.debug("Remote: " + status.GUID); + this._log.debug("Local: " + this._snapshot.GUID); + this._store.resetGUIDs(); + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.GUID = status.GUID; + } + + if (this._snapshot.version < status.snapVersion) { + this._log.trace("Local snapshot version < server snapVersion"); + + if (this._snapshot.version >= 0) + this._log.info("Local snapshot is out of date"); + + this._log.info("Downloading server snapshot"); + this._dav.GET(this.snapshotFile, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not download snapshot."); + Crypto.PBEdecrypt.async(Crypto, self.cb, + resp.responseText, + this._cryptoId, + status.snapEncryption); + let data = yield; + snap.data = this._json.decode(data); + + this._log.info("Downloading server deltas"); + this._dav.GET(this.deltasFile, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not download deltas."); + Crypto.PBEdecrypt.async(Crypto, self.cb, + resp.responseText, + this._cryptoId, + status.deltasEncryption); + data = yield; + allDeltas = this._json.decode(data); + deltas = this._json.decode(data); + + } else if (this._snapshot.version >= status.snapVersion && + this._snapshot.version < status.maxVersion) { + this._log.trace("Server snapVersion <= local snapshot version < server maxVersion"); + snap.data = Utils.deepCopy(this._snapshot.data); + + this._log.info("Downloading server deltas"); + this._dav.GET(this.deltasFile, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not download deltas."); + Crypto.PBEdecrypt.async(Crypto, self.cb, + resp.responseText, + this._cryptoId, + status.deltasEncryption); + let data = yield; + allDeltas = this._json.decode(data); + deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); + + } else if (this._snapshot.version == status.maxVersion) { + this._log.trace("Local snapshot version == server maxVersion"); + snap.data = Utils.deepCopy(this._snapshot.data); + + // FIXME: could optimize this case by caching deltas file + this._log.info("Downloading server deltas"); + this._dav.GET(this.deltasFile, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not download deltas."); + Crypto.PBEdecrypt.async(Crypto, self.cb, + resp.responseText, + this._cryptoId, + status.deltasEncryption); + let data = yield; + allDeltas = this._json.decode(data); + deltas = []; + + } else { // this._snapshot.version > status.maxVersion + this._log.error("Server snapshot is older than local snapshot"); + break; + } + + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands(deltas[i]); + } + + ret.status = 0; + ret.formatVersion = status.formatVersion; + ret.maxVersion = status.maxVersion; + ret.snapVersion = status.snapVersion; + ret.snapEncryption = status.snapEncryption; + ret.deltasEncryption = status.deltasEncryption; + ret.snapshot = snap.data; + ret.deltas = allDeltas; + this._core.detectUpdates(self.cb, this._snapshot.data, snap.data); + ret.updates = yield; + } break; + + case 404: { + this._log.info("Server has no status file, Initial upload to server"); + + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + + this._fullUpload.async(this, self.cb); + let uploadStatus = yield; + if (!uploadStatus) + break; + + this._log.info("Initial upload to server successful"); + this._snapshot.save(); + + ret.status = 0; + ret.formatVersion = STORAGE_FORMAT_VERSION; + ret.maxVersion = this._snapshot.version; + ret.snapVersion = this._snapshot.version; + ret.snapEncryption = Crypto.defaultAlgorithm; + ret.deltasEncryption = Crypto.defaultAlgorithm; + ret.snapshot = Utils.deepCopy(this._snapshot.data); + ret.deltas = []; + ret.updates = []; + } break; + + default: + this._log.error("Could not get status file: unknown HTTP status code " + + status); + break; } - this._log.warn("generator not properly closed"); + + self.done(ret) }, - _fullUpload: function Engine__fullUpload(onComplete) { - let [self, cont] = yield; + _fullUpload: function Engine__fullUpload() { + let self = yield; let ret = false; - try { - Crypto.PBEencrypt.async(Crypto, cont, - this._snapshot.serialize(), - this._cryptoId); - let data = yield; - this._dav.PUT(this.snapshotFile, data, cont); - resp = yield; - Utils.checkStatus(resp.status, "Could not upload snapshot."); + let gen = Crypto.PBEencrypt.async(Crypto, self.cb, + this._snapshot.serialize(), + this._cryptoId); + let data = yield; + if (gen.failed) throw "Encryption failed."; + + this._dav.PUT(this.snapshotFile, data, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not upload snapshot."); - this._dav.PUT(this.deltasFile, "[]", cont); - resp = yield; - Utils.checkStatus(resp.status, "Could not upload deltas."); + this._dav.PUT(this.deltasFile, "[]", self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not upload deltas."); - let c = 0; - for (GUID in this._snapshot.data) - c++; + let c = 0; + for (GUID in this._snapshot.data) + c++; - this._dav.PUT(this.statusFile, - this._json.encode( - {GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: this._snapshot.version, - maxVersion: this._snapshot.version, - snapEncryption: Crypto.defaultAlgorithm, - deltasEncryption: "none", - itemCount: c}), cont); - resp = yield; - Utils.checkStatus(resp.status, "Could not upload status file."); + this._dav.PUT(this.statusFile, + this._json.encode( + {GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: this._snapshot.version, + maxVersion: this._snapshot.version, + snapEncryption: Crypto.defaultAlgorithm, + deltasEncryption: "none", + itemCount: c}), self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not upload status file."); - this._log.info("Full upload to server successful"); - ret = true; - - } catch (e) { - if (e != 'checkStatus failed') - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - Utils.generatorDone(this, self, onComplete, ret) - yield; // onComplete is responsible for closing the generator - } - this._log.warn("generator not properly closed"); + this._log.info("Full upload to server successful"); + ret = true; + self.done(ret) }, sync: function Engine_sync(onComplete) { diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 2cec498dd883..517b19ae4969 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -66,6 +66,10 @@ Identity.prototype = { get username() { return this._username; }, set username(value) { this._username = value; }, + _key: null, + get key() { return this._key; }, + set key(value) { this._key = value; }, + _password: null, get password() { if (this._password === null) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 3365d40a557c..3142ff24ca29 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -478,7 +478,7 @@ Log4MozService.prototype = { return new FileAppender(file, formatter); case "rotating": // FIXME: hardcoded constants - return new RotatingFileAppender(file, formatter, ONE_MEGABYTE * 5, 0); + return new RotatingFileAppender(file, formatter, ONE_MEGABYTE * 2, 0); default: dump("log4moz: unknown appender kind: " + kind); return; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2850ec0b8ddf..1ae68d1d18ae 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -49,9 +49,10 @@ Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/async.js"); - -Function.prototype.async = Utils.generatorAsync; +Function.prototype.async = Async.sugar; +let Crypto = new WeaveCrypto(); /* * Service singleton @@ -303,79 +304,103 @@ WeaveSyncService.prototype = { this._os.notifyObservers(null, "weave:service-unlock:success", ""); }, - _login: function WeaveSync__login(onComplete) { - let [self, cont] = yield; - let success = false; + _login: function WeaveSync__login() { + let self = yield; try { this._log.debug("Logging in"); this._os.notifyObservers(null, "weave:service-login:start", ""); - if (!this.username) { - this._log.warn("No username set, login failed"); - return; - } - if (!this.password) { - this._log.warn("No password given or found in password manager"); - return; - } + if (!this.username) + throw "No username set, login failed"; + if (!this.password) + throw "No password given or found in password manager"; let serverURL = this._prefs.getCharPref("serverURL"); this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; this._log.info("Using server URL: " + this._dav.baseURL); - this._dav.login.async(this._dav, cont, this.username, this.password); - success = yield; + this._dav.login.async(this._dav, self.cb, this.username, this.password); + let success = yield; // FIXME: we want to limit this to when we get a 404! if (!success) { this._log.debug("Attempting to create user directory"); this._dav.baseURL = serverURL; - this._dav.MKCOL("user/" + this.userPath, cont); + this._dav.MKCOL("user/" + this.userPath, self.cb); let ret = yield; + if (!ret) + throw "Could not create user directory. Got status: " + ret.status; - if (ret.status == 201) { - this._log.debug("Successfully created user directory. Re-attempting login."); - this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; - this._dav.login.async(this._dav, cont, this.username, this.password); - success = yield; - } else { - this._log.debug("Could not create user directory. Got status: " + ret.status); - } + this._log.debug("Successfully created user directory. Re-attempting login."); + this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; + this._dav.login.async(this._dav, self.cb, this.username, this.password); + success = yield; + if (!success) + throw "Created user directory, but login still failed. Aborting."; } + this._dav.GET("private/privkey", self.cb); + let keyResp = yield; + Utils.ensureStatus(keyResp.status, + "Could not get private key from server", [[200,300],404]); + + if (keyResp.status != 404) { + this._cryptoId.key = keyResp.responseText; + + } else { + // generate a new key + this._log.debug("Generating new RSA key"); + Crypto.RSAkeygen.async(Crypto, self.cb, this._cryptoId.password); + let [privkey, pubkey] = yield; + + this._cryptoId.key = privkey; + + this._dav.MKCOL("private/", self.cb); + ret = yield; + if (!ret) + throw "Could not create private key directory"; + + this._dav.MKCOL("public/", self.cb); + ret = yield; + if (!ret) + throw "Could not create public key directory"; + + this._dav.PUT("private/privkey", privkey, self.cb); + ret = yield; + Utils.ensureStatus(ret.status, "Could not upload private key"); + + this._dav.PUT("public/pubkey", pubkey, self.cb); + ret = yield; + Utils.ensureStatus(ret.status, "Could not upload public key"); + } + + this._passphrase = null; + this._os.notifyObservers(null, "weave:service-login:success", ""); + self.done(true); + } catch (e) { - this._log.error("Exception caught: " + e.message); - - } finally { - this._passphrase = null; - if (success) { - //this._log.debug("Login successful"); // chrome prints this too, hm - this._os.notifyObservers(null, "weave:service-login:success", ""); - } else { - //this._log.debug("Login error"); - this._os.notifyObservers(null, "weave:service-login:error", ""); - } - Utils.generatorDone(this, self, onComplete, success); - yield; // onComplete is responsible for closing the generator + this._log.warn(Async.exceptionStr(self, e)); + this._log.trace(e.trace); + this._os.notifyObservers(null, "weave:service-login:error", ""); + self.done(false); } - this._log.warn("generator not properly closed"); }, - _resetLock: function WeaveSync__resetLock(onComplete) { - let [self, cont] = yield; + _resetLock: function WeaveSync__resetLock() { + let self = yield; let success = false; try { this._log.debug("Resetting server lock"); this._os.notifyObservers(null, "weave:server-lock-reset:start", ""); - this._dav.forceUnlock.async(this._dav, cont); + this._dav.forceUnlock.async(this._dav, self.cb); success = yield; } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); + throw e; } finally { if (success) { @@ -385,14 +410,12 @@ WeaveSyncService.prototype = { this._log.debug("Server lock reset failed"); this._os.notifyObservers(null, "weave:server-lock-reset:error", ""); } - Utils.generatorDone(this, self, onComplete, success); - yield; // generatorDone is responsible for closing the generator + self.done(success); } - this._log.warn("generator not properly closed"); }, _sync: function WeaveSync__sync() { - let [self, cont] = yield; + let self = yield; let success = false; try { @@ -402,11 +425,11 @@ WeaveSyncService.prototype = { this._os.notifyObservers(null, "weave:service:sync:start", ""); if (this._prefs.getBoolPref("bookmarks")) { - this._bmkEngine.sync(cont); + this._bmkEngine.sync(self.cb); yield; } if (this._prefs.getBoolPref("history")) { - this._histEngine.sync(cont); + this._histEngine.sync(self.cb); yield; } @@ -414,61 +437,41 @@ WeaveSyncService.prototype = { this._unlock(); } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); + throw e; } finally { if (success) this._os.notifyObservers(null, "weave:service:sync:success", ""); else this._os.notifyObservers(null, "weave:service:sync:error", ""); - Utils.generatorDone(this, self); - yield; // generatorDone is responsible for closing the generator + self.done(); } - this._log.warn("generator not properly closed"); }, _resetServer: function WeaveSync__resetServer() { - let [self, cont] = yield; + let self = yield; - try { - if (!this._lock()) - return; + if (!this._lock()) + return; - this._bmkEngine.resetServer(cont); - this._histEngine.resetServer(cont); + this._bmkEngine.resetServer(self.cb); + this._histEngine.resetServer(self.cb); - this._unlock(); - - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - Utils.generatorDone(this, self); - yield; // generatorDone is responsible for closing the generator - } - this._log.warn("generator not properly closed"); + this._unlock(); + self.done(); }, _resetClient: function WeaveSync__resetClient() { - let [self, cont] = yield; + let self = yield; - try { - if (!this._lock()) - return; + if (!this._lock()) + return; - this._bmkEngine.resetClient(cont); - this._histEngine.resetClient(cont); + this._bmkEngine.resetClient(self.cb); + this._histEngine.resetClient(self.cb); - this._unlock(); - - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - - } finally { - Utils.generatorDone(this, self); - yield; // generatorDone is responsible for closing the generator - } - this._log.warn("generator not properly closed"); + this._unlock(); + self.done(); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 87a0415b080e..b3de598f105b 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -45,8 +45,9 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); -Function.prototype.async = Utils.generatorAsync; +Function.prototype.async = Async.sugar; /* * SyncCore objects @@ -89,9 +90,9 @@ SyncCore.prototype = { return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); }, - _detectUpdates: function SC__detectUpdates(onComplete, a, b) { - let [self, cont] = yield; - let listener = new Utils.EventListener(cont); + _detectUpdates: function SC__detectUpdates(a, b) { + let self = yield; + let listener = new Utils.EventListener(self.cb); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let cmds = []; @@ -116,6 +117,7 @@ SyncCore.prototype = { depth: parents.length, parents: parents}); } } + for (let GUID in b) { timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); @@ -141,14 +143,12 @@ SyncCore.prototype = { }); } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); + throw e; } finally { timer = null; - Utils.generatorDone(this, self, onComplete, cmds); - yield; // onComplete is responsible for closing the generator + self.done(cmds); } - this._log.warn("generator not properly closed"); }, _commandLike: function SC__commandLike(a, b) { @@ -216,9 +216,9 @@ SyncCore.prototype = { return false; }, - _reconcile: function SC__reconcile(onComplete, listA, listB) { - let [self, cont] = yield; - let listener = new Utils.EventListener(cont); + _reconcile: function SC__reconcile(listA, listB) { + let self = yield; + let listener = new Utils.EventListener(self.cb); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let propagations = [[], []]; @@ -297,14 +297,13 @@ SyncCore.prototype = { ret = {propagations: propagations, conflicts: conflicts}; } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); + this._log.error("Exception caught: " + (e.message? e.message : e) + + " - " + (e.location? e.location : "_reconcile")); } finally { timer = null; - Utils.generatorDone(this, self, onComplete, ret); - yield; // onComplete is responsible for closing the generator + self.done(ret); } - this._log.warn("generator not properly closed"); }, // Public methods diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index fe8d6682f8ca..309dd98b8e16 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -113,6 +113,23 @@ let Utils = { return ret; }, + exceptionStr: function Weave_exceptionStr(e) { + let message = e.message? e.message : e; + let location = e.location? " (" + e.location + ")" : ""; + return message + location; + }, + + stackTrace: function Weave_stackTrace(stackFrame, str) { + if (stackFrame.caller) + str = Utils.stackTrace(stackFrame.caller, str); + + if (!str) + str = ""; + str += stackFrame + "\n"; + + return str; + }, + checkStatus: function Weave_checkStatus(code, msg, ranges) { if (!ranges) ranges = [[200,300]]; @@ -120,14 +137,19 @@ let Utils = { for (let i = 0; i < ranges.length; i++) { rng = ranges[i]; if (typeof(rng) == "object" && code >= rng[0] && code < rng[1]) - return; + return true; else if (typeof(rng) == "integer" && code == rng) - return; + return true; } let log = Log4Moz.Service.getLogger("Service.Util"); log.error(msg + " Error code: " + code); - throw 'checkStatus failed'; + return false; + }, + + ensureStatus: function Weave_ensureStatus(args) { + if (!Utils.checkStatus.apply(Utils, arguments)) + throw 'checkStatus failed'; }, makeURI: function Weave_makeURI(URIString) { @@ -147,73 +169,6 @@ let Utils = { Ci.nsIDOMXPathResult.ANY_TYPE, null); }, - bind2: function Weave_bind2(object, method) { - return function innerBind() { return method.apply(object, arguments); } - }, - - // Meant to be used like this in code that imports this file: - // - // Function.prototype.async = generatorAsync; - // - // So that you can do: - // - // gen = fooGen.async(...); - // ret = yield; - // - // where fooGen is a generator function, and gen is the running generator. - // ret is whatever the generator 'returns' via generatorDone(). - - generatorAsync: function Weave_generatorAsync(self, extra_args) { - try { - let args = Array.prototype.slice.call(arguments, 1); - let gen = this.apply(self, args); - gen.next(); // must initialize before sending - gen.send([gen, function(data) {Utils.continueGenerator(gen, data);}]); - return gen; - } catch (e) { - if (e instanceof StopIteration) { - dump("async warning: generator stopped unexpectedly"); - return null; - } else { - dump("Exception caught: " + e.message); - } - } - }, - - continueGenerator: function Weave_continueGenerator(generator, data) { - try { generator.send(data); } - catch (e) { - if (e instanceof StopIteration) - dump("continueGenerator warning: generator stopped unexpectedly"); - else - dump("Exception caught: " + e.message); - } - }, - - // generators created using Function.async can't simply call the - // callback with the return value, since that would cause the calling - // function to end up running (after the yield) from inside the - // generator. Instead, generators can call this method which sets up - // a timer to call the callback from a timer (and cleans up the timer - // to avoid leaks). It also closes generators after the timeout, to - // keep things clean. - generatorDone: function Weave_generatorDone(object, generator, callback, retval) { - if (object._timer) - throw "Called generatorDone when there is a timer already set." - - let cb = Utils.bind2(object, function(event) { - generator.close(); - generator = null; - object._timer = null; - if (callback) - callback(retval); - }); - - object._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - object._timer.initWithCallback(new Utils.EventListener(cb), - 0, object._timer.TYPE_ONE_SHOT); - }, - runCmd: function Weave_runCmd() { var binary; var args = []; @@ -318,6 +273,10 @@ let Utils = { return ret; }, + bind2: function Async_bind2(object, method) { + return function innerBind() { return method.apply(object, arguments); } + }, + /* * Event listener object * Used to handle XMLHttpRequest and nsITimer callbacks @@ -341,7 +300,7 @@ Utils.EventListener.prototype = { // nsITimerCallback notify: function EL_notify(timer) { - this._log.trace("Timer fired"); + //this._log.trace("Timer fired"); this._handler(timer); } } From 89a086575c6f5807b02edab94734a7a5732c4e10 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 7 Mar 2008 01:57:18 -0800 Subject: [PATCH 0148/1860] version bump --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index d14217c6f967..59f54494d5f6 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.19"; +const WEAVE_VERSION = "0.1.20"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From b6535bbbe7349b1322aa569dc069d1f8a78d8ff7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 7 Mar 2008 04:20:55 -0800 Subject: [PATCH 0149/1860] add a dav method for getting a list of server files/directories; delete all server data when private key isn't found (this will cause everyone's server data to get wiped, since no one has a key yet); fix a bug in checkStatus with specific status codes (as opposed to ranges) --- services/sync/modules/dav.js | 30 ++++++++++++++++++++++++++++++ services/sync/modules/service.js | 23 +++++++++++++++++++++-- services/sync/modules/util.js | 3 ++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 89beb84bbe53..77c8e152a438 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -224,6 +224,36 @@ DAVCollection.prototype = { return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers); }, + // Get all files + listFiles: function DC_listFiles(path) { + let self = yield; + + if (!path) + path = ""; + + let headers = {'Content-type': 'text/xml; charset="utf-8"', + 'Depth': '1'}; + headers.__proto__ = this._defaultHeaders; + + this._makeRequest.async(this, self.cb, "PROPFIND", path, headers, + "" + + ""); + let resp = yield; + Utils.ensureStatus(resp.status, "propfind failed"); + + // FIXME: shouldn't depend on the first one being the root + let tokens = Utils.xpath(resp.responseXML, '//D:href'); + let ret = [], + token, + root = tokens.iterateNext(); + root = root.textContent; + + while (token = tokens.iterateNext()) + ret.push(token.textContent.replace(root, '')); + + self.done(ret); + }, + // Login / Logout login: function DC_login(username, password) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1ae68d1d18ae..ee3f09e30d04 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -350,6 +350,10 @@ WeaveSyncService.prototype = { this._cryptoId.key = keyResp.responseText; } else { + // FIXME: hack to wipe everyone's server data... needs to be removed at some point + this._serverWipe.async(this, self.cb); + yield; + // generate a new key this._log.debug("Generating new RSA key"); Crypto.RSAkeygen.async(Crypto, self.cb, this._cryptoId.password); @@ -452,7 +456,7 @@ WeaveSyncService.prototype = { let self = yield; if (!this._lock()) - return; + throw "Could not acrquire lock"; this._bmkEngine.resetServer(self.cb); this._histEngine.resetServer(self.cb); @@ -465,7 +469,7 @@ WeaveSyncService.prototype = { let self = yield; if (!this._lock()) - return; + throw "Could not acrquire lock"; this._bmkEngine.resetClient(self.cb); this._histEngine.resetClient(self.cb); @@ -474,6 +478,21 @@ WeaveSyncService.prototype = { self.done(); }, + _serverWipe: function WeaveSync__serverWipe() { + let self = yield; + + this._dav.listFiles.async(this._dav, self.cb); + let names = yield; + + for (let i = 0; i < names.length; i++) { + this._dav.DELETE(names[i], self.cb); + let resp = yield; + this._log.debug(resp.status); + } + + self.done(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), // nsIObserver diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 309dd98b8e16..aa9b9688c658 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -138,8 +138,9 @@ let Utils = { rng = ranges[i]; if (typeof(rng) == "object" && code >= rng[0] && code < rng[1]) return true; - else if (typeof(rng) == "integer" && code == rng) + else if (typeof(rng) == "number" && code == rng) { return true; + } } let log = Log4Moz.Service.getLogger("Service.Util"); From 7316c0a704131be12926d15a05649148136842aa Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 7 Mar 2008 04:49:56 -0800 Subject: [PATCH 0150/1860] fix full (first) upload brokenness --- services/sync/modules/engines.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 983d2c40b049..fdd46dd1ad90 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -659,11 +659,10 @@ Engine.prototype = { let self = yield; let ret = false; - let gen = Crypto.PBEencrypt.async(Crypto, self.cb, - this._snapshot.serialize(), - this._cryptoId); + Crypto.PBEencrypt.async(Crypto, self.cb, + this._snapshot.serialize(), + this._cryptoId); let data = yield; - if (gen.failed) throw "Encryption failed."; this._dav.PUT(this.snapshotFile, data, self.cb); resp = yield; From aeb25732219d67b46b7ff8ef0bfdbda304a90008 Mon Sep 17 00:00:00 2001 From: Date: Tue, 11 Mar 2008 12:08:38 -0500 Subject: [PATCH 0151/1860] My changes to weave to enable cookie synchronization. So far untested and still missing a thing or two. --- services/sync/locales/en-US/preferences.dtd | 1 + services/sync/modules/engines.js | 26 ++++ services/sync/modules/service.js | 12 +- services/sync/modules/stores.js | 131 ++++++++++++++++++++ services/sync/modules/syncCores.js | 54 ++++++++ services/sync/services-sync.js | 1 + 6 files changed, 224 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index b0e41483e34b..1fcabc87a77b 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -21,6 +21,7 @@ + diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index fdd46dd1ad90..944a454ae7d3 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -753,3 +753,29 @@ HistoryEngine.prototype = { } }; HistoryEngine.prototype.__proto__ = new Engine(); + + +// Jono: the following is copy-and-paste code +function CookieEngine(davCollection, cryptoId) { + this._init(davCollection, cryptoId); +} +CookieEngine.prototype = { + get name() { return "cookie-engine"; }, + get logName() { return "CookieEngine"; }, + get serverPrefix() { return "user-data/cookies/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new CookieSyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new CookieStore(); + return this.__store; + } +}; +CookieEngine.prototype.__proto__ = new Engine(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ee3f09e30d04..5627f6a1d82b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -112,6 +112,13 @@ WeaveSyncService.prototype = { return this.__histEngine; }, + __cookieEngine: null, + get _cookieEngine() { + if (!this.__cookieEngine) + this.__cookieEngine = new CookieEngine(this._dav, this._cryptoId); + return this.__cookieEngine; + }, + // Logger object _log: null, @@ -436,7 +443,10 @@ WeaveSyncService.prototype = { this._histEngine.sync(self.cb); yield; } - + if (this._prefs.getBoolPref("cookies")) { + this._cookieEngine.sync(self.cb); + yield; + } success = true; this._unlock(); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index ca77ce30a7a7..a3b2841f9f39 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -646,3 +646,134 @@ HistoryStore.prototype = { } }; HistoryStore.prototype.__proto__ = new Store(); + + +function CookieStore() { + this._init(); +} +CookieStore.prototype = { + _logName: "CookieStore", + + + // Documentation of the nsICookie interface says: + // name ACString The name of the cookie. Read only. + // value ACString The cookie value. Read only. + // isDomain boolean True if the cookie is a domain cookie, false otherwise. Read only. + // host AUTF8String The host (possibly fully qualified) of the cookie. Read only. + // path AUTF8String The path pertaining to the cookie. Read only. + // isSecure boolean True if the cookie was transmitted over ssl, false otherwise. Read only. + // expires PRUint64 Expiration time (local timezone) expressed as number of seconds since Jan 1, 1970. Read only. + // status nsCookieStatus Holds the P3P status of cookie. Read only. + // policy nsCookiePolicy Holds the site's compact policy value. Read only. + // nsICookie2 deprecates expires, status, and policy, and adds: + //rawHost AUTF8String The host (possibly fully qualified) of the cookie without a leading dot to represent if it is a domain cookie. Read only. + //isSession boolean True if the cookie is a session cookie. Read only. + //expiry PRInt64 the actual expiry time of the cookie (where 0 does not represent a session cookie). Read only. + //isHttpOnly boolean True if the cookie is an http only cookie. Read only. + + + __cookieManager: null, + get _cookieManager() { + if (!this.__cookieManager) + this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. + getService(Ci.nsICookieManager2); + // need the 2nd revision of the ICookieManager interface + // because it supports add() and the 1st one doesn't. + return this.__cookieManager + }, + + _createCommand: function HistStore__createCommand(command) { + // we got a command to create a cookie in the local browser + // in order to sync with the server. + + this._log.info("CookieStore got createCommand: " + command ); + + // this assumes command.data fits the nsICookie2 interface + this.__cookieManager.add( command.data.host, + command.data.path, + command.data.name, + command.data.value, + command.data.isSecure, + command.data.isSession, + command.data.expiry ); + }, + + _removeCommand: function CookieStore__removeCommand(command) { + // we got a command to remove a cookie from the local browser + // in order to sync with the server. + // command.data appears to be equivalent to what wrap() puts in + // the JSON dictionary. + + this._log.info("CookieStore got removeCommand: " + command ); + + // I think it goes like this, according to + // http://developer.mozilla.org/en/docs/nsICookieManager + // the last argument is "always block cookies from this domain?" + // and the answer is "no". + this._cookieManager.remove( command.data.host, + command.data.name, + command.data.path, + false ); + }, + + _editCommand: function CookieStore__editCommand(command) { + // we got a command to change a cookie in the local browser + // in order to sync with the server. + + // TODO implement this!! + this._log.info("CookieStore got editCommand: " + command ); + }, + + + wrap: function CookieStore_wrap() { + // Return contents of this store, as JSON. + // A dictionary of cookies where the keys are GUIDs and the + // values are sub-dictionaries containing all cookie fields. + + let items = {}; + var iter = this.__cookieManager.enumerator; + while (iter.hasMoreElements()){ + var cookie = iter.getNext(); + if (cookie instanceof Ci.nsICookie){ + // String used to identify cookies is + // host:path:name + let key = cookie.host + ":" + cookie.path + ":" + cookie.name + items[ key ] = { parentGUID: '', + name: cookie.name, + value: cookie.value, + isDomain: cookie.isDomain, + host: cookie.host, + path: cookie.path, + isSecure: cookie.isSecure, + // nsICookie2 values: + rawHost: cookie.rawHost, + isSession: cookie.isSession, + expiry: cookie.expiry, + isHttpOnly: cookie.isHttpOnly } + + // http://developer.mozilla.org/en/docs/nsICookie + // Note: not syncing "expires", "status", or "policy" + // since they're deprecated. + + } + return items; + }, + + wipe: function CookieStore_wipe() { + // Remove everything from the store. Return nothing. + // TODO are the semantics of this just wiping out an internal + // buffer, or am I supposed to wipe out all cookies from + // the browser itself for reals? + + this.__cookieManager.removeAll() + }, + + resetGUIDs: function CookieStore_resetGUIDs() { + // called in the case where remote/local sync GUIDs do not + // match. We do need to override this, but since we're deriving + // GUIDs from the cookie data itself and not generating them, + // there's basically no way they can get "out of sync" so there's + // nothing to do here. + } +}; +CookieStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index b3de598f105b..7ecf7d5805f6 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -415,3 +415,57 @@ HistorySyncCore.prototype = { } }; HistorySyncCore.prototype.__proto__ = new SyncCore(); + + + + +function CookiesSyncCore() { + this._init(); +} +CookiesSyncCore.prototype = { + _logName: "CookieSync", + + __cookieManager: null, + get _cookieManager() { + if (!this.__cookieManager) + this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. + getService(Ci.nsICookieManager2); + // need the 2nd revision of the ICookieManager interface + // because it supports add() and the 1st one doesn't. + return this.__cookieManager + }, + + + _itemExists: function CSC__itemExists(GUID) { + // true if a cookie with the given GUID exists. + // The GUID that we are passed should correspond to the keys + // that we define in the JSON returned by CookieStore.wrap() + // That is, it will be a string of the form + // "host:path:name". + + // TODO verify that colons can't normally appear in any of + // the fields -- if they did it then we can't rely on .split(":") + // to parse correctly. + + let unused = 0; // for outparam from findMatchingCookie + let cookieArray = GUID.split( ":" ); + // create a generic object to represent the cookie -- just has + // to implement nsICookie2 interface. + cookie = Object(); + cookie.host = cookieArray[0] + cookie.path = cookieArray[1] + cookie.name = cookieArray[2]; + return this.__cookieManager.findMatchingCookie( cookie, unused ); + }, + + _commandLike: function CSC_commandLike(a, b) { + // Method required to be overridden. + // a and b each have a .data and a .GUID + // If this function returns true, an editCommand will be + // generated to try to resolve the thing. + // but are a and b objects of the type in the Store or + // are they "commands"?? + return false; + } +}; +CookiesSyncCore.prototype.__proto__ = new SyncCore(); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 4ff2bc74417d..8fdd154d4918 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -11,6 +11,7 @@ pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); pref("extensions.weave.bookmarks", true); pref("extensions.weave.history", true); +pref("extensions.weave.cookies", false ); pref("extensions.weave.schedule", 1); From a35cadb99f772686b471358dcbccb9df335514ce Mon Sep 17 00:00:00 2001 From: Date: Tue, 11 Mar 2008 11:47:54 -0700 Subject: [PATCH 0152/1860] add missing async.js module --- services/sync/modules/async.js | 275 +++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 services/sync/modules/async.js diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js new file mode 100644 index 000000000000..e72487282e4d --- /dev/null +++ b/services/sync/modules/async.js @@ -0,0 +1,275 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Async']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); + +/* + * Asynchronous generator helpers + */ + +function AsyncException(generator, message) { + this.generator = generator; + this.message = message; + this._trace = this.generator.trace; +} +AsyncException.prototype = { + get generator() { return this._generator; }, + set generator(value) { + if (!(value instanceof Generator)) + throw "expected type 'Generator'"; + this._generator = value; + }, + + get message() { return this._message; }, + set message(value) { this._message = value; }, + + get trace() { return this._trace; }, + set trace(value) { this._trace = value; }, + + toString: function AsyncException_toString() { + return this.message; + } +}; + +function Generator(object, method, onComplete, args) { + this._log = Log4Moz.Service.getLogger("Async.Generator"); + this._object = object; + this._method = method; + this.onComplete = onComplete; + this._args = args; + + let frame = Components.stack.caller; + if (frame.name == "Async_run") + frame = frame.caller; + if (frame.name == "Async_sugar") + frame = frame.caller; + + this._initFrame = frame; +} +Generator.prototype = { + get name() { return this._method.name; }, + get generator() { return this._generator; }, + + // set to true to error when generators to bottom out/return. + // you will then have to ensure that all generators yield as the + // last thing they do, and never call 'return' + get errorOnStop() { return this._errorOnStop; }, + set errorOnStop(value) { this._errorOnStop = value; }, + + get cb() { + let cb = Utils.bind2(this, function(data) { this.cont(data); }); + cb.parentGenerator = this; + return cb; + }, + get listener() { return new Utils.EventListener(this.cb); }, + + get _object() { return this.__object; }, + set _object(value) { + if (typeof value != "object") + throw "expected type 'object', got type '" + typeof(value) + "'"; + this.__object = value; + }, + + get _method() { return this.__method; }, + set _method(value) { + if (typeof value != "function") + throw "expected type 'function', got type '" + typeof(value) + "'"; + this.__method = value; + }, + + get onComplete() { + if (this._onComplete) + return this._onComplete; + return function() { this._log.trace("Generator " + this.name + " has no onComplete"); }; + }, + set onComplete(value) { + if (value && typeof value != "function") + throw "expected type 'function', got type '" + typeof(value) + "'"; + this._onComplete = value; + }, + + get trace() { + return Utils.stackTrace(this._initFrame) + + "JS frame :: unknown (async) :: " + this.name; + }, + + _handleException: function AsyncGen__handleException(e) { + if (e instanceof StopIteration) { + if (this.errorOnStop) { + this._log.error("Generator stopped unexpectedly"); + this._log.trace("Stack trace:\n" + this.trace); + this._exception = "Generator stopped unexpectedly"; // don't propagate StopIteration + } + + } else if (this.onComplete.parentGenerator instanceof Generator) { + //this._log.trace("Saving exception and stack trace"); + + switch (typeof e) { + case "string": + e = new AsyncException(this, e); + break; + case "object": + if (e.trace) // means we're re-throwing up the stack + e.trace = this.trace + "\n" + e.trace; + else + e.trace = this.trace; + break; + default: + this._log.debug("Unknown exception type: " + typeof(e)); + break; + } + + this._exception = e; + + } else { + this._log.error(Async.exceptionStr(this, e)); + this._log.trace("Stack trace:\n" + this.trace + + (e.trace? "\n" + e.trace : "")); + } + + // continue execution of caller. + // in the case of StopIteration we could return an error right + // away, but instead it's easiest/best to let the caller handle + // the error after a yield / in a callback. + this.done(); + }, + + run: function AsyncGen_run() { + try { + this._generator = this._method.apply(this._object, this._args); + this.generator.next(); // must initialize before sending + this.generator.send(this); + } catch (e) { + this._handleException(e); + } + }, + + cont: function AsyncGen_cont(data) { + try { this.generator.send(data); } + catch (e) { this._handleException(e); } + }, + + throw: function AsyncGen_throw(exception) { + try { this.generator.throw(exception); } + catch (e) { this._handleException(e); } + }, + + // async generators can't simply call a callback with the return + // value, since that would cause the calling function to end up + // running (after the yield) from inside the generator. Instead, + // generators can call this method which sets up a timer to call the + // callback from a timer (and cleans up the timer to avoid leaks). + // It also closes generators after the timeout, to keep things + // clean. + done: function AsyncGen_done(retval) { + if (this._timer) // the generator/_handleException called self.done() twice + return; + let self = this; + let cb = function() { self._done(retval); }; + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback(new Utils.EventListener(cb), + 0, this._timer.TYPE_ONE_SHOT); + }, + + _done: function AsyncGen__done(retval) { + this._generator.close(); + this._generator = null; + this._timer = null; + + if (this._exception) { + this._log.trace("Propagating exception to parent generator"); + this.onComplete.parentGenerator.throw(this._exception); + } else { + try { + this.onComplete(retval); + } catch (e) { + this._log.error("Exception caught from onComplete handler of " + + this.name + " generator"); + this._log.error("Exception: " + Utils.exceptionStr(e)); + this._log.trace("Current stack trace:\n" + Utils.stackTrace(Components.stack)); + this._log.trace("Initial stack trace:\n" + this.trace); + } + } + } +}; + +Async = { + + // Use: + // let gen = Async.run(this, this.fooGen, ...); + // let ret = yield; + // + // where fooGen is a generator function, and gen is a Generator instance + // ret is whatever the generator 'returns' via Generator.done(). + + run: function Async_run(object, method, onComplete, args) { + let args = Array.prototype.slice.call(arguments, 3); + let gen = new Generator(object, method, onComplete, args); + gen.run(); + return gen; + }, + + // Syntactic sugar for run(). Meant to be used like this in code + // that imports this file: + // + // Function.prototype.async = Async.sugar; + // + // So that you can do: + // + // let gen = fooGen.async(...); + // let ret = yield; + // + // Note that 'this' refers to the method being called, not the + // Async object. + + sugar: function Async_sugar(object, onComplete, extra_args) { + let args = Array.prototype.slice.call(arguments, 1); + args.unshift(object, this); + Async.run.apply(Async, args); + }, + + exceptionStr: function Async_exceptionStr(gen, ex) { + return "Exception caught in " + gen.name + ": " + Utils.exceptionStr(ex); + } +}; From 529d3ab772ca6d1d2787fa4f4268727d9679394d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 12 Mar 2008 23:06:28 -0700 Subject: [PATCH 0153/1860] workaround for services.m.c 500 error on some GET requests --- services/sync/modules/service.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ee3f09e30d04..0a4ed2022c41 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -341,6 +341,13 @@ WeaveSyncService.prototype = { throw "Created user directory, but login still failed. Aborting."; } + // FIXME: remove this after services.m.c gets fixed to not + // return 500 from a GET when parent dirs don't exist + this._dav.MKCOL("private/", self.cb); + ret = yield; + if (!ret) + throw "Could not create private key directory"; + this._dav.GET("private/privkey", self.cb); let keyResp = yield; Utils.ensureStatus(keyResp.status, From 55e742bb0f8357721b6658233c244f17210f4f68 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 12 Mar 2008 23:07:04 -0700 Subject: [PATCH 0154/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 59f54494d5f6..96aad8d96e06 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.20"; +const WEAVE_VERSION = "0.1.21"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 245fcb585d7ed3dbd710bd0e86810d9446cf7f28 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Mar 2008 15:17:04 -0700 Subject: [PATCH 0155/1860] Various improvements: * Logging improvements / default log levels tweaked. Less chatty now. Ability to tweak log levels of individual loggers via prefs. * Various crypto module fixes, specially for RSA. * 'service' lock removed, reuses server lock now. dav module supports temporarily blocking locks to simulate the 'service' lock (login() needs this, since dav is not configured at that time). * PKI support: data encryption uses randomly-generated symmetric keys, which are then encrypted with RSA public keys and stored on the server. --- services/sync/modules/async.js | 14 +- services/sync/modules/crypto.js | 92 +++++-- services/sync/modules/dav.js | 39 ++- services/sync/modules/engines.js | 420 ++++++++++++++---------------- services/sync/modules/identity.js | 36 ++- services/sync/modules/service.js | 253 ++++++++++-------- services/sync/modules/util.js | 15 +- services/sync/services-sync.js | 11 +- 8 files changed, 513 insertions(+), 367 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index e72487282e4d..effc3fb03907 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -43,6 +43,7 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/constants.js"); /* * Asynchronous generator helpers @@ -74,6 +75,8 @@ AsyncException.prototype = { function Generator(object, method, onComplete, args) { this._log = Log4Moz.Service.getLogger("Async.Generator"); + this._log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; this._object = object; this._method = method; this.onComplete = onComplete; @@ -121,7 +124,9 @@ Generator.prototype = { get onComplete() { if (this._onComplete) return this._onComplete; - return function() { this._log.trace("Generator " + this.name + " has no onComplete"); }; + return function() { + //this._log.trace("Generator " + this.name + " has no onComplete"); + }; }, set onComplete(value) { if (value && typeof value != "function") @@ -164,7 +169,7 @@ Generator.prototype = { } else { this._log.error(Async.exceptionStr(this, e)); - this._log.trace("Stack trace:\n" + this.trace + + this._log.debug("Stack trace:\n" + this.trace + (e.trace? "\n" + e.trace : "")); } @@ -213,6 +218,11 @@ Generator.prototype = { }, _done: function AsyncGen__done(retval) { + if (!this._generator) { + this._log.error("Generator '" + this.name + "' called 'done' after it's finalized"); + this._log.trace("Initial stack trace:\n" + this.trace); + return; + } this._generator.close(); this._generator = null; this._timer = null; diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 185e9079cba9..5aca7bca5d70 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -88,6 +88,8 @@ WeaveCrypto.prototype = { _init: function Crypto__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.crypto")]; let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch2); branch.addObserver("extensions.weave.encryption", this, false); @@ -163,7 +165,7 @@ WeaveCrypto.prototype = { // generates a random string that can be used as a passphrase _opensslRand: function Crypto__opensslRand(length) { if (!length) - length = 256; + length = 128; let outputFile = Utils.getTmp("output"); if (outputFile.exists()) @@ -180,7 +182,7 @@ WeaveCrypto.prototype = { }, // generates an rsa public/private key pair, with the private key encrypted - _opensslRSAKeyGen: function Crypto__opensslRSAKeyGen(password, algorithm, bits) { + _opensslRSAKeyGen: function Crypto__opensslRSAKeyGen(identity, algorithm, bits) { if (!algorithm) algorithm = "aes-256-cbc"; if (!bits) @@ -206,7 +208,7 @@ WeaveCrypto.prototype = { // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.writeString(password); + passFOS.writeString(identity.password); passFOS.close(); try { @@ -235,7 +237,7 @@ WeaveCrypto.prototype = { }, // returns 'input' encrypted with the 'pubkey' public RSA key - _opensslRSAencrypt: function Crypto__opensslRSAencrypt(input, pubkey) { + _opensslRSAencrypt: function Crypto__opensslRSAencrypt(input, identity) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.writeString(input); @@ -243,18 +245,23 @@ WeaveCrypto.prototype = { let keyFile = Utils.getTmp("key"); let [keyFOS] = Utils.open(keyFile, ">"); - keyFOS.writeString(pubkey); + keyFOS.writeString(identity.pubkey); keyFOS.close(); + let tmpFile = Utils.getTmp("tmp-output"); + if (tmpFile.exists()) + tmpFile.remove(false); + let outputFile = Utils.getTmp("output"); if (outputFile.exists()) outputFile.remove(false); this._openssl("rsautl", "-encrypt", "-pubin", "-inkey", "key", - "-in", "input", "-out", "output"); + "-in", "input", "-out", "tmp-output"); + this._openssl("base64", "-in", "tmp-output", "-out", "output"); let [outputFIS] = Utils.open(outputFile, "<"); - let output = Utils.readStream(outpusFIS); + let output = Utils.readStream(outputFIS); outputFIS.close(); outputFile.remove(false); @@ -262,7 +269,7 @@ WeaveCrypto.prototype = { }, // returns 'input' decrypted with the 'privkey' private RSA key and password - _opensslRSAdecrypt: function Crypto__opensslRSAdecrypt(input, privkey, password) { + _opensslRSAdecrypt: function Crypto__opensslRSAdecrypt(input, identity) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.writeString(input); @@ -270,9 +277,13 @@ WeaveCrypto.prototype = { let keyFile = Utils.getTmp("key"); let [keyFOS] = Utils.open(keyFile, ">"); - keyFOS.writeString(privkey); + keyFOS.writeString(identity.privkey); keyFOS.close(); + let tmpFile = Utils.getTmp("tmp-output"); + if (tmpFile.exists()) + tmpFile.remove(false); + let outputFile = Utils.getTmp("output"); if (outputFile.exists()) outputFile.remove(false); @@ -280,12 +291,13 @@ WeaveCrypto.prototype = { // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.writeString(password); + passFOS.writeString(identity.password); passFOS.close(); try { - this._openssl("rsautl", "-decrypt", "-inkey", "key", "-pass", - "file:pass", "-in", "input", "-out", "output"); + this._openssl("base64", "-d", "-in", "input", "-out", "tmp-output"); + this._openssl("rsautl", "-decrypt", "-inkey", "key", "-passin", + "file:pass", "-in", "tmp-output", "-out", "output"); } catch(e) { throw e; @@ -295,7 +307,43 @@ WeaveCrypto.prototype = { } let [outputFIS] = Utils.open(outputFile, "<"); - let output = Utils.readStream(outpusFIS); + let output = Utils.readStream(outputFIS); + outputFIS.close(); + outputFile.remove(false); + + return output; + }, + + // returns the public key from the private key + _opensslRSAkeydecrypt: function Crypto__opensslRSAkeydecrypt(identity) { + let keyFile = Utils.getTmp("key"); + let [keyFOS] = Utils.open(keyFile, ">"); + keyFOS.writeString(identity.privkey); + keyFOS.close(); + + let outputFile = Utils.getTmp("output"); + if (outputFile.exists()) + outputFile.remove(false); + + // nsIProcess doesn't support stdin, so we write a file instead + let passFile = Utils.getTmp("pass"); + let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); + passFOS.writeString(identity.password); + passFOS.close(); + + try { + this._openssl("rsa", "-in", "key", "-pubout", "-out", "output", + "-passin", "file:pass"); + + } catch(e) { + throw e; + + } finally { + passFile.remove(false); + } + + let [outputFIS] = Utils.open(outputFile, "<"); + let output = Utils.readStream(outputFIS); outputFIS.close(); outputFile.remove(false); @@ -445,21 +493,27 @@ WeaveCrypto.prototype = { self.done(ret); }, - RSAkeygen: function Crypto_RSAkeygen(password) { + RSAkeygen: function Crypto_RSAkeygen(identity) { let self = yield; - let ret = this._opensslRSAKeyGen(password); + let ret = this._opensslRSAKeyGen(identity); self.done(ret); }, - RSAencrypt: function Crypto_RSAencrypt(data, key) { + RSAencrypt: function Crypto_RSAencrypt(data, identity) { let self = yield; - let ret = this._opensslRSAencrypt(data, key); + let ret = this._opensslRSAencrypt(data, identity); self.done(ret); }, - RSAdecrypt: function Crypto_RSAdecrypt(data, key, password) { + RSAdecrypt: function Crypto_RSAdecrypt(data, identity) { let self = yield; - let ret = this._opensslRSAdecrypt(data, key, password); + let ret = this._opensslRSAdecrypt(data, identity); + self.done(ret); + }, + + RSAkeydecrypt: function Crypto_RSAkeydecrypt(identity) { + let self = yield; + let ret = this._opensslRSAkeydecrypt(identity); self.done(ret); } }; diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 77c8e152a438..5b03126488c2 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -45,6 +45,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/constants.js"); Function.prototype.async = Async.sugar; @@ -57,8 +58,11 @@ function DAVCollection(baseURL) { this._baseURL = baseURL; this._authProvider = new DummyAuthProvider(); this._log = Log4Moz.Service.getLogger("Service.DAV"); + this._log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.dav")]; } DAVCollection.prototype = { + __dp: null, get _dp() { if (!this.__dp) @@ -81,11 +85,23 @@ DAVCollection.prototype = { return this._loggedIn; }, + get locked() { + return !this._lockAllowed || this._token != null; + }, + + _lockAllowed: true, + get allowLock() { + return this._lockAllowed; + }, + set allowLock(value) { + this._lockAllowed = value; + }, + _makeRequest: function DC__makeRequest(op, path, headers, data) { let self = yield; let ret; - this._log.debug("Creating " + op + " request for " + this._baseURL + path); + this._log.debug(op + " request for " + path); let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); @@ -106,9 +122,9 @@ DAVCollection.prototype = { let key; for (key in headers) { if (key == 'Authorization') - this._log.debug("HTTP Header " + key + ": ***** (suppressed)"); + this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); else - this._log.debug("HTTP Header " + key + ": " + headers[key]); + this._log.trace("HTTP Header " + key + ": " + headers[key]); request.setRequestHeader(key, headers[key]); } @@ -155,7 +171,7 @@ DAVCollection.prototype = { this._makeRequest.async(this, self.cb, "GET", path2 + "/", this._defaultHeaders); let ret = yield; if (!(ret.status == 404 || ret.status == 500)) { // FIXME: 500 is a services.m.c oddity - this._log.debug("Skipping creation of path " + path2 + + this._log.trace("Skipping creation of path " + path2 + " (got status " + ret.status + ")"); } else { this._log.debug("Creating path: " + path2); @@ -211,6 +227,9 @@ DAVCollection.prototype = { }, LOCK: function DC_LOCK(path, data, onComplete) { + if (!this._lockAllowed) + throw "Cannot acquire lock (internal lock)"; + let headers = {'Content-type': 'text/xml; charset="utf-8"', 'Depth': 'infinity', 'Timeout': 'Second-600'}; @@ -326,7 +345,7 @@ DAVCollection.prototype = { let self = yield; this._token = null; - this._log.info("Acquiring lock"); + this._log.trace("Acquiring lock"); if (this._token) { this._log.debug("Lock called, but we already hold a token"); @@ -354,7 +373,7 @@ DAVCollection.prototype = { this._token = token.textContent; if (this._token) - this._log.debug("Lock acquired"); + this._log.trace("Lock acquired"); else this._log.warn("Could not acquire lock"); @@ -364,9 +383,9 @@ DAVCollection.prototype = { unlock: function DC_unlock() { let self = yield; - this._log.info("Releasing lock"); + this._log.trace("Releasing lock"); - if (this._token === null) { + if (!this.locked) { this._log.debug("Unlock called, but we don't hold a token right now"); self.done(true); yield; @@ -384,9 +403,9 @@ DAVCollection.prototype = { this._token = null; if (this._token) - this._log.info("Could not release lock"); + this._log.trace("Could not release lock"); else - this._log.info("Lock released (or we didn't have one)"); + this._log.trace("Lock released (or we didn't have one)"); self.done(!this._token); }, diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index fdd46dd1ad90..a7fb1ef9a2d2 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -46,6 +46,7 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/async.js"); @@ -53,8 +54,8 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; let Crypto = new WeaveCrypto(); -function Engine(davCollection, cryptoId) { - //this._init(davCollection, cryptoId); +function Engine(davCollection, pbeId) { + //this._init(davCollection, pbeId); } Engine.prototype = { // "default-engine"; @@ -114,35 +115,36 @@ Engine.prototype = { this.__snapshot = value; }, - _init: function Engine__init(davCollection, cryptoId) { + _init: function Engine__init(davCollection, pbeId) { this._dav = davCollection; - this._cryptoId = cryptoId; + this._pbeId = pbeId; + this._engineId = new Identity(pbeId.realm + " - " + this.logName, + pbeId.username); this._log = Log4Moz.Service.getLogger("Service." + this.logName); + this._log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.engine")]; this._osPrefix = "weave:" + this.name + ":"; this._snapshot.load(); }, - _getSymKey: function Engine__getCryptoId(cryptoId) { + _getSymKey: function Engine__getSymKey() { let self = yield; - let done = false; this._dav.GET(this.keysFile, self.cb); let keysResp = yield; Utils.ensureStatus(keysResp.status, "Could not get keys file.", [[200,300]]); - let keys = keysResp.responseText; + let keys = this._json.decode(keysResp.responseText); - if (!keys[this._userHash]) { - this._log.error("Keyring does not contain a key for this user"); - return; - } + if (!keys || !keys.ring || !keys.ring[this._engineId.userHash]) + throw "Keyring does not contain a key for this user"; - //Crypto.RSAdecrypt.async(Crypto, self.cb, - // keys[this._userHash], - - self.done(done); - yield; - this._log.warn("_getSymKey generator not properly closed"); + Crypto.RSAdecrypt.async(Crypto, self.cb, + keys.ring[this._engineId.userHash], this._pbeId); + let symkey = yield; + this._engineId.setTempPassword(symkey); + + self.done(true); }, _serializeCommands: function Engine__serializeCommands(commands) { @@ -165,11 +167,6 @@ Engine.prototype = { this._log.debug("Resetting server data"); this._os.notifyObservers(null, this._osPrefix + "reset-server:start", ""); - this._dav.lock.async(this._dav, self.cb); - let locked = yield; - if (!locked) - throw "Could not acquire lock, aborting server reset"; - // try to delete all 3, check status after this._dav.DELETE(this.statusFile, self.cb); let statusResp = yield; @@ -178,9 +175,6 @@ Engine.prototype = { this._dav.DELETE(this.deltasFile, self.cb); let deltasResp = yield; - this._dav.unlock.async(this._dav, self.cb); - let unlocked = yield; - Utils.ensureStatus(statusResp.status, "Could not delete status file.", [[200,300],404]); Utils.ensureStatus(snapshotResp.status, @@ -258,215 +252,184 @@ Engine.prototype = { _sync: function BmkEngine__sync() { let self = yield; - let synced = false, locked = null; - try { - this._log.info("Beginning sync"); - this._os.notifyObservers(null, this._osPrefix + "sync:start", ""); + this._log.info("Beginning sync"); - this._dav.lock.async(this._dav, self.cb); - locked = yield; + // Before we get started, make sure we have a remote directory to play in + this._dav.MKCOL(this.serverPrefix, self.cb); + let ret = yield; + if (!ret) + throw "Could not create remote folder"; - if (locked) - this._log.info("Lock acquired"); - else { - this._log.warn("Could not acquire lock, aborting sync"); - return; - } + // 1) Fetch server deltas + this._getServerData.async(this, self.cb); + let server = yield; - // Before we get started, make sure we have a remote directory to play in - this._dav.MKCOL(this.serverPrefix, self.cb); - let ret = yield; - if (!ret) - throw "Could not create remote folder"; + this._log.info("Local snapshot version: " + this._snapshot.version); + this._log.info("Server status: " + server.status); + this._log.info("Server maxVersion: " + server.maxVersion); + this._log.info("Server snapVersion: " + server.snapVersion); - // 1) Fetch server deltas - this._getServerData.async(this, self.cb); - let server = yield; + if (server.status != 0) { + this._log.fatal("Sync error: could not get server status, " + + "or initial upload failed. Aborting sync."); + return; + } - this._log.info("Local snapshot version: " + this._snapshot.version); - this._log.info("Server status: " + server.status); - this._log.info("Server maxVersion: " + server.maxVersion); - this._log.info("Server snapVersion: " + server.snapVersion); + // 2) Generate local deltas from snapshot -> current client status - if (server.status != 0) { - this._log.fatal("Sync error: could not get server status, " + - "or initial upload failed. Aborting sync."); - return; - } + let localJson = new SnapshotStore(); + localJson.data = this._store.wrap(); + this._core.detectUpdates(self.cb, this._snapshot.data, localJson.data); + let localUpdates = yield; - // 2) Generate local deltas from snapshot -> current client status + this._log.trace("local json:\n" + localJson.serialize()); + this._log.trace("Local updates: " + this._serializeCommands(localUpdates)); + this._log.trace("Server updates: " + this._serializeCommands(server.updates)); - let localJson = new SnapshotStore(); - localJson.data = this._store.wrap(); - this._core.detectUpdates(self.cb, this._snapshot.data, localJson.data); - let localUpdates = yield; + if (server.updates.length == 0 && localUpdates.length == 0) { + this._snapshot.version = server.maxVersion; + this._log.info("Sync complete: no changes needed on client or server"); + self.done(true); + return; + } + + // 3) Reconcile client/server deltas and generate new deltas for them. - this._log.trace("local json:\n" + localJson.serialize()); - this._log.trace("Local updates: " + this._serializeCommands(localUpdates)); - this._log.trace("Server updates: " + this._serializeCommands(server.updates)); + this._log.info("Reconciling client/server updates"); + this._core.reconcile(self.cb, localUpdates, server.updates); + ret = yield; - if (server.updates.length == 0 && localUpdates.length == 0) { - this._snapshot.version = server.maxVersion; - this._log.info("Sync complete (1): no changes needed on client or server"); - synced = true; - return; - } - - // 3) Reconcile client/server deltas and generate new deltas for them. + let clientChanges = ret.propagations[0]; + let serverChanges = ret.propagations[1]; + let clientConflicts = ret.conflicts[0]; + let serverConflicts = ret.conflicts[1]; - this._log.info("Reconciling client/server updates"); - this._core.reconcile(self.cb, localUpdates, server.updates); - ret = yield; + this._log.info("Changes for client: " + clientChanges.length); + this._log.info("Predicted changes for server: " + serverChanges.length); + this._log.info("Client conflicts: " + clientConflicts.length); + this._log.info("Server conflicts: " + serverConflicts.length); + this._log.trace("Changes for client: " + this._serializeCommands(clientChanges)); + this._log.trace("Predicted changes for server: " + this._serializeCommands(serverChanges)); + this._log.trace("Client conflicts: " + this._serializeConflicts(clientConflicts)); + this._log.trace("Server conflicts: " + this._serializeConflicts(serverConflicts)); - let clientChanges = ret.propagations[0]; - let serverChanges = ret.propagations[1]; - let clientConflicts = ret.conflicts[0]; - let serverConflicts = ret.conflicts[1]; + if (!(clientChanges.length || serverChanges.length || + clientConflicts.length || serverConflicts.length)) { + this._log.info("Sync complete: no changes needed on client or server"); + this._snapshot.data = localJson.data; + this._snapshot.version = server.maxVersion; + this._snapshot.save(); + self.done(true); + return; + } - this._log.info("Changes for client: " + clientChanges.length); - this._log.info("Predicted changes for server: " + serverChanges.length); - this._log.info("Client conflicts: " + clientConflicts.length); - this._log.info("Server conflicts: " + serverConflicts.length); - this._log.trace("Changes for client: " + this._serializeCommands(clientChanges)); - this._log.trace("Predicted changes for server: " + this._serializeCommands(serverChanges)); - this._log.trace("Client conflicts: " + this._serializeConflicts(clientConflicts)); - this._log.trace("Server conflicts: " + this._serializeConflicts(serverConflicts)); + if (clientConflicts.length || serverConflicts.length) { + this._log.warn("Conflicts found! Discarding server changes"); + } - if (!(clientChanges.length || serverChanges.length || - clientConflicts.length || serverConflicts.length)) { - this._log.info("Sync complete (2): no changes needed on client or server"); - this._snapshot.data = localJson.data; - this._snapshot.version = server.maxVersion; - this._snapshot.save(); - synced = true; - return; - } + let savedSnap = Utils.deepCopy(this._snapshot.data); + let savedVersion = this._snapshot.version; + let newSnapshot; - if (clientConflicts.length || serverConflicts.length) { - this._log.warn("Conflicts found! Discarding server changes"); - } - - let savedSnap = Utils.deepCopy(this._snapshot.data); - let savedVersion = this._snapshot.version; - let newSnapshot; - - // 3.1) Apply server changes to local store - if (clientChanges.length) { - this._log.info("Applying changes locally"); - // Note that we need to need to apply client changes to the - // current tree, not the saved snapshot - - localJson.applyCommands(clientChanges); - this._snapshot.data = localJson.data; - this._snapshot.version = server.maxVersion; - this._store.applyCommands(clientChanges); - newSnapshot = this._store.wrap(); - - this._core.detectUpdates(self.cb, this._snapshot.data, newSnapshot); - let diff = yield; - if (diff.length != 0) { - this._log.warn("Commands did not apply correctly"); - this._log.debug("Diff from snapshot+commands -> " + - "new snapshot after commands:\n" + - this._serializeCommands(diff)); - // FIXME: do we really want to revert the snapshot here? - this._snapshot.data = Utils.deepCopy(savedSnap); - this._snapshot.version = savedVersion; - } - this._snapshot.save(); - } - - // 3.2) Append server delta to the delta file and upload - - // Generate a new diff, from the current server snapshot to the - // current client snapshot. In the case where there are no - // conflicts, it should be the same as what the resolver returned + // 3.1) Apply server changes to local store + if (clientChanges.length) { + this._log.info("Applying changes locally"); + // Note that we need to need to apply client changes to the + // current tree, not the saved snapshot + localJson.applyCommands(clientChanges); + this._snapshot.data = localJson.data; + this._snapshot.version = server.maxVersion; + this._store.applyCommands(clientChanges); newSnapshot = this._store.wrap(); - this._core.detectUpdates(self.cb, server.snapshot, newSnapshot); - let serverDelta = yield; - // Log an error if not the same - if (!(serverConflicts.length || - Utils.deepEquals(serverChanges, serverDelta))) - this._log.warn("Predicted server changes differ from " + - "actual server->client diff (can be ignored in many cases)"); - - this._log.info("Actual changes for server: " + serverDelta.length); - this._log.debug("Actual changes for server: " + - this._serializeCommands(serverDelta)); - - if (serverDelta.length) { - this._log.info("Uploading changes to server"); - - this._snapshot.data = newSnapshot; - this._snapshot.version = ++server.maxVersion; - - server.deltas.push(serverDelta); - - if (server.formatVersion != STORAGE_FORMAT_VERSION || - this._encryptionChanged) { - this._fullUpload.async(this, self.cb); - let status = yield; - if (!status) - this._log.error("Could not upload files to server"); // eep? - - } else { - Crypto.PBEencrypt.async(Crypto, self.cb, - this._serializeCommands(server.deltas), - this._cryptoId); - let data = yield; - this._dav.PUT(this.deltasFile, data, self.cb); - let deltasPut = yield; - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._dav.PUT(this.statusFile, - this._json.encode( - {GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshot.version, - snapEncryption: server.snapEncryption, - deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c}), self.cb); - let statusPut = yield; - - if (deltasPut.status >= 200 && deltasPut.status < 300 && - statusPut.status >= 200 && statusPut.status < 300) { - this._log.info("Successfully updated deltas and status on server"); - this._snapshot.save(); - } else { - // FIXME: revert snapshot here? - can't, we already applied - // updates locally! - need to save and retry - this._log.error("Could not update deltas on server"); - } - } + this._core.detectUpdates(self.cb, this._snapshot.data, newSnapshot); + let diff = yield; + if (diff.length != 0) { + this._log.warn("Commands did not apply correctly"); + this._log.debug("Diff from snapshot+commands -> " + + "new snapshot after commands:\n" + + this._serializeCommands(diff)); + // FIXME: do we really want to revert the snapshot here? + this._snapshot.data = Utils.deepCopy(savedSnap); + this._snapshot.version = savedVersion; } + this._snapshot.save(); + } - this._log.info("Sync complete"); - synced = true; + // 3.2) Append server delta to the delta file and upload - } catch (e) { - throw e; + // Generate a new diff, from the current server snapshot to the + // current client snapshot. In the case where there are no + // conflicts, it should be the same as what the resolver returned + + newSnapshot = this._store.wrap(); + this._core.detectUpdates(self.cb, server.snapshot, newSnapshot); + let serverDelta = yield; + + // Log an error if not the same + if (!(serverConflicts.length || + Utils.deepEquals(serverChanges, serverDelta))) + this._log.warn("Predicted server changes differ from " + + "actual server->client diff (can be ignored in many cases)"); + + this._log.info("Actual changes for server: " + serverDelta.length); + this._log.debug("Actual changes for server: " + + this._serializeCommands(serverDelta)); + + if (serverDelta.length) { + this._log.info("Uploading changes to server"); + + this._snapshot.data = newSnapshot; + this._snapshot.version = ++server.maxVersion; + + server.deltas.push(serverDelta); + + if (server.formatVersion != STORAGE_FORMAT_VERSION || + this._encryptionChanged) { + this._fullUpload.async(this, self.cb); + let status = yield; + if (!status) + this._log.error("Could not upload files to server"); // eep? - } finally { - let ok = false; - if (locked) { - this._dav.unlock.async(this._dav, self.cb); - ok = yield; - } - if (ok && synced) { - this._os.notifyObservers(null, this._osPrefix + "sync:success", ""); - self.done(true); } else { - this._os.notifyObservers(null, this._osPrefix + "sync:error", ""); - self.done(false); + Crypto.PBEencrypt.async(Crypto, self.cb, + this._serializeCommands(server.deltas), + this._engineId); + let data = yield; + this._dav.PUT(this.deltasFile, data, self.cb); + let deltasPut = yield; + + let c = 0; + for (GUID in this._snapshot.data) + c++; + + this._dav.PUT(this.statusFile, + this._json.encode( + {GUID: this._snapshot.GUID, + formatVersion: STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshot.version, + snapEncryption: server.snapEncryption, + deltasEncryption: Crypto.defaultAlgorithm, + itemCount: c}), self.cb); + let statusPut = yield; + + if (deltasPut.status >= 200 && deltasPut.status < 300 && + statusPut.status >= 200 && statusPut.status < 300) { + this._log.info("Successfully updated deltas and status on server"); + this._snapshot.save(); + } else { + // FIXME: revert snapshot here? - can't, we already applied + // updates locally! - need to save and retry + this._log.error("Could not update deltas on server"); + } } } + + this._log.info("Sync complete"); + self.done(true); }, /* Get the deltas/combined updates from the server @@ -520,6 +483,9 @@ Engine.prototype = { break; } + this._getSymKey.async(this, self.cb); + yield; + if (status.formatVersion == 0) { ret.snapEncryption = status.snapEncryption = "none"; ret.deltasEncryption = status.deltasEncryption = "none"; @@ -548,7 +514,7 @@ Engine.prototype = { Utils.ensureStatus(resp.status, "Could not download snapshot."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._cryptoId, + this._engineId, status.snapEncryption); let data = yield; snap.data = this._json.decode(data); @@ -559,7 +525,7 @@ Engine.prototype = { Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._cryptoId, + this._engineId, status.deltasEncryption); data = yield; allDeltas = this._json.decode(data); @@ -576,7 +542,7 @@ Engine.prototype = { Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._cryptoId, + this._engineId, status.deltasEncryption); let data = yield; allDeltas = this._json.decode(data); @@ -593,7 +559,7 @@ Engine.prototype = { Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._cryptoId, + this._engineId, status.deltasEncryption); let data = yield; allDeltas = this._json.decode(data); @@ -659,11 +625,29 @@ Engine.prototype = { let self = yield; let ret = false; + Crypto.PBEkeygen.async(Crypto, self.cb); + let symkey = yield; + this._engineId.setTempPassword(symkey); + if (!this._engineId.password) + throw "Could not generate a symmetric encryption key"; + + Crypto.RSAencrypt.async(Crypto, self.cb, + this._engineId.password, this._pbeId); + let enckey = yield; + if (!enckey) + throw "Could not encrypt symmetric encryption key"; + + let keys = {ring: {}}; + keys.ring[this._engineId.userHash] = enckey; + this._dav.PUT(this.keysFile, this._json.encode(keys), self.cb); + let resp = yield; + Utils.ensureStatus(resp.status, "Could not upload keyring file."); + Crypto.PBEencrypt.async(Crypto, self.cb, this._snapshot.serialize(), - this._cryptoId); + this._engineId); let data = yield; - + this._dav.PUT(this.snapshotFile, data, self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not upload snapshot."); @@ -706,8 +690,8 @@ Engine.prototype = { } }; -function BookmarksEngine(davCollection, cryptoId) { - this._init(davCollection, cryptoId); +function BookmarksEngine(davCollection, pbeId) { + this._init(davCollection, pbeId); } BookmarksEngine.prototype = { get name() { return "bookmarks-engine"; }, @@ -730,8 +714,8 @@ BookmarksEngine.prototype = { }; BookmarksEngine.prototype.__proto__ = new Engine(); -function HistoryEngine(davCollection, cryptoId) { - this._init(davCollection, cryptoId); +function HistoryEngine(davCollection, pbeId) { + this._init(davCollection, pbeId); } HistoryEngine.prototype = { get name() { return "history-engine"; }, diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 517b19ae4969..fd6694e3a317 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -66,9 +66,39 @@ Identity.prototype = { get username() { return this._username; }, set username(value) { this._username = value; }, - _key: null, - get key() { return this._key; }, - set key(value) { this._key = value; }, + get userHash() { + //this._log.trace("Hashing username " + this.username); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let data = converter.convertToByteArray(this.username, {}); + hasher.update(data, data.length); + let rawHash = hasher.finish(false); + + // return the two-digit hexadecimal code for a byte + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); + //this._log.trace("Username hashes to " + hash); + return hash; + }, + + _privkey: null, + get privkey() { return this._privkey; }, + set privkey(value) { this._privkey = value; }, + + // FIXME: get this from the privkey using crypto.js? + _pubkey: null, + get pubkey() { return this._pubkey; }, + set pubkey(value) { this._pubkey = value; }, _password: null, get password() { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0a4ed2022c41..cdc7ff5c23cc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -62,17 +62,6 @@ let Crypto = new WeaveCrypto(); function WeaveSyncService() { this._init(); } WeaveSyncService.prototype = { - __prefs: null, - get _prefs() { - if (!this.__prefs) { - this.__prefs = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefService); - this.__prefs = this.__prefs.getBranch(PREFS_BRANCH); - this.__prefs.QueryInterface(Ci.nsIPrefBranch2); - } - return this.__prefs; - }, - __os: null, get _os() { if (!this.__os) @@ -134,13 +123,13 @@ WeaveSyncService.prototype = { }, get username() { - return this._prefs.getCharPref("username"); + return Utils.prefs.getCharPref("username"); }, set username(value) { if (value) - this._prefs.setCharPref("username", value); + Utils.prefs.setCharPref("username", value); else - this._prefs.clearUserPref("username"); + Utils.prefs.clearUserPref("username"); // fixme - need to loop over all Identity objects - needs some rethinking... this._mozId.username = value; @@ -153,30 +142,7 @@ WeaveSyncService.prototype = { get passphrase() { return this._cryptoId.password; }, set passphrase(value) { this._cryptoId.password = value; }, - get userPath() { - this._log.info("Hashing username " + this.username); - - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA1); - - let data = converter.convertToByteArray(this.username, {}); - hasher.update(data, data.length); - let rawHash = hasher.finish(false); - - // return the two-digit hexadecimal code for a byte - function toHexString(charCode) { - return ("0" + charCode.toString(16)).slice(-2); - } - - let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); - this._log.debug("Username hashes to " + hash); - return hash; - }, + get userPath() { return this._mozId.userHash; }, get currentUser() { if (this._dav.loggedIn) @@ -185,20 +151,20 @@ WeaveSyncService.prototype = { }, get enabled() { - return this._prefs.getBoolPref("enabled"); + return Utils.prefs.getBoolPref("enabled"); }, get schedule() { if (!this.enabled) return 0; // manual/off - return this._prefs.getIntPref("schedule"); + return Utils.prefs.getIntPref("schedule"); }, _init: function WeaveSync__init() { this._initLogs(); this._log.info("Weave Sync Service Initializing"); - this._prefs.addObserver("", this, false); + Utils.prefs.addObserver("", this, false); if (!this.enabled) { this._log.info("Weave Sync disabled"); @@ -252,17 +218,19 @@ WeaveSyncService.prototype = { _initLogs: function WeaveSync__initLogs() { this._log = Log4Moz.Service.getLogger("Service.Main"); + this._log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.main")]; let formatter = Log4Moz.Service.newFormatter("basic"); let root = Log4Moz.Service.rootLogger; - root.level = Log4Moz.Level[this._prefs.getCharPref("log.rootLogger")]; + root.level = Log4Moz.Level[Utils.prefs.getCharPref("log.rootLogger")]; let capp = Log4Moz.Service.newAppender("console", formatter); - capp.level = Log4Moz.Level[this._prefs.getCharPref("log.appender.console")]; + capp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.console")]; root.addAppender(capp); let dapp = Log4Moz.Service.newAppender("dump", formatter); - dapp.level = Log4Moz.Level[this._prefs.getCharPref("log.appender.dump")]; + dapp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.dump")]; root.addAppender(dapp); let brief = this._dirSvc.get("ProfD", Ci.nsIFile); @@ -279,35 +247,62 @@ WeaveSyncService.prototype = { verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); let fapp = Log4Moz.Service.newFileAppender("rotating", brief, formatter); - fapp.level = Log4Moz.Level[this._prefs.getCharPref("log.appender.briefLog")]; + fapp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.briefLog")]; root.addAppender(fapp); let vapp = Log4Moz.Service.newFileAppender("rotating", verbose, formatter); - vapp.level = Log4Moz.Level[this._prefs.getCharPref("log.appender.debugLog")]; + vapp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.debugLog")]; root.addAppender(vapp); }, _lock: function weaveSync__lock() { - if (this._locked) { + let self = yield; + + this._dav.lock.async(this._dav, self.cb); + let locked = yield; + + if (!locked) { this._log.warn("Service lock failed: already locked"); this._os.notifyObservers(null, "weave:service-lock:error", ""); - return false; + self.done(false); + return; } - this._locked = true; + this._log.debug("Service lock acquired"); this._os.notifyObservers(null, "weave:service-lock:success", ""); - return true; + self.done(true); }, _unlock: function WeaveSync__unlock() { - this._locked = false; + let self = yield; + + this._dav.unlock.async(this._dav, self.cb); + let unlocked = yield; + + if (!unlocked) { + this._log.error("Service unlock failed"); + this._os.notifyObservers(null, "weave:service-unlock:error", ""); + self.done(false); + return; + } + this._log.debug("Service lock released"); this._os.notifyObservers(null, "weave:service-unlock:success", ""); + self.done(true); }, - _login: function WeaveSync__login() { + _login: function WeaveSync__login(password, passphrase) { let self = yield; try { + if (this._dav.locked) + throw "Could not login: could not acquire lock"; + this._dav.allowLock = false; + + // cache password & passphrase + // if null, we'll try to get them from the pw manager below + this._mozId.setTempPassword(password); + this._cryptoId.setTempPassword(passphrase); + this._log.debug("Logging in"); this._os.notifyObservers(null, "weave:service-login:start", ""); @@ -316,7 +311,7 @@ WeaveSyncService.prototype = { if (!this.password) throw "No password given or found in password manager"; - let serverURL = this._prefs.getCharPref("serverURL"); + let serverURL = Utils.prefs.getCharPref("serverURL"); this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; this._log.info("Using server URL: " + this._dav.baseURL); @@ -354,7 +349,9 @@ WeaveSyncService.prototype = { "Could not get private key from server", [[200,300],404]); if (keyResp.status != 404) { - this._cryptoId.key = keyResp.responseText; + this._cryptoId.privkey = keyResp.responseText; + Crypto.RSAkeydecrypt.async(Crypto, self.cb, this._cryptoId); + this._cryptoId.pubkey = yield; } else { // FIXME: hack to wipe everyone's server data... needs to be removed at some point @@ -363,10 +360,11 @@ WeaveSyncService.prototype = { // generate a new key this._log.debug("Generating new RSA key"); - Crypto.RSAkeygen.async(Crypto, self.cb, this._cryptoId.password); + Crypto.RSAkeygen.async(Crypto, self.cb, this._cryptoId); let [privkey, pubkey] = yield; - this._cryptoId.key = privkey; + this._cryptoId.privkey = privkey; + this._cryptoId.pubkey = pubkey; this._dav.MKCOL("private/", self.cb); ret = yield; @@ -388,17 +386,34 @@ WeaveSyncService.prototype = { } this._passphrase = null; + this._dav.allowLock = true; this._os.notifyObservers(null, "weave:service-login:success", ""); self.done(true); } catch (e) { - this._log.warn(Async.exceptionStr(self, e)); - this._log.trace(e.trace); + this._dav.allowLock = true; this._os.notifyObservers(null, "weave:service-login:error", ""); - self.done(false); + throw e; } }, + // NOTE: doesn't lock because it's called from _login() which already holds the lock + _serverWipe: function WeaveSync__serverWipe() { + let self = yield; + + this._dav.listFiles.async(this._dav, self.cb); + let names = yield; + + for (let i = 0; i < names.length; i++) { + this._dav.DELETE(names[i], self.cb); + let resp = yield; + this._log.debug(resp.status); + } + + self.done(); + }, + + // NOTE: can't lock because it assumes the lock is being held ;) _resetLock: function WeaveSync__resetLock() { let self = yield; let success = false; @@ -425,76 +440,104 @@ WeaveSyncService.prototype = { } }, + _syncEngine: function WeaveSync__syncEngine(eng) { + let self = yield; + + this._os.notifyObservers(null, "weave:" + eng.name + ":sync:start", ""); + + let ret; + try { + eng.sync(self.cb); + ret = yield; + } catch (e) { + this._log.warn("Engine failed with error: " + Utils.exceptionStr(e)); + if (e.trace) + this._log.debug("Engine stack trace: " + Utils.stackTrace(e.trace)); + } + + if (ret) + this._os.notifyObservers(null, "weave:" + eng.name + ":sync:success", ""); + else + this._os.notifyObservers(null, "weave:" + eng.name + ":sync:error", ""); + + self.done(); + }, + _sync: function WeaveSync__sync() { let self = yield; - let success = false; try { - if (!this._lock()) - return; + this._lock.async(this, self.cb) + let locked = yield; + if (!locked) + return; this._os.notifyObservers(null, "weave:service:sync:start", ""); - if (this._prefs.getBoolPref("bookmarks")) { - this._bmkEngine.sync(self.cb); + if (Utils.prefs.getBoolPref("bookmarks")) { + this._syncEngine.async(this, self.cb, this._bmkEngine); yield; } - if (this._prefs.getBoolPref("history")) { - this._histEngine.sync(self.cb); + if (Utils.prefs.getBoolPref("history")) { + this._syncEngine.async(this, self.cb, this._histEngine); yield; } - success = true; - this._unlock(); + this._unlock.async(this, self.cb) + yield; + this._os.notifyObservers(null, "weave:service:sync:success", ""); } catch (e) { + this._unlock.async(this, self.cb) + yield; + this._os.notifyObservers(null, "weave:service:sync:error", ""); throw e; - - } finally { - if (success) - this._os.notifyObservers(null, "weave:service:sync:success", ""); - else - this._os.notifyObservers(null, "weave:service:sync:error", ""); - self.done(); } + + self.done(); }, _resetServer: function WeaveSync__resetServer() { let self = yield; - if (!this._lock()) - throw "Could not acrquire lock"; + this._lock.async(this, self.cb) + let locked = yield; + if (!locked) + return; - this._bmkEngine.resetServer(self.cb); - this._histEngine.resetServer(self.cb); + try { + this._bmkEngine.resetServer(self.cb); + this._histEngine.resetServer(self.cb); + + } catch (e) { + throw e; + + } finally { + this._unlock.async(this, self.cb) + yield; + } - this._unlock(); self.done(); }, _resetClient: function WeaveSync__resetClient() { let self = yield; - if (!this._lock()) - throw "Could not acrquire lock"; + this._lock.async(this, self.cb) + let locked = yield; + if (!locked) + return; - this._bmkEngine.resetClient(self.cb); - this._histEngine.resetClient(self.cb); + try { + this._bmkEngine.resetClient(self.cb); + this._histEngine.resetClient(self.cb); - this._unlock(); - self.done(); - }, + } catch (e) { + throw e; - _serverWipe: function WeaveSync__serverWipe() { - let self = yield; - - this._dav.listFiles.async(this._dav, self.cb); - let names = yield; - - for (let i = 0; i < names.length; i++) { - this._dav.DELETE(names[i], self.cb); - let resp = yield; - this._log.debug(resp.status); + } finally { + this._unlock.async(this, self.cb) + yield; } self.done(); @@ -519,14 +562,7 @@ WeaveSyncService.prototype = { // These are global (for all engines) login: function WeaveSync_login(password, passphrase) { - if (!this._lock()) - return; - // cache password & passphrase - // if null, _login() will try to get them from the pw manager - this._mozId.setTempPassword(password); - this._cryptoId.setTempPassword(passphrase); - let self = this; - this._login.async(this, function() {self._unlock()}); + this._login.async(this, null, password, passphrase); }, logout: function WeaveSync_logout() { @@ -537,12 +573,7 @@ WeaveSyncService.prototype = { this._os.notifyObservers(null, "weave:service-logout:success", ""); }, - resetLock: function WeaveSync_resetLock() { - if (!this._lock()) - return; - let self = this; - this._resetLock.async(this, function() {self._unlock()}); - }, + resetLock: function WeaveSync_resetLock() { this._resetLock.async(this); }, // These are per-engine diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index aa9b9688c658..990ecbb225fb 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -278,6 +278,17 @@ let Utils = { return function innerBind() { return method.apply(object, arguments); } }, + _prefs: null, + get prefs() { + if (!this.__prefs) { + this.__prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService); + this.__prefs = this.__prefs.getBranch(PREFS_BRANCH); + this.__prefs.QueryInterface(Ci.nsIPrefBranch2); + } + return this.__prefs; + }, + /* * Event listener object * Used to handle XMLHttpRequest and nsITimer callbacks @@ -286,7 +297,9 @@ let Utils = { EventListener: function Weave_EventListener(handler, eventName) { this._handler = handler; this._eventName = eventName; - this._log = Log4Moz.Service.getLogger("Service.EventHandler"); + this._log = Log4Moz.Service.getLogger("Async.EventHandler"); + this._log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; } }; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 4ff2bc74417d..7dd634c3b028 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -13,9 +13,14 @@ pref("extensions.weave.bookmarks", true); pref("extensions.weave.history", true); pref("extensions.weave.schedule", 1); - -pref("extensions.weave.log.rootLogger", "Config"); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); pref("extensions.weave.log.appender.briefLog", "Info"); -pref("extensions.weave.log.appender.debugLog", "Config"); +pref("extensions.weave.log.appender.debugLog", "Trace"); + +pref("extensions.weave.log.rootLogger", "Debug"); +pref("extensions.weave.log.logger.async", "Debug"); +pref("extensions.weave.log.logger.service.crypto", "Debug"); +pref("extensions.weave.log.logger.service.dav", "Debug"); +pref("extensions.weave.log.logger.service.engine", "Debug"); +pref("extensions.weave.log.logger.service.main", "Trace"); From 98f5ab97b133ab9e4e3af15bc6d160b1f882952d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Mar 2008 17:31:00 -0700 Subject: [PATCH 0156/1860] bookmarks sync core: always include type information when generating commands. also, allow for properties to not be set when comparing commands for likeness. --- services/sync/modules/syncCores.js | 43 +++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index b3de598f105b..fe29795ec9ed 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -335,7 +335,24 @@ BookmarksSyncCore.prototype = { return this._bms.getItemIdForGUID(GUID) >= 0; }, - _commandLike: function BSC_commandLike(a, b) { + _getEdits: function BSC__getEdits(a, b) { + // NOTE: we do not increment ret.numProps, as that would cause + // edit commands to always get generated + let ret = SyncCore.prototype._getEdits.call(this, a, b); + ret.props.type = a.type; + return ret; + }, + + // compares properties + // returns true if the property is not set in either object + // returns true if the property is set and equal in both objects + // returns false otherwise + _comp: function BSC__comp(a, b, prop) { + return (!a.data[prop] && !b.data[prop]) || + (a.data[prop] && b.data[prop] && (a.data[prop] == b.data[prop])); + }, + + _commandLike: function BSC__commandLike(a, b) { // Check that neither command is null, that their actions, types, // and parents are the same, and that they don't have the same // GUID. @@ -357,33 +374,33 @@ BookmarksSyncCore.prototype = { // the same index to qualify for 'likeness'. switch (a.data.type) { case "bookmark": - if (a.data.URI == b.data.URI && - a.data.title == b.data.title) + if (this._comp(a.data, b.data, 'URI') && + this._comp(a.data, b.data, 'title')) return true; return false; case "query": - if (a.data.URI == b.data.URI && - a.data.title == b.data.title) + if (this._comp(a.data, b.data, 'URI') && + this._comp(a.data, b.data, 'title')) return true; return false; case "microsummary": - if (a.data.URI == b.data.URI && - a.data.generatorURI == b.data.generatorURI) + if (this._comp(a.data, b.data, 'URI') && + this._comp(a.data, b.data, 'generatorURI')) return true; return false; case "folder": - if (a.index == b.index && - a.data.title == b.data.title) + if (this._comp(a, b, 'index') && + this._comp(a.data, b.data, 'title')) return true; return false; case "livemark": - if (a.data.title == b.data.title && - a.data.siteURI == b.data.siteURI && - a.data.feedURI == b.data.feedURI) + if (this._comp(a.data, b.data, 'title') && + this._comp(a.data, b.data, 'siteURI') && + this._comp(a.data, b.data, 'feedURI')) return true; return false; case "separator": - if (a.index == b.index) + if (this._comp(a, b, 'index')) return true; return false; default: From a46f6b88e0d4db8b181b5b2585bb90ec4cf88dba Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Mar 2008 17:31:45 -0700 Subject: [PATCH 0157/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 96aad8d96e06..a0ffe08828d7 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -41,7 +41,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.21"; +const WEAVE_VERSION = "0.1.22"; const STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 6ef92be233c9d7bda8641ee9f7fe0f261cf27b98 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Mar 2008 18:42:12 -0700 Subject: [PATCH 0158/1860] split up/simplify login functions; add a global (server-wide) storage version; wipe server for storage version upgrades --- services/sync/modules/constants.js | 4 +- services/sync/modules/engines.js | 12 +-- services/sync/modules/service.js | 150 +++++++++++++++++++---------- services/sync/modules/util.js | 7 +- 4 files changed, 115 insertions(+), 58 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index a0ffe08828d7..5e13b6263d6d 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -35,6 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', + 'ENGINE_STORAGE_FORMAT_VERSION', 'PREFS_BRANCH', 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', @@ -42,7 +43,8 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; const WEAVE_VERSION = "0.1.22"; -const STORAGE_FORMAT_VERSION = 2; +const STORAGE_FORMAT_VERSION = 0; +const ENGINE_STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a7fb1ef9a2d2..ac3826526ce9 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -386,7 +386,7 @@ Engine.prototype = { server.deltas.push(serverDelta); - if (server.formatVersion != STORAGE_FORMAT_VERSION || + if (server.formatVersion != ENGINE_STORAGE_FORMAT_VERSION || this._encryptionChanged) { this._fullUpload.async(this, self.cb); let status = yield; @@ -408,7 +408,7 @@ Engine.prototype = { this._dav.PUT(this.statusFile, this._json.encode( {GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, + formatVersion: ENGINE_STORAGE_FORMAT_VERSION, snapVersion: server.snapVersion, maxVersion: this._snapshot.version, snapEncryption: server.snapEncryption, @@ -477,9 +477,9 @@ Engine.prototype = { let snap = new SnapshotStore(); // Bail out if the server has a newer format version than we can parse - if (status.formatVersion > STORAGE_FORMAT_VERSION) { + if (status.formatVersion > ENGINE_STORAGE_FORMAT_VERSION) { this._log.error("Server uses storage format v" + status.formatVersion + - ", this client understands up to v" + STORAGE_FORMAT_VERSION); + ", this client understands up to v" + ENGINE_STORAGE_FORMAT_VERSION); break; } @@ -602,7 +602,7 @@ Engine.prototype = { this._snapshot.save(); ret.status = 0; - ret.formatVersion = STORAGE_FORMAT_VERSION; + ret.formatVersion = ENGINE_STORAGE_FORMAT_VERSION; ret.maxVersion = this._snapshot.version; ret.snapVersion = this._snapshot.version; ret.snapEncryption = Crypto.defaultAlgorithm; @@ -663,7 +663,7 @@ Engine.prototype = { this._dav.PUT(this.statusFile, this._json.encode( {GUID: this._snapshot.GUID, - formatVersion: STORAGE_FORMAT_VERSION, + formatVersion: ENGINE_STORAGE_FORMAT_VERSION, snapVersion: this._snapshot.version, maxVersion: this._snapshot.version, snapEncryption: Crypto.defaultAlgorithm, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index cdc7ff5c23cc..061a6be9454e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -290,6 +290,101 @@ WeaveSyncService.prototype = { self.done(true); }, + _createUserDir: function WeaveSync__createUserDir(serverURL) { + let self = yield; + + this._log.debug("Attempting to create user directory"); + + this._dav.baseURL = serverURL; + this._dav.MKCOL("user/" + this.userPath, self.cb); + let ret = yield; + if (!ret) + throw "Could not create user directory"; + + this._log.debug("Successfully created user directory. Re-attempting login."); + this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; + this._dav.login.async(this._dav, self.cb, this.username, this.password); + let success = yield; + if (!success) + throw "Created user directory, but login still failed. Aborting."; + + self.done(); + }, + + _uploadVersion: function WeaveSync__uploadVersion() { + let self = yield; + + this._dav.MKCOL("meta", self.cb); + let ret = yield; + if (!ret) + throw "Could not create meta information directory"; + + this._dav.PUT("meta/version", STORAGE_FORMAT_VERSION, self.cb); + ret = yield; + Utils.ensureStatus(ret.status, "Could not upload server version file"); + + self.done(); + }, + + // force a server wipe when the version is lower than ours (or there is none) + _versionCheck: function WeaveSync__versionCheck() { + let self = yield; + + this._dav.GET("meta/version", self.cb); + let ret = yield; + + if (!Utils.checkStatus(ret.status)) { + this._log.info("Server has no version file. Wiping server data."); + this._serverWipe.async(this, self.cb); + yield; + this._uploadVersion.async(this, self.cb); + yield; + + } else if (ret.responseText < STORAGE_FORMAT_VERSION) { + this._log.info("Server version too low. Wiping server data."); + this._serverWipe.async(this, self.cb); + yield; + this._uploadVersion.async(this, self.cb); + yield; + + } else if (ret.responseText > STORAGE_FORMAT_VERSION) { + // FIXME: should we do something here? + } + + self.done(); + }, + + _generateKeys: function WeaveSync__generateKeys() { + let self = yield; + + this._log.debug("Generating new RSA key"); + Crypto.RSAkeygen.async(Crypto, self.cb, this._cryptoId); + let [privkey, pubkey] = yield; + + this._cryptoId.privkey = privkey; + this._cryptoId.pubkey = pubkey; + + this._dav.MKCOL("private/", self.cb); + let ret = yield; + if (!ret) + throw "Could not create private key directory"; + + this._dav.MKCOL("public/", self.cb); + ret = yield; + if (!ret) + throw "Could not create public key directory"; + + this._dav.PUT("private/privkey", privkey, self.cb); + ret = yield; + Utils.ensureStatus(ret.status, "Could not upload private key"); + + this._dav.PUT("public/pubkey", pubkey, self.cb); + ret = yield; + Utils.ensureStatus(ret.status, "Could not upload public key"); + + self.done(); + }, + _login: function WeaveSync__login(password, passphrase) { let self = yield; @@ -318,30 +413,14 @@ WeaveSyncService.prototype = { this._dav.login.async(this._dav, self.cb, this.username, this.password); let success = yield; - // FIXME: we want to limit this to when we get a 404! if (!success) { - this._log.debug("Attempting to create user directory"); - - this._dav.baseURL = serverURL; - this._dav.MKCOL("user/" + this.userPath, self.cb); - let ret = yield; - if (!ret) - throw "Could not create user directory. Got status: " + ret.status; - - this._log.debug("Successfully created user directory. Re-attempting login."); - this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; - this._dav.login.async(this._dav, self.cb, this.username, this.password); - success = yield; - if (!success) - throw "Created user directory, but login still failed. Aborting."; + // FIXME: we should actually limit this to when we get a 404 + this._createUserDir.async(this, self.cb, serverURL); + yield; } - // FIXME: remove this after services.m.c gets fixed to not - // return 500 from a GET when parent dirs don't exist - this._dav.MKCOL("private/", self.cb); - ret = yield; - if (!ret) - throw "Could not create private key directory"; + this._versionCheck.async(this, self.cb); + yield; this._dav.GET("private/privkey", self.cb); let keyResp = yield; @@ -354,35 +433,8 @@ WeaveSyncService.prototype = { this._cryptoId.pubkey = yield; } else { - // FIXME: hack to wipe everyone's server data... needs to be removed at some point - this._serverWipe.async(this, self.cb); + this._generateKeys.async(this, self.cb); yield; - - // generate a new key - this._log.debug("Generating new RSA key"); - Crypto.RSAkeygen.async(Crypto, self.cb, this._cryptoId); - let [privkey, pubkey] = yield; - - this._cryptoId.privkey = privkey; - this._cryptoId.pubkey = pubkey; - - this._dav.MKCOL("private/", self.cb); - ret = yield; - if (!ret) - throw "Could not create private key directory"; - - this._dav.MKCOL("public/", self.cb); - ret = yield; - if (!ret) - throw "Could not create public key directory"; - - this._dav.PUT("private/privkey", privkey, self.cb); - ret = yield; - Utils.ensureStatus(ret.status, "Could not upload private key"); - - this._dav.PUT("public/pubkey", pubkey, self.cb); - ret = yield; - Utils.ensureStatus(ret.status, "Could not upload public key"); } this._passphrase = null; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 990ecbb225fb..d0a1276ea7fd 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -143,8 +143,11 @@ let Utils = { } } - let log = Log4Moz.Service.getLogger("Service.Util"); - log.error(msg + " Error code: " + code); + if (msg) { + let log = Log4Moz.Service.getLogger("Service.Util"); + log.error(msg + " Error code: " + code); + } + return false; }, From be7f708507792c82b4e42e15b94fd3131465e3d6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Mar 2008 14:59:20 -0700 Subject: [PATCH 0159/1860] bookmarks syncCore: remove actions never qualify for 'likeness' --- services/sync/modules/syncCores.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index fe29795ec9ed..bdea87e28a2b 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -359,14 +359,18 @@ BookmarksSyncCore.prototype = { // Items with the same GUID do not qualify for 'likeness' because // we already consider them to be the same object, and therefore // we need to process any edits. + // Remove commands don't qualify for likeness either, since two + // remove commands for different GUIDs are guaranteed to refer to + // two different items // The parent GUID check works because reconcile() fixes up the // parent GUIDs as it runs, and the command list is sorted by // depth if (!a || !b || - a.action != b.action || - a.data.type != b.data.type || - a.data.parentGUID != b.data.parentGUID || - a.GUID == b.GUID) + a.action != b.action || + a.action == "remove" || + a.data.type != b.data.type || + a.data.parentGUID != b.data.parentGUID || + a.GUID == b.GUID) return false; // Bookmarks are allowed to be in a different index as long as From f2c758cc1846efc679f5e8185c430c3ebfb36a80 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Mar 2008 14:59:59 -0700 Subject: [PATCH 0160/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 5e13b6263d6d..c99b46f10ef4 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.22"; +const WEAVE_VERSION = "0.1.23"; const STORAGE_FORMAT_VERSION = 0; const ENGINE_STORAGE_FORMAT_VERSION = 2; From 53491b6c5cf87895cba62a49d17c46dd580facc5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Mar 2008 15:56:41 -0700 Subject: [PATCH 0161/1860] fix resetClient's locking. change bookmarks wrap format to have hardcoded guids for the 3 roots (menu, toolbar, unfiled) rather than random ones. --- services/sync/modules/service.js | 12 +- services/sync/modules/stores.js | 216 ++++++++++++++++------------- services/sync/modules/syncCores.js | 2 +- 3 files changed, 127 insertions(+), 103 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 061a6be9454e..946cfdb4d508 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -390,7 +390,7 @@ WeaveSyncService.prototype = { try { if (this._dav.locked) - throw "Could not login: could not acquire lock"; + throw "Login failed: could not acquire lock"; this._dav.allowLock = false; // cache password & passphrase @@ -575,10 +575,9 @@ WeaveSyncService.prototype = { _resetClient: function WeaveSync__resetClient() { let self = yield; - this._lock.async(this, self.cb) - let locked = yield; - if (!locked) - return; + if (this._dav.locked) + throw "Reset client data failed: could not acquire lock"; + this._dav.allowLock = false; try { this._bmkEngine.resetClient(self.cb); @@ -588,8 +587,7 @@ WeaveSyncService.prototype = { throw e; } finally { - this._unlock.async(this, self.cb) - yield; + this._dav.allowLock = true; } self.done(); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index ca77ce30a7a7..ff8800332cf6 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -308,86 +308,23 @@ BookmarksStore.prototype = { return this.__ans; }, - _getFolderNodes: function BSS__getFolderNodes(folder) { - let query = this._hsvc.getNewQuery(); - query.setFolders([folder], 1); - return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; - }, - - _wrapNode: function BSS__wrapNode(node) { - var items = {}; - this._wrapNodeInternal(node, items, null, null); - return items; - }, - - _wrapNodeInternal: function BSS__wrapNodeInternal(node, items, parentGUID, index) { - let GUID = this._bms.getItemGUID(node.itemId); - let item = {parentGUID: parentGUID, - index: index}; - - if (node.type == node.RESULT_TYPE_FOLDER) { - if (this._ls.isLivemark(node.itemId)) { - item.type = "livemark"; - let siteURI = this._ls.getSiteURI(node.itemId); - let feedURI = this._ls.getFeedURI(node.itemId); - item.siteURI = siteURI? siteURI.spec : ""; - item.feedURI = feedURI? feedURI.spec : ""; - } else { - item.type = "folder"; - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._wrapNodeInternal(node.getChild(i), items, GUID, i); - } - } - item.title = node.title; - } else if (node.type == node.RESULT_TYPE_URI || - node.type == node.RESULT_TYPE_QUERY) { - if (this._ms.hasMicrosummary(node.itemId)) { - item.type = "microsummary"; - let micsum = this._ms.getMicrosummary(node.itemId); - item.generatorURI = micsum.generator.uri.spec; // breaks local generators - } else if (node.type == node.RESULT_TYPE_QUERY) { - item.type = "query"; - item.title = node.title; - } else { - item.type = "bookmark"; - item.title = node.title; - } - item.URI = node.uri; - item.tags = this._ts.getTagsForURI(Utils.makeURI(node.uri), {}); - item.keyword = this._bms.getKeywordForBookmark(node.itemId); - } else if (node.type == node.RESULT_TYPE_SEPARATOR) { - item.type = "separator"; - } else { - this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); - return; - } - - items[GUID] = item; - }, - - _getWrappedBookmarks: function BSS__getWrappedBookmarks(folder) { - return this._wrapNode(this._getFolderNodes(folder)); - }, - - _resetGUIDsInt: function BSS__resetGUIDsInt(node) { - if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) - this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); - - if (node.type == node.RESULT_TYPE_FOLDER && - !this._ls.isLivemark(node.itemId)) { - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._resetGUIDsInt(node.getChild(i)); - } + _getItemIdForGUID: function BStore__getItemIdForGUID(GUID) { + switch (GUID) { + case "menu": + return this._bms.bookmarksMenuFolder; + case "toolbar": + return this._bms.toolbarFolder; + case "unfiled": + return this._bms.unfiledBookmarksFolder; + default: + return this._bms.getItemIdForGUID(GUID); } + return null; }, _createCommand: function BStore__createCommand(command) { let newId; - let parentId = this._bms.getItemIdForGUID(command.data.parentGUID); + let parentId = this._getItemIdForGUID(command.data.parentGUID); if (parentId < 0) { this._log.warn("Creating node with unknown parent -> reparenting to root"); @@ -445,6 +382,14 @@ BookmarksStore.prototype = { }, _removeCommand: function BStore__removeCommand(command) { + if (command.GUID == "menu" || + command.GUID == "toolbar" || + command.GUID == "unfiled") { + this._log.warn("Attempted to remove root node (" + command.GUID + + "). Skipping command."); + return; + } + var itemId = this._bms.getItemIdForGUID(command.GUID); if (itemId < 0) { this._log.warn("Attempted to remove item " + command.GUID + @@ -473,6 +418,14 @@ BookmarksStore.prototype = { }, _editCommand: function BStore__editCommand(command) { + if (command.GUID == "menu" || + command.GUID == "toolbar" || + command.GUID == "unfiled") { + this._log.warn("Attempted to edit root node (" + command.GUID + + "). Skipping command."); + return; + } + var itemId = this._bms.getItemIdForGUID(command.GUID); if (itemId < 0) { this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); @@ -482,7 +435,7 @@ BookmarksStore.prototype = { for (let key in command.data) { switch (key) { case "GUID": - var existing = this._bms.getItemIdForGUID(command.data.GUID); + var existing = this._getItemIdForGUID(command.data.GUID); if (existing < 0) this._bms.setItemGUID(itemId, command.data.GUID); else @@ -504,7 +457,7 @@ BookmarksStore.prototype = { if (command.data.index && command.data.index >= 0) index = command.data.index; this._bms.moveItem( - itemId, this._bms.getItemIdForGUID(command.data.parentGUID), index); + itemId, this._getItemIdForGUID(command.data.parentGUID), index); } break; case "tags": { let tagsURI = this._bms.getBookmarkURI(itemId); @@ -533,22 +486,95 @@ BookmarksStore.prototype = { } }, + _getNode: function BSS__getNode(folder) { + let query = this._hsvc.getNewQuery(); + query.setFolders([folder], 1); + return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + }, + + __wrap: function BSS__wrap(node, items, parentGUID, index, guidOverride) { + let GUID, item; + + // we override the guid for the root items, "menu", "toolbar", etc. + if (guidOverride) { + GUID = guidOverride; + item = {}; + } else { + GUID = this._bms.getItemGUID(node.itemId); + item = {parentGUID: parentGUID, index: index}; + } + + if (node.type == node.RESULT_TYPE_FOLDER) { + if (this._ls.isLivemark(node.itemId)) { + item.type = "livemark"; + let siteURI = this._ls.getSiteURI(node.itemId); + let feedURI = this._ls.getFeedURI(node.itemId); + item.siteURI = siteURI? siteURI.spec : ""; + item.feedURI = feedURI? feedURI.spec : ""; + } else { + item.type = "folder"; + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this.__wrap(node.getChild(i), items, GUID, i); + } + } + if (!guidOverride) + item.title = node.title; // no titles for root nodes + + } else if (node.type == node.RESULT_TYPE_URI || + node.type == node.RESULT_TYPE_QUERY) { + if (this._ms.hasMicrosummary(node.itemId)) { + item.type = "microsummary"; + let micsum = this._ms.getMicrosummary(node.itemId); + item.generatorURI = micsum.generator.uri.spec; // breaks local generators + } else if (node.type == node.RESULT_TYPE_QUERY) { + item.type = "query"; + item.title = node.title; + } else { + item.type = "bookmark"; + item.title = node.title; + } + item.URI = node.uri; + item.tags = this._ts.getTagsForURI(Utils.makeURI(node.uri), {}); + item.keyword = this._bms.getKeywordForBookmark(node.itemId); + + } else if (node.type == node.RESULT_TYPE_SEPARATOR) { + item.type = "separator"; + + } else { + this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); + return; + } + + items[GUID] = item; + }, + + // helper + _wrap: function BStore_wrap(node, items, rootName) { + return this.__wrap(node, items, null, null, rootName); + }, + + _resetGUIDs: function BSS__resetGUIDs(node) { + if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) + this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); + + if (node.type == node.RESULT_TYPE_FOLDER && + !this._ls.isLivemark(node.itemId)) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._resetGUIDs(node.getChild(i)); + } + } + }, + wrap: function BStore_wrap() { - let filed = this._getWrappedBookmarks(this._bms.bookmarksMenuFolder); - let toolbar = this._getWrappedBookmarks(this._bms.toolbarFolder); - let unfiled = this._getWrappedBookmarks(this._bms.unfiledBookmarksFolder); - - for (let guid in unfiled) { - if (!(guid in filed)) - filed[guid] = unfiled[guid]; - } - - for (let guid in toolbar) { - if (!(guid in filed)) - filed[guid] = toolbar[guid]; - } - - return filed; // (combined) + var items = {}; + this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu"); + this._wrap(this._getNode(this._bms.toolbarFolder), items, "toolbar"); + this._wrap(this._getNode(this._bms.unfiledBookmarksFolder), items, "unfiled"); + return items; }, wipe: function BStore_wipe() { @@ -558,9 +584,9 @@ BookmarksStore.prototype = { }, resetGUIDs: function BStore_resetGUIDs() { - this._resetGUIDsInt(this._getFolderNodes(this._bms.bookmarksMenuFolder)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.toolbarFolder)); - this._resetGUIDsInt(this._getFolderNodes(this._bms.unfiledBookmarksFolder)); + this._resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder)); + this._resetGUIDs(this._getNode(this._bms.toolbarFolder)); + this._resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); } }; BookmarksStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index bdea87e28a2b..b09b2bd87dbf 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -256,7 +256,7 @@ SyncCore.prototype = { skip = true; return false; // b, but we add it back from guidChanges } - + // watch out for create commands with GUIDs that already exist if (b.action == "create" && this._itemExists(b.GUID)) { this._log.error("Remote command has GUID that already exists " + From 47159e454c49860088116c8c555bc3d4db99b5b6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 21 Mar 2008 16:07:44 -0700 Subject: [PATCH 0162/1860] bump version; bump storage format version --- services/sync/modules/constants.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index c99b46f10ef4..6e79edf803db 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,8 +42,8 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.23"; -const STORAGE_FORMAT_VERSION = 0; +const WEAVE_VERSION = "0.1.24"; +const STORAGE_FORMAT_VERSION = 1; const ENGINE_STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 241913827b902d727dfb1aaf21e9d60f1d312fd3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 24 Mar 2008 16:04:29 -0700 Subject: [PATCH 0163/1860] decrypt private rsa keys before decrypting data with it (rather than doing it in one step). fix for windows openssl.exe --- services/sync/modules/crypto.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 5aca7bca5d70..6d7e25d1ea4d 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -280,6 +280,10 @@ WeaveCrypto.prototype = { keyFOS.writeString(identity.privkey); keyFOS.close(); + let tmpKeyFile = Utils.getTmp("tmp-key"); + if (tmpKeyFile.exists()) + tmpKeyFile.remove(false); + let tmpFile = Utils.getTmp("tmp-output"); if (tmpFile.exists()) tmpFile.remove(false); @@ -296,14 +300,20 @@ WeaveCrypto.prototype = { try { this._openssl("base64", "-d", "-in", "input", "-out", "tmp-output"); - this._openssl("rsautl", "-decrypt", "-inkey", "key", "-passin", - "file:pass", "-in", "tmp-output", "-out", "output"); + // FIXME: this is because openssl.exe (in windows only) doesn't + // seem to support -passin for rsautl, but it works for rsa. + this._openssl("rsa", "-in", "key", "-out", "tmp-key", "-passin", "file:pass"); + this._openssl("rsautl", "-decrypt", "-inkey", "tmp-key", + "-in", "tmp-output", "-out", "output"); } catch(e) { throw e; } finally { passFile.remove(false); + tmpKeyFile.remove(false); + tmpFile.remove(false); + keyFile.remove(false); } let [outputFIS] = Utils.open(outputFile, "<"); From 91590684f3b3136ad8e311a1ceb04eb55ca1dfd8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 24 Mar 2008 16:05:02 -0700 Subject: [PATCH 0164/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 6e79edf803db..7a63f648981f 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.24"; +const WEAVE_VERSION = "0.1.25"; const STORAGE_FORMAT_VERSION = 1; const ENGINE_STORAGE_FORMAT_VERSION = 2; From 4ea8090aebc61bad48800d4de3067ac9ccb997ba Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 24 Mar 2008 19:08:43 -0700 Subject: [PATCH 0165/1860] work with all exception objects, whether we can modify them or not --- services/sync/modules/async.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index effc3fb03907..f816b1ca0973 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -150,20 +150,10 @@ Generator.prototype = { } else if (this.onComplete.parentGenerator instanceof Generator) { //this._log.trace("Saving exception and stack trace"); - switch (typeof e) { - case "string": + if (e instanceof AsyncException) + e.trace = this.trace + e.trace? "\n" + e.trace : ""; + else e = new AsyncException(this, e); - break; - case "object": - if (e.trace) // means we're re-throwing up the stack - e.trace = this.trace + "\n" + e.trace; - else - e.trace = this.trace; - break; - default: - this._log.debug("Unknown exception type: " + typeof(e)); - break; - } this._exception = e; From 4eb78368cafbc8ea4daa322f28a70f26527cea0f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Mar 2008 13:55:34 -0700 Subject: [PATCH 0166/1860] lazy-load service --- services/sync/modules/log4moz.js | 48 ++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 3142ff24ca29..c16f8e9d0715 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -58,28 +58,36 @@ const ONE_BYTE = 1; const ONE_KILOBYTE = 1024 * ONE_BYTE; const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; -const Log4Moz = {}; -Log4Moz.Level = {}; -Log4Moz.Level.Fatal = 70; -Log4Moz.Level.Error = 60; -Log4Moz.Level.Warn = 50; -Log4Moz.Level.Info = 40; -Log4Moz.Level.Config = 30; -Log4Moz.Level.Debug = 20; -Log4Moz.Level.Trace = 10; -Log4Moz.Level.All = 0; +let Log4Moz = { + Level: { + Fatal: 70, + Error: 60, + Warn: 50, + Info: 40, + Config: 30, + Debug: 20, + Trace: 10, + All: 0, + Desc: { + 70: "FATAL", + 60: "ERROR", + 50: "WARN", + 40: "INFO", + 30: "CONFIG", + 20: "DEBUG", + 10: "TRACE", + 0: "ALL" + } + }, -Log4Moz.Level.Desc = { - 70: "FATAL", - 60: "ERROR", - 50: "WARN", - 40: "INFO", - 30: "CONFIG", - 20: "DEBUG", - 10: "TRACE", - 0: "ALL" + get Service() { + delete Log4Moz.Service; + Log4Moz.Service = new Log4MozService(); + return Log4Moz.Service; + } }; + /* * LogMessage * Encapsulates a single log event's data @@ -495,5 +503,3 @@ Log4MozService.prototype = { } } }; - -Log4Moz.Service = new Log4MozService(); From b970fad02606c4416dd29df9745a80f248f3ab32 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Mar 2008 15:14:00 -0700 Subject: [PATCH 0167/1860] make service.js the main entry point from chrome/content; make the service be lazy-loaded; make crypto be lazy-loaded --- services/sync/modules/crypto.js | 8 ++++-- services/sync/modules/dav.js | 2 ++ services/sync/modules/engines.js | 1 - services/sync/modules/service.js | 21 +++++++++++--- services/sync/modules/util.js | 11 +++++++ services/sync/modules/weave.js | 49 -------------------------------- 6 files changed, 35 insertions(+), 57 deletions(-) delete mode 100644 services/sync/modules/weave.js diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 6d7e25d1ea4d..7d6ffd86d446 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['WeaveCrypto']; +const EXPORTED_SYMBOLS = ['Crypto']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -49,10 +49,12 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; -function WeaveCrypto() { +Utils.lazy(this, 'Crypto', CryptoSvc); + +function CryptoSvc() { this._init(); } -WeaveCrypto.prototype = { +CryptoSvc.prototype = { _logName: "Crypto", __os: null, diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 5b03126488c2..e717b44f9d88 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -49,6 +49,8 @@ Cu.import("resource://weave/constants.js"); Function.prototype.async = Async.sugar; +Utils.lazy(this, 'DAV', DAVCollection); + /* * DAV object * Abstracts the raw DAV commands diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index ac3826526ce9..db549c25ec1b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -52,7 +52,6 @@ Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; -let Crypto = new WeaveCrypto(); function Engine(davCollection, pbeId) { //this._init(davCollection, pbeId); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 946cfdb4d508..634786207fc9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['WeaveSyncService']; +const EXPORTED_SYMBOLS = ['Weave']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -52,15 +52,28 @@ Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; -let Crypto = new WeaveCrypto(); + +// for export +let Weave = {}; +Components.utils.import("resource://weave/constants.js", Weave); +Components.utils.import("resource://weave/util.js", Weave); +Components.utils.import("resource://weave/async.js", Weave); +Components.utils.import("resource://weave/crypto.js", Weave); +Components.utils.import("resource://weave/identity.js", Weave); +Components.utils.import("resource://weave/dav.js", Weave); +Components.utils.import("resource://weave/stores.js", Weave); +Components.utils.import("resource://weave/syncCores.js", Weave); +Components.utils.import("resource://weave/engines.js", Weave); +Components.utils.import("resource://weave/service.js", Weave); +Utils.lazy(Weave, 'Service', WeaveSvc); /* * Service singleton * Main entry point into Weave's sync framework */ -function WeaveSyncService() { this._init(); } -WeaveSyncService.prototype = { +function WeaveSvc() { this._init(); } +WeaveSvc.prototype = { __os: null, get _os() { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index d0a1276ea7fd..96878a404094 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -51,6 +51,17 @@ Cu.import("resource://weave/log4moz.js"); let Utils = { + // lazy load objects from a constructor on first access. It will + // work with the global object ('this' in the global context). + lazy: function Weave_lazy(dest, prop, ctr) { + let getter = function() { + delete dest[prop]; + dest[prop] = new ctr(); + return dest[prop]; + }; + dest.__defineGetter__(prop, getter); + }, + deepEquals: function Weave_deepEquals(a, b) { if (!a && !b) return true; diff --git a/services/sync/modules/weave.js b/services/sync/modules/weave.js deleted file mode 100644 index b6c5080fe72b..000000000000 --- a/services/sync/modules/weave.js +++ /dev/null @@ -1,49 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['Weave']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/service.js"); - -let Weave = {}; -//Weave.Crypto = new WeaveCrypto(); -Weave.Service = new WeaveSyncService(); From 48fe8e8ccab7cd2dfd4af5994b2737cf98c8103e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Mar 2008 23:01:34 -0700 Subject: [PATCH 0168/1860] add sharing ui+backend code (not working yet); make engines less chatty when applying commands --- services/sync/locales/en-US/share.dtd | 3 ++ services/sync/locales/en-US/share.properties | 0 services/sync/locales/en-US/sync.dtd | 1 + services/sync/modules/engines.js | 53 ++++++++++++++++++ services/sync/modules/identity.js | 25 +-------- services/sync/modules/service.js | 29 +++++++++- services/sync/modules/stores.js | 57 ++++++++++---------- services/sync/modules/util.js | 22 ++++++++ 8 files changed, 134 insertions(+), 56 deletions(-) create mode 100644 services/sync/locales/en-US/share.dtd create mode 100644 services/sync/locales/en-US/share.properties diff --git a/services/sync/locales/en-US/share.dtd b/services/sync/locales/en-US/share.dtd new file mode 100644 index 000000000000..d79cd3dcb03f --- /dev/null +++ b/services/sync/locales/en-US/share.dtd @@ -0,0 +1,3 @@ + + + diff --git a/services/sync/locales/en-US/share.properties b/services/sync/locales/en-US/share.properties new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index e911359c4341..c8b3a20fd0f9 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -2,5 +2,6 @@ + diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index db549c25ec1b..5d8183b75b94 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -676,10 +676,63 @@ Engine.prototype = { self.done(ret) }, + _share: function Engine__share(username) { + let self = yield; + + this._getSymKey.async(this, self.cb); + yield; + + let base = this._dav.baseURL; + try { + // copied from getSymKey + this._dav.GET(this.keysFile, self.cb); + let ret = yield; + Utils.ensureStatus(ret.status, + "Could not get keys file.", [[200,300]]); + let keys = this._json.decode(keysResp.responseText); + + // get the other user's pubkey + let hash = Utils.sha1(username); + this._dav.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! + + this._dav.GET("public/pubkey", self.cb); + ret = yield; + Utils.ensureStatus(ret.status, + "Could not get public key for user" + username); + let id = new Identity(); + id.pubkey = ret.responseText; + + this._dav.baseURL = base; + + // now encrypt the symkey with their pubkey and upload the new keyring + Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, id); + let enckey = yield; + if (!enckey) + throw "Could not encrypt symmetric encryption key"; + + keys.ring[hash] = enckey; + this._dav.PUT(this.keysFile, this._json.encode(keys), self.cb); + ret = yield; + Utils.ensureStatus(ret.status, "Could not upload keyring file."); + + } catch (e) { + throw e; + + } finally { + this._dav.baseURL = base; + } + + self.done(); + }, + sync: function Engine_sync(onComplete) { return this._sync.async(this, onComplete); }, + share: function Engine_share(onComplete, username) { + return this._share.async(this, onComplete, username); + }, + resetServer: function Engine_resetServer(onComplete) { return this._resetServer.async(this, onComplete); }, diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index fd6694e3a317..4b7d2014b1d6 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -66,30 +66,7 @@ Identity.prototype = { get username() { return this._username; }, set username(value) { this._username = value; }, - get userHash() { - //this._log.trace("Hashing username " + this.username); - - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA1); - - let data = converter.convertToByteArray(this.username, {}); - hasher.update(data, data.length); - let rawHash = hasher.finish(false); - - // return the two-digit hexadecimal code for a byte - function toHexString(charCode) { - return ("0" + charCode.toString(16)).slice(-2); - } - - let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); - //this._log.trace("Username hashes to " + hash); - return hash; - }, + get userHash() { return Utils.sha1(this.username); }, _privkey: null, get privkey() { return this._privkey; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 634786207fc9..183d0ed6cce9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -565,7 +565,7 @@ WeaveSvc.prototype = { _resetServer: function WeaveSync__resetServer() { let self = yield; - this._lock.async(this, self.cb) + this._lock.async(this, self.cb); let locked = yield; if (!locked) return; @@ -606,6 +606,28 @@ WeaveSvc.prototype = { self.done(); }, + _shareBookmarks: function WeaveSync__shareBookmarks(username) { + let self = yield; + + try { + //this._lock.async(this, self.cb); + //let locked = yield; + //if (!locked) + // return; + + this._bmkEngine.share(self.cb, username); + + } catch (e) { + throw e; + + } finally { + //this._unlock.async(this, self.cb); + //yield; + } + + self.done(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), // nsIObserver @@ -642,5 +664,8 @@ WeaveSvc.prototype = { sync: function WeaveSync_sync() { this._sync.async(this); }, resetServer: function WeaveSync_resetServer() { this._resetServer.async(this); }, - resetClient: function WeaveSync_resetClient() { this._resetClient.async(this); } + resetClient: function WeaveSync_resetClient() { this._resetClient.async(this); }, + shareBookmarks: function WeaveSync_shareBookmarks(username) { + this._shareBookmarks.async(this, null, username); + } }; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index ff8800332cf6..e1b6078cb470 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -335,7 +335,7 @@ BookmarksStore.prototype = { case "query": case "bookmark": case "microsummary": { - this._log.info(" -> creating bookmark \"" + command.data.title + "\""); + this._log.debug(" -> creating bookmark \"" + command.data.title + "\""); let URI = Utils.makeURI(command.data.URI); newId = this._bms.insertBookmark(parentId, URI, @@ -346,7 +346,7 @@ BookmarksStore.prototype = { this._bms.setKeywordForBookmark(newId, command.data.keyword); if (command.data.type == "microsummary") { - this._log.info(" \-> is a microsummary"); + this._log.debug(" \-> is a microsummary"); let genURI = Utils.makeURI(command.data.generatorURI); try { let micsum = this._ms.createMicrosummary(URI, genURI); @@ -356,13 +356,13 @@ BookmarksStore.prototype = { } } break; case "folder": - this._log.info(" -> creating folder \"" + command.data.title + "\""); + this._log.debug(" -> creating folder \"" + command.data.title + "\""); newId = this._bms.createFolder(parentId, command.data.title, command.data.index); break; case "livemark": - this._log.info(" -> creating livemark \"" + command.data.title + "\""); + this._log.debug(" -> creating livemark \"" + command.data.title + "\""); newId = this._ls.createLivemark(parentId, command.data.title, Utils.makeURI(command.data.siteURI), @@ -370,7 +370,7 @@ BookmarksStore.prototype = { command.data.index); break; case "separator": - this._log.info(" -> creating separator"); + this._log.debug(" -> creating separator"); newId = this._bms.insertSeparator(parentId, command.data.index); break; default: @@ -400,15 +400,15 @@ BookmarksStore.prototype = { switch (type) { case this._bms.TYPE_BOOKMARK: - this._log.info(" -> removing bookmark " + command.GUID); + this._log.debug(" -> removing bookmark " + command.GUID); this._bms.removeItem(itemId); break; case this._bms.TYPE_FOLDER: - this._log.info(" -> removing folder " + command.GUID); + this._log.debug(" -> removing folder " + command.GUID); this._bms.removeFolder(itemId); break; case this._bms.TYPE_SEPARATOR: - this._log.info(" -> removing separator " + command.GUID); + this._log.debug(" -> removing separator " + command.GUID); this._bms.removeItem(itemId); break; default: @@ -599,40 +599,37 @@ HistoryStore.prototype = { __hsvc: null, get _hsvc() { - if (!this.__hsvc) + if (!this.__hsvc) { this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. getService(Ci.nsINavHistoryService); + this.__hsvc.QueryInterface(Ci.nsIGlobalHistory2); + this.__hsvc.QueryInterface(Ci.nsIBrowserHistory); + } return this.__hsvc; }, - __browserHist: null, - get _browserHist() { - if (!this.__browserHist) - this.__browserHist = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsIBrowserHistory); - return this.__browserHist; - }, - _createCommand: function HistStore__createCommand(command) { - this._log.info(" -> creating history entry: " + command.GUID); + this._log.debug(" -> creating history entry: " + command.GUID); try { - this._browserHist.addPageWithDetails(Utils.makeURI(command.GUID), - command.data.title, - command.data.time); - this._hsvc.setPageDetails(Utils.makeURI(command.GUID), command.data.title, - command.data.accessCount, false, false); + let uri = Utils.makeURI(command.GUID); + this._hsvc.addVisit(uri, command.data.time, null, + this._hsvc.TRANSITION_TYPED, false, null); + this._hsvc.setPageTitle(uri, command.data.title); } catch (e) { this._log.error("Exception caught: " + (e.message? e.message : e)); } }, _removeCommand: function HistStore__removeCommand(command) { - this._log.info(" -> NOT removing history entry: " + command.GUID); - //this._browserHist.removePage(command.GUID); + this._log.debug(" -> NOT removing history entry: " + command.GUID); + // we can't remove because we only sync the last 500 items, not + // the whole store. So we don't know if remove commands were + // generated due to the user removing an entry or because it + // dropped past the 500 item mark. }, _editCommand: function HistStore__editCommand(command) { - this._log.info(" -> FIXME: NOT editing history entry: " + command.GUID); + this._log.debug(" -> FIXME: NOT editing history entry: " + command.GUID); // FIXME: implement! }, @@ -642,6 +639,7 @@ HistoryStore.prototype = { query.minVisits = 1; options.maxResults = 500; + options.resultType = options.RESULTS_AS_FULL_VISIT; options.sortingMode = query.SORT_BY_LASTMODIFIED_DESCENDING; options.queryType = options.QUERY_TYPE_HISTORY; @@ -659,16 +657,15 @@ HistoryStore.prototype = { items[item.uri] = {parentGUID: '', title: item.title, URI: item.uri, - time: item.time, - accessCount: item.accessCount, - dateAdded: item.dateAdded, + time: item.time }; + // FIXME: sync transition type } return items; }, wipe: function HistStore_wipe() { - this._browserHist.removeAllPages(); + this._hsvc.removeAllPages(); } }; HistoryStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 96878a404094..3d19bb723022 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -167,6 +167,28 @@ let Utils = { throw 'checkStatus failed'; }, + sha1: function Weave_sha1(string) { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let data = converter.convertToByteArray(string, {}); + hasher.update(data, data.length); + let rawHash = hasher.finish(false); + + // return the two-digit hexadecimal code for a byte + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); + return hash; + }, + makeURI: function Weave_makeURI(URIString) { if (URIString === null || URIString == "") return null; From c9fa0d0425b9974766a0f06e92e3722e7ec3a822 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Mar 2008 00:59:34 -0700 Subject: [PATCH 0169/1860] improve async generator logging; don't call done() on StopIteration when we already have a timer set (it means the generator just 'bottomed out' after calling done()); make XHRs be synchronous (blocking) - temporarily; fix up sharing code (adding to the keyring) --- services/sync/modules/async.js | 15 +++++++++++---- services/sync/modules/dav.js | 12 +++++++----- services/sync/modules/engines.js | 23 ++++++++++++++++------- services/sync/modules/service.js | 15 ++++++++------- 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index f816b1ca0973..069dc670fc1d 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -141,14 +141,17 @@ Generator.prototype = { _handleException: function AsyncGen__handleException(e) { if (e instanceof StopIteration) { + if (!this._timer) + this._log.trace("[" + this.name + "] Generator stopped unexpectedly"); if (this.errorOnStop) { - this._log.error("Generator stopped unexpectedly"); + this._log.error("[" + this.name + "] Generator stopped unexpectedly"); this._log.trace("Stack trace:\n" + this.trace); this._exception = "Generator stopped unexpectedly"; // don't propagate StopIteration } } else if (this.onComplete.parentGenerator instanceof Generator) { - //this._log.trace("Saving exception and stack trace"); + this._log.trace("[" + this.name + "] Saving exception and stack trace"); + this._log.trace(Async.exceptionStr(this, e)); if (e instanceof AsyncException) e.trace = this.trace + e.trace? "\n" + e.trace : ""; @@ -167,7 +170,10 @@ Generator.prototype = { // in the case of StopIteration we could return an error right // away, but instead it's easiest/best to let the caller handle // the error after a yield / in a callback. - this.done(); + if (!this._timer) { + this._log.trace("[" + this.name + "] running done() from _handleException()"); + this.done(); + } }, run: function AsyncGen_run() { @@ -218,10 +224,11 @@ Generator.prototype = { this._timer = null; if (this._exception) { - this._log.trace("Propagating exception to parent generator"); + this._log.trace("[" + this.name + "] Propagating exception to parent generator"); this.onComplete.parentGenerator.throw(this._exception); } else { try { + this._log.trace("[" + this.name + "] Running onComplete()"); this.onComplete(retval); } catch (e) { this._log.error("Exception caught from onComplete handler of " + diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index e717b44f9d88..c05a64d85c3a 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -108,10 +108,11 @@ DAVCollection.prototype = { let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); - request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); - request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); +// request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); +// request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); - request.open(op, this._baseURL + path, true); +// request.open(op, this._baseURL + path, true); + request.open(op, this._baseURL + path, false); // FIXME: blocking // Force cache validation @@ -134,8 +135,9 @@ DAVCollection.prototype = { request.channel.notificationCallbacks = this._authProvider; request.send(data); - let event = yield; - ret = event.target; + ret = request; +// let event = yield; +// ret = event.target; if (this._authProvider._authFailed) this._log.warn("_makeRequest: authentication failed"); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5d8183b75b94..30fd062cb28c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -678,23 +678,28 @@ Engine.prototype = { _share: function Engine__share(username) { let self = yield; - - this._getSymKey.async(this, self.cb); - yield; - let base = this._dav.baseURL; + + this._log.debug("Sharing bookmarks with " + username); + try { + this._getSymKey.async(this, self.cb); + yield; + // copied from getSymKey + this._log.debug("Getting keyring from server"); this._dav.GET(this.keysFile, self.cb); let ret = yield; - Utils.ensureStatus(ret.status, - "Could not get keys file.", [[200,300]]); - let keys = this._json.decode(keysResp.responseText); + Utils.ensureStatus(ret.status, "Could not get keys file."); + let keys = this._json.decode(ret.responseText); // get the other user's pubkey + this._log.debug("Constructing other user's URL"); let hash = Utils.sha1(username); + let serverURL = Utils.prefs.getCharPref("serverURL"); this._dav.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! + this._log.debug("Getting other user's public key"); this._dav.GET("public/pubkey", self.cb); ret = yield; Utils.ensureStatus(ret.status, @@ -705,16 +710,20 @@ Engine.prototype = { this._dav.baseURL = base; // now encrypt the symkey with their pubkey and upload the new keyring + this._log.debug("Encrypting symmetric key with other user's public key"); Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, id); let enckey = yield; if (!enckey) throw "Could not encrypt symmetric encryption key"; + this._log.debug("Uploading new keyring"); keys.ring[hash] = enckey; this._dav.PUT(this.keysFile, this._json.encode(keys), self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload keyring file."); + this._log.debug("All done sharing!"); + } catch (e) { throw e; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 183d0ed6cce9..fc0dc539adf6 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -610,19 +610,20 @@ WeaveSvc.prototype = { let self = yield; try { - //this._lock.async(this, self.cb); - //let locked = yield; - //if (!locked) - // return; + this._lock.async(this, self.cb); + let locked = yield; + if (!locked) + return; this._bmkEngine.share(self.cb, username); + yield; } catch (e) { throw e; } finally { - //this._unlock.async(this, self.cb); - //yield; + this._unlock.async(this, self.cb); + yield; } self.done(); @@ -666,6 +667,6 @@ WeaveSvc.prototype = { resetServer: function WeaveSync_resetServer() { this._resetServer.async(this); }, resetClient: function WeaveSync_resetClient() { this._resetClient.async(this); }, shareBookmarks: function WeaveSync_shareBookmarks(username) { - this._shareBookmarks.async(this, null, username); + this._shareBookmarks.async(this, function() {}, username); } }; From 6cb8b40f9872511058420871f90ec2d1ca01706b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Mar 2008 23:11:15 -0700 Subject: [PATCH 0170/1860] make applyCommands asynchronous --- services/sync/modules/engines.js | 9 ++++++--- services/sync/modules/stores.js | 11 +++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 30fd062cb28c..a800bd8369a5 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -337,10 +337,12 @@ Engine.prototype = { // Note that we need to need to apply client changes to the // current tree, not the saved snapshot - localJson.applyCommands(clientChanges); + localJson.applyCommands.async(localJson, self.cb, clientChanges); + yield; this._snapshot.data = localJson.data; this._snapshot.version = server.maxVersion; - this._store.applyCommands(clientChanges); + this._store.applyCommands.async(this._store, self.cb, clientChanges); + yield; newSnapshot = this._store.wrap(); this._core.detectUpdates(self.cb, this._snapshot.data, newSnapshot); @@ -570,7 +572,8 @@ Engine.prototype = { } for (var i = 0; i < deltas.length; i++) { - snap.applyCommands(deltas[i]); + snap.applyCommands.async(snap, self.cb, deltas[i]); + yield; } ret.status = 0; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index e1b6078cb470..1c264b5ed27b 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -57,6 +57,7 @@ function Store() { } Store.prototype = { _logName: "Store", + _yieldDuringApply: true, __json: null, get _json() { @@ -71,7 +72,16 @@ Store.prototype = { }, applyCommands: function Store_applyCommands(commandList) { + let self = yield; + + if (this._yieldDuringApply) + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + for (var i = 0; i < commandList.length; i++) { + if (this._yieldDuringApply) { + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + } var command = commandList[i]; this._log.debug("Processing command: " + this._json.encode(command)); switch (command["action"]) { @@ -89,6 +99,7 @@ Store.prototype = { break; } } + self.done(); }, // override these in derived objects From e0ffc183b0a6f23c9aeb790a5a75de34fcc8f804 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Mar 2008 23:12:58 -0700 Subject: [PATCH 0171/1860] fix typo --- services/sync/modules/stores.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 1c264b5ed27b..6c54d1c44ed0 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -72,10 +72,10 @@ Store.prototype = { }, applyCommands: function Store_applyCommands(commandList) { - let self = yield; + let self = yield, timer; if (this._yieldDuringApply) - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); for (var i = 0; i < commandList.length; i++) { if (this._yieldDuringApply) { From bbfdced738e8789d8e78eb03f6f92349b3d8076e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Mar 2008 23:22:35 -0700 Subject: [PATCH 0172/1860] make XHRs non-blocking again; change sharing dialog so sharing is done without closing the dialog (still lacks any feedback though) --- services/sync/locales/en-US/share.dtd | 10 +++++++--- services/sync/modules/dav.js | 12 +++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/services/sync/locales/en-US/share.dtd b/services/sync/locales/en-US/share.dtd index d79cd3dcb03f..703fa82075d8 100644 --- a/services/sync/locales/en-US/share.dtd +++ b/services/sync/locales/en-US/share.dtd @@ -1,3 +1,7 @@ - - - + + + + + + + diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index c05a64d85c3a..e717b44f9d88 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -108,11 +108,10 @@ DAVCollection.prototype = { let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); -// request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); -// request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); + request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); + request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); -// request.open(op, this._baseURL + path, true); - request.open(op, this._baseURL + path, false); // FIXME: blocking + request.open(op, this._baseURL + path, true); // Force cache validation @@ -135,9 +134,8 @@ DAVCollection.prototype = { request.channel.notificationCallbacks = this._authProvider; request.send(data); - ret = request; -// let event = yield; -// ret = event.target; + let event = yield; + ret = event.target; if (this._authProvider._authFailed) this._log.warn("_makeRequest: authentication failed"); From c0ab5bc51b437e8f2947e9e66c25934d6d01944a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Mar 2008 23:51:01 -0700 Subject: [PATCH 0173/1860] remove old '500 error' hack for services.m.c during mkcol; fix applyCommands to do async right & make a listener correctly --- services/sync/modules/dav.js | 2 +- services/sync/modules/stores.js | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index e717b44f9d88..cf00bafbf3fe 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -172,7 +172,7 @@ DAVCollection.prototype = { // check if it exists first this._makeRequest.async(this, self.cb, "GET", path2 + "/", this._defaultHeaders); let ret = yield; - if (!(ret.status == 404 || ret.status == 500)) { // FIXME: 500 is a services.m.c oddity + if (ret.status != 404) { this._log.trace("Skipping creation of path " + path2 + " (got status " + ret.status + ")"); } else { diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 6c54d1c44ed0..6dc3bc47749a 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -46,6 +46,9 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; /* * Data Stores @@ -72,10 +75,12 @@ Store.prototype = { }, applyCommands: function Store_applyCommands(commandList) { - let self = yield, timer; + let self = yield, timer, listener; - if (this._yieldDuringApply) + if (this._yieldDuringApply) { + listener = new Utils.EventListener(self.cb); timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } for (var i = 0; i < commandList.length; i++) { if (this._yieldDuringApply) { From 625b0ab0e4a0396b5564f275940b0c40383afee4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 27 Mar 2008 00:36:50 -0700 Subject: [PATCH 0174/1860] tweak logging defaults; add a hidden pref to make the 'sync now' menu option visible again --- services/sync/services-sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 7dd634c3b028..28a12730a502 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -6,6 +6,7 @@ pref("extensions.weave.encryption", "aes-256-cbc"); pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.lastsync", "0"); +pref("extensions.weave.ui.syncnow", false); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); @@ -18,7 +19,7 @@ pref("extensions.weave.log.appender.dump", "Error"); pref("extensions.weave.log.appender.briefLog", "Info"); pref("extensions.weave.log.appender.debugLog", "Trace"); -pref("extensions.weave.log.rootLogger", "Debug"); +pref("extensions.weave.log.rootLogger", "Trace"); pref("extensions.weave.log.logger.async", "Debug"); pref("extensions.weave.log.logger.service.crypto", "Debug"); pref("extensions.weave.log.logger.service.dav", "Debug"); From 62a87462ae6add10496f6cd9e0d29b1a9bba6919 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 27 Mar 2008 01:05:21 -0700 Subject: [PATCH 0175/1860] add a 'clear logs' button to the log window --- services/sync/locales/en-US/log.dtd | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/log.dtd b/services/sync/locales/en-US/log.dtd index 992e41ee93aa..f5d44b37727b 100644 --- a/services/sync/locales/en-US/log.dtd +++ b/services/sync/locales/en-US/log.dtd @@ -1,4 +1,5 @@ + From e29cb3269b86176cb9448851c14f1102ca941703 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 27 Mar 2008 19:12:53 -0700 Subject: [PATCH 0176/1860] status label now has service status instead of username; service [un]lock observer notifications are gone; don't error when async methods don't call done() - consider them methods with no return value; don't require passing null into login() to cause the identity to look up the password in the pw mgr; make some wrapper 'method generators' to make lock handling and observer notification simpler --- services/sync/locales/en-US/sync.dtd | 2 + services/sync/locales/en-US/sync.properties | 4 + services/sync/modules/async.js | 22 +- services/sync/modules/identity.js | 2 +- services/sync/modules/service.js | 357 ++++++++------------ services/sync/modules/wrap.js | 159 +++++++++ 6 files changed, 311 insertions(+), 235 deletions(-) create mode 100644 services/sync/modules/wrap.js diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index c8b3a20fd0f9..3c3f89839aba 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -5,3 +5,5 @@ + + diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 34ddf5f5e362..82e3f6850d9b 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -1,2 +1,6 @@ # %S is the date and time at which the last sync successfully completed lastSync.label = Last Update: %S +status.idle = Idle +status.active = Working... +status.offline = Offline +status.error = Error diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 069dc670fc1d..729aa44f964b 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -43,7 +43,6 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/constants.js"); /* * Asynchronous generator helpers @@ -101,7 +100,7 @@ Generator.prototype = { set errorOnStop(value) { this._errorOnStop = value; }, get cb() { - let cb = Utils.bind2(this, function(data) { this.cont(data); }); + let self = this, cb = function(data) { self.cont(data); }; cb.parentGenerator = this; return cb; }, @@ -141,8 +140,6 @@ Generator.prototype = { _handleException: function AsyncGen__handleException(e) { if (e instanceof StopIteration) { - if (!this._timer) - this._log.trace("[" + this.name + "] Generator stopped unexpectedly"); if (this.errorOnStop) { this._log.error("[" + this.name + "] Generator stopped unexpectedly"); this._log.trace("Stack trace:\n" + this.trace); @@ -182,18 +179,25 @@ Generator.prototype = { this.generator.next(); // must initialize before sending this.generator.send(this); } catch (e) { - this._handleException(e); + if (!(e instanceof StopIteration) || !this._timer) + this._handleException(e); } }, cont: function AsyncGen_cont(data) { try { this.generator.send(data); } - catch (e) { this._handleException(e); } + catch (e) { + if (!(e instanceof StopIteration) || !this._timer) + this._handleException(e); + } }, throw: function AsyncGen_throw(exception) { try { this.generator.throw(exception); } - catch (e) { this._handleException(e); } + catch (e) { + if (!(e instanceof StopIteration) || !this._timer) + this._handleException(e); + } }, // async generators can't simply call a callback with the return @@ -250,7 +254,7 @@ Async = { // where fooGen is a generator function, and gen is a Generator instance // ret is whatever the generator 'returns' via Generator.done(). - run: function Async_run(object, method, onComplete, args) { + run: function Async_run(object, method, onComplete /* , arg1, arg2, ... */) { let args = Array.prototype.slice.call(arguments, 3); let gen = new Generator(object, method, onComplete, args); gen.run(); @@ -270,7 +274,7 @@ Async = { // Note that 'this' refers to the method being called, not the // Async object. - sugar: function Async_sugar(object, onComplete, extra_args) { + sugar: function Async_sugar(object, onComplete /* , arg1, arg2, ... */) { let args = Array.prototype.slice.call(arguments, 1); args.unshift(object, this); Async.run.apply(Async, args); diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 4b7d2014b1d6..20d8c9cddf0f 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -79,7 +79,7 @@ Identity.prototype = { _password: null, get password() { - if (this._password === null) + if (!this._password) return findPassword(this.realm, this.username); return this._password; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index fc0dc539adf6..bf718609b5b9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -45,6 +45,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/dav.js"); @@ -72,9 +73,26 @@ Utils.lazy(Weave, 'Service', WeaveSvc); * Main entry point into Weave's sync framework */ -function WeaveSvc() { this._init(); } +function WeaveSvc() { + this._initLogs(); + this._log.info("Weave Sync Service Initializing"); + + Utils.prefs.addObserver("", this, false); + + if (!this.enabled) { + this._log.info("Weave Sync disabled"); + return; + } + + this._setSchedule(this.schedule); +} WeaveSvc.prototype = { + _notify: Wrap.notify, + _lock: Wrap.lock, + _localLock: Wrap.localLock, + _osPrefix: "weave:service:", + __os: null, get _os() { if (!this.__os) @@ -173,20 +191,6 @@ WeaveSvc.prototype = { return Utils.prefs.getIntPref("schedule"); }, - _init: function WeaveSync__init() { - this._initLogs(); - this._log.info("Weave Sync Service Initializing"); - - Utils.prefs.addObserver("", this, false); - - if (!this.enabled) { - this._log.info("Weave Sync disabled"); - return; - } - - this._setSchedule(this.schedule); - }, - _setSchedule: function Weave__setSchedule(schedule) { switch (this.schedule) { case 0: @@ -267,42 +271,6 @@ WeaveSvc.prototype = { root.addAppender(vapp); }, - _lock: function weaveSync__lock() { - let self = yield; - - this._dav.lock.async(this._dav, self.cb); - let locked = yield; - - if (!locked) { - this._log.warn("Service lock failed: already locked"); - this._os.notifyObservers(null, "weave:service-lock:error", ""); - self.done(false); - return; - } - - this._log.debug("Service lock acquired"); - this._os.notifyObservers(null, "weave:service-lock:success", ""); - self.done(true); - }, - - _unlock: function WeaveSync__unlock() { - let self = yield; - - this._dav.unlock.async(this._dav, self.cb); - let unlocked = yield; - - if (!unlocked) { - this._log.error("Service unlock failed"); - this._os.notifyObservers(null, "weave:service-unlock:error", ""); - self.done(false); - return; - } - - this._log.debug("Service lock released"); - this._os.notifyObservers(null, "weave:service-unlock:success", ""); - self.done(true); - }, - _createUserDir: function WeaveSync__createUserDir(serverURL) { let self = yield; @@ -320,8 +288,6 @@ WeaveSvc.prototype = { let success = yield; if (!success) throw "Created user directory, but login still failed. Aborting."; - - self.done(); }, _uploadVersion: function WeaveSync__uploadVersion() { @@ -335,8 +301,6 @@ WeaveSvc.prototype = { this._dav.PUT("meta/version", STORAGE_FORMAT_VERSION, self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload server version file"); - - self.done(); }, // force a server wipe when the version is lower than ours (or there is none) @@ -363,8 +327,6 @@ WeaveSvc.prototype = { } else if (ret.responseText > STORAGE_FORMAT_VERSION) { // FIXME: should we do something here? } - - self.done(); }, _generateKeys: function WeaveSync__generateKeys() { @@ -394,8 +356,6 @@ WeaveSvc.prototype = { this._dav.PUT("public/pubkey", pubkey, self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload public key"); - - self.done(); }, _login: function WeaveSync__login(password, passphrase) { @@ -462,171 +422,26 @@ WeaveSvc.prototype = { } }, - // NOTE: doesn't lock because it's called from _login() which already holds the lock _serverWipe: function WeaveSync__serverWipe() { let self = yield; - this._dav.listFiles.async(this._dav, self.cb); - let names = yield; + let cb = function Weave_serverWipe() { + let innerSelf = yield; - for (let i = 0; i < names.length; i++) { - this._dav.DELETE(names[i], self.cb); - let resp = yield; - this._log.debug(resp.status); - } + this._dav.listFiles.async(this._dav, self.cb); + let names = yield; - self.done(); - }, - - // NOTE: can't lock because it assumes the lock is being held ;) - _resetLock: function WeaveSync__resetLock() { - let self = yield; - let success = false; - - try { - this._log.debug("Resetting server lock"); - this._os.notifyObservers(null, "weave:server-lock-reset:start", ""); - - this._dav.forceUnlock.async(this._dav, self.cb); - success = yield; - - } catch (e) { - throw e; - - } finally { - if (success) { - this._log.debug("Server lock reset successful"); - this._os.notifyObservers(null, "weave:server-lock-reset:success", ""); - } else { - this._log.debug("Server lock reset failed"); - this._os.notifyObservers(null, "weave:server-lock-reset:error", ""); + for (let i = 0; i < names.length; i++) { + this._dav.DELETE(names[i], self.cb); + let resp = yield; + this._log.debug(resp.status); } - self.done(success); - } - }, + }; - _syncEngine: function WeaveSync__syncEngine(eng) { - let self = yield; - - this._os.notifyObservers(null, "weave:" + eng.name + ":sync:start", ""); - - let ret; - try { - eng.sync(self.cb); - ret = yield; - } catch (e) { - this._log.warn("Engine failed with error: " + Utils.exceptionStr(e)); - if (e.trace) - this._log.debug("Engine stack trace: " + Utils.stackTrace(e.trace)); - } - - if (ret) - this._os.notifyObservers(null, "weave:" + eng.name + ":sync:success", ""); - else - this._os.notifyObservers(null, "weave:" + eng.name + ":sync:error", ""); - - self.done(); - }, - - _sync: function WeaveSync__sync() { - let self = yield; - - try { - this._lock.async(this, self.cb) - let locked = yield; - if (!locked) - return; - - this._os.notifyObservers(null, "weave:service:sync:start", ""); - - if (Utils.prefs.getBoolPref("bookmarks")) { - this._syncEngine.async(this, self.cb, this._bmkEngine); - yield; - } - if (Utils.prefs.getBoolPref("history")) { - this._syncEngine.async(this, self.cb, this._histEngine); - yield; - } - - this._unlock.async(this, self.cb) - yield; - this._os.notifyObservers(null, "weave:service:sync:success", ""); - - } catch (e) { - this._unlock.async(this, self.cb) - yield; - this._os.notifyObservers(null, "weave:service:sync:error", ""); - throw e; - } - - self.done(); - }, - - _resetServer: function WeaveSync__resetServer() { - let self = yield; - - this._lock.async(this, self.cb); - let locked = yield; - if (!locked) - return; - - try { - this._bmkEngine.resetServer(self.cb); - this._histEngine.resetServer(self.cb); - - } catch (e) { - throw e; - - } finally { - this._unlock.async(this, self.cb) - yield; - } - - self.done(); - }, - - _resetClient: function WeaveSync__resetClient() { - let self = yield; - - if (this._dav.locked) - throw "Reset client data failed: could not acquire lock"; - this._dav.allowLock = false; - - try { - this._bmkEngine.resetClient(self.cb); - this._histEngine.resetClient(self.cb); - - } catch (e) { - throw e; - - } finally { - this._dav.allowLock = true; - } - - self.done(); - }, - - _shareBookmarks: function WeaveSync__shareBookmarks(username) { - let self = yield; - - try { - this._lock.async(this, self.cb); - let locked = yield; - if (!locked) - return; - - this._bmkEngine.share(self.cb, username); - yield; - - } catch (e) { - throw e; - - } finally { - this._unlock.async(this, self.cb); - yield; - } - - self.done(); + // NOTE: doesn't lock because it's called from _login() which + // already holds the lock + this._notify("server-wipe", cb).async(this, self.cb); + yield; }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), @@ -647,8 +462,8 @@ WeaveSvc.prototype = { // These are global (for all engines) - login: function WeaveSync_login(password, passphrase) { - this._login.async(this, null, password, passphrase); + login: function WeaveSync_login(onComplete, password, passphrase) { + this._login.async(this, onComplete, password, passphrase); }, logout: function WeaveSync_logout() { @@ -659,14 +474,106 @@ WeaveSvc.prototype = { this._os.notifyObservers(null, "weave:service-logout:success", ""); }, - resetLock: function WeaveSync_resetLock() { this._resetLock.async(this); }, + resetLock: function WeaveSync_resetLock(onComplete) { + this._resetLock.async(this, onComplete); + }, + _resetLock: function WeaveSync__resetLock() { + let self = yield; + + let cb = function Weave_resetLock() { + let innerSelf = yield; + this._dav.forceUnlock.async(this._dav, innerSelf.cb); + yield; + }; + + this._notify("reset-server-lock", cb).async(this, self.cb); + yield; + }, + // These are per-engine - sync: function WeaveSync_sync() { this._sync.async(this); }, - resetServer: function WeaveSync_resetServer() { this._resetServer.async(this); }, - resetClient: function WeaveSync_resetClient() { this._resetClient.async(this); }, - shareBookmarks: function WeaveSync_shareBookmarks(username) { - this._shareBookmarks.async(this, function() {}, username); + sync: function WeaveSync_sync(onComplete) { + this._sync.async(this, onComplete); + }, + _sync: function WeaveSync__sync() { + let self = yield; + + let cb = function Weave_sync() { + let innerSelf = yield; + + let engineCb = function WeaveSvc_syncEngine(engine) { + let innerInnerSelf = yield; + engine.sync(innerInnerSelf.cb); + yield; + }; + + if (Utils.prefs.getBoolPref("bookmarks")) { + this._notify(this._bmkEngine.name + ":sync", + engineCb, this._bmkEngine).async(this, innerSelf.cb); + yield; + } + if (Utils.prefs.getBoolPref("history")) { + this._notify(this._histEngine.name + ":sync", + engineCb, this._histEngine).async(this, innerSelf.cb); + yield; + } + }; + + this._lock(this._notify("sync", cb)).async(this, self.cb); + yield; + }, + + resetServer: function WeaveSync_resetServer(onComplete) { + this._resetServer.async(this, onComplete); + }, + _resetServer: function WeaveSync__resetServer() { + let self = yield; + + let cb = function Weave_resetServer() { + let innerSelf = yield; + this._bmkEngine.resetServer(innerSelf.cb); + yield; + this._histEngine.resetServer(innerSelf.cb); + yield; + }; + + this._lock(this._notify("reset-server",cb)).async(this, self.cb); + yield; + }, + + resetClient: function WeaveSync_resetClient(onComplete) { + this._resetClient.async(this, onComplete); + }, + _resetClient: function WeaveSync__resetClient() { + let self = yield; + + let cb = function Weave_resetClient() { + let innerSelf = yield; + this._bmkEngine.resetClient(innerSelf.cb); + yield; + this._histEngine.resetClient(innerSelf.cb); + yield; + }; + + this._localLock(this._notify("reset-client", cb)).async(this, self.cb); + yield; + }, + + shareBookmarks: function WeaveSync_shareBookmarks(onComplete, username) { + this._shareBookmarks.async(this, onComplete, username); + }, + _shareBookmarks: function WeaveSync__shareBookmarks(username) { + let self = yield; + + let cb = function Weave_shareBookmarks() { + let innerSelf = yield; + this._bmkEngine.share(innerSelf.cb, username); + yield; + }; + + this._lock(this._notify("share-bookmarks", cb)).async(this, self.cb); + yield; } + }; diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js new file mode 100644 index 000000000000..f1ffa3a30c2c --- /dev/null +++ b/services/sync/modules/wrap.js @@ -0,0 +1,159 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Wrap']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; + +/* + * Wrapper utility functions + * + * Not in util.js because that would cause a circular dependency + * between util.js and async.js (we include async.js so that our + * returned generator functions have the .async sugar defined) + */ + +let Wrap = { + + // NOTE: requires _osPrefix string property in your object + // NOTE2: copy this function over to your objects, use like this: + // + // MyObj.prototype = { + // _notify: Wrap.notify, + // ... + // method: function MyMethod() { + // let self = yield; + // ... + // // doFoo is assumed to be an asynchronous method + // this._notify("foo", this._doFoo, arg1, arg2).async(this, self.cb); + // let ret = yield; + // ... + // } + // }; + notify: function Weave_notify(name, method /* , arg1, arg2, ... */) { + let savedName = name; + let savedMethod = method; + let args = Array.prototype.slice.call(arguments, 2); + + return function WeaveNotifyWrapper() { + let self = yield; + let ret; + + try { + this._os.notifyObservers(null, this._osPrefix + savedName + ":start", ""); + + args.unshift(this, savedMethod, self.cb); + Async.run.apply(Async, args); + ret = yield; + + this._os.notifyObservers(null, this._osPrefix + savedName + ":success", ""); + + } catch (e) { + this._os.notifyObservers(null, this._osPrefix + savedName + ":error", ""); + throw e; + } + + self.done(ret); + }; + }, + + // NOTE: see notify, this works the same way. they can be + // chained together as well. + lock: function WeaveSync_lock(method /* , arg1, arg2, ... */) { + let savedMethod = method; + let args = Array.prototype.slice.call(arguments, 1); + + return function WeaveLockWrapper() { + let self = yield; + let ret; + + this._dav.lock.async(this._dav, self.cb); + let locked = yield; + if (!locked) + throw "Could not acquire lock"; + + try { + args.unshift(this, savedMethod, self.cb); + Async.run.apply(Async, args); + ret = yield; + + } catch (e) { + throw e; + + } finally { + this._dav.unlock.async(this._dav, self.cb); + yield; + } + + self.done(ret); + }; + }, + + // NOTE: see notify, this works the same way. they can be + // chained together as well. + localLock: function WeaveSync_localLock(method /* , arg1, arg2, ... */) { + let savedMethod = method; + let args = Array.prototype.slice.call(arguments, 1); + + return function WeaveLocalLockWrapper() { + let self = yield; + let ret; + + if (this._dav.locked) + throw "Could not acquire lock"; + this._dav.allowLock = false; + + try { + args.unshift(this, savedMethod, self.cb); + Async.run.apply(Async, args); + ret = yield; + } + catch (e) { throw e; } + finally { this._dav.allowLock = true; } + + self.done(ret); + }; + } +} From 902e60b7df87ab5087fc6c4a6aa96c7bde45eb13 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 28 Mar 2008 03:25:51 -0700 Subject: [PATCH 0177/1860] add status notification to sharing dialog; fix async generators in the case where an async method is missing a yield (better error, continue execution in parent); add guts of demo sharing code to engine & store (for bookmarks, with some spillage) --- services/sync/locales/en-US/share.dtd | 3 +- services/sync/locales/en-US/share.properties | 3 + services/sync/modules/async.js | 7 +- services/sync/modules/engines.js | 190 +++++++++++++++---- services/sync/modules/service.js | 8 +- services/sync/modules/stores.js | 53 +++++- 6 files changed, 217 insertions(+), 47 deletions(-) diff --git a/services/sync/locales/en-US/share.dtd b/services/sync/locales/en-US/share.dtd index 703fa82075d8..233ed463de16 100644 --- a/services/sync/locales/en-US/share.dtd +++ b/services/sync/locales/en-US/share.dtd @@ -2,6 +2,5 @@ - - + diff --git a/services/sync/locales/en-US/share.properties b/services/sync/locales/en-US/share.properties index e69de29bb2d1..2d4f42b76f71 100644 --- a/services/sync/locales/en-US/share.properties +++ b/services/sync/locales/en-US/share.properties @@ -0,0 +1,3 @@ +status.ok = Weave sharing complete! +status.error = Error. Please check the Weave ID and try again. +status.working = Working... diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 729aa44f964b..ca2ff654bacd 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -219,11 +219,12 @@ Generator.prototype = { _done: function AsyncGen__done(retval) { if (!this._generator) { - this._log.error("Generator '" + this.name + "' called 'done' after it's finalized"); + this._log.error("Async method '" + this.name + "' is missing a 'yield' call " + + "(or called done() after being finalized)"); this._log.trace("Initial stack trace:\n" + this.trace); - return; + } else { + this._generator.close(); } - this._generator.close(); this._generator = null; this._timer = null; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a800bd8369a5..5a875db6e1ff 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -685,56 +685,83 @@ Engine.prototype = { this._log.debug("Sharing bookmarks with " + username); + this._getSymKey.async(this, self.cb); + yield; + + // copied from getSymKey + this._dav.GET(this.keysFile, self.cb); + let ret = yield; + Utils.ensureStatus(ret.status, "Could not get keys file."); + let keys = this._json.decode(ret.responseText); + + // get the other user's pubkey + let hash = Utils.sha1(username); + let serverURL = Utils.prefs.getCharPref("serverURL"); + try { - this._getSymKey.async(this, self.cb); - yield; - - // copied from getSymKey - this._log.debug("Getting keyring from server"); - this._dav.GET(this.keysFile, self.cb); - let ret = yield; - Utils.ensureStatus(ret.status, "Could not get keys file."); - let keys = this._json.decode(ret.responseText); - - // get the other user's pubkey - this._log.debug("Constructing other user's URL"); - let hash = Utils.sha1(username); - let serverURL = Utils.prefs.getCharPref("serverURL"); this._dav.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! - - this._log.debug("Getting other user's public key"); this._dav.GET("public/pubkey", self.cb); ret = yield; - Utils.ensureStatus(ret.status, - "Could not get public key for user" + username); - let id = new Identity(); - id.pubkey = ret.responseText; + } + catch (e) { throw e; } + finally { this._dav.baseURL = base; } - this._dav.baseURL = base; + Utils.ensureStatus(ret.status, "Could not get public key for " + username); - // now encrypt the symkey with their pubkey and upload the new keyring - this._log.debug("Encrypting symmetric key with other user's public key"); - Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, id); - let enckey = yield; - if (!enckey) - throw "Could not encrypt symmetric encryption key"; + let id = new Identity(); + id.pubkey = ret.responseText; - this._log.debug("Uploading new keyring"); - keys.ring[hash] = enckey; - this._dav.PUT(this.keysFile, this._json.encode(keys), self.cb); - ret = yield; - Utils.ensureStatus(ret.status, "Could not upload keyring file."); + // now encrypt the symkey with their pubkey and upload the new keyring + Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, id); + let enckey = yield; + if (!enckey) + throw "Could not encrypt symmetric encryption key"; - this._log.debug("All done sharing!"); + keys.ring[hash] = enckey; + this._dav.PUT(this.keysFile, this._json.encode(keys), self.cb); + ret = yield; + Utils.ensureStatus(ret.status, "Could not upload keyring file."); - } catch (e) { - throw e; + this._createShare(username, username); - } finally { - this._dav.baseURL = base; + this._log.debug("All done sharing!"); + + self.done(true); + }, + + // FIXME: EEK bookmarks specific + _createShare: function Engine__createShare(id, title) { + let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + let ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + + let root; + let a = ans.getItemsWithAnnotation("weave/mounted-shares-folder", {}); + if (a.length == 1) + root = a[0]; + + if (!root) { + root = bms.createFolder(bms.toolbarFolder, "Shared Folders", + bms.DEFAULT_INDEX); + ans.setItemAnnotation(root, "weave/mounted-shares-folder", true, 0, + ans.EXPIRE_NEVER); } - self.done(); + let item + a = ans.getItemsWithAnnotation("weave/mounted-share-id", {}); + for (let i = 0; i < a.length; i++) { + if (ans.getItemAnnotation(a[i], "weave/mounted-share-id") == id) { + item = a[i]; + break; + } + } + + if (!item) { + let newId = bms.createFolder(root, title, bms.DEFAULT_INDEX); + ans.setItemAnnotation(newId, "weave/mounted-share-id", id, 0, + ans.EXPIRE_NEVER); + } }, sync: function Engine_sync(onComplete) { @@ -774,6 +801,93 @@ BookmarksEngine.prototype = { if (!this.__store) this.__store = new BookmarksStore(); return this.__store; + }, + + syncMounts: function BmkEngine_syncMounts(onComplete) { + this._syncMounts.async(this, onComplete); + }, + _syncMounts: function BmkEngine__syncMounts() { + let self = yield; + let mounts = this._store.findMounts(); + + for (i = 0; i < mounts.length; i++) { + try { + this._syncOneMount.async(this, self.cb, mounts[i]); + yield; + } catch (e) { + this._log.warn("Could not sync shared folder from " + mounts[i].userid); + this._log.trace(Utils.stackTrace(e)); + } + } + }, + + _syncOneMount: function BmkEngine__syncOneMount(mountData) { + let self = yield; + let user = mountData.userid; + let base = this._dav.baseURL; + let serverURL = Utils.prefs.getCharPref("serverURL"); + let snap = new SnapshotStore(); + + this._log.debug("Syncing shared folder from user " + user); + + try { + let hash = Utils.sha1(user); + this._dav.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! + + this._getSymKey.async(this, self.cb); + yield; + + this._log.trace("Getting status file for " + user); + this._dav.GET(this.statusFile, self.cb); + let resp = yield; + Utils.ensureStatus(resp.status, "Could not download status file."); + let status = this._json.decode(resp.responseText); + + this._log.trace("Downloading server snapshot for " + user); + this._dav.GET(this.snapshotFile, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not download snapshot."); + Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, + this._engineId, status.snapEncryption); + let data = yield; + snap.data = this._json.decode(data); + + this._log.trace("Downloading server deltas for " + user); + this._dav.GET(this.deltasFile, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not download deltas."); + Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, + this._engineId, status.deltasEncryption); + data = yield; + deltas = this._json.decode(data); + } + catch (e) { throw e; } + finally { this._dav.baseURL = base; } + + // apply deltas to get current snapshot + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands.async(snap, self.cb, deltas[i]); + yield; + } + + // prune tree / get what we want + for (let guid in snap.data) { + if (snap.data[guid].type != "bookmark") + delete snap.data[guid]; + else + snap.data[guid].parentGUID = mountData.rootGUID; + } + + this._log.trace("Got bookmarks fror " + user + ", comparing with local copy"); + this._core.detectUpdates(self.cb, mountData.snapshot, snap.data); + let diff = yield; + + // FIXME: should make sure all GUIDs here live under the mountpoint + this._log.trace("Applying changes to folder from " + user); + this._store.applyCommands.async(this._store, self.cb, diff); + yield; + + this._log.trace("Shared folder from " + user + " successfully synced!"); } }; BookmarksEngine.prototype.__proto__ = new Engine(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bf718609b5b9..2485949885e3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -512,6 +512,8 @@ WeaveSvc.prototype = { this._notify(this._bmkEngine.name + ":sync", engineCb, this._bmkEngine).async(this, innerSelf.cb); yield; + this._bmkEngine.syncMounts(innerSelf.cb); + yield; } if (Utils.prefs.getBoolPref("history")) { this._notify(this._histEngine.name + ":sync", @@ -569,11 +571,13 @@ WeaveSvc.prototype = { let cb = function Weave_shareBookmarks() { let innerSelf = yield; this._bmkEngine.share(innerSelf.cb, username); - yield; + let ret = yield; + innerSelf.done(ret); }; this._lock(this._notify("share-bookmarks", cb)).async(this, self.cb); - yield; + let ret = yield; + self.done(ret); } }; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 6dc3bc47749a..1c7657a5cebc 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -385,6 +385,15 @@ BookmarksStore.prototype = { Utils.makeURI(command.data.feedURI), command.data.index); break; + case "mounted-share": + this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\""); + newId = this._bms.createFolder(parentId, + command.data.title, + command.data.index); + + this._ans.setItemAnnotation(newId, "weave/mounted-share-id", + command.data.mountId, 0, this._ans.EXPIRE_NEVER); + break; case "separator": this._log.debug(" -> creating separator"); newId = this._bms.insertSeparator(parentId, command.data.index); @@ -508,7 +517,7 @@ BookmarksStore.prototype = { return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; }, - __wrap: function BSS__wrap(node, items, parentGUID, index, guidOverride) { + __wrap: function BSS___wrap(node, items, parentGUID, index, guidOverride) { let GUID, item; // we override the guid for the root items, "menu", "toolbar", etc. @@ -527,6 +536,14 @@ BookmarksStore.prototype = { let feedURI = this._ls.getFeedURI(node.itemId); item.siteURI = siteURI? siteURI.spec : ""; item.feedURI = feedURI? feedURI.spec : ""; + + } else if (this._ans.itemHasAnnotation(node.itemId, + "weave/mounted-share-id")) { + item.type = "mounted-share"; + item.title = node.title; + item.mountId = this._ans.getItemAnnotation(node.itemId, + "weave/mounted-share-id"); + } else { item.type = "folder"; node.QueryInterface(Ci.nsINavHistoryQueryResultNode); @@ -567,10 +584,32 @@ BookmarksStore.prototype = { }, // helper - _wrap: function BStore_wrap(node, items, rootName) { + _wrap: function BStore__wrap(node, items, rootName) { return this.__wrap(node, items, null, null, rootName); }, + _wrapMount: function BStore__wrapMount(node, id) { + if (node.type != node.RESULT_TYPE_FOLDER) + throw "Trying to wrap a non-folder mounted share"; + + let GUID = this._bms.getItemGUID(node.itemId); + let ret = {rootGUID: GUID, userid: id, snapshot: {}}; + + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this.__wrap(node.getChild(i), ret.snapshot, GUID, i); + } + + // remove any share mountpoints + for (let guid in ret.snapshot) { + if (ret.snapshot[guid].type == "mounted-share") + delete ret.snapshot[guid]; + } + + return ret; + }, + _resetGUIDs: function BSS__resetGUIDs(node) { if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); @@ -585,6 +624,16 @@ BookmarksStore.prototype = { } }, + findMounts: function BStore_findMounts() { + let ret = []; + let a = this._ans.getItemsWithAnnotation("weave/mounted-share-id", {}); + for (let i = 0; i < a.length; i++) { + let id = this._ans.getItemAnnotation(a[i], "weave/mounted-share-id"); + ret.push(this._wrapMount(this._getNode(a[i]), id)); + } + return ret; + }, + wrap: function BStore_wrap() { var items = {}; this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu"); From 8ad96d3a19b0a8de2cb91edd7bb10936cd83564f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 28 Mar 2008 03:27:05 -0700 Subject: [PATCH 0178/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 7a63f648981f..a8a7409f8145 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.25"; +const WEAVE_VERSION = "0.1.26"; const STORAGE_FORMAT_VERSION = 1; const ENGINE_STORAGE_FORMAT_VERSION = 2; From 5160f0c0aba311ec4e5ddc6f0f918acd27766a69 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 28 Mar 2008 19:36:11 -0700 Subject: [PATCH 0179/1860] fix 'clear logs' on windows --- services/sync/modules/log4moz.js | 5 +++++ services/sync/modules/service.js | 17 +++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index c16f8e9d0715..e116c0188742 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -397,6 +397,11 @@ FileAppender.prototype = { } catch(e) { dump("Error writing file:\n" + e); } + }, + + clear: function FApp_clear() { + this.closeStream(); + this._file.remove(false); } }; FileAppender.prototype.__proto__ = new Appender(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2485949885e3..a0d02b2eb11e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -263,12 +263,17 @@ WeaveSvc.prototype = { if (!verbose.exists()) verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); - let fapp = Log4Moz.Service.newFileAppender("rotating", brief, formatter); - fapp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.briefLog")]; - root.addAppender(fapp); - let vapp = Log4Moz.Service.newFileAppender("rotating", verbose, formatter); - vapp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.debugLog")]; - root.addAppender(vapp); + this._briefApp = Log4Moz.Service.newFileAppender("rotating", brief, formatter); + this._briefApp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.briefLog")]; + root.addAppender(this._briefApp); + this._debugApp = Log4Moz.Service.newFileAppender("rotating", verbose, formatter); + this._debugApp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.debugLog")]; + root.addAppender(this._debugApp); + }, + + clearLogs: function WeaveSvc_clearLogs() { + this._briefApp.clear(); + this._debugApp.clear(); }, _createUserDir: function WeaveSync__createUserDir(serverURL) { From 81814371b34fd88f2b69bda13c71cdb94873a687 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 28 Mar 2008 22:55:23 -0700 Subject: [PATCH 0180/1860] ignore 'type' properties in edit commands; fix history sync --- services/sync/modules/stores.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 1c7657a5cebc..bc2c711eff53 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -459,6 +459,10 @@ BookmarksStore.prototype = { for (let key in command.data) { switch (key) { + case "type": + // all commands have this to help in reconciliation, but it makes + // no sense to edit it + break; case "GUID": var existing = this._getItemIdForGUID(command.data.GUID); if (existing < 0) @@ -676,7 +680,7 @@ HistoryStore.prototype = { _createCommand: function HistStore__createCommand(command) { this._log.debug(" -> creating history entry: " + command.GUID); try { - let uri = Utils.makeURI(command.GUID); + let uri = Utils.makeURI(command.data.URI); this._hsvc.addVisit(uri, command.data.time, null, this._hsvc.TRANSITION_TYPED, false, null); this._hsvc.setPageTitle(uri, command.data.title); @@ -686,15 +690,15 @@ HistoryStore.prototype = { }, _removeCommand: function HistStore__removeCommand(command) { - this._log.debug(" -> NOT removing history entry: " + command.GUID); - // we can't remove because we only sync the last 500 items, not + this._log.trace(" -> NOT removing history entry: " + command.GUID); + // we can't remove because we only sync the last 1000 items, not // the whole store. So we don't know if remove commands were // generated due to the user removing an entry or because it - // dropped past the 500 item mark. + // dropped past the 1000 item mark. }, _editCommand: function HistStore__editCommand(command) { - this._log.debug(" -> FIXME: NOT editing history entry: " + command.GUID); + this._log.trace(" -> FIXME: NOT editing history entry: " + command.GUID); // FIXME: implement! }, @@ -703,9 +707,9 @@ HistoryStore.prototype = { options = this._hsvc.getNewQueryOptions(); query.minVisits = 1; - options.maxResults = 500; - options.resultType = options.RESULTS_AS_FULL_VISIT; - options.sortingMode = query.SORT_BY_LASTMODIFIED_DESCENDING; + options.maxResults = 1000; + options.resultType = options.RESULTS_AS_VISIT; // FULL_VISIT does not work + options.sortingMode = options.SORT_BY_DATE_DESCENDING; options.queryType = options.QUERY_TYPE_HISTORY; let root = this._hsvc.executeQuery(query, options).root; @@ -719,12 +723,13 @@ HistoryStore.prototype = { let items = {}; for (let i = 0; i < root.childCount; i++) { let item = root.getChild(i); - items[item.uri] = {parentGUID: '', + let guid = item.time + ":" + item.uri + items[guid] = {parentGUID: '', title: item.title, URI: item.uri, time: item.time }; - // FIXME: sync transition type + // FIXME: sync transition type - requires FULL_VISITs } return items; }, From 32fe172c50b35ef1d5d087b3148b5eb060200882 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 29 Mar 2008 00:00:16 -0700 Subject: [PATCH 0181/1860] fix the bookmarks likeness comparator (a.data undefined bug) --- services/sync/modules/syncCores.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index b09b2bd87dbf..c0d3241d514a 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -378,29 +378,29 @@ BookmarksSyncCore.prototype = { // the same index to qualify for 'likeness'. switch (a.data.type) { case "bookmark": - if (this._comp(a.data, b.data, 'URI') && - this._comp(a.data, b.data, 'title')) + if (this._comp(a, b, 'URI') && + this._comp(a, b, 'title')) return true; return false; case "query": - if (this._comp(a.data, b.data, 'URI') && - this._comp(a.data, b.data, 'title')) + if (this._comp(a, b, 'URI') && + this._comp(a, b, 'title')) return true; return false; case "microsummary": - if (this._comp(a.data, b.data, 'URI') && - this._comp(a.data, b.data, 'generatorURI')) + if (this._comp(a, b, 'URI') && + this._comp(a, b, 'generatorURI')) return true; return false; case "folder": if (this._comp(a, b, 'index') && - this._comp(a.data, b.data, 'title')) + this._comp(a, b, 'title')) return true; return false; case "livemark": - if (this._comp(a.data, b.data, 'title') && - this._comp(a.data, b.data, 'siteURI') && - this._comp(a.data, b.data, 'feedURI')) + if (this._comp(a, b, 'title') && + this._comp(a, b, 'siteURI') && + this._comp(a, b, 'feedURI')) return true; return false; case "separator": From 756231f2840c8eb22011d6bd3fde4172f53e5fd6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 29 Mar 2008 00:22:28 -0700 Subject: [PATCH 0182/1860] show 'sync now' menuitem by default --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 28a12730a502..2f69fdb12573 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -6,7 +6,7 @@ pref("extensions.weave.encryption", "aes-256-cbc"); pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.lastsync", "0"); -pref("extensions.weave.ui.syncnow", false); +pref("extensions.weave.ui.syncnow", true); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); From 858cfff035d3ac10d3836c0263544af61fe128b1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 30 Mar 2008 03:36:25 -0700 Subject: [PATCH 0183/1860] remove try/catch block in the reconciler; correctly fix parent guids of remove commands during guid changes; only allow guid changes for create commands --- services/sync/modules/syncCores.js | 157 ++++++++++++++--------------- 1 file changed, 75 insertions(+), 82 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index c0d3241d514a..fca32157f4d4 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -179,7 +179,7 @@ SyncCore.prototype = { for (let i = 0; i < list.length; i++) { if (!list[i]) continue; - if (list[i].data.parentGUID == oldGUID) + if (list[i].data && list[i].data.parentGUID == oldGUID) list[i].data.parentGUID = newGUID; for (let j = 0; j < list[i].parents.length; j++) { if (list[i].parents[j] == oldGUID) @@ -227,83 +227,76 @@ SyncCore.prototype = { this._log.debug("Reconciling " + listA.length + " against " + listB.length + "commands"); - try { - let guidChanges = []; - for (let i = 0; i < listA.length; i++) { - let a = listA[i]; + let guidChanges = []; + for (let i = 0; i < listA.length; i++) { + let a = listA[i]; + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + //this._log.debug("comparing " + i + ", listB length: " + listB.length); + + let skip = false; + listB = listB.filter(function(b) { + // fast path for when we already found a matching command + if (skip) + return true; + + if (Utils.deepEquals(a, b)) { + delete listA[i]; // a + skip = true; + return false; // b + + } else if (this._commandLike(a, b)) { + this._fixParents(listA, a.GUID, b.GUID); + guidChanges.push({action: "edit", + GUID: a.GUID, + data: {GUID: b.GUID}}); + delete listA[i]; // a + skip = true; + return false; // b, but we add it back from guidChanges + } + + // watch out for create commands with GUIDs that already exist + if (b.action == "create" && this._itemExists(b.GUID)) { + this._log.error("Remote command has GUID that already exists " + + "locally. Dropping command."); + return false; // delete b + } + return true; // keep b + }, this); + } + + listA = listA.filter(function(elt) { return elt }); + listB = guidChanges.concat(listB); + + for (let i = 0; i < listA.length; i++) { + for (let j = 0; j < listB.length; j++) { + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); yield; // Yield to main loop - //this._log.debug("comparing " + i + ", listB length: " + listB.length); - - let skip = false; - listB = listB.filter(function(b) { - // fast path for when we already found a matching command - if (skip) - return true; - - if (Utils.deepEquals(a, b)) { - delete listA[i]; // a - skip = true; - return false; // b - - } else if (this._commandLike(a, b)) { - this._fixParents(listA, a.GUID, b.GUID); - guidChanges.push({action: "edit", - GUID: a.GUID, - data: {GUID: b.GUID}}); - delete listA[i]; // a - skip = true; - return false; // b, but we add it back from guidChanges - } - - // watch out for create commands with GUIDs that already exist - if (b.action == "create" && this._itemExists(b.GUID)) { - this._log.error("Remote command has GUID that already exists " + - "locally. Dropping command."); - return false; // delete b - } - return true; // keep b - }, this); - } - - listA = listA.filter(function(elt) { return elt }); - listB = guidChanges.concat(listB); - - for (let i = 0; i < listA.length; i++) { - for (let j = 0; j < listB.length; j++) { - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - if (this._conflicts(listA[i], listB[j]) || - this._conflicts(listB[j], listA[i])) { - if (!conflicts[0].some( - function(elt) { return elt.GUID == listA[i].GUID })) - conflicts[0].push(listA[i]); - if (!conflicts[1].some( - function(elt) { return elt.GUID == listB[j].GUID })) - conflicts[1].push(listB[j]); - } + if (this._conflicts(listA[i], listB[j]) || + this._conflicts(listB[j], listA[i])) { + if (!conflicts[0].some( + function(elt) { return elt.GUID == listA[i].GUID })) + conflicts[0].push(listA[i]); + if (!conflicts[1].some( + function(elt) { return elt.GUID == listB[j].GUID })) + conflicts[1].push(listB[j]); } } - - this._getPropagations(listA, conflicts[0], propagations[1]); - - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - - this._getPropagations(listB, conflicts[1], propagations[0]); - ret = {propagations: propagations, conflicts: conflicts}; - - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e) + - " - " + (e.location? e.location : "_reconcile")); - - } finally { - timer = null; - self.done(ret); } + + this._getPropagations(listA, conflicts[0], propagations[1]); + + timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + yield; // Yield to main loop + + this._getPropagations(listB, conflicts[1], propagations[0]); + ret = {propagations: propagations, conflicts: conflicts}; + + timer = null; + self.done(ret); }, // Public methods @@ -356,18 +349,18 @@ BookmarksSyncCore.prototype = { // Check that neither command is null, that their actions, types, // and parents are the same, and that they don't have the same // GUID. - // Items with the same GUID do not qualify for 'likeness' because - // we already consider them to be the same object, and therefore - // we need to process any edits. - // Remove commands don't qualify for likeness either, since two - // remove commands for different GUIDs are guaranteed to refer to - // two different items - // The parent GUID check works because reconcile() fixes up the - // parent GUIDs as it runs, and the command list is sorted by - // depth + // * Items with the same GUID do not qualify for 'likeness' because + // we already consider them to be the same object, and therefore + // we need to process any edits. + // * Remove or edit commands don't qualify for likeness either, + // since remove or edit commands with different GUIDs are + // guaranteed to refer to two different items + // * The parent GUID check works because reconcile() fixes up the + // parent GUIDs as it runs, and the command list is sorted by + // depth if (!a || !b || a.action != b.action || - a.action == "remove" || + a.action != "create" || a.data.type != b.data.type || a.data.parentGUID != b.data.parentGUID || a.GUID == b.GUID) From 8170927065ce94fa0329d195ccbf8ee30de4eac8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 30 Mar 2008 08:40:23 -0700 Subject: [PATCH 0184/1860] use the wrapper notifier for login(), change observers to the slightly different observer topics; allow server url to not have a trailing slash (add one automatically); dial down dav.js verbosity; add serverWipe service method; change 'reset server data' button in prefs pane to do serverWipe instead of resetServer; allow for wrappers to have extra args both saved in the closure (at wrap creation time) as well as passed in later (via .async()) --- services/sync/modules/dav.js | 25 +-- services/sync/modules/service.js | 263 +++++++++++++------------------ services/sync/modules/wrap.js | 24 +-- 3 files changed, 136 insertions(+), 176 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index cf00bafbf3fe..ba38769d283d 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -79,6 +79,8 @@ DAVCollection.prototype = { return this._baseURL; }, set baseURL(value) { + if (value[value.length-1] != '/') + value = value + '/'; this._baseURL = value; }, @@ -286,7 +288,7 @@ DAVCollection.prototype = { yield; } - this._log.info("Logging in"); + this._log.debug("Logging in"); let URI = Utils.makeURI(this._baseURL); this._auth = "Basic " + btoa(username + ":" + password); @@ -307,7 +309,7 @@ DAVCollection.prototype = { }, logout: function DC_logout() { - this._log.debug("Logging out (forgetting auth header)"); + this._log.trace("Logging out (forgetting auth header)"); this._loggedIn = false; this.__auth = null; }, @@ -318,7 +320,7 @@ DAVCollection.prototype = { let self = yield; let ret = null; - this._log.info("Getting active lock token"); + this._log.debug("Getting active lock token"); this.PROPFIND("", "" + "" + @@ -334,12 +336,13 @@ DAVCollection.prototype = { let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); - ret = token.textContent; + if (token) + ret = token.textContent; if (ret) - this._log.debug("Found an active lock token"); + this._log.trace("Found an active lock token"); else - this._log.debug("No active lock token found"); + this._log.trace("No active lock token found"); self.done(ret); }, @@ -416,25 +419,25 @@ DAVCollection.prototype = { let self = yield; let unlocked = true; - this._log.info("Forcibly releasing any server locks"); + this._log.debug("Forcibly releasing any server locks"); this._getActiveLock.async(this, self.cb); this._token = yield; if (!this._token) { - this._log.info("No server lock found"); + this._log.debug("No server lock found"); self.done(true); yield; } - this._log.info("Server lock found, unlocking"); + this._log.trace("Server lock found, unlocking"); this.unlock.async(this, self.cb); unlocked = yield; if (unlocked) - this._log.debug("Lock released"); + this._log.trace("Lock released"); else - this._log.debug("No lock released"); + this._log.trace("No lock released"); self.done(unlocked); }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a0d02b2eb11e..afcf55a401bc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -363,92 +363,6 @@ WeaveSvc.prototype = { Utils.ensureStatus(ret.status, "Could not upload public key"); }, - _login: function WeaveSync__login(password, passphrase) { - let self = yield; - - try { - if (this._dav.locked) - throw "Login failed: could not acquire lock"; - this._dav.allowLock = false; - - // cache password & passphrase - // if null, we'll try to get them from the pw manager below - this._mozId.setTempPassword(password); - this._cryptoId.setTempPassword(passphrase); - - this._log.debug("Logging in"); - this._os.notifyObservers(null, "weave:service-login:start", ""); - - if (!this.username) - throw "No username set, login failed"; - if (!this.password) - throw "No password given or found in password manager"; - - let serverURL = Utils.prefs.getCharPref("serverURL"); - this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; - this._log.info("Using server URL: " + this._dav.baseURL); - - this._dav.login.async(this._dav, self.cb, this.username, this.password); - let success = yield; - - if (!success) { - // FIXME: we should actually limit this to when we get a 404 - this._createUserDir.async(this, self.cb, serverURL); - yield; - } - - this._versionCheck.async(this, self.cb); - yield; - - this._dav.GET("private/privkey", self.cb); - let keyResp = yield; - Utils.ensureStatus(keyResp.status, - "Could not get private key from server", [[200,300],404]); - - if (keyResp.status != 404) { - this._cryptoId.privkey = keyResp.responseText; - Crypto.RSAkeydecrypt.async(Crypto, self.cb, this._cryptoId); - this._cryptoId.pubkey = yield; - - } else { - this._generateKeys.async(this, self.cb); - yield; - } - - this._passphrase = null; - this._dav.allowLock = true; - this._os.notifyObservers(null, "weave:service-login:success", ""); - self.done(true); - - } catch (e) { - this._dav.allowLock = true; - this._os.notifyObservers(null, "weave:service-login:error", ""); - throw e; - } - }, - - _serverWipe: function WeaveSync__serverWipe() { - let self = yield; - - let cb = function Weave_serverWipe() { - let innerSelf = yield; - - this._dav.listFiles.async(this._dav, self.cb); - let names = yield; - - for (let i = 0; i < names.length; i++) { - this._dav.DELETE(names[i], self.cb); - let resp = yield; - this._log.debug(resp.status); - } - }; - - // NOTE: doesn't lock because it's called from _login() which - // already holds the lock - this._notify("server-wipe", cb).async(this, self.cb); - yield; - }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), // nsIObserver @@ -468,7 +382,59 @@ WeaveSvc.prototype = { // These are global (for all engines) login: function WeaveSync_login(onComplete, password, passphrase) { - this._login.async(this, onComplete, password, passphrase); + this._localLock(this._notify("login", this._login, + password, passphrase)).async(this, onComplete); + }, + _login: function WeaveSync__login(password, passphrase) { + let self = yield; + + // cache password & passphrase + // if null, we'll try to get them from the pw manager below + this._mozId.setTempPassword(password); + this._cryptoId.setTempPassword(passphrase); + + this._log.debug("Logging in"); + + if (!this.username) + throw "No username set, login failed"; + if (!this.password) + throw "No password given or found in password manager"; + + let serverURL = Utils.prefs.getCharPref("serverURL"); + if (serverURL[serverURL.length-1] != '/') + serverURL = serverURL + '/'; + this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; + this._log.info("Using server URL: " + this._dav.baseURL); + + this._dav.login.async(this._dav, self.cb, this.username, this.password); + let success = yield; + + if (!success) { + // FIXME: we should actually limit this to when we get a 404 + this._createUserDir.async(this, self.cb, serverURL); + yield; + } + + this._versionCheck.async(this, self.cb); + yield; + + this._dav.GET("private/privkey", self.cb); + let keyResp = yield; + Utils.ensureStatus(keyResp.status, + "Could not get private key from server", [[200,300],404]); + + if (keyResp.status != 404) { + this._cryptoId.privkey = keyResp.responseText; + Crypto.RSAkeydecrypt.async(Crypto, self.cb, this._cryptoId); + this._cryptoId.pubkey = yield; + + } else { + this._generateKeys.async(this, self.cb); + yield; + } + + this._passphrase = null; + self.done(true); }, logout: function WeaveSync_logout() { @@ -476,111 +442,96 @@ WeaveSvc.prototype = { this._dav.logout(); this._mozId.setTempPassword(null); // clear cached password this._cryptoId.setTempPassword(null); // and passphrase - this._os.notifyObservers(null, "weave:service-logout:success", ""); + this._os.notifyObservers(null, "weave:service:logout:success", ""); }, - resetLock: function WeaveSync_resetLock(onComplete) { - this._resetLock.async(this, onComplete); + resetLock: function WeaveSvc_resetLock(onComplete) { + this._notify("reset-server-lock", this._resetLock).async(this, onComplete); }, - _resetLock: function WeaveSync__resetLock() { + _resetLock: function WeaveSvc__resetLock() { let self = yield; - - let cb = function Weave_resetLock() { - let innerSelf = yield; - this._dav.forceUnlock.async(this._dav, innerSelf.cb); - yield; - }; - - this._notify("reset-server-lock", cb).async(this, self.cb); + this._dav.forceUnlock.async(this._dav, self.cb); yield; }, + serverWipe: function WeaveSvc_serverWipe(onComplete) { + this._lock(this._notify("server-wipe", + this._serverWipe)).async(this, onComplete); + }, + _serverWipe: function WeaveSvc__serverWipe() { + let self = yield; + + this._dav.listFiles.async(this._dav, self.cb); + let names = yield; + + for (let i = 0; i < names.length; i++) { + this._dav.DELETE(names[i], self.cb); + let resp = yield; + this._log.debug(resp.status); + } + }, + + // These are per-engine sync: function WeaveSync_sync(onComplete) { - this._sync.async(this, onComplete); + this._lock(this._notify("sync", this._sync)).async(this, onComplete); }, _sync: function WeaveSync__sync() { let self = yield; - let cb = function Weave_sync() { - let innerSelf = yield; - - let engineCb = function WeaveSvc_syncEngine(engine) { - let innerInnerSelf = yield; - engine.sync(innerInnerSelf.cb); - yield; - }; - - if (Utils.prefs.getBoolPref("bookmarks")) { - this._notify(this._bmkEngine.name + ":sync", - engineCb, this._bmkEngine).async(this, innerSelf.cb); - yield; - this._bmkEngine.syncMounts(innerSelf.cb); - yield; - } - if (Utils.prefs.getBoolPref("history")) { - this._notify(this._histEngine.name + ":sync", - engineCb, this._histEngine).async(this, innerSelf.cb); - yield; - } - }; - - this._lock(this._notify("sync", cb)).async(this, self.cb); + if (Utils.prefs.getBoolPref("bookmarks")) { + this._notify(this._bmkEngine.name + ":sync", + this._syncEngine, this._bmkEngine).async(this, self.cb); + yield; + this._bmkEngine.syncMounts(self.cb); // FIXME + yield; + } + if (Utils.prefs.getBoolPref("history")) { + this._notify(this._histEngine.name + ":sync", + this._syncEngine, this._histEngine).async(this, self.cb); + yield; + } + }, + _syncEngine: function WeaveSvc__syncEngine(engine) { + let self = yield; + engine.sync(self.cb); yield; }, resetServer: function WeaveSync_resetServer(onComplete) { - this._resetServer.async(this, onComplete); + this._lock(this._notify("reset-server", + this._resetServer)).async(this, onComplete); }, _resetServer: function WeaveSync__resetServer() { let self = yield; - - let cb = function Weave_resetServer() { - let innerSelf = yield; - this._bmkEngine.resetServer(innerSelf.cb); - yield; - this._histEngine.resetServer(innerSelf.cb); - yield; - }; - - this._lock(this._notify("reset-server",cb)).async(this, self.cb); + this._bmkEngine.resetServer(self.cb); + yield; + this._histEngine.resetServer(self.cb); yield; }, resetClient: function WeaveSync_resetClient(onComplete) { - this._resetClient.async(this, onComplete); + this._localLock(this._notify("reset-client", + this._resetClient)).async(this, onComplete); }, _resetClient: function WeaveSync__resetClient() { let self = yield; - - let cb = function Weave_resetClient() { - let innerSelf = yield; - this._bmkEngine.resetClient(innerSelf.cb); - yield; - this._histEngine.resetClient(innerSelf.cb); - yield; - }; - - this._localLock(this._notify("reset-client", cb)).async(this, self.cb); + this._bmkEngine.resetClient(self.cb); + yield; + this._histEngine.resetClient(self.cb); yield; }, shareBookmarks: function WeaveSync_shareBookmarks(onComplete, username) { - this._shareBookmarks.async(this, onComplete, username); + this._lock(this._notify("share-bookmarks", + this._shareBookmarks, + username)).async(this, onComplete); }, _shareBookmarks: function WeaveSync__shareBookmarks(username) { let self = yield; - - let cb = function Weave_shareBookmarks() { - let innerSelf = yield; - this._bmkEngine.share(innerSelf.cb, username); - let ret = yield; - innerSelf.done(ret); - }; - - this._lock(this._notify("share-bookmarks", cb)).async(this, self.cb); + this._bmkEngine.share(self.cb, username); let ret = yield; self.done(ret); } diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index f1ffa3a30c2c..238994a11e79 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -72,18 +72,20 @@ let Wrap = { // ... // } // }; - notify: function Weave_notify(name, method /* , arg1, arg2, ... */) { + notify: function Weave_notify(name, method /* , arg1, arg2, ..., argN */) { let savedName = name; let savedMethod = method; - let args = Array.prototype.slice.call(arguments, 2); + let savedArgs = Array.prototype.slice.call(arguments, 2); - return function WeaveNotifyWrapper() { + return function WeaveNotifyWrapper(/* argN+1, argN+2, ... */) { let self = yield; let ret; + let args = Array.prototype.slice.call(arguments); try { this._os.notifyObservers(null, this._osPrefix + savedName + ":start", ""); + args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); Async.run.apply(Async, args); ret = yield; @@ -101,13 +103,14 @@ let Wrap = { // NOTE: see notify, this works the same way. they can be // chained together as well. - lock: function WeaveSync_lock(method /* , arg1, arg2, ... */) { + lock: function WeaveSync_lock(method /* , arg1, arg2, ..., argN */) { let savedMethod = method; - let args = Array.prototype.slice.call(arguments, 1); + let savedArgs = Array.prototype.slice.call(arguments, 1); - return function WeaveLockWrapper() { + return function WeaveLockWrapper( /* argN+1, argN+2, ... */) { let self = yield; let ret; + let args = Array.prototype.slice.call(arguments); this._dav.lock.async(this._dav, self.cb); let locked = yield; @@ -115,6 +118,7 @@ let Wrap = { throw "Could not acquire lock"; try { + args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); Async.run.apply(Async, args); ret = yield; @@ -133,19 +137,21 @@ let Wrap = { // NOTE: see notify, this works the same way. they can be // chained together as well. - localLock: function WeaveSync_localLock(method /* , arg1, arg2, ... */) { + localLock: function WeaveSync_localLock(method /* , arg1, arg2, ..., argN */) { let savedMethod = method; - let args = Array.prototype.slice.call(arguments, 1); + let savedArgs = Array.prototype.slice.call(arguments, 1); - return function WeaveLocalLockWrapper() { + return function WeaveLocalLockWrapper(/* argN+1, argN+2, ... */) { let self = yield; let ret; + let args = Array.prototype.slice.call(arguments); if (this._dav.locked) throw "Could not acquire lock"; this._dav.allowLock = false; try { + args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); Async.run.apply(Async, args); ret = yield; From 6474d9fd0e063396e36b4e67b6a75965f160b519 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 30 Mar 2008 08:42:35 -0700 Subject: [PATCH 0185/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index a8a7409f8145..0970e1d3a9c1 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.26"; +const WEAVE_VERSION = "0.1.27"; const STORAGE_FORMAT_VERSION = 1; const ENGINE_STORAGE_FORMAT_VERSION = 2; From b79f98a988f062cc52a9c32d131153ba5aeeb39a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 30 Mar 2008 08:52:49 -0700 Subject: [PATCH 0186/1860] bump storage format version to cause a server wipe --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 0970e1d3a9c1..6af0917d1602 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,7 +43,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; const WEAVE_VERSION = "0.1.27"; -const STORAGE_FORMAT_VERSION = 1; +const STORAGE_FORMAT_VERSION = 2; const ENGINE_STORAGE_FORMAT_VERSION = 2; const PREFS_BRANCH = "extensions.weave."; From 24bf1666452ddd7c2a94d85f30af8891ed3cd1ea Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 31 Mar 2008 07:20:09 -0700 Subject: [PATCH 0187/1860] [mostly] beat login dialog into submission; use DAV singleton instead of making a new DAVCollection in service.js; split up checks from login into their own functions, call them on sync(); check we are logged in before syncing --- services/sync/modules/dav.js | 26 ++--- services/sync/modules/engines.js | 64 ++++++------ services/sync/modules/service.js | 174 ++++++++++++++++--------------- services/sync/modules/wrap.js | 11 +- 4 files changed, 141 insertions(+), 134 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index ba38769d283d..2171ed64551d 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['DAVCollection']; +const EXPORTED_SYMBOLS = ['DAV', 'DAVCollection']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -109,7 +109,7 @@ DAVCollection.prototype = { let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); - + request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); @@ -162,15 +162,15 @@ DAVCollection.prototype = { try { let components = path.split('/'); let path2 = ''; - + for (let i = 0; i < components.length; i++) { - + // trailing slashes will cause an empty path component at the end if (components[i] == '') break; - + path2 = path2 + components[i]; - + // check if it exists first this._makeRequest.async(this, self.cb, "GET", path2 + "/", this._defaultHeaders); let ret = yield; @@ -182,13 +182,13 @@ DAVCollection.prototype = { this._makeRequest.async(this, self.cb, "MKCOL", path2, this._defaultHeaders); ret = yield; - + if (ret.status != 201) { this._log.debug(ret.responseText); throw 'request failed: ' + ret.status; } } - + // add slash *after* the request, trailing slashes cause a 412! path2 = path2 + "/"; } @@ -287,7 +287,7 @@ DAVCollection.prototype = { self.done(true); yield; } - + this._log.debug("Logging in"); let URI = Utils.makeURI(this._baseURL); @@ -467,7 +467,7 @@ function DummyAuthProvider() {} DummyAuthProvider.prototype = { // Implement notification callback interfaces so we can suppress UI // and abort loads for bad SSL certs and HTTP authorization requests. - + // Interfaces this component implements. interfaces: [Ci.nsIBadCertListener, Ci.nsIAuthPromptProvider, @@ -503,7 +503,7 @@ DummyAuthProvider.prototype = { }, // nsIInterfaceRequestor - + getInterface: function DAP_getInterface(iid) { return this.QueryInterface(iid); }, @@ -511,7 +511,7 @@ DummyAuthProvider.prototype = { // nsIBadCertListener // Suppress UI and abort secure loads from servers with bad SSL certificates. - + confirmUnknownIssuer: function DAP_confirmUnknownIssuer(socketInfo, cert, certAddType) { return false; }, @@ -528,7 +528,7 @@ DummyAuthProvider.prototype = { }, // nsIAuthPromptProvider - + getAuthPrompt: function(aPromptReason, aIID) { this._authFailed = true; throw Cr.NS_ERROR_NOT_AVAILABLE; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5a875db6e1ff..3fb77ab427e3 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -46,6 +46,7 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/syncCores.js"); @@ -115,7 +116,6 @@ Engine.prototype = { }, _init: function Engine__init(davCollection, pbeId) { - this._dav = davCollection; this._pbeId = pbeId; this._engineId = new Identity(pbeId.realm + " - " + this.logName, pbeId.username); @@ -129,7 +129,7 @@ Engine.prototype = { _getSymKey: function Engine__getSymKey() { let self = yield; - this._dav.GET(this.keysFile, self.cb); + DAV.GET(this.keysFile, self.cb); let keysResp = yield; Utils.ensureStatus(keysResp.status, "Could not get keys file.", [[200,300]]); @@ -151,7 +151,7 @@ Engine.prototype = { //json = json.replace(/ {action/g, "\n {action"); return json; }, - + _serializeConflicts: function Engine__serializeConflicts(conflicts) { let json = this._json.encode(conflicts); //json = json.replace(/ {action/g, "\n {action"); @@ -167,11 +167,11 @@ Engine.prototype = { this._os.notifyObservers(null, this._osPrefix + "reset-server:start", ""); // try to delete all 3, check status after - this._dav.DELETE(this.statusFile, self.cb); + DAV.DELETE(this.statusFile, self.cb); let statusResp = yield; - this._dav.DELETE(this.snapshotFile, self.cb); + DAV.DELETE(this.snapshotFile, self.cb); let snapshotResp = yield; - this._dav.DELETE(this.deltasFile, self.cb); + DAV.DELETE(this.deltasFile, self.cb); let deltasResp = yield; Utils.ensureStatus(statusResp.status, @@ -184,7 +184,7 @@ Engine.prototype = { this._log.debug("Server files deleted"); done = true; this._os.notifyObservers(null, this._osPrefix + "reset-server:success", ""); - + } catch (e) { this._log.error("Could not delete server files"); this._os.notifyObservers(null, this._osPrefix + "reset-server:error", ""); @@ -255,7 +255,7 @@ Engine.prototype = { this._log.info("Beginning sync"); // Before we get started, make sure we have a remote directory to play in - this._dav.MKCOL(this.serverPrefix, self.cb); + DAV.MKCOL(this.serverPrefix, self.cb); let ret = yield; if (!ret) throw "Could not create remote folder"; @@ -292,7 +292,7 @@ Engine.prototype = { self.done(true); return; } - + // 3) Reconcile client/server deltas and generate new deltas for them. this._log.info("Reconciling client/server updates"); @@ -399,14 +399,14 @@ Engine.prototype = { this._serializeCommands(server.deltas), this._engineId); let data = yield; - this._dav.PUT(this.deltasFile, data, self.cb); + DAV.PUT(this.deltasFile, data, self.cb); let deltasPut = yield; let c = 0; for (GUID in this._snapshot.data) c++; - this._dav.PUT(this.statusFile, + DAV.PUT(this.statusFile, this._json.encode( {GUID: this._snapshot.GUID, formatVersion: ENGINE_STORAGE_FORMAT_VERSION, @@ -465,7 +465,7 @@ Engine.prototype = { snapshot: null, deltas: null, updates: null}; this._log.debug("Getting status file from server"); - this._dav.GET(this.statusFile, self.cb); + DAV.GET(this.statusFile, self.cb); let resp = yield; let status = resp.status; @@ -510,7 +510,7 @@ Engine.prototype = { this._log.info("Local snapshot is out of date"); this._log.info("Downloading server snapshot"); - this._dav.GET(this.snapshotFile, self.cb); + DAV.GET(this.snapshotFile, self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not download snapshot."); Crypto.PBEdecrypt.async(Crypto, self.cb, @@ -521,7 +521,7 @@ Engine.prototype = { snap.data = this._json.decode(data); this._log.info("Downloading server deltas"); - this._dav.GET(this.deltasFile, self.cb); + DAV.GET(this.deltasFile, self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, @@ -538,7 +538,7 @@ Engine.prototype = { snap.data = Utils.deepCopy(this._snapshot.data); this._log.info("Downloading server deltas"); - this._dav.GET(this.deltasFile, self.cb); + DAV.GET(this.deltasFile, self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, @@ -555,7 +555,7 @@ Engine.prototype = { // FIXME: could optimize this case by caching deltas file this._log.info("Downloading server deltas"); - this._dav.GET(this.deltasFile, self.cb); + DAV.GET(this.deltasFile, self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, @@ -641,7 +641,7 @@ Engine.prototype = { let keys = {ring: {}}; keys.ring[this._engineId.userHash] = enckey; - this._dav.PUT(this.keysFile, this._json.encode(keys), self.cb); + DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); let resp = yield; Utils.ensureStatus(resp.status, "Could not upload keyring file."); @@ -650,11 +650,11 @@ Engine.prototype = { this._engineId); let data = yield; - this._dav.PUT(this.snapshotFile, data, self.cb); + DAV.PUT(this.snapshotFile, data, self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not upload snapshot."); - this._dav.PUT(this.deltasFile, "[]", self.cb); + DAV.PUT(this.deltasFile, "[]", self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not upload deltas."); @@ -662,7 +662,7 @@ Engine.prototype = { for (GUID in this._snapshot.data) c++; - this._dav.PUT(this.statusFile, + DAV.PUT(this.statusFile, this._json.encode( {GUID: this._snapshot.GUID, formatVersion: ENGINE_STORAGE_FORMAT_VERSION, @@ -681,7 +681,7 @@ Engine.prototype = { _share: function Engine__share(username) { let self = yield; - let base = this._dav.baseURL; + let base = DAV.baseURL; this._log.debug("Sharing bookmarks with " + username); @@ -689,7 +689,7 @@ Engine.prototype = { yield; // copied from getSymKey - this._dav.GET(this.keysFile, self.cb); + DAV.GET(this.keysFile, self.cb); let ret = yield; Utils.ensureStatus(ret.status, "Could not get keys file."); let keys = this._json.decode(ret.responseText); @@ -699,12 +699,12 @@ Engine.prototype = { let serverURL = Utils.prefs.getCharPref("serverURL"); try { - this._dav.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! - this._dav.GET("public/pubkey", self.cb); + DAV.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! + DAV.GET("public/pubkey", self.cb); ret = yield; } catch (e) { throw e; } - finally { this._dav.baseURL = base; } + finally { DAV.baseURL = base; } Utils.ensureStatus(ret.status, "Could not get public key for " + username); @@ -718,7 +718,7 @@ Engine.prototype = { throw "Could not encrypt symmetric encryption key"; keys.ring[hash] = enckey; - this._dav.PUT(this.keysFile, this._json.encode(keys), self.cb); + DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload keyring file."); @@ -824,7 +824,7 @@ BookmarksEngine.prototype = { _syncOneMount: function BmkEngine__syncOneMount(mountData) { let self = yield; let user = mountData.userid; - let base = this._dav.baseURL; + let base = DAV.baseURL; let serverURL = Utils.prefs.getCharPref("serverURL"); let snap = new SnapshotStore(); @@ -832,19 +832,19 @@ BookmarksEngine.prototype = { try { let hash = Utils.sha1(user); - this._dav.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! + DAV.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! this._getSymKey.async(this, self.cb); yield; this._log.trace("Getting status file for " + user); - this._dav.GET(this.statusFile, self.cb); + DAV.GET(this.statusFile, self.cb); let resp = yield; Utils.ensureStatus(resp.status, "Could not download status file."); let status = this._json.decode(resp.responseText); this._log.trace("Downloading server snapshot for " + user); - this._dav.GET(this.snapshotFile, self.cb); + DAV.GET(this.snapshotFile, self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not download snapshot."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, @@ -853,7 +853,7 @@ BookmarksEngine.prototype = { snap.data = this._json.decode(data); this._log.trace("Downloading server deltas for " + user); - this._dav.GET(this.deltasFile, self.cb); + DAV.GET(this.deltasFile, self.cb); resp = yield; Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, @@ -862,7 +862,7 @@ BookmarksEngine.prototype = { deltas = this._json.decode(data); } catch (e) { throw e; } - finally { this._dav.baseURL = base; } + finally { DAV.baseURL = base; } // apply deltas to get current snapshot for (var i = 0; i < deltas.length; i++) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index afcf55a401bc..d645f3ad57ca 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -56,16 +56,16 @@ Function.prototype.async = Async.sugar; // for export let Weave = {}; -Components.utils.import("resource://weave/constants.js", Weave); -Components.utils.import("resource://weave/util.js", Weave); -Components.utils.import("resource://weave/async.js", Weave); -Components.utils.import("resource://weave/crypto.js", Weave); -Components.utils.import("resource://weave/identity.js", Weave); -Components.utils.import("resource://weave/dav.js", Weave); -Components.utils.import("resource://weave/stores.js", Weave); -Components.utils.import("resource://weave/syncCores.js", Weave); -Components.utils.import("resource://weave/engines.js", Weave); -Components.utils.import("resource://weave/service.js", Weave); +Cu.import("resource://weave/constants.js", Weave); +Cu.import("resource://weave/util.js", Weave); +Cu.import("resource://weave/async.js", Weave); +Cu.import("resource://weave/crypto.js", Weave); +Cu.import("resource://weave/identity.js", Weave); +Cu.import("resource://weave/dav.js", Weave); +Cu.import("resource://weave/stores.js", Weave); +Cu.import("resource://weave/syncCores.js", Weave); +Cu.import("resource://weave/engines.js", Weave); +Cu.import("resource://weave/service.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); /* @@ -109,32 +109,22 @@ WeaveSvc.prototype = { return this.__dirSvc; }, - __dav: null, - get _dav() { - if (!this.__dav) - this.__dav = new DAVCollection(); - return this.__dav; - }, - // FIXME: engines should be loaded dynamically somehow / need API to register __bmkEngine: null, get _bmkEngine() { if (!this.__bmkEngine) - this.__bmkEngine = new BookmarksEngine(this._dav, this._cryptoId); + this.__bmkEngine = new BookmarksEngine(DAV, this._cryptoId); return this.__bmkEngine; }, __histEngine: null, get _histEngine() { if (!this.__histEngine) - this.__histEngine = new HistoryEngine(this._dav, this._cryptoId); + this.__histEngine = new HistoryEngine(DAV, this._cryptoId); return this.__histEngine; }, - // Logger object - _log: null, - // Timer object for automagically syncing _scheduleTimer: null, @@ -176,7 +166,7 @@ WeaveSvc.prototype = { get userPath() { return this._mozId.userHash; }, get currentUser() { - if (this._dav.loggedIn) + if (DAV.loggedIn) return this.username; return null; }, @@ -276,34 +266,15 @@ WeaveSvc.prototype = { this._debugApp.clear(); }, - _createUserDir: function WeaveSync__createUserDir(serverURL) { - let self = yield; - - this._log.debug("Attempting to create user directory"); - - this._dav.baseURL = serverURL; - this._dav.MKCOL("user/" + this.userPath, self.cb); - let ret = yield; - if (!ret) - throw "Could not create user directory"; - - this._log.debug("Successfully created user directory. Re-attempting login."); - this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; - this._dav.login.async(this._dav, self.cb, this.username, this.password); - let success = yield; - if (!success) - throw "Created user directory, but login still failed. Aborting."; - }, - _uploadVersion: function WeaveSync__uploadVersion() { let self = yield; - this._dav.MKCOL("meta", self.cb); + DAV.MKCOL("meta", self.cb); let ret = yield; if (!ret) throw "Could not create meta information directory"; - this._dav.PUT("meta/version", STORAGE_FORMAT_VERSION, self.cb); + DAV.PUT("meta/version", STORAGE_FORMAT_VERSION, self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload server version file"); }, @@ -312,7 +283,7 @@ WeaveSvc.prototype = { _versionCheck: function WeaveSync__versionCheck() { let self = yield; - this._dav.GET("meta/version", self.cb); + DAV.GET("meta/version", self.cb); let ret = yield; if (!Utils.checkStatus(ret.status)) { @@ -334,6 +305,44 @@ WeaveSvc.prototype = { } }, + _checkUserDir: function WeaveSvc__checkUserDir() { + let self = yield; + + this._log.trace("Checking user directory exists"); + + let serverURL = Utils.prefs.getCharPref("serverURL"); + if (serverURL[serverURL.length-1] != '/') + serverURL = serverURL + '/'; + + DAV.baseURL = serverURL; + DAV.MKCOL("user/" + this.userPath, self.cb); + let ret = yield; + if (!ret) + throw "Could not create user directory"; + + DAV.baseURL = serverURL + "user/" + this.userPath + "/"; + this._log.info("Using server URL: " + DAV.baseURL); + }, + + _keyCheck: function WeaveSvc__keyCheck() { + let self = yield; + + DAV.GET("private/privkey", self.cb); + let keyResp = yield; + Utils.ensureStatus(keyResp.status, + "Could not get private key from server", [[200,300],404]); + + if (keyResp.status != 404) { + this._cryptoId.privkey = keyResp.responseText; + Crypto.RSAkeydecrypt.async(Crypto, self.cb, this._cryptoId); + this._cryptoId.pubkey = yield; + + } else { + this._generateKeys.async(this, self.cb); + yield; + } + }, + _generateKeys: function WeaveSync__generateKeys() { let self = yield; @@ -344,21 +353,21 @@ WeaveSvc.prototype = { this._cryptoId.privkey = privkey; this._cryptoId.pubkey = pubkey; - this._dav.MKCOL("private/", self.cb); + DAV.MKCOL("private/", self.cb); let ret = yield; if (!ret) throw "Could not create private key directory"; - this._dav.MKCOL("public/", self.cb); + DAV.MKCOL("public/", self.cb); ret = yield; if (!ret) throw "Could not create public key directory"; - this._dav.PUT("private/privkey", privkey, self.cb); + DAV.PUT("private/privkey", privkey, self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload private key"); - this._dav.PUT("public/pubkey", pubkey, self.cb); + DAV.PUT("public/pubkey", pubkey, self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload public key"); }, @@ -400,46 +409,26 @@ WeaveSvc.prototype = { if (!this.password) throw "No password given or found in password manager"; - let serverURL = Utils.prefs.getCharPref("serverURL"); - if (serverURL[serverURL.length-1] != '/') - serverURL = serverURL + '/'; - this._dav.baseURL = serverURL + "user/" + this.userPath + "/"; - this._log.info("Using server URL: " + this._dav.baseURL); + this._checkUserDir.async(this, self.cb); + yield; - this._dav.login.async(this._dav, self.cb, this.username, this.password); + DAV.login.async(DAV, self.cb, this.username, this.password); let success = yield; - - if (!success) { - // FIXME: we should actually limit this to when we get a 404 - this._createUserDir.async(this, self.cb, serverURL); - yield; - } + if (!success) + throw "Login failed"; this._versionCheck.async(this, self.cb); yield; - this._dav.GET("private/privkey", self.cb); - let keyResp = yield; - Utils.ensureStatus(keyResp.status, - "Could not get private key from server", [[200,300],404]); + this._keyCheck.async(this, self.cb); + yield; - if (keyResp.status != 404) { - this._cryptoId.privkey = keyResp.responseText; - Crypto.RSAkeydecrypt.async(Crypto, self.cb, this._cryptoId); - this._cryptoId.pubkey = yield; - - } else { - this._generateKeys.async(this, self.cb); - yield; - } - - this._passphrase = null; self.done(true); }, logout: function WeaveSync_logout() { this._log.info("Logging out"); - this._dav.logout(); + DAV.logout(); this._mozId.setTempPassword(null); // clear cached password this._cryptoId.setTempPassword(null); // and passphrase this._os.notifyObservers(null, "weave:service:logout:success", ""); @@ -450,29 +439,33 @@ WeaveSvc.prototype = { }, _resetLock: function WeaveSvc__resetLock() { let self = yield; - this._dav.forceUnlock.async(this._dav, self.cb); + DAV.forceUnlock.async(DAV, self.cb); yield; }, serverWipe: function WeaveSvc_serverWipe(onComplete) { - this._lock(this._notify("server-wipe", - this._serverWipe)).async(this, onComplete); + let cb = function WeaveSvc_serverWipeCb() { + let self = yield; + this._serverWipe.async(this, self.cb); + yield; + this.logout(); + self.done(); + }; + this._lock(this._notify("server-wipe", cb)).async(this, onComplete); }, _serverWipe: function WeaveSvc__serverWipe() { let self = yield; - this._dav.listFiles.async(this._dav, self.cb); + DAV.listFiles.async(DAV, self.cb); let names = yield; for (let i = 0; i < names.length; i++) { - this._dav.DELETE(names[i], self.cb); + DAV.DELETE(names[i], self.cb); let resp = yield; this._log.debug(resp.status); } }, - - // These are per-engine sync: function WeaveSync_sync(onComplete) { @@ -481,6 +474,15 @@ WeaveSvc.prototype = { _sync: function WeaveSync__sync() { let self = yield; + if (!DAV.loggedIn) + throw "Can't sync: Not logged in"; + + this._versionCheck.async(this, self.cb); + yield; + + this._keyCheck.async(this, self.cb); + yield; + if (Utils.prefs.getBoolPref("bookmarks")) { this._notify(this._bmkEngine.name + ":sync", this._syncEngine, this._bmkEngine).async(this, self.cb); @@ -506,6 +508,10 @@ WeaveSvc.prototype = { }, _resetServer: function WeaveSync__resetServer() { let self = yield; + + if (!DAV.loggedIn) + throw "Can't reset server: Not logged in"; + this._bmkEngine.resetServer(self.cb); yield; this._histEngine.resetServer(self.cb); diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 238994a11e79..90d5696ab17c 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -43,6 +43,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; @@ -112,7 +113,7 @@ let Wrap = { let ret; let args = Array.prototype.slice.call(arguments); - this._dav.lock.async(this._dav, self.cb); + DAV.lock.async(DAV, self.cb); let locked = yield; if (!locked) throw "Could not acquire lock"; @@ -127,7 +128,7 @@ let Wrap = { throw e; } finally { - this._dav.unlock.async(this._dav, self.cb); + DAV.unlock.async(DAV, self.cb); yield; } @@ -146,9 +147,9 @@ let Wrap = { let ret; let args = Array.prototype.slice.call(arguments); - if (this._dav.locked) + if (DAV.locked) throw "Could not acquire lock"; - this._dav.allowLock = false; + DAV.allowLock = false; try { args = savedArgs.concat(args); @@ -157,7 +158,7 @@ let Wrap = { ret = yield; } catch (e) { throw e; } - finally { this._dav.allowLock = true; } + finally { DAV.allowLock = true; } self.done(ret); }; From a87c5a2247fca578d797eca07b5fa3f1d2a66b44 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 31 Mar 2008 16:24:43 -0700 Subject: [PATCH 0188/1860] add warnings to reset client/server data buttons in the prefs --- services/sync/locales/en-US/preferences.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index f3f854d05f1a..0eb963ffbe81 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -1,2 +1,6 @@ # %S is the username of the signed in user signedIn.description = Signed in as %S +reset.server.warning.title = Warning +reset.server.warning = This will delete all data on the server.\n\nYou *must* restart this and any other instances of Firefox you may have running (on any computer). +reset.client.warning.title = Warning +reset.client.warning = This will delete all bookmarks and history data. Are you sure you want to do this? From e405e15b179945e59dee5f1e64b5343b13bcdcb2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 31 Mar 2008 16:55:54 -0700 Subject: [PATCH 0189/1860] hide share bookmarks menu item; clean up ui init code --- services/sync/services-sync.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 2f69fdb12573..f9ffa8f456a6 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -7,6 +7,7 @@ pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.lastsync", "0"); pref("extensions.weave.ui.syncnow", true); +pref("extensions.weave.ui.sharebookmarks", false); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); From 3ea41d65fd528acac2bdf8ba85d05b811028558b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 31 Mar 2008 17:07:40 -0700 Subject: [PATCH 0190/1860] fix tag sync --- services/sync/modules/stores.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index bc2c711eff53..3aaae70470a4 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -390,7 +390,7 @@ BookmarksStore.prototype = { newId = this._bms.createFolder(parentId, command.data.title, command.data.index); - + this._ans.setItemAnnotation(newId, "weave/mounted-share-id", command.data.mountId, 0, this._ans.EXPIRE_NEVER); break; @@ -490,7 +490,7 @@ BookmarksStore.prototype = { } break; case "tags": { let tagsURI = this._bms.getBookmarkURI(itemId); - this._ts.untagURI(URI, null); + this._ts.untagURI(tagsURI, null); this._ts.tagURI(tagsURI, command.data.tags); } break; case "keyword": @@ -612,7 +612,7 @@ BookmarksStore.prototype = { } return ret; - }, + }, _resetGUIDs: function BSS__resetGUIDs(node) { if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) From 3a1498b0eb03a32e538cd16b3ee8223754f3591e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 31 Mar 2008 17:46:29 -0700 Subject: [PATCH 0191/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 6af0917d1602..50f4054c8427 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.27"; +const WEAVE_VERSION = "0.1.28"; const STORAGE_FORMAT_VERSION = 2; const ENGINE_STORAGE_FORMAT_VERSION = 2; From 18ca53cd5a729598fc153454620ffeefc2bba2f2 Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Apr 2008 16:36:08 -0700 Subject: [PATCH 0192/1860] Fixed a couple of syntax errors that were preventing weave from starting --- services/sync/modules/stores.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 9004fd28002d..a753cfeb148a 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -75,7 +75,8 @@ Store.prototype = { }, applyCommands: function Store_applyCommands(commandList) { - let self = yield, timer, listener; + let self = yield; + let timer, listener; if (this._yieldDuringApply) { listener = new Utils.EventListener(self.cb); @@ -849,6 +850,7 @@ CookieStore.prototype = { // since they're deprecated. } + } return items; }, From 61ace2de442ec9edc740fb97734b76ce407c7b5a Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Apr 2008 17:32:14 -0700 Subject: [PATCH 0193/1860] Fixed a couple of missing symbol exports that were stopping CookieEngine from being able to be instantiated. --- services/sync/modules/engines.js | 6 +++--- services/sync/modules/service.js | 5 ++++- services/sync/modules/stores.js | 2 +- services/sync/modules/syncCores.js | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3b6e0f0e7c92..c60521cda27b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Engine', 'BookmarksEngine', 'HistoryEngine']; +const EXPORTED_SYMBOLS = ['Engine', 'BookmarksEngine', 'HistoryEngine', 'CookieEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -917,8 +917,8 @@ HistoryEngine.prototype = { HistoryEngine.prototype.__proto__ = new Engine(); // Jono: the following is copy-and-paste code -function CookieEngine(davCollection, cryptoId) { - this._init(davCollection, cryptoId); +function CookieEngine(davCollection, pbeId) { + this._init(davCollection, pbeId); } CookieEngine.prototype = { get name() { return "cookie-engine"; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2777d914cc40..5b86cee900aa 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -127,6 +127,9 @@ WeaveSvc.prototype = { __cookieEngine: null, get _cookieEngine() { + // This gets an error that "CookieEngine" is undefined. Why? + // BookmarksEngine and HistoryEngine are both defined in engines.js + // and so is CookieEngine, but... if (!this.__cookieEngine) this.__cookieEngine = new CookieEngine(DAV, this._cryptoId); return this.__cookieEngine; @@ -503,7 +506,7 @@ WeaveSvc.prototype = { this._syncEngine, this._histEngine).async(this, self.cb); yield; } - if (this._prefs.getBoolPref("cookies")) { + if (Utils.prefs.getBoolPref("cookies")) { this._notify(this._cookieEngine.name + ":sync", this._syncEngine, this._cookieEngine).async(this, self.cb); yield; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index a753cfeb148a..8d3816d8e024 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore']; + 'HistoryStore', 'CookieStore']; const Cc = Components.classes; const Ci = Components.interfaces; diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 5f48d7669841..a53a0e3d936e 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore']; +const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', 'CookiesSyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; From a4967f202d3ba7793231a2e21bea6a6fd67cfb1e Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Apr 2008 17:51:10 -0700 Subject: [PATCH 0194/1860] Fixed some minor errors in cookieStore / cookieSyncCore (still getting used to the getter idiom in javascript -- had too many underscores). Syncing cookies now works in as much as it can upload all cookies to the server without raising any exceptions; now to see if it can download and merge cookies on the other side... --- services/sync/modules/stores.js | 16 +++++++++++----- services/sync/modules/syncCores.js | 16 +++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 8d3816d8e024..14d967f5dce0 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -781,9 +781,10 @@ CookieStore.prototype = { // in order to sync with the server. this._log.info("CookieStore got createCommand: " + command ); - + if (this._cookieManager == null ) + throw "Cookie manager is null in CookieStore._createCommand."; // this assumes command.data fits the nsICookie2 interface - this.__cookieManager.add( command.data.host, + this._cookieManager.add( command.data.host, command.data.path, command.data.name, command.data.value, @@ -804,6 +805,8 @@ CookieStore.prototype = { // http://developer.mozilla.org/en/docs/nsICookieManager // the last argument is "always block cookies from this domain?" // and the answer is "no". + if (this._cookieManager == null ) + throw "Cookie manager is null in CookieStore._removeCommand."; this._cookieManager.remove( command.data.host, command.data.name, command.data.path, @@ -825,7 +828,9 @@ CookieStore.prototype = { // values are sub-dictionaries containing all cookie fields. let items = {}; - var iter = this.__cookieManager.enumerator; + if (this._cookieManager == null ) + throw "Cookie manager is null in CookieStore.wrap."; + var iter = this._cookieManager.enumerator; while (iter.hasMoreElements()){ var cookie = iter.getNext(); if (cookie instanceof Ci.nsICookie){ @@ -859,8 +864,9 @@ CookieStore.prototype = { // TODO are the semantics of this just wiping out an internal // buffer, or am I supposed to wipe out all cookies from // the browser itself for reals? - - this.__cookieManager.removeAll() + if (this._cookieManager == null ) + throw "Cookie manager is null in CookieStore.wipe"; + this._cookieManager.removeAll() }, resetGUIDs: function CookieStore_resetGUIDs() { diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index a53a0e3d936e..8fbd822762ab 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', 'CookiesSyncCore']; +const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', 'CookieSyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -431,10 +431,10 @@ HistorySyncCore.prototype = { HistorySyncCore.prototype.__proto__ = new SyncCore(); -function CookiesSyncCore() { +function CookieSyncCore() { this._init(); } -CookiesSyncCore.prototype = { +CookieSyncCore.prototype = { _logName: "CookieSync", __cookieManager: null, @@ -464,10 +464,12 @@ CookiesSyncCore.prototype = { // create a generic object to represent the cookie -- just has // to implement nsICookie2 interface. cookie = Object(); - cookie.host = cookieArray[0] - cookie.path = cookieArray[1] + cookie.host = cookieArray[0]; + cookie.path = cookieArray[1]; cookie.name = cookieArray[2]; - return this.__cookieManager.findMatchingCookie( cookie, unused ); + if (this._cookieManager == null ) + throw "Cookie manager is null in CookieSyncCore._itemExists."; + return this._cookieManager.findMatchingCookie( cookie, unused ); }, _commandLike: function CSC_commandLike(a, b) { @@ -480,4 +482,4 @@ CookiesSyncCore.prototype = { return false; } }; -CookiesSyncCore.prototype.__proto__ = new SyncCore(); +CookieSyncCore.prototype.__proto__ = new SyncCore(); From 795f585e969402bbf8ac5d46e92b461a1a9f3179 Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Apr 2008 19:12:03 -0700 Subject: [PATCH 0195/1860] Removed some lines that were only in there for debugging purposes. --- services/sync/modules/stores.js | 13 ++++--------- services/sync/modules/syncCores.js | 2 -- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 14d967f5dce0..da080c2826b3 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -781,8 +781,6 @@ CookieStore.prototype = { // in order to sync with the server. this._log.info("CookieStore got createCommand: " + command ); - if (this._cookieManager == null ) - throw "Cookie manager is null in CookieStore._createCommand."; // this assumes command.data fits the nsICookie2 interface this._cookieManager.add( command.data.host, command.data.path, @@ -805,8 +803,6 @@ CookieStore.prototype = { // http://developer.mozilla.org/en/docs/nsICookieManager // the last argument is "always block cookies from this domain?" // and the answer is "no". - if (this._cookieManager == null ) - throw "Cookie manager is null in CookieStore._removeCommand."; this._cookieManager.remove( command.data.host, command.data.name, command.data.path, @@ -817,7 +813,10 @@ CookieStore.prototype = { // we got a command to change a cookie in the local browser // in order to sync with the server. - // TODO implement this!! + // TODO implement this!! The behavior should be that if the + // local copy of the cookie is more-recently modified, it should + // be kept, but if it's older, it should be replaced with the + // server's copy. this._log.info("CookieStore got editCommand: " + command ); }, @@ -828,8 +827,6 @@ CookieStore.prototype = { // values are sub-dictionaries containing all cookie fields. let items = {}; - if (this._cookieManager == null ) - throw "Cookie manager is null in CookieStore.wrap."; var iter = this._cookieManager.enumerator; while (iter.hasMoreElements()){ var cookie = iter.getNext(); @@ -864,8 +861,6 @@ CookieStore.prototype = { // TODO are the semantics of this just wiping out an internal // buffer, or am I supposed to wipe out all cookies from // the browser itself for reals? - if (this._cookieManager == null ) - throw "Cookie manager is null in CookieStore.wipe"; this._cookieManager.removeAll() }, diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 8fbd822762ab..bbdb1ef7e296 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -467,8 +467,6 @@ CookieSyncCore.prototype = { cookie.host = cookieArray[0]; cookie.path = cookieArray[1]; cookie.name = cookieArray[2]; - if (this._cookieManager == null ) - throw "Cookie manager is null in CookieSyncCore._itemExists."; return this._cookieManager.findMatchingCookie( cookie, unused ); }, From 2d2ff137095da1fff39111cbb4e5bb28a1b9b968 Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Apr 2008 19:16:27 -0700 Subject: [PATCH 0196/1860] Updated the comment in CookieStore.editCommand. --- services/sync/modules/stores.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index da080c2826b3..59cc0f2b8d2e 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -783,12 +783,12 @@ CookieStore.prototype = { this._log.info("CookieStore got createCommand: " + command ); // this assumes command.data fits the nsICookie2 interface this._cookieManager.add( command.data.host, - command.data.path, - command.data.name, - command.data.value, - command.data.isSecure, - command.data.isSession, - command.data.expiry ); + command.data.path, + command.data.name, + command.data.value, + command.data.isSecure, + command.data.isSession, + command.data.expiry ); }, _removeCommand: function CookieStore__removeCommand(command) { @@ -813,10 +813,14 @@ CookieStore.prototype = { // we got a command to change a cookie in the local browser // in order to sync with the server. - // TODO implement this!! The behavior should be that if the + // The behavior should be that if the // local copy of the cookie is more-recently modified, it should // be kept, but if it's older, it should be replaced with the // server's copy. + + // The nsICookie interface doesn't seem to include the date/time + // that the cookie was set -- only the expiry. TODO: Figure out + // the best way to deal with this. this._log.info("CookieStore got editCommand: " + command ); }, From a6fc5d2f752ccab3ed27c0ee7e8f06a52b4645cd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 1 Apr 2008 23:43:14 -0700 Subject: [PATCH 0197/1860] make login more robust, specially work around first-login oddities with services.m.c --- services/sync/modules/dav.js | 19 +++++++------ services/sync/modules/service.js | 49 +++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 2171ed64551d..8a4085724ee9 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -264,15 +264,16 @@ DAVCollection.prototype = { let resp = yield; Utils.ensureStatus(resp.status, "propfind failed"); - // FIXME: shouldn't depend on the first one being the root - let tokens = Utils.xpath(resp.responseXML, '//D:href'); - let ret = [], - token, - root = tokens.iterateNext(); - root = root.textContent; - - while (token = tokens.iterateNext()) - ret.push(token.textContent.replace(root, '')); + let ret = []; + try { + let tokens = Utils.xpath(resp.responseXML, '//D:href'); + // FIXME: shouldn't depend on the first one being the root + let root = tokens.iterateNext(); + root = root.textContent; + let token; + while (token = tokens.iterateNext()) + ret.push(token.textContent.replace(root, '')); + } catch (e) {} self.done(ret); }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5b86cee900aa..ba6673200775 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -127,15 +127,14 @@ WeaveSvc.prototype = { __cookieEngine: null, get _cookieEngine() { - // This gets an error that "CookieEngine" is undefined. Why? - // BookmarksEngine and HistoryEngine are both defined in engines.js - // and so is CookieEngine, but... + // This gets an error that "CookieEngine" is undefined. Why? + // BookmarksEngine and HistoryEngine are both defined in engines.js + // and so is CookieEngine, but... if (!this.__cookieEngine) this.__cookieEngine = new CookieEngine(DAV, this._cryptoId); return this.__cookieEngine; }, - // Timer object for automagically syncing _scheduleTimer: null, @@ -325,13 +324,20 @@ WeaveSvc.prototype = { if (serverURL[serverURL.length-1] != '/') serverURL = serverURL + '/'; - DAV.baseURL = serverURL; - DAV.MKCOL("user/" + this.userPath, self.cb); - let ret = yield; - if (!ret) - throw "Could not create user directory"; + try { + DAV.baseURL = serverURL; + DAV.MKCOL("user/" + this.userPath, self.cb); + let ret = yield; + if (!ret) + throw "Could not create user directory"; + + } catch (e) { + throw e; + + } finally { + DAV.baseURL = serverURL + "user/" + this.userPath + "/"; + } - DAV.baseURL = serverURL + "user/" + this.userPath + "/"; this._log.info("Using server URL: " + DAV.baseURL); }, @@ -420,17 +426,26 @@ WeaveSvc.prototype = { if (!this.password) throw "No password given or found in password manager"; - this._checkUserDir.async(this, self.cb); - yield; + let serverURL = Utils.prefs.getCharPref("serverURL"); + if (serverURL[serverURL.length-1] != '/') + serverURL = serverURL + '/'; + DAV.baseURL = serverURL + "user/" + this.userPath + "/"; DAV.login.async(DAV, self.cb, this.username, this.password); let success = yield; - if (!success) - throw "Login failed"; + if (!success) { + try { + this._checkUserDir.async(this, self.cb); + yield; + } catch (e) { /* FIXME: tmp workaround for services.m.c */ } + DAV.login.async(DAV, self.cb, this.username, this.password); + let success = yield; + if (!success) + throw "Login failed"; + } this._versionCheck.async(this, self.cb); yield; - this._keyCheck.async(this, self.cb); yield; @@ -507,8 +522,8 @@ WeaveSvc.prototype = { yield; } if (Utils.prefs.getBoolPref("cookies")) { - this._notify(this._cookieEngine.name + ":sync", - this._syncEngine, this._cookieEngine).async(this, self.cb); + this._notify(this._cookieEngine.name + ":sync", + this._syncEngine, this._cookieEngine).async(this, self.cb); yield; } }, From 51ba3c36ab23cf370941dece3b4d6bc092303261 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 1 Apr 2008 23:44:39 -0700 Subject: [PATCH 0198/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 6af0917d1602..32da6cb430d2 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.27"; +const WEAVE_VERSION = "0.1.29"; const STORAGE_FORMAT_VERSION = 2; const ENGINE_STORAGE_FORMAT_VERSION = 2; From 18228c27e85e6e2fe2da5a3e4a08d69e0913e4ad Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 2 Apr 2008 00:00:24 -0700 Subject: [PATCH 0199/1860] don't fail on invalid server deltas which don't apply to the local snapshot; wipe the local snapshot instead --- services/sync/modules/engines.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c60521cda27b..e0c09eed67fd 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -571,9 +571,17 @@ Engine.prototype = { break; } - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands.async(snap, self.cb, deltas[i]); - yield; + try { + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands.async(snap, self.cb, deltas[i]); + yield; + } + } catch (e) { + this._log.error("Error applying remote deltas to saved snapshot"); + this._log.error("Clearing local snapshot; next sync will merge"); + this._log.debug("Exception: " + Utils.exceptionStr(e)); + this._log.trace("Stack:\n" + Utils.stackTrace(e)); + this._snapshot.wipe(); } ret.status = 0; From 73696da50f9213816c2f1b7e9f3a8ba7efd41b9c Mon Sep 17 00:00:00 2001 From: Date: Thu, 3 Apr 2008 14:26:06 -0700 Subject: [PATCH 0200/1860] Fixed bugs in cookieSyncCore.itemExists and cookieStore.addCommand. The problem in addCommand was just a missing argument, but in itemExists I had to change the implementation to use cookieManager.enumerator rather than cookieManager.findMatchingCookie -- the latter function apparently does not exist in the nsICookieManager2 interface despite what MDC says about it. --- services/sync/modules/stores.js | 3 +- services/sync/modules/syncCores.js | 60 +++++++++++++++++------------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 59cc0f2b8d2e..b839e023c810 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -787,6 +787,7 @@ CookieStore.prototype = { command.data.name, command.data.value, command.data.isSecure, + command.data.isHttpOnly, command.data.isSession, command.data.expiry ); }, @@ -822,8 +823,8 @@ CookieStore.prototype = { // that the cookie was set -- only the expiry. TODO: Figure out // the best way to deal with this. this._log.info("CookieStore got editCommand: " + command ); - }, + }, wrap: function CookieStore_wrap() { // Return contents of this store, as JSON. diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index bbdb1ef7e296..0ed8b663f1fa 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -449,35 +449,45 @@ CookieSyncCore.prototype = { _itemExists: function CSC__itemExists(GUID) { - // true if a cookie with the given GUID exists. - // The GUID that we are passed should correspond to the keys - // that we define in the JSON returned by CookieStore.wrap() - // That is, it will be a string of the form - // "host:path:name". + // true if a cookie with the given GUID exists. + // The GUID that we are passed should correspond to the keys + // that we define in the JSON returned by CookieStore.wrap() + // That is, it will be a string of the form + // "host:path:name". + + // TODO verify that colons can't normally appear in any of + // the fields -- if they did it then we can't rely on .split(":") + // to parse correctly. + + let cookieArray = GUID.split( ":" ); + let cookieHost = cookieArray[0]; + let cookiePath = cookieArray[1]; + let cookieName = cookieArray[2]; - // TODO verify that colons can't normally appear in any of - // the fields -- if they did it then we can't rely on .split(":") - // to parse correctly. - - let unused = 0; // for outparam from findMatchingCookie - let cookieArray = GUID.split( ":" ); - // create a generic object to represent the cookie -- just has - // to implement nsICookie2 interface. - cookie = Object(); - cookie.host = cookieArray[0]; - cookie.path = cookieArray[1]; - cookie.name = cookieArray[2]; - return this._cookieManager.findMatchingCookie( cookie, unused ); + // alternate implementation would be to instantiate a cookie from + // cookieHost, cookiePath, and cookieName, + // then call cookieManager.cookieExists(). + let enumerator = this._cookieManager.enumerator; + while (enumerator.hasMoreElements()) + { + let aCookie = enumerator.getNext(); + if (aCookie.host == cookieHost && + aCookie.path == cookiePath && + aCookie.name == cookieName ) { + return true; + } + } + return false; }, _commandLike: function CSC_commandLike(a, b) { - // Method required to be overridden. - // a and b each have a .data and a .GUID - // If this function returns true, an editCommand will be - // generated to try to resolve the thing. - // but are a and b objects of the type in the Store or - // are they "commands"?? - return false; + // Method required to be overridden. + // a and b each have a .data and a .GUID + // If this function returns true, an editCommand will be + // generated to try to resolve the thing. + // but are a and b objects of the type in the Store or + // are they "commands"?? + return false; } }; CookieSyncCore.prototype.__proto__ = new SyncCore(); From bffe4b630ef29b65dad918c40a2b3f45f2927848 Mon Sep 17 00:00:00 2001 From: Date: Thu, 3 Apr 2008 14:30:34 -0700 Subject: [PATCH 0201/1860] Discovered that trying to use duck-typing in passing an object into cookieExists() will hard-crash Firefox, and figured out why; added comment to syncCores.js explaining this. --- services/sync/modules/syncCores.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 0ed8b663f1fa..a2a25122a708 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -449,24 +449,25 @@ CookieSyncCore.prototype = { _itemExists: function CSC__itemExists(GUID) { - // true if a cookie with the given GUID exists. - // The GUID that we are passed should correspond to the keys - // that we define in the JSON returned by CookieStore.wrap() - // That is, it will be a string of the form - // "host:path:name". + /* true if a cookie with the given GUID exists. + The GUID that we are passed should correspond to the keys + that we define in the JSON returned by CookieStore.wrap() + That is, it will be a string of the form + "host:path:name". */ - // TODO verify that colons can't normally appear in any of - // the fields -- if they did it then we can't rely on .split(":") - // to parse correctly. + /* TODO verify that colons can't normally appear in any of + the fields -- if they did it then we can't rely on .split(":") + to parse correctly.*/ let cookieArray = GUID.split( ":" ); let cookieHost = cookieArray[0]; let cookiePath = cookieArray[1]; let cookieName = cookieArray[2]; - // alternate implementation would be to instantiate a cookie from - // cookieHost, cookiePath, and cookieName, - // then call cookieManager.cookieExists(). + /* alternate implementation would be to instantiate a cookie from + cookieHost, cookiePath, and cookieName, then call + cookieManager.cookieExists(). Maybe that would have better + performance? This implementation seems pretty slow.*/ let enumerator = this._cookieManager.enumerator; while (enumerator.hasMoreElements()) { @@ -478,6 +479,12 @@ CookieSyncCore.prototype = { } } return false; + /* Note: We can't just call cookieManager.cookieExists() with a generic + javascript object with .host, .path, and .name attributes attatched. + cookieExists is implemented in C and does a hard static_cast to an + nsCookie object, so duck typing doesn't work (and in fact makes + Firefox hard-crash as the static_cast returns null and is not checked.) + */ }, _commandLike: function CSC_commandLike(a, b) { From 3fcf2fd6eeb275c54ff134362a5c8b1ae7c69c0f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 3 Apr 2008 17:16:22 -0700 Subject: [PATCH 0202/1860] rethrow exception when applyCommands fails --- services/sync/modules/engines.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e0c09eed67fd..c92321966aa3 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -582,6 +582,7 @@ Engine.prototype = { this._log.debug("Exception: " + Utils.exceptionStr(e)); this._log.trace("Stack:\n" + Utils.stackTrace(e)); this._snapshot.wipe(); + throw e; } ret.status = 0; From 7df9c0ed2b7315a61755cd18abe7c3da9bd40afb Mon Sep 17 00:00:00 2001 From: Date: Fri, 4 Apr 2008 10:49:41 -0700 Subject: [PATCH 0203/1860] Removed comment from load-weave.js that I put in while debugging and committed by accident. --- services/sync/modules/stores.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index b839e023c810..a7e70fe9a6d9 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -824,6 +824,15 @@ CookieStore.prototype = { // the best way to deal with this. this._log.info("CookieStore got editCommand: " + command ); + this._log.debug( "Info on command object passed in: " ); + for ( var x in command ) + { + this._log.debug( "Command." + x + " = " + command[x] ); + } + for ( var y in command.data ) + { + this._log.debug( "Command.data." + y + " = " + command.data[y] ); + } }, wrap: function CookieStore_wrap() { From cf52369889895926dc1335ce0690892eb1cf3f5b Mon Sep 17 00:00:00 2001 From: Date: Fri, 4 Apr 2008 12:08:04 -0700 Subject: [PATCH 0204/1860] Prettied up my code by using block comments and making my indents 2 spaces. --- services/sync/modules/stores.js | 168 ++++++++++++++--------------- services/sync/modules/syncCores.js | 16 +-- 2 files changed, 92 insertions(+), 92 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index a7e70fe9a6d9..1ee982236901 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -777,113 +777,113 @@ CookieStore.prototype = { }, _createCommand: function HistStore__createCommand(command) { - // we got a command to create a cookie in the local browser - // in order to sync with the server. + /* we got a command to create a cookie in the local browser + in order to sync with the server. */ - this._log.info("CookieStore got createCommand: " + command ); - // this assumes command.data fits the nsICookie2 interface - this._cookieManager.add( command.data.host, - command.data.path, - command.data.name, - command.data.value, - command.data.isSecure, - command.data.isHttpOnly, - command.data.isSession, - command.data.expiry ); + this._log.info("CookieStore got createCommand: " + command ); + // this assumes command.data fits the nsICookie2 interface + this._cookieManager.add( command.data.host, + command.data.path, + command.data.name, + command.data.value, + command.data.isSecure, + command.data.isHttpOnly, + command.data.isSession, + command.data.expiry ); }, _removeCommand: function CookieStore__removeCommand(command) { - // we got a command to remove a cookie from the local browser - // in order to sync with the server. - // command.data appears to be equivalent to what wrap() puts in - // the JSON dictionary. + /* we got a command to remove a cookie from the local browser + in order to sync with the server. + command.data appears to be equivalent to what wrap() puts in + the JSON dictionary. */ - this._log.info("CookieStore got removeCommand: " + command ); + this._log.info("CookieStore got removeCommand: " + command ); - // I think it goes like this, according to - // http://developer.mozilla.org/en/docs/nsICookieManager - // the last argument is "always block cookies from this domain?" - // and the answer is "no". - this._cookieManager.remove( command.data.host, - command.data.name, - command.data.path, - false ); + /* I think it goes like this, according to + http://developer.mozilla.org/en/docs/nsICookieManager + the last argument is "always block cookies from this domain?" + and the answer is "no". */ + this._cookieManager.remove( command.data.host, + command.data.name, + command.data.path, + false ); }, _editCommand: function CookieStore__editCommand(command) { - // we got a command to change a cookie in the local browser - // in order to sync with the server. + /* we got a command to change a cookie in the local browser + in order to sync with the server. - // The behavior should be that if the - // local copy of the cookie is more-recently modified, it should - // be kept, but if it's older, it should be replaced with the - // server's copy. + if the local copy of the cookie is more-recently modified, it + should be kept, but if it's older, it should be replaced with + the server's copy. + */ - // The nsICookie interface doesn't seem to include the date/time - // that the cookie was set -- only the expiry. TODO: Figure out - // the best way to deal with this. - this._log.info("CookieStore got editCommand: " + command ); + /* Problem: The nsICookie interface doesn't seem to include the date/time + that the cookie was set -- only the expiry. TODO: Figure out + the best way to deal with this. */ + this._log.info("CookieStore got editCommand: " + command ); - this._log.debug( "Info on command object passed in: " ); - for ( var x in command ) - { - this._log.debug( "Command." + x + " = " + command[x] ); - } - for ( var y in command.data ) - { - this._log.debug( "Command.data." + y + " = " + command.data[y] ); - } + this._log.debug( "Info on command object passed in: " ); + for ( var x in command ) + { + this._log.debug( "Command." + x + " = " + command[x] ); + } + for ( var y in command.data ) + { + this._log.debug( "Command.data." + y + " = " + command.data[y] ); + } }, wrap: function CookieStore_wrap() { - // Return contents of this store, as JSON. - // A dictionary of cookies where the keys are GUIDs and the - // values are sub-dictionaries containing all cookie fields. + /* Return contents of this store, as JSON. + A dictionary of cookies where the keys are GUIDs and the + values are sub-dictionaries containing all cookie fields. */ - let items = {}; - var iter = this._cookieManager.enumerator; - while (iter.hasMoreElements()){ - var cookie = iter.getNext(); - if (cookie instanceof Ci.nsICookie){ - // String used to identify cookies is - // host:path:name - let key = cookie.host + ":" + cookie.path + ":" + cookie.name - items[ key ] = { parentGUID: '', - name: cookie.name, - value: cookie.value, - isDomain: cookie.isDomain, - host: cookie.host, - path: cookie.path, - isSecure: cookie.isSecure, - // nsICookie2 values: - rawHost: cookie.rawHost, - isSession: cookie.isSession, - expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly } - - // http://developer.mozilla.org/en/docs/nsICookie - // Note: not syncing "expires", "status", or "policy" - // since they're deprecated. - - } - } + let items = {}; + var iter = this._cookieManager.enumerator; + while (iter.hasMoreElements()){ + var cookie = iter.getNext(); + if (cookie instanceof Ci.nsICookie){ + // String used to identify cookies is + // host:path:name + let key = cookie.host + ":" + cookie.path + ":" + cookie.name + items[ key ] = { parentGUID: '', + name: cookie.name, + value: cookie.value, + isDomain: cookie.isDomain, + host: cookie.host, + path: cookie.path, + isSecure: cookie.isSecure, + // nsICookie2 values: + rawHost: cookie.rawHost, + isSession: cookie.isSession, + expiry: cookie.expiry, + isHttpOnly: cookie.isHttpOnly } + + /* See http://developer.mozilla.org/en/docs/nsICookie + Note: not syncing "expires", "status", or "policy" + since they're deprecated. */ + + } + } return items; }, wipe: function CookieStore_wipe() { - // Remove everything from the store. Return nothing. - // TODO are the semantics of this just wiping out an internal - // buffer, or am I supposed to wipe out all cookies from - // the browser itself for reals? - this._cookieManager.removeAll() + /* Remove everything from the store. Return nothing. + TODO are the semantics of this just wiping out an internal + buffer, or am I supposed to wipe out all cookies from + the browser itself for reals? */ + this._cookieManager.removeAll() }, resetGUIDs: function CookieStore_resetGUIDs() { - // called in the case where remote/local sync GUIDs do not - // match. We do need to override this, but since we're deriving - // GUIDs from the cookie data itself and not generating them, - // there's basically no way they can get "out of sync" so there's - // nothing to do here. + /* called in the case where remote/local sync GUIDs do not + match. We do need to override this, but since we're deriving + GUIDs from the cookie data itself and not generating them, + there's basically no way they can get "out of sync" so there's + nothing to do here. */ } }; CookieStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index a2a25122a708..e3fce52513e1 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -442,8 +442,8 @@ CookieSyncCore.prototype = { if (!this.__cookieManager) this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. getService(Ci.nsICookieManager2); - // need the 2nd revision of the ICookieManager interface - // because it supports add() and the 1st one doesn't. + /* need the 2nd revision of the ICookieManager interface + because it supports add() and the 1st one doesn't. */ return this.__cookieManager }, @@ -488,12 +488,12 @@ CookieSyncCore.prototype = { }, _commandLike: function CSC_commandLike(a, b) { - // Method required to be overridden. - // a and b each have a .data and a .GUID - // If this function returns true, an editCommand will be - // generated to try to resolve the thing. - // but are a and b objects of the type in the Store or - // are they "commands"?? + /* Method required to be overridden. + a and b each have a .data and a .GUID + If this function returns true, an editCommand will be + generated to try to resolve the thing. + but are a and b objects of the type in the Store or + are they "commands"?? */ return false; } }; From e4801d6ade09d9b0a2df76fe6786be47f66e717b Mon Sep 17 00:00:00 2001 From: Date: Fri, 4 Apr 2008 12:09:04 -0700 Subject: [PATCH 0205/1860] Semicolon was missing in cookieStore.wrap() --- services/sync/modules/stores.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 1ee982236901..5920d2e6e3ac 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -847,19 +847,19 @@ CookieStore.prototype = { if (cookie instanceof Ci.nsICookie){ // String used to identify cookies is // host:path:name - let key = cookie.host + ":" + cookie.path + ":" + cookie.name - items[ key ] = { parentGUID: '', - name: cookie.name, - value: cookie.value, - isDomain: cookie.isDomain, - host: cookie.host, - path: cookie.path, - isSecure: cookie.isSecure, - // nsICookie2 values: - rawHost: cookie.rawHost, - isSession: cookie.isSession, - expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly } + let key = cookie.host + ":" + cookie.path + ":" + cookie.name; + items[ key ] = { parentGUID: '', + name: cookie.name, + value: cookie.value, + isDomain: cookie.isDomain, + host: cookie.host, + path: cookie.path, + isSecure: cookie.isSecure, + // nsICookie2 values: + rawHost: cookie.rawHost, + isSession: cookie.isSession, + expiry: cookie.expiry, + isHttpOnly: cookie.isHttpOnly } /* See http://developer.mozilla.org/en/docs/nsICookie Note: not syncing "expires", "status", or "policy" From e3a081ac0de5eb46e7bfeff122b6b8bc7c07afa9 Mon Sep 17 00:00:00 2001 From: Date: Mon, 7 Apr 2008 11:53:55 -0700 Subject: [PATCH 0206/1860] Implemented cookieStore.editCommand(). --- services/sync/modules/stores.js | 56 ++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 5920d2e6e3ac..e89aa3eb68d3 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -812,27 +812,45 @@ CookieStore.prototype = { _editCommand: function CookieStore__editCommand(command) { /* we got a command to change a cookie in the local browser - in order to sync with the server. - - if the local copy of the cookie is more-recently modified, it - should be kept, but if it's older, it should be replaced with - the server's copy. - */ - - /* Problem: The nsICookie interface doesn't seem to include the date/time - that the cookie was set -- only the expiry. TODO: Figure out - the best way to deal with this. */ + in order to sync with the server. */ this._log.info("CookieStore got editCommand: " + command ); - this._log.debug( "Info on command object passed in: " ); - for ( var x in command ) - { - this._log.debug( "Command." + x + " = " + command[x] ); - } - for ( var y in command.data ) - { - this._log.debug( "Command.data." + y + " = " + command.data[y] ); - } + /* Look up the cookie that matches the one in the command: */ + var iter = this._cookieManager.enumerator; + var matchingCookie = null; + while (iter.hasMoreElements()){ + let cookie = iter.getNext(); + if (cookie instanceof Ci.nsICookie){ + // see if host:path:name of cookie matches GUID given in command + let key = cookie.host + ":" + cookie.path + ":" + cookie.name; + if (key == command.GUID) { + matchingCookie = cookie; + break; + } + } + } + // Update values in the cookie: + for (var key in command.data) { + // Whatever values command.data has, use them + matchingCookie[ key ] = command.data[ key ] + } + // Remove the old incorrect cookie from the manager: + this._cookieManager.remove( matchingCookie.host, + matchingCookie.name, + matchingCookie.path, + false ); + // Re-add the new updated cookie: + this._cookieManager.add( matchingCookie.host, + matchingCookie.path, + matchingCookie.name, + matchingCookie.value, + matchingCookie.isSecure, + matchingCookie.isHttpOnly, + matchingCookie.isSession, + matchingCookie.expiry ); + + // Also, there's an exception raised because + // this._data[comand.GUID] is undefined }, wrap: function CookieStore_wrap() { From 1adee5531c436a5657b9f9442261dd5fe2839582 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 10 Apr 2008 21:38:15 -0700 Subject: [PATCH 0207/1860] Various improvements: * async generators: much better stack traces * dav: use global identity system rather than login/logout to manage usernames and passwords. * dav: there is a checkLogin() instead of login() which can be used to verify auth at any time. * dav: make it so that we can (only internally atm) hold multiple locks for different URLs. * identity: add an identity manager singleton service to keep identities globally, referenced by a name, with aliasing support (so e.g., dav can ask for the 'dav' identity, while something else can alias 'dav' to another identity). * service: keep track of logged in status here, rather than in the dav service. Use the global id manager. --- services/sync/modules/async.js | 94 ++++++++++++--------- services/sync/modules/dav.js | 133 +++++++++++++----------------- services/sync/modules/identity.js | 33 +++++++- services/sync/modules/service.js | 17 ++-- services/sync/modules/util.js | 6 +- 5 files changed, 156 insertions(+), 127 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index ca2ff654bacd..9ff028beb2a1 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -48,25 +48,21 @@ Cu.import("resource://weave/util.js"); * Asynchronous generator helpers */ -function AsyncException(generator, message) { - this.generator = generator; +function AsyncException(initFrame, message) { this.message = message; - this._trace = this.generator.trace; + this._trace = initFrame; } AsyncException.prototype = { - get generator() { return this._generator; }, - set generator(value) { - if (!(value instanceof Generator)) - throw "expected type 'Generator'"; - this._generator = value; - }, - get message() { return this._message; }, set message(value) { this._message = value; }, get trace() { return this._trace; }, set trace(value) { this._trace = value; }, - + + addFrame: function AsyncException_addFrame(frame) { + this.trace += (this.trace? "\n" : "") + formatFrame(frame); + }, + toString: function AsyncException_toString() { return this.message; } @@ -80,25 +76,16 @@ function Generator(object, method, onComplete, args) { this._method = method; this.onComplete = onComplete; this._args = args; - - let frame = Components.stack.caller; - if (frame.name == "Async_run") - frame = frame.caller; - if (frame.name == "Async_sugar") - frame = frame.caller; - - this._initFrame = frame; + this._initFrame = Components.stack.caller; + // skip our frames + // FIXME: we should have a pref for this, for debugging async.js itself + while (this._initFrame.name.match(/^Async(Gen|)_/)) + this._initFrame = this._initFrame.caller; } Generator.prototype = { get name() { return this._method.name; }, get generator() { return this._generator; }, - // set to true to error when generators to bottom out/return. - // you will then have to ensure that all generators yield as the - // last thing they do, and never call 'return' - get errorOnStop() { return this._errorOnStop; }, - set errorOnStop(value) { this._errorOnStop = value; }, - get cb() { let self = this, cb = function(data) { self.cont(data); }; cb.parentGenerator = this; @@ -134,33 +121,31 @@ Generator.prototype = { }, get trace() { - return Utils.stackTrace(this._initFrame) + - "JS frame :: unknown (async) :: " + this.name; + return "unknown (async) :: " + this.name + "\n" + trace(this._initFrame); }, _handleException: function AsyncGen__handleException(e) { if (e instanceof StopIteration) { - if (this.errorOnStop) { - this._log.error("[" + this.name + "] Generator stopped unexpectedly"); - this._log.trace("Stack trace:\n" + this.trace); - this._exception = "Generator stopped unexpectedly"; // don't propagate StopIteration - } + // skip to calling done() } else if (this.onComplete.parentGenerator instanceof Generator) { this._log.trace("[" + this.name + "] Saving exception and stack trace"); - this._log.trace(Async.exceptionStr(this, e)); + this._log.trace("Exception: " + Utils.exceptionStr(e)); - if (e instanceof AsyncException) - e.trace = this.trace + e.trace? "\n" + e.trace : ""; - else - e = new AsyncException(this, e); + if (e instanceof AsyncException) { + // FIXME: attempt to skip repeated frames, which can happen if the + // child generator never yielded. Would break for valid repeats (recursion) + if (e.trace.indexOf(formatFrame(this._initFrame)) == -1) + e.addFrame(this._initFrame); + } else { + e = new AsyncException(this.trace, e); + } this._exception = e; } else { - this._log.error(Async.exceptionStr(this, e)); - this._log.debug("Stack trace:\n" + this.trace + - (e.trace? "\n" + e.trace : "")); + this._log.error("Exception: " + Utils.exceptionStr(e)); + this._log.debug("Stack trace:\n" + (e.trace? e.trace : this.trace)); } // continue execution of caller. @@ -174,6 +159,7 @@ Generator.prototype = { }, run: function AsyncGen_run() { + this._continued = false; try { this._generator = this._method.apply(this._object, this._args); this.generator.next(); // must initialize before sending @@ -185,6 +171,7 @@ Generator.prototype = { }, cont: function AsyncGen_cont(data) { + this._continued = true; try { this.generator.send(data); } catch (e) { if (!(e instanceof StopIteration) || !this._timer) @@ -239,13 +226,38 @@ Generator.prototype = { this._log.error("Exception caught from onComplete handler of " + this.name + " generator"); this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.trace("Current stack trace:\n" + Utils.stackTrace(Components.stack)); + this._log.trace("Current stack trace:\n" + trace(Components.stack)); this._log.trace("Initial stack trace:\n" + this.trace); } } } }; +function formatFrame(frame) { + // FIXME: sort of hackish, might be confusing if there are multiple + // extensions with similar filenames + let tmp = frame.filename.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); + tmp += ":" + frame.lineNumber + " :: " + frame.name; + return tmp; +} + +function trace(frame, str) { + if (!str) + str = ""; + + // skip our frames + // FIXME: we should have a pref for this, for debugging async.js itself + while (frame.name.match(/^Async(Gen|)_/)) + frame = frame.caller; + + if (frame.caller) + str = trace(frame.caller, str); + str = formatFrame(frame) + (str? "\n" : "") + str; + + return str; +} + + Async = { // Use: diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 8a4085724ee9..76c5ab0ad320 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -43,21 +43,25 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; Utils.lazy(this, 'DAV', DAVCollection); +let DAVLocks = {}; + /* * DAV object * Abstracts the raw DAV commands */ -function DAVCollection(baseURL) { - this._baseURL = baseURL; +function DAVCollection(baseURL, defaultPrefix) { + this.baseURL = baseURL; + this.defaultPrefix = defaultPrefix; this._authProvider = new DummyAuthProvider(); this._log = Log4Moz.Service.getLogger("Service.DAV"); this._log.level = @@ -73,24 +77,29 @@ DAVCollection.prototype = { return this.__dp; }, - _auth: null, - get baseURL() { return this._baseURL; }, set baseURL(value) { - if (value[value.length-1] != '/') + if (value && value[value.length-1] != '/') value = value + '/'; this._baseURL = value; }, - _loggedIn: false, - get loggedIn() { - return this._loggedIn; + get defaultPrefix() { + return this._defaultPrefix; + }, + set defaultPrefix(value) { + if (value && value[value.length-1] != '/') + value = value + '/'; + if (value && value[0] == '/') + value = value.slice(1); + this._defaultPrefix = value; }, get locked() { - return !this._lockAllowed || this._token != null; + return !this._lockAllowed || (DAVLocks['default'] && + DAVLocks['default'].token); }, _lockAllowed: true, @@ -148,10 +157,14 @@ DAVCollection.prototype = { }, get _defaultHeaders() { - return {'Authorization': this._auth? this._auth : '', - 'Content-type': 'text/plain', - 'If': this._token? - "<" + this._baseURL + "> (<" + this._token + ">)" : ''}; + let h = {'Content-type': 'text/plain'}, + id = ID.get('DAV:default'), + lock = DAVLocks['default']; + if (id) + h['Authorization'] = 'Basic ' + btoa(id.username + ":" + id.password); + if (lock) + h['If'] = "<" + lock.URL + "> (<" + lock.token + ">)"; + return h; }, // mkdir -p @@ -242,7 +255,7 @@ DAVCollection.prototype = { }, UNLOCK: function DC_UNLOCK(path, onComplete) { - let headers = {'Lock-Token': '<' + this._token + '>'}; + let headers = {'Lock-Token': '<' + DAVLocks['default'].token + '>'}; headers.__proto__ = this._defaultHeaders; return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers); }, @@ -266,13 +279,13 @@ DAVCollection.prototype = { let ret = []; try { - let tokens = Utils.xpath(resp.responseXML, '//D:href'); + let elts = Utils.xpath(resp.responseXML, '//D:href'); // FIXME: shouldn't depend on the first one being the root - let root = tokens.iterateNext(); + let root = elts.iterateNext(); root = root.textContent; - let token; - while (token = tokens.iterateNext()) - ret.push(token.textContent.replace(root, '')); + let elt; + while (elt = elts.iterateNext()) + ret.push(elt.textContent.replace(root, '')); } catch (e) {} self.done(ret); @@ -280,19 +293,10 @@ DAVCollection.prototype = { // Login / Logout - login: function DC_login(username, password) { + checkLogin: function DC_checkLogin() { let self = yield; - if (this._loggedIn) { - this._log.debug("Login requested, but already logged in"); - self.done(true); - yield; - } - - this._log.debug("Logging in"); - - let URI = Utils.makeURI(this._baseURL); - this._auth = "Basic " + btoa(username + ":" + password); + this._log.debug("Checking login"); // Make a call to make sure it's working this.GET("", self.cb); @@ -304,17 +308,9 @@ DAVCollection.prototype = { yield; } - this._loggedIn = true; - self.done(true); }, - logout: function DC_logout() { - this._log.trace("Logging out (forgetting auth header)"); - this._loggedIn = false; - this.__auth = null; - }, - // Locking _getActiveLock: function DC__getActiveLock() { @@ -344,18 +340,17 @@ DAVCollection.prototype = { this._log.trace("Found an active lock token"); else this._log.trace("No active lock token found"); - self.done(ret); + self.done({URL: this._baseURL, token: ret}); }, lock: function DC_lock() { let self = yield; - this._token = null; this._log.trace("Acquiring lock"); - if (this._token) { + if (DAVLocks['default']) { this._log.debug("Lock called, but we already hold a token"); - self.done(this._token); + self.done(DAVLocks['default']); yield; } @@ -369,21 +364,27 @@ DAVCollection.prototype = { if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) { - self.done(this._token); + self.done(); yield; } let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); - if (token) - this._token = token.textContent; + if (token) { + DAVLocks['default'] = { + URL: this._baseURL, + token: token.textContent + }; + } - if (this._token) - this._log.trace("Lock acquired"); - else + if (!DAVLocks['default']) { this._log.warn("Could not acquire lock"); + self.done(); + return; + } - self.done(this._token); + this._log.trace("Lock acquired"); + self.done(DAVLocks['default']); }, unlock: function DC_unlock() { @@ -406,14 +407,9 @@ DAVCollection.prototype = { yield; } - this._token = null; - - if (this._token) - this._log.trace("Could not release lock"); - else - this._log.trace("Lock released (or we didn't have one)"); - - self.done(!this._token); + delete DAVLocks['default'] + this._log.trace("Lock released (or we didn't have one)"); + self.done(true); }, forceUnlock: function DC_forceUnlock() { @@ -423,9 +419,9 @@ DAVCollection.prototype = { this._log.debug("Forcibly releasing any server locks"); this._getActiveLock.async(this, self.cb); - this._token = yield; + DAVLocks['default'] = yield; - if (!this._token) { + if (!DAVLocks['default']) { this._log.debug("No server lock found"); self.done(true); yield; @@ -440,21 +436,6 @@ DAVCollection.prototype = { else this._log.trace("No lock released"); self.done(unlocked); - }, - - stealLock: function DC_stealLock() { - let self = yield; - let stolen = null; - - this.forceUnlock.async(this, self.cb); - let unlocked = yield; - - if (unlocked) { - this.lock.async(this, self.cb); - stolen = yield; - } - - self.done(stolen); } }; diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 20d8c9cddf0f..cb953ef2446a 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Identity']; +const EXPORTED_SYMBOLS = ['Identity', 'ID']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -46,6 +46,37 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Utils.lazy(this, 'ID', IDManager); + +// For storing identities we'll use throughout Weave +function IDManager() { + this._ids = {}; + this._aliases = {}; +} +IDManager.prototype = { + get: function IDMgr_get(name) { + if (this._aliases[name]) + return this._ids[this._aliases[name]]; + else + return this._ids[name] + }, + set: function IDMgr_set(name, id) { + this._ids[name] = id; + }, + del: function IDMgr_del(name) { + delete this._ids[name]; + }, + getAlias: function IDMgr_getAlias(alias) { + return this._aliases[alias]; + }, + setAlias: function IDMgr_setAlias(name, alias) { + this._aliases[alias] = name; + }, + delAlias: function IDMgr_delAlias(alias) { + delete this._aliases[alias]; + } +}; + /* * Identity * These objects hold a realm, username, and password diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ba6673200775..8d7f868b4842 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -176,7 +176,7 @@ WeaveSvc.prototype = { get userPath() { return this._mozId.userHash; }, get currentUser() { - if (DAV.loggedIn) + if (this._loggedIn) return this.username; return null; }, @@ -419,6 +419,9 @@ WeaveSvc.prototype = { this._mozId.setTempPassword(password); this._cryptoId.setTempPassword(passphrase); + ID.set('MozID', this._mozId); + ID.setAlias('MozID', 'DAV:default'); + this._log.debug("Logging in"); if (!this.username) @@ -431,14 +434,14 @@ WeaveSvc.prototype = { serverURL = serverURL + '/'; DAV.baseURL = serverURL + "user/" + this.userPath + "/"; - DAV.login.async(DAV, self.cb, this.username, this.password); + DAV.checkLogin.async(DAV, self.cb, this.username, this.password); let success = yield; if (!success) { try { this._checkUserDir.async(this, self.cb); yield; } catch (e) { /* FIXME: tmp workaround for services.m.c */ } - DAV.login.async(DAV, self.cb, this.username, this.password); + DAV.checkLogin.async(DAV, self.cb, this.username, this.password); let success = yield; if (!success) throw "Login failed"; @@ -449,12 +452,14 @@ WeaveSvc.prototype = { this._keyCheck.async(this, self.cb); yield; + this._loggedIn = true; + self.done(true); }, logout: function WeaveSync_logout() { this._log.info("Logging out"); - DAV.logout(); + this._loggedIn = false; this._mozId.setTempPassword(null); // clear cached password this._cryptoId.setTempPassword(null); // and passphrase this._os.notifyObservers(null, "weave:service:logout:success", ""); @@ -500,7 +505,7 @@ WeaveSvc.prototype = { _sync: function WeaveSync__sync() { let self = yield; - if (!DAV.loggedIn) + if (!this._loggedIn) throw "Can't sync: Not logged in"; this._versionCheck.async(this, self.cb); @@ -540,7 +545,7 @@ WeaveSvc.prototype = { _resetServer: function WeaveSync__resetServer() { let self = yield; - if (!DAV.loggedIn) + if (!this._loggedIn) throw "Can't reset server: Not logged in"; this._bmkEngine.resetServer(self.cb); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 3d19bb723022..f87aeb96af21 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -61,7 +61,7 @@ let Utils = { }; dest.__defineGetter__(prop, getter); }, - + deepEquals: function Weave_deepEquals(a, b) { if (!a && !b) return true; @@ -136,7 +136,7 @@ let Utils = { if (!str) str = ""; - str += stackFrame + "\n"; + str = stackFrame + "\n" + str; return str; }, @@ -248,7 +248,7 @@ let Utils = { return tmp; }, - + open: function open(pathOrFile, mode, perms) { let stream, file; From 4e3d850b9400c013da17df554b69be6a26189ba1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 14 Apr 2008 18:53:35 -0700 Subject: [PATCH 0208/1860] add engine registration service; use global id service in service.js --- services/sync/modules/engines.js | 41 ++++++++-- services/sync/modules/service.js | 126 ++++++++++++------------------- 2 files changed, 83 insertions(+), 84 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c92321966aa3..f5d43429a427 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -34,7 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Engine', 'BookmarksEngine', 'HistoryEngine', 'CookieEngine']; +const EXPORTED_SYMBOLS = ['Engines', 'Engine', + 'BookmarksEngine', 'HistoryEngine', 'CookieEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -54,9 +55,36 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; -function Engine(davCollection, pbeId) { - //this._init(davCollection, pbeId); +// Singleton service, holds registered engines + +Utils.lazy(this, 'Engines', EngineManagerSvc); + +function EngineManagerSvc() { + this._engines = {}; } +EngineManagerSvc.prototype = { + get: function EngMgr_get(name) { + return this._engines[name] + }, + getAll: function EngMgr_getAll() { + let ret = []; + for (key in this._engines) { + ret.push(this._engines[key]); + } + return ret; + }, + register: function EngMgr_register(engine) { + this._engines[engine.name] = engine; + }, + unregister: function EngMgr_unregister(val) { + let name = val; + if (val instanceof Engine) + name = val.name; + delete this._engines[name]; + } +}; + +function Engine() {} Engine.prototype = { // "default-engine"; get name() { throw "name property must be overridden in subclasses"; }, @@ -794,7 +822,7 @@ function BookmarksEngine(davCollection, pbeId) { this._init(davCollection, pbeId); } BookmarksEngine.prototype = { - get name() { return "bookmarks-engine"; }, + get name() { return "bookmarks"; }, get logName() { return "BmkEngine"; }, get serverPrefix() { return "user-data/bookmarks/"; }, @@ -905,7 +933,7 @@ function HistoryEngine(davCollection, pbeId) { this._init(davCollection, pbeId); } HistoryEngine.prototype = { - get name() { return "history-engine"; }, + get name() { return "history"; }, get logName() { return "HistEngine"; }, get serverPrefix() { return "user-data/history/"; }, @@ -925,12 +953,11 @@ HistoryEngine.prototype = { }; HistoryEngine.prototype.__proto__ = new Engine(); -// Jono: the following is copy-and-paste code function CookieEngine(davCollection, pbeId) { this._init(davCollection, pbeId); } CookieEngine.prototype = { - get name() { return "cookie-engine"; }, + get name() { return "cookies"; }, get logName() { return "CookieEngine"; }, get serverPrefix() { return "user-data/cookies/"; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8d7f868b4842..721c29c8f0da 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -77,6 +77,17 @@ function WeaveSvc() { this._initLogs(); this._log.info("Weave Sync Service Initializing"); + ID.set('WeaveID', new Identity('Mozilla Services Password', this.username)); + ID.setAlias('WeaveID', 'DAV:default'); + + ID.set('WeaveCryptoID', + new Identity('Mozilla Services Encryption Passphrase', this.username)); + //ID.setAlias('WeaveCryptoID', '...'); + + Engines.register(new BookmarksEngine(DAV, ID.get('WeaveCryptoID'))); + Engines.register(new HistoryEngine(DAV, ID.get('WeaveCryptoID'))); + Engines.register(new CookieEngine(DAV, ID.get('WeaveCryptoID'))); + Utils.prefs.addObserver("", this, false); if (!this.enabled) { @@ -109,50 +120,9 @@ WeaveSvc.prototype = { return this.__dirSvc; }, - // FIXME: engines should be loaded dynamically somehow / need API to register - - __bmkEngine: null, - get _bmkEngine() { - if (!this.__bmkEngine) - this.__bmkEngine = new BookmarksEngine(DAV, this._cryptoId); - return this.__bmkEngine; - }, - - __histEngine: null, - get _histEngine() { - if (!this.__histEngine) - this.__histEngine = new HistoryEngine(DAV, this._cryptoId); - return this.__histEngine; - }, - - __cookieEngine: null, - get _cookieEngine() { - // This gets an error that "CookieEngine" is undefined. Why? - // BookmarksEngine and HistoryEngine are both defined in engines.js - // and so is CookieEngine, but... - if (!this.__cookieEngine) - this.__cookieEngine = new CookieEngine(DAV, this._cryptoId); - return this.__cookieEngine; - }, - // Timer object for automagically syncing _scheduleTimer: null, - __mozId: null, - get _mozId() { - if (this.__mozId === null) - this.__mozId = new Identity('Mozilla Services Password', this.username); - return this.__mozId; - }, - - __cryptoId: null, - get _cryptoId() { - if (this.__cryptoId === null) - this.__cryptoId = new Identity('Mozilla Services Encryption Passphrase', - this.username); - return this.__cryptoId; - }, - get username() { return Utils.prefs.getCharPref("username"); }, @@ -163,17 +133,17 @@ WeaveSvc.prototype = { Utils.prefs.clearUserPref("username"); // fixme - need to loop over all Identity objects - needs some rethinking... - this._mozId.username = value; - this._cryptoId.username = value; + ID.get('WeaveID').username = value; + ID.get('WeaveCryptoID').username = value; }, - get password() { return this._mozId.password; }, - set password(value) { this._mozId.password = value; }, + get password() { return ID.get('WeaveID').password; }, + set password(value) { ID.get('WeaveID').password = value; }, - get passphrase() { return this._cryptoId.password; }, - set passphrase(value) { this._cryptoId.password = value; }, + get passphrase() { return ID.get('WeaveCryptoID').password; }, + set passphrase(value) { ID.get('WeaveCryptoID').password = value; }, - get userPath() { return this._mozId.userHash; }, + get userPath() { return ID.get('WeaveID').userHash; }, get currentUser() { if (this._loggedIn) @@ -350,9 +320,10 @@ WeaveSvc.prototype = { "Could not get private key from server", [[200,300],404]); if (keyResp.status != 404) { - this._cryptoId.privkey = keyResp.responseText; - Crypto.RSAkeydecrypt.async(Crypto, self.cb, this._cryptoId); - this._cryptoId.pubkey = yield; + let id = ID.get('WeaveCryptoID'); + id.privkey = keyResp.responseText; + Crypto.RSAkeydecrypt.async(Crypto, self.cb, id); + id.pubkey = yield; } else { this._generateKeys.async(this, self.cb); @@ -364,11 +335,13 @@ WeaveSvc.prototype = { let self = yield; this._log.debug("Generating new RSA key"); - Crypto.RSAkeygen.async(Crypto, self.cb, this._cryptoId); + + let id = ID.get('WeaveCryptoID'); + Crypto.RSAkeygen.async(Crypto, self.cb, id); let [privkey, pubkey] = yield; - this._cryptoId.privkey = privkey; - this._cryptoId.pubkey = pubkey; + id.privkey = privkey; + id.pubkey = pubkey; DAV.MKCOL("private/", self.cb); let ret = yield; @@ -416,11 +389,8 @@ WeaveSvc.prototype = { // cache password & passphrase // if null, we'll try to get them from the pw manager below - this._mozId.setTempPassword(password); - this._cryptoId.setTempPassword(passphrase); - - ID.set('MozID', this._mozId); - ID.setAlias('MozID', 'DAV:default'); + ID.get('WeaveID').setTempPassword(password); + ID.get('WeaveCryptoID').setTempPassword(passphrase); this._log.debug("Logging in"); @@ -460,8 +430,8 @@ WeaveSvc.prototype = { logout: function WeaveSync_logout() { this._log.info("Logging out"); this._loggedIn = false; - this._mozId.setTempPassword(null); // clear cached password - this._cryptoId.setTempPassword(null); // and passphrase + ID.get('WeaveID').setTempPassword(null); // clear cached password + ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase this._os.notifyObservers(null, "weave:service:logout:success", ""); }, @@ -515,20 +485,20 @@ WeaveSvc.prototype = { yield; if (Utils.prefs.getBoolPref("bookmarks")) { - this._notify(this._bmkEngine.name + ":sync", - this._syncEngine, this._bmkEngine).async(this, self.cb); + this._notify(Engines.get("bookmarks").name + "-engine:sync", + this._syncEngine, Engines.get("bookmarks")).async(this, self.cb); yield; - this._bmkEngine.syncMounts(self.cb); // FIXME + Engines.get("bookmarks").syncMounts(self.cb); // FIXME yield; } if (Utils.prefs.getBoolPref("history")) { - this._notify(this._histEngine.name + ":sync", - this._syncEngine, this._histEngine).async(this, self.cb); + this._notify(Engines.get("history").name + "-engine:sync", + this._syncEngine, Engines.get("history")).async(this, self.cb); yield; } if (Utils.prefs.getBoolPref("cookies")) { - this._notify(this._cookieEngine.name + ":sync", - this._syncEngine, this._cookieEngine).async(this, self.cb); + this._notify(Engines.get("cookies").name + "-engine:sync", + this._syncEngine, Engines.get("cookies")).async(this, self.cb); yield; } }, @@ -548,10 +518,11 @@ WeaveSvc.prototype = { if (!this._loggedIn) throw "Can't reset server: Not logged in"; - this._bmkEngine.resetServer(self.cb); - yield; - this._histEngine.resetServer(self.cb); - yield; + let engines = Engines.getAll(); + for (let i = 0; i < engines.length; i++) { + engines[i].resetServer(self.cb); + yield; + } }, resetClient: function WeaveSync_resetClient(onComplete) { @@ -560,10 +531,11 @@ WeaveSvc.prototype = { }, _resetClient: function WeaveSync__resetClient() { let self = yield; - this._bmkEngine.resetClient(self.cb); - yield; - this._histEngine.resetClient(self.cb); - yield; + let engines = Engines.getAll(); + for (let i = 0; i < engines.length; i++) { + engines[i].resetClient(self.cb); + yield; + } }, shareBookmarks: function WeaveSync_shareBookmarks(onComplete, username) { @@ -573,7 +545,7 @@ WeaveSvc.prototype = { }, _shareBookmarks: function WeaveSync__shareBookmarks(username) { let self = yield; - this._bmkEngine.share(self.cb, username); + Engines.get("bookmarks").share(self.cb, username); let ret = yield; self.done(ret); } From a6b8cbfa14636d9724e8135a8134b240f8ae9373 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 15 Apr 2008 17:21:34 -0700 Subject: [PATCH 0209/1860] engines now get the pbe identity directly from the identity manager; engines now know their 'enabled' status (pref); main service syncs *all* registered (enabled) engines --- services/sync/modules/engines.js | 32 +++++++++++++------- services/sync/modules/service.js | 50 ++++++++++++++++++-------------- services/sync/services-sync.js | 6 ++-- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f5d43429a427..01d7c3bbf636 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -102,6 +102,10 @@ Engine.prototype = { get snapshotFile() { return this.serverPrefix + "snapshot.json"; }, get deltasFile() { return this.serverPrefix + "deltas.json"; }, + get enabled() { + return Utils.prefs.getBoolPref("engine." + this.name); + }, + __os: null, get _os() { if (!this.__os) @@ -143,10 +147,18 @@ Engine.prototype = { this.__snapshot = value; }, - _init: function Engine__init(davCollection, pbeId) { - this._pbeId = pbeId; - this._engineId = new Identity(pbeId.realm + " - " + this.logName, - pbeId.username); + get _pbeId() { + let id = ID.get('Engine:PBE:' + this.name); + if (!id) + id = ID.get('Engine:PBE:default'); + if (!id) + throw "No identity found for engine PBE!"; + return id; + }, + + _init: function Engine__init() { + this._engineId = new Identity(this._pbeId.realm + " - " + this.logName, + this._pbeId.username); this._log = Log4Moz.Service.getLogger("Service." + this.logName); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.engine")]; @@ -818,8 +830,8 @@ Engine.prototype = { } }; -function BookmarksEngine(davCollection, pbeId) { - this._init(davCollection, pbeId); +function BookmarksEngine(pbeId) { + this._init(pbeId); } BookmarksEngine.prototype = { get name() { return "bookmarks"; }, @@ -929,8 +941,8 @@ BookmarksEngine.prototype = { }; BookmarksEngine.prototype.__proto__ = new Engine(); -function HistoryEngine(davCollection, pbeId) { - this._init(davCollection, pbeId); +function HistoryEngine(pbeId) { + this._init(pbeId); } HistoryEngine.prototype = { get name() { return "history"; }, @@ -953,8 +965,8 @@ HistoryEngine.prototype = { }; HistoryEngine.prototype.__proto__ = new Engine(); -function CookieEngine(davCollection, pbeId) { - this._init(davCollection, pbeId); +function CookieEngine(pbeId) { + this._init(pbeId); } CookieEngine.prototype = { get name() { return "cookies"; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 721c29c8f0da..5db097c01fa0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -77,17 +77,21 @@ function WeaveSvc() { this._initLogs(); this._log.info("Weave Sync Service Initializing"); + // Create Weave identities (for logging in, and for encryption) ID.set('WeaveID', new Identity('Mozilla Services Password', this.username)); - ID.setAlias('WeaveID', 'DAV:default'); - ID.set('WeaveCryptoID', new Identity('Mozilla Services Encryption Passphrase', this.username)); - //ID.setAlias('WeaveCryptoID', '...'); - Engines.register(new BookmarksEngine(DAV, ID.get('WeaveCryptoID'))); - Engines.register(new HistoryEngine(DAV, ID.get('WeaveCryptoID'))); - Engines.register(new CookieEngine(DAV, ID.get('WeaveCryptoID'))); + // Set up aliases for other modules to use our IDs + ID.setAlias('WeaveID', 'DAV:default'); + ID.setAlias('WeaveCryptoID', 'Engine:PBE:default'); + // Register built-in engines + Engines.register(new BookmarksEngine()); + Engines.register(new HistoryEngine()); + Engines.register(new CookieEngine()); + + // Other misc startup Utils.prefs.addObserver("", this, false); if (!this.enabled) { @@ -484,22 +488,18 @@ WeaveSvc.prototype = { this._keyCheck.async(this, self.cb); yield; - if (Utils.prefs.getBoolPref("bookmarks")) { - this._notify(Engines.get("bookmarks").name + "-engine:sync", - this._syncEngine, Engines.get("bookmarks")).async(this, self.cb); - yield; - Engines.get("bookmarks").syncMounts(self.cb); // FIXME - yield; - } - if (Utils.prefs.getBoolPref("history")) { - this._notify(Engines.get("history").name + "-engine:sync", - this._syncEngine, Engines.get("history")).async(this, self.cb); - yield; - } - if (Utils.prefs.getBoolPref("cookies")) { - this._notify(Engines.get("cookies").name + "-engine:sync", - this._syncEngine, Engines.get("cookies")).async(this, self.cb); - yield; + let engines = Engines.getAll(); + for (let i = 0; i < engines.length; i++) { + if (engines[i].enabled) { + this._notify(engines[i].name + "-engine:sync", + this._syncEngine, engines[i]).async(this, self.cb); + engines[i].resetServer(self.cb); + yield; + if (engines[i].name == "bookmarks") { // FIXME + Engines.get("bookmarks").syncMounts(self.cb); + yield; + } + } } }, _syncEngine: function WeaveSvc__syncEngine(engine) { @@ -520,6 +520,8 @@ WeaveSvc.prototype = { let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { + if (!engines[i].enabled) + continue; engines[i].resetServer(self.cb); yield; } @@ -533,6 +535,8 @@ WeaveSvc.prototype = { let self = yield; let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { + if (!engines[i].enabled) + continue; engines[i].resetClient(self.cb); yield; } @@ -545,6 +549,8 @@ WeaveSvc.prototype = { }, _shareBookmarks: function WeaveSync__shareBookmarks(username) { let self = yield; + if (Engines.get("bookmarks").enabled) + return; Engines.get("bookmarks").share(self.cb, username); let ret = yield; self.done(ret); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index e0c46f6f5e52..20c08d3566eb 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -11,10 +11,10 @@ pref("extensions.weave.ui.sharebookmarks", false); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); -pref("extensions.weave.bookmarks", true); -pref("extensions.weave.history", true); -pref("extensions.weave.cookies", false ); pref("extensions.weave.schedule", 1); +pref("extensions.weave.engine.bookmarks", true); +pref("extensions.weave.engine.history", true); +pref("extensions.weave.engine.cookies", false ); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 34fec2e43a9994f531f6f14f1bb2e56c56afc58c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 16 Apr 2008 14:46:57 -0700 Subject: [PATCH 0210/1860] service: remove accidental 'resetServer' call in sync (\!); dav: lock a file called 'lock', instead of the entire collection (workaround for bug 421610) --- services/sync/modules/dav.js | 4 ++-- services/sync/modules/service.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 76c5ab0ad320..250e34f51bef 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -354,7 +354,7 @@ DAVCollection.prototype = { yield; } - this.LOCK("", + this.LOCK("lock", "\n" + "\n" + " \n" + @@ -398,7 +398,7 @@ DAVCollection.prototype = { yield; } - this.UNLOCK("", self.cb); + this.UNLOCK("lock", self.cb); let resp = yield; if (this._authProvider._authFailed || diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5db097c01fa0..8de097fa8f99 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -493,7 +493,6 @@ WeaveSvc.prototype = { if (engines[i].enabled) { this._notify(engines[i].name + "-engine:sync", this._syncEngine, engines[i]).async(this, self.cb); - engines[i].resetServer(self.cb); yield; if (engines[i].name == "bookmarks") { // FIXME Engines.get("bookmarks").syncMounts(self.cb); From 2f9b8082f13dd64632d6681424229c5a123cbd5c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 25 Apr 2008 18:28:31 -0700 Subject: [PATCH 0211/1860] add a c++ crypto component, ported from bug 400742; with a specialized makefile to build in the extension (with the gecko sdk) --- services/crypto/IWeaveCrypto.idl | 93 +++++++++++ services/crypto/Makefile | 179 +++++++++++++++++++++ services/crypto/WeaveCrypto.cpp | 219 ++++++++++++++++++++++++++ services/crypto/WeaveCrypto.h | 72 +++++++++ services/crypto/WeaveCryptoModule.cpp | 54 +++++++ 5 files changed, 617 insertions(+) create mode 100644 services/crypto/IWeaveCrypto.idl create mode 100644 services/crypto/Makefile create mode 100644 services/crypto/WeaveCrypto.cpp create mode 100644 services/crypto/WeaveCrypto.h create mode 100644 services/crypto/WeaveCryptoModule.cpp diff --git a/services/crypto/IWeaveCrypto.idl b/services/crypto/IWeaveCrypto.idl new file mode 100644 index 000000000000..e57518b15525 --- /dev/null +++ b/services/crypto/IWeaveCrypto.idl @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills (original author) + * Honza Bambas + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsISupports.idl" + +[scriptable, uuid(d3b0f750-c976-46d0-be20-96b24f4684bc)] +interface IWeaveCrypto : nsISupports +{ + /** + * Shortcuts for some algorithm SEC OIDs. Full list available here: + * http://lxr.mozilla.org/seamonkey/source/security/nss/lib/util/secoidt.h + */ + + const unsigned long DES_EDE3_CBC = 156; + + // Unsupported now... + const unsigned long AES_128_ECB = 183; + const unsigned long AES_128_CBC = 184; + const unsigned long AES_192_ECB = 185; + const unsigned long AES_192_CBC = 186; + const unsigned long AES_256_ECB = 187; + const unsigned long AES_256_CBC = 188; + + /** + * One of the above constants. Assigning differnt value + * will fail cause the encrypt method fail. + * Default value is DES_EDE3_CBC. + */ + attribute unsigned long algorithm; + + /** + * Encrypt data using a passphrase + * See algorithm attribute, it determines which mechanisms will be used + * + * @param pass + * Passphrase to encrypt with + * @param iter + * Number of iterations for key strengthening + * This parameter is currently ignored and is always 1 + * @param clearText + * Data to be encrypted + * @returns Encrypted data, base64 encoded + */ + ACString encrypt(in ACString pass, in ACString clearText); + + /** + * Decrypt data using a passphrase + * + * @param pass + * Passphrase to decrypt with + * @param cipherText + * Base64 encoded data to be decrypted + * @returns Decrypted data + */ + ACString decrypt(in ACString pass, in ACString cipherText); +}; + diff --git a/services/crypto/Makefile b/services/crypto/Makefile new file mode 100644 index 000000000000..19e1a59aa205 --- /dev/null +++ b/services/crypto/Makefile @@ -0,0 +1,179 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Weave code. +# +# The Initial Developer of the Original Code is +# Mozilla Corporation +# Portions created by the Initial Developer are Copyright (C) 2008 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Dan Mills (original author) +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +idl = IWeaveCrypto.idl +cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp +target = WeaveCrypto # will have .so / .dylib / .dll appended + +sdkdir = +destdir = .. +platformdir = $(destdir)/platform/$(platform) + +xpidl = $(sdkdir)/bin/xpidl + +# FIXME: we don't actually require this for e.g. clean +ifndef sdkdir + $(warning No 'sdkdir' variable given) + $(warning It should point to the location of the Gecko SDK) + $(warning For example: "make sdkdir=/foo/bar/baz") + $(error ) +endif + +###################################################################### + +headers = -I$(sdkdir)/include \ + -I$(sdkdir)/include/system_wrappers \ + -I$(sdkdir)/include/nss \ + -I$(sdkdir)/include/xpcom \ + -I$(sdkdir)/include/string \ + -I$(sdkdir)/include/pipnss \ + -I$(sdkdir)/include/nspr \ + -I$(sdkdir)/sdk/include + +cppflags += -c -pipe -Os \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fpascal-strings -fno-common -fshort-wchar -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) + +libdirs = -L$(sdkdir)/lib -L$(sdkdir)/bin +libs = -lxpcomglue_s -lxpcom \ + -lcrmf -lsmime3 -lssl3 -lnss3 -lnssutil3 -lsoftokn3 \ + -lplds4 -lplc4 -lnspr4 + +ldflags += -pthread -pipe -bundle \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + $(libdirs) $(libs) + +###################################################################### +# Platform detection + +sys := $(shell uname -s) + +ifeq ($(sys), Linux) + os = Linux + compiler = gcc3 + cxx = c++ + so = so + cppflags += -shared +else + ifeq ($(sys), Darwin) + os = Darwin + compiler = gcc3 + cxx = c++ + so = dylib + cppflags += -dynamiclib + else + ifeq ($(os), MINGW32_NT-5.1) + $(error Sorry, windows is not supported yet) + os = WINNT + compiler = msvc + cxx = c++ # fixme + so = dll + cppflags += -shared + else + $(error Sorry, your os is unknown/unsupported: $(os)) + endif + endif +endif + +machine := $(shell uname -m) + +ifeq ($(machine), i386) + arch = x86 +else + ifeq ($(machine), i586) + arch = x86 + else + ifeq ($(machine), i686) + arch = x86 + else + ifeq ($(machine), ppc) # FIXME: verify + arch = ppc + else + # FIXME: x86_64, ia64, sparc, Alpha + $(error Sorry, your arch is unknown/unsupported: $(machine)) + endif + endif + endif +endif + +platform = $(os)_$(arch)-$(compiler) + +idl_headers = $(idl:.idl=.h) +idl_typelib = $(idl:.idl=.xpt) +cpp_objects = $(cpp_sources:.cpp=.o) +so_target = $(target:=.$(so)) + +###################################################################### + +all: build # default target + +build: $(so_target) $(idl_typelib) + +install: build + mkdir -p $(destdir)/components + mkdir -p $(platformdir)/components + cp $(idl_typelib) $(destdir)/components + cp $(so_target) $(platformdir)/components + +clean: + rm -f $(so_target) $(cpp_objects) $(idl_typelib) $(idl_headers) + +# rules to build the c headers and .xpt from idl + +$(idl_headers): $(idl) + $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) + +$(idl_typelib): $(idl) + $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) + +# "main" (internal) rules, build sources and link the component + +$(cpp_objects): $(cpp_sources) + $(cxx) -o $@ $(cppflags) $(@:.o=.cpp) + +$(so_target): $(idl_headers) $(cpp_objects) + $(cxx) -o $@ $(ldflags) $(cpp_objects) + chmod +x $@ +# strip $@ diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp new file mode 100644 index 000000000000..80e80a8181e3 --- /dev/null +++ b/services/crypto/WeaveCrypto.cpp @@ -0,0 +1,219 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills (original author) + * Honza Bambas + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "WeaveCrypto.h" + +#include "nsStringAPI.h" +#include "nsAutoPtr.h" +#include "plbase64.h" +#include "secerr.h" +#include "secpkcs7.h" + +NS_IMPL_ISUPPORTS1(WeaveCrypto, IWeaveCrypto) + +WeaveCrypto::WeaveCrypto() +: mAlgorithm(SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_3KEY_TRIPLE_DES_CBC) +{ +} + +WeaveCrypto::~WeaveCrypto() +{ +} + +nsresult +WeaveCrypto::EncodeBase64(const nsACString& binary, nsACString& retval) +{ + PRUint32 encodedLength = (binary.Length() * 4 + 2) / 3; + + nsAutoArrayPtr encoded; + encoded = new char[encodedLength + 2]; + NS_ENSURE_TRUE(encoded, NS_ERROR_OUT_OF_MEMORY); + + PromiseFlatCString fBinary(binary); + PL_Base64Encode(fBinary.get(), fBinary.Length(), encoded); + + retval.Assign(encoded, encodedLength); + return NS_OK; +} + +nsresult +WeaveCrypto::DecodeBase64(const nsACString& base64, nsACString& retval) +{ + PromiseFlatCString flat(base64); + + PRUint32 decodedLength = (flat.Length() * 3) / 4; + + nsAutoArrayPtr decoded; + decoded = new char[decodedLength]; + NS_ENSURE_TRUE(decoded, NS_ERROR_OUT_OF_MEMORY); + + if (!PL_Base64Decode(flat.get(), flat.Length(), decoded)) + return NS_ERROR_ILLEGAL_VALUE; + + retval.Assign(decoded, decodedLength); + return NS_OK; +} + +// static +void +WeaveCrypto::StoreToStringCallback(void *arg, const char *buf, unsigned long len) +{ + nsACString* aText = (nsACString*)arg; + aText->Append(buf, len); +} + +// static +PK11SymKey * +WeaveCrypto::GetSymmetricKeyCallback(void *arg, SECAlgorithmID *algid) +{ + SECItem *pwitem = (SECItem *)arg; + + PK11SlotInfo *slot = PK11_GetInternalSlot(); + if (!slot) + return nsnull; + + PK11SymKey *key = PK11_PBEKeyGen(slot, algid, pwitem, PR_FALSE, nsnull); + PK11_FreeSlot(slot); + + if (key) + PK11_SetSymKeyUserData(key, pwitem, nsnull); + + return key; +} + +// static +PRBool +WeaveCrypto::DecryptionAllowedCallback(SECAlgorithmID *algid, PK11SymKey *key) +{ + return PR_TRUE; +} + +////////////////////////////////////////////////////////////////////////////// +// nsIPBECipher + +NS_IMETHODIMP +WeaveCrypto::GetAlgorithm(PRUint32 *aAlgorithm) +{ + *aAlgorithm = mAlgorithm; + return NS_OK; +} + +NS_IMETHODIMP +WeaveCrypto::SetAlgorithm(PRUint32 aAlgorithm) +{ + mAlgorithm = (SECOidTag)aAlgorithm; + return NS_OK; +} + +NS_IMETHODIMP +WeaveCrypto::Encrypt(const nsACString& aPass, + const nsACString& aClearText, + nsACString& aCipherText) +{ + nsresult rv = NS_ERROR_FAILURE; + + SEC_PKCS7ContentInfo *cinfo = SEC_PKCS7CreateEncryptedData(mAlgorithm, 0, nsnull, nsnull); + NS_ENSURE_TRUE(cinfo, NS_ERROR_FAILURE); + + SECAlgorithmID* encalgid = SEC_PKCS7GetEncryptionAlgorithm(cinfo); + if (encalgid) + { + PromiseFlatCString fPass(aPass); + SECItem pwitem = {siBuffer, (unsigned char*)fPass.get(), fPass.Length()}; + PK11SymKey *key = GetSymmetricKeyCallback(&pwitem, encalgid); + if (key) + { + nsCString result; + SEC_PKCS7EncoderContext *ecx = SEC_PKCS7EncoderStart( + cinfo, StoreToStringCallback, &result, key); + PK11_FreeSymKey(key); + + if (ecx) + { + SECStatus srv; + + PromiseFlatCString fClearText(aClearText); + srv = SEC_PKCS7EncoderUpdate(ecx, fClearText.get(), fClearText.Length()); + + SEC_PKCS7EncoderFinish(ecx, nsnull, nsnull); + + if (SECSuccess == srv) + rv = EncodeBase64(result, aCipherText); + } + else + { + NS_WARNING("Could not create PKCS#7 encoder context"); + } + } + } + + SEC_PKCS7DestroyContentInfo(cinfo); + return rv; +} + +NS_IMETHODIMP +WeaveCrypto::Decrypt(const nsACString& aPass, + const nsACString& aCipherText, + nsACString& aClearText) +{ + nsresult rv; + + nsCString fCipherText; + rv = DecodeBase64(aCipherText, fCipherText); + NS_ENSURE_SUCCESS(rv, rv); + + aClearText.Truncate(); + + PromiseFlatCString fPass(aPass); + SECItem pwitem = {siBuffer, (unsigned char*)fPass.get(), fPass.Length()}; + SEC_PKCS7DecoderContext *dcx = SEC_PKCS7DecoderStart( + StoreToStringCallback, &aClearText, nsnull, nsnull, + GetSymmetricKeyCallback, &pwitem, + DecryptionAllowedCallback); + NS_ENSURE_TRUE(dcx, NS_ERROR_FAILURE); + + SECStatus srv = SEC_PKCS7DecoderUpdate(dcx, fCipherText.get(), fCipherText.Length()); + rv = (SECSuccess == srv) ? NS_OK : NS_ERROR_FAILURE; + + SEC_PKCS7ContentInfo *cinfo = SEC_PKCS7DecoderFinish(dcx); + if (cinfo) + SEC_PKCS7DestroyContentInfo(cinfo); + + return rv; +} diff --git a/services/crypto/WeaveCrypto.h b/services/crypto/WeaveCrypto.h new file mode 100644 index 000000000000..c8b16ac135ce --- /dev/null +++ b/services/crypto/WeaveCrypto.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is WeaveCrypto code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills (original author) + * Honza Bambas + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef WeaveCrypto_h_ +#define WeaveCrypto_h_ + +#include "IWeaveCrypto.h" +#include "pk11pub.h" + +#define WEAVE_CRYPTO_CONTRACTID "@labs.mozilla.com/Weave/Crypto;1" +#define WEAVE_CRYPTO_CLASSNAME "A Simple XPCOM Sample" +#define WEAVE_CRYPTO_CID { 0xd3b0f750, 0xc976, 0x46d0, \ + { 0xbe, 0x20, 0x96, 0xb2, 0x4f, 0x46, 0x84, 0xbc } } + +class WeaveCrypto : public IWeaveCrypto +{ +public: + WeaveCrypto(); + + NS_DECL_ISUPPORTS + NS_DECL_IWEAVECRYPTO + +private: + ~WeaveCrypto(); + + SECOidTag mAlgorithm; + + nsresult DecodeBase64(const nsACString& base64, nsACString& retval); + nsresult EncodeBase64(const nsACString& binary, nsACString& retval); + + static void WeaveCrypto::StoreToStringCallback(void *arg, const char *buf, unsigned long len); + static PK11SymKey *GetSymmetricKeyCallback(void *arg, SECAlgorithmID *algid); + static PRBool DecryptionAllowedCallback(SECAlgorithmID *algid, PK11SymKey *key); +}; + +#endif // WeaveCrypto_h_ diff --git a/services/crypto/WeaveCryptoModule.cpp b/services/crypto/WeaveCryptoModule.cpp new file mode 100644 index 000000000000..2aebc64dabd2 --- /dev/null +++ b/services/crypto/WeaveCryptoModule.cpp @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsIGenericFactory.h" +#include "WeaveCrypto.h" + +NS_GENERIC_FACTORY_CONSTRUCTOR(WeaveCrypto) + +static nsModuleComponentInfo components[] = +{ + { + WEAVE_CRYPTO_CLASSNAME, + WEAVE_CRYPTO_CID, + WEAVE_CRYPTO_CONTRACTID, + WeaveCryptoConstructor, + } +}; + +NS_IMPL_NSGETMODULE("WeaveCryptoModule", components) From b9fca1f94d23fa7e16936727ee4c92b93caa2d92 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 30 Apr 2008 13:01:17 -0700 Subject: [PATCH 0212/1860] add a simple unit test harness based on xpcshell; add two tests for PBE and to load all the modules; add some hacks to the component's makefile to make tests work correctly --- services/crypto/Makefile | 12 +++++++++- services/sync/tests/unit/Makefile | 1 + services/sync/tests/unit/head_first.js | 28 ++++++++++++++++++++++++ services/sync/tests/unit/test_loadall.js | 15 +++++++++++++ services/sync/tests/unit/test_pbe.js | 24 ++++++++++++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) create mode 120000 services/sync/tests/unit/Makefile create mode 100644 services/sync/tests/unit/head_first.js create mode 100644 services/sync/tests/unit/test_loadall.js create mode 100644 services/sync/tests/unit/test_pbe.js diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 19e1a59aa205..2dfacede220f 100644 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -40,7 +40,7 @@ idl = IWeaveCrypto.idl cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp target = WeaveCrypto # will have .so / .dylib / .dll appended -sdkdir = +sdkdir ?= ${MOZSDKDIR} destdir = .. platformdir = $(destdir)/platform/$(platform) @@ -147,6 +147,8 @@ so_target = $(target:=.$(so)) ###################################################################### +.PHONY: all build install test-install clean + all: build # default target build: $(so_target) $(idl_typelib) @@ -157,6 +159,14 @@ install: build cp $(idl_typelib) $(destdir)/components cp $(so_target) $(platformdir)/components +# gross hack to get around component registration for xpcshell tests +test-install: install + ln -sf `pwd`/$(destdir)/defaults/preferences/sync.js $(sdkdir)/bin/defaults/pref/sync.js # fixme!! + ln -sf `pwd`/$(destdir)/components/$(idl_typelib) $(sdkdir)/bin/components/$(idl_typelib) + ln -sf `pwd`/$(platformdir)/components/$(so_target) $(sdkdir)/bin/components/$(so_target) + rm -f $(sdkdir)/bin/components/compreg.dat + rm -f $(sdkdir)/bin/components/xpti.dat + clean: rm -f $(so_target) $(cpp_objects) $(idl_typelib) $(idl_headers) diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile new file mode 120000 index 000000000000..615b4be7d757 --- /dev/null +++ b/services/sync/tests/unit/Makefile @@ -0,0 +1 @@ +../harness/Makefile \ No newline at end of file diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js new file mode 100644 index 000000000000..5b4375d9ded0 --- /dev/null +++ b/services/sync/tests/unit/head_first.js @@ -0,0 +1,28 @@ +version(180); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +let ds = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + +let provider = { + getFile: function(prop, persistent) { + persistent.value = true; + if (prop == "ExtPrefDL") { + return [ds.get("CurProcD", Ci.nsIFile)]; + } + throw Cr.NS_ERROR_FAILURE; + }, + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIDirectoryServiceProvider) || + iid.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; +ds.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider); + +do_bind_resource(do_get_file("modules"), "weave"); diff --git a/services/sync/tests/unit/test_loadall.js b/services/sync/tests/unit/test_loadall.js new file mode 100644 index 000000000000..8c354b91b505 --- /dev/null +++ b/services/sync/tests/unit/test_loadall.js @@ -0,0 +1,15 @@ +function run_test() { + Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + Components.utils.import("resource://weave/constants.js"); + Components.utils.import("resource://weave/log4moz.js"); + Components.utils.import("resource://weave/util.js"); + Components.utils.import("resource://weave/async.js"); + Components.utils.import("resource://weave/crypto.js"); + Components.utils.import("resource://weave/identity.js"); + Components.utils.import("resource://weave/dav.js"); + Components.utils.import("resource://weave/wrap.js"); + Components.utils.import("resource://weave/stores.js"); + Components.utils.import("resource://weave/syncCores.js"); + Components.utils.import("resource://weave/engines.js"); + Components.utils.import("resource://weave/service.js"); +} diff --git a/services/sync/tests/unit/test_pbe.js b/services/sync/tests/unit/test_pbe.js new file mode 100644 index 000000000000..364ca0e26087 --- /dev/null +++ b/services/sync/tests/unit/test_pbe.js @@ -0,0 +1,24 @@ +function run_test() { + // initialize nss + let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + + let pbe = Cc["@labs.mozilla.com/Weave/Crypto;1"].getService(Ci.IWeaveCrypto); + + pbe.algorithm = pbe.DES_EDE3_CBC; + let cipherTxt = pbe.encrypt("passphrase", "my very secret message!"); + + do_check_true(cipherTxt != "my very secret message!"); + + let clearTxt = pbe.decrypt("passphrase", cipherTxt); + do_check_true(clearTxt == "my very secret message!"); + + // The following check with wrong password must cause decryption to fail + // beuase of used padding-schema cipher, RFC 3852 Section 6.3 + let failure = false; + try { + pbe.decrypt("wrongpassphrase", cipherTxt); + } catch (e) { + failure = true; + } + do_check_true(failure); +} From 66e5e9a5d088cd697b38170774cda7bc87e0dec9 Mon Sep 17 00:00:00 2001 From: Date: Wed, 30 Apr 2008 16:27:32 -0700 Subject: [PATCH 0213/1860] First commit of my XMPP client, as demonstrated at the meeting today, to weave/modules. This does not include the test-synchronization stuff. xmppClient.js is the main client class; transportLayer.js and sasl.js (which does authentication) are the helper classes. --- services/sync/modules/xmpp/sasl.js | 370 +++++++++++++++++ services/sync/modules/xmpp/transportLayer.js | 336 +++++++++++++++ services/sync/modules/xmpp/xmppClient.js | 404 +++++++++++++++++++ 3 files changed, 1110 insertions(+) create mode 100644 services/sync/modules/xmpp/sasl.js create mode 100644 services/sync/modules/xmpp/transportLayer.js create mode 100644 services/sync/modules/xmpp/xmppClient.js diff --git a/services/sync/modules/xmpp/sasl.js b/services/sync/modules/xmpp/sasl.js new file mode 100644 index 000000000000..0b1b97e15463 --- /dev/null +++ b/services/sync/modules/xmpp/sasl.js @@ -0,0 +1,370 @@ +if(typeof(atob) == 'undefined') { +// This code was written by Tyler Akins and has been placed in the +// public domain. It would be nice if you left this header intact. +// Base64 code from Tyler Akins -- http://rumkin.com + +var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + +function btoa(input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + do { + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + + output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + + keyStr.charAt(enc3) + keyStr.charAt(enc4); + } while (i < input.length); + + return output; +} + +function atob(input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + // remove all characters that are not A-Z, a-z, 0-9, +, /, or = + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + + do { + enc1 = keyStr.indexOf(input.charAt(i++)); + enc2 = keyStr.indexOf(input.charAt(i++)); + enc3 = keyStr.indexOf(input.charAt(i++)); + enc4 = keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCharCode(chr1); + + if (enc3 != 64) { + output = output + String.fromCharCode(chr2); + } + if (enc4 != 64) { + output = output + String.fromCharCode(chr3); + } + } while (i < input.length); + + return output; +} +} + + +/* Two implementations of SASL authentication: + one using MD5-DIGEST, the other using PLAIN. + + +Here's the interface that each implementation must obey: +initialize( clientName, clientRealm, clientPassword ); + +generateResponse( rootElem ); + +getError(); + returns text of error message +*/ + +function BaseAuthenticator() { +} +BaseAuthenticator.prototype = { + COMPLETION_CODE: "success!", + + initialize: function( userName, realm, password ) { + this._name = userName; + this._realm = realm; + this._password = password; + this._stepNumber = 0; + this._errorMsg = ""; + }, + + getError: function () { + /* Returns text of most recent error message. + Client code should call this if generateResponse() returns false + to see what the problem was. */ + return this._errorMsg; + }, + + generateResponse: function( rootElem ) { + /* Subclasses must override this. rootElem is a DOM node which is + the root element of the XML the server has sent to us as part + of the authentication protocol. return value: the string that + should be sent back to the server in response. 'false' if + there's a failure, or COMPLETION_CODE if nothing else needs to + be sent because authentication is a success. */ + + this._errorMsg = "generateResponse() should be overridden by subclass."; + return false; + }, + + verifyProtocolSupport: function( rootElem, protocolName ) { + /* Parses the incoming stream from the server to check whether the + server supports the type of authentication we want to do + (specified in the protocolName argument). + Returns false if there is any problem. + */ + if ( rootElem.nodeName != "stream:stream" ) { + this._errorMsg = "Expected stream:stream but got " + rootElem.nodeName; + return false; + } + + dump( "Got response from server...\n" ); + dump( "ID is " + rootElem.getAttribute( "id" ) + "\n" ); + // TODO: Do I need to do anything with this ID value??? + dump( "From: " + rootElem.getAttribute( "from" ) + "\n" ); + if (rootElem.childNodes.length == 0) { + // No child nodes is unexpected, but allowed by the protocol. + // this shouldn't be an error. + this._errorMsg = "Expected child nodes but got none."; + return false; + } + + var child = rootElem.childNodes[0]; + if (child.nodeName == "stream:error" ) { + this._errorMsg = this.parseError( child ); + return false; + } + + if ( child.nodeName != "stream:features" ) { + this._errorMsg = "Expected stream:features but got " + child.nodeName; + return false; + } + + var protocolSupported = false; + var mechanisms = child.getElementsByTagName( "mechanism" ); + for ( var x = 0; x < mechanisms.length; x++ ) { + if ( mechanisms[x].firstChild.nodeValue == protocolName ) { + protocolSupported = true; + } + } + + if ( !protocolSupported ) { + this._errorMsg = protocolName + " not supported by server!"; + return false; + } + return true; + } + +}; + +function Md5DigestAuthenticator( ) { + /* SASL using DIGEST-MD5 authentication. + Uses complicated hash of password + with nonce and cnonce to obscure password while preventing replay + attacks. + + See http://www.faqs.org/rfcs/rfc2831.html + "Using Digest Authentication as a SASL mechanism" + + TODO: currently, this is getting rejected by my server. + What's wrong? + */ +} +Md5DigestAuthenticator.prototype = { + + _makeCNonce: function( ) { + return "\"" + Math.floor( 10000000000 * Math.random() ) + "\""; + }, + + generateResponse: function Md5__generateResponse( rootElem ) { + if ( this._stepNumber == 0 ) { + + if ( this.verifyProtocolSupport( rootElem, "DIGEST-MD5" ) == false ) { + return false; + } + // SASL step 1: request that we use DIGEST-MD5 authentication. + this._stepNumber = 1; + return ""; + + } else if ( this._stepNumber == 1 ) { + + // proceed to SASL step 2: are you asking for a CHALLENGE?!? + var challenge = this._unpackChallenge( rootElem.firstChild.nodeValue ); + dump( "Nonce is " + challenge.nonce + "\n" ); + // eg: + // nonce="3816627940",qop="auth",charset=utf-8,algorithm=md5-sess + + // Now i have the nonce: make a digest-response out of + /* username: required + realm: only needed if realm is in challenge + nonce: required, just as recieved + cnonce: required, opaque quoted string, 64 bits entropy + nonce-count: optional + qop: (quality of protection) optional + serv-type: optional? + host: optional? + serv-name: optional? + digest-uri: "service/host/serv-name" (replaces those three?) + response: required (32 lowercase hex), + maxbuf: optional, + charset, + LHEX (32 hex digits = ??), + cipher: required if auth-conf is negotiatedd?? + authzid: optional + */ + + + // TODO: Are these acceptable values for realm, nonceCount, and + // digestUri?? + var nonceCount = "00000001"; + var digestUri = "xmpp/" + this.realm; + var cNonce = this._makeCNonce(); + // Section 2.1.2.1 of RFC2831 + var A1 = str_md5( this.name + ":" + this.realm + ":" + this.password ) + ":" + challenge.nonce + ":" + cNonce; + var A2 = "AUTHENTICATE:" + digestUri; + var myResponse = hex_md5( hex_md5( A1 ) + ":" + challenge.nonce + ":" + nonceCount + ":" + cNonce + ":auth" + hex_md5( A2 ) ); + + var responseDict = { + username: "\"" + this.name + "\"", + nonce: challenge.nonce, + nc: nonceCount, + cnonce: cNonce, + qop: "\"auth\"", + algorithm: "md5-sess", + charset: "utf-8", + response: myResponse + }; + responseDict[ "digest-uri" ] = "\"" + digestUri + "\""; + var responseStr = this._packChallengeResponse( responseDict ); + this._stepNumber = 2; + return "" + responseStr + ""; + + } else if ( this._stepNumber = 2 ) { + dump( "Got to step 3!" ); + // At this point the server might reject us with a + // + if ( rootElem.nodeName == "failure" ) { + this._errorMsg = rootElem.firstChild.nodeName; + return false; + } + //this._connectionStatus = this.REQUESTED_SASL_3; + } + this._errorMsg = "Can't happen."; + return false; + }, + + _unpackChallenge: function( challengeString ) { + var challenge = atob( challengeString ); + dump( "After b64 decoding: " + challenge + "\n" ); + var challengeItemStrings = challenge.split( "," ); + var challengeItems = {}; + for ( var x in challengeItemStrings ) { + var stuff = challengeItemStrings[x].split( "=" ); + challengeItems[ stuff[0] ] = stuff[1]; + } + return challengeItems; + }, + + _packChallengeResponse: function( responseDict ) { + var responseArray = [] + for( var x in responseDict ) { + responseArray.push( x + "=" + responseDict[x] ); + } + var responseString = responseArray.join( "," ); + dump( "Here's my response string: \n" ); + dump( responseString + "\n" ); + return btoa( responseString ); + } +}; +Md5DigestAuthenticator.prototype.__proto__ = new BaseAuthenticator(); + + +function PlainAuthenticator( ) { + /* SASL using PLAIN authentication, which sends password in the clear. */ +} +PlainAuthenticator.prototype = { + + generateResponse: function( rootElem ) { + if ( this._stepNumber == 0 ) { + if ( this.verifyProtocolSupport( rootElem, "PLAIN" ) == false ) { + return false; + } + var authString = btoa( this._realm + '\0' + this._name + '\0' + this._password ); + this._stepNumber = 1; + return "" + authString + ""; + } else if ( this._stepNumber == 1 ) { + if ( rootElem.nodeName == "failure" ) { + // Authentication rejected: username or password may be wrong. + this._errorMsg = rootElem.firstChild.nodeName; + return false; + } else if ( rootElem.nodeName == "success" ) { + // Authentication accepted: now we start a new stream for + // resource binding. + /* RFC3920 part 7 says: upon receiving a success indication within the + SASL negotiation, the client MUST send a new stream header to the + server, to which the serer MUST respond with a stream header + as well as a list of available stream features. */ + // TODO: resource binding happens in any authentication mechanism + // so should be moved to base class. + this._stepNumber = 2; + return ""; + } + } else if ( this._stepNumber == 2 ) { + // See if the server is asking us to bind a resource, and if it's + // asking us to start a session: + var bindNodes = rootElem.getElementsByTagName( "bind" ); + if ( bindNodes.length > 0 ) { + this._needBinding = true; + } + var sessionNodes = rootElem.getElementsByTagName( "session" ); + if ( sessionNodes.length > 0 ) { + this._needSession = true; + } + + if ( !this._needBinding && !this._needSession ) { + // Server hasn't requested either: we're done. + return this.COMPLETION_CODE; + } + + if ( this._needBinding ) { + // Do resource binding: + // Tell the server to generate the resource ID for us. + this._stepNumber = 3; + return ""; + } + + this._errorMsg = "Server requested session not binding: can't happen?"; + return false; + } else if ( this._stepNumber == 3 ) { + // Pull the JID out of the stuff the server sends us. + var jidNodes = rootElem.getElementsByTagName( "jid" ); + if ( jidNodes.length == 0 ) { + this._errorMsg = "Expected JID node from server, got none."; + return false; + } + this._jid = jidNodes[0].firstChild.nodeValue; + // TODO: Does the client need to do anything special with its new + // "client@host.com/resourceID" full JID? + dump( "JID set to " + this._jid ); + + // If we still need to do session, then we're not done yet: + if ( this._needSession ) { + this._stepNumber = 4; + return ""; + } else { + return this.COMPLETION_CODE; + } + } else if ( this._stepNumber == 4 ) { + // OK, now we're done. + return this.COMPLETION_CODE; + } + } + +}; +PlainAuthenticator.prototype.__proto__ = new BaseAuthenticator(); diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js new file mode 100644 index 000000000000..d632527235e1 --- /dev/null +++ b/services/sync/modules/xmpp/transportLayer.js @@ -0,0 +1,336 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +function InputStreamBuffer() { +} +InputStreamBuffer.prototype = { + _data: "", + append: function( stuff ) { + this._data = this._data + stuff; + }, + clear: function() { + this._data = ""; + }, + getData: function() { + return this._data; + } +} + +function SocketClient( host, port ) { + this._init( host, port ); +} +SocketClient.prototype = { + __threadManager: null, + get _threadManager() { + if (!this.__threadManager) + this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + return this.__threadManager; + }, + __transport: null, + get _transport() { + if (!this.__transport) { + var transportService = Cc["@mozilla.org/network/socket-transport-service;1"].getService(Ci.nsISocketTransportService); + this.__transport = transportService.createTransport(['starttls'], + 1, // ssl only + this._host, + this._port, + null); // proxy + } + return this.__transport; + }, + + _init: function( host, port ) { + this._host = host; + this._port = port; + this._contentRead = ""; + this._buffer = null; + this.connect(); + }, + + connect: function() { + var outstream = this._transport.openOutputStream( 0, // flags + 0, // buffer size + 0 ); // number of buffers + this._outstream = outstream; + + var buffer = new InputStreamBuffer; + this._buffer = buffer; + + // Wrap input stream is C only, nonscriptable, so wrap it in scriptable + // stream: + var rawInputStream = this._transport.openInputStream( 0, 0, 0 ); + var scriptablestream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); + scriptablestream.init(rawInputStream); + + // input stream pump for asynchronous reading + var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(Ci.nsIInputStreamPump); + pump.init(rawInputStream, -1, -1, 0, 0, + false); //automatically close once all data read? + + // create dataListener class for callback: + var dataListener = { + data : "", + onStartRequest: function(request, context){ + }, + onStopRequest: function(request, context, status){ + rawInputStream.close(); + outstream.close(); + }, + onDataAvailable: function(request, context, inputStream, offset, count){ + // use scriptable stream wrapper, not "real" stream. + // count is number of bytes available, offset is position in stream. + // Do stuff with data here! + buffer.append( scriptablestream.read( count )); + } + }; + // register it: + pump.asyncRead(dataListener, null); // second argument is a context + //which gets passed in as the context argument to methods of dataListener + + //Should be done connecting now. TODO: Catch and report errors. + }, + + send: function( messageText ) { + this._outstream.write( messageText, messageText.length ); + }, + + getBinaryOutStream: function() { + var binaryOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + binaryOutStream.setOutputStream( this._outstream ); // is this right? + return binaryOutStream; + }, + + disconnect: function() { + var thread = this._threadManager.currentThread; + while( thread.hasPendingEvents() ) + { + thread.processNextEvent( true ); + } + }, + + checkResponse: function() { + return this._getData(); + }, + + waitForResponse: function() { + var thread = this._threadManager.currentThread; + while( this._buffer.getData().length == 0 ) + { + thread.processNextEvent( true ); + } + var output = this._buffer.getData(); + this._buffer.clear(); + return output; + }, + + startTLS: function() { + this._transport.securityInfo.QueryInterface(Ci.nsISSLSocketControl); + this._transport.securityInfo.StartTLS(); + }, + + // TODO have a variant of waitForResponse that gets binary data + // binaryInStream = Cc["@mozilla.org/binaryinputstream;1].createInstance( Ci.nsIBinaryInputStream ); + // binaryInStream.setInputStream( this._rawInputStream ); + +}; + + +/* The interface that should be implemented by any Transport object: + send( messageXml ); + setCallbackObject( object with .onIncomingData and .onTransportError ); + connect(); + disconnect(); +*/ + +function HTTPPollingTransport( serverUrl, useKeys, interval ) { + /* Send HTTP requests periodically to the server using a timer. + HTTP POST requests with content-type application/x-www-form-urlencoded. + responses from the server have content-type text/xml + request and response are UTF-8 encoded (ignore what HTTP header says) + identify session by always using set-cookie header with cookie named ID + first request sets this to 0 to indicate new session. */ + + this._init( serverUrl, useKeys, interval ); +} +HTTPPollingTransport.prototype = { + _init: function( serverUrl, useKeys, interval ) { + this._serverUrl = serverUrl + this._n = 0; + this._key = this._makeSeed(); + this._latestCookie = ""; + this._connectionId = 0; + this._callbackObject = null; + this._useKeys = useKeys; + this._interval = interval; + }, + + __request: null, + get _request() { + if (!this.__request) + this.__request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance( Ci.nsIXMLHttpRequest ); + return this.__request; + }, + + __hasher: null, + get _hasher() { + if (!this.__hasher) + this.__hasher = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash ); + return this.__hasher; + }, + + __timer: null, + get _timer() { + if (!this.__timer) + this.__timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); + return this.__timer; + }, + + _makeSeed: function() { + return "foo";//"MyKeyOfHorrors"; + }, + + _advanceKey: function() { + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + + // we use UTF-8 here, you can choose other encodings. + converter.charset = "UTF-8"; + // result is an out parameter, + // result.value will contain the array length + var result = {}; + // data is an array of bytes + var data = converter.convertToByteArray( this._key, result); + + this._n += 1; + this._hasher.initWithString( "SHA1" ); + this._hasher.update( data, data.length ); + this._key = this._hasher.finish( true ); // true means B64encode + }, + + _setIdFromCookie: function( self, cookie ) { + // parse connection ID out of the cookie: + // dump( "Cookie is " + cookie + "\n" ); + var cookieSegments = cookie.split( ";" ); + cookieSegments = cookieSegments[0].split( "=" ); + var newConnectionId = cookieSegments[1]; + switch( newConnectionId) { + case "0:0": + self._onError( "Unknown error!\n" ); + break; + case "-1:0": + self._onError( "Server error!\n" ); + break; + case "-2:0": + self._onError( "Bad request!\n" ); + break; + case "-3:0": + self._onError( "Key sequence error!\n" ); + break; + default : + self._connectionId = cookieSegments[1]; + // dump( "Connection ID set to " + self._connectionId + "\n" ); + break; + } + }, + + _onError: function( errorText ) { + dump( "Transport error: " + errorText + "\n" ); + if ( this._callbackObject != null ) { + this._callbackObject.onTransportError( errorText ); + } + this.disconnect(); + }, + + _doPost: function( requestXml ) { + var request = this._request; + var callbackObj = this._callbackObject; + var self = this; + var contents = ""; + + + if ( this._useKey ) { + this._advanceKey(); + contents = this._connectionId + ";" + this._key + "," + requestXml; + } else { + contents = this._connectionId + "," + requestXml; + /* TODO: + Currently I get a "-3:0" error (key sequence error) from the 2nd + exchange if using the keys is enabled. */ + } + + _processReqChange = function( ) { + //Callback for XMLHTTPRequest object state change messages + if ( request.readyState == 4 ) { + if ( request.status == 200) { + dump( "Server says: " + request.responseText + "\n" ); + // Look for a set-cookie header: + var latestCookie = request.getResponseHeader( "Set-Cookie" ); + if ( latestCookie.length > 0 ) { + self._setIdFromCookie( self, latestCookie ); + } + if ( callbackObj != null && request.responseText.length > 0 ) { + callbackObj.onIncomingData( request.responseText ); + } + } else { + dump ( "Error! Got HTTP status code " + request.status + "\n" ); + } + } + }; + + request.open( "POST", this._serverUrl, true ); //async = true + request.setRequestHeader( "Content-type", + "application/x-www-form-urlencoded;charset=UTF-8" ); + request.setRequestHeader( "Content-length", contents.length ); + request.setRequestHeader( "Connection", "close" ); + request.onreadystatechange = _processReqChange; + dump( "Sending: " + contents + "\n" ); + request.send( contents ); + }, + + send: function( messageXml ) { + this._doPost( messageXml ); + }, + + setCallbackObject: function( callbackObject ) { + this._callbackObject = callbackObject; + }, + + notify: function( timer ) { + /* having a notify method makes this object satisfy the nsITimerCallback + interface, so the object can be passed to timer.initWithCallback. */ + + /* Periodically, if we don't have anything else to post, we should + post an empty message just to see if the server has any queued + data it's waiting to give us in return. */ + this._doPost( "" ); + }, + + connect: function() { + /* Set up a timer to poll the server periodically. */ + + // TODO doPost isn't reentrant; don't try to doPost if there's + //already a post in progress... or can that never happen? + + this._timer.initWithCallback( this, + this._interval, + this._timer.TYPE_REPEATING_SLACK ); + }, + + disconnect: function () { + this._timer.cancel(); + }, + + testKeys: function () { + + this._key = "foo"; + dump( this._key + "\n" ); + for ( var x = 1; x < 7; x++ ) { + this._advanceKey(); + dump( this._key + "\n" ); + } + }, + +}; + + +//transport = new HTTPPollingTransport( "http://127.0.0.1:5280/http-poll" ); +//transport.testKeys(); diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js new file mode 100644 index 000000000000..0262efc07762 --- /dev/null +++ b/services/sync/modules/xmpp/xmppClient.js @@ -0,0 +1,404 @@ + +// See www.xulplanet.com/tutorials/mozsdk/sockets.php +// http://www.xmpp.org/specs/rfc3920.html +// http://www.process-one.net/docs/ejabberd/guide_en.html +// http://www.xulplanet.com/tutorials/mozsdk/xmlparse.php +// http://developer.mozilla.org/en/docs/xpcshell +// http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests + +var Cc = Components.classes; +var Ci = Components.interfaces; + + +function JabberClient( clientName, realm, clientPassword, transport, authenticator ) { + this._init( clientName, realm, clientPassword, transport, authenticator ); +} +JabberClient.prototype = { + //connection status codes: + NOT_CONNECTED: 0, + CALLED_SERVER: 1, + AUTHENTICATING: 2, + CONNECTED: 3, + FAILED: -1, + + // IQ stanza status codes: + IQ_WAIT: 0, + IQ_OK: 1, + IQ_ERROR: -1, + + _init: function( clientName, realm, clientPassword, transport, authenticator ) { + this._myName = clientName; + this._realm = realm; + this._fullName = clientName + "@" + realm; + this._myPassword = clientPassword; + this._connectionStatus = this.NOT_CONNECTED; + this._error = null; + this._streamOpen = false; + this._transportLayer = transport; + this._authenticationLayer = authenticator; + this._authenticationLayer.initialize( clientName, realm, clientPassword ); + this._messageHandlers = []; + this._iqResponders = []; + this._nextIqId = 0; + this._pendingIqs = {}; + }, + + __parser: null, + get _parser() { + if (!this.__parser) + this.__parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance( Ci.nsIDOMParser ); + return this.__parser; + }, + + __threadManager: null, + get _threadManager() { + if (!this.__threadManager) + this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + return this.__threadManager; + }, + + + parseError: function( streamErrorNode ) { + dump( "Uh-oh, there was an error!\n" ); + var error = streamErrorNode.childNodes[0]; + dump( "Name: " + error.nodeName + " Value: " + error.nodeValue + "\n" ); + this._error = error.nodeName; + this.disconnect(); + /* Note there can be an optional bla bla node inside + stream: error giving additional info; there can also optionally + be an app-specific condition element qualified by an app-defined + namespace */ + }, + + setError: function( errorText ) { + dump( "Error: " + errorText + "\n" ); + this._error = errorText; + this._connectionStatus = this.FAILED; + }, + + onIncomingData: function( messageText ) { + var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); + + if (responseDOM.documentElement.nodeName == "parsererror" ) { + /* Before giving up, remember that XMPP doesn't close the top-level + element until the communication is done; this means + that what we get from the server is often technically only an + xml fragment. Try manually appending the closing tag to simulate + a complete xml document and then parsing that. */ + var response = messageText + this._makeClosingXml(); + responseDOM = this._parser.parseFromString( response, "text/xml" ); + } + if ( responseDOM.documentElement.nodeName == "parsererror" ) { + /* If that still doesn't work, it might be that we're getting a fragment + with multiple top-level tags, which is a no-no. Try wrapping it + all inside one proper top-level stream element and parsing. */ + response = this._makeHeaderXml( this._fullName ) + messageText + this._makeClosingXml(); + responseDOM = this._parser.parseFromString( response, "text/xml" ); + } + if ( responseDOM.documentElement.nodeName == "parsererror" ) { + /* Still can't parse it, give up. */ + this.setError( "Can't parse incoming XML." ); + return; + } + + var rootElem = responseDOM.documentElement; + + if ( this._connectionStatus == this.CALLED_SERVER ) { + // skip TLS, go straight to SALS. (encryption should be negotiated + // at the HTTP layer, i.e. use HTTPS) + + //dispatch whatever the next stage of the connection protocol is. + response = this._authenticationLayer.generateResponse( rootElem ); + if ( response == false ) { + this.setError( this._authenticationLayer.getError() ); + } else if ( response == this._authenticationLayer.COMPLETION_CODE ){ + this._connectionStatus = this.CONNECTED; + dump( "We be connected!!\n" ); + } else { + this._transportLayer.send( response ); + } + return; + } + + if ( this._connectionStatus == this.CONNECTED ) { + /* Check incoming xml to see if it contains errors, presence info, + or a message: */ + var errors = rootElem.getElementsByTagName( "stream:error" ); + if ( errors.length > 0 ) { + this.setError( errors[0].firstChild.nodeName ); + return; + } + var presences = rootElem.getElementsByTagName( "presence" ); + if (presences.length > 0 ) { + var from = presences[0].getAttribute( "from" ); + if ( from != undefined ) { + dump( "I see that " + from + " is online.\n" ); + } + } + if ( rootElem.nodeName == "message" ) { + this.processIncomingMessage( rootElem ); + } else { + var messages = rootElem.getElementsByTagName( "message" ); + if (messages.length > 0 ) { + for ( var message in messages ) { + this.processIncomingMessage( messages[ message ] ); + } + } + } + if ( rootElem.nodeName == "iq" ) { + this.processIncomingIq( rootElem ); + } else { + var iqs = rootElem.getElementsByTagName( "iq" ); + if ( iqs.length > 0 ) { + for ( var iq in iqs ) { + this.processIncomingIq( iqs[ iq ] ); + } + } + } + } + }, + + processIncomingMessage: function( messageElem ) { + dump( "in processIncomingMessage: messageElem is a " + messageElem + "\n" ); + var from = messageElem.getAttribute( "from" ); + var contentElem = messageElem.firstChild; + // Go down till we find the element with nodeType = 3 (TEXT_NODE) + while ( contentElem.nodeType != 3 ) { + contentElem = contentElem.firstChild; + } + dump( "Incoming message to you from " + from + ":\n" ); + dump( contentElem.nodeValue ); + for ( var x in this._messageHandlers ) { + // TODO do messages have standard place for metadata? + // will want to have handlers that trigger only on certain metadata. + this._messageHandlers[x].handle( contentElem.nodeValue, from ); + } + }, + + processIncomingIq: function( iqElem ) { + /* This processes both kinds of incoming IQ stanzas -- + ones that are new (initated by another jabber client) and those that + are responses to ones we sent out previously. We can tell the + difference by the type attribute. */ + var buddy = iqElem.getAttribute( "from " ); + var id = iqElem.getAttribute( id ); + + switch( iqElem.getAttribute( "type" ) ) { + case "get": + /* Someone is asking us for the value of a variable. + Delegate this to the registered iqResponder; package the answer + up in an IQ stanza of the same ID and send it back to the asker. */ + var variable = iqElem.firstChild.firstChild.getAttribute( "var" ); + // TODO what to do here if there's more than one registered + // iqResponder? + var value = this._iqResponders[0].get( variable ); + var query = ""; + var xml = _makeIqXml( this._fullName, buddy, "result", id, query ); + this._transportLayer.send( xml ); + break; + case "set": + /* Someone is telling us to set the value of a variable. + Delegate this to the registered iqResponder; we can reply + either with an empty iq type="result" stanza, or else an + iq type="error" stanza */ + var variable = iqElem.firstChild.firstChild.getAttribute( "var" ); + var newValue = iqElem.firstChild.firstChildgetAttribute( "value" ); + // TODO what happens when there's more than one reigistered + // responder? + // TODO give the responder a chance to say "no" and give an error. + this._iqResponders[0].set( variable, value ); + var xml = _makeIqXml( this._fullName, buddy, "result", id, "" ); + this._transportLayer.send( xml ); + break; + case "result": + /* If all is right with the universe, then the id of this iq stanza + corresponds to a set or get stanza that we sent out, so it should + be in our pending dictionary. + */ + if ( this._pendingIqs[ id ] == undefined ) { + this.setError( "Unexpected IQ reply id" + id ); + return; + } + /* The result stanza may have a query with a value in it, in + which case this is the value of the variable we requested. + If there's no value, it was probably a set query, and should + just be considred a success. */ + var newValue = iqElem.firstChild.firstChild.getAttribute( "value" ); + if ( newValue != undefined ) { + this._pendingIqs[ id ].value = newValue; + } else { + this._pendingIqs[ id ].value = true; + } + this._pendingIqs[ id ].status = this.IQ_OK; + break; + case "error": + /* Dig down through the element tree till we find the one with + the error text... */ + var elems = iqElem.getElementsByTagName( "error" ); + var errorNode = elems[0].firstChild; + if ( errorNode.nodeValue != null ) { + this.setError( errorNode.nodeValue ); + } else { + this.setError( errorNode.nodeName ); + } + if ( this._pendingIqs[ id ] != undefined ) { + this._pendingIqs[ id ].status = this.IQ_ERROR; + } + break; + } + }, + + registerMessageHandler: function( handlerObject ) { + /* messageHandler object must have + handle( messageText, from ) method. + */ + this._messageHandlers.push( handlerObject ); + }, + + registerIQResponder: function( handlerObject ) { + /* IQResponder object must have + .get( variable ) and + .set( variable, newvalue ) methods. */ + this._iqResponders.push( handlerObject ); + }, + + onTransportError: function( errorText ) { + this.setError( errorText ); + }, + + connect: function( host ) { + // Do the handshake to connect with the server and authenticate. + this._transportLayer.connect(); + this._transportLayer.setCallbackObject( this ); + this._transportLayer.send( this._makeHeaderXml( host ) ); + + this._connectionStatus = this.CALLED_SERVER; + // Now we wait... the rest of the protocol will be driven by + // onIncomingData. + }, + + _makeHeaderXml: function( recipient ) { + return ""; + }, + + _makeMessageXml: function( messageText, fullName, recipient ) { + /* a "message stanza". Note the message element must have the + full namespace info or it will be rejected. */ + msgXml = "" + messageText + ""; + dump( "Message xml: \n" ); + dump( msgXml ); + return msgXml; + }, + + _makePresenceXml: function( fullName ) { + // a "presence stanza", sent to announce my presence to the server; + // the server is supposed to multiplex this to anyone subscribed to + // presence notifications. + return ""; + }, + + _makeIqXml: function( fullName, recipient, type, id, query ) { + /* an "iq (info/query) stanza". This can be used for structured data + exchange: I send an containing a query, + and get back an containing the answer to my + query. I can also send an to set a value + remotely. The recipient answers with either or + , with an id matching the id of my set or get. */ + + //Useful!! + return "" + query + ""; + }, + + _makeClosingXml: function () { + return ""; + }, + + _generateIqId: function() { + // Each time this is called, it returns an ID that has not + // previously been used this session. + var id = "client_" + this._nextIqId; + this._nextIqId = this._nextIqId + 1; + return id; + }, + + _sendIq: function( recipient, query, type ) { + var id = this._generateIqId(); + this._pendingIqs[ id ] = { status: this.IQ_WAIT }; + this._transportLayer.send( this._makeIqXml( this._fullName, + recipient, + type, + id, + query ) ); + /* And then wait for a response with the same ID to come back... + When we get a reply, the pendingIq dictionary entry will have + its status set to IQ_OK or IQ_ERROR and, if it's IQ_OK and + this was a query that's supposed to return a value, the value + will be in the value field of the entry. */ + var thread = this._threadManager.currentThread; + while( this._pendingIqs[ id ].status == this.IQ_WAIT ) { + thread.processNextEvent( true ); + } + if ( this._pendingIqs[ id ].status == this.IQ_OK ) { + return this._pendingIqs[ id ].value; + } else if ( this._pendingIqs[ id ].status == this.IQ_ERROR ) { + return false; + } + // Can't happen? + }, + + iqGet: function( recipient, variable ) { + var query = ""; + return this._sendIq( recipient, query, "get" ); + }, + + iqSet: function( recipient, variable, value ) { + var query = ""; + return this._sendIq( recipient, query, "set" ); + }, + + sendMessage: function( recipient, messageText ) { + // OK so now I'm doing that part, but what am I supposed to do with the + // new JID that I'm bound to?? + var body = this._makeMessageXml( messageText, this._fullName, recipient ); + this._transportLayer.send( body ); + }, + + announcePresence: function() { + this._transportLayer.send( "" ); + }, + + subscribeForPresence: function( buddyId ) { + // OK, there are 'subscriptions' and also 'rosters'...? + //this._transportLayer.send( "" ); + // TODO + // other side must then approve this by sending back a presence to + // me with type ='subscribed'. + }, + + disconnect: function() { + // todo: only send closing xml if the stream has not already been + // closed (if there was an error, the server will have closed the stream.) + this._transportLayer.send( this._makeClosingXml() ); + this._transportLayer.disconnect(); + }, + + waitForConnection: function( ) { + var thread = this._threadManager.currentThread; + while ( this._connectionStatus != this.CONNECTED && + this._connectionStatus != this.FAILED ) { + thread.processNextEvent( true ); + } + }, + + waitForDisconnect: function() { + var thread = this._threadManager.currentThread; + while ( this._connectionStatus == this.CONNECTED ) { + thread.processNextEvent( true ); + } + } + +}; + +// IM level protocol stuff: presence announcements, conversations, etc. +// ftp://ftp.isi.edu/in-notes/rfc3921.txt + From cbc4816741691eb4c8c89afad2f8bfb348652c7a Mon Sep 17 00:00:00 2001 From: Date: Wed, 30 Apr 2008 16:29:03 -0700 Subject: [PATCH 0214/1860] Gave sasl.js a more descriptive name -- authentication is what it does. --- services/sync/modules/xmpp/{sasl.js => authenticationLayer.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename services/sync/modules/xmpp/{sasl.js => authenticationLayer.js} (100%) diff --git a/services/sync/modules/xmpp/sasl.js b/services/sync/modules/xmpp/authenticationLayer.js similarity index 100% rename from services/sync/modules/xmpp/sasl.js rename to services/sync/modules/xmpp/authenticationLayer.js From 106d47be174802320b10a443284f03d1f6d1d53d Mon Sep 17 00:00:00 2001 From: Date: Wed, 30 Apr 2008 16:55:34 -0700 Subject: [PATCH 0215/1860] Made xmppClient, transportLayer, and authenticationLayer into proper modules using Components.Utils. Also renamed the JabberClient class to XMPPClient, which is more accurate (as it implements XMPP which is a newer protocol than Jabber.) --- services/sync/modules/xmpp/authenticationLayer.js | 2 ++ services/sync/modules/xmpp/transportLayer.js | 14 ++++++-------- services/sync/modules/xmpp/xmppClient.js | 8 ++++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js index 0b1b97e15463..efd48481c183 100644 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ b/services/sync/modules/xmpp/authenticationLayer.js @@ -1,3 +1,5 @@ +const EXPORTED_SYMBOLS = ['PlainAuthenticator', 'Md5DigestAuthenticator']; + if(typeof(atob) == 'undefined') { // This code was written by Tyler Akins and has been placed in the // public domain. It would be nice if you left this header intact. diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index d632527235e1..bda52aa1404e 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -1,3 +1,5 @@ +const EXPORTED_SYMBOLS = ['HTTPPollingTransport']; + var Cc = Components.classes; var Ci = Components.interfaces; @@ -17,6 +19,10 @@ InputStreamBuffer.prototype = { } function SocketClient( host, port ) { + /* A transport layer that uses raw sockets. + Not recommended for use; currently fails when trying to negotiate + TLS. + Use HTTPPollingTransport instead. */ this._init( host, port ); } SocketClient.prototype = { @@ -128,10 +134,6 @@ SocketClient.prototype = { this._transport.securityInfo.StartTLS(); }, - // TODO have a variant of waitForResponse that gets binary data - // binaryInStream = Cc["@mozilla.org/binaryinputstream;1].createInstance( Ci.nsIBinaryInputStream ); - // binaryInStream.setInputStream( this._rawInputStream ); - }; @@ -330,7 +332,3 @@ HTTPPollingTransport.prototype = { }, }; - - -//transport = new HTTPPollingTransport( "http://127.0.0.1:5280/http-poll" ); -//transport.testKeys(); diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 0262efc07762..4ca33395ef44 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -1,3 +1,4 @@ +const EXPORTED_SYMBOLS = ['XMPPClient', 'HTTPPollingTransport', 'PlainAuthenticator', 'Md5DigestAuthenticator']; // See www.xulplanet.com/tutorials/mozsdk/sockets.php // http://www.xmpp.org/specs/rfc3920.html @@ -8,12 +9,15 @@ var Cc = Components.classes; var Ci = Components.interfaces; +var Cu = Components.utils; +Cu.import("resource://weave/xmpp/transportLayer.js"); +Cu.import("resource://weave/xmpp/authenticationLayer.js"); -function JabberClient( clientName, realm, clientPassword, transport, authenticator ) { +function XmppClient( clientName, realm, clientPassword, transport, authenticator ) { this._init( clientName, realm, clientPassword, transport, authenticator ); } -JabberClient.prototype = { +XmppClient.prototype = { //connection status codes: NOT_CONNECTED: 0, CALLED_SERVER: 1, From f425652bb7c630a54f10ae0ba43eb9f5838e49e9 Mon Sep 17 00:00:00 2001 From: Date: Wed, 30 Apr 2008 17:08:39 -0700 Subject: [PATCH 0216/1860] Oops, I miscapitalized XmppClient in the export statement -- fixed. --- services/sync/modules/xmpp/xmppClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 4ca33395ef44..a557513481ab 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -1,4 +1,4 @@ -const EXPORTED_SYMBOLS = ['XMPPClient', 'HTTPPollingTransport', 'PlainAuthenticator', 'Md5DigestAuthenticator']; +const EXPORTED_SYMBOLS = ['XmppClient', 'HTTPPollingTransport', 'PlainAuthenticator', 'Md5DigestAuthenticator']; // See www.xulplanet.com/tutorials/mozsdk/sockets.php // http://www.xmpp.org/specs/rfc3920.html From 267181b8f97529e2ffe98e6e5323db7ed53d0fb1 Mon Sep 17 00:00:00 2001 From: Date: Tue, 6 May 2008 12:04:04 -0700 Subject: [PATCH 0217/1860] renamed testSynchronizer.js to just synchronizer.js, because otherwise the test framework tries to run it as a test itself. --- services/sync/modules/xmpp/authenticationLayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js index efd48481c183..838321352827 100644 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ b/services/sync/modules/xmpp/authenticationLayer.js @@ -1,4 +1,4 @@ -const EXPORTED_SYMBOLS = ['PlainAuthenticator', 'Md5DigestAuthenticator']; +const EXPORTED_SYMBOLS = [ "PlainAuthenticator", "Md5DigestAuthenticator" ]; if(typeof(atob) == 'undefined') { // This code was written by Tyler Akins and has been placed in the From 9690848ca72db495fe23feada0fec9719b76fab7 Mon Sep 17 00:00:00 2001 From: Date: Tue, 6 May 2008 13:13:26 -0700 Subject: [PATCH 0218/1860] Made a very simple unit test for xmppClient; it's passing, but currently it connects to the hard-coded URL of a jabber server running on localhost; I'll need to change this if other people are going to be able to run this test. --- services/sync/tests/unit/test_xmpp.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 services/sync/tests/unit/test_xmpp.js diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js new file mode 100644 index 000000000000..956e1195d62d --- /dev/null +++ b/services/sync/tests/unit/test_xmpp.js @@ -0,0 +1,25 @@ +var Cu = Components.utils; + +Cu.import( "resource://weave/xmpp/xmppClient.js" ); + +var serverUrl = "http://127.0.0.1:5280/http-poll"; +var jabberName = "alice"; +var jabberDomain = "jonathan-dicarlos-macbook-pro.local"; +var jabberPassword = "iamalice"; + +function run_test() { + /* First, just see if we can connect: */ + var transport = new HTTPPollingTransport( serverUrl, + false, + 10000 ); + var auth = new PlainAuthenticator(); + var client = new XmppClient( jabberName, jabberDomain, jabberPassword, + transport, auth ); + + client.connect( jabberDomain ); + client.waitForConnection(); + do_check_neq( client._connectionStatus, client.FAILED ); + if ( client._connectionStatus != client.FAILED ) { + client.disconnect(); + }; +}; From 8f80e122b7365f7c1d5d11513c261db2ac895d6b Mon Sep 17 00:00:00 2001 From: Date: Wed, 7 May 2008 15:29:42 -0700 Subject: [PATCH 0219/1860] Expanded test_xmpp -- currently failing tests but it's got the setup now for timing out and failing if the expected message is not received. --- services/sync/tests/unit/test_xmpp.js | 77 +++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index 956e1195d62d..6c0b27c2caa5 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -3,23 +3,80 @@ var Cu = Components.utils; Cu.import( "resource://weave/xmpp/xmppClient.js" ); var serverUrl = "http://127.0.0.1:5280/http-poll"; -var jabberName = "alice"; var jabberDomain = "jonathan-dicarlos-macbook-pro.local"; -var jabberPassword = "iamalice"; + +var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); +var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); function run_test() { /* First, just see if we can connect: */ var transport = new HTTPPollingTransport( serverUrl, false, - 10000 ); + 4000 ); var auth = new PlainAuthenticator(); - var client = new XmppClient( jabberName, jabberDomain, jabberPassword, + var alice = new XmppClient( "alice", jabberDomain, "iamalice", transport, auth ); - - client.connect( jabberDomain ); - client.waitForConnection(); - do_check_neq( client._connectionStatus, client.FAILED ); - if ( client._connectionStatus != client.FAILED ) { - client.disconnect(); + + /* + alice.connect( jabberDomain ); + alice.waitForConnection(); + do_check_neq( alice._connectionStatus, alice.FAILED ); + if ( alice._connectionStatus != alice.FAILED ) { + alice.disconnect(); }; + //A flaw here: once alice disconnects, she can't connect again? + */ + + + // The talking-to-myself test: + var testIsOver = false; + var sometext = "bla bla how you doin bla"; + var transport2 = new HTTPPollingTransport( serverUrl, false, 4000 ); + var auth2 = new PlainAuthenticator(); + var bob = new XmppClient( "bob", jabberDomain, "iambob", transport2, auth2 ); + + // Timer that will make the test fail if message is not received after + // a certain amount of time + var timerResponder = { + notify: function( timer ) { + testIsOver = true; + do_throw( "Timed out waiting for message." ); + } + }; + timer.initWithCallback( timerResponder, 10000, timer.TYPE_ONE_SHOT ); + + + // Handler that listens for the incoming message: + var aliceMessageHandler = { + handle: function( msgText, from ) { + dump( "Alice got a message.\n" ); + do_check_eq( msgText, sometext ); + do_check_eq( from, "bob@" + jabberDomain ); + timer.cancel(); + testIsOver = true; + } + }; + alice.registerMessageHandler( aliceMessageHandler ); + + // Start both clients + bob.connect( jabberDomain ); + bob.waitForConnection(); + do_check_neq( bob._connectionStatus, bob.FAILED ); + alice.connect( jabberDomain ); + alice.waitForConnection(); + do_check_neq( alice._connectionStatus, alice.FAILED ); + + dump( "Is test over? " + testIsOver + "\n" ); + // Send the message + bob.sendMessage( "alice@" + jabberDomain, sometext ); + dump( "Is test over? " + testIsOver + "\n" ); + // Wait until either the message is received, or the timeout expires. + var currentThread = threadManager.currentThread; + while( !testIsOver ) { + currentThread.processNextEvent( true ); + } + dump( "I'm past the while loop!\n " ); + + alice.disconnect(); + bob.disconnect(); }; From 66254d0749262099a9873ddd3eee2070dbebe9b5 Mon Sep 17 00:00:00 2001 From: Date: Thu, 8 May 2008 18:50:12 -0700 Subject: [PATCH 0220/1860] Created a readme for using the XMPP client module. --- services/sync/modules/xmpp/readme.txt | 136 ++++++++++++++++++++++++++ services/sync/tests/unit/test_xmpp.js | 3 +- 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 services/sync/modules/xmpp/readme.txt diff --git a/services/sync/modules/xmpp/readme.txt b/services/sync/modules/xmpp/readme.txt new file mode 100644 index 000000000000..5f766dcf5763 --- /dev/null +++ b/services/sync/modules/xmpp/readme.txt @@ -0,0 +1,136 @@ +About the XMPP module + +Here is sample code demonstrating how client code can use the XMPP module. It assumes that a Jabber server is running on localhost on port 5280. + + Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); + + var serverUrl = "http://127.0.0.1:5280/http-poll"; + var jabberDomain = "mylaptop.local"; + + var transport = new HTTPPollingTransport( serverUrl, + false, + 4000 ); + // "false" tells the transport not to use session keys. 4000 is the number of + // milliseconds to wait between attempts to poll the server. + var auth = new PlainAuthenticator(); + var alice = new XmppClient( "alice", jabberDomain, "iamalice", + transport, auth ); + // This sets up an XMPP client for the jabber ID + // "alice@jonathan-dicarlos-macbook-pro.local", who has password + // "iamalice". + + // Set up callback for incoming messages: + var aliceMessageHandler = { + handle: function( msgText, from ) { + // Your code goes here. It will be called whenever another XMPP client + // sends a message to Alice. + // msgText is the text of the incoming message, and "from" is the + // jabber ID of the sender. + } + }; + alice.registerMessageHandler( aliceMessageHandler ); + + // Connect + alice.connect( jabberDomain ); + alice.waitForConnection(); + // this will block until our connection attempt has either succeeded or failed. + // Check if connection succeeded: + if ( alice._connectionStatus == alice.FAILED ) { + // handle error + } + + // Send a message + alice.sendMessage( "bob@mylaptop.local", "Hello, Bob." ); + + // Disconnect + alice.disconnect(); + + +Installing an XMPP server to test against: + +I'm using ejabberd, which works well and is open source but is implemented in Erlang. (That's bad, because I don't know how to hack Erlang. I'm thinking of moving to jabberd, implemented in C.) + +ejabberd: http://www.ejabberd.im/ +jabberd: http://jabberd.jabberstudio.org/ + +Installation of ejabberd was simple. After it's installed, configure it (create an admin account, and enable http-polling) by editing the configuration file: + + ejabberd/conf/ejabberd.cfg + + Its web admin interface can be used to create accounts (so that the "alice" and "bob" accounts assumed by the testing code will actually exist). This admin interface is at: + + http://localhost:5280/admin/ + +The ejabberd process is started simply by running: + + ejabberd/bin/ejabberdctl start + + + +Outstanding Issues -- bugs and things to do. + + +* Occasionally (by which I mean, randomly) the server will respond to + a request with an HTTP status 0. Generally things will go back to + normal with the next request and then keep working, so it's not + actually preventing anything from working, but I don't understand + what status 0 means and that makes me uncomfortable as it could be + hiding other problems. + +* Occasionally (randomly) a message is received that has a raw integer + for a messageElem, which causes XML parsing of the incoming message + to fail. I don't see any pattern for the values of the raw + integers, nor do I understand how we can get an integer in place of + an XML element when requesting the root node of the incoming XML + document. + +* Duplicate messages. Occasionally, even though one instance of the + client sends only a single copy of a stanza, the intended + target will receive it multiple times. Sometimes if the recipient + signs off and then signs back on, it will recieve another copy of + the message. This makes me think it's probably a feature of the + server to resend messages that it thinks the recipient might have + missed while offline. Duplicate messages are a problem especially + when using xmpp to synchronize two processes, for instance. Do I + need to tweak the server settings, or change server implementations? + Do I need to have the client receiving messages acknowledge them to the + server somehow? + Do I need to move to using stanzas for synchronization? Or + start putting GUIDs into s and discarding duplicates + myself? + +* Whenever I send an request, I get a error back + from the server. Documentation for the standard error messages + indicates that bad-request is supposed to happen when the type of + the iq element is invalid, i.e. something other than 'set', 'get', + 'result', or 'error'. But I'm using 'get' or 'set' and still + getting . I don't understand what's happening here. + +* The authentication layer currently doesn't work with MD5-digest auth, only plain + auth. + +* The HTTPPolling transport layer gets a "key sequence error" if useKeys is turned on. + (Everything seems to be working OK with useKeys turned off, but that's less + secure.) + +* Speaking of security, I need to try using HTTP polling transport over an HTTPS + connection and see if everything works. If it does, that will be great, because + we'll have SSL/TLS for free, and it won't matter so much that we're using + plain auth because the password will be encrypted as part of SSL. + +* Need to implement the presence-notification/subscription/"buddy list" stuff + so that clients can more easily know when other clients are online. + + +To anyone reading this, I'd appreciate any help in debugging these problems. +Can you duplicate these problems? Do you have any suggestions of things to try? + + + +For a forum post: +copy the outstanding issues list + +Here's where/how I'm trying to install the jabberd server. + + +For email to the list: diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index 6c0b27c2caa5..566cca9c9ed7 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -24,7 +24,8 @@ function run_test() { if ( alice._connectionStatus != alice.FAILED ) { alice.disconnect(); }; - //A flaw here: once alice disconnects, she can't connect again? + // A flaw here: once alice disconnects, she can't connect again? + // Make an explicit test out of that. */ From 35d5bb7618c70f6848abb84eae67778b4a63629f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 12 May 2008 10:11:07 -0700 Subject: [PATCH 0221/1860] sync bookmark descriptions --- services/sync/modules/stores.js | 34 ++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index e89aa3eb68d3..03428bbc8fdf 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -361,6 +361,11 @@ BookmarksStore.prototype = { this._ts.untagURI(URI, null); this._ts.tagURI(URI, command.data.tags); this._bms.setKeywordForBookmark(newId, command.data.keyword); + if (command.data.description) { + this._ans.setItemAnnotation(newId, "bookmarkProperties/description", + command.data.description, 0, + this._ans.EXPIRE_NEVER); + } if (command.data.type == "microsummary") { this._log.debug(" \-> is a microsummary"); @@ -497,6 +502,13 @@ BookmarksStore.prototype = { case "keyword": this._bms.setKeywordForBookmark(itemId, command.data.keyword); break; + case "description": + if (command.data.description) { + this._ans.setItemAnnotation(itemId, "bookmarkProperties/description", + command.data.description, 0, + this._ans.EXPIRE_NEVER); + } + break; case "generatorURI": { let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); let genURI = Utils.makeURI(command.data.generatorURI); @@ -573,6 +585,14 @@ BookmarksStore.prototype = { item.type = "bookmark"; item.title = node.title; } + + try { + item.description = + this._ans.getItemAnnotation(node.itemId, "bookmarkProperties/description"); + } catch (e) { + item.description = undefined; + } + item.URI = node.uri; item.tags = this._ts.getTagsForURI(Utils.makeURI(node.uri), {}); item.keyword = this._bms.getKeywordForBookmark(node.itemId); @@ -797,10 +817,10 @@ CookieStore.prototype = { in order to sync with the server. command.data appears to be equivalent to what wrap() puts in the JSON dictionary. */ - + this._log.info("CookieStore got removeCommand: " + command ); - /* I think it goes like this, according to + /* I think it goes like this, according to http://developer.mozilla.org/en/docs/nsICookieManager the last argument is "always block cookies from this domain?" and the answer is "no". */ @@ -821,13 +841,13 @@ CookieStore.prototype = { while (iter.hasMoreElements()){ let cookie = iter.getNext(); if (cookie instanceof Ci.nsICookie){ - // see if host:path:name of cookie matches GUID given in command + // see if host:path:name of cookie matches GUID given in command let key = cookie.host + ":" + cookie.path + ":" + cookie.name; if (key == command.GUID) { matchingCookie = cookie; break; } - } + } } // Update values in the cookie: for (var key in command.data) { @@ -848,7 +868,7 @@ CookieStore.prototype = { matchingCookie.isHttpOnly, matchingCookie.isSession, matchingCookie.expiry ); - + // Also, there's an exception raised because // this._data[comand.GUID] is undefined }, @@ -878,11 +898,11 @@ CookieStore.prototype = { isSession: cookie.isSession, expiry: cookie.expiry, isHttpOnly: cookie.isHttpOnly } - + /* See http://developer.mozilla.org/en/docs/nsICookie Note: not syncing "expires", "status", or "policy" since they're deprecated. */ - + } } return items; From 9888d13bd3db433d3237d085c382f409fafb5c01 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 12 May 2008 10:18:20 -0700 Subject: [PATCH 0222/1860] temporarily disable xmpp unit test --- services/sync/tests/unit/test_xmpp.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index 566cca9c9ed7..e6429485ba3e 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -9,6 +9,10 @@ var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); function run_test() { + + // FIXME: this test hangs when you don't have a server, disabling for now + return; + /* First, just see if we can connect: */ var transport = new HTTPPollingTransport( serverUrl, false, @@ -17,7 +21,7 @@ function run_test() { var alice = new XmppClient( "alice", jabberDomain, "iamalice", transport, auth ); - /* + /* alice.connect( jabberDomain ); alice.waitForConnection(); do_check_neq( alice._connectionStatus, alice.FAILED ); @@ -28,7 +32,7 @@ function run_test() { // Make an explicit test out of that. */ - + // The talking-to-myself test: var testIsOver = false; var sometext = "bla bla how you doin bla"; @@ -46,7 +50,7 @@ function run_test() { }; timer.initWithCallback( timerResponder, 10000, timer.TYPE_ONE_SHOT ); - + // Handler that listens for the incoming message: var aliceMessageHandler = { handle: function( msgText, from ) { From d4568f9013cbd6014970db017f318bfc5ba1cb5c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 13 May 2008 18:37:07 -0700 Subject: [PATCH 0223/1860] windows build fixes for crypto component --- services/crypto/Makefile | 134 +++++++++++++++++------------- services/crypto/WeaveCrypto.rc.in | 94 +++++++++++++++++++++ 2 files changed, 172 insertions(+), 56 deletions(-) mode change 100644 => 100755 services/crypto/Makefile create mode 100644 services/crypto/WeaveCrypto.rc.in diff --git a/services/crypto/Makefile b/services/crypto/Makefile old mode 100644 new mode 100755 index 2dfacede220f..1518bc724250 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -36,56 +36,6 @@ # # ***** END LICENSE BLOCK ***** -idl = IWeaveCrypto.idl -cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp -target = WeaveCrypto # will have .so / .dylib / .dll appended - -sdkdir ?= ${MOZSDKDIR} -destdir = .. -platformdir = $(destdir)/platform/$(platform) - -xpidl = $(sdkdir)/bin/xpidl - -# FIXME: we don't actually require this for e.g. clean -ifndef sdkdir - $(warning No 'sdkdir' variable given) - $(warning It should point to the location of the Gecko SDK) - $(warning For example: "make sdkdir=/foo/bar/baz") - $(error ) -endif - -###################################################################### - -headers = -I$(sdkdir)/include \ - -I$(sdkdir)/include/system_wrappers \ - -I$(sdkdir)/include/nss \ - -I$(sdkdir)/include/xpcom \ - -I$(sdkdir)/include/string \ - -I$(sdkdir)/include/pipnss \ - -I$(sdkdir)/include/nspr \ - -I$(sdkdir)/sdk/include - -cppflags += -c -pipe -Os \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fpascal-strings -fno-common -fshort-wchar -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) - -libdirs = -L$(sdkdir)/lib -L$(sdkdir)/bin -libs = -lxpcomglue_s -lxpcom \ - -lcrmf -lsmime3 -lssl3 -lnss3 -lnssutil3 -lsoftokn3 \ - -lplds4 -lplc4 -lnspr4 - -ldflags += -pthread -pipe -bundle \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - $(libdirs) $(libs) - -###################################################################### # Platform detection sys := $(shell uname -s) @@ -104,15 +54,14 @@ else so = dylib cppflags += -dynamiclib else - ifeq ($(os), MINGW32_NT-5.1) - $(error Sorry, windows is not supported yet) + ifeq ($(sys), MINGW32_NT-6.0) os = WINNT compiler = msvc - cxx = c++ # fixme + cxx = cl so = dll - cppflags += -shared +# cppflags += -LD else - $(error Sorry, your os is unknown/unsupported: $(os)) + $(error Sorry, your os is unknown/unsupported: $(sys)) endif endif endif @@ -140,11 +89,71 @@ endif platform = $(os)_$(arch)-$(compiler) +###################################################################### + +idl = IWeaveCrypto.idl +cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp +target = WeaveCrypto # will have .so / .dylib / .dll appended + +sdkdir ?= ${MOZSDKDIR} +destdir = .. +platformdir = $(destdir)/platform/$(platform) + +xpidl = $(sdkdir)/bin/xpidl + +# FIXME: we don't actually require this for e.g. clean +ifeq ($(sdkdir),) + $(warning No 'sdkdir' variable given) + $(warning It should point to the location of the Gecko SDK) + $(warning For example: "make sdkdir=/foo/bar/baz") + $(warning Or set the MOZSDKDIR environment variable to point to it) + $(error ) +endif + idl_headers = $(idl:.idl=.h) idl_typelib = $(idl:.idl=.xpt) cpp_objects = $(cpp_sources:.cpp=.o) so_target = $(target:=.$(so)) +headers = -I$(sdkdir)/include \ + -I$(sdkdir)/include/system_wrappers \ + -I$(sdkdir)/include/nss \ + -I$(sdkdir)/include/xpcom \ + -I$(sdkdir)/include/string \ + -I$(sdkdir)/include/pipnss \ + -I$(sdkdir)/include/nspr \ + -I$(sdkdir)/sdk/include + +libdirs := $(sdkdir)/lib $(sdkdir)/bin +libs := xpcomglue_s xpcom nspr4 \ + crmf smime3 ssl3 nss3 nssutil3 softokn3 \ + plds4 plc4 + +ifeq ($(compiler),msvc) +libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) +libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) +cppflags += -c -nologo -O1 -GR- -TP -nologo -Zc:wchar_t- -W3 -Gy $(headers) +ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS $(libdirs) $(libs) kernel32.lib \ + user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib +rcflags := -r $(headers) +else +libdirs := $(patsubst %,-L%,$(libdirs)) +libs := $(patsubst %,-l%,$(libs)) +cppflags += -c -pipe -Os \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fpascal-strings -fno-common -fshort-wchar -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) +ldflags += -pthread -pipe -bundle \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + $(libdirs) $(libs) +endif + ###################################################################### .PHONY: all build install test-install clean @@ -181,9 +190,22 @@ $(idl_typelib): $(idl) # "main" (internal) rules, build sources and link the component $(cpp_objects): $(cpp_sources) +ifeq ($(cxx),cl) + $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) +else $(cxx) -o $@ $(cppflags) $(@:.o=.cpp) +endif -$(so_target): $(idl_headers) $(cpp_objects) +$(target:=.res): $(target:=.rc) +ifeq ($(compiler),msvc) + rc -Fo$@ $(rcflags) $(target:=.rc) +endif + +$(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) +ifeq ($(compiler),msvc) + link -out:$@ $(ldflags) $(cpp_objects) +else $(cxx) -o $@ $(ldflags) $(cpp_objects) +endif chmod +x $@ # strip $@ diff --git a/services/crypto/WeaveCrypto.rc.in b/services/crypto/WeaveCrypto.rc.in new file mode 100644 index 000000000000..85d02f93f8a0 --- /dev/null +++ b/services/crypto/WeaveCrypto.rc.in @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* See http://msdn.microsoft.com/en-us/library/aa381058.aspx for format docs, + * and mozilla/config/version_win.pl for what Mozilla uses + */ + +#include + +#define VER_BUILDID_STR "@buildid@" +#define VER_FILEVERSION 1,9,0,@buildid@ +#define VER_PRODUCTVERSION 1,9,0,@buildid@ + +#define VER_FILEFLAGS 0 | VS_FF_PRIVATEBUILD | VS_FF_PRERELEASE + +#define VER_PRODUCTNAME_STR "Weave" +#define VER_INTERNALNAME_STR "WeaveCrypto" +#define VER_FILEVERSION_STR "1.9pre" +#define VER_PRODUCTVERSION_STR "1.9pre" + +#define VER_COMPANYNAME_STR "Mozilla Corporation" +#define VER_LEGALTRADEMARKS_STR "Mozilla" +#define VER_LEGALCOPYRIGHT_STR "License: MPL 1.1/GPL 2.0/LGPL 2.1" + +#define VER_COMMENTS_STR "" +#define VER_FILEDESCRIPTION_STR "" +#define VER_ORIGINALFILENAME_STR "" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_PRODUCTVERSION +PRODUCTVERSION VER_PRODUCTVERSION +FILEFLAGSMASK 0x3fL +FILEFLAGS VER_FILEFLAGS +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000004b0" + BEGIN + VALUE "Comments", VER_COMMENTS_STR + VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR + VALUE "CompanyName", VER_COMPANYNAME_STR + VALUE "FileDescription", VER_FILEDESCRIPTION_STR + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + VALUE "InternalName", VER_INTERNALNAME_STR + VALUE "LegalTrademarks", VER_LEGALTRADEMARKS_STR + VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR + VALUE "ProductName", VER_PRODUCTNAME_STR + VALUE "BuildID", VER_BUILDID_STR + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0, 1200 + END +END From 4d68b705e38486cdb2367b6678928ca758c5e201 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 14 May 2008 17:15:55 -0700 Subject: [PATCH 0224/1860] more windows (msvc) build fixes --- services/crypto/Makefile | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 1518bc724250..1f670dec8213 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -59,7 +59,6 @@ else compiler = msvc cxx = cl so = dll -# cppflags += -LD else $(error Sorry, your os is unknown/unsupported: $(sys)) endif @@ -132,9 +131,18 @@ libs := xpcomglue_s xpcom nspr4 \ ifeq ($(compiler),msvc) libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) -cppflags += -c -nologo -O1 -GR- -TP -nologo -Zc:wchar_t- -W3 -Gy $(headers) -ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS $(libdirs) $(libs) kernel32.lib \ - user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib +cppflags += -c -nologo -O1 -GR- -TP -MT -Zc:wchar_t- -W3 -Gy $(headers) \ +-DNDEBUG -DTRIMMED -D_CRT_SECURE_NO_DEPRECATE=1 \ +-D_CRT_NONSTDC_NO_DEPRECATE=1 -DWINVER=0x500 -D_WIN32_WINNT=0x500 \ +-D_WIN32_IE=0x0500 -DX_DISPLAY_MISSING=1 -DMOZILLA_VERSION=\"1.9pre\" \ +-DMOZILLA_VERSION_U=1.9pre -DHAVE_SNPRINTF=1 -D_WINDOWS=1 -D_WIN32=1 \ +-DWIN32=1 -DXP_WIN=1 -DXP_WIN32=1 -DHW_THREADS=1 -DSTDC_HEADERS=1 \ +-DWIN32_LEAN_AND_MEAN=1 -DNO_X11=1 -DHAVE_MMINTRIN_H=1 \ +-DHAVE_OLEACC_IDL=1 -DHAVE_ATLBASE_H=1 -DHAVE_WPCAPI_H=1 -D_X86_=1 \ +-DD_INO=d_ino +ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ + $(libdirs) $(libs) \ + kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib rcflags := -r $(headers) else libdirs := $(patsubst %,-L%,$(libdirs)) @@ -177,7 +185,7 @@ test-install: install rm -f $(sdkdir)/bin/components/xpti.dat clean: - rm -f $(so_target) $(cpp_objects) $(idl_typelib) $(idl_headers) + rm -f $(so_target) $(cpp_objects) $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp # rules to build the c headers and .xpt from idl @@ -203,7 +211,7 @@ endif $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) ifeq ($(compiler),msvc) - link -out:$@ $(ldflags) $(cpp_objects) + link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) else $(cxx) -o $@ $(ldflags) $(cpp_objects) endif From 3d1a9547a69ae0d0bbe0b3452c7ea53fcee8f22a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 15 May 2008 18:08:13 -0700 Subject: [PATCH 0225/1860] DAV: separate the root url of the DAV repository from the default prefix for all operations (i.e., the user's subdirectory) --- services/sync/modules/dav.js | 12 ++++++++++-- services/sync/modules/engines.js | 12 ++++++------ services/sync/modules/service.js | 25 ++++++++----------------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 250e34f51bef..11055ac0350f 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -62,6 +62,7 @@ let DAVLocks = {}; function DAVCollection(baseURL, defaultPrefix) { this.baseURL = baseURL; this.defaultPrefix = defaultPrefix; + this._identity = 'DAV:default'; this._authProvider = new DummyAuthProvider(); this._log = Log4Moz.Service.getLogger("Service.DAV"); this._log.level = @@ -77,6 +78,9 @@ DAVCollection.prototype = { return this.__dp; }, + get identity() { return this._identity; }, + set identity(value) { this._identity = value; }, + get baseURL() { return this._baseURL; }, @@ -94,6 +98,8 @@ DAVCollection.prototype = { value = value + '/'; if (value && value[0] == '/') value = value.slice(1); + if (!value) + value = ''; this._defaultPrefix = value; }, @@ -114,7 +120,9 @@ DAVCollection.prototype = { let self = yield; let ret; - this._log.debug(op + " request for " + path); + this._log.debug(op + " request for " + (path? path : 'root folder')); + + path = this._defaultPrefix + path; let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); @@ -158,7 +166,7 @@ DAVCollection.prototype = { get _defaultHeaders() { let h = {'Content-type': 'text/plain'}, - id = ID.get('DAV:default'), + id = ID.get(this.identity), lock = DAVLocks['default']; if (id) h['Authorization'] = 'Basic ' + btoa(id.username + ":" + id.password); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 01d7c3bbf636..fd241d4a4147 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -730,7 +730,7 @@ Engine.prototype = { _share: function Engine__share(username) { let self = yield; - let base = DAV.baseURL; + let prefix = DAV.defaultPrefix; this._log.debug("Sharing bookmarks with " + username); @@ -748,12 +748,12 @@ Engine.prototype = { let serverURL = Utils.prefs.getCharPref("serverURL"); try { - DAV.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! + DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly! DAV.GET("public/pubkey", self.cb); ret = yield; } catch (e) { throw e; } - finally { DAV.baseURL = base; } + finally { DAV.defaultPrefix = prefix; } Utils.ensureStatus(ret.status, "Could not get public key for " + username); @@ -873,7 +873,7 @@ BookmarksEngine.prototype = { _syncOneMount: function BmkEngine__syncOneMount(mountData) { let self = yield; let user = mountData.userid; - let base = DAV.baseURL; + let prefix = DAV.defaultPrefix; let serverURL = Utils.prefs.getCharPref("serverURL"); let snap = new SnapshotStore(); @@ -881,7 +881,7 @@ BookmarksEngine.prototype = { try { let hash = Utils.sha1(user); - DAV.baseURL = serverURL + "user/" + hash + "/"; //FIXME: very ugly! + DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly! this._getSymKey.async(this, self.cb); yield; @@ -911,7 +911,7 @@ BookmarksEngine.prototype = { deltas = this._json.decode(data); } catch (e) { throw e; } - finally { DAV.baseURL = base; } + finally { DAV.defaultPrefix = prefix; } // apply deltas to get current snapshot for (var i = 0; i < deltas.length; i++) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8de097fa8f99..c305ebdfcc29 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -291,28 +291,19 @@ WeaveSvc.prototype = { _checkUserDir: function WeaveSvc__checkUserDir() { let self = yield; + let prefix = DAV.defaultPrefix; this._log.trace("Checking user directory exists"); - let serverURL = Utils.prefs.getCharPref("serverURL"); - if (serverURL[serverURL.length-1] != '/') - serverURL = serverURL + '/'; - try { - DAV.baseURL = serverURL; + DAV.defaultPrefix = ''; DAV.MKCOL("user/" + this.userPath, self.cb); let ret = yield; if (!ret) throw "Could not create user directory"; - - } catch (e) { - throw e; - - } finally { - DAV.baseURL = serverURL + "user/" + this.userPath + "/"; } - - this._log.info("Using server URL: " + DAV.baseURL); + catch (e) { throw e; } + finally { DAV.defaultPrefix = prefix; } }, _keyCheck: function WeaveSvc__keyCheck() { @@ -403,10 +394,8 @@ WeaveSvc.prototype = { if (!this.password) throw "No password given or found in password manager"; - let serverURL = Utils.prefs.getCharPref("serverURL"); - if (serverURL[serverURL.length-1] != '/') - serverURL = serverURL + '/'; - DAV.baseURL = serverURL + "user/" + this.userPath + "/"; + DAV.baseURL = Utils.prefs.getCharPref("serverURL"); + DAV.defaultPrefix = "user/" + this.userPath; DAV.checkLogin.async(DAV, self.cb, this.username, this.password); let success = yield; @@ -421,6 +410,8 @@ WeaveSvc.prototype = { throw "Login failed"; } + this._log.info("Using server URL: " + DAV.baseURL + DAV.defaltPrefix); + this._versionCheck.async(this, self.cb); yield; this._keyCheck.async(this, self.cb); From 54fd8a594d09e5f247a6c8feaf0f262eec855288 Mon Sep 17 00:00:00 2001 From: Date: Mon, 19 May 2008 19:40:45 -0700 Subject: [PATCH 0226/1860] Created some documentation of how to add synchronization functionality for a new user data type to Weave. Created a new directory called docs to put this into. --- services/sync/modules/xmpp/readme.txt | 13 +++++++++-- services/sync/modules/xmpp/transportLayer.js | 24 ++++++++++++++++---- services/sync/modules/xmpp/xmppClient.js | 2 +- services/sync/tests/unit/test_xmpp.js | 5 +--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/xmpp/readme.txt b/services/sync/modules/xmpp/readme.txt index 5f766dcf5763..43dd3affa242 100644 --- a/services/sync/modules/xmpp/readme.txt +++ b/services/sync/modules/xmpp/readme.txt @@ -69,14 +69,23 @@ The ejabberd process is started simply by running: Outstanding Issues -- bugs and things to do. +* The test above is failing with a timeout. How to debug this? Let's start + by making it two processes again and seeing if that makes a difference... + + Nope. It doesn't. Bob just gets an HTTP status 0 * Occasionally (by which I mean, randomly) the server will respond to - a request with an HTTP status 0. Generally things will go back to - normal with the next request and then keep working, so it's not + a POST with an HTTP status 0. Generally things will go back to + normal with the next POST and then keep working, so it's not actually preventing anything from working, but I don't understand what status 0 means and that makes me uncomfortable as it could be hiding other problems. + What if I modify the sending code to repeat the sending of any messages + that result in HTTP status 0? + -- seems to work, but doesn't help. (It's not HTTP status 0 that's preventing + the test from passing...) + * Occasionally (randomly) a message is received that has a raw integer for a messageElem, which causes XML parsing of the incoming message to fail. I don't see any pattern for the values of the raw diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index bda52aa1404e..a4c03537ecc0 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -164,6 +164,7 @@ HTTPPollingTransport.prototype = { this._callbackObject = null; this._useKeys = useKeys; this._interval = interval; + this._outgoingRetryBuffer = ""; }, __request: null, @@ -263,17 +264,28 @@ HTTPPollingTransport.prototype = { //Callback for XMLHTTPRequest object state change messages if ( request.readyState == 4 ) { if ( request.status == 200) { + // 200 means success. + dump( "Server says: " + request.responseText + "\n" ); // Look for a set-cookie header: var latestCookie = request.getResponseHeader( "Set-Cookie" ); if ( latestCookie.length > 0 ) { self._setIdFromCookie( self, latestCookie ); } + // Respond to any text we get back from the server in response if ( callbackObj != null && request.responseText.length > 0 ) { callbackObj.onIncomingData( request.responseText ); } } else { dump ( "Error! Got HTTP status code " + request.status + "\n" ); + if ( request.status == 0 ) { + /* Sometimes the server gives us HTTP status code 0 in response + to an attempt to POST. I'm not sure why this happens, but + if we re-send the POST it seems to usually work the second + time. So put the message into a buffer and try again later: + */ + self._outgoingRetryBuffer = requestXml; + } } } }; @@ -300,10 +312,14 @@ HTTPPollingTransport.prototype = { /* having a notify method makes this object satisfy the nsITimerCallback interface, so the object can be passed to timer.initWithCallback. */ - /* Periodically, if we don't have anything else to post, we should - post an empty message just to see if the server has any queued - data it's waiting to give us in return. */ - this._doPost( "" ); + /* With HTTPPolling, we need to be contacting the server on a regular + heartbeat, even if we don't have anything to send, just to see if + the server has any data it's waiting to give us. + If we have a message waiting in the outgoingRetryBuffer, send that; + otherwise send nothing. */ + var outgoingMsg = this._outgoingRetryBuffer + this._outgoingRetryBuffer = ""; + this._doPost( outgoingMsg ); }, connect: function() { diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index a557513481ab..34c1f302a27c 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -288,7 +288,7 @@ XmppClient.prototype = { _makeMessageXml: function( messageText, fullName, recipient ) { /* a "message stanza". Note the message element must have the full namespace info or it will be rejected. */ - msgXml = "" + messageText + ""; + var msgXml = "" + messageText + ""; dump( "Message xml: \n" ); dump( msgXml ); return msgXml; diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index e6429485ba3e..dcc84c5abadc 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -48,7 +48,7 @@ function run_test() { do_throw( "Timed out waiting for message." ); } }; - timer.initWithCallback( timerResponder, 10000, timer.TYPE_ONE_SHOT ); + timer.initWithCallback( timerResponder, 20000, timer.TYPE_ONE_SHOT ); // Handler that listens for the incoming message: @@ -71,16 +71,13 @@ function run_test() { alice.waitForConnection(); do_check_neq( alice._connectionStatus, alice.FAILED ); - dump( "Is test over? " + testIsOver + "\n" ); // Send the message bob.sendMessage( "alice@" + jabberDomain, sometext ); - dump( "Is test over? " + testIsOver + "\n" ); // Wait until either the message is received, or the timeout expires. var currentThread = threadManager.currentThread; while( !testIsOver ) { currentThread.processNextEvent( true ); } - dump( "I'm past the while loop!\n " ); alice.disconnect(); bob.disconnect(); From c1808724d548983ff8ef5c6ae35e3664c9d981fa Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 20 May 2008 16:53:14 -0700 Subject: [PATCH 0227/1860] Patching to support building component on Linux. --- services/crypto/Makefile | 34 +++++++++++++++++++++++----------- services/crypto/WeaveCrypto.h | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 1f670dec8213..96d655d7a1e5 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -124,7 +124,7 @@ headers = -I$(sdkdir)/include \ -I$(sdkdir)/sdk/include libdirs := $(sdkdir)/lib $(sdkdir)/bin -libs := xpcomglue_s xpcom nspr4 \ +libs := xpcomglue_s xpcom xpcom_core nspr4 \ crmf smime3 ssl3 nss3 nssutil3 softokn3 \ plds4 plc4 @@ -145,21 +145,30 @@ ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib rcflags := -r $(headers) else -libdirs := $(patsubst %,-L%,$(libdirs)) -libs := $(patsubst %,-l%,$(libs)) -cppflags += -c -pipe -Os \ + libdirs := $(patsubst %,-L%,$(libdirs)) + libs := $(patsubst %,-l%,$(libs)) + cppflags += -pipe -Os \ -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fpascal-strings -fno-common -fshort-wchar -pthread \ + -fno-common -fshort-wchar -pthread \ -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ -Wno-long-long \ -include xpcom-config.h $(headers) -ldflags += -pthread -pipe -bundle \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - $(libdirs) $(libs) + ifeq ($(os), Linux) + ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + $(sdkdir)/lib/libxpcomglue_s.a \ + $(libdirs) $(libs) + else + cppflags += -fpascal-strings -c + ldflags += -pthread -pipe -bundle \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + $(libdirs) $(libs) + endif endif ###################################################################### @@ -212,6 +221,9 @@ endif $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) ifeq ($(compiler),msvc) link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) +endif +ifeq ($(os),Linux) + $(cxx) -o $@ $(cppflags) $(ldflags) $(cpp_sources) else $(cxx) -o $@ $(ldflags) $(cpp_objects) endif diff --git a/services/crypto/WeaveCrypto.h b/services/crypto/WeaveCrypto.h index c8b16ac135ce..12969a8fa46a 100644 --- a/services/crypto/WeaveCrypto.h +++ b/services/crypto/WeaveCrypto.h @@ -64,7 +64,7 @@ private: nsresult DecodeBase64(const nsACString& base64, nsACString& retval); nsresult EncodeBase64(const nsACString& binary, nsACString& retval); - static void WeaveCrypto::StoreToStringCallback(void *arg, const char *buf, unsigned long len); + static void StoreToStringCallback(void *arg, const char *buf, unsigned long len); static PK11SymKey *GetSymmetricKeyCallback(void *arg, SECAlgorithmID *algid); static PRBool DecryptionAllowedCallback(SECAlgorithmID *algid, PK11SymKey *key); }; From a053f5bb9be1529d7e80949b2f5fff7e7471a46f Mon Sep 17 00:00:00 2001 From: Date: Tue, 20 May 2008 18:14:18 -0700 Subject: [PATCH 0228/1860] Made cookie-sync sync only persistent cookies, drop single-session cookies. (Needs testing.) --- services/sync/modules/stores.js | 46 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 03428bbc8fdf..24ab680f98cb 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -802,14 +802,17 @@ CookieStore.prototype = { this._log.info("CookieStore got createCommand: " + command ); // this assumes command.data fits the nsICookie2 interface - this._cookieManager.add( command.data.host, - command.data.path, - command.data.name, - command.data.value, - command.data.isSecure, - command.data.isHttpOnly, - command.data.isSession, - command.data.expiry ); + if ( command.data.expiry ) { + // Add only persistent cookies (those with an expiry date). + this._cookieManager.add( command.data.host, + command.data.path, + command.data.name, + command.data.value, + command.data.isSecure, + command.data.isHttpOnly, + command.data.isSession, + command.data.expiry ); + } }, _removeCommand: function CookieStore__removeCommand(command) { @@ -859,15 +862,20 @@ CookieStore.prototype = { matchingCookie.name, matchingCookie.path, false ); + // Re-add the new updated cookie: - this._cookieManager.add( matchingCookie.host, - matchingCookie.path, - matchingCookie.name, - matchingCookie.value, - matchingCookie.isSecure, - matchingCookie.isHttpOnly, - matchingCookie.isSession, - matchingCookie.expiry ); + if ( command.data.expiry ) { + /* ignore single-session cookies, add only persistent + cookies. */ + this._cookieManager.add( matchingCookie.host, + matchingCookie.path, + matchingCookie.name, + matchingCookie.value, + matchingCookie.isSecure, + matchingCookie.isHttpOnly, + matchingCookie.isSession, + matchingCookie.expiry ); + } // Also, there's an exception raised because // this._data[comand.GUID] is undefined @@ -885,6 +893,12 @@ CookieStore.prototype = { if (cookie instanceof Ci.nsICookie){ // String used to identify cookies is // host:path:name + if ( !cookie.expiry ) { + /* Skip cookies that do not have an expiration date. + (Persistent cookies have one, session-only cookies don't.) */ + continue; + } + let key = cookie.host + ":" + cookie.path + ":" + cookie.name; items[ key ] = { parentGUID: '', name: cookie.name, From a5bd139c5cae76421d4f714be1e910485e0c8fbd Mon Sep 17 00:00:00 2001 From: Date: Tue, 20 May 2008 18:24:28 -0700 Subject: [PATCH 0229/1860] Added TODO that expired cookies should be thrown out, in the unlikely event that we get one from a createCommand or editCommand. --- services/sync/modules/stores.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 24ab680f98cb..8ab852f3724f 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -804,6 +804,7 @@ CookieStore.prototype = { // this assumes command.data fits the nsICookie2 interface if ( command.data.expiry ) { // Add only persistent cookies (those with an expiry date). + // TODO: throw out cookies with expiration date in the past? this._cookieManager.add( command.data.host, command.data.path, command.data.name, @@ -866,7 +867,8 @@ CookieStore.prototype = { // Re-add the new updated cookie: if ( command.data.expiry ) { /* ignore single-session cookies, add only persistent - cookies. */ + cookies. + TODO: throw out cookies with expiration dates in the past?*/ this._cookieManager.add( matchingCookie.host, matchingCookie.path, matchingCookie.name, @@ -895,7 +897,9 @@ CookieStore.prototype = { // host:path:name if ( !cookie.expiry ) { /* Skip cookies that do not have an expiration date. - (Persistent cookies have one, session-only cookies don't.) */ + (Persistent cookies have one, session-only cookies don't.) + TODO: Throw out any cookies that have expiration dates in the + past?*/ continue; } From 2e33e2f18480c82384ffeda73bb04d57ef870e0f Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 21 May 2008 11:16:39 -0700 Subject: [PATCH 0230/1860] Typo in log message --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c305ebdfcc29..e28919a0b99b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -410,7 +410,7 @@ WeaveSvc.prototype = { throw "Login failed"; } - this._log.info("Using server URL: " + DAV.baseURL + DAV.defaltPrefix); + this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); this._versionCheck.async(this, self.cb); yield; From 56d4afafc6c340e8771321ff21f0fb30200261c9 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 21 May 2008 16:28:23 -0700 Subject: [PATCH 0231/1860] Allow login & sync when encryption is off --- services/sync/modules/engines.js | 12 ++++++++---- services/sync/modules/service.js | 29 +++++++++++++++-------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index fd241d4a4147..f93f552b9014 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -681,10 +681,14 @@ Engine.prototype = { this._engineId.setTempPassword(symkey); if (!this._engineId.password) throw "Could not generate a symmetric encryption key"; - - Crypto.RSAencrypt.async(Crypto, self.cb, - this._engineId.password, this._pbeId); - let enckey = yield; + + let enckey = this._engineId.password; + if ("none" != Utils.prefs.getCharPref("encryption")) { + Crypto.RSAencrypt.async(Crypto, self.cb, + this._engineId.password, this._pbeId); + enckey = yield; + } + if (!enckey) throw "Could not encrypt symmetric encryption key"; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e28919a0b99b..8865542152f9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -308,21 +308,22 @@ WeaveSvc.prototype = { _keyCheck: function WeaveSvc__keyCheck() { let self = yield; + + if ("none" != Utils.prefs.getCharPref("encryption")) { + DAV.GET("private/privkey", self.cb); + let keyResp = yield; + Utils.ensureStatus(keyResp.status, + "Could not get private key from server", [[200,300],404]); - DAV.GET("private/privkey", self.cb); - let keyResp = yield; - Utils.ensureStatus(keyResp.status, - "Could not get private key from server", [[200,300],404]); - - if (keyResp.status != 404) { - let id = ID.get('WeaveCryptoID'); - id.privkey = keyResp.responseText; - Crypto.RSAkeydecrypt.async(Crypto, self.cb, id); - id.pubkey = yield; - - } else { - this._generateKeys.async(this, self.cb); - yield; + if (keyResp.status != 404) { + let id = ID.get('WeaveCryptoID'); + id.privkey = keyResp.responseText; + Crypto.RSAkeydecrypt.async(Crypto, self.cb, id); + id.pubkey = yield; + } else { + this._generateKeys.async(this, self.cb); + yield; + } } }, From 95466ffeea92f4d0428dd4fca1c5bfbe9f7564ba Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Wed, 21 May 2008 17:53:35 -0700 Subject: [PATCH 0232/1860] Cleanup some trivial nits with cookie (whitespace and function name). --- services/sync/modules/stores.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 8ab852f3724f..817bc419cd03 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -796,7 +796,7 @@ CookieStore.prototype = { return this.__cookieManager }, - _createCommand: function HistStore__createCommand(command) { + _createCommand: function CookieStore__createCommand(command) { /* we got a command to create a cookie in the local browser in order to sync with the server. */ From ae027328a0119d529b8d798c6c024d7d65f6488a Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Wed, 21 May 2008 18:09:19 -0700 Subject: [PATCH 0233/1860] =?UTF-8?q?Bug=20433762=20=E2=80=93=20Sync=20sto?= =?UTF-8?q?red=20passwords.=20r=3Dthunder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/sync/locales/en-US/preferences.dtd | 1 + services/sync/modules/engines.js | 52 +++++++++++- services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 92 ++++++++++++++++++++- services/sync/modules/syncCores.js | 41 ++++++++- services/sync/services-sync.js | 1 + 6 files changed, 185 insertions(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 48b6fda0f4d4..041820157dbf 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -22,6 +22,7 @@ + diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f93f552b9014..45e5e75c96c8 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,7 +35,8 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'BookmarksEngine', 'HistoryEngine', 'CookieEngine']; + 'BookmarksEngine', 'HistoryEngine', 'CookieEngine', + 'PasswordEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -992,3 +993,52 @@ CookieEngine.prototype = { } }; CookieEngine.prototype.__proto__ = new Engine(); + +function PasswordEngine(pbeId) { + this._init(pbeId); +} +PasswordEngine.prototype = { + get name() { return "passwords"; }, + get logName() { return "PasswordEngine"; }, + get serverPrefix() { return "user-data/passwords/"; }, + + __core: null, + get _core() { + if (!this.__core) { + this.__core = new PasswordSyncCore(); + this.__core._hashLoginInfo = this._hashLoginInfo; + } + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) { + this.__store = new PasswordStore(); + this.__store._hashLoginInfo = this._hashLoginInfo; + } + return this.__store; + }, + + /* + * _hashLoginInfo + * + * nsILoginInfo objects don't have a unique GUID, so we need to generate one + * on the fly. This is done by taking a hash of every field in the object. + * Note that the resulting GUID could potentiually reveal passwords via + * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by + * anyone, so this should generally be safe. + */ + _hashLoginInfo : function (aLogin) { + var loginKey = aLogin.hostname + ":" + + aLogin.formSubmitURL + ":" + + aLogin.httpRealm + ":" + + aLogin.username + ":" + + aLogin.password + ":" + + aLogin.usernameField + ":" + + aLogin.passwordField; + + return Utils.sha1(loginKey); + } +}; +PasswordEngine.prototype.__proto__ = new Engine(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8865542152f9..23c5be3e1fa1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -90,6 +90,7 @@ function WeaveSvc() { Engines.register(new BookmarksEngine()); Engines.register(new HistoryEngine()); Engines.register(new CookieEngine()); + Engines.register(new PasswordEngine()); // Other misc startup Utils.prefs.addObserver("", this, false); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 817bc419cd03..5d9f2913c767 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore', 'CookieStore']; + 'HistoryStore', 'CookieStore', 'PasswordStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -943,3 +943,93 @@ CookieStore.prototype = { } }; CookieStore.prototype.__proto__ = new Store(); + +function PasswordStore() { + this._init(); +} +PasswordStore.prototype = { + _logName: "PasswordStore", + + __loginManager : null, + get _loginManager() { + if (!this.__loginManager) + this.__loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + return this.__loginManager; + }, + + __nsLoginInfo : null, + get _nsLoginInfo() { + if (!this.__nsLoginInfo) + this.__nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + return this.__nsLoginInfo; + }, + + + _createCommand: function PasswordStore__createCommand(command) { + this._log.info("PasswordStore got createCommand: " + command ); + + var login = new this._nsLoginInfo(command.data.hostname, + command.data.formSubmitURL, + command.data.httpRealm, + command.data.username, + command.data.password, + command.data.usernameField, + command.data.passwordField); + + this._loginManager.addLogin(login); + }, + + _removeCommand: function PasswordStore__removeCommand(command) { + this._log.info("PasswordStore got removeCommand: " + command ); + + var login = new this._nsLoginInfo(command.data.hostname, + command.data.formSubmitURL, + command.data.httpRealm, + command.data.username, + command.data.password, + command.data.usernameField, + command.data.passwordField); + + this._loginManager.removeLogin(login); + }, + + _editCommand: function PasswordStore__editCommand(command) { + this._log.info("PasswordStore got editCommand: " + command ); + throw "Password syncs are expected to only be create/remove!"; + }, + + wrap: function PasswordStore_wrap() { + /* Return contents of this store, as JSON. */ + var items = []; + + var logins = this._loginManager.getAllLogins({}); + + for (var i = 0; i < logins.length; i++) { + var login = logins[i]; + + var key = this._hashLoginInfo(login); + + items[key] = { hostname : login.hostname, + formSubmitURL : login.formSubmitURL, + httpRealm : login.httpRealm, + username : login.username, + password : login.password, + usernameField : login.usernameField, + passwordField : login.passwordField }; + } + + return items; + }, + + wipe: function PasswordStore_wipe() { + this._loginManager.removeAllLogins(); + }, + + resetGUIDs: function PasswordStore_resetGUIDs() { + // Not needed. + } +}; +PasswordStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index e3fce52513e1..370b5c9d835c 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,7 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', 'CookieSyncCore']; +const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', + 'CookieSyncCore', 'PasswordSyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -498,3 +499,41 @@ CookieSyncCore.prototype = { } }; CookieSyncCore.prototype.__proto__ = new SyncCore(); + + +function PasswordSyncCore() { + this._init(); +} +PasswordSyncCore.prototype = { + _logName: "PasswordSync", + + __loginManager : null, + get _loginManager() { + if (!this.__loginManager) + this.__loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + return this.__loginManager; + }, + + _itemExists: function PSC__itemExists(GUID) { + var found = false; + var logins = this._loginManager.getAllLogins({}); + + // XXX It would be more efficient to compute all the hashes in one shot, + // cache the results, and check the cache here. That would need to happen + // once per sync -- not sure how to invalidate cache after current sync? + for (var i = 0; i < logins.length && !found; i++) { + var hash = this._hashLoginInfo(logins[i]); + if (hash == GUID) + found = true;; + } + + return found; + }, + + _commandLike: function PSC_commandLike(a, b) { + // Not used. + return false; + } +}; +PasswordSyncCore.prototype.__proto__ = new SyncCore(); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 20c08d3566eb..c8362f006196 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -15,6 +15,7 @@ pref("extensions.weave.schedule", 1); pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.cookies", false ); +pref("extensions.weave.engine.passwords", false ); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 25640a1e6697d68657dcaf0503879925615f1d7b Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Thu, 22 May 2008 11:41:05 -0700 Subject: [PATCH 0234/1860] Bug 419121 - Weave chokes on microsummaries (r=thunder) --- services/sync/modules/stores.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 51feef1348ca..e12ddb41ff7c 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -369,6 +369,8 @@ BookmarksStore.prototype = { if (command.data.type == "microsummary") { this._log.debug(" \-> is a microsummary"); + this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", + command.data.staticTitle || "", 0, this._ans.EXPIRE_NEVER); let genURI = Utils.makeURI(command.data.generatorURI); try { let micsum = this._ms.createMicrosummary(URI, genURI); @@ -578,6 +580,7 @@ BookmarksStore.prototype = { item.type = "microsummary"; let micsum = this._ms.getMicrosummary(node.itemId); item.generatorURI = micsum.generator.uri.spec; // breaks local generators + item.staticTitle = this._ans.getItemAnnotation(node.itemId, "bookmarks/staticTitle"); } else if (node.type == node.RESULT_TYPE_QUERY) { item.type = "query"; item.title = node.title; From 105feca41490bab7c5fcc9b24105a6c2e9d26aa3 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 22 May 2008 11:44:51 -0700 Subject: [PATCH 0235/1860] Changeset c5a909fd128d (r336 on hg.mozilla.org) seems to have broken the OS X build, because there is no xpcom_core library on OS X, so I've modified the Makefile so that the library is only included if we're on Linux. I'm not sure what this will do for Windows builds, though, so it may break the build on Windows. --- services/crypto/Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 96d655d7a1e5..9fc1f7ec501b 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -124,10 +124,14 @@ headers = -I$(sdkdir)/include \ -I$(sdkdir)/sdk/include libdirs := $(sdkdir)/lib $(sdkdir)/bin -libs := xpcomglue_s xpcom xpcom_core nspr4 \ +libs := xpcomglue_s xpcom nspr4 \ crmf smime3 ssl3 nss3 nssutil3 softokn3 \ plds4 plc4 +ifeq ($(os), linux) + libs := xpcom_core $(libs) +endif + ifeq ($(compiler),msvc) libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) From 116cbb4de79e1037d9ec470d92c596e71d0a74e7 Mon Sep 17 00:00:00 2001 From: Date: Thu, 22 May 2008 14:15:35 -0700 Subject: [PATCH 0236/1860] cleaning up strings in sync prefs --- services/sync/locales/en-US/preferences.dtd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 041820157dbf..7a0d22c0dabb 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -21,8 +21,8 @@ - - + + From 83edfa562ed088082248b4eaf9744cd0787b1546 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 22 May 2008 15:36:44 -0700 Subject: [PATCH 0237/1860] Sync form data: bug #434818, r=thunder --- services/sync/locales/en-US/preferences.dtd | 1 + services/sync/modules/engines.js | 26 +++++++- services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 70 ++++++++++++++++++++- services/sync/modules/syncCores.js | 47 +++++++++++++- services/sync/services-sync.js | 1 + 6 files changed, 143 insertions(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 041820157dbf..0f00ad43eb3e 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -23,6 +23,7 @@ + diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 45e5e75c96c8..68f29334a0c0 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,7 @@ const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'BookmarksEngine', 'HistoryEngine', 'CookieEngine', - 'PasswordEngine']; + 'PasswordEngine', 'FormEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -1042,3 +1042,27 @@ PasswordEngine.prototype = { } }; PasswordEngine.prototype.__proto__ = new Engine(); + +function FormEngine(pbeId) { + this._init(pbeId); +} +FormEngine.prototype = { + get name() { return "forms"; }, + get logName() { return "FormEngine"; }, + get serverPrefix() { return "user-data/forms/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new FormSyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new FormStore(); + return this.__store; + } +}; +FormEngine.prototype.__proto__ = new Engine(); \ No newline at end of file diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 23c5be3e1fa1..6e41b6f0a418 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -91,6 +91,7 @@ function WeaveSvc() { Engines.register(new HistoryEngine()); Engines.register(new CookieEngine()); Engines.register(new PasswordEngine()); + Engines.register(new FormEngine()); // Other misc startup Utils.prefs.addObserver("", this, false); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 5d9f2913c767..cc8fd775fbe5 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore', 'CookieStore', 'PasswordStore']; + 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -1033,3 +1033,71 @@ PasswordStore.prototype = { } }; PasswordStore.prototype.__proto__ = new Store(); + +function FormStore() { + this._init(); +} +FormStore.prototype = { + _logName: "FormStore", + + __formDB: null, + get _formDB() { + if (!this.__formDB) { + var file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("formhistory.sqlite"); + var stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__formDB = stor.openDatabase(file); + } + return this.__formDB; + }, + + __formHistory: null, + get _formHistory() { + if (!this.__formHistory) + this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"]. + getService(Ci.nsIFormHistory2); + return this.__formHistory; + }, + + _createCommand: function FormStore__createCommand(command) { + this._log.info("FormStore got createCommand: " + command ); + this._formHistory.addEntry(command.data.name, command.data.value); + }, + + _removeCommand: function FormStore__removeCommand(command) { + this._log.info("FormStore got removeCommand: " + command ); + this._formHistory.removeEntry(command.data.name, command.data.value); + }, + + _editCommand: function FormStore__editCommand(command) { + this._log.info("FormStore got editCommand: " + command ); + this._log.warn("Form syncs are expected to only be create/remove!"); + }, + + wrap: function FormStore_wrap() { + var items = []; + var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); + + while (stmnt.executeStep()) { + var nam = stmnt.getUTF8String(1); + var val = stmnt.getUTF8String(2); + var key = Utils.sha1(nam + val); + + items[key] = { name: nam, value: val }; + } + + return items; + }, + + wipe: function FormStore_wipe() { + this._formHistory.removeAllEntries(); + }, + + resetGUIDs: function FormStore_resetGUIDs() { + // Not needed. + } +}; +FormStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 370b5c9d835c..1f9493533ad8 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', - 'CookieSyncCore', 'PasswordSyncCore']; + 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -537,3 +537,48 @@ PasswordSyncCore.prototype = { } }; PasswordSyncCore.prototype.__proto__ = new SyncCore(); + +function FormSyncCore() { + this._init(); +} +FormSyncCore.prototype = { + _logName: "FormSync", + + __formDB: null, + get _formDB() { + if (!this.__formDB) { + var file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("formhistory.sqlite"); + var stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__formDB = stor.openDatabase(file); + } + return this.__formDB; + }, + + _itemExists: function FSC__itemExists(GUID) { + var found = false; + var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); + + /* Same performance restrictions as PasswordSyncCore apply here: + caching required */ + while (stmnt.executeStep()) { + var nam = stmnt.getUTF8String(1); + var val = stmnt.getUTF8String(2); + var key = Utils.sha1(nam + val); + + if (key == GUID) + found = true; + } + + return found; + }, + + _commandLike: function FSC_commandLike(a, b) { + /* Not required as GUIDs for similar data sets will be the same */ + return false; + } +}; +FormSyncCore.prototype.__proto__ = new SyncCore(); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index c8362f006196..9890ce14e463 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -16,6 +16,7 @@ pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.cookies", false ); pref("extensions.weave.engine.passwords", false ); +pref("extensions.weave.engine.forms", false ); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 7daaa10625435708a135dd226401f5261720ad74 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 22 May 2008 15:58:29 -0700 Subject: [PATCH 0238/1860] Add support for engine 'scores'. Bug #434812, r=thunder --- services/sync/modules/engines.js | 17 +++- services/sync/modules/trackers.js | 143 ++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 services/sync/modules/trackers.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 68f29334a0c0..47c82d01e3c3 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -52,6 +52,7 @@ Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; @@ -123,7 +124,7 @@ Engine.prototype = { return this.__json; }, - // _core, and _store need to be overridden in subclasses + // _core, _store and _tracker need to be overridden in subclasses __core: null, get _core() { if (!this.__core) @@ -137,6 +138,13 @@ Engine.prototype = { this.__store = new Store(); return this.__store; }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new Tracker(); + return this.__tracker; + }, __snapshot: null, get _snapshot() { @@ -857,6 +865,13 @@ BookmarksEngine.prototype = { return this.__store; }, + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new BookmarksTracker(); + return this.__tracker; + }, + syncMounts: function BmkEngine_syncMounts(onComplete) { this._syncMounts.async(this, onComplete); }, diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js new file mode 100644 index 000000000000..118fcc716ffd --- /dev/null +++ b/services/sync/modules/trackers.js @@ -0,0 +1,143 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Anant Narayanan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; + +/* + * Trackers are associated with a single engine and deal with + * listening for changes to their particular data type + * and updating their 'score', indicating how urgently they + * want to sync. + * + * 'score's range from 0 (Nothing's changed) + * to 100 (I need to sync now!) + * -1 is also a valid score + * (don't sync me unless the user specifically requests it) + * + * Setting a score outside of this range will raise an exception. + * Well not yet, but it will :) + */ + function Tracker() { + this._init(); + } + Tracker.prototype = { + _logName: "Tracker", + _score: 0, + + _init: function T__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + }, + + get score() { + if (this._score >= 100) + return 100; + else + return this._score; + }, + + /* Should be called by service everytime a sync + * has been done for an engine + */ + resetScore: function T_resetScore() { + this._score = 0; + } + }; + + /* + * Tracker objects for each engine may need to subclass the + * getScore routine, which returns the current 'score' for that + * engine. How the engine decides to set the score is upto it, + * as long as the value between 0 and 100 actually corresponds + * to its urgency to sync. + * + * Here's an example BookmarksTracker. We don't subclass getScore + * because the observer methods take care of updating _score which + * getScore returns by default. + */ + function BookmarksTracker() { + this._init(); + } + BookmarksTracker.prototype = { + _logName: "BMTracker", + + /* We don't care about the first three */ + onBeginUpdateBatch: function BMT_onBeginUpdateBatch() { + + }, + onEndUpdateBatch: function BMT_onEndUpdateBatch() { + + }, + onItemVisited: function BMT_onItemChanged() { + + }, + /* Every add or remove is worth 4 points, + * on the basis that adding or removing 20 bookmarks + * means its time to sync? + */ + onItemAdded: function BMT_onEndUpdateBatch() { + this._score += 4; + }, + onItemRemoved: function BMT_onItemRemoved() { + this._score += 4; + }, + /* Changes are worth 2 points? */ + onItemChanged: function BMT_onItemChanged() { + this._score += 2; + }, + + _init: function BMT__init() { + super._init(); + Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService). + addObserver(this, false); + } + } + BookmarksTracker.prototype.__proto__ = new Tracker(); + From 926c5a4337b16917b9dc66b8cd55cbcc0706eaad Mon Sep 17 00:00:00 2001 From: Date: Thu, 22 May 2008 16:04:56 -0700 Subject: [PATCH 0239/1860] replacing minefield logo with weave logo --- services/sync/locales/en-US/preferences.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 7a0d22c0dabb..9799a0ac2e77 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -20,7 +20,7 @@ - + From 8e4cd5bdbf589fb0886e82d63dde213468eb05ba Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 22 May 2008 18:06:47 -0700 Subject: [PATCH 0240/1860] Correct typo in BookmarksTracker --- services/sync/modules/trackers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 118fcc716ffd..a4c070b71c46 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -114,7 +114,7 @@ Function.prototype.async = Async.sugar; onEndUpdateBatch: function BMT_onEndUpdateBatch() { }, - onItemVisited: function BMT_onItemChanged() { + onItemVisited: function BMT_onItemVisited() { }, /* Every add or remove is worth 4 points, From a3856ec1c8e64ba1f477fd99f1a4ae4ad9939a37 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 22 May 2008 18:37:24 -0700 Subject: [PATCH 0241/1860] Tracking support for HistoryEngine. Bug #435321, r=thunder --- services/sync/modules/engines.js | 7 +++++ services/sync/modules/trackers.js | 47 ++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 47c82d01e3c3..f9abdf565a46 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -981,6 +981,13 @@ HistoryEngine.prototype = { if (!this.__store) this.__store = new HistoryStore(); return this.__store; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new HistoryTracker(); + return this.__tracker; } }; HistoryEngine.prototype.__proto__ = new Engine(); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index a4c070b71c46..9c7fb1ffe966 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker']; +const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -141,3 +141,48 @@ Function.prototype.async = Async.sugar; } BookmarksTracker.prototype.__proto__ = new Tracker(); + function HistoryTracker() { + this._init(); + } + HistoryTracker.prototype = { + _logName: "HistoryTracker", + + /* We don't care about the first four */ + onBeginUpdateBatch: function HT_onBeginUpdateBatch() { + + }, + onEndUpdateBatch: function HT_onEndUpdateBatch() { + + }, + onPageChanged: function HT_onPageChanged() { + + }, + onTitleChanged: function HT_onTitleChanged() { + + }, + /* Every add or remove is worth 1 point. + * Clearing the whole history is worth 50 points, + * to ensure we're above the cutoff for syncing + * ASAP. + */ + onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { + this._score += 1; + }, + onPageExpired: function HT_onPageExpired(uri, time, entry) { + this._score += 1; + }, + onDeleteURI: function HT_onDeleteURI(uri) { + this._score += 1; + }, + onClearHistory: function HT_onClearHistory() { + this._score += 50; + }, + + _init: function HT__init() { + super._init(); + Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService). + addObserver(this, false); + } + } + HistoryTracker.prototype.__proto__ = new Tracker(); From b07832805b8f10519038c598817f8ec4e12b0bf8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 22 May 2008 18:52:52 -0700 Subject: [PATCH 0242/1860] fix build regression in windows from linux fixes --- services/crypto/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 9fc1f7ec501b..d6efcc6d26d8 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -225,11 +225,12 @@ endif $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) ifeq ($(compiler),msvc) link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) -endif +else ifeq ($(os),Linux) $(cxx) -o $@ $(cppflags) $(ldflags) $(cpp_sources) else $(cxx) -o $@ $(ldflags) $(cpp_objects) +endif endif chmod +x $@ # strip $@ From 26f399b441ed3670f17e094513554e085af052a0 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Fri, 23 May 2008 09:29:26 -0700 Subject: [PATCH 0243/1860] Add tracking support to FormsEngine. Bug #435319, r=thunder --- services/sync/modules/engines.js | 7 +++ services/sync/modules/trackers.js | 72 ++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f9abdf565a46..2fa95e4d042c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -1085,6 +1085,13 @@ FormEngine.prototype = { if (!this.__store) this.__store = new FormStore(); return this.__store; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new FormsTracker(); + return this.__tracker; } }; FormEngine.prototype.__proto__ = new Engine(); \ No newline at end of file diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 9c7fb1ffe966..cb2af47e7ef6 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -34,7 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker']; +const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker', + 'FormsTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -186,3 +187,72 @@ Function.prototype.async = Async.sugar; } } HistoryTracker.prototype.__proto__ = new Tracker(); + + function FormsTracker() { + this._init(); + } + FormsTracker.prototype = { + _logName: "FormsTracker", + + __formDB: null, + get _formDB() { + if (!this.__formDB) { + var file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("formhistory.sqlite"); + var stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__formDB = stor.openDatabase(file); + } + + return this.__formDB; + } + + /* nsIFormSubmitObserver is not available in JS. + * To calculate scores, we instead just count the changes in + * the database since the last time we were asked. + * + * FIXME!: Buggy, because changes in a row doesn't result in + * an increment of our score. A possible fix is to do a + * SELECT for each fieldname and compare those instead of the + * whole row count. + * + * Each change is worth 2 points. At some point, we may + * want to differentiate between search-history rows and other + * form items, and assign different scores. + */ + _rowCount: 0, + get score() { + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + var count = stmnt.getInt32(0); + stmnt.reset(); + + this._score = abs(this._rowCount - count) * 2; + + if (this._score >= 100) + return 100; + else + return this._score; + }, + + resetScore: function FormsTracker_resetScore() { + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); + stmnt.reset(); + + super.resetScore(); + }, + + _init: function FormsTracker__init() { + super._init(); + + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); + stmnt.reset(); + } + } + FormsTracker.prototype.__proto__ = new Tracker(); From cf0fd90958091c5fa2b76427e77d8fa5f730d7b6 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Fri, 23 May 2008 09:59:35 -0700 Subject: [PATCH 0244/1860] Fix indentation --- services/sync/modules/trackers.js | 357 +++++++++++++++--------------- 1 file changed, 181 insertions(+), 176 deletions(-) diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index cb2af47e7ef6..3a6bb436c4e4 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -64,195 +64,200 @@ Function.prototype.async = Async.sugar; * Setting a score outside of this range will raise an exception. * Well not yet, but it will :) */ - function Tracker() { - this._init(); - } - Tracker.prototype = { - _logName: "Tracker", - _score: 0, - - _init: function T__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - }, - - get score() { - if (this._score >= 100) - return 100; - else - return this._score; - }, - - /* Should be called by service everytime a sync - * has been done for an engine - */ - resetScore: function T_resetScore() { - this._score = 0; - } - }; - - /* - * Tracker objects for each engine may need to subclass the - * getScore routine, which returns the current 'score' for that - * engine. How the engine decides to set the score is upto it, - * as long as the value between 0 and 100 actually corresponds - * to its urgency to sync. - * - * Here's an example BookmarksTracker. We don't subclass getScore - * because the observer methods take care of updating _score which - * getScore returns by default. +function Tracker() { + this._init(); +} +Tracker.prototype = { + _logName: "Tracker", + _score: 0, + + _init: function T__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + }, + + /* Should be called by service periodically + * before deciding which engines to sync */ - function BookmarksTracker() { - this._init(); - } - BookmarksTracker.prototype = { - _logName: "BMTracker", - - /* We don't care about the first three */ - onBeginUpdateBatch: function BMT_onBeginUpdateBatch() { + get score() { + if (this._score >= 100) + return 100; + else + return this._score; + }, - }, - onEndUpdateBatch: function BMT_onEndUpdateBatch() { + /* Should be called by service everytime a sync + * has been done for an engine + */ + resetScore: function T_resetScore() { + this._score = 0; + } +}; + +/* + * Tracker objects for each engine may need to subclass the + * getScore routine, which returns the current 'score' for that + * engine. How the engine decides to set the score is upto it, + * as long as the value between 0 and 100 actually corresponds + * to its urgency to sync. + * + * Here's an example BookmarksTracker. We don't subclass getScore + * because the observer methods take care of updating _score which + * getScore returns by default. + */ +function BookmarksTracker() { + this._init(); +} +BookmarksTracker.prototype = { + _logName: "BMTracker", - }, - onItemVisited: function BMT_onItemVisited() { + /* We don't care about the first three */ + onBeginUpdateBatch: function BMT_onBeginUpdateBatch() { - }, - /* Every add or remove is worth 4 points, - * on the basis that adding or removing 20 bookmarks - * means its time to sync? - */ - onItemAdded: function BMT_onEndUpdateBatch() { - this._score += 4; - }, - onItemRemoved: function BMT_onItemRemoved() { - this._score += 4; - }, - /* Changes are worth 2 points? */ - onItemChanged: function BMT_onItemChanged() { - this._score += 2; - }, - - _init: function BMT__init() { - super._init(); - Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService). - addObserver(this, false); - } - } - BookmarksTracker.prototype.__proto__ = new Tracker(); + }, + onEndUpdateBatch: function BMT_onEndUpdateBatch() { + + }, + onItemVisited: function BMT_onItemVisited() { + + }, - function HistoryTracker() { - this._init(); + /* Every add or remove is worth 4 points, + * on the basis that adding or removing 20 bookmarks + * means its time to sync? + */ + onItemAdded: function BMT_onEndUpdateBatch() { + this._score += 4; + }, + onItemRemoved: function BMT_onItemRemoved() { + this._score += 4; + }, + /* Changes are worth 2 points? */ + onItemChanged: function BMT_onItemChanged() { + this._score += 2; + }, + + _init: function BMT__init() { + super._init(); + Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService). + addObserver(this, false); } - HistoryTracker.prototype = { - _logName: "HistoryTracker", - - /* We don't care about the first four */ - onBeginUpdateBatch: function HT_onBeginUpdateBatch() { +} +BookmarksTracker.prototype.__proto__ = new Tracker(); + +function HistoryTracker() { + this._init(); +} +HistoryTracker.prototype = { + _logName: "HistoryTracker", - }, - onEndUpdateBatch: function HT_onEndUpdateBatch() { + /* We don't care about the first four */ + onBeginUpdateBatch: function HT_onBeginUpdateBatch() { - }, - onPageChanged: function HT_onPageChanged() { + }, + onEndUpdateBatch: function HT_onEndUpdateBatch() { - }, - onTitleChanged: function HT_onTitleChanged() { + }, + onPageChanged: function HT_onPageChanged() { - }, - /* Every add or remove is worth 1 point. - * Clearing the whole history is worth 50 points, - * to ensure we're above the cutoff for syncing - * ASAP. - */ - onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { - this._score += 1; - }, - onPageExpired: function HT_onPageExpired(uri, time, entry) { - this._score += 1; - }, - onDeleteURI: function HT_onDeleteURI(uri) { - this._score += 1; - }, - onClearHistory: function HT_onClearHistory() { - this._score += 50; - }, - - _init: function HT__init() { - super._init(); - Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService). - addObserver(this, false); - } + }, + onTitleChanged: function HT_onTitleChanged() { + + }, + + /* Every add or remove is worth 1 point. + * Clearing the whole history is worth 50 points, + * to ensure we're above the cutoff for syncing + * ASAP. + */ + onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { + this._score += 1; + }, + onPageExpired: function HT_onPageExpired(uri, time, entry) { + this._score += 1; + }, + onDeleteURI: function HT_onDeleteURI(uri) { + this._score += 1; + }, + onClearHistory: function HT_onClearHistory() { + this._score += 50; + }, + + _init: function HT__init() { + super._init(); + Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService). + addObserver(this, false); } - HistoryTracker.prototype.__proto__ = new Tracker(); +} +HistoryTracker.prototype.__proto__ = new Tracker(); - function FormsTracker() { - this._init(); - } - FormsTracker.prototype = { - _logName: "FormsTracker", +function FormsTracker() { + this._init(); +} +FormsTracker.prototype = { + _logName: "FormsTracker", - __formDB: null, - get _formDB() { - if (!this.__formDB) { - var file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("formhistory.sqlite"); - var stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__formDB = stor.openDatabase(file); - } - - return this.__formDB; + __formDB: null, + get _formDB() { + if (!this.__formDB) { + var file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("formhistory.sqlite"); + var stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__formDB = stor.openDatabase(file); } - /* nsIFormSubmitObserver is not available in JS. - * To calculate scores, we instead just count the changes in - * the database since the last time we were asked. - * - * FIXME!: Buggy, because changes in a row doesn't result in - * an increment of our score. A possible fix is to do a - * SELECT for each fieldname and compare those instead of the - * whole row count. - * - * Each change is worth 2 points. At some point, we may - * want to differentiate between search-history rows and other - * form items, and assign different scores. - */ - _rowCount: 0, - get score() { - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); - stmnt.executeStep(); - var count = stmnt.getInt32(0); - stmnt.reset(); - - this._score = abs(this._rowCount - count) * 2; - - if (this._score >= 100) - return 100; - else - return this._score; - }, - - resetScore: function FormsTracker_resetScore() { - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); - stmnt.executeStep(); - this._rowCount = stmnt.getInt32(0); - stmnt.reset(); - - super.resetScore(); - }, - - _init: function FormsTracker__init() { - super._init(); - - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); - stmnt.executeStep(); - this._rowCount = stmnt.getInt32(0); - stmnt.reset(); - } + return this.__formDB; } - FormsTracker.prototype.__proto__ = new Tracker(); + + /* nsIFormSubmitObserver is not available in JS. + * To calculate scores, we instead just count the changes in + * the database since the last time we were asked. + * + * FIXME!: Buggy, because changes in a row doesn't result in + * an increment of our score. A possible fix is to do a + * SELECT for each fieldname and compare those instead of the + * whole row count. + * + * Each change is worth 2 points. At some point, we may + * want to differentiate between search-history rows and other + * form items, and assign different scores. + */ + _rowCount: 0, + get score() { + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + var count = stmnt.getInt32(0); + stmnt.reset(); + + this._score = abs(this._rowCount - count) * 2; + + if (this._score >= 100) + return 100; + else + return this._score; + }, + + resetScore: function FormsTracker_resetScore() { + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); + stmnt.reset(); + + super.resetScore(); + }, + + _init: function FormsTracker__init() { + super._init(); + + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); + stmnt.reset(); + } +} +FormsTracker.prototype.__proto__ = new Tracker(); From 9bafd6f172d0fcf60074d4aa87a208865d75d63a Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Fri, 23 May 2008 10:28:43 -0700 Subject: [PATCH 0245/1860] Correct syntax error in trackers.js --- services/sync/modules/trackers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 3a6bb436c4e4..cfcefe825658 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -212,7 +212,7 @@ FormsTracker.prototype = { } return this.__formDB; - } + }, /* nsIFormSubmitObserver is not available in JS. * To calculate scores, we instead just count the changes in @@ -240,7 +240,7 @@ FormsTracker.prototype = { return 100; else return this._score; - }, + }, resetScore: function FormsTracker_resetScore() { var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); From 7ed6ddb2b23cd2dc7e83430bed86e202773dad80 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Fri, 23 May 2008 11:01:32 -0700 Subject: [PATCH 0246/1860] Correct more syntax errors (super) in trackers.js --- services/sync/modules/trackers.js | 60 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index cfcefe825658..90f27fa42d93 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -65,33 +65,33 @@ Function.prototype.async = Async.sugar; * Well not yet, but it will :) */ function Tracker() { - this._init(); + this._init(); } Tracker.prototype = { - _logName: "Tracker", - _score: 0, + _logName: "Tracker", + _score: 0, - _init: function T__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - }, + _init: function T__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + }, - /* Should be called by service periodically - * before deciding which engines to sync - */ - get score() { - if (this._score >= 100) - return 100; - else - return this._score; - }, + /* Should be called by service periodically + * before deciding which engines to sync + */ + get score() { + if (this._score >= 100) + return 100; + else + return this._score; + }, - /* Should be called by service everytime a sync - * has been done for an engine - */ - resetScore: function T_resetScore() { - this._score = 0; - } + /* Should be called by service everytime a sync + * has been done for an engine + */ + resetScore: function T_resetScore() { + this._score = 0; + } }; /* @@ -138,7 +138,9 @@ BookmarksTracker.prototype = { }, _init: function BMT__init() { - super._init(); + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService). addObserver(this, false); @@ -185,7 +187,9 @@ HistoryTracker.prototype = { }, _init: function HT__init() { - super._init(); + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + Cc["@mozilla.org/browser/nav-history-service;1"]. getService(Ci.nsINavHistoryService). addObserver(this, false); @@ -234,7 +238,7 @@ FormsTracker.prototype = { var count = stmnt.getInt32(0); stmnt.reset(); - this._score = abs(this._rowCount - count) * 2; + this._score = Math.abs(this._rowCount - count) * 2; if (this._score >= 100) return 100; @@ -247,12 +251,12 @@ FormsTracker.prototype = { stmnt.executeStep(); this._rowCount = stmnt.getInt32(0); stmnt.reset(); - - super.resetScore(); + this._score = 0; }, _init: function FormsTracker__init() { - super._init(); + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); stmnt.executeStep(); From 1f19a03129cbf3046f5f87c817887fa29e895555 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 23 May 2008 11:05:42 -0700 Subject: [PATCH 0247/1860] Fixed a variety of style issues and minor warnings raised by js2-mode. --- services/sync/modules/engines.js | 20 ++++++++++---------- services/sync/modules/syncCores.js | 19 ++++++++++--------- services/sync/modules/util.js | 4 ++-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 2fa95e4d042c..70442bdc29c4 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -66,7 +66,7 @@ function EngineManagerSvc() { } EngineManagerSvc.prototype = { get: function EngMgr_get(name) { - return this._engines[name] + return this._engines[name]; }, getAll: function EngMgr_getAll() { let ret = []; @@ -138,7 +138,7 @@ Engine.prototype = { this.__store = new Store(); return this.__store; }, - + __tracker: null, get _tracker() { if (!this.__tracker) @@ -240,7 +240,7 @@ Engine.prototype = { throw e; } - self.done(done) + self.done(done); }, _resetClient: function Engine__resetClient() { @@ -690,14 +690,14 @@ Engine.prototype = { this._engineId.setTempPassword(symkey); if (!this._engineId.password) throw "Could not generate a symmetric encryption key"; - + let enckey = this._engineId.password; if ("none" != Utils.prefs.getCharPref("encryption")) { Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, this._pbeId); enckey = yield; } - + if (!enckey) throw "Could not encrypt symmetric encryption key"; @@ -738,7 +738,7 @@ Engine.prototype = { this._log.info("Full upload to server successful"); ret = true; - self.done(ret) + self.done(ret); }, _share: function Engine__share(username) { @@ -810,7 +810,7 @@ Engine.prototype = { ans.EXPIRE_NEVER); } - let item + let item; a = ans.getItemsWithAnnotation("weave/mounted-share-id", {}); for (let i = 0; i < a.length; i++) { if (ans.getItemAnnotation(a[i], "weave/mounted-share-id") == id) { @@ -871,7 +871,7 @@ BookmarksEngine.prototype = { this.__tracker = new BookmarksTracker(); return this.__tracker; }, - + syncMounts: function BmkEngine_syncMounts(onComplete) { this._syncMounts.async(this, onComplete); }, @@ -982,7 +982,7 @@ HistoryEngine.prototype = { this.__store = new HistoryStore(); return this.__store; }, - + __tracker: null, get _tracker() { if (!this.__tracker) @@ -1086,7 +1086,7 @@ FormEngine.prototype = { this.__store = new FormStore(); return this.__store; }, - + __tracker: null, get _tracker() { if (!this.__tracker) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 1f9493533ad8..1ce7c3d12ca9 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -68,7 +68,8 @@ SyncCore.prototype = { this._log = Log4Moz.Service.getLogger("Service." + this._logName); }, - // FIXME: this won't work for deep objects, or objects with optional properties + // FIXME: this won't work for deep objects, or objects with optional + // properties _getEdits: function SC__getEdits(a, b) { let ret = {numProps: 0, props: {}}; for (prop in a) { @@ -119,7 +120,7 @@ SyncCore.prototype = { } } - for (let GUID in b) { + for (GUID in b) { timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); yield; // Yield to main loop @@ -166,7 +167,7 @@ SyncCore.prototype = { if (key != "GUID" && !Utils.deepEquals(a[key], b[key])) return false; } - for (let key in b) { + for (key in b) { if (key != "GUID" && !Utils.deepEquals(a[key], b[key])) return false; } @@ -445,7 +446,7 @@ CookieSyncCore.prototype = { getService(Ci.nsICookieManager2); /* need the 2nd revision of the ICookieManager interface because it supports add() and the 1st one doesn't. */ - return this.__cookieManager + return this.__cookieManager; }, @@ -455,18 +456,18 @@ CookieSyncCore.prototype = { that we define in the JSON returned by CookieStore.wrap() That is, it will be a string of the form "host:path:name". */ - + /* TODO verify that colons can't normally appear in any of the fields -- if they did it then we can't rely on .split(":") to parse correctly.*/ - + let cookieArray = GUID.split( ":" ); let cookieHost = cookieArray[0]; let cookiePath = cookieArray[1]; let cookieName = cookieArray[2]; /* alternate implementation would be to instantiate a cookie from - cookieHost, cookiePath, and cookieName, then call + cookieHost, cookiePath, and cookieName, then call cookieManager.cookieExists(). Maybe that would have better performance? This implementation seems pretty slow.*/ let enumerator = this._cookieManager.enumerator; @@ -483,7 +484,7 @@ CookieSyncCore.prototype = { /* Note: We can't just call cookieManager.cookieExists() with a generic javascript object with .host, .path, and .name attributes attatched. cookieExists is implemented in C and does a hard static_cast to an - nsCookie object, so duck typing doesn't work (and in fact makes + nsCookie object, so duck typing doesn't work (and in fact makes Firefox hard-crash as the static_cast returns null and is not checked.) */ }, @@ -568,7 +569,7 @@ FormSyncCore.prototype = { var nam = stmnt.getUTF8String(1); var val = stmnt.getUTF8String(2); var key = Utils.sha1(nam + val); - + if (key == GUID) found = true; } diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index f87aeb96af21..873382f63918 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -199,7 +199,7 @@ let Utils = { xpath: function Weave_xpath(xmlDoc, xpathString) { let root = xmlDoc.ownerDocument == null ? - xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement + xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement; let nsResolver = xmlDoc.createNSResolver(root); return xmlDoc.evaluate(xpathString, xmlDoc, nsResolver, @@ -311,7 +311,7 @@ let Utils = { }, bind2: function Async_bind2(object, method) { - return function innerBind() { return method.apply(object, arguments); } + return function innerBind() { return method.apply(object, arguments); }; }, _prefs: null, From c477fbd20079cad50215d81a2e2d5821b9ca291c Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 23 May 2008 12:08:03 -0700 Subject: [PATCH 0248/1860] Fixed bug 435103 (https://bugzilla.mozilla.org/show_bug.cgi?id=435103) --- services/sync/modules/service.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6e41b6f0a418..12ac659267b0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -74,6 +74,7 @@ Utils.lazy(Weave, 'Service', WeaveSvc); */ function WeaveSvc() { + this._startupFinished = false; this._initLogs(); this._log.info("Weave Sync Service Initializing"); @@ -167,6 +168,15 @@ WeaveSvc.prototype = { return Utils.prefs.getIntPref("schedule"); }, + onWindowOpened: function Weave__onWindowOpened() { + if (!this._startupFinished && + Utils.prefs.getBoolPref("autoconnect") && + this.username && this.username != 'nobody@mozilla.com') { + this._startupFinished = true; + this.login(); + } + }, + _setSchedule: function Weave__setSchedule(schedule) { switch (this.schedule) { case 0: @@ -310,7 +320,7 @@ WeaveSvc.prototype = { _keyCheck: function WeaveSvc__keyCheck() { let self = yield; - + if ("none" != Utils.prefs.getCharPref("encryption")) { DAV.GET("private/privkey", self.cb); let keyResp = yield; From 37e3b844e95e581e1bd40fd9502baaf0a876513e Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 23 May 2008 12:22:08 -0700 Subject: [PATCH 0249/1860] Fixed bug 427113: https://bugzilla.mozilla.org/show_bug.cgi?id=427113 I should note that I think that there's a more idiomatic way of doing what I did, but I don't yet fully understand how Weave does things asynchronously. As such, this commit can probably be refactored to be more in the style of the surrounding code. --- services/sync/modules/service.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 12ac659267b0..056788b4d919 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -169,11 +169,14 @@ WeaveSvc.prototype = { }, onWindowOpened: function Weave__onWindowOpened() { - if (!this._startupFinished && - Utils.prefs.getBoolPref("autoconnect") && - this.username && this.username != 'nobody@mozilla.com') { + if (!this._startupFinished) { + if (Utils.prefs.getBoolPref("autoconnect") && + this.username && this.username != 'nobody@mozilla.com') { + // Login, then sync. + let self = this; + this.login(function() { self.sync(); }); + } this._startupFinished = true; - this.login(); } }, From e2611bf0adfdffb0afac963da40125e53d08ba3d Mon Sep 17 00:00:00 2001 From: Date: Fri, 23 May 2008 17:49:58 -0700 Subject: [PATCH 0250/1860] Added unit test file for CookieStore. It has a test to make sure that only persistent cookies are wrapped, not session cookies. --- services/sync/modules/stores.js | 34 +++--- services/sync/tests/unit/test_cookie_store.js | 113 ++++++++++++++++++ 2 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 services/sync/tests/unit/test_cookie_store.js diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index cc8fd775fbe5..e1c4bb5b2504 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -762,8 +762,13 @@ HistoryStore.prototype = { HistoryStore.prototype.__proto__ = new Store(); -function CookieStore() { +function CookieStore( cookieManagerStub ) { + /* If no argument is passed in, this store will query/write to the real + Mozilla cookie manager component. This is the normal way to use this + class in production code. But for unit-testing purposes, you can pass + in a stub object that will be used in place of the cookieManager. */ this._init(); + this._cookieManagerStub = cookieManagerStub; } CookieStore.prototype = { _logName: "CookieStore", @@ -785,9 +790,12 @@ CookieStore.prototype = { //expiry PRInt64 the actual expiry time of the cookie (where 0 does not represent a session cookie). Read only. //isHttpOnly boolean True if the cookie is an http only cookie. Read only. - __cookieManager: null, get _cookieManager() { + if ( this._cookieManagerStub != undefined ) { + return this._cookieManagerStub; + } + // otherwise, use the real one if (!this.__cookieManager) this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. getService(Ci.nsICookieManager2); @@ -802,9 +810,8 @@ CookieStore.prototype = { this._log.info("CookieStore got createCommand: " + command ); // this assumes command.data fits the nsICookie2 interface - if ( command.data.expiry ) { - // Add only persistent cookies (those with an expiry date). - // TODO: throw out cookies with expiration date in the past? + if ( !command.data.isSession ) { + // Add only persistent cookies ( not session cookies ) this._cookieManager.add( command.data.host, command.data.path, command.data.name, @@ -844,7 +851,7 @@ CookieStore.prototype = { var matchingCookie = null; while (iter.hasMoreElements()){ let cookie = iter.getNext(); - if (cookie instanceof Ci.nsICookie){ + if (cookie.QueryInterface( Ci.nsICookie ) ){ // see if host:path:name of cookie matches GUID given in command let key = cookie.host + ":" + cookie.path + ":" + cookie.name; if (key == command.GUID) { @@ -865,10 +872,8 @@ CookieStore.prototype = { false ); // Re-add the new updated cookie: - if ( command.data.expiry ) { - /* ignore single-session cookies, add only persistent - cookies. - TODO: throw out cookies with expiration dates in the past?*/ + if ( !command.data.isSession ) { + /* ignore single-session cookies, add only persistent cookies. */ this._cookieManager.add( matchingCookie.host, matchingCookie.path, matchingCookie.name, @@ -892,14 +897,11 @@ CookieStore.prototype = { var iter = this._cookieManager.enumerator; while (iter.hasMoreElements()){ var cookie = iter.getNext(); - if (cookie instanceof Ci.nsICookie){ + if (cookie.QueryInterface( Ci.nsICookie )){ // String used to identify cookies is // host:path:name - if ( !cookie.expiry ) { - /* Skip cookies that do not have an expiration date. - (Persistent cookies have one, session-only cookies don't.) - TODO: Throw out any cookies that have expiration dates in the - past?*/ + if ( cookie.isSession ) { + /* Skip session-only cookies, sync only persistent cookies. */ continue; } diff --git a/services/sync/tests/unit/test_cookie_store.js b/services/sync/tests/unit/test_cookie_store.js new file mode 100644 index 000000000000..6329674186a7 --- /dev/null +++ b/services/sync/tests/unit/test_cookie_store.js @@ -0,0 +1,113 @@ +function FakeCookie( host, path, name, value, + isSecure, isHttpOnly, isSession, expiry ) { + this._init( host, path, name, value, + isSecure, isHttpOnly, isSession, expiry ); +} +FakeCookie.prototype = { + _init: function( host, path, name, value, + isSecure, isHttpOnly, isSession, expiry) { + this.host = host; + this.path = path; + this.name = name; + this.value = value; + this.isSecure = isSecure; + this.isHttpOnly = isHttpOnly; + this.isSession = isSession; + }, + + QueryInterface: function( aIID ) { + if ( !aIID.equals( Components.interfaces.nsICookie ) ) { + throw Components.results.NS_ERROR_NO_INTERFACE; + } + return this; + } +}; + +function StubEnumerator( list ) { + this._init( list ); +} +StubEnumerator.prototype = { + _init: function( list ) { + this._list = list; + this._pointer = 0; + }, + hasMoreElements: function() { + return ( this._list.length > this._pointer ); + }, + getNext: function() { + var theThing = this._list[ this._pointer ]; + this._pointer++; + return theThing; + } +}; + +function FakeCookieManager() { + this._init(); +} +FakeCookieManager.prototype = { + _init: function() { + this._cookieList = []; + }, + + add: function( host, path, name, value, + isSecure, isHttpOnly, isSession, expiry) { + var newCookie = new FakeCookie( host, + path, + name, + value, + isSecure, + isHttpOnly, + isSession, + expiry ); + this._cookieList.push( newCookie ); + }, + remove: function( host, name, path, alwaysBlock ) { + for (var x in this._cookieList ) { + var cookie = this._cookieList[x]; + if ( cookie.host == host && + cookie.name == name && + cookie.path == path ) { + this._cookieList.splice( x, 1 ); + break; + } + } + }, + + get enumerator() { + var stubEnum = new StubEnumerator( this._cookieList ); + return stubEnum; + }, + + removeAll: function() { + this._cookieList = []; + } +}; + + +function run_test() { + /* Set a persistent cookie and a non-persistent cookie + then call cookieStore.wrap() and make sure it returns the persistent + one and not the non-persistent one */ + + Components.utils.import("resource://weave/stores.js"); + + // My stub object to replace the real cookieManager: + var fakeCookieManager = new FakeCookieManager(); + + // add a persistent cookie: + var d = new Date(); + d.setDate( d.getDate() + 1 ); + fakeCookieManager.add( "evilbrainjono.net", "/", "login", "jono", + false, true, false, d.getTime() ); + // and a session cookie: + fakeCookieManager.add( "humanized.com", "/", "langauge", "en", + false, true, true, 0 ); + var myStore = new CookieStore( fakeCookieManager ); + var json = myStore.wrap(); + // The json should include only the persistent cookie, not the session + // cookie: + + var jsonGuids = [ guid for ( guid in json ) ]; + do_check_eq( jsonGuids.length, 1 ); + do_check_eq( jsonGuids[0], "evilbrainjono.net:/:login" ); +} From 1ebc81b4948982d05c4bccda64a4368ddee024e5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 23 May 2008 17:50:24 -0700 Subject: [PATCH 0251/1860] don't require same index for folder 'likeness' --- services/sync/modules/syncCores.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 1ce7c3d12ca9..a7ea0b5f2db3 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -368,8 +368,8 @@ BookmarksSyncCore.prototype = { a.GUID == b.GUID) return false; - // Bookmarks are allowed to be in a different index as long as - // they are in the same folder. Folders and separators must be at + // Bookmarks and folders are allowed to be in a different index as long as + // they are in the same folder. Separators must be at // the same index to qualify for 'likeness'. switch (a.data.type) { case "bookmark": @@ -388,8 +388,7 @@ BookmarksSyncCore.prototype = { return true; return false; case "folder": - if (this._comp(a, b, 'index') && - this._comp(a, b, 'title')) + if (this._comp(a, b, 'title')) return true; return false; case "livemark": From 06fc76bee2125548142de8cf7a04ee68ba14b741 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 23 May 2008 17:50:54 -0700 Subject: [PATCH 0252/1860] whitespace police --- services/sync/modules/stores.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index cc8fd775fbe5..df816f30fc4b 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -867,7 +867,7 @@ CookieStore.prototype = { // Re-add the new updated cookie: if ( command.data.expiry ) { /* ignore single-session cookies, add only persistent - cookies. + cookies. TODO: throw out cookies with expiration dates in the past?*/ this._cookieManager.add( matchingCookie.host, matchingCookie.path, @@ -897,12 +897,12 @@ CookieStore.prototype = { // host:path:name if ( !cookie.expiry ) { /* Skip cookies that do not have an expiration date. - (Persistent cookies have one, session-only cookies don't.) + (Persistent cookies have one, session-only cookies don't.) TODO: Throw out any cookies that have expiration dates in the past?*/ continue; } - + let key = cookie.host + ":" + cookie.path + ":" + cookie.name; items[ key ] = { parentGUID: '', name: cookie.name, From 7e8a69008c14619f4916ed964f48460a0a12d957 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 23 May 2008 19:47:25 -0700 Subject: [PATCH 0253/1860] regenerate the engine id when the engine's pbe id changes --- services/sync/modules/engines.js | 36 ++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 70442bdc29c4..b6522d55776b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -165,9 +165,23 @@ Engine.prototype = { return id; }, + get _engineId() { + if ((this._pbeId.realm != this._last_pbeid_realm) || + (this._pbeId.username != this._last_pbeid_username) || + !this.__engineId) { + let password = null; + if (this.__engineId) + password = this.__engineId.password; + this._last_pbeid_realm = this._pbeId.realm; + this._last_pbeid_username = this._pbeId.username; + this.__engineId = new Identity(this._pbeId.realm + " - " + this.logName, + this._pbeId.username); + this.__engineId.password = password; + } + return this.__engineId; + }, + _init: function Engine__init() { - this._engineId = new Identity(this._pbeId.realm + " - " + this.logName, - this._pbeId.username); this._log = Log4Moz.Service.getLogger("Service." + this.logName); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.engine")]; @@ -446,7 +460,7 @@ Engine.prototype = { } else { Crypto.PBEencrypt.async(Crypto, self.cb, this._serializeCommands(server.deltas), - this._engineId); + this._engineId); let data = yield; DAV.PUT(this.deltasFile, data, self.cb); let deltasPut = yield; @@ -564,8 +578,8 @@ Engine.prototype = { Utils.ensureStatus(resp.status, "Could not download snapshot."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._engineId, - status.snapEncryption); + this._engineId, + status.snapEncryption); let data = yield; snap.data = this._json.decode(data); @@ -575,8 +589,8 @@ Engine.prototype = { Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._engineId, - status.deltasEncryption); + this._engineId, + status.deltasEncryption); data = yield; allDeltas = this._json.decode(data); deltas = this._json.decode(data); @@ -592,8 +606,8 @@ Engine.prototype = { Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._engineId, - status.deltasEncryption); + this._engineId, + status.deltasEncryption); let data = yield; allDeltas = this._json.decode(data); deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); @@ -609,8 +623,8 @@ Engine.prototype = { Utils.ensureStatus(resp.status, "Could not download deltas."); Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._engineId, - status.deltasEncryption); + this._engineId, + status.deltasEncryption); let data = yield; allDeltas = this._json.decode(data); deltas = []; From 7cfac504c866a536d278e3c203d83cf10d75bf2b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 23 May 2008 19:57:38 -0700 Subject: [PATCH 0254/1860] don't attempt to get a symkey if encryption is set to 'none' --- services/sync/modules/engines.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b6522d55776b..470e5c5ad304 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -192,6 +192,9 @@ Engine.prototype = { _getSymKey: function Engine__getSymKey() { let self = yield; + if ("none" == Utils.prefs.getCharPref("encryption")) + return; + DAV.GET(this.keysFile, self.cb); let keysResp = yield; Utils.ensureStatus(keysResp.status, @@ -206,7 +209,7 @@ Engine.prototype = { let symkey = yield; this._engineId.setTempPassword(symkey); - self.done(true); + self.done(); }, _serializeCommands: function Engine__serializeCommands(commands) { From 5f7e7b67acabab047aa4d24162060b81512160e2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 23 May 2008 23:58:53 -0700 Subject: [PATCH 0255/1860] continue sync of other engines even after one of them throws an exception; use constructor name instead of instanceof to check for array object in modules --- services/sync/modules/service.js | 8 ++++++-- services/sync/modules/util.js | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 056788b4d919..266f703a7855 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -510,8 +510,12 @@ WeaveSvc.prototype = { }, _syncEngine: function WeaveSvc__syncEngine(engine) { let self = yield; - engine.sync(self.cb); - yield; + try { + engine.sync(self.cb); + yield; + } catch(e) { + this._log.error(e.toString()); + } }, resetServer: function WeaveSync_resetServer(onComplete) { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 873382f63918..d28e7326b288 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -109,8 +109,7 @@ let Utils = { return thing; let ret; - if (thing instanceof Array) { - dump("making a cipy of an array!\n\n"); + if ("Array" == thing.constructor.name) { ret = []; for (let i = 0; i < thing.length; i++) ret.push(Utils.deepCopy(thing[i])); From 4a0f61913ddf84121c5b2db56daae7a2ea6c0147 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 24 May 2008 17:30:22 -0700 Subject: [PATCH 0256/1860] remove some debugging output, fix some remaining array detection code --- services/sync/modules/util.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index d28e7326b288..0454290ab6f2 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -76,12 +76,7 @@ let Utils = { if (typeof(a) != "object" || typeof(b) != "object") return false; - if (a instanceof Array) - dump("a is an array\n"); - if (b instanceof Array) - dump("b is an array\n"); - - if (a instanceof Array && b instanceof Array) { + if ("Array" == a.constructor.name && "Array" == b.constructor.name) { if (a.length != b.length) return false; @@ -92,7 +87,7 @@ let Utils = { } else { // check if only one of them is an array - if (a instanceof Array || b instanceof Array) + if ("Array" == a.constructor.name || "Array" == b.constructor.name) return false; for (let key in a) { From 3e99389f55125966d09fb2cfb001da11bc3c7854 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 24 May 2008 18:56:42 -0700 Subject: [PATCH 0257/1860] bump version constant --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 32da6cb430d2..9e447fe00506 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.29"; +const WEAVE_VERSION = "0.1.30"; const STORAGE_FORMAT_VERSION = 2; const ENGINE_STORAGE_FORMAT_VERSION = 2; From a17c3d5e411fdbbc0f919900e93f154aaa5c0dc0 Mon Sep 17 00:00:00 2001 From: Date: Tue, 27 May 2008 09:44:26 -0700 Subject: [PATCH 0258/1860] Created a cookie tracker in trackers.js, and a test for it in tests/unit/test_cookie_store.js. --- services/sync/modules/engines.js | 9 ++++- services/sync/modules/trackers.js | 37 +++++++++++++++++- services/sync/tests/unit/test_cookie_store.js | 39 +++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 470e5c5ad304..e60c95ffcf1a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -1029,6 +1029,13 @@ CookieEngine.prototype = { if (!this.__store) this.__store = new CookieStore(); return this.__store; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new CookieTracker(); + return this.__tracker; } }; CookieEngine.prototype.__proto__ = new Engine(); @@ -1111,4 +1118,4 @@ FormEngine.prototype = { return this.__tracker; } }; -FormEngine.prototype.__proto__ = new Engine(); \ No newline at end of file +FormEngine.prototype.__proto__ = new Engine(); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 90f27fa42d93..7ba1a09060d7 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker', - 'FormsTracker']; + 'FormsTracker', 'CookieTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -197,6 +197,41 @@ HistoryTracker.prototype = { } HistoryTracker.prototype.__proto__ = new Tracker(); +function CookieTracker() { + this._init(); +} +CookieTracker.prototype = { + _logName: "CookieTracker", + + _init: function CT__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + /* cookieService can't register observers, but what we CAN do is + register a general observer with the global observerService + to watch for the 'cookie-changed' message. */ + let observerService = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + observerService.addObserver( this, 'cookie-changed', false ); + }, + + // implement observe method to satisfy nsIObserver interface + observe: function ( aSubject, aTopic, aData ) { + /* This gets called when any cookie is added, changed, or removed. + aData will contain a string "added", "changed", etc. to tell us which, + but for now we can treat them all the same. aSubject is the new + cookie object itself. */ + var newCookie = aSubject.QueryInterface( Ci.nsICookie2 ); + if ( newCookie ) { + if ( !newCookie.isSession ) { + /* Any modification to a persistent cookie is worth + 10 points out of 100. Ignore session cookies. */ + this._score += 10; + } + } + } +} +CookieTracker.prototype.__proto__ = new Tracker(); + function FormsTracker() { this._init(); } diff --git a/services/sync/tests/unit/test_cookie_store.js b/services/sync/tests/unit/test_cookie_store.js index 6329674186a7..ae0dbbb43805 100644 --- a/services/sync/tests/unit/test_cookie_store.js +++ b/services/sync/tests/unit/test_cookie_store.js @@ -83,6 +83,44 @@ FakeCookieManager.prototype = { } }; +function sub_test_cookie_tracker() { + Components.utils.import("resource://weave/trackers.js"); + + var ct = new CookieTracker(); + + // gonna have to use the real cookie manager here... + var cookieManager = Cc["@mozilla.org/cookiemanager;1"]. + getService(Ci.nsICookieManager2); + var d = new Date(); + d.setDate( d.getDate() + 1 ); + cookieManager.add( "www.evilbrainjono.net", + "/blog/", + "comments", + "on", + false, + true, + false, + d.getTime() ); + cookieManager.add( "www.evilbrainjono.net", + "/comic/", + "lang", + "jp", + false, + true, + false, + d.getTime() ); + cookieManager.add( "www.evilbrainjono.net", + "/blog/", + "comments", + "off", + false, + true, + true, // session + d.getTime() ); + // score is 10 per cookie changed, but we should be ignoring the + // session cookie, so we should be at 20 now. + do_check_eq( ct.score, 20 ); +}; function run_test() { /* Set a persistent cookie and a non-persistent cookie @@ -110,4 +148,5 @@ function run_test() { var jsonGuids = [ guid for ( guid in json ) ]; do_check_eq( jsonGuids.length, 1 ); do_check_eq( jsonGuids[0], "evilbrainjono.net:/:login" ); + sub_test_cookie_tracker(); } From c92af445fdf84f5e132b691b22b4efb6572bda9d Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 28 May 2008 11:25:28 -0700 Subject: [PATCH 0259/1860] Added a trivial fault tolerance JS module with a trivial test suite. --- services/sync/modules/faultTolerance.js | 18 ++++++++++++++++++ .../sync/tests/unit/test_fault_tolerance.js | 7 +++++++ 2 files changed, 25 insertions(+) create mode 100644 services/sync/modules/faultTolerance.js create mode 100644 services/sync/tests/unit/test_fault_tolerance.js diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js new file mode 100644 index 000000000000..6c89100f76c7 --- /dev/null +++ b/services/sync/modules/faultTolerance.js @@ -0,0 +1,18 @@ +const EXPORTED_SYMBOLS = ["FaultTolerance"]; + +FaultTolerance = { + get Service() { + if (!this._Service) + this._Service = new FaultToleranceService(); + return this._Service; + } +} + +function FaultToleranceService() { +} + +FaultToleranceService.prototype = { + processMessage: function FTApp_doAppend(message) { + dump(message); + } +}; diff --git a/services/sync/tests/unit/test_fault_tolerance.js b/services/sync/tests/unit/test_fault_tolerance.js new file mode 100644 index 000000000000..a0c9d4108c81 --- /dev/null +++ b/services/sync/tests/unit/test_fault_tolerance.js @@ -0,0 +1,7 @@ +function run_test() { + Components.utils.import("resource://weave/faultTolerance.js"); + + // Just make sure the getter works and the service is a singleton. + FaultTolerance.Service._testProperty = "hi"; + do_check_eq(FaultTolerance.Service._testProperty, "hi"); +} From 1272373795c9c22e4a1f9411bf16558f9066c7e3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 28 May 2008 20:11:39 -0700 Subject: [PATCH 0260/1860] Add and start using an object to represent a remote resource (file), and a server object to hold them. Resources will attempt to retry network operations if they fail. --- services/sync/modules/dav.js | 2 +- services/sync/modules/engines.js | 458 +++++++++++++------------------ services/sync/modules/remote.js | 231 ++++++++++++++++ 3 files changed, 427 insertions(+), 264 deletions(-) create mode 100644 services/sync/modules/remote.js diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 11055ac0350f..2e5a96c0aa88 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -326,7 +326,7 @@ DAVCollection.prototype = { let ret = null; this._log.debug("Getting active lock token"); - this.PROPFIND("", + this.PROPFIND("lock", "" + "" + " " + diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e60c95ffcf1a..ea934a018376 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -47,8 +47,10 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/dav.js"); +Cu.import("resource://weave/remote.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/syncCores.js"); @@ -88,6 +90,8 @@ EngineManagerSvc.prototype = { function Engine() {} Engine.prototype = { + _notify: Wrap.notify, + // "default-engine"; get name() { throw "name property must be overridden in subclasses"; }, @@ -97,12 +101,11 @@ Engine.prototype = { // "user-data/default-engine/"; get serverPrefix() { throw "serverPrefix property must be overridden in subclasses"; }, - // These can be overridden in subclasses, but don't need to be (assuming - // serverPrefix is not shared with anything else) - get statusFile() { return this.serverPrefix + "status.json"; }, - get keysFile() { return this.serverPrefix + "keys.json"; }, - get snapshotFile() { return this.serverPrefix + "snapshot.json"; }, - get deltasFile() { return this.serverPrefix + "deltas.json"; }, + get _remote() { + if (!this.__remote) + this.__remote = new RemoteStore(this.serverPrefix); + return this.__remote; + }, get enabled() { return Utils.prefs.getBoolPref("engine." + this.name); @@ -195,11 +198,9 @@ Engine.prototype = { if ("none" == Utils.prefs.getCharPref("encryption")) return; - DAV.GET(this.keysFile, self.cb); - let keysResp = yield; - Utils.ensureStatus(keysResp.status, - "Could not get keys file.", [[200,300]]); - let keys = this._json.decode(keysResp.responseText); + this._remote.keys.get(self.cb); + yield; + let keys = this._json.decode(this._remote.keys.data); if (!keys || !keys.ring || !keys.ring[this._engineId.userHash]) throw "Keyring does not contain a key for this user"; @@ -226,65 +227,24 @@ Engine.prototype = { _resetServer: function Engine__resetServer() { let self = yield; - let done = false; - - try { - this._log.debug("Resetting server data"); - this._os.notifyObservers(null, this._osPrefix + "reset-server:start", ""); - - // try to delete all 3, check status after - DAV.DELETE(this.statusFile, self.cb); - let statusResp = yield; - DAV.DELETE(this.snapshotFile, self.cb); - let snapshotResp = yield; - DAV.DELETE(this.deltasFile, self.cb); - let deltasResp = yield; - - Utils.ensureStatus(statusResp.status, - "Could not delete status file.", [[200,300],404]); - Utils.ensureStatus(snapshotResp.status, - "Could not delete snapshot file.", [[200,300],404]); - Utils.ensureStatus(deltasResp.status, - "Could not delete deltas file.", [[200,300],404]); - - this._log.debug("Server files deleted"); - done = true; - this._os.notifyObservers(null, this._osPrefix + "reset-server:success", ""); - - } catch (e) { - this._log.error("Could not delete server files"); - this._os.notifyObservers(null, this._osPrefix + "reset-server:error", ""); - throw e; - } - - self.done(done); + this._log.debug("Resetting server data"); + this._remote.status.delete(self.cb); + yield; + this._remote.keys.delete(self.cb); + yield; + this._remote.snapshot.delete(self.cb); + yield; + this._remote.deltas.delete(self.cb); + yield; + this._log.debug("Server files deleted"); }, _resetClient: function Engine__resetClient() { let self = yield; - let done = false; - - try { - this._log.debug("Resetting client state"); - this._os.notifyObservers(null, this._osPrefix + "reset-client:start", ""); - - this._snapshot.wipe(); - this._store.wipe(); - done = true; - - } catch (e) { - throw e; - - } finally { - if (done) { - this._log.debug("Client reset completed successfully"); - this._os.notifyObservers(null, this._osPrefix + "reset-client:success", ""); - } else { - this._log.debug("Client reset failed"); - this._os.notifyObservers(null, this._osPrefix + "reset-client:error", ""); - } - self.done(done); - } + this._log.debug("Resetting client state"); + this._snapshot.wipe(); + this._store.wipe(); + this._log.debug("Client reset completed successfully"); }, // original @@ -465,33 +425,24 @@ Engine.prototype = { this._serializeCommands(server.deltas), this._engineId); let data = yield; - DAV.PUT(this.deltasFile, data, self.cb); - let deltasPut = yield; + this._remote.deltas.put(self.cb, data); + yield; let c = 0; for (GUID in this._snapshot.data) c++; - DAV.PUT(this.statusFile, - this._json.encode( - {GUID: this._snapshot.GUID, - formatVersion: ENGINE_STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshot.version, - snapEncryption: server.snapEncryption, - deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c}), self.cb); - let statusPut = yield; + this._remote.status.put(self.cb, this._json.encode( + {GUID: this._snapshot.GUID, + formatVersion: ENGINE_STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshot.version, + snapEncryption: server.snapEncryption, + deltasEncryption: Crypto.defaultAlgorithm, + itemCount: c})); - if (deltasPut.status >= 200 && deltasPut.status < 300 && - statusPut.status >= 200 && statusPut.status < 300) { - this._log.info("Successfully updated deltas and status on server"); - this._snapshot.save(); - } else { - // FIXME: revert snapshot here? - can't, we already applied - // updates locally! - need to save and retry - this._log.error("Could not update deltas on server"); - } + this._log.info("Successfully updated deltas and status on server"); + this._snapshot.save(); } } @@ -525,145 +476,14 @@ Engine.prototype = { */ _getServerData: function BmkEngine__getServerData() { let self = yield; - let ret = {status: -1, - formatVersion: null, maxVersion: null, snapVersion: null, - snapEncryption: null, deltasEncryption: null, - snapshot: null, deltas: null, updates: null}; - this._log.debug("Getting status file from server"); - DAV.GET(this.statusFile, self.cb); - let resp = yield; - let status = resp.status; - - switch (status) { - case 200: { + try { + this._log.debug("Getting status file from server"); + this._remote.status.get(self.cb); + yield; this._log.info("Got status file from server"); - let status = this._json.decode(resp.responseText); - let deltas, allDeltas; - let snap = new SnapshotStore(); - - // Bail out if the server has a newer format version than we can parse - if (status.formatVersion > ENGINE_STORAGE_FORMAT_VERSION) { - this._log.error("Server uses storage format v" + status.formatVersion + - ", this client understands up to v" + ENGINE_STORAGE_FORMAT_VERSION); - break; - } - - this._getSymKey.async(this, self.cb); - yield; - - if (status.formatVersion == 0) { - ret.snapEncryption = status.snapEncryption = "none"; - ret.deltasEncryption = status.deltasEncryption = "none"; - } - - if (status.GUID != this._snapshot.GUID) { - this._log.info("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._log.debug("Remote: " + status.GUID); - this._log.debug("Local: " + this._snapshot.GUID); - this._store.resetGUIDs(); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.GUID = status.GUID; - } - - if (this._snapshot.version < status.snapVersion) { - this._log.trace("Local snapshot version < server snapVersion"); - - if (this._snapshot.version >= 0) - this._log.info("Local snapshot is out of date"); - - this._log.info("Downloading server snapshot"); - DAV.GET(this.snapshotFile, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not download snapshot."); - Crypto.PBEdecrypt.async(Crypto, self.cb, - resp.responseText, - this._engineId, - status.snapEncryption); - let data = yield; - snap.data = this._json.decode(data); - - this._log.info("Downloading server deltas"); - DAV.GET(this.deltasFile, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not download deltas."); - Crypto.PBEdecrypt.async(Crypto, self.cb, - resp.responseText, - this._engineId, - status.deltasEncryption); - data = yield; - allDeltas = this._json.decode(data); - deltas = this._json.decode(data); - - } else if (this._snapshot.version >= status.snapVersion && - this._snapshot.version < status.maxVersion) { - this._log.trace("Server snapVersion <= local snapshot version < server maxVersion"); - snap.data = Utils.deepCopy(this._snapshot.data); - - this._log.info("Downloading server deltas"); - DAV.GET(this.deltasFile, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not download deltas."); - Crypto.PBEdecrypt.async(Crypto, self.cb, - resp.responseText, - this._engineId, - status.deltasEncryption); - let data = yield; - allDeltas = this._json.decode(data); - deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); - - } else if (this._snapshot.version == status.maxVersion) { - this._log.trace("Local snapshot version == server maxVersion"); - snap.data = Utils.deepCopy(this._snapshot.data); - - // FIXME: could optimize this case by caching deltas file - this._log.info("Downloading server deltas"); - DAV.GET(this.deltasFile, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not download deltas."); - Crypto.PBEdecrypt.async(Crypto, self.cb, - resp.responseText, - this._engineId, - status.deltasEncryption); - let data = yield; - allDeltas = this._json.decode(data); - deltas = []; - - } else { // this._snapshot.version > status.maxVersion - this._log.error("Server snapshot is older than local snapshot"); - break; - } - - try { - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands.async(snap, self.cb, deltas[i]); - yield; - } - } catch (e) { - this._log.error("Error applying remote deltas to saved snapshot"); - this._log.error("Clearing local snapshot; next sync will merge"); - this._log.debug("Exception: " + Utils.exceptionStr(e)); - this._log.trace("Stack:\n" + Utils.stackTrace(e)); - this._snapshot.wipe(); - throw e; - } - - ret.status = 0; - ret.formatVersion = status.formatVersion; - ret.maxVersion = status.maxVersion; - ret.snapVersion = status.snapVersion; - ret.snapEncryption = status.snapEncryption; - ret.deltasEncryption = status.deltasEncryption; - ret.snapshot = snap.data; - ret.deltas = allDeltas; - this._core.detectUpdates(self.cb, this._snapshot.data, snap.data); - ret.updates = yield; - } break; - - case 404: { + } catch (e if e.message.status == 404) { this._log.info("Server has no status file, Initial upload to server"); this._snapshot.data = this._store.wrap(); @@ -673,28 +493,146 @@ Engine.prototype = { this._fullUpload.async(this, self.cb); let uploadStatus = yield; if (!uploadStatus) - break; + throw "Initial upload failed"; this._log.info("Initial upload to server successful"); this._snapshot.save(); - ret.status = 0; - ret.formatVersion = ENGINE_STORAGE_FORMAT_VERSION; - ret.maxVersion = this._snapshot.version; - ret.snapVersion = this._snapshot.version; - ret.snapEncryption = Crypto.defaultAlgorithm; - ret.deltasEncryption = Crypto.defaultAlgorithm; - ret.snapshot = Utils.deepCopy(this._snapshot.data); - ret.deltas = []; - ret.updates = []; - } break; - - default: - this._log.error("Could not get status file: unknown HTTP status code " + - status); - break; + self.done({status: 0, + formatVersion: ENGINE_STORAGE_FORMAT_VERSION, + maxVersion: this._snapshot.version, + snapVersion: this._snapshot.version, + snapEncryption: Crypto.defaultAlgorithm, + deltasEncryption: Crypto.defaultAlgorithm, + snapshot: Utils.deepCopy(this._snapshot.data), + deltas: [], + updates: []}); + return; } + let ret = {status: -1, + formatVersion: null, maxVersion: null, snapVersion: null, + snapEncryption: null, deltasEncryption: null, + snapshot: null, deltas: null, updates: null}; + let status = this._json.decode(this._remote.status.data); + let deltas, allDeltas; + let snap = new SnapshotStore(); + + // Bail out if the server has a newer format version than we can parse + if (status.formatVersion > ENGINE_STORAGE_FORMAT_VERSION) { + this._log.error("Server uses storage format v" + status.formatVersion + + ", this client understands up to v" + ENGINE_STORAGE_FORMAT_VERSION); + throw "Incompatible server format for engine data"; + } + + this._getSymKey.async(this, self.cb); + yield; + + if (status.formatVersion == 0) { + ret.snapEncryption = status.snapEncryption = "none"; + ret.deltasEncryption = status.deltasEncryption = "none"; + } + + if (status.GUID != this._snapshot.GUID) { + this._log.info("Remote/local sync GUIDs do not match. " + + "Forcing initial sync."); + this._log.debug("Remote: " + status.GUID); + this._log.debug("Local: " + this._snapshot.GUID); + this._store.resetGUIDs(); + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.GUID = status.GUID; + } + + if (this._snapshot.version < status.snapVersion) { + this._log.trace("Local snapshot version < server snapVersion"); + + if (this._snapshot.version >= 0) + this._log.info("Local snapshot is out of date"); + + this._log.info("Downloading server snapshot"); + this._remote.snapshot.get(self.cb); + yield; + Crypto.PBEdecrypt.async(Crypto, self.cb, + this._remote.snapshot.data, + this._engineId, + status.snapEncryption); + let data = yield; + snap.data = this._json.decode(data); + + this._log.info("Downloading server deltas"); + this._remote.deltas.get(self.cb); + yield; + Crypto.PBEdecrypt.async(Crypto, self.cb, + this._remote.deltas.data, + this._engineId, + status.deltasEncryption); + data = yield; + allDeltas = this._json.decode(data); + deltas = this._json.decode(data); + + } else if (this._snapshot.version >= status.snapVersion && + this._snapshot.version < status.maxVersion) { + this._log.trace("Server snapVersion <= local snapshot version < server maxVersion"); + snap.data = Utils.deepCopy(this._snapshot.data); + + this._log.info("Downloading server deltas"); + this._remote.deltas.get(self.cb); + yield; + Crypto.PBEdecrypt.async(Crypto, self.cb, + this._remote.deltas.data, + this._engineId, + status.deltasEncryption); + let data = yield; + allDeltas = this._json.decode(data); + deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); + + } else if (this._snapshot.version == status.maxVersion) { + this._log.trace("Local snapshot version == server maxVersion"); + snap.data = Utils.deepCopy(this._snapshot.data); + + // FIXME: could optimize this case by caching deltas file + this._log.info("Downloading server deltas"); + this._remote.deltas.get(self.cb); + yield; + Crypto.PBEdecrypt.async(Crypto, self.cb, + this._remote.deltas.data, + this._engineId, + status.deltasEncryption); + let data = yield; + allDeltas = this._json.decode(data); + deltas = []; + + } else { // this._snapshot.version > status.maxVersion + this._log.error("Server snapshot is older than local snapshot"); + throw "Server snapshot is older than local snapshot"; + } + + try { + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands.async(snap, self.cb, deltas[i]); + yield; + } + } catch (e) { + this._log.error("Error applying remote deltas to saved snapshot"); + this._log.error("Clearing local snapshot; next sync will merge"); + this._log.debug("Exception: " + Utils.exceptionStr(e)); + this._log.trace("Stack:\n" + Utils.stackTrace(e)); + this._snapshot.wipe(); + throw e; + } + + ret.status = 0; + ret.formatVersion = status.formatVersion; + ret.maxVersion = status.maxVersion; + ret.snapVersion = status.snapVersion; + ret.snapEncryption = status.snapEncryption; + ret.deltasEncryption = status.deltasEncryption; + ret.snapshot = snap.data; + ret.deltas = allDeltas; + this._core.detectUpdates(self.cb, this._snapshot.data, snap.data); + ret.updates = yield; + self.done(ret) }, @@ -720,38 +658,32 @@ Engine.prototype = { let keys = {ring: {}}; keys.ring[this._engineId.userHash] = enckey; - DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); - let resp = yield; - Utils.ensureStatus(resp.status, "Could not upload keyring file."); - + this._remote.keys.put(self.cb, this._json.encode(keys)); + yield; Crypto.PBEencrypt.async(Crypto, self.cb, this._snapshot.serialize(), this._engineId); let data = yield; - DAV.PUT(this.snapshotFile, data, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not upload snapshot."); - - DAV.PUT(this.deltasFile, "[]", self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not upload deltas."); + this._remote.snapshot.put(self.cb, data); + yield; + this._remote.deltas.put(self.cb, "[]"); + yield; let c = 0; for (GUID in this._snapshot.data) c++; - DAV.PUT(this.statusFile, - this._json.encode( - {GUID: this._snapshot.GUID, - formatVersion: ENGINE_STORAGE_FORMAT_VERSION, - snapVersion: this._snapshot.version, - maxVersion: this._snapshot.version, - snapEncryption: Crypto.defaultAlgorithm, - deltasEncryption: "none", - itemCount: c}), self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not upload status file."); + this._remote.status.put(self.cb, + this._json.encode( + {GUID: this._snapshot.GUID, + formatVersion: ENGINE_STORAGE_FORMAT_VERSION, + snapVersion: this._snapshot.version, + maxVersion: this._snapshot.version, + snapEncryption: Crypto.defaultAlgorithm, + deltasEncryption: "none", + itemCount: c})); + yield; this._log.info("Full upload to server successful"); ret = true; @@ -852,11 +784,11 @@ Engine.prototype = { }, resetServer: function Engine_resetServer(onComplete) { - return this._resetServer.async(this, onComplete); + this._notify("reset-server", this._resetServer).async(this, onComplete); }, resetClient: function Engine_resetClient(onComplete) { - return this._resetClient.async(this, onComplete); + this._notify("reset-client", this._resetClient).async(this, onComplete); } }; diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js new file mode 100644 index 000000000000..8f51a89e3111 --- /dev/null +++ b/services/sync/modules/remote.js @@ -0,0 +1,231 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Resource', 'RemoteStore']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/dav.js"); +Cu.import("resource://weave/stores.js"); + +Function.prototype.async = Async.sugar; + + +function RequestException(resource, action, request) { + this._resource = resource; + this._action = action; + this._request = request; +} +RequestException.prototype = { + get resource() { return this._resource; }, + get action() { return this._action; }, + get request() { return this._request; }, + get status() { return this._request.status; }, + toString: function ReqEx_toString() { + return "Could not " + this._action + " resource " + this._resource.path + + " (" + this._request.status + ")"; + } +}; + +function Resource(path) { + this._identity = null; // unused + this._dav = null; // unused + this._path = path; + this._data = null; + this._downloaded = false; + this._dirty = false; +} +Resource.prototype = { + get identity() { return this._identity; }, + set identity(value) { this._identity = value; }, + + get dav() { return this._dav; }, + set dav(value) { this._dav = value; }, + + get path() { return this._path; }, + set path(value) { + this._dirty = true; + this._path = value; + }, + + get data() { return this._data; }, + set data(value) { + this._dirty = true; + this._data = value; + }, + + get downloaded() { return this._downloaded; }, + get dirty() { return this._dirty; }, + + _sync: function Res__sync() { + let self = yield; + let ret; + + // If we've set the locally stored value, upload it. If we + // haven't, and we haven't yet downloaded this resource, then get + // it. Otherwise do nothing (don't try to get it every time) + + if (this.dirty) { + this.put(self.cb, this.data); + ret = yield; + + } else if (!this.downloaded) { + this.get(self.cb); + ret = yield; + } + + self.done(ret); + }, + sync: function Res_sync(onComplete) { + this._sync.async(this, onComplete); + }, + + _request: function Res__request(action, data) { + let self = yield; + let listener, timer; + let iter = 0; + let ret; + + while (true) { + switch (action) { + case "GET": + DAV.GET(this.path, self.cb); + break; + case "PUT": + DAV.PUT(this.path, data, self.cb); + break; + case "DELETE": + DAV.DELETE(this.path, self.cb); + break; + default: + throw "Unknown request action for Resource"; + } + ret = yield; + + if (action == "DELETE" && + Utils.checkStatus(ret.status, null, [[200,300],404])) { + this._dirty = false; + this._data = null; + break; + + } else if (Utils.checkStatus(ret.status)) { + this._dirty = false; + if (action == "GET") + this._data = ret.responseText; + else if (action == "PUT") + this._data = data; + break; + + } else if (action == "GET" && ret.status == 404) { + throw new RequestException(this, action, ret); + + } else if (iter >= 10) { + // iter too big? bail + throw new RequestException(this, action, ret); + + } else { + // wait for a bit and try again + if (!timer) { + listener = new Utils.EventListener(self.cb); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + timer.initWithCallback(listener, iter * 100, timer.TYPE_ONE_SHOT); + yield; + iter++; + } + } + + self.done(ret); + }, + + get: function Res_get(onComplete) { + this._request.async(this, onComplete, "GET"); + }, + + put: function Res_put(onComplete, data) { + this._request.async(this, onComplete, "PUT", data); + }, + + delete: function Res_delete(onComplete) { + this._request.async(this, onComplete, "DELETE"); + } +}; + +function JsonResource(path) { + +} +JsonResource.prototype = { + __proto__: new Resource(), + + _get: function(onComplete) { + let self = yield; + this.__proto__.get(onComplete); + yield; + }, + get: function JRes_get(onComplete) { + foo.async(); + } +}; + +function RemoteStore(serverPrefix) { + this._prefix = serverPrefix; + this._status = new Resource(serverPrefix + "status.json"); + this._keys = new Resource(serverPrefix + "keys.json"); + this._snapshot = new Resource(serverPrefix + "snapshot.json"); + this._deltas = new Resource(serverPrefix + "deltas.json"); +} +RemoteStore.prototype = { + get status() { + return this._status; + }, + get keys() { + return this._keys; + }, + get snapshot() { + return this._snapshot; + }, + get deltas() { + return this._deltas; + } +}; From a446850cbeccdc7d7d38bed837ddfddfe9279788 Mon Sep 17 00:00:00 2001 From: Date: Thu, 29 May 2008 11:17:54 -0700 Subject: [PATCH 0261/1860] Moved the Share Bookmarks item out of the Weave popup menu into the Bookmarks menu, folder submenu. --- services/sync/locales/en-US/share.dtd | 2 +- services/sync/modules/xmpp/readme.txt | 20 ------------------- services/sync/tests/unit/test_cookie_store.js | 1 + 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/services/sync/locales/en-US/share.dtd b/services/sync/locales/en-US/share.dtd index 233ed463de16..ac1ffdbeb5f2 100644 --- a/services/sync/locales/en-US/share.dtd +++ b/services/sync/locales/en-US/share.dtd @@ -1,6 +1,6 @@ - + diff --git a/services/sync/modules/xmpp/readme.txt b/services/sync/modules/xmpp/readme.txt index 43dd3affa242..071e46a33ef5 100644 --- a/services/sync/modules/xmpp/readme.txt +++ b/services/sync/modules/xmpp/readme.txt @@ -122,24 +122,4 @@ Outstanding Issues -- bugs and things to do. (Everything seems to be working OK with useKeys turned off, but that's less secure.) -* Speaking of security, I need to try using HTTP polling transport over an HTTPS - connection and see if everything works. If it does, that will be great, because - we'll have SSL/TLS for free, and it won't matter so much that we're using - plain auth because the password will be encrypted as part of SSL. -* Need to implement the presence-notification/subscription/"buddy list" stuff - so that clients can more easily know when other clients are online. - - -To anyone reading this, I'd appreciate any help in debugging these problems. -Can you duplicate these problems? Do you have any suggestions of things to try? - - - -For a forum post: -copy the outstanding issues list - -Here's where/how I'm trying to install the jabberd server. - - -For email to the list: diff --git a/services/sync/tests/unit/test_cookie_store.js b/services/sync/tests/unit/test_cookie_store.js index ae0dbbb43805..5cc15b8e3c2e 100644 --- a/services/sync/tests/unit/test_cookie_store.js +++ b/services/sync/tests/unit/test_cookie_store.js @@ -149,4 +149,5 @@ function run_test() { do_check_eq( jsonGuids.length, 1 ); do_check_eq( jsonGuids[0], "evilbrainjono.net:/:login" ); sub_test_cookie_tracker(); + sub_test_cookie_engine(); } From 881ef309ed976b3380151e0a8917933837dd69e2 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 29 May 2008 17:56:52 -0700 Subject: [PATCH 0262/1860] Added a testing suite for log4moz, along w/ a few refactorings necessary to perform the tests. --- services/sync/modules/log4moz.js | 6 +++++- services/sync/tests/unit/test_log4moz.js | 27 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 services/sync/tests/unit/test_log4moz.js diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index e116c0188742..ddc3414bdc8c 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -84,7 +84,11 @@ let Log4Moz = { delete Log4Moz.Service; Log4Moz.Service = new Log4MozService(); return Log4Moz.Service; - } + }, + + get Appender() { return Appender; }, + get Formatter() { return Formatter; }, + get BasicFormatter() { return BasicFormatter; } }; diff --git a/services/sync/tests/unit/test_log4moz.js b/services/sync/tests/unit/test_log4moz.js new file mode 100644 index 000000000000..ee4072ef4a55 --- /dev/null +++ b/services/sync/tests/unit/test_log4moz.js @@ -0,0 +1,27 @@ +Components.utils.import("resource://weave/log4moz.js"); + +function MockAppender(formatter) { + this._formatter = formatter; + this.messages = []; +} +MockAppender.prototype = { + doAppend: function DApp_doAppend(message) { + this.messages.push(message); + } +}; +MockAppender.prototype.__proto__ = new Log4Moz.Appender(); + +function run_test() { + var log = Log4Moz.Service.rootLogger; + var appender = new MockAppender(new Log4Moz.BasicFormatter()); + + log.level = Log4Moz.Level.Debug; + appender.level = Log4Moz.Level.Info; + log.addAppender(appender); + log.info("info test"); + log.debug("this should be logged but not appended."); + + do_check_eq(appender.messages.length, 1); + do_check_true(appender.messages[0].indexOf("info test") > 0); + do_check_true(appender.messages[0].indexOf("INFO") > 0); +} From a51b80c9273160670cd316a8cec432dcc7388355 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 29 May 2008 18:15:50 -0700 Subject: [PATCH 0263/1860] Refactored logging system so that clients don't need to call factory functions to create specific instances of formatters and appenders. --- services/sync/modules/log4moz.js | 49 ++++++++------------------------ services/sync/modules/service.js | 10 +++---- 2 files changed, 17 insertions(+), 42 deletions(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index ddc3414bdc8c..26a9f341966c 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -86,9 +86,13 @@ let Log4Moz = { return Log4Moz.Service; }, - get Appender() { return Appender; }, get Formatter() { return Formatter; }, - get BasicFormatter() { return BasicFormatter; } + get BasicFormatter() { return BasicFormatter; }, + get Appender() { return Appender; }, + get DumpAppender() { return DumpAppender; }, + get ConsoleAppender() { return ConsoleAppender; }, + get FileAppender() { return FileAppender; }, + get RotatingFileAppender() { return RotatingFileAppender; } }; @@ -416,6 +420,12 @@ FileAppender.prototype.__proto__ = new Appender(); */ function RotatingFileAppender(file, formatter, maxSize, maxBackups) { + if (maxSize === undefined) + maxSize = ONE_MEGABYTE * 2; + + if (maxBackups === undefined) + maxBackups = 0; + this._name = "RotatingFileAppender"; this._file = file; // nsIFile this._formatter = formatter; @@ -475,40 +485,5 @@ Log4MozService.prototype = { getLogger: function LogSvc_getLogger(name) { return this._repository.getLogger(name); - }, - - newAppender: function LogSvc_newAppender(kind, formatter) { - switch (kind) { - case "dump": - return new DumpAppender(formatter); - case "console": - return new ConsoleAppender(formatter); - default: - dump("log4moz: unknown appender kind: " + kind); - return; - } - }, - - newFileAppender: function LogSvc_newAppender(kind, file, formatter) { - switch (kind) { - case "file": - return new FileAppender(file, formatter); - case "rotating": - // FIXME: hardcoded constants - return new RotatingFileAppender(file, formatter, ONE_MEGABYTE * 2, 0); - default: - dump("log4moz: unknown appender kind: " + kind); - return; - } - }, - - newFormatter: function LogSvc_newFormatter(kind) { - switch (kind) { - case "basic": - return new BasicFormatter(); - default: - dump("log4moz: unknown formatter kind: " + kind); - return; - } } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 266f703a7855..dc9520be1385 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -227,15 +227,15 @@ WeaveSvc.prototype = { this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.main")]; - let formatter = Log4Moz.Service.newFormatter("basic"); + let formatter = new Log4Moz.BasicFormatter(); let root = Log4Moz.Service.rootLogger; root.level = Log4Moz.Level[Utils.prefs.getCharPref("log.rootLogger")]; - let capp = Log4Moz.Service.newAppender("console", formatter); + let capp = new Log4Moz.ConsoleAppender(formatter); capp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.console")]; root.addAppender(capp); - let dapp = Log4Moz.Service.newAppender("dump", formatter); + let dapp = new Log4Moz.DumpAppender(formatter); dapp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.dump")]; root.addAppender(dapp); @@ -252,10 +252,10 @@ WeaveSvc.prototype = { if (!verbose.exists()) verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); - this._briefApp = Log4Moz.Service.newFileAppender("rotating", brief, formatter); + this._briefApp = new Log4Moz.RotatingFileAppender(brief, formatter); this._briefApp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.briefLog")]; root.addAppender(this._briefApp); - this._debugApp = Log4Moz.Service.newFileAppender("rotating", verbose, formatter); + this._debugApp = new Log4Moz.RotatingFileAppender(verbose, formatter); this._debugApp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.debugLog")]; root.addAppender(this._debugApp); }, From 1e34eec45764589ad530dbbdbf62bb71d5cf0aca Mon Sep 17 00:00:00 2001 From: Date: Fri, 30 May 2008 13:52:39 -0700 Subject: [PATCH 0264/1860] Fixed broken unit test test_cookie_store.js --- services/sync/tests/unit/test_cookie_store.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/tests/unit/test_cookie_store.js b/services/sync/tests/unit/test_cookie_store.js index 5cc15b8e3c2e..ae0dbbb43805 100644 --- a/services/sync/tests/unit/test_cookie_store.js +++ b/services/sync/tests/unit/test_cookie_store.js @@ -149,5 +149,4 @@ function run_test() { do_check_eq( jsonGuids.length, 1 ); do_check_eq( jsonGuids[0], "evilbrainjono.net:/:login" ); sub_test_cookie_tracker(); - sub_test_cookie_engine(); } From fd8c97eb6052ae1be36121adbba4e8142ac802b1 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 30 May 2008 16:32:06 -0700 Subject: [PATCH 0265/1860] Added a trivial appender to faultTolerance.js. --- services/sync/modules/faultTolerance.js | 31 +++++++++++++++++-- .../sync/tests/unit/test_fault_tolerance.js | 7 +++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index 6c89100f76c7..cc848b403919 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -1,3 +1,6 @@ +const Cu = Components.utils; +Cu.import("resource://weave/log4moz.js"); + const EXPORTED_SYMBOLS = ["FaultTolerance"]; FaultTolerance = { @@ -6,13 +9,35 @@ FaultTolerance = { this._Service = new FaultToleranceService(); return this._Service; } -} +}; function FaultToleranceService() { } FaultToleranceService.prototype = { - processMessage: function FTApp_doAppend(message) { - dump(message); + init: function FTS_init() { + var appender = new Appender(); + + Log4Moz.Service.rootLogger.addAppender(appender); } }; + +function Formatter() { +} +Formatter.prototype = { + format: function FTF_format(message) { + return message; + } +}; +Formatter.prototype.__proto__ = new Log4Moz.Formatter(); + +function Appender() { + this._name = "FaultToleranceAppender"; + this._formatter = new Formatter(); +} +Appender.prototype = { + doAppend: function FTA_append(message) { + // TODO: Implement this. + } +}; +Appender.prototype.__proto__ = new Log4Moz.Appender(); diff --git a/services/sync/tests/unit/test_fault_tolerance.js b/services/sync/tests/unit/test_fault_tolerance.js index a0c9d4108c81..db380894c90b 100644 --- a/services/sync/tests/unit/test_fault_tolerance.js +++ b/services/sync/tests/unit/test_fault_tolerance.js @@ -1,7 +1,14 @@ function run_test() { + Components.utils.import("resource://weave/log4moz.js"); Components.utils.import("resource://weave/faultTolerance.js"); // Just make sure the getter works and the service is a singleton. FaultTolerance.Service._testProperty = "hi"; do_check_eq(FaultTolerance.Service._testProperty, "hi"); + + FaultTolerance.Service.init(); + + var log = Log4Moz.Service.rootLogger; + log.level = Log4Moz.Level.All; + log.info("Testing."); } From 159c059d87abd670fa39d4725514e709af760fef Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 30 May 2008 17:02:25 -0700 Subject: [PATCH 0266/1860] Added a simple test to test_fault_tolerance.js that wasn't working before, but now is, and I don't know why... --- services/sync/tests/unit/test_fault_tolerance.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/tests/unit/test_fault_tolerance.js b/services/sync/tests/unit/test_fault_tolerance.js index db380894c90b..b7dbe57edcbf 100644 --- a/services/sync/tests/unit/test_fault_tolerance.js +++ b/services/sync/tests/unit/test_fault_tolerance.js @@ -11,4 +11,5 @@ function run_test() { var log = Log4Moz.Service.rootLogger; log.level = Log4Moz.Level.All; log.info("Testing."); + do_check_eq(Log4Moz.Service.rootLogger.appenders.length, 1); } From 6f2188f2915c850449934b7357727246a2f28edd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 30 May 2008 17:38:27 -0700 Subject: [PATCH 0267/1860] make some async generator errors clearer --- services/sync/modules/async.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 9ff028beb2a1..45b9c3d14658 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -96,14 +96,14 @@ Generator.prototype = { get _object() { return this.__object; }, set _object(value) { if (typeof value != "object") - throw "expected type 'object', got type '" + typeof(value) + "'"; + throw "Generator: expected type 'object', got type '" + typeof(value) + "'"; this.__object = value; }, get _method() { return this.__method; }, set _method(value) { if (typeof value != "function") - throw "expected type 'function', got type '" + typeof(value) + "'"; + throw "Generator: expected type 'function', got type '" + typeof(value) + "'"; this.__method = value; }, @@ -116,7 +116,7 @@ Generator.prototype = { }, set onComplete(value) { if (value && typeof value != "function") - throw "expected type 'function', got type '" + typeof(value) + "'"; + throw "Generator: expected type 'function', got type '" + typeof(value) + "'"; this._onComplete = value; }, From a7ff173f19caec47ab3ed0e0c639dbc805133814 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 30 May 2008 17:40:08 -0700 Subject: [PATCH 0268/1860] format exception correctly when an engine throws during sync --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index dc9520be1385..eb9868907d93 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -514,7 +514,7 @@ WeaveSvc.prototype = { engine.sync(self.cb); yield; } catch(e) { - this._log.error(e.toString()); + this._log.error(Utils.exceptionStr(e)); } }, From c372145a85af8d48e6335b46eed1bde08859e872 Mon Sep 17 00:00:00 2001 From: Date: Fri, 30 May 2008 18:19:47 -0700 Subject: [PATCH 0269/1860] Made the bookmark-share dialog box display the name of the folder you selected; also, the 'Share This Folder' menu text is now pulled from share.properties for easier i18n. --- services/sync/locales/en-US/share.properties | 1 + services/sync/locales/en-US/sync.properties | 1 + 2 files changed, 2 insertions(+) diff --git a/services/sync/locales/en-US/share.properties b/services/sync/locales/en-US/share.properties index 2d4f42b76f71..9f5914754368 100644 --- a/services/sync/locales/en-US/share.properties +++ b/services/sync/locales/en-US/share.properties @@ -1,3 +1,4 @@ status.ok = Weave sharing complete! status.error = Error. Please check the Weave ID and try again. status.working = Working... +folder.message = You have chosen to share the bookmark folder "%S". \ No newline at end of file diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 82e3f6850d9b..dc3e50e89899 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -4,3 +4,4 @@ status.idle = Idle status.active = Working... status.offline = Offline status.error = Error +shareBookmark.menuItem = Share This Folder... \ No newline at end of file From dac0f4d962f6485c9915e1c6e6761d53185ff086 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 30 May 2008 18:53:07 -0700 Subject: [PATCH 0270/1860] only require the .rc file on windows --- services/crypto/Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index d6efcc6d26d8..54f0475d4a50 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -217,18 +217,18 @@ else $(cxx) -o $@ $(cppflags) $(@:.o=.cpp) endif -$(target:=.res): $(target:=.rc) ifeq ($(compiler),msvc) +$(target:=.res): $(target:=.rc) rc -Fo$@ $(rcflags) $(target:=.rc) -endif $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) -ifeq ($(compiler),msvc) link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) else ifeq ($(os),Linux) +$(so_target): $(idl_headers) $(cpp_objects) $(cxx) -o $@ $(cppflags) $(ldflags) $(cpp_sources) else +$(so_target): $(idl_headers) $(cpp_objects) $(cxx) -o $@ $(ldflags) $(cpp_objects) endif endif From 96f2aa92fed6b7d3ffd26189bee3da4c3ce425ec Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 30 May 2008 20:43:55 -0700 Subject: [PATCH 0271/1860] move json and crypto into remote resource 'filters', so the engine doesn't have to explicitly encode/decode anything. note--known regression: filters will not use the encryption algorithm in the status file --- services/sync/modules/engines.js | 101 ++++++----------- services/sync/modules/remote.js | 182 ++++++++++++++++++++++++------- services/sync/modules/stores.js | 24 ++-- 3 files changed, 191 insertions(+), 116 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index ea934a018376..1bcbcc672ae6 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -103,7 +103,7 @@ Engine.prototype = { get _remote() { if (!this.__remote) - this.__remote = new RemoteStore(this.serverPrefix); + this.__remote = new RemoteStore(this.serverPrefix, this._engineId); return this.__remote; }, @@ -180,6 +180,7 @@ Engine.prototype = { this.__engineId = new Identity(this._pbeId.realm + " - " + this.logName, this._pbeId.username); this.__engineId.password = password; + this.__remote = new RemoteStore(this.serverPrefix, this._engineId); // hack } return this.__engineId; }, @@ -200,7 +201,7 @@ Engine.prototype = { this._remote.keys.get(self.cb); yield; - let keys = this._json.decode(this._remote.keys.data); + let keys = this._remote.keys.data; if (!keys || !keys.ring || !keys.ring[this._engineId.userHash]) throw "Keyring does not contain a key for this user"; @@ -209,6 +210,7 @@ Engine.prototype = { keys.ring[this._engineId.userHash], this._pbeId); let symkey = yield; this._engineId.setTempPassword(symkey); + this.__remote = new RemoteStore(this.serverPrefix, this._engineId); // hack self.done(); }, @@ -421,25 +423,21 @@ Engine.prototype = { this._log.error("Could not upload files to server"); // eep? } else { - Crypto.PBEencrypt.async(Crypto, self.cb, - this._serializeCommands(server.deltas), - this._engineId); - let data = yield; - this._remote.deltas.put(self.cb, data); + this._remote.deltas.put(self.cb, this._serializeCommands(server.deltas)); yield; let c = 0; for (GUID in this._snapshot.data) c++; - this._remote.status.put(self.cb, this._json.encode( - {GUID: this._snapshot.GUID, - formatVersion: ENGINE_STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshot.version, - snapEncryption: server.snapEncryption, - deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c})); + this._remote.status.put(self.cb, + {GUID: this._snapshot.GUID, + formatVersion: ENGINE_STORAGE_FORMAT_VERSION, + snapVersion: server.snapVersion, + maxVersion: this._snapshot.version, + snapEncryption: server.snapEncryption, + deltasEncryption: Crypto.defaultAlgorithm, + itemCount: c}); this._log.info("Successfully updated deltas and status on server"); this._snapshot.save(); @@ -476,11 +474,12 @@ Engine.prototype = { */ _getServerData: function BmkEngine__getServerData() { let self = yield; + let status; try { this._log.debug("Getting status file from server"); this._remote.status.get(self.cb); - yield; + status = yield; this._log.info("Got status file from server"); } catch (e if e.message.status == 404) { @@ -514,7 +513,6 @@ Engine.prototype = { formatVersion: null, maxVersion: null, snapVersion: null, snapEncryption: null, deltasEncryption: null, snapshot: null, deltas: null, updates: null}; - let status = this._json.decode(this._remote.status.data); let deltas, allDeltas; let snap = new SnapshotStore(); @@ -551,25 +549,13 @@ Engine.prototype = { this._log.info("Local snapshot is out of date"); this._log.info("Downloading server snapshot"); - this._remote.snapshot.get(self.cb); - yield; - Crypto.PBEdecrypt.async(Crypto, self.cb, - this._remote.snapshot.data, - this._engineId, - status.snapEncryption); - let data = yield; - snap.data = this._json.decode(data); + this._remote.snapshot.get(self.cb); // fixme: doesn't use status.snapEncryption + snap.data = yield; this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); - yield; - Crypto.PBEdecrypt.async(Crypto, self.cb, - this._remote.deltas.data, - this._engineId, - status.deltasEncryption); - data = yield; - allDeltas = this._json.decode(data); - deltas = this._json.decode(data); + this._remote.deltas.get(self.cb); // fixme: doesn't use status.deltasEncryption + allDeltas = yield; + deltas = allDeltas; } else if (this._snapshot.version >= status.snapVersion && this._snapshot.version < status.maxVersion) { @@ -577,14 +563,8 @@ Engine.prototype = { snap.data = Utils.deepCopy(this._snapshot.data); this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); - yield; - Crypto.PBEdecrypt.async(Crypto, self.cb, - this._remote.deltas.data, - this._engineId, - status.deltasEncryption); - let data = yield; - allDeltas = this._json.decode(data); + this._remote.deltas.get(self.cb); // fixme: doesn't use status.deltasEncryption + allDeltas = yield; deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); } else if (this._snapshot.version == status.maxVersion) { @@ -593,14 +573,8 @@ Engine.prototype = { // FIXME: could optimize this case by caching deltas file this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); - yield; - Crypto.PBEdecrypt.async(Crypto, self.cb, - this._remote.deltas.data, - this._engineId, - status.deltasEncryption); - let data = yield; - allDeltas = this._json.decode(data); + this._remote.deltas.get(self.cb); // fixme: doesn't use status.deltasEncryption + allDeltas = yield; deltas = []; } else { // this._snapshot.version > status.maxVersion @@ -658,16 +632,14 @@ Engine.prototype = { let keys = {ring: {}}; keys.ring[this._engineId.userHash] = enckey; - this._remote.keys.put(self.cb, this._json.encode(keys)); + this._remote.keys.put(self.cb, keys); yield; - Crypto.PBEencrypt.async(Crypto, self.cb, - this._snapshot.serialize(), - this._engineId); - let data = yield; - this._remote.snapshot.put(self.cb, data); + this.__remote = new RemoteStore(this.serverPrefix, this._engineId); // hack + + this._remote.snapshot.put(self.cb, this._snapshot.wrap()); yield; - this._remote.deltas.put(self.cb, "[]"); + this._remote.deltas.put(self.cb, []); yield; let c = 0; @@ -675,14 +647,13 @@ Engine.prototype = { c++; this._remote.status.put(self.cb, - this._json.encode( - {GUID: this._snapshot.GUID, - formatVersion: ENGINE_STORAGE_FORMAT_VERSION, - snapVersion: this._snapshot.version, - maxVersion: this._snapshot.version, - snapEncryption: Crypto.defaultAlgorithm, - deltasEncryption: "none", - itemCount: c})); + {GUID: this._snapshot.GUID, + formatVersion: ENGINE_STORAGE_FORMAT_VERSION, + snapVersion: this._snapshot.version, + maxVersion: this._snapshot.version, + snapEncryption: Crypto.defaultAlgorithm, + deltasEncryption: "none", + itemCount: c}); yield; this._log.info("Full upload to server successful"); diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 8f51a89e3111..569dbdcab592 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -45,6 +45,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/stores.js"); @@ -69,12 +70,7 @@ RequestException.prototype = { }; function Resource(path) { - this._identity = null; // unused - this._dav = null; // unused - this._path = path; - this._data = null; - this._downloaded = false; - this._dirty = false; + this._init(path); } Resource.prototype = { get identity() { return this._identity; }, @@ -95,9 +91,34 @@ Resource.prototype = { this._data = value; }, + get lastRequest() { return this._lastRequest; }, get downloaded() { return this._downloaded; }, get dirty() { return this._dirty; }, + pushFilter: function Res_pushFilter(filter) { + this._filters.push(filter); + }, + + popFilter: function Res_popFilter() { + return this._filters.pop(); + }, + + clearFilters: function Res_clearFilters() { + this._filters = []; + }, + + _init: function Res__init(path) { + this._identity = null; // unused + this._dav = null; // unused + this._path = path; + this._data = null; + this._downloaded = false; + this._dirty = false; + this._filters = []; + this._lastRequest = null; + this._log = Log4Moz.Service.getLogger("Service.Resource"); + }, + _sync: function Res__sync() { let self = yield; let ret; @@ -125,7 +146,13 @@ Resource.prototype = { let self = yield; let listener, timer; let iter = 0; - let ret; + + if ("PUT" == action) { + for each (let filter in this._filters) { + filter.beforePUT.async(filter, self.cb, data); + data = yield; + } + } while (true) { switch (action) { @@ -141,28 +168,29 @@ Resource.prototype = { default: throw "Unknown request action for Resource"; } - ret = yield; + this._lastRequest = yield; if (action == "DELETE" && - Utils.checkStatus(ret.status, null, [[200,300],404])) { + Utils.checkStatus(this._lastRequest.status, null, [[200,300],404])) { this._dirty = false; this._data = null; break; - } else if (Utils.checkStatus(ret.status)) { + } else if (Utils.checkStatus(this._lastRequest.status)) { + this._log.debug(action + " request successful"); this._dirty = false; if (action == "GET") - this._data = ret.responseText; + this._data = this._lastRequest.responseText; else if (action == "PUT") this._data = data; break; - } else if (action == "GET" && ret.status == 404) { - throw new RequestException(this, action, ret); + } else if (action == "GET" && this._lastRequest.status == 404) { + throw new RequestException(this, action, this._lastRequest); } else if (iter >= 10) { // iter too big? bail - throw new RequestException(this, action, ret); + throw new RequestException(this, action, this._lastRequest); } else { // wait for a bit and try again @@ -176,7 +204,14 @@ Resource.prototype = { } } - self.done(ret); + if ("GET" == action) { + for each (let filter in this._filters.reverse()) { + filter.afterGET.async(filter, self.cb, this._data); + this._data = yield; + } + } + + self.done(this._data); }, get: function Res_get(onComplete) { @@ -192,40 +227,109 @@ Resource.prototype = { } }; -function JsonResource(path) { - +function ResourceFilter() { + this._log = Log4Moz.Service.getLogger("Service.ResourceFilter"); } -JsonResource.prototype = { - __proto__: new Resource(), - - _get: function(onComplete) { +ResourceFilter.prototype = { + beforePUT: function ResFilter_beforePUT(data) { let self = yield; - this.__proto__.get(onComplete); - yield; + this._log.debug("Doing absolutely nothing") + self.done(data); }, - get: function JRes_get(onComplete) { - foo.async(); + afterGET: function ResFilter_afterGET(data) { + let self = yield; + this._log.debug("Doing absolutely nothing") + self.done(data); } }; -function RemoteStore(serverPrefix) { +function JsonFilter() { + this._log = Log4Moz.Service.getLogger("Service.JsonFilter"); +} +JsonFilter.prototype = { + __proto__: new ResourceFilter(), + + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return this._json; + }, + + beforePUT: function JsonFilter_beforePUT(data) { + let self = yield; + this._log.debug("Encoding data as JSON") + self.done(this._json.encode(data)); + }, + + afterGET: function JsonFilter_afterGET(data) { + let self = yield; + this._log.debug("Decoding JSON data") + self.done(this._json.decode(data)); + } +}; + +// FIXME: doesn't use the crypto algorithm stored in the status file +function PBECryptoFilter(identity) { + this._identity = identity; + this._log = Log4Moz.Service.getLogger("Service.PBECryptoFilter"); +} +PBECryptoFilter.prototype = { + __proto__: new ResourceFilter(), + + beforePUT: function PBEFilter_beforePUT(data) { + let self = yield; + this._log.debug("Encrypting data") + Crypto.PBEencrypt.async(Crypto, self.cb, data, this._identity); + let ret = yield; + self.done(ret); + }, + + afterGET: function PBEFilter_afterGET(data) { + let self = yield; + this._log.debug("Decrypting data") + Crypto.PBEdecrypt.async(Crypto, self.cb, data, this._identity); + let ret = yield; + self.done(ret); + } +}; + +function RemoteStore(serverPrefix, cryptoId) { this._prefix = serverPrefix; - this._status = new Resource(serverPrefix + "status.json"); - this._keys = new Resource(serverPrefix + "keys.json"); - this._snapshot = new Resource(serverPrefix + "snapshot.json"); - this._deltas = new Resource(serverPrefix + "deltas.json"); + this._cryptoId = cryptoId; + this._init(); } RemoteStore.prototype = { - get status() { - return this._status; + _init: function Remote__init(serverPrefix, cryptoId) { + if (!this._prefix || !this._cryptoId) + return; + let json = new JsonFilter(); + let crypto = new PBECryptoFilter(this._cryptoId); + this._status = new Resource(this._prefix + "status.json"); + this._status.pushFilter(json); + this._keys = new Resource(this._prefix + "keys.json"); + this._keys.pushFilter(new JsonFilter()); + this._snapshot = new Resource(this._prefix + "snapshot.json"); + this._snapshot.pushFilter(json); + this._snapshot.pushFilter(crypto); + this._deltas = new Resource(this._prefix + "deltas.json"); + this._deltas.pushFilter(json); + this._deltas.pushFilter(crypto); }, - get keys() { - return this._keys; + + get status() this._status, + get keys() this._keys, + get snapshot() this._snapshot, + get deltas() this._deltas, + + get serverPrefix() this._prefix, + set serverPrefix(value) { + this._prefix = value; + this._init(); }, - get snapshot() { - return this._snapshot; - }, - get deltas() { - return this._deltas; + + get cryptoId() this._cryptoId, + set cryptoId(value) { + this._cryptoId = value; + this._init(); } }; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 900f6365fb4b..17988920391a 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -89,7 +89,7 @@ Store.prototype = { yield; // Yield to main loop } var command = commandList[i]; - this._log.debug("Processing command: " + this._json.encode(command)); + this._log.trace("Processing command: " + this._json.encode(command)); switch (command["action"]) { case "create": this._createCommand(command); @@ -900,7 +900,7 @@ CookieStore.prototype = { if (cookie.QueryInterface( Ci.nsICookie )){ // String used to identify cookies is // host:path:name - if ( cookie.isSession ) { + if ( cookie.isSession ) { /* Skip session-only cookies, sync only persistent cookies. */ continue; } @@ -1041,7 +1041,7 @@ function FormStore() { } FormStore.prototype = { _logName: "FormStore", - + __formDB: null, get _formDB() { if (!this.__formDB) { @@ -1055,45 +1055,45 @@ FormStore.prototype = { } return this.__formDB; }, - + __formHistory: null, get _formHistory() { - if (!this.__formHistory) + if (!this.__formHistory) this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"]. getService(Ci.nsIFormHistory2); return this.__formHistory; }, - + _createCommand: function FormStore__createCommand(command) { this._log.info("FormStore got createCommand: " + command ); this._formHistory.addEntry(command.data.name, command.data.value); }, - + _removeCommand: function FormStore__removeCommand(command) { this._log.info("FormStore got removeCommand: " + command ); this._formHistory.removeEntry(command.data.name, command.data.value); }, - + _editCommand: function FormStore__editCommand(command) { this._log.info("FormStore got editCommand: " + command ); this._log.warn("Form syncs are expected to only be create/remove!"); }, - + wrap: function FormStore_wrap() { var items = []; var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); - + while (stmnt.executeStep()) { var nam = stmnt.getUTF8String(1); var val = stmnt.getUTF8String(2); var key = Utils.sha1(nam + val); - + items[key] = { name: nam, value: val }; } return items; }, - + wipe: function FormStore_wipe() { this._formHistory.removeAllEntries(); }, From 4ae8031686e884dbd9e6d7b423387157bfcf5aad Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 2 Jun 2008 11:10:11 +0900 Subject: [PATCH 0272/1860] change remote store to keep track of identity names/aliases and fetch the objects from the id manager --- services/sync/modules/engines.js | 26 ++++++++++---------------- services/sync/modules/identity.js | 1 + services/sync/modules/remote.js | 8 +++++--- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 1bcbcc672ae6..776842a2879d 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -103,7 +103,7 @@ Engine.prototype = { get _remote() { if (!this.__remote) - this.__remote = new RemoteStore(this.serverPrefix, this._engineId); + this.__remote = new RemoteStore(this.serverPrefix, 'Engine:' + this.name); return this.__remote; }, @@ -169,20 +169,17 @@ Engine.prototype = { }, get _engineId() { - if ((this._pbeId.realm != this._last_pbeid_realm) || - (this._pbeId.username != this._last_pbeid_username) || - !this.__engineId) { + let id = ID.get('Engine:' + this.name) + if (!id || + id.username != this._pbeId.username || id.realm != this._pbeId.realm) { let password = null; - if (this.__engineId) - password = this.__engineId.password; - this._last_pbeid_realm = this._pbeId.realm; - this._last_pbeid_username = this._pbeId.username; - this.__engineId = new Identity(this._pbeId.realm + " - " + this.logName, - this._pbeId.username); - this.__engineId.password = password; - this.__remote = new RemoteStore(this.serverPrefix, this._engineId); // hack + if (id) + password = id.password; + id = new Identity(this._pbeId.realm + ' - ' + this.logName, + this._pbeId.username, password); + ID.set('Engine:' + this.name, id); } - return this.__engineId; + return id; }, _init: function Engine__init() { @@ -210,7 +207,6 @@ Engine.prototype = { keys.ring[this._engineId.userHash], this._pbeId); let symkey = yield; this._engineId.setTempPassword(symkey); - this.__remote = new RemoteStore(this.serverPrefix, this._engineId); // hack self.done(); }, @@ -635,8 +631,6 @@ Engine.prototype = { this._remote.keys.put(self.cb, keys); yield; - this.__remote = new RemoteStore(this.serverPrefix, this._engineId); // hack - this._remote.snapshot.put(self.cb, this._snapshot.wrap()); yield; this._remote.deltas.put(self.cb, []); diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index cb953ef2446a..5eaf2ecd39f4 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -62,6 +62,7 @@ IDManager.prototype = { }, set: function IDMgr_set(name, id) { this._ids[name] = id; + return id; }, del: function IDMgr_del(name) { delete this._ids[name]; diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 569dbdcab592..8a008547a79d 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -47,6 +47,7 @@ Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/stores.js"); @@ -119,6 +120,7 @@ Resource.prototype = { this._log = Log4Moz.Service.getLogger("Service.Resource"); }, + // note: this is unused, and it's not clear whether it's useful or not _sync: function Res__sync() { let self = yield; let ret; @@ -279,7 +281,7 @@ PBECryptoFilter.prototype = { beforePUT: function PBEFilter_beforePUT(data) { let self = yield; this._log.debug("Encrypting data") - Crypto.PBEencrypt.async(Crypto, self.cb, data, this._identity); + Crypto.PBEencrypt.async(Crypto, self.cb, data, ID.get(this._identity)); let ret = yield; self.done(ret); }, @@ -287,7 +289,7 @@ PBECryptoFilter.prototype = { afterGET: function PBEFilter_afterGET(data) { let self = yield; this._log.debug("Decrypting data") - Crypto.PBEdecrypt.async(Crypto, self.cb, data, this._identity); + Crypto.PBEdecrypt.async(Crypto, self.cb, data, ID.get(this._identity)); let ret = yield; self.done(ret); } @@ -299,7 +301,7 @@ function RemoteStore(serverPrefix, cryptoId) { this._init(); } RemoteStore.prototype = { - _init: function Remote__init(serverPrefix, cryptoId) { + _init: function RemoteStore__init() { if (!this._prefix || !this._cryptoId) return; let json = new JsonFilter(); From 8c093ae8851a94b8bacebb283abba2fbe209354d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 2 Jun 2008 13:02:04 +0900 Subject: [PATCH 0273/1860] log stack traces from sync exceptions --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eb9868907d93..7ae93cb82848 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -515,6 +515,8 @@ WeaveSvc.prototype = { yield; } catch(e) { this._log.error(Utils.exceptionStr(e)); + if (e.trace) + this._log.trace(Utils.stackTrace(e.trace)); } }, From ae2d777620f1f3239ddc15d93f66783bbbf877d7 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 2 Jun 2008 15:24:52 -0700 Subject: [PATCH 0274/1860] minor typo fixes --- services/sync/modules/engines.js | 2 +- services/sync/modules/stores.js | 2 +- services/sync/modules/syncCores.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 776842a2879d..71ca41aae9df 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -273,7 +273,7 @@ Engine.prototype = { // 3.1) Apply local delta with server changes ("D") // 3.2) Append server delta to the delta file and upload ("C") - _sync: function BmkEngine__sync() { + _sync: function Engine__sync() { let self = yield; this._log.info("Beginning sync"); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 17988920391a..51feef1348ca 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -1005,7 +1005,7 @@ PasswordStore.prototype = { wrap: function PasswordStore_wrap() { /* Return contents of this store, as JSON. */ - var items = []; + var items = {}; var logins = this._loginManager.getAllLogins({}); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index a7ea0b5f2db3..39e46901651e 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -227,7 +227,7 @@ SyncCore.prototype = { let conflicts = [[], []]; let ret = {propagations: propagations, conflicts: conflicts}; this._log.debug("Reconciling " + listA.length + - " against " + listB.length + "commands"); + " against " + listB.length + " commands"); let guidChanges = []; for (let i = 0; i < listA.length; i++) { @@ -416,12 +416,12 @@ function HistorySyncCore() { HistorySyncCore.prototype = { _logName: "HistSync", - _itemExists: function BSC__itemExists(GUID) { + _itemExists: function HSC__itemExists(GUID) { // we don't care about already-existing items; just try to re-add them return false; }, - _commandLike: function BSC_commandLike(a, b) { + _commandLike: function HSC_commandLike(a, b) { // History commands never qualify for likeness. We will always // take the union of all client/server items. We use the URL as // the GUID, so the same sites will map to the same item (same From 579a84c8ad2da78e629aa6b18bcdbd21ac784475 Mon Sep 17 00:00:00 2001 From: Date: Mon, 2 Jun 2008 20:13:46 -0700 Subject: [PATCH 0275/1860] Bookmark share now leaves an annotation ('weave/share/sahred_outgoing' = true or false) on a bookmark folder to note whether it's being shared or not; when a folder is being shared, the menu item in the folder submenu changes to 'Stop sharing this folder'. --- services/sync/locales/en-US/sync.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index dc3e50e89899..90dab966f847 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -4,4 +4,5 @@ status.idle = Idle status.active = Working... status.offline = Offline status.error = Error -shareBookmark.menuItem = Share This Folder... \ No newline at end of file +shareBookmark.menuItem = Share This Folder... +unShareBookmark.menuItem = Stop Sharing This Folder \ No newline at end of file From 254ef479ae8a7c33491abb4d02b2deeca3a0dfb1 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 11:11:44 -0700 Subject: [PATCH 0276/1860] Moved all code related to the syncing of cookies--e.g. CookieStore, CookieTracker, CookieEngine, CookieSyncCore--into their own file at modules/engines/cookies.js. I'll be doing the same to the other engines shortly. This helps with code organization--all the logic for dealing with a particular data type is now in one place--and should also make it easier to write unit/regression tests. --- services/sync/modules/engines.js | 33 +- services/sync/modules/engines/cookies.js | 330 ++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 187 +--------- services/sync/modules/syncCores.js | 72 +--- services/sync/modules/trackers.js | 53 +-- services/sync/tests/unit/test_cookie_store.js | 14 +- 7 files changed, 349 insertions(+), 341 deletions(-) create mode 100644 services/sync/modules/engines/cookies.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 71ca41aae9df..c402bfa0fd1a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'BookmarksEngine', 'HistoryEngine', 'CookieEngine', + 'BookmarksEngine', 'HistoryEngine', 'PasswordEngine', 'FormEngine']; const Cc = Components.classes; @@ -906,37 +906,6 @@ HistoryEngine.prototype = { }; HistoryEngine.prototype.__proto__ = new Engine(); -function CookieEngine(pbeId) { - this._init(pbeId); -} -CookieEngine.prototype = { - get name() { return "cookies"; }, - get logName() { return "CookieEngine"; }, - get serverPrefix() { return "user-data/cookies/"; }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new CookieSyncCore(); - return this.__core; - }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new CookieStore(); - return this.__store; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new CookieTracker(); - return this.__tracker; - } -}; -CookieEngine.prototype.__proto__ = new Engine(); - function PasswordEngine(pbeId) { this._init(pbeId); } diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js new file mode 100644 index 000000000000..a2ea23294345 --- /dev/null +++ b/services/sync/modules/engines/cookies.js @@ -0,0 +1,330 @@ +const EXPORTED_SYMBOLS = ['CookieEngine', 'CookieTracker', 'CookieStore']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); + +function CookieEngine(pbeId) { + this._init(pbeId); +} +CookieEngine.prototype = { + get name() { return "cookies"; }, + get logName() { return "CookieEngine"; }, + get serverPrefix() { return "user-data/cookies/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new CookieSyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new CookieStore(); + return this.__store; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new CookieTracker(); + return this.__tracker; + } +}; +CookieEngine.prototype.__proto__ = new Engine(); + +function CookieSyncCore() { + this._init(); +} +CookieSyncCore.prototype = { + _logName: "CookieSync", + + __cookieManager: null, + get _cookieManager() { + if (!this.__cookieManager) + this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. + getService(Ci.nsICookieManager2); + /* need the 2nd revision of the ICookieManager interface + because it supports add() and the 1st one doesn't. */ + return this.__cookieManager; + }, + + + _itemExists: function CSC__itemExists(GUID) { + /* true if a cookie with the given GUID exists. + The GUID that we are passed should correspond to the keys + that we define in the JSON returned by CookieStore.wrap() + That is, it will be a string of the form + "host:path:name". */ + + /* TODO verify that colons can't normally appear in any of + the fields -- if they did it then we can't rely on .split(":") + to parse correctly.*/ + + let cookieArray = GUID.split( ":" ); + let cookieHost = cookieArray[0]; + let cookiePath = cookieArray[1]; + let cookieName = cookieArray[2]; + + /* alternate implementation would be to instantiate a cookie from + cookieHost, cookiePath, and cookieName, then call + cookieManager.cookieExists(). Maybe that would have better + performance? This implementation seems pretty slow.*/ + let enumerator = this._cookieManager.enumerator; + while (enumerator.hasMoreElements()) + { + let aCookie = enumerator.getNext(); + if (aCookie.host == cookieHost && + aCookie.path == cookiePath && + aCookie.name == cookieName ) { + return true; + } + } + return false; + /* Note: We can't just call cookieManager.cookieExists() with a generic + javascript object with .host, .path, and .name attributes attatched. + cookieExists is implemented in C and does a hard static_cast to an + nsCookie object, so duck typing doesn't work (and in fact makes + Firefox hard-crash as the static_cast returns null and is not checked.) + */ + }, + + _commandLike: function CSC_commandLike(a, b) { + /* Method required to be overridden. + a and b each have a .data and a .GUID + If this function returns true, an editCommand will be + generated to try to resolve the thing. + but are a and b objects of the type in the Store or + are they "commands"?? */ + return false; + } +}; +CookieSyncCore.prototype.__proto__ = new SyncCore(); + +function CookieStore( cookieManagerStub ) { + /* If no argument is passed in, this store will query/write to the real + Mozilla cookie manager component. This is the normal way to use this + class in production code. But for unit-testing purposes, you can pass + in a stub object that will be used in place of the cookieManager. */ + this._init(); + this._cookieManagerStub = cookieManagerStub; +} +CookieStore.prototype = { + _logName: "CookieStore", + + + // Documentation of the nsICookie interface says: + // name ACString The name of the cookie. Read only. + // value ACString The cookie value. Read only. + // isDomain boolean True if the cookie is a domain cookie, false otherwise. Read only. + // host AUTF8String The host (possibly fully qualified) of the cookie. Read only. + // path AUTF8String The path pertaining to the cookie. Read only. + // isSecure boolean True if the cookie was transmitted over ssl, false otherwise. Read only. + // expires PRUint64 Expiration time (local timezone) expressed as number of seconds since Jan 1, 1970. Read only. + // status nsCookieStatus Holds the P3P status of cookie. Read only. + // policy nsCookiePolicy Holds the site's compact policy value. Read only. + // nsICookie2 deprecates expires, status, and policy, and adds: + //rawHost AUTF8String The host (possibly fully qualified) of the cookie without a leading dot to represent if it is a domain cookie. Read only. + //isSession boolean True if the cookie is a session cookie. Read only. + //expiry PRInt64 the actual expiry time of the cookie (where 0 does not represent a session cookie). Read only. + //isHttpOnly boolean True if the cookie is an http only cookie. Read only. + + __cookieManager: null, + get _cookieManager() { + if ( this._cookieManagerStub != undefined ) { + return this._cookieManagerStub; + } + // otherwise, use the real one + if (!this.__cookieManager) + this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. + getService(Ci.nsICookieManager2); + // need the 2nd revision of the ICookieManager interface + // because it supports add() and the 1st one doesn't. + return this.__cookieManager + }, + + _createCommand: function CookieStore__createCommand(command) { + /* we got a command to create a cookie in the local browser + in order to sync with the server. */ + + this._log.info("CookieStore got createCommand: " + command ); + // this assumes command.data fits the nsICookie2 interface + if ( !command.data.isSession ) { + // Add only persistent cookies ( not session cookies ) + this._cookieManager.add( command.data.host, + command.data.path, + command.data.name, + command.data.value, + command.data.isSecure, + command.data.isHttpOnly, + command.data.isSession, + command.data.expiry ); + } + }, + + _removeCommand: function CookieStore__removeCommand(command) { + /* we got a command to remove a cookie from the local browser + in order to sync with the server. + command.data appears to be equivalent to what wrap() puts in + the JSON dictionary. */ + + this._log.info("CookieStore got removeCommand: " + command ); + + /* I think it goes like this, according to + http://developer.mozilla.org/en/docs/nsICookieManager + the last argument is "always block cookies from this domain?" + and the answer is "no". */ + this._cookieManager.remove( command.data.host, + command.data.name, + command.data.path, + false ); + }, + + _editCommand: function CookieStore__editCommand(command) { + /* we got a command to change a cookie in the local browser + in order to sync with the server. */ + this._log.info("CookieStore got editCommand: " + command ); + + /* Look up the cookie that matches the one in the command: */ + var iter = this._cookieManager.enumerator; + var matchingCookie = null; + while (iter.hasMoreElements()){ + let cookie = iter.getNext(); + if (cookie.QueryInterface( Ci.nsICookie ) ){ + // see if host:path:name of cookie matches GUID given in command + let key = cookie.host + ":" + cookie.path + ":" + cookie.name; + if (key == command.GUID) { + matchingCookie = cookie; + break; + } + } + } + // Update values in the cookie: + for (var key in command.data) { + // Whatever values command.data has, use them + matchingCookie[ key ] = command.data[ key ] + } + // Remove the old incorrect cookie from the manager: + this._cookieManager.remove( matchingCookie.host, + matchingCookie.name, + matchingCookie.path, + false ); + + // Re-add the new updated cookie: + if ( !command.data.isSession ) { + /* ignore single-session cookies, add only persistent cookies. */ + this._cookieManager.add( matchingCookie.host, + matchingCookie.path, + matchingCookie.name, + matchingCookie.value, + matchingCookie.isSecure, + matchingCookie.isHttpOnly, + matchingCookie.isSession, + matchingCookie.expiry ); + } + + // Also, there's an exception raised because + // this._data[comand.GUID] is undefined + }, + + wrap: function CookieStore_wrap() { + /* Return contents of this store, as JSON. + A dictionary of cookies where the keys are GUIDs and the + values are sub-dictionaries containing all cookie fields. */ + + let items = {}; + var iter = this._cookieManager.enumerator; + while (iter.hasMoreElements()){ + var cookie = iter.getNext(); + if (cookie.QueryInterface( Ci.nsICookie )){ + // String used to identify cookies is + // host:path:name + if ( cookie.isSession ) { + /* Skip session-only cookies, sync only persistent cookies. */ + continue; + } + + let key = cookie.host + ":" + cookie.path + ":" + cookie.name; + items[ key ] = { parentGUID: '', + name: cookie.name, + value: cookie.value, + isDomain: cookie.isDomain, + host: cookie.host, + path: cookie.path, + isSecure: cookie.isSecure, + // nsICookie2 values: + rawHost: cookie.rawHost, + isSession: cookie.isSession, + expiry: cookie.expiry, + isHttpOnly: cookie.isHttpOnly } + + /* See http://developer.mozilla.org/en/docs/nsICookie + Note: not syncing "expires", "status", or "policy" + since they're deprecated. */ + + } + } + return items; + }, + + wipe: function CookieStore_wipe() { + /* Remove everything from the store. Return nothing. + TODO are the semantics of this just wiping out an internal + buffer, or am I supposed to wipe out all cookies from + the browser itself for reals? */ + this._cookieManager.removeAll() + }, + + resetGUIDs: function CookieStore_resetGUIDs() { + /* called in the case where remote/local sync GUIDs do not + match. We do need to override this, but since we're deriving + GUIDs from the cookie data itself and not generating them, + there's basically no way they can get "out of sync" so there's + nothing to do here. */ + } +}; +CookieStore.prototype.__proto__ = new Store(); + +function CookieTracker() { + this._init(); +} +CookieTracker.prototype = { + _logName: "CookieTracker", + + _init: function CT__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + /* cookieService can't register observers, but what we CAN do is + register a general observer with the global observerService + to watch for the 'cookie-changed' message. */ + let observerService = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + observerService.addObserver( this, 'cookie-changed', false ); + }, + + // implement observe method to satisfy nsIObserver interface + observe: function ( aSubject, aTopic, aData ) { + /* This gets called when any cookie is added, changed, or removed. + aData will contain a string "added", "changed", etc. to tell us which, + but for now we can treat them all the same. aSubject is the new + cookie object itself. */ + var newCookie = aSubject.QueryInterface( Ci.nsICookie2 ); + if ( newCookie ) { + if ( !newCookie.isSession ) { + /* Any modification to a persistent cookie is worth + 10 points out of 100. Ignore session cookies. */ + this._score += 10; + } + } + } +} +CookieTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eb9868907d93..e2c68d969df7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -51,6 +51,7 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/engines/cookies.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index e12ddb41ff7c..b698477a560c 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore']; + 'HistoryStore', 'PasswordStore', 'FormStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -764,191 +764,6 @@ HistoryStore.prototype = { }; HistoryStore.prototype.__proto__ = new Store(); - -function CookieStore( cookieManagerStub ) { - /* If no argument is passed in, this store will query/write to the real - Mozilla cookie manager component. This is the normal way to use this - class in production code. But for unit-testing purposes, you can pass - in a stub object that will be used in place of the cookieManager. */ - this._init(); - this._cookieManagerStub = cookieManagerStub; -} -CookieStore.prototype = { - _logName: "CookieStore", - - - // Documentation of the nsICookie interface says: - // name ACString The name of the cookie. Read only. - // value ACString The cookie value. Read only. - // isDomain boolean True if the cookie is a domain cookie, false otherwise. Read only. - // host AUTF8String The host (possibly fully qualified) of the cookie. Read only. - // path AUTF8String The path pertaining to the cookie. Read only. - // isSecure boolean True if the cookie was transmitted over ssl, false otherwise. Read only. - // expires PRUint64 Expiration time (local timezone) expressed as number of seconds since Jan 1, 1970. Read only. - // status nsCookieStatus Holds the P3P status of cookie. Read only. - // policy nsCookiePolicy Holds the site's compact policy value. Read only. - // nsICookie2 deprecates expires, status, and policy, and adds: - //rawHost AUTF8String The host (possibly fully qualified) of the cookie without a leading dot to represent if it is a domain cookie. Read only. - //isSession boolean True if the cookie is a session cookie. Read only. - //expiry PRInt64 the actual expiry time of the cookie (where 0 does not represent a session cookie). Read only. - //isHttpOnly boolean True if the cookie is an http only cookie. Read only. - - __cookieManager: null, - get _cookieManager() { - if ( this._cookieManagerStub != undefined ) { - return this._cookieManagerStub; - } - // otherwise, use the real one - if (!this.__cookieManager) - this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. - getService(Ci.nsICookieManager2); - // need the 2nd revision of the ICookieManager interface - // because it supports add() and the 1st one doesn't. - return this.__cookieManager - }, - - _createCommand: function CookieStore__createCommand(command) { - /* we got a command to create a cookie in the local browser - in order to sync with the server. */ - - this._log.info("CookieStore got createCommand: " + command ); - // this assumes command.data fits the nsICookie2 interface - if ( !command.data.isSession ) { - // Add only persistent cookies ( not session cookies ) - this._cookieManager.add( command.data.host, - command.data.path, - command.data.name, - command.data.value, - command.data.isSecure, - command.data.isHttpOnly, - command.data.isSession, - command.data.expiry ); - } - }, - - _removeCommand: function CookieStore__removeCommand(command) { - /* we got a command to remove a cookie from the local browser - in order to sync with the server. - command.data appears to be equivalent to what wrap() puts in - the JSON dictionary. */ - - this._log.info("CookieStore got removeCommand: " + command ); - - /* I think it goes like this, according to - http://developer.mozilla.org/en/docs/nsICookieManager - the last argument is "always block cookies from this domain?" - and the answer is "no". */ - this._cookieManager.remove( command.data.host, - command.data.name, - command.data.path, - false ); - }, - - _editCommand: function CookieStore__editCommand(command) { - /* we got a command to change a cookie in the local browser - in order to sync with the server. */ - this._log.info("CookieStore got editCommand: " + command ); - - /* Look up the cookie that matches the one in the command: */ - var iter = this._cookieManager.enumerator; - var matchingCookie = null; - while (iter.hasMoreElements()){ - let cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie ) ){ - // see if host:path:name of cookie matches GUID given in command - let key = cookie.host + ":" + cookie.path + ":" + cookie.name; - if (key == command.GUID) { - matchingCookie = cookie; - break; - } - } - } - // Update values in the cookie: - for (var key in command.data) { - // Whatever values command.data has, use them - matchingCookie[ key ] = command.data[ key ] - } - // Remove the old incorrect cookie from the manager: - this._cookieManager.remove( matchingCookie.host, - matchingCookie.name, - matchingCookie.path, - false ); - - // Re-add the new updated cookie: - if ( !command.data.isSession ) { - /* ignore single-session cookies, add only persistent cookies. */ - this._cookieManager.add( matchingCookie.host, - matchingCookie.path, - matchingCookie.name, - matchingCookie.value, - matchingCookie.isSecure, - matchingCookie.isHttpOnly, - matchingCookie.isSession, - matchingCookie.expiry ); - } - - // Also, there's an exception raised because - // this._data[comand.GUID] is undefined - }, - - wrap: function CookieStore_wrap() { - /* Return contents of this store, as JSON. - A dictionary of cookies where the keys are GUIDs and the - values are sub-dictionaries containing all cookie fields. */ - - let items = {}; - var iter = this._cookieManager.enumerator; - while (iter.hasMoreElements()){ - var cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie )){ - // String used to identify cookies is - // host:path:name - if ( cookie.isSession ) { - /* Skip session-only cookies, sync only persistent cookies. */ - continue; - } - - let key = cookie.host + ":" + cookie.path + ":" + cookie.name; - items[ key ] = { parentGUID: '', - name: cookie.name, - value: cookie.value, - isDomain: cookie.isDomain, - host: cookie.host, - path: cookie.path, - isSecure: cookie.isSecure, - // nsICookie2 values: - rawHost: cookie.rawHost, - isSession: cookie.isSession, - expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly } - - /* See http://developer.mozilla.org/en/docs/nsICookie - Note: not syncing "expires", "status", or "policy" - since they're deprecated. */ - - } - } - return items; - }, - - wipe: function CookieStore_wipe() { - /* Remove everything from the store. Return nothing. - TODO are the semantics of this just wiping out an internal - buffer, or am I supposed to wipe out all cookies from - the browser itself for reals? */ - this._cookieManager.removeAll() - }, - - resetGUIDs: function CookieStore_resetGUIDs() { - /* called in the case where remote/local sync GUIDs do not - match. We do need to override this, but since we're deriving - GUIDs from the cookie data itself and not generating them, - there's basically no way they can get "out of sync" so there's - nothing to do here. */ - } -}; -CookieStore.prototype.__proto__ = new Store(); - function PasswordStore() { this._init(); } diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 39e46901651e..d4f2f8636105 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', - 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore']; + 'PasswordSyncCore', 'FormSyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -431,76 +431,6 @@ HistorySyncCore.prototype = { }; HistorySyncCore.prototype.__proto__ = new SyncCore(); - -function CookieSyncCore() { - this._init(); -} -CookieSyncCore.prototype = { - _logName: "CookieSync", - - __cookieManager: null, - get _cookieManager() { - if (!this.__cookieManager) - this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. - getService(Ci.nsICookieManager2); - /* need the 2nd revision of the ICookieManager interface - because it supports add() and the 1st one doesn't. */ - return this.__cookieManager; - }, - - - _itemExists: function CSC__itemExists(GUID) { - /* true if a cookie with the given GUID exists. - The GUID that we are passed should correspond to the keys - that we define in the JSON returned by CookieStore.wrap() - That is, it will be a string of the form - "host:path:name". */ - - /* TODO verify that colons can't normally appear in any of - the fields -- if they did it then we can't rely on .split(":") - to parse correctly.*/ - - let cookieArray = GUID.split( ":" ); - let cookieHost = cookieArray[0]; - let cookiePath = cookieArray[1]; - let cookieName = cookieArray[2]; - - /* alternate implementation would be to instantiate a cookie from - cookieHost, cookiePath, and cookieName, then call - cookieManager.cookieExists(). Maybe that would have better - performance? This implementation seems pretty slow.*/ - let enumerator = this._cookieManager.enumerator; - while (enumerator.hasMoreElements()) - { - let aCookie = enumerator.getNext(); - if (aCookie.host == cookieHost && - aCookie.path == cookiePath && - aCookie.name == cookieName ) { - return true; - } - } - return false; - /* Note: We can't just call cookieManager.cookieExists() with a generic - javascript object with .host, .path, and .name attributes attatched. - cookieExists is implemented in C and does a hard static_cast to an - nsCookie object, so duck typing doesn't work (and in fact makes - Firefox hard-crash as the static_cast returns null and is not checked.) - */ - }, - - _commandLike: function CSC_commandLike(a, b) { - /* Method required to be overridden. - a and b each have a .data and a .GUID - If this function returns true, an editCommand will be - generated to try to resolve the thing. - but are a and b objects of the type in the Store or - are they "commands"?? */ - return false; - } -}; -CookieSyncCore.prototype.__proto__ = new SyncCore(); - - function PasswordSyncCore() { this._init(); } diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 7ba1a09060d7..1b1f20599475 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker', - 'FormsTracker', 'CookieTracker']; + 'FormsTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -55,7 +55,7 @@ Function.prototype.async = Async.sugar; * listening for changes to their particular data type * and updating their 'score', indicating how urgently they * want to sync. - * + * * 'score's range from 0 (Nothing's changed) * to 100 (I need to sync now!) * -1 is also a valid score @@ -93,7 +93,7 @@ Tracker.prototype = { this._score = 0; } }; - + /* * Tracker objects for each engine may need to subclass the * getScore routine, which returns the current 'score' for that @@ -121,7 +121,7 @@ BookmarksTracker.prototype = { onItemVisited: function BMT_onItemVisited() { }, - + /* Every add or remove is worth 4 points, * on the basis that adding or removing 20 bookmarks * means its time to sync? @@ -140,14 +140,14 @@ BookmarksTracker.prototype = { _init: function BMT__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); this._score = 0; - + Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService). addObserver(this, false); } } BookmarksTracker.prototype.__proto__ = new Tracker(); - + function HistoryTracker() { this._init(); } @@ -167,7 +167,7 @@ HistoryTracker.prototype = { onTitleChanged: function HT_onTitleChanged() { }, - + /* Every add or remove is worth 1 point. * Clearing the whole history is worth 50 points, * to ensure we're above the cutoff for syncing @@ -189,7 +189,7 @@ HistoryTracker.prototype = { _init: function HT__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); this._score = 0; - + Cc["@mozilla.org/browser/nav-history-service;1"]. getService(Ci.nsINavHistoryService). addObserver(this, false); @@ -197,41 +197,6 @@ HistoryTracker.prototype = { } HistoryTracker.prototype.__proto__ = new Tracker(); -function CookieTracker() { - this._init(); -} -CookieTracker.prototype = { - _logName: "CookieTracker", - - _init: function CT__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - /* cookieService can't register observers, but what we CAN do is - register a general observer with the global observerService - to watch for the 'cookie-changed' message. */ - let observerService = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - observerService.addObserver( this, 'cookie-changed', false ); - }, - - // implement observe method to satisfy nsIObserver interface - observe: function ( aSubject, aTopic, aData ) { - /* This gets called when any cookie is added, changed, or removed. - aData will contain a string "added", "changed", etc. to tell us which, - but for now we can treat them all the same. aSubject is the new - cookie object itself. */ - var newCookie = aSubject.QueryInterface( Ci.nsICookie2 ); - if ( newCookie ) { - if ( !newCookie.isSession ) { - /* Any modification to a persistent cookie is worth - 10 points out of 100. Ignore session cookies. */ - this._score += 10; - } - } - } -} -CookieTracker.prototype.__proto__ = new Tracker(); - function FormsTracker() { this._init(); } @@ -279,7 +244,7 @@ FormsTracker.prototype = { return 100; else return this._score; - }, + }, resetScore: function FormsTracker_resetScore() { var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); diff --git a/services/sync/tests/unit/test_cookie_store.js b/services/sync/tests/unit/test_cookie_store.js index ae0dbbb43805..4131ac29a4f1 100644 --- a/services/sync/tests/unit/test_cookie_store.js +++ b/services/sync/tests/unit/test_cookie_store.js @@ -1,10 +1,12 @@ -function FakeCookie( host, path, name, value, +Components.utils.import("resource://weave/engines/cookies.js"); + +function FakeCookie( host, path, name, value, isSecure, isHttpOnly, isSession, expiry ) { - this._init( host, path, name, value, + this._init( host, path, name, value, isSecure, isHttpOnly, isSession, expiry ); } FakeCookie.prototype = { - _init: function( host, path, name, value, + _init: function( host, path, name, value, isSecure, isHttpOnly, isSession, expiry) { this.host = host; this.path = path; @@ -49,7 +51,7 @@ FakeCookieManager.prototype = { this._cookieList = []; }, - add: function( host, path, name, value, + add: function( host, path, name, value, isSecure, isHttpOnly, isSession, expiry) { var newCookie = new FakeCookie( host, path, @@ -84,8 +86,6 @@ FakeCookieManager.prototype = { }; function sub_test_cookie_tracker() { - Components.utils.import("resource://weave/trackers.js"); - var ct = new CookieTracker(); // gonna have to use the real cookie manager here... @@ -127,8 +127,6 @@ function run_test() { then call cookieStore.wrap() and make sure it returns the persistent one and not the non-persistent one */ - Components.utils.import("resource://weave/stores.js"); - // My stub object to replace the real cookieManager: var fakeCookieManager = new FakeCookieManager(); From 720c720b9ba4e2dd5f6426b1237edc6b26a52dda Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Tue, 3 Jun 2008 11:32:59 -0700 Subject: [PATCH 0277/1860] bug 434817: sync tabs --- services/sync/locales/en-US/preferences.dtd | 1 + services/sync/locales/en-US/sync.dtd | 7 + services/sync/modules/engines.js | 34 ++- services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 294 +++++++++++++++++++- services/sync/modules/syncCores.js | 55 +++- services/sync/modules/trackers.js | 95 ++++++- services/sync/services-sync.js | 1 + 8 files changed, 484 insertions(+), 4 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 5196cdff55ad..40f1e185dfc8 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -24,6 +24,7 @@ + diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 3c3f89839aba..8e4e52ee10a8 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -7,3 +7,10 @@ + + + + + + + diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 71ca41aae9df..e3cbd368a47c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -19,6 +19,7 @@ * * Contributor(s): * Dan Mills + * Myk Melez * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -36,7 +37,7 @@ const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'BookmarksEngine', 'HistoryEngine', 'CookieEngine', - 'PasswordEngine', 'FormEngine']; + 'PasswordEngine', 'FormEngine', 'TabEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -1016,3 +1017,34 @@ FormEngine.prototype = { } }; FormEngine.prototype.__proto__ = new Engine(); + +function TabEngine(pbeId) { + this._init(pbeId); +} +TabEngine.prototype = { + __proto__: new Engine(), + + get name() "tabs", + get logName() "TabEngine", + get serverPrefix() "user-data/tabs/", + get store() this._store, + + get _core() { + let core = new TabSyncCore(this); + this.__defineGetter__("_core", function() core); + return this._core; + }, + + get _store() { + let store = new TabStore(); + this.__defineGetter__("_store", function() store); + return this._store; + }, + + get _tracker() { + let tracker = new TabTracker(this); + this.__defineGetter__("_tracker", function() tracker); + return this._tracker; + } + +}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eb9868907d93..2382a0a94633 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -93,6 +93,7 @@ function WeaveSvc() { Engines.register(new CookieEngine()); Engines.register(new PasswordEngine()); Engines.register(new FormEngine()); + Engines.register(new TabEngine()); // Other misc startup Utils.prefs.addObserver("", this, false); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 51feef1348ca..10e6063859af 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,8 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore']; + 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore', + 'TabStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -1103,3 +1104,294 @@ FormStore.prototype = { } }; FormStore.prototype.__proto__ = new Store(); + +function TabStore() { + this._virtualTabs = {}; + this._init(); +} +TabStore.prototype = { + __proto__: new Store(), + + _logName: "TabStore", + + get _sessionStore() { + let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + this.__defineGetter__("_sessionStore", function() sessionStore); + return this._sessionStore; + }, + + get _windowMediator() { + let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + this.__defineGetter__("_windowMediator", function() windowMediator); + return this._windowMediator; + }, + + get _os() { + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + this.__defineGetter__("_os", function() os); + return this._os; + }, + + get _dirSvc() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + this.__defineGetter__("_dirSvc", function() dirSvc); + return this._dirSvc; + }, + + /** + * A cache of "virtual" tabs from other devices synced to the server + * that the user hasn't opened locally. Unlike other stores, we don't + * immediately apply create commands, which would be jarring to users. + * Instead, we store them in this cache and prompt the user to pick + * which ones she wants to open. + * + * We also persist this cache on disk and include it in the list of tabs + * we generate in this.wrap to reduce ping-pong updates between clients + * running simultaneously and to maintain a consistent state across restarts. + */ + _virtualTabs: null, + + get virtualTabs() { + // Make sure the list of virtual tabs is completely up-to-date (the user + // might have independently opened some of these virtual tabs since the last + // time we synced). + let realTabs = this._wrapRealTabs(); + let virtualTabsChanged = false; + for (let id in this._virtualTabs) { + if (id in realTabs) { + this._log.warn("get virtualTabs: both real and virtual tabs exist for " + + id + "; removing virtual one"); + delete this._virtualTabs[id]; + virtualTabsChanged = true; + } + } + if (virtualTabsChanged) + this._saveVirtualTabs(); + + return this._virtualTabs; + }, + + set virtualTabs(newValue) { + this._virtualTabs = newValue; + this._saveVirtualTabs(); + }, + + // The file in which we store the state of virtual tabs. + get _file() { + let file = this._dirSvc.get("ProfD", Ci.nsILocalFile); + file.append("weave"); + file.append("store"); + file.append("tabs"); + file.append("virtual.json"); + this.__defineGetter__("_file", function() file); + return this._file; + }, + + _saveVirtualTabs: function TabStore__saveVirtualTabs() { + try { + if (!this._file.exists()) + this._file.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + let out = this._json.encode(this._virtualTabs); + let [fos] = Utils.open(this._file, ">"); + fos.writeString(out); + fos.close(); + } + catch(ex) { + this._log.warn("could not serialize virtual tabs to disk: " + ex); + } + }, + + _restoreVirtualTabs: function TabStore__restoreVirtualTabs() { + try { + if (this._file.exists()) { + let [is] = Utils.open(this._file, "<"); + let json = Utils.readStream(is); + is.close(); + this._virtualTabs = this._json.decode(json); + } + } + catch (ex) { + this._log.warn("could not parse virtual tabs from disk: " + ex); + } + }, + + _init: function TabStore__init() { + this._restoreVirtualTabs(); + + this.__proto__.__proto__._init(); + }, + + /** + * Apply commands generated by a diff during a sync operation. This method + * overrides the one in its superclass so it can save a copy of the latest set + * of virtual tabs to disk so they can be restored on startup. + */ + applyCommands: function TabStore_applyCommands(commandList) { + let self = yield; + + this.__proto__.__proto__.applyCommands.async(this, self.cb, commandList); + yield; + + this._saveVirtualTabs(); + + self.done(); + }, + + _createCommand: function TabStore__createCommand(command) { + this._log.debug("_createCommand: " + command.GUID); + + if (command.GUID in this._virtualTabs || command.GUID in this._wrapRealTabs()) + throw "trying to create a tab that already exists; id: " + command.GUID; + + // Cache the tab and notify the UI to prompt the user to open it. + this._virtualTabs[command.GUID] = command.data; + this._os.notifyObservers(null, "weave:store:tabs:virtual:created", null); + }, + + _removeCommand: function TabStore__removeCommand(command) { + this._log.debug("_removeCommand: " + command.GUID); + + // If this is a virtual tab, it's ok to remove it, since it was never really + // added to this session in the first place. But we don't remove it if it's + // a real tab, since that would be unexpected, unpleasant, and unwanted. + if (command.GUID in this._virtualTabs) { + delete this._virtualTabs[command.GUID]; + this._os.notifyObservers(null, "weave:store:tabs:virtual:removed", null); + } + }, + + _editCommand: function TabStore__editCommand(command) { + this._log.debug("_editCommand: " + command.GUID); + + // We don't edit real tabs, because that isn't what the user would expect, + // but it's ok to edit virtual tabs, so that if users do open them, they get + // the most up-to-date version of them (and also to reduce sync churn). + + if (this._virtualTabs[command.GUID]) + this._virtualTabs[command.GUID] = command.data; + }, + + /** + * Serialize the current state of tabs. + * + * Note: the state includes both tabs on this device and those on others. + * We get the former from the session store. The latter we retrieved from + * the Weave server and stored in this._virtualTabs. Including virtual tabs + * in the serialized state prevents ping-pong deletes between two clients + * running at the same time. + */ + wrap: function TabStore_wrap() { + let items; + + let virtualTabs = this._wrapVirtualTabs(); + let realTabs = this._wrapRealTabs(); + + // Real tabs override virtual ones, which means ping-pong edits when two + // clients have the same URL loaded with different history/attributes. + // We could fix that by overriding real tabs with virtual ones, but then + // we'd have stale tab metadata in same cases. + items = virtualTabs; + let virtualTabsChanged = false; + for (let id in realTabs) { + // Since virtual tabs can sometimes get out of sync with real tabs + // (the user could have independently opened a new tab that exists + // in the virtual tabs cache since the last time we updated the cache), + // we sync them up in the process of merging them here. + if (this._virtualTabs[id]) { + this._log.warn("wrap: both real and virtual tabs exist for " + id + + "; removing virtual one"); + delete this._virtualTabs[id]; + virtualTabsChanged = true; + } + + items[id] = realTabs[id]; + } + if (virtualTabsChanged) + this._saveVirtualTabs(); + + return items; + }, + + _wrapVirtualTabs: function TabStore__wrapVirtualTabs() { + let items = {}; + + for (let id in this._virtualTabs) { + let virtualTab = this._virtualTabs[id]; + + // Copy the virtual tab without private properties (those that begin + // with an underscore character) so that we don't sync data private to + // this particular Weave client (like the _disposed flag). + let item = {}; + for (let property in virtualTab) + if (property[0] != "_") + item[property] = virtualTab[property]; + + items[id] = item; + } + + return items; + }, + + _wrapRealTabs: function TabStore__wrapRealTabs() { + let items = {}; + + let session = this._json.decode(this._sessionStore.getBrowserState()); + + for (let i = 0; i < session.windows.length; i++) { + let window = session.windows[i]; + // For some reason, session store uses one-based array index references, + // (f.e. in the "selectedWindow" and each tab's "index" properties), so we + // convert them to and from JavaScript's zero-based indexes as needed. + let windowID = i + 1; + this._log.debug("_wrapRealTabs: window " + windowID); + for (let j = 0; j < window.tabs.length; j++) { + let tab = window.tabs[j]; + + // The session history entry for the page currently loaded in the tab. + // We use the URL of the current page as the ID for the tab. + let currentEntry = tab.entries[tab.index - 1]; + + if (!currentEntry || !currentEntry.url) { + this._log.warn("_wrapRealTabs: no current entry or no URL, can't " + + "identify " + this._json.encode(tab)); + continue; + } + + let tabID = currentEntry.url; + this._log.debug("_wrapRealTabs: tab " + tabID); + + items[tabID] = { + // Identify this item as a tab in case we start serializing windows + // in the future. + type: "tab", + + // The position of this tab relative to other tabs in the window. + // For consistency with session store data, we make this one-based. + position: j + 1, + + windowID: windowID, + + state: tab + }; + } + } + + return items; + }, + + wipe: function TabStore_wipe() { + // We're not going to close tabs, since that's probably not what + // the user wants, but we'll clear the cache of virtual tabs. + this._virtualTabs = {}; + this._saveVirtualTabs(); + }, + + resetGUIDs: function TabStore_resetGUIDs() { + // Not needed. + } + +}; diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 39e46901651e..356e484c68a3 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,8 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', - 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore']; + 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore', + 'TabSyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -582,3 +583,55 @@ FormSyncCore.prototype = { } }; FormSyncCore.prototype.__proto__ = new SyncCore(); + +function TabSyncCore(engine) { + this._engine = engine; + this._init(); +} +TabSyncCore.prototype = { + __proto__: new SyncCore(), + + _logName: "TabSync", + + _engine: null, + + get _sessionStore() { + let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + this.__defineGetter__("_sessionStore", function() sessionStore); + return this._sessionStore; + }, + + // XXX Should we put this into SyncCore so it's available to all subclasses? + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return this._json; + }, + + _itemExists: function TSC__itemExists(GUID) { + // Note: this method returns true if the tab exists in any window, not just + // the window from which the tab came. In the future, if we care about + // windows, we might need to make this more specific, although in that case + // we'll have to identify tabs by something other than URL, since even + // window-specific tabs look the same when identified by URL. + + // Get the set of all real and virtual tabs. + let tabs = this._engine.store.wrap(); + + // XXX Should we convert both to nsIURIs and then use nsIURI::equals + // to compare them? + if (GUID in tabs) { + this._log.debug("_itemExists: " + GUID + " exists"); + return true; + } + + this._log.debug("_itemExists: " + GUID + " doesn't exist"); + return false; + }, + + _commandLike: function TSC_commandLike(a, b) { + // Not implemented. + return false; + } +}; diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 7ba1a09060d7..e91e90643c54 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker', - 'FormsTracker', 'CookieTracker']; + 'FormsTracker', 'CookieTracker', 'TabTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -300,3 +300,96 @@ FormsTracker.prototype = { } } FormsTracker.prototype.__proto__ = new Tracker(); + +function TabTracker(engine) { + this._engine = engine; + this._init(); +} +TabTracker.prototype = { + __proto__: new Tracker(), + + _logName: "TabTracker", + + _engine: null, + + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return this._json; + }, + + /** + * There are two ways we could calculate the score. We could calculate it + * incrementally by using the window mediator to watch for windows opening/ + * closing and FUEL (or some other API) to watch for tabs opening/closing + * and changing location. + * + * Or we could calculate it on demand by comparing the state of tabs + * according to the session store with the state according to the snapshot. + * + * It's hard to say which is better. The incremental approach is less + * accurate if it simply increments the score whenever there's a change, + * but it might be more performant. The on-demand approach is more accurate, + * but it might be less performant depending on how often it's called. + * + * In this case we've decided to go with the on-demand approach, and we + * calculate the score as the percent difference between the snapshot set + * and the current tab set, where tabs that only exist in one set are + * completely different, while tabs that exist in both sets but whose data + * doesn't match (f.e. because of variations in history) are considered + * "half different". + * + * So if the sets don't match at all, we return 100; + * if they completely match, we return 0; + * if half the tabs match, and their data is the same, we return 50; + * and if half the tabs match, but their data is all different, we return 75. + */ + get score() { + // The snapshot data is a singleton that we can't modify, so we have to + // copy its unique items to a new hash. + let snapshotData = this._engine.snapshot.data; + let a = {}; + + // The wrapped current state is a unique instance we can munge all we want. + let b = this._engine.store.wrap(); + + // An array that counts the number of intersecting IDs between a and b + // (represented as the length of c) and whether or not their values match + // (represented by the boolean value of each item in c). + let c = []; + + // Generate c and update a and b to contain only unique items. + for (id in snapshotData) { + if (id in b) { + c.push(this._json.encode(snapshotData[id]) == this._json.encode(b[id])); + delete b[id]; + } + else { + a[id] = snapshotData[id]; + } + } + + let numShared = c.length; + let numUnique = [true for (id in a)].length + [true for (id in b)].length; + let numTotal = numShared + numUnique; + + // We're going to divide by the total later, so make sure we don't try + // to divide by zero, even though we should never be in a state where there + // are no tabs in either set. + if (numTotal == 0) + return 0; + + // The number of shared items whose data is different. + let numChanged = c.filter(function(v) v).length; + + let fractionSimilar = (numShared - (numChanged / 2)) / numTotal; + let fractionDissimilar = 1 - fractionSimilar; + let percentDissimilar = Math.round(fractionDissimilar * 100); + + return percentDissimilar; + }, + + resetScore: function FormsTracker_resetScore() { + // Not implemented, since we calculate the score on demand. + } +} diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9890ce14e463..561ee2937afe 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -17,6 +17,7 @@ pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.cookies", false ); pref("extensions.weave.engine.passwords", false ); pref("extensions.weave.engine.forms", false ); +pref("extensions.weave.engine.tabs", false); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 48d203eefad0ab1884c8713c088b41c727bab7a3 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Tue, 3 Jun 2008 11:50:08 -0700 Subject: [PATCH 0278/1860] remove unused _json property from TabSyncCore --- services/sync/modules/syncCores.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 356e484c68a3..9fcbe264bddf 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -602,13 +602,6 @@ TabSyncCore.prototype = { return this._sessionStore; }, - // XXX Should we put this into SyncCore so it's available to all subclasses? - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return this._json; - }, - _itemExists: function TSC__itemExists(GUID) { // Note: this method returns true if the tab exists in any window, not just // the window from which the tab came. In the future, if we care about From 1e010ff7f6a71c2c5ee13e1a83361bb13cf447ab Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 12:38:48 -0700 Subject: [PATCH 0279/1860] Re-removed cookie-related changes that were accidentally re-added by c1a58b24679c and/or 5a49daf87c94. Also moved all bookmark syncing logic into modules/engines/bookmarks.js. --- services/sync/modules/engines.js | 151 +---- services/sync/modules/engines/bookmarks.js | 700 +++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 602 +----------------- services/sync/modules/syncCores.js | 172 +---- services/sync/modules/trackers.js | 93 +-- 6 files changed, 708 insertions(+), 1011 deletions(-) create mode 100644 services/sync/modules/engines/bookmarks.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e3cbd368a47c..aaa45771c77c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'BookmarksEngine', 'HistoryEngine', 'CookieEngine', + 'HistoryEngine', 'PasswordEngine', 'FormEngine', 'TabEngine']; const Cc = Components.classes; @@ -758,124 +758,6 @@ Engine.prototype = { } }; -function BookmarksEngine(pbeId) { - this._init(pbeId); -} -BookmarksEngine.prototype = { - get name() { return "bookmarks"; }, - get logName() { return "BmkEngine"; }, - get serverPrefix() { return "user-data/bookmarks/"; }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new BookmarksSyncCore(); - return this.__core; - }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new BookmarksStore(); - return this.__store; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new BookmarksTracker(); - return this.__tracker; - }, - - syncMounts: function BmkEngine_syncMounts(onComplete) { - this._syncMounts.async(this, onComplete); - }, - _syncMounts: function BmkEngine__syncMounts() { - let self = yield; - let mounts = this._store.findMounts(); - - for (i = 0; i < mounts.length; i++) { - try { - this._syncOneMount.async(this, self.cb, mounts[i]); - yield; - } catch (e) { - this._log.warn("Could not sync shared folder from " + mounts[i].userid); - this._log.trace(Utils.stackTrace(e)); - } - } - }, - - _syncOneMount: function BmkEngine__syncOneMount(mountData) { - let self = yield; - let user = mountData.userid; - let prefix = DAV.defaultPrefix; - let serverURL = Utils.prefs.getCharPref("serverURL"); - let snap = new SnapshotStore(); - - this._log.debug("Syncing shared folder from user " + user); - - try { - let hash = Utils.sha1(user); - DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly! - - this._getSymKey.async(this, self.cb); - yield; - - this._log.trace("Getting status file for " + user); - DAV.GET(this.statusFile, self.cb); - let resp = yield; - Utils.ensureStatus(resp.status, "Could not download status file."); - let status = this._json.decode(resp.responseText); - - this._log.trace("Downloading server snapshot for " + user); - DAV.GET(this.snapshotFile, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not download snapshot."); - Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._engineId, status.snapEncryption); - let data = yield; - snap.data = this._json.decode(data); - - this._log.trace("Downloading server deltas for " + user); - DAV.GET(this.deltasFile, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not download deltas."); - Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._engineId, status.deltasEncryption); - data = yield; - deltas = this._json.decode(data); - } - catch (e) { throw e; } - finally { DAV.defaultPrefix = prefix; } - - // apply deltas to get current snapshot - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands.async(snap, self.cb, deltas[i]); - yield; - } - - // prune tree / get what we want - for (let guid in snap.data) { - if (snap.data[guid].type != "bookmark") - delete snap.data[guid]; - else - snap.data[guid].parentGUID = mountData.rootGUID; - } - - this._log.trace("Got bookmarks fror " + user + ", comparing with local copy"); - this._core.detectUpdates(self.cb, mountData.snapshot, snap.data); - let diff = yield; - - // FIXME: should make sure all GUIDs here live under the mountpoint - this._log.trace("Applying changes to folder from " + user); - this._store.applyCommands.async(this._store, self.cb, diff); - yield; - - this._log.trace("Shared folder from " + user + " successfully synced!"); - } -}; -BookmarksEngine.prototype.__proto__ = new Engine(); - function HistoryEngine(pbeId) { this._init(pbeId); } @@ -907,37 +789,6 @@ HistoryEngine.prototype = { }; HistoryEngine.prototype.__proto__ = new Engine(); -function CookieEngine(pbeId) { - this._init(pbeId); -} -CookieEngine.prototype = { - get name() { return "cookies"; }, - get logName() { return "CookieEngine"; }, - get serverPrefix() { return "user-data/cookies/"; }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new CookieSyncCore(); - return this.__core; - }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new CookieStore(); - return this.__store; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new CookieTracker(); - return this.__tracker; - } -}; -CookieEngine.prototype.__proto__ = new Engine(); - function PasswordEngine(pbeId) { this._init(pbeId); } diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js new file mode 100644 index 000000000000..f3d1ca72b233 --- /dev/null +++ b/services/sync/modules/engines/bookmarks.js @@ -0,0 +1,700 @@ +const EXPORTED_SYMBOLS = ['BookmarksEngine']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/dav.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); + +Function.prototype.async = Async.sugar; + +function BookmarksEngine(pbeId) { + this._init(pbeId); +} +BookmarksEngine.prototype = { + get name() { return "bookmarks"; }, + get logName() { return "BmkEngine"; }, + get serverPrefix() { return "user-data/bookmarks/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new BookmarksSyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new BookmarksStore(); + return this.__store; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new BookmarksTracker(); + return this.__tracker; + }, + + syncMounts: function BmkEngine_syncMounts(onComplete) { + this._syncMounts.async(this, onComplete); + }, + _syncMounts: function BmkEngine__syncMounts() { + let self = yield; + let mounts = this._store.findMounts(); + + for (i = 0; i < mounts.length; i++) { + try { + this._syncOneMount.async(this, self.cb, mounts[i]); + yield; + } catch (e) { + this._log.warn("Could not sync shared folder from " + mounts[i].userid); + this._log.trace(Utils.stackTrace(e)); + } + } + }, + + _syncOneMount: function BmkEngine__syncOneMount(mountData) { + let self = yield; + let user = mountData.userid; + let prefix = DAV.defaultPrefix; + let serverURL = Utils.prefs.getCharPref("serverURL"); + let snap = new SnapshotStore(); + + this._log.debug("Syncing shared folder from user " + user); + + try { + let hash = Utils.sha1(user); + DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly! + + this._getSymKey.async(this, self.cb); + yield; + + this._log.trace("Getting status file for " + user); + DAV.GET(this.statusFile, self.cb); + let resp = yield; + Utils.ensureStatus(resp.status, "Could not download status file."); + let status = this._json.decode(resp.responseText); + + this._log.trace("Downloading server snapshot for " + user); + DAV.GET(this.snapshotFile, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not download snapshot."); + Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, + this._engineId, status.snapEncryption); + let data = yield; + snap.data = this._json.decode(data); + + this._log.trace("Downloading server deltas for " + user); + DAV.GET(this.deltasFile, self.cb); + resp = yield; + Utils.ensureStatus(resp.status, "Could not download deltas."); + Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, + this._engineId, status.deltasEncryption); + data = yield; + deltas = this._json.decode(data); + } + catch (e) { throw e; } + finally { DAV.defaultPrefix = prefix; } + + // apply deltas to get current snapshot + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands.async(snap, self.cb, deltas[i]); + yield; + } + + // prune tree / get what we want + for (let guid in snap.data) { + if (snap.data[guid].type != "bookmark") + delete snap.data[guid]; + else + snap.data[guid].parentGUID = mountData.rootGUID; + } + + this._log.trace("Got bookmarks fror " + user + ", comparing with local copy"); + this._core.detectUpdates(self.cb, mountData.snapshot, snap.data); + let diff = yield; + + // FIXME: should make sure all GUIDs here live under the mountpoint + this._log.trace("Applying changes to folder from " + user); + this._store.applyCommands.async(this._store, self.cb, diff); + yield; + + this._log.trace("Shared folder from " + user + " successfully synced!"); + } +}; +BookmarksEngine.prototype.__proto__ = new Engine(); + +function BookmarksSyncCore() { + this._init(); +} +BookmarksSyncCore.prototype = { + _logName: "BMSync", + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + _itemExists: function BSC__itemExists(GUID) { + return this._bms.getItemIdForGUID(GUID) >= 0; + }, + + _getEdits: function BSC__getEdits(a, b) { + // NOTE: we do not increment ret.numProps, as that would cause + // edit commands to always get generated + let ret = SyncCore.prototype._getEdits.call(this, a, b); + ret.props.type = a.type; + return ret; + }, + + // compares properties + // returns true if the property is not set in either object + // returns true if the property is set and equal in both objects + // returns false otherwise + _comp: function BSC__comp(a, b, prop) { + return (!a.data[prop] && !b.data[prop]) || + (a.data[prop] && b.data[prop] && (a.data[prop] == b.data[prop])); + }, + + _commandLike: function BSC__commandLike(a, b) { + // Check that neither command is null, that their actions, types, + // and parents are the same, and that they don't have the same + // GUID. + // * Items with the same GUID do not qualify for 'likeness' because + // we already consider them to be the same object, and therefore + // we need to process any edits. + // * Remove or edit commands don't qualify for likeness either, + // since remove or edit commands with different GUIDs are + // guaranteed to refer to two different items + // * The parent GUID check works because reconcile() fixes up the + // parent GUIDs as it runs, and the command list is sorted by + // depth + if (!a || !b || + a.action != b.action || + a.action != "create" || + a.data.type != b.data.type || + a.data.parentGUID != b.data.parentGUID || + a.GUID == b.GUID) + return false; + + // Bookmarks and folders are allowed to be in a different index as long as + // they are in the same folder. Separators must be at + // the same index to qualify for 'likeness'. + switch (a.data.type) { + case "bookmark": + if (this._comp(a, b, 'URI') && + this._comp(a, b, 'title')) + return true; + return false; + case "query": + if (this._comp(a, b, 'URI') && + this._comp(a, b, 'title')) + return true; + return false; + case "microsummary": + if (this._comp(a, b, 'URI') && + this._comp(a, b, 'generatorURI')) + return true; + return false; + case "folder": + if (this._comp(a, b, 'title')) + return true; + return false; + case "livemark": + if (this._comp(a, b, 'title') && + this._comp(a, b, 'siteURI') && + this._comp(a, b, 'feedURI')) + return true; + return false; + case "separator": + if (this._comp(a, b, 'index')) + return true; + return false; + default: + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this._log.error("commandLike: Unknown item type: " + json.encode(a)); + return false; + } + } +}; +BookmarksSyncCore.prototype.__proto__ = new SyncCore(); + +function BookmarksStore() { + this._init(); +} +BookmarksStore.prototype = { + _logName: "BStore", + + __bms: null, + get _bms() { + if (!this.__bms) + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + + __hsvc: null, + get _hsvc() { + if (!this.__hsvc) + this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + return this.__hsvc; + }, + + __ls: null, + get _ls() { + if (!this.__ls) + this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. + getService(Ci.nsILivemarkService); + return this.__ls; + }, + + __ms: null, + get _ms() { + if (!this.__ms) + this.__ms = Cc["@mozilla.org/microsummary/service;1"]. + getService(Ci.nsIMicrosummaryService); + return this.__ms; + }, + + __ts: null, + get _ts() { + if (!this.__ts) + this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); + return this.__ts; + }, + + __ans: null, + get _ans() { + if (!this.__ans) + this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + return this.__ans; + }, + + _getItemIdForGUID: function BStore__getItemIdForGUID(GUID) { + switch (GUID) { + case "menu": + return this._bms.bookmarksMenuFolder; + case "toolbar": + return this._bms.toolbarFolder; + case "unfiled": + return this._bms.unfiledBookmarksFolder; + default: + return this._bms.getItemIdForGUID(GUID); + } + return null; + }, + + _createCommand: function BStore__createCommand(command) { + let newId; + let parentId = this._getItemIdForGUID(command.data.parentGUID); + + if (parentId < 0) { + this._log.warn("Creating node with unknown parent -> reparenting to root"); + parentId = this._bms.bookmarksMenuFolder; + } + + switch (command.data.type) { + case "query": + case "bookmark": + case "microsummary": { + this._log.debug(" -> creating bookmark \"" + command.data.title + "\""); + let URI = Utils.makeURI(command.data.URI); + newId = this._bms.insertBookmark(parentId, + URI, + command.data.index, + command.data.title); + this._ts.untagURI(URI, null); + this._ts.tagURI(URI, command.data.tags); + this._bms.setKeywordForBookmark(newId, command.data.keyword); + if (command.data.description) { + this._ans.setItemAnnotation(newId, "bookmarkProperties/description", + command.data.description, 0, + this._ans.EXPIRE_NEVER); + } + + if (command.data.type == "microsummary") { + this._log.debug(" \-> is a microsummary"); + this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", + command.data.staticTitle || "", 0, this._ans.EXPIRE_NEVER); + let genURI = Utils.makeURI(command.data.generatorURI); + try { + let micsum = this._ms.createMicrosummary(URI, genURI); + this._ms.setMicrosummary(newId, micsum); + } + catch(ex) { /* ignore "missing local generator" exceptions */ } + } + } break; + case "folder": + this._log.debug(" -> creating folder \"" + command.data.title + "\""); + newId = this._bms.createFolder(parentId, + command.data.title, + command.data.index); + break; + case "livemark": + this._log.debug(" -> creating livemark \"" + command.data.title + "\""); + newId = this._ls.createLivemark(parentId, + command.data.title, + Utils.makeURI(command.data.siteURI), + Utils.makeURI(command.data.feedURI), + command.data.index); + break; + case "mounted-share": + this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\""); + newId = this._bms.createFolder(parentId, + command.data.title, + command.data.index); + + this._ans.setItemAnnotation(newId, "weave/mounted-share-id", + command.data.mountId, 0, this._ans.EXPIRE_NEVER); + break; + case "separator": + this._log.debug(" -> creating separator"); + newId = this._bms.insertSeparator(parentId, command.data.index); + break; + default: + this._log.error("_createCommand: Unknown item type: " + command.data.type); + break; + } + if (newId) + this._bms.setItemGUID(newId, command.GUID); + }, + + _removeCommand: function BStore__removeCommand(command) { + if (command.GUID == "menu" || + command.GUID == "toolbar" || + command.GUID == "unfiled") { + this._log.warn("Attempted to remove root node (" + command.GUID + + "). Skipping command."); + return; + } + + var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Attempted to remove item " + command.GUID + + ", but it does not exist. Skipping."); + return; + } + var type = this._bms.getItemType(itemId); + + switch (type) { + case this._bms.TYPE_BOOKMARK: + this._log.debug(" -> removing bookmark " + command.GUID); + this._bms.removeItem(itemId); + break; + case this._bms.TYPE_FOLDER: + this._log.debug(" -> removing folder " + command.GUID); + this._bms.removeFolder(itemId); + break; + case this._bms.TYPE_SEPARATOR: + this._log.debug(" -> removing separator " + command.GUID); + this._bms.removeItem(itemId); + break; + default: + this._log.error("removeCommand: Unknown item type: " + type); + break; + } + }, + + _editCommand: function BStore__editCommand(command) { + if (command.GUID == "menu" || + command.GUID == "toolbar" || + command.GUID == "unfiled") { + this._log.warn("Attempted to edit root node (" + command.GUID + + "). Skipping command."); + return; + } + + var itemId = this._bms.getItemIdForGUID(command.GUID); + if (itemId < 0) { + this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); + return; + } + + for (let key in command.data) { + switch (key) { + case "type": + // all commands have this to help in reconciliation, but it makes + // no sense to edit it + break; + case "GUID": + var existing = this._getItemIdForGUID(command.data.GUID); + if (existing < 0) + this._bms.setItemGUID(itemId, command.data.GUID); + else + this._log.warn("Can't change GUID " + command.GUID + + " to " + command.data.GUID + ": GUID already exists."); + break; + case "title": + this._bms.setItemTitle(itemId, command.data.title); + break; + case "URI": + this._bms.changeBookmarkURI(itemId, Utils.makeURI(command.data.URI)); + break; + case "index": + this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), + command.data.index); + break; + case "parentGUID": { + let index = -1; + if (command.data.index && command.data.index >= 0) + index = command.data.index; + this._bms.moveItem( + itemId, this._getItemIdForGUID(command.data.parentGUID), index); + } break; + case "tags": { + let tagsURI = this._bms.getBookmarkURI(itemId); + this._ts.untagURI(tagsURI, null); + this._ts.tagURI(tagsURI, command.data.tags); + } break; + case "keyword": + this._bms.setKeywordForBookmark(itemId, command.data.keyword); + break; + case "description": + if (command.data.description) { + this._ans.setItemAnnotation(itemId, "bookmarkProperties/description", + command.data.description, 0, + this._ans.EXPIRE_NEVER); + } + break; + case "generatorURI": { + let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); + let genURI = Utils.makeURI(command.data.generatorURI); + let micsum = this._ms.createMicrosummary(micsumURI, genURI); + this._ms.setMicrosummary(itemId, micsum); + } break; + case "siteURI": + this._ls.setSiteURI(itemId, Utils.makeURI(command.data.siteURI)); + break; + case "feedURI": + this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI)); + break; + default: + this._log.warn("Can't change item property: " + key); + break; + } + } + }, + + _getNode: function BSS__getNode(folder) { + let query = this._hsvc.getNewQuery(); + query.setFolders([folder], 1); + return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; + }, + + __wrap: function BSS___wrap(node, items, parentGUID, index, guidOverride) { + let GUID, item; + + // we override the guid for the root items, "menu", "toolbar", etc. + if (guidOverride) { + GUID = guidOverride; + item = {}; + } else { + GUID = this._bms.getItemGUID(node.itemId); + item = {parentGUID: parentGUID, index: index}; + } + + if (node.type == node.RESULT_TYPE_FOLDER) { + if (this._ls.isLivemark(node.itemId)) { + item.type = "livemark"; + let siteURI = this._ls.getSiteURI(node.itemId); + let feedURI = this._ls.getFeedURI(node.itemId); + item.siteURI = siteURI? siteURI.spec : ""; + item.feedURI = feedURI? feedURI.spec : ""; + + } else if (this._ans.itemHasAnnotation(node.itemId, + "weave/mounted-share-id")) { + item.type = "mounted-share"; + item.title = node.title; + item.mountId = this._ans.getItemAnnotation(node.itemId, + "weave/mounted-share-id"); + + } else { + item.type = "folder"; + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this.__wrap(node.getChild(i), items, GUID, i); + } + } + if (!guidOverride) + item.title = node.title; // no titles for root nodes + + } else if (node.type == node.RESULT_TYPE_URI || + node.type == node.RESULT_TYPE_QUERY) { + if (this._ms.hasMicrosummary(node.itemId)) { + item.type = "microsummary"; + let micsum = this._ms.getMicrosummary(node.itemId); + item.generatorURI = micsum.generator.uri.spec; // breaks local generators + item.staticTitle = this._ans.getItemAnnotation(node.itemId, "bookmarks/staticTitle"); + } else if (node.type == node.RESULT_TYPE_QUERY) { + item.type = "query"; + item.title = node.title; + } else { + item.type = "bookmark"; + item.title = node.title; + } + + try { + item.description = + this._ans.getItemAnnotation(node.itemId, "bookmarkProperties/description"); + } catch (e) { + item.description = undefined; + } + + item.URI = node.uri; + item.tags = this._ts.getTagsForURI(Utils.makeURI(node.uri), {}); + item.keyword = this._bms.getKeywordForBookmark(node.itemId); + + } else if (node.type == node.RESULT_TYPE_SEPARATOR) { + item.type = "separator"; + + } else { + this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); + return; + } + + items[GUID] = item; + }, + + // helper + _wrap: function BStore__wrap(node, items, rootName) { + return this.__wrap(node, items, null, null, rootName); + }, + + _wrapMount: function BStore__wrapMount(node, id) { + if (node.type != node.RESULT_TYPE_FOLDER) + throw "Trying to wrap a non-folder mounted share"; + + let GUID = this._bms.getItemGUID(node.itemId); + let ret = {rootGUID: GUID, userid: id, snapshot: {}}; + + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this.__wrap(node.getChild(i), ret.snapshot, GUID, i); + } + + // remove any share mountpoints + for (let guid in ret.snapshot) { + if (ret.snapshot[guid].type == "mounted-share") + delete ret.snapshot[guid]; + } + + return ret; + }, + + _resetGUIDs: function BSS__resetGUIDs(node) { + if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) + this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); + + if (node.type == node.RESULT_TYPE_FOLDER && + !this._ls.isLivemark(node.itemId)) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this._resetGUIDs(node.getChild(i)); + } + } + }, + + findMounts: function BStore_findMounts() { + let ret = []; + let a = this._ans.getItemsWithAnnotation("weave/mounted-share-id", {}); + for (let i = 0; i < a.length; i++) { + let id = this._ans.getItemAnnotation(a[i], "weave/mounted-share-id"); + ret.push(this._wrapMount(this._getNode(a[i]), id)); + } + return ret; + }, + + wrap: function BStore_wrap() { + var items = {}; + this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu"); + this._wrap(this._getNode(this._bms.toolbarFolder), items, "toolbar"); + this._wrap(this._getNode(this._bms.unfiledBookmarksFolder), items, "unfiled"); + return items; + }, + + wipe: function BStore_wipe() { + this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder); + this._bms.removeFolderChildren(this._bms.toolbarFolder); + this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder); + }, + + resetGUIDs: function BStore_resetGUIDs() { + this._resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder)); + this._resetGUIDs(this._getNode(this._bms.toolbarFolder)); + this._resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); + } +}; +BookmarksStore.prototype.__proto__ = new Store(); + +/* + * Tracker objects for each engine may need to subclass the + * getScore routine, which returns the current 'score' for that + * engine. How the engine decides to set the score is upto it, + * as long as the value between 0 and 100 actually corresponds + * to its urgency to sync. + * + * Here's an example BookmarksTracker. We don't subclass getScore + * because the observer methods take care of updating _score which + * getScore returns by default. + */ +function BookmarksTracker() { + this._init(); +} +BookmarksTracker.prototype = { + _logName: "BMTracker", + + /* We don't care about the first three */ + onBeginUpdateBatch: function BMT_onBeginUpdateBatch() { + + }, + onEndUpdateBatch: function BMT_onEndUpdateBatch() { + + }, + onItemVisited: function BMT_onItemVisited() { + + }, + + /* Every add or remove is worth 4 points, + * on the basis that adding or removing 20 bookmarks + * means its time to sync? + */ + onItemAdded: function BMT_onEndUpdateBatch() { + this._score += 4; + }, + onItemRemoved: function BMT_onItemRemoved() { + this._score += 4; + }, + /* Changes are worth 2 points? */ + onItemChanged: function BMT_onItemChanged() { + this._score += 2; + }, + + _init: function BMT__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + + Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService). + addObserver(this, false); + } +} +BookmarksTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 24d336d120f5..0a29f18f819d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -52,6 +52,7 @@ Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines/cookies.js"); +Cu.import("resource://weave/engines/bookmarks.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index f98da7886769..f8c9083988a4 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', 'BookmarksStore', - 'HistoryStore', 'CookieStore', 'PasswordStore', 'FormStore', +const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', + 'HistoryStore', 'PasswordStore', 'FormStore', 'TabStore']; const Cc = Components.classes; @@ -272,419 +272,6 @@ SnapshotStore.prototype = { }; SnapshotStore.prototype.__proto__ = new Store(); -function BookmarksStore() { - this._init(); -} -BookmarksStore.prototype = { - _logName: "BStore", - - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - __hsvc: null, - get _hsvc() { - if (!this.__hsvc) - this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - return this.__hsvc; - }, - - __ls: null, - get _ls() { - if (!this.__ls) - this.__ls = Cc["@mozilla.org/browser/livemark-service;2"]. - getService(Ci.nsILivemarkService); - return this.__ls; - }, - - __ms: null, - get _ms() { - if (!this.__ms) - this.__ms = Cc["@mozilla.org/microsummary/service;1"]. - getService(Ci.nsIMicrosummaryService); - return this.__ms; - }, - - __ts: null, - get _ts() { - if (!this.__ts) - this.__ts = Cc["@mozilla.org/browser/tagging-service;1"]. - getService(Ci.nsITaggingService); - return this.__ts; - }, - - __ans: null, - get _ans() { - if (!this.__ans) - this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - return this.__ans; - }, - - _getItemIdForGUID: function BStore__getItemIdForGUID(GUID) { - switch (GUID) { - case "menu": - return this._bms.bookmarksMenuFolder; - case "toolbar": - return this._bms.toolbarFolder; - case "unfiled": - return this._bms.unfiledBookmarksFolder; - default: - return this._bms.getItemIdForGUID(GUID); - } - return null; - }, - - _createCommand: function BStore__createCommand(command) { - let newId; - let parentId = this._getItemIdForGUID(command.data.parentGUID); - - if (parentId < 0) { - this._log.warn("Creating node with unknown parent -> reparenting to root"); - parentId = this._bms.bookmarksMenuFolder; - } - - switch (command.data.type) { - case "query": - case "bookmark": - case "microsummary": { - this._log.debug(" -> creating bookmark \"" + command.data.title + "\""); - let URI = Utils.makeURI(command.data.URI); - newId = this._bms.insertBookmark(parentId, - URI, - command.data.index, - command.data.title); - this._ts.untagURI(URI, null); - this._ts.tagURI(URI, command.data.tags); - this._bms.setKeywordForBookmark(newId, command.data.keyword); - if (command.data.description) { - this._ans.setItemAnnotation(newId, "bookmarkProperties/description", - command.data.description, 0, - this._ans.EXPIRE_NEVER); - } - - if (command.data.type == "microsummary") { - this._log.debug(" \-> is a microsummary"); - this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", - command.data.staticTitle || "", 0, this._ans.EXPIRE_NEVER); - let genURI = Utils.makeURI(command.data.generatorURI); - try { - let micsum = this._ms.createMicrosummary(URI, genURI); - this._ms.setMicrosummary(newId, micsum); - } - catch(ex) { /* ignore "missing local generator" exceptions */ } - } - } break; - case "folder": - this._log.debug(" -> creating folder \"" + command.data.title + "\""); - newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); - break; - case "livemark": - this._log.debug(" -> creating livemark \"" + command.data.title + "\""); - newId = this._ls.createLivemark(parentId, - command.data.title, - Utils.makeURI(command.data.siteURI), - Utils.makeURI(command.data.feedURI), - command.data.index); - break; - case "mounted-share": - this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\""); - newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); - - this._ans.setItemAnnotation(newId, "weave/mounted-share-id", - command.data.mountId, 0, this._ans.EXPIRE_NEVER); - break; - case "separator": - this._log.debug(" -> creating separator"); - newId = this._bms.insertSeparator(parentId, command.data.index); - break; - default: - this._log.error("_createCommand: Unknown item type: " + command.data.type); - break; - } - if (newId) - this._bms.setItemGUID(newId, command.GUID); - }, - - _removeCommand: function BStore__removeCommand(command) { - if (command.GUID == "menu" || - command.GUID == "toolbar" || - command.GUID == "unfiled") { - this._log.warn("Attempted to remove root node (" + command.GUID + - "). Skipping command."); - return; - } - - var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId < 0) { - this._log.warn("Attempted to remove item " + command.GUID + - ", but it does not exist. Skipping."); - return; - } - var type = this._bms.getItemType(itemId); - - switch (type) { - case this._bms.TYPE_BOOKMARK: - this._log.debug(" -> removing bookmark " + command.GUID); - this._bms.removeItem(itemId); - break; - case this._bms.TYPE_FOLDER: - this._log.debug(" -> removing folder " + command.GUID); - this._bms.removeFolder(itemId); - break; - case this._bms.TYPE_SEPARATOR: - this._log.debug(" -> removing separator " + command.GUID); - this._bms.removeItem(itemId); - break; - default: - this._log.error("removeCommand: Unknown item type: " + type); - break; - } - }, - - _editCommand: function BStore__editCommand(command) { - if (command.GUID == "menu" || - command.GUID == "toolbar" || - command.GUID == "unfiled") { - this._log.warn("Attempted to edit root node (" + command.GUID + - "). Skipping command."); - return; - } - - var itemId = this._bms.getItemIdForGUID(command.GUID); - if (itemId < 0) { - this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); - return; - } - - for (let key in command.data) { - switch (key) { - case "type": - // all commands have this to help in reconciliation, but it makes - // no sense to edit it - break; - case "GUID": - var existing = this._getItemIdForGUID(command.data.GUID); - if (existing < 0) - this._bms.setItemGUID(itemId, command.data.GUID); - else - this._log.warn("Can't change GUID " + command.GUID + - " to " + command.data.GUID + ": GUID already exists."); - break; - case "title": - this._bms.setItemTitle(itemId, command.data.title); - break; - case "URI": - this._bms.changeBookmarkURI(itemId, Utils.makeURI(command.data.URI)); - break; - case "index": - this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), - command.data.index); - break; - case "parentGUID": { - let index = -1; - if (command.data.index && command.data.index >= 0) - index = command.data.index; - this._bms.moveItem( - itemId, this._getItemIdForGUID(command.data.parentGUID), index); - } break; - case "tags": { - let tagsURI = this._bms.getBookmarkURI(itemId); - this._ts.untagURI(tagsURI, null); - this._ts.tagURI(tagsURI, command.data.tags); - } break; - case "keyword": - this._bms.setKeywordForBookmark(itemId, command.data.keyword); - break; - case "description": - if (command.data.description) { - this._ans.setItemAnnotation(itemId, "bookmarkProperties/description", - command.data.description, 0, - this._ans.EXPIRE_NEVER); - } - break; - case "generatorURI": { - let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); - let genURI = Utils.makeURI(command.data.generatorURI); - let micsum = this._ms.createMicrosummary(micsumURI, genURI); - this._ms.setMicrosummary(itemId, micsum); - } break; - case "siteURI": - this._ls.setSiteURI(itemId, Utils.makeURI(command.data.siteURI)); - break; - case "feedURI": - this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI)); - break; - default: - this._log.warn("Can't change item property: " + key); - break; - } - } - }, - - _getNode: function BSS__getNode(folder) { - let query = this._hsvc.getNewQuery(); - query.setFolders([folder], 1); - return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; - }, - - __wrap: function BSS___wrap(node, items, parentGUID, index, guidOverride) { - let GUID, item; - - // we override the guid for the root items, "menu", "toolbar", etc. - if (guidOverride) { - GUID = guidOverride; - item = {}; - } else { - GUID = this._bms.getItemGUID(node.itemId); - item = {parentGUID: parentGUID, index: index}; - } - - if (node.type == node.RESULT_TYPE_FOLDER) { - if (this._ls.isLivemark(node.itemId)) { - item.type = "livemark"; - let siteURI = this._ls.getSiteURI(node.itemId); - let feedURI = this._ls.getFeedURI(node.itemId); - item.siteURI = siteURI? siteURI.spec : ""; - item.feedURI = feedURI? feedURI.spec : ""; - - } else if (this._ans.itemHasAnnotation(node.itemId, - "weave/mounted-share-id")) { - item.type = "mounted-share"; - item.title = node.title; - item.mountId = this._ans.getItemAnnotation(node.itemId, - "weave/mounted-share-id"); - - } else { - item.type = "folder"; - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this.__wrap(node.getChild(i), items, GUID, i); - } - } - if (!guidOverride) - item.title = node.title; // no titles for root nodes - - } else if (node.type == node.RESULT_TYPE_URI || - node.type == node.RESULT_TYPE_QUERY) { - if (this._ms.hasMicrosummary(node.itemId)) { - item.type = "microsummary"; - let micsum = this._ms.getMicrosummary(node.itemId); - item.generatorURI = micsum.generator.uri.spec; // breaks local generators - item.staticTitle = this._ans.getItemAnnotation(node.itemId, "bookmarks/staticTitle"); - } else if (node.type == node.RESULT_TYPE_QUERY) { - item.type = "query"; - item.title = node.title; - } else { - item.type = "bookmark"; - item.title = node.title; - } - - try { - item.description = - this._ans.getItemAnnotation(node.itemId, "bookmarkProperties/description"); - } catch (e) { - item.description = undefined; - } - - item.URI = node.uri; - item.tags = this._ts.getTagsForURI(Utils.makeURI(node.uri), {}); - item.keyword = this._bms.getKeywordForBookmark(node.itemId); - - } else if (node.type == node.RESULT_TYPE_SEPARATOR) { - item.type = "separator"; - - } else { - this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); - return; - } - - items[GUID] = item; - }, - - // helper - _wrap: function BStore__wrap(node, items, rootName) { - return this.__wrap(node, items, null, null, rootName); - }, - - _wrapMount: function BStore__wrapMount(node, id) { - if (node.type != node.RESULT_TYPE_FOLDER) - throw "Trying to wrap a non-folder mounted share"; - - let GUID = this._bms.getItemGUID(node.itemId); - let ret = {rootGUID: GUID, userid: id, snapshot: {}}; - - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this.__wrap(node.getChild(i), ret.snapshot, GUID, i); - } - - // remove any share mountpoints - for (let guid in ret.snapshot) { - if (ret.snapshot[guid].type == "mounted-share") - delete ret.snapshot[guid]; - } - - return ret; - }, - - _resetGUIDs: function BSS__resetGUIDs(node) { - if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) - this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); - - if (node.type == node.RESULT_TYPE_FOLDER && - !this._ls.isLivemark(node.itemId)) { - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._resetGUIDs(node.getChild(i)); - } - } - }, - - findMounts: function BStore_findMounts() { - let ret = []; - let a = this._ans.getItemsWithAnnotation("weave/mounted-share-id", {}); - for (let i = 0; i < a.length; i++) { - let id = this._ans.getItemAnnotation(a[i], "weave/mounted-share-id"); - ret.push(this._wrapMount(this._getNode(a[i]), id)); - } - return ret; - }, - - wrap: function BStore_wrap() { - var items = {}; - this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu"); - this._wrap(this._getNode(this._bms.toolbarFolder), items, "toolbar"); - this._wrap(this._getNode(this._bms.unfiledBookmarksFolder), items, "unfiled"); - return items; - }, - - wipe: function BStore_wipe() { - this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder); - this._bms.removeFolderChildren(this._bms.toolbarFolder); - this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder); - }, - - resetGUIDs: function BStore_resetGUIDs() { - this._resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder)); - this._resetGUIDs(this._getNode(this._bms.toolbarFolder)); - this._resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); - } -}; -BookmarksStore.prototype.__proto__ = new Store(); - function HistoryStore() { this._init(); } @@ -765,191 +352,6 @@ HistoryStore.prototype = { }; HistoryStore.prototype.__proto__ = new Store(); - -function CookieStore( cookieManagerStub ) { - /* If no argument is passed in, this store will query/write to the real - Mozilla cookie manager component. This is the normal way to use this - class in production code. But for unit-testing purposes, you can pass - in a stub object that will be used in place of the cookieManager. */ - this._init(); - this._cookieManagerStub = cookieManagerStub; -} -CookieStore.prototype = { - _logName: "CookieStore", - - - // Documentation of the nsICookie interface says: - // name ACString The name of the cookie. Read only. - // value ACString The cookie value. Read only. - // isDomain boolean True if the cookie is a domain cookie, false otherwise. Read only. - // host AUTF8String The host (possibly fully qualified) of the cookie. Read only. - // path AUTF8String The path pertaining to the cookie. Read only. - // isSecure boolean True if the cookie was transmitted over ssl, false otherwise. Read only. - // expires PRUint64 Expiration time (local timezone) expressed as number of seconds since Jan 1, 1970. Read only. - // status nsCookieStatus Holds the P3P status of cookie. Read only. - // policy nsCookiePolicy Holds the site's compact policy value. Read only. - // nsICookie2 deprecates expires, status, and policy, and adds: - //rawHost AUTF8String The host (possibly fully qualified) of the cookie without a leading dot to represent if it is a domain cookie. Read only. - //isSession boolean True if the cookie is a session cookie. Read only. - //expiry PRInt64 the actual expiry time of the cookie (where 0 does not represent a session cookie). Read only. - //isHttpOnly boolean True if the cookie is an http only cookie. Read only. - - __cookieManager: null, - get _cookieManager() { - if ( this._cookieManagerStub != undefined ) { - return this._cookieManagerStub; - } - // otherwise, use the real one - if (!this.__cookieManager) - this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. - getService(Ci.nsICookieManager2); - // need the 2nd revision of the ICookieManager interface - // because it supports add() and the 1st one doesn't. - return this.__cookieManager - }, - - _createCommand: function CookieStore__createCommand(command) { - /* we got a command to create a cookie in the local browser - in order to sync with the server. */ - - this._log.info("CookieStore got createCommand: " + command ); - // this assumes command.data fits the nsICookie2 interface - if ( !command.data.isSession ) { - // Add only persistent cookies ( not session cookies ) - this._cookieManager.add( command.data.host, - command.data.path, - command.data.name, - command.data.value, - command.data.isSecure, - command.data.isHttpOnly, - command.data.isSession, - command.data.expiry ); - } - }, - - _removeCommand: function CookieStore__removeCommand(command) { - /* we got a command to remove a cookie from the local browser - in order to sync with the server. - command.data appears to be equivalent to what wrap() puts in - the JSON dictionary. */ - - this._log.info("CookieStore got removeCommand: " + command ); - - /* I think it goes like this, according to - http://developer.mozilla.org/en/docs/nsICookieManager - the last argument is "always block cookies from this domain?" - and the answer is "no". */ - this._cookieManager.remove( command.data.host, - command.data.name, - command.data.path, - false ); - }, - - _editCommand: function CookieStore__editCommand(command) { - /* we got a command to change a cookie in the local browser - in order to sync with the server. */ - this._log.info("CookieStore got editCommand: " + command ); - - /* Look up the cookie that matches the one in the command: */ - var iter = this._cookieManager.enumerator; - var matchingCookie = null; - while (iter.hasMoreElements()){ - let cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie ) ){ - // see if host:path:name of cookie matches GUID given in command - let key = cookie.host + ":" + cookie.path + ":" + cookie.name; - if (key == command.GUID) { - matchingCookie = cookie; - break; - } - } - } - // Update values in the cookie: - for (var key in command.data) { - // Whatever values command.data has, use them - matchingCookie[ key ] = command.data[ key ] - } - // Remove the old incorrect cookie from the manager: - this._cookieManager.remove( matchingCookie.host, - matchingCookie.name, - matchingCookie.path, - false ); - - // Re-add the new updated cookie: - if ( !command.data.isSession ) { - /* ignore single-session cookies, add only persistent cookies. */ - this._cookieManager.add( matchingCookie.host, - matchingCookie.path, - matchingCookie.name, - matchingCookie.value, - matchingCookie.isSecure, - matchingCookie.isHttpOnly, - matchingCookie.isSession, - matchingCookie.expiry ); - } - - // Also, there's an exception raised because - // this._data[comand.GUID] is undefined - }, - - wrap: function CookieStore_wrap() { - /* Return contents of this store, as JSON. - A dictionary of cookies where the keys are GUIDs and the - values are sub-dictionaries containing all cookie fields. */ - - let items = {}; - var iter = this._cookieManager.enumerator; - while (iter.hasMoreElements()){ - var cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie )){ - // String used to identify cookies is - // host:path:name - if ( cookie.isSession ) { - /* Skip session-only cookies, sync only persistent cookies. */ - continue; - } - - let key = cookie.host + ":" + cookie.path + ":" + cookie.name; - items[ key ] = { parentGUID: '', - name: cookie.name, - value: cookie.value, - isDomain: cookie.isDomain, - host: cookie.host, - path: cookie.path, - isSecure: cookie.isSecure, - // nsICookie2 values: - rawHost: cookie.rawHost, - isSession: cookie.isSession, - expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly } - - /* See http://developer.mozilla.org/en/docs/nsICookie - Note: not syncing "expires", "status", or "policy" - since they're deprecated. */ - - } - } - return items; - }, - - wipe: function CookieStore_wipe() { - /* Remove everything from the store. Return nothing. - TODO are the semantics of this just wiping out an internal - buffer, or am I supposed to wipe out all cookies from - the browser itself for reals? */ - this._cookieManager.removeAll() - }, - - resetGUIDs: function CookieStore_resetGUIDs() { - /* called in the case where remote/local sync GUIDs do not - match. We do need to override this, but since we're deriving - GUIDs from the cookie data itself and not generating them, - there's basically no way they can get "out of sync" so there's - nothing to do here. */ - } -}; -CookieStore.prototype.__proto__ = new Store(); - function PasswordStore() { this._init(); } diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 9fcbe264bddf..229720898acd 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', 'BookmarksSyncCore', 'HistorySyncCore', - 'CookieSyncCore', 'PasswordSyncCore', 'FormSyncCore', +const EXPORTED_SYMBOLS = ['SyncCore', 'HistorySyncCore', + 'PasswordSyncCore', 'FormSyncCore', 'TabSyncCore']; const Cc = Components.classes; @@ -313,104 +313,6 @@ SyncCore.prototype = { } }; -function BookmarksSyncCore() { - this._init(); -} -BookmarksSyncCore.prototype = { - _logName: "BMSync", - - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - _itemExists: function BSC__itemExists(GUID) { - return this._bms.getItemIdForGUID(GUID) >= 0; - }, - - _getEdits: function BSC__getEdits(a, b) { - // NOTE: we do not increment ret.numProps, as that would cause - // edit commands to always get generated - let ret = SyncCore.prototype._getEdits.call(this, a, b); - ret.props.type = a.type; - return ret; - }, - - // compares properties - // returns true if the property is not set in either object - // returns true if the property is set and equal in both objects - // returns false otherwise - _comp: function BSC__comp(a, b, prop) { - return (!a.data[prop] && !b.data[prop]) || - (a.data[prop] && b.data[prop] && (a.data[prop] == b.data[prop])); - }, - - _commandLike: function BSC__commandLike(a, b) { - // Check that neither command is null, that their actions, types, - // and parents are the same, and that they don't have the same - // GUID. - // * Items with the same GUID do not qualify for 'likeness' because - // we already consider them to be the same object, and therefore - // we need to process any edits. - // * Remove or edit commands don't qualify for likeness either, - // since remove or edit commands with different GUIDs are - // guaranteed to refer to two different items - // * The parent GUID check works because reconcile() fixes up the - // parent GUIDs as it runs, and the command list is sorted by - // depth - if (!a || !b || - a.action != b.action || - a.action != "create" || - a.data.type != b.data.type || - a.data.parentGUID != b.data.parentGUID || - a.GUID == b.GUID) - return false; - - // Bookmarks and folders are allowed to be in a different index as long as - // they are in the same folder. Separators must be at - // the same index to qualify for 'likeness'. - switch (a.data.type) { - case "bookmark": - if (this._comp(a, b, 'URI') && - this._comp(a, b, 'title')) - return true; - return false; - case "query": - if (this._comp(a, b, 'URI') && - this._comp(a, b, 'title')) - return true; - return false; - case "microsummary": - if (this._comp(a, b, 'URI') && - this._comp(a, b, 'generatorURI')) - return true; - return false; - case "folder": - if (this._comp(a, b, 'title')) - return true; - return false; - case "livemark": - if (this._comp(a, b, 'title') && - this._comp(a, b, 'siteURI') && - this._comp(a, b, 'feedURI')) - return true; - return false; - case "separator": - if (this._comp(a, b, 'index')) - return true; - return false; - default: - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this._log.error("commandLike: Unknown item type: " + json.encode(a)); - return false; - } - } -}; -BookmarksSyncCore.prototype.__proto__ = new SyncCore(); - function HistorySyncCore() { this._init(); } @@ -432,76 +334,6 @@ HistorySyncCore.prototype = { }; HistorySyncCore.prototype.__proto__ = new SyncCore(); - -function CookieSyncCore() { - this._init(); -} -CookieSyncCore.prototype = { - _logName: "CookieSync", - - __cookieManager: null, - get _cookieManager() { - if (!this.__cookieManager) - this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. - getService(Ci.nsICookieManager2); - /* need the 2nd revision of the ICookieManager interface - because it supports add() and the 1st one doesn't. */ - return this.__cookieManager; - }, - - - _itemExists: function CSC__itemExists(GUID) { - /* true if a cookie with the given GUID exists. - The GUID that we are passed should correspond to the keys - that we define in the JSON returned by CookieStore.wrap() - That is, it will be a string of the form - "host:path:name". */ - - /* TODO verify that colons can't normally appear in any of - the fields -- if they did it then we can't rely on .split(":") - to parse correctly.*/ - - let cookieArray = GUID.split( ":" ); - let cookieHost = cookieArray[0]; - let cookiePath = cookieArray[1]; - let cookieName = cookieArray[2]; - - /* alternate implementation would be to instantiate a cookie from - cookieHost, cookiePath, and cookieName, then call - cookieManager.cookieExists(). Maybe that would have better - performance? This implementation seems pretty slow.*/ - let enumerator = this._cookieManager.enumerator; - while (enumerator.hasMoreElements()) - { - let aCookie = enumerator.getNext(); - if (aCookie.host == cookieHost && - aCookie.path == cookiePath && - aCookie.name == cookieName ) { - return true; - } - } - return false; - /* Note: We can't just call cookieManager.cookieExists() with a generic - javascript object with .host, .path, and .name attributes attatched. - cookieExists is implemented in C and does a hard static_cast to an - nsCookie object, so duck typing doesn't work (and in fact makes - Firefox hard-crash as the static_cast returns null and is not checked.) - */ - }, - - _commandLike: function CSC_commandLike(a, b) { - /* Method required to be overridden. - a and b each have a .data and a .GUID - If this function returns true, an editCommand will be - generated to try to resolve the thing. - but are a and b objects of the type in the Store or - are they "commands"?? */ - return false; - } -}; -CookieSyncCore.prototype.__proto__ = new SyncCore(); - - function PasswordSyncCore() { this._init(); } diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index abb5b9891db9..108a194a5309 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Tracker', 'BookmarksTracker', 'HistoryTracker', - 'FormsTracker', 'CookieTracker', 'TabTracker']; +const EXPORTED_SYMBOLS = ['Tracker', 'HistoryTracker', + 'FormsTracker', 'TabTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -94,60 +94,6 @@ Tracker.prototype = { } }; -/* - * Tracker objects for each engine may need to subclass the - * getScore routine, which returns the current 'score' for that - * engine. How the engine decides to set the score is upto it, - * as long as the value between 0 and 100 actually corresponds - * to its urgency to sync. - * - * Here's an example BookmarksTracker. We don't subclass getScore - * because the observer methods take care of updating _score which - * getScore returns by default. - */ -function BookmarksTracker() { - this._init(); -} -BookmarksTracker.prototype = { - _logName: "BMTracker", - - /* We don't care about the first three */ - onBeginUpdateBatch: function BMT_onBeginUpdateBatch() { - - }, - onEndUpdateBatch: function BMT_onEndUpdateBatch() { - - }, - onItemVisited: function BMT_onItemVisited() { - - }, - - /* Every add or remove is worth 4 points, - * on the basis that adding or removing 20 bookmarks - * means its time to sync? - */ - onItemAdded: function BMT_onEndUpdateBatch() { - this._score += 4; - }, - onItemRemoved: function BMT_onItemRemoved() { - this._score += 4; - }, - /* Changes are worth 2 points? */ - onItemChanged: function BMT_onItemChanged() { - this._score += 2; - }, - - _init: function BMT__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - - Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService). - addObserver(this, false); - } -} -BookmarksTracker.prototype.__proto__ = new Tracker(); - function HistoryTracker() { this._init(); } @@ -197,41 +143,6 @@ HistoryTracker.prototype = { } HistoryTracker.prototype.__proto__ = new Tracker(); -function CookieTracker() { - this._init(); -} -CookieTracker.prototype = { - _logName: "CookieTracker", - - _init: function CT__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - /* cookieService can't register observers, but what we CAN do is - register a general observer with the global observerService - to watch for the 'cookie-changed' message. */ - let observerService = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - observerService.addObserver( this, 'cookie-changed', false ); - }, - - // implement observe method to satisfy nsIObserver interface - observe: function ( aSubject, aTopic, aData ) { - /* This gets called when any cookie is added, changed, or removed. - aData will contain a string "added", "changed", etc. to tell us which, - but for now we can treat them all the same. aSubject is the new - cookie object itself. */ - var newCookie = aSubject.QueryInterface( Ci.nsICookie2 ); - if ( newCookie ) { - if ( !newCookie.isSession ) { - /* Any modification to a persistent cookie is worth - 10 points out of 100. Ignore session cookies. */ - this._score += 10; - } - } - } -} -CookieTracker.prototype.__proto__ = new Tracker(); - function FormsTracker() { this._init(); } From dc51daff5a5a3bcf1d4b760ce76b68aaf6992dbf Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 13:56:16 -0700 Subject: [PATCH 0280/1860] Moved all history-related functionality into modules/engines/history.js. --- services/sync/modules/engines.js | 32 ---- services/sync/modules/engines/history.js | 193 +++++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 82 +--------- services/sync/modules/syncCores.js | 23 +-- services/sync/modules/trackers.js | 51 +----- 6 files changed, 197 insertions(+), 185 deletions(-) create mode 100644 services/sync/modules/engines/history.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index aaa45771c77c..cb19d93e7999 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,6 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'HistoryEngine', 'PasswordEngine', 'FormEngine', 'TabEngine']; const Cc = Components.classes; @@ -758,37 +757,6 @@ Engine.prototype = { } }; -function HistoryEngine(pbeId) { - this._init(pbeId); -} -HistoryEngine.prototype = { - get name() { return "history"; }, - get logName() { return "HistEngine"; }, - get serverPrefix() { return "user-data/history/"; }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new HistorySyncCore(); - return this.__core; - }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new HistoryStore(); - return this.__store; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new HistoryTracker(); - return this.__tracker; - } -}; -HistoryEngine.prototype.__proto__ = new Engine(); - function PasswordEngine(pbeId) { this._init(pbeId); } diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js new file mode 100644 index 000000000000..ec742427ee51 --- /dev/null +++ b/services/sync/modules/engines/history.js @@ -0,0 +1,193 @@ +const EXPORTED_SYMBOLS = ['HistoryEngine']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); + +function HistoryEngine(pbeId) { + this._init(pbeId); +} +HistoryEngine.prototype = { + get name() { return "history"; }, + get logName() { return "HistEngine"; }, + get serverPrefix() { return "user-data/history/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new HistorySyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new HistoryStore(); + return this.__store; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new HistoryTracker(); + return this.__tracker; + } +}; +HistoryEngine.prototype.__proto__ = new Engine(); + +function HistorySyncCore() { + this._init(); +} +HistorySyncCore.prototype = { + _logName: "HistSync", + + _itemExists: function HSC__itemExists(GUID) { + // we don't care about already-existing items; just try to re-add them + return false; + }, + + _commandLike: function HSC_commandLike(a, b) { + // History commands never qualify for likeness. We will always + // take the union of all client/server items. We use the URL as + // the GUID, so the same sites will map to the same item (same + // GUID), without our intervention. + return false; + } +}; +HistorySyncCore.prototype.__proto__ = new SyncCore(); + +function HistoryStore() { + this._init(); +} +HistoryStore.prototype = { + _logName: "HistStore", + + __hsvc: null, + get _hsvc() { + if (!this.__hsvc) { + this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + this.__hsvc.QueryInterface(Ci.nsIGlobalHistory2); + this.__hsvc.QueryInterface(Ci.nsIBrowserHistory); + } + return this.__hsvc; + }, + + _createCommand: function HistStore__createCommand(command) { + this._log.debug(" -> creating history entry: " + command.GUID); + try { + let uri = Utils.makeURI(command.data.URI); + this._hsvc.addVisit(uri, command.data.time, null, + this._hsvc.TRANSITION_TYPED, false, null); + this._hsvc.setPageTitle(uri, command.data.title); + } catch (e) { + this._log.error("Exception caught: " + (e.message? e.message : e)); + } + }, + + _removeCommand: function HistStore__removeCommand(command) { + this._log.trace(" -> NOT removing history entry: " + command.GUID); + // we can't remove because we only sync the last 1000 items, not + // the whole store. So we don't know if remove commands were + // generated due to the user removing an entry or because it + // dropped past the 1000 item mark. + }, + + _editCommand: function HistStore__editCommand(command) { + this._log.trace(" -> FIXME: NOT editing history entry: " + command.GUID); + // FIXME: implement! + }, + + _historyRoot: function HistStore__historyRoot() { + let query = this._hsvc.getNewQuery(), + options = this._hsvc.getNewQueryOptions(); + + query.minVisits = 1; + options.maxResults = 1000; + options.resultType = options.RESULTS_AS_VISIT; // FULL_VISIT does not work + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.queryType = options.QUERY_TYPE_HISTORY; + + let root = this._hsvc.executeQuery(query, options).root; + root.QueryInterface(Ci.nsINavHistoryQueryResultNode); + return root; + }, + + wrap: function HistStore_wrap() { + let root = this._historyRoot(); + root.containerOpen = true; + let items = {}; + for (let i = 0; i < root.childCount; i++) { + let item = root.getChild(i); + let guid = item.time + ":" + item.uri + items[guid] = {parentGUID: '', + title: item.title, + URI: item.uri, + time: item.time + }; + // FIXME: sync transition type - requires FULL_VISITs + } + return items; + }, + + wipe: function HistStore_wipe() { + this._hsvc.removeAllPages(); + } +}; +HistoryStore.prototype.__proto__ = new Store(); + +function HistoryTracker() { + this._init(); +} +HistoryTracker.prototype = { + _logName: "HistoryTracker", + + /* We don't care about the first four */ + onBeginUpdateBatch: function HT_onBeginUpdateBatch() { + + }, + onEndUpdateBatch: function HT_onEndUpdateBatch() { + + }, + onPageChanged: function HT_onPageChanged() { + + }, + onTitleChanged: function HT_onTitleChanged() { + + }, + + /* Every add or remove is worth 1 point. + * Clearing the whole history is worth 50 points, + * to ensure we're above the cutoff for syncing + * ASAP. + */ + onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { + this._score += 1; + }, + onPageExpired: function HT_onPageExpired(uri, time, entry) { + this._score += 1; + }, + onDeleteURI: function HT_onDeleteURI(uri) { + this._score += 1; + }, + onClearHistory: function HT_onClearHistory() { + this._score += 50; + }, + + _init: function HT__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + + Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService). + addObserver(this, false); + } +} +HistoryTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0a29f18f819d..e277ec9be73b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -53,6 +53,7 @@ Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines/cookies.js"); Cu.import("resource://weave/engines/bookmarks.js"); +Cu.import("resource://weave/engines/history.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index f8c9083988a4..7a74cb918400 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', - 'HistoryStore', 'PasswordStore', 'FormStore', + 'PasswordStore', 'FormStore', 'TabStore']; const Cc = Components.classes; @@ -272,86 +272,6 @@ SnapshotStore.prototype = { }; SnapshotStore.prototype.__proto__ = new Store(); -function HistoryStore() { - this._init(); -} -HistoryStore.prototype = { - _logName: "HistStore", - - __hsvc: null, - get _hsvc() { - if (!this.__hsvc) { - this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - this.__hsvc.QueryInterface(Ci.nsIGlobalHistory2); - this.__hsvc.QueryInterface(Ci.nsIBrowserHistory); - } - return this.__hsvc; - }, - - _createCommand: function HistStore__createCommand(command) { - this._log.debug(" -> creating history entry: " + command.GUID); - try { - let uri = Utils.makeURI(command.data.URI); - this._hsvc.addVisit(uri, command.data.time, null, - this._hsvc.TRANSITION_TYPED, false, null); - this._hsvc.setPageTitle(uri, command.data.title); - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - } - }, - - _removeCommand: function HistStore__removeCommand(command) { - this._log.trace(" -> NOT removing history entry: " + command.GUID); - // we can't remove because we only sync the last 1000 items, not - // the whole store. So we don't know if remove commands were - // generated due to the user removing an entry or because it - // dropped past the 1000 item mark. - }, - - _editCommand: function HistStore__editCommand(command) { - this._log.trace(" -> FIXME: NOT editing history entry: " + command.GUID); - // FIXME: implement! - }, - - _historyRoot: function HistStore__historyRoot() { - let query = this._hsvc.getNewQuery(), - options = this._hsvc.getNewQueryOptions(); - - query.minVisits = 1; - options.maxResults = 1000; - options.resultType = options.RESULTS_AS_VISIT; // FULL_VISIT does not work - options.sortingMode = options.SORT_BY_DATE_DESCENDING; - options.queryType = options.QUERY_TYPE_HISTORY; - - let root = this._hsvc.executeQuery(query, options).root; - root.QueryInterface(Ci.nsINavHistoryQueryResultNode); - return root; - }, - - wrap: function HistStore_wrap() { - let root = this._historyRoot(); - root.containerOpen = true; - let items = {}; - for (let i = 0; i < root.childCount; i++) { - let item = root.getChild(i); - let guid = item.time + ":" + item.uri - items[guid] = {parentGUID: '', - title: item.title, - URI: item.uri, - time: item.time - }; - // FIXME: sync transition type - requires FULL_VISITs - } - return items; - }, - - wipe: function HistStore_wipe() { - this._hsvc.removeAllPages(); - } -}; -HistoryStore.prototype.__proto__ = new Store(); - function PasswordStore() { this._init(); } diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 229720898acd..017cfb1d321e 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', 'HistorySyncCore', +const EXPORTED_SYMBOLS = ['SyncCore', 'PasswordSyncCore', 'FormSyncCore', 'TabSyncCore']; @@ -313,27 +313,6 @@ SyncCore.prototype = { } }; -function HistorySyncCore() { - this._init(); -} -HistorySyncCore.prototype = { - _logName: "HistSync", - - _itemExists: function HSC__itemExists(GUID) { - // we don't care about already-existing items; just try to re-add them - return false; - }, - - _commandLike: function HSC_commandLike(a, b) { - // History commands never qualify for likeness. We will always - // take the union of all client/server items. We use the URL as - // the GUID, so the same sites will map to the same item (same - // GUID), without our intervention. - return false; - } -}; -HistorySyncCore.prototype.__proto__ = new SyncCore(); - function PasswordSyncCore() { this._init(); } diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 108a194a5309..6a78a6fa93ba 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Tracker', 'HistoryTracker', +const EXPORTED_SYMBOLS = ['Tracker', 'FormsTracker', 'TabTracker']; const Cc = Components.classes; @@ -94,55 +94,6 @@ Tracker.prototype = { } }; -function HistoryTracker() { - this._init(); -} -HistoryTracker.prototype = { - _logName: "HistoryTracker", - - /* We don't care about the first four */ - onBeginUpdateBatch: function HT_onBeginUpdateBatch() { - - }, - onEndUpdateBatch: function HT_onEndUpdateBatch() { - - }, - onPageChanged: function HT_onPageChanged() { - - }, - onTitleChanged: function HT_onTitleChanged() { - - }, - - /* Every add or remove is worth 1 point. - * Clearing the whole history is worth 50 points, - * to ensure we're above the cutoff for syncing - * ASAP. - */ - onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { - this._score += 1; - }, - onPageExpired: function HT_onPageExpired(uri, time, entry) { - this._score += 1; - }, - onDeleteURI: function HT_onDeleteURI(uri) { - this._score += 1; - }, - onClearHistory: function HT_onClearHistory() { - this._score += 50; - }, - - _init: function HT__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - - Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService). - addObserver(this, false); - } -} -HistoryTracker.prototype.__proto__ = new Tracker(); - function FormsTracker() { this._init(); } From a4445f2a5e63f1a873de996902f8ac3f60685f7c Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 14:08:53 -0700 Subject: [PATCH 0281/1860] Moved all password-syncing code into modules/engines/passwords.js. --- services/sync/modules/engines.js | 51 +----- services/sync/modules/engines/passwords.js | 187 +++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 92 +--------- services/sync/modules/syncCores.js | 39 +---- 5 files changed, 191 insertions(+), 179 deletions(-) create mode 100644 services/sync/modules/engines/passwords.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cb19d93e7999..9eb059758ad6 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'PasswordEngine', 'FormEngine', 'TabEngine']; + 'FormEngine', 'TabEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -757,55 +757,6 @@ Engine.prototype = { } }; -function PasswordEngine(pbeId) { - this._init(pbeId); -} -PasswordEngine.prototype = { - get name() { return "passwords"; }, - get logName() { return "PasswordEngine"; }, - get serverPrefix() { return "user-data/passwords/"; }, - - __core: null, - get _core() { - if (!this.__core) { - this.__core = new PasswordSyncCore(); - this.__core._hashLoginInfo = this._hashLoginInfo; - } - return this.__core; - }, - - __store: null, - get _store() { - if (!this.__store) { - this.__store = new PasswordStore(); - this.__store._hashLoginInfo = this._hashLoginInfo; - } - return this.__store; - }, - - /* - * _hashLoginInfo - * - * nsILoginInfo objects don't have a unique GUID, so we need to generate one - * on the fly. This is done by taking a hash of every field in the object. - * Note that the resulting GUID could potentiually reveal passwords via - * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by - * anyone, so this should generally be safe. - */ - _hashLoginInfo : function (aLogin) { - var loginKey = aLogin.hostname + ":" + - aLogin.formSubmitURL + ":" + - aLogin.httpRealm + ":" + - aLogin.username + ":" + - aLogin.password + ":" + - aLogin.usernameField + ":" + - aLogin.passwordField; - - return Utils.sha1(loginKey); - } -}; -PasswordEngine.prototype.__proto__ = new Engine(); - function FormEngine(pbeId) { this._init(pbeId); } diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js new file mode 100644 index 000000000000..572ddfaad6e2 --- /dev/null +++ b/services/sync/modules/engines/passwords.js @@ -0,0 +1,187 @@ +const EXPORTED_SYMBOLS = ['PasswordEngine']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/stores.js"); + +function PasswordEngine(pbeId) { + this._init(pbeId); +} +PasswordEngine.prototype = { + get name() { return "passwords"; }, + get logName() { return "PasswordEngine"; }, + get serverPrefix() { return "user-data/passwords/"; }, + + __core: null, + get _core() { + if (!this.__core) { + this.__core = new PasswordSyncCore(); + this.__core._hashLoginInfo = this._hashLoginInfo; + } + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) { + this.__store = new PasswordStore(); + this.__store._hashLoginInfo = this._hashLoginInfo; + } + return this.__store; + }, + + /* + * _hashLoginInfo + * + * nsILoginInfo objects don't have a unique GUID, so we need to generate one + * on the fly. This is done by taking a hash of every field in the object. + * Note that the resulting GUID could potentiually reveal passwords via + * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by + * anyone, so this should generally be safe. + */ + _hashLoginInfo : function (aLogin) { + var loginKey = aLogin.hostname + ":" + + aLogin.formSubmitURL + ":" + + aLogin.httpRealm + ":" + + aLogin.username + ":" + + aLogin.password + ":" + + aLogin.usernameField + ":" + + aLogin.passwordField; + + return Utils.sha1(loginKey); + } +}; +PasswordEngine.prototype.__proto__ = new Engine(); + +function PasswordSyncCore() { + this._init(); +} +PasswordSyncCore.prototype = { + _logName: "PasswordSync", + + __loginManager : null, + get _loginManager() { + if (!this.__loginManager) + this.__loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + return this.__loginManager; + }, + + _itemExists: function PSC__itemExists(GUID) { + var found = false; + var logins = this._loginManager.getAllLogins({}); + + // XXX It would be more efficient to compute all the hashes in one shot, + // cache the results, and check the cache here. That would need to happen + // once per sync -- not sure how to invalidate cache after current sync? + for (var i = 0; i < logins.length && !found; i++) { + var hash = this._hashLoginInfo(logins[i]); + if (hash == GUID) + found = true;; + } + + return found; + }, + + _commandLike: function PSC_commandLike(a, b) { + // Not used. + return false; + } +}; +PasswordSyncCore.prototype.__proto__ = new SyncCore(); + +function PasswordStore() { + this._init(); +} +PasswordStore.prototype = { + _logName: "PasswordStore", + + __loginManager : null, + get _loginManager() { + if (!this.__loginManager) + this.__loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + return this.__loginManager; + }, + + __nsLoginInfo : null, + get _nsLoginInfo() { + if (!this.__nsLoginInfo) + this.__nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + return this.__nsLoginInfo; + }, + + + _createCommand: function PasswordStore__createCommand(command) { + this._log.info("PasswordStore got createCommand: " + command ); + + var login = new this._nsLoginInfo(command.data.hostname, + command.data.formSubmitURL, + command.data.httpRealm, + command.data.username, + command.data.password, + command.data.usernameField, + command.data.passwordField); + + this._loginManager.addLogin(login); + }, + + _removeCommand: function PasswordStore__removeCommand(command) { + this._log.info("PasswordStore got removeCommand: " + command ); + + var login = new this._nsLoginInfo(command.data.hostname, + command.data.formSubmitURL, + command.data.httpRealm, + command.data.username, + command.data.password, + command.data.usernameField, + command.data.passwordField); + + this._loginManager.removeLogin(login); + }, + + _editCommand: function PasswordStore__editCommand(command) { + this._log.info("PasswordStore got editCommand: " + command ); + throw "Password syncs are expected to only be create/remove!"; + }, + + wrap: function PasswordStore_wrap() { + /* Return contents of this store, as JSON. */ + var items = {}; + + var logins = this._loginManager.getAllLogins({}); + + for (var i = 0; i < logins.length; i++) { + var login = logins[i]; + + var key = this._hashLoginInfo(login); + + items[key] = { hostname : login.hostname, + formSubmitURL : login.formSubmitURL, + httpRealm : login.httpRealm, + username : login.username, + password : login.password, + usernameField : login.usernameField, + passwordField : login.passwordField }; + } + + return items; + }, + + wipe: function PasswordStore_wipe() { + this._loginManager.removeAllLogins(); + }, + + resetGUIDs: function PasswordStore_resetGUIDs() { + // Not needed. + } +}; +PasswordStore.prototype.__proto__ = new Store(); + diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e277ec9be73b..a26a0600e585 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -54,6 +54,7 @@ Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines/cookies.js"); Cu.import("resource://weave/engines/bookmarks.js"); Cu.import("resource://weave/engines/history.js"); +Cu.import("resource://weave/engines/passwords.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 7a74cb918400..66638bd2c10e 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', - 'PasswordStore', 'FormStore', + 'FormStore', 'TabStore']; const Cc = Components.classes; @@ -272,96 +272,6 @@ SnapshotStore.prototype = { }; SnapshotStore.prototype.__proto__ = new Store(); -function PasswordStore() { - this._init(); -} -PasswordStore.prototype = { - _logName: "PasswordStore", - - __loginManager : null, - get _loginManager() { - if (!this.__loginManager) - this.__loginManager = Cc["@mozilla.org/login-manager;1"]. - getService(Ci.nsILoginManager); - return this.__loginManager; - }, - - __nsLoginInfo : null, - get _nsLoginInfo() { - if (!this.__nsLoginInfo) - this.__nsLoginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", - Ci.nsILoginInfo, "init"); - return this.__nsLoginInfo; - }, - - - _createCommand: function PasswordStore__createCommand(command) { - this._log.info("PasswordStore got createCommand: " + command ); - - var login = new this._nsLoginInfo(command.data.hostname, - command.data.formSubmitURL, - command.data.httpRealm, - command.data.username, - command.data.password, - command.data.usernameField, - command.data.passwordField); - - this._loginManager.addLogin(login); - }, - - _removeCommand: function PasswordStore__removeCommand(command) { - this._log.info("PasswordStore got removeCommand: " + command ); - - var login = new this._nsLoginInfo(command.data.hostname, - command.data.formSubmitURL, - command.data.httpRealm, - command.data.username, - command.data.password, - command.data.usernameField, - command.data.passwordField); - - this._loginManager.removeLogin(login); - }, - - _editCommand: function PasswordStore__editCommand(command) { - this._log.info("PasswordStore got editCommand: " + command ); - throw "Password syncs are expected to only be create/remove!"; - }, - - wrap: function PasswordStore_wrap() { - /* Return contents of this store, as JSON. */ - var items = {}; - - var logins = this._loginManager.getAllLogins({}); - - for (var i = 0; i < logins.length; i++) { - var login = logins[i]; - - var key = this._hashLoginInfo(login); - - items[key] = { hostname : login.hostname, - formSubmitURL : login.formSubmitURL, - httpRealm : login.httpRealm, - username : login.username, - password : login.password, - usernameField : login.usernameField, - passwordField : login.passwordField }; - } - - return items; - }, - - wipe: function PasswordStore_wipe() { - this._loginManager.removeAllLogins(); - }, - - resetGUIDs: function PasswordStore_resetGUIDs() { - // Not needed. - } -}; -PasswordStore.prototype.__proto__ = new Store(); - function FormStore() { this._init(); } diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 017cfb1d321e..c1825ec5451f 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', - 'PasswordSyncCore', 'FormSyncCore', + 'FormSyncCore', 'TabSyncCore']; const Cc = Components.classes; @@ -313,43 +313,6 @@ SyncCore.prototype = { } }; -function PasswordSyncCore() { - this._init(); -} -PasswordSyncCore.prototype = { - _logName: "PasswordSync", - - __loginManager : null, - get _loginManager() { - if (!this.__loginManager) - this.__loginManager = Cc["@mozilla.org/login-manager;1"]. - getService(Ci.nsILoginManager); - return this.__loginManager; - }, - - _itemExists: function PSC__itemExists(GUID) { - var found = false; - var logins = this._loginManager.getAllLogins({}); - - // XXX It would be more efficient to compute all the hashes in one shot, - // cache the results, and check the cache here. That would need to happen - // once per sync -- not sure how to invalidate cache after current sync? - for (var i = 0; i < logins.length && !found; i++) { - var hash = this._hashLoginInfo(logins[i]); - if (hash == GUID) - found = true;; - } - - return found; - }, - - _commandLike: function PSC_commandLike(a, b) { - // Not used. - return false; - } -}; -PasswordSyncCore.prototype.__proto__ = new SyncCore(); - function FormSyncCore() { this._init(); } From effc9c604e13c28bb154b2a6571b7cc903afc949 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 14:20:51 -0700 Subject: [PATCH 0282/1860] Moved all form-syncing code into modules/engines/forms.js. --- services/sync/modules/engines.js | 33 +--- services/sync/modules/engines/forms.js | 225 +++++++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 69 -------- services/sync/modules/syncCores.js | 46 ----- services/sync/modules/trackers.js | 71 +------- 6 files changed, 228 insertions(+), 217 deletions(-) create mode 100644 services/sync/modules/engines/forms.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9eb059758ad6..663aa36c0ee9 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -36,7 +36,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'FormEngine', 'TabEngine']; + 'TabEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -757,37 +757,6 @@ Engine.prototype = { } }; -function FormEngine(pbeId) { - this._init(pbeId); -} -FormEngine.prototype = { - get name() { return "forms"; }, - get logName() { return "FormEngine"; }, - get serverPrefix() { return "user-data/forms/"; }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new FormSyncCore(); - return this.__core; - }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new FormStore(); - return this.__store; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new FormsTracker(); - return this.__tracker; - } -}; -FormEngine.prototype.__proto__ = new Engine(); - function TabEngine(pbeId) { this._init(pbeId); } diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js new file mode 100644 index 000000000000..24411de1ba8c --- /dev/null +++ b/services/sync/modules/engines/forms.js @@ -0,0 +1,225 @@ +const EXPORTED_SYMBOLS = ['FormEngine']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); + +function FormEngine(pbeId) { + this._init(pbeId); +} +FormEngine.prototype = { + get name() { return "forms"; }, + get logName() { return "FormEngine"; }, + get serverPrefix() { return "user-data/forms/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new FormSyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new FormStore(); + return this.__store; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new FormsTracker(); + return this.__tracker; + } +}; +FormEngine.prototype.__proto__ = new Engine(); + +function FormSyncCore() { + this._init(); +} +FormSyncCore.prototype = { + _logName: "FormSync", + + __formDB: null, + get _formDB() { + if (!this.__formDB) { + var file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("formhistory.sqlite"); + var stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__formDB = stor.openDatabase(file); + } + return this.__formDB; + }, + + _itemExists: function FSC__itemExists(GUID) { + var found = false; + var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); + + /* Same performance restrictions as PasswordSyncCore apply here: + caching required */ + while (stmnt.executeStep()) { + var nam = stmnt.getUTF8String(1); + var val = stmnt.getUTF8String(2); + var key = Utils.sha1(nam + val); + + if (key == GUID) + found = true; + } + + return found; + }, + + _commandLike: function FSC_commandLike(a, b) { + /* Not required as GUIDs for similar data sets will be the same */ + return false; + } +}; +FormSyncCore.prototype.__proto__ = new SyncCore(); + +function FormStore() { + this._init(); +} +FormStore.prototype = { + _logName: "FormStore", + + __formDB: null, + get _formDB() { + if (!this.__formDB) { + var file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("formhistory.sqlite"); + var stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__formDB = stor.openDatabase(file); + } + return this.__formDB; + }, + + __formHistory: null, + get _formHistory() { + if (!this.__formHistory) + this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"]. + getService(Ci.nsIFormHistory2); + return this.__formHistory; + }, + + _createCommand: function FormStore__createCommand(command) { + this._log.info("FormStore got createCommand: " + command ); + this._formHistory.addEntry(command.data.name, command.data.value); + }, + + _removeCommand: function FormStore__removeCommand(command) { + this._log.info("FormStore got removeCommand: " + command ); + this._formHistory.removeEntry(command.data.name, command.data.value); + }, + + _editCommand: function FormStore__editCommand(command) { + this._log.info("FormStore got editCommand: " + command ); + this._log.warn("Form syncs are expected to only be create/remove!"); + }, + + wrap: function FormStore_wrap() { + var items = []; + var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); + + while (stmnt.executeStep()) { + var nam = stmnt.getUTF8String(1); + var val = stmnt.getUTF8String(2); + var key = Utils.sha1(nam + val); + + items[key] = { name: nam, value: val }; + } + + return items; + }, + + wipe: function FormStore_wipe() { + this._formHistory.removeAllEntries(); + }, + + resetGUIDs: function FormStore_resetGUIDs() { + // Not needed. + } +}; +FormStore.prototype.__proto__ = new Store(); + +function FormsTracker() { + this._init(); +} +FormsTracker.prototype = { + _logName: "FormsTracker", + + __formDB: null, + get _formDB() { + if (!this.__formDB) { + var file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("formhistory.sqlite"); + var stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__formDB = stor.openDatabase(file); + } + + return this.__formDB; + }, + + /* nsIFormSubmitObserver is not available in JS. + * To calculate scores, we instead just count the changes in + * the database since the last time we were asked. + * + * FIXME!: Buggy, because changes in a row doesn't result in + * an increment of our score. A possible fix is to do a + * SELECT for each fieldname and compare those instead of the + * whole row count. + * + * Each change is worth 2 points. At some point, we may + * want to differentiate between search-history rows and other + * form items, and assign different scores. + */ + _rowCount: 0, + get score() { + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + var count = stmnt.getInt32(0); + stmnt.reset(); + + this._score = Math.abs(this._rowCount - count) * 2; + + if (this._score >= 100) + return 100; + else + return this._score; + }, + + resetScore: function FormsTracker_resetScore() { + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); + stmnt.reset(); + this._score = 0; + }, + + _init: function FormsTracker__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + + var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); + stmnt.reset(); + } +} +FormsTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a26a0600e585..e80ed2eb06a2 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -55,6 +55,7 @@ Cu.import("resource://weave/engines/cookies.js"); Cu.import("resource://weave/engines/bookmarks.js"); Cu.import("resource://weave/engines/history.js"); Cu.import("resource://weave/engines/passwords.js"); +Cu.import("resource://weave/engines/forms.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 66638bd2c10e..290eb3bfa17c 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -35,7 +35,6 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', - 'FormStore', 'TabStore']; const Cc = Components.classes; @@ -272,74 +271,6 @@ SnapshotStore.prototype = { }; SnapshotStore.prototype.__proto__ = new Store(); -function FormStore() { - this._init(); -} -FormStore.prototype = { - _logName: "FormStore", - - __formDB: null, - get _formDB() { - if (!this.__formDB) { - var file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("formhistory.sqlite"); - var stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__formDB = stor.openDatabase(file); - } - return this.__formDB; - }, - - __formHistory: null, - get _formHistory() { - if (!this.__formHistory) - this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"]. - getService(Ci.nsIFormHistory2); - return this.__formHistory; - }, - - _createCommand: function FormStore__createCommand(command) { - this._log.info("FormStore got createCommand: " + command ); - this._formHistory.addEntry(command.data.name, command.data.value); - }, - - _removeCommand: function FormStore__removeCommand(command) { - this._log.info("FormStore got removeCommand: " + command ); - this._formHistory.removeEntry(command.data.name, command.data.value); - }, - - _editCommand: function FormStore__editCommand(command) { - this._log.info("FormStore got editCommand: " + command ); - this._log.warn("Form syncs are expected to only be create/remove!"); - }, - - wrap: function FormStore_wrap() { - var items = []; - var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); - - while (stmnt.executeStep()) { - var nam = stmnt.getUTF8String(1); - var val = stmnt.getUTF8String(2); - var key = Utils.sha1(nam + val); - - items[key] = { name: nam, value: val }; - } - - return items; - }, - - wipe: function FormStore_wipe() { - this._formHistory.removeAllEntries(); - }, - - resetGUIDs: function FormStore_resetGUIDs() { - // Not needed. - } -}; -FormStore.prototype.__proto__ = new Store(); - function TabStore() { this._virtualTabs = {}; this._init(); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index c1825ec5451f..49156d28dc35 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -35,7 +35,6 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['SyncCore', - 'FormSyncCore', 'TabSyncCore']; const Cc = Components.classes; @@ -313,51 +312,6 @@ SyncCore.prototype = { } }; -function FormSyncCore() { - this._init(); -} -FormSyncCore.prototype = { - _logName: "FormSync", - - __formDB: null, - get _formDB() { - if (!this.__formDB) { - var file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("formhistory.sqlite"); - var stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__formDB = stor.openDatabase(file); - } - return this.__formDB; - }, - - _itemExists: function FSC__itemExists(GUID) { - var found = false; - var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); - - /* Same performance restrictions as PasswordSyncCore apply here: - caching required */ - while (stmnt.executeStep()) { - var nam = stmnt.getUTF8String(1); - var val = stmnt.getUTF8String(2); - var key = Utils.sha1(nam + val); - - if (key == GUID) - found = true; - } - - return found; - }, - - _commandLike: function FSC_commandLike(a, b) { - /* Not required as GUIDs for similar data sets will be the same */ - return false; - } -}; -FormSyncCore.prototype.__proto__ = new SyncCore(); - function TabSyncCore(engine) { this._engine = engine; this._init(); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 6a78a6fa93ba..c55f9eb2f6a4 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Tracker', - 'FormsTracker', 'TabTracker']; + 'TabTracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -94,75 +94,6 @@ Tracker.prototype = { } }; -function FormsTracker() { - this._init(); -} -FormsTracker.prototype = { - _logName: "FormsTracker", - - __formDB: null, - get _formDB() { - if (!this.__formDB) { - var file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("formhistory.sqlite"); - var stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__formDB = stor.openDatabase(file); - } - - return this.__formDB; - }, - - /* nsIFormSubmitObserver is not available in JS. - * To calculate scores, we instead just count the changes in - * the database since the last time we were asked. - * - * FIXME!: Buggy, because changes in a row doesn't result in - * an increment of our score. A possible fix is to do a - * SELECT for each fieldname and compare those instead of the - * whole row count. - * - * Each change is worth 2 points. At some point, we may - * want to differentiate between search-history rows and other - * form items, and assign different scores. - */ - _rowCount: 0, - get score() { - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); - stmnt.executeStep(); - var count = stmnt.getInt32(0); - stmnt.reset(); - - this._score = Math.abs(this._rowCount - count) * 2; - - if (this._score >= 100) - return 100; - else - return this._score; - }, - - resetScore: function FormsTracker_resetScore() { - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); - stmnt.executeStep(); - this._rowCount = stmnt.getInt32(0); - stmnt.reset(); - this._score = 0; - }, - - _init: function FormsTracker__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); - stmnt.executeStep(); - this._rowCount = stmnt.getInt32(0); - stmnt.reset(); - } -} -FormsTracker.prototype.__proto__ = new Tracker(); - function TabTracker(engine) { this._engine = engine; this._init(); From 838067045017a96668ae5a7dea82c5ddc5cfec90 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 14:45:53 -0700 Subject: [PATCH 0283/1860] Moved all tab-syncing code to modules/engines/tabsjs. --- services/sync/modules/engines.js | 35 +- services/sync/modules/engines/tabs.js | 475 ++++++++++++++++++++++++++ services/sync/modules/service.js | 1 + services/sync/modules/stores.js | 295 +--------------- services/sync/modules/syncCores.js | 48 +-- services/sync/modules/trackers.js | 96 +----- 6 files changed, 482 insertions(+), 468 deletions(-) create mode 100644 services/sync/modules/engines/tabs.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 663aa36c0ee9..05639e36032d 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,8 +35,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Engines', 'Engine', - 'TabEngine']; +const EXPORTED_SYMBOLS = ['Engines', + 'Engine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -756,34 +756,3 @@ Engine.prototype = { this._notify("reset-client", this._resetClient).async(this, onComplete); } }; - -function TabEngine(pbeId) { - this._init(pbeId); -} -TabEngine.prototype = { - __proto__: new Engine(), - - get name() "tabs", - get logName() "TabEngine", - get serverPrefix() "user-data/tabs/", - get store() this._store, - - get _core() { - let core = new TabSyncCore(this); - this.__defineGetter__("_core", function() core); - return this._core; - }, - - get _store() { - let store = new TabStore(); - this.__defineGetter__("_store", function() store); - return this._store; - }, - - get _tracker() { - let tracker = new TabTracker(this); - this.__defineGetter__("_tracker", function() tracker); - return this._tracker; - } - -}; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js new file mode 100644 index 000000000000..bb53a191793f --- /dev/null +++ b/services/sync/modules/engines/tabs.js @@ -0,0 +1,475 @@ +const EXPORTED_SYMBOLS = ['TabEngine']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); + +Function.prototype.async = Async.sugar; + +function TabEngine(pbeId) { + this._init(pbeId); +} + +TabEngine.prototype = { + __proto__: new Engine(), + + get name() "tabs", + get logName() "TabEngine", + get serverPrefix() "user-data/tabs/", + get store() this._store, + + get _core() { + let core = new TabSyncCore(this); + this.__defineGetter__("_core", function() core); + return this._core; + }, + + get _store() { + let store = new TabStore(); + this.__defineGetter__("_store", function() store); + return this._store; + }, + + get _tracker() { + let tracker = new TabTracker(this); + this.__defineGetter__("_tracker", function() tracker); + return this._tracker; + } + +}; + +function TabSyncCore(engine) { + this._engine = engine; + this._init(); +} +TabSyncCore.prototype = { + __proto__: new SyncCore(), + + _logName: "TabSync", + + _engine: null, + + get _sessionStore() { + let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + this.__defineGetter__("_sessionStore", function() sessionStore); + return this._sessionStore; + }, + + _itemExists: function TSC__itemExists(GUID) { + // Note: this method returns true if the tab exists in any window, not just + // the window from which the tab came. In the future, if we care about + // windows, we might need to make this more specific, although in that case + // we'll have to identify tabs by something other than URL, since even + // window-specific tabs look the same when identified by URL. + + // Get the set of all real and virtual tabs. + let tabs = this._engine.store.wrap(); + + // XXX Should we convert both to nsIURIs and then use nsIURI::equals + // to compare them? + if (GUID in tabs) { + this._log.debug("_itemExists: " + GUID + " exists"); + return true; + } + + this._log.debug("_itemExists: " + GUID + " doesn't exist"); + return false; + }, + + _commandLike: function TSC_commandLike(a, b) { + // Not implemented. + return false; + } +}; + +function TabStore() { + this._virtualTabs = {}; + this._init(); +} +TabStore.prototype = { + __proto__: new Store(), + + _logName: "TabStore", + + get _sessionStore() { + let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + this.__defineGetter__("_sessionStore", function() sessionStore); + return this._sessionStore; + }, + + get _windowMediator() { + let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + this.__defineGetter__("_windowMediator", function() windowMediator); + return this._windowMediator; + }, + + get _os() { + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + this.__defineGetter__("_os", function() os); + return this._os; + }, + + get _dirSvc() { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + this.__defineGetter__("_dirSvc", function() dirSvc); + return this._dirSvc; + }, + + /** + * A cache of "virtual" tabs from other devices synced to the server + * that the user hasn't opened locally. Unlike other stores, we don't + * immediately apply create commands, which would be jarring to users. + * Instead, we store them in this cache and prompt the user to pick + * which ones she wants to open. + * + * We also persist this cache on disk and include it in the list of tabs + * we generate in this.wrap to reduce ping-pong updates between clients + * running simultaneously and to maintain a consistent state across restarts. + */ + _virtualTabs: null, + + get virtualTabs() { + // Make sure the list of virtual tabs is completely up-to-date (the user + // might have independently opened some of these virtual tabs since the last + // time we synced). + let realTabs = this._wrapRealTabs(); + let virtualTabsChanged = false; + for (let id in this._virtualTabs) { + if (id in realTabs) { + this._log.warn("get virtualTabs: both real and virtual tabs exist for " + + id + "; removing virtual one"); + delete this._virtualTabs[id]; + virtualTabsChanged = true; + } + } + if (virtualTabsChanged) + this._saveVirtualTabs(); + + return this._virtualTabs; + }, + + set virtualTabs(newValue) { + this._virtualTabs = newValue; + this._saveVirtualTabs(); + }, + + // The file in which we store the state of virtual tabs. + get _file() { + let file = this._dirSvc.get("ProfD", Ci.nsILocalFile); + file.append("weave"); + file.append("store"); + file.append("tabs"); + file.append("virtual.json"); + this.__defineGetter__("_file", function() file); + return this._file; + }, + + _saveVirtualTabs: function TabStore__saveVirtualTabs() { + try { + if (!this._file.exists()) + this._file.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); + let out = this._json.encode(this._virtualTabs); + let [fos] = Utils.open(this._file, ">"); + fos.writeString(out); + fos.close(); + } + catch(ex) { + this._log.warn("could not serialize virtual tabs to disk: " + ex); + } + }, + + _restoreVirtualTabs: function TabStore__restoreVirtualTabs() { + try { + if (this._file.exists()) { + let [is] = Utils.open(this._file, "<"); + let json = Utils.readStream(is); + is.close(); + this._virtualTabs = this._json.decode(json); + } + } + catch (ex) { + this._log.warn("could not parse virtual tabs from disk: " + ex); + } + }, + + _init: function TabStore__init() { + this._restoreVirtualTabs(); + + this.__proto__.__proto__._init(); + }, + + /** + * Apply commands generated by a diff during a sync operation. This method + * overrides the one in its superclass so it can save a copy of the latest set + * of virtual tabs to disk so they can be restored on startup. + */ + applyCommands: function TabStore_applyCommands(commandList) { + let self = yield; + + this.__proto__.__proto__.applyCommands.async(this, self.cb, commandList); + yield; + + this._saveVirtualTabs(); + + self.done(); + }, + + _createCommand: function TabStore__createCommand(command) { + this._log.debug("_createCommand: " + command.GUID); + + if (command.GUID in this._virtualTabs || command.GUID in this._wrapRealTabs()) + throw "trying to create a tab that already exists; id: " + command.GUID; + + // Cache the tab and notify the UI to prompt the user to open it. + this._virtualTabs[command.GUID] = command.data; + this._os.notifyObservers(null, "weave:store:tabs:virtual:created", null); + }, + + _removeCommand: function TabStore__removeCommand(command) { + this._log.debug("_removeCommand: " + command.GUID); + + // If this is a virtual tab, it's ok to remove it, since it was never really + // added to this session in the first place. But we don't remove it if it's + // a real tab, since that would be unexpected, unpleasant, and unwanted. + if (command.GUID in this._virtualTabs) { + delete this._virtualTabs[command.GUID]; + this._os.notifyObservers(null, "weave:store:tabs:virtual:removed", null); + } + }, + + _editCommand: function TabStore__editCommand(command) { + this._log.debug("_editCommand: " + command.GUID); + + // We don't edit real tabs, because that isn't what the user would expect, + // but it's ok to edit virtual tabs, so that if users do open them, they get + // the most up-to-date version of them (and also to reduce sync churn). + + if (this._virtualTabs[command.GUID]) + this._virtualTabs[command.GUID] = command.data; + }, + + /** + * Serialize the current state of tabs. + * + * Note: the state includes both tabs on this device and those on others. + * We get the former from the session store. The latter we retrieved from + * the Weave server and stored in this._virtualTabs. Including virtual tabs + * in the serialized state prevents ping-pong deletes between two clients + * running at the same time. + */ + wrap: function TabStore_wrap() { + let items; + + let virtualTabs = this._wrapVirtualTabs(); + let realTabs = this._wrapRealTabs(); + + // Real tabs override virtual ones, which means ping-pong edits when two + // clients have the same URL loaded with different history/attributes. + // We could fix that by overriding real tabs with virtual ones, but then + // we'd have stale tab metadata in same cases. + items = virtualTabs; + let virtualTabsChanged = false; + for (let id in realTabs) { + // Since virtual tabs can sometimes get out of sync with real tabs + // (the user could have independently opened a new tab that exists + // in the virtual tabs cache since the last time we updated the cache), + // we sync them up in the process of merging them here. + if (this._virtualTabs[id]) { + this._log.warn("wrap: both real and virtual tabs exist for " + id + + "; removing virtual one"); + delete this._virtualTabs[id]; + virtualTabsChanged = true; + } + + items[id] = realTabs[id]; + } + if (virtualTabsChanged) + this._saveVirtualTabs(); + + return items; + }, + + _wrapVirtualTabs: function TabStore__wrapVirtualTabs() { + let items = {}; + + for (let id in this._virtualTabs) { + let virtualTab = this._virtualTabs[id]; + + // Copy the virtual tab without private properties (those that begin + // with an underscore character) so that we don't sync data private to + // this particular Weave client (like the _disposed flag). + let item = {}; + for (let property in virtualTab) + if (property[0] != "_") + item[property] = virtualTab[property]; + + items[id] = item; + } + + return items; + }, + + _wrapRealTabs: function TabStore__wrapRealTabs() { + let items = {}; + + let session = this._json.decode(this._sessionStore.getBrowserState()); + + for (let i = 0; i < session.windows.length; i++) { + let window = session.windows[i]; + // For some reason, session store uses one-based array index references, + // (f.e. in the "selectedWindow" and each tab's "index" properties), so we + // convert them to and from JavaScript's zero-based indexes as needed. + let windowID = i + 1; + this._log.debug("_wrapRealTabs: window " + windowID); + for (let j = 0; j < window.tabs.length; j++) { + let tab = window.tabs[j]; + + // The session history entry for the page currently loaded in the tab. + // We use the URL of the current page as the ID for the tab. + let currentEntry = tab.entries[tab.index - 1]; + + if (!currentEntry || !currentEntry.url) { + this._log.warn("_wrapRealTabs: no current entry or no URL, can't " + + "identify " + this._json.encode(tab)); + continue; + } + + let tabID = currentEntry.url; + this._log.debug("_wrapRealTabs: tab " + tabID); + + items[tabID] = { + // Identify this item as a tab in case we start serializing windows + // in the future. + type: "tab", + + // The position of this tab relative to other tabs in the window. + // For consistency with session store data, we make this one-based. + position: j + 1, + + windowID: windowID, + + state: tab + }; + } + } + + return items; + }, + + wipe: function TabStore_wipe() { + // We're not going to close tabs, since that's probably not what + // the user wants, but we'll clear the cache of virtual tabs. + this._virtualTabs = {}; + this._saveVirtualTabs(); + }, + + resetGUIDs: function TabStore_resetGUIDs() { + // Not needed. + } + +}; + +function TabTracker(engine) { + this._engine = engine; + this._init(); +} +TabTracker.prototype = { + __proto__: new Tracker(), + + _logName: "TabTracker", + + _engine: null, + + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return this._json; + }, + + /** + * There are two ways we could calculate the score. We could calculate it + * incrementally by using the window mediator to watch for windows opening/ + * closing and FUEL (or some other API) to watch for tabs opening/closing + * and changing location. + * + * Or we could calculate it on demand by comparing the state of tabs + * according to the session store with the state according to the snapshot. + * + * It's hard to say which is better. The incremental approach is less + * accurate if it simply increments the score whenever there's a change, + * but it might be more performant. The on-demand approach is more accurate, + * but it might be less performant depending on how often it's called. + * + * In this case we've decided to go with the on-demand approach, and we + * calculate the score as the percent difference between the snapshot set + * and the current tab set, where tabs that only exist in one set are + * completely different, while tabs that exist in both sets but whose data + * doesn't match (f.e. because of variations in history) are considered + * "half different". + * + * So if the sets don't match at all, we return 100; + * if they completely match, we return 0; + * if half the tabs match, and their data is the same, we return 50; + * and if half the tabs match, but their data is all different, we return 75. + */ + get score() { + // The snapshot data is a singleton that we can't modify, so we have to + // copy its unique items to a new hash. + let snapshotData = this._engine.snapshot.data; + let a = {}; + + // The wrapped current state is a unique instance we can munge all we want. + let b = this._engine.store.wrap(); + + // An array that counts the number of intersecting IDs between a and b + // (represented as the length of c) and whether or not their values match + // (represented by the boolean value of each item in c). + let c = []; + + // Generate c and update a and b to contain only unique items. + for (id in snapshotData) { + if (id in b) { + c.push(this._json.encode(snapshotData[id]) == this._json.encode(b[id])); + delete b[id]; + } + else { + a[id] = snapshotData[id]; + } + } + + let numShared = c.length; + let numUnique = [true for (id in a)].length + [true for (id in b)].length; + let numTotal = numShared + numUnique; + + // We're going to divide by the total later, so make sure we don't try + // to divide by zero, even though we should never be in a state where there + // are no tabs in either set. + if (numTotal == 0) + return 0; + + // The number of shared items whose data is different. + let numChanged = c.filter(function(v) v).length; + + let fractionSimilar = (numShared - (numChanged / 2)) / numTotal; + let fractionDissimilar = 1 - fractionSimilar; + let percentDissimilar = Math.round(fractionDissimilar * 100); + + return percentDissimilar; + }, + + resetScore: function FormsTracker_resetScore() { + // Not implemented, since we calculate the score on demand. + } +} diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e80ed2eb06a2..6820c64b3a31 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -56,6 +56,7 @@ Cu.import("resource://weave/engines/bookmarks.js"); Cu.import("resource://weave/engines/history.js"); Cu.import("resource://weave/engines/passwords.js"); Cu.import("resource://weave/engines/forms.js"); +Cu.import("resource://weave/engines/tabs.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 290eb3bfa17c..d214e09201cc 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Store', 'SnapshotStore', - 'TabStore']; +const EXPORTED_SYMBOLS = ['Store', + 'SnapshotStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -270,294 +270,3 @@ SnapshotStore.prototype = { } }; SnapshotStore.prototype.__proto__ = new Store(); - -function TabStore() { - this._virtualTabs = {}; - this._init(); -} -TabStore.prototype = { - __proto__: new Store(), - - _logName: "TabStore", - - get _sessionStore() { - let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. - getService(Ci.nsISessionStore); - this.__defineGetter__("_sessionStore", function() sessionStore); - return this._sessionStore; - }, - - get _windowMediator() { - let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. - getService(Ci.nsIWindowMediator); - this.__defineGetter__("_windowMediator", function() windowMediator); - return this._windowMediator; - }, - - get _os() { - let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - this.__defineGetter__("_os", function() os); - return this._os; - }, - - get _dirSvc() { - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - this.__defineGetter__("_dirSvc", function() dirSvc); - return this._dirSvc; - }, - - /** - * A cache of "virtual" tabs from other devices synced to the server - * that the user hasn't opened locally. Unlike other stores, we don't - * immediately apply create commands, which would be jarring to users. - * Instead, we store them in this cache and prompt the user to pick - * which ones she wants to open. - * - * We also persist this cache on disk and include it in the list of tabs - * we generate in this.wrap to reduce ping-pong updates between clients - * running simultaneously and to maintain a consistent state across restarts. - */ - _virtualTabs: null, - - get virtualTabs() { - // Make sure the list of virtual tabs is completely up-to-date (the user - // might have independently opened some of these virtual tabs since the last - // time we synced). - let realTabs = this._wrapRealTabs(); - let virtualTabsChanged = false; - for (let id in this._virtualTabs) { - if (id in realTabs) { - this._log.warn("get virtualTabs: both real and virtual tabs exist for " - + id + "; removing virtual one"); - delete this._virtualTabs[id]; - virtualTabsChanged = true; - } - } - if (virtualTabsChanged) - this._saveVirtualTabs(); - - return this._virtualTabs; - }, - - set virtualTabs(newValue) { - this._virtualTabs = newValue; - this._saveVirtualTabs(); - }, - - // The file in which we store the state of virtual tabs. - get _file() { - let file = this._dirSvc.get("ProfD", Ci.nsILocalFile); - file.append("weave"); - file.append("store"); - file.append("tabs"); - file.append("virtual.json"); - this.__defineGetter__("_file", function() file); - return this._file; - }, - - _saveVirtualTabs: function TabStore__saveVirtualTabs() { - try { - if (!this._file.exists()) - this._file.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); - let out = this._json.encode(this._virtualTabs); - let [fos] = Utils.open(this._file, ">"); - fos.writeString(out); - fos.close(); - } - catch(ex) { - this._log.warn("could not serialize virtual tabs to disk: " + ex); - } - }, - - _restoreVirtualTabs: function TabStore__restoreVirtualTabs() { - try { - if (this._file.exists()) { - let [is] = Utils.open(this._file, "<"); - let json = Utils.readStream(is); - is.close(); - this._virtualTabs = this._json.decode(json); - } - } - catch (ex) { - this._log.warn("could not parse virtual tabs from disk: " + ex); - } - }, - - _init: function TabStore__init() { - this._restoreVirtualTabs(); - - this.__proto__.__proto__._init(); - }, - - /** - * Apply commands generated by a diff during a sync operation. This method - * overrides the one in its superclass so it can save a copy of the latest set - * of virtual tabs to disk so they can be restored on startup. - */ - applyCommands: function TabStore_applyCommands(commandList) { - let self = yield; - - this.__proto__.__proto__.applyCommands.async(this, self.cb, commandList); - yield; - - this._saveVirtualTabs(); - - self.done(); - }, - - _createCommand: function TabStore__createCommand(command) { - this._log.debug("_createCommand: " + command.GUID); - - if (command.GUID in this._virtualTabs || command.GUID in this._wrapRealTabs()) - throw "trying to create a tab that already exists; id: " + command.GUID; - - // Cache the tab and notify the UI to prompt the user to open it. - this._virtualTabs[command.GUID] = command.data; - this._os.notifyObservers(null, "weave:store:tabs:virtual:created", null); - }, - - _removeCommand: function TabStore__removeCommand(command) { - this._log.debug("_removeCommand: " + command.GUID); - - // If this is a virtual tab, it's ok to remove it, since it was never really - // added to this session in the first place. But we don't remove it if it's - // a real tab, since that would be unexpected, unpleasant, and unwanted. - if (command.GUID in this._virtualTabs) { - delete this._virtualTabs[command.GUID]; - this._os.notifyObservers(null, "weave:store:tabs:virtual:removed", null); - } - }, - - _editCommand: function TabStore__editCommand(command) { - this._log.debug("_editCommand: " + command.GUID); - - // We don't edit real tabs, because that isn't what the user would expect, - // but it's ok to edit virtual tabs, so that if users do open them, they get - // the most up-to-date version of them (and also to reduce sync churn). - - if (this._virtualTabs[command.GUID]) - this._virtualTabs[command.GUID] = command.data; - }, - - /** - * Serialize the current state of tabs. - * - * Note: the state includes both tabs on this device and those on others. - * We get the former from the session store. The latter we retrieved from - * the Weave server and stored in this._virtualTabs. Including virtual tabs - * in the serialized state prevents ping-pong deletes between two clients - * running at the same time. - */ - wrap: function TabStore_wrap() { - let items; - - let virtualTabs = this._wrapVirtualTabs(); - let realTabs = this._wrapRealTabs(); - - // Real tabs override virtual ones, which means ping-pong edits when two - // clients have the same URL loaded with different history/attributes. - // We could fix that by overriding real tabs with virtual ones, but then - // we'd have stale tab metadata in same cases. - items = virtualTabs; - let virtualTabsChanged = false; - for (let id in realTabs) { - // Since virtual tabs can sometimes get out of sync with real tabs - // (the user could have independently opened a new tab that exists - // in the virtual tabs cache since the last time we updated the cache), - // we sync them up in the process of merging them here. - if (this._virtualTabs[id]) { - this._log.warn("wrap: both real and virtual tabs exist for " + id + - "; removing virtual one"); - delete this._virtualTabs[id]; - virtualTabsChanged = true; - } - - items[id] = realTabs[id]; - } - if (virtualTabsChanged) - this._saveVirtualTabs(); - - return items; - }, - - _wrapVirtualTabs: function TabStore__wrapVirtualTabs() { - let items = {}; - - for (let id in this._virtualTabs) { - let virtualTab = this._virtualTabs[id]; - - // Copy the virtual tab without private properties (those that begin - // with an underscore character) so that we don't sync data private to - // this particular Weave client (like the _disposed flag). - let item = {}; - for (let property in virtualTab) - if (property[0] != "_") - item[property] = virtualTab[property]; - - items[id] = item; - } - - return items; - }, - - _wrapRealTabs: function TabStore__wrapRealTabs() { - let items = {}; - - let session = this._json.decode(this._sessionStore.getBrowserState()); - - for (let i = 0; i < session.windows.length; i++) { - let window = session.windows[i]; - // For some reason, session store uses one-based array index references, - // (f.e. in the "selectedWindow" and each tab's "index" properties), so we - // convert them to and from JavaScript's zero-based indexes as needed. - let windowID = i + 1; - this._log.debug("_wrapRealTabs: window " + windowID); - for (let j = 0; j < window.tabs.length; j++) { - let tab = window.tabs[j]; - - // The session history entry for the page currently loaded in the tab. - // We use the URL of the current page as the ID for the tab. - let currentEntry = tab.entries[tab.index - 1]; - - if (!currentEntry || !currentEntry.url) { - this._log.warn("_wrapRealTabs: no current entry or no URL, can't " + - "identify " + this._json.encode(tab)); - continue; - } - - let tabID = currentEntry.url; - this._log.debug("_wrapRealTabs: tab " + tabID); - - items[tabID] = { - // Identify this item as a tab in case we start serializing windows - // in the future. - type: "tab", - - // The position of this tab relative to other tabs in the window. - // For consistency with session store data, we make this one-based. - position: j + 1, - - windowID: windowID, - - state: tab - }; - } - } - - return items; - }, - - wipe: function TabStore_wipe() { - // We're not going to close tabs, since that's probably not what - // the user wants, but we'll clear the cache of virtual tabs. - this._virtualTabs = {}; - this._saveVirtualTabs(); - }, - - resetGUIDs: function TabStore_resetGUIDs() { - // Not needed. - } - -}; diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 49156d28dc35..461e5d04f2b5 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -34,8 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['SyncCore', - 'TabSyncCore']; +const EXPORTED_SYMBOLS = ['SyncCore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -311,48 +310,3 @@ SyncCore.prototype = { return this._reconcile.async(this, onComplete, listA, listB); } }; - -function TabSyncCore(engine) { - this._engine = engine; - this._init(); -} -TabSyncCore.prototype = { - __proto__: new SyncCore(), - - _logName: "TabSync", - - _engine: null, - - get _sessionStore() { - let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. - getService(Ci.nsISessionStore); - this.__defineGetter__("_sessionStore", function() sessionStore); - return this._sessionStore; - }, - - _itemExists: function TSC__itemExists(GUID) { - // Note: this method returns true if the tab exists in any window, not just - // the window from which the tab came. In the future, if we care about - // windows, we might need to make this more specific, although in that case - // we'll have to identify tabs by something other than URL, since even - // window-specific tabs look the same when identified by URL. - - // Get the set of all real and virtual tabs. - let tabs = this._engine.store.wrap(); - - // XXX Should we convert both to nsIURIs and then use nsIURI::equals - // to compare them? - if (GUID in tabs) { - this._log.debug("_itemExists: " + GUID + " exists"); - return true; - } - - this._log.debug("_itemExists: " + GUID + " doesn't exist"); - return false; - }, - - _commandLike: function TSC_commandLike(a, b) { - // Not implemented. - return false; - } -}; diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index c55f9eb2f6a4..cfb70d5eb592 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -34,8 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Tracker', - 'TabTracker']; +const EXPORTED_SYMBOLS = ['Tracker']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -93,96 +92,3 @@ Tracker.prototype = { this._score = 0; } }; - -function TabTracker(engine) { - this._engine = engine; - this._init(); -} -TabTracker.prototype = { - __proto__: new Tracker(), - - _logName: "TabTracker", - - _engine: null, - - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return this._json; - }, - - /** - * There are two ways we could calculate the score. We could calculate it - * incrementally by using the window mediator to watch for windows opening/ - * closing and FUEL (or some other API) to watch for tabs opening/closing - * and changing location. - * - * Or we could calculate it on demand by comparing the state of tabs - * according to the session store with the state according to the snapshot. - * - * It's hard to say which is better. The incremental approach is less - * accurate if it simply increments the score whenever there's a change, - * but it might be more performant. The on-demand approach is more accurate, - * but it might be less performant depending on how often it's called. - * - * In this case we've decided to go with the on-demand approach, and we - * calculate the score as the percent difference between the snapshot set - * and the current tab set, where tabs that only exist in one set are - * completely different, while tabs that exist in both sets but whose data - * doesn't match (f.e. because of variations in history) are considered - * "half different". - * - * So if the sets don't match at all, we return 100; - * if they completely match, we return 0; - * if half the tabs match, and their data is the same, we return 50; - * and if half the tabs match, but their data is all different, we return 75. - */ - get score() { - // The snapshot data is a singleton that we can't modify, so we have to - // copy its unique items to a new hash. - let snapshotData = this._engine.snapshot.data; - let a = {}; - - // The wrapped current state is a unique instance we can munge all we want. - let b = this._engine.store.wrap(); - - // An array that counts the number of intersecting IDs between a and b - // (represented as the length of c) and whether or not their values match - // (represented by the boolean value of each item in c). - let c = []; - - // Generate c and update a and b to contain only unique items. - for (id in snapshotData) { - if (id in b) { - c.push(this._json.encode(snapshotData[id]) == this._json.encode(b[id])); - delete b[id]; - } - else { - a[id] = snapshotData[id]; - } - } - - let numShared = c.length; - let numUnique = [true for (id in a)].length + [true for (id in b)].length; - let numTotal = numShared + numUnique; - - // We're going to divide by the total later, so make sure we don't try - // to divide by zero, even though we should never be in a state where there - // are no tabs in either set. - if (numTotal == 0) - return 0; - - // The number of shared items whose data is different. - let numChanged = c.filter(function(v) v).length; - - let fractionSimilar = (numShared - (numChanged / 2)) / numTotal; - let fractionDissimilar = 1 - fractionSimilar; - let percentDissimilar = Math.round(fractionDissimilar * 100); - - return percentDissimilar; - }, - - resetScore: function FormsTracker_resetScore() { - // Not implemented, since we calculate the score on demand. - } -} From 8d55a6afdc65f8f3ba23110fc0ded3cf1f2e4add Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 14:49:22 -0700 Subject: [PATCH 0284/1860] Removed unused code from cookies.js, fixed a few js2-mode warnings. --- services/sync/modules/engines/cookies.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index a2ea23294345..9098755697b1 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -2,7 +2,6 @@ const EXPORTED_SYMBOLS = ['CookieEngine', 'CookieTracker', 'CookieStore']; const Cc = Components.classes; const Ci = Components.interfaces; -const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); @@ -149,7 +148,7 @@ CookieStore.prototype = { getService(Ci.nsICookieManager2); // need the 2nd revision of the ICookieManager interface // because it supports add() and the 1st one doesn't. - return this.__cookieManager + return this.__cookieManager; }, _createCommand: function CookieStore__createCommand(command) { @@ -211,7 +210,7 @@ CookieStore.prototype = { // Update values in the cookie: for (var key in command.data) { // Whatever values command.data has, use them - matchingCookie[ key ] = command.data[ key ] + matchingCookie[ key ] = command.data[ key ]; } // Remove the old incorrect cookie from the manager: this._cookieManager.remove( matchingCookie.host, @@ -265,7 +264,7 @@ CookieStore.prototype = { rawHost: cookie.rawHost, isSession: cookie.isSession, expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly } + isHttpOnly: cookie.isHttpOnly }; /* See http://developer.mozilla.org/en/docs/nsICookie Note: not syncing "expires", "status", or "policy" @@ -281,7 +280,7 @@ CookieStore.prototype = { TODO are the semantics of this just wiping out an internal buffer, or am I supposed to wipe out all cookies from the browser itself for reals? */ - this._cookieManager.removeAll() + this._cookieManager.removeAll(); }, resetGUIDs: function CookieStore_resetGUIDs() { @@ -307,7 +306,7 @@ CookieTracker.prototype = { register a general observer with the global observerService to watch for the 'cookie-changed' message. */ let observerService = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); + getService(Ci.nsIObserverService); observerService.addObserver( this, 'cookie-changed', false ); }, From 8c70f1d52c1b3cb7baddfbf736df28538253cbc8 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 15:14:27 -0700 Subject: [PATCH 0285/1860] Minor js2-mode warning fixes. --- services/sync/modules/engines.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 05639e36032d..882bd8098426 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -169,7 +169,7 @@ Engine.prototype = { }, get _engineId() { - let id = ID.get('Engine:' + this.name) + let id = ID.get('Engine:' + this.name); if (!id || id.username != this._pbeId.username || id.realm != this._pbeId.realm) { let password = null; @@ -603,7 +603,7 @@ Engine.prototype = { this._core.detectUpdates(self.cb, this._snapshot.data, snap.data); ret.updates = yield; - self.done(ret) + self.done(ret); }, _fullUpload: function Engine__fullUpload() { From 074ce523623cb41c1b997b222c4cf1ebda6e4cce Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 16:56:58 -0700 Subject: [PATCH 0286/1860] In passwords.js, turned _hashLoginInfo() into a module-level function. --- services/sync/modules/engines/passwords.js | 54 ++++++++++------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 572ddfaad6e2..beef082a5c81 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -9,6 +9,27 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); +/* + * _hashLoginInfo + * + * nsILoginInfo objects don't have a unique GUID, so we need to generate one + * on the fly. This is done by taking a hash of every field in the object. + * Note that the resulting GUID could potentiually reveal passwords via + * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by + * anyone, so this should generally be safe. + */ +function _hashLoginInfo(aLogin) { + var loginKey = aLogin.hostname + ":" + + aLogin.formSubmitURL + ":" + + aLogin.httpRealm + ":" + + aLogin.username + ":" + + aLogin.password + ":" + + aLogin.usernameField + ":" + + aLogin.passwordField; + + return Utils.sha1(loginKey); +} + function PasswordEngine(pbeId) { this._init(pbeId); } @@ -19,41 +40,16 @@ PasswordEngine.prototype = { __core: null, get _core() { - if (!this.__core) { + if (!this.__core) this.__core = new PasswordSyncCore(); - this.__core._hashLoginInfo = this._hashLoginInfo; - } return this.__core; }, __store: null, get _store() { - if (!this.__store) { + if (!this.__store) this.__store = new PasswordStore(); - this.__store._hashLoginInfo = this._hashLoginInfo; - } return this.__store; - }, - - /* - * _hashLoginInfo - * - * nsILoginInfo objects don't have a unique GUID, so we need to generate one - * on the fly. This is done by taking a hash of every field in the object. - * Note that the resulting GUID could potentiually reveal passwords via - * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by - * anyone, so this should generally be safe. - */ - _hashLoginInfo : function (aLogin) { - var loginKey = aLogin.hostname + ":" + - aLogin.formSubmitURL + ":" + - aLogin.httpRealm + ":" + - aLogin.username + ":" + - aLogin.password + ":" + - aLogin.usernameField + ":" + - aLogin.passwordField; - - return Utils.sha1(loginKey); } }; PasswordEngine.prototype.__proto__ = new Engine(); @@ -80,7 +76,7 @@ PasswordSyncCore.prototype = { // cache the results, and check the cache here. That would need to happen // once per sync -- not sure how to invalidate cache after current sync? for (var i = 0; i < logins.length && !found; i++) { - var hash = this._hashLoginInfo(logins[i]); + var hash = _hashLoginInfo(logins[i]); if (hash == GUID) found = true;; } @@ -161,7 +157,7 @@ PasswordStore.prototype = { for (var i = 0; i < logins.length; i++) { var login = logins[i]; - var key = this._hashLoginInfo(login); + var key = _hashLoginInfo(login); items[key] = { hostname : login.hostname, formSubmitURL : login.formSubmitURL, From e64d4f2441ef2379ea078ee062485b9d2434b988 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 3 Jun 2008 18:37:36 -0700 Subject: [PATCH 0287/1860] Added a basic testing suite for engines/passwords.js. It currently only tests _hashLoginInfo() and PasswordSyncCore._itemExists(). --- services/sync/tests/unit/test_passwords.js | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 services/sync/tests/unit/test_passwords.js diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js new file mode 100644 index 000000000000..0f8726a27c86 --- /dev/null +++ b/services/sync/tests/unit/test_passwords.js @@ -0,0 +1,44 @@ +function loadInSandbox(aUri) { + var sandbox = Components.utils.Sandbox(this); + var request = Components. + classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(); + + request.open("GET", aUri, false); + request.send(null); + Components.utils.evalInSandbox(request.responseText, sandbox); + + return sandbox; +} + +function run_test() { + // The JS module we're testing, with all members exposed. + var passwords = loadInSandbox("resource://weave/engines/passwords.js"); + + // Fake nsILoginInfo object. + var fakeUser = { + hostname: "www.boogle.com", + formSubmitURL: "http://www.boogle.com/search", + httpRealm: "", + username: "", + password: "", + usernameField: "test_person", + passwordField: "test_password" + }; + + // Fake nsILoginManager object. + var fakeLoginManager = { + getAllLogins: function() { return [fakeUser]; } + }; + + // Ensure that _hashLoginInfo() works. + var fakeUserHash = passwords._hashLoginInfo(fakeUser); + do_check_eq(typeof fakeUserHash, 'string'); + do_check_eq(fakeUserHash.length, 40); + + // Ensure that PasswordSyncCore._itemExists() works. + var psc = new passwords.PasswordSyncCore(); + psc.__loginManager = fakeLoginManager; + do_check_false(psc._itemExists("invalid guid")); + do_check_true(psc._itemExists(fakeUserHash)); +} From b19ee8cf5adf0a9f25b48abafa40d8a520887a07 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 4 Jun 2008 23:07:07 +0900 Subject: [PATCH 0288/1860] Bug 436303: Fix misspelling --- services/sync/locales/en-US/wizard.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 6392dc94bb32..6f3abbf330c3 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -3,7 +3,7 @@ verifyStatusLoginVerified.label = Status: Account Verified verifyStatusLoginFailed.label = Status: Authentication Failed initStatusReadyToSync.label = Status: Ready to Transfer Data -initStatusSyncing.label = Status: Transfering Data... +initStatusSyncing.label = Status: Transferring Data... initStatusSyncComplete.label = Status: Transfer Complete initStatusSyncFailed.label = Status: Transfer Failed From 8d91ec47a97f5f7e6b2725fb0a492d11ad94791b Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 12:14:28 -0700 Subject: [PATCH 0289/1860] [mq]: xmpp-cleanup --- .../sync/modules/xmpp/authenticationLayer.js | 254 +++++++++--------- services/sync/modules/xmpp/transportLayer.js | 226 ++++++++-------- services/sync/modules/xmpp/xmppClient.js | 211 ++++++++------- services/sync/tests/unit/test_pbe.js | 2 +- services/sync/tests/unit/test_xmpp.js | 45 +++- services/sync/tests/unit/test_xmpp_simple.js | 72 +++++ 6 files changed, 467 insertions(+), 343 deletions(-) create mode 100644 services/sync/tests/unit/test_xmpp_simple.js diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js index 838321352827..990592463275 100644 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ b/services/sync/modules/xmpp/authenticationLayer.js @@ -1,72 +1,76 @@ const EXPORTED_SYMBOLS = [ "PlainAuthenticator", "Md5DigestAuthenticator" ]; -if(typeof(atob) == 'undefined') { -// This code was written by Tyler Akins and has been placed in the -// public domain. It would be nice if you left this header intact. -// Base64 code from Tyler Akins -- http://rumkin.com - -var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - -function btoa(input) { - var output = ""; - var chr1, chr2, chr3; - var enc1, enc2, enc3, enc4; - var i = 0; - - do { - chr1 = input.charCodeAt(i++); - chr2 = input.charCodeAt(i++); - chr3 = input.charCodeAt(i++); - - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; - - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (isNaN(chr3)) { - enc4 = 64; - } - - output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + - keyStr.charAt(enc3) + keyStr.charAt(enc4); - } while (i < input.length); - - return output; +function LOG(aMsg) { + dump("Weave::AuthenticationLayer: " + aMsg + "\n"); } -function atob(input) { - var output = ""; - var chr1, chr2, chr3; - var enc1, enc2, enc3, enc4; - var i = 0; +if (typeof(atob) == 'undefined') { + // This code was written by Tyler Akins and has been placed in the + // public domain. It would be nice if you left this header intact. + // Base64 code from Tyler Akins -- http://rumkin.com - // remove all characters that are not A-Z, a-z, 0-9, +, /, or = - input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - do { - enc1 = keyStr.indexOf(input.charAt(i++)); - enc2 = keyStr.indexOf(input.charAt(i++)); - enc3 = keyStr.indexOf(input.charAt(i++)); - enc4 = keyStr.indexOf(input.charAt(i++)); + function btoa(input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; - chr1 = (enc1 << 2) | (enc2 >> 4); - chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); - chr3 = ((enc3 & 3) << 6) | enc4; + do { + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); - output = output + String.fromCharCode(chr1); + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; - if (enc3 != 64) { - output = output + String.fromCharCode(chr2); - } - if (enc4 != 64) { - output = output + String.fromCharCode(chr3); - } - } while (i < input.length); + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } - return output; -} + output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + + keyStr.charAt(enc3) + keyStr.charAt(enc4); + } while (i < input.length); + + return output; + } + + function atob(input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + // remove all characters that are not A-Z, a-z, 0-9, +, /, or = + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + + do { + enc1 = keyStr.indexOf(input.charAt(i++)); + enc2 = keyStr.indexOf(input.charAt(i++)); + enc3 = keyStr.indexOf(input.charAt(i++)); + enc4 = keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCharCode(chr1); + + if (enc3 != 64) { + output = output + String.fromCharCode(chr2); + } + if (enc4 != 64) { + output = output + String.fromCharCode(chr3); + } + } while (i < input.length); + + return output; + } } @@ -75,12 +79,16 @@ function atob(input) { Here's the interface that each implementation must obey: -initialize( clientName, clientRealm, clientPassword ); -generateResponse( rootElem ); +{ + initialize( clientName, clientRealm, clientPassword ); + + generateResponse( rootElem ); + + // returns text of error message + getError(); +} -getError(); - returns text of error message */ function BaseAuthenticator() { @@ -94,14 +102,14 @@ BaseAuthenticator.prototype = { this._password = password; this._stepNumber = 0; this._errorMsg = ""; - }, + }, getError: function () { /* Returns text of most recent error message. Client code should call this if generateResponse() returns false to see what the problem was. */ return this._errorMsg; - }, + }, generateResponse: function( rootElem ) { /* Subclasses must override this. rootElem is a DOM node which is @@ -113,7 +121,7 @@ BaseAuthenticator.prototype = { this._errorMsg = "generateResponse() should be overridden by subclass."; return false; - }, + }, verifyProtocolSupport: function( rootElem, protocolName ) { /* Parses the incoming stream from the server to check whether the @@ -152,7 +160,7 @@ BaseAuthenticator.prototype = { var mechanisms = child.getElementsByTagName( "mechanism" ); for ( var x = 0; x < mechanisms.length; x++ ) { if ( mechanisms[x].firstChild.nodeValue == protocolName ) { - protocolSupported = true; + protocolSupported = true; } } @@ -161,7 +169,7 @@ BaseAuthenticator.prototype = { return false; } return true; - } + } }; @@ -180,15 +188,15 @@ function Md5DigestAuthenticator( ) { } Md5DigestAuthenticator.prototype = { - _makeCNonce: function( ) { + _makeCNonce: function( ) { return "\"" + Math.floor( 10000000000 * Math.random() ) + "\""; }, - - generateResponse: function Md5__generateResponse( rootElem ) { + + generateResponse: function Md5__generateResponse( rootElem ) { if ( this._stepNumber == 0 ) { if ( this.verifyProtocolSupport( rootElem, "DIGEST-MD5" ) == false ) { - return false; + return false; } // SASL step 1: request that we use DIGEST-MD5 authentication. this._stepNumber = 1; @@ -204,21 +212,21 @@ Md5DigestAuthenticator.prototype = { // Now i have the nonce: make a digest-response out of /* username: required - realm: only needed if realm is in challenge - nonce: required, just as recieved - cnonce: required, opaque quoted string, 64 bits entropy - nonce-count: optional - qop: (quality of protection) optional - serv-type: optional? - host: optional? - serv-name: optional? - digest-uri: "service/host/serv-name" (replaces those three?) - response: required (32 lowercase hex), - maxbuf: optional, - charset, - LHEX (32 hex digits = ??), - cipher: required if auth-conf is negotiatedd?? - authzid: optional + realm: only needed if realm is in challenge + nonce: required, just as recieved + cnonce: required, opaque quoted string, 64 bits entropy + nonce-count: optional + qop: (quality of protection) optional + serv-type: optional? + host: optional? + serv-name: optional? + digest-uri: "service/host/serv-name" (replaces those three?) + response: required (32 lowercase hex), + maxbuf: optional, + charset, + LHEX (32 hex digits = ??), + cipher: required if auth-conf is negotiatedd?? + authzid: optional */ @@ -252,8 +260,8 @@ Md5DigestAuthenticator.prototype = { // At this point the server might reject us with a // if ( rootElem.nodeName == "failure" ) { - this._errorMsg = rootElem.firstChild.nodeName; - return false; + this._errorMsg = rootElem.firstChild.nodeName; + return false; } //this._connectionStatus = this.REQUESTED_SASL_3; } @@ -261,7 +269,7 @@ Md5DigestAuthenticator.prototype = { return false; }, - _unpackChallenge: function( challengeString ) { + _unpackChallenge: function( challengeString ) { var challenge = atob( challengeString ); dump( "After b64 decoding: " + challenge + "\n" ); var challengeItemStrings = challenge.split( "," ); @@ -273,7 +281,7 @@ Md5DigestAuthenticator.prototype = { return challengeItems; }, - _packChallengeResponse: function( responseDict ) { + _packChallengeResponse: function( responseDict ) { var responseArray = [] for( var x in responseDict ) { responseArray.push( x + "=" + responseDict[x] ); @@ -292,53 +300,59 @@ function PlainAuthenticator( ) { } PlainAuthenticator.prototype = { - generateResponse: function( rootElem ) { + generateResponse: function( rootElem ) { if ( this._stepNumber == 0 ) { if ( this.verifyProtocolSupport( rootElem, "PLAIN" ) == false ) { - return false; + return false; } var authString = btoa( this._realm + '\0' + this._name + '\0' + this._password ); this._stepNumber = 1; + + // XXX why does this not match the stanzas in XEP-025? return "" + authString + ""; + } else if ( this._stepNumber == 1 ) { if ( rootElem.nodeName == "failure" ) { - // Authentication rejected: username or password may be wrong. - this._errorMsg = rootElem.firstChild.nodeName; - return false; + // Authentication rejected: username or password may be wrong. + this._errorMsg = rootElem.firstChild.nodeName; + return false; } else if ( rootElem.nodeName == "success" ) { - // Authentication accepted: now we start a new stream for - // resource binding. - /* RFC3920 part 7 says: upon receiving a success indication within the - SASL negotiation, the client MUST send a new stream header to the - server, to which the serer MUST respond with a stream header - as well as a list of available stream features. */ - // TODO: resource binding happens in any authentication mechanism - // so should be moved to base class. - this._stepNumber = 2; - return ""; + // Authentication accepted: now we start a new stream for + // resource binding. + /* RFC3920 part 7 says: upon receiving a success indication within the + SASL negotiation, the client MUST send a new stream header to the + server, to which the serer MUST respond with a stream header + as well as a list of available stream features. */ + // TODO: resource binding happens in any authentication mechanism + // so should be moved to base class. + this._stepNumber = 2; + return ""; } } else if ( this._stepNumber == 2 ) { // See if the server is asking us to bind a resource, and if it's // asking us to start a session: var bindNodes = rootElem.getElementsByTagName( "bind" ); if ( bindNodes.length > 0 ) { - this._needBinding = true; + this._needBinding = true; } + var sessionNodes = rootElem.getElementsByTagName( "session" ); if ( sessionNodes.length > 0 ) { - this._needSession = true; + this._needSession = true; } if ( !this._needBinding && !this._needSession ) { - // Server hasn't requested either: we're done. - return this.COMPLETION_CODE; + // Server hasn't requested either: we're done. + return this.COMPLETION_CODE; } if ( this._needBinding ) { - // Do resource binding: - // Tell the server to generate the resource ID for us. - this._stepNumber = 3; - return ""; + // Do resource binding: + // Tell the server to generate the resource ID for us. + this._stepNumber = 3; + return ""; } this._errorMsg = "Server requested session not binding: can't happen?"; @@ -347,20 +361,20 @@ PlainAuthenticator.prototype = { // Pull the JID out of the stuff the server sends us. var jidNodes = rootElem.getElementsByTagName( "jid" ); if ( jidNodes.length == 0 ) { - this._errorMsg = "Expected JID node from server, got none."; - return false; + this._errorMsg = "Expected JID node from server, got none."; + return false; } this._jid = jidNodes[0].firstChild.nodeValue; // TODO: Does the client need to do anything special with its new // "client@host.com/resourceID" full JID? - dump( "JID set to " + this._jid ); + LOG( "JID set to " + this._jid ); // If we still need to do session, then we're not done yet: if ( this._needSession ) { - this._stepNumber = 4; - return ""; + this._stepNumber = 4; + return ""; } else { - return this.COMPLETION_CODE; + return this.COMPLETION_CODE; } } else if ( this._stepNumber == 4 ) { // OK, now we're done. diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index a4c03537ecc0..de3d63f91f4e 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -3,26 +3,32 @@ const EXPORTED_SYMBOLS = ['HTTPPollingTransport']; var Cc = Components.classes; var Ci = Components.interfaces; +function LOG(aMsg) { + dump("Weave::Transport-HTTP-Poll: " + aMsg + "\n"); +} + function InputStreamBuffer() { } InputStreamBuffer.prototype = { - _data: "", - append: function( stuff ) { + _data: "", + append: function( stuff ) { this._data = this._data + stuff; }, - clear: function() { + clear: function() { this._data = ""; }, - getData: function() { + getData: function() { return this._data; } } +/** + * A transport layer that uses raw sockets. + * Not recommended for use; currently fails when trying to negotiate + * TLS. + * Use HTTPPollingTransport instead. + */ function SocketClient( host, port ) { - /* A transport layer that uses raw sockets. - Not recommended for use; currently fails when trying to negotiate - TLS. - Use HTTPPollingTransport instead. */ this._init( host, port ); } SocketClient.prototype = { @@ -32,6 +38,7 @@ SocketClient.prototype = { this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); return this.__threadManager; }, + __transport: null, get _transport() { if (!this.__transport) { @@ -45,7 +52,7 @@ SocketClient.prototype = { return this.__transport; }, - _init: function( host, port ) { + _init: function( host, port ) { this._host = host; this._port = port; this._contentRead = ""; @@ -53,7 +60,7 @@ SocketClient.prototype = { this.connect(); }, - connect: function() { + connect: function() { var outstream = this._transport.openOutputStream( 0, // flags 0, // buffer size 0 ); // number of buffers @@ -75,18 +82,18 @@ SocketClient.prototype = { // create dataListener class for callback: var dataListener = { - data : "", - onStartRequest: function(request, context){ + data : "", + onStartRequest: function(request, context){ }, - onStopRequest: function(request, context, status){ - rawInputStream.close(); - outstream.close(); + onStopRequest: function(request, context, status){ + rawInputStream.close(); + outstream.close(); }, - onDataAvailable: function(request, context, inputStream, offset, count){ - // use scriptable stream wrapper, not "real" stream. - // count is number of bytes available, offset is position in stream. - // Do stuff with data here! - buffer.append( scriptablestream.read( count )); + onDataAvailable: function(request, context, inputStream, offset, count){ + // use scriptable stream wrapper, not "real" stream. + // count is number of bytes available, offset is position in stream. + // Do stuff with data here! + buffer.append( scriptablestream.read( count )); } }; // register it: @@ -108,22 +115,20 @@ SocketClient.prototype = { disconnect: function() { var thread = this._threadManager.currentThread; - while( thread.hasPendingEvents() ) - { - thread.processNextEvent( true ); - } + while ( thread.hasPendingEvents() ) { + thread.processNextEvent( true ); + } }, - checkResponse: function() { + checkResponse: function() { return this._getData(); }, - waitForResponse: function() { + waitForResponse: function() { var thread = this._threadManager.currentThread; - while( this._buffer.getData().length == 0 ) - { - thread.processNextEvent( true ); - } + while( this._buffer.getData().length == 0 ) { + thread.processNextEvent( true ); + } var output = this._buffer.getData(); this._buffer.clear(); return output; @@ -132,30 +137,33 @@ SocketClient.prototype = { startTLS: function() { this._transport.securityInfo.QueryInterface(Ci.nsISSLSocketControl); this._transport.securityInfo.StartTLS(); - }, - + } }; -/* The interface that should be implemented by any Transport object: - send( messageXml ); - setCallbackObject( object with .onIncomingData and .onTransportError ); - connect(); - disconnect(); +/* + The interface that should be implemented by any Transport object: + + send( messageXml ); + setCallbackObject( object with .onIncomingData and .onTransportError ); + connect(); + disconnect(); */ +/** + * Send HTTP requests periodically to the server using a timer. + * HTTP POST requests with content-type application/x-www-form-urlencoded. + * responses from the server have content-type text/xml + * request and response are UTF-8 encoded (ignore what HTTP header says) + * identify session by always using set-cookie header with cookie named ID + * first request sets this to 0 to indicate new session. + */ function HTTPPollingTransport( serverUrl, useKeys, interval ) { - /* Send HTTP requests periodically to the server using a timer. - HTTP POST requests with content-type application/x-www-form-urlencoded. - responses from the server have content-type text/xml - request and response are UTF-8 encoded (ignore what HTTP header says) - identify session by always using set-cookie header with cookie named ID - first request sets this to 0 to indicate new session. */ - this._init( serverUrl, useKeys, interval ); } HTTPPollingTransport.prototype = { - _init: function( serverUrl, useKeys, interval ) { + _init: function( serverUrl, useKeys, interval ) { + LOG("Initializing transport: serverUrl=" + serverUrl + ", useKeys=" + useKeys + ", interval=" + interval); this._serverUrl = serverUrl this._n = 0; this._key = this._makeSeed(); @@ -167,41 +175,43 @@ HTTPPollingTransport.prototype = { this._outgoingRetryBuffer = ""; }, - __request: null, - get _request() { + __request: null, + get _request() { if (!this.__request) this.__request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance( Ci.nsIXMLHttpRequest ); return this.__request; }, - __hasher: null, - get _hasher() { + __hasher: null, + get _hasher() { if (!this.__hasher) this.__hasher = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash ); return this.__hasher; }, - __timer: null, - get _timer() { + __timer: null, + get _timer() { if (!this.__timer) this.__timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); return this.__timer; }, - _makeSeed: function() { + _makeSeed: function() { return "foo";//"MyKeyOfHorrors"; }, - _advanceKey: function() { - var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + _advanceKey: function() { + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); // we use UTF-8 here, you can choose other encodings. + // TODO make configurable converter.charset = "UTF-8"; // result is an out parameter, // result.value will contain the array length var result = {}; // data is an array of bytes - var data = converter.convertToByteArray( this._key, result); + var data = converter.convertToByteArray(this._key, result); this._n += 1; this._hasher.initWithString( "SHA1" ); @@ -209,8 +219,8 @@ HTTPPollingTransport.prototype = { this._key = this._hasher.finish( true ); // true means B64encode }, - _setIdFromCookie: function( self, cookie ) { - // parse connection ID out of the cookie: + _setIdFromCookie: function( self, cookie ) { + // parse connection ID out of the cookie: // dump( "Cookie is " + cookie + "\n" ); var cookieSegments = cookie.split( ";" ); cookieSegments = cookieSegments[0].split( "=" ); @@ -235,7 +245,7 @@ HTTPPollingTransport.prototype = { } }, - _onError: function( errorText ) { + _onError: function( errorText ) { dump( "Transport error: " + errorText + "\n" ); if ( this._callbackObject != null ) { this._callbackObject.onTransportError( errorText ); @@ -243,72 +253,71 @@ HTTPPollingTransport.prototype = { this.disconnect(); }, - _doPost: function( requestXml ) { - var request = this._request; + _doPost: function( requestXml ) { + var request = this._request; var callbackObj = this._callbackObject; var self = this; var contents = ""; - if ( this._useKey ) { this._advanceKey(); contents = this._connectionId + ";" + this._key + "," + requestXml; } else { contents = this._connectionId + "," + requestXml; /* TODO: - Currently I get a "-3:0" error (key sequence error) from the 2nd - exchange if using the keys is enabled. */ + Currently I get a "-3:0" error (key sequence error) from the 2nd + exchange if using the keys is enabled. */ } - _processReqChange = function( ) { - //Callback for XMLHTTPRequest object state change messages + var _processReqChange = function() { + // Callback for XMLHTTPRequest object state change messages if ( request.readyState == 4 ) { - if ( request.status == 200) { - // 200 means success. - - dump( "Server says: " + request.responseText + "\n" ); - // Look for a set-cookie header: - var latestCookie = request.getResponseHeader( "Set-Cookie" ); - if ( latestCookie.length > 0 ) { - self._setIdFromCookie( self, latestCookie ); - } - // Respond to any text we get back from the server in response - if ( callbackObj != null && request.responseText.length > 0 ) { - callbackObj.onIncomingData( request.responseText ); - } - } else { - dump ( "Error! Got HTTP status code " + request.status + "\n" ); - if ( request.status == 0 ) { - /* Sometimes the server gives us HTTP status code 0 in response - to an attempt to POST. I'm not sure why this happens, but - if we re-send the POST it seems to usually work the second - time. So put the message into a buffer and try again later: - */ - self._outgoingRetryBuffer = requestXml; - } - } + if ( request.status == 200) { + // 200 means success. + + LOG("Server says: " + request.responseText); + // Look for a set-cookie header: + var latestCookie = request.getResponseHeader( "Set-Cookie" ); + if ( latestCookie.length > 0 ) { + self._setIdFromCookie( self, latestCookie ); + } + + // Respond to any text we get back from the server in response + if ( callbackObj != null && request.responseText.length > 0 ) { + callbackObj.onIncomingData( request.responseText ); + } + } else { + LOG( "Error! Got HTTP status code " + request.status ); + if ( request.status == 0 ) { + /* Sometimes the server gives us HTTP status code 0 in response + to an attempt to POST. I'm not sure why this happens, but + if we re-send the POST it seems to usually work the second + time. So put the message into a buffer and try again later: + */ + self._outgoingRetryBuffer = requestXml; + } + } } }; request.open( "POST", this._serverUrl, true ); //async = true - request.setRequestHeader( "Content-type", - "application/x-www-form-urlencoded;charset=UTF-8" ); + request.setRequestHeader( "Content-type", "application/x-www-form-urlencoded;charset=UTF-8" ); request.setRequestHeader( "Content-length", contents.length ); request.setRequestHeader( "Connection", "close" ); request.onreadystatechange = _processReqChange; - dump( "Sending: " + contents + "\n" ); + LOG("Sending: " + contents); request.send( contents ); }, - send: function( messageXml ) { + send: function( messageXml ) { this._doPost( messageXml ); }, - setCallbackObject: function( callbackObject ) { + setCallbackObject: function( callbackObject ) { this._callbackObject = callbackObject; }, - notify: function( timer ) { + notify: function( timer ) { /* having a notify method makes this object satisfy the nsITimerCallback interface, so the object can be passed to timer.initWithCallback. */ @@ -322,29 +331,28 @@ HTTPPollingTransport.prototype = { this._doPost( outgoingMsg ); }, - connect: function() { - /* Set up a timer to poll the server periodically. */ + connect: function() { + /* Set up a timer to poll the server periodically. */ - // TODO doPost isn't reentrant; don't try to doPost if there's - //already a post in progress... or can that never happen? + // TODO doPost isn't reentrant; don't try to doPost if there's + //already a post in progress... or can that never happen? - this._timer.initWithCallback( this, - this._interval, - this._timer.TYPE_REPEATING_SLACK ); + this._timer.initWithCallback( this, + this._interval, + this._timer.TYPE_REPEATING_SLACK ); }, - disconnect: function () { + disconnect: function () { + this._request.abort(); this._timer.cancel(); }, - testKeys: function () { - + testKeys: function () { this._key = "foo"; - dump( this._key + "\n" ); + LOG(this._key); for ( var x = 1; x < 7; x++ ) { this._advanceKey(); - dump( this._key + "\n" ); + LOG(this._key); } - }, - + } }; diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 34c1f302a27c..1d53f94d3a96 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -11,6 +11,10 @@ var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; +function LOG(aMsg) { + dump("Weave::XMPPClient: " + aMsg + "\n"); +} + Cu.import("resource://weave/xmpp/transportLayer.js"); Cu.import("resource://weave/xmpp/authenticationLayer.js"); @@ -18,19 +22,19 @@ function XmppClient( clientName, realm, clientPassword, transport, authenticator this._init( clientName, realm, clientPassword, transport, authenticator ); } XmppClient.prototype = { - //connection status codes: - NOT_CONNECTED: 0, - CALLED_SERVER: 1, - AUTHENTICATING: 2, - CONNECTED: 3, - FAILED: -1, + //connection status codes: + NOT_CONNECTED: 0, + CALLED_SERVER: 1, + AUTHENTICATING: 2, + CONNECTED: 3, + FAILED: -1, - // IQ stanza status codes: - IQ_WAIT: 0, - IQ_OK: 1, - IQ_ERROR: -1, + // IQ stanza status codes: + IQ_WAIT: 0, + IQ_OK: 1, + IQ_ERROR: -1, - _init: function( clientName, realm, clientPassword, transport, authenticator ) { + _init: function( clientName, realm, clientPassword, transport, authenticator ) { this._myName = clientName; this._realm = realm; this._fullName = clientName + "@" + realm; @@ -40,6 +44,7 @@ XmppClient.prototype = { this._streamOpen = false; this._transportLayer = transport; this._authenticationLayer = authenticator; + LOG("initialized auth with clientName=" + clientName + ", realm=" + realm + ", pw=" + clientPassword); this._authenticationLayer.initialize( clientName, realm, clientPassword ); this._messageHandlers = []; this._iqResponders = []; @@ -47,9 +52,9 @@ XmppClient.prototype = { this._pendingIqs = {}; }, - __parser: null, + __parser: null, get _parser() { - if (!this.__parser) + if (!this.__parser) this.__parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance( Ci.nsIDOMParser ); return this.__parser; }, @@ -61,11 +66,10 @@ XmppClient.prototype = { return this.__threadManager; }, - - parseError: function( streamErrorNode ) { - dump( "Uh-oh, there was an error!\n" ); + parseError: function( streamErrorNode ) { + LOG( "Uh-oh, there was an error!" ); var error = streamErrorNode.childNodes[0]; - dump( "Name: " + error.nodeName + " Value: " + error.nodeValue + "\n" ); + LOG( "Name: " + error.nodeName + " Value: " + error.nodeValue ); this._error = error.nodeName; this.disconnect(); /* Note there can be an optional bla bla node inside @@ -74,13 +78,13 @@ XmppClient.prototype = { namespace */ }, - setError: function( errorText ) { - dump( "Error: " + errorText + "\n" ); + setError: function( errorText ) { + LOG( "Error: " + errorText ); this._error = errorText; this._connectionStatus = this.FAILED; }, - onIncomingData: function( messageText ) { + onIncomingData: function( messageText ) { var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); if (responseDOM.documentElement.nodeName == "parsererror" ) { @@ -92,13 +96,15 @@ XmppClient.prototype = { var response = messageText + this._makeClosingXml(); responseDOM = this._parser.parseFromString( response, "text/xml" ); } + if ( responseDOM.documentElement.nodeName == "parsererror" ) { /* If that still doesn't work, it might be that we're getting a fragment - with multiple top-level tags, which is a no-no. Try wrapping it - all inside one proper top-level stream element and parsing. */ + with multiple top-level tags, which is a no-no. Try wrapping it + all inside one proper top-level stream element and parsing. */ response = this._makeHeaderXml( this._fullName ) + messageText + this._makeClosingXml(); responseDOM = this._parser.parseFromString( response, "text/xml" ); } + if ( responseDOM.documentElement.nodeName == "parsererror" ) { /* Still can't parse it, give up. */ this.setError( "Can't parse incoming XML." ); @@ -114,64 +120,63 @@ XmppClient.prototype = { //dispatch whatever the next stage of the connection protocol is. response = this._authenticationLayer.generateResponse( rootElem ); if ( response == false ) { - this.setError( this._authenticationLayer.getError() ); + this.setError( this._authenticationLayer.getError() ); } else if ( response == this._authenticationLayer.COMPLETION_CODE ){ - this._connectionStatus = this.CONNECTED; - dump( "We be connected!!\n" ); + this._connectionStatus = this.CONNECTED; + LOG( "We be connected!!" ); } else { - this._transportLayer.send( response ); + this._transportLayer.send( response ); } return; } if ( this._connectionStatus == this.CONNECTED ) { /* Check incoming xml to see if it contains errors, presence info, - or a message: */ + or a message: */ var errors = rootElem.getElementsByTagName( "stream:error" ); if ( errors.length > 0 ) { - this.setError( errors[0].firstChild.nodeName ); - return; + this.setError( errors[0].firstChild.nodeName ); + return; } var presences = rootElem.getElementsByTagName( "presence" ); if (presences.length > 0 ) { - var from = presences[0].getAttribute( "from" ); - if ( from != undefined ) { - dump( "I see that " + from + " is online.\n" ); - } + var from = presences[0].getAttribute( "from" ); + if ( from != undefined ) { + LOG( "I see that " + from + " is online." ); + } } if ( rootElem.nodeName == "message" ) { - this.processIncomingMessage( rootElem ); + this.processIncomingMessage( rootElem ); } else { - var messages = rootElem.getElementsByTagName( "message" ); - if (messages.length > 0 ) { - for ( var message in messages ) { - this.processIncomingMessage( messages[ message ] ); - } - } + var messages = rootElem.getElementsByTagName( "message" ); + if (messages.length > 0 ) { + for ( var message in messages ) { + this.processIncomingMessage( messages[ message ] ); + } + } } if ( rootElem.nodeName == "iq" ) { - this.processIncomingIq( rootElem ); + this.processIncomingIq( rootElem ); } else { - var iqs = rootElem.getElementsByTagName( "iq" ); - if ( iqs.length > 0 ) { - for ( var iq in iqs ) { - this.processIncomingIq( iqs[ iq ] ); - } - } + var iqs = rootElem.getElementsByTagName( "iq" ); + if ( iqs.length > 0 ) { + for ( var iq in iqs ) { + this.processIncomingIq( iqs[ iq ] ); + } + } } } }, - processIncomingMessage: function( messageElem ) { - dump( "in processIncomingMessage: messageElem is a " + messageElem + "\n" ); + processIncomingMessage: function( messageElem ) { + LOG( "in processIncomingMessage: messageElem is a " + messageElem ); var from = messageElem.getAttribute( "from" ); var contentElem = messageElem.firstChild; // Go down till we find the element with nodeType = 3 (TEXT_NODE) while ( contentElem.nodeType != 3 ) { contentElem = contentElem.firstChild; } - dump( "Incoming message to you from " + from + ":\n" ); - dump( contentElem.nodeValue ); + LOG( "Incoming message to you from " + from + ":" + contentElem.nodeValue ); for ( var x in this._messageHandlers ) { // TODO do messages have standard place for metadata? // will want to have handlers that trigger only on certain metadata. @@ -179,7 +184,7 @@ XmppClient.prototype = { } }, - processIncomingIq: function( iqElem ) { + processIncomingIq: function( iqElem ) { /* This processes both kinds of incoming IQ stanzas -- ones that are new (initated by another jabber client) and those that are responses to ones we sent out previously. We can tell the @@ -202,9 +207,9 @@ XmppClient.prototype = { break; case "set": /* Someone is telling us to set the value of a variable. - Delegate this to the registered iqResponder; we can reply - either with an empty iq type="result" stanza, or else an - iq type="error" stanza */ + Delegate this to the registered iqResponder; we can reply + either with an empty iq type="result" stanza, or else an + iq type="error" stanza */ var variable = iqElem.firstChild.firstChild.getAttribute( "var" ); var newValue = iqElem.firstChild.firstChildgetAttribute( "value" ); // TODO what happens when there's more than one reigistered @@ -216,61 +221,61 @@ XmppClient.prototype = { break; case "result": /* If all is right with the universe, then the id of this iq stanza - corresponds to a set or get stanza that we sent out, so it should - be in our pending dictionary. + corresponds to a set or get stanza that we sent out, so it should + be in our pending dictionary. */ if ( this._pendingIqs[ id ] == undefined ) { - this.setError( "Unexpected IQ reply id" + id ); - return; + this.setError( "Unexpected IQ reply id" + id ); + return; } /* The result stanza may have a query with a value in it, in - which case this is the value of the variable we requested. - If there's no value, it was probably a set query, and should - just be considred a success. */ + which case this is the value of the variable we requested. + If there's no value, it was probably a set query, and should + just be considred a success. */ var newValue = iqElem.firstChild.firstChild.getAttribute( "value" ); if ( newValue != undefined ) { - this._pendingIqs[ id ].value = newValue; + this._pendingIqs[ id ].value = newValue; } else { - this._pendingIqs[ id ].value = true; + this._pendingIqs[ id ].value = true; } this._pendingIqs[ id ].status = this.IQ_OK; break; case "error": /* Dig down through the element tree till we find the one with - the error text... */ + the error text... */ var elems = iqElem.getElementsByTagName( "error" ); var errorNode = elems[0].firstChild; if ( errorNode.nodeValue != null ) { - this.setError( errorNode.nodeValue ); + this.setError( errorNode.nodeValue ); } else { - this.setError( errorNode.nodeName ); + this.setError( errorNode.nodeName ); } if ( this._pendingIqs[ id ] != undefined ) { - this._pendingIqs[ id ].status = this.IQ_ERROR; + this._pendingIqs[ id ].status = this.IQ_ERROR; } break; } }, - registerMessageHandler: function( handlerObject ) { + registerMessageHandler: function( handlerObject ) { /* messageHandler object must have handle( messageText, from ) method. */ this._messageHandlers.push( handlerObject ); }, - registerIQResponder: function( handlerObject ) { + registerIQResponder: function( handlerObject ) { /* IQResponder object must have .get( variable ) and .set( variable, newvalue ) methods. */ this._iqResponders.push( handlerObject ); }, - - onTransportError: function( errorText ) { + + onTransportError: function( errorText ) { this.setError( errorText ); }, - - connect: function( host ) { + + connect: function( host ) { // Do the handshake to connect with the server and authenticate. this._transportLayer.connect(); this._transportLayer.setCallbackObject( this ); @@ -281,27 +286,31 @@ XmppClient.prototype = { // onIncomingData. }, - _makeHeaderXml: function( recipient ) { - return ""; + _makeHeaderXml: function( recipient ) { + return ""; }, - _makeMessageXml: function( messageText, fullName, recipient ) { + _makeMessageXml: function( messageText, fullName, recipient ) { /* a "message stanza". Note the message element must have the full namespace info or it will be rejected. */ - var msgXml = "" + messageText + ""; - dump( "Message xml: \n" ); - dump( msgXml ); + var msgXml = "" + + messageText + ""; + LOG( "Message xml: " ); + LOG( msgXml ); return msgXml; }, - _makePresenceXml: function( fullName ) { + _makePresenceXml: function( fullName ) { // a "presence stanza", sent to announce my presence to the server; // the server is supposed to multiplex this to anyone subscribed to // presence notifications. return ""; }, - - _makeIqXml: function( fullName, recipient, type, id, query ) { + + _makeIqXml: function( fullName, recipient, type, id, query ) { /* an "iq (info/query) stanza". This can be used for structured data exchange: I send an containing a query, and get back an containing the answer to my @@ -313,11 +322,11 @@ XmppClient.prototype = { return "" + query + ""; }, - _makeClosingXml: function () { + _makeClosingXml: function () { return ""; }, - _generateIqId: function() { + _generateIqId: function() { // Each time this is called, it returns an ID that has not // previously been used this session. var id = "client_" + this._nextIqId; @@ -325,14 +334,14 @@ XmppClient.prototype = { return id; }, - _sendIq: function( recipient, query, type ) { + _sendIq: function( recipient, query, type ) { var id = this._generateIqId(); this._pendingIqs[ id ] = { status: this.IQ_WAIT }; this._transportLayer.send( this._makeIqXml( this._fullName, - recipient, - type, - id, - query ) ); + recipient, + type, + id, + query ) ); /* And then wait for a response with the same ID to come back... When we get a reply, the pendingIq dictionary entry will have its status set to IQ_OK or IQ_ERROR and, if it's IQ_OK and @@ -350,28 +359,28 @@ XmppClient.prototype = { // Can't happen? }, - iqGet: function( recipient, variable ) { + iqGet: function( recipient, variable ) { var query = ""; return this._sendIq( recipient, query, "get" ); }, - - iqSet: function( recipient, variable, value ) { + + iqSet: function( recipient, variable, value ) { var query = ""; return this._sendIq( recipient, query, "set" ); }, - sendMessage: function( recipient, messageText ) { + sendMessage: function( recipient, messageText ) { // OK so now I'm doing that part, but what am I supposed to do with the // new JID that I'm bound to?? var body = this._makeMessageXml( messageText, this._fullName, recipient ); this._transportLayer.send( body ); }, - announcePresence: function() { - this._transportLayer.send( "" ); + announcePresence: function() { + this._transportLayer.send( this._makePresenceXml(this._myName) ); }, - subscribeForPresence: function( buddyId ) { + subscribeForPresence: function( buddyId ) { // OK, there are 'subscriptions' and also 'rosters'...? //this._transportLayer.send( "" ); // TODO @@ -379,22 +388,22 @@ XmppClient.prototype = { // me with type ='subscribed'. }, - disconnect: function() { + disconnect: function() { // todo: only send closing xml if the stream has not already been // closed (if there was an error, the server will have closed the stream.) this._transportLayer.send( this._makeClosingXml() ); this._transportLayer.disconnect(); }, - waitForConnection: function( ) { + waitForConnection: function( ) { var thread = this._threadManager.currentThread; while ( this._connectionStatus != this.CONNECTED && - this._connectionStatus != this.FAILED ) { + this._connectionStatus != this.FAILED ) { thread.processNextEvent( true ); } }, - waitForDisconnect: function() { + waitForDisconnect: function() { var thread = this._threadManager.currentThread; while ( this._connectionStatus == this.CONNECTED ) { thread.processNextEvent( true ); diff --git a/services/sync/tests/unit/test_pbe.js b/services/sync/tests/unit/test_pbe.js index 364ca0e26087..f00ffb6cc408 100644 --- a/services/sync/tests/unit/test_pbe.js +++ b/services/sync/tests/unit/test_pbe.js @@ -13,7 +13,7 @@ function run_test() { do_check_true(clearTxt == "my very secret message!"); // The following check with wrong password must cause decryption to fail - // beuase of used padding-schema cipher, RFC 3852 Section 6.3 + // because of used padding-schema cipher, RFC 3852 Section 6.3 let failure = false; try { pbe.decrypt("wrongpassphrase", cipherTxt); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index dcc84c5abadc..a4d8d1f1a2d6 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -2,14 +2,20 @@ var Cu = Components.utils; Cu.import( "resource://weave/xmpp/xmppClient.js" ); +function LOG(aMsg) { + dump("TEST_XMPP_SIMPLE: " + aMsg + "\n"); +} + var serverUrl = "http://127.0.0.1:5280/http-poll"; -var jabberDomain = "jonathan-dicarlos-macbook-pro.local"; +var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. + getService(Ci.nsIDNSService).myHostName; var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); -function run_test() { +var alice; +function run_test() { // FIXME: this test hangs when you don't have a server, disabling for now return; @@ -18,20 +24,34 @@ function run_test() { false, 4000 ); var auth = new PlainAuthenticator(); - var alice = new XmppClient( "alice", jabberDomain, "iamalice", + alice = new XmppClient( "alice", jabberDomain, "iamalice", transport, auth ); - /* + // test connection alice.connect( jabberDomain ); alice.waitForConnection(); - do_check_neq( alice._connectionStatus, alice.FAILED ); - if ( alice._connectionStatus != alice.FAILED ) { - alice.disconnect(); - }; - // A flaw here: once alice disconnects, she can't connect again? - // Make an explicit test out of that. - */ + do_check_eq( alice._connectionStatus, alice.CONNECTED); + // test re-connection + alice.disconnect(); + LOG("disconnected"); + alice.connect( jabberDomain ); + LOG("wait"); + alice.waitForConnection(); + LOG("waited"); + do_check_eq( alice._connectionStatus, alice.CONNECTED); + + /* + // test connection failure + alice.disconnect(); + alice.connect( "bad domain" ); + alice.waitForConnection(); + do_check_eq( alice._connectionStatus, alice.FAILED ); + + // re-connect and move on + alice.connect( jabberDomain ); + alice.waitForConnection(); + do_check_eq( alice._connectionStatus, alice.CONNECTED); // The talking-to-myself test: var testIsOver = false; @@ -78,7 +98,8 @@ function run_test() { while( !testIsOver ) { currentThread.processNextEvent( true ); } + */ alice.disconnect(); - bob.disconnect(); + //bob.disconnect(); }; diff --git a/services/sync/tests/unit/test_xmpp_simple.js b/services/sync/tests/unit/test_xmpp_simple.js new file mode 100644 index 000000000000..50b01da295f8 --- /dev/null +++ b/services/sync/tests/unit/test_xmpp_simple.js @@ -0,0 +1,72 @@ +function LOG(aMsg) { + dump("TEST_XMPP_SIMPLE: " + aMsg + "\n"); +} + +Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); + +var serverUrl = "http://127.0.0.1:5280/http-poll"; +var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. + getService(Ci.nsIDNSService).myHostName; + +function run_test() { + // FIXME: this test hangs when you don't have a server, disabling for now + return; + + // async test + do_test_pending(); + + var testMessage = "Hello Bob."; + + var aliceHandler = { + handle: function(msgText, from) { + LOG("ALICE RCVD from " + from + ": " + msgText); + } + }; + var aliceClient = getClientForUser("alice", "iamalice", aliceHandler); + + var bobHandler = { + handle: function(msgText, from) { + LOG("BOB RCVD from " + from + ": " + msgText); + do_check_eq(from.split("/")[0], "alice@" + jabberDomain); + do_check_eq(msgText, testMessage); + LOG("messages checked out"); + + aliceClient.disconnect(); + bobClient.disconnect(); + LOG("disconnected"); + + do_test_finished(); + } + }; + var bobClient = getClientForUser("bob", "iambob", bobHandler); + bobClient.announcePresence(); + + + // Send a message + aliceClient.sendMessage("bob@" + jabberDomain, testMessage); +} + +function getClientForUser(aName, aPassword, aHandler) { + // "false" tells the transport not to use session keys. 4000 is the number of + // milliseconds to wait between attempts to poll the server. + var transport = new HTTPPollingTransport(serverUrl, false, 4000); + + var auth = new PlainAuthenticator(); + + var client = new XmppClient(aName, jabberDomain, aPassword, + transport, auth); + + client.registerMessageHandler(aHandler); + + // Connect + client.connect(jabberDomain); + client.waitForConnection(); + + // this will block until our connection attempt has either succeeded or failed. + // Check if connection succeeded: + if ( client._connectionStatus == client.FAILED ) { + do_throw("connection failed"); + } + + return client; +} From bbc77b820c4196e8da45ef1970bdde8e14d4a601 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 4 Jun 2008 13:40:53 -0700 Subject: [PATCH 0290/1860] bug 436696: make sure we pass a valid URI to nsITaggingService::getTagsForURI when the bookmark record doesn't include a URI so the method doesn't throw and hork bookmarks sync --- services/sync/modules/engines/bookmarks.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f3d1ca72b233..5f3226f80520 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -558,7 +558,24 @@ BookmarksStore.prototype = { } item.URI = node.uri; - item.tags = this._ts.getTagsForURI(Utils.makeURI(node.uri), {}); + + // This will throw if makeURI can't make an nsIURI object out of the + // node.uri string (or return null if node.uri is null), in which case + // we won't be able to get tags for the bookmark (but we'll still sync + // the rest of the record). + let uri; + try { + uri = Utils.makeURI(node.uri); + } + catch(e) { + this._log.error("error parsing URI string <" + node.uri + "> " + + "for item " + node.itemId + " (" + node.title + "): " + + e); + } + + if (uri) + item.tags = this._ts.getTagsForURI(uri, {}); + item.keyword = this._bms.getKeywordForBookmark(node.itemId); } else if (node.type == node.RESULT_TYPE_SEPARATOR) { From a6e457b95bad06a7dc8de25ac72980651e492fb6 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 14:02:47 -0700 Subject: [PATCH 0291/1860] [mq]: xmpp-disconnect --- services/sync/modules/xmpp/xmppClient.js | 26 +++++++++++++++---- services/sync/tests/unit/test_xmpp.js | 33 ++++++++++++++---------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 1d53f94d3a96..52e326ffe969 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -85,14 +85,23 @@ XmppClient.prototype = { }, onIncomingData: function( messageText ) { + LOG("onIncomingData(): rcvd: " + messageText); var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); if (responseDOM.documentElement.nodeName == "parsererror" ) { - /* Before giving up, remember that XMPP doesn't close the top-level - element until the communication is done; this means - that what we get from the server is often technically only an - xml fragment. Try manually appending the closing tag to simulate - a complete xml document and then parsing that. */ + // handle server disconnection + if (messageText.match("^$")) { + this._handleServerDisconnection(); + return; + } + + /* + Before giving up, remember that XMPP doesn't close the top-level + element until the communication is done; this means + that what we get from the server is often technically only an + xml fragment. Try manually appending the closing tag to simulate + a complete xml document and then parsing that. */ + var response = messageText + this._makeClosingXml(); responseDOM = this._parser.parseFromString( response, "text/xml" ); } @@ -392,7 +401,13 @@ XmppClient.prototype = { // todo: only send closing xml if the stream has not already been // closed (if there was an error, the server will have closed the stream.) this._transportLayer.send( this._makeClosingXml() ); + + this.waitForDisconnect(); + }, + + _handleServerDisconnection: function() { this._transportLayer.disconnect(); + this._connectionStatus = this.NOT_CONNECTED; }, waitForConnection: function( ) { @@ -404,6 +419,7 @@ XmppClient.prototype = { }, waitForDisconnect: function() { + LOG("waitForDisconnect(): starting"); var thread = this._threadManager.currentThread; while ( this._connectionStatus == this.CONNECTED ) { thread.processNextEvent( true ); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index a4d8d1f1a2d6..04958495bfde 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -3,18 +3,17 @@ var Cu = Components.utils; Cu.import( "resource://weave/xmpp/xmppClient.js" ); function LOG(aMsg) { - dump("TEST_XMPP_SIMPLE: " + aMsg + "\n"); + dump("TEST_XMPP: " + aMsg + "\n"); } var serverUrl = "http://127.0.0.1:5280/http-poll"; var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. getService(Ci.nsIDNSService).myHostName; +LOG("DOMAIN: " + jabberDomain); var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); -var alice; - function run_test() { // FIXME: this test hangs when you don't have a server, disabling for now return; @@ -24,26 +23,32 @@ function run_test() { false, 4000 ); var auth = new PlainAuthenticator(); - alice = new XmppClient( "alice", jabberDomain, "iamalice", - transport, auth ); + var alice = new XmppClient("alice", jabberDomain, "iamalice", + transport, auth); // test connection + LOG("connecting"); alice.connect( jabberDomain ); alice.waitForConnection(); do_check_eq( alice._connectionStatus, alice.CONNECTED); + LOG("connected"); - // test re-connection + // test disconnection + LOG("disconnecting"); alice.disconnect(); + do_check_eq( alice._connectionStatus, alice.NOT_CONNECTED); LOG("disconnected"); - alice.connect( jabberDomain ); - LOG("wait"); - alice.waitForConnection(); - LOG("waited"); - do_check_eq( alice._connectionStatus, alice.CONNECTED); /* - // test connection failure + // test re-connection + LOG("reconnecting"); + alice.connect( jabberDomain ); + alice.waitForConnection(); + LOG("reconnected"); + do_check_eq( alice._connectionStatus, alice.CONNECTED); alice.disconnect(); + + // test connection failure alice.connect( "bad domain" ); alice.waitForConnection(); do_check_eq( alice._connectionStatus, alice.FAILED ); @@ -98,8 +103,8 @@ function run_test() { while( !testIsOver ) { currentThread.processNextEvent( true ); } - */ alice.disconnect(); - //bob.disconnect(); + bob.disconnect(); + */ }; From c7d755bf9af596bfedff3d0b2848ad372a8b1a08 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 17:00:02 -0700 Subject: [PATCH 0292/1860] [mq]: xmpp-reconnect --- services/sync/modules/xmpp/readme.txt | 6 +++--- services/sync/modules/xmpp/transportLayer.js | 5 ++++- services/sync/modules/xmpp/xmppClient.js | 9 +++------ services/sync/tests/unit/test_xmpp.js | 14 +++++--------- services/sync/tests/unit/test_xmpp_simple.js | 5 ++--- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/xmpp/readme.txt b/services/sync/modules/xmpp/readme.txt index 071e46a33ef5..29397a9dd13a 100644 --- a/services/sync/modules/xmpp/readme.txt +++ b/services/sync/modules/xmpp/readme.txt @@ -1,6 +1,8 @@ About the XMPP module -Here is sample code demonstrating how client code can use the XMPP module. It assumes that a Jabber server is running on localhost on port 5280. +Here is sample code demonstrating how client code can use the XMPP module. +It assumes that a Jabber server capable of HTTP-Polling is running on localhost +on port 5280. Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); @@ -66,7 +68,6 @@ The ejabberd process is started simply by running: ejabberd/bin/ejabberdctl start - Outstanding Issues -- bugs and things to do. * The test above is failing with a timeout. How to debug this? Let's start @@ -122,4 +123,3 @@ Outstanding Issues -- bugs and things to do. (Everything seems to be working OK with useKeys turned off, but that's less secure.) - diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index de3d63f91f4e..0b08f9d8630d 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -314,7 +314,7 @@ HTTPPollingTransport.prototype = { }, setCallbackObject: function( callbackObject ) { - this._callbackObject = callbackObject; + this._callbackObject = callbackObject; }, notify: function( timer ) { @@ -332,6 +332,9 @@ HTTPPollingTransport.prototype = { }, connect: function() { + // In case this is a reconnect, make sure to re-initialize. + this._init(this._serverUrl, this._useKeys, this._interval); + /* Set up a timer to poll the server periodically. */ // TODO doPost isn't reentrant; don't try to doPost if there's diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 52e326ffe969..77d9ec5ce136 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -7,6 +7,9 @@ const EXPORTED_SYMBOLS = ['XmppClient', 'HTTPPollingTransport', 'PlainAuthentica // http://developer.mozilla.org/en/docs/xpcshell // http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests +// IM level protocol stuff: presence announcements, conversations, etc. +// ftp://ftp.isi.edu/in-notes/rfc3921.txt + var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; @@ -419,15 +422,9 @@ XmppClient.prototype = { }, waitForDisconnect: function() { - LOG("waitForDisconnect(): starting"); var thread = this._threadManager.currentThread; while ( this._connectionStatus == this.CONNECTED ) { thread.processNextEvent( true ); } } - }; - -// IM level protocol stuff: presence announcements, conversations, etc. -// ftp://ftp.isi.edu/in-notes/rfc3921.txt - diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index 04958495bfde..09f718d8f0d5 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -6,10 +6,8 @@ function LOG(aMsg) { dump("TEST_XMPP: " + aMsg + "\n"); } -var serverUrl = "http://127.0.0.1:5280/http-poll"; -var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. - getService(Ci.nsIDNSService).myHostName; -LOG("DOMAIN: " + jabberDomain); +var serverUrl = "http://localhost:5280/http-poll"; +var jabberDomain = "localhost"; var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); @@ -19,12 +17,10 @@ function run_test() { return; /* First, just see if we can connect: */ - var transport = new HTTPPollingTransport( serverUrl, - false, - 4000 ); + var transport = new HTTPPollingTransport(serverUrl, false, 4000); var auth = new PlainAuthenticator(); var alice = new XmppClient("alice", jabberDomain, "iamalice", - transport, auth); + transport, auth); // test connection LOG("connecting"); @@ -39,7 +35,6 @@ function run_test() { do_check_eq( alice._connectionStatus, alice.NOT_CONNECTED); LOG("disconnected"); - /* // test re-connection LOG("reconnecting"); alice.connect( jabberDomain ); @@ -48,6 +43,7 @@ function run_test() { do_check_eq( alice._connectionStatus, alice.CONNECTED); alice.disconnect(); + /* // test connection failure alice.connect( "bad domain" ); alice.waitForConnection(); diff --git a/services/sync/tests/unit/test_xmpp_simple.js b/services/sync/tests/unit/test_xmpp_simple.js index 50b01da295f8..b6b54eabd04b 100644 --- a/services/sync/tests/unit/test_xmpp_simple.js +++ b/services/sync/tests/unit/test_xmpp_simple.js @@ -4,9 +4,8 @@ function LOG(aMsg) { Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); -var serverUrl = "http://127.0.0.1:5280/http-poll"; -var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. - getService(Ci.nsIDNSService).myHostName; +var serverUrl = "http://localhost:5280/http-poll"; +var jabberDomain = "localhost"; function run_test() { // FIXME: this test hangs when you don't have a server, disabling for now From f66813a928a55e83ace01cd6e27f2ca92253ca20 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 17:36:37 -0700 Subject: [PATCH 0293/1860] xmpp-stream-error-handling --- services/sync/modules/xmpp/xmppClient.js | 37 +++++++++++++++--------- services/sync/tests/unit/test_xmpp.js | 9 ++++-- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 77d9ec5ce136..aae2ce171e8c 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -91,13 +91,15 @@ XmppClient.prototype = { LOG("onIncomingData(): rcvd: " + messageText); var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); - if (responseDOM.documentElement.nodeName == "parsererror" ) { - // handle server disconnection - if (messageText.match("^$")) { - this._handleServerDisconnection(); - return; - } + // Handle server disconnection + if (messageText.match("^$")) { + this._handleServerDisconnection(); + return; + } + // Detect parse errors, and attempt to handle them in the valid cases. + + if (responseDOM.documentElement.nodeName == "parsererror" ) { /* Before giving up, remember that XMPP doesn't close the top-level element until the communication is done; this means @@ -123,8 +125,19 @@ XmppClient.prototype = { return; } + // Message is parseable, now look for message-level errors. + var rootElem = responseDOM.documentElement; + var errors = rootElem.getElementsByTagName( "stream:error" ); + if ( errors.length > 0 ) { + this.setError( errors[0].firstChild.nodeName ); + return; + } + + // Stream is valid. + + // Detect and handle mid-authentication steps. if ( this._connectionStatus == this.CALLED_SERVER ) { // skip TLS, go straight to SALS. (encryption should be negotiated // at the HTTP layer, i.e. use HTTPS) @@ -142,14 +155,8 @@ XmppClient.prototype = { return; } + // Detect and handle regular communication. if ( this._connectionStatus == this.CONNECTED ) { - /* Check incoming xml to see if it contains errors, presence info, - or a message: */ - var errors = rootElem.getElementsByTagName( "stream:error" ); - if ( errors.length > 0 ) { - this.setError( errors[0].firstChild.nodeName ); - return; - } var presences = rootElem.getElementsByTagName( "presence" ); if (presences.length > 0 ) { var from = presences[0].getAttribute( "from" ); @@ -157,6 +164,7 @@ XmppClient.prototype = { LOG( "I see that " + from + " is online." ); } } + if ( rootElem.nodeName == "message" ) { this.processIncomingMessage( rootElem ); } else { @@ -167,6 +175,7 @@ XmppClient.prototype = { } } } + if ( rootElem.nodeName == "iq" ) { this.processIncomingIq( rootElem ); } else { @@ -289,7 +298,7 @@ XmppClient.prototype = { connect: function( host ) { // Do the handshake to connect with the server and authenticate. - this._transportLayer.connect(); + this._transportLayer.connect(host); this._transportLayer.setCallbackObject( this ); this._transportLayer.send( this._makeHeaderXml( host ) ); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index 09f718d8f0d5..f5346f74d956 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -20,7 +20,7 @@ function run_test() { var transport = new HTTPPollingTransport(serverUrl, false, 4000); var auth = new PlainAuthenticator(); var alice = new XmppClient("alice", jabberDomain, "iamalice", - transport, auth); + transport, auth); // test connection LOG("connecting"); @@ -43,12 +43,15 @@ function run_test() { do_check_eq( alice._connectionStatus, alice.CONNECTED); alice.disconnect(); - /* - // test connection failure + // TODO test connection failure - no server + // TODO test connection failure - server up, bad URL + + // test connection failure - bad domain alice.connect( "bad domain" ); alice.waitForConnection(); do_check_eq( alice._connectionStatus, alice.FAILED ); + /* // re-connect and move on alice.connect( jabberDomain ); alice.waitForConnection(); From 06bc2edce9c7530515629f7490976dcf6fcfcf85 Mon Sep 17 00:00:00 2001 From: Dietrich Ayala Date: Wed, 4 Jun 2008 18:34:37 -0700 Subject: [PATCH 0294/1860] imported patch xmpp-transport-fault-tolerance-and-test --- services/sync/modules/xmpp/transportLayer.js | 53 ++++++++++++------ services/sync/modules/xmpp/xmppClient.js | 2 +- services/sync/tests/unit/test_xmpp.js | 3 - .../tests/unit/test_xmpp_transport_http.js | 55 +++++++++++++++++++ 4 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 services/sync/tests/unit/test_xmpp_transport_http.js diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index 0b08f9d8630d..55e83b7c332f 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -7,6 +7,15 @@ function LOG(aMsg) { dump("Weave::Transport-HTTP-Poll: " + aMsg + "\n"); } +/* + The interface that should be implemented by any Transport object: + + send( messageXml ); + setCallbackObject(object with .onIncomingData(aStringData) and .onTransportError(aErrorText) ); + connect(); + disconnect(); +*/ + function InputStreamBuffer() { } InputStreamBuffer.prototype = { @@ -141,15 +150,6 @@ SocketClient.prototype = { }; -/* - The interface that should be implemented by any Transport object: - - send( messageXml ); - setCallbackObject( object with .onIncomingData and .onTransportError ); - connect(); - disconnect(); -*/ - /** * Send HTTP requests periodically to the server using a timer. * HTTP POST requests with content-type application/x-www-form-urlencoded. @@ -173,6 +173,8 @@ HTTPPollingTransport.prototype = { this._useKeys = useKeys; this._interval = interval; this._outgoingRetryBuffer = ""; + this._retryCount = 0; + this._retryCap = 0; }, __request: null, @@ -294,19 +296,36 @@ HTTPPollingTransport.prototype = { if we re-send the POST it seems to usually work the second time. So put the message into a buffer and try again later: */ - self._outgoingRetryBuffer = requestXml; + if (self._retryCount >= self._retryCap) { + self._onError("Maximum number of retries reached. Unable to communicate with the server."); + } + else { + self._outgoingRetryBuffer = requestXml; + self._retryCount++; + } + } + else if (request.status == 404) { + self._onError("Provided URL is not valid."); + } + else { + self._onError("Unable to communicate with the server."); } } } }; - request.open( "POST", this._serverUrl, true ); //async = true - request.setRequestHeader( "Content-type", "application/x-www-form-urlencoded;charset=UTF-8" ); - request.setRequestHeader( "Content-length", contents.length ); - request.setRequestHeader( "Connection", "close" ); - request.onreadystatechange = _processReqChange; - LOG("Sending: " + contents); - request.send( contents ); + try { + request.open( "POST", this._serverUrl, true ); //async = true + request.setRequestHeader( "Content-type", "application/x-www-form-urlencoded;charset=UTF-8" ); + request.setRequestHeader( "Content-length", contents.length ); + request.setRequestHeader( "Connection", "close" ); + request.onreadystatechange = _processReqChange; + LOG("Sending: " + contents); + request.send( contents ); + } catch(ex) { + this._onError("Unable to send message to server: " + this._serverUrl); + LOG("Connection failure: " + ex); + } }, send: function( messageXml ) { diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index aae2ce171e8c..2ddc7fdcd28c 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -298,7 +298,7 @@ XmppClient.prototype = { connect: function( host ) { // Do the handshake to connect with the server and authenticate. - this._transportLayer.connect(host); + this._transportLayer.connect(); this._transportLayer.setCallbackObject( this ); this._transportLayer.send( this._makeHeaderXml( host ) ); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index f5346f74d956..60396b2e054c 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -43,9 +43,6 @@ function run_test() { do_check_eq( alice._connectionStatus, alice.CONNECTED); alice.disconnect(); - // TODO test connection failure - no server - // TODO test connection failure - server up, bad URL - // test connection failure - bad domain alice.connect( "bad domain" ); alice.waitForConnection(); diff --git a/services/sync/tests/unit/test_xmpp_transport_http.js b/services/sync/tests/unit/test_xmpp_transport_http.js new file mode 100644 index 000000000000..73b2896465fe --- /dev/null +++ b/services/sync/tests/unit/test_xmpp_transport_http.js @@ -0,0 +1,55 @@ +function LOG(aMsg) { + dump("TEST_XMPP_TRANSPORT_HTTP: " + aMsg + "\n"); +} + +Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); + +var tests = []; + +// test connection failure - no server +tests.push(function run_test_bad_server() { + LOG("starting test: bad server"); + + var transport = new HTTPPollingTransport("this is not a server URL", false, 4000); + transport.connect(); + transport.setCallbackObject({ + onIncomingData: function(aData) { + do_throw("onIncomingData was called instead of onTransportError, for a bad URL"); + }, + onTransportError: function(aErrorMessage) { + do_check_true(/^Unable to send message to server:/.test(aErrorMessage)); + // continue test suite + tests.shift()(); + } + }); + transport.send(); +}); + +tests.push(function run_test_bad_url() { + LOG("starting test: bad url"); + // test connection failure - server up, bad URL + var serverUrl = "http://localhost:5280/http-polly-want-a-cracker"; + var transport = new HTTPPollingTransport(serverUrl, false, 4000); + transport.connect(); + transport.setCallbackObject({ + onIncomingData: function(aData) { + do_throw("onIncomingData was called instead of onTransportError, for a bad URL"); + }, + onTransportError: function(aErrorMessage) { + LOG("ERROR: " + aErrorMessage); + do_check_true(/^Provided URL is not valid./.test(aErrorMessage)); + do_test_finished(); + } + }); + transport.send(); +}); + +function run_test() { + // FIXME: this test hangs when you don't have a server, disabling for now + return; + + // async test + do_test_pending(); + + tests.shift()(); +} From 872ff6a21ad09a93f0e6d6eeed9e2a18e5a165a7 Mon Sep 17 00:00:00 2001 From: Date: Wed, 4 Jun 2008 20:30:37 -0700 Subject: [PATCH 0295/1860] The menu icon of a bookmark folder now changes when that folder is being shared out with others. --- services/sync/tests/unit/test_xmpp_simple.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_xmpp_simple.js b/services/sync/tests/unit/test_xmpp_simple.js index 50b01da295f8..1079aab72b36 100644 --- a/services/sync/tests/unit/test_xmpp_simple.js +++ b/services/sync/tests/unit/test_xmpp_simple.js @@ -10,7 +10,7 @@ var jabberDomain = Cc["@mozilla.org/network/dns-service;1"]. function run_test() { // FIXME: this test hangs when you don't have a server, disabling for now - return; + // return; // async test do_test_pending(); From e34040aaf71460cae1f0938ac4ba224359707eac Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 5 Jun 2008 16:17:32 +0900 Subject: [PATCH 0296/1860] create objects for the various server resources (these will later abstract away the actual resources); fix a bug where we were reversing the order of resource filters on every GET request --- services/sync/modules/engines.js | 22 ++-- services/sync/modules/remote.js | 176 ++++++++++++++++++++++++------- 2 files changed, 148 insertions(+), 50 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 776842a2879d..62497864e6b6 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -196,15 +196,7 @@ Engine.prototype = { if ("none" == Utils.prefs.getCharPref("encryption")) return; - this._remote.keys.get(self.cb); - yield; - let keys = this._remote.keys.data; - - if (!keys || !keys.ring || !keys.ring[this._engineId.userHash]) - throw "Keyring does not contain a key for this user"; - - Crypto.RSAdecrypt.async(Crypto, self.cb, - keys.ring[this._engineId.userHash], this._pbeId); + this._remote.keys.getKey(self.cb, this._pbeId); let symkey = yield; this._engineId.setTempPassword(symkey); @@ -284,6 +276,8 @@ Engine.prototype = { if (!ret) throw "Could not create remote folder"; + this._remote.initSession(); + // 1) Fetch server deltas this._getServerData.async(this, self.cb); let server = yield; @@ -545,11 +539,11 @@ Engine.prototype = { this._log.info("Local snapshot is out of date"); this._log.info("Downloading server snapshot"); - this._remote.snapshot.get(self.cb); // fixme: doesn't use status.snapEncryption + this._remote.snapshot.get(self.cb); snap.data = yield; this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); // fixme: doesn't use status.deltasEncryption + this._remote.deltas.get(self.cb); allDeltas = yield; deltas = allDeltas; @@ -559,7 +553,7 @@ Engine.prototype = { snap.data = Utils.deepCopy(this._snapshot.data); this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); // fixme: doesn't use status.deltasEncryption + this._remote.deltas.get(self.cb); allDeltas = yield; deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); @@ -569,7 +563,7 @@ Engine.prototype = { // FIXME: could optimize this case by caching deltas file this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); // fixme: doesn't use status.deltasEncryption + this._remote.deltas.get(self.cb); allDeltas = yield; deltas = []; @@ -627,7 +621,7 @@ Engine.prototype = { throw "Could not encrypt symmetric encryption key"; let keys = {ring: {}}; - keys.ring[this._engineId.userHash] = enckey; + keys.ring[this._engineId.username] = enckey; this._remote.keys.put(self.cb, keys); yield; diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 8a008547a79d..309feb2baf00 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -207,7 +207,8 @@ Resource.prototype = { } if ("GET" == action) { - for each (let filter in this._filters.reverse()) { + let filters = this._filters.slice(); // reverse() mutates, so we copy + for each (let filter in filters.reverse()) { filter.afterGET.async(filter, self.cb, this._data); this._data = yield; } @@ -270,68 +271,171 @@ JsonFilter.prototype = { } }; -// FIXME: doesn't use the crypto algorithm stored in the status file -function PBECryptoFilter(identity) { - this._identity = identity; - this._log = Log4Moz.Service.getLogger("Service.PBECryptoFilter"); +function CryptoFilter(remoteStore, algProp) { + this._remote = remoteStore; + this._algProp = algProp; // hackish, but we don't know if it's a delta or snap + this._log = Log4Moz.Service.getLogger("Service.CryptoFilter"); } -PBECryptoFilter.prototype = { +CryptoFilter.prototype = { __proto__: new ResourceFilter(), - beforePUT: function PBEFilter_beforePUT(data) { + beforePUT: function CryptoFilter_beforePUT(data) { let self = yield; this._log.debug("Encrypting data") - Crypto.PBEencrypt.async(Crypto, self.cb, data, ID.get(this._identity)); + Crypto.PBEencrypt.async(Crypto, self.cb, data, ID.get(this._remote.cryptoId)); let ret = yield; self.done(ret); }, - afterGET: function PBEFilter_afterGET(data) { + afterGET: function CryptoFilter_afterGET(data) { let self = yield; this._log.debug("Decrypting data") - Crypto.PBEdecrypt.async(Crypto, self.cb, data, ID.get(this._identity)); + if (!this._remote.status.data) + throw "Remote status must be initialized before crypto filter can be used" + let alg = this._remote.status.data[this._algProp]; + Crypto.PBEdecrypt.async(Crypto, self.cb, data, ID.get(this._remote.cryptoId)); let ret = yield; self.done(ret); } }; +function Status(remoteStore) { + this._init(remoteStore); +} +Status.prototype = { + __proto__: new Resource(), + _init: function Status__init(remoteStore) { + this._remote = remoteStore; + this.__proto__.__proto__._init(this._remote.serverPrefix + "status.json"); + this.pushFilter(new JsonFilter()); + } +}; + +function Keychain(remoteStore) { + this._init(remoteStore); +} +Keychain.prototype = { + __proto__: new Resource(), + _init: function Keychain__init(remoteStore) { + this._remote = remoteStore; + this.__proto__.__proto__._init(this._remote.serverPrefix + "keys.json"); + this.pushFilter(new JsonFilter()); + }, + _getKey: function Keychain__getKey(identity) { + let self = yield; + + this.get(self.cb); + yield; + if (!this.data || !this.data.ring || !this.data.ring[identity.username]) + throw "Keyring does not contain a key for this user"; + Crypto.RSAdecrypt.async(Crypto, self.cb, + this.data.ring[identity.username], identity); + let symkey = yield; + + self.done(symkey); + }, + getKey: function Keychain_getKey(onComplete, identity) { + this._getKey.async(this, onComplete, identity); + } + // FIXME: implement setKey() +}; + +function Snapshot(remoteStore) { + this._init(remoteStore); +} +Snapshot.prototype = { + __proto__: new Resource(), + _init: function Snapshot__init(remoteStore) { + this._remote = remoteStore; + this.__proto__.__proto__._init(this._remote.serverPrefix + "snapshot.json"); + this.pushFilter(new JsonFilter()); + this.pushFilter(new CryptoFilter(remoteStore, "snapshotEncryption")); + } +}; + +function Deltas(remoteStore) { + this._init(remoteStore); +} +Deltas.prototype = { + __proto__: new Resource(), + _init: function Deltas__init(remoteStore) { + this._remote = remoteStore; + this.__proto__.__proto__._init(this._remote.serverPrefix + "deltas.json"); + this.pushFilter(new JsonFilter()); + this.pushFilter(new CryptoFilter(remoteStore, "deltasEncryption")); + } +}; + function RemoteStore(serverPrefix, cryptoId) { this._prefix = serverPrefix; this._cryptoId = cryptoId; - this._init(); } RemoteStore.prototype = { - _init: function RemoteStore__init() { - if (!this._prefix || !this._cryptoId) - return; - let json = new JsonFilter(); - let crypto = new PBECryptoFilter(this._cryptoId); - this._status = new Resource(this._prefix + "status.json"); - this._status.pushFilter(json); - this._keys = new Resource(this._prefix + "keys.json"); - this._keys.pushFilter(new JsonFilter()); - this._snapshot = new Resource(this._prefix + "snapshot.json"); - this._snapshot.pushFilter(json); - this._snapshot.pushFilter(crypto); - this._deltas = new Resource(this._prefix + "deltas.json"); - this._deltas.pushFilter(json); - this._deltas.pushFilter(crypto); - }, - - get status() this._status, - get keys() this._keys, - get snapshot() this._snapshot, - get deltas() this._deltas, - get serverPrefix() this._prefix, set serverPrefix(value) { this._prefix = value; - this._init(); + this.status.serverPrefix = value; + this.keys.serverPrefix = value; + this.snapshot.serverPrefix = value; + this.deltas.serverPrefix = value; }, get cryptoId() this._cryptoId, set cryptoId(value) { - this._cryptoId = value; - this._init(); + this.__cryptoId = value; + // FIXME: do we need to reset anything here? + }, + + get status() { + let status = new Status(this); + this.__defineGetter__("status", function() status); + return status; + }, + + get keys() { + let keys = new Keychain(this); + this.__defineGetter__("keys", function() keys); + return keys; + }, + + get snapshot() { + let snapshot = new Snapshot(this); + this.__defineGetter__("snapshot", function() snapshot); + return snapshot; + }, + + get deltas() { + let deltas = new Deltas(this); + this.__defineGetter__("deltas", function() deltas); + return deltas; + }, + + initSession: function RStore_initSession(serverPrefix, cryptoId) { + let self = yield; + + if (serverPrefix) + this.serverPrefix = serverPrefix; + if (cryptoId) + this.cryptoId = cryptoId; + if (!this.serverPrefix || !this.cryptoId) + throw "RemoteStore: cannot initialize without a server prefix and crypto ID"; + + this.status.data = null; + this.keys.data = null; + this.snapshot.data = null; + this.deltas.data = null; + + this.status.get(self.cb); + yield; + + this._inited = true; + }, + + closeSession: function RStore_closeSession() { + this._inited = false; + this.status.data = null; + this.keys.data = null; + this.snapshot.data = null; + this.deltas.data = null; } }; From 39148f9c35237b9399bb132cfdc43411126930ac Mon Sep 17 00:00:00 2001 From: Date: Thu, 5 Jun 2008 12:18:16 -0700 Subject: [PATCH 0297/1860] Re-disabled test_xmpp_simple.js; it shouldn't be run without a local jabber server (and it wasn't, until I accidentally comitted my local change enabling the test. --- services/sync/tests/unit/test_xmpp_simple.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_xmpp_simple.js b/services/sync/tests/unit/test_xmpp_simple.js index fb0f85185a8b..b6b54eabd04b 100644 --- a/services/sync/tests/unit/test_xmpp_simple.js +++ b/services/sync/tests/unit/test_xmpp_simple.js @@ -9,7 +9,7 @@ var jabberDomain = "localhost"; function run_test() { // FIXME: this test hangs when you don't have a server, disabling for now - // return; + return; // async test do_test_pending(); From 932c5d2e1771a1a40a9c634a53c768273a71db0f Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Thu, 5 Jun 2008 16:25:55 -0700 Subject: [PATCH 0298/1860] bug 437523: fix NS_ERROR_FAILURE on nsIJSON.decode in JsonFilter_afterGET --- services/sync/modules/engines.js | 2 +- services/sync/modules/engines/tabs.js | 2 +- services/sync/modules/remote.js | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 20beb82a05aa..20cf1b1aafeb 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -413,7 +413,7 @@ Engine.prototype = { this._log.error("Could not upload files to server"); // eep? } else { - this._remote.deltas.put(self.cb, this._serializeCommands(server.deltas)); + this._remote.deltas.put(self.cb, server.deltas); yield; let c = 0; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index bb53a191793f..9ebae8da490a 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -207,7 +207,7 @@ TabStore.prototype = { _init: function TabStore__init() { this._restoreVirtualTabs(); - this.__proto__.__proto__._init(); + this.__proto__.__proto__._init.call(this); }, /** diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 309feb2baf00..0caf0d58e993 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -260,13 +260,13 @@ JsonFilter.prototype = { beforePUT: function JsonFilter_beforePUT(data) { let self = yield; - this._log.debug("Encoding data as JSON") + this._log.debug("Encoding data as JSON"); self.done(this._json.encode(data)); }, afterGET: function JsonFilter_afterGET(data) { let self = yield; - this._log.debug("Decoding JSON data") + this._log.debug("Decoding JSON data"); self.done(this._json.decode(data)); } }; @@ -281,7 +281,7 @@ CryptoFilter.prototype = { beforePUT: function CryptoFilter_beforePUT(data) { let self = yield; - this._log.debug("Encrypting data") + this._log.debug("Encrypting data"); Crypto.PBEencrypt.async(Crypto, self.cb, data, ID.get(this._remote.cryptoId)); let ret = yield; self.done(ret); @@ -289,7 +289,7 @@ CryptoFilter.prototype = { afterGET: function CryptoFilter_afterGET(data) { let self = yield; - this._log.debug("Decrypting data") + this._log.debug("Decrypting data"); if (!this._remote.status.data) throw "Remote status must be initialized before crypto filter can be used" let alg = this._remote.status.data[this._algProp]; @@ -306,7 +306,7 @@ Status.prototype = { __proto__: new Resource(), _init: function Status__init(remoteStore) { this._remote = remoteStore; - this.__proto__.__proto__._init(this._remote.serverPrefix + "status.json"); + this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "status.json"); this.pushFilter(new JsonFilter()); } }; @@ -318,7 +318,7 @@ Keychain.prototype = { __proto__: new Resource(), _init: function Keychain__init(remoteStore) { this._remote = remoteStore; - this.__proto__.__proto__._init(this._remote.serverPrefix + "keys.json"); + this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "keys.json"); this.pushFilter(new JsonFilter()); }, _getKey: function Keychain__getKey(identity) { @@ -347,7 +347,7 @@ Snapshot.prototype = { __proto__: new Resource(), _init: function Snapshot__init(remoteStore) { this._remote = remoteStore; - this.__proto__.__proto__._init(this._remote.serverPrefix + "snapshot.json"); + this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "snapshot.json"); this.pushFilter(new JsonFilter()); this.pushFilter(new CryptoFilter(remoteStore, "snapshotEncryption")); } @@ -360,7 +360,7 @@ Deltas.prototype = { __proto__: new Resource(), _init: function Deltas__init(remoteStore) { this._remote = remoteStore; - this.__proto__.__proto__._init(this._remote.serverPrefix + "deltas.json"); + this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "deltas.json"); this.pushFilter(new JsonFilter()); this.pushFilter(new CryptoFilter(remoteStore, "deltasEncryption")); } From 8660c1b670ffadba17fb432868e068a5ff4db779 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 5 Jun 2008 17:21:53 -0700 Subject: [PATCH 0299/1860] Fix linux builds (bug 433922, r=thunder) --- services/crypto/Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 54f0475d4a50..d83052475b01 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -162,6 +162,8 @@ else ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ -Wl,-dead_strip \ -Wl,-exported_symbol \ + -Wl,-z,defs -Wl,-h,WeaveCrypto.so \ + -Wl,-rpath-link,$(sdkdir)/bin \ $(sdkdir)/lib/libxpcomglue_s.a \ $(libdirs) $(libs) else @@ -213,6 +215,10 @@ $(idl_typelib): $(idl) $(cpp_objects): $(cpp_sources) ifeq ($(cxx),cl) $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) +endif +ifeq ($(os),Linux) + # don't need object files, but we need to satisfy make + touch $(cpp_objects) else $(cxx) -o $@ $(cppflags) $(@:.o=.cpp) endif @@ -226,7 +232,7 @@ $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) else ifeq ($(os),Linux) $(so_target): $(idl_headers) $(cpp_objects) - $(cxx) -o $@ $(cppflags) $(ldflags) $(cpp_sources) + $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) else $(so_target): $(idl_headers) $(cpp_objects) $(cxx) -o $@ $(ldflags) $(cpp_objects) From 0f9dfd1cfba657a42a51d5bdeec8d334e6e8fc2f Mon Sep 17 00:00:00 2001 From: Date: Fri, 6 Jun 2008 14:18:50 -0700 Subject: [PATCH 0300/1860] - this check in will break everyone temporarily, as it involves the changes necessary to shift us from sha1(email) to usernames, and to enable sharing on the server. - we are also changing the default preferences on the trunk to point to the new staging server at https://sm-labs01.mozilla.org:81 that has been modified to support usernames - everyone will need to create a new account and this will be streamlined within the startup function, which will now kick off on first run (we'll check in the updated setup wizard shortly) - this checkin also cleans up a number of strings --- services/sync/locales/en-US/login.dtd | 4 ++-- services/sync/locales/en-US/preferences.dtd | 6 +++++- services/sync/locales/en-US/share.dtd | 4 ++-- services/sync/locales/en-US/sync.dtd | 3 +-- services/sync/locales/en-US/wizard.dtd | 2 +- services/sync/locales/en-US/wizard.properties | 2 +- services/sync/modules/constants.js | 2 +- services/sync/modules/engines.js | 6 ++---- services/sync/modules/engines/bookmarks.js | 3 +-- services/sync/modules/service.js | 4 ++-- services/sync/services-sync.js | 12 ++++++------ 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/services/sync/locales/en-US/login.dtd b/services/sync/locales/en-US/login.dtd index 501b5bb33b67..ba5927dea3f9 100644 --- a/services/sync/locales/en-US/login.dtd +++ b/services/sync/locales/en-US/login.dtd @@ -1,6 +1,6 @@ - + - + diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 40f1e185dfc8..12eab2060011 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -20,11 +20,15 @@ - + + + + + diff --git a/services/sync/locales/en-US/share.dtd b/services/sync/locales/en-US/share.dtd index ac1ffdbeb5f2..c14d4873cf00 100644 --- a/services/sync/locales/en-US/share.dtd +++ b/services/sync/locales/en-US/share.dtd @@ -1,6 +1,6 @@ - - + + diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 8e4e52ee10a8..e548bbd19a89 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -2,8 +2,7 @@ - - + diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 592339da5d9a..96ad1c5b41d9 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -16,7 +16,7 @@ - + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 6f3abbf330c3..da6ce0fe1993 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -10,4 +10,4 @@ initStatusSyncFailed.label = Status: Transfer Failed invalidCredentials.alert = You must provide a valid Weave user name and password to continue. noPassphrase.alert = You must enter a passphrase -samePasswordAndPassphrase.alert = Your password and passphrase must be different +samePasswordAndPassphrase.alert = Your password and passphrase must be different \ No newline at end of file diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 9e447fe00506..107099dc9ebb 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.30"; +const WEAVE_VERSION = "0.1.31"; const STORAGE_FORMAT_VERSION = 2; const ENGINE_STORAGE_FORMAT_VERSION = 2; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 20cf1b1aafeb..1cc1ff188427 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -665,11 +665,9 @@ Engine.prototype = { let keys = this._json.decode(ret.responseText); // get the other user's pubkey - let hash = Utils.sha1(username); let serverURL = Utils.prefs.getCharPref("serverURL"); - try { - DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly! + DAV.defaultPrefix = "user/" + username + "/"; //FIXME: very ugly! DAV.GET("public/pubkey", self.cb); ret = yield; } @@ -687,7 +685,7 @@ Engine.prototype = { if (!enckey) throw "Could not encrypt symmetric encryption key"; - keys.ring[hash] = enckey; + keys.ring[username] = enckey; DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload keyring file."); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5f3226f80520..612acdd1ce83 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -73,8 +73,7 @@ BookmarksEngine.prototype = { this._log.debug("Syncing shared folder from user " + user); try { - let hash = Utils.sha1(user); - DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly! + DAV.defaultPrefix = "user/" + user + "/"; //FIXME: very ugly! this._getSymKey.async(this, self.cb); yield; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 36911ed47c56..458c0db0954a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -157,7 +157,7 @@ WeaveSvc.prototype = { get passphrase() { return ID.get('WeaveCryptoID').password; }, set passphrase(value) { ID.get('WeaveCryptoID').password = value; }, - get userPath() { return ID.get('WeaveID').userHash; }, + get userPath() { return ID.get('WeaveID').username; }, get currentUser() { if (this._loggedIn) @@ -178,7 +178,7 @@ WeaveSvc.prototype = { onWindowOpened: function Weave__onWindowOpened() { if (!this._startupFinished) { if (Utils.prefs.getBoolPref("autoconnect") && - this.username && this.username != 'nobody@mozilla.com') { + this.username && this.username != 'nobody') { // Login, then sync. let self = this; this.login(function() { self.sync(); }); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 561ee2937afe..6825d0f26a62 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,5 +1,5 @@ -pref("extensions.weave.serverURL", "https://services.mozilla.com/"); -pref("extensions.weave.username", "nobody@mozilla.com"); +pref("extensions.weave.serverURL", "https://sm-labs01.mozilla.org:81/"); +pref("extensions.weave.username", "nobody"); pref("extensions.weave.encryption", "aes-256-cbc"); @@ -14,10 +14,10 @@ pref("extensions.weave.enabled", true); pref("extensions.weave.schedule", 1); pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); -pref("extensions.weave.engine.cookies", false ); -pref("extensions.weave.engine.passwords", false ); -pref("extensions.weave.engine.forms", false ); -pref("extensions.weave.engine.tabs", false); +pref("extensions.weave.engine.cookies", true ); +pref("extensions.weave.engine.passwords", true ); +pref("extensions.weave.engine.forms", true ); +pref("extensions.weave.engine.tabs", true); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From d302d00bc4d57cce176b4e72f8afc021ee6d4a8a Mon Sep 17 00:00:00 2001 From: Date: Fri, 6 Jun 2008 14:40:35 -0700 Subject: [PATCH 0301/1860] - turning in the bookmarks sharing UI, although it's still not fully functional. --- services/sync/services-sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 6825d0f26a62..5769981402aa 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -7,11 +7,12 @@ pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.lastsync", "0"); pref("extensions.weave.ui.syncnow", true); -pref("extensions.weave.ui.sharebookmarks", false); +pref("extensions.weave.ui.sharebookmarks", true); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); pref("extensions.weave.schedule", 1); + pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.cookies", true ); From 5fe713d6c70f50c048e1d03a50f7e9ce22d62baa Mon Sep 17 00:00:00 2001 From: Date: Fri, 6 Jun 2008 17:33:44 -0700 Subject: [PATCH 0302/1860] Moved _createShare and _share() from engines.js to BookmarkEngine class in engines/bookmarks.js. The identity of the folder to be shared is now passed from the share dialog box (share.xul) into BookmarkEngine._share(). --- services/sync/modules/engines.js | 92 ++--------------- services/sync/modules/engines/bookmarks.js | 112 ++++++++++++++++++++- services/sync/modules/service.js | 33 ++++-- 3 files changed, 145 insertions(+), 92 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 20beb82a05aa..a8895ad628d7 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -649,97 +649,21 @@ Engine.prototype = { self.done(ret); }, - _share: function Engine__share(username) { - let self = yield; - let prefix = DAV.defaultPrefix; - - this._log.debug("Sharing bookmarks with " + username); - - this._getSymKey.async(this, self.cb); - yield; - - // copied from getSymKey - DAV.GET(this.keysFile, self.cb); - let ret = yield; - Utils.ensureStatus(ret.status, "Could not get keys file."); - let keys = this._json.decode(ret.responseText); - - // get the other user's pubkey - let hash = Utils.sha1(username); - let serverURL = Utils.prefs.getCharPref("serverURL"); - - try { - DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly! - DAV.GET("public/pubkey", self.cb); - ret = yield; - } - catch (e) { throw e; } - finally { DAV.defaultPrefix = prefix; } - - Utils.ensureStatus(ret.status, "Could not get public key for " + username); - - let id = new Identity(); - id.pubkey = ret.responseText; - - // now encrypt the symkey with their pubkey and upload the new keyring - Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, id); - let enckey = yield; - if (!enckey) - throw "Could not encrypt symmetric encryption key"; - - keys.ring[hash] = enckey; - DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); - ret = yield; - Utils.ensureStatus(ret.status, "Could not upload keyring file."); - - this._createShare(username, username); - - this._log.debug("All done sharing!"); - - self.done(true); + _share: function Engine__share(guid, username) { + /* This should be overridden by the engine subclass for each datatype. + Implementation should share the data node identified by guid, + and all its children, if any, with the user identified by username. */ + return; }, - // FIXME: EEK bookmarks specific - _createShare: function Engine__createShare(id, title) { - let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - let ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - - let root; - let a = ans.getItemsWithAnnotation("weave/mounted-shares-folder", {}); - if (a.length == 1) - root = a[0]; - - if (!root) { - root = bms.createFolder(bms.toolbarFolder, "Shared Folders", - bms.DEFAULT_INDEX); - ans.setItemAnnotation(root, "weave/mounted-shares-folder", true, 0, - ans.EXPIRE_NEVER); - } - - let item; - a = ans.getItemsWithAnnotation("weave/mounted-share-id", {}); - for (let i = 0; i < a.length; i++) { - if (ans.getItemAnnotation(a[i], "weave/mounted-share-id") == id) { - item = a[i]; - break; - } - } - - if (!item) { - let newId = bms.createFolder(root, title, bms.DEFAULT_INDEX); - ans.setItemAnnotation(newId, "weave/mounted-share-id", id, 0, - ans.EXPIRE_NEVER); - } - }, + // TODO need a "stop sharing" function. sync: function Engine_sync(onComplete) { return this._sync.async(this, onComplete); }, - share: function Engine_share(onComplete, username) { - return this._share.async(this, onComplete, username); + share: function Engine_share(onComplete, guid, username) { + return this._share.async(this, onComplete, guid, username); }, resetServer: function Engine_resetServer(onComplete) { diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5f3226f80520..3d58c4199f22 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -63,6 +63,108 @@ BookmarksEngine.prototype = { } }, + // TODO modify this as neccessary since I just moved it from the engine + // superclass into BookmarkEngine. + _share: function BookmarkEngine__share(guid, username) { + let self = yield; + let prefix = DAV.defaultPrefix; + + this._log.debug("Sharing bookmarks with " + username); + + this._getSymKey.async(this, self.cb); + yield; + + // copied from getSymKey + DAV.GET(this.keysFile, self.cb); + let ret = yield; + Utils.ensureStatus(ret.status, "Could not get keys file."); + let keys = this._json.decode(ret.responseText); + + // get the other user's pubkey + let serverURL = Utils.prefs.getCharPref("serverURL"); + + try { + DAV.defaultPrefix = "user/" + username + "/"; + DAV.GET("public/pubkey", self.cb); + ret = yield; + } + catch (e) { throw e; } + finally { DAV.defaultPrefix = prefix; } + + Utils.ensureStatus(ret.status, "Could not get public key for " + username); + + let id = new Identity(); + id.pubkey = ret.responseText; + + // now encrypt the symkey with their pubkey and upload the new keyring + Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, id); + let enckey = yield; + if (!enckey) + throw "Could not encrypt symmetric encryption key"; + + keys.ring[username] = enckey; + DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); + ret = yield; + Utils.ensureStatus(ret.status, "Could not upload keyring file."); + + this._createShare(guid, username, username); + + this._log.debug("All done sharing!"); + + self.done(true); + }, + + _createShare: function BookmarkEngine__createShare(guid, id, title) { + /* the bookmark item identified by guid, and the whole subtree under it, + must be copied out from the main file into a separate file which is + put into the new directory and encrypted with the key in the keychain. + id is the userid of the user we're sharing with. + */ + + /* TODO it appears that this just creates the folder and puts the + annotation on it; the mechanics of sharing must be done when syncing? + + Or has that not been done yet at all? + Do we have to create a new Mount? + */ + let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + let ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + + let root; + let a = ans.getItemsWithAnnotation("weave/mounted-shares-folder", {}); + if (a.length == 1) + root = a[0]; + + if (!root) { + root = bms.createFolder(bms.toolbarFolder, "Shared Folders", + bms.DEFAULT_INDEX); + ans.setItemAnnotation(root, "weave/mounted-shares-folder", true, 0, + ans.EXPIRE_NEVER); + } + + let item; + a = ans.getItemsWithAnnotation("weave/mounted-share-id", {}); + for (let i = 0; i < a.length; i++) { + if (ans.getItemAnnotation(a[i], "weave/mounted-share-id") == id) { + item = a[i]; + break; + } + } + + if (!item) { + let newId = bms.createFolder(root, title, bms.DEFAULT_INDEX); + ans.setItemAnnotation(newId, "weave/mounted-share-id", id, 0, + ans.EXPIRE_NEVER); + } + }, + + _stopShare: function BookmarkeEngine__stopShare( guid, username) { + // TODO implement this; also give a way to call it from outside + // the service. + }, + _syncOneMount: function BmkEngine__syncOneMount(mountData) { let self = yield; let user = mountData.userid; @@ -70,11 +172,11 @@ BookmarksEngine.prototype = { let serverURL = Utils.prefs.getCharPref("serverURL"); let snap = new SnapshotStore(); + // TODO this is obviously what we want. this._log.debug("Syncing shared folder from user " + user); try { - let hash = Utils.sha1(user); - DAV.defaultPrefix = "user/" + hash + "/"; //FIXME: very ugly! + DAV.defaultPrefix = "user/" + user + "/"; this._getSymKey.async(this, self.cb); yield; @@ -355,6 +457,9 @@ BookmarksStore.prototype = { command.data.index); break; case "mounted-share": + // TODO this is to create the shared-items folder on another machine + // to duplicate the one that's on the first machine; we don't need that + // anymore. OR is it to create the folder on the sharee's computer? this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\""); newId = this._bms.createFolder(parentId, command.data.title, @@ -519,6 +624,8 @@ BookmarksStore.prototype = { } else if (this._ans.itemHasAnnotation(node.itemId, "weave/mounted-share-id")) { + /* TODO this is for wrapping the special shared folder created by + the old-style share command. */ item.type = "mounted-share"; item.title = node.title; item.mountId = this._ans.getItemAnnotation(node.itemId, @@ -609,6 +716,7 @@ BookmarksStore.prototype = { // remove any share mountpoints for (let guid in ret.snapshot) { + // TODO decide what to do with this... if (ret.snapshot[guid].type == "mounted-share") delete ret.snapshot[guid]; } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 36911ed47c56..dfd679e2d8a4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -561,16 +561,37 @@ WeaveSvc.prototype = { } }, - shareBookmarks: function WeaveSync_shareBookmarks(onComplete, username) { - this._lock(this._notify("share-bookmarks", - this._shareBookmarks, + shareData: function WeaveSync_shareData(dataType, + onComplete, + guid, + username) { + /* Shares data of the specified datatype (which must correspond to + one of the registered engines) with the user specified by username. + The data node indicated by guid will be shared, along with all its + children, if it has any. onComplete is a function that will be called + when sharing is done; it takes an argument that will be true or false + to indicate whether sharing succeeded or failed. + Implementation, as well as the interpretation of what 'guid' means, + is left up to the engine for the specific dataType. */ + + // TODO who is listening for the share-bookmarks message? + let messageName = "share-" + dataType; + // so for instance, if dataType is "bookmarks" then a message + // "share-bookmarks" will be sent out to any observers who are listening + // for it. + this._lock(this._notify(messageName, + this._shareData, + dataType, + guid, username)).async(this, onComplete); }, - _shareBookmarks: function WeaveSync__shareBookmarks(username) { + _shareBookmarks: function WeaveSync__shareBookmarks(dataType, + guid, + username) { let self = yield; - if (Engines.get("bookmarks").enabled) + if (Engines.get(dataType).enabled) return; - Engines.get("bookmarks").share(self.cb, username); + Engines.get(dataType).share(self.cb, guid, username); let ret = yield; self.done(ret); } From c04bd0bc2741e35199f74f4ba73217ee48f077c6 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 6 Jun 2008 17:46:34 -0700 Subject: [PATCH 0303/1860] In async.js, renamed 'object' to 'thisArg', which makes the code clearer and easier to understand. --- services/sync/modules/async.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 45b9c3d14658..93e9f2f0a4d0 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -68,11 +68,11 @@ AsyncException.prototype = { } }; -function Generator(object, method, onComplete, args) { +function Generator(thisArg, method, onComplete, args) { this._log = Log4Moz.Service.getLogger("Async.Generator"); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; - this._object = object; + this._thisArg = thisArg; this._method = method; this.onComplete = onComplete; this._args = args; @@ -93,11 +93,11 @@ Generator.prototype = { }, get listener() { return new Utils.EventListener(this.cb); }, - get _object() { return this.__object; }, - set _object(value) { + get _thisArg() { return this.__thisArg; }, + set _thisArg(value) { if (typeof value != "object") throw "Generator: expected type 'object', got type '" + typeof(value) + "'"; - this.__object = value; + this.__thisArg = value; }, get _method() { return this.__method; }, @@ -161,7 +161,7 @@ Generator.prototype = { run: function AsyncGen_run() { this._continued = false; try { - this._generator = this._method.apply(this._object, this._args); + this._generator = this._method.apply(this._thisArg, this._args); this.generator.next(); // must initialize before sending this.generator.send(this); } catch (e) { @@ -267,9 +267,9 @@ Async = { // where fooGen is a generator function, and gen is a Generator instance // ret is whatever the generator 'returns' via Generator.done(). - run: function Async_run(object, method, onComplete /* , arg1, arg2, ... */) { + run: function Async_run(thisArg, method, onComplete /* , arg1, arg2, ... */) { let args = Array.prototype.slice.call(arguments, 3); - let gen = new Generator(object, method, onComplete, args); + let gen = new Generator(thisArg, method, onComplete, args); gen.run(); return gen; }, @@ -287,9 +287,9 @@ Async = { // Note that 'this' refers to the method being called, not the // Async object. - sugar: function Async_sugar(object, onComplete /* , arg1, arg2, ... */) { + sugar: function Async_sugar(thisArg, onComplete /* , arg1, arg2, ... */) { let args = Array.prototype.slice.call(arguments, 1); - args.unshift(object, this); + args.unshift(thisArg, this); Async.run.apply(Async, args); }, From 75b9029880a9c5e3a5803e3a7463c797741aebdc Mon Sep 17 00:00:00 2001 From: Date: Fri, 6 Jun 2008 17:57:16 -0700 Subject: [PATCH 0304/1860] - Adding temporary button to load account creation form for sm-labs01 while work continues on the first run process. - Minor reorganization of preferences, namely, moving the advanced debugging tools into a sub-dialog and ensure each have a descriptive confirmation prompt. --- services/sync/locales/en-US/preferences.dtd | 7 ++++++- services/sync/locales/en-US/preferences.properties | 12 ++++++++---- services/sync/locales/en-US/wizard.dtd | 4 +++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 12eab2060011..217d3ccdc75f 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -35,13 +35,16 @@ + + + - + @@ -55,4 +58,6 @@ + + diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index 0eb963ffbe81..d5f95c0006ff 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -1,6 +1,10 @@ # %S is the username of the signed in user signedIn.description = Signed in as %S -reset.server.warning.title = Warning -reset.server.warning = This will delete all data on the server.\n\nYou *must* restart this and any other instances of Firefox you may have running (on any computer). -reset.client.warning.title = Warning -reset.client.warning = This will delete all bookmarks and history data. Are you sure you want to do this? +reset.server.warning.title = Reset Server Data +reset.server.warning = This will delete all data on the Weave server.\n\nYou must restart this and any other instances of Firefox you may have running on any computer once you do this.\n\nAre you sure you want to do this? +reset.client.warning.title = Reset Client Data +reset.client.warning = This will permanently delete all of your bookmarks and browsing history.\n\nAre you sure you want to do this? +reset.login.warning.title = Reset Login +reset.login.warning = This will permanently remove your username, password and passphrase from this instance of Firefox.\n\nAre you sure you want to do this? +reset.lock.warning.title = Reset Server Lock +reset.lock.warning = This will reset the write lock on the Weave server for your account. To avoid corruption, you should only do this if a lock has become stale.\n\nAre you sure you want to do this? \ No newline at end of file diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 96ad1c5b41d9..e88150ed9fca 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -3,7 +3,9 @@ - + + + From ee4657bbd1de16c0b4bd137b7a1063cd934f8ed7 Mon Sep 17 00:00:00 2001 From: Date: Fri, 6 Jun 2008 19:22:23 -0700 Subject: [PATCH 0305/1860] Made BookmarkEngine.sync() responsible for calling BookmarkEngine.syncMounts (to get the incoming shared bookmark folder contents), eliminating the FIXME that previously had this being called from special-case code in WeaveSvc.sync(). --- services/sync/modules/engines/bookmarks.js | 7 +++++++ services/sync/modules/service.js | 12 ++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 3d58c4199f22..f08457ed6faa 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -45,6 +45,13 @@ BookmarksEngine.prototype = { return this.__tracker; }, + sync: function BmkEngine_sync(onComplete) { + /* After syncing, also call syncMounts to get the + incoming shared bookmark folder contents. */ + Engine.sync.call(this, onComplete); + this.syncMounts(onComplete); + } + syncMounts: function BmkEngine_syncMounts(onComplete) { this._syncMounts.async(this, onComplete); }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d8d68bb26fb0..fad2175597ee 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -508,10 +508,6 @@ WeaveSvc.prototype = { this._notify(engines[i].name + "-engine:sync", this._syncEngine, engines[i]).async(this, self.cb); yield; - if (engines[i].name == "bookmarks") { // FIXME - Engines.get("bookmarks").syncMounts(self.cb); - yield; - } } } }, @@ -574,11 +570,11 @@ WeaveSvc.prototype = { Implementation, as well as the interpretation of what 'guid' means, is left up to the engine for the specific dataType. */ - // TODO who is listening for the share-bookmarks message? let messageName = "share-" + dataType; - // so for instance, if dataType is "bookmarks" then a message - // "share-bookmarks" will be sent out to any observers who are listening - // for it. + /* so for instance, if dataType is "bookmarks" then a message + "share-bookmarks" will be sent out to any observers who are listening + for it. As far as I know, there aren't currently any listeners for + "share-bookmarks" but we'll send it out just in case. */ this._lock(this._notify(messageName, this._shareData, dataType, From c80460c6c9fddd5dd9a32b60bfa552480d64e216 Mon Sep 17 00:00:00 2001 From: Date: Fri, 6 Jun 2008 19:28:01 -0700 Subject: [PATCH 0306/1860] Added license block and explanatory comments to bookmarks.js --- services/sync/modules/engines/bookmarks.js | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f08457ed6faa..6e7a3f5bd653 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -1,3 +1,40 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * Jono DiCarlo + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = ['BookmarksEngine']; const Cc = Components.classes; @@ -56,6 +93,13 @@ BookmarksEngine.prototype = { this._syncMounts.async(this, onComplete); }, _syncMounts: function BmkEngine__syncMounts() { + /* For every bookmark folder in my tree that has the annotation + marking it as an incoming shared folder, pull down its latest + contents from its owner's account on the server. (This is + a one-way data transfer because I can't modify bookmarks that + are owned by someone else but shared to me; any changes I make + to the folder contents are simply wiped out by the latest + server contents.) */ let self = yield; let mounts = this._store.findMounts(); From 0c77f0eb4d9bf3e5108b02781630aba07b85ab69 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 6 Jun 2008 21:40:30 -0700 Subject: [PATCH 0307/1860] Added a unit test suite for modules/async.js. --- services/sync/modules/async.js | 13 +++++-- services/sync/tests/unit/head_first.js | 13 +++++++ services/sync/tests/unit/test_async.js | 44 ++++++++++++++++++++++ services/sync/tests/unit/test_passwords.js | 13 ------- 4 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 services/sync/tests/unit/test_async.js diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 93e9f2f0a4d0..55aaa42659ee 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -48,6 +48,15 @@ Cu.import("resource://weave/util.js"); * Asynchronous generator helpers */ +// Returns a timer that is scheduled to call the given callback as +// soon as possible. +function makeTimer(cb) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(new Utils.EventListener(cb), + 0, timer.TYPE_ONE_SHOT); + return timer; +} + function AsyncException(initFrame, message) { this.message = message; this._trace = initFrame; @@ -199,9 +208,7 @@ Generator.prototype = { return; let self = this; let cb = function() { self._done(retval); }; - this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._timer.initWithCallback(new Utils.EventListener(cb), - 0, this._timer.TYPE_ONE_SHOT); + this._timer = makeTimer(cb); }, _done: function AsyncGen__done(retval) { diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 5b4375d9ded0..d0b41c7969ec 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -26,3 +26,16 @@ let provider = { ds.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider); do_bind_resource(do_get_file("modules"), "weave"); + +function loadInSandbox(aUri) { + var sandbox = Components.utils.Sandbox(this); + var request = Components. + classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(); + + request.open("GET", aUri, false); + request.send(null); + Components.utils.evalInSandbox(request.responseText, sandbox); + + return sandbox; +} diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js new file mode 100644 index 000000000000..1d3cffb4c1f8 --- /dev/null +++ b/services/sync/tests/unit/test_async.js @@ -0,0 +1,44 @@ +function run_test() { + var async = loadInSandbox("resource://weave/async.js"); + var callbackQueue = []; + + Function.prototype.async = async.Async.sugar; + + async.makeTimer = function fake_makeTimer(cb) { + callbackQueue.push(cb); + }; + + var onCompleteCalled = false; + + function onComplete() { + onCompleteCalled = true; + } + + let timesYielded = 0; + + function testAsyncFunc() { + let self = yield; + timesYielded++; + + callbackQueue.push(self.cb); + yield; + + timesYielded++; + } + + testAsyncFunc.async({}, onComplete); + + do_check_eq(timesYielded, 1); + + let func = callbackQueue.pop(); + do_check_eq(typeof func, "function"); + func(); + + do_check_eq(timesYielded, 2); + + func = callbackQueue.pop(); + do_check_eq(typeof func, "function"); + func(); + + do_check_eq(callbackQueue.length, 0); +} diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 0f8726a27c86..a8ce63940855 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -1,16 +1,3 @@ -function loadInSandbox(aUri) { - var sandbox = Components.utils.Sandbox(this); - var request = Components. - classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(); - - request.open("GET", aUri, false); - request.send(null); - Components.utils.evalInSandbox(request.responseText, sandbox); - - return sandbox; -} - function run_test() { // The JS module we're testing, with all members exposed. var passwords = loadInSandbox("resource://weave/engines/passwords.js"); From 037ba906d19617ff250fbef5255ba78b4f6dda07 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sat, 7 Jun 2008 00:22:57 -0700 Subject: [PATCH 0308/1860] Fixed an inaccuracy in the async.js test suite. --- services/sync/tests/unit/test_async.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js index 1d3cffb4c1f8..1e39c50b21e7 100644 --- a/services/sync/tests/unit/test_async.js +++ b/services/sync/tests/unit/test_async.js @@ -6,6 +6,7 @@ function run_test() { async.makeTimer = function fake_makeTimer(cb) { callbackQueue.push(cb); + return "fake_nsITimer"; }; var onCompleteCalled = false; @@ -24,6 +25,7 @@ function run_test() { yield; timesYielded++; + self.done(); } testAsyncFunc.async({}, onComplete); From 0143b19db7385dc252828054a0397ee73fcc6f3a Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sat, 7 Jun 2008 00:34:33 -0700 Subject: [PATCH 0309/1860] Added more tests to test_async.js and documented them a bit. --- services/sync/tests/unit/test_async.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js index 1e39c50b21e7..1279ac690c00 100644 --- a/services/sync/tests/unit/test_async.js +++ b/services/sync/tests/unit/test_async.js @@ -5,8 +5,10 @@ function run_test() { Function.prototype.async = async.Async.sugar; async.makeTimer = function fake_makeTimer(cb) { + // Just add the callback to our queue and we'll call it later, so + // as to simulate a real nsITimer. callbackQueue.push(cb); - return "fake_nsITimer"; + return "fake nsITimer"; }; var onCompleteCalled = false; @@ -17,10 +19,18 @@ function run_test() { let timesYielded = 0; - function testAsyncFunc() { + function testAsyncFunc(x) { let self = yield; timesYielded++; + // Ensure that argument was passed in properly. + do_check_eq(x, 5); + + // Ensure that 'this' is set properly. + do_check_eq(this.sampleProperty, true); + + // Simulate the calling of an asynchronous function that will call + // our callback. callbackQueue.push(self.cb); yield; @@ -28,7 +38,8 @@ function run_test() { self.done(); } - testAsyncFunc.async({}, onComplete); + var thisArg = {sampleProperty: true}; + testAsyncFunc.async(thisArg, onComplete, 5); do_check_eq(timesYielded, 1); From 5a74763a12e4c254ab19ab11025ba8a3280bdbc1 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 9 Jun 2008 09:45:55 -0700 Subject: [PATCH 0310/1860] Just added a few semicolons that js2-mode warned me about. --- services/sync/modules/dav.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 2e5a96c0aa88..fad2489d828f 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -415,7 +415,7 @@ DAVCollection.prototype = { yield; } - delete DAVLocks['default'] + delete DAVLocks['default']; this._log.trace("Lock released (or we didn't have one)"); self.done(true); }, @@ -471,12 +471,12 @@ DummyAuthProvider.prototype = { // redirects us to a "you're not authorized" page), so we have to set a flag // to let the load handler know to treat the load as a failure. get _authFailed() { return this.__authFailed; }, - set _authFailed(newValue) { return this.__authFailed = newValue }, + set _authFailed(newValue) { return this.__authFailed = newValue; }, // nsISupports QueryInterface: function DAP_QueryInterface(iid) { - if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + if (!this.interfaces.some( function(v) { return iid.equals(v); } )) throw Cr.NS_ERROR_NO_INTERFACE; // nsIAuthPrompt and nsIPrompt need separate implementations because From cf939769bbeb36f7d4209f22f9b6989f768107be Mon Sep 17 00:00:00 2001 From: Date: Mon, 9 Jun 2008 11:48:52 -0700 Subject: [PATCH 0311/1860] Fixed my code so that BookmarkEngine._sync() now works correctly to call Engine._sync() and BookmarkEngine.syncMounts(), asynchronously. Added in some TODO comments about what's going to happen in the next round of refactoring. --- services/sync/modules/engines/bookmarks.js | 61 ++++++++++++++-------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 6e7a3f5bd653..12bee06177a6 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -41,6 +41,10 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +// Annotation to use for shared bookmark folders, incoming and outgoing: +const INCOMING_SHARED_ANNO = "weave/shared-incoming"; +const OUTGOING_SHARED_ANNO = "weave/shared-outgoing"; + Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/util.js"); @@ -82,12 +86,16 @@ BookmarksEngine.prototype = { return this.__tracker; }, - sync: function BmkEngine_sync(onComplete) { + _sync: function BmkEngine_sync() { /* After syncing, also call syncMounts to get the incoming shared bookmark folder contents. */ - Engine.sync.call(this, onComplete); - this.syncMounts(onComplete); - } + let self = yield; + this.__proto__.__proto__._sync.async(this, self.cb ); + yield; + this.syncMounts(self.cb); + yield; + self.done(); + }, syncMounts: function BmkEngine_syncMounts(onComplete) { this._syncMounts.async(this, onComplete); @@ -153,30 +161,27 @@ BookmarksEngine.prototype = { if (!enckey) throw "Could not encrypt symmetric encryption key"; + /* TODO this function needs to be broken up into createOutgoingShare + and updateOutgoingShare. */ keys.ring[username] = enckey; DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload keyring file."); - this._createShare(guid, username, username); + /* TODO send an XMPP message to the recipient of the share to tell + them to call _createIncomingShare. */ this._log.debug("All done sharing!"); self.done(true); }, - _createShare: function BookmarkEngine__createShare(guid, id, title) { - /* the bookmark item identified by guid, and the whole subtree under it, - must be copied out from the main file into a separate file which is - put into the new directory and encrypted with the key in the keychain. - id is the userid of the user we're sharing with. - */ + _createIncomingShare: function BookmarkEngine__createShare(guid, id, title) { - /* TODO it appears that this just creates the folder and puts the - annotation on it; the mechanics of sharing must be done when syncing? - - Or has that not been done yet at all? - Do we have to create a new Mount? + /* TODO This used to be called just _createShare, but its semantics + have changed slightly -- its purpose now is to create a new empty + incoming shared bookmark folder. To do this is mostly the same code, + but it will need a few tweaks. */ let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); @@ -211,19 +216,25 @@ BookmarksEngine.prototype = { } }, - _stopShare: function BookmarkeEngine__stopShare( guid, username) { - // TODO implement this; also give a way to call it from outside - // the service. - }, _syncOneMount: function BmkEngine__syncOneMount(mountData) { + /* Pull down bookmarks from the server for a single incoming + shared folder. */ + + /* TODO modify this: the old implementation assumes we want to copy + everything that the other user has, by pulling down snapshot and + diffs and applying the diffs to the snapshot. Instead, now we just + want to get a single subfolder and its children, which will be in + a separate file. */ + + // TODO for clarity maybe rename this to updateIncomingShare. + let self = yield; let user = mountData.userid; let prefix = DAV.defaultPrefix; let serverURL = Utils.prefs.getCharPref("serverURL"); let snap = new SnapshotStore(); - // TODO this is obviously what we want. this._log.debug("Syncing shared folder from user " + user); try { @@ -790,10 +801,14 @@ BookmarksStore.prototype = { }, findMounts: function BStore_findMounts() { + /* Returns list of mount data structures, each of which + represents one incoming shared-bookmark folder. */ let ret = []; - let a = this._ans.getItemsWithAnnotation("weave/mounted-share-id", {}); + let a = this._ans.getItemsWithAnnotation(INCOMING_SHARED_ANNO, {}); for (let i = 0; i < a.length; i++) { - let id = this._ans.getItemAnnotation(a[i], "weave/mounted-share-id"); + /* The value of the incoming-shared annotation is the id of the + person who has shared it with us. Get that value: */ + let id = this._ans.getItemAnnotation(a[i], INCOMING_SHARED_ANNO); ret.push(this._wrapMount(this._getNode(a[i]), id)); } return ret; From baf93b68096a9bd83cf783ebf7ec3e7f8d6a2d96 Mon Sep 17 00:00:00 2001 From: Date: Mon, 9 Jun 2008 12:12:51 -0700 Subject: [PATCH 0312/1860] The folder annotation for outgoing shared folders is now a string containing the username of the person the folder is being shared with. Also moved to using const strings in bookmark-menu-overlay.js. --- services/sync/modules/engines.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 6d5381cec167..b025a535ebf4 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -656,7 +656,9 @@ Engine.prototype = { return; }, - // TODO need a "stop sharing" function. + /* TODO need a "stop sharing" function. + Actually, stopping an outgoing share and stopping an incoming share + are two different things. */ sync: function Engine_sync(onComplete) { return this._sync.async(this, onComplete); @@ -666,7 +668,7 @@ Engine.prototype = { return this._share.async(this, onComplete, guid, username); }, - resetServer: function Engine_resetServer(onComplete) { + resetServer: function Engimne_resetServer(onComplete) { this._notify("reset-server", this._resetServer).async(this, onComplete); }, From 685cb10231385222ad024e9e21c51626a4efed03 Mon Sep 17 00:00:00 2001 From: Date: Mon, 9 Jun 2008 15:27:09 -0700 Subject: [PATCH 0313/1860] Renamed bookmarkeEngine methods so they make more sense with the new sharing model, e.g. updateAllIncomingShares instead of syncMounts. --- services/sync/modules/engines/bookmarks.js | 100 ++++++++++++++++++--- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 12bee06177a6..ac2e9ca83092 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -54,6 +54,11 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +/* LONGTERM TODO: when we start working on the ability to share other types +of data besides bookmarks, the xmppClient instance should be moved to hang +off of Weave.Service instead of hanging off the BookmarksEngine. But for +now this is the easiest place to deal with it. */ +Cu.import("resource://weave/xmpp/xmppClient.js"); Function.prototype.async = Async.sugar; @@ -86,21 +91,98 @@ BookmarksEngine.prototype = { return this.__tracker; }, + _startXmppClient: function BmkEngine__startXmppClient() { + /* this should probably be called asynchronously as it can take a while. */ + + // TODO add preferences for serverUrl and realm. + // Also add a boolean preference to turn XMPP messaging on or off. + let serverUrl = "http://sm-labs01.mozilla.org:5280/http_poll"; + let realm = "sm-labs01.mozilla.org"; + + // TODO once we have ejabberd talking to LDAP, the username/password + // for xmpp will be the same as the ones for Weave itself, so we can + // read username/password from ID.get('WeaveID' + let clientName = ID.get('WeaveID').username; + let clientPassword = ID.get('WeaveID').password; + + let transport = new HTTPPollingTransport( serverUrl, false, 15000 ); + let auth = new PlainAuthenticator(); + // TODO use MD5Authenticator instead once we get it working -- plain is + // a security hole. + this._xmppClient = new XmppClient( clientName, + realm, + clientPassword, + transport, + auth ); + let self = this; + let messageHandler = { + handle: function ( messageText, from ) { + /* The callback function for incoming xmpp messages. + We expect message text to be either: + "share " + (sender offers to share directory dir with us) + or "stop " + (sender has stopped sharing directory dir with us.) + or "accept " + (sharee has accepted our offer to share our dir.) + or "decline " + (sharee has declined our offer to share our dir.) + */ + let words = messageText.split(" "); + let commandWord = words[0]; + let directoryName = words[1]; + if ( commandWord == "share" ) { + self._incomingShareOffer( directoryName, from ); + } else if ( commandWord == "stop" ) { + self._incomingShareWithdrawn( directoryName, from ); + } + } + } + this._xmppClient.registerMessageHandler( messageHandler ); + this._xmppClient.connect( realm ); + }, + + _incomingShareOffer: function BmkEngine__incomingShareOffer( dir, user ) { + /* Called when we receive an offer from another user to share a + directory. + + TODO what should happen is that we add a notification to the queue + telling that the incoming share has been offered; when the offer + is accepted we will call createIncomingShare and then + updateIncomingShare. + + But since we don't have notification in place yet, I'm going to skip + right ahead to creating the incoming share. + */ + + }, + + _incomingShareWithdrawn: function BmkEngine__incomingShareStop( dir, user ) { + /* Called when we receive a message telling us that a user who has + already shared a directory with us has chosen to stop sharing + the directory. + + TODO Find the incomingShare in our bookmark tree that corresponds + to the shared directory, and delete it; add a notification to + the queue telling us what has happened. + */ + }, + _sync: function BmkEngine_sync() { /* After syncing, also call syncMounts to get the incoming shared bookmark folder contents. */ let self = yield; this.__proto__.__proto__._sync.async(this, self.cb ); yield; - this.syncMounts(self.cb); + this.updateAllIncomingShares(self.cb); yield; self.done(); }, - syncMounts: function BmkEngine_syncMounts(onComplete) { + updateAllIncomingShares: function BmkEngine_updateAllIncoming(onComplete) { this._syncMounts.async(this, onComplete); }, - _syncMounts: function BmkEngine__syncMounts() { + _updateAllIncomingShares: function BmkEngine__updateAllIncoming() { /* For every bookmark folder in my tree that has the annotation marking it as an incoming shared folder, pull down its latest contents from its owner's account on the server. (This is @@ -109,11 +191,11 @@ BookmarksEngine.prototype = { to the folder contents are simply wiped out by the latest server contents.) */ let self = yield; - let mounts = this._store.findMounts(); + let mounts = this._store.findIncomingShares(); for (i = 0; i < mounts.length; i++) { try { - this._syncOneMount.async(this, self.cb, mounts[i]); + this._updateIncomingShare.async(this, self.cb, mounts[i]); yield; } catch (e) { this._log.warn("Could not sync shared folder from " + mounts[i].userid); @@ -124,7 +206,7 @@ BookmarksEngine.prototype = { // TODO modify this as neccessary since I just moved it from the engine // superclass into BookmarkEngine. - _share: function BookmarkEngine__share(guid, username) { + _createOutgoingShare: function BmkEngine__createOutgoing(guid, username) { let self = yield; let prefix = DAV.defaultPrefix; @@ -217,7 +299,7 @@ BookmarksEngine.prototype = { }, - _syncOneMount: function BmkEngine__syncOneMount(mountData) { + _updateIncomingShare: function BmkEngine__updateIncomingShare(mountData) { /* Pull down bookmarks from the server for a single incoming shared folder. */ @@ -227,8 +309,6 @@ BookmarksEngine.prototype = { want to get a single subfolder and its children, which will be in a separate file. */ - // TODO for clarity maybe rename this to updateIncomingShare. - let self = yield; let user = mountData.userid; let prefix = DAV.defaultPrefix; @@ -800,7 +880,7 @@ BookmarksStore.prototype = { } }, - findMounts: function BStore_findMounts() { + findIncomingShares: function BStore_findIncomingShares() { /* Returns list of mount data structures, each of which represents one incoming shared-bookmark folder. */ let ret = []; From 6260b1a6166b3c7043adc7fb1148c4c8605cb01a Mon Sep 17 00:00:00 2001 From: Date: Mon, 9 Jun 2008 16:19:58 -0700 Subject: [PATCH 0314/1860] Fixed a couple minor bugs that were preventing bookmark engine from starting up properly --- services/sync/modules/engines/bookmarks.js | 28 ++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index ac2e9ca83092..0501663bb0e4 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -168,7 +168,7 @@ BookmarksEngine.prototype = { */ }, - _sync: function BmkEngine_sync() { + _sync: function BmkEngine__sync() { /* After syncing, also call syncMounts to get the incoming shared bookmark folder contents. */ let self = yield; @@ -179,8 +179,21 @@ BookmarksEngine.prototype = { self.done(); }, + _share: function BmkEngine__share( guid, username ) { + /* TODO check to see we're not already sharing this thing. */ + // this._createOutgoingShare( guid, username ); + /* TODO Setting the annotations should happen here instead of in + share.js? */ + // this._updateOutgoingShare( guid, username ); + + // or something like this: + //this._xmppClient.sendMessage( "Hey I share with you ", username ); + dump( "In bookmarkEngine._share. Sharing " + guid + " with " + username ); + return true; + }, + updateAllIncomingShares: function BmkEngine_updateAllIncoming(onComplete) { - this._syncMounts.async(this, onComplete); + this._updateAllIncomingShares.async(this, onComplete); }, _updateAllIncomingShares: function BmkEngine__updateAllIncoming() { /* For every bookmark folder in my tree that has the annotation @@ -204,13 +217,11 @@ BookmarksEngine.prototype = { } }, - // TODO modify this as neccessary since I just moved it from the engine - // superclass into BookmarkEngine. _createOutgoingShare: function BmkEngine__createOutgoing(guid, username) { let self = yield; let prefix = DAV.defaultPrefix; - this._log.debug("Sharing bookmarks with " + username); + this._log.debug("Sharing bookmarks from " + guid + " with " + username); this._getSymKey.async(this, self.cb); yield; @@ -243,8 +254,6 @@ BookmarksEngine.prototype = { if (!enckey) throw "Could not encrypt symmetric encryption key"; - /* TODO this function needs to be broken up into createOutgoingShare - and updateOutgoingShare. */ keys.ring[username] = enckey; DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); ret = yield; @@ -258,6 +267,11 @@ BookmarksEngine.prototype = { self.done(true); }, + _updateOutgoingShare: function BmkEngine__updateOutgoing(guid, username) { + /* TODO this needs to have the logic to break the shared bookmark + subtree out of the store and put it in a separate file...*/ + }, + _createIncomingShare: function BookmarkEngine__createShare(guid, id, title) { /* TODO This used to be called just _createShare, but its semantics From 2c994123fc1f7fc7b0f8ebc161ac31e262a56ba6 Mon Sep 17 00:00:00 2001 From: Date: Mon, 9 Jun 2008 16:49:04 -0700 Subject: [PATCH 0315/1860] Moved the writing of the outgoing-share annotation on the bookmark folder to BookmarkEngine._share, where it makes a lot more sense than it does in the share.js dialog-box code where it used to be. --- services/sync/modules/engines/bookmarks.js | 25 +++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 0501663bb0e4..7b1f4ba83976 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -179,16 +179,31 @@ BookmarksEngine.prototype = { self.done(); }, - _share: function BmkEngine__share( guid, username ) { + _share: function BmkEngine__share( selectedFolder, username ) { + // Return true if success, false if failure. + /* TODO check to see we're not already sharing this thing. */ // this._createOutgoingShare( guid, username ); - /* TODO Setting the annotations should happen here instead of in - share.js? */ // this._updateOutgoingShare( guid, username ); - // or something like this: + /* Set the annotation on the folder so we know + it's an outgoing share: */ + let folderItemId = selectedFolder.node.itemId; + let folderName = selectedFolder.getAttribute( "label" ); + let annotation = { name: "weave/share/shared_outgoing", + value: username, + flags: 0, + mimeType: null, + type: PlacesUtils.TYPE_STRING, + expires: PlacesUtils.EXPIRE_NEVER }; + // TODO: does this clobber existing annotations? + PlacesUtils.setAnnotationsForItem( folderItemId, [ annotation ] ); + /* LONGTERM TODO: in the future when we allow sharing one folder + with many people, the value of the annotation can be a whole list + of usernames instead of just one. */ + //this._xmppClient.sendMessage( "Hey I share with you ", username ); - dump( "In bookmarkEngine._share. Sharing " + guid + " with " + username ); + dump( "In bookmarkEngine._share. Sharing " +folderName + " with " + username ); return true; }, From 7ee4ec54d94af0d6d1a9dbe2f9045b3ea8cc8aab Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 9 Jun 2008 17:36:54 -0700 Subject: [PATCH 0316/1860] Added a POST method to dav.js. --- services/sync/modules/dav.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index fad2489d828f..66463de72bc7 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -229,6 +229,11 @@ DAVCollection.prototype = { this._defaultHeaders); }, + POST: function DC_POST(path, data, onComplete) { + return this._makeRequest.async(this, onComplete, "POST", path, + this._defaultHeaders, data); + }, + PUT: function DC_PUT(path, data, onComplete) { return this._makeRequest.async(this, onComplete, "PUT", path, this._defaultHeaders, data); From 142c6d82248d58fda46ec2695393c3b14e617a77 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 9 Jun 2008 18:40:30 -0700 Subject: [PATCH 0317/1860] Fixed a bug that was causing failing unit tests using async.js to not properly report error conditions. --- services/sync/modules/async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 55aaa42659ee..81cade31d398 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -254,7 +254,7 @@ function trace(frame, str) { // skip our frames // FIXME: we should have a pref for this, for debugging async.js itself - while (frame.name.match(/^Async(Gen|)_/)) + while (frame.name && frame.name.match(/^Async(Gen|)_/)) frame = frame.caller; if (frame.caller) From 841ec7677c0b8855b1715d173496d8cd3d02464d Mon Sep 17 00:00:00 2001 From: Date: Mon, 9 Jun 2008 18:44:13 -0700 Subject: [PATCH 0318/1860] Fixed a couple of minor bugs that were preventing bookmark share from starting (like, i was skipping enabled engines instead of disabled engines... duhh) --- services/sync/modules/engines.js | 3 ++- services/sync/modules/engines/bookmarks.js | 27 +++++++++++----------- services/sync/modules/service.js | 10 ++++---- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b025a535ebf4..ff52d2870328 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -650,10 +650,11 @@ Engine.prototype = { }, _share: function Engine__share(guid, username) { + let self = yield; /* This should be overridden by the engine subclass for each datatype. Implementation should share the data node identified by guid, and all its children, if any, with the user identified by username. */ - return; + self.done(); }, /* TODO need a "stop sharing" function. diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 7b1f4ba83976..40adefc9f1a0 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -54,6 +54,7 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); + /* LONGTERM TODO: when we start working on the ability to share other types of data besides bookmarks, the xmppClient instance should be moved to hang off of Weave.Service instead of hanging off the BookmarksEngine. But for @@ -181,30 +182,30 @@ BookmarksEngine.prototype = { _share: function BmkEngine__share( selectedFolder, username ) { // Return true if success, false if failure. + let ret = false; + let ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + let self = yield; /* TODO check to see we're not already sharing this thing. */ - // this._createOutgoingShare( guid, username ); - // this._updateOutgoingShare( guid, username ); - + // TODO call this._createOutgoingShare( guid, username ); + // TODO call this._updateOutgoingShare( guid, username ); + // TODO call + // this._xmppClient.sendMessage( "Hey I share with you ", username ); /* Set the annotation on the folder so we know it's an outgoing share: */ let folderItemId = selectedFolder.node.itemId; let folderName = selectedFolder.getAttribute( "label" ); - let annotation = { name: "weave/share/shared_outgoing", - value: username, - flags: 0, - mimeType: null, - type: PlacesUtils.TYPE_STRING, - expires: PlacesUtils.EXPIRE_NEVER }; + ans.setItemAnnotation(folderItemId, OUTGOING_SHARED_ANNO, username, 0, + ans.EXPIRE_NEVER); // TODO: does this clobber existing annotations? - PlacesUtils.setAnnotationsForItem( folderItemId, [ annotation ] ); /* LONGTERM TODO: in the future when we allow sharing one folder with many people, the value of the annotation can be a whole list of usernames instead of just one. */ - //this._xmppClient.sendMessage( "Hey I share with you ", username ); - dump( "In bookmarkEngine._share. Sharing " +folderName + " with " + username ); - return true; + dump( "Bookmark engine shared " +folderName + " with " + username ); + ret = true; + self.done( ret ); }, updateAllIncomingShares: function BmkEngine_updateAllIncoming(onComplete) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index fad2175597ee..ce9d1cf1c1fd 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -581,12 +581,14 @@ WeaveSvc.prototype = { guid, username)).async(this, onComplete); }, - _shareBookmarks: function WeaveSync__shareBookmarks(dataType, - guid, - username) { + _shareData: function WeaveSync__shareData(dataType, + guid, + username) { let self = yield; - if (Engines.get(dataType).enabled) + if (!Engines.get(dataType).enabled) { + this._log.warn( "Can't share disabled data type: " + dataType ); return; + } Engines.get(dataType).share(self.cb, guid, username); let ret = yield; self.done(ret); From f107a5fae3dab6b3a8510243dbee85812c7581a3 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 9 Jun 2008 18:55:26 -0700 Subject: [PATCH 0319/1860] Added modules/sharing.js, which provides access to the RESTful sharing API, and a unit test suite. The unit test suite is pretty heinous right now and should get a bit of refactoring. --- services/sync/modules/sharing.js | 57 ++++++++++++++++++++++ services/sync/tests/unit/test_sharing.js | 62 ++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 services/sync/modules/sharing.js create mode 100644 services/sync/tests/unit/test_sharing.js diff --git a/services/sync/modules/sharing.js b/services/sync/modules/sharing.js new file mode 100644 index 000000000000..bbf5c1a3692c --- /dev/null +++ b/services/sync/modules/sharing.js @@ -0,0 +1,57 @@ +EXPORTED_SYMBOLS = ["Sharing"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; + +function Api(dav) { + this._dav = dav; +} + +Api.prototype = { + shareWithUsers: function Api_shareWithUsers(path, users, onComplete) { + this._shareGenerator.async(this, + onComplete, + path, + users); + }, + + _shareGenerator: function Api__shareGenerator(path, users) { + let self = yield; + + this._dav.defaultPrefix = ""; + + let cmd = {"version" : 1, + "directory" : path, + "share_to_users" : users}; + let jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + let json = jsonSvc.encode(cmd); + + this._dav.POST("share/", "cmd=" + escape(json), self.cb); + let xhr = yield; + + let retval; + + if (xhr.status == 200) { + if (xhr.responseText == "OK") { + retval = {wasSuccessful: true}; + } else { + retval = {wasSuccessful: false, + errorText: xhr.responseText}; + } + } else { + retval = {wasSuccessful: false, + errorText: "Server returned status " + xhr.status}; + } + + self.done(retval); + } +}; + +Sharing = { + Api: Api +}; diff --git a/services/sync/tests/unit/test_sharing.js b/services/sync/tests/unit/test_sharing.js new file mode 100644 index 000000000000..7d1d4e4cc5d3 --- /dev/null +++ b/services/sync/tests/unit/test_sharing.js @@ -0,0 +1,62 @@ +const Cu = Components.utils; + +Cu.import("resource://weave/sharing.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/log4moz.js"); + +function runTestGenerator() { + let self = yield; + + let fakeDav = { + POST: function fakeDav_POST(url, data, callback) { + let result = {status: 200, responseText: "OK"}; + let cb = function() { callback(result); }; + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(new Utils.EventListener(cb), + 0, timer.TYPE_ONE_SHOT); + } + }; + + let api = new Sharing.Api(fakeDav); + api.shareWithUsers("/fake/dir", ["johndoe"], self.cb); + let result = yield; + + do_check_eq(result.wasSuccessful, true); + self.done(); +} + +function BasicFormatter() { + this.errors = 0; +} +BasicFormatter.prototype = { + format: function BF_format(message) { + let date = new Date(message.time); + if (message.level == Log4Moz.Level.Error) + this.errors += 1; + return message.loggerName + "\t" + message.levelDesc + "\t" + + message.message + "\n"; + } +}; +BasicFormatter.prototype.__proto__ = new Log4Moz.Formatter(); + +function run_test() { + var log = Log4Moz.Service.rootLogger; + var formatter = new BasicFormatter(); + var appender = new Log4Moz.DumpAppender(formatter); + log.level = Log4Moz.Level.Debug; + appender.level = Log4Moz.Level.Debug; + log.addAppender(appender); + + var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + let thread = threadManager.currentThread; + let gen = Async.run({}, runTestGenerator); + + while (gen.generator && !formatter.errors) { + thread.processNextEvent(true); + } + + if (formatter.errors) + throw new Error("Errors occurred."); +} From 925953b251e481dff4230d75dc9405d239cc2a13 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 9 Jun 2008 19:18:14 -0700 Subject: [PATCH 0320/1860] Refactorings to test_sharing.js to make it use the async do_test_pending()/do_test_finished() calls. --- services/sync/tests/unit/test_sharing.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/services/sync/tests/unit/test_sharing.js b/services/sync/tests/unit/test_sharing.js index 7d1d4e4cc5d3..939c9e74b0ec 100644 --- a/services/sync/tests/unit/test_sharing.js +++ b/services/sync/tests/unit/test_sharing.js @@ -32,7 +32,6 @@ function BasicFormatter() { } BasicFormatter.prototype = { format: function BF_format(message) { - let date = new Date(message.time); if (message.level == Log4Moz.Level.Error) this.errors += 1; return message.loggerName + "\t" + message.levelDesc + "\t" + @@ -49,14 +48,14 @@ function run_test() { appender.level = Log4Moz.Level.Debug; log.addAppender(appender); - var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); - let thread = threadManager.currentThread; - let gen = Async.run({}, runTestGenerator); + do_test_pending(); - while (gen.generator && !formatter.errors) { - thread.processNextEvent(true); - } + let onComplete = function() { + if (formatter.errors) + do_throw("Errors were logged."); + else + do_test_finished(); + }; - if (formatter.errors) - throw new Error("Errors occurred."); + Async.run({}, runTestGenerator, onComplete); } From bdbe7c5a8993122a9299f115bfc16c7db6de329b Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 9 Jun 2008 19:30:11 -0700 Subject: [PATCH 0321/1860] Refactoring: made a new function, Utils.makeTimerForCall(), which is used by test suites and async.js. --- services/sync/modules/async.js | 11 +---------- services/sync/modules/util.js | 9 +++++++++ services/sync/tests/unit/test_async.js | 10 +++++++--- services/sync/tests/unit/test_sharing.js | 6 +----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 81cade31d398..ace09b9e5c17 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -48,15 +48,6 @@ Cu.import("resource://weave/util.js"); * Asynchronous generator helpers */ -// Returns a timer that is scheduled to call the given callback as -// soon as possible. -function makeTimer(cb) { - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(new Utils.EventListener(cb), - 0, timer.TYPE_ONE_SHOT); - return timer; -} - function AsyncException(initFrame, message) { this.message = message; this._trace = initFrame; @@ -208,7 +199,7 @@ Generator.prototype = { return; let self = this; let cb = function() { self._done(retval); }; - this._timer = makeTimer(cb); + this._timer = Utils.makeTimerForCall(cb); }, _done: function AsyncGen__done(retval) { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 0454290ab6f2..b31ff6b1e63d 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -243,6 +243,15 @@ let Utils = { return tmp; }, + // Returns a timer that is scheduled to call the given callback as + // soon as possible. + makeTimerForCall: function makeTimerForCall(cb) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(new Utils.EventListener(cb), + 0, timer.TYPE_ONE_SHOT); + return timer; + }, + open: function open(pathOrFile, mode, perms) { let stream, file; diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js index 1279ac690c00..36795e9948a4 100644 --- a/services/sync/tests/unit/test_async.js +++ b/services/sync/tests/unit/test_async.js @@ -1,10 +1,14 @@ +const Cu = Components.utils; + +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); + function run_test() { - var async = loadInSandbox("resource://weave/async.js"); var callbackQueue = []; - Function.prototype.async = async.Async.sugar; + Function.prototype.async = Async.sugar; - async.makeTimer = function fake_makeTimer(cb) { + Utils.makeTimerForCall = function fake_makeTimerForCall(cb) { // Just add the callback to our queue and we'll call it later, so // as to simulate a real nsITimer. callbackQueue.push(cb); diff --git a/services/sync/tests/unit/test_sharing.js b/services/sync/tests/unit/test_sharing.js index 939c9e74b0ec..1d5646dcf511 100644 --- a/services/sync/tests/unit/test_sharing.js +++ b/services/sync/tests/unit/test_sharing.js @@ -11,11 +11,7 @@ function runTestGenerator() { let fakeDav = { POST: function fakeDav_POST(url, data, callback) { let result = {status: 200, responseText: "OK"}; - let cb = function() { callback(result); }; - - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(new Utils.EventListener(cb), - 0, timer.TYPE_ONE_SHOT); + Utils.makeTimerForCall(function() { callback(result); }); } }; From f4939cbbc3eacb9f1167971f2c3d18b06ad6ba60 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 9 Jun 2008 20:51:23 -0700 Subject: [PATCH 0322/1860] Factored out all the logging+async setup code from test_sharing.js into a new global function, makeAsyncTestRunner(), which turns an async.js-style generator into a unit test. --- services/sync/tests/unit/head_first.js | 42 ++++++++++++++++++++++++ services/sync/tests/unit/test_sharing.js | 35 +------------------- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index d0b41c7969ec..ac1d1543c1d3 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -39,3 +39,45 @@ function loadInSandbox(aUri) { return sandbox; } + +function makeAsyncTestRunner(generator) { + const Cu = Components.utils; + + Cu.import("resource://weave/log4moz.js"); + Cu.import("resource://weave/async.js"); + + var errorsLogged = 0; + + function _TestFormatter() {} + _TestFormatter.prototype = { + format: function BF_format(message) { + if (message.level == Log4Moz.Level.Error) + errorsLogged += 1; + return message.loggerName + "\t" + message.levelDesc + "\t" + + message.message + "\n"; + } + }; + _TestFormatter.prototype.__proto__ = new Log4Moz.Formatter(); + + var log = Log4Moz.Service.rootLogger; + var formatter = new _TestFormatter(); + var appender = new Log4Moz.DumpAppender(formatter); + log.level = Log4Moz.Level.Debug; + appender.level = Log4Moz.Level.Debug; + log.addAppender(appender); + + function run_test() { + do_test_pending(); + + let onComplete = function() { + if (errorsLogged) + do_throw("Errors were logged."); + else + do_test_finished(); + }; + + Async.run({}, generator, onComplete); + } + + return run_test; +} diff --git a/services/sync/tests/unit/test_sharing.js b/services/sync/tests/unit/test_sharing.js index 1d5646dcf511..b962404e9d37 100644 --- a/services/sync/tests/unit/test_sharing.js +++ b/services/sync/tests/unit/test_sharing.js @@ -1,9 +1,7 @@ const Cu = Components.utils; Cu.import("resource://weave/sharing.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/log4moz.js"); function runTestGenerator() { let self = yield; @@ -23,35 +21,4 @@ function runTestGenerator() { self.done(); } -function BasicFormatter() { - this.errors = 0; -} -BasicFormatter.prototype = { - format: function BF_format(message) { - if (message.level == Log4Moz.Level.Error) - this.errors += 1; - return message.loggerName + "\t" + message.levelDesc + "\t" + - message.message + "\n"; - } -}; -BasicFormatter.prototype.__proto__ = new Log4Moz.Formatter(); - -function run_test() { - var log = Log4Moz.Service.rootLogger; - var formatter = new BasicFormatter(); - var appender = new Log4Moz.DumpAppender(formatter); - log.level = Log4Moz.Level.Debug; - appender.level = Log4Moz.Level.Debug; - log.addAppender(appender); - - do_test_pending(); - - let onComplete = function() { - if (formatter.errors) - do_throw("Errors were logged."); - else - do_test_finished(); - }; - - Async.run({}, runTestGenerator, onComplete); -} +var run_test = makeAsyncTestRunner(runTestGenerator); From 792044d55ca8ccc0bb691903d13ee317b1e7d1db Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 10 Jun 2008 13:45:37 -0700 Subject: [PATCH 0323/1860] Make sync service check score before syncing: bug #434816 (r=thunder) --- services/sync/modules/service.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d8d68bb26fb0..4093648eebb0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -209,7 +209,7 @@ WeaveSvc.prototype = { this._scheduleTimer = Cc["@mozilla.org/timer;1"]. createInstance(Ci.nsITimer); let listener = new Utils.EventListener(Utils.bind2(this, this._onSchedule)); - this._scheduleTimer.initWithCallback(listener, 1800000, // 30 min + this._scheduleTimer.initWithCallback(listener, 120000, // 2 min this._scheduleTimer.TYPE_REPEATING_SLACK); this._log.info("Weave scheduler enabled"); }, @@ -504,7 +504,7 @@ WeaveSvc.prototype = { let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { - if (engines[i].enabled) { + if (engines[i].enabled && engines[i]._tracker.score >= 30) { this._notify(engines[i].name + "-engine:sync", this._syncEngine, engines[i]).async(this, self.cb); yield; @@ -520,6 +520,8 @@ WeaveSvc.prototype = { try { engine.sync(self.cb); yield; + engine._tracker.resetScore(); + yield; } catch(e) { this._log.error(Utils.exceptionStr(e)); if (e.trace) From e58a2162c5c4d7355a44947062ff4012b2654dc3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Jun 2008 11:12:04 +0900 Subject: [PATCH 0324/1860] Various changes: Engine/RemoteStore: * Move code to make the engine remote directory into RemoteStore. * Fix initSession call in Engine to properly use callback / call yield. * Do not check '_getServerData' return status in _sync, we will use exceptions from RemoteStore instead. * Move code to push a new delta into RemoteStore (appendDelta()). Currently comments out code that forces a re-upload in cases where the server (engine) format version was different. We may add this back later into RemoteStore (?). * Note that this patch also removes the 'this._encryptionChanged' conditional, which I believe is currently useless (we never set it). Service: * When wiping the server (due to a server version mismatch), skip .htaccess files, since they are usually not user-modifiable. --- services/sync/modules/engines.js | 58 +++++++++++--------------------- services/sync/modules/remote.js | 37 +++++++++++++++++++- services/sync/modules/service.js | 6 ++-- 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 6d5381cec167..fb7f2e90c5ef 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -270,29 +270,17 @@ Engine.prototype = { this._log.info("Beginning sync"); - // Before we get started, make sure we have a remote directory to play in - DAV.MKCOL(this.serverPrefix, self.cb); - let ret = yield; - if (!ret) - throw "Could not create remote folder"; + this._remote.initSession(self.cb); + yield; - this._remote.initSession(); + this._log.info("Local snapshot version: " + this._snapshot.version); + this._log.info("Server maxVersion: " + this._remote.status.data.maxVersion); + this._log.debug("Server snapVersion: " + this._remote.status.data.snapVersion); // 1) Fetch server deltas this._getServerData.async(this, self.cb); let server = yield; - this._log.info("Local snapshot version: " + this._snapshot.version); - this._log.info("Server status: " + server.status); - this._log.info("Server maxVersion: " + server.maxVersion); - this._log.info("Server snapVersion: " + server.snapVersion); - - if (server.status != 0) { - this._log.fatal("Sync error: could not get server status, " + - "or initial upload failed. Aborting sync."); - return; - } - // 2) Generate local deltas from snapshot -> current client status let localJson = new SnapshotStore(); @@ -399,39 +387,31 @@ Engine.prototype = { if (serverDelta.length) { this._log.info("Uploading changes to server"); - this._snapshot.data = newSnapshot; this._snapshot.version = ++server.maxVersion; - server.deltas.push(serverDelta); - - if (server.formatVersion != ENGINE_STORAGE_FORMAT_VERSION || - this._encryptionChanged) { + /* + if (server.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) { this._fullUpload.async(this, self.cb); let status = yield; if (!status) this._log.error("Could not upload files to server"); // eep? + */ - } else { - this._remote.deltas.put(self.cb, server.deltas); - yield; + this._remote.appendDelta(self.cb, serverDelta); + yield; - let c = 0; - for (GUID in this._snapshot.data) - c++; + let c = 0; + for (GUID in this._snapshot.data) + c++; - this._remote.status.put(self.cb, - {GUID: this._snapshot.GUID, - formatVersion: ENGINE_STORAGE_FORMAT_VERSION, - snapVersion: server.snapVersion, - maxVersion: this._snapshot.version, - snapEncryption: server.snapEncryption, - deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c}); + this._remote.status.data.maxVersion = this._snapshot.version; + this._remote.status.data.snapEncryption = Crypto.defaultAlgorithm; + this._remote.status.data.itemCount = c; + this._remote.status.put(self.cb, this._remote.status.data); - this._log.info("Successfully updated deltas and status on server"); - this._snapshot.save(); - } + this._log.info("Successfully updated deltas and status on server"); + this._snapshot.save(); } this._log.info("Sync complete"); diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 0caf0d58e993..8ec83bc36b60 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -369,6 +369,7 @@ Deltas.prototype = { function RemoteStore(serverPrefix, cryptoId) { this._prefix = serverPrefix; this._cryptoId = cryptoId; + this._log = Log4Moz.Service.getLogger("Service.RemoteStore"); } RemoteStore.prototype = { get serverPrefix() this._prefix, @@ -410,7 +411,7 @@ RemoteStore.prototype = { return deltas; }, - initSession: function RStore_initSession(serverPrefix, cryptoId) { + _initSession: function RStore__initSession(serverPrefix, cryptoId) { let self = yield; if (serverPrefix) @@ -425,11 +426,21 @@ RemoteStore.prototype = { this.snapshot.data = null; this.deltas.data = null; + DAV.MKCOL(this.serverPrefix, self.cb); + let ret = yield; + if (!ret) + throw "Could not create remote folder"; + + this._log.debug("Downloading status file"); this.status.get(self.cb); yield; + this._log.debug("Downloading status file... done"); this._inited = true; }, + initSession: function RStore_initSession(onComplete, serverPrefix, cryptoId) { + this._initSession.async(this, onComplete, serverPrefix, cryptoId); + }, closeSession: function RStore_closeSession() { this._inited = false; @@ -437,5 +448,29 @@ RemoteStore.prototype = { this.keys.data = null; this.snapshot.data = null; this.deltas.data = null; + }, + + _appendDelta: function RStore__appendDelta(delta) { + let self = yield; + if (this.deltas.data == null) { + this.deltas.get(self.cb); + yield; + if (this.deltas.data == null) + this.deltas.data = []; + } + this.deltas.data.push(delta); + this.deltas.put(self.cb, this.deltas.data); + yield; + }, + appendDelta: function RStore_appendDelta(onComplete, delta) { + this._appendDeltas.async(this, onComplete, delta); + }, + + _getUpdates: function RStore__getUpdates(lastSyncSnap) { + let self = yield; + + }, + getUpdates: function RStore_getUpdates(onComplete, lastSyncSnap) { + this._getUpdates.async(this, onComplete); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d8d68bb26fb0..83fcf2a9191d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -479,6 +479,8 @@ WeaveSvc.prototype = { let names = yield; for (let i = 0; i < names.length; i++) { + if (names[i].match(/\.htaccess$/)) + continue; DAV.DELETE(names[i], self.cb); let resp = yield; this._log.debug(resp.status); @@ -573,7 +575,7 @@ WeaveSvc.prototype = { to indicate whether sharing succeeded or failed. Implementation, as well as the interpretation of what 'guid' means, is left up to the engine for the specific dataType. */ - + // TODO who is listening for the share-bookmarks message? let messageName = "share-" + dataType; // so for instance, if dataType is "bookmarks" then a message @@ -585,7 +587,7 @@ WeaveSvc.prototype = { guid, username)).async(this, onComplete); }, - _shareBookmarks: function WeaveSync__shareBookmarks(dataType, + _shareBookmarks: function WeaveSync__shareBookmarks(dataType, guid, username) { let self = yield; From 1cf7a11fdc6d5f3242a8b4ac19fd3597197ba754 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 00:03:28 -0700 Subject: [PATCH 0325/1860] give Engine a public getter for its snapshot that TabTracker can access when generating a sync urgency score --- services/sync/modules/engines.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 6d5381cec167..b9ea81b9b2c0 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -101,6 +101,8 @@ Engine.prototype = { // "user-data/default-engine/"; get serverPrefix() { throw "serverPrefix property must be overridden in subclasses"; }, + get snapshot() this._snapshot, + get _remote() { if (!this.__remote) this.__remote = new RemoteStore(this.serverPrefix, 'Engine:' + this.name); From b5a948750db6e0d733d4c8dbce8163fc71ff8eb2 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 10:38:25 -0700 Subject: [PATCH 0326/1860] bug 434816: use a decreasing threshold algorithm for the periodic scheduled sync to make sure we eventually sync even small changes to data; r=thunder --- services/sync/modules/service.js | 106 +++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4093648eebb0..21f5db80b548 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -41,6 +41,28 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +// The following constants determine when Weave will automatically sync data. + +// An interval of one minute, initial threshold of 100, and step of 5 means +// that we'll try to sync each engine 21 times, once per minute, at +// consecutively lower thresholds (from 100 down to 5 in steps of 5 and then +// one more time with the threshold set to the minimum 1) before resetting +// the engine's threshold to the initial value and repeating the cycle +// until at some point the engine's score exceeds the threshold, at which point +// we'll sync it, reset its threshold to the initial value, rinse, and repeat. + +// How long we wait between sync checks. +const SCHEDULED_SYNC_INTERVAL = 60 * 1000; // one minute + +// INITIAL_THRESHOLD represents the value an engine's score has to exceed +// in order for us to sync it the first time we start up (and the first time +// we do a sync check after having synced the engine or reset the threshold). +const INITIAL_THRESHOLD = 100; + +// THRESHOLD_DECREMENT_STEP is the amount by which we decrement an engine's +// threshold each time we do a sync check and don't sync that engine. +const THRESHOLD_DECREMENT_STEP = 5; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); @@ -209,7 +231,7 @@ WeaveSvc.prototype = { this._scheduleTimer = Cc["@mozilla.org/timer;1"]. createInstance(Ci.nsITimer); let listener = new Utils.EventListener(Utils.bind2(this, this._onSchedule)); - this._scheduleTimer.initWithCallback(listener, 120000, // 2 min + this._scheduleTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL, this._scheduleTimer.TYPE_REPEATING_SLACK); this._log.info("Weave scheduler enabled"); }, @@ -225,7 +247,7 @@ WeaveSvc.prototype = { _onSchedule: function WeaveSync__onSchedule() { if (this.enabled) { this._log.info("Running scheduled sync"); - this.sync(); + this._lock(this._notify("sync", this._syncAsNeeded)).async(this); } }, @@ -490,6 +512,7 @@ WeaveSvc.prototype = { sync: function WeaveSync_sync(onComplete) { this._lock(this._notify("sync", this._sync)).async(this, onComplete); }, + _sync: function WeaveSync__sync() { let self = yield; @@ -504,17 +527,88 @@ WeaveSvc.prototype = { let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { - if (engines[i].enabled && engines[i]._tracker.score >= 30) { - this._notify(engines[i].name + "-engine:sync", - this._syncEngine, engines[i]).async(this, self.cb); + if (!engines[i].enabled) + continue; + this._notify(engines[i].name + "-engine:sync", + this._syncEngine, engines[i]).async(this, self.cb); + yield; + if (engines[i].name == "bookmarks") { // FIXME + Engines.get("bookmarks").syncMounts(self.cb); yield; - if (engines[i].name == "bookmarks") { // FIXME + } + } + }, + + // The values that engine scores must meet or exceed before we sync them + // as needed. These are engine-specific, as different kinds of data change + // at different rates, so we store them in a hash indexed by engine name. + _syncThresholds: {}, + + _syncAsNeeded: function WeaveSync__syncAsNeeded() { + let self = yield; + + if (!this._loggedIn) + throw "Can't sync: Not logged in"; + + this._versionCheck.async(this, self.cb); + yield; + + this._keyCheck.async(this, self.cb); + yield; + + let engines = Engines.getAll(); + for each (let engine in engines) { + if (!engine.enabled) + continue; + + if (!(engine.name in this._syncThresholds)) + this._syncThresholds[engine.name] = INITIAL_THRESHOLD; + + if (engine._tracker.score >= this._syncThresholds[engine.name]) { + this._log.debug(engine.name + " score " + engine._tracker.score + + " exceeds threshold " + + this._syncThresholds[engine.name] + "; syncing"); + this._notify(engine.name + "-engine:sync", + this._syncEngine, engine).async(this, self.cb); + yield; + + // Reset the engine's threshold to the initial value. + // Note: we do this after syncing the engine so that we'll try again + // next time around if syncing fails for some reason. The upside + // of this approach is that we'll sync again as soon as possible; + // but the downside is that if the error is caused by the server being + // overloaded, we'll contribute to the problem by trying to sync + // repeatedly at the maximum rate. + this._syncThresholds[engine.name] = INITIAL_THRESHOLD; + + if (engine.name == "bookmarks") { // FIXME Engines.get("bookmarks").syncMounts(self.cb); yield; } } + else { + this._log.debug(engine.name + " score " + engine._tracker.score + + " does not exceed threshold " + + this._syncThresholds[engine.name] + "; not syncing"); + + if (this._syncThresholds[engine.name] == 1) { + // We've gone as low as we can go, which means there are no changes + // at all, so start again from the initial threshold. + this._syncThresholds[engine.name] = INITIAL_THRESHOLD; + } + else { + // Decrement the threshold by the standard amount, but if this puts us + // at or below zero, then set the threshold to one so we can try once + // at that lowest level to make sure we sync any changes no matter how + // small before resetting to the initial threshold and starting over. + this._syncThresholds[engine.name] -= THRESHOLD_DECREMENT_STEP; + if (this._syncThresholds[engine.name] <= 0) + this._syncThresholds[engine.name] = 1; + } + } } }, + _syncEngine: function WeaveSvc__syncEngine(engine) { let self = yield; try { From 185ca9b4cf9fbb09f0e88a5ddb2ca8fd27bf25c6 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 10:40:24 -0700 Subject: [PATCH 0327/1860] bug 430363: ignore remove commands when generating deltas for history so the deltas file on the server doesn't grow too large; r=thunder --- services/sync/modules/engines/history.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index ec742427ee51..679f67d48247 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -10,6 +10,9 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; function HistoryEngine(pbeId) { this._init(pbeId); @@ -59,6 +62,21 @@ HistorySyncCore.prototype = { // the GUID, so the same sites will map to the same item (same // GUID), without our intervention. return false; + }, + + /** + * Determine the differences between two snapshots. This method overrides + * the one in its superclass so it can ignore removes, since removes don't + * matter for history (and would cause deltas to grow too large too fast). + */ + _detectUpdates: function HSC__detectUpdates(a, b) { + let self = yield; + + this.__proto__.__proto__._detectUpdates.async(this, self.cb, a, b); + let cmds = yield; + cmds = cmds.filter(function (v) v.action != "remove"); + + self.done(cmds); } }; HistorySyncCore.prototype.__proto__ = new SyncCore(); From bee2c780ba275ac273ac26b675d8bc61a6dbf706 Mon Sep 17 00:00:00 2001 From: Date: Wed, 11 Jun 2008 11:01:45 -0700 Subject: [PATCH 0328/1860] Expanded bookmarkEngine.share and added some more todos for the next round of functions to implement --- services/sync/modules/engines/bookmarks.js | 34 ++++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 40adefc9f1a0..489cf3b360ed 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -187,11 +187,15 @@ BookmarksEngine.prototype = { getService(Ci.nsIAnnotationService); let self = yield; - /* TODO check to see we're not already sharing this thing. */ - // TODO call this._createOutgoingShare( guid, username ); - // TODO call this._updateOutgoingShare( guid, username ); - // TODO call - // this._xmppClient.sendMessage( "Hey I share with you ", username ); + /* TODO What should the behavior be if i'm already sharing it with user + A and I ask to share it with user B? (This should be prevented by + the UI. */ + + // Create the outgoing share folder on the server + // TODO do I need to call these asynchronously? + this._createOutgoingShare.async( this, selectedFolder, username ); + this._updateOutgoingShare.async( this, selectedFolder, username ); + /* Set the annotation on the folder so we know it's an outgoing share: */ let folderItemId = selectedFolder.node.itemId; @@ -199,6 +203,13 @@ BookmarksEngine.prototype = { ans.setItemAnnotation(folderItemId, OUTGOING_SHARED_ANNO, username, 0, ans.EXPIRE_NEVER); // TODO: does this clobber existing annotations? + + // Send an xmpp message to the share-ee + if ( this._xmppClient ) { + let msgText = "share " + folderName; + this._xmppClient.sendMessage( username, msgText ); + } + /* LONGTERM TODO: in the future when we allow sharing one folder with many people, the value of the annotation can be a whole list of usernames instead of just one. */ @@ -233,7 +244,7 @@ BookmarksEngine.prototype = { } }, - _createOutgoingShare: function BmkEngine__createOutgoing(guid, username) { + _createOutgoingShare: function BmkEngine__createOutgoing(folder, username) { let self = yield; let prefix = DAV.defaultPrefix; @@ -246,6 +257,7 @@ BookmarksEngine.prototype = { DAV.GET(this.keysFile, self.cb); let ret = yield; Utils.ensureStatus(ret.status, "Could not get keys file."); + // note: this._json is just an encoder/decoder, no state. let keys = this._json.decode(ret.responseText); // get the other user's pubkey @@ -275,11 +287,11 @@ BookmarksEngine.prototype = { ret = yield; Utils.ensureStatus(ret.status, "Could not upload keyring file."); - /* TODO send an XMPP message to the recipient of the share to tell - them to call _createIncomingShare. */ - this._log.debug("All done sharing!"); + // TODO this function also needs to call Atul's js api for setting + // htaccess. + self.done(true); }, @@ -288,6 +300,10 @@ BookmarksEngine.prototype = { subtree out of the store and put it in a separate file...*/ }, + _stopOutgoingShare: function BmkEngine__stopOutgoingShare( guid, username ) { + /* TODO implement this... */ + }, + _createIncomingShare: function BookmarkEngine__createShare(guid, id, title) { /* TODO This used to be called just _createShare, but its semantics From c2422c1f46197abb9501230a3342ce7562d0de5a Mon Sep 17 00:00:00 2001 From: Date: Wed, 11 Jun 2008 11:13:35 -0700 Subject: [PATCH 0329/1860] Created preferences for xmpp connection info (server url, realm, username, password), and a preference to turn xmpp messaging on or off, and made BookmarkEngine._init() call startXmppClient when this preference is true. --- services/sync/modules/engines/bookmarks.js | 26 ++++++++++++++++------ services/sync/services-sync.js | 7 ++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 489cf3b360ed..051a3c6e2b1a 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -92,19 +92,31 @@ BookmarksEngine.prototype = { return this.__tracker; }, + _init: function BmkEngine__init( pbeId ) { + this.__proto__.__proto__._init.call( this, pbeId ); + if ( Utils.prefs.getBoolPref( "xmpp.enabled" ) ) { + dump( "Starting XMPP client for bookmark engine..." ); + this._startXmppClient(); + // TODO catch errors if connection fails. + } + } + _startXmppClient: function BmkEngine__startXmppClient() { /* this should probably be called asynchronously as it can take a while. */ - // TODO add preferences for serverUrl and realm. - // Also add a boolean preference to turn XMPP messaging on or off. - let serverUrl = "http://sm-labs01.mozilla.org:5280/http_poll"; - let realm = "sm-labs01.mozilla.org"; + // Get serverUrl and realm of the jabber server from preferences: + let serverUrl = Utils.prefs.getStringPref( "xmpp.server.url" ); + let realm = Utils.prefs.getStringPref( "xmpp.server.realm" ); + //"http://sm-labs01.mozilla.org:5280/http_poll"; // TODO once we have ejabberd talking to LDAP, the username/password // for xmpp will be the same as the ones for Weave itself, so we can - // read username/password from ID.get('WeaveID' - let clientName = ID.get('WeaveID').username; - let clientPassword = ID.get('WeaveID').password; + // read username/password like this: + // let clientName = ID.get('WeaveID').username; + // let clientPassword = ID.get('WeaveID').password; + // until then get these from preferences as well: + let clientName = Utils.prefs.getStringPref( "xmpp.client.name" ); + let clientPassword = Utils.prefs.getStringPref( "xmpp.client.password" ); let transport = new HTTPPollingTransport( serverUrl, false, 15000 ); let auth = new PlainAuthenticator(); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 5769981402aa..1c0958640ca8 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -31,3 +31,10 @@ pref("extensions.weave.log.logger.service.crypto", "Debug"); pref("extensions.weave.log.logger.service.dav", "Debug"); pref("extensions.weave.log.logger.service.engine", "Debug"); pref("extensions.weave.log.logger.service.main", "Trace"); + +pref("extensions.weave.xmpp.enabled", true); +pref("extensions.weave.xmpp.server.url", + "http://sm-labs01.mozilla.org:5280/http_poll"); +pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); +pref("extensions.weave.xmpp.client.name", ""); +pref("extensions.weave.xmpp.client.password", ""); From 01e5045ece5c1471359cedde87c28b472c56107b Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 13:50:47 -0700 Subject: [PATCH 0330/1860] once sync thresholds reach 1 (the lowest possible value), leave them there until something changes and we sync --- services/sync/modules/service.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d4c13bd623a4..b1ac19b23ead 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -593,20 +593,13 @@ WeaveSvc.prototype = { " does not exceed threshold " + this._syncThresholds[engine.name] + "; not syncing"); - if (this._syncThresholds[engine.name] == 1) { - // We've gone as low as we can go, which means there are no changes - // at all, so start again from the initial threshold. - this._syncThresholds[engine.name] = INITIAL_THRESHOLD; - } - else { - // Decrement the threshold by the standard amount, but if this puts us - // at or below zero, then set the threshold to one so we can try once - // at that lowest level to make sure we sync any changes no matter how - // small before resetting to the initial threshold and starting over. - this._syncThresholds[engine.name] -= THRESHOLD_DECREMENT_STEP; - if (this._syncThresholds[engine.name] <= 0) - this._syncThresholds[engine.name] = 1; - } + // Decrement the threshold by the standard amount, and if this puts it + // at or below zero, then set it to 1, the lowest possible value, where + // it'll stay until there's something to sync (whereupon we'll sync it, + // reset the threshold to the initial value, and start over again). + this._syncThresholds[engine.name] -= THRESHOLD_DECREMENT_STEP; + if (this._syncThresholds[engine.name] <= 0) + this._syncThresholds[engine.name] = 1; } } }, From 8ae0c301c8e734a582f65b4847eb7e5c717b8fc4 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 14:14:04 -0700 Subject: [PATCH 0331/1860] fix typo in recent checkin that broke appending deltas to the deltas file on the server --- services/sync/modules/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 8ec83bc36b60..4caaa8d35c48 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -463,7 +463,7 @@ RemoteStore.prototype = { yield; }, appendDelta: function RStore_appendDelta(onComplete, delta) { - this._appendDeltas.async(this, onComplete, delta); + this._appendDelta.async(this, onComplete, delta); }, _getUpdates: function RStore__getUpdates(lastSyncSnap) { From 04367adfeaee87f7a96a699d793d32e915739df3 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 14:16:03 -0700 Subject: [PATCH 0332/1860] clarify wording in scheduled sync threshold debug statements --- services/sync/modules/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b1ac19b23ead..e2300aed5f4f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -568,7 +568,7 @@ WeaveSvc.prototype = { if (engine._tracker.score >= this._syncThresholds[engine.name]) { this._log.debug(engine.name + " score " + engine._tracker.score + - " exceeds threshold " + + " reaches threshold " + this._syncThresholds[engine.name] + "; syncing"); this._notify(engine.name + "-engine:sync", this._syncEngine, engine).async(this, self.cb); @@ -590,7 +590,7 @@ WeaveSvc.prototype = { } else { this._log.debug(engine.name + " score " + engine._tracker.score + - " does not exceed threshold " + + " does not reach threshold " + this._syncThresholds[engine.name] + "; not syncing"); // Decrement the threshold by the standard amount, and if this puts it From c45c85e027ab1e81cf9bd99e524d846e80a35cf4 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 15:23:54 -0700 Subject: [PATCH 0333/1860] resetting the score is not an asynchronous operation, so Service::_syncEngine shouldn't yield after calling it --- services/sync/modules/service.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e2300aed5f4f..9ccc98df1cca 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -610,7 +610,6 @@ WeaveSvc.prototype = { engine.sync(self.cb); yield; engine._tracker.resetScore(); - yield; } catch(e) { this._log.error(Utils.exceptionStr(e)); if (e.trace) From 84e495255119d83418902e0ab6b16e3b0e5b023b Mon Sep 17 00:00:00 2001 From: Date: Wed, 11 Jun 2008 15:43:12 -0700 Subject: [PATCH 0334/1860] Fixed Utils.prefs.getStringPref (should be getCharPref) --- services/sync/modules/engines/bookmarks.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 051a3c6e2b1a..7042611d1bcf 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -96,18 +96,18 @@ BookmarksEngine.prototype = { this.__proto__.__proto__._init.call( this, pbeId ); if ( Utils.prefs.getBoolPref( "xmpp.enabled" ) ) { dump( "Starting XMPP client for bookmark engine..." ); + // TODO call startXmppClient asynchronously? this._startXmppClient(); // TODO catch errors if connection fails. } - } + }, _startXmppClient: function BmkEngine__startXmppClient() { - /* this should probably be called asynchronously as it can take a while. */ + // TODO this should probably be called asynchronously as it can take a while. // Get serverUrl and realm of the jabber server from preferences: - let serverUrl = Utils.prefs.getStringPref( "xmpp.server.url" ); - let realm = Utils.prefs.getStringPref( "xmpp.server.realm" ); - //"http://sm-labs01.mozilla.org:5280/http_poll"; + let serverUrl = Utils.prefs.getCharPref( "xmpp.server.url" ); + let realm = Utils.prefs.getCharPref( "xmpp.server.realm" ); // TODO once we have ejabberd talking to LDAP, the username/password // for xmpp will be the same as the ones for Weave itself, so we can @@ -115,8 +115,8 @@ BookmarksEngine.prototype = { // let clientName = ID.get('WeaveID').username; // let clientPassword = ID.get('WeaveID').password; // until then get these from preferences as well: - let clientName = Utils.prefs.getStringPref( "xmpp.client.name" ); - let clientPassword = Utils.prefs.getStringPref( "xmpp.client.password" ); + let clientName = Utils.prefs.getCharPref( "xmpp.client.name" ); + let clientPassword = Utils.prefs.getCharPref( "xmpp.client.password" ); let transport = new HTTPPollingTransport( serverUrl, false, 15000 ); let auth = new PlainAuthenticator(); @@ -301,8 +301,10 @@ BookmarksEngine.prototype = { this._log.debug("All done sharing!"); - // TODO this function also needs to call Atul's js api for setting - // htaccess. + // Call Atul's js api for setting htaccess: + let api = new Sharing.Api( DAV ); + api.shareWithUsers( directory, [username], self.cb ); + let result = yield; self.done(true); }, From 2b2e9033406c554106c667aaa4d2062957569bd9 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 11 Jun 2008 16:38:22 -0700 Subject: [PATCH 0335/1860] Added a unit test for async exceptions. --- .../sync/tests/unit/test_async_exceptions.js | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 services/sync/tests/unit/test_async_exceptions.js diff --git a/services/sync/tests/unit/test_async_exceptions.js b/services/sync/tests/unit/test_async_exceptions.js new file mode 100644 index 000000000000..fa754d835188 --- /dev/null +++ b/services/sync/tests/unit/test_async_exceptions.js @@ -0,0 +1,47 @@ +const Cu = Components.utils; + +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; + +function thirdGen() { + let self = yield; + + Utils.makeTimerForCall(self.cb); + yield; + + throw new Error("intentional failure"); +} + +function secondGen() { + let self = yield; + + thirdGen.async({}, self.cb); + try { + let result = yield; + } catch (e) { + do_check_eq(e.message, "Error: intentional failure"); + throw e; + } + + self.done(); +} + +function runTestGenerator() { + let self = yield; + + secondGen.async({}, self.cb); + let wasCaught = false; + try { + let result = yield; + } catch (e) { + do_check_eq(e.message, "Error: intentional failure"); + wasCaught = true; + } + + do_check_true(wasCaught); + self.done(); +} + +var run_test = makeAsyncTestRunner(runTestGenerator); From 421ec9f2042e797a31dd4224d4429bf10ef4eded Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 11 Jun 2008 17:10:39 -0700 Subject: [PATCH 0336/1860] Modified test_async_exceptions to use a fake nsiTimer. --- .../sync/tests/unit/test_async_exceptions.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_async_exceptions.js b/services/sync/tests/unit/test_async_exceptions.js index fa754d835188..f1fe78eadb28 100644 --- a/services/sync/tests/unit/test_async_exceptions.js +++ b/services/sync/tests/unit/test_async_exceptions.js @@ -5,6 +5,15 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; +var callbackQueue = []; + +Utils.makeTimerForCall = function fake_makeTimerForCall(cb) { + // Just add the callback to our queue and we'll call it later, so + // as to simulate a real nsITimer. + callbackQueue.push(cb); + return "fake nsITimer"; +}; + function thirdGen() { let self = yield; @@ -44,4 +53,13 @@ function runTestGenerator() { self.done(); } -var run_test = makeAsyncTestRunner(runTestGenerator); +function run_test() { + runTestGenerator.async({}); + let i = 1; + while (callbackQueue.length > 0) { + let cb = callbackQueue.pop(); + cb(); + i += 1; + } + do_check_eq(i, 5); +} From 4a92e1d58fd7589c8ca3f04bf1b64a06e1c5fa19 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 17:44:08 -0700 Subject: [PATCH 0337/1860] bug 437529: yield after starting to put the status file to the server so we don't finalize the sync until the PUT request completes --- services/sync/modules/engines.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5a14087a05c6..a7a7cfa467b3 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -411,6 +411,7 @@ Engine.prototype = { this._remote.status.data.snapEncryption = Crypto.defaultAlgorithm; this._remote.status.data.itemCount = c; this._remote.status.put(self.cb, this._remote.status.data); + yield; this._log.info("Successfully updated deltas and status on server"); this._snapshot.save(); From 6203749bbe4a4b789d262f4d82849cff25ccfa4a Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Wed, 11 Jun 2008 17:56:02 -0700 Subject: [PATCH 0338/1860] bug 438033: implement a better first-run wizard process; r=myk --- services/sync/locales/en-US/wizard.dtd | 65 ++++++++++--------- services/sync/locales/en-US/wizard.properties | 20 +++++- services/sync/modules/service.js | 2 +- 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index e88150ed9fca..4c1c016eb15a 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,37 +1,42 @@ - - - - - + + + + - - + + + + + + + + - - - - - + + + + + + + + + + - - - - - - - - - + + + + + + + + - - - - - - + + - - - + + + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index da6ce0fe1993..fbfea30a2f05 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,4 +1,5 @@ verifyStatusUnverified.label = Status: Unverified +verifyStatusVerifying.label = Status: Verifying account... verifyStatusLoginVerified.label = Status: Account Verified verifyStatusLoginFailed.label = Status: Authentication Failed @@ -9,5 +10,22 @@ initStatusSyncFailed.label = Status: Transfer Failed invalidCredentials.alert = You must provide a valid Weave user name and password to continue. +usernameTaken.label = That username is already taken. Please choose another. +usernameAvailable.label = That username is available. + +loginFailed.label = Please enter a valid username and password. + +requiredFields.label = Please enter all required fields. + +passwordsUnmatched.label = Passwords don't match. Please re-enter. +passphrasesUnmatched.label = Passphrases don't match. Please re-enter. + noPassphrase.alert = You must enter a passphrase -samePasswordAndPassphrase.alert = Your password and passphrase must be different \ No newline at end of file +samePasswordAndPassphrase.label = Your password and passphrase must be different +samePasswordAndPassphrase.alert = Your password and passphrase must be different + +invalidEmail.label = Invalid email address. +emailAlreadyExists.label = Email address already exists. +missingCaptchaResponse.label = Please enter the words in the Captcha box. +incorrectCaptcha.label = Incorrect Captcha response. Try again. +internalError.label = Sorry! We had a problem. Please try again. diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9ccc98df1cca..290310a6dd2b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -196,7 +196,7 @@ WeaveSvc.prototype = { return 0; // manual/off return Utils.prefs.getIntPref("schedule"); }, - + onWindowOpened: function Weave__onWindowOpened() { if (!this._startupFinished) { if (Utils.prefs.getBoolPref("autoconnect") && From ad9850eeee0ec1e0f8605676bec0010cb7ca6b2b Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 11 Jun 2008 18:02:46 -0700 Subject: [PATCH 0339/1860] Added test_async_missing_yield. It's very messy right now and duplicates code from other tests, but I've got some ideas about how to write better tests for async ops that I'll commit soon. --- .../tests/unit/test_async_missing_yield.js | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 services/sync/tests/unit/test_async_missing_yield.js diff --git a/services/sync/tests/unit/test_async_missing_yield.js b/services/sync/tests/unit/test_async_missing_yield.js new file mode 100644 index 000000000000..14347ae518b9 --- /dev/null +++ b/services/sync/tests/unit/test_async_missing_yield.js @@ -0,0 +1,69 @@ +const Cu = Components.utils; + +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; + +var callbackQueue = []; + +Utils.makeTimerForCall = function fake_makeTimerForCall(cb) { + // Just add the callback to our queue and we'll call it later, so + // as to simulate a real nsITimer. + callbackQueue.push(cb); + return "fake nsITimer"; +}; + +function secondGen() { + let self = yield; + + callbackQueue.push(self.cb); + + self.done(); +} + +function runTestGenerator() { + let self = yield; + + secondGen.async({}, self.cb); + let result = yield; + + self.done(); +} + +function run_test() { + const Cu = Components.utils; + + Cu.import("resource://weave/log4moz.js"); + Cu.import("resource://weave/async.js"); + + var errorsLogged = 0; + + function _TestFormatter() {} + _TestFormatter.prototype = { + format: function BF_format(message) { + if (message.level == Log4Moz.Level.Error) + errorsLogged += 1; + return message.loggerName + "\t" + message.levelDesc + "\t" + + message.message + "\n"; + } + }; + _TestFormatter.prototype.__proto__ = new Log4Moz.Formatter(); + + var log = Log4Moz.Service.rootLogger; + var formatter = new _TestFormatter(); + var appender = new Log4Moz.DumpAppender(formatter); + log.level = Log4Moz.Level.Trace; + appender.level = Log4Moz.Level.Trace; + log.addAppender(appender); + + runTestGenerator.async({}); + let i = 1; + while (callbackQueue.length > 0) { + let cb = callbackQueue.pop(); + cb(); + i += 1; + } + + do_check_eq(errorsLogged, 3); +} From 04a7cdc22be29699d31deef9650c45680ed862d6 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 18:47:56 -0700 Subject: [PATCH 0340/1860] numChanged should be the number of shared items whose data is different, not the same --- services/sync/modules/engines/tabs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 9ebae8da490a..194210e4dac9 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -460,7 +460,7 @@ TabTracker.prototype = { return 0; // The number of shared items whose data is different. - let numChanged = c.filter(function(v) v).length; + let numChanged = c.filter(function(v) !v).length; let fractionSimilar = (numShared - (numChanged / 2)) / numTotal; let fractionDissimilar = 1 - fractionSimilar; From bc3cae1d9fc0b8738f961e99dd40695b264bf8a6 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 11 Jun 2008 18:58:30 -0700 Subject: [PATCH 0341/1860] Added a few log messages to hopefully make the debugging of generators easier. Also added an id component to generators, which is part of their name, to help distinguish between concurrent instances of the same generator function. The following debug output represents the new logging infomation: -- Async.Generator DEBUG runTestGenerator-0: self.cb generated at test_async_missing_yield.js:28 Async.Generator DEBUG secondGen-1: self.cb generated at test_async_missing_yield.js:20 Async.Generator DEBUG secondGen-1: done() called. Async.Generator DEBUG runTestGenerator-0: self.cb() called, resuming coroutine. Async.Generator DEBUG runTestGenerator-0: done() called. Async.Generator DEBUG secondGen-1: self.cb() called, resuming coroutine. Async.Generator DEBUG secondGen-1: done() called. Async.Generator ERROR Async method 'secondGen-1' is missing a 'yield' call (or called done() after being finalized) -- As you can see, I've added log messages whenever the Generator's 'cb' property is accessed--this is almost guaranteed to be very close to a 'yield' statement, and therefore provides us with a decently accurate idea of where the generator 'stopped'. We also log a message when the generator continues, and by doing so we get an idea of how the coroutines interleave. Another idea I had was to actually match calls to self.cb with calls to 'yield' to automatically detect e.g. two yields in a row (which will ordinarily result in a generator 'hanging'), a generator exiting while a self.cb still hasn't been called, but I'm not sure what kinds of reprecussions it may have. --- services/sync/modules/async.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index ace09b9e5c17..e51346561339 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -48,6 +48,8 @@ Cu.import("resource://weave/util.js"); * Asynchronous generator helpers */ +let currentId = 0; + function AsyncException(initFrame, message) { this.message = message; this._trace = initFrame; @@ -74,6 +76,7 @@ function Generator(thisArg, method, onComplete, args) { Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; this._thisArg = thisArg; this._method = method; + this._id = currentId++; this.onComplete = onComplete; this._args = args; this._initFrame = Components.stack.caller; @@ -83,10 +86,14 @@ function Generator(thisArg, method, onComplete, args) { this._initFrame = this._initFrame.caller; } Generator.prototype = { - get name() { return this._method.name; }, + get name() { return this._method.name + "-" + this._id; }, get generator() { return this._generator; }, get cb() { + let caller = Components.stack.caller; + this._log.debug(this.name + + ": self.cb generated at " + + caller.filename + ":" + caller.lineNumber); let self = this, cb = function(data) { self.cont(data); }; cb.parentGenerator = this; return cb; @@ -126,6 +133,7 @@ Generator.prototype = { _handleException: function AsyncGen__handleException(e) { if (e instanceof StopIteration) { + this._log.debug(this.name + ": End of coroutine reached."); // skip to calling done() } else if (this.onComplete.parentGenerator instanceof Generator) { @@ -171,6 +179,7 @@ Generator.prototype = { }, cont: function AsyncGen_cont(data) { + this._log.debug(this.name + ": self.cb() called, resuming coroutine."); this._continued = true; try { this.generator.send(data); } catch (e) { @@ -199,6 +208,7 @@ Generator.prototype = { return; let self = this; let cb = function() { self._done(retval); }; + this._log.debug(this.name + ": done() called."); this._timer = Utils.makeTimerForCall(cb); }, From b368713b533ed30269a7ecb9b6c5777a1f3dbe53 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 11 Jun 2008 19:19:16 -0700 Subject: [PATCH 0342/1860] async.js now keeps track of how many outstanding callbacks it has and uses this information to log warnings about coroutines that may have yielded without an outstanding callback, and coroutines that may have finished while a callback is still outstanding. These are merely 'warnings' rather than certainties because this code assumes that there is a 1:1 correspondence between accesses to self.cb and yields, and also that self.cb's are actually passed to asynchronous functions. It'd be really cool if we could actually keep track of whether a callback got garbage collected before it was called or something, though I don't know how much it'd help in the end. --- services/sync/modules/async.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index e51346561339..2c4d7f38ee33 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -71,6 +71,7 @@ AsyncException.prototype = { }; function Generator(thisArg, method, onComplete, args) { + this._outstandingCbs = 0; this._log = Log4Moz.Service.getLogger("Async.Generator"); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; @@ -91,6 +92,7 @@ Generator.prototype = { get cb() { let caller = Components.stack.caller; + this._outstandingCbs++; this._log.debug(this.name + ": self.cb generated at " + caller.filename + ":" + caller.lineNumber); @@ -166,12 +168,20 @@ Generator.prototype = { } }, + _detectDeadlock: function AsyncGen_detectDeadlock() { + if (this._outstandingCbs == 0) + this._log.warn("Async method '" + this.name + + "' may have yielded without an " + + "outstanding callback."); + }, + run: function AsyncGen_run() { this._continued = false; try { this._generator = this._method.apply(this._thisArg, this._args); this.generator.next(); // must initialize before sending this.generator.send(this); + this._detectDeadlock(); } catch (e) { if (!(e instanceof StopIteration) || !this._timer) this._handleException(e); @@ -179,10 +189,13 @@ Generator.prototype = { }, cont: function AsyncGen_cont(data) { + this._outstandingCbs--; this._log.debug(this.name + ": self.cb() called, resuming coroutine."); this._continued = true; - try { this.generator.send(data); } - catch (e) { + try { + this.generator.send(data); + this._detectDeadlock(); + } catch (e) { if (!(e instanceof StopIteration) || !this._timer) this._handleException(e); } @@ -209,6 +222,9 @@ Generator.prototype = { let self = this; let cb = function() { self._done(retval); }; this._log.debug(this.name + ": done() called."); + if (this._outstandingCbs > 0) + this._log.warn("Async method '" + this.name + + "' may have outstanding callbacks."); this._timer = Utils.makeTimerForCall(cb); }, From 7b3a034696ba05d1dc23b8b3425b620382ab8aed Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 20:00:48 -0700 Subject: [PATCH 0343/1860] don't sync tab entry IDs, which change with every session, to avoid generating edit commands for every tab on restart even when the tabs haven't actually changed --- services/sync/modules/engines/tabs.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 194210e4dac9..893ba5a75473 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -349,6 +349,13 @@ TabStore.prototype = { let tabID = currentEntry.url; this._log.debug("_wrapRealTabs: tab " + tabID); + // The ID property of each entry in the tab, which I think contains + // nsISHEntry::ID, changes every time session store restores the tab, + // so we can't sync them, or we would generate edit commands on every + // restart (even though nothing has actually changed). + for each (let entry in tab.entries) + delete entry.ID; + items[tabID] = { // Identify this item as a tab in case we start serializing windows // in the future. From e5a976111411e74f55ffe7bd31dfdcd17184de5a Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 11 Jun 2008 20:07:35 -0700 Subject: [PATCH 0344/1860] only retrieve score once per engine when doing a scheduled sync, since retrieving the score can be a non-negligible cost for trackers that calculate the score on-demand (like the tab tracker) --- services/sync/modules/service.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 290310a6dd2b..1501a9fb7df3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -566,8 +566,9 @@ WeaveSvc.prototype = { if (!(engine.name in this._syncThresholds)) this._syncThresholds[engine.name] = INITIAL_THRESHOLD; - if (engine._tracker.score >= this._syncThresholds[engine.name]) { - this._log.debug(engine.name + " score " + engine._tracker.score + + let score = engine._tracker.score; + if (score >= this._syncThresholds[engine.name]) { + this._log.debug(engine.name + " score " + score + " reaches threshold " + this._syncThresholds[engine.name] + "; syncing"); this._notify(engine.name + "-engine:sync", @@ -589,7 +590,7 @@ WeaveSvc.prototype = { } } else { - this._log.debug(engine.name + " score " + engine._tracker.score + + this._log.debug(engine.name + " score " + score + " does not reach threshold " + this._syncThresholds[engine.name] + "; not syncing"); From 1043b4d8201191bb710413dbc0511c91a5c269b7 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 12 Jun 2008 12:00:19 -0700 Subject: [PATCH 0345/1860] Changed some of the debug() logging statements I added a few commits ago into trace() statemetns b/c they were drowning the log. --- services/sync/modules/async.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 2c4d7f38ee33..619de53e7579 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -93,8 +93,7 @@ Generator.prototype = { get cb() { let caller = Components.stack.caller; this._outstandingCbs++; - this._log.debug(this.name + - ": self.cb generated at " + + this._log.trace(this.name + ": self.cb generated at " + caller.filename + ":" + caller.lineNumber); let self = this, cb = function(data) { self.cont(data); }; cb.parentGenerator = this; @@ -135,7 +134,7 @@ Generator.prototype = { _handleException: function AsyncGen__handleException(e) { if (e instanceof StopIteration) { - this._log.debug(this.name + ": End of coroutine reached."); + this._log.trace(this.name + ": End of coroutine reached."); // skip to calling done() } else if (this.onComplete.parentGenerator instanceof Generator) { @@ -190,7 +189,7 @@ Generator.prototype = { cont: function AsyncGen_cont(data) { this._outstandingCbs--; - this._log.debug(this.name + ": self.cb() called, resuming coroutine."); + this._log.trace(this.name + ": self.cb() called, resuming coroutine."); this._continued = true; try { this.generator.send(data); @@ -221,7 +220,7 @@ Generator.prototype = { return; let self = this; let cb = function() { self._done(retval); }; - this._log.debug(this.name + ": done() called."); + this._log.trace(this.name + ": done() called."); if (this._outstandingCbs > 0) this._log.warn("Async method '" + this.name + "' may have outstanding callbacks."); From 1dc8f21397b18b7443480d52223eef7e67a10c04 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Thu, 12 Jun 2008 12:36:58 -0700 Subject: [PATCH 0346/1860] bug 410550: stop running scheduled sync when the user is not logged into weave --- services/sync/modules/service.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1501a9fb7df3..063a06d02698 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -19,6 +19,7 @@ * * Contributor(s): * Dan Mills + * Myk Melez * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -130,8 +131,6 @@ function WeaveSvc() { this._log.info("Weave Sync disabled"); return; } - - this._setSchedule(this.schedule); } WeaveSvc.prototype = { @@ -139,6 +138,7 @@ WeaveSvc.prototype = { _lock: Wrap.lock, _localLock: Wrap.localLock, _osPrefix: "weave:service:", + _loggedIn: false, __os: null, get _os() { @@ -464,11 +464,14 @@ WeaveSvc.prototype = { this._loggedIn = true; + this._setSchedule(this.schedule); + self.done(true); }, logout: function WeaveSync_logout() { this._log.info("Logging out"); + this._disableSchedule(); this._loggedIn = false; ID.get('WeaveID').setTempPassword(null); // clear cached password ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase From 1a48eb4c06a110764745cdb5b33c708816be6795 Mon Sep 17 00:00:00 2001 From: Date: Thu, 12 Jun 2008 14:30:39 -0700 Subject: [PATCH 0347/1860] Fixed the server URL for xmpp connections. --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 1c0958640ca8..9941d41c9d6a 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -34,7 +34,7 @@ pref("extensions.weave.log.logger.service.main", "Trace"); pref("extensions.weave.xmpp.enabled", true); pref("extensions.weave.xmpp.server.url", - "http://sm-labs01.mozilla.org:5280/http_poll"); + "http://sm-labs01.mozilla.org:5280/http-poll"); pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); pref("extensions.weave.xmpp.client.name", ""); pref("extensions.weave.xmpp.client.password", ""); From 3002b7fb4afff40c2c903728a091ec909cfe71a1 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Thu, 12 Jun 2008 16:23:59 -0700 Subject: [PATCH 0348/1860] work around XmlHttpRequest bug 317600 by pausing for a 0ms timeout before trying to log in --- services/sync/modules/service.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 063a06d02698..ed2bbe9b08b8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -427,6 +427,15 @@ WeaveSvc.prototype = { _login: function WeaveSync__login(password, passphrase) { let self = yield; + // XmlHttpRequests fail when the window that triggers them goes away + // because of bug 317600, and the first XmlHttpRequest we do happens + // just before the login dialog closes itself (for logins prompted by + // that dialog), so it triggers the bug. To work around it, we pause + // here and then continue after a 0ms timeout. + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback({ notify: self.cb }, 0, Ci.nsITimer.TYPE_ONE_SHOT); + yield; + // cache password & passphrase // if null, we'll try to get them from the pw manager below ID.get('WeaveID').setTempPassword(password); From cf0b9b60b2ae317b78a2fe8ffe9f2d1ca500078c Mon Sep 17 00:00:00 2001 From: Date: Thu, 12 Jun 2008 17:35:44 -0700 Subject: [PATCH 0349/1860] Made the initialization of the xmppClient an asynchronous call. This included modifying xmppClient.js so that connect() can be passed a callback function that will get called once the connection has succeeded or failed. For most of our purposes this is probably a better API than what we had before where you call waitForConnection() and it busy-waits until the connection has succeeded or failed. --- services/sync/modules/engines/bookmarks.js | 40 +++++++++++++++------- services/sync/modules/xmpp/xmppClient.js | 20 ++++++++--- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 7042611d1bcf..d5607c0c308e 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -96,14 +96,14 @@ BookmarksEngine.prototype = { this.__proto__.__proto__._init.call( this, pbeId ); if ( Utils.prefs.getBoolPref( "xmpp.enabled" ) ) { dump( "Starting XMPP client for bookmark engine..." ); - // TODO call startXmppClient asynchronously? - this._startXmppClient(); - // TODO catch errors if connection fails. + this._startXmppClient.async(this); + //this._startXmppClient(); } }, _startXmppClient: function BmkEngine__startXmppClient() { - // TODO this should probably be called asynchronously as it can take a while. + // To be called asynchronously. + let self = yield; // Get serverUrl and realm of the jabber server from preferences: let serverUrl = Utils.prefs.getCharPref( "xmpp.server.url" ); @@ -127,7 +127,7 @@ BookmarksEngine.prototype = { clientPassword, transport, auth ); - let self = this; + let bmkEngine = this; let messageHandler = { handle: function ( messageText, from ) { /* The callback function for incoming xmpp messages. @@ -145,16 +145,25 @@ BookmarksEngine.prototype = { let commandWord = words[0]; let directoryName = words[1]; if ( commandWord == "share" ) { - self._incomingShareOffer( directoryName, from ); + bmkEngine._incomingShareOffer( directoryName, from ); } else if ( commandWord == "stop" ) { - self._incomingShareWithdrawn( directoryName, from ); + bmkEngine._incomingShareWithdrawn( directoryName, from ); } } } this._xmppClient.registerMessageHandler( messageHandler ); - this._xmppClient.connect( realm ); + this._xmppClient.connect( realm, self.cb ); + yield; + if ( this._xmppClient._connectionStatus == this._xmppClient.FAILED ) { + this._log.warn( "Weave can't log in to xmpp server: xmpp disabled." ); + } else if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { + this._log.info( "Weave logged into xmpp OK." ); + } + yield; + self.done(); }, + _incomingShareOffer: function BmkEngine__incomingShareOffer( dir, user ) { /* Called when we receive an offer from another user to share a directory. @@ -167,6 +176,7 @@ BookmarksEngine.prototype = { But since we don't have notification in place yet, I'm going to skip right ahead to creating the incoming share. */ + dump( "I was offered the directory " + dir + " from user " + dir ); }, @@ -205,8 +215,8 @@ BookmarksEngine.prototype = { // Create the outgoing share folder on the server // TODO do I need to call these asynchronously? - this._createOutgoingShare.async( this, selectedFolder, username ); - this._updateOutgoingShare.async( this, selectedFolder, username ); + //this._createOutgoingShare.async( this, selectedFolder, username ); + //this._updateOutgoingShare.async( this, selectedFolder, username ); /* Set the annotation on the folder so we know it's an outgoing share: */ @@ -218,9 +228,13 @@ BookmarksEngine.prototype = { // Send an xmpp message to the share-ee if ( this._xmppClient ) { - let msgText = "share " + folderName; - this._xmppClient.sendMessage( username, msgText ); - } + if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { + let msgText = "share " + folderName; + this._xmppClient.sendMessage( username, msgText ); + } else { + this._log.info( "XMPP connection not available for share notification." ); + } + } /* LONGTERM TODO: in the future when we allow sharing one folder with many people, the value of the annotation can be a whole list diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 2ddc7fdcd28c..054bf28eea81 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -53,6 +53,7 @@ XmppClient.prototype = { this._iqResponders = []; this._nextIqId = 0; this._pendingIqs = {}; + this._callbackOnConnect = null; }, __parser: null, @@ -81,10 +82,17 @@ XmppClient.prototype = { namespace */ }, + _finishConnectionAttempt: function() { + if ( this._callbackOnConnect ) { + this._callbackOnConnect.call(); + } + }, + setError: function( errorText ) { LOG( "Error: " + errorText ); this._error = errorText; this._connectionStatus = this.FAILED; + this._finishConnectionAttempt(); }, onIncomingData: function( messageText ) { @@ -148,7 +156,7 @@ XmppClient.prototype = { this.setError( this._authenticationLayer.getError() ); } else if ( response == this._authenticationLayer.COMPLETION_CODE ){ this._connectionStatus = this.CONNECTED; - LOG( "We be connected!!" ); + this._finishConnectionAttempt(); } else { this._transportLayer.send( response ); } @@ -296,12 +304,16 @@ XmppClient.prototype = { this.setError( errorText ); }, - connect: function( host ) { - // Do the handshake to connect with the server and authenticate. + connect: function( host, callback ) { + /* Do the handshake to connect with the server and authenticate. + callback is optional: if provided, it will be called (with no arguments) + when the connection has either succeeded or failed. */ + if ( callback ) { + this._callbackOnConnect = callback; + } this._transportLayer.connect(); this._transportLayer.setCallbackObject( this ); this._transportLayer.send( this._makeHeaderXml( host ) ); - this._connectionStatus = this.CALLED_SERVER; // Now we wait... the rest of the protocol will be driven by // onIncomingData. From 0730232ef7756f88aabe30b1ff0247a4e0225c95 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 13 Jun 2008 15:47:41 +0900 Subject: [PATCH 0350/1860] Make some 'debug' log calls into 'trace' ones. Use the frame formatter, which will remove long paths to extension dirs from the output. Don't warn about outstanding callbacks if we caught an exception from the generator. --- services/sync/modules/async.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 2c4d7f38ee33..36258a08a08b 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -93,9 +93,7 @@ Generator.prototype = { get cb() { let caller = Components.stack.caller; this._outstandingCbs++; - this._log.debug(this.name + - ": self.cb generated at " + - caller.filename + ":" + caller.lineNumber); + this._log.trace(this.name + ": cb generated at:\n" + formatFrame(caller)); let self = this, cb = function(data) { self.cont(data); }; cb.parentGenerator = this; return cb; @@ -135,7 +133,7 @@ Generator.prototype = { _handleException: function AsyncGen__handleException(e) { if (e instanceof StopIteration) { - this._log.debug(this.name + ": End of coroutine reached."); + this._log.trace(this.name + ": End of coroutine reached."); // skip to calling done() } else if (this.onComplete.parentGenerator instanceof Generator) { @@ -168,11 +166,10 @@ Generator.prototype = { } }, - _detectDeadlock: function AsyncGen_detectDeadlock() { + _detectDeadlock: function AsyncGen__detectDeadlock() { if (this._outstandingCbs == 0) this._log.warn("Async method '" + this.name + - "' may have yielded without an " + - "outstanding callback."); + "' may have yielded without an outstanding callback."); }, run: function AsyncGen_run() { @@ -190,7 +187,7 @@ Generator.prototype = { cont: function AsyncGen_cont(data) { this._outstandingCbs--; - this._log.debug(this.name + ": self.cb() called, resuming coroutine."); + this._log.trace(this.name + ": resuming coroutine."); this._continued = true; try { this.generator.send(data); @@ -221,8 +218,8 @@ Generator.prototype = { return; let self = this; let cb = function() { self._done(retval); }; - this._log.debug(this.name + ": done() called."); - if (this._outstandingCbs > 0) + this._log.trace(this.name + ": done() called."); + if (!this._exception && this._outstandingCbs > 0) this._log.warn("Async method '" + this.name + "' may have outstanding callbacks."); this._timer = Utils.makeTimerForCall(cb); From c11e140b0d6df854bf4bb42fe08edde09a60cb70 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 13 Jun 2008 15:49:18 +0900 Subject: [PATCH 0351/1860] To avoid some async.js warnings: Turn some 'yield' calls into 'return' ones, and don't call 'self.cb' twice in a row when constructing event listeners for XHRs. --- services/sync/modules/dav.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 66463de72bc7..dfcba2cf04f3 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -127,8 +127,9 @@ DAVCollection.prototype = { let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); request = request.QueryInterface(Ci.nsIDOMEventTarget); - request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); - request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); + let cb = self.cb; + request.addEventListener("load", new Utils.EventListener(cb, "load"), false); + request.addEventListener("error", new Utils.EventListener(cb, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); request.open(op, this._baseURL + path, true); @@ -318,7 +319,7 @@ DAVCollection.prototype = { if (this._authProvider._authFailed || resp.status < 200 || resp.status >= 300) { self.done(false); - yield; + return; } self.done(true); @@ -364,7 +365,7 @@ DAVCollection.prototype = { if (DAVLocks['default']) { this._log.debug("Lock called, but we already hold a token"); self.done(DAVLocks['default']); - yield; + return; } this.LOCK("lock", @@ -376,10 +377,8 @@ DAVCollection.prototype = { let resp = yield; if (this._authProvider._authFailed || - resp.status < 200 || resp.status >= 300) { - self.done(); - yield; - } + resp.status < 200 || resp.status >= 300) + return; let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); From 581813bade070ad59f39bda3e8e59494a875ca3a Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 13 Jun 2008 13:08:36 -0700 Subject: [PATCH 0352/1860] use nsIXMLHttpRequest::mozBackgroundRequest instead of DummyAuthProvider to suppress authentication dialogs and ensure XMLHttpRequests succeed even when the window that originated the request goes away --- services/sync/modules/dav.js | 171 +------------------------------ services/sync/modules/service.js | 9 -- 2 files changed, 5 insertions(+), 175 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 66463de72bc7..deb5c8627112 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -63,7 +63,6 @@ function DAVCollection(baseURL, defaultPrefix) { this.baseURL = baseURL; this.defaultPrefix = defaultPrefix; this._identity = 'DAV:default'; - this._authProvider = new DummyAuthProvider(); this._log = Log4Moz.Service.getLogger("Service.DAV"); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.dav")]; @@ -130,9 +129,9 @@ DAVCollection.prototype = { request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); request = request.QueryInterface(Ci.nsIXMLHttpRequest); + request.mozBackgroundRequest = true; request.open(op, this._baseURL + path, true); - // Force cache validation let channel = request.channel; channel = channel.QueryInterface(Ci.nsIRequest); @@ -149,15 +148,10 @@ DAVCollection.prototype = { request.setRequestHeader(key, headers[key]); } - this._authProvider._authFailed = false; - request.channel.notificationCallbacks = this._authProvider; - request.send(data); let event = yield; ret = event.target; - if (this._authProvider._authFailed) - this._log.warn("_makeRequest: authentication failed"); if (ret.status < 200 || ret.status >= 300) this._log.warn("_makeRequest: got status " + ret.status); @@ -315,8 +309,7 @@ DAVCollection.prototype = { this.GET("", self.cb); let resp = yield; - if (this._authProvider._authFailed || - resp.status < 200 || resp.status >= 300) { + if (resp.status < 200 || resp.status >= 300) { self.done(false); yield; } @@ -338,8 +331,7 @@ DAVCollection.prototype = { "", self.cb); let resp = yield; - if (this._authProvider._authFailed || - resp.status < 200 || resp.status >= 300) { + if (resp.status < 200 || resp.status >= 300) { self.done(false); yield; } @@ -375,8 +367,7 @@ DAVCollection.prototype = { "", self.cb); let resp = yield; - if (this._authProvider._authFailed || - resp.status < 200 || resp.status >= 300) { + if (resp.status < 200 || resp.status >= 300) { self.done(); yield; } @@ -414,8 +405,7 @@ DAVCollection.prototype = { this.UNLOCK("lock", self.cb); let resp = yield; - if (this._authProvider._authFailed || - resp.status < 200 || resp.status >= 300) { + if (resp.status < 200 || resp.status >= 300) { self.done(false); yield; } @@ -451,154 +441,3 @@ DAVCollection.prototype = { self.done(unlocked); } }; - - -/* - * Auth provider object - * Taken from nsMicrosummaryService.js and massaged slightly - */ - -function DummyAuthProvider() {} -DummyAuthProvider.prototype = { - // Implement notification callback interfaces so we can suppress UI - // and abort loads for bad SSL certs and HTTP authorization requests. - - // Interfaces this component implements. - interfaces: [Ci.nsIBadCertListener, - Ci.nsIAuthPromptProvider, - Ci.nsIAuthPrompt, - Ci.nsIPrompt, - Ci.nsIProgressEventSink, - Ci.nsIInterfaceRequestor, - Ci.nsISupports], - - // Auth requests appear to succeed when we cancel them (since the server - // redirects us to a "you're not authorized" page), so we have to set a flag - // to let the load handler know to treat the load as a failure. - get _authFailed() { return this.__authFailed; }, - set _authFailed(newValue) { return this.__authFailed = newValue; }, - - // nsISupports - - QueryInterface: function DAP_QueryInterface(iid) { - if (!this.interfaces.some( function(v) { return iid.equals(v); } )) - throw Cr.NS_ERROR_NO_INTERFACE; - - // nsIAuthPrompt and nsIPrompt need separate implementations because - // their method signatures conflict. The other interfaces we implement - // within DummyAuthProvider itself. - switch(iid) { - case Ci.nsIAuthPrompt: - return this.authPrompt; - case Ci.nsIPrompt: - return this.prompt; - default: - return this; - } - }, - - // nsIInterfaceRequestor - - getInterface: function DAP_getInterface(iid) { - return this.QueryInterface(iid); - }, - - // nsIBadCertListener - - // Suppress UI and abort secure loads from servers with bad SSL certificates. - - confirmUnknownIssuer: function DAP_confirmUnknownIssuer(socketInfo, cert, certAddType) { - return false; - }, - - confirmMismatchDomain: function DAP_confirmMismatchDomain(socketInfo, targetURL, cert) { - return false; - }, - - confirmCertExpired: function DAP_confirmCertExpired(socketInfo, cert) { - return false; - }, - - notifyCrlNextupdate: function DAP_notifyCrlNextupdate(socketInfo, targetURL, cert) { - }, - - // nsIAuthPromptProvider - - getAuthPrompt: function(aPromptReason, aIID) { - this._authFailed = true; - throw Cr.NS_ERROR_NOT_AVAILABLE; - }, - - // HTTP always requests nsIAuthPromptProvider first, so it never needs - // nsIAuthPrompt, but not all channels use nsIAuthPromptProvider, so we - // implement nsIAuthPrompt too. - - // nsIAuthPrompt - - get authPrompt() { - var resource = this; - return { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), - prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) { - resource._authFailed = true; - return false; - }, - promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) { - resource._authFailed = true; - return false; - }, - promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) { - resource._authFailed = true; - return false; - } - }; - }, - - // nsIPrompt - - get prompt() { - var resource = this; - return { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), - alert: function(dialogTitle, text) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - alertCheck: function(dialogTitle, text, checkMessage, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirm: function(dialogTitle, text) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirmCheck: function(dialogTitle, text, checkMessage, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - confirmEx: function(dialogTitle, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - prompt: function(dialogTitle, text, value, checkMsg, checkValue) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - }, - promptPassword: function(dialogTitle, text, password, checkMsg, checkValue) { - resource._authFailed = true; - return false; - }, - promptUsernameAndPassword: function(dialogTitle, text, username, password, checkMsg, checkValue) { - resource._authFailed = true; - return false; - }, - select: function(dialogTitle, text, count, selectList, outSelection) { - throw Cr.NS_ERROR_NOT_IMPLEMENTED; - } - }; - }, - - // nsIProgressEventSink - - onProgress: function DAP_onProgress(aRequest, aContext, - aProgress, aProgressMax) { - }, - - onStatus: function DAP_onStatus(aRequest, aContext, - aStatus, aStatusArg) { - } -}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ed2bbe9b08b8..063a06d02698 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -427,15 +427,6 @@ WeaveSvc.prototype = { _login: function WeaveSync__login(password, passphrase) { let self = yield; - // XmlHttpRequests fail when the window that triggers them goes away - // because of bug 317600, and the first XmlHttpRequest we do happens - // just before the login dialog closes itself (for logins prompted by - // that dialog), so it triggers the bug. To work around it, we pause - // here and then continue after a 0ms timeout. - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback({ notify: self.cb }, 0, Ci.nsITimer.TYPE_ONE_SHOT); - yield; - // cache password & passphrase // if null, we'll try to get them from the pw manager below ID.get('WeaveID').setTempPassword(password); From c2099b485b89fa279a8ffdb53d639005dce5f616 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 13 Jun 2008 15:39:06 -0700 Subject: [PATCH 0353/1860] Fixed an 'outstanding callbacks' warning and simplified the code a tiny bit. --- services/sync/modules/dav.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index deb5c8627112..5f17c1dcba2a 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -123,12 +123,12 @@ DAVCollection.prototype = { path = this._defaultPrefix + path; - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(); - request = request.QueryInterface(Ci.nsIDOMEventTarget); + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); - request.addEventListener("load", new Utils.EventListener(self.cb, "load"), false); - request.addEventListener("error", new Utils.EventListener(self.cb, "error"), false); - request = request.QueryInterface(Ci.nsIXMLHttpRequest); + let xhrCb = self.cb; + + request.onload = new Utils.EventListener(xhrCb, "load"); + request.onerror = new Utils.EventListener(xhrCb, "error"); request.mozBackgroundRequest = true; request.open(op, this._baseURL + path, true); From efa4b887cf8fe98982f683cce3c42290e6929c50 Mon Sep 17 00:00:00 2001 From: Date: Fri, 13 Jun 2008 16:20:43 -0700 Subject: [PATCH 0354/1860] Fixed some minor bugs -- the name of the incoming shared folder is parsed correctly, and xmppClient now catches bounce errors that were previously parsed as messages. --- services/sync/modules/engines/bookmarks.js | 7 ++++--- services/sync/modules/service.js | 1 + services/sync/modules/xmpp/xmppClient.js | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d5607c0c308e..7bcd19d42fa4 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -143,7 +143,7 @@ BookmarksEngine.prototype = { */ let words = messageText.split(" "); let commandWord = words[0]; - let directoryName = words[1]; + let directoryName = words.slice(1).join(" "); if ( commandWord == "share" ) { bmkEngine._incomingShareOffer( directoryName, from ); } else if ( commandWord == "stop" ) { @@ -163,7 +163,6 @@ BookmarksEngine.prototype = { self.done(); }, - _incomingShareOffer: function BmkEngine__incomingShareOffer( dir, user ) { /* Called when we receive an offer from another user to share a directory. @@ -220,15 +219,17 @@ BookmarksEngine.prototype = { /* Set the annotation on the folder so we know it's an outgoing share: */ + dump( "I'm in _share.\n" ); let folderItemId = selectedFolder.node.itemId; let folderName = selectedFolder.getAttribute( "label" ); ans.setItemAnnotation(folderItemId, OUTGOING_SHARED_ANNO, username, 0, ans.EXPIRE_NEVER); // TODO: does this clobber existing annotations? - + dump( "I set the annotation...\n" ); // Send an xmpp message to the share-ee if ( this._xmppClient ) { if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { + dump( "Gonna send notification...\n" ); let msgText = "share " + folderName; this._xmppClient.sendMessage( username, msgText ); } else { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3da7770de3b3..70f426c81b9a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -673,6 +673,7 @@ WeaveSvc.prototype = { "share-bookmarks" will be sent out to any observers who are listening for it. As far as I know, there aren't currently any listeners for "share-bookmarks" but we'll send it out just in case. */ + dump( "This fails with an Exception: cannot aquire internal lock.\n" ); this._lock(this._notify(messageName, this._shareData, dataType, diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 054bf28eea81..782abedc8238 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -134,17 +134,19 @@ XmppClient.prototype = { } // Message is parseable, now look for message-level errors. - var rootElem = responseDOM.documentElement; - var errors = rootElem.getElementsByTagName( "stream:error" ); if ( errors.length > 0 ) { this.setError( errors[0].firstChild.nodeName ); return; } + errors = rootElem.getElementsByTagName( "error" ); + if ( errors.length > 0 ) { + this.setError( errors[0].firstChild.nodeName ); + return; + } // Stream is valid. - // Detect and handle mid-authentication steps. if ( this._connectionStatus == this.CALLED_SERVER ) { // skip TLS, go straight to SALS. (encryption should be negotiated From 86ae0aecdaf26df1f60c28b3ce799ec6d816e587 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 14 Jun 2008 17:07:06 +0900 Subject: [PATCH 0355/1860] Move remote init code ('fullUpload') into RemoteStore; make RemoteStore hold an Engine object (tightly coupling them); make the server prefix and identity properties of Engine public --- services/sync/modules/engines.js | 105 +++++++------------------------ services/sync/modules/remote.js | 82 +++++++++++++++--------- 2 files changed, 75 insertions(+), 112 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a7a7cfa467b3..ee512f8f2dab 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -105,7 +105,7 @@ Engine.prototype = { get _remote() { if (!this.__remote) - this.__remote = new RemoteStore(this.serverPrefix, 'Engine:' + this.name); + this.__remote = new RemoteStore(this); return this.__remote; }, @@ -161,7 +161,7 @@ Engine.prototype = { this.__snapshot = value; }, - get _pbeId() { + get pbeId() { let id = ID.get('Engine:PBE:' + this.name); if (!id) id = ID.get('Engine:PBE:default'); @@ -170,15 +170,15 @@ Engine.prototype = { return id; }, - get _engineId() { + get engineId() { let id = ID.get('Engine:' + this.name); if (!id || - id.username != this._pbeId.username || id.realm != this._pbeId.realm) { + id.username != this.pbeId.username || id.realm != this.pbeId.realm) { let password = null; if (id) password = id.password; - id = new Identity(this._pbeId.realm + ' - ' + this.logName, - this._pbeId.username, password); + id = new Identity(this.pbeId.realm + ' - ' + this.logName, + this.pbeId.username, password); ID.set('Engine:' + this.name, id); } return id; @@ -194,15 +194,10 @@ Engine.prototype = { _getSymKey: function Engine__getSymKey() { let self = yield; - if ("none" == Utils.prefs.getCharPref("encryption")) return; - - this._remote.keys.getKey(self.cb, this._pbeId); - let symkey = yield; - this._engineId.setTempPassword(symkey); - - self.done(); + let symkey = yield this._remote.keys.getKey(self.cb, this.pbeId); + this.engineId.setTempPassword(symkey); }, _serializeCommands: function Engine__serializeCommands(commands) { @@ -392,13 +387,8 @@ Engine.prototype = { this._snapshot.data = newSnapshot; this._snapshot.version = ++server.maxVersion; - /* - if (server.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) { - this._fullUpload.async(this, self.cb); - let status = yield; - if (!status) - this._log.error("Could not upload files to server"); // eep? - */ + if (server.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) + yield this._remote.initialize(self.cb, this._snapshot); this._remote.appendDelta(self.cb, serverDelta); yield; @@ -421,6 +411,15 @@ Engine.prototype = { self.done(true); }, + _initialUpload: function Engine__initialUpload() { + this._log.info("Initial upload to server"); + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + yield this._remote.initialize(self.cb, this._snapshot); + this._snapshot.save(); + }, + /* Get the deltas/combined updates from the server * Returns: * status: @@ -445,7 +444,7 @@ Engine.prototype = { * the relevant deltas (from our snapshot version to current), * combined into a single set. */ - _getServerData: function BmkEngine__getServerData() { + _getServerData: function Engine__getServerData() { let self = yield; let status; @@ -456,20 +455,7 @@ Engine.prototype = { this._log.info("Got status file from server"); } catch (e if e.message.status == 404) { - this._log.info("Server has no status file, Initial upload to server"); - - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - - this._fullUpload.async(this, self.cb); - let uploadStatus = yield; - if (!uploadStatus) - throw "Initial upload failed"; - - this._log.info("Initial upload to server successful"); - this._snapshot.save(); - + this._initialUpload.async(this, self.cb); self.done({status: 0, formatVersion: ENGINE_STORAGE_FORMAT_VERSION, maxVersion: this._snapshot.version, @@ -583,55 +569,6 @@ Engine.prototype = { self.done(ret); }, - _fullUpload: function Engine__fullUpload() { - let self = yield; - let ret = false; - - Crypto.PBEkeygen.async(Crypto, self.cb); - let symkey = yield; - this._engineId.setTempPassword(symkey); - if (!this._engineId.password) - throw "Could not generate a symmetric encryption key"; - - let enckey = this._engineId.password; - if ("none" != Utils.prefs.getCharPref("encryption")) { - Crypto.RSAencrypt.async(Crypto, self.cb, - this._engineId.password, this._pbeId); - enckey = yield; - } - - if (!enckey) - throw "Could not encrypt symmetric encryption key"; - - let keys = {ring: {}}; - keys.ring[this._engineId.username] = enckey; - this._remote.keys.put(self.cb, keys); - yield; - - this._remote.snapshot.put(self.cb, this._snapshot.wrap()); - yield; - this._remote.deltas.put(self.cb, []); - yield; - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._remote.status.put(self.cb, - {GUID: this._snapshot.GUID, - formatVersion: ENGINE_STORAGE_FORMAT_VERSION, - snapVersion: this._snapshot.version, - maxVersion: this._snapshot.version, - snapEncryption: Crypto.defaultAlgorithm, - deltasEncryption: "none", - itemCount: c}); - yield; - - this._log.info("Full upload to server successful"); - ret = true; - self.done(ret); - }, - _share: function Engine__share(guid, username) { /* This should be overridden by the engine subclass for each datatype. Implementation should share the data node identified by guid, diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 4caaa8d35c48..71452ffe836d 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -282,7 +282,7 @@ CryptoFilter.prototype = { beforePUT: function CryptoFilter_beforePUT(data) { let self = yield; this._log.debug("Encrypting data"); - Crypto.PBEencrypt.async(Crypto, self.cb, data, ID.get(this._remote.cryptoId)); + Crypto.PBEencrypt.async(Crypto, self.cb, data, this._remote.engineId); let ret = yield; self.done(ret); }, @@ -293,7 +293,7 @@ CryptoFilter.prototype = { if (!this._remote.status.data) throw "Remote status must be initialized before crypto filter can be used" let alg = this._remote.status.data[this._algProp]; - Crypto.PBEdecrypt.async(Crypto, self.cb, data, ID.get(this._remote.cryptoId)); + Crypto.PBEdecrypt.async(Crypto, self.cb, data, this._remote.engineId); let ret = yield; self.done(ret); } @@ -366,26 +366,14 @@ Deltas.prototype = { } }; -function RemoteStore(serverPrefix, cryptoId) { - this._prefix = serverPrefix; - this._cryptoId = cryptoId; +function RemoteStore(engine) { + this._engine = engine; this._log = Log4Moz.Service.getLogger("Service.RemoteStore"); } RemoteStore.prototype = { - get serverPrefix() this._prefix, - set serverPrefix(value) { - this._prefix = value; - this.status.serverPrefix = value; - this.keys.serverPrefix = value; - this.snapshot.serverPrefix = value; - this.deltas.serverPrefix = value; - }, - - get cryptoId() this._cryptoId, - set cryptoId(value) { - this.__cryptoId = value; - // FIXME: do we need to reset anything here? - }, + get serverPrefix() this._engine.serverPrefix, + get engineId() this._engine.engineId, + get pbeId() this._engine.pbeId, get status() { let status = new Status(this); @@ -411,15 +399,11 @@ RemoteStore.prototype = { return deltas; }, - _initSession: function RStore__initSession(serverPrefix, cryptoId) { + _initSession: function RStore__initSession() { let self = yield; - if (serverPrefix) - this.serverPrefix = serverPrefix; - if (cryptoId) - this.cryptoId = cryptoId; - if (!this.serverPrefix || !this.cryptoId) - throw "RemoteStore: cannot initialize without a server prefix and crypto ID"; + if (!this.serverPrefix || !this.engineId) + throw "Cannot initialize RemoteStore: engine has no server prefix or crypto ID"; this.status.data = null; this.keys.data = null; @@ -438,8 +422,8 @@ RemoteStore.prototype = { this._inited = true; }, - initSession: function RStore_initSession(onComplete, serverPrefix, cryptoId) { - this._initSession.async(this, onComplete, serverPrefix, cryptoId); + initSession: function RStore_initSession(onComplete) { + this._initSession.async(this, onComplete); }, closeSession: function RStore_closeSession() { @@ -450,6 +434,48 @@ RemoteStore.prototype = { this.deltas.data = null; }, + _initialize: function RStore__initialize(snapshot) { + let self = yield; + let symkey; + + if ("none" != Utils.prefs.getCharPref("encryption")) { + symkey = yield Crypto.PBEkeygen.async(Crypto, self.cb); + if (!symkey) + throw "Could not generate a symmetric encryption key"; + this.engineId.setTempPassword(symkey); + + symkey = yield Crypto.RSAencrypt.async(Crypto, self.cb, + this.engineId.password, + this.pbeId); + if (!symkey) + throw "Could not encrypt symmetric encryption key"; + } + + let keys = {ring: {}}; + keys.ring[this.engineId.username] = symkey; + yield this._remote.keys.put(self.cb, keys); + + this.snapshot.put(self.cb, snapshot.data); + this.deltas.put(self.cb, []); + + let c = 0; + for (GUID in snapshot.data) + c++; + + yield this.status.put(self.cb, + {GUID: snapshot.GUID, + formatVersion: ENGINE_STORAGE_FORMAT_VERSION, + snapVersion: snapshot.version, + maxVersion: snapshot.version, + snapEncryption: Crypto.defaultAlgorithm, + deltasEncryption: Crypto.defaultAlgorithm, + itemCount: c}); + this._log.info("Full upload to server successful"); + }, + initialize: function RStore_initialize(onComplete, snapshot) { + this._initialize.async(this, onComplete, snapshot); + }, + _appendDelta: function RStore__appendDelta(delta) { let self = yield; if (this.deltas.data == null) { From 59f50b09f1f9586af73db1877f314d0f4832a198 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 16 Jun 2008 00:21:44 +0900 Subject: [PATCH 0356/1860] catch 404s from initSession and do an initial upload; fix some bugs (missing yields, references to Engine properties) in RemoteStore.initialize --- services/sync/modules/engines.js | 44 ++++++++++---------------------- services/sync/modules/remote.js | 12 ++++----- services/sync/modules/service.js | 4 +-- 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cab118576ded..5315dcf0df38 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -267,8 +267,12 @@ Engine.prototype = { this._log.info("Beginning sync"); - this._remote.initSession(self.cb); - yield; + try { + yield this._remote.initSession(self.cb); + } catch (e if e.message.status == 404) { + yield this._initialUpload.async(this, self.cb); + return; + } this._log.info("Local snapshot version: " + this._snapshot.version); this._log.info("Server maxVersion: " + this._remote.status.data.maxVersion); @@ -412,12 +416,13 @@ Engine.prototype = { }, _initialUpload: function Engine__initialUpload() { - this._log.info("Initial upload to server"); - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - yield this._remote.initialize(self.cb, this._snapshot); - this._snapshot.save(); + let self = yield; + this._log.info("Initial upload to server"); + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + yield this._remote.initialize(self.cb, this._snapshot); + this._snapshot.save(); }, /* Get the deltas/combined updates from the server @@ -446,28 +451,7 @@ Engine.prototype = { */ _getServerData: function Engine__getServerData() { let self = yield; - let status; - - try { - this._log.debug("Getting status file from server"); - this._remote.status.get(self.cb); - status = yield; - this._log.info("Got status file from server"); - - } catch (e if e.message.status == 404) { - this._initialUpload.async(this, self.cb); - self.done({status: 0, - formatVersion: ENGINE_STORAGE_FORMAT_VERSION, - maxVersion: this._snapshot.version, - snapVersion: this._snapshot.version, - snapEncryption: Crypto.defaultAlgorithm, - deltasEncryption: Crypto.defaultAlgorithm, - snapshot: Utils.deepCopy(this._snapshot.data), - deltas: [], - updates: []}); - return; - } - + let status = this._remote.status.data; let ret = {status: -1, formatVersion: null, maxVersion: null, snapVersion: null, snapEncryption: null, deltasEncryption: null, diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 71452ffe836d..4201f4c45b92 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -453,10 +453,10 @@ RemoteStore.prototype = { let keys = {ring: {}}; keys.ring[this.engineId.username] = symkey; - yield this._remote.keys.put(self.cb, keys); + yield this.keys.put(self.cb, keys); - this.snapshot.put(self.cb, snapshot.data); - this.deltas.put(self.cb, []); + yield this.snapshot.put(self.cb, snapshot.data); + yield this.deltas.put(self.cb, []); let c = 0; for (GUID in snapshot.data) @@ -479,14 +479,12 @@ RemoteStore.prototype = { _appendDelta: function RStore__appendDelta(delta) { let self = yield; if (this.deltas.data == null) { - this.deltas.get(self.cb); - yield; + yield this.deltas.get(self.cb); if (this.deltas.data == null) this.deltas.data = []; } this.deltas.data.push(delta); - this.deltas.put(self.cb, this.deltas.data); - yield; + yield this.deltas.put(self.cb, this.deltas.data); }, appendDelta: function RStore_appendDelta(onComplete, delta) { this._appendDelta.async(this, onComplete, delta); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f8005ac2b2e8..eb8543417bd2 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -196,7 +196,7 @@ WeaveSvc.prototype = { return 0; // manual/off return Utils.prefs.getIntPref("schedule"); }, - + onWindowOpened: function Weave__onWindowOpened() { if (!this._startupFinished) { if (Utils.prefs.getBoolPref("autoconnect") && @@ -672,7 +672,7 @@ WeaveSvc.prototype = { username)).async(this, onComplete); }, - _shareData: function WeaveSync__shareData(dataType, + _shareData: function WeaveSync__shareData(dataType, guid, username) { let self = yield; From 172710dc433e5800e619aa162b6feb2c1e852462 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 17 Jun 2008 01:04:23 +0900 Subject: [PATCH 0357/1860] move more remote-interaction code into RemoteStore (and out of Engine) --- services/sync/modules/engines.js | 169 +++++-------------------------- services/sync/modules/remote.js | 75 +++++++++++++- 2 files changed, 98 insertions(+), 146 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5315dcf0df38..cb647e9f9d12 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -192,14 +192,6 @@ Engine.prototype = { this._snapshot.load(); }, - _getSymKey: function Engine__getSymKey() { - let self = yield; - if ("none" == Utils.prefs.getCharPref("encryption")) - return; - let symkey = yield this._remote.keys.getKey(self.cb, this.pbeId); - this.engineId.setTempPassword(symkey); - }, - _serializeCommands: function Engine__serializeCommands(commands) { let json = this._json.encode(commands); //json = json.replace(/ {action/g, "\n {action"); @@ -274,13 +266,32 @@ Engine.prototype = { return; } + if (this._remote.status.data.GUID != this._snapshot.GUID) { + this._log.debug("Remote/local sync GUIDs do not match. " + + "Forcing initial sync."); + this._log.trace("Remote: " + this._remote.status.data.GUID); + this._log.trace("Local: " + this._snapshot.GUID); + this._store.resetGUIDs(); + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.GUID = this._remote.status.data.GUID; + } + this._log.info("Local snapshot version: " + this._snapshot.version); this._log.info("Server maxVersion: " + this._remote.status.data.maxVersion); this._log.debug("Server snapVersion: " + this._remote.status.data.snapVersion); + if ("none" != Utils.prefs.getCharPref("encryption")) { + let symkey = yield this._remote.keys.getKey(self.cb, this.pbeId); + this.engineId.setTempPassword(symkey); + } + // 1) Fetch server deltas - this._getServerData.async(this, self.cb); - let server = yield; + let server = {}; + let serverSnap = yield this._remote.getLatestFromSnap(self.cb, this._snapshot); + server.snapshot = serverSnap.data; + this._core.detectUpdates(self.cb, this._snapshot.data, server.snapshot); + server.updates = yield; // 2) Generate local deltas from snapshot -> current client status @@ -294,7 +305,7 @@ Engine.prototype = { this._log.trace("Server updates: " + this._serializeCommands(server.updates)); if (server.updates.length == 0 && localUpdates.length == 0) { - this._snapshot.version = server.maxVersion; + this._snapshot.version = this._remote.status.data.maxVersion; this._log.info("Sync complete: no changes needed on client or server"); self.done(true); return; @@ -324,7 +335,7 @@ Engine.prototype = { clientConflicts.length || serverConflicts.length)) { this._log.info("Sync complete: no changes needed on client or server"); this._snapshot.data = localJson.data; - this._snapshot.version = server.maxVersion; + this._snapshot.version = this._remote.status.data.maxVersion; this._snapshot.save(); self.done(true); return; @@ -347,7 +358,7 @@ Engine.prototype = { localJson.applyCommands.async(localJson, self.cb, clientChanges); yield; this._snapshot.data = localJson.data; - this._snapshot.version = server.maxVersion; + this._snapshot.version = this._remote.status.data.maxVersion; this._store.applyCommands.async(this._store, self.cb, clientChanges); yield; newSnapshot = this._store.wrap(); @@ -389,9 +400,9 @@ Engine.prototype = { if (serverDelta.length) { this._log.info("Uploading changes to server"); this._snapshot.data = newSnapshot; - this._snapshot.version = ++server.maxVersion; + this._snapshot.version = ++this._remote.status.data.maxVersion; - if (server.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) + if (this._remote.status.data.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) yield this._remote.initialize(self.cb, this._snapshot); this._remote.appendDelta(self.cb, serverDelta); @@ -425,134 +436,6 @@ Engine.prototype = { this._snapshot.save(); }, - /* Get the deltas/combined updates from the server - * Returns: - * status: - * -1: error - * 0: ok - * These fields may be null when status is -1: - * formatVersion: - * version of the data format itself. For compatibility checks. - * maxVersion: - * the latest version on the server - * snapVersion: - * the version of the current snapshot on the server (deltas not applied) - * snapEncryption: - * encryption algorithm currently used on the server-stored snapshot - * deltasEncryption: - * encryption algorithm currently used on the server-stored deltas - * snapshot: - * full snapshot of the latest server version (deltas applied) - * deltas: - * all of the individual deltas on the server - * updates: - * the relevant deltas (from our snapshot version to current), - * combined into a single set. - */ - _getServerData: function Engine__getServerData() { - let self = yield; - let status = this._remote.status.data; - let ret = {status: -1, - formatVersion: null, maxVersion: null, snapVersion: null, - snapEncryption: null, deltasEncryption: null, - snapshot: null, deltas: null, updates: null}; - let deltas, allDeltas; - let snap = new SnapshotStore(); - - // Bail out if the server has a newer format version than we can parse - if (status.formatVersion > ENGINE_STORAGE_FORMAT_VERSION) { - this._log.error("Server uses storage format v" + status.formatVersion + - ", this client understands up to v" + ENGINE_STORAGE_FORMAT_VERSION); - throw "Incompatible server format for engine data"; - } - - this._getSymKey.async(this, self.cb); - yield; - - if (status.formatVersion == 0) { - ret.snapEncryption = status.snapEncryption = "none"; - ret.deltasEncryption = status.deltasEncryption = "none"; - } - - if (status.GUID != this._snapshot.GUID) { - this._log.info("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._log.debug("Remote: " + status.GUID); - this._log.debug("Local: " + this._snapshot.GUID); - this._store.resetGUIDs(); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.GUID = status.GUID; - } - - if (this._snapshot.version < status.snapVersion) { - this._log.trace("Local snapshot version < server snapVersion"); - - if (this._snapshot.version >= 0) - this._log.info("Local snapshot is out of date"); - - this._log.info("Downloading server snapshot"); - this._remote.snapshot.get(self.cb); - snap.data = yield; - - this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); - allDeltas = yield; - deltas = allDeltas; - - } else if (this._snapshot.version >= status.snapVersion && - this._snapshot.version < status.maxVersion) { - this._log.trace("Server snapVersion <= local snapshot version < server maxVersion"); - snap.data = Utils.deepCopy(this._snapshot.data); - - this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); - allDeltas = yield; - deltas = allDeltas.slice(this._snapshot.version - status.snapVersion); - - } else if (this._snapshot.version == status.maxVersion) { - this._log.trace("Local snapshot version == server maxVersion"); - snap.data = Utils.deepCopy(this._snapshot.data); - - // FIXME: could optimize this case by caching deltas file - this._log.info("Downloading server deltas"); - this._remote.deltas.get(self.cb); - allDeltas = yield; - deltas = []; - - } else { // this._snapshot.version > status.maxVersion - this._log.error("Server snapshot is older than local snapshot"); - throw "Server snapshot is older than local snapshot"; - } - - try { - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands.async(snap, self.cb, deltas[i]); - yield; - } - } catch (e) { - this._log.error("Error applying remote deltas to saved snapshot"); - this._log.error("Clearing local snapshot; next sync will merge"); - this._log.debug("Exception: " + Utils.exceptionStr(e)); - this._log.trace("Stack:\n" + Utils.stackTrace(e)); - this._snapshot.wipe(); - throw e; - } - - ret.status = 0; - ret.formatVersion = status.formatVersion; - ret.maxVersion = status.maxVersion; - ret.snapVersion = status.snapVersion; - ret.snapEncryption = status.snapEncryption; - ret.deltasEncryption = status.deltasEncryption; - ret.snapshot = snap.data; - ret.deltas = allDeltas; - this._core.detectUpdates(self.cb, this._snapshot.data, snap.data); - ret.updates = yield; - - self.done(ret); - }, - _share: function Engine__share(guid, username) { let self = yield; /* This should be overridden by the engine subclass for each datatype. diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 4201f4c45b92..091488685991 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -420,6 +420,15 @@ RemoteStore.prototype = { yield; this._log.debug("Downloading status file... done"); + // Bail out if the server has a newer format version than we can parse + if (this.status.data.formatVersion > ENGINE_STORAGE_FORMAT_VERSION) { + this._log.error("Server uses storage format v" + + this.status.data.formatVersion + + ", this client understands up to v" + + ENGINE_STORAGE_FORMAT_VERSION); + throw "Incompatible remote store format"; + } + this._inited = true; }, initSession: function RStore_initSession(onComplete) { @@ -490,11 +499,71 @@ RemoteStore.prototype = { this._appendDelta.async(this, onComplete, delta); }, - _getUpdates: function RStore__getUpdates(lastSyncSnap) { + _getLatestFromScratch: function RStore__getLatestFromScratch() { let self = yield; + this._log.info("Downloading all server data from scratch"); + + this._log.debug("Downloading server snapshot"); + let data = yield this.snapshot.get(self.cb); + this._log.debug("Downloading server deltas"); + let deltas = yield this.deltas.get(self.cb); + + let snap = new SnapshotStore(); + snap.version = this.status.data.maxVersion; + snap.data = data; + for (let i = 0; i < deltas.length; i++) { + snap.applyCommands.async(snap, self.cb, deltas[i]); + yield; + } + + self.done(snap); }, - getUpdates: function RStore_getUpdates(onComplete, lastSyncSnap) { - this._getUpdates.async(this, onComplete); + + _getLatestFromSnap: function RStore__getLatestFromSnap(lastSyncSnap) { + let self = yield; + let deltas, snap = new SnapshotStore(); + snap.version = this.status.data.maxVersion; + + if (lastSyncSnap.version < this.status.data.snapVersion) { + snap = this._getLatestFromScratch.async(this, self.cb); + self.done(snap); + return; + + } else if (lastSyncSnap.version >= this.status.data.snapVersion && + lastSyncSnap.version < this.status.data.maxVersion) { + this._log.debug("Using last sync snapshot as starting point for server snapshot"); + snap.data = Utils.deepCopy(lastSyncSnap.data); + this._log.info("Downloading server deltas"); + let allDeltas = yield this.deltas.get(self.cb); + deltas = allDeltas.slice(lastSyncSnap.version - this.status.data.snapVersion); + + } else if (lastSyncSnap.version == this.status.data.maxVersion) { + this._log.debug("Using last sync snapshot as server snapshot (snap version == max version)"); + this._log.trace("Local snapshot version == server maxVersion"); + snap.data = Utils.deepCopy(lastSyncSnap.data); + deltas = []; + + } else { // lastSyncSnap.version > this.status.data.maxVersion + this._log.error("Server snapshot is older than local snapshot"); + throw "Server snapshot is older than local snapshot"; + } + + try { + for (var i = 0; i < deltas.length; i++) { + snap.applyCommands.async(snap, self.cb, deltas[i]); + yield; + } + } catch (e) { + this._log.warn("Error applying remote deltas to saved snapshot, attempting a full download"); + this._log.debug("Exception: " + Utils.exceptionStr(e)); + this._log.trace("Stack:\n" + Utils.stackTrace(e)); + snap = this._getLatestFromScratch.async(this, self.cb); + } + + self.done(snap); + }, + getLatestFromSnap: function RStore_getLatestFromSnap(onComplete, lastSyncSnap) { + this._getLatestFromSnap.async(this, onComplete, lastSyncSnap); } }; From 461e25482ae207427056aec5d59e73a1f9326a8d Mon Sep 17 00:00:00 2001 From: Date: Mon, 16 Jun 2008 11:24:41 -0700 Subject: [PATCH 0358/1860] Merged --- services/sync/modules/engines/bookmarks.js | 28 ++++++++++++++++------ services/sync/modules/service.js | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 7bcd19d42fa4..79fa9a790c15 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -176,7 +176,7 @@ BookmarksEngine.prototype = { right ahead to creating the incoming share. */ dump( "I was offered the directory " + dir + " from user " + dir ); - + _createIncomingShare( dir, user, dir ); }, _incomingShareWithdrawn: function BmkEngine__incomingShareStop( dir, user ) { @@ -214,12 +214,15 @@ BookmarksEngine.prototype = { // Create the outgoing share folder on the server // TODO do I need to call these asynchronously? - //this._createOutgoingShare.async( this, selectedFolder, username ); + dump( "About to call _createOutgoingShare asynchronously.\n" ); + this._createOutgoingShare.async( this, self.cb, selectedFolder, username ); + yield; + dump( "Done calling _createOutgoingShare asynchronously.\n" ); //this._updateOutgoingShare.async( this, selectedFolder, username ); /* Set the annotation on the folder so we know it's an outgoing share: */ - dump( "I'm in _share.\n" ); + let folderItemId = selectedFolder.node.itemId; let folderName = selectedFolder.getAttribute( "label" ); ans.setItemAnnotation(folderItemId, OUTGOING_SHARED_ANNO, username, 0, @@ -241,7 +244,7 @@ BookmarksEngine.prototype = { with many people, the value of the annotation can be a whole list of usernames instead of just one. */ - dump( "Bookmark engine shared " +folderName + " with " + username ); + dump( "Bookmark engine shared " +folderName + " with " + username + "\n" ); ret = true; self.done( ret ); }, @@ -275,11 +278,19 @@ BookmarksEngine.prototype = { let self = yield; let prefix = DAV.defaultPrefix; - this._log.debug("Sharing bookmarks from " + guid + " with " + username); + dump( "CreateOutgoingShare: " + folder + ", " + username + "\n" ); + this._log.debug("Sharing bookmarks from " + folder + " with " + username); this._getSymKey.async(this, self.cb); yield; + dump( "Trying DAV.GET...\n" ); +/* reateOutgoingShare: [object XULElement], jono +writing RSA key +Trying DAV.GET... +2008-06-16 10:36:06 Service.Util ERROR Could not get keys file. Error code: 404 +2008-06-16 10:36:06 Async.Generator ERROR Exception: checkStatus failed + */ // copied from getSymKey DAV.GET(this.keysFile, self.cb); let ret = yield; @@ -287,6 +298,7 @@ BookmarksEngine.prototype = { // note: this._json is just an encoder/decoder, no state. let keys = this._json.decode(ret.responseText); + dump( "Trying to get public key...\n" ); // get the other user's pubkey let serverURL = Utils.prefs.getCharPref("serverURL"); @@ -302,20 +314,22 @@ BookmarksEngine.prototype = { let id = new Identity(); id.pubkey = ret.responseText; - + dump( "Trying encrypt...\n" ); // now encrypt the symkey with their pubkey and upload the new keyring Crypto.RSAencrypt.async(Crypto, self.cb, this._engineId.password, id); let enckey = yield; if (!enckey) throw "Could not encrypt symmetric encryption key"; + dump( "Trying DAV.PUT...\n" ); keys.ring[username] = enckey; DAV.PUT(this.keysFile, this._json.encode(keys), self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload keyring file."); - this._log.debug("All done sharing!"); + this._log.debug("All done sharing!\n"); + dump( "Trying atul's API for setting htaccess...\n" ); // Call Atul's js api for setting htaccess: let api = new Sharing.Api( DAV ); api.shareWithUsers( directory, [username], self.cb ); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eb8543417bd2..c105ce32d0c2 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -664,7 +664,6 @@ WeaveSvc.prototype = { "share-bookmarks" will be sent out to any observers who are listening for it. As far as I know, there aren't currently any listeners for "share-bookmarks" but we'll send it out just in case. */ - dump( "This fails with an Exception: cannot aquire internal lock.\n" ); this._lock(this._notify(messageName, this._shareData, dataType, @@ -675,6 +674,7 @@ WeaveSvc.prototype = { _shareData: function WeaveSync__shareData(dataType, guid, username) { + dump( "in _shareData...\n" ); let self = yield; if (!Engines.get(dataType).enabled) { this._log.warn( "Can't share disabled data type: " + dataType ); From 61340bc8bb448f060ad90a40619be39a1c21e4f4 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 16 Jun 2008 14:06:05 -0700 Subject: [PATCH 0359/1860] Defined Cu -> Components.utils in head.js so that test files don't have to put it in as boilerplate. --- services/sync/tests/unit/head_first.js | 1 + services/sync/tests/unit/test_async.js | 2 -- services/sync/tests/unit/test_async_exceptions.js | 2 -- services/sync/tests/unit/test_async_missing_yield.js | 2 -- services/sync/tests/unit/test_sharing.js | 2 -- services/sync/tests/unit/test_xmpp.js | 2 -- 6 files changed, 1 insertion(+), 10 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index ac1d1543c1d3..e928d8d0a98f 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -3,6 +3,7 @@ version(180); const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; +const Cu = Components.utils; let ds = Cc["@mozilla.org/file/directory_service;1"] .getService(Ci.nsIProperties); diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js index 36795e9948a4..ed742d25abeb 100644 --- a/services/sync/tests/unit/test_async.js +++ b/services/sync/tests/unit/test_async.js @@ -1,5 +1,3 @@ -const Cu = Components.utils; - Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); diff --git a/services/sync/tests/unit/test_async_exceptions.js b/services/sync/tests/unit/test_async_exceptions.js index f1fe78eadb28..c9d071113615 100644 --- a/services/sync/tests/unit/test_async_exceptions.js +++ b/services/sync/tests/unit/test_async_exceptions.js @@ -1,5 +1,3 @@ -const Cu = Components.utils; - Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); diff --git a/services/sync/tests/unit/test_async_missing_yield.js b/services/sync/tests/unit/test_async_missing_yield.js index 14347ae518b9..840e7ce782ab 100644 --- a/services/sync/tests/unit/test_async_missing_yield.js +++ b/services/sync/tests/unit/test_async_missing_yield.js @@ -1,5 +1,3 @@ -const Cu = Components.utils; - Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); diff --git a/services/sync/tests/unit/test_sharing.js b/services/sync/tests/unit/test_sharing.js index b962404e9d37..e18f6de3be95 100644 --- a/services/sync/tests/unit/test_sharing.js +++ b/services/sync/tests/unit/test_sharing.js @@ -1,5 +1,3 @@ -const Cu = Components.utils; - Cu.import("resource://weave/sharing.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/test_xmpp.js index 60396b2e054c..9c76eed9308a 100644 --- a/services/sync/tests/unit/test_xmpp.js +++ b/services/sync/tests/unit/test_xmpp.js @@ -1,5 +1,3 @@ -var Cu = Components.utils; - Cu.import( "resource://weave/xmpp/xmppClient.js" ); function LOG(aMsg) { From 0b8eade791f4abfad9ede869d3cf9e993abf3479 Mon Sep 17 00:00:00 2001 From: Date: Mon, 16 Jun 2008 15:52:15 -0700 Subject: [PATCH 0360/1860] Trying to debug _createOutgoingShare... --- services/sync/modules/engines/bookmarks.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 79fa9a790c15..065815d8d1c0 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -281,16 +281,22 @@ BookmarksEngine.prototype = { dump( "CreateOutgoingShare: " + folder + ", " + username + "\n" ); this._log.debug("Sharing bookmarks from " + folder + " with " + username); + // _getSymKey is undefined? this._getSymKey.async(this, self.cb); yield; dump( "Trying DAV.GET...\n" ); + dump( "Keyfile is " + this.keysFile + "\n" ); /* reateOutgoingShare: [object XULElement], jono writing RSA key Trying DAV.GET... 2008-06-16 10:36:06 Service.Util ERROR Could not get keys file. Error code: 404 2008-06-16 10:36:06 Async.Generator ERROR Exception: checkStatus failed */ + // look in /var/www/fs: is the keys file even there? + // The URL we should be loading is /user/username/public/pubkey. + + // copied from getSymKey DAV.GET(this.keysFile, self.cb); let ret = yield; From f0485e65f6937a8622a710faf754eab6fb461b3e Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 16 Jun 2008 16:22:00 -0700 Subject: [PATCH 0361/1860] Refactored head.js for unit tests so that we now have an 'initTestLogging()' function. --- services/sync/tests/unit/head_first.js | 31 +++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index e928d8d0a98f..5c7ed021c3d5 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -41,37 +41,42 @@ function loadInSandbox(aUri) { return sandbox; } -function makeAsyncTestRunner(generator) { - const Cu = Components.utils; - +function initTestLogging() { Cu.import("resource://weave/log4moz.js"); - Cu.import("resource://weave/async.js"); - var errorsLogged = 0; - - function _TestFormatter() {} - _TestFormatter.prototype = { + function LogStats() { + this.errorsLogged = 0; + } + LogStats.prototype = { format: function BF_format(message) { if (message.level == Log4Moz.Level.Error) - errorsLogged += 1; + this.errorsLogged += 1; return message.loggerName + "\t" + message.levelDesc + "\t" + message.message + "\n"; } }; - _TestFormatter.prototype.__proto__ = new Log4Moz.Formatter(); + LogStats.prototype.__proto__ = new Log4Moz.Formatter(); var log = Log4Moz.Service.rootLogger; - var formatter = new _TestFormatter(); - var appender = new Log4Moz.DumpAppender(formatter); + var logStats = new LogStats(); + var appender = new Log4Moz.DumpAppender(logStats); log.level = Log4Moz.Level.Debug; appender.level = Log4Moz.Level.Debug; log.addAppender(appender); + return logStats; +} + +function makeAsyncTestRunner(generator) { + Cu.import("resource://weave/async.js"); + + var logStats = initTestLogging(); + function run_test() { do_test_pending(); let onComplete = function() { - if (errorsLogged) + if (logStats.errorsLogged) do_throw("Errors were logged."); else do_test_finished(); From 46b33dfde07832b14dafb721a0590f18b290e341 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 16 Jun 2008 16:42:32 -0700 Subject: [PATCH 0362/1860] Factored out fake-timer code into a separate class and moved it to head.js. --- services/sync/tests/unit/head_first.js | 27 ++++++++++++++++++++++++++ services/sync/tests/unit/test_async.js | 23 +++++----------------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 5c7ed021c3d5..7af151bfd8f4 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -41,6 +41,33 @@ function loadInSandbox(aUri) { return sandbox; } +function FakeTimerService() { + Cu.import("resource://weave/util.js"); + + this.callbackQueue = []; + + var self = this; + + this.__proto__ = { + makeTimerForCall: function FTS_makeTimerForCall(cb) { + // Just add the callback to our queue and we'll call it later, so + // as to simulate a real nsITimer. + self.callbackQueue.push(cb); + return "fake nsITimer"; + }, + processCallback: function FTS_processCallbacks() { + var cb = self.callbackQueue.pop(); + if (cb) { + cb(); + return true; + } + return false; + } + }; + + Utils.makeTimerForCall = self.makeTimerForCall; +}; + function initTestLogging() { Cu.import("resource://weave/log4moz.js"); diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js index ed742d25abeb..746033ac4754 100644 --- a/services/sync/tests/unit/test_async.js +++ b/services/sync/tests/unit/test_async.js @@ -2,17 +2,10 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); function run_test() { - var callbackQueue = []; + var fts = new FakeTimerService(); Function.prototype.async = Async.sugar; - Utils.makeTimerForCall = function fake_makeTimerForCall(cb) { - // Just add the callback to our queue and we'll call it later, so - // as to simulate a real nsITimer. - callbackQueue.push(cb); - return "fake nsITimer"; - }; - var onCompleteCalled = false; function onComplete() { @@ -31,9 +24,7 @@ function run_test() { // Ensure that 'this' is set properly. do_check_eq(this.sampleProperty, true); - // Simulate the calling of an asynchronous function that will call - // our callback. - callbackQueue.push(self.cb); + fts.makeTimerForCall(self.cb); yield; timesYielded++; @@ -45,15 +36,11 @@ function run_test() { do_check_eq(timesYielded, 1); - let func = callbackQueue.pop(); - do_check_eq(typeof func, "function"); - func(); + do_check_true(fts.processCallback()); do_check_eq(timesYielded, 2); - func = callbackQueue.pop(); - do_check_eq(typeof func, "function"); - func(); + do_check_true(fts.processCallback()); - do_check_eq(callbackQueue.length, 0); + do_check_false(fts.processCallback()); } From 5791486be366d56420ad02fa3371c13cf69845b5 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 16 Jun 2008 16:53:53 -0700 Subject: [PATCH 0363/1860] Refactored test suites based on recent changes. --- .../sync/tests/unit/test_async_exceptions.js | 19 ++------ .../tests/unit/test_async_missing_yield.js | 44 +++---------------- 2 files changed, 8 insertions(+), 55 deletions(-) diff --git a/services/sync/tests/unit/test_async_exceptions.js b/services/sync/tests/unit/test_async_exceptions.js index c9d071113615..b15a2e8dd666 100644 --- a/services/sync/tests/unit/test_async_exceptions.js +++ b/services/sync/tests/unit/test_async_exceptions.js @@ -3,15 +3,6 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; -var callbackQueue = []; - -Utils.makeTimerForCall = function fake_makeTimerForCall(cb) { - // Just add the callback to our queue and we'll call it later, so - // as to simulate a real nsITimer. - callbackQueue.push(cb); - return "fake nsITimer"; -}; - function thirdGen() { let self = yield; @@ -52,12 +43,8 @@ function runTestGenerator() { } function run_test() { + var fts = new FakeTimerService(); runTestGenerator.async({}); - let i = 1; - while (callbackQueue.length > 0) { - let cb = callbackQueue.pop(); - cb(); - i += 1; - } - do_check_eq(i, 5); + for (var i = 0; fts.processCallback(); i++) {} + do_check_eq(i, 4); } diff --git a/services/sync/tests/unit/test_async_missing_yield.js b/services/sync/tests/unit/test_async_missing_yield.js index 840e7ce782ab..b3ae785627fe 100644 --- a/services/sync/tests/unit/test_async_missing_yield.js +++ b/services/sync/tests/unit/test_async_missing_yield.js @@ -3,19 +3,10 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; -var callbackQueue = []; - -Utils.makeTimerForCall = function fake_makeTimerForCall(cb) { - // Just add the callback to our queue and we'll call it later, so - // as to simulate a real nsITimer. - callbackQueue.push(cb); - return "fake nsITimer"; -}; - function secondGen() { let self = yield; - callbackQueue.push(self.cb); + Utils.makeTimerForCall(self.cb); self.done(); } @@ -30,38 +21,13 @@ function runTestGenerator() { } function run_test() { - const Cu = Components.utils; - Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/async.js"); - var errorsLogged = 0; - - function _TestFormatter() {} - _TestFormatter.prototype = { - format: function BF_format(message) { - if (message.level == Log4Moz.Level.Error) - errorsLogged += 1; - return message.loggerName + "\t" + message.levelDesc + "\t" + - message.message + "\n"; - } - }; - _TestFormatter.prototype.__proto__ = new Log4Moz.Formatter(); - - var log = Log4Moz.Service.rootLogger; - var formatter = new _TestFormatter(); - var appender = new Log4Moz.DumpAppender(formatter); - log.level = Log4Moz.Level.Trace; - appender.level = Log4Moz.Level.Trace; - log.addAppender(appender); + var fts = new FakeTimerService(); + var logStats = initTestLogging(); runTestGenerator.async({}); - let i = 1; - while (callbackQueue.length > 0) { - let cb = callbackQueue.pop(); - cb(); - i += 1; - } - - do_check_eq(errorsLogged, 3); + while (fts.processCallback()) {}; + do_check_eq(logStats.errorsLogged, 3); } From 1c2a11418ea363595066a2024060b7632eef882f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 17 Jun 2008 09:51:02 +0900 Subject: [PATCH 0364/1860] treat paths beginning with '/' as absolute --- services/sync/modules/dav.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index d6f1961a4768..91079a75824d 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -121,7 +121,8 @@ DAVCollection.prototype = { this._log.debug(op + " request for " + (path? path : 'root folder')); - path = this._defaultPrefix + path; + if (!path || path[0] != '/') + path = this._defaultPrefix + path; let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); From b26ab3e1ae10f09d2c842a5648ad77498e6fedf9 Mon Sep 17 00:00:00 2001 From: Date: Mon, 16 Jun 2008 17:52:24 -0700 Subject: [PATCH 0365/1860] In the middle of trying to make createOutgoingShare work with Dan's changes (getSymKey having been moved to remote.js, etc.) --- services/sync/modules/engines/bookmarks.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 065815d8d1c0..b3d963aa0fc0 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -282,11 +282,11 @@ BookmarksEngine.prototype = { this._log.debug("Sharing bookmarks from " + folder + " with " + username); // _getSymKey is undefined? - this._getSymKey.async(this, self.cb); - yield; - - dump( "Trying DAV.GET...\n" ); - dump( "Keyfile is " + this.keysFile + "\n" ); + let keychain = this._remote.keys; + let identity = 'jono'; + keychain.getKey( this.cb, identity ); + let symKey = yield; + dump( "SymKey is " + symKey + "\n" ); /* reateOutgoingShare: [object XULElement], jono writing RSA key Trying DAV.GET... @@ -298,6 +298,7 @@ Trying DAV.GET... // copied from getSymKey + dump( "Trying DAV.GET...\n" ); DAV.GET(this.keysFile, self.cb); let ret = yield; Utils.ensureStatus(ret.status, "Could not get keys file."); From f36b1db09310a519aa24cf96b674d60d67d2149e Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 16 Jun 2008 18:08:37 -0700 Subject: [PATCH 0366/1860] Added test_service, which currently tests the case in which the server's meta/version and private/privkey files are correct, as well as all authentication information. --- services/sync/tests/unit/test_service.js | 100 +++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 services/sync/tests/unit/test_service.js diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js new file mode 100644 index 000000000000..0ea747b523df --- /dev/null +++ b/services/sync/tests/unit/test_service.js @@ -0,0 +1,100 @@ +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/wrap.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/dav.js"); +Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/identity.js"); + +Function.prototype.async = Async.sugar; + +function makeFakeAsyncFunc(retval) { + function fakeAsyncFunc() { + let self = yield; + + Utils.makeTimerForCall(self.cb); + yield; + + self.done(retval); + } + + return fakeAsyncFunc; +} + +Crypto.__proto__ = { + RSAkeydecrypt: function fake_RSAkeydecrypt(identity) { + let self = yield; + + if (identity.password == "passphrase" && + identity.privkey == "fake private key") + self.done("fake public key"); + else + throw new Error("Unexpected identity information."); + } +}; + +DAV.__proto__ = { + checkLogin: makeFakeAsyncFunc(true), + + __contents: {"meta/version" : "2", + "private/privkey" : "fake private key"}, + + GET: function fake_GET(path, onComplete) { + Log4Moz.Service.rootLogger.info("Retrieving " + path); + var result = {status: 404}; + if (path in this.__contents) + result = {status: 200, responseText: this.__contents[path]}; + + return makeFakeAsyncFunc(result).async(this, onComplete); + } +}; + +function FakeID(realm, username, password) { + this.realm = realm; + this.username = username; + this.password = password; + this.setTempPassword = function FID_setTempPassword(value) { + if (typeof value != "undefined") + this.password = value; + }; +} + +ID.__proto__ = { + __contents: {WeaveID: new FakeID("", "foo", "bar"), + WeaveCryptoID: new FakeID("", "", "passphrase")}, + + get: function fake_ID_get(name) { + return this.__contents[name]; + } +}; + +let Service = loadInSandbox("resource://weave/service.js"); + +function TestService() { + this._startupFinished = false; + this._log = Log4Moz.Service.getLogger("Service.Main"); +} + +TestService.prototype = { +}; +TestService.prototype.__proto__ = Service.WeaveSvc.prototype; + +function test_login_works() { + var fts = new FakeTimerService(); + var logStats = initTestLogging(); + var testService = new TestService(); + var finished = false; + var successful = false; + var onComplete = function(result) { + finished = true; + successful = result; + }; + + testService.login(onComplete); + + while (fts.processCallback()) {} + + do_check_true(finished); + do_check_true(successful); + do_check_eq(logStats.errorsLogged, 0); +} From 5b072b6fd2ed0e537e4d120b4a75933ff68f7830 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 16 Jun 2008 18:11:41 -0700 Subject: [PATCH 0367/1860] Fixed a strict warning in util.js. --- services/sync/modules/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index b31ff6b1e63d..e8d8ce926fa2 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -140,7 +140,7 @@ let Utils = { ranges = [[200,300]]; for (let i = 0; i < ranges.length; i++) { - rng = ranges[i]; + var rng = ranges[i]; if (typeof(rng) == "object" && code >= rng[0] && code < rng[1]) return true; else if (typeof(rng) == "number" && code == rng) { From 3d2c2d54f5890872e711aa96622b80fb3b95f9e9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 17 Jun 2008 18:01:48 +0900 Subject: [PATCH 0368/1860] Don't allow re-entrance! Fail to lock when we already hold a lock. --- services/sync/modules/dav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 91079a75824d..5c70610d12d7 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -356,7 +356,7 @@ DAVCollection.prototype = { if (DAVLocks['default']) { this._log.debug("Lock called, but we already hold a token"); - self.done(DAVLocks['default']); + self.done(); return; } From d9e65d64b6a20a945f296d4a663eea8a244c84be Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 17 Jun 2008 18:03:02 +0900 Subject: [PATCH 0369/1860] make RemoteStore a but more Store-like (wrap, wipe methods); fix a missing yield --- services/sync/modules/engines.js | 16 +--- services/sync/modules/remote.js | 126 ++++++++++++++++--------------- 2 files changed, 68 insertions(+), 74 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cb647e9f9d12..e2627822b401 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -206,16 +206,7 @@ Engine.prototype = { _resetServer: function Engine__resetServer() { let self = yield; - this._log.debug("Resetting server data"); - this._remote.status.delete(self.cb); - yield; - this._remote.keys.delete(self.cb); - yield; - this._remote.snapshot.delete(self.cb); - yield; - this._remote.deltas.delete(self.cb); - yield; - this._log.debug("Server files deleted"); + yield this._remote.wipe(self.cb); }, _resetClient: function Engine__resetClient() { @@ -260,7 +251,7 @@ Engine.prototype = { this._log.info("Beginning sync"); try { - yield this._remote.initSession(self.cb); + yield this._remote.openSession(self.cb); } catch (e if e.message.status == 404) { yield this._initialUpload.async(this, self.cb); return; @@ -279,7 +270,6 @@ Engine.prototype = { this._log.info("Local snapshot version: " + this._snapshot.version); this._log.info("Server maxVersion: " + this._remote.status.data.maxVersion); - this._log.debug("Server snapVersion: " + this._remote.status.data.snapVersion); if ("none" != Utils.prefs.getCharPref("encryption")) { let symkey = yield this._remote.keys.getKey(self.cb, this.pbeId); @@ -288,7 +278,7 @@ Engine.prototype = { // 1) Fetch server deltas let server = {}; - let serverSnap = yield this._remote.getLatestFromSnap(self.cb, this._snapshot); + let serverSnap = yield this._remote.wrap(self.cb, this._snapshot); server.snapshot = serverSnap.data; this._core.detectUpdates(self.cb, this._snapshot.data, server.snapshot); server.updates = yield; diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 091488685991..4ff0c51a1486 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -340,32 +340,6 @@ Keychain.prototype = { // FIXME: implement setKey() }; -function Snapshot(remoteStore) { - this._init(remoteStore); -} -Snapshot.prototype = { - __proto__: new Resource(), - _init: function Snapshot__init(remoteStore) { - this._remote = remoteStore; - this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "snapshot.json"); - this.pushFilter(new JsonFilter()); - this.pushFilter(new CryptoFilter(remoteStore, "snapshotEncryption")); - } -}; - -function Deltas(remoteStore) { - this._init(remoteStore); -} -Deltas.prototype = { - __proto__: new Resource(), - _init: function Deltas__init(remoteStore) { - this._remote = remoteStore; - this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "deltas.json"); - this.pushFilter(new JsonFilter()); - this.pushFilter(new CryptoFilter(remoteStore, "deltasEncryption")); - } -}; - function RemoteStore(engine) { this._engine = engine; this._log = Log4Moz.Service.getLogger("Service.RemoteStore"); @@ -387,19 +361,23 @@ RemoteStore.prototype = { return keys; }, - get snapshot() { - let snapshot = new Snapshot(this); - this.__defineGetter__("snapshot", function() snapshot); + get _snapshot() { + let snapshot = new Resource(this.serverPrefix + "snapshot.json"); + snapshot.pushFilter(new JsonFilter()); + snapshot.pushFilter(new CryptoFilter(this, "snapshotEncryption")); + this.__defineGetter__("_snapshot", function() snapshot); return snapshot; }, - get deltas() { - let deltas = new Deltas(this); - this.__defineGetter__("deltas", function() deltas); + get _deltas() { + let deltas = new Resource(this.serverPrefix + "deltas.json"); + deltas.pushFilter(new JsonFilter()); + deltas.pushFilter(new CryptoFilter(this, "deltasEncryption")); + this.__defineGetter__("_deltas", function() deltas); return deltas; }, - _initSession: function RStore__initSession() { + _openSession: function RStore__openSession() { let self = yield; if (!this.serverPrefix || !this.engineId) @@ -407,8 +385,8 @@ RemoteStore.prototype = { this.status.data = null; this.keys.data = null; - this.snapshot.data = null; - this.deltas.data = null; + this._snapshot.data = null; + this._deltas.data = null; DAV.MKCOL(this.serverPrefix, self.cb); let ret = yield; @@ -428,21 +406,19 @@ RemoteStore.prototype = { ENGINE_STORAGE_FORMAT_VERSION); throw "Incompatible remote store format"; } - - this._inited = true; }, - initSession: function RStore_initSession(onComplete) { - this._initSession.async(this, onComplete); + openSession: function RStore_openSession(onComplete) { + this._openSession.async(this, onComplete); }, closeSession: function RStore_closeSession() { - this._inited = false; this.status.data = null; this.keys.data = null; - this.snapshot.data = null; - this.deltas.data = null; + this._snapshot.data = null; + this._deltas.data = null; }, + // Does a fresh upload of the given snapshot to a new store _initialize: function RStore__initialize(snapshot) { let self = yield; let symkey; @@ -464,8 +440,8 @@ RemoteStore.prototype = { keys.ring[this.engineId.username] = symkey; yield this.keys.put(self.cb, keys); - yield this.snapshot.put(self.cb, snapshot.data); - yield this.deltas.put(self.cb, []); + yield this._snapshot.put(self.cb, snapshot.data); + yield this._deltas.put(self.cb, []); let c = 0; for (GUID in snapshot.data) @@ -485,29 +461,31 @@ RemoteStore.prototype = { this._initialize.async(this, onComplete, snapshot); }, - _appendDelta: function RStore__appendDelta(delta) { + // Removes server files - you may want to run initialize() after this + _wipe: function Engine__wipe() { let self = yield; - if (this.deltas.data == null) { - yield this.deltas.get(self.cb); - if (this.deltas.data == null) - this.deltas.data = []; - } - this.deltas.data.push(delta); - yield this.deltas.put(self.cb, this.deltas.data); + this._log.debug("Deleting remote store data"); + yield this.status.delete(self.cb); + yield this.keys.delete(self.cb); + yield this._snapshot.delete(self.cb); + yield this._deltas.delete(self.cb); + this._log.debug("Server files deleted"); }, - appendDelta: function RStore_appendDelta(onComplete, delta) { - this._appendDelta.async(this, onComplete, delta); + wipe: function Engine_wipe(onComplete) { + this._wipe.async(this, onComplete) }, + // Gets the latest server snapshot by downloading all server files + // (snapshot + deltas) _getLatestFromScratch: function RStore__getLatestFromScratch() { let self = yield; this._log.info("Downloading all server data from scratch"); this._log.debug("Downloading server snapshot"); - let data = yield this.snapshot.get(self.cb); + let data = yield this._snapshot.get(self.cb); this._log.debug("Downloading server deltas"); - let deltas = yield this.deltas.get(self.cb); + let deltas = yield this._deltas.get(self.cb); let snap = new SnapshotStore(); snap.version = this.status.data.maxVersion; @@ -520,14 +498,15 @@ RemoteStore.prototype = { self.done(snap); }, + // Gets the latest server snapshot by downloading only the necessary + // deltas from the given snapshot (but may fall back to a full download) _getLatestFromSnap: function RStore__getLatestFromSnap(lastSyncSnap) { let self = yield; let deltas, snap = new SnapshotStore(); snap.version = this.status.data.maxVersion; if (lastSyncSnap.version < this.status.data.snapVersion) { - snap = this._getLatestFromScratch.async(this, self.cb); - self.done(snap); + self.done(yield this.getLatestFromScratch(self.cb)); return; } else if (lastSyncSnap.version >= this.status.data.snapVersion && @@ -535,7 +514,7 @@ RemoteStore.prototype = { this._log.debug("Using last sync snapshot as starting point for server snapshot"); snap.data = Utils.deepCopy(lastSyncSnap.data); this._log.info("Downloading server deltas"); - let allDeltas = yield this.deltas.get(self.cb); + let allDeltas = yield this._deltas.get(self.cb); deltas = allDeltas.slice(lastSyncSnap.version - this.status.data.snapVersion); } else if (lastSyncSnap.version == this.status.data.maxVersion) { @@ -563,7 +542,32 @@ RemoteStore.prototype = { self.done(snap); }, - getLatestFromSnap: function RStore_getLatestFromSnap(onComplete, lastSyncSnap) { - this._getLatestFromSnap.async(this, onComplete, lastSyncSnap); + + // get the latest server snapshot. If a snapshot is given, try to + // download only the necessary deltas to get to the latest + _wrap: function RStore__wrap(snapshot) { + let self = yield; + if (snapshot) + self.done(yield this._getLatestFromSnap.async(this, self.cb, snapshot)); + else + self.done(yield this._getLatestFromScratch.async(this, self.cb)); + }, + wrap: function RStore_wrap(onComplete, snapshot) { + this._wrap.async(this, onComplete, snapshot); + }, + + // Adds a new set of changes (a delta) to this store + _appendDelta: function RStore__appendDelta(delta) { + let self = yield; + if (this._deltas.data == null) { + yield this._deltas.get(self.cb); + if (this._deltas.data == null) + this._deltas.data = []; + } + this._deltas.data.push(delta); + yield this._deltas.put(self.cb, this._deltas.data); + }, + appendDelta: function RStore_appendDelta(onComplete, delta) { + this._appendDelta.async(this, onComplete, delta); } }; From 03895ad61b5e1fa56111a5b849c40e45eec89b0d Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 17 Jun 2008 10:23:35 -0700 Subject: [PATCH 0370/1860] Added a fixme/todo in the code. --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eb8543417bd2..62625f5fb223 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -446,6 +446,8 @@ WeaveSvc.prototype = { let success = yield; if (!success) { try { + // FIXME: This code may not be needed any more, due to the way + // that the server is expected to create the user dir for us. this._checkUserDir.async(this, self.cb); yield; } catch (e) { /* FIXME: tmp workaround for services.m.c */ } From 4fa5b731aeed5c7f11db60967183ecd60ce28a4c Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 17 Jun 2008 11:11:56 -0700 Subject: [PATCH 0371/1860] Fixed a minor typo, though there's a better way to do the unary getter which Myk told me about; will probably substitute that soon. --- services/sync/modules/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index e8d8ce926fa2..7cd98d556b90 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -317,7 +317,7 @@ let Utils = { return function innerBind() { return method.apply(object, arguments); }; }, - _prefs: null, + __prefs: null, get prefs() { if (!this.__prefs) { this.__prefs = Cc["@mozilla.org/preferences-service;1"] From 6bee07649991981691feedcb3e18055bc32008b0 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 17 Jun 2008 11:45:13 -0700 Subject: [PATCH 0372/1860] Added a fake preference service to test_service. --- services/sync/tests/unit/test_service.js | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index 0ea747b523df..839a664650d2 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -21,6 +21,32 @@ function makeFakeAsyncFunc(retval) { return fakeAsyncFunc; } +function FakePrefs() {} + +FakePrefs.prototype = { + __contents: {"log.logger.async" : "Debug", + "username" : "foo", + "serverURL" : "https://example.com/", + "encryption" : true, + "enabled" : true, + "schedule" : 0}, + _getPref: function fake__getPref(pref) { + Log4Moz.Service.rootLogger.trace("Getting pref: " + pref); + return this.__contents[pref]; + }, + getCharPref: function fake_getCharPref(pref) { + return this._getPref(pref); + }, + getBoolPref: function fake_getBoolPref(pref) { + return this._getPref(pref); + }, + getIntPref: function fake_getIntPref(pref) { + return this._getPref(pref); + } +}; + +Utils.__prefs = new FakePrefs(); + Crypto.__proto__ = { RSAkeydecrypt: function fake_RSAkeydecrypt(identity) { let self = yield; From 7955679c2460dc288c571fd50e667f78fa010fd6 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 17 Jun 2008 12:04:40 -0700 Subject: [PATCH 0373/1860] The manage.py test-runner now compares expected results to actual results if a '.log.expected' file exists in the test directory; if they don't match, a unified diff is displayed. Note that this is only done via manage.py, it's not currently implemented in the Makefile test framework because makefiles make me barf. --- services/sync/tests/unit/test_service.log.expected | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 services/sync/tests/unit/test_service.log.expected diff --git a/services/sync/tests/unit/test_service.log.expected b/services/sync/tests/unit/test_service.log.expected new file mode 100644 index 000000000000..a1b14e2fd17f --- /dev/null +++ b/services/sync/tests/unit/test_service.log.expected @@ -0,0 +1,11 @@ +*** test pending +Running test: test_login_works +Service.Main DEBUG Logging in +Service.Main INFO Using server URL: https://example.com/user/foo +root INFO Retrieving meta/version +root INFO Retrieving private/privkey +Service.Main INFO Weave scheduler disabled +1 of 1 tests passed. +*** test finished +*** exiting +*** PASS *** From 3f3568c73c3bf350a41cbf35006523b20b3cbaea Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 17 Jun 2008 19:54:09 -0700 Subject: [PATCH 0374/1860] Refactored test_service and module code so that the weave service constructor is called, and auth is done more accurately. --- services/sync/modules/identity.js | 41 ++----------------- services/sync/modules/service.js | 22 ++++++---- services/sync/modules/util.js | 35 ++++++++++++++++ services/sync/tests/unit/test_service.js | 37 +++++++---------- .../sync/tests/unit/test_service.log.expected | 1 + 5 files changed, 68 insertions(+), 68 deletions(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 5eaf2ecd39f4..a2c28c637a69 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -58,7 +58,7 @@ IDManager.prototype = { if (this._aliases[name]) return this._ids[this._aliases[name]]; else - return this._ids[name] + return this._ids[name]; }, set: function IDMgr_set(name, id) { this._ids[name] = id; @@ -112,49 +112,14 @@ Identity.prototype = { _password: null, get password() { if (!this._password) - return findPassword(this.realm, this.username); + return Utils.findPassword(this.realm, this.username); return this._password; }, set password(value) { - setPassword(this.realm, this.username, value); + Utils.setPassword(this.realm, this.username, value); }, setTempPassword: function Id_setTempPassword(value) { this._password = value; } }; - -// fixme: move these to util.js? -function findPassword(realm, username) { - // fixme: make a request and get the realm ? - let password; - let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://sync', null, realm); - - for (let i = 0; i < logins.length; i++) { - if (logins[i].username == username) { - password = logins[i].password; - break; - } - } - return password; -} - -function setPassword(realm, username, password) { - // cleanup any existing passwords - let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://sync', null, realm); - for(let i = 0; i < logins.length; i++) { - lm.removeLogin(logins[i]); - } - - if (!password) - return; - - // save the new one - let nsLoginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new nsLoginInfo('chrome://sync', null, realm, - username, password, "", ""); - lm.addLogin(login); -} diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 62625f5fb223..2ab847fb72be 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -102,7 +102,7 @@ Utils.lazy(Weave, 'Service', WeaveSvc); * Main entry point into Weave's sync framework */ -function WeaveSvc() { +function WeaveSvc(engines) { this._startupFinished = false; this._initLogs(); this._log.info("Weave Sync Service Initializing"); @@ -116,13 +116,19 @@ function WeaveSvc() { ID.setAlias('WeaveID', 'DAV:default'); ID.setAlias('WeaveCryptoID', 'Engine:PBE:default'); - // Register built-in engines - Engines.register(new BookmarksEngine()); - Engines.register(new HistoryEngine()); - Engines.register(new CookieEngine()); - Engines.register(new PasswordEngine()); - Engines.register(new FormEngine()); - Engines.register(new TabEngine()); + if (typeof engines == "undefined") + engines = [ + new BookmarksEngine(), + new HistoryEngine(), + new CookieEngine(), + new PasswordEngine(), + new FormEngine(), + new TabEngine() + ]; + + // Register engines + for (let i = 0; i < engines.length; i++) + Engines.register(engines[i]); // Other misc startup Utils.prefs.addObserver("", this, false); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 7cd98d556b90..e91d056e3064 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -51,6 +51,41 @@ Cu.import("resource://weave/log4moz.js"); let Utils = { + findPassword: function findPassword(realm, username) { + // fixme: make a request and get the realm ? + let password; + let lm = Cc["@mozilla.org/login-manager;1"] + .getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, 'chrome://sync', null, realm); + + for (let i = 0; i < logins.length; i++) { + if (logins[i].username == username) { + password = logins[i].password; + break; + } + } + return password; + }, + + setPassword: function setPassword(realm, username, password) { + // cleanup any existing passwords + let lm = Cc["@mozilla.org/login-manager;1"] + .getService(Ci.nsILoginManager); + let logins = lm.findLogins({}, 'chrome://sync', null, realm); + for (let i = 0; i < logins.length; i++) + lm.removeLogin(logins[i]); + + if (!password) + return; + + // save the new one + let nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo('chrome://sync', null, realm, + username, password, "", ""); + lm.addLogin(login); + }, + // lazy load objects from a constructor on first access. It will // work with the global object ('this' in the global context). lazy: function Weave_lazy(dest, prop, ctr) { diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index 839a664650d2..7cff9846d68f 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -42,11 +42,20 @@ FakePrefs.prototype = { }, getIntPref: function fake_getIntPref(pref) { return this._getPref(pref); - } + }, + addObserver: function fake_addObserver() {} }; Utils.__prefs = new FakePrefs(); +Utils.findPassword = function fake_findPassword(realm, username) { + let contents = { + 'Mozilla Services Password': {foo: "bar"}, + 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} + }; + return contents[realm][username]; +}; + Crypto.__proto__ = { RSAkeydecrypt: function fake_RSAkeydecrypt(identity) { let self = yield; @@ -75,33 +84,17 @@ DAV.__proto__ = { } }; -function FakeID(realm, username, password) { - this.realm = realm; - this.username = username; - this.password = password; - this.setTempPassword = function FID_setTempPassword(value) { - if (typeof value != "undefined") - this.password = value; - }; -} - -ID.__proto__ = { - __contents: {WeaveID: new FakeID("", "foo", "bar"), - WeaveCryptoID: new FakeID("", "", "passphrase")}, - - get: function fake_ID_get(name) { - return this.__contents[name]; - } -}; - let Service = loadInSandbox("resource://weave/service.js"); function TestService() { - this._startupFinished = false; - this._log = Log4Moz.Service.getLogger("Service.Main"); + this.__superclassConstructor = Service.WeaveSvc; + this.__superclassConstructor([]); } TestService.prototype = { + _initLogs: function TS__initLogs() { + this._log = Log4Moz.Service.getLogger("Service.Main"); + } }; TestService.prototype.__proto__ = Service.WeaveSvc.prototype; diff --git a/services/sync/tests/unit/test_service.log.expected b/services/sync/tests/unit/test_service.log.expected index a1b14e2fd17f..766883fc7d13 100644 --- a/services/sync/tests/unit/test_service.log.expected +++ b/services/sync/tests/unit/test_service.log.expected @@ -1,5 +1,6 @@ *** test pending Running test: test_login_works +Service.Main INFO Weave Sync Service Initializing Service.Main DEBUG Logging in Service.Main INFO Using server URL: https://example.com/user/foo root INFO Retrieving meta/version From 5562b50377c1d5d6f9efed6d34a20c32b29ece4f Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 11:54:24 -0700 Subject: [PATCH 0375/1860] Moved XPCOM-specific stuff from engines/passwords.js to util.js so they could be easily stubbed-out by unit tests. --- services/sync/modules/engines/passwords.js | 13 +++---------- services/sync/modules/util.js | 12 ++++++++++++ services/sync/tests/unit/test_passwords.js | 11 ++++++----- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index beef082a5c81..24f259934f5a 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -1,7 +1,5 @@ const EXPORTED_SYMBOLS = ['PasswordEngine']; -const Cc = Components.classes; -const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://weave/util.js"); @@ -63,8 +61,7 @@ PasswordSyncCore.prototype = { __loginManager : null, get _loginManager() { if (!this.__loginManager) - this.__loginManager = Cc["@mozilla.org/login-manager;1"]. - getService(Ci.nsILoginManager); + this.__loginManager = Utils.getLoginManager(); return this.__loginManager; }, @@ -100,21 +97,17 @@ PasswordStore.prototype = { __loginManager : null, get _loginManager() { if (!this.__loginManager) - this.__loginManager = Cc["@mozilla.org/login-manager;1"]. - getService(Ci.nsILoginManager); + this.__loginManager = Utils.getLoginManager(); return this.__loginManager; }, __nsLoginInfo : null, get _nsLoginInfo() { if (!this.__nsLoginInfo) - this.__nsLoginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", - Ci.nsILoginInfo, "init"); + this.__nsLoginInfo = Utils.makeNewLoginInfo(); return this.__nsLoginInfo; }, - _createCommand: function PasswordStore__createCommand(command) { this._log.info("PasswordStore got createCommand: " + command ); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index e91d056e3064..8361da440a1b 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -50,6 +50,18 @@ Cu.import("resource://weave/log4moz.js"); */ let Utils = { + getLoginManager: function getLoginManager() { + return Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + }, + + makeNewLoginInfo: function getNewLoginInfo() { + return new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" + ); + }, findPassword: function findPassword(realm, username) { // fixme: make a request and get the realm ? diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index a8ce63940855..bd1f69ef65e3 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -1,3 +1,5 @@ +Cu.import("resource://weave/util.js"); + function run_test() { // The JS module we're testing, with all members exposed. var passwords = loadInSandbox("resource://weave/engines/passwords.js"); @@ -13,10 +15,10 @@ function run_test() { passwordField: "test_password" }; - // Fake nsILoginManager object. - var fakeLoginManager = { - getAllLogins: function() { return [fakeUser]; } - }; + Utils.getLoginManager = function fake_getLoginManager() { + // Return a fake nsILoginManager object. + return {getAllLogins: function() { return [fakeUser]; }}; + }; // Ensure that _hashLoginInfo() works. var fakeUserHash = passwords._hashLoginInfo(fakeUser); @@ -25,7 +27,6 @@ function run_test() { // Ensure that PasswordSyncCore._itemExists() works. var psc = new passwords.PasswordSyncCore(); - psc.__loginManager = fakeLoginManager; do_check_false(psc._itemExists("invalid guid")); do_check_true(psc._itemExists(fakeUserHash)); } From 68e12e5b83781975021754982cc086bfe361cc21 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 12:04:49 -0700 Subject: [PATCH 0376/1860] Removed an unused constructor parameter from PasswordEngine. --- services/sync/modules/engines/passwords.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 24f259934f5a..689fa820d742 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -28,8 +28,8 @@ function _hashLoginInfo(aLogin) { return Utils.sha1(loginKey); } -function PasswordEngine(pbeId) { - this._init(pbeId); +function PasswordEngine() { + this._init(); } PasswordEngine.prototype = { get name() { return "passwords"; }, From fbae14b84dfda95e49d04c84b1e9405c8cde6640 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 12:29:25 -0700 Subject: [PATCH 0377/1860] Added documentation to BookmarkEngine._updateOutgoingShare. --- services/sync/modules/engines/bookmarks.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 8772e6e1099b..d8323811d256 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -363,17 +363,29 @@ BookmarksEngine.prototype = { }, _updateOutgoingShare: function BmkEngine__updateOutgoing(folderNode) { + /* Puts all the bookmark data from the specified bookmark folder, + encrypted, onto the shared directory on the server (replacing + anything that was already there). + + TODO: error handling*/ let self = yield; let myUserName = ID.get('WeaveID').username; + // The folder has an annotation specifying the server path to the + // directory: let ans = Cc["@mozilla.org/browser/annotation-service;1"]. getService(Ci.nsIAnnotationService); let serverPath = ans.getItemAnnotation(folderNode, SERVER_PATH_ANNO); + // From that directory, get the keyring file, and from it, the symmetric + // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); let keyring = keyringFile.get(); let symKey = keyring[ myUserName ]; + // Get the let json = this._store._wrapMount( folderNode, myUserName ); - // TODO what does wrapMount do with this username? Should I be passing - // in my own or that of the person I share with? + /* TODO what does wrapMount do with this username? Should I be passing + in my own or that of the person I share with? */ + + // Encrypt it with the symkey and put it into the shared-bookmark file. let bookmarkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); Crypto.PBEencrypt.async( Crypto, self.cb, json, {password:symKey} ); let cyphertext = yield; From bfb7414b1025c1a783019018c2a878459394f13b Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 12:32:20 -0700 Subject: [PATCH 0378/1860] Replaced a bunch of boilerplate XPCOM with a call to Utils.makeTimerForCall(). --- services/sync/modules/stores.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index d214e09201cc..e4df0b2072ce 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -76,16 +76,10 @@ Store.prototype = { applyCommands: function Store_applyCommands(commandList) { let self = yield; - let timer, listener; - - if (this._yieldDuringApply) { - listener = new Utils.EventListener(self.cb); - timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - } for (var i = 0; i < commandList.length; i++) { if (this._yieldDuringApply) { - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + Utils.makeTimerForCall(self.cb); yield; // Yield to main loop } var command = commandList[i]; @@ -182,7 +176,7 @@ SnapshotStore.prototype = { oldGUID = command.GUID; this._data[newGUID] = this._data[oldGUID]; - delete this._data[oldGUID] + delete this._data[oldGUID]; for (let GUID in this._data) { if (this._data[GUID].parentGUID == oldGUID) From 1c48ebce1a24c819089bd1cdc468380c1c489c82 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 12:41:34 -0700 Subject: [PATCH 0379/1860] Removed the check of whether the directory exists before creating it, in _createOutgoingShare(): realized it's not neccessary since DAV.MKCOL already does it. --- services/sync/modules/engines/bookmarks.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d8323811d256..6210b557c2dc 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -312,13 +312,11 @@ BookmarksEngine.prototype = { /* Create the directory on the server if it does not exist already. */ let serverPath = "/user/" + myUserName + "/share/" + folderGuid; - if (!server.exists(serverPath)) { - DAV.MKCOL(serverPath, self.cb); - let ret = yeild; - if (!ret) { - this._log.error("Can't create remote folder for outgoing share."); - self.done(false); - } + DAV.MKCOL(serverPath, self.cb); + let ret = yeild; + if (!ret) { + this._log.error("Can't create remote folder for outgoing share."); + self.done(false); } /* Store the path to the server directory in an annotation on the shared From a2bf0b0ac3d360e0688b02606a8f4f0f44944a97 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 12:45:02 -0700 Subject: [PATCH 0380/1860] Fixed typos. --- services/sync/modules/engines/bookmarks.js | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 6210b557c2dc..cae581af1c66 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -111,7 +111,7 @@ BookmarksEngine.prototype = { // Get serverUrl and realm of the jabber server from preferences: let serverUrl = Utils.prefs.getCharPref( "xmpp.server.url" ); let realm = Utils.prefs.getCharPref( "xmpp.server.realm" ); - + // TODO once we have ejabberd talking to LDAP, the username/password // for xmpp will be the same as the ones for Weave itself, so we can // read username/password like this: @@ -121,7 +121,7 @@ BookmarksEngine.prototype = { let clientName = Utils.prefs.getCharPref( "xmpp.client.name" ); let clientPassword = Utils.prefs.getCharPref( "xmpp.client.password" ); let transport = new HTTPPollingTransport( serverUrl, false, 15000 ); - let auth = new PlainAuthenticator(); + let auth = new PlainAuthenticator(); // TODO use MD5Authenticator instead once we get it working -- plain is // a security hole. this._xmppClient = new XmppClient( clientName, @@ -154,7 +154,8 @@ BookmarksEngine.prototype = { bmkEngine._incomingShareWithdrawn( directoryName, from ); } } - } + }; + this._xmppClient.registerMessageHandler( messageHandler ); this._xmppClient.connect( realm, self.cb ); yield; @@ -168,8 +169,8 @@ BookmarksEngine.prototype = { }, _incomingShareOffer: function BmkEngine__incomingShareOffer( dir, user ) { - /* Called when we receive an offer from another user to share a - directory. + /* Called when we receive an offer from another user to share a + directory. TODO what should happen is that we add a notification to the queue telling that the incoming share has been offered; when the offer @@ -187,7 +188,7 @@ BookmarksEngine.prototype = { /* Called when we receive a message telling us that a user who has already shared a directory with us has chosen to stop sharing the directory. - + TODO Find the incomingShare in our bookmark tree that corresponds to the shared directory, and delete it; add a notification to the queue telling us what has happened. @@ -243,7 +244,7 @@ BookmarksEngine.prototype = { } else { this._log.warn( "No XMPP connection for share notification." ); } - } + } /* LONGTERM TODO: in the future when we allow sharing one folder with many people, the value of the annotation can be a whole list @@ -301,7 +302,7 @@ BookmarksEngine.prototype = { _updateOutgoingShare().) */ let self = yield; let myUserName = ID.get('WeaveID').username; - this._log.debug("Sharing bookmarks from " + folder.getAttribute( "label" ) + this._log.debug("Sharing bookmarks from " + folder.getAttribute( "label" ) + " with " + username); /* Generate a new GUID to use as the new directory name on the server @@ -313,7 +314,7 @@ BookmarksEngine.prototype = { /* Create the directory on the server if it does not exist already. */ let serverPath = "/user/" + myUserName + "/share/" + folderGuid; DAV.MKCOL(serverPath, self.cb); - let ret = yeild; + let ret = yield; if (!ret) { this._log.error("Can't create remote folder for outgoing share."); self.done(false); @@ -378,8 +379,8 @@ BookmarksEngine.prototype = { let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); let keyring = keyringFile.get(); let symKey = keyring[ myUserName ]; - // Get the - let json = this._store._wrapMount( folderNode, myUserName ); + // Get the + let json = this._store._wrapMount( folderNode, myUserName ); /* TODO what does wrapMount do with this username? Should I be passing in my own or that of the person I share with? */ From cdfc3e92d140f79a08f8f084d74346c86a82fb34 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 12:48:20 -0700 Subject: [PATCH 0381/1860] made all calls to Resource.get() and Resource.put() properly asynchronous. --- services/sync/modules/engines/bookmarks.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 6210b557c2dc..8298aeb1cc0b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -313,11 +313,12 @@ BookmarksEngine.prototype = { /* Create the directory on the server if it does not exist already. */ let serverPath = "/user/" + myUserName + "/share/" + folderGuid; DAV.MKCOL(serverPath, self.cb); - let ret = yeild; + let ret = yield; if (!ret) { this._log.error("Can't create remote folder for outgoing share."); self.done(false); } + // TODO more error handling /* Store the path to the server directory in an annotation on the shared bookmark folder, so we can easily get back to it later. */ @@ -336,9 +337,11 @@ BookmarksEngine.prototype = { /* Get public keys for me and the user I'm sharing with. Each user's public key is stored in /user/username/public/pubkey. */ let myPubKeyFile = new Resource("/user/" + myUserName + "/public/pubkey"); - let myPubKey = myPubKeyFile.get(); // TODO call asynchronously? + myPubKeyFile.get(self.cb); + let myPubKey = yield; let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); - let userPubKey = userPubKeyFile.get(); + userPubKeyFile.get(self.cb); + let userPubKey = yield; /* Create the keyring, containing the sym key encrypted with each of our public keys: */ @@ -349,7 +352,8 @@ BookmarksEngine.prototype = { let keyring = { myUserName: encryptedForMe, username: encryptedForYou }; let keyringFile = new Resource( serverPath + "/" + KEYRING_FILE_NAME ); - keyringFile.put( this._json.encode( keyring ) ); // TODO call async? + keyringFile.put( self.cb, this._json.encode( keyring ) ); + yield; // Call Atul's js api for setting htaccess: let sharingApi = new Sharing.Api( DAV ); @@ -376,7 +380,8 @@ BookmarksEngine.prototype = { // From that directory, get the keyring file, and from it, the symmetric // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); - let keyring = keyringFile.get(); + keyringFile.get(self.cb); + let keyring = yield; let symKey = keyring[ myUserName ]; // Get the let json = this._store._wrapMount( folderNode, myUserName ); @@ -387,7 +392,8 @@ BookmarksEngine.prototype = { let bookmarkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); Crypto.PBEencrypt.async( Crypto, self.cb, json, {password:symKey} ); let cyphertext = yield; - bookmarkFile.put( cyphertext ); + bookmarkFile.put( self.cb, cyphertext ); + yield; self.done(); }, From 931327616dc0da92fb8cc8a43db7c1f79052943c Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 13:16:32 -0700 Subject: [PATCH 0382/1860] The xmpp messages that are sent when a share is offered now include the server-side path to the share directory. --- services/sync/modules/engines/bookmarks.js | 49 ++++++++++++---------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d01b8ce84b7f..8638d3ce7066 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -133,25 +133,27 @@ BookmarksEngine.prototype = { let messageHandler = { handle: function ( messageText, from ) { /* The callback function for incoming xmpp messages. - We expect message text to be either: - "share " - (sender offers to share directory dir with us) - or "stop " - (sender has stopped sharing directory dir with us.) - or "accept " - (sharee has accepted our offer to share our dir.) - or "decline " - (sharee has declined our offer to share our dir.) + We expect message text to be in the form of: + . + Where command is one of: + "share" (sender offers to share directory dir with us) + or "stop" (sender has stopped sharing directory dir with us.) + or "accept" (sharee has accepted our offer to share our dir.) + or "decline" (sharee has declined our offer to share our dir.) + + Folder name is the human-readable name of the bookmark folder + being shared (it can contain spaces). serverpath is the path + on the server to the directory where the data is stored: + only the machine seese this, and it can't have spaces. */ let words = messageText.split(" "); let commandWord = words[0]; - let directoryName = words.slice(1).join(" "); - // TODO also parse out the serverPath of the share, which should - // be in the message. + let serverPath = words[1]; + let directoryName = words.slice(2).join(" "); if ( commandWord == "share" ) { - bmkEngine._incomingShareOffer( directoryName, from ); + bmkEngine._incomingShareOffer(from, serverPath, folderName); } else if ( commandWord == "stop" ) { - bmkEngine._incomingShareWithdrawn( directoryName, from ); + bmkEngine._incomingShareWithdrawn(from, serverPath, folderName); } } }; @@ -168,9 +170,11 @@ BookmarksEngine.prototype = { self.done(); }, - _incomingShareOffer: function BmkEngine__incomingShareOffer( dir, user ) { + _incomingShareOffer: function BmkEngine__incomingShareOffer(user, + serverPath, + folderName) { /* Called when we receive an offer from another user to share a - directory. + folder. TODO what should happen is that we add a notification to the queue telling that the incoming share has been offered; when the offer @@ -181,10 +185,13 @@ BookmarksEngine.prototype = { right ahead to creating the incoming share. */ dump( "I was offered the directory " + dir + " from user " + dir ); - _createIncomingShare( dir, user, dir ); + _createIncomingShare( user, serverPath, folderName ); }, - _incomingShareWithdrawn: function BmkEngine__incomingShareStop( dir, user ) { + _incomingShareWithdrawn: function BmkEngine__incomingShareStop(user, + serverPath, + folderName) { + /* Called when we receive a message telling us that a user who has already shared a directory with us has chosen to stop sharing the directory. @@ -225,6 +232,7 @@ BookmarksEngine.prototype = { let serverPath = yield; dump( "Done calling _createOutgoingShare asynchronously.\n" ); this._updateOutgoingShare.async( this, self.cb, selectedFolder ); + yield; /* Set the annotation on the folder so we know it's an outgoing share: */ @@ -237,9 +245,8 @@ BookmarksEngine.prototype = { // Send an xmpp message to the share-ee if ( this._xmppClient ) { if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { - dump( "Gonna send notification...\n" ); - // TODO add serverPath to the message!! - let msgText = "share " + folderName; + let msgText = "share " + serverPath + " " + folderName; + this._log.debug( "Sending XMPP message: " + msgText ); this._xmppClient.sendMessage( username, msgText ); } else { this._log.warn( "No XMPP connection for share notification." ); From 83bf7ddc93632054d8ac423b9d9f0e645bb96a09 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Wed, 18 Jun 2008 13:25:58 -0700 Subject: [PATCH 0383/1860] You have: no tea. (Remove old TEA crypto support) --- services/sync/modules/crypto.js | 65 ++----------- services/sync/modules/xxtea.js | 163 -------------------------------- 2 files changed, 9 insertions(+), 219 deletions(-) delete mode 100644 services/sync/modules/xxtea.js diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 7d6ffd86d446..44ae609ef374 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -65,16 +65,6 @@ CryptoSvc.prototype = { return this.__os; }, - __xxtea: {}, - __xxteaLoaded: false, - get _xxtea() { - if (!this.__xxteaLoaded) { - Cu.import("resource://weave/xxtea.js", this.__xxtea); - this.__xxteaLoaded = true; - } - return this.__xxtea; - }, - get defaultAlgorithm() { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); @@ -374,20 +364,17 @@ CryptoSvc.prototype = { let cur = branch.getCharPref("extensions.weave.encryption"); if (cur == data) - return; + return; switch (data) { - case "none": - this._log.info("Encryption disabled"); - break; - case "XXTEA": - case "XXXTEA": // Weave 0.1 had this typo - this._log.info("Using encryption algorithm: " + data); - break; - default: - this._log.warn("Unknown encryption algorithm, resetting"); - branch.setCharPref("extensions.weave.encryption", "XXTEA"); - return; // otherwise we'll send the alg changed event twice + case "none": + this._log.info("Encryption disabled"); + break; + + default: + this._log.warn("Unknown encryption algorithm, resetting"); + branch.clearUserPref("extensions.weave.encryption"); + return; // otherwise we'll send the alg changed event twice } // FIXME: listen to this bad boy somewhere this._os.notifyObservers(null, "weave:encryption:algorithm-changed", ""); @@ -414,23 +401,6 @@ CryptoSvc.prototype = { ret = data; break; - case "XXXTEA": // Weave 0.1.12.10 and below had this typo - case "XXTEA": { - let gen = this._xxtea.encrypt(data, identity.password); - ret = gen.next(); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - try { - while (typeof(ret) == "object") { - timer.initWithCallback(self.listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - ret = gen.next(); - } - gen.close(); - } finally { - timer = null; - } - } break; - case "aes-128-cbc": case "aes-192-cbc": case "aes-256-cbc": @@ -464,23 +434,6 @@ CryptoSvc.prototype = { ret = data; break; - case "XXXTEA": // Weave 0.1.12.10 and below had this typo - case "XXTEA": { - let gen = this._xxtea.decrypt(data, identity.password); - ret = gen.next(); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - try { - while (typeof(ret) == "object") { - timer.initWithCallback(self.listener, 0, timer.TYPE_ONE_SHOT); - yield; // Yield to main loop - ret = gen.next(); - } - gen.close(); - } finally { - timer = null; - } - } break; - case "aes-128-cbc": case "aes-192-cbc": case "aes-256-cbc": diff --git a/services/sync/modules/xxtea.js b/services/sync/modules/xxtea.js deleted file mode 100644 index 4323c2f8283a..000000000000 --- a/services/sync/modules/xxtea.js +++ /dev/null @@ -1,163 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Corrected Block TEA. - * - * The Initial Developer of the Original Code is - * Chris Veness - * Portions created by the Initial Developer are Copyright (C) 2005 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Chris Veness - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -// Original 'Corrected Block TEA' algorithm David Wheeler & Roger Needham -// See http://en.wikipedia.org/wiki/XXTEA -// -// Javascript version by Chris Veness -// http://www.movable-type.co.uk/scripts/tea.html - -const EXPORTED_SYMBOLS = ['encrypt', 'decrypt']; - -function Paused() { -} -Paused.prototype = { - toString: function Paused_toString() { - return "[Generator Paused]"; - } -} - -// use (16 chars of) 'password' to encrypt 'plaintext' -// -// note1:this is a generator so the caller can pause and give control -// to the UI thread -// -// note2: if plaintext or password are passed as string objects, rather -// than strings, this function will throw an 'Object doesn't support -// this property or method' error - -function encrypt(plaintext, password) { - var v = new Array(2), k = new Array(4), s = "", i; - - // use escape() so only have single-byte chars to encode - plaintext = escape(plaintext); - - // build key directly from 1st 16 chars of password - for (i = 0; i < 4; i++) - k[i] = Str4ToLong(password.slice(i * 4, (i + 1) * 4)); - - for (i = 0; i < plaintext.length; i += 8) { - // encode plaintext into s in 64-bit (8 char) blocks - // ... note this is 'electronic codebook' mode - v[0] = Str4ToLong(plaintext.slice(i, i + 4)); - v[1] = Str4ToLong(plaintext.slice(i + 4, i + 8)); - code(v, k); - s += LongToStr4(v[0]) + LongToStr4(v[1]); - - if (i % 512 == 0) - yield new Paused(); - } - - yield escCtrlCh(s); -} - -// use (16 chars of) 'password' to decrypt 'ciphertext' with xTEA - -function decrypt(ciphertext, password) { - var v = new Array(2), k = new Array(4), s = "", i; - - for (i = 0; i < 4; i++) - k[i] = Str4ToLong(password.slice(i * 4, (i + 1) * 4)); - - ciphertext = unescCtrlCh(ciphertext); - for (i = 0; i < ciphertext.length; i += 8) { - // decode ciphertext into s in 64-bit (8 char) blocks - v[0] = Str4ToLong(ciphertext.slice(i, i + 4)); - v[1] = Str4ToLong(ciphertext.slice(i + 4, i + 8)); - decode(v, k); - s += LongToStr4(v[0]) + LongToStr4(v[1]); - - if (i % 512 == 0) - yield new Paused(); - } - - // strip trailing null chars resulting from filling 4-char blocks: - s = s.replace(/\0+$/, ''); - - yield unescape(s); -} - - -function code(v, k) { - // Extended TEA: this is the 1997 revised version of Needham & Wheeler's algorithm - // params: v[2] 64-bit value block; k[4] 128-bit key - var y = v[0], z = v[1]; - var delta = 0x9E3779B9, limit = delta*32, sum = 0; - - while (sum != limit) { - y += (z<<4 ^ z>>>5)+z ^ sum+k[sum & 3]; - sum += delta; - z += (y<<4 ^ y>>>5)+y ^ sum+k[sum>>>11 & 3]; - // note: unsigned right-shift '>>>' is used in place of original '>>', due to lack - // of 'unsigned' type declaration in JavaScript (thanks to Karsten Kraus for this) - } - v[0] = y; v[1] = z; -} - -function decode(v, k) { - var y = v[0], z = v[1]; - var delta = 0x9E3779B9, sum = delta*32; - - while (sum != 0) { - z -= (y<<4 ^ y>>>5)+y ^ sum+k[sum>>>11 & 3]; - sum -= delta; - y -= (z<<4 ^ z>>>5)+z ^ sum+k[sum & 3]; - } - v[0] = y; v[1] = z; -} - - -// supporting functions - -function Str4ToLong(s) { // convert 4 chars of s to a numeric long - var v = 0; - for (var i=0; i<4; i++) v |= s.charCodeAt(i) << i*8; - return isNaN(v) ? 0 : v; -} - -function LongToStr4(v) { // convert a numeric long to 4 char string - var s = String.fromCharCode(v & 0xFF, v>>8 & 0xFF, v>>16 & 0xFF, v>>24 & 0xFF); - return s; -} - -function escCtrlCh(str) { // escape control chars which might cause problems with encrypted texts - return str.replace(/[\0\t\n\v\f\r\xa0'"!]/g, function(c) { return '!' + c.charCodeAt(0) + '!'; }); -} - -function unescCtrlCh(str) { // unescape potentially problematic nulls and control characters - return str.replace(/!\d\d?\d?!/g, function(c) { return String.fromCharCode(c.slice(1,-1)); }); -} From 5718bacac8e0755ccffd9bfeb9bce68f84b6735f Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 14:12:24 -0700 Subject: [PATCH 0384/1860] Refactored some file operations into a new function in Utils and out of engines.js. --- services/sync/modules/stores.js | 26 ++++--------------- services/sync/modules/util.js | 30 ++++++++++++++++++++++ services/sync/tests/unit/test_passwords.js | 7 +++++ 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index e4df0b2072ce..0a27c4b7c729 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -124,14 +124,6 @@ SnapshotStore.prototype = { this._filename = value + ".json"; }, - __dirSvc: null, - get _dirSvc() { - if (!this.__dirSvc) - this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - return this.__dirSvc; - }, - // Last synced tree, version, and GUID (to detect if the store has // been completely replaced and invalidate the snapshot) @@ -193,14 +185,10 @@ SnapshotStore.prototype = { save: function SStore_save() { this._log.info("Saving snapshot to disk"); - let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.QueryInterface(Ci.nsILocalFile); - - file.append("weave"); - file.append("snapshots"); - file.append(this.filename); - if (!file.exists()) - file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); + let file = Utils.getProfileFile( + {path: "weave/snapshots/" + this.filename, + autoCreate: true} + ); let out = {version: this.version, GUID: this.GUID, @@ -213,11 +201,7 @@ SnapshotStore.prototype = { }, load: function SStore_load() { - let file = this._dirSvc.get("ProfD", Ci.nsIFile); - file.append("weave"); - file.append("snapshots"); - file.append(this.filename); - + let file = Utils.getProfileFile("weave/snapshots/" + this.filename); if (!file.exists()) return; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 8361da440a1b..c5a021c20522 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -50,6 +50,36 @@ Cu.import("resource://weave/log4moz.js"); */ let Utils = { + // Returns a nsILocalFile representing a file relative to the + // current user's profile directory. If the argument is a string, + // it should be a string with unix-style slashes for directory names + // (these slashes are automatically converted to platform-specific + // path separators). + // + // Alternatively, if the argument is an object, it should contain + // the following attributes: + // + // path: the path to the file, relative to the current user's + // profile dir. + // + // autoCreate: whether or not the file should be created if it + // doesn't already exist. + getProfileFile: function getProfileFile(arg) { + if (typeof arg == "string") + arg = {path: arg}; + + let pathParts = arg.path.split("/"); + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + let file = dirSvc.get("ProfD", Ci.nsIFile); + file.QueryInterface(Ci.nsILocalFile); + for (let i = 0; i < pathParts.length; i++) + file.append(pathParts[i]); + if (arg.autoCreate && !file.exists()) + file.create(file.NORMAL_FILE_TYPE, PERMS_FILE); + return file; + }, + getLoginManager: function getLoginManager() { return Cc["@mozilla.org/login-manager;1"]. getService(Ci.nsILoginManager); diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index bd1f69ef65e3..4e411d5af01a 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -20,6 +20,10 @@ function run_test() { return {getAllLogins: function() { return [fakeUser]; }}; }; + Utils.getProfileFile = function fake_getProfileFile(arg) { + return {exists: function() {return false;}}; + }; + // Ensure that _hashLoginInfo() works. var fakeUserHash = passwords._hashLoginInfo(fakeUser); do_check_eq(typeof fakeUserHash, 'string'); @@ -29,4 +33,7 @@ function run_test() { var psc = new passwords.PasswordSyncCore(); do_check_false(psc._itemExists("invalid guid")); do_check_true(psc._itemExists(fakeUserHash)); + + var engine = new passwords.PasswordEngine(); + } From e9e01bbf6224197bc25d89d444d468ae4e15f750 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 14:50:02 -0700 Subject: [PATCH 0385/1860] XMPPClient sends its debugging output to log4moz (mostly debug level) now instead of dump, so it's less annoyingly verbose. --- .../sync/modules/xmpp/authenticationLayer.js | 5 ---- services/sync/modules/xmpp/transportLayer.js | 19 +++++++-------- services/sync/modules/xmpp/xmppClient.js | 24 ++++++++----------- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js index 990592463275..13e3c79aefd6 100644 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ b/services/sync/modules/xmpp/authenticationLayer.js @@ -1,9 +1,5 @@ const EXPORTED_SYMBOLS = [ "PlainAuthenticator", "Md5DigestAuthenticator" ]; -function LOG(aMsg) { - dump("Weave::AuthenticationLayer: " + aMsg + "\n"); -} - if (typeof(atob) == 'undefined') { // This code was written by Tyler Akins and has been placed in the // public domain. It would be nice if you left this header intact. @@ -367,7 +363,6 @@ PlainAuthenticator.prototype = { this._jid = jidNodes[0].firstChild.nodeValue; // TODO: Does the client need to do anything special with its new // "client@host.com/resourceID" full JID? - LOG( "JID set to " + this._jid ); // If we still need to do session, then we're not done yet: if ( this._needSession ) { diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index 55e83b7c332f..34f387a41caa 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -3,10 +3,6 @@ const EXPORTED_SYMBOLS = ['HTTPPollingTransport']; var Cc = Components.classes; var Ci = Components.interfaces; -function LOG(aMsg) { - dump("Weave::Transport-HTTP-Poll: " + aMsg + "\n"); -} - /* The interface that should be implemented by any Transport object: @@ -163,7 +159,8 @@ function HTTPPollingTransport( serverUrl, useKeys, interval ) { } HTTPPollingTransport.prototype = { _init: function( serverUrl, useKeys, interval ) { - LOG("Initializing transport: serverUrl=" + serverUrl + ", useKeys=" + useKeys + ", interval=" + interval); + this._log = Log4Moz.Service.getLogger("Service.XmppTransportLayer"); + this._log.info("Initializing transport: serverUrl=" + serverUrl + ", useKeys=" + useKeys + ", interval=" + interval); this._serverUrl = serverUrl this._n = 0; this._key = this._makeSeed(); @@ -277,7 +274,7 @@ HTTPPollingTransport.prototype = { if ( request.status == 200) { // 200 means success. - LOG("Server says: " + request.responseText); + self._log.debug("Server says: " + request.responseText); // Look for a set-cookie header: var latestCookie = request.getResponseHeader( "Set-Cookie" ); if ( latestCookie.length > 0 ) { @@ -289,7 +286,7 @@ HTTPPollingTransport.prototype = { callbackObj.onIncomingData( request.responseText ); } } else { - LOG( "Error! Got HTTP status code " + request.status ); + self._log.error( "Got HTTP status code " + request.status ); if ( request.status == 0 ) { /* Sometimes the server gives us HTTP status code 0 in response to an attempt to POST. I'm not sure why this happens, but @@ -320,11 +317,11 @@ HTTPPollingTransport.prototype = { request.setRequestHeader( "Content-length", contents.length ); request.setRequestHeader( "Connection", "close" ); request.onreadystatechange = _processReqChange; - LOG("Sending: " + contents); + this._log.debug("Sending: " + contents); request.send( contents ); } catch(ex) { this._onError("Unable to send message to server: " + this._serverUrl); - LOG("Connection failure: " + ex); + this._log.error("Connection failure: " + ex); } }, @@ -371,10 +368,10 @@ HTTPPollingTransport.prototype = { testKeys: function () { this._key = "foo"; - LOG(this._key); + this._log.debug(this._key); for ( var x = 1; x < 7; x++ ) { this._advanceKey(); - LOG(this._key); + this._log.debug(this._key); } } }; diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 782abedc8238..a16d9ea6f8d0 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -14,10 +14,7 @@ var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; -function LOG(aMsg) { - dump("Weave::XMPPClient: " + aMsg + "\n"); -} - +Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/xmpp/transportLayer.js"); Cu.import("resource://weave/xmpp/authenticationLayer.js"); @@ -38,6 +35,7 @@ XmppClient.prototype = { IQ_ERROR: -1, _init: function( clientName, realm, clientPassword, transport, authenticator ) { + this._log = Log4Moz.Service.getLogger("Service.XmppClient"); this._myName = clientName; this._realm = realm; this._fullName = clientName + "@" + realm; @@ -47,7 +45,7 @@ XmppClient.prototype = { this._streamOpen = false; this._transportLayer = transport; this._authenticationLayer = authenticator; - LOG("initialized auth with clientName=" + clientName + ", realm=" + realm + ", pw=" + clientPassword); + this._log.debug("initialized auth with clientName=" + clientName + ", realm=" + realm + ", pw=" + clientPassword); this._authenticationLayer.initialize( clientName, realm, clientPassword ); this._messageHandlers = []; this._iqResponders = []; @@ -71,9 +69,8 @@ XmppClient.prototype = { }, parseError: function( streamErrorNode ) { - LOG( "Uh-oh, there was an error!" ); var error = streamErrorNode.childNodes[0]; - LOG( "Name: " + error.nodeName + " Value: " + error.nodeValue ); + this._log.error( "Name: " + error.nodeName + " Value: " + error.nodeValue ); this._error = error.nodeName; this.disconnect(); /* Note there can be an optional bla bla node inside @@ -89,14 +86,14 @@ XmppClient.prototype = { }, setError: function( errorText ) { - LOG( "Error: " + errorText ); + this._log.error( errorText ); this._error = errorText; this._connectionStatus = this.FAILED; this._finishConnectionAttempt(); }, onIncomingData: function( messageText ) { - LOG("onIncomingData(): rcvd: " + messageText); + this._log.debug("onIncomingData(): rcvd: " + messageText); var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); // Handle server disconnection @@ -171,7 +168,7 @@ XmppClient.prototype = { if (presences.length > 0 ) { var from = presences[0].getAttribute( "from" ); if ( from != undefined ) { - LOG( "I see that " + from + " is online." ); + this._log.debug( "I see that " + from + " is online." ); } } @@ -200,14 +197,14 @@ XmppClient.prototype = { }, processIncomingMessage: function( messageElem ) { - LOG( "in processIncomingMessage: messageElem is a " + messageElem ); + this._log.debug("processIncomingMsg: messageElem is a " + messageElem); var from = messageElem.getAttribute( "from" ); var contentElem = messageElem.firstChild; // Go down till we find the element with nodeType = 3 (TEXT_NODE) while ( contentElem.nodeType != 3 ) { contentElem = contentElem.firstChild; } - LOG( "Incoming message to you from " + from + ":" + contentElem.nodeValue ); + this._log.debug("Incoming msg from " + from + ":" + contentElem.nodeValue); for ( var x in this._messageHandlers ) { // TODO do messages have standard place for metadata? // will want to have handlers that trigger only on certain metadata. @@ -333,8 +330,7 @@ XmppClient.prototype = { var msgXml = "" + messageText + ""; - LOG( "Message xml: " ); - LOG( msgXml ); + this._log.debug( "Outgoing Message xml: " + msgXml ); return msgXml; }, From 894d727afac30238619063d5d34b063db68bb53c Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 14:59:06 -0700 Subject: [PATCH 0386/1860] Imported Log4Moz to xmpp/transportLayer.js and xmpp/authenticationLayer.js, so they're no longer raising errors about Log4Moz being undefined (oops, sorry, should have tested before pushing.) --- services/sync/modules/xmpp/authenticationLayer.js | 6 ++++++ services/sync/modules/xmpp/transportLayer.js | 3 +++ 2 files changed, 9 insertions(+) diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js index 13e3c79aefd6..5a7cacb42ff0 100644 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ b/services/sync/modules/xmpp/authenticationLayer.js @@ -1,5 +1,11 @@ const EXPORTED_SYMBOLS = [ "PlainAuthenticator", "Md5DigestAuthenticator" ]; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); + if (typeof(atob) == 'undefined') { // This code was written by Tyler Akins and has been placed in the // public domain. It would be nice if you left this header intact. diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index 34f387a41caa..691136264c85 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -2,6 +2,9 @@ const EXPORTED_SYMBOLS = ['HTTPPollingTransport']; var Cc = Components.classes; var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); /* The interface that should be implemented by any Transport object: From b375a60ff9cfe2dd8cc727707945a41b16827c20 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 16:11:15 -0700 Subject: [PATCH 0387/1860] Added a new property to async.js, Async.outstandingGenerators, which returns the number of generators that haven't yet been finalized. This can be used for diagnostic purposes to determine whether generators haven't yet been called back. --- services/sync/modules/async.js | 8 ++++++-- services/sync/tests/unit/test_async.js | 4 ++++ services/sync/tests/unit/test_async_exceptions.js | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 36258a08a08b..acb27647cf6b 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -48,7 +48,8 @@ Cu.import("resource://weave/util.js"); * Asynchronous generator helpers */ -let currentId = 0; +let gCurrentId = 0; +let gOutstandingGenerators = 0; function AsyncException(initFrame, message) { this.message = message; @@ -71,13 +72,14 @@ AsyncException.prototype = { }; function Generator(thisArg, method, onComplete, args) { + gOutstandingGenerators++; this._outstandingCbs = 0; this._log = Log4Moz.Service.getLogger("Async.Generator"); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; this._thisArg = thisArg; this._method = method; - this._id = currentId++; + this._id = gCurrentId++; this.onComplete = onComplete; this._args = args; this._initFrame = Components.stack.caller; @@ -251,6 +253,7 @@ Generator.prototype = { this._log.trace("Initial stack trace:\n" + this.trace); } } + gOutstandingGenerators--; } }; @@ -280,6 +283,7 @@ function trace(frame, str) { Async = { + get outstandingGenerators() { return gOutstandingGenerators; }, // Use: // let gen = Async.run(this, this.fooGen, ...); diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js index 746033ac4754..7f68c710a448 100644 --- a/services/sync/tests/unit/test_async.js +++ b/services/sync/tests/unit/test_async.js @@ -40,7 +40,11 @@ function run_test() { do_check_eq(timesYielded, 2); + do_check_eq(Async.outstandingGenerators, 1); + do_check_true(fts.processCallback()); do_check_false(fts.processCallback()); + + do_check_eq(Async.outstandingGenerators, 0); } diff --git a/services/sync/tests/unit/test_async_exceptions.js b/services/sync/tests/unit/test_async_exceptions.js index b15a2e8dd666..3fa5c0740f0a 100644 --- a/services/sync/tests/unit/test_async_exceptions.js +++ b/services/sync/tests/unit/test_async_exceptions.js @@ -47,4 +47,5 @@ function run_test() { runTestGenerator.async({}); for (var i = 0; fts.processCallback(); i++) {} do_check_eq(i, 4); + do_check_eq(Async.outstandingGenerators, 0); } From cae112deb455d6ef83580c8f93e37a9c22e21b37 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 16:51:54 -0700 Subject: [PATCH 0388/1860] Refactoring; moved code out of test_service.js and into unit/head.js for use by other tests. --- services/sync/tests/unit/head_first.js | 69 ++++++++++++++++++++ services/sync/tests/unit/test_service.js | 83 +++++------------------- 2 files changed, 87 insertions(+), 65 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 7af151bfd8f4..d0f22fbed0f6 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -114,3 +114,72 @@ function makeAsyncTestRunner(generator) { return run_test; } + +function FakePrefService(contents) { + Cu.import("resource://weave/util.js"); + this.fakeContents = contents; + Utils.__prefs = this; +} + +FakePrefService.prototype = { + _getPref: function fake__getPref(pref) { + Log4Moz.Service.rootLogger.trace("Getting pref: " + pref); + return this.fakeContents[pref]; + }, + getCharPref: function fake_getCharPref(pref) { + return this._getPref(pref); + }, + getBoolPref: function fake_getBoolPref(pref) { + return this._getPref(pref); + }, + getIntPref: function fake_getIntPref(pref) { + return this._getPref(pref); + }, + addObserver: function fake_addObserver() {} +}; + +function makeFakeAsyncFunc(retval) { + Cu.import("resource://weave/async.js"); + Function.prototype.async = Async.sugar; + + function fakeAsyncFunc() { + let self = yield; + + Utils.makeTimerForCall(self.cb); + yield; + + self.done(retval); + } + + return fakeAsyncFunc; +} + +function FakeDAVService(contents) { + Cu.import("resource://weave/dav.js"); + + this.fakeContents = contents; + DAV.__proto__ = this; + this.checkLogin = makeFakeAsyncFunc(true); +} + +FakeDAVService.prototype = { + GET: function fake_GET(path, onComplete) { + Log4Moz.Service.rootLogger.info("Retrieving " + path); + var result = {status: 404}; + if (path in this.fakeContents) + result = {status: 200, responseText: this.fakeContents[path]}; + + return makeFakeAsyncFunc(result).async(this, onComplete); + } +}; + +function FakePasswordService(contents) { + Cu.import("resource://weave/util.js"); + + this.fakeContents = contents; + let self = this; + + Utils.findPassword = function fake_findPassword(realm, username) { + return self.fakeContents[realm][username]; + }; +}; diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index 7cff9846d68f..42f7cb63887b 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -1,59 +1,24 @@ Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/crypto.js"); -Cu.import("resource://weave/identity.js"); -Function.prototype.async = Async.sugar; - -function makeFakeAsyncFunc(retval) { - function fakeAsyncFunc() { - let self = yield; - - Utils.makeTimerForCall(self.cb); - yield; - - self.done(retval); - } - - return fakeAsyncFunc; -} - -function FakePrefs() {} - -FakePrefs.prototype = { - __contents: {"log.logger.async" : "Debug", - "username" : "foo", - "serverURL" : "https://example.com/", - "encryption" : true, - "enabled" : true, - "schedule" : 0}, - _getPref: function fake__getPref(pref) { - Log4Moz.Service.rootLogger.trace("Getting pref: " + pref); - return this.__contents[pref]; - }, - getCharPref: function fake_getCharPref(pref) { - return this._getPref(pref); - }, - getBoolPref: function fake_getBoolPref(pref) { - return this._getPref(pref); - }, - getIntPref: function fake_getIntPref(pref) { - return this._getPref(pref); - }, - addObserver: function fake_addObserver() {} +let __fakePrefs = { + "log.logger.async" : "Debug", + "username" : "foo", + "serverURL" : "https://example.com/", + "encryption" : true, + "enabled" : true, + "schedule" : 0 }; -Utils.__prefs = new FakePrefs(); +let __fakeDAVContents = { + "meta/version" : "2", + "private/privkey" : "fake private key" +}; -Utils.findPassword = function fake_findPassword(realm, username) { - let contents = { - 'Mozilla Services Password': {foo: "bar"}, - 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} - }; - return contents[realm][username]; +let __fakePasswords = { + 'Mozilla Services Password': {foo: "bar"}, + 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} }; Crypto.__proto__ = { @@ -68,22 +33,6 @@ Crypto.__proto__ = { } }; -DAV.__proto__ = { - checkLogin: makeFakeAsyncFunc(true), - - __contents: {"meta/version" : "2", - "private/privkey" : "fake private key"}, - - GET: function fake_GET(path, onComplete) { - Log4Moz.Service.rootLogger.info("Retrieving " + path); - var result = {status: 404}; - if (path in this.__contents) - result = {status: 200, responseText: this.__contents[path]}; - - return makeFakeAsyncFunc(result).async(this, onComplete); - } -}; - let Service = loadInSandbox("resource://weave/service.js"); function TestService() { @@ -99,6 +48,9 @@ TestService.prototype = { TestService.prototype.__proto__ = Service.WeaveSvc.prototype; function test_login_works() { + var fds = new FakeDAVService(__fakeDAVContents); + var fprefs = new FakePrefService(__fakePrefs); + var fpasses = new FakePasswordService(__fakePasswords); var fts = new FakeTimerService(); var logStats = initTestLogging(); var testService = new TestService(); @@ -116,4 +68,5 @@ function test_login_works() { do_check_true(finished); do_check_true(successful); do_check_eq(logStats.errorsLogged, 0); + do_check_eq(Async.outstandingGenerators, 0); } From ee7d0a7e79db58266669cb292a15b5775aab9fcb Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 17:23:04 -0700 Subject: [PATCH 0389/1860] Turned all remaining dump()s in transportLayer.js into Log4Moz debug calls. --- services/sync/modules/xmpp/transportLayer.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index 691136264c85..4df4cd5feefd 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -223,7 +223,6 @@ HTTPPollingTransport.prototype = { _setIdFromCookie: function( self, cookie ) { // parse connection ID out of the cookie: - // dump( "Cookie is " + cookie + "\n" ); var cookieSegments = cookie.split( ";" ); cookieSegments = cookieSegments[0].split( "=" ); var newConnectionId = cookieSegments[1]; @@ -242,13 +241,13 @@ HTTPPollingTransport.prototype = { break; default : self._connectionId = cookieSegments[1]; - // dump( "Connection ID set to " + self._connectionId + "\n" ); + this._log.debug("Connection ID set to " + self._connectionId); break; } }, _onError: function( errorText ) { - dump( "Transport error: " + errorText + "\n" ); + this._log.error( errorText ); if ( this._callbackObject != null ) { this._callbackObject.onTransportError( errorText ); } From 20c587b79d149046dc60d77e703777e9ce92e92b Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 17:25:46 -0700 Subject: [PATCH 0390/1860] Set the default preference for weave's xmpp server-url to be the new secure url for the ejabberd server on sm-labs01. --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9941d41c9d6a..3c12de390b92 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -34,7 +34,7 @@ pref("extensions.weave.log.logger.service.main", "Trace"); pref("extensions.weave.xmpp.enabled", true); pref("extensions.weave.xmpp.server.url", - "http://sm-labs01.mozilla.org:5280/http-poll"); + "https://sm-labs01.mozilla.org:81/xmpp"); pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); pref("extensions.weave.xmpp.client.name", ""); pref("extensions.weave.xmpp.client.password", ""); From 821c0dd89f51376761dbdcbc1b67a48e618b6bee Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 17:28:28 -0700 Subject: [PATCH 0391/1860] test_passwords.js now performs a fake sync, but I ran into an issue that may be a bug in remote.js. --- services/sync/tests/unit/head_first.js | 17 ++++++++++++++++- services/sync/tests/unit/test_passwords.js | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index d0f22fbed0f6..616ff0246ec5 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -163,6 +163,11 @@ function FakeDAVService(contents) { } FakeDAVService.prototype = { + PUT: function fake_PUT(path, data, onComplete) { + this.fakeContents[path] = data; + makeFakeAsyncFunc({status: 200}).async(this, onComplete); + }, + GET: function fake_GET(path, onComplete) { Log4Moz.Service.rootLogger.info("Retrieving " + path); var result = {status: 404}; @@ -170,6 +175,11 @@ FakeDAVService.prototype = { result = {status: 200, responseText: this.fakeContents[path]}; return makeFakeAsyncFunc(result).async(this, onComplete); + }, + + MKCOL: function fake_MKCOL(path, onComplete) { + Log4Moz.Service.rootLogger.info("Creating dir " + path); + makeFakeAsyncFunc(true).async(this, onComplete); } }; @@ -180,6 +190,11 @@ function FakePasswordService(contents) { let self = this; Utils.findPassword = function fake_findPassword(realm, username) { - return self.fakeContents[realm][username]; + Log4Moz.Service.rootLogger.trace("Password requested for " + + realm + ":" + username); + if (realm in self.fakeContents && username in self.fakeContents[realm]) + return self.fakeContents[realm][username]; + else + return null; }; }; diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 4e411d5af01a..0ff0df85380b 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -1,6 +1,22 @@ Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/dav.js"); +Cu.import("resource://weave/identity.js"); + +let __fakePasswords = { + 'Mozilla Services Password': {foo: "bar"}, + 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} +}; function run_test() { + var fpasses = new FakePasswordService(__fakePasswords); + var fprefs = new FakePrefService({"encryption" : "none"}); + var fds = new FakeDAVService({}); + var fts = new FakeTimerService(); + var logStats = initTestLogging(); + + ID.set('Engine:PBE:default', new Identity('Mozilla Services Encryption Passphrase', 'foo')); + // The JS module we're testing, with all members exposed. var passwords = loadInSandbox("resource://weave/engines/passwords.js"); @@ -34,6 +50,11 @@ function run_test() { do_check_false(psc._itemExists("invalid guid")); do_check_true(psc._itemExists(fakeUserHash)); + // Make sure the engine can sync. var engine = new passwords.PasswordEngine(); + engine.sync(); + while (fts.processCallback()) {} + do_check_eq(logStats.errorsLogged, 0); + do_check_eq(Async.outstandingGenerators, 0); } From 5f3833014f9163bfaa6ef23f48bf5962f78421af Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 17:33:44 -0700 Subject: [PATCH 0392/1860] Fixed a preference typo in test_service.js. --- services/sync/tests/unit/test_service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index 42f7cb63887b..412c8d8c52e7 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -6,7 +6,7 @@ let __fakePrefs = { "log.logger.async" : "Debug", "username" : "foo", "serverURL" : "https://example.com/", - "encryption" : true, + "encryption" : "aes-256-cbc", "enabled" : true, "schedule" : 0 }; From ad28b53f4933e7f1410e11b95ef21ffdc0080d01 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 18:04:01 -0700 Subject: [PATCH 0393/1860] Replaced boilerplate XPCOM code with calls to util.js functions. --- services/sync/modules/crypto.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 44ae609ef374..e0d34e4c3651 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -66,16 +66,11 @@ CryptoSvc.prototype = { }, get defaultAlgorithm() { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - return branch.getCharPref("extensions.weave.encryption"); + return Utils.prefs.getCharPref("encryption"); }, set defaultAlgorithm(value) { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - let cur = branch.getCharPref("extensions.weave.encryption"); - if (value != cur) - branch.setCharPref("extensions.weave.encryption", value); + if (value != Utils.prefs.getCharPref("encryption")) + Utils.prefs.setCharPref("encryption", value); }, _init: function Crypto__init() { From 777d7bc6b1df9f7e5e6e514d2421dcb15cc1806a Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 18 Jun 2008 18:08:33 -0700 Subject: [PATCH 0394/1860] Finished test_passwords.js, although it raises a number of strict warnings. --- services/sync/tests/unit/head_first.js | 1 + services/sync/tests/unit/test_passwords.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 616ff0246ec5..7dce5359baa5 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -164,6 +164,7 @@ function FakeDAVService(contents) { FakeDAVService.prototype = { PUT: function fake_PUT(path, data, onComplete) { + Log4Moz.Service.rootLogger.info("Putting " + path); this.fakeContents[path] = data; makeFakeAsyncFunc({status: 200}).async(this, onComplete); }, diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 0ff0df85380b..270489035953 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -15,7 +15,8 @@ function run_test() { var fts = new FakeTimerService(); var logStats = initTestLogging(); - ID.set('Engine:PBE:default', new Identity('Mozilla Services Encryption Passphrase', 'foo')); + ID.set('Engine:PBE:default', + new Identity('Mozilla Services Encryption Passphrase', 'foo')); // The JS module we're testing, with all members exposed. var passwords = loadInSandbox("resource://weave/engines/passwords.js"); @@ -40,6 +41,14 @@ function run_test() { return {exists: function() {return false;}}; }; + Utils.open = function fake_open(file, mode) { + let fakeStream = { + writeString: function(data) {Log4Moz.Service.rootLogger.debug(data);}, + close: function() {} + }; + return [fakeStream]; + }; + // Ensure that _hashLoginInfo() works. var fakeUserHash = passwords._hashLoginInfo(fakeUser); do_check_eq(typeof fakeUserHash, 'string'); From 2c5edffb61cf143fc4e6b8d3babe9a840afa86fd Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 18:48:17 -0700 Subject: [PATCH 0395/1860] Removed an unneeded yield statement from startXmppClient (this was raising warnings about yeilding without a callback.) --- services/sync/modules/engines/bookmarks.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 8638d3ce7066..a4f6ef27a091 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -166,7 +166,6 @@ BookmarksEngine.prototype = { } else if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { this._log.info( "Weave logged into xmpp OK." ); } - yield; self.done(); }, From ec66f1dbf998c97c6b2e984b39c82f535ea808c2 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 18 Jun 2008 19:35:23 -0700 Subject: [PATCH 0396/1860] bug 439553: add a 'change password' form to the weave preferences --- services/sync/locales/en-US/preferences.dtd | 7 +++++++ services/sync/locales/en-US/preferences.properties | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 217d3ccdc75f..7354cc3f5bf6 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -14,6 +14,13 @@ + + + + + + + diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index d5f95c0006ff..596a1fea8021 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -7,4 +7,12 @@ reset.client.warning = This will permanently delete all of your bookmarks and br reset.login.warning.title = Reset Login reset.login.warning = This will permanently remove your username, password and passphrase from this instance of Firefox.\n\nAre you sure you want to do this? reset.lock.warning.title = Reset Server Lock -reset.lock.warning = This will reset the write lock on the Weave server for your account. To avoid corruption, you should only do this if a lock has become stale.\n\nAre you sure you want to do this? \ No newline at end of file +reset.lock.warning = This will reset the write lock on the Weave server for your account. To avoid corruption, you should only do this if a lock has become stale.\n\nAre you sure you want to do this? + +change.password.status.active = Changing your password... +change.password.status.success = Your password has been changed. +change.password.status.error = There was an error changing your password. +change.password.status.passwordsDoNotMatch = The passwords you entered do not match. +change.password.status.noOldPassword = You didn't enter your current password. +change.password.status.noNewPassword = You didn't enter a new password. +change.password.status.badOldPassword = Your current password is incorrect. From 35c2bb9ca60a457c1a6eb35dc48f41dab5d157e5 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 20:45:01 -0700 Subject: [PATCH 0397/1860] Implemented _updateAllOutgoingShares in bookmarkEngine. --- services/sync/modules/engines/bookmarks.js | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index a4f6ef27a091..f0dc089a307d 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -190,7 +190,6 @@ BookmarksEngine.prototype = { _incomingShareWithdrawn: function BmkEngine__incomingShareStop(user, serverPath, folderName) { - /* Called when we receive a message telling us that a user who has already shared a directory with us has chosen to stop sharing the directory. @@ -226,10 +225,8 @@ BookmarksEngine.prototype = { the UI. */ // Create the outgoing share folder on the server - dump( "About to call _createOutgoingShare asynchronously.\n" ); this._createOutgoingShare.async( this, self.cb, selectedFolder, username ); let serverPath = yield; - dump( "Done calling _createOutgoingShare asynchronously.\n" ); this._updateOutgoingShare.async( this, self.cb, selectedFolder ); yield; @@ -239,8 +236,6 @@ BookmarksEngine.prototype = { let folderName = selectedFolder.getAttribute( "label" ); ans.setItemAnnotation(folderItemId, OUTGOING_SHARED_ANNO, username, 0, ans.EXPIRE_NEVER); - // TODO: does this clobber existing annotations? - dump( "I set the annotation...\n" ); // Send an xmpp message to the share-ee if ( this._xmppClient ) { if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { @@ -256,7 +251,7 @@ BookmarksEngine.prototype = { with many people, the value of the annotation can be a whole list of usernames instead of just one. */ - dump( "Bookmark engine shared " +folderName + " with " + username + "\n" ); + this._log.info("Shared " + folderName +" with " + username); ret = true; self.done( true ); }, @@ -275,7 +270,7 @@ BookmarksEngine.prototype = { let self = yield; let mounts = this._store.findIncomingShares(); - for (i = 0; i < mounts.length; i++) { + for (let i = 0; i < mounts.length; i++) { try { this._updateIncomingShare.async(this, self.cb, mounts[i]); yield; @@ -287,16 +282,21 @@ BookmarksEngine.prototype = { }, updateAllOutgoingShares: function BmkEngine_updateAllOutgoing(onComplete) { - // TODO implement me. - // Pseudocode: - // let shares = findFoldersWithAnnotation( OUTGOING_SHARE_ANNO ); - // for ( let share in shares ) { - // if ( share.hasChanged() ) { - // this._updateOutgoingShare( share ); - // } - // } - // Not sure how to implement share.hasChanged(). See if there's a - // corresponding entry in the latest diff? + this._updateAllOutgoingShares.async(this, onComplete); + }, + _updateAllOutgoingShares: function BmkEngine__updateAllOutgoing() { + let self = yield; + let ans = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + let shares = ans.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, {}); + for ( let i=0; i < shares.length; i++ ) { + /* TODO only update the shares that have changed. Perhaps we can + do this by checking whether there's a corresponding entry in the + diff produced by the latest sync. */ + this._updateOutgoingShare.async(this, self.cb, shares[i]); + yield; + } + self.done(); }, _createOutgoingShare: function BmkEngine__createOutgoing(folder, username) { @@ -375,7 +375,7 @@ BookmarksEngine.prototype = { /* Puts all the bookmark data from the specified bookmark folder, encrypted, onto the shared directory on the server (replacing anything that was already there). - + To be called asynchronously. TODO: error handling*/ let self = yield; let myUserName = ID.get('WeaveID').username; @@ -390,7 +390,7 @@ BookmarksEngine.prototype = { keyringFile.get(self.cb); let keyring = yield; let symKey = keyring[ myUserName ]; - // Get the + // Get the json-wrapped contents of everything in the folder: let json = this._store._wrapMount( folderNode, myUserName ); /* TODO what does wrapMount do with this username? Should I be passing in my own or that of the person I share with? */ From 83ddc593f4244e9f4346b546f5b3a16fc74da944 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 20:53:11 -0700 Subject: [PATCH 0398/1860] Gave the bookmarksEngine a permanent (lazy-initialized) reference to the annotation service, rather than recreating the reference every single time we use the service. Which is a lot of times now. --- services/sync/modules/engines/bookmarks.js | 62 +++++++++++++--------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f0dc089a307d..bf4a953a80fc 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -95,6 +95,14 @@ BookmarksEngine.prototype = { this.__tracker = new BookmarksTracker(); return this.__tracker; }, + + __annoSvc: null, + get _annoSvc() { + if (!this.__anoSvc) + this.__annoSvc = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + return this.__annoSvc; + } _init: function BmkEngine__init( pbeId ) { this.__proto__.__proto__._init.call( this, pbeId ); @@ -216,8 +224,6 @@ BookmarksEngine.prototype = { _share: function BmkEngine__share( selectedFolder, username ) { // Return true if success, false if failure. let ret = false; - let ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); let self = yield; /* TODO What should the behavior be if i'm already sharing it with user @@ -234,8 +240,11 @@ BookmarksEngine.prototype = { it's an outgoing share: */ let folderItemId = selectedFolder.node.itemId; let folderName = selectedFolder.getAttribute( "label" ); - ans.setItemAnnotation(folderItemId, OUTGOING_SHARED_ANNO, username, 0, - ans.EXPIRE_NEVER); + this._annoSvc.setItemAnnotation(folderItemId, + OUTGOING_SHARED_ANNO, + username, + 0, + this._annoSvc.EXPIRE_NEVER); // Send an xmpp message to the share-ee if ( this._xmppClient ) { if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { @@ -286,9 +295,8 @@ BookmarksEngine.prototype = { }, _updateAllOutgoingShares: function BmkEngine__updateAllOutgoing() { let self = yield; - let ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - let shares = ans.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, {}); + let shares = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, + {}); for ( let i=0; i < shares.length; i++ ) { /* TODO only update the shares that have changed. Perhaps we can do this by checking whether there's a corresponding entry in the @@ -329,13 +337,11 @@ BookmarksEngine.prototype = { /* Store the path to the server directory in an annotation on the shared bookmark folder, so we can easily get back to it later. */ - let ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - ans.setItemAnnotation(folder.node.itemId, - SERVER_PATH_ANNO, - serverPath, - 0, - ans.EXPIRE_NEVER); + this._annoSvc.setItemAnnotation(folder.node.itemId, + SERVER_PATH_ANNO, + serverPath, + 0, + this._annoSvc.EXPIRE_NEVER); // Create a new symmetric key, to be used only for encrypting this share. Crypto.PBEkeygen.async(Crypto, self.cb); @@ -381,9 +387,8 @@ BookmarksEngine.prototype = { let myUserName = ID.get('WeaveID').username; // The folder has an annotation specifying the server path to the // directory: - let ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - let serverPath = ans.getItemAnnotation(folderNode, SERVER_PATH_ANNO); + let serverPath = this._annoSvc.getItemAnnotation(folderNode, + SERVER_PATH_ANNO); // From that directory, get the keyring file, and from it, the symmetric // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); @@ -423,25 +428,27 @@ BookmarksEngine.prototype = { */ let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); - let ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); let root; - let a = ans.getItemsWithAnnotation("weave/mounted-shares-folder", {}); + let a = this._annoSvc.getItemsWithAnnotation("weave/mounted-shares-folder", + {}); if (a.length == 1) root = a[0]; if (!root) { root = bms.createFolder(bms.toolbarFolder, "Shared Folders", bms.DEFAULT_INDEX); - ans.setItemAnnotation(root, "weave/mounted-shares-folder", true, 0, - ans.EXPIRE_NEVER); + this._annoSvc.setItemAnnotation(root, + "weave/mounted-shares-folder", + true, + 0, + this._annoSvc.EXPIRE_NEVER); } let item; - a = ans.getItemsWithAnnotation("weave/mounted-share-id", {}); + a = this._annoSvc.getItemsWithAnnotation("weave/mounted-share-id", {}); for (let i = 0; i < a.length; i++) { - if (ans.getItemAnnotation(a[i], "weave/mounted-share-id") == id) { + if (this._annoSvc.getItemAnnotation(a[i], "weave/mounted-share-id")==id){ item = a[i]; break; } @@ -449,8 +456,11 @@ BookmarksEngine.prototype = { if (!item) { let newId = bms.createFolder(root, title, bms.DEFAULT_INDEX); - ans.setItemAnnotation(newId, "weave/mounted-share-id", id, 0, - ans.EXPIRE_NEVER); + this._annoSvc.setItemAnnotation(newId, + "weave/mounted-share-id", + id, + 0, + this._annoSvc.EXPIRE_NEVER); } }, From 1b213535e0a462746ae615737e64d30bce052f50 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Wed, 18 Jun 2008 21:42:16 -0700 Subject: [PATCH 0399/1860] Checkin of NSS-ized WeaveCrypto module (not yet used) --- services/crypto/IWeaveCrypto.idl | 148 ++++- services/crypto/WeaveCrypto.cpp | 940 +++++++++++++++++++++++++++---- services/crypto/WeaveCrypto.h | 30 +- 3 files changed, 969 insertions(+), 149 deletions(-) diff --git a/services/crypto/IWeaveCrypto.idl b/services/crypto/IWeaveCrypto.idl index e57518b15525..4c4f5da1cdd4 100644 --- a/services/crypto/IWeaveCrypto.idl +++ b/services/crypto/IWeaveCrypto.idl @@ -22,6 +22,7 @@ * Contributor(s): * Dan Mills (original author) * Honza Bambas + * Justin Dolske * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -48,46 +49,135 @@ interface IWeaveCrypto : nsISupports */ const unsigned long DES_EDE3_CBC = 156; - - // Unsupported now... - const unsigned long AES_128_ECB = 183; - const unsigned long AES_128_CBC = 184; - const unsigned long AES_192_ECB = 185; - const unsigned long AES_192_CBC = 186; - const unsigned long AES_256_ECB = 187; - const unsigned long AES_256_CBC = 188; + const unsigned long AES_128_CBC = 184; + const unsigned long AES_192_CBC = 186; + const unsigned long AES_256_CBC = 188; /** - * One of the above constants. Assigning differnt value - * will fail cause the encrypt method fail. - * Default value is DES_EDE3_CBC. + * One of the above constants. Used as the mechanism for encrypting bulk + * data and wrapping keys. + * + * Default is AES_256_CBC. */ attribute unsigned long algorithm; /** - * Encrypt data using a passphrase - * See algorithm attribute, it determines which mechanisms will be used + * The size of the RSA key to create with generateKeypair(). * - * @param pass - * Passphrase to encrypt with - * @param iter - * Number of iterations for key strengthening - * This parameter is currently ignored and is always 1 - * @param clearText - * Data to be encrypted - * @returns Encrypted data, base64 encoded + * Default is 2048. */ - ACString encrypt(in ACString pass, in ACString clearText); + attribute unsigned long keypairBits; /** - * Decrypt data using a passphrase + * Encrypt data using a symmetric key. + * The algorithm attribute specifies how the encryption is performed. * - * @param pass - * Passphrase to decrypt with - * @param cipherText - * Base64 encoded data to be decrypted - * @returns Decrypted data + * @param clearText + * The data to be encrypted (not base64 encoded). + * @param symmetricKey + * A base64-encoded symmetric key (eg, one from generateRandomKey). + * @param iv + * A base64-encoded initialization vector + * @returns Encrypted data, base64 encoded */ - ACString decrypt(in ACString pass, in ACString cipherText); + ACString encrypt(in ACString clearText, + in ACString symmetricKey, in ACString iv); + + /** + * Encrypt data using a symmetric key. + * The algorithm attribute specifies how the encryption is performed. + * + * @param cipherText + * The base64-encoded data to be decrypted + * @param symmetricKey + * A base64-encoded symmetric key (eg, one from unwrapSymmetricKey) + * @param iv + * A base64-encoded initialization vector + * @returns Decrypted data (not base64-encoded) + */ + ACString decrypt(in ACString cipherText, + in ACString symmetricKey, in ACString iv); + + /** + * Generate a RSA public/private keypair. + * + * @param aPassphrase + * User's passphrase. Used with PKCS#5 to generate a symmetric key + * for wrapping the private key. + * @param aSalt + * Salt for the user's passphrase. + * @param aIV + * Random IV, used when wrapping the private key. + * @param aEncodedPublicKey + * The public key, base-64 encoded. + * @param aWrappedPrivateKey + * The public key, encrypted with the user's passphrase, and base-64 encoded. + */ + void generateKeypair(in ACString aPassphrase, in ACString aSalt, in ACString aIV, + out ACString aEncodedPublicKey, out ACString aWrappedPrivateKey); + + /* + * Generate a random symmetric key. + * + * @returns The random key, base64 encoded + */ + ACString generateRandomKey(); + + /* + * Generate a random IV. + * + * The IV will be sized for the algorithm specified in the algorithm + * attribute of IWeaveCrypto. + * + * @returns The random IV, base64 encoded + */ + ACString generateRandomIV(); + + /* + * Generate random data. + * + * @param aByteCount + * The number of bytes of random data to generate. + * @returns The random bytes, base64-encoded + */ + ACString generateRandomBytes(in unsigned long aByteCount); + + + /** + * Encrypts a symmetric key with a user's public key. + * + * @param aSymmetricKey + * The base64 encoded string holding a symmetric key. + * @param aEncodedPublicKey + * The base64 encoded string holding a public key. + * @returns The wrapped symmetric key, base64 encoded + * + * For RSA, the unencoded public key is a PKCS#1 object. + */ + ACString wrapSymmetricKey(in ACString aSymmetricKey, + in ACString aEncodedPublicKey); + + /** + * Decrypts a symmetric key with a user's private key. + * + * @param aWrappedSymmetricKey + * The base64 encoded string holding an encrypted symmetric key. + * @param aWrappedPrivateKey + * The base64 encoded string holdering an encrypted private key. + * @param aPassphrase + * The passphrase to decrypt the private key. + * @param aSalt + * The salt for the passphrase. + * @param aIV + * The random IV used when unwrapping the private key. + * @returns The unwrapped symmetric key, base64 encoded + * + * For RSA, the unencoded, decrypted key is a PKCS#1 object. + */ + ACString unwrapSymmetricKey(in ACString aWrappedSymmetricKey, + in ACString aWrappedPrivateKey, + in ACString aPassphrase, + in ACString aSalt, + in ACString aIV); }; diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index 80e80a8181e3..f49a69f84ff0 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -22,6 +22,7 @@ * Contributor(s): * Dan Mills (original author) * Honza Bambas + * Justin Dolske * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -43,12 +44,17 @@ #include "nsAutoPtr.h" #include "plbase64.h" #include "secerr.h" -#include "secpkcs7.h" + +#include "pk11func.h" +#include "keyhi.h" +#include "nss.h" + NS_IMPL_ISUPPORTS1(WeaveCrypto, IWeaveCrypto) -WeaveCrypto::WeaveCrypto() -: mAlgorithm(SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_3KEY_TRIPLE_DES_CBC) +WeaveCrypto::WeaveCrypto() : + mAlgorithm(SEC_OID_AES_256_CBC), + mKeypairBits(2048) { } @@ -56,76 +62,67 @@ WeaveCrypto::~WeaveCrypto() { } -nsresult -WeaveCrypto::EncodeBase64(const nsACString& binary, nsACString& retval) +/* + * Base 64 encoding and decoding... + */ + +void +WeaveCrypto::EncodeBase64(const char *aData, PRUint32 aLength, nsACString& retval) { - PRUint32 encodedLength = (binary.Length() * 4 + 2) / 3; + PRUint32 encodedLength = (aLength + 2) / 3 * 4; + char encoded[encodedLength]; - nsAutoArrayPtr encoded; - encoded = new char[encodedLength + 2]; - NS_ENSURE_TRUE(encoded, NS_ERROR_OUT_OF_MEMORY); - - PromiseFlatCString fBinary(binary); - PL_Base64Encode(fBinary.get(), fBinary.Length(), encoded); + PL_Base64Encode(aData, aLength, encoded); retval.Assign(encoded, encodedLength); +} + +void +WeaveCrypto::EncodeBase64(const nsACString& binary, nsACString& retval) +{ + PromiseFlatCString fBinary(binary); + EncodeBase64(fBinary.get(), fBinary.Length(), retval); +} + +nsresult +WeaveCrypto::DecodeBase64(const nsACString& base64, + char *decoded, PRUint32 *decodedSize) +{ + PromiseFlatCString fBase64(base64); + + PRUint32 size = (fBase64.Length() * 3) / 4; + // Adjust for padding. + if (*(fBase64.get() + fBase64.Length() - 1) == '=') + size--; + if (*(fBase64.get() + fBase64.Length() - 2) == '=') + size--; + + // Just to be paranoid about buffer overflow. + if (*decodedSize < size) + return NS_ERROR_FAILURE; + *decodedSize = size; + + if (!PL_Base64Decode(fBase64.get(), fBase64.Length(), decoded)) + return NS_ERROR_ILLEGAL_VALUE; + return NS_OK; } nsresult WeaveCrypto::DecodeBase64(const nsACString& base64, nsACString& retval) { - PromiseFlatCString flat(base64); + char decoded[base64.Length()]; + PRUint32 decodedLength = base64.Length(); - PRUint32 decodedLength = (flat.Length() * 3) / 4; - - nsAutoArrayPtr decoded; - decoded = new char[decodedLength]; - NS_ENSURE_TRUE(decoded, NS_ERROR_OUT_OF_MEMORY); - - if (!PL_Base64Decode(flat.get(), flat.Length(), decoded)) - return NS_ERROR_ILLEGAL_VALUE; + nsresult rv = DecodeBase64(base64, decoded, &decodedLength); + NS_ENSURE_SUCCESS(rv, rv); retval.Assign(decoded, decodedLength); return NS_OK; } -// static -void -WeaveCrypto::StoreToStringCallback(void *arg, const char *buf, unsigned long len) -{ - nsACString* aText = (nsACString*)arg; - aText->Append(buf, len); -} - -// static -PK11SymKey * -WeaveCrypto::GetSymmetricKeyCallback(void *arg, SECAlgorithmID *algid) -{ - SECItem *pwitem = (SECItem *)arg; - - PK11SlotInfo *slot = PK11_GetInternalSlot(); - if (!slot) - return nsnull; - - PK11SymKey *key = PK11_PBEKeyGen(slot, algid, pwitem, PR_FALSE, nsnull); - PK11_FreeSlot(slot); - - if (key) - PK11_SetSymKeyUserData(key, pwitem, nsnull); - - return key; -} - -// static -PRBool -WeaveCrypto::DecryptionAllowedCallback(SECAlgorithmID *algid, PK11SymKey *key) -{ - return PR_TRUE; -} ////////////////////////////////////////////////////////////////////////////// -// nsIPBECipher NS_IMETHODIMP WeaveCrypto::GetAlgorithm(PRUint32 *aAlgorithm) @@ -142,78 +139,791 @@ WeaveCrypto::SetAlgorithm(PRUint32 aAlgorithm) } NS_IMETHODIMP -WeaveCrypto::Encrypt(const nsACString& aPass, - const nsACString& aClearText, - nsACString& aCipherText) +WeaveCrypto::GetKeypairBits(PRUint32 *aBits) { - nsresult rv = NS_ERROR_FAILURE; - - SEC_PKCS7ContentInfo *cinfo = SEC_PKCS7CreateEncryptedData(mAlgorithm, 0, nsnull, nsnull); - NS_ENSURE_TRUE(cinfo, NS_ERROR_FAILURE); - - SECAlgorithmID* encalgid = SEC_PKCS7GetEncryptionAlgorithm(cinfo); - if (encalgid) - { - PromiseFlatCString fPass(aPass); - SECItem pwitem = {siBuffer, (unsigned char*)fPass.get(), fPass.Length()}; - PK11SymKey *key = GetSymmetricKeyCallback(&pwitem, encalgid); - if (key) - { - nsCString result; - SEC_PKCS7EncoderContext *ecx = SEC_PKCS7EncoderStart( - cinfo, StoreToStringCallback, &result, key); - PK11_FreeSymKey(key); - - if (ecx) - { - SECStatus srv; - - PromiseFlatCString fClearText(aClearText); - srv = SEC_PKCS7EncoderUpdate(ecx, fClearText.get(), fClearText.Length()); - - SEC_PKCS7EncoderFinish(ecx, nsnull, nsnull); - - if (SECSuccess == srv) - rv = EncodeBase64(result, aCipherText); - } - else - { - NS_WARNING("Could not create PKCS#7 encoder context"); - } - } - } - - SEC_PKCS7DestroyContentInfo(cinfo); - return rv; + *aBits = mKeypairBits; + return NS_OK; } NS_IMETHODIMP -WeaveCrypto::Decrypt(const nsACString& aPass, - const nsACString& aCipherText, +WeaveCrypto::SetKeypairBits(PRUint32 aBits) +{ + mKeypairBits = aBits; + return NS_OK; +} + + +/* + * Encrypt + */ +NS_IMETHODIMP +WeaveCrypto::Encrypt(const nsACString& aClearText, + const nsACString& aSymmetricKey, + const nsACString& aIV, + nsACString& aCipherText) +{ + nsresult rv; + + // When using CBC padding, the output is 1 block larger than the input. + CK_MECHANISM_TYPE mech = PK11_AlgtagToMechanism(mAlgorithm); + PRUint32 blockSize = PK11_GetBlockSize(mech, nsnull); + + char outputBuffer[aClearText.Length() + blockSize]; + PRUint32 outputBufferSize = sizeof(outputBuffer); + PromiseFlatCString input(aClearText); + + rv = CommonCrypt(input.get(), input.Length(), + outputBuffer, &outputBufferSize, + aSymmetricKey, aIV, CKA_ENCRYPT); + NS_ENSURE_SUCCESS(rv, rv); + + EncodeBase64(outputBuffer, outputBufferSize, aCipherText); + + return NS_OK; +} + + +/* + * Decrypt + */ +NS_IMETHODIMP +WeaveCrypto::Decrypt(const nsACString& aCipherText, + const nsACString& aSymmetricKey, + const nsACString& aIV, nsACString& aClearText) { nsresult rv; - nsCString fCipherText; - rv = DecodeBase64(aCipherText, fCipherText); - NS_ENSURE_SUCCESS(rv, rv); + char inputBuffer[aCipherText.Length()]; + PRUint32 inputBufferSize = sizeof(inputBuffer); + char outputBuffer[aCipherText.Length()]; + PRUint32 outputBufferSize = sizeof(outputBuffer); - aClearText.Truncate(); + rv = DecodeBase64(aCipherText, inputBuffer, &inputBufferSize); + NS_ENSURE_SUCCESS(rv, rv); - PromiseFlatCString fPass(aPass); - SECItem pwitem = {siBuffer, (unsigned char*)fPass.get(), fPass.Length()}; - SEC_PKCS7DecoderContext *dcx = SEC_PKCS7DecoderStart( - StoreToStringCallback, &aClearText, nsnull, nsnull, - GetSymmetricKeyCallback, &pwitem, - DecryptionAllowedCallback); - NS_ENSURE_TRUE(dcx, NS_ERROR_FAILURE); + rv = CommonCrypt(inputBuffer, inputBufferSize, + outputBuffer, &outputBufferSize, + aSymmetricKey, aIV, CKA_DECRYPT); + NS_ENSURE_SUCCESS(rv, rv); - SECStatus srv = SEC_PKCS7DecoderUpdate(dcx, fCipherText.get(), fCipherText.Length()); - rv = (SECSuccess == srv) ? NS_OK : NS_ERROR_FAILURE; + aClearText.Assign(outputBuffer, outputBufferSize); - SEC_PKCS7ContentInfo *cinfo = SEC_PKCS7DecoderFinish(dcx); - if (cinfo) - SEC_PKCS7DestroyContentInfo(cinfo); + return NS_OK; +} + + +/* + * CommonCrypt + */ +nsresult +WeaveCrypto::CommonCrypt(const char *input, PRUint32 inputSize, + char *output, PRUint32 *outputSize, + const nsACString& aSymmetricKey, + const nsACString& aIV, + CK_ATTRIBUTE_TYPE aOperation) +{ + nsresult rv = NS_OK; + PK11SymKey *symKey = nsnull; + PK11Context *ctx = nsnull; + PK11SlotInfo *slot = nsnull; + SECItem *ivParam = nsnull; + + char keyData[aSymmetricKey.Length()]; + PRUint32 keyDataSize = sizeof(keyData); + char ivData[aIV.Length()]; + PRUint32 ivDataSize = sizeof(ivData); + + rv = DecodeBase64(aSymmetricKey, keyData, &keyDataSize); + NS_ENSURE_SUCCESS(rv, rv); + rv = DecodeBase64(aIV, ivData, &ivDataSize); + NS_ENSURE_SUCCESS(rv, rv); + + SECItem keyItem = {siBuffer, (unsigned char*)keyData, keyDataSize}; + SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; + + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + CK_MECHANISM_TYPE mechanism = PK11_AlgtagToMechanism(mAlgorithm); + mechanism = PK11_GetPadMechanism(mechanism); + if (mechanism == CKM_INVALID_MECHANISM) { + NS_WARNING("Unknown key mechanism"); + rv = NS_ERROR_FAILURE; + goto crypt_done; + } + + ivParam = PK11_ParamFromIV(mechanism, &ivItem); + if (!ivParam) { + NS_WARNING("Couldn't create IV param"); + rv = NS_ERROR_FAILURE; + goto crypt_done; + } + + slot = PK11_GetInternalKeySlot(); + if (!slot) { + NS_WARNING("PK11_GetInternalKeySlot failed"); + rv = NS_ERROR_FAILURE; + goto crypt_done; + } + + symKey = PK11_ImportSymKey(slot, mechanism, PK11_OriginUnwrap, aOperation, &keyItem, NULL); + if (!symKey) { + NS_WARNING("PK11_ImportSymKey failed"); + rv = NS_ERROR_FAILURE; + goto crypt_done; + } + + ctx = PK11_CreateContextBySymKey(mechanism, aOperation, symKey, ivParam); + if (!ctx) { + NS_WARNING("PK11_CreateContextBySymKey failed"); + rv = NS_ERROR_FAILURE; + goto crypt_done; + } + + int tmpOutSize; // XXX retarded. why is an signed int suddenly needed? + PRUint32 maxOutputSize = *outputSize; + rv = PK11_CipherOp(ctx, + (unsigned char *)output, &tmpOutSize, maxOutputSize, + (unsigned char *)input, inputSize); + if (NS_FAILED(rv)) { + NS_WARNING("PK11_CipherOp failed"); + rv = NS_ERROR_FAILURE; + goto crypt_done; + } + + *outputSize = tmpOutSize; + output += tmpOutSize; + maxOutputSize -= tmpOutSize; + + // PK11_DigestFinal sure sounds like the last step for *hashing*, but it + // just seems to be an odd name -- NSS uses this to finish the current + // cipher operation. You'd think it would be called PK11_CipherOpFinal... + PRUint32 tmpOutSize2; // XXX WTF? Now unsigned again? What kind of crazy API is this? + rv = PK11_DigestFinal(ctx, (unsigned char *)output, &tmpOutSize2, maxOutputSize); + if (NS_FAILED(rv)) { + NS_WARNING("PK11_DigestFinal failed"); + rv = NS_ERROR_FAILURE; + goto crypt_done; + } + + *outputSize += tmpOutSize2; + +crypt_done: + if (ctx) + PK11_DestroyContext(ctx, PR_TRUE); + if (symKey) + PK11_FreeSymKey(symKey); + if (slot) + PK11_FreeSlot(slot); + if (ivParam) + SECITEM_FreeItem(ivParam, PR_TRUE); + + return rv; +} + + +/* + * GenerateKeypair + * + * Generates an RSA key pair, encrypts (wraps) the private key with the + * supplied passphrase, and returns both to the caller. + * + * Based on code from: + * http://www.mozilla.org/projects/security/pki/nss/sample-code/sample1.html + */ +NS_IMETHODIMP +WeaveCrypto::GenerateKeypair(const nsACString& aPassphrase, + const nsACString& aSalt, + const nsACString& aIV, + nsACString& aEncodedPublicKey, + nsACString& aWrappedPrivateKey) +{ + nsresult rv; + SECStatus s; + SECKEYPrivateKey *privKey = nsnull; + SECKEYPublicKey *pubKey = nsnull; + PK11SlotInfo *slot = nsnull; + PK11RSAGenParams rsaParams; + + + rsaParams.keySizeInBits = mKeypairBits; // 1024, 2048, etc. + rsaParams.pe = 65537; // public exponent. + + slot = PK11_GetInternalKeySlot(); + if (!slot) { + NS_WARNING("PK11_GetInternalKeySlot failed"); + rv = NS_ERROR_FAILURE; + goto keygen_done; + } + + // Generate the keypair. + // XXX isSensitive sets PK11_ATTR_SENSITIVE | PK11_ATTR_PRIVATE + // Might want to use PK11_GenerateKeyPairWithFlags and not set + // CKA_PRIVATE, since that may trigger a master password entry, which is + // kind of pointless for session objects... + privKey = PK11_GenerateKeyPair(slot, + CKM_RSA_PKCS_KEY_PAIR_GEN, + &rsaParams, &pubKey, + PR_FALSE, // isPerm + PR_TRUE, // isSensitive + nsnull); // wincx + + if (!privKey) { + NS_WARNING("PK11_GenerateKeyPair failed"); + rv = NS_ERROR_FAILURE; + goto keygen_done; + } + + s = PK11_SetPrivateKeyNickname(privKey, "Weave User PrivKey"); + if (s != SECSuccess) { + NS_WARNING("PK11_SetPrivateKeyNickname failed"); + rv = NS_ERROR_FAILURE; + goto keygen_done; + } + + + rv = WrapPrivateKey(privKey, aPassphrase, aSalt, aIV, aWrappedPrivateKey); + if (NS_FAILED(rv)) { + NS_WARNING("WrapPrivateKey failed"); + rv = NS_ERROR_FAILURE; + goto keygen_done; + } + + rv = EncodePublicKey(pubKey, aEncodedPublicKey); + if (NS_FAILED(rv)) { + NS_WARNING("EncodePublicKey failed"); + rv = NS_ERROR_FAILURE; + goto keygen_done; + } + +keygen_done: + // Cleanup allocated resources. + if (pubKey) + SECKEY_DestroyPublicKey(pubKey); + if (privKey) + SECKEY_DestroyPrivateKey(privKey); + if (slot) + PK11_FreeSlot(slot); + + return rv; +} + + +/* + * DeriveKeyFromPassphrase + * + * Creates a symmertic key from a passphrase, using PKCS#5. + */ +nsresult +WeaveCrypto::DeriveKeyFromPassphrase(const nsACString& aPassphrase, + const nsACString& aSalt, + PK11SymKey **aSymKey) +{ + nsresult rv; + + PromiseFlatCString fPass(aPassphrase); + SECItem passphrase = {siBuffer, (unsigned char *)fPass.get(), fPass.Length()}; + + char saltBytes[aSalt.Length()]; + PRUint32 saltBytesLength = aSalt.Length(); + rv = DecodeBase64(aSalt, saltBytes, &saltBytesLength); + NS_ENSURE_SUCCESS(rv, rv); + SECItem salt = {siBuffer, (unsigned char*)saltBytes, saltBytesLength}; + + // http://mxr.mozilla.org/seamonkey/source/security/nss/lib/pk11wrap/pk11pbe.c#1261 + + // Bug 436577 prevents us from just using SEC_OID_PKCS5_PBKDF2 here + SECOidTag pbeAlg = mAlgorithm; + SECOidTag cipherAlg = mAlgorithm; // ignored by callee when pbeAlg != a pkcs5 mech. + SECOidTag prfAlg = SEC_OID_HMAC_SHA1; // callee picks if SEC_OID_UNKNOWN, but only SHA1 is supported + + PRInt32 keyLength = 0; // Callee will pick. + PRInt32 iterations = 4096; // PKCS#5 recommends at least 1000. + + SECAlgorithmID *algid = PK11_CreatePBEV2AlgorithmID(pbeAlg, cipherAlg, prfAlg, + keyLength, iterations, &salt); + if (!algid) + return NS_ERROR_FAILURE; + + PK11SlotInfo *slot = PK11_GetInternalSlot(); + if (!slot) + return NS_ERROR_FAILURE; + + *aSymKey = PK11_PBEKeyGen(slot, algid, &passphrase, PR_FALSE, nsnull); + + SECOID_DestroyAlgorithmID(algid, PR_TRUE); + PK11_FreeSlot(slot); + + return (*aSymKey ? NS_OK : NS_ERROR_FAILURE); +} + + +/* + * WrapPrivateKey + * + * Extract the private key by wrapping it (ie, converting it to a binary + * blob and encoding it with a symmetric key) + * + * See: + * http://www.mozilla.org/projects/security/pki/nss/tech-notes/tn5.html, + * "Symmetric Key Wrapping/Unwrapping of a Private Key" + * + * The NSS softtoken uses sftk_PackagePrivateKey() to prepare the private key + * for wrapping. For RSA, that's an ASN1 encoded PKCS1 object. + */ +nsresult +WeaveCrypto::WrapPrivateKey(SECKEYPrivateKey *aPrivateKey, + const nsACString& aPassphrase, + const nsACString& aSalt, + const nsACString& aIV, + nsACString& aWrappedPrivateKey) + +{ + nsresult rv; + SECStatus s; + PK11SymKey *pbeKey = nsnull; + + // Convert our passphrase to a symkey and get the IV in the form we want. + rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); + NS_ENSURE_SUCCESS(rv, rv); + + char ivData[aIV.Length()]; + PRUint32 ivDataSize = sizeof(ivData); + rv = DecodeBase64(aIV, ivData, &ivDataSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); + wrapMech = PK11_GetPadMechanism(wrapMech); + if (wrapMech == CKM_INVALID_MECHANISM) { + NS_WARNING("Unknown key mechanism"); + return NS_ERROR_FAILURE; + } + + SECItem *ivParam = PK11_ParamFromIV(wrapMech, &ivItem); + if (!ivParam) { + NS_WARNING("Couldn't create IV param"); + return NS_ERROR_FAILURE; + } + + + // Use a stack buffer to hold the wrapped key. NSS says about 1200 bytes for + // a 2048-bit RSA key, so our 4096 byte buffer should be plenty. + unsigned char stackBuffer[4096]; + SECItem wrappedKey = {siBuffer, stackBuffer, sizeof(stackBuffer)}; + + s = PK11_WrapPrivKey(aPrivateKey->pkcs11Slot, + pbeKey, aPrivateKey, + wrapMech, ivParam, + &wrappedKey, nsnull); + + SECITEM_FreeItem(ivParam, PR_TRUE); + PK11_FreeSymKey(pbeKey); + + if (s != SECSuccess) { + NS_WARNING("PK11_WrapPrivKey failed"); + return(NS_ERROR_FAILURE); + } + + EncodeBase64((char *)wrappedKey.data, wrappedKey.len, aWrappedPrivateKey); + + return NS_OK; +} + + +/* + * EncodePublicKey + * + * http://svn.mozilla.org/projects/mccoy/trunk/components/src/KeyPair.cpp + */ +nsresult +WeaveCrypto::EncodePublicKey(SECKEYPublicKey *aPublicKey, + nsACString& aEncodedPublicKey) +{ + //SECItem *derKey = PK11_DEREncodePublicKey(aPublicKey); + SECItem *derKey = SECKEY_EncodeDERSubjectPublicKeyInfo(aPublicKey); + if (!derKey) + return NS_ERROR_FAILURE; + + EncodeBase64((char *)derKey->data, derKey->len, aEncodedPublicKey); + + // XXX destroy derKey? + + return NS_OK; +} + + +/* + * GenerateRandomBytes + */ +NS_IMETHODIMP +WeaveCrypto::GenerateRandomBytes(PRUint32 aByteCount, + nsACString& aEncodedBytes) +{ + nsresult rv; + char random[aByteCount]; + + rv = PK11_GenerateRandom((unsigned char *)random, aByteCount); + NS_ENSURE_SUCCESS(rv, rv); + + EncodeBase64(random, aByteCount, aEncodedBytes); + + return NS_OK; +} + + +/* + * GenerateRandomIV + */ +NS_IMETHODIMP +WeaveCrypto::GenerateRandomIV(nsACString& aEncodedBytes) +{ + nsresult rv; + + CK_MECHANISM_TYPE mech = PK11_AlgtagToMechanism(mAlgorithm); + PRUint32 size = PK11_GetIVLength(mech); + + char random[size]; + + rv = PK11_GenerateRandom((unsigned char *)random, size); + NS_ENSURE_SUCCESS(rv, rv); + + EncodeBase64(random, size, aEncodedBytes); + + return NS_OK; +} + + +/* + * GenerateRandomKey + * + * Generates a random symmetric key, and returns it in base64 encoded form. + * + * Note that we could just use GenerateRandomBytes to do all this (at least + * for AES), but ideally we'd be able to switch to passing around key handles + * insead of the actual key bits, which would require generating an actual + * key. + */ +NS_IMETHODIMP +WeaveCrypto::GenerateRandomKey(nsACString& aEncodedKey) +{ + nsresult rv = NS_OK; + PRUint32 keySize; + CK_MECHANISM_TYPE keygenMech; + + // XXX doesn't NSS have a lookup function to do this? + switch (mAlgorithm) { + case AES_128_CBC: + keygenMech = CKM_AES_KEY_GEN; + keySize = 16; + break; + + case AES_192_CBC: + keygenMech = CKM_AES_KEY_GEN; + keySize = 24; + break; + + case AES_256_CBC: + keygenMech = CKM_AES_KEY_GEN; + keySize = 32; + break; + + default: + NS_WARNING("Unknown random keygen algorithm"); + return NS_ERROR_FAILURE; + } + + PK11SlotInfo *slot = PK11_GetInternalSlot(); + if (!slot) + return NS_ERROR_FAILURE; + + PK11SymKey* randKey = PK11_KeyGen(slot, keygenMech, NULL, keySize, NULL); + if (!randKey) { + NS_WARNING("PK11_KeyGen failed"); + rv = NS_ERROR_FAILURE; + goto keygen_done; + } + + if (PK11_ExtractKeyValue(randKey)) { + NS_WARNING("PK11_ExtractKeyValue failed"); + rv = NS_ERROR_FAILURE; + goto keygen_done; + } + + SECItem *keydata = PK11_GetKeyData(randKey); + if (!keydata) { + NS_WARNING("PK11_GetKeyData failed"); + rv = NS_ERROR_FAILURE; + goto keygen_done; + } + + EncodeBase64((char *)keydata->data, keydata->len, aEncodedKey); + +keygen_done: + // XXX does keydata need freed? + if (randKey) + PK11_FreeSymKey(randKey); + if (slot) + PK11_FreeSlot(slot); + + return rv; +} + + +/* + * WrapSymmetricKey + */ +NS_IMETHODIMP +WeaveCrypto::WrapSymmetricKey(const nsACString& aSymmetricKey, + const nsACString& aPublicKey, + nsACString& aWrappedKey) +{ + nsresult rv = NS_OK; + SECStatus s; + PK11SlotInfo *slot = nsnull; + PK11SymKey *symKey = nsnull; + SECKEYPublicKey *pubKey = nsnull; + CERTSubjectPublicKeyInfo *pubKeyInfo = nsnull; + + // Step 1. Get rid of the base64 encoding on the inputs. + + char publicKeyBuffer[aPublicKey.Length()]; + PRUint32 publicKeyBufferSize = aPublicKey.Length(); + rv = DecodeBase64(aPublicKey, publicKeyBuffer, &publicKeyBufferSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem pubKeyData = {siBuffer, (unsigned char *)publicKeyBuffer, publicKeyBufferSize}; + + char symKeyBuffer[aSymmetricKey.Length()]; + PRUint32 symKeyBufferSize = aSymmetricKey.Length(); + rv = DecodeBase64(aSymmetricKey, symKeyBuffer, &symKeyBufferSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem symKeyData = {siBuffer, (unsigned char *)symKeyBuffer, symKeyBufferSize}; + + char wrappedBuffer[4096]; + SECItem wrappedKey = {siBuffer, (unsigned char *)wrappedBuffer, sizeof(wrappedBuffer)}; + + + // Step 2. Put the symmetric key bits into a P11 key object. + + slot = PK11_GetInternalSlot(); + if (!slot) { + NS_WARNING("Can't get internal PK11 slot"); + rv = NS_ERROR_FAILURE; + goto wrap_done; + } + + // ImportSymKey wants a mechanism, from which it derives the key type. + CK_MECHANISM_TYPE keyMech = PK11_AlgtagToMechanism(mAlgorithm); + if (keyMech == CKM_INVALID_MECHANISM) { + NS_WARNING("Unknown key mechanism"); + rv = NS_ERROR_FAILURE; + goto wrap_done; + } + + // This imports a key with the usage set for encryption, but that doesn't + // really matter because we're just going to wrap it up and not use it. + symKey = PK11_ImportSymKey(slot, + keyMech, + PK11_OriginUnwrap, + CKA_ENCRYPT, + &symKeyData, + NULL); + if (!symKey) { + NS_WARNING("PK11_ImportSymKey failed"); + rv = NS_ERROR_FAILURE; + goto wrap_done; + } + + + // Step 3. Put the public key bits into a P11 key object. + + // Can't just do this directly, it's expecting a minimal ASN1 blob + // pubKey = SECKEY_ImportDERPublicKey(&pubKeyData, CKK_RSA); + pubKeyInfo = SECKEY_DecodeDERSubjectPublicKeyInfo(&pubKeyData); + if (!pubKeyInfo) { + NS_WARNING("SECKEY_DecodeDERSubjectPublicKeyInfo failed"); + rv = NS_ERROR_FAILURE; + goto wrap_done; + } + + pubKey = SECKEY_ExtractPublicKey(pubKeyInfo); + if (!pubKey) { + NS_WARNING("SECKEY_ExtractPublicKey failed"); + rv = NS_ERROR_FAILURE; + goto wrap_done; + } + + + // Step 4. Wrap the symmetric key with the public key. + + CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(SEC_OID_PKCS1_RSA_ENCRYPTION); + + s = PK11_PubWrapSymKey(wrapMech, pubKey, symKey, &wrappedKey); + if (s != SECSuccess) { + NS_WARNING("PK11_PubWrapSymKey failed"); + rv = NS_ERROR_FAILURE; + goto wrap_done; + } + + + // Step 5. Base64 encode the wrapped key, cleanup, and return to caller. + + EncodeBase64((char *)wrappedKey.data, wrappedKey.len, aWrappedKey); + +wrap_done: + if (pubKey) + SECKEY_DestroyPublicKey(pubKey); + if (pubKeyInfo) + SECKEY_DestroySubjectPublicKeyInfo(pubKeyInfo); + if (symKey) + PK11_FreeSymKey(symKey); + if (slot) + PK11_FreeSlot(slot); + + return rv; +} + + +/* + * UnwrapSymmetricKey + */ +NS_IMETHODIMP +WeaveCrypto::UnwrapSymmetricKey(const nsACString& aWrappedSymmetricKey, + const nsACString& aWrappedPrivateKey, + const nsACString& aPassphrase, + const nsACString& aSalt, + const nsACString& aIV, + nsACString& aSymmetricKey) +{ + nsresult rv = NS_OK; + PK11SlotInfo *slot = nsnull; + PK11SymKey *pbeKey = nsnull; + PK11SymKey *symKey = nsnull; + SECKEYPrivateKey *privKey = nsnull; + SECItem *ivParam = nsnull; + + CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; + PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); + + + // Step 1. Get rid of the base64 encoding on the inputs. + + char privateKeyBuffer[aWrappedPrivateKey.Length()]; + PRUint32 privateKeyBufferSize = aWrappedPrivateKey.Length(); + rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; + + char wrappedKeyBuffer[aWrappedSymmetricKey.Length()]; + PRUint32 wrappedKeyBufferSize = aWrappedSymmetricKey.Length(); + rv = DecodeBase64(aWrappedSymmetricKey, wrappedKeyBuffer, &wrappedKeyBufferSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem wrappedSymKey = {siBuffer, (unsigned char *)wrappedKeyBuffer, wrappedKeyBufferSize}; + + + // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. + rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); + NS_ENSURE_SUCCESS(rv, rv); + + char ivData[aIV.Length()]; + PRUint32 ivDataSize = sizeof(ivData); + rv = DecodeBase64(aIV, ivData, &ivDataSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); + wrapMech = PK11_GetPadMechanism(wrapMech); + if (wrapMech == CKM_INVALID_MECHANISM) { + NS_WARNING("Unknown key mechanism"); + rv = NS_ERROR_FAILURE; + goto unwrap_done; + } + + ivParam = PK11_ParamFromIV(wrapMech, &ivItem); + if (!ivParam) { + NS_WARNING("Couldn't create IV param"); + rv = NS_ERROR_FAILURE; + goto unwrap_done; + } + + + // Step 3. Unwrap the private key with the key from the passphrase. + + slot = PK11_GetInternalSlot(); + if (!slot) { + NS_WARNING("Can't get internal PK11 slot"); + rv = NS_ERROR_FAILURE; + goto unwrap_done; + } + + + // Normally, one wants to associate a private key with a public key. + // P11_UnwrapPrivKey() passes its keyID arg to PK11_MakeIDFromPubKey(), + // which hashes the public key to create an ID (or, for small inputs, + // assumes it's already hashed and does nothing). + // We don't really care about this, because our unwrapped private key will + // just live long enough to unwrap the bulk data key. So, we'll just jam in + // a random value... We have an IV handy, so that will suffice. + SECItem *keyID = &ivItem; + + privKey = PK11_UnwrapPrivKey(slot, + pbeKey, wrapMech, ivParam, &wrappedPrivKey, + nsnull, // label (SECItem) + keyID, + PR_FALSE, // isPerm (token object) + PR_TRUE, // isSensitive + CKK_RSA, + privKeyUsage, privKeyUsageLength, + nsnull); // wincx + if (!privKey) { + NS_WARNING("PK11_UnwrapPrivKey failed"); + rv = NS_ERROR_FAILURE; + goto unwrap_done; + } + + // Step 4. Unwrap the symmetric key with the user's private key. + + // XXX also have PK11_PubUnwrapSymKeyWithFlags() if more control is needed. + // (last arg is keySize, 0 seems to work) + symKey = PK11_PubUnwrapSymKey(privKey, &wrappedSymKey, wrapMech, + CKA_DECRYPT, 0); + if (!symKey) { + NS_WARNING("PK11_PubUnwrapSymKey failed"); + rv = NS_ERROR_FAILURE; + goto unwrap_done; + } + + // Step 5. Base64 encode the unwrapped key, cleanup, and return to caller. + + if (PK11_ExtractKeyValue(symKey)) { + NS_WARNING("PK11_ExtractKeyValue failed"); + rv = NS_ERROR_FAILURE; + goto unwrap_done; + } + + // XXX need to free this? + SECItem *symKeyData = PK11_GetKeyData(symKey); + if (!symKeyData) { + NS_WARNING("PK11_GetKeyData failed"); + rv = NS_ERROR_FAILURE; + goto unwrap_done; + } + + EncodeBase64((char *)symKeyData->data, symKeyData->len, aSymmetricKey); + +unwrap_done: + if (privKey) + SECKEY_DestroyPrivateKey(privKey); + if (symKey) + PK11_FreeSymKey(symKey); + if (pbeKey) + PK11_FreeSymKey(pbeKey); + if (slot) + PK11_FreeSlot(slot); + if (ivParam) + SECITEM_FreeItem(ivParam, PR_TRUE); return rv; } diff --git a/services/crypto/WeaveCrypto.h b/services/crypto/WeaveCrypto.h index 12969a8fa46a..9cfcb2dbe295 100644 --- a/services/crypto/WeaveCrypto.h +++ b/services/crypto/WeaveCrypto.h @@ -44,7 +44,7 @@ #include "pk11pub.h" #define WEAVE_CRYPTO_CONTRACTID "@labs.mozilla.com/Weave/Crypto;1" -#define WEAVE_CRYPTO_CLASSNAME "A Simple XPCOM Sample" +#define WEAVE_CRYPTO_CLASSNAME "Weave crypto module" #define WEAVE_CRYPTO_CID { 0xd3b0f750, 0xc976, 0x46d0, \ { 0xbe, 0x20, 0x96, 0xb2, 0x4f, 0x46, 0x84, 0xbc } } @@ -60,13 +60,33 @@ private: ~WeaveCrypto(); SECOidTag mAlgorithm; + PRUint32 mKeypairBits; nsresult DecodeBase64(const nsACString& base64, nsACString& retval); - nsresult EncodeBase64(const nsACString& binary, nsACString& retval); + nsresult DecodeBase64(const nsACString& base64, char *aData, PRUint32 *aLength); + void EncodeBase64(const nsACString& binary, nsACString& retval); + void EncodeBase64(const char *aData, PRUint32 aLength, nsACString& retval); + + nsresult CommonCrypt(const char *input, PRUint32 inputSize, + char *output, PRUint32 *outputSize, + const nsACString& aSymmetricKey, + const nsACString& aIV, + CK_ATTRIBUTE_TYPE aOperation); + + + nsresult DeriveKeyFromPassphrase(const nsACString& aPassphrase, + const nsACString& aSalt, + PK11SymKey **aSymKey); + + nsresult WrapPrivateKey(SECKEYPrivateKey *aPrivateKey, + const nsACString& aPassphrase, + const nsACString& aSalt, + const nsACString& aIV, + nsACString& aEncodedPublicKey); + nsresult EncodePublicKey(SECKEYPublicKey *aPublicKey, + nsACString& aEncodedPublicKey); + - static void StoreToStringCallback(void *arg, const char *buf, unsigned long len); - static PK11SymKey *GetSymmetricKeyCallback(void *arg, SECAlgorithmID *algid); - static PRBool DecryptionAllowedCallback(SECAlgorithmID *algid, PK11SymKey *key); }; #endif // WeaveCrypto_h_ From e60d9188bc8dd08521a00aeeab1cc59e9d11ebde Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 22:37:06 -0700 Subject: [PATCH 0400/1860] Added documentation to createIncomingShare, rewrote it a little to be consistent with the other new stuff, and made it use annotations to keep track of the server path to the shared data. --- services/sync/modules/engines/bookmarks.js | 49 ++++++++++++++++------ 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index bf4a953a80fc..f84a8353c229 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -419,22 +419,27 @@ BookmarksEngine.prototype = { // server.deleteAllFromDirectory( serverPath ); }, - _createIncomingShare: function BookmarkEngine__createShare(guid, id, title) { + _createIncomingShare: function BookmarkEngine__createShare(user, + serverPath, + title) { + /* Creates a new folder in the bookmark menu for the incoming share. + user is the weave id of the user who is offering the folder to us; + serverPath is the path on the server where the shared data is located, + and title is the human-readable title to use for the new folder. - /* TODO This used to be called just _createShare, but its semantics - have changed slightly -- its purpose now is to create a new empty - incoming shared bookmark folder. To do this is mostly the same code, - but it will need a few tweaks. + It is safe to call this again for a folder that already exist: this + function will exit without doing anything. It won't create a duplicate. */ let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); + /* Get the toolbar "Shared Folders" folder (identified by its annotation). + If it doesn't already exist, create it: */ let root; let a = this._annoSvc.getItemsWithAnnotation("weave/mounted-shares-folder", {}); if (a.length == 1) root = a[0]; - if (!root) { root = bms.createFolder(bms.toolbarFolder, "Shared Folders", bms.DEFAULT_INDEX); @@ -444,27 +449,45 @@ BookmarksEngine.prototype = { 0, this._annoSvc.EXPIRE_NEVER); } - - let item; + /* Inside "Shared Folders", create a new folder annotated with + the originating user and the directory path specified by the incoming + share offer. Unless a folder with these exact annotations already + exists, in which case do nothing. */ + let itemExists = false; a = this._annoSvc.getItemsWithAnnotation("weave/mounted-share-id", {}); for (let i = 0; i < a.length; i++) { - if (this._annoSvc.getItemAnnotation(a[i], "weave/mounted-share-id")==id){ - item = a[i]; + let creator = this._annoSvc.getItemAnnotation(a[i], OUTGOING_SHARED_ANNO); + let path = this._annoSvc.getItemAnnotation(a[i], SERVER_PATH_ANNO); + if ( creator == user && path == serverPath ) { + itemExists = true; break; } } - - if (!item) { + if (!itemExists) { let newId = bms.createFolder(root, title, bms.DEFAULT_INDEX); + /* TODO: weave/mounted-share-id is kind of redundant now, but it's + treated specially by the sync code. + If i change it here, i have to change it there as well. */ this._annoSvc.setItemAnnotation(newId, "weave/mounted-share-id", id, 0, this._annoSvc.EXPIRE_NEVER); + // Keep track of who shared this folder with us... + this._annoSvc.setItemAnnotation(newId, + OUTGOING_SHARED_ANNO + user, + 0, + this._annoSvc.EXPIRE_NEVER); + // and what the path to the shared data on the server is... + this._annoSvc.setItemAnnotation(newId, + SERVER_PATH_ANNO, + serverPath, + 0, + this._annoSvc.EXPIRE_NEVER); } }, - _updateIncomingShare: function BmkEngine__updateIncomingShare(mountData) { /* Pull down bookmarks from the server for a single incoming shared folder. */ From bf40e04d194cf578b658f5a751bb20665e069071 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 23:00:09 -0700 Subject: [PATCH 0401/1860] Implemented _stopOutgoingShare (though it's still not being called from anywhere). --- services/sync/modules/engines/bookmarks.js | 55 ++++++++++++++++++---- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f84a8353c229..d1a16b894f44 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -401,22 +401,61 @@ BookmarksEngine.prototype = { in my own or that of the person I share with? */ // Encrypt it with the symkey and put it into the shared-bookmark file. - let bookmarkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); + let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); Crypto.PBEencrypt.async( Crypto, self.cb, json, {password:symKey} ); let cyphertext = yield; - bookmarkFile.put( self.cb, cyphertext ); + bmkFile.put( self.cb, cyphertext ); yield; self.done(); }, _stopOutgoingShare: function BmkEngine__stopOutgoingShare(folderNode) { - /* TODO implement this... */ + /* Stops sharing the specified folder. Deletes its data from the + server, deletes the annotations that mark it as shared, and sends + a message to the shar-ee to let them know it's been withdrawn. */ + // TODO: currently not called from anywhere. + let self = yield; + let serverPath = this._annoSvc.getItemAnnotation( folderNode, + SERVER_PATH_ANNO ); + let username = this._annoSvc.getItemAnnotation( folderNode, + OUTGOING_SHARE_ANNO ); + + // Delete the share from the server: + let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); + keyringFile.delete(self.cb); + yield; + let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); + keyringFile.delete(self.cb); + yield; + // TODO this leaves the folder itself in place... is there a way to + // get rid of that, say through DAV? + + // Remove the annotations from the local folder: + this._annoSvc.setItemAnnotation(folderNode, + SERVER_PATH_ANNO + "", + 0, + this._annoSvc.EXPIRE_NEVER); + this._annoSvc.setItemAnnotation(folderNode, + SERVER_PATH_ANNO + "", + 0, + this._annoSvc.EXPIRE_NEVER); + // TODO is there a way to remove the annotations entirely rather than + // setting it to an empty string?? + + // Send the message to the share-ee: + if ( this._xmppClient ) { + if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { + let folderName = folderNode.getAttribute( "label" ); + let msgText = "stop " + serverPath + " " + folderName; + this._log.debug( "Sending XMPP message: " + msgText ); + this._xmppClient.sendMessage( username, msgText ); + } else { + this._log.warn( "No XMPP connection for share notification." ); + } + } - // pseudocode: - // let serverPath = getAnnotation( folderNode, SERVER_PATH_ANNO ); - // removeAnnotationFromFolder( folderNode, SERVER_PATH_ANNO ); - // removeAnnotationFromFolder( folderNode, OUTGOING_SHARED_ANNO ); - // server.deleteAllFromDirectory( serverPath ); }, _createIncomingShare: function BookmarkEngine__createShare(user, From 20a4e14df39627f6f6900db61a31481e44500ce2 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Jun 2008 23:27:48 -0700 Subject: [PATCH 0402/1860] Reimplemented most of _updateIncomingShare to work with the new implementation of _updateOutgoingShare. No more need for the ugly hack to temporarily set the root directory of DAV, hooray --- services/sync/modules/engines/bookmarks.js | 90 ++++++++-------------- 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d1a16b894f44..57c7ea5cbbc3 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -529,73 +529,47 @@ BookmarksEngine.prototype = { _updateIncomingShare: function BmkEngine__updateIncomingShare(mountData) { /* Pull down bookmarks from the server for a single incoming - shared folder. */ + shared folder, obliterating whatever was in that folder before. - // TODO can use this._remote.keys.getKey( sharer_identity ) to get - // the symmetric key for decryption of the resource. (That will throw - // exception if the resource isn't shared with me.) - /* TODO modify this: the old implementation assumes we want to copy - everything that the other user has, by pulling down snapshot and - diffs and applying the diffs to the snapshot. Instead, now we just - want to get a single subfolder and its children, which will be in - a separate file. */ + mountData is an object that's expected to have member data: + userid: weave id of the user sharing the folder with us, + rootGUID: guid in our bookmark store of the share mount point, + node: the bookmark menu node for the share mount point folder, + snapshot: the json-wrapped current contents of the share. */ let self = yield; let user = mountData.userid; - let prefix = DAV.defaultPrefix; - let serverURL = Utils.prefs.getCharPref("serverURL"); - let snap = new SnapshotStore(); + let myUserName = ID.get('WeaveID').username; + // The folder has an annotation specifying the server path to the + // directory: + let serverPath = this._annoSvc.getItemAnnotation(mountData.node, + SERVER_PATH_ANNO); + // From that directory, get the keyring file, and from it, the symmetric + // key that we'll use to encrypt. + let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); + keyringFile.get(self.cb); + let keyring = yield; + let symKey = keyring[ myUserName ]; - this._log.debug("Syncing shared folder from user " + user); - - try { - DAV.defaultPrefix = "user/" + user + "/"; - - this._getSymKey.async(this, self.cb); - yield; - - this._log.trace("Getting status file for " + user); - DAV.GET(this.statusFile, self.cb); - let resp = yield; - Utils.ensureStatus(resp.status, "Could not download status file."); - let status = this._json.decode(resp.responseText); - - this._log.trace("Downloading server snapshot for " + user); - DAV.GET(this.snapshotFile, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not download snapshot."); - Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._engineId, status.snapEncryption); - let data = yield; - snap.data = this._json.decode(data); - - this._log.trace("Downloading server deltas for " + user); - DAV.GET(this.deltasFile, self.cb); - resp = yield; - Utils.ensureStatus(resp.status, "Could not download deltas."); - Crypto.PBEdecrypt.async(Crypto, self.cb, resp.responseText, - this._engineId, status.deltasEncryption); - data = yield; - deltas = this._json.decode(data); - } - catch (e) { throw e; } - finally { DAV.defaultPrefix = prefix; } - - // apply deltas to get current snapshot - for (var i = 0; i < deltas.length; i++) { - snap.applyCommands.async(snap, self.cb, deltas[i]); - yield; - } + // Decrypt the contents of the bookmark file with the symmetric key: + let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); + bmkFile.get(self.cb); + let cyphertext = yield; + Crypto.PBEdecrypt.async( Crypto, self.cb, cyphertext, {password:symKey} ); + let json = yield; + // TODO error handling (see what Resource can throw or return...) // prune tree / get what we want - for (let guid in snap.data) { - if (snap.data[guid].type != "bookmark") - delete snap.data[guid]; + for (let guid in json) { + if (json[guid].type != "bookmark") + delete json[guid]; else - snap.data[guid].parentGUID = mountData.rootGUID; + json[guid].parentGUID = mountData.rootGUID; } - this._log.trace("Got bookmarks fror " + user + ", comparing with local copy"); + /* Create diff between the json from server and the current contents; + then apply the diff. */ + this._log.trace("Got bookmarks from " + user + ", comparing with local copy"); this._core.detectUpdates(self.cb, mountData.snapshot, snap.data); let diff = yield; @@ -1079,7 +1053,7 @@ BookmarksStore.prototype = { throw "Trying to wrap a non-folder mounted share"; let GUID = this._bms.getItemGUID(node.itemId); - let ret = {rootGUID: GUID, userid: id, snapshot: {}}; + let ret = {rootGUID: GUID, userid: id, snapshot: {}, folderNode: node}; node.QueryInterface(Ci.nsINavHistoryQueryResultNode); node.containerOpen = true; From 5af29ff3c7520f156acdac1700b6d6c1a19b0380 Mon Sep 17 00:00:00 2001 From: Date: Thu, 19 Jun 2008 11:41:57 -0700 Subject: [PATCH 0403/1860] Applied dolske's patch from bug 433949 to make the bookmarkEngine work with the new improved Crypto interface. --- services/sync/modules/engines/bookmarks.js | 59 ++++++++++++++++------ 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 57c7ea5cbbc3..682d9e521897 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -344,28 +344,39 @@ BookmarksEngine.prototype = { this._annoSvc.EXPIRE_NEVER); // Create a new symmetric key, to be used only for encrypting this share. - Crypto.PBEkeygen.async(Crypto, self.cb); - let newSymKey = yield; + let tmpIdentity = { realm : "temp ID", + bulkKey : null, + bulkIV : null + }; + Crypto.randomKeyGen.async(Crypto, self.cb, tmpIdentity); + yield; + let bulkKey = tmpIdentity.bulkKey; + let bulkIV = tmpIdentity.bulkIV; + /* Get public keys for me and the user I'm sharing with. Each user's public key is stored in /user/username/public/pubkey. */ - let myPubKeyFile = new Resource("/user/" + myUserName + "/public/pubkey"); - myPubKeyFile.get(self.cb); - let myPubKey = yield; + let idRSA = ID.get('WeaveCryptoID'); + let myPubKey = idRSA.pubkey; let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); userPubKeyFile.get(self.cb); let userPubKey = yield; /* Create the keyring, containing the sym key encrypted with each of our public keys: */ - Crypto.RSAencrypt.async(Crypto, self.cb, symKey, {pubkey: myPubKey} ); + Crypto.wrapKey.async(Crypto, self.cb, bulkKey, + {realm : "tmpWrapID", pubkey: myPubKey} ); let encryptedForMe = yield; - Crypto.RSAencrypt.async(Crypto, self.cb, symKey, {pubkey: userPubKey} ); + Crypto.wrapKey.async(Crypto, self.cb, bulkKey, + {realm : "tmpWrapID", pubkey: userPubKey} ); let encryptedForYou = yield; - let keyring = { myUserName: encryptedForMe, - username: encryptedForYou }; + let keys = { ring : { }, + bulkIV : bulkIV + }; + keys.ring[myUserName] = encryptedForMe; + keys.ring[username] = encryptedForYou; let keyringFile = new Resource( serverPath + "/" + KEYRING_FILE_NAME ); - keyringFile.put( self.cb, this._json.encode( keyring ) ); + keyringFile.put( self.cb, this._json.encode( keys ) ); yield; // Call Atul's js api for setting htaccess: @@ -393,8 +404,12 @@ BookmarksEngine.prototype = { // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.get(self.cb); - let keyring = yield; - let symKey = keyring[ myUserName ]; + let keys = yield; + // Unwrap (decrypt) the key with the user's private key. + let idRSA = ID.get('WeaveCryptoID'); + let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, + keys.ring[myUserName], idRSA); + let bulkIV = keys.bulkIV; // Get the json-wrapped contents of everything in the folder: let json = this._store._wrapMount( folderNode, myUserName ); /* TODO what does wrapMount do with this username? Should I be passing @@ -402,7 +417,11 @@ BookmarksEngine.prototype = { // Encrypt it with the symkey and put it into the shared-bookmark file. let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); - Crypto.PBEencrypt.async( Crypto, self.cb, json, {password:symKey} ); + let tmpIdentity = { realm : "temp ID", + bulkKey : bulkKey, + bulkIV : bulkIV + }; + Crypto.encryptData.async( Crypto, self.cb, json, tmpIdentity ); let cyphertext = yield; bmkFile.put( self.cb, cyphertext ); yield; @@ -548,14 +567,22 @@ BookmarksEngine.prototype = { // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.get(self.cb); - let keyring = yield; - let symKey = keyring[ myUserName ]; + let keys = yield; + // Unwrap (decrypt) the key with the user's private key. + let idRSA = ID.get('WeaveCryptoID'); + let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, + keys.ring[myUserName], idRSA); + let bulkIV = keys.bulkIV; // Decrypt the contents of the bookmark file with the symmetric key: let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); bmkFile.get(self.cb); let cyphertext = yield; - Crypto.PBEdecrypt.async( Crypto, self.cb, cyphertext, {password:symKey} ); + let tmpIdentity = { realm : "temp ID", + bulkKey : bulkKey, + bulkIV : bulkIV + }; + Crypto.decryptData.async( Crypto, self.cb, cyphertext, tmpIdentity ); let json = yield; // TODO error handling (see what Resource can throw or return...) From 989bf006d88aa4feca04a0459ea994cfd3a573c4 Mon Sep 17 00:00:00 2001 From: Date: Thu, 19 Jun 2008 11:59:52 -0700 Subject: [PATCH 0404/1860] Backed out changeset 7720a1dd564a because the new crypto stuff that it uses hasn't been enabled yet. --- services/sync/modules/engines/bookmarks.js | 59 ++++++---------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 682d9e521897..57c7ea5cbbc3 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -344,39 +344,28 @@ BookmarksEngine.prototype = { this._annoSvc.EXPIRE_NEVER); // Create a new symmetric key, to be used only for encrypting this share. - let tmpIdentity = { realm : "temp ID", - bulkKey : null, - bulkIV : null - }; - Crypto.randomKeyGen.async(Crypto, self.cb, tmpIdentity); - yield; - let bulkKey = tmpIdentity.bulkKey; - let bulkIV = tmpIdentity.bulkIV; - + Crypto.PBEkeygen.async(Crypto, self.cb); + let newSymKey = yield; /* Get public keys for me and the user I'm sharing with. Each user's public key is stored in /user/username/public/pubkey. */ - let idRSA = ID.get('WeaveCryptoID'); - let myPubKey = idRSA.pubkey; + let myPubKeyFile = new Resource("/user/" + myUserName + "/public/pubkey"); + myPubKeyFile.get(self.cb); + let myPubKey = yield; let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); userPubKeyFile.get(self.cb); let userPubKey = yield; /* Create the keyring, containing the sym key encrypted with each of our public keys: */ - Crypto.wrapKey.async(Crypto, self.cb, bulkKey, - {realm : "tmpWrapID", pubkey: myPubKey} ); + Crypto.RSAencrypt.async(Crypto, self.cb, symKey, {pubkey: myPubKey} ); let encryptedForMe = yield; - Crypto.wrapKey.async(Crypto, self.cb, bulkKey, - {realm : "tmpWrapID", pubkey: userPubKey} ); + Crypto.RSAencrypt.async(Crypto, self.cb, symKey, {pubkey: userPubKey} ); let encryptedForYou = yield; - let keys = { ring : { }, - bulkIV : bulkIV - }; - keys.ring[myUserName] = encryptedForMe; - keys.ring[username] = encryptedForYou; + let keyring = { myUserName: encryptedForMe, + username: encryptedForYou }; let keyringFile = new Resource( serverPath + "/" + KEYRING_FILE_NAME ); - keyringFile.put( self.cb, this._json.encode( keys ) ); + keyringFile.put( self.cb, this._json.encode( keyring ) ); yield; // Call Atul's js api for setting htaccess: @@ -404,12 +393,8 @@ BookmarksEngine.prototype = { // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.get(self.cb); - let keys = yield; - // Unwrap (decrypt) the key with the user's private key. - let idRSA = ID.get('WeaveCryptoID'); - let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, - keys.ring[myUserName], idRSA); - let bulkIV = keys.bulkIV; + let keyring = yield; + let symKey = keyring[ myUserName ]; // Get the json-wrapped contents of everything in the folder: let json = this._store._wrapMount( folderNode, myUserName ); /* TODO what does wrapMount do with this username? Should I be passing @@ -417,11 +402,7 @@ BookmarksEngine.prototype = { // Encrypt it with the symkey and put it into the shared-bookmark file. let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); - let tmpIdentity = { realm : "temp ID", - bulkKey : bulkKey, - bulkIV : bulkIV - }; - Crypto.encryptData.async( Crypto, self.cb, json, tmpIdentity ); + Crypto.PBEencrypt.async( Crypto, self.cb, json, {password:symKey} ); let cyphertext = yield; bmkFile.put( self.cb, cyphertext ); yield; @@ -567,22 +548,14 @@ BookmarksEngine.prototype = { // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.get(self.cb); - let keys = yield; - // Unwrap (decrypt) the key with the user's private key. - let idRSA = ID.get('WeaveCryptoID'); - let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, - keys.ring[myUserName], idRSA); - let bulkIV = keys.bulkIV; + let keyring = yield; + let symKey = keyring[ myUserName ]; // Decrypt the contents of the bookmark file with the symmetric key: let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); bmkFile.get(self.cb); let cyphertext = yield; - let tmpIdentity = { realm : "temp ID", - bulkKey : bulkKey, - bulkIV : bulkIV - }; - Crypto.decryptData.async( Crypto, self.cb, cyphertext, tmpIdentity ); + Crypto.PBEdecrypt.async( Crypto, self.cb, cyphertext, {password:symKey} ); let json = yield; // TODO error handling (see what Resource can throw or return...) From 8720add52edfcfc70e2295e8a8ff3603605645d6 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Thu, 19 Jun 2008 13:03:10 -0700 Subject: [PATCH 0405/1860] Minor tweaks for compile errors on Linux. --- services/crypto/WeaveCrypto.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index f49a69f84ff0..af96980eaf00 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -228,6 +228,7 @@ WeaveCrypto::CommonCrypt(const char *input, PRUint32 inputSize, PK11Context *ctx = nsnull; PK11SlotInfo *slot = nsnull; SECItem *ivParam = nsnull; + PRUint32 maxOutputSize; char keyData[aSymmetricKey.Length()]; PRUint32 keyDataSize = sizeof(keyData); @@ -281,7 +282,7 @@ WeaveCrypto::CommonCrypt(const char *input, PRUint32 inputSize, } int tmpOutSize; // XXX retarded. why is an signed int suddenly needed? - PRUint32 maxOutputSize = *outputSize; + maxOutputSize = *outputSize; rv = PK11_CipherOp(ctx, (unsigned char *)output, &tmpOutSize, maxOutputSize, (unsigned char *)input, inputSize); @@ -611,6 +612,7 @@ WeaveCrypto::GenerateRandomKey(nsACString& aEncodedKey) nsresult rv = NS_OK; PRUint32 keySize; CK_MECHANISM_TYPE keygenMech; + SECItem *keydata = nsnull; // XXX doesn't NSS have a lookup function to do this? switch (mAlgorithm) { @@ -651,7 +653,7 @@ WeaveCrypto::GenerateRandomKey(nsACString& aEncodedKey) goto keygen_done; } - SECItem *keydata = PK11_GetKeyData(randKey); + keydata = PK11_GetKeyData(randKey); if (!keydata) { NS_WARNING("PK11_GetKeyData failed"); rv = NS_ERROR_FAILURE; @@ -685,6 +687,7 @@ WeaveCrypto::WrapSymmetricKey(const nsACString& aSymmetricKey, PK11SymKey *symKey = nsnull; SECKEYPublicKey *pubKey = nsnull; CERTSubjectPublicKeyInfo *pubKeyInfo = nsnull; + CK_MECHANISM_TYPE keyMech, wrapMech; // Step 1. Get rid of the base64 encoding on the inputs. @@ -714,7 +717,7 @@ WeaveCrypto::WrapSymmetricKey(const nsACString& aSymmetricKey, } // ImportSymKey wants a mechanism, from which it derives the key type. - CK_MECHANISM_TYPE keyMech = PK11_AlgtagToMechanism(mAlgorithm); + keyMech = PK11_AlgtagToMechanism(mAlgorithm); if (keyMech == CKM_INVALID_MECHANISM) { NS_WARNING("Unknown key mechanism"); rv = NS_ERROR_FAILURE; @@ -757,7 +760,7 @@ WeaveCrypto::WrapSymmetricKey(const nsACString& aSymmetricKey, // Step 4. Wrap the symmetric key with the public key. - CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(SEC_OID_PKCS1_RSA_ENCRYPTION); + wrapMech = PK11_AlgtagToMechanism(SEC_OID_PKCS1_RSA_ENCRYPTION); s = PK11_PubWrapSymKey(wrapMech, pubKey, symKey, &wrappedKey); if (s != SECSuccess) { @@ -802,6 +805,8 @@ WeaveCrypto::UnwrapSymmetricKey(const nsACString& aWrappedSymmetricKey, PK11SymKey *symKey = nsnull; SECKEYPrivateKey *privKey = nsnull; SECItem *ivParam = nsnull; + SECItem *symKeyData = nsnull; + SECItem *keyID = nsnull; CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); @@ -866,7 +871,7 @@ WeaveCrypto::UnwrapSymmetricKey(const nsACString& aWrappedSymmetricKey, // We don't really care about this, because our unwrapped private key will // just live long enough to unwrap the bulk data key. So, we'll just jam in // a random value... We have an IV handy, so that will suffice. - SECItem *keyID = &ivItem; + keyID = &ivItem; privKey = PK11_UnwrapPrivKey(slot, pbeKey, wrapMech, ivParam, &wrappedPrivKey, @@ -904,7 +909,7 @@ WeaveCrypto::UnwrapSymmetricKey(const nsACString& aWrappedSymmetricKey, } // XXX need to free this? - SECItem *symKeyData = PK11_GetKeyData(symKey); + symKeyData = PK11_GetKeyData(symKey); if (!symKeyData) { NS_WARNING("PK11_GetKeyData failed"); rv = NS_ERROR_FAILURE; From de31172c673bf63ef01ad9b8d93a94727c1a8631 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Thu, 19 Jun 2008 14:51:20 -0700 Subject: [PATCH 0406/1860] Apparently NS_IMPL_NSGETMODULE("FOO") doesn't work on Linux, but NS_IMPL_NSGETMODULE(FOO) does. --- services/crypto/WeaveCryptoModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/crypto/WeaveCryptoModule.cpp b/services/crypto/WeaveCryptoModule.cpp index 2aebc64dabd2..16a7315df900 100644 --- a/services/crypto/WeaveCryptoModule.cpp +++ b/services/crypto/WeaveCryptoModule.cpp @@ -51,4 +51,4 @@ static nsModuleComponentInfo components[] = } }; -NS_IMPL_NSGETMODULE("WeaveCryptoModule", components) +NS_IMPL_NSGETMODULE(WeaveCryptoModule, components) From b4799754a7f76f780bbd50629768d3726117c539 Mon Sep 17 00:00:00 2001 From: Date: Thu, 19 Jun 2008 15:23:01 -0700 Subject: [PATCH 0407/1860] Added missing comma after function definition --- services/sync/modules/engines/bookmarks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 57c7ea5cbbc3..86492cb22d10 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -102,7 +102,7 @@ BookmarksEngine.prototype = { this.__annoSvc = Cc["@mozilla.org/browser/annotation-service;1"]. getService(Ci.nsIAnnotationService); return this.__annoSvc; - } + }, _init: function BmkEngine__init( pbeId ) { this.__proto__.__proto__._init.call( this, pbeId ); From 44c0db5fc76bd6b642e92375cb3ad950e55e34d0 Mon Sep 17 00:00:00 2001 From: Date: Thu, 19 Jun 2008 15:25:25 -0700 Subject: [PATCH 0408/1860] Fixed some more missing commas that were preventing engines/bookmarks.js from loading --- services/sync/modules/engines/bookmarks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 86492cb22d10..4c61960542e6 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -432,12 +432,12 @@ BookmarksEngine.prototype = { // Remove the annotations from the local folder: this._annoSvc.setItemAnnotation(folderNode, - SERVER_PATH_ANNO + SERVER_PATH_ANNO, "", 0, this._annoSvc.EXPIRE_NEVER); this._annoSvc.setItemAnnotation(folderNode, - SERVER_PATH_ANNO + OUTGOING_SHARE_ANNO, "", 0, this._annoSvc.EXPIRE_NEVER); @@ -514,7 +514,7 @@ BookmarksEngine.prototype = { this._annoSvc.EXPIRE_NEVER); // Keep track of who shared this folder with us... this._annoSvc.setItemAnnotation(newId, - OUTGOING_SHARED_ANNO + OUTGOING_SHARED_ANNO, user, 0, this._annoSvc.EXPIRE_NEVER); From 5b60a961ece9db03a5a0ff7c84206ccc5ab9c1d3 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Thu, 19 Jun 2008 15:36:24 -0700 Subject: [PATCH 0409/1860] Fix reference to undefined function and bad async arg. --- services/sync/modules/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 4ff0c51a1486..9bd0c85c6d6a 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -506,7 +506,7 @@ RemoteStore.prototype = { snap.version = this.status.data.maxVersion; if (lastSyncSnap.version < this.status.data.snapVersion) { - self.done(yield this.getLatestFromScratch(self.cb)); + self.done(yield this._getLatestFromScratch(this, self.cb)); return; } else if (lastSyncSnap.version >= this.status.data.snapVersion && From 4065f7ed2f64f82d98cb9045f3fa133fcc3edf3e Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Thu, 19 Jun 2008 15:43:50 -0700 Subject: [PATCH 0410/1860] updating wizard --- services/sync/locales/en-US/wizard.dtd | 39 ++++++++++++++----- services/sync/locales/en-US/wizard.properties | 29 +++++++++++++- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 4c1c016eb15a..d4f282d57b0e 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,19 +1,22 @@ - - - + + + + + - - + + + @@ -23,20 +26,36 @@ + + - + - + + + - - - + + + + + + + + + + + + + + + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index fbfea30a2f05..ee121d505254 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -11,7 +11,7 @@ initStatusSyncFailed.label = Status: Transfer Failed invalidCredentials.alert = You must provide a valid Weave user name and password to continue. usernameTaken.label = That username is already taken. Please choose another. -usernameAvailable.label = That username is available. +usernameAvailable.label = Available. loginFailed.label = Please enter a valid username and password. @@ -26,6 +26,33 @@ samePasswordAndPassphrase.alert = Your password and passphrase must be different invalidEmail.label = Invalid email address. emailAlreadyExists.label = Email address already exists. + +emailTaken.label = Email address already exists. +emailInvalid.label = Invalid email address. +emailOk.label = Email address is valid. + missingCaptchaResponse.label = Please enter the words in the Captcha box. incorrectCaptcha.label = Incorrect Captcha response. Try again. internalError.label = Sorry! We had a problem. Please try again. + +initialLogin-progress.label = Signing you in... +initialLogin-done.label = Sign-in successful. +initialLogin-error.label = Problem signing in. + +initialPrefs-progress.label = Setting your preferences... +initialPrefs-done.label = Preferences set. + +initialReset-progress.label = Clearing your default bookmarks... +initialReset-done.label = Default bookmarks cleared. + +initialSync-progress.label = Synchronizing your data... +initialSync-done.label = Sync successful. +initialSync-error.label = Problem syncing data. + +initialLoginFailed.description1 = Our server is having problems and we couldn't log you in. +initialLoginFailed.description2 = to try again, or click "Done" to exit the setup wizard and try again later. + +initialSyncFailed.description1 = Our server is having problems and we couldn't synchronize your data. +initialSyncFailed.description2 = to try again, or click "Done" to exit the setup wizard and try again later. + +tryAgain.text = Click here From 5945149c52c1679adc649280719a300d0d1ea8e2 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 19 Jun 2008 16:03:42 -0700 Subject: [PATCH 0411/1860] Added more fake prefs to the password test to eliminate strict warnings, added logging code to identity.js to aid in debugging. --- services/sync/modules/identity.js | 7 +++++-- services/sync/tests/unit/test_passwords.js | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index a2c28c637a69..19791fbb4a24 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -52,13 +52,16 @@ Utils.lazy(this, 'ID', IDManager); function IDManager() { this._ids = {}; this._aliases = {}; + this._log = Log4Moz.Service.getLogger("Service.Identity"); } IDManager.prototype = { get: function IDMgr_get(name) { - if (this._aliases[name]) + if (name in this._aliases) return this._ids[this._aliases[name]]; - else + if (name in this._ids) return this._ids[name]; + this._log.warn("No identity found for '" + name + "'."); + return null; }, set: function IDMgr_set(name, id) { this._ids[name] = id; diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 270489035953..b5ba0c913dc8 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -8,9 +8,16 @@ let __fakePasswords = { 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} }; +let __fakePrefs = { + "encryption" : "none", + "log.logger.service.crypto" : "Debug", + "log.logger.service.engine" : "Debug", + "log.logger.async" : "Debug" +}; + function run_test() { var fpasses = new FakePasswordService(__fakePasswords); - var fprefs = new FakePrefService({"encryption" : "none"}); + var fprefs = new FakePrefService(__fakePrefs); var fds = new FakeDAVService({}); var fts = new FakeTimerService(); var logStats = initTestLogging(); From 578f34176fd683a1df73244ef0b160df2939857a Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 19 Jun 2008 16:37:53 -0700 Subject: [PATCH 0412/1860] Removed warning message from ID.get() because it's actually normal to pass in an ID that doesn't exist to it, to test for its existence. Though perhaps there should be an exists() method for such a use case instead. --- services/sync/modules/identity.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 19791fbb4a24..f46ceace2d6d 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -42,7 +42,6 @@ const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -52,7 +51,6 @@ Utils.lazy(this, 'ID', IDManager); function IDManager() { this._ids = {}; this._aliases = {}; - this._log = Log4Moz.Service.getLogger("Service.Identity"); } IDManager.prototype = { get: function IDMgr_get(name) { @@ -60,7 +58,6 @@ IDManager.prototype = { return this._ids[this._aliases[name]]; if (name in this._ids) return this._ids[name]; - this._log.warn("No identity found for '" + name + "'."); return null; }, set: function IDMgr_set(name, id) { From 8c3638ef5ebfd0acecb43f92b25bdbb1e2abdc2a Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 19 Jun 2008 17:04:04 -0700 Subject: [PATCH 0413/1860] Modified the way logging works in unit testing; fake testing components now log as part of the 'Testing' logger instead of the root logger. --- services/sync/tests/unit/head_first.js | 27 ++++++++++++------- services/sync/tests/unit/test_passwords.js | 12 +++++++-- .../sync/tests/unit/test_service.log.expected | 4 +-- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 7dce5359baa5..f2cdf0f6c7ff 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -68,7 +68,11 @@ function FakeTimerService() { Utils.makeTimerForCall = self.makeTimerForCall; }; -function initTestLogging() { +function getTestLogger(component) { + return Log4Moz.Service.getLogger("Testing"); +} + +function initTestLogging(level) { Cu.import("resource://weave/log4moz.js"); function LogStats() { @@ -87,8 +91,13 @@ function initTestLogging() { var log = Log4Moz.Service.rootLogger; var logStats = new LogStats(); var appender = new Log4Moz.DumpAppender(logStats); - log.level = Log4Moz.Level.Debug; - appender.level = Log4Moz.Level.Debug; + + if (typeof(level) == "undefined") + level = "Debug"; + getTestLogger().level = Log4Moz.Level[level]; + + log.level = Log4Moz.Level.Trace; + appender.level = Log4Moz.Level.Trace; log.addAppender(appender); return logStats; @@ -123,7 +132,7 @@ function FakePrefService(contents) { FakePrefService.prototype = { _getPref: function fake__getPref(pref) { - Log4Moz.Service.rootLogger.trace("Getting pref: " + pref); + getTestLogger().trace("Getting pref: " + pref); return this.fakeContents[pref]; }, getCharPref: function fake_getCharPref(pref) { @@ -164,13 +173,13 @@ function FakeDAVService(contents) { FakeDAVService.prototype = { PUT: function fake_PUT(path, data, onComplete) { - Log4Moz.Service.rootLogger.info("Putting " + path); + getTestLogger().info("Putting " + path); this.fakeContents[path] = data; makeFakeAsyncFunc({status: 200}).async(this, onComplete); }, GET: function fake_GET(path, onComplete) { - Log4Moz.Service.rootLogger.info("Retrieving " + path); + getTestLogger().info("Retrieving " + path); var result = {status: 404}; if (path in this.fakeContents) result = {status: 200, responseText: this.fakeContents[path]}; @@ -179,7 +188,7 @@ FakeDAVService.prototype = { }, MKCOL: function fake_MKCOL(path, onComplete) { - Log4Moz.Service.rootLogger.info("Creating dir " + path); + getTestLogger().info("Creating dir " + path); makeFakeAsyncFunc(true).async(this, onComplete); } }; @@ -191,8 +200,8 @@ function FakePasswordService(contents) { let self = this; Utils.findPassword = function fake_findPassword(realm, username) { - Log4Moz.Service.rootLogger.trace("Password requested for " + - realm + ":" + username); + getTestLogger().trace("Password requested for " + + realm + ":" + username); if (realm in self.fakeContents && username in self.fakeContents[realm]) return self.fakeContents[realm][username]; else diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index b5ba0c913dc8..5e049d85210c 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -12,7 +12,7 @@ let __fakePrefs = { "encryption" : "none", "log.logger.service.crypto" : "Debug", "log.logger.service.engine" : "Debug", - "log.logger.async" : "Debug" + "log.logger.async" : "Trace" }; function run_test() { @@ -68,9 +68,17 @@ function run_test() { // Make sure the engine can sync. var engine = new passwords.PasswordEngine(); - engine.sync(); + let calledBack = false; + + function cb() { + calledBack = true; + } + + engine.sync(cb); while (fts.processCallback()) {} + + do_check_true(calledBack); do_check_eq(logStats.errorsLogged, 0); do_check_eq(Async.outstandingGenerators, 0); } diff --git a/services/sync/tests/unit/test_service.log.expected b/services/sync/tests/unit/test_service.log.expected index 766883fc7d13..9a35eb81e8bb 100644 --- a/services/sync/tests/unit/test_service.log.expected +++ b/services/sync/tests/unit/test_service.log.expected @@ -3,8 +3,8 @@ Running test: test_login_works Service.Main INFO Weave Sync Service Initializing Service.Main DEBUG Logging in Service.Main INFO Using server URL: https://example.com/user/foo -root INFO Retrieving meta/version -root INFO Retrieving private/privkey +Testing INFO Retrieving meta/version +Testing INFO Retrieving private/privkey Service.Main INFO Weave scheduler disabled 1 of 1 tests passed. *** test finished From e153efe703e15cd7ca902621225706f76d9fde00 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 19 Jun 2008 19:03:10 -0700 Subject: [PATCH 0414/1860] Fixed a bug in the outstanding-callback-warning system and made Generator.throw() and Generator.cont() private methods because no client code was using them and it could introduce bugs in the system if they do; we can revisit making them public again later if we want. --- services/sync/modules/async.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index acb27647cf6b..250b9da4bbfe 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -49,6 +49,7 @@ Cu.import("resource://weave/util.js"); */ let gCurrentId = 0; +let gCurrentCbId = 0; let gOutstandingGenerators = 0; function AsyncException(initFrame, message) { @@ -94,9 +95,15 @@ Generator.prototype = { get cb() { let caller = Components.stack.caller; + let cbId = gCurrentCbId++; this._outstandingCbs++; - this._log.trace(this.name + ": cb generated at:\n" + formatFrame(caller)); - let self = this, cb = function(data) { self.cont(data); }; + this._log.trace(this.name + ": cb-" + cbId + " generated at:\n" + + formatFrame(caller)); + let self = this; + let cb = function(data) { + self._log.trace(self.name + ": cb-" + cbId + " called."); + self._cont(data); + }; cb.parentGenerator = this; return cb; }, @@ -187,7 +194,7 @@ Generator.prototype = { } }, - cont: function AsyncGen_cont(data) { + _cont: function AsyncGen__cont(data) { this._outstandingCbs--; this._log.trace(this.name + ": resuming coroutine."); this._continued = true; @@ -200,7 +207,8 @@ Generator.prototype = { } }, - throw: function AsyncGen_throw(exception) { + _throw: function AsyncGen__throw(exception) { + this._outstandingCbs--; try { this.generator.throw(exception); } catch (e) { if (!(e instanceof StopIteration) || !this._timer) @@ -240,7 +248,7 @@ Generator.prototype = { if (this._exception) { this._log.trace("[" + this.name + "] Propagating exception to parent generator"); - this.onComplete.parentGenerator.throw(this._exception); + this.onComplete.parentGenerator._throw(this._exception); } else { try { this._log.trace("[" + this.name + "] Running onComplete()"); From 707e955a877585a3a549f470f79d6b0f8ef44dbf Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Thu, 19 Jun 2008 20:18:59 -0700 Subject: [PATCH 0415/1860] Small bug fix from my last small bugfix. :( --- services/sync/modules/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 9bd0c85c6d6a..91786e46ddfc 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -506,7 +506,7 @@ RemoteStore.prototype = { snap.version = this.status.data.maxVersion; if (lastSyncSnap.version < this.status.data.snapVersion) { - self.done(yield this._getLatestFromScratch(this, self.cb)); + self.done(yield this._getLatestFromScratch.async(this, self.cb)); return; } else if (lastSyncSnap.version >= this.status.data.snapVersion && From 12584b94e3c11c2ba3183f4ca24146c49943eb67 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 12:24:02 -0700 Subject: [PATCH 0416/1860] Changed log level in a unit test. --- services/sync/tests/unit/test_passwords.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 5e049d85210c..1255beeb92c1 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -12,7 +12,7 @@ let __fakePrefs = { "encryption" : "none", "log.logger.service.crypto" : "Debug", "log.logger.service.engine" : "Debug", - "log.logger.async" : "Trace" + "log.logger.async" : "Debug" }; function run_test() { From 031be0e4d8d8c8167f26fb09862a55ad529b21b9 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 12:27:17 -0700 Subject: [PATCH 0417/1860] Minor code tidying. --- services/sync/modules/crypto.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index e0d34e4c3651..226f597991f9 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -83,8 +83,8 @@ CryptoSvc.prototype = { }, _openssl: function Crypto__openssl() { - let extMgr = Components.classes["@mozilla.org/extensions/manager;1"] - .getService(Components.interfaces.nsIExtensionManager); + let extMgr = Cc["@mozilla.org/extensions/manager;1"] + .getService(Ci.nsIExtensionManager); let loc = extMgr.getInstallLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); let wrap = loc.getItemLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); From a0fae220fa84fc7cc8437e73d482cc74f82b0c27 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 12:34:29 -0700 Subject: [PATCH 0418/1860] Refactoring: consolidated duplicate code creating GUIDs via XPCOM gunk into a new function, Utils.makeGUID(). Note that there are some strange things re: whitespace in engines/bookmarks.js in this changeset; I literally only changed one line, though, and I'm not sure where they came from. Maybe it was js2-mode? --- services/sync/modules/engines/bookmarks.js | 14 ++++++-------- services/sync/modules/stores.js | 7 ++----- services/sync/modules/util.js | 7 +++++++ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 4c61960542e6..f84ce1ed9e45 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -95,7 +95,7 @@ BookmarksEngine.prototype = { this.__tracker = new BookmarksTracker(); return this.__tracker; }, - + __annoSvc: null, get _annoSvc() { if (!this.__anoSvc) @@ -295,7 +295,7 @@ BookmarksEngine.prototype = { }, _updateAllOutgoingShares: function BmkEngine__updateAllOutgoing() { let self = yield; - let shares = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, + let shares = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, {}); for ( let i=0; i < shares.length; i++ ) { /* TODO only update the shares that have changed. Perhaps we can @@ -321,9 +321,7 @@ BookmarksEngine.prototype = { /* Generate a new GUID to use as the new directory name on the server in which we'll store the shared data. */ - let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. - getService(Ci.nsIUUIDGenerator); - let folderGuid = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); + let folderGuid = Utils.makeGUID(); /* Create the directory on the server if it does not exist already. */ let serverPath = "/user/" + myUserName + "/share/" + folderGuid; @@ -419,7 +417,7 @@ BookmarksEngine.prototype = { SERVER_PATH_ANNO ); let username = this._annoSvc.getItemAnnotation( folderNode, OUTGOING_SHARE_ANNO ); - + // Delete the share from the server: let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.delete(self.cb); @@ -429,7 +427,7 @@ BookmarksEngine.prototype = { yield; // TODO this leaves the folder itself in place... is there a way to // get rid of that, say through DAV? - + // Remove the annotations from the local folder: this._annoSvc.setItemAnnotation(folderNode, SERVER_PATH_ANNO, @@ -529,7 +527,7 @@ BookmarksEngine.prototype = { _updateIncomingShare: function BmkEngine__updateIncomingShare(mountData) { /* Pull down bookmarks from the server for a single incoming - shared folder, obliterating whatever was in that folder before. + shared folder, obliterating whatever was in that folder before. mountData is an object that's expected to have member data: userid: weave id of the user sharing the folder with us, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 0a27c4b7c729..4ca580efe2f3 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -137,11 +137,8 @@ SnapshotStore.prototype = { _GUID: null, get GUID() { - if (!this._GUID) { - let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. - getService(Ci.nsIUUIDGenerator); - this._GUID = uuidgen.generateUUID().toString().replace(/[{}]/g, ''); - } + if (!this._GUID) + this._GUID = Utils.makeGUID(); return this._GUID; }, set GUID(GUID) { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index c5a021c20522..c66975b79bb9 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -50,6 +50,13 @@ Cu.import("resource://weave/log4moz.js"); */ let Utils = { + // Generates a brand-new globally unique identifier (GUID). + makeGUID: function makeGUID() { + let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator); + return uuidgen.generateUUID().toString().replace(/[{}]/g, ''); + }, + // Returns a nsILocalFile representing a file relative to the // current user's profile directory. If the argument is a string, // it should be a string with unix-style slashes for directory names From bb1f12759889c5b94238d0fa1443b4b702240c2e Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 12:39:50 -0700 Subject: [PATCH 0419/1860] Added an expected-log for the passwords sync engine unit test. --- services/sync/tests/unit/test_passwords.js | 4 +++ .../tests/unit/test_passwords.log.expected | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 services/sync/tests/unit/test_passwords.log.expected diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 1255beeb92c1..3527774a0039 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -39,6 +39,10 @@ function run_test() { passwordField: "test_password" }; + Utils.makeGUID = function fake_makeGUID() { + return "fake-guid"; + }; + Utils.getLoginManager = function fake_getLoginManager() { // Return a fake nsILoginManager object. return {getAllLogins: function() { return [fakeUser]; }}; diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected new file mode 100644 index 000000000000..31b2292ce3bc --- /dev/null +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -0,0 +1,26 @@ +*** test pending +Service.PasswordEngine INFO Beginning sync +Testing INFO Creating dir user-data/passwords/ +Service.RemoteStore DEBUG Downloading status file +Testing INFO Retrieving user-data/passwords/status.json +Service.PasswordEngine INFO Initial upload to server +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO Putting user-data/passwords/keys.json +Service.Resource DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Testing INFO Putting user-data/passwords/snapshot.json +Service.Resource DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Testing INFO Putting user-data/passwords/deltas.json +Service.Resource DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO Putting user-data/passwords/status.json +Service.Resource DEBUG PUT request successful +Service.RemoteStore INFO Full upload to server successful +Service.SnapStore INFO Saving snapshot to disk +root DEBUG {"version":0,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} +*** test finished +*** exiting +*** PASS *** From 36a90ab50cac18192f9a08af67bfddd63d75f700 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 12:49:25 -0700 Subject: [PATCH 0420/1860] Added more informative output for fake DAV, so that log-based tests are both more self-documenting re: Weave's behavior and serve as more robust test cases. --- services/sync/tests/unit/head_first.js | 6 +++--- services/sync/tests/unit/test_passwords.log.expected | 10 +++++----- services/sync/tests/unit/test_service.log.expected | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index f2cdf0f6c7ff..736e1cf8c12d 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -173,17 +173,17 @@ function FakeDAVService(contents) { FakeDAVService.prototype = { PUT: function fake_PUT(path, data, onComplete) { - getTestLogger().info("Putting " + path); + getTestLogger().info("Putting " + path + " with data: " + data); this.fakeContents[path] = data; makeFakeAsyncFunc({status: 200}).async(this, onComplete); }, GET: function fake_GET(path, onComplete) { - getTestLogger().info("Retrieving " + path); var result = {status: 404}; if (path in this.fakeContents) result = {status: 200, responseText: this.fakeContents[path]}; - + getTestLogger().info("Retrieving " + path + ", returning status " + + result.status); return makeFakeAsyncFunc(result).async(this, onComplete); }, diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index 31b2292ce3bc..4e8d99b3cff5 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -2,21 +2,21 @@ Service.PasswordEngine INFO Beginning sync Testing INFO Creating dir user-data/passwords/ Service.RemoteStore DEBUG Downloading status file -Testing INFO Retrieving user-data/passwords/status.json +Testing INFO Retrieving user-data/passwords/status.json, returning status 404 Service.PasswordEngine INFO Initial upload to server Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO Putting user-data/passwords/keys.json +Testing INFO Putting user-data/passwords/keys.json with data: {"ring":{}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data -Testing INFO Putting user-data/passwords/snapshot.json +Testing INFO Putting user-data/passwords/snapshot.json with data: {"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data -Testing INFO Putting user-data/passwords/deltas.json +Testing INFO Putting user-data/passwords/deltas.json with data: [] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO Putting user-data/passwords/status.json +Testing INFO Putting user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} Service.Resource DEBUG PUT request successful Service.RemoteStore INFO Full upload to server successful Service.SnapStore INFO Saving snapshot to disk diff --git a/services/sync/tests/unit/test_service.log.expected b/services/sync/tests/unit/test_service.log.expected index 9a35eb81e8bb..a1c574502fa3 100644 --- a/services/sync/tests/unit/test_service.log.expected +++ b/services/sync/tests/unit/test_service.log.expected @@ -3,8 +3,8 @@ Running test: test_login_works Service.Main INFO Weave Sync Service Initializing Service.Main DEBUG Logging in Service.Main INFO Using server URL: https://example.com/user/foo -Testing INFO Retrieving meta/version -Testing INFO Retrieving private/privkey +Testing INFO Retrieving meta/version, returning status 200 +Testing INFO Retrieving private/privkey, returning status 200 Service.Main INFO Weave scheduler disabled 1 of 1 tests passed. *** test finished From 0bf4303b86cd77e5cc3ea6848606ce95025a9243 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 13:58:56 -0700 Subject: [PATCH 0421/1860] Made the tracking of async generators/coroutines more robust for debugging purposes. Refactored code in syncCores.js to use Utils.makeTimerForCall(). Improved test_passwords to perform an additional sync after the initial one. --- services/sync/modules/async.js | 31 +++++++++++++++++-- services/sync/modules/syncCores.js | 16 +++------- services/sync/tests/unit/test_async.js | 6 ++-- .../sync/tests/unit/test_async_exceptions.js | 2 +- services/sync/tests/unit/test_passwords.js | 14 ++++++++- .../tests/unit/test_passwords.log.expected | 13 ++++++++ services/sync/tests/unit/test_service.js | 2 +- 7 files changed, 64 insertions(+), 20 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 250b9da4bbfe..b02acc9d9634 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -48,9 +48,32 @@ Cu.import("resource://weave/util.js"); * Asynchronous generator helpers */ +// Simple class to help us keep track of "namable" objects: that is, +// any object with a string property called "name". +function NamableTracker() { + this.__length = 0; + this.__dict = {}; +} + +NamableTracker.prototype = { + get length() { return this.__length; }, + add: function GD_add(item) { + this.__dict[item.name] = item; + this.__length++; + }, + remove: function GD_remove(item) { + delete this.__dict[item.name]; + this.__length--; + }, + __iterator__: function GD_iterator() { + for (name in this.__dict) + yield name; + } +} + let gCurrentId = 0; let gCurrentCbId = 0; -let gOutstandingGenerators = 0; +let gOutstandingGenerators = new NamableTracker(); function AsyncException(initFrame, message) { this.message = message; @@ -73,7 +96,6 @@ AsyncException.prototype = { }; function Generator(thisArg, method, onComplete, args) { - gOutstandingGenerators++; this._outstandingCbs = 0; this._log = Log4Moz.Service.getLogger("Async.Generator"); this._log.level = @@ -83,6 +105,9 @@ function Generator(thisArg, method, onComplete, args) { this._id = gCurrentId++; this.onComplete = onComplete; this._args = args; + + gOutstandingGenerators.add(this); + this._initFrame = Components.stack.caller; // skip our frames // FIXME: we should have a pref for this, for debugging async.js itself @@ -261,7 +286,7 @@ Generator.prototype = { this._log.trace("Initial stack trace:\n" + this.trace); } } - gOutstandingGenerators--; + gOutstandingGenerators.remove(this); } }; diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 461e5d04f2b5..26e14d5200ea 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -93,15 +93,13 @@ SyncCore.prototype = { _detectUpdates: function SC__detectUpdates(a, b) { let self = yield; - let listener = new Utils.EventListener(self.cb); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let cmds = []; try { for (let GUID in a) { - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + Utils.makeTimerForCall(self.cb); yield; // Yield to main loop if (GUID in b) { @@ -121,7 +119,7 @@ SyncCore.prototype = { for (GUID in b) { - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + Utils.makeTimerForCall(self.cb); yield; // Yield to main loop if (GUID in a) @@ -147,7 +145,6 @@ SyncCore.prototype = { throw e; } finally { - timer = null; self.done(cmds); } }, @@ -219,8 +216,6 @@ SyncCore.prototype = { _reconcile: function SC__reconcile(listA, listB) { let self = yield; - let listener = new Utils.EventListener(self.cb); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let propagations = [[], []]; let conflicts = [[], []]; @@ -231,7 +226,7 @@ SyncCore.prototype = { let guidChanges = []; for (let i = 0; i < listA.length; i++) { let a = listA[i]; - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + Utils.makeTimerForCall(self.cb); yield; // Yield to main loop //this._log.debug("comparing " + i + ", listB length: " + listB.length); @@ -273,7 +268,7 @@ SyncCore.prototype = { for (let i = 0; i < listA.length; i++) { for (let j = 0; j < listB.length; j++) { - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + Utils.makeTimerForCall(self.cb); yield; // Yield to main loop if (this._conflicts(listA[i], listB[j]) || @@ -290,13 +285,12 @@ SyncCore.prototype = { this._getPropagations(listA, conflicts[0], propagations[1]); - timer.initWithCallback(listener, 0, timer.TYPE_ONE_SHOT); + Utils.makeTimerForCall(self.cb); yield; // Yield to main loop this._getPropagations(listB, conflicts[1], propagations[0]); ret = {propagations: propagations, conflicts: conflicts}; - timer = null; self.done(ret); }, diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js index 7f68c710a448..06f42c7e2854 100644 --- a/services/sync/tests/unit/test_async.js +++ b/services/sync/tests/unit/test_async.js @@ -40,11 +40,11 @@ function run_test() { do_check_eq(timesYielded, 2); - do_check_eq(Async.outstandingGenerators, 1); - + do_check_eq(Async.outstandingGenerators.length, 1); + do_check_true(fts.processCallback()); do_check_false(fts.processCallback()); - do_check_eq(Async.outstandingGenerators, 0); + do_check_eq(Async.outstandingGenerators.length, 0); } diff --git a/services/sync/tests/unit/test_async_exceptions.js b/services/sync/tests/unit/test_async_exceptions.js index 3fa5c0740f0a..6eebf4ab3448 100644 --- a/services/sync/tests/unit/test_async_exceptions.js +++ b/services/sync/tests/unit/test_async_exceptions.js @@ -47,5 +47,5 @@ function run_test() { runTestGenerator.async({}); for (var i = 0; fts.processCallback(); i++) {} do_check_eq(i, 4); - do_check_eq(Async.outstandingGenerators, 0); + do_check_eq(Async.outstandingGenerators.length, 0); } diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 3527774a0039..d6df74c541a5 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -83,6 +83,18 @@ function run_test() { while (fts.processCallback()) {} do_check_true(calledBack); + calledBack = false; + + getTestLogger().info("Initial sync done, re-syncing now."); + + engine.sync(cb); + + while (fts.processCallback()) {} + + for (name in Async.outstandingGenerators) + getTestLogger().warn("Outstanding generator exists: " + name); + do_check_eq(logStats.errorsLogged, 0); - do_check_eq(Async.outstandingGenerators, 0); + do_check_eq(Async.outstandingGenerators.length, 0); + do_check_true(calledBack); } diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index 4e8d99b3cff5..b9301a061928 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -21,6 +21,19 @@ Service.Resource DEBUG PUT request successful Service.RemoteStore INFO Full upload to server successful Service.SnapStore INFO Saving snapshot to disk root DEBUG {"version":0,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} +Testing INFO Initial sync done, re-syncing now. +Service.PasswordEngine INFO Beginning sync +Testing INFO Creating dir user-data/passwords/ +Service.RemoteStore DEBUG Downloading status file +Testing INFO Retrieving user-data/passwords/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.PasswordEngine INFO Local snapshot version: 0 +Service.PasswordEngine INFO Server maxVersion: 0 +Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) +Service.RemoteStore TRACE Local snapshot version == server maxVersion +Service.PasswordEngine INFO Sync complete: no changes needed on client or server *** test finished *** exiting *** PASS *** diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index 412c8d8c52e7..b072a79f484f 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -68,5 +68,5 @@ function test_login_works() { do_check_true(finished); do_check_true(successful); do_check_eq(logStats.errorsLogged, 0); - do_check_eq(Async.outstandingGenerators, 0); + do_check_eq(Async.outstandingGenerators.length, 0); } From 3e75cc8a28d2630f9143cbe886dfe08217fb7a71 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 14:22:06 -0700 Subject: [PATCH 0422/1860] Improved test_passwords so that it syncs, re-syncs, adds a new user, and re-syncs again. Fixed a strict warning in engines.js. --- services/sync/modules/engines.js | 2 +- services/sync/tests/unit/test_passwords.js | 26 +++++++++++--- .../tests/unit/test_passwords.log.expected | 36 +++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e2627822b401..cf0ad67ee317 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -305,7 +305,7 @@ Engine.prototype = { this._log.info("Reconciling client/server updates"); this._core.reconcile(self.cb, localUpdates, server.updates); - ret = yield; + let ret = yield; let clientChanges = ret.propagations[0]; let serverChanges = ret.propagations[1]; diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index d6df74c541a5..e0774d4659fd 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -39,13 +39,15 @@ function run_test() { passwordField: "test_password" }; + var fakeUsers = [fakeUser]; + Utils.makeGUID = function fake_makeGUID() { return "fake-guid"; }; Utils.getLoginManager = function fake_getLoginManager() { // Return a fake nsILoginManager object. - return {getAllLogins: function() { return [fakeUser]; }}; + return {getAllLogins: function() { return fakeUsers; }}; }; Utils.getProfileFile = function fake_getProfileFile(arg) { @@ -88,13 +90,29 @@ function run_test() { getTestLogger().info("Initial sync done, re-syncing now."); engine.sync(cb); - while (fts.processCallback()) {} + do_check_true(calledBack); + calledBack = false; + + getTestLogger().info("Re-sync done, adding a login and re-syncing."); + + fakeUsers.push( + {hostname: "www.yoogle.com", + formSubmitURL: "http://www.yoogle.com/search", + httpRealm: "", + username: "", + password: "", + usernameField: "test_person2", + passwordField: "test_password2"} + ); + + engine.sync(cb); + while (fts.processCallback()) {} + do_check_true(calledBack); + calledBack = false; for (name in Async.outstandingGenerators) getTestLogger().warn("Outstanding generator exists: " + name); - do_check_eq(logStats.errorsLogged, 0); do_check_eq(Async.outstandingGenerators.length, 0); - do_check_true(calledBack); } diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index b9301a061928..db46e3caff8e 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -34,6 +34,42 @@ Service.PasswordEngine INFO Server maxVersion: 0 Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) Service.RemoteStore TRACE Local snapshot version == server maxVersion Service.PasswordEngine INFO Sync complete: no changes needed on client or server +Testing INFO Re-sync done, adding a login and re-syncing. +Service.PasswordEngine INFO Beginning sync +Testing INFO Creating dir user-data/passwords/ +Service.RemoteStore DEBUG Downloading status file +Testing INFO Retrieving user-data/passwords/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.PasswordEngine INFO Local snapshot version: 0 +Service.PasswordEngine INFO Server maxVersion: 0 +Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) +Service.RemoteStore TRACE Local snapshot version == server maxVersion +Service.PasswordEngine INFO Reconciling client/server updates +Service.PasswordSync DEBUG Reconciling 1 against 0 commands +Service.PasswordEngine INFO Changes for client: 0 +Service.PasswordEngine INFO Predicted changes for server: 1 +Service.PasswordEngine INFO Client conflicts: 0 +Service.PasswordEngine INFO Server conflicts: 0 +Service.PasswordEngine INFO Actual changes for server: 1 +Service.PasswordEngine DEBUG Actual changes for server: [{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}] +Service.PasswordEngine INFO Uploading changes to server +Testing INFO Retrieving user-data/passwords/deltas.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Testing INFO Putting user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}]] +Service.Resource DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO Putting user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} +Service.Resource DEBUG PUT request successful +Service.PasswordEngine INFO Successfully updated deltas and status on server +Service.SnapStore INFO Saving snapshot to disk +root DEBUG {"version":1,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"},"1b3869fc36234b39cd354f661ed1d7d148394ca3":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}} +Service.PasswordEngine INFO Sync complete *** test finished *** exiting *** PASS *** From ca0c17f117302e4f8c1db00fb173e616f1c20b7d Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 20 Jun 2008 14:26:40 -0700 Subject: [PATCH 0423/1860] make xmpp requests be background requests so they don't get horked by load group problems or throw up auth dialogs --- services/sync/modules/xmpp/transportLayer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index 4df4cd5feefd..472313b3ce03 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -256,6 +256,7 @@ HTTPPollingTransport.prototype = { _doPost: function( requestXml ) { var request = this._request; + request.mozBackgroundRequest = true; var callbackObj = this._callbackObject; var self = this; var contents = ""; From 3691c86a4cfff983c689f8b779c70956fceca621 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 15:39:07 -0700 Subject: [PATCH 0424/1860] Refactored test_passwords to make it more modular and readable, changed the formatting of some log messages to be more descriptive. --- services/sync/tests/unit/head_first.js | 6 +- services/sync/tests/unit/test_passwords.js | 195 +++++++++++------- .../tests/unit/test_passwords.log.expected | 46 +++-- .../sync/tests/unit/test_service.log.expected | 4 +- 4 files changed, 154 insertions(+), 97 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 736e1cf8c12d..85ff54cd24a2 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -173,7 +173,7 @@ function FakeDAVService(contents) { FakeDAVService.prototype = { PUT: function fake_PUT(path, data, onComplete) { - getTestLogger().info("Putting " + path + " with data: " + data); + getTestLogger().info("HTTP PUT to " + path + " with data: " + data); this.fakeContents[path] = data; makeFakeAsyncFunc({status: 200}).async(this, onComplete); }, @@ -182,13 +182,13 @@ FakeDAVService.prototype = { var result = {status: 404}; if (path in this.fakeContents) result = {status: 200, responseText: this.fakeContents[path]}; - getTestLogger().info("Retrieving " + path + ", returning status " + + getTestLogger().info("HTTP GET from " + path + ", returning status " + result.status); return makeFakeAsyncFunc(result).async(this, onComplete); }, MKCOL: function fake_MKCOL(path, onComplete) { - getTestLogger().info("Creating dir " + path); + getTestLogger().info("HTTP MKCOL on " + path); makeFakeAsyncFunc(true).async(this, onComplete); } }; diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index e0774d4659fd..17e2ba25dc53 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -3,6 +3,10 @@ Cu.import("resource://weave/async.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); +// ---------------------------------------- +// Fake Data +// ---------------------------------------- + let __fakePasswords = { 'Mozilla Services Password': {foo: "bar"}, 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} @@ -15,55 +19,30 @@ let __fakePrefs = { "log.logger.async" : "Debug" }; -function run_test() { - var fpasses = new FakePasswordService(__fakePasswords); - var fprefs = new FakePrefService(__fakePrefs); - var fds = new FakeDAVService({}); - var fts = new FakeTimerService(); - var logStats = initTestLogging(); +let __fakeUsers = [ + // Fake nsILoginInfo object. + {hostname: "www.boogle.com", + formSubmitURL: "http://www.boogle.com/search", + httpRealm: "", + username: "", + password: "", + usernameField: "test_person", + passwordField: "test_password"} +]; +// ---------------------------------------- +// Test Logic +// ---------------------------------------- + +function run_test() { ID.set('Engine:PBE:default', new Identity('Mozilla Services Encryption Passphrase', 'foo')); // The JS module we're testing, with all members exposed. var passwords = loadInSandbox("resource://weave/engines/passwords.js"); - // Fake nsILoginInfo object. - var fakeUser = { - hostname: "www.boogle.com", - formSubmitURL: "http://www.boogle.com/search", - httpRealm: "", - username: "", - password: "", - usernameField: "test_person", - passwordField: "test_password" - }; - - var fakeUsers = [fakeUser]; - - Utils.makeGUID = function fake_makeGUID() { - return "fake-guid"; - }; - - Utils.getLoginManager = function fake_getLoginManager() { - // Return a fake nsILoginManager object. - return {getAllLogins: function() { return fakeUsers; }}; - }; - - Utils.getProfileFile = function fake_getProfileFile(arg) { - return {exists: function() {return false;}}; - }; - - Utils.open = function fake_open(file, mode) { - let fakeStream = { - writeString: function(data) {Log4Moz.Service.rootLogger.debug(data);}, - close: function() {} - }; - return [fakeStream]; - }; - // Ensure that _hashLoginInfo() works. - var fakeUserHash = passwords._hashLoginInfo(fakeUser); + var fakeUserHash = passwords._hashLoginInfo(__fakeUsers[0]); do_check_eq(typeof fakeUserHash, 'string'); do_check_eq(fakeUserHash.length, 40); @@ -73,46 +52,112 @@ function run_test() { do_check_true(psc._itemExists(fakeUserHash)); // Make sure the engine can sync. - var engine = new passwords.PasswordEngine(); - let calledBack = false; + function freshEngineSync(cb) { + let engine = new passwords.PasswordEngine(); + engine.sync(cb); + }; - function cb() { - calledBack = true; - } + runAndEnsureSuccess("initial sync", freshEngineSync); - engine.sync(cb); + runAndEnsureSuccess("trivial re-sync", freshEngineSync); + __fakeUsers.push({hostname: "www.yoogle.com", + formSubmitURL: "http://www.yoogle.com/search", + httpRealm: "", + username: "", + password: "", + usernameField: "test_person2", + passwordField: "test_password2"}); + + runAndEnsureSuccess("add user and re-sync", freshEngineSync); +} + +// ---------------------------------------- +// Helper Functions +// ---------------------------------------- + +var callbackCalled = false; + +function __makeCallback() { + callbackCalled = false; + return function callback() { + callbackCalled = true; + }; +} + +function runAndEnsureSuccess(name, func) { + getTestLogger().info("Step '" + name + "' starting."); + func(__makeCallback()); while (fts.processCallback()) {} - - do_check_true(calledBack); - calledBack = false; - - getTestLogger().info("Initial sync done, re-syncing now."); - - engine.sync(cb); - while (fts.processCallback()) {} - do_check_true(calledBack); - calledBack = false; - - getTestLogger().info("Re-sync done, adding a login and re-syncing."); - - fakeUsers.push( - {hostname: "www.yoogle.com", - formSubmitURL: "http://www.yoogle.com/search", - httpRealm: "", - username: "", - password: "", - usernameField: "test_person2", - passwordField: "test_password2"} - ); - - engine.sync(cb); - while (fts.processCallback()) {} - do_check_true(calledBack); - calledBack = false; - + do_check_true(callbackCalled); for (name in Async.outstandingGenerators) getTestLogger().warn("Outstanding generator exists: " + name); do_check_eq(logStats.errorsLogged, 0); do_check_eq(Async.outstandingGenerators.length, 0); + getTestLogger().info("Step '" + name + "' succeeded."); } + +// ---------------------------------------- +// Fake Infrastructure +// ---------------------------------------- + +var fpasses = new FakePasswordService(__fakePasswords); +var fprefs = new FakePrefService(__fakePrefs); +var fds = new FakeDAVService({}); +var fts = new FakeTimerService(); +var logStats = initTestLogging(); +var fakeFilesystem = {}; + +Utils.makeGUID = function fake_makeGUID() { + return "fake-guid"; +}; + +Utils.getLoginManager = function fake_getLoginManager() { + // Return a fake nsILoginManager object. + return {getAllLogins: function() { return __fakeUsers; }}; +}; + +Utils.getProfileFile = function fake_getProfileFile(arg) { + return { + exists: function() { + return this._fakeFilename in fakeFilesystem; + }, + _fakeFilename: (typeof(arg) == "object") ? arg.path : arg + }; +}; + +Utils.readStream = function fake_readStream(stream) { + getTestLogger().info("Reading from stream."); + return stream._fakeContents; +}; + +Utils.open = function fake_open(file, mode) { + switch (mode) { + case "<": + mode = "reading"; + break; + case ">": + mode = "writing"; + break; + default: + throw new Error("Unexpected mode: " + mode); + } + + getTestLogger().info("Opening '" + file._fakeFilename + "' for " + + mode + "."); + var contents = ""; + if (file._fakeFilename in fakeFilesystem && mode == "reading") + contents = fakeFilesystem[file._fakeFilename]; + let fakeStream = { + writeString: function(data) { + contents += data; + getTestLogger().info("Writing data to local file '" + + file._fakeFilename +"': " + data); + }, + close: function() { + fakeFilesystem[file._fakeFilename] = contents; + }, + get _fakeContents() { return contents; } + }; + return [fakeStream]; +}; diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index db46e3caff8e..53c7f8275cf0 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -1,31 +1,37 @@ *** test pending +Testing INFO Step 'initial sync' starting. Service.PasswordEngine INFO Beginning sync -Testing INFO Creating dir user-data/passwords/ +Testing INFO HTTP MKCOL on user-data/passwords/ Service.RemoteStore DEBUG Downloading status file -Testing INFO Retrieving user-data/passwords/status.json, returning status 404 +Testing INFO HTTP GET from user-data/passwords/status.json, returning status 404 Service.PasswordEngine INFO Initial upload to server Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO Putting user-data/passwords/keys.json with data: {"ring":{}} +Testing INFO HTTP PUT to user-data/passwords/keys.json with data: {"ring":{}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data -Testing INFO Putting user-data/passwords/snapshot.json with data: {"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} +Testing INFO HTTP PUT to user-data/passwords/snapshot.json with data: {"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data -Testing INFO Putting user-data/passwords/deltas.json with data: [] +Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO Putting user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} Service.Resource DEBUG PUT request successful Service.RemoteStore INFO Full upload to server successful Service.SnapStore INFO Saving snapshot to disk -root DEBUG {"version":0,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} -Testing INFO Initial sync done, re-syncing now. +Testing INFO Opening 'weave/snapshots/passwords.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":0,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} +Testing INFO Step 'initial sync' succeeded. +Testing INFO Step 'trivial re-sync' starting. +Testing INFO Opening 'weave/snapshots/passwords.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk Service.PasswordEngine INFO Beginning sync -Testing INFO Creating dir user-data/passwords/ +Testing INFO HTTP MKCOL on user-data/passwords/ Service.RemoteStore DEBUG Downloading status file -Testing INFO Retrieving user-data/passwords/status.json, returning status 200 +Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 Service.Resource DEBUG GET request successful Service.JsonFilter DEBUG Decoding JSON data Service.RemoteStore DEBUG Downloading status file... done @@ -34,11 +40,15 @@ Service.PasswordEngine INFO Server maxVersion: 0 Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) Service.RemoteStore TRACE Local snapshot version == server maxVersion Service.PasswordEngine INFO Sync complete: no changes needed on client or server -Testing INFO Re-sync done, adding a login and re-syncing. +Testing INFO Step 'trivial re-sync' succeeded. +Testing INFO Step 'add user and re-sync' starting. +Testing INFO Opening 'weave/snapshots/passwords.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk Service.PasswordEngine INFO Beginning sync -Testing INFO Creating dir user-data/passwords/ +Testing INFO HTTP MKCOL on user-data/passwords/ Service.RemoteStore DEBUG Downloading status file -Testing INFO Retrieving user-data/passwords/status.json, returning status 200 +Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 Service.Resource DEBUG GET request successful Service.JsonFilter DEBUG Decoding JSON data Service.RemoteStore DEBUG Downloading status file... done @@ -55,21 +65,23 @@ Service.PasswordEngine INFO Server conflicts: 0 Service.PasswordEngine INFO Actual changes for server: 1 Service.PasswordEngine DEBUG Actual changes for server: [{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}] Service.PasswordEngine INFO Uploading changes to server -Testing INFO Retrieving user-data/passwords/deltas.json, returning status 200 +Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 Service.Resource DEBUG GET request successful Service.CryptoFilter DEBUG Decrypting data Service.JsonFilter DEBUG Decoding JSON data Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data -Testing INFO Putting user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}]] +Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}]] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO Putting user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} Service.Resource DEBUG PUT request successful Service.PasswordEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk -root DEBUG {"version":1,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"},"1b3869fc36234b39cd354f661ed1d7d148394ca3":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}} +Testing INFO Opening 'weave/snapshots/passwords.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":1,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"},"1b3869fc36234b39cd354f661ed1d7d148394ca3":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}} Service.PasswordEngine INFO Sync complete +Testing INFO Step 'add user and re-sync' succeeded. *** test finished *** exiting *** PASS *** diff --git a/services/sync/tests/unit/test_service.log.expected b/services/sync/tests/unit/test_service.log.expected index a1c574502fa3..f41afc67fa11 100644 --- a/services/sync/tests/unit/test_service.log.expected +++ b/services/sync/tests/unit/test_service.log.expected @@ -3,8 +3,8 @@ Running test: test_login_works Service.Main INFO Weave Sync Service Initializing Service.Main DEBUG Logging in Service.Main INFO Using server URL: https://example.com/user/foo -Testing INFO Retrieving meta/version, returning status 200 -Testing INFO Retrieving private/privkey, returning status 200 +Testing INFO HTTP GET from meta/version, returning status 200 +Testing INFO HTTP GET from private/privkey, returning status 200 Service.Main INFO Weave scheduler disabled 1 of 1 tests passed. *** test finished From 0dec0f51e26e7beb02fca05d19ef16cdc6cd4d04 Mon Sep 17 00:00:00 2001 From: Date: Fri, 20 Jun 2008 16:46:38 -0700 Subject: [PATCH 0425/1860] fixed some minor formatting --- services/sync/modules/xmpp/xmppClient.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index a16d9ea6f8d0..d25d315d69e9 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -95,7 +95,7 @@ XmppClient.prototype = { onIncomingData: function( messageText ) { this._log.debug("onIncomingData(): rcvd: " + messageText); var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); - + // Handle server disconnection if (messageText.match("^$")) { this._handleServerDisconnection(); @@ -236,7 +236,7 @@ XmppClient.prototype = { case "set": /* Someone is telling us to set the value of a variable. Delegate this to the registered iqResponder; we can reply - either with an empty iq type="result" stanza, or else an + either with an empty iq type="result" stanza, or else an iq type="error" stanza */ var variable = iqElem.firstChild.firstChild.getAttribute( "var" ); var newValue = iqElem.firstChild.firstChildgetAttribute( "value" ); @@ -286,14 +286,14 @@ XmppClient.prototype = { }, registerMessageHandler: function( handlerObject ) { - /* messageHandler object must have + /* messageHandler object must have handle( messageText, from ) method. */ this._messageHandlers.push( handlerObject ); }, registerIQResponder: function( handlerObject ) { - /* IQResponder object must have + /* IQResponder object must have .get( variable ) and .set( variable, newvalue ) methods. */ this._iqResponders.push( handlerObject ); @@ -312,7 +312,7 @@ XmppClient.prototype = { } this._transportLayer.connect(); this._transportLayer.setCallbackObject( this ); - this._transportLayer.send( this._makeHeaderXml( host ) ); + this._transportLayer.send( this._makeHeaderXml( host ) ); this._connectionStatus = this.CALLED_SERVER; // Now we wait... the rest of the protocol will be driven by // onIncomingData. @@ -346,7 +346,7 @@ XmppClient.prototype = { exchange: I send an containing a query, and get back an containing the answer to my query. I can also send an to set a value - remotely. The recipient answers with either or + remotely. The recipient answers with either or , with an id matching the id of my set or get. */ //Useful!! From bb08edfab8e96d728dd1606fee764bb6d2eab294 Mon Sep 17 00:00:00 2001 From: Date: Fri, 20 Jun 2008 17:19:10 -0700 Subject: [PATCH 0426/1860] The XMPP server is now configured to use LDAP for authentication (on sm-labs01, and soon to be on services.mozilla too) so the xmpp username/password will now be the same as the weave username/password. So I now use those to login, and I got rid of the two extra preferences which we used to have for xmpp username/password. --- services/sync/modules/engines/bookmarks.js | 19 +++++++++---------- services/sync/services-sync.js | 2 -- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f84ce1ed9e45..7d1246e50ae1 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -58,6 +58,7 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/identity.js"); /* LONGTERM TODO: when we start working on the ability to share other types of data besides bookmarks, the xmppClient instance should be moved to hang @@ -120,18 +121,16 @@ BookmarksEngine.prototype = { let serverUrl = Utils.prefs.getCharPref( "xmpp.server.url" ); let realm = Utils.prefs.getCharPref( "xmpp.server.realm" ); - // TODO once we have ejabberd talking to LDAP, the username/password - // for xmpp will be the same as the ones for Weave itself, so we can - // read username/password like this: - // let clientName = ID.get('WeaveID').username; - // let clientPassword = ID.get('WeaveID').password; - // until then get these from preferences as well: - let clientName = Utils.prefs.getCharPref( "xmpp.client.name" ); - let clientPassword = Utils.prefs.getCharPref( "xmpp.client.password" ); + /* Username/password for XMPP are the same; as the ones for Weave, + so read them from the weave identity: */ + let clientName = ID.get('WeaveID').username; + let clientPassword = ID.get('WeaveID').password; + let transport = new HTTPPollingTransport( serverUrl, false, 15000 ); let auth = new PlainAuthenticator(); - // TODO use MD5Authenticator instead once we get it working -- plain is - // a security hole. + /* LONGTERM TODO would prefer to use MD5Authenticator instead, + once we get it working, but since we are connecting over SSL, the + Plain auth is probably fine for now. */ this._xmppClient = new XmppClient( clientName, realm, clientPassword, diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 3c12de390b92..d815f62b0931 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -36,5 +36,3 @@ pref("extensions.weave.xmpp.enabled", true); pref("extensions.weave.xmpp.server.url", "https://sm-labs01.mozilla.org:81/xmpp"); pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); -pref("extensions.weave.xmpp.client.name", ""); -pref("extensions.weave.xmpp.client.password", ""); From d9db94ccb57a9737f836993728513e5db0d8fbd9 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 17:47:32 -0700 Subject: [PATCH 0427/1860] Factored out the fake filesystem related functions in test_passwords.js into a FakeFilesystemService class in head.js. --- services/sync/tests/unit/head_first.js | 52 ++++++++++++++++++++++ services/sync/tests/unit/test_passwords.js | 47 +------------------ 2 files changed, 53 insertions(+), 46 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 85ff54cd24a2..7864ba09d6e8 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -208,3 +208,55 @@ function FakePasswordService(contents) { return null; }; }; + +function FakeFilesystemService(contents) { + this.fakeContents = contents; + + let self = this; + + Utils.getProfileFile = function fake_getProfileFile(arg) { + let fakeNsILocalFile = { + exists: function() { + return this._fakeFilename in self.fakeContents; + }, + _fakeFilename: (typeof(arg) == "object") ? arg.path : arg + }; + return fakeNsILocalFile; + }; + + Utils.readStream = function fake_readStream(stream) { + getTestLogger().info("Reading from stream."); + return stream._fakeData; + }; + + Utils.open = function fake_open(file, mode) { + switch (mode) { + case "<": + mode = "reading"; + break; + case ">": + mode = "writing"; + break; + default: + throw new Error("Unexpected mode: " + mode); + } + + getTestLogger().info("Opening '" + file._fakeFilename + "' for " + + mode + "."); + var contents = ""; + if (file._fakeFilename in self.fakeContents && mode == "reading") + contents = self.fakeContents[file._fakeFilename]; + let fakeStream = { + writeString: function(data) { + contents += data; + getTestLogger().info("Writing data to local file '" + + file._fakeFilename +"': " + data); + }, + close: function() { + self.fakeContents[file._fakeFilename] = contents; + }, + get _fakeData() { return contents; } + }; + return [fakeStream]; + }; +}; diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 17e2ba25dc53..b51ff1d67fa7 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -106,7 +106,7 @@ var fprefs = new FakePrefService(__fakePrefs); var fds = new FakeDAVService({}); var fts = new FakeTimerService(); var logStats = initTestLogging(); -var fakeFilesystem = {}; +var ffs = new FakeFilesystemService({}); Utils.makeGUID = function fake_makeGUID() { return "fake-guid"; @@ -116,48 +116,3 @@ Utils.getLoginManager = function fake_getLoginManager() { // Return a fake nsILoginManager object. return {getAllLogins: function() { return __fakeUsers; }}; }; - -Utils.getProfileFile = function fake_getProfileFile(arg) { - return { - exists: function() { - return this._fakeFilename in fakeFilesystem; - }, - _fakeFilename: (typeof(arg) == "object") ? arg.path : arg - }; -}; - -Utils.readStream = function fake_readStream(stream) { - getTestLogger().info("Reading from stream."); - return stream._fakeContents; -}; - -Utils.open = function fake_open(file, mode) { - switch (mode) { - case "<": - mode = "reading"; - break; - case ">": - mode = "writing"; - break; - default: - throw new Error("Unexpected mode: " + mode); - } - - getTestLogger().info("Opening '" + file._fakeFilename + "' for " + - mode + "."); - var contents = ""; - if (file._fakeFilename in fakeFilesystem && mode == "reading") - contents = fakeFilesystem[file._fakeFilename]; - let fakeStream = { - writeString: function(data) { - contents += data; - getTestLogger().info("Writing data to local file '" + - file._fakeFilename +"': " + data); - }, - close: function() { - fakeFilesystem[file._fakeFilename] = contents; - }, - get _fakeContents() { return contents; } - }; - return [fakeStream]; -}; From 80d1486e23e2707d8b2b7f3c0f30d893647cc4e0 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 18:04:59 -0700 Subject: [PATCH 0428/1860] test_passwords now also removes a user after doing everything else. --- services/sync/tests/unit/test_passwords.js | 4 ++ .../tests/unit/test_passwords.log.expected | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index b51ff1d67fa7..3414a3f72aae 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -70,6 +70,10 @@ function run_test() { passwordField: "test_password2"}); runAndEnsureSuccess("add user and re-sync", freshEngineSync); + + __fakeUsers.pop(); + + runAndEnsureSuccess("remove user and re-sync", freshEngineSync); } // ---------------------------------------- diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index 53c7f8275cf0..8c6bee0edf41 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -82,6 +82,47 @@ Testing INFO Opening 'weave/snapshots/passwords.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":1,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"},"1b3869fc36234b39cd354f661ed1d7d148394ca3":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}} Service.PasswordEngine INFO Sync complete Testing INFO Step 'add user and re-sync' succeeded. +Testing INFO Step 'remove user and re-sync' starting. +Testing INFO Opening 'weave/snapshots/passwords.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.PasswordEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/passwords/ +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.PasswordEngine INFO Local snapshot version: 1 +Service.PasswordEngine INFO Server maxVersion: 1 +Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) +Service.RemoteStore TRACE Local snapshot version == server maxVersion +Service.PasswordEngine INFO Reconciling client/server updates +Service.PasswordSync DEBUG Reconciling 1 against 0 commands +Service.PasswordEngine INFO Changes for client: 0 +Service.PasswordEngine INFO Predicted changes for server: 1 +Service.PasswordEngine INFO Client conflicts: 0 +Service.PasswordEngine INFO Server conflicts: 0 +Service.PasswordEngine INFO Actual changes for server: 1 +Service.PasswordEngine DEBUG Actual changes for server: [{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}] +Service.PasswordEngine INFO Uploading changes to server +Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}],[{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}]] +Service.Resource DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} +Service.Resource DEBUG PUT request successful +Service.PasswordEngine INFO Successfully updated deltas and status on server +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/passwords.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} +Service.PasswordEngine INFO Sync complete +Testing INFO Step 'remove user and re-sync' succeeded. *** test finished *** exiting *** PASS *** From 02b74d96a4dfccd7dbe8c187554c0b0cf97b3c6b Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 18:25:21 -0700 Subject: [PATCH 0429/1860] Fake GUID generator now provides a different yet deterministic GUID upon each invocation. --- services/sync/tests/unit/test_passwords.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 3414a3f72aae..47773958e134 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -111,10 +111,15 @@ var fds = new FakeDAVService({}); var fts = new FakeTimerService(); var logStats = initTestLogging(); var ffs = new FakeFilesystemService({}); +var fgs = new FakeGUIDService(); -Utils.makeGUID = function fake_makeGUID() { - return "fake-guid"; -}; +function FakeGUIDService() { + let latestGUID = 0; + + Utils.makeGUID = function fake_makeGUID() { + return "fake-guid-" + latestGUID++; + }; +} Utils.getLoginManager = function fake_getLoginManager() { // Return a fake nsILoginManager object. From 0697ba7db50f534c5d51968dbd99ccf8e77291d1 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 18:36:11 -0700 Subject: [PATCH 0430/1860] Oops, forgot to recommit the canonical log for test_passwords in my last commit. --- services/sync/tests/unit/test_passwords.log.expected | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index 8c6bee0edf41..b5e9fb1117a0 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -17,12 +17,12 @@ Service.CryptoFilter DEBUG Encrypting data Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} Service.Resource DEBUG PUT request successful Service.RemoteStore INFO Full upload to server successful Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/passwords.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":0,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} +Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":0,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} Testing INFO Step 'initial sync' succeeded. Testing INFO Step 'trivial re-sync' starting. Testing INFO Opening 'weave/snapshots/passwords.json' for reading. @@ -74,12 +74,12 @@ Service.CryptoFilter DEBUG Encrypting data Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}]] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} Service.Resource DEBUG PUT request successful Service.PasswordEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/passwords.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":1,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"},"1b3869fc36234b39cd354f661ed1d7d148394ca3":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}} +Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":1,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"},"1b3869fc36234b39cd354f661ed1d7d148394ca3":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}} Service.PasswordEngine INFO Sync complete Testing INFO Step 'add user and re-sync' succeeded. Testing INFO Step 'remove user and re-sync' starting. @@ -115,12 +115,12 @@ Service.CryptoFilter DEBUG Encrypting data Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}],[{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}]] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid","formatVersion":2,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} Service.Resource DEBUG PUT request successful Service.PasswordEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/passwords.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} +Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} Service.PasswordEngine INFO Sync complete Testing INFO Step 'remove user and re-sync' succeeded. *** test finished From 88c8d3306892b74e3772d4a97f68ca97320ea9c8 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Fri, 20 Jun 2008 18:36:33 -0700 Subject: [PATCH 0431/1860] A tiny bit more refactoring to test_passwords. --- services/sync/tests/unit/test_passwords.js | 37 ++++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 47773958e134..be9e9b4f21e7 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -19,7 +19,7 @@ let __fakePrefs = { "log.logger.async" : "Debug" }; -let __fakeUsers = [ +let __fakeLogins = [ // Fake nsILoginInfo object. {hostname: "www.boogle.com", formSubmitURL: "http://www.boogle.com/search", @@ -42,7 +42,7 @@ function run_test() { var passwords = loadInSandbox("resource://weave/engines/passwords.js"); // Ensure that _hashLoginInfo() works. - var fakeUserHash = passwords._hashLoginInfo(__fakeUsers[0]); + var fakeUserHash = passwords._hashLoginInfo(__fakeLogins[0]); do_check_eq(typeof fakeUserHash, 'string'); do_check_eq(fakeUserHash.length, 40); @@ -61,17 +61,19 @@ function run_test() { runAndEnsureSuccess("trivial re-sync", freshEngineSync); - __fakeUsers.push({hostname: "www.yoogle.com", - formSubmitURL: "http://www.yoogle.com/search", - httpRealm: "", - username: "", - password: "", - usernameField: "test_person2", - passwordField: "test_password2"}); + fakeLoginManager.fakeLogins.push( + {hostname: "www.yoogle.com", + formSubmitURL: "http://www.yoogle.com/search", + httpRealm: "", + username: "", + password: "", + usernameField: "test_person2", + passwordField: "test_password2"} + ); runAndEnsureSuccess("add user and re-sync", freshEngineSync); - __fakeUsers.pop(); + fakeLoginManager.fakeLogins.pop(); runAndEnsureSuccess("remove user and re-sync", freshEngineSync); } @@ -112,6 +114,7 @@ var fts = new FakeTimerService(); var logStats = initTestLogging(); var ffs = new FakeFilesystemService({}); var fgs = new FakeGUIDService(); +var fakeLoginManager = new FakeLoginManager(__fakeLogins); function FakeGUIDService() { let latestGUID = 0; @@ -121,7 +124,13 @@ function FakeGUIDService() { }; } -Utils.getLoginManager = function fake_getLoginManager() { - // Return a fake nsILoginManager object. - return {getAllLogins: function() { return __fakeUsers; }}; -}; +function FakeLoginManager(fakeLogins) { + this.fakeLogins = fakeLogins; + + let self = this; + + Utils.getLoginManager = function fake_getLoginManager() { + // Return a fake nsILoginManager object. + return {getAllLogins: function() { return self.fakeLogins; }}; + }; +} From afda6336185a9bcc26c9ec01924afca38e0b67cf Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Sun, 22 Jun 2008 14:43:09 -0700 Subject: [PATCH 0432/1860] Add Makefile support for building on ARM, move comment that triggers bug on scratchbox's Make 3.80 --- services/crypto/Makefile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index d83052475b01..bebd6e46d3ee 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -79,8 +79,12 @@ else ifeq ($(machine), ppc) # FIXME: verify arch = ppc else - # FIXME: x86_64, ia64, sparc, Alpha - $(error Sorry, your arch is unknown/unsupported: $(machine)) + ifeq ($(machine), arm) + arch = arm + else + # FIXME: x86_64, ia64, sparc, Alpha + $(error Sorry, your arch is unknown/unsupported: $(machine)) + endif endif endif endif @@ -92,7 +96,8 @@ platform = $(os)_$(arch)-$(compiler) idl = IWeaveCrypto.idl cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp -target = WeaveCrypto # will have .so / .dylib / .dll appended +# will have .so / .dylib / .dll appended +target = WeaveCrypto sdkdir ?= ${MOZSDKDIR} destdir = .. From bca801a95a24fa9b193778edbb528d1a5064625c Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 23 Jun 2008 14:13:46 -0700 Subject: [PATCH 0433/1860] Moved the FakeGUIDService from test_passwords.js to head.js. --- services/sync/tests/unit/head_first.js | 8 ++++++++ services/sync/tests/unit/test_passwords.js | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 7864ba09d6e8..ba4895471d41 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -260,3 +260,11 @@ function FakeFilesystemService(contents) { return [fakeStream]; }; }; + +function FakeGUIDService() { + let latestGUID = 0; + + Utils.makeGUID = function fake_makeGUID() { + return "fake-guid-" + latestGUID++; + }; +} diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index be9e9b4f21e7..9deddd6b30d3 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -116,14 +116,6 @@ var ffs = new FakeFilesystemService({}); var fgs = new FakeGUIDService(); var fakeLoginManager = new FakeLoginManager(__fakeLogins); -function FakeGUIDService() { - let latestGUID = 0; - - Utils.makeGUID = function fake_makeGUID() { - return "fake-guid-" + latestGUID++; - }; -} - function FakeLoginManager(fakeLogins) { this.fakeLogins = fakeLogins; From 0fc2b8a9c6cd3f51739130d8c6a0119c2ad3382d Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 23 Jun 2008 14:32:55 -0700 Subject: [PATCH 0434/1860] Removed a line of code that was raising a strict warning, and which also just defined an unused local variable that called no functions and therefore had no side effects (unless there were property getters that had side-effects involved, which hopefully wasn't the case). --- services/sync/modules/remote.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 91786e46ddfc..42a07705cb0d 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -292,7 +292,6 @@ CryptoFilter.prototype = { this._log.debug("Decrypting data"); if (!this._remote.status.data) throw "Remote status must be initialized before crypto filter can be used" - let alg = this._remote.status.data[this._algProp]; Crypto.PBEdecrypt.async(Crypto, self.cb, data, this._remote.engineId); let ret = yield; self.done(ret); From 50b702afc05e13198cc14f641b3609d0c6239ab4 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 23 Jun 2008 14:45:58 -0700 Subject: [PATCH 0435/1860] Added 'resync on second computer' step to test_passwords. --- services/sync/tests/unit/test_passwords.js | 16 ++++++- .../tests/unit/test_passwords.log.expected | 42 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 9deddd6b30d3..f5054a1aee6f 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -76,6 +76,11 @@ function run_test() { fakeLoginManager.fakeLogins.pop(); runAndEnsureSuccess("remove user and re-sync", freshEngineSync); + + fakeFilesystem.fakeContents = {}; + fakeLoginManager.fakeLogins = []; + + runAndEnsureSuccess("resync on second computer", freshEngineSync); } // ---------------------------------------- @@ -112,7 +117,7 @@ var fprefs = new FakePrefService(__fakePrefs); var fds = new FakeDAVService({}); var fts = new FakeTimerService(); var logStats = initTestLogging(); -var ffs = new FakeFilesystemService({}); +var fakeFilesystem = new FakeFilesystemService({}); var fgs = new FakeGUIDService(); var fakeLoginManager = new FakeLoginManager(__fakeLogins); @@ -123,6 +128,13 @@ function FakeLoginManager(fakeLogins) { Utils.getLoginManager = function fake_getLoginManager() { // Return a fake nsILoginManager object. - return {getAllLogins: function() { return self.fakeLogins; }}; + return { + getAllLogins: function() { return self.fakeLogins; }, + addLogin: function(login) { + getTestLogger().info("nsILoginManager.addLogin() called " + + "with hostname '" + login.hostname + "'."); + self.fakeLogins.push(login); + } + }; }; } diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index b5e9fb1117a0..e86489631f90 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -123,6 +123,48 @@ Testing INFO Opening 'weave/snapshots/passwords.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} Service.PasswordEngine INFO Sync complete Testing INFO Step 'remove user and re-sync' succeeded. +Testing INFO Step 'resync on second computer' starting. +Service.PasswordEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/passwords/ +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.PasswordEngine DEBUG Remote/local sync GUIDs do not match. Forcing initial sync. +Service.PasswordEngine INFO Local snapshot version: -1 +Service.PasswordEngine INFO Server maxVersion: 2 +Service.RemoteStore INFO Downloading all server data from scratch +Service.RemoteStore DEBUG Downloading server snapshot +Testing INFO HTTP GET from user-data/passwords/snapshot.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading server deltas +Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Service.SnapStore TRACE Processing command: {"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}} +Service.SnapStore TRACE Processing command: {"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]} +Service.PasswordEngine INFO Reconciling client/server updates +Service.PasswordSync DEBUG Reconciling 0 against 1 commands +Service.PasswordEngine INFO Changes for client: 1 +Service.PasswordEngine INFO Predicted changes for server: 0 +Service.PasswordEngine INFO Client conflicts: 0 +Service.PasswordEngine INFO Server conflicts: 0 +Service.PasswordEngine INFO Applying changes locally +Service.SnapStore TRACE Processing command: {"action":"create","GUID":"805ec58eb8dcded602999967e139be21acd0f194","depth":0,"parents":[],"data":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} +Service.PasswordStore TRACE Processing command: {"action":"create","GUID":"805ec58eb8dcded602999967e139be21acd0f194","depth":0,"parents":[],"data":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} +Service.PasswordStore INFO PasswordStore got createCommand: [object Object] +Testing INFO nsILoginManager.addLogin() called with hostname 'www.boogle.com'. +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/passwords.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} +Service.PasswordEngine INFO Actual changes for server: 0 +Service.PasswordEngine DEBUG Actual changes for server: [] +Service.PasswordEngine INFO Sync complete +Testing INFO Step 'resync on second computer' succeeded. *** test finished *** exiting *** PASS *** From 3e164efe86d7298f609a93d33e6997826536fb01 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Mon, 23 Jun 2008 15:48:01 -0700 Subject: [PATCH 0436/1860] work in progress, push for wizard --- services/sync/locales/en-US/wizard.dtd | 102 ++++++++++-------- services/sync/locales/en-US/wizard.properties | 56 +++++----- 2 files changed, 86 insertions(+), 72 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index d4f282d57b0e..785f9a5430e8 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,61 +1,79 @@ + + + + - - - - - - + + + + + + + + + + + + - - - - - - - + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + - + + - - - - - - - - - + + + + + + + + - - + - - - - + + + + - + + + + - - + + - + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index ee121d505254..13e383b6c2ca 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,39 +1,34 @@ -verifyStatusUnverified.label = Status: Unverified -verifyStatusVerifying.label = Status: Verifying account... -verifyStatusLoginVerified.label = Status: Account Verified -verifyStatusLoginFailed.label = Status: Authentication Failed +verifyStatusUnverified.label = Unverified +verifyStatusVerifying.label = Verifying username and password +verifyStatusLoginVerified.label = Username and password verified +verifyStatusLoginFailed.label = Incorrect username or password -initStatusReadyToSync.label = Status: Ready to Transfer Data -initStatusSyncing.label = Status: Transferring Data... -initStatusSyncComplete.label = Status: Transfer Complete -initStatusSyncFailed.label = Status: Transfer Failed invalidCredentials.alert = You must provide a valid Weave user name and password to continue. +missingUsername.label = Please enter all fields. -usernameTaken.label = That username is already taken. Please choose another. -usernameAvailable.label = Available. -loginFailed.label = Please enter a valid username and password. +checkingUsername.label = Checking if it's available... +usernameTaken.label = That username is already taken. Please choose another. +usernameAvailable.label = Available. +usernameUnverified.label = Unverified -requiredFields.label = Please enter all required fields. +loginFailed.label = Please enter a valid username and password. -passwordsUnmatched.label = Passwords don't match. Please re-enter. -passphrasesUnmatched.label = Passphrases don't match. Please re-enter. +requiredFields.label = Please enter all required fields. -noPassphrase.alert = You must enter a passphrase +passphrasesUnmatched.label = Passphrases do not match. samePasswordAndPassphrase.label = Your password and passphrase must be different -samePasswordAndPassphrase.alert = Your password and passphrase must be different -invalidEmail.label = Invalid email address. -emailAlreadyExists.label = Email address already exists. +emailUnverified.label = Unverified +checkingEmail.label = Checking your email address. +emailTaken.label = Email address already exists. +emailInvalid.label = Invalid email address. +emailOk.label = Email address is valid. -emailTaken.label = Email address already exists. -emailInvalid.label = Invalid email address. -emailOk.label = Email address is valid. - -missingCaptchaResponse.label = Please enter the words in the Captcha box. -incorrectCaptcha.label = Incorrect Captcha response. Try again. -internalError.label = Sorry! We had a problem. Please try again. +missingCaptchaResponse.label = Please enter the words in the Captcha box. +incorrectCaptcha.label = Incorrect Captcha response. Try again. +internalError.label = Sorry! We had a problem. Please try again. initialLogin-progress.label = Signing you in... initialLogin-done.label = Sign-in successful. @@ -49,10 +44,11 @@ initialSync-progress.label = Synchronizing your data... initialSync-done.label = Sync successful. initialSync-error.label = Problem syncing data. -initialLoginFailed.description1 = Our server is having problems and we couldn't log you in. -initialLoginFailed.description2 = to try again, or click "Done" to exit the setup wizard and try again later. -initialSyncFailed.description1 = Our server is having problems and we couldn't synchronize your data. -initialSyncFailed.description2 = to try again, or click "Done" to exit the setup wizard and try again later. +tryAgain.text = Click here -tryAgain.text = Click here + +data-verify.title = Data (Step 3 of 4) +data-create.title = Data (Step 5 of 6) +final-verify.title = Finish (Step 4 of 4) +final-create.title = Finish (Step 6 of 6) From f71d6590082950848cba0c6a5d159f770cd655fd Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 23 Jun 2008 16:10:31 -0700 Subject: [PATCH 0437/1860] bug 435341: sync automatically when Firefox quits --- services/sync/locales/en-US/status.dtd | 2 + services/sync/locales/en-US/status.properties | 3 ++ services/sync/modules/service.js | 40 +++++++++++++++---- services/sync/services-sync.js | 2 + 4 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 services/sync/locales/en-US/status.dtd create mode 100644 services/sync/locales/en-US/status.properties diff --git a/services/sync/locales/en-US/status.dtd b/services/sync/locales/en-US/status.dtd new file mode 100644 index 000000000000..7680899e0785 --- /dev/null +++ b/services/sync/locales/en-US/status.dtd @@ -0,0 +1,2 @@ + + diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties new file mode 100644 index 000000000000..41b71288045d --- /dev/null +++ b/services/sync/locales/en-US/status.properties @@ -0,0 +1,3 @@ +status.active = Syncing with Weave... +status.success = Sync succeeded. +status.error = Sync failed. diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c7c004b931f3..c0f1424dfce4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -132,6 +132,7 @@ function WeaveSvc(engines) { // Other misc startup Utils.prefs.addObserver("", this, false); + this._os.addObserver(this, "quit-application", true); if (!this.enabled) { this._log.info("Weave Sync disabled"); @@ -408,20 +409,43 @@ WeaveSvc.prototype = { Utils.ensureStatus(ret.status, "Could not upload public key"); }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), // nsIObserver observe: function WeaveSync__observe(subject, topic, data) { - if (topic != "nsPref:changed") + switch (topic) { + case "nsPref:changed": + switch (data) { + case "enabled": // this works because this.schedule is 0 when disabled + case "schedule": + this._setSchedule(this.schedule); + break; + } + break; + case "quit-application": + this._onQuitApplication(); + break; + } + }, + + _onQuitApplication: function WeaveSync__onQuitApplication() { + if (!this.enabled || + !Utils.prefs.getBoolPref("syncOnQuit.enabled") || + !this._loggedIn) return; - switch (data) { - case "enabled": // this works because this.schedule is 0 when disabled - case "schedule": - this._setSchedule(this.schedule); - break; - } + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + + // This window has to be modal to prevent the application from quitting + // until the sync finishes and the window closes. + let window = ww.openWindow(null, + "chrome://weave/content/status.xul", + "Weave:status", + "chrome,centerscreen,modal", + null); }, // These are global (for all engines) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 3c12de390b92..a462ee355e72 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -13,6 +13,8 @@ pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); pref("extensions.weave.schedule", 1); +pref("extensions.weave.syncOnQuit.enabled", true); + pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.cookies", true ); From 053afd91543299554ae8b2d4ccf31b7c6923d51a Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Mon, 23 Jun 2008 16:14:55 -0700 Subject: [PATCH 0438/1860] Use allocated buffers instead of stack when the buffers can be large (OS X has a 1MB limit) --- services/crypto/WeaveCrypto.cpp | 113 ++++++++++++++++++++++++-------- services/crypto/WeaveCrypto.h | 4 +- 2 files changed, 88 insertions(+), 29 deletions(-) diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index af96980eaf00..55d82f01bbb0 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -43,6 +43,7 @@ #include "nsStringAPI.h" #include "nsAutoPtr.h" #include "plbase64.h" +#include "prmem.h" #include "secerr.h" #include "pk11func.h" @@ -66,22 +67,33 @@ WeaveCrypto::~WeaveCrypto() * Base 64 encoding and decoding... */ -void +nsresult WeaveCrypto::EncodeBase64(const char *aData, PRUint32 aLength, nsACString& retval) { + // Empty input? Nothing to do. + if (!aLength) { + retval.Assign(EmptyCString()); + return NS_OK; + } + PRUint32 encodedLength = (aLength + 2) / 3 * 4; - char encoded[encodedLength]; + char *encoded = (char *)PR_Malloc(encodedLength); + if (!encoded) + return NS_ERROR_OUT_OF_MEMORY; PL_Base64Encode(aData, aLength, encoded); retval.Assign(encoded, encodedLength); + + PR_Free(encoded); + return NS_OK; } -void +nsresult WeaveCrypto::EncodeBase64(const nsACString& binary, nsACString& retval) { PromiseFlatCString fBinary(binary); - EncodeBase64(fBinary.get(), fBinary.Length(), retval); + return EncodeBase64(fBinary.get(), fBinary.Length(), retval); } nsresult @@ -90,6 +102,15 @@ WeaveCrypto::DecodeBase64(const nsACString& base64, { PromiseFlatCString fBase64(base64); + if (fBase64.Length() == 0) { + *decodedSize = 0; + return NS_OK; + } + + // We expect at least 4 bytes of input + if (fBase64.Length() < 4) + return NS_ERROR_FAILURE; + PRUint32 size = (fBase64.Length() * 3) / 4; // Adjust for padding. if (*(fBase64.get() + fBase64.Length() - 1) == '=') @@ -111,13 +132,20 @@ WeaveCrypto::DecodeBase64(const nsACString& base64, nsresult WeaveCrypto::DecodeBase64(const nsACString& base64, nsACString& retval) { - char decoded[base64.Length()]; PRUint32 decodedLength = base64.Length(); + char *decoded = (char *)PR_Malloc(decodedLength); + if (!decoded) + return NS_ERROR_OUT_OF_MEMORY; nsresult rv = DecodeBase64(base64, decoded, &decodedLength); - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) { + PR_Free(decoded); + return rv; + } retval.Assign(decoded, decodedLength); + + PR_Free(decoded); return NS_OK; } @@ -162,24 +190,32 @@ WeaveCrypto::Encrypt(const nsACString& aClearText, const nsACString& aIV, nsACString& aCipherText) { - nsresult rv; + nsresult rv = NS_OK; // When using CBC padding, the output is 1 block larger than the input. CK_MECHANISM_TYPE mech = PK11_AlgtagToMechanism(mAlgorithm); PRUint32 blockSize = PK11_GetBlockSize(mech, nsnull); - char outputBuffer[aClearText.Length() + blockSize]; - PRUint32 outputBufferSize = sizeof(outputBuffer); + PRUint32 outputBufferSize = aClearText.Length() + blockSize; + char *outputBuffer = (char *)PR_Malloc(outputBufferSize); + if (!outputBuffer) + return NS_ERROR_OUT_OF_MEMORY; + PromiseFlatCString input(aClearText); rv = CommonCrypt(input.get(), input.Length(), outputBuffer, &outputBufferSize, aSymmetricKey, aIV, CKA_ENCRYPT); - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) + goto encrypt_done; - EncodeBase64(outputBuffer, outputBufferSize, aCipherText); + rv = EncodeBase64(outputBuffer, outputBufferSize, aCipherText); + if (NS_FAILED(rv)) + goto encrypt_done; - return NS_OK; +encrypt_done: + PR_Free(outputBuffer); + return rv; } @@ -192,24 +228,31 @@ WeaveCrypto::Decrypt(const nsACString& aCipherText, const nsACString& aIV, nsACString& aClearText) { - nsresult rv; + nsresult rv = NS_OK; - char inputBuffer[aCipherText.Length()]; - PRUint32 inputBufferSize = sizeof(inputBuffer); - char outputBuffer[aCipherText.Length()]; - PRUint32 outputBufferSize = sizeof(outputBuffer); + PRUint32 inputBufferSize = aCipherText.Length(); + PRUint32 outputBufferSize = aCipherText.Length(); + char *outputBuffer = (char *)PR_Malloc(outputBufferSize); + char *inputBuffer = (char *)PR_Malloc(inputBufferSize); + if (!inputBuffer || !outputBuffer) + return NS_ERROR_OUT_OF_MEMORY; rv = DecodeBase64(aCipherText, inputBuffer, &inputBufferSize); - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) + goto decrypt_done; rv = CommonCrypt(inputBuffer, inputBufferSize, outputBuffer, &outputBufferSize, aSymmetricKey, aIV, CKA_DECRYPT); - NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(rv)) + goto decrypt_done; aClearText.Assign(outputBuffer, outputBufferSize); - return NS_OK; +decrypt_done: + PR_Free(outputBuffer); + PR_Free(inputBuffer); + return rv; } @@ -527,7 +570,8 @@ WeaveCrypto::WrapPrivateKey(SECKEYPrivateKey *aPrivateKey, return(NS_ERROR_FAILURE); } - EncodeBase64((char *)wrappedKey.data, wrappedKey.len, aWrappedPrivateKey); + rv = EncodeBase64((char *)wrappedKey.data, wrappedKey.len, aWrappedPrivateKey); + NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } @@ -547,7 +591,8 @@ WeaveCrypto::EncodePublicKey(SECKEYPublicKey *aPublicKey, if (!derKey) return NS_ERROR_FAILURE; - EncodeBase64((char *)derKey->data, derKey->len, aEncodedPublicKey); + nsresult rv = EncodeBase64((char *)derKey->data, derKey->len, aEncodedPublicKey); + NS_ENSURE_SUCCESS(rv, rv); // XXX destroy derKey? @@ -568,7 +613,8 @@ WeaveCrypto::GenerateRandomBytes(PRUint32 aByteCount, rv = PK11_GenerateRandom((unsigned char *)random, aByteCount); NS_ENSURE_SUCCESS(rv, rv); - EncodeBase64(random, aByteCount, aEncodedBytes); + rv = EncodeBase64(random, aByteCount, aEncodedBytes); + NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } @@ -590,7 +636,8 @@ WeaveCrypto::GenerateRandomIV(nsACString& aEncodedBytes) rv = PK11_GenerateRandom((unsigned char *)random, size); NS_ENSURE_SUCCESS(rv, rv); - EncodeBase64(random, size, aEncodedBytes); + rv = EncodeBase64(random, size, aEncodedBytes); + NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } @@ -660,7 +707,11 @@ WeaveCrypto::GenerateRandomKey(nsACString& aEncodedKey) goto keygen_done; } - EncodeBase64((char *)keydata->data, keydata->len, aEncodedKey); + rv = EncodeBase64((char *)keydata->data, keydata->len, aEncodedKey); + if (NS_FAILED(rv)) { + NS_WARNING("EncodeBase64 failed"); + goto keygen_done; + } keygen_done: // XXX does keydata need freed? @@ -772,7 +823,11 @@ WeaveCrypto::WrapSymmetricKey(const nsACString& aSymmetricKey, // Step 5. Base64 encode the wrapped key, cleanup, and return to caller. - EncodeBase64((char *)wrappedKey.data, wrappedKey.len, aWrappedKey); + rv = EncodeBase64((char *)wrappedKey.data, wrappedKey.len, aWrappedKey); + if (NS_FAILED(rv)) { + NS_WARNING("EncodeBase64 failed"); + goto wrap_done; + } wrap_done: if (pubKey) @@ -916,7 +971,11 @@ WeaveCrypto::UnwrapSymmetricKey(const nsACString& aWrappedSymmetricKey, goto unwrap_done; } - EncodeBase64((char *)symKeyData->data, symKeyData->len, aSymmetricKey); + rv = EncodeBase64((char *)symKeyData->data, symKeyData->len, aSymmetricKey); + if (NS_FAILED(rv)) { + NS_WARNING("EncodeBase64 failed"); + goto unwrap_done; + } unwrap_done: if (privKey) diff --git a/services/crypto/WeaveCrypto.h b/services/crypto/WeaveCrypto.h index 9cfcb2dbe295..1e45f5f37308 100644 --- a/services/crypto/WeaveCrypto.h +++ b/services/crypto/WeaveCrypto.h @@ -64,8 +64,8 @@ private: nsresult DecodeBase64(const nsACString& base64, nsACString& retval); nsresult DecodeBase64(const nsACString& base64, char *aData, PRUint32 *aLength); - void EncodeBase64(const nsACString& binary, nsACString& retval); - void EncodeBase64(const char *aData, PRUint32 aLength, nsACString& retval); + nsresult EncodeBase64(const nsACString& binary, nsACString& retval); + nsresult EncodeBase64(const char *aData, PRUint32 aLength, nsACString& retval); nsresult CommonCrypt(const char *input, PRUint32 inputSize, char *output, PRUint32 *outputSize, From e9513c336e493cd1ccabf9a0b7561b959d6c69a1 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 23 Jun 2008 16:22:05 -0700 Subject: [PATCH 0439/1860] wrap notify around lock instead of the other way around so that notify includes locking failure/success in its notifications --- services/sync/modules/service.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c0f1424dfce4..95d412e06e8e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -254,7 +254,7 @@ WeaveSvc.prototype = { _onSchedule: function WeaveSync__onSchedule() { if (this.enabled) { this._log.info("Running scheduled sync"); - this._lock(this._notify("sync", this._syncAsNeeded)).async(this); + this._notify("sync", this._lock(this._syncAsNeeded)).async(this); } }, @@ -527,7 +527,7 @@ WeaveSvc.prototype = { this.logout(); self.done(); }; - this._lock(this._notify("server-wipe", cb)).async(this, onComplete); + this._notify("server-wipe", this._lock(cb)).async(this, onComplete); }, _serverWipe: function WeaveSvc__serverWipe() { let self = yield; @@ -547,7 +547,7 @@ WeaveSvc.prototype = { // These are per-engine sync: function WeaveSync_sync(onComplete) { - this._lock(this._notify("sync", this._sync)).async(this, onComplete); + this._notify("sync", this._lock(this._sync)).async(this, onComplete); }, _sync: function WeaveSync__sync() { @@ -645,8 +645,8 @@ WeaveSvc.prototype = { }, resetServer: function WeaveSync_resetServer(onComplete) { - this._lock(this._notify("reset-server", - this._resetServer)).async(this, onComplete); + this._notify("reset-server", + this._lock(this._resetServer)).async(this, onComplete); }, _resetServer: function WeaveSync__resetServer() { let self = yield; @@ -696,11 +696,10 @@ WeaveSvc.prototype = { "share-bookmarks" will be sent out to any observers who are listening for it. As far as I know, there aren't currently any listeners for "share-bookmarks" but we'll send it out just in case. */ - this._lock(this._notify(messageName, - this._shareData, - dataType, - guid, - username)).async(this, onComplete); + this._notify(messageName, this._lock(this._shareData, + dataType, + guid, + username)).async(this, onComplete); }, _shareData: function WeaveSync__shareData(dataType, From af56e5f29f148086f99befa4bb92110304cfcd15 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Mon, 23 Jun 2008 16:23:57 -0700 Subject: [PATCH 0440/1860] Bug 433949 - Use WeaveCrypto component (NSS) instead of OpenSSL --- services/sync/modules/crypto.js | 411 ++++-------------- services/sync/modules/engines.js | 27 +- services/sync/modules/engines/bookmarks.js | 59 ++- services/sync/modules/identity.js | 28 +- services/sync/modules/remote.js | 45 +- services/sync/modules/service.js | 93 +++- services/sync/tests/unit/head_first.js | 9 +- services/sync/tests/unit/test_crypto_crypt.js | 149 +++++++ .../sync/tests/unit/test_crypto_keypair.js | 63 +++ .../sync/tests/unit/test_crypto_random.js | 65 +++ services/sync/tests/unit/test_passwords.js | 2 +- services/sync/tests/unit/test_pbe.js | 24 - services/sync/tests/unit/test_service.js | 12 - 13 files changed, 524 insertions(+), 463 deletions(-) create mode 100644 services/sync/tests/unit/test_crypto_crypt.js create mode 100644 services/sync/tests/unit/test_crypto_keypair.js create mode 100644 services/sync/tests/unit/test_crypto_random.js delete mode 100644 services/sync/tests/unit/test_pbe.js diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 226f597991f9..439bf4957ae7 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -19,6 +19,7 @@ * * Contributor(s): * Dan Mills + * Justin Dolske * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -65,6 +66,15 @@ CryptoSvc.prototype = { return this.__os; }, + __crypto: null, + get _crypto() { + if (!this.__crypto) + this.__crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + createInstance(Ci.IWeaveCrypto); + return this.__crypto; + }, + + get defaultAlgorithm() { return Utils.prefs.getCharPref("encryption"); }, @@ -82,271 +92,6 @@ CryptoSvc.prototype = { branch.addObserver("extensions.weave.encryption", this, false); }, - _openssl: function Crypto__openssl() { - let extMgr = Cc["@mozilla.org/extensions/manager;1"] - .getService(Ci.nsIExtensionManager); - let loc = extMgr.getInstallLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); - - let wrap = loc.getItemLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); - wrap.append("openssl"); - let bin; - - let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; - switch(os) { - case "WINNT": - wrap.append("win32"); - wrap.append("exec.bat"); - bin = wrap.parent.path + "\\openssl.exe"; - break; - case "Linux": - case "Darwin": - wrap.append("unix"); - wrap.append("exec.sh"); - bin = "openssl"; - break; - default: - throw "encryption not supported on this platform: " + os; - } - - let args = Array.prototype.slice.call(arguments); - args.unshift(wrap, Utils.getTmp().path, bin); - - let rv = Utils.runCmd.apply(null, args); - if (rv != 0) - throw "openssl did not run successfully, error code " + rv; - }, - - _opensslPBE: function Crypto__openssl(op, algorithm, input, password) { - let inputFile = Utils.getTmp("input"); - let [inputFOS] = Utils.open(inputFile, ">"); - inputFOS.writeString(input); - inputFOS.close(); - - // nsIProcess doesn't support stdin, so we write a file instead - let passFile = Utils.getTmp("pass"); - let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.writeString(password); - passFOS.close(); - - try { - this._openssl(algorithm, op, "-a", "-salt", "-in", "input", - "-out", "output", "-pass", "file:pass"); - - } catch (e) { - throw e; - - } finally { - passFile.remove(false); - inputFile.remove(false); - } - - let outputFile = Utils.getTmp("output"); - let [outputFIS] = Utils.open(outputFile, "<"); - let ret = Utils.readStream(outputFIS); - outputFIS.close(); - outputFile.remove(false); - - return ret; - }, - - // generates a random string that can be used as a passphrase - _opensslRand: function Crypto__opensslRand(length) { - if (!length) - length = 128; - - let outputFile = Utils.getTmp("output"); - if (outputFile.exists()) - outputFile.remove(false); - - this._openssl("rand", "-base64", "-out", "output", length); - - let [outputFIS] = Utils.open(outputFile, "<"); - let ret = Utils.readStream(outputFIS); - outputFIS.close(); - outputFile.remove(false); - - return ret; - }, - - // generates an rsa public/private key pair, with the private key encrypted - _opensslRSAKeyGen: function Crypto__opensslRSAKeyGen(identity, algorithm, bits) { - if (!algorithm) - algorithm = "aes-256-cbc"; - if (!bits) - bits = "2048"; - - let privKeyF = Utils.getTmp("privkey.pem"); - if (privKeyF.exists()) - privKeyF.remove(false); - - this._openssl("genrsa", "-out", "privkey.pem", bits); - - let pubKeyF = Utils.getTmp("pubkey.pem"); - if (pubKeyF.exists()) - pubKeyF.remove(false); - - this._openssl("rsa", "-in", "privkey.pem", "-out", "pubkey.pem", - "-outform", "PEM", "-pubout"); - - let cryptedKeyF = Utils.getTmp("enckey.pem"); - if (cryptedKeyF.exists()) - cryptedKeyF.remove(false); - - // nsIProcess doesn't support stdin, so we write a file instead - let passFile = Utils.getTmp("pass"); - let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.writeString(identity.password); - passFOS.close(); - - try { - this._openssl("pkcs8", "-in", "privkey.pem", "-out", "enckey.pem", - "-topk8", "-v2", algorithm, "-passout", "file:pass"); - - } catch (e) { - throw e; - - } finally { - passFile.remove(false); - privKeyF.remove(false); - } - - let [cryptedKeyFIS] = Utils.open(cryptedKeyF, "<"); - let cryptedKey = Utils.readStream(cryptedKeyFIS); - cryptedKeyFIS.close(); - cryptedKeyF.remove(false); - - let [pubKeyFIS] = Utils.open(pubKeyF, "<"); - let pubKey = Utils.readStream(pubKeyFIS); - pubKeyFIS.close(); - pubKeyF.remove(false); - - return [cryptedKey, pubKey]; - }, - - // returns 'input' encrypted with the 'pubkey' public RSA key - _opensslRSAencrypt: function Crypto__opensslRSAencrypt(input, identity) { - let inputFile = Utils.getTmp("input"); - let [inputFOS] = Utils.open(inputFile, ">"); - inputFOS.writeString(input); - inputFOS.close(); - - let keyFile = Utils.getTmp("key"); - let [keyFOS] = Utils.open(keyFile, ">"); - keyFOS.writeString(identity.pubkey); - keyFOS.close(); - - let tmpFile = Utils.getTmp("tmp-output"); - if (tmpFile.exists()) - tmpFile.remove(false); - - let outputFile = Utils.getTmp("output"); - if (outputFile.exists()) - outputFile.remove(false); - - this._openssl("rsautl", "-encrypt", "-pubin", "-inkey", "key", - "-in", "input", "-out", "tmp-output"); - this._openssl("base64", "-in", "tmp-output", "-out", "output"); - - let [outputFIS] = Utils.open(outputFile, "<"); - let output = Utils.readStream(outputFIS); - outputFIS.close(); - outputFile.remove(false); - - return output; - }, - - // returns 'input' decrypted with the 'privkey' private RSA key and password - _opensslRSAdecrypt: function Crypto__opensslRSAdecrypt(input, identity) { - let inputFile = Utils.getTmp("input"); - let [inputFOS] = Utils.open(inputFile, ">"); - inputFOS.writeString(input); - inputFOS.close(); - - let keyFile = Utils.getTmp("key"); - let [keyFOS] = Utils.open(keyFile, ">"); - keyFOS.writeString(identity.privkey); - keyFOS.close(); - - let tmpKeyFile = Utils.getTmp("tmp-key"); - if (tmpKeyFile.exists()) - tmpKeyFile.remove(false); - - let tmpFile = Utils.getTmp("tmp-output"); - if (tmpFile.exists()) - tmpFile.remove(false); - - let outputFile = Utils.getTmp("output"); - if (outputFile.exists()) - outputFile.remove(false); - - // nsIProcess doesn't support stdin, so we write a file instead - let passFile = Utils.getTmp("pass"); - let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.writeString(identity.password); - passFOS.close(); - - try { - this._openssl("base64", "-d", "-in", "input", "-out", "tmp-output"); - // FIXME: this is because openssl.exe (in windows only) doesn't - // seem to support -passin for rsautl, but it works for rsa. - this._openssl("rsa", "-in", "key", "-out", "tmp-key", "-passin", "file:pass"); - this._openssl("rsautl", "-decrypt", "-inkey", "tmp-key", - "-in", "tmp-output", "-out", "output"); - - } catch(e) { - throw e; - - } finally { - passFile.remove(false); - tmpKeyFile.remove(false); - tmpFile.remove(false); - keyFile.remove(false); - } - - let [outputFIS] = Utils.open(outputFile, "<"); - let output = Utils.readStream(outputFIS); - outputFIS.close(); - outputFile.remove(false); - - return output; - }, - - // returns the public key from the private key - _opensslRSAkeydecrypt: function Crypto__opensslRSAkeydecrypt(identity) { - let keyFile = Utils.getTmp("key"); - let [keyFOS] = Utils.open(keyFile, ">"); - keyFOS.writeString(identity.privkey); - keyFOS.close(); - - let outputFile = Utils.getTmp("output"); - if (outputFile.exists()) - outputFile.remove(false); - - // nsIProcess doesn't support stdin, so we write a file instead - let passFile = Utils.getTmp("pass"); - let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); - passFOS.writeString(identity.password); - passFOS.close(); - - try { - this._openssl("rsa", "-in", "key", "-pubout", "-out", "output", - "-passin", "file:pass"); - - } catch(e) { - throw e; - - } finally { - passFile.remove(false); - } - - let [outputFIS] = Utils.open(outputFile, "<"); - let output = Utils.readStream(outputFIS); - outputFIS.close(); - outputFile.remove(false); - - return output; - }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), // nsIObserver @@ -381,99 +126,107 @@ CryptoSvc.prototype = { // Crypto - PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) { + encryptData: function Crypto_encryptData(data, identity) { let self = yield; let ret; - if (!algorithm) - algorithm = this.defaultAlgorithm; + this._log.trace("encrypt called. [id=" + identity.realm + "]"); - if (algorithm != "none") - this._log.debug("Encrypting data"); - - switch (algorithm) { - case "none": + if ("none" == this.defaultAlgorithm) { + this._log.debug("NOT encrypting data"); ret = data; - break; - - case "aes-128-cbc": - case "aes-192-cbc": - case "aes-256-cbc": - case "bf-cbc": - case "des-ede3-cbc": - ret = this._opensslPBE("-e", algorithm, data, identity.password); - break; - - default: - throw "Unknown encryption algorithm: " + algorithm; + } else { + let symkey = identity.bulkKey; + let iv = identity.bulkIV; + ret = this._crypto.encrypt(data, symkey, iv); } - if (algorithm != "none") - this._log.debug("Done encrypting data"); - self.done(ret); }, - PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) { + decryptData: function Crypto_decryptData(data, identity) { let self = yield; let ret; - if (!algorithm) - algorithm = this.defaultAlgorithm; + this._log.trace("decrypt called. [id=" + identity.realm + "]"); - if (algorithm != "none") - this._log.debug("Decrypting data"); - - switch (algorithm) { - case "none": + if ("none" == this.defaultAlgorithm) { + this._log.debug("NOT decrypting data"); ret = data; - break; - - case "aes-128-cbc": - case "aes-192-cbc": - case "aes-256-cbc": - case "bf-cbc": - case "des-ede3-cbc": - ret = this._opensslPBE("-d", algorithm, data, identity.password); - break; - - default: - throw "Unknown encryption algorithm: " + algorithm; + } else { + let symkey = identity.bulkKey; + let iv = identity.bulkIV; + ret = this._crypto.decrypt(data, symkey, iv); } - if (algorithm != "none") - this._log.debug("Done decrypting data"); - self.done(ret); }, - PBEkeygen: function Crypto_PBEkeygen() { + /* + * randomKeyGen + * + * Generates a random symmetric key and IV, and puts them in the specified + * identity. + */ + randomKeyGen: function Crypto_randomKeyGen(identity) { let self = yield; - let ret = this._opensslRand(); - self.done(ret); + + this._log.trace("randomKeyGen called. [id=" + identity.realm + "]"); + + let symkey = this._crypto.generateRandomKey(); + let iv = this._crypto.generateRandomIV(); + + identity.bulkKey = symkey; + identity.bulkIV = iv; }, + /* + * RSAkeygen + * + * Generates a new RSA keypair, as well as the salt/IV used for protecting + * the private key, and puts them in the specified identity. + */ RSAkeygen: function Crypto_RSAkeygen(identity) { let self = yield; - let ret = this._opensslRSAKeyGen(identity); + + this._log.trace("RSAkeygen called. [id=" + identity.realm + "]"); + let privOut = {}; + let pubOut = {}; + + // Generate a blob of random data for salting the passphrase used to + // encrypt the private key. + let salt = this._crypto.generateRandomBytes(32); + let iv = this._crypto.generateRandomIV(); + + this._crypto.generateKeypair(identity.password, + salt, iv, + pubOut, privOut); + + identity.keypairAlg = "RSA"; + identity.pubkey = pubOut.value; + identity.privkey = privOut.value; + identity.passphraseSalt = salt; + identity.privkeyWrapIV = iv; + }, + + wrapKey : function Crypto_wrapKey(data, identity) { + let self = yield; + + this._log.trace("wrapKey called. [id=" + identity.realm + "]"); + let ret = this._crypto.wrapSymmetricKey(data, identity.pubkey); + self.done(ret); }, - RSAencrypt: function Crypto_RSAencrypt(data, identity) { + unwrapKey: function Crypto_unwrapKey(data, identity) { let self = yield; - let ret = this._opensslRSAencrypt(data, identity); + + this._log.trace("upwrapKey called. [id=" + identity.realm + "]"); + let ret = this._crypto.unwrapSymmetricKey(data, + identity.privkey, + identity.password, + identity.passphraseSalt, + identity.privkeyWrapIV); self.done(ret); }, - - RSAdecrypt: function Crypto_RSAdecrypt(data, identity) { - let self = yield; - let ret = this._opensslRSAdecrypt(data, identity); - self.done(ret); - }, - - RSAkeydecrypt: function Crypto_RSAkeydecrypt(identity) { - let self = yield; - let ret = this._opensslRSAkeydecrypt(identity); - self.done(ret); - } }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cf0ad67ee317..08ee457ca734 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -161,24 +161,13 @@ Engine.prototype = { this.__snapshot = value; }, - get pbeId() { - let id = ID.get('Engine:PBE:' + this.name); - if (!id) - id = ID.get('Engine:PBE:default'); - if (!id) - throw "No identity found for engine PBE!"; - return id; - }, - get engineId() { let id = ID.get('Engine:' + this.name); - if (!id || - id.username != this.pbeId.username || id.realm != this.pbeId.realm) { - let password = null; - if (id) - password = id.password; - id = new Identity(this.pbeId.realm + ' - ' + this.logName, - this.pbeId.username, password); + if (!id) { + // Copy the service login from WeaveID + let masterID = ID.get('WeaveID'); + + id = new Identity(this.logName, masterID.username, masterID.password); ID.set('Engine:' + this.name, id); } return id; @@ -271,10 +260,8 @@ Engine.prototype = { this._log.info("Local snapshot version: " + this._snapshot.version); this._log.info("Server maxVersion: " + this._remote.status.data.maxVersion); - if ("none" != Utils.prefs.getCharPref("encryption")) { - let symkey = yield this._remote.keys.getKey(self.cb, this.pbeId); - this.engineId.setTempPassword(symkey); - } + if ("none" != Utils.prefs.getCharPref("encryption")) + yield this._remote.keys.getKeyAndIV(self.cb, this.engineId); // 1) Fetch server deltas let server = {}; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 7d1246e50ae1..d008f7a6ff30 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -341,28 +341,40 @@ BookmarksEngine.prototype = { this._annoSvc.EXPIRE_NEVER); // Create a new symmetric key, to be used only for encrypting this share. - Crypto.PBEkeygen.async(Crypto, self.cb); - let newSymKey = yield; + // XXX HACK. Seems like the engine shouldn't have to be doing any of this, or + // should use its own identity here. + let tmpIdentity = { + realm : "temp ID", + bulkKey : null, + bulkIV : null + }; + Crypto.randomKeyGen.async(Crypto, self.cb, tmpIdentity); + yield; + let bulkKey = tmpIdentity.bulkKey; + let bulkIV = tmpIdentity.bulkIV; /* Get public keys for me and the user I'm sharing with. Each user's public key is stored in /user/username/public/pubkey. */ - let myPubKeyFile = new Resource("/user/" + myUserName + "/public/pubkey"); - myPubKeyFile.get(self.cb); - let myPubKey = yield; + let idRSA = ID.get('WeaveCryptoID'); let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); userPubKeyFile.get(self.cb); let userPubKey = yield; /* Create the keyring, containing the sym key encrypted with each of our public keys: */ - Crypto.RSAencrypt.async(Crypto, self.cb, symKey, {pubkey: myPubKey} ); + Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", pubkey: idRSA.pubkey} ); let encryptedForMe = yield; - Crypto.RSAencrypt.async(Crypto, self.cb, symKey, {pubkey: userPubKey} ); + Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", pubkey: userPubKey} ); let encryptedForYou = yield; - let keyring = { myUserName: encryptedForMe, - username: encryptedForYou }; + let keys = { + ring : { }, + bulkIV : bulkIV + }; + keys.ring[myUserName] = encryptedForMe; + keys.ring[username] = encryptedForYou; + let keyringFile = new Resource( serverPath + "/" + KEYRING_FILE_NAME ); - keyringFile.put( self.cb, this._json.encode( keyring ) ); + keyringFile.put( self.cb, this._json.encode( keys ) ); yield; // Call Atul's js api for setting htaccess: @@ -390,8 +402,14 @@ BookmarksEngine.prototype = { // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.get(self.cb); - let keyring = yield; - let symKey = keyring[ myUserName ]; + let keys = yield; + + // Unwrap (decrypt) the key with the user's private key. + let idRSA = ID.get('WeaveCryptoID'); + let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, + keys.ring[myUserName], idRSA); + let bulkIV = keys.bulkIV; + // Get the json-wrapped contents of everything in the folder: let json = this._store._wrapMount( folderNode, myUserName ); /* TODO what does wrapMount do with this username? Should I be passing @@ -399,7 +417,12 @@ BookmarksEngine.prototype = { // Encrypt it with the symkey and put it into the shared-bookmark file. let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); - Crypto.PBEencrypt.async( Crypto, self.cb, json, {password:symKey} ); + let tmpIdentity = { + realm : "temp ID", + bulkKey : bulkKey, + bulkIV : bulkIV + }; + Crypto.encryptData.async( Crypto, self.cb, json, tmpIdentity ); let cyphertext = yield; bmkFile.put( self.cb, cyphertext ); yield; @@ -545,14 +568,18 @@ BookmarksEngine.prototype = { // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.get(self.cb); - let keyring = yield; - let symKey = keyring[ myUserName ]; + let keys = yield; // Decrypt the contents of the bookmark file with the symmetric key: let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); bmkFile.get(self.cb); let cyphertext = yield; - Crypto.PBEdecrypt.async( Crypto, self.cb, cyphertext, {password:symKey} ); + let tmpIdentity = { + realm : "temp ID", + bulkKey : keys.ring[myUserName], + bulkIV : keys.bulkIV + }; + Crypto.decryptData.async( Crypto, self.cb, cyphertext, tmpIdentity ); let json = yield; // TODO error handling (see what Resource can throw or return...) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index f46ceace2d6d..23fbd2f66054 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -87,28 +87,26 @@ IDManager.prototype = { */ function Identity(realm, username, password) { - this._realm = realm; - this._username = username; + this.realm = realm; + this.username = username; this._password = password; } Identity.prototype = { - get realm() { return this._realm; }, - set realm(value) { this._realm = value; }, + realm : null, - get username() { return this._username; }, - set username(value) { this._username = value; }, + // Only the "WeaveCryptoID" realm uses these: + privkey : null, + pubkey : null, + passphraseSalt : null, + privkeyWrapIV : null, + // Only the per-engine identity uses these: + bulkKey : null, + bulkIV : null, + + username : null, get userHash() { return Utils.sha1(this.username); }, - _privkey: null, - get privkey() { return this._privkey; }, - set privkey(value) { this._privkey = value; }, - - // FIXME: get this from the privkey using crypto.js? - _pubkey: null, - get pubkey() { return this._pubkey; }, - set pubkey(value) { this._pubkey = value; }, - _password: null, get password() { if (!this._password) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 42a07705cb0d..2a54af932caa 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -282,7 +282,7 @@ CryptoFilter.prototype = { beforePUT: function CryptoFilter_beforePUT(data) { let self = yield; this._log.debug("Encrypting data"); - Crypto.PBEencrypt.async(Crypto, self.cb, data, this._remote.engineId); + Crypto.encryptData.async(Crypto, self.cb, data, this._remote.engineId); let ret = yield; self.done(ret); }, @@ -292,7 +292,7 @@ CryptoFilter.prototype = { this._log.debug("Decrypting data"); if (!this._remote.status.data) throw "Remote status must be initialized before crypto filter can be used" - Crypto.PBEdecrypt.async(Crypto, self.cb, data, this._remote.engineId); + Crypto.decryptData.async(Crypto, self.cb, data, this._remote.engineId); let ret = yield; self.done(ret); } @@ -320,21 +320,25 @@ Keychain.prototype = { this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "keys.json"); this.pushFilter(new JsonFilter()); }, - _getKey: function Keychain__getKey(identity) { + _getKeyAndIV: function Keychain__getKeyAndIV(identity) { let self = yield; this.get(self.cb); yield; if (!this.data || !this.data.ring || !this.data.ring[identity.username]) throw "Keyring does not contain a key for this user"; - Crypto.RSAdecrypt.async(Crypto, self.cb, - this.data.ring[identity.username], identity); - let symkey = yield; - self.done(symkey); + // Unwrap (decrypt) the key with the user's private key. + let idRSA = ID.get('WeaveCryptoID'); + let symkey = yield Crypto.unwrapKey.async(Crypto, self.cb, + this.data.ring[identity.username], idRSA); + let iv = this.data.bulkIV; + + identity.bulkKey = symkey; + identity.bulkIV = iv; }, - getKey: function Keychain_getKey(onComplete, identity) { - this._getKey.async(this, onComplete, identity); + getKeyAndIV: function Keychain_getKeyAndIV(onComplete, identity) { + this._getKeyAndIV.async(this, onComplete, identity); } // FIXME: implement setKey() }; @@ -346,7 +350,6 @@ function RemoteStore(engine) { RemoteStore.prototype = { get serverPrefix() this._engine.serverPrefix, get engineId() this._engine.engineId, - get pbeId() this._engine.pbeId, get status() { let status = new Status(this); @@ -420,23 +423,20 @@ RemoteStore.prototype = { // Does a fresh upload of the given snapshot to a new store _initialize: function RStore__initialize(snapshot) { let self = yield; - let symkey; + let wrappedSymkey; if ("none" != Utils.prefs.getCharPref("encryption")) { - symkey = yield Crypto.PBEkeygen.async(Crypto, self.cb); - if (!symkey) - throw "Could not generate a symmetric encryption key"; - this.engineId.setTempPassword(symkey); + Crypto.randomKeyGen.async(Crypto, self.cb, this.engineId); + yield; - symkey = yield Crypto.RSAencrypt.async(Crypto, self.cb, - this.engineId.password, - this.pbeId); - if (!symkey) - throw "Could not encrypt symmetric encryption key"; + // Wrap (encrypt) this key with the user's public key. + let idRSA = ID.get('WeaveCryptoID'); + wrappedSymkey = yield Crypto.wrapKey.async(Crypto, self.cb, + this.engineId.bulkKey, idRSA); } - let keys = {ring: {}}; - keys.ring[this.engineId.username] = symkey; + let keys = {ring: {}, bulkIV: this.engineId.bulkIV}; + keys.ring[this.engineId.username] = wrappedSymkey; yield this.keys.put(self.cb, keys); yield this._snapshot.put(self.cb, snapshot.data); @@ -505,6 +505,7 @@ RemoteStore.prototype = { snap.version = this.status.data.maxVersion; if (lastSyncSnap.version < this.status.data.snapVersion) { + this._log.trace("Getting latest from snap --> scratch"); self.done(yield this._getLatestFromScratch.async(this, self.cb)); return; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 95d412e06e8e..749ec84e0769 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -163,6 +163,14 @@ WeaveSvc.prototype = { return this.__dirSvc; }, + __json: null, + get _json() { + if (!this.__json) + this.__json = Cc["@mozilla.org/dom/json;1"]. + createInstance(Ci.nsIJSON); + return this.__json; + }, + // Timer object for automagically syncing _scheduleTimer: null, @@ -357,25 +365,53 @@ WeaveSvc.prototype = { finally { DAV.defaultPrefix = prefix; } }, - _keyCheck: function WeaveSvc__keyCheck() { + _getKeypair : function WeaveSync__getKeypair() { let self = yield; - if ("none" != Utils.prefs.getCharPref("encryption")) { - DAV.GET("private/privkey", self.cb); - let keyResp = yield; - Utils.ensureStatus(keyResp.status, - "Could not get private key from server", [[200,300],404]); + if ("none" == Utils.prefs.getCharPref("encryption")) + return; - if (keyResp.status != 404) { - let id = ID.get('WeaveCryptoID'); - id.privkey = keyResp.responseText; - Crypto.RSAkeydecrypt.async(Crypto, self.cb, id); - id.pubkey = yield; - } else { + this._log.trace("Retrieving keypair from server"); + + // XXX this kind of replaces _keyCheck + // seems like key generation should only happen during setup? + DAV.GET("private/privkey", self.cb); + let privkeyResp = yield; + Utils.ensureStatus(privkeyResp.status, + "Could not get private key from server", [[200,300],404]); + + DAV.GET("public/pubkey", self.cb); + let pubkeyResp = yield; + Utils.ensureStatus(pubkeyResp.status, + "Could not get public key from server", [[200,300],404]); + + if (privkeyResp.status == 404 || pubkeyResp.status == 404) { this._generateKeys.async(this, self.cb); yield; - } + return; } + + let privkeyData = this._json.decode(privkeyResp.responseText); + let pubkeyData = this._json.decode(pubkeyResp.responseText); + + if (!privkeyData || !pubkeyData) + throw "Bad keypair JSON"; + if (privkeyData.version != 1 || pubkeyData.version != 1) + throw "Unexpected keypair data version"; + if (privkeyData.algorithm != "RSA" || pubkeyData.algorithm != "RSA") + throw "Only RSA keys currently supported"; + + + let id = ID.get('WeaveCryptoID'); + id.keypairAlg = privkeyData.algorithm; + id.privkey = privkeyData.privkey; + id.privkeyWrapIV = privkeyData.privkeyIV; + id.passphraseSalt = privkeyData.privkeySalt; + + id.pubkey = pubkeyData.pubkey; + + // XXX note that we have not used the private key, so we don't yet + // know if the user's passphrase works or not. }, _generateKeys: function WeaveSync__generateKeys() { @@ -383,12 +419,10 @@ WeaveSvc.prototype = { this._log.debug("Generating new RSA key"); + // RSAkeygen will set the needed |id| properties. let id = ID.get('WeaveCryptoID'); Crypto.RSAkeygen.async(Crypto, self.cb, id); - let [privkey, pubkey] = yield; - - id.privkey = privkey; - id.pubkey = pubkey; + yield; DAV.MKCOL("private/", self.cb); let ret = yield; @@ -400,11 +434,26 @@ WeaveSvc.prototype = { if (!ret) throw "Could not create public key directory"; - DAV.PUT("private/privkey", privkey, self.cb); + let privkeyData = { version : 1, + algorithm : id.keypairAlg, + privkey : id.privkey, + privkeyIV : id.privkeyWrapIV, + privkeySalt : id.passphraseSalt + }; + let data = this._json.encode(privkeyData); + + DAV.PUT("private/privkey", data, self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload private key"); - DAV.PUT("public/pubkey", pubkey, self.cb); + + let pubkeyData = { version : 1, + algorithm : id.keypairAlg, + pubkey : id.pubkey, + }; + data = this._json.encode(pubkeyData); + + DAV.PUT("public/pubkey", data, self.cb); ret = yield; Utils.ensureStatus(ret.status, "Could not upload public key"); }, @@ -491,7 +540,7 @@ WeaveSvc.prototype = { this._versionCheck.async(this, self.cb); yield; - this._keyCheck.async(this, self.cb); + this._getKeypair.async(this, self.cb); yield; this._loggedIn = true; @@ -559,7 +608,7 @@ WeaveSvc.prototype = { this._versionCheck.async(this, self.cb); yield; - this._keyCheck.async(this, self.cb); + this._getKeypair.async(this, self.cb); yield; let engines = Engines.getAll(); @@ -586,7 +635,7 @@ WeaveSvc.prototype = { this._versionCheck.async(this, self.cb); yield; - this._keyCheck.async(this, self.cb); + this._getKeypair.async(this, self.cb); yield; let engines = Engines.getAll(); diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index ba4895471d41..d2157c5238ad 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -5,15 +5,20 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +// initialize nss +let ch = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + let ds = Cc["@mozilla.org/file/directory_service;1"] .getService(Ci.nsIProperties); let provider = { getFile: function(prop, persistent) { persistent.value = true; - if (prop == "ExtPrefDL") { + if (prop == "ExtPrefDL") return [ds.get("CurProcD", Ci.nsIFile)]; - } + else if (prop == "ProfD") + return ds.get("CurProcD", Ci.nsIFile); throw Cr.NS_ERROR_FAILURE; }, QueryInterface: function(iid) { diff --git a/services/sync/tests/unit/test_crypto_crypt.js b/services/sync/tests/unit/test_crypto_crypt.js new file mode 100644 index 000000000000..32fd708f1497 --- /dev/null +++ b/services/sync/tests/unit/test_crypto_crypt.js @@ -0,0 +1,149 @@ +function run_test() { + var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + + // First, do a normal run with expected usage... Generate a random key and + // iv, encrypt and decrypt a string. + var iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); + + var key = cryptoSvc.generateRandomKey(); + do_check_eq(key.length, 44); + + var mySecret = "bacon is a vegetable"; + var cipherText = cryptoSvc.encrypt(mySecret, key, iv); + do_check_eq(cipherText.length, 44); + + var clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(clearText.length, 20); + + // Did the text survive the encryption round-trip? + do_check_eq(clearText, mySecret); + do_check_neq(cipherText, mySecret); // just to be explicit + + + // Do some more tests with a fixed key/iv, to check for reproducable results. + cryptoSvc.algorithm = Ci.IWeaveCrypto.AES_128_CBC; + key = "St1tFCor7vQEJNug/465dQ=="; + iv = "oLjkfrLIOnK2bDRvW4kXYA=="; + + // Test small input sizes + mySecret = ""; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "OGQjp6mK1a3fs9k9Ml4L3w=="); + do_check_eq(clearText, mySecret); + + mySecret = "x"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "96iMl4vhOxFUW/lVHHzVqg=="); + do_check_eq(clearText, mySecret); + + mySecret = "xx"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "olpPbETRYROCSqFWcH2SWg=="); + do_check_eq(clearText, mySecret); + + mySecret = "xxx"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "rRbpHGyVSZizLX/x43Wm+Q=="); + do_check_eq(clearText, mySecret); + + mySecret = "xxxx"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "HeC7miVGDcpxae9RmiIKAw=="); + do_check_eq(clearText, mySecret); + + // Tests input spanning a block boundary (AES block size is 16 bytes) + mySecret = "123456789012345"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "e6c5hwphe45/3VN/M0bMUA=="); + do_check_eq(clearText, mySecret); + + mySecret = "1234567890123456"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "V6aaOZw8pWlYkoIHNkhsP1JOIQF87E2vTUvBUQnyV04="); + do_check_eq(clearText, mySecret); + + mySecret = "12345678901234567"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "V6aaOZw8pWlYkoIHNkhsP5GvxWJ9+GIAS6lXw+5fHTI="); + do_check_eq(clearText, mySecret); + + + // Test with 192 bit key. + cryptoSvc.algorithm = Ci.IWeaveCrypto.AES_192_CBC; + key = "iz35tuIMq4/H+IYw2KTgow=="; + iv = "TJYrvva2KxvkM8hvOIvWp3xgjTXgq5Ss"; + mySecret = "i like pie"; + + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "DLGx8BWqSCLGG7i/xwvvxg=="); + do_check_eq(clearText, mySecret); + + // Test with 256 bit key. + cryptoSvc.algorithm = Ci.IWeaveCrypto.AES_256_CBC; + key = "c5hG3YG+NC61FFy8NOHQak1ZhMEWO79bwiAfar2euzI="; + iv = "gsgLRDaxWvIfKt75RjuvFWERt83FFsY2A0TW+0b2iVk="; + mySecret = "i like pie"; + + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "o+ADtdMd8ubzNWurS6jt0Q=="); + do_check_eq(clearText, mySecret); + + + // Test with bogus inputs + cryptoSvc.algorithm = Ci.IWeaveCrypto.AES_128_CBC; + key = "St1tFCor7vQEJNug/465dQ=="; + iv = "oLjkfrLIOnK2bDRvW4kXYA=="; + mySecret = "does thunder read testcases?"; + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + do_check_eq(cipherText, "T6fik9Ros+DB2ablH9zZ8FWZ0xm/szSwJjIHZu7sjPs="); + + var badkey = "badkeybadkeybadkeybadk=="; + var badiv = "badivbadivbadivbadivbad=="; + var badcipher = "crapinputcrapinputcrapinputcrapinputcrapinp="; + var failure; + + try { + failure = false; + clearText = cryptoSvc.decrypt(cipherText, badkey, iv); + } catch (e) { + failure = true; + } + do_check_true(failure); + + try { + failure = false; + clearText = cryptoSvc.decrypt(cipherText, key, badiv); + } catch (e) { + failure = true; + } + do_check_true(failure); + + try { + failure = false; + clearText = cryptoSvc.decrypt(cipherText, badkey, badiv); + } catch (e) { + failure = true; + } + do_check_true(failure); + + try { + failure = false; + clearText = cryptoSvc.decrypt(badcipher, key, iv); + } catch (e) { + failure = true; + } + do_check_true(failure); + +} diff --git a/services/sync/tests/unit/test_crypto_keypair.js b/services/sync/tests/unit/test_crypto_keypair.js new file mode 100644 index 000000000000..174f158c3035 --- /dev/null +++ b/services/sync/tests/unit/test_crypto_keypair.js @@ -0,0 +1,63 @@ +function run_test() { + var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + + var salt = cryptoSvc.generateRandomBytes(16); + do_check_eq(salt.length, 24); + + var iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); + + var symKey = cryptoSvc.generateRandomKey(); + do_check_eq(symKey.length, 44); + + + // Tests with a 2048 bit key (the default) + do_check_eq(cryptoSvc.keypairBits, 2048) + + var pubOut = {}; + var privOut = {}; + cryptoSvc.generateKeypair("my passphrase", salt, iv, pubOut, privOut); + var pubKey = pubOut.value; + var privKey = privOut.value; + do_check_true(!!pubKey); + do_check_true(!!privKey); + do_check_eq(pubKey.length, 392); + do_check_eq(privKey.length, 1644); + + // do some key wrapping + var wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey); + do_check_eq(wrappedKey.length, 344); + + var unwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, privKey, + "my passphrase", salt, iv); + do_check_eq(unwrappedKey.length, 44); + + // The acid test... Is our unwrapped key the same thing we started with? + do_check_eq(unwrappedKey, symKey); + + + // Tests with a 1024 bit key + cryptoSvc.keypairBits = 1024; + do_check_eq(cryptoSvc.keypairBits, 1024) + + cryptoSvc.generateKeypair("my passphrase", salt, iv, pubOut, privOut); + var pubKey = pubOut.value; + var privKey = privOut.value; + do_check_true(!!pubKey); + do_check_true(!!privKey); + do_check_eq(pubKey.length, 216); + do_check_eq(privKey.length, 856); + + // do some key wrapping + wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey); + do_check_eq(wrappedKey.length, 172); + unwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, privKey, + "my passphrase", salt, iv); + do_check_eq(unwrappedKey.length, 44); + + // The acid test... Is our unwrapped key the same thing we started with? + do_check_eq(unwrappedKey, symKey); + + +} diff --git a/services/sync/tests/unit/test_crypto_random.js b/services/sync/tests/unit/test_crypto_random.js new file mode 100644 index 000000000000..426e35319bd1 --- /dev/null +++ b/services/sync/tests/unit/test_crypto_random.js @@ -0,0 +1,65 @@ +function run_test() { + var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + + // Test salt generation. + var salt; + + salt = cryptoSvc.generateRandomBytes(0); + do_check_eq(salt.length, 0); + salt = cryptoSvc.generateRandomBytes(1); + do_check_eq(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(2); + do_check_eq(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(3); + do_check_eq(salt.length, 4); + salt = cryptoSvc.generateRandomBytes(4); + do_check_eq(salt.length, 8); + salt = cryptoSvc.generateRandomBytes(8); + do_check_eq(salt.length, 12); + + // sanity check to make sure salts seem random + var salt2 = cryptoSvc.generateRandomBytes(8); + do_check_eq(salt2.length, 12); + do_check_neq(salt, salt2); + + salt = cryptoSvc.generateRandomBytes(16); + do_check_eq(salt.length, 24); + salt = cryptoSvc.generateRandomBytes(1024); + do_check_eq(salt.length, 1368); + + + // Test random key generation + var keydata, keydata2, iv; + + keydata = cryptoSvc.generateRandomKey(); + do_check_eq(keydata.length, 44); + keydata2 = cryptoSvc.generateRandomKey(); + do_check_neq(keydata, keydata2); // sanity check for randomness + iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); + + cryptoSvc.algorithm = Ci.IWeaveCrypto.AES_256_CBC; + keydata = cryptoSvc.generateRandomKey(); + do_check_eq(keydata.length, 44); + keydata2 = cryptoSvc.generateRandomKey(); + do_check_neq(keydata, keydata2); // sanity check for randomness + iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); + + cryptoSvc.algorithm = Ci.IWeaveCrypto.AES_192_CBC; + keydata = cryptoSvc.generateRandomKey(); + do_check_eq(keydata.length, 32); + keydata2 = cryptoSvc.generateRandomKey(); + do_check_neq(keydata, keydata2); // sanity check for randomness + iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); + + cryptoSvc.algorithm = Ci.IWeaveCrypto.AES_128_CBC; + keydata = cryptoSvc.generateRandomKey(); + do_check_eq(keydata.length, 24); + keydata2 = cryptoSvc.generateRandomKey(); + do_check_neq(keydata, keydata2); // sanity check for randomness + iv = cryptoSvc.generateRandomIV(); + do_check_eq(iv.length, 24); +} diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index f5054a1aee6f..16ce58fdef0f 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -35,7 +35,7 @@ let __fakeLogins = [ // ---------------------------------------- function run_test() { - ID.set('Engine:PBE:default', + ID.set('WeaveID', new Identity('Mozilla Services Encryption Passphrase', 'foo')); // The JS module we're testing, with all members exposed. diff --git a/services/sync/tests/unit/test_pbe.js b/services/sync/tests/unit/test_pbe.js deleted file mode 100644 index f00ffb6cc408..000000000000 --- a/services/sync/tests/unit/test_pbe.js +++ /dev/null @@ -1,24 +0,0 @@ -function run_test() { - // initialize nss - let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); - - let pbe = Cc["@labs.mozilla.com/Weave/Crypto;1"].getService(Ci.IWeaveCrypto); - - pbe.algorithm = pbe.DES_EDE3_CBC; - let cipherTxt = pbe.encrypt("passphrase", "my very secret message!"); - - do_check_true(cipherTxt != "my very secret message!"); - - let clearTxt = pbe.decrypt("passphrase", cipherTxt); - do_check_true(clearTxt == "my very secret message!"); - - // The following check with wrong password must cause decryption to fail - // because of used padding-schema cipher, RFC 3852 Section 6.3 - let failure = false; - try { - pbe.decrypt("wrongpassphrase", cipherTxt); - } catch (e) { - failure = true; - } - do_check_true(failure); -} diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index b072a79f484f..1f027df7889d 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -21,18 +21,6 @@ let __fakePasswords = { 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} }; -Crypto.__proto__ = { - RSAkeydecrypt: function fake_RSAkeydecrypt(identity) { - let self = yield; - - if (identity.password == "passphrase" && - identity.privkey == "fake private key") - self.done("fake public key"); - else - throw new Error("Unexpected identity information."); - } -}; - let Service = loadInSandbox("resource://weave/service.js"); function TestService() { From 8e14726573c7a7d5a0291961510300a6368fb837 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 23 Jun 2008 17:41:01 -0700 Subject: [PATCH 0441/1860] Updated expected log for test_passwords based on recent code changes. --- services/sync/tests/unit/test_passwords.log.expected | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index e86489631f90..6903fcfedfdd 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -6,14 +6,16 @@ Service.RemoteStore DEBUG Downloading status file Testing INFO HTTP GET from user-data/passwords/status.json, returning status 404 Service.PasswordEngine INFO Initial upload to server Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/keys.json with data: {"ring":{}} +Testing INFO HTTP PUT to user-data/passwords/keys.json with data: {"ring":{},"bulkIV":null} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/passwords/snapshot.json with data: {"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON @@ -68,9 +70,11 @@ Service.PasswordEngine INFO Uploading changes to server Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 Service.Resource DEBUG GET request successful Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data Service.JsonFilter DEBUG Decoding JSON data Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}]] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON @@ -109,9 +113,11 @@ Service.PasswordEngine INFO Uploading changes to server Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 Service.Resource DEBUG GET request successful Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data Service.JsonFilter DEBUG Decoding JSON data Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}],[{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}]] Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON @@ -134,16 +140,19 @@ Service.RemoteStore DEBUG Downloading status file... done Service.PasswordEngine DEBUG Remote/local sync GUIDs do not match. Forcing initial sync. Service.PasswordEngine INFO Local snapshot version: -1 Service.PasswordEngine INFO Server maxVersion: 2 +Service.RemoteStore TRACE Getting latest from snap --> scratch Service.RemoteStore INFO Downloading all server data from scratch Service.RemoteStore DEBUG Downloading server snapshot Testing INFO HTTP GET from user-data/passwords/snapshot.json, returning status 200 Service.Resource DEBUG GET request successful Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data Service.JsonFilter DEBUG Decoding JSON data Service.RemoteStore DEBUG Downloading server deltas Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 Service.Resource DEBUG GET request successful Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data Service.JsonFilter DEBUG Decoding JSON data Service.SnapStore TRACE Processing command: {"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}} Service.SnapStore TRACE Processing command: {"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]} From 1efa59e8f2822a2daceba524a05f4c37f13eaaaa Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 23 Jun 2008 18:01:48 -0700 Subject: [PATCH 0442/1860] Added some more visibility to the individual steps of test_passwords. --- services/sync/tests/unit/test_passwords.js | 2 ++ services/sync/tests/unit/test_passwords.log.expected | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 16ce58fdef0f..9667768f946a 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -97,7 +97,9 @@ function __makeCallback() { } function runAndEnsureSuccess(name, func) { + getTestLogger().info("-----------------------------------------"); getTestLogger().info("Step '" + name + "' starting."); + getTestLogger().info("-----------------------------------------"); func(__makeCallback()); while (fts.processCallback()) {} do_check_true(callbackCalled); diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_passwords.log.expected index 6903fcfedfdd..050ab72e4f28 100644 --- a/services/sync/tests/unit/test_passwords.log.expected +++ b/services/sync/tests/unit/test_passwords.log.expected @@ -1,5 +1,7 @@ *** test pending +Testing INFO ----------------------------------------- Testing INFO Step 'initial sync' starting. +Testing INFO ----------------------------------------- Service.PasswordEngine INFO Beginning sync Testing INFO HTTP MKCOL on user-data/passwords/ Service.RemoteStore DEBUG Downloading status file @@ -26,7 +28,9 @@ Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/passwords.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":0,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} Testing INFO Step 'initial sync' succeeded. +Testing INFO ----------------------------------------- Testing INFO Step 'trivial re-sync' starting. +Testing INFO ----------------------------------------- Testing INFO Opening 'weave/snapshots/passwords.json' for reading. Testing INFO Reading from stream. Service.SnapStore INFO Read saved snapshot from disk @@ -43,7 +47,9 @@ Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap vers Service.RemoteStore TRACE Local snapshot version == server maxVersion Service.PasswordEngine INFO Sync complete: no changes needed on client or server Testing INFO Step 'trivial re-sync' succeeded. +Testing INFO ----------------------------------------- Testing INFO Step 'add user and re-sync' starting. +Testing INFO ----------------------------------------- Testing INFO Opening 'weave/snapshots/passwords.json' for reading. Testing INFO Reading from stream. Service.SnapStore INFO Read saved snapshot from disk @@ -86,7 +92,9 @@ Testing INFO Opening 'weave/snapshots/passwords.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":1,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"},"1b3869fc36234b39cd354f661ed1d7d148394ca3":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}} Service.PasswordEngine INFO Sync complete Testing INFO Step 'add user and re-sync' succeeded. +Testing INFO ----------------------------------------- Testing INFO Step 'remove user and re-sync' starting. +Testing INFO ----------------------------------------- Testing INFO Opening 'weave/snapshots/passwords.json' for reading. Testing INFO Reading from stream. Service.SnapStore INFO Read saved snapshot from disk @@ -129,7 +137,9 @@ Testing INFO Opening 'weave/snapshots/passwords.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} Service.PasswordEngine INFO Sync complete Testing INFO Step 'remove user and re-sync' succeeded. +Testing INFO ----------------------------------------- Testing INFO Step 'resync on second computer' starting. +Testing INFO ----------------------------------------- Service.PasswordEngine INFO Beginning sync Testing INFO HTTP MKCOL on user-data/passwords/ Service.RemoteStore DEBUG Downloading status file From 5ba017bb1ba25a1de59a5572e08a4f534eeeae80 Mon Sep 17 00:00:00 2001 From: Date: Mon, 23 Jun 2008 18:23:08 -0700 Subject: [PATCH 0443/1860] Make the stop-sharing-data command work --- services/sync/modules/engines.js | 14 +++- services/sync/modules/engines/bookmarks.js | 78 ++++++++++++++++------ services/sync/modules/service.js | 8 +++ 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cf0ad67ee317..0f083b0b4eb2 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -434,9 +434,13 @@ Engine.prototype = { self.done(); }, - /* TODO need a "stop sharing" function. - Actually, stopping an outgoing share and stopping an incoming share - are two different things. */ + _stopSharing: function Engine__stopSharing(guid, username) { + let self = yield; + /* This should be overridden by the engine subclass for each datatype. + Stop sharing the data node identified by guid with the user identified + by username.*/ + self.done(); + }, sync: function Engine_sync(onComplete) { return this._sync.async(this, onComplete); @@ -446,6 +450,10 @@ Engine.prototype = { return this._share.async(this, onComplete, guid, username); }, + stopSharing: function Engine_share(onComplete, guid, username) { + return this._stopSharing.async(this, onComplete, guid, username); + }, + resetServer: function Engimne_resetServer(onComplete) { this._notify("reset-server", this._resetServer).async(this, onComplete); }, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 7d1246e50ae1..d0ef4c23fb72 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -190,8 +190,8 @@ BookmarksEngine.prototype = { But since we don't have notification in place yet, I'm going to skip right ahead to creating the incoming share. */ - dump( "I was offered the directory " + dir + " from user " + dir ); - _createIncomingShare( user, serverPath, folderName ); + this._log.info("User " + user + " offered to share folder " + folderName); + this._createIncomingShare( user, serverPath, folderName ); }, _incomingShareWithdrawn: function BmkEngine__incomingShareStop(user, @@ -200,11 +200,9 @@ BookmarksEngine.prototype = { /* Called when we receive a message telling us that a user who has already shared a directory with us has chosen to stop sharing the directory. - - TODO Find the incomingShare in our bookmark tree that corresponds - to the shared directory, and delete it; add a notification to - the queue telling us what has happened. */ + this._log.info("User " + user + " stopped sharing folder " + folderName); + this._stopIncomingShare(user, serverPath, folderName); }, _sync: function BmkEngine__sync() { @@ -261,6 +259,36 @@ BookmarksEngine.prototype = { this._log.info("Shared " + folderName +" with " + username); ret = true; + self.done( ret ); + }, + + _stopSharing: function BmkEngine__stopSharing( selectedFolder, username ) { + let self = yield; + let folderName = selectedFolder.getAttribute( "label" ); + let serverPath = this._annoSvc.getItemAnnotation(folderNode, + SERVER_PATH_ANNO); + + /* LONGTERM TODO: when we move to being able to share one folder with + * multiple people, this needs to be modified so we can stop sharing with + * one person but keep sharing with others. + */ + + // Stop the outgoing share: + this._stopOutgoingShare.async( this, self.cb, selectedFolder); + yield; + + // Send message to the share-ee, so they can stop their incoming share: + if ( this._xmppClient ) { + if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { + let msgText = "stop " + serverPath + " " + folderName; + this._log.debug( "Sending XMPP message: " + msgText ); + this._xmppClient.sendMessage( username, msgText ); + } else { + this._log.warn( "No XMPP connection for share notification." ); + } + } + + this._log.info("Stopped sharing " + folderName + "with " + username); self.done( true ); }, @@ -440,19 +468,7 @@ BookmarksEngine.prototype = { this._annoSvc.EXPIRE_NEVER); // TODO is there a way to remove the annotations entirely rather than // setting it to an empty string?? - - // Send the message to the share-ee: - if ( this._xmppClient ) { - if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { - let folderName = folderNode.getAttribute( "label" ); - let msgText = "stop " + serverPath + " " + folderName; - this._log.debug( "Sending XMPP message: " + msgText ); - this._xmppClient.sendMessage( username, msgText ); - } else { - this._log.warn( "No XMPP connection for share notification." ); - } - } - + self.done(); }, _createIncomingShare: function BookmarkEngine__createShare(user, @@ -576,6 +592,27 @@ BookmarksEngine.prototype = { yield; this._log.trace("Shared folder from " + user + " successfully synced!"); + }, + + _stopIncomingShare: function BmkEngine__stopIncomingShare(user, + serverPath, + folderName) + { + /* Delete the incoming share folder. Since the update of incoming folders + * is triggered when the engine spots a folder with a certain annotation on + * it, just getting rid of this folder is all we need to do. + */ + let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + + let a = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, {}); + for (let i = 0; i < a.length; i++) { + let creator = this._annoSvc.getItemAnnotation(a[i], OUTGOING_SHARED_ANNO); + let path = this._annoSvc.getItemAnnotation(a[i], SERVER_PATH_ANNO); + if ( creator == user && path == serverPath ) { + bms.removeFolder( a[i]); + } + } } }; BookmarksEngine.prototype.__proto__ = new Engine(); @@ -801,9 +838,6 @@ BookmarksStore.prototype = { command.data.index); break; case "mounted-share": - // TODO this is to create the shared-items folder on another machine - // to duplicate the one that's on the first machine; we don't need that - // anymore. OR is it to create the folder on the sharee's computer? this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\""); newId = this._bms.createFolder(parentId, command.data.title, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c7c004b931f3..49a5bb94b32a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -691,6 +691,14 @@ WeaveSvc.prototype = { Engines.get(dataType).share(self.cb, guid, username); let ret = yield; self.done(ret); + }, + + stopSharingData: function WeaveSync_stopSharingData(dataType, + onComplete, + guid, + username) { + + } }; From 59d5da906905fa41d473b632d451ce103a4d0fc4 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 23 Jun 2008 20:57:10 -0700 Subject: [PATCH 0444/1860] Refactored stuff out of test_passwords and into head.js to make creating new sync tests for different engines easier. --- services/sync/tests/unit/head_first.js | 51 ++++++++++++++++ services/sync/tests/unit/test_passwords.js | 70 ++++------------------ 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index d2157c5238ad..2e55b348b0a8 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -273,3 +273,54 @@ function FakeGUIDService() { return "fake-guid-" + latestGUID++; }; } + +function SyncTestingInfrastructure() { + let __fakePasswords = { + 'Mozilla Services Password': {foo: "bar"}, + 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} + }; + + let __fakePrefs = { + "encryption" : "none", + "log.logger.service.crypto" : "Debug", + "log.logger.service.engine" : "Debug", + "log.logger.async" : "Debug" + }; + + Cu.import("resource://weave/identity.js"); + + ID.set('WeaveID', + new Identity('Mozilla Services Encryption Passphrase', 'foo')); + + this.fakePasswordService = new FakePasswordService(__fakePasswords); + this.fakePrefService = new FakePrefService(__fakePrefs); + this.fakeDAVService = new FakeDAVService({}); + this.fakeTimerService = new FakeTimerService(); + this.logStats = initTestLogging(); + this.fakeFilesystem = new FakeFilesystemService({}); + this.fakeGUIDService = new FakeGUIDService(); + + this.__makeCallback = function __makeCallback() { + this.__callbackCalled = false; + let self = this; + return function callback() { + self.__callbackCalled = true; + }; + }; + + this.runAsyncFunc = function runAsyncFunc(name, func) { + let logger = getTestLogger(); + + logger.info("-----------------------------------------"); + logger.info("Step '" + name + "' starting."); + logger.info("-----------------------------------------"); + func(this.__makeCallback()); + while (this.fakeTimerService.processCallback()) {} + do_check_true(this.__callbackCalled); + for (name in Async.outstandingGenerators) + logger.warn("Outstanding generator exists: " + name); + do_check_eq(this.logStats.errorsLogged, 0); + do_check_eq(Async.outstandingGenerators.length, 0); + logger.info("Step '" + name + "' succeeded."); + }; +} diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 9667768f946a..9b69927314de 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -1,24 +1,9 @@ Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/dav.js"); -Cu.import("resource://weave/identity.js"); // ---------------------------------------- // Fake Data // ---------------------------------------- -let __fakePasswords = { - 'Mozilla Services Password': {foo: "bar"}, - 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} -}; - -let __fakePrefs = { - "encryption" : "none", - "log.logger.service.crypto" : "Debug", - "log.logger.service.engine" : "Debug", - "log.logger.async" : "Debug" -}; - let __fakeLogins = [ // Fake nsILoginInfo object. {hostname: "www.boogle.com", @@ -35,9 +20,6 @@ let __fakeLogins = [ // ---------------------------------------- function run_test() { - ID.set('WeaveID', - new Identity('Mozilla Services Encryption Passphrase', 'foo')); - // The JS module we're testing, with all members exposed. var passwords = loadInSandbox("resource://weave/engines/passwords.js"); @@ -48,18 +30,22 @@ function run_test() { // Ensure that PasswordSyncCore._itemExists() works. var psc = new passwords.PasswordSyncCore(); + psc.__loginManager = {getAllLogins: function() { return __fakeLogins; }}; do_check_false(psc._itemExists("invalid guid")); do_check_true(psc._itemExists(fakeUserHash)); // Make sure the engine can sync. + var syncTesting = new SyncTestingInfrastructure(); + var fakeLoginManager = new FakeLoginManager(__fakeLogins); + function freshEngineSync(cb) { let engine = new passwords.PasswordEngine(); engine.sync(cb); }; - runAndEnsureSuccess("initial sync", freshEngineSync); + syncTesting.runAsyncFunc("initial sync", freshEngineSync); - runAndEnsureSuccess("trivial re-sync", freshEngineSync); + syncTesting.runAsyncFunc("trivial re-sync", freshEngineSync); fakeLoginManager.fakeLogins.push( {hostname: "www.yoogle.com", @@ -71,58 +57,22 @@ function run_test() { passwordField: "test_password2"} ); - runAndEnsureSuccess("add user and re-sync", freshEngineSync); + syncTesting.runAsyncFunc("add user and re-sync", freshEngineSync); fakeLoginManager.fakeLogins.pop(); - runAndEnsureSuccess("remove user and re-sync", freshEngineSync); + syncTesting.runAsyncFunc("remove user and re-sync", freshEngineSync); - fakeFilesystem.fakeContents = {}; + syncTesting.fakeFilesystem.fakeContents = {}; fakeLoginManager.fakeLogins = []; - runAndEnsureSuccess("resync on second computer", freshEngineSync); -} - -// ---------------------------------------- -// Helper Functions -// ---------------------------------------- - -var callbackCalled = false; - -function __makeCallback() { - callbackCalled = false; - return function callback() { - callbackCalled = true; - }; -} - -function runAndEnsureSuccess(name, func) { - getTestLogger().info("-----------------------------------------"); - getTestLogger().info("Step '" + name + "' starting."); - getTestLogger().info("-----------------------------------------"); - func(__makeCallback()); - while (fts.processCallback()) {} - do_check_true(callbackCalled); - for (name in Async.outstandingGenerators) - getTestLogger().warn("Outstanding generator exists: " + name); - do_check_eq(logStats.errorsLogged, 0); - do_check_eq(Async.outstandingGenerators.length, 0); - getTestLogger().info("Step '" + name + "' succeeded."); + syncTesting.runAsyncFunc("resync on second computer", freshEngineSync); } // ---------------------------------------- // Fake Infrastructure // ---------------------------------------- -var fpasses = new FakePasswordService(__fakePasswords); -var fprefs = new FakePrefService(__fakePrefs); -var fds = new FakeDAVService({}); -var fts = new FakeTimerService(); -var logStats = initTestLogging(); -var fakeFilesystem = new FakeFilesystemService({}); -var fgs = new FakeGUIDService(); -var fakeLoginManager = new FakeLoginManager(__fakeLogins); - function FakeLoginManager(fakeLogins) { this.fakeLogins = fakeLogins; From bb3c62b6e71c5ac3242bfd89598ffd75b148cbfd Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 23 Jun 2008 21:21:40 -0700 Subject: [PATCH 0445/1860] Separated test_passwords into itself, which contains pure unit tests, and test_password_syncing, which contains a suite that's more along the lines of a system test, though still with plenty of stuff faked-out. fake_login_manager.js contains code shared between the two suites. --- .../sync/tests/unit/fake_login_manager.js | 38 ++++++++ .../sync/tests/unit/test_password_syncing.js | 42 +++++++++ ...ted => test_password_syncing.log.expected} | 0 services/sync/tests/unit/test_passwords.js | 91 ++----------------- 4 files changed, 89 insertions(+), 82 deletions(-) create mode 100644 services/sync/tests/unit/fake_login_manager.js create mode 100644 services/sync/tests/unit/test_password_syncing.js rename services/sync/tests/unit/{test_passwords.log.expected => test_password_syncing.log.expected} (100%) diff --git a/services/sync/tests/unit/fake_login_manager.js b/services/sync/tests/unit/fake_login_manager.js new file mode 100644 index 000000000000..3c01d24479eb --- /dev/null +++ b/services/sync/tests/unit/fake_login_manager.js @@ -0,0 +1,38 @@ +Cu.import("resource://weave/util.js"); + +// ---------------------------------------- +// Fake Sample Data +// ---------------------------------------- + +let fakeSampleLogins = [ + // Fake nsILoginInfo object. + {hostname: "www.boogle.com", + formSubmitURL: "http://www.boogle.com/search", + httpRealm: "", + username: "", + password: "", + usernameField: "test_person", + passwordField: "test_password"} +]; + +// ---------------------------------------- +// Fake Login Manager +// ---------------------------------------- + +function FakeLoginManager(fakeLogins) { + this.fakeLogins = fakeLogins; + + let self = this; + + Utils.getLoginManager = function fake_getLoginManager() { + // Return a fake nsILoginManager object. + return { + getAllLogins: function() { return self.fakeLogins; }, + addLogin: function(login) { + getTestLogger().info("nsILoginManager.addLogin() called " + + "with hostname '" + login.hostname + "'."); + self.fakeLogins.push(login); + } + }; + }; +} diff --git a/services/sync/tests/unit/test_password_syncing.js b/services/sync/tests/unit/test_password_syncing.js new file mode 100644 index 000000000000..90af22cb7fbe --- /dev/null +++ b/services/sync/tests/unit/test_password_syncing.js @@ -0,0 +1,42 @@ +Cu.import("resource://weave/engines/passwords.js"); + +load("fake_login_manager.js"); + +// ---------------------------------------- +// Test Logic +// ---------------------------------------- + +function run_test() { + var syncTesting = new SyncTestingInfrastructure(); + var fakeLoginManager = new FakeLoginManager(fakeSampleLogins); + + function freshEngineSync(cb) { + let engine = new PasswordEngine(); + engine.sync(cb); + }; + + syncTesting.runAsyncFunc("initial sync", freshEngineSync); + + syncTesting.runAsyncFunc("trivial re-sync", freshEngineSync); + + fakeLoginManager.fakeLogins.push( + {hostname: "www.yoogle.com", + formSubmitURL: "http://www.yoogle.com/search", + httpRealm: "", + username: "", + password: "", + usernameField: "test_person2", + passwordField: "test_password2"} + ); + + syncTesting.runAsyncFunc("add user and re-sync", freshEngineSync); + + fakeLoginManager.fakeLogins.pop(); + + syncTesting.runAsyncFunc("remove user and re-sync", freshEngineSync); + + syncTesting.fakeFilesystem.fakeContents = {}; + fakeLoginManager.fakeLogins = []; + + syncTesting.runAsyncFunc("resync on second computer", freshEngineSync); +} diff --git a/services/sync/tests/unit/test_passwords.log.expected b/services/sync/tests/unit/test_password_syncing.log.expected similarity index 100% rename from services/sync/tests/unit/test_passwords.log.expected rename to services/sync/tests/unit/test_password_syncing.log.expected diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 9b69927314de..ee9ca4fda7b3 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -1,92 +1,19 @@ -Cu.import("resource://weave/util.js"); +load("fake_login_manager.js"); -// ---------------------------------------- -// Fake Data -// ---------------------------------------- +var loginMgr = new FakeLoginManager(fakeSampleLogins); -let __fakeLogins = [ - // Fake nsILoginInfo object. - {hostname: "www.boogle.com", - formSubmitURL: "http://www.boogle.com/search", - httpRealm: "", - username: "", - password: "", - usernameField: "test_person", - passwordField: "test_password"} -]; +// The JS module we're testing, with all members exposed. +var passwords = loadInSandbox("resource://weave/engines/passwords.js"); -// ---------------------------------------- -// Test Logic -// ---------------------------------------- - -function run_test() { - // The JS module we're testing, with all members exposed. - var passwords = loadInSandbox("resource://weave/engines/passwords.js"); - - // Ensure that _hashLoginInfo() works. - var fakeUserHash = passwords._hashLoginInfo(__fakeLogins[0]); +function test_hashLoginInfo_works() { + var fakeUserHash = passwords._hashLoginInfo(fakeSampleLogins[0]); do_check_eq(typeof fakeUserHash, 'string'); do_check_eq(fakeUserHash.length, 40); +} - // Ensure that PasswordSyncCore._itemExists() works. +function test_synccore_itemexists_works() { + var fakeUserHash = passwords._hashLoginInfo(fakeSampleLogins[0]); var psc = new passwords.PasswordSyncCore(); - psc.__loginManager = {getAllLogins: function() { return __fakeLogins; }}; do_check_false(psc._itemExists("invalid guid")); do_check_true(psc._itemExists(fakeUserHash)); - - // Make sure the engine can sync. - var syncTesting = new SyncTestingInfrastructure(); - var fakeLoginManager = new FakeLoginManager(__fakeLogins); - - function freshEngineSync(cb) { - let engine = new passwords.PasswordEngine(); - engine.sync(cb); - }; - - syncTesting.runAsyncFunc("initial sync", freshEngineSync); - - syncTesting.runAsyncFunc("trivial re-sync", freshEngineSync); - - fakeLoginManager.fakeLogins.push( - {hostname: "www.yoogle.com", - formSubmitURL: "http://www.yoogle.com/search", - httpRealm: "", - username: "", - password: "", - usernameField: "test_person2", - passwordField: "test_password2"} - ); - - syncTesting.runAsyncFunc("add user and re-sync", freshEngineSync); - - fakeLoginManager.fakeLogins.pop(); - - syncTesting.runAsyncFunc("remove user and re-sync", freshEngineSync); - - syncTesting.fakeFilesystem.fakeContents = {}; - fakeLoginManager.fakeLogins = []; - - syncTesting.runAsyncFunc("resync on second computer", freshEngineSync); -} - -// ---------------------------------------- -// Fake Infrastructure -// ---------------------------------------- - -function FakeLoginManager(fakeLogins) { - this.fakeLogins = fakeLogins; - - let self = this; - - Utils.getLoginManager = function fake_getLoginManager() { - // Return a fake nsILoginManager object. - return { - getAllLogins: function() { return self.fakeLogins; }, - addLogin: function(login) { - getTestLogger().info("nsILoginManager.addLogin() called " + - "with hostname '" + login.hostname + "'."); - self.fakeLogins.push(login); - } - }; - }; } From dd3985575f02e75541b342d4eff36ff9ebc47d84 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Mon, 23 Jun 2008 22:48:44 -0700 Subject: [PATCH 0446/1860] consolidated error reporting, formatting tbd --- services/sync/locales/en-US/wizard.dtd | 21 ++++---- services/sync/locales/en-US/wizard.properties | 50 ++++++++----------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 785f9a5430e8..e7f17682f2b7 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,43 +1,45 @@ - - + + - + + - + - + - + - - + - + + + @@ -77,3 +79,4 @@ + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 13e383b6c2ca..c1f4c2d62465 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,34 +1,28 @@ -verifyStatusUnverified.label = Unverified -verifyStatusVerifying.label = Verifying username and password -verifyStatusLoginVerified.label = Username and password verified -verifyStatusLoginFailed.label = Incorrect username or password +serverError.label = Server Error +verify-progress.label = Verifying username and password +verify-error.label = Invalid username and password +verify-success.label = Username and password verified -invalidCredentials.alert = You must provide a valid Weave user name and password to continue. -missingUsername.label = Please enter all fields. - - -checkingUsername.label = Checking if it's available... -usernameTaken.label = That username is already taken. Please choose another. -usernameAvailable.label = Available. -usernameUnverified.label = Unverified - -loginFailed.label = Please enter a valid username and password. - -requiredFields.label = Please enter all required fields. +createUsername-progress.label = Checking availability +createUsername-error.label = Already in use +createUsername-success.label = Available +passwordsUnmatched.label = Passwords do not match passphrasesUnmatched.label = Passphrases do not match. samePasswordAndPassphrase.label = Your password and passphrase must be different -emailUnverified.label = Unverified -checkingEmail.label = Checking your email address. -emailTaken.label = Email address already exists. -emailInvalid.label = Invalid email address. -emailOk.label = Email address is valid. +email-progress.label = Checking your email address. +email-unavailable.label = Email address already exists. +email-invalid.label = Invalid email address. +email-success.label = Email address is valid. + missingCaptchaResponse.label = Please enter the words in the Captcha box. incorrectCaptcha.label = Incorrect Captcha response. Try again. -internalError.label = Sorry! We had a problem. Please try again. + + + initialLogin-progress.label = Signing you in... initialLogin-done.label = Sign-in successful. @@ -45,10 +39,10 @@ initialSync-done.label = Sync successful. initialSync-error.label = Problem syncing data. -tryAgain.text = Click here +tryAgain.text = Try again now. +clickHere.text = Click here - -data-verify.title = Data (Step 3 of 4) -data-create.title = Data (Step 5 of 6) -final-verify.title = Finish (Step 4 of 4) -final-create.title = Finish (Step 6 of 6) +data-verify.title = Data (Step 2 of 3) +data-create.title = Data (Step 4 of 5) +final-verify.title = Finish (Step 3 of 3) +final-create.title = Finish (Step 5 of 5) From 2cb34138fe8332b1f83f244f2b0da0a3e5c0d793 Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 08:51:40 -0700 Subject: [PATCH 0447/1860] another pass at polishing the first run experience, work in progress --- services/sync/locales/en-US/wizard.dtd | 58 ++++++++++--------- services/sync/locales/en-US/wizard.properties | 24 ++++---- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index e7f17682f2b7..3e4c83e44b4e 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -4,20 +4,18 @@ - - - - - - - - - - + + + + + + + + + - @@ -25,29 +23,29 @@ - - - - - + + + + + + + + - + + + + - - - - - - - - - + + + @@ -59,6 +57,14 @@ + + + + + + + + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index c1f4c2d62465..aaede913805d 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,27 +1,31 @@ serverError.label = Server Error +serverTimeoutError.label = Server Error -verify-progress.label = Verifying username and password -verify-error.label = Invalid username and password -verify-success.label = Username and password verified -createUsername-progress.label = Checking availability -createUsername-error.label = Already in use -createUsername-success.label = Available +createUsername-progress.label = +createUsername-error.label = %S is already taken. Please choose another. +createUsername-success.label = %S is available. + +email-progress.label = +email-unavailable.label = %S is already taken. Please choose another. +email-invalid.label = Please re-enter a valid email address. +email-success.label = %S is available. + passwordsUnmatched.label = Passwords do not match passphrasesUnmatched.label = Passphrases do not match. samePasswordAndPassphrase.label = Your password and passphrase must be different -email-progress.label = Checking your email address. -email-unavailable.label = Email address already exists. -email-invalid.label = Invalid email address. -email-success.label = Email address is valid. missingCaptchaResponse.label = Please enter the words in the Captcha box. incorrectCaptcha.label = Incorrect Captcha response. Try again. +verify-progress.label = Verifying username and password +verify-error.label = Invalid username and password +verify-success.label = Username and password verified + initialLogin-progress.label = Signing you in... From c115770087bf2323df772f8f73ddb9863690f5b9 Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 12:22:32 -0700 Subject: [PATCH 0448/1860] Finished the bridge between UI code and bookmark engine so that stopSharing gets called when you pick the menu itme. --- services/sync/modules/service.js | 36 +++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 49a5bb94b32a..6f55d051628a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -682,23 +682,45 @@ WeaveSvc.prototype = { _shareData: function WeaveSync__shareData(dataType, guid, username) { - dump( "in _shareData...\n" ); let self = yield; - if (!Engines.get(dataType).enabled) { + let ret; + if (Engines.get(dataType).enabled) { + Engines.get(dataType).share(self.cb, guid, username); + ret = yield; + } else { this._log.warn( "Can't share disabled data type: " + dataType ); - return; + ret = false; } - Engines.get(dataType).share(self.cb, guid, username); - let ret = yield; self.done(ret); }, + /* LONGTERM TODO this is almost duplicated code, maybe just have + * one function where we pass in true to share and false to stop + * sharing. */ stopSharingData: function WeaveSync_stopSharingData(dataType, onComplete, guid, username) { + let messageName = "stop-sharing-" + dataType; + this._lock(this._notify(messageName, + this._stopSharingData, + dataType, + guid, + username)).async(this, onComplete); + }, - + _stopSharingData: function WeaveSync__stopSharingData(dataType, + onComplete, + guid, + username) { + let self = yield; + let ret; + if (Engines.get(dataType).enabled) { + Engines.get(dataType).stopSharing(self.cb, guid, username); + ret = yield; + } else { + ret = false; + } + self.done(ret); } - }; From eda3b8e22471d491ec7730fc52f33ee12547245e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 24 Jun 2008 12:33:27 -0700 Subject: [PATCH 0449/1860] Store each delta as a separate file on the server --- services/sync/modules/engines.js | 12 ++-- services/sync/modules/remote.js | 113 ++++++++++++++++++++----------- 2 files changed, 79 insertions(+), 46 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 08ee457ca734..ee37ba01c80a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -379,20 +379,18 @@ Engine.prototype = { this._snapshot.data = newSnapshot; this._snapshot.version = ++this._remote.status.data.maxVersion; + // XXX don't append delta if we do a full upload? if (this._remote.status.data.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) yield this._remote.initialize(self.cb, this._snapshot); - this._remote.appendDelta(self.cb, serverDelta); - yield; - let c = 0; for (GUID in this._snapshot.data) c++; - this._remote.status.data.maxVersion = this._snapshot.version; - this._remote.status.data.snapEncryption = Crypto.defaultAlgorithm; - this._remote.status.data.itemCount = c; - this._remote.status.put(self.cb, this._remote.status.data); + this._remote.appendDelta(self.cb, serverDelta, + {maxVersion: this._snapshot.version, + deltasEncryption: Crypto.defaultAlgorithm, + itemCount: c}); yield; this._log.info("Successfully updated deltas and status on server"); diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 2a54af932caa..49dfa172102d 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -230,6 +230,49 @@ Resource.prototype = { } }; +function ResourceSet(basePath) { + this._init(basePath); +} +ResourceSet.prototype = { + __proto__: new Resource(), + _init: function ResSet__init(basePath) { + this.__proto__.__proto__._init.call(this); + this._basePath = basePath; + this._log = Log4Moz.Service.getLogger("Service.ResourceSet"); + }, + + _hack: function ResSet__hack(action, id, data) { + let self = yield; + let savedData = this._data; + + if ("PUT" == action) + this._data = data; + + this._path = this._basePath + id; + yield this._request.async(this, self.cb, action, data); + + let newData = this._data; + this._data = savedData; + if (this._data == null) + this._data = {}; + this._data[id] = newData; + + self.done(this._data[id]); + }, + + get: function ResSet_get(onComplete, id) { + this._hack.async(this, onComplete, "GET", id); + }, + + put: function ResSet_put(onComplete, id, data) { + this._hack.async(this, onComplete, "PUT", id, data); + }, + + delete: function ResSet_delete(onComplete, id) { + this._hack.async(this, onComplete, "DELETE", id); + } +}; + function ResourceFilter() { this._log = Log4Moz.Service.getLogger("Service.ResourceFilter"); } @@ -298,18 +341,6 @@ CryptoFilter.prototype = { } }; -function Status(remoteStore) { - this._init(remoteStore); -} -Status.prototype = { - __proto__: new Resource(), - _init: function Status__init(remoteStore) { - this._remote = remoteStore; - this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "status.json"); - this.pushFilter(new JsonFilter()); - } -}; - function Keychain(remoteStore) { this._init(remoteStore); } @@ -352,7 +383,8 @@ RemoteStore.prototype = { get engineId() this._engine.engineId, get status() { - let status = new Status(this); + let status = new Resource(this.serverPrefix + "status.json"); + status.pushFilter(new JsonFilter()); this.__defineGetter__("status", function() status); return status; }, @@ -372,7 +404,7 @@ RemoteStore.prototype = { }, get _deltas() { - let deltas = new Resource(this.serverPrefix + "deltas.json"); + let deltas = new ResourceSet(this.serverPrefix + "deltas/"); deltas.pushFilter(new JsonFilter()); deltas.pushFilter(new CryptoFilter(this, "deltasEncryption")); this.__defineGetter__("_deltas", function() deltas); @@ -390,8 +422,7 @@ RemoteStore.prototype = { this._snapshot.data = null; this._deltas.data = null; - DAV.MKCOL(this.serverPrefix, self.cb); - let ret = yield; + let ret = yield DAV.MKCOL(this.serverPrefix + "deltas", self.cb); if (!ret) throw "Could not create remote folder"; @@ -421,6 +452,7 @@ RemoteStore.prototype = { }, // Does a fresh upload of the given snapshot to a new store + // FIXME: add 'metadata' arg here like appendDelta's _initialize: function RStore__initialize(snapshot) { let self = yield; let wrappedSymkey; @@ -440,7 +472,7 @@ RemoteStore.prototype = { yield this.keys.put(self.cb, keys); yield this._snapshot.put(self.cb, snapshot.data); - yield this._deltas.put(self.cb, []); + //yield this._deltas.put(self.cb, []); let c = 0; for (GUID in snapshot.data) @@ -467,7 +499,7 @@ RemoteStore.prototype = { yield this.status.delete(self.cb); yield this.keys.delete(self.cb); yield this._snapshot.delete(self.cb); - yield this._deltas.delete(self.cb); + //yield this._deltas.delete(self.cb); this._log.debug("Server files deleted"); }, wipe: function Engine_wipe(onComplete) { @@ -478,20 +510,17 @@ RemoteStore.prototype = { // (snapshot + deltas) _getLatestFromScratch: function RStore__getLatestFromScratch() { let self = yield; + let status = this.status.data; this._log.info("Downloading all server data from scratch"); - this._log.debug("Downloading server snapshot"); - let data = yield this._snapshot.get(self.cb); - this._log.debug("Downloading server deltas"); - let deltas = yield this._deltas.get(self.cb); - let snap = new SnapshotStore(); - snap.version = this.status.data.maxVersion; - snap.data = data; - for (let i = 0; i < deltas.length; i++) { - snap.applyCommands.async(snap, self.cb, deltas[i]); - yield; + snap.data = yield this._snapshot.get(self.cb); + snap.version = status.maxVersion; + + for (let id = status.snapVersion + 1; id <= status.maxVersion; id++) { + let delta = yield this._deltas.get(self.cb, id); + yield snap.applyCommands.async(snap, self.cb, delta); } self.done(snap); @@ -514,8 +543,12 @@ RemoteStore.prototype = { this._log.debug("Using last sync snapshot as starting point for server snapshot"); snap.data = Utils.deepCopy(lastSyncSnap.data); this._log.info("Downloading server deltas"); - let allDeltas = yield this._deltas.get(self.cb); - deltas = allDeltas.slice(lastSyncSnap.version - this.status.data.snapVersion); + deltas = []; + let min = lastSyncSnap.version + 1; + let max = this.status.data.maxVersion; + for (let id = min; id <= max; id++) { + deltas.push(yield this._deltas.get(self.cb, id)); + } } else if (lastSyncSnap.version == this.status.data.maxVersion) { this._log.debug("Using last sync snapshot as server snapshot (snap version == max version)"); @@ -557,17 +590,19 @@ RemoteStore.prototype = { }, // Adds a new set of changes (a delta) to this store - _appendDelta: function RStore__appendDelta(delta) { + _appendDelta: function RStore__appendDelta(delta, metadata) { let self = yield; - if (this._deltas.data == null) { - yield this._deltas.get(self.cb); - if (this._deltas.data == null) - this._deltas.data = []; + + if (metadata) { + for (let key in metadata) + this.status.data[key] = metadata[key]; } - this._deltas.data.push(delta); - yield this._deltas.put(self.cb, this._deltas.data); + + let id = this.status.data.maxVersion; // FIXME: we increment maxVersion in Engine + yield this._deltas.put(self.cb, id, delta); + yield this.status.put(self.cb, this.status.data); }, - appendDelta: function RStore_appendDelta(onComplete, delta) { - this._appendDelta.async(this, onComplete, delta); + appendDelta: function RStore_appendDelta(onComplete, delta, metadata) { + this._appendDelta.async(this, onComplete, delta, metadata); } }; From 2d16278a6494d95abbf9473564b136f863f5a80c Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 12:42:52 -0700 Subject: [PATCH 0450/1860] added embedded verification method to login (this wants to be it's own object method, but should work well enough for now.) some formatted cleanup for wizard.js --- services/sync/locales/en-US/wizard.dtd | 1 - services/sync/modules/service.js | 19 +++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 3e4c83e44b4e..db9dd5611f97 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,7 +1,6 @@ - diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 749ec84e0769..6fa7633eb429 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -499,11 +499,11 @@ WeaveSvc.prototype = { // These are global (for all engines) - login: function WeaveSync_login(onComplete, password, passphrase) { + login: function WeaveSync_login(onComplete, password, passphrase, verifyonly) { this._localLock(this._notify("login", this._login, - password, passphrase)).async(this, onComplete); + password, passphrase, verifyonly)).async(this, onComplete); }, - _login: function WeaveSync__login(password, passphrase) { + _login: function WeaveSync__login(password, passphrase, verifyonly) { let self = yield; // cache password & passphrase @@ -511,7 +511,10 @@ WeaveSvc.prototype = { ID.get('WeaveID').setTempPassword(password); ID.get('WeaveCryptoID').setTempPassword(passphrase); - this._log.debug("Logging in"); + if(verifyonly) + this._log.debug("Verifying login"); + else + this._log.debug("Logging in"); if (!this.username) throw "No username set, login failed"; @@ -536,6 +539,14 @@ WeaveSvc.prototype = { throw "Login failed"; } + // If being called from the Wizard to verify credentials, stop here. + if (verifyonly) { + this._log.debug("Login verified"); + self.done(true); + return; + } + // Otherwise, setup the user session. + this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); this._versionCheck.async(this, self.cb); From 4efb28d988f4ac798c963e977b52c86c45383a7d Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 13:41:01 -0700 Subject: [PATCH 0451/1860] Added todo about exception handling --- services/sync/modules/engines/bookmarks.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d0ef4c23fb72..67690d73a7fa 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -414,6 +414,8 @@ BookmarksEngine.prototype = { // directory: let serverPath = this._annoSvc.getItemAnnotation(folderNode, SERVER_PATH_ANNO); + // TODO the above can throw an exception if the expected anotation isn't + // there. // From that directory, get the keyring file, and from it, the symmetric // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); From 5d3a15245b792122c565870953b4cbb0e0505242 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 24 Jun 2008 16:55:56 -0700 Subject: [PATCH 0452/1860] Added a really, really basic sync test for bookmarks. --- services/sync/tests/unit/head_first.js | 3 +- .../sync/tests/unit/test_bookmark_syncing.js | 18 +++++++ .../unit/test_bookmark_syncing.log.expected | 47 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 services/sync/tests/unit/test_bookmark_syncing.js create mode 100644 services/sync/tests/unit/test_bookmark_syncing.log.expected diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 2e55b348b0a8..1ab0c3db6743 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -284,7 +284,8 @@ function SyncTestingInfrastructure() { "encryption" : "none", "log.logger.service.crypto" : "Debug", "log.logger.service.engine" : "Debug", - "log.logger.async" : "Debug" + "log.logger.async" : "Debug", + "xmpp.enabled" : false }; Cu.import("resource://weave/identity.js"); diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/test_bookmark_syncing.js new file mode 100644 index 000000000000..4a3319ff2563 --- /dev/null +++ b/services/sync/tests/unit/test_bookmark_syncing.js @@ -0,0 +1,18 @@ +Cu.import("resource://weave/engines/bookmarks.js"); + +// ---------------------------------------- +// Test Logic +// ---------------------------------------- + +function run_test() { + var syncTesting = new SyncTestingInfrastructure(); + + function freshEngineSync(cb) { + let engine = new BookmarksEngine(); + engine.sync(cb); + }; + + syncTesting.runAsyncFunc("initial sync", freshEngineSync); + + syncTesting.runAsyncFunc("trivial re-sync", freshEngineSync); +} diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/test_bookmark_syncing.log.expected new file mode 100644 index 000000000000..775c0d80dfa9 --- /dev/null +++ b/services/sync/tests/unit/test_bookmark_syncing.log.expected @@ -0,0 +1,47 @@ +*** test pending +Testing INFO ----------------------------------------- +Testing INFO Step 'initial sync' starting. +Testing INFO ----------------------------------------- +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 404 +Service.BmkEngine INFO Initial upload to server +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO HTTP PUT to user-data/bookmarks/keys.json with data: {"ring":{},"bulkIV":null} +Service.Resource DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data +Testing INFO HTTP PUT to user-data/bookmarks/snapshot.json with data: {"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}} +Service.Resource DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":3} +Service.Resource DEBUG PUT request successful +Service.RemoteStore INFO Full upload to server successful +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":0,"GUID":"fake-guid-0","snapshot":{"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} +Testing INFO Step 'initial sync' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 'trivial re-sync' starting. +Testing INFO ----------------------------------------- +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.BmkEngine INFO Local snapshot version: 0 +Service.BmkEngine INFO Server maxVersion: 0 +Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) +Service.RemoteStore TRACE Local snapshot version == server maxVersion +Service.BmkEngine INFO Sync complete: no changes needed on client or server +Testing INFO Step 'trivial re-sync' succeeded. +*** test finished +*** exiting +*** PASS *** From 0f9b4b6d1fbfaebbf6ea898e531317e61f00b37e Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Tue, 24 Jun 2008 17:45:37 -0700 Subject: [PATCH 0453/1860] bug 441446: set mozBackgroundRequest on XMPP request object when created rather than when used, since it only needs to be set once --- services/sync/modules/xmpp/transportLayer.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index 472313b3ce03..b48168301895 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -177,11 +177,11 @@ HTTPPollingTransport.prototype = { this._retryCap = 0; }, - __request: null, get _request() { - if (!this.__request) - this.__request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance( Ci.nsIXMLHttpRequest ); - return this.__request; + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance( Ci.nsIXMLHttpRequest ); + request.mozBackgroundRequest = true; + this.__defineGetter__("_request", function() request); + return this._request; }, __hasher: null, @@ -256,7 +256,6 @@ HTTPPollingTransport.prototype = { _doPost: function( requestXml ) { var request = this._request; - request.mozBackgroundRequest = true; var callbackObj = this._callbackObject; var self = this; var contents = ""; From 2149714c1d592ab6ecf1f7478deed96cf2229b57 Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 18:09:41 -0700 Subject: [PATCH 0454/1860] Annotations on incoming and outgoing share folders are now wrapped for sync, and handled on incoming createCommands. --- services/sync/modules/engines/bookmarks.js | 46 ++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 2ba923d71427..297c8a9ec17a 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -450,7 +450,7 @@ BookmarksEngine.prototype = { let tmpIdentity = { realm : "temp ID", bulkKey : bulkKey, - bulkIV : bulkIV + bulkIV : bulkIV }; Crypto.encryptData.async( Crypto, self.cb, json, tmpIdentity ); let cyphertext = yield; @@ -857,6 +857,20 @@ BookmarksStore.prototype = { newId = this._bms.createFolder(parentId, command.data.title, command.data.index); + // If folder is an outgoing share, put the annotations on it: + if ( command.data.outgoingShareAnno != undefined ) { + this._ans.setItemAnnotation(newId, + OUTGOING_SHARE_ANNO, + command.data.outgoingShareAnno, + 0, + this._ans.EXPIRE_NEVER); + this._ans.setItemAnnotation(newId, + SERVER_PATH_ANNO, + command.data.serverPathAnno, + 0, + this._ans.EXPIRE_NEVER); + + } break; case "livemark": this._log.debug(" -> creating livemark \"" + command.data.title + "\""); @@ -866,6 +880,16 @@ BookmarksStore.prototype = { Utils.makeURI(command.data.feedURI), command.data.index); break; + case "incoming-share": + this._log.debug(" -> creating incoming-share \"" + command.data.title + "\""); + newId = this._bms.createFolder(parentId, + command.data.title, + command.data.index); + this._ans.setItemAnnotation(newId, INCOMING_SHARE_ANNO, + command.data.username, 0, this._ans.EXPIRE_NEVER); + this._ans.setItemAnnotation(newId, SERVER_PATH_ANNO, + command.data.serverPath, 0, this._ans.EXPIRE_NEVER); + break; case "mounted-share": this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\""); newId = this._bms.createFolder(parentId, @@ -1028,7 +1052,15 @@ BookmarksStore.prototype = { let feedURI = this._ls.getFeedURI(node.itemId); item.siteURI = siteURI? siteURI.spec : ""; item.feedURI = feedURI? feedURI.spec : ""; - + } else if (this._ans.itemHasAnnotation(node.itemId, INCOMING_SHARE_ANNO)){ + // When there's an incoming share, we just sync the folder itself + // and the values of its annotations: NOT any of its contents. + item.type = "incoming-share"; + item.title = node.title; + item.serverPath = this._ans.getItemAnnotation(node.itemId, + SERVER_PATH_ANNO); + item.username = this._ans.getItemAnnotation(node.itemId, + INCOMING_SHARE_ANNO); } else if (this._ans.itemHasAnnotation(node.itemId, "weave/mounted-share-id")) { /* TODO this is for wrapping the special shared folder created by @@ -1037,11 +1069,19 @@ BookmarksStore.prototype = { item.title = node.title; item.mountId = this._ans.getItemAnnotation(node.itemId, "weave/mounted-share-id"); - } else { item.type = "folder"; node.QueryInterface(Ci.nsINavHistoryQueryResultNode); node.containerOpen = true; + // If folder is an outgoing share, wrap its annotations: + if (this._ans.itemHasAnnotation(node.itemId, OUTGOING_SHARE_ANNO)) { + + item.serverPathAnno = this._ans.getItemAnnotation(node.itemId, + SERVER_PATH_ANNO); + item.outgoingShareAnno = this._ans.getItemAnnotation(node.itemId, + OUTGOING_SHARE_ANNO); + } + for (var i = 0; i < node.childCount; i++) { this.__wrap(node.getChild(i), items, GUID, i); } From 486414c3fd58c5a005fe12ab98bbc188bde17e56 Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 18:15:17 -0700 Subject: [PATCH 0455/1860] Changes in the annotations relevant to incoming/outgoing share folders are now handled by editCommands in the syncCore. --- services/sync/modules/engines/bookmarks.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 297c8a9ec17a..2ea44733817d 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -944,6 +944,7 @@ BookmarksStore.prototype = { default: this._log.error("removeCommand: Unknown item type: " + type); break; + // TODO do we have to sync removal of incomingShares? } }, @@ -1020,6 +1021,21 @@ BookmarksStore.prototype = { case "feedURI": this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI)); break; + case "outgoingShareAnno": + this._ans.setItemAnnotation(itemId, OUTGOING_SHARE_ANNO, + command.data.outgoingShareAnno, 0, + this._ans.EXPIRE_NEVER); + break; + case "incomingShareAnno": + this._ans.setItemAnnotation(itemId, INCOMING_SHARE_ANNO, + command.data.incomingShareAnno, 0, + this._ans.EXPIRE_NEVER); + break; + case "serverPathAnno": + this._ans.setItemAnnotation(itemId, SERVER_PATH_ANNO, + command.data.serverPathAnno, 0, + this._ans.EXPIRE_NEVER); + break; default: this._log.warn("Can't change item property: " + key); break; From 5e58e13f2e1d1b943c68098438d38e690cd86093 Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 18:23:43 -0700 Subject: [PATCH 0456/1860] Removed all uses of the 'weave/mounted-shares-id' annotation, since it's been made obsolete by INCOMING_SHARED_ANNO. --- services/sync/modules/engines/bookmarks.js | 42 ++++++---------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 2ea44733817d..e304b746ed7d 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -531,9 +531,9 @@ BookmarksEngine.prototype = { share offer. Unless a folder with these exact annotations already exists, in which case do nothing. */ let itemExists = false; - a = this._annoSvc.getItemsWithAnnotation("weave/mounted-share-id", {}); + a = this._annoSvc.getItemsWithAnnotation(INCOMING_SHARED_ANNO, {}); for (let i = 0; i < a.length; i++) { - let creator = this._annoSvc.getItemAnnotation(a[i], OUTGOING_SHARED_ANNO); + let creator = this._annoSvc.getItemAnnotation(a[i], INCOMING_SHARED_ANNO); let path = this._annoSvc.getItemAnnotation(a[i], SERVER_PATH_ANNO); if ( creator == user && path == serverPath ) { itemExists = true; @@ -542,17 +542,9 @@ BookmarksEngine.prototype = { } if (!itemExists) { let newId = bms.createFolder(root, title, bms.DEFAULT_INDEX); - /* TODO: weave/mounted-share-id is kind of redundant now, but it's - treated specially by the sync code. - If i change it here, i have to change it there as well. */ - this._annoSvc.setItemAnnotation(newId, - "weave/mounted-share-id", - id, - 0, - this._annoSvc.EXPIRE_NEVER); // Keep track of who shared this folder with us... this._annoSvc.setItemAnnotation(newId, - OUTGOING_SHARED_ANNO, + INCOMING_SHARED_ANNO, user, 0, this._annoSvc.EXPIRE_NEVER); @@ -881,6 +873,9 @@ BookmarksStore.prototype = { command.data.index); break; case "incoming-share": + /* even though incoming shares are folders according to the + * bookmarkService, _wrap() wraps them as type=incoming-share, so we + * handle them separately, like so: */ this._log.debug(" -> creating incoming-share \"" + command.data.title + "\""); newId = this._bms.createFolder(parentId, command.data.title, @@ -890,15 +885,6 @@ BookmarksStore.prototype = { this._ans.setItemAnnotation(newId, SERVER_PATH_ANNO, command.data.serverPath, 0, this._ans.EXPIRE_NEVER); break; - case "mounted-share": - this._log.debug(" -> creating share mountpoint \"" + command.data.title + "\""); - newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); - - this._ans.setItemAnnotation(newId, "weave/mounted-share-id", - command.data.mountId, 0, this._ans.EXPIRE_NEVER); - break; case "separator": this._log.debug(" -> creating separator"); newId = this._bms.insertSeparator(parentId, command.data.index); @@ -944,7 +930,6 @@ BookmarksStore.prototype = { default: this._log.error("removeCommand: Unknown item type: " + type); break; - // TODO do we have to sync removal of incomingShares? } }, @@ -1069,22 +1054,15 @@ BookmarksStore.prototype = { item.siteURI = siteURI? siteURI.spec : ""; item.feedURI = feedURI? feedURI.spec : ""; } else if (this._ans.itemHasAnnotation(node.itemId, INCOMING_SHARE_ANNO)){ - // When there's an incoming share, we just sync the folder itself - // and the values of its annotations: NOT any of its contents. + /* When there's an incoming share, we just sync the folder itself + and the values of its annotations: NOT any of its contents. So + we'll wrap it as type=incoming-share, not as a "folder". */ item.type = "incoming-share"; item.title = node.title; item.serverPath = this._ans.getItemAnnotation(node.itemId, SERVER_PATH_ANNO); item.username = this._ans.getItemAnnotation(node.itemId, INCOMING_SHARE_ANNO); - } else if (this._ans.itemHasAnnotation(node.itemId, - "weave/mounted-share-id")) { - /* TODO this is for wrapping the special shared folder created by - the old-style share command. */ - item.type = "mounted-share"; - item.title = node.title; - item.mountId = this._ans.getItemAnnotation(node.itemId, - "weave/mounted-share-id"); } else { item.type = "folder"; node.QueryInterface(Ci.nsINavHistoryQueryResultNode); @@ -1180,7 +1158,7 @@ BookmarksStore.prototype = { // remove any share mountpoints for (let guid in ret.snapshot) { // TODO decide what to do with this... - if (ret.snapshot[guid].type == "mounted-share") + if (ret.snapshot[guid].type == "incoming-share") delete ret.snapshot[guid]; } From 6b1ac2c5cd707184768c591257f64de9b98e2f5d Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 18:28:01 -0700 Subject: [PATCH 0457/1860] Fixed all the places where I was accidentally calling it incomingShareAnno instead of incomingSharedAnno (note the missing letter d). Same for outgoingSharedAnno. --- services/sync/modules/engines/bookmarks.js | 50 ++++++++++++---------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e304b746ed7d..1b506c70898d 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -468,7 +468,7 @@ BookmarksEngine.prototype = { let serverPath = this._annoSvc.getItemAnnotation( folderNode, SERVER_PATH_ANNO ); let username = this._annoSvc.getItemAnnotation( folderNode, - OUTGOING_SHARE_ANNO ); + OUTGOING_SHARED_ANNO ); // Delete the share from the server: let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); @@ -487,7 +487,7 @@ BookmarksEngine.prototype = { 0, this._annoSvc.EXPIRE_NEVER); this._annoSvc.setItemAnnotation(folderNode, - OUTGOING_SHARE_ANNO, + OUTGOING_SHARED_ANNO, "", 0, this._annoSvc.EXPIRE_NEVER); @@ -850,10 +850,10 @@ BookmarksStore.prototype = { command.data.title, command.data.index); // If folder is an outgoing share, put the annotations on it: - if ( command.data.outgoingShareAnno != undefined ) { + if ( command.data.outgoingSharedAnno != undefined ) { this._ans.setItemAnnotation(newId, - OUTGOING_SHARE_ANNO, - command.data.outgoingShareAnno, + OUTGOING_SHARED_ANNO, + command.data.outgoingSharedAnno, 0, this._ans.EXPIRE_NEVER); this._ans.setItemAnnotation(newId, @@ -880,10 +880,16 @@ BookmarksStore.prototype = { newId = this._bms.createFolder(parentId, command.data.title, command.data.index); - this._ans.setItemAnnotation(newId, INCOMING_SHARE_ANNO, - command.data.username, 0, this._ans.EXPIRE_NEVER); - this._ans.setItemAnnotation(newId, SERVER_PATH_ANNO, - command.data.serverPath, 0, this._ans.EXPIRE_NEVER); + this._ans.setItemAnnotation(newId, + INCOMING_SHARED_ANNO, + command.data.incomingSharedAnno, + 0, + this._ans.EXPIRE_NEVER); + this._ans.setItemAnnotation(newId, + SERVER_PATH_ANNO, + command.data.serverPathAnno, + 0, + this._ans.EXPIRE_NEVER); break; case "separator": this._log.debug(" -> creating separator"); @@ -1006,14 +1012,14 @@ BookmarksStore.prototype = { case "feedURI": this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI)); break; - case "outgoingShareAnno": - this._ans.setItemAnnotation(itemId, OUTGOING_SHARE_ANNO, - command.data.outgoingShareAnno, 0, + case "outgoingSharedAnno": + this._ans.setItemAnnotation(itemId, OUTGOING_SHARED_ANNO, + command.data.outgoingSharedAnno, 0, this._ans.EXPIRE_NEVER); break; - case "incomingShareAnno": - this._ans.setItemAnnotation(itemId, INCOMING_SHARE_ANNO, - command.data.incomingShareAnno, 0, + case "incomingSharedAnno": + this._ans.setItemAnnotation(itemId, INCOMING_SHARED_ANNO, + command.data.incomingSharedAnno, 0, this._ans.EXPIRE_NEVER); break; case "serverPathAnno": @@ -1053,27 +1059,27 @@ BookmarksStore.prototype = { let feedURI = this._ls.getFeedURI(node.itemId); item.siteURI = siteURI? siteURI.spec : ""; item.feedURI = feedURI? feedURI.spec : ""; - } else if (this._ans.itemHasAnnotation(node.itemId, INCOMING_SHARE_ANNO)){ + } else if (this._ans.itemHasAnnotation(node.itemId, INCOMING_SHARED_ANNO)){ /* When there's an incoming share, we just sync the folder itself and the values of its annotations: NOT any of its contents. So we'll wrap it as type=incoming-share, not as a "folder". */ item.type = "incoming-share"; item.title = node.title; - item.serverPath = this._ans.getItemAnnotation(node.itemId, + item.serverPathAnno = this._ans.getItemAnnotation(node.itemId, SERVER_PATH_ANNO); - item.username = this._ans.getItemAnnotation(node.itemId, - INCOMING_SHARE_ANNO); + item.incomingSharedAnno = this._ans.getItemAnnotation(node.itemId, + INCOMING_SHARED_ANNO); } else { item.type = "folder"; node.QueryInterface(Ci.nsINavHistoryQueryResultNode); node.containerOpen = true; // If folder is an outgoing share, wrap its annotations: - if (this._ans.itemHasAnnotation(node.itemId, OUTGOING_SHARE_ANNO)) { + if (this._ans.itemHasAnnotation(node.itemId, OUTGOING_SHARED_ANNO)) { item.serverPathAnno = this._ans.getItemAnnotation(node.itemId, SERVER_PATH_ANNO); - item.outgoingShareAnno = this._ans.getItemAnnotation(node.itemId, - OUTGOING_SHARE_ANNO); + item.outgoingSharedAnno = this._ans.getItemAnnotation(node.itemId, + OUTGOING_SHARED_ANNO); } for (var i = 0; i < node.childCount; i++) { From 1cd75956aedc85e01e19af1b52dacb5a091c1dec Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 18:38:29 -0700 Subject: [PATCH 0458/1860] Moved the folder name and annotation for the incoming shares root folder to string constants. --- services/sync/modules/engines/bookmarks.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 1b506c70898d..1aa8c2a514b3 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -48,6 +48,10 @@ const SERVER_PATH_ANNO = "weave/shared-server-path"; // Standard names for shared files on the server const KEYRING_FILE_NAME = "keyring"; const SHARED_BOOKMARK_FILE_NAME = "shared_bookmarks"; +// Information for the folder that contains all incoming shares +const INCOMING_SHARE_ROOT_ANNO = "weave/mounted-shares-folder"; +const INCOMING_SHARE_ROOT_NAME = "Shared Folders"; + Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/dav.js"); @@ -513,15 +517,16 @@ BookmarksEngine.prototype = { /* Get the toolbar "Shared Folders" folder (identified by its annotation). If it doesn't already exist, create it: */ let root; - let a = this._annoSvc.getItemsWithAnnotation("weave/mounted-shares-folder", + let a = this._annoSvc.getItemsWithAnnotation(INCOMING_SHARE_ROOT_ANNO, {}); if (a.length == 1) root = a[0]; if (!root) { - root = bms.createFolder(bms.toolbarFolder, "Shared Folders", + root = bms.createFolder(bms.toolbarFolder, + INCOMING_SHARE_ROOT_NAME, bms.DEFAULT_INDEX); this._annoSvc.setItemAnnotation(root, - "weave/mounted-shares-folder", + INCOMING_SHARE_ROOT_ANNO, true, 0, this._annoSvc.EXPIRE_NEVER); From 445ae2603080de44aeeba55c903d62a321564879 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 24 Jun 2008 19:08:35 -0700 Subject: [PATCH 0459/1860] Renamed the global trace() function in async.js to traceAsyncFrame(), to avoid confusing it with Logger.trace(), AsyncException.trace, and Generator.trace, all of which are also used in that file. --- services/sync/modules/async.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index b02acc9d9634..b50d7647b5e5 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -162,7 +162,7 @@ Generator.prototype = { }, get trace() { - return "unknown (async) :: " + this.name + "\n" + trace(this._initFrame); + return "unknown (async) :: " + this.name + "\n" + traceAsyncFrame(this._initFrame); }, _handleException: function AsyncGen__handleException(e) { @@ -282,7 +282,7 @@ Generator.prototype = { this._log.error("Exception caught from onComplete handler of " + this.name + " generator"); this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.trace("Current stack trace:\n" + trace(Components.stack)); + this._log.trace("Current stack trace:\n" + traceAsyncFrame(Components.stack)); this._log.trace("Initial stack trace:\n" + this.trace); } } @@ -298,7 +298,7 @@ function formatFrame(frame) { return tmp; } -function trace(frame, str) { +function traceAsyncFrame(frame, str) { if (!str) str = ""; @@ -308,7 +308,7 @@ function trace(frame, str) { frame = frame.caller; if (frame.caller) - str = trace(frame.caller, str); + str = traceAsyncFrame(frame.caller, str); str = formatFrame(frame) + (str? "\n" : "") + str; return str; From ae8f893511abb7aa54808436009d4fed612469cf Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 24 Jun 2008 19:15:54 -0700 Subject: [PATCH 0460/1860] Renamed AsyncException.trace and Generator.trace to AsyncException.traceback and Generator.traceback, respectively, to distinguish them from Logger.trace() and also explicitly indicate their noun-like nature (since they're properties). --- services/sync/modules/async.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index b50d7647b5e5..a07e1bdb8e18 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -77,17 +77,17 @@ let gOutstandingGenerators = new NamableTracker(); function AsyncException(initFrame, message) { this.message = message; - this._trace = initFrame; + this._traceback = initFrame; } AsyncException.prototype = { get message() { return this._message; }, set message(value) { this._message = value; }, - get trace() { return this._trace; }, - set trace(value) { this._trace = value; }, + get traceback() { return this._traceback; }, + set traceback(value) { this._traceback = value; }, addFrame: function AsyncException_addFrame(frame) { - this.trace += (this.trace? "\n" : "") + formatFrame(frame); + this.traceback += (this.traceback? "\n" : "") + formatFrame(frame); }, toString: function AsyncException_toString() { @@ -161,7 +161,7 @@ Generator.prototype = { this._onComplete = value; }, - get trace() { + get traceback() { return "unknown (async) :: " + this.name + "\n" + traceAsyncFrame(this._initFrame); }, @@ -177,17 +177,17 @@ Generator.prototype = { if (e instanceof AsyncException) { // FIXME: attempt to skip repeated frames, which can happen if the // child generator never yielded. Would break for valid repeats (recursion) - if (e.trace.indexOf(formatFrame(this._initFrame)) == -1) + if (e.traceback.indexOf(formatFrame(this._initFrame)) == -1) e.addFrame(this._initFrame); } else { - e = new AsyncException(this.trace, e); + e = new AsyncException(this.traceback, e); } this._exception = e; } else { this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.debug("Stack trace:\n" + (e.trace? e.trace : this.trace)); + this._log.debug("Stack trace:\n" + (e.traceback? e.traceback : this.traceback)); } // continue execution of caller. @@ -264,7 +264,7 @@ Generator.prototype = { if (!this._generator) { this._log.error("Async method '" + this.name + "' is missing a 'yield' call " + "(or called done() after being finalized)"); - this._log.trace("Initial stack trace:\n" + this.trace); + this._log.trace("Initial stack trace:\n" + this.traceback); } else { this._generator.close(); } @@ -283,7 +283,7 @@ Generator.prototype = { this.name + " generator"); this._log.error("Exception: " + Utils.exceptionStr(e)); this._log.trace("Current stack trace:\n" + traceAsyncFrame(Components.stack)); - this._log.trace("Initial stack trace:\n" + this.trace); + this._log.trace("Initial stack trace:\n" + this.traceback); } } gOutstandingGenerators.remove(this); From 69e1bb6f7ddd2d379454e15dca1a935da93ae40c Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Tue, 24 Jun 2008 19:39:58 -0700 Subject: [PATCH 0461/1860] Bustage fix: frame.filename can be null, and the async stack dump can then fail (which causes other problems) --- services/sync/modules/async.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index a07e1bdb8e18..ec1f6a1253c1 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -293,7 +293,9 @@ Generator.prototype = { function formatFrame(frame) { // FIXME: sort of hackish, might be confusing if there are multiple // extensions with similar filenames - let tmp = frame.filename.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); + let tmp = ""; + if (frame.filename) + tmp = frame.filename.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); tmp += ":" + frame.lineNumber + " :: " + frame.name; return tmp; } From a11839a0fd31a013749db399f1383ece4db244b0 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 24 Jun 2008 19:58:50 -0700 Subject: [PATCH 0462/1860] Fixed something that I should've changed in my last commit; also removed a call to Utils.stackTrace() that shouldn't have been made. --- services/sync/modules/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 75538e1e3915..58985fb2eb50 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -699,8 +699,8 @@ WeaveSvc.prototype = { engine._tracker.resetScore(); } catch(e) { this._log.error(Utils.exceptionStr(e)); - if (e.trace) - this._log.trace(Utils.stackTrace(e.trace)); + if (e.traceback) + this._log.trace(e.traceback); } }, From bb1115e9566164eb93dfad0237520e177b17f260 Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Jun 2008 21:15:14 -0700 Subject: [PATCH 0463/1860] Moved all of the bookmark-share stuff out of the bookmarkEngine class into a new BookmarksSharingManager class. --- services/sync/modules/engines/bookmarks.js | 157 +++++++++++---------- 1 file changed, 83 insertions(+), 74 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 1aa8c2a514b3..c6a5177dc171 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -52,7 +52,6 @@ const SHARED_BOOKMARK_FILE_NAME = "shared_bookmarks"; const INCOMING_SHARE_ROOT_ANNO = "weave/mounted-shares-folder"; const INCOMING_SHARE_ROOT_NAME = "Shared Folders"; - Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/util.js"); @@ -63,44 +62,14 @@ Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/identity.js"); - -/* LONGTERM TODO: when we start working on the ability to share other types -of data besides bookmarks, the xmppClient instance should be moved to hang -off of Weave.Service instead of hanging off the BookmarksEngine. But for -now this is the easiest place to deal with it. */ Cu.import("resource://weave/xmpp/xmppClient.js"); Function.prototype.async = Async.sugar; -function BookmarksEngine(pbeId) { - this._init(pbeId); +function BookmarksSharingManager(engine) { + this._init(engine); } -BookmarksEngine.prototype = { - get name() { return "bookmarks"; }, - get logName() { return "BmkEngine"; }, - get serverPrefix() { return "user-data/bookmarks/"; }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new BookmarksSyncCore(); - return this.__core; - }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new BookmarksStore(); - return this.__store; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new BookmarksTracker(); - return this.__tracker; - }, - +BookmarksSharingManager.prototype = { __annoSvc: null, get _annoSvc() { if (!this.__anoSvc) @@ -109,15 +78,16 @@ BookmarksEngine.prototype = { return this.__annoSvc; }, - _init: function BmkEngine__init( pbeId ) { - this.__proto__.__proto__._init.call( this, pbeId ); + _init: function SharingManager__init(engine) { + this._engine = engine; + this._log = Log4Moz.Service.getLogger("Bookmark Share"); if ( Utils.prefs.getBoolPref( "xmpp.enabled" ) ) { dump( "Starting XMPP client for bookmark engine..." ); this._startXmppClient.async(this); } }, - _startXmppClient: function BmkEngine__startXmppClient() { + _startXmppClient: function BmkSharing__startXmppClient() { // To be called asynchronously. let self = yield; @@ -140,7 +110,7 @@ BookmarksEngine.prototype = { clientPassword, transport, auth ); - let bmkEngine = this; + let bmkSharing = this; let messageHandler = { handle: function ( messageText, from ) { /* The callback function for incoming xmpp messages. @@ -162,9 +132,9 @@ BookmarksEngine.prototype = { let serverPath = words[1]; let directoryName = words.slice(2).join(" "); if ( commandWord == "share" ) { - bmkEngine._incomingShareOffer(from, serverPath, folderName); + bmkSharing._incomingShareOffer(from, serverPath, folderName); } else if ( commandWord == "stop" ) { - bmkEngine._incomingShareWithdrawn(from, serverPath, folderName); + bmkSharing._incomingShareWithdrawn(from, serverPath, folderName); } } }; @@ -180,7 +150,7 @@ BookmarksEngine.prototype = { self.done(); }, - _incomingShareOffer: function BmkEngine__incomingShareOffer(user, + _incomingShareOffer: function BmkSharing__incomingShareOffer(user, serverPath, folderName) { /* Called when we receive an offer from another user to share a @@ -195,10 +165,10 @@ BookmarksEngine.prototype = { right ahead to creating the incoming share. */ this._log.info("User " + user + " offered to share folder " + folderName); - this._createIncomingShare( user, serverPath, folderName ); + this._createIncomingShare(user, serverPath, folderName); }, - _incomingShareWithdrawn: function BmkEngine__incomingShareStop(user, + _incomingShareWithdrawn: function BmkSharing__incomingShareStop(user, serverPath, folderName) { /* Called when we receive a message telling us that a user who has @@ -208,21 +178,7 @@ BookmarksEngine.prototype = { this._log.info("User " + user + " stopped sharing folder " + folderName); this._stopIncomingShare(user, serverPath, folderName); }, - - _sync: function BmkEngine__sync() { - /* After syncing, also call syncMounts to get the - incoming shared bookmark folder contents. */ - let self = yield; - this.__proto__.__proto__._sync.async(this, self.cb ); - yield; - this.updateAllOutgoingShares(self.cb); - yield; - this.updateAllIncomingShares(self.cb); - yield; - self.done(); - }, - - _share: function BmkEngine__share( selectedFolder, username ) { + _share: function BmkSharing__share( selectedFolder, username ) { // Return true if success, false if failure. let ret = false; let self = yield; @@ -266,7 +222,7 @@ BookmarksEngine.prototype = { self.done( ret ); }, - _stopSharing: function BmkEngine__stopSharing( selectedFolder, username ) { + _stopSharing: function BmkSharing__stopSharing( selectedFolder, username ) { let self = yield; let folderName = selectedFolder.getAttribute( "label" ); let serverPath = this._annoSvc.getItemAnnotation(folderNode, @@ -296,10 +252,10 @@ BookmarksEngine.prototype = { self.done( true ); }, - updateAllIncomingShares: function BmkEngine_updateAllIncoming(onComplete) { + updateAllIncomingShares: function BmkSharing_updateAllIncoming(onComplete) { this._updateAllIncomingShares.async(this, onComplete); }, - _updateAllIncomingShares: function BmkEngine__updateAllIncoming() { + _updateAllIncomingShares: function BmkSharing__updateAllIncoming() { /* For every bookmark folder in my tree that has the annotation marking it as an incoming shared folder, pull down its latest contents from its owner's account on the server. (This is @@ -308,7 +264,7 @@ BookmarksEngine.prototype = { to the folder contents are simply wiped out by the latest server contents.) */ let self = yield; - let mounts = this._store.findIncomingShares(); + let mounts = this._engine._store.findIncomingShares(); for (let i = 0; i < mounts.length; i++) { try { @@ -321,10 +277,10 @@ BookmarksEngine.prototype = { } }, - updateAllOutgoingShares: function BmkEngine_updateAllOutgoing(onComplete) { + updateAllOutgoingShares: function BmkSharing_updateAllOutgoing(onComplete) { this._updateAllOutgoingShares.async(this, onComplete); }, - _updateAllOutgoingShares: function BmkEngine__updateAllOutgoing() { + _updateAllOutgoingShares: function BmkSharing__updateAllOutgoing() { let self = yield; let shares = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, {}); @@ -338,7 +294,7 @@ BookmarksEngine.prototype = { self.done(); }, - _createOutgoingShare: function BmkEngine__createOutgoing(folder, username) { + _createOutgoingShare: function BmkSharing__createOutgoing(folder, username) { /* To be called asynchronously. Folder is a node indicating the bookmark folder that is being shared; username is a string indicating the user that it is to be shared with. This function creates the directory and @@ -406,7 +362,8 @@ BookmarksEngine.prototype = { keys.ring[username] = encryptedForYou; let keyringFile = new Resource( serverPath + "/" + KEYRING_FILE_NAME ); - keyringFile.put( self.cb, this._json.encode( keys ) ); + let jsonService = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + keyringFile.put( self.cb, jsonService.encode( keys ) ); yield; // Call Atul's js api for setting htaccess: @@ -418,7 +375,7 @@ BookmarksEngine.prototype = { self.done( serverPath ); }, - _updateOutgoingShare: function BmkEngine__updateOutgoing(folderNode) { + _updateOutgoingShare: function BmkSharing__updateOutgoing(folderNode) { /* Puts all the bookmark data from the specified bookmark folder, encrypted, onto the shared directory on the server (replacing anything that was already there). @@ -445,7 +402,7 @@ BookmarksEngine.prototype = { let bulkIV = keys.bulkIV; // Get the json-wrapped contents of everything in the folder: - let json = this._store._wrapMount( folderNode, myUserName ); + let json = this._engine._store._wrapMount( folderNode, myUserName ); /* TODO what does wrapMount do with this username? Should I be passing in my own or that of the person I share with? */ @@ -463,11 +420,10 @@ BookmarksEngine.prototype = { self.done(); }, - _stopOutgoingShare: function BmkEngine__stopOutgoingShare(folderNode) { + _stopOutgoingShare: function BmkSharing__stopOutgoingShare(folderNode) { /* Stops sharing the specified folder. Deletes its data from the server, deletes the annotations that mark it as shared, and sends a message to the shar-ee to let them know it's been withdrawn. */ - // TODO: currently not called from anywhere. let self = yield; let serverPath = this._annoSvc.getItemAnnotation( folderNode, SERVER_PATH_ANNO ); @@ -562,7 +518,7 @@ BookmarksEngine.prototype = { } }, - _updateIncomingShare: function BmkEngine__updateIncomingShare(mountData) { + _updateIncomingShare: function BmkSharing__updateIncomingShare(mountData) { /* Pull down bookmarks from the server for a single incoming shared folder, obliterating whatever was in that folder before. @@ -609,18 +565,18 @@ BookmarksEngine.prototype = { /* Create diff between the json from server and the current contents; then apply the diff. */ this._log.trace("Got bookmarks from " + user + ", comparing with local copy"); - this._core.detectUpdates(self.cb, mountData.snapshot, snap.data); + this._engine._core.detectUpdates(self.cb, mountData.snapshot, snap.data); let diff = yield; // FIXME: should make sure all GUIDs here live under the mountpoint this._log.trace("Applying changes to folder from " + user); - this._store.applyCommands.async(this._store, self.cb, diff); + this._engine._store.applyCommands.async(this._engine._store, self.cb, diff); yield; this._log.trace("Shared folder from " + user + " successfully synced!"); }, - _stopIncomingShare: function BmkEngine__stopIncomingShare(user, + _stopIncomingShare: function BmkSharing__stopIncomingShare(user, serverPath, folderName) { @@ -640,6 +596,59 @@ BookmarksEngine.prototype = { } } } +} + + + +function BookmarksEngine(pbeId) { + this._init(pbeId); +} +BookmarksEngine.prototype = { + get name() { return "bookmarks"; }, + get logName() { return "BmkEngine"; }, + get serverPrefix() { return "user-data/bookmarks/"; }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new BookmarksSyncCore(); + return this.__core; + }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new BookmarksStore(); + return this.__store; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new BookmarksTracker(); + return this.__tracker; + }, + + __sharing: null, + get _sharing() { + if (!this.__sharing) + this.__sharing = new BookmarksSharingManager(this); + return this.__sharing; + }, + + _sync: function BmkEngine__sync() { + /* After syncing the regular bookmark folder contents, + * also update both the incoming and outgoing shared folders. */ + let self = yield; + this.__proto__.__proto__._sync.async(this, self.cb ); + yield; + this._sharing.updateAllOutgoingShares(self.cb); + yield; + this._sharing.updateAllIncomingShares(self.cb); + yield; + self.done(); + } + }; BookmarksEngine.prototype.__proto__ = new Engine(); From af4678ebe01bea2c4bcff5feec88572eb3c017a9 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 25 Jun 2008 00:13:36 -0700 Subject: [PATCH 0464/1860] Made exception logging routines more informative and added logtests for them. --- services/sync/modules/util.js | 36 ++++++++++++++----- .../sync/tests/unit/test_util_tracebacks.js | 36 +++++++++++++++++++ .../unit/test_util_tracebacks.log.expected | 36 +++++++++++++++++++ 3 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 services/sync/tests/unit/test_util_tracebacks.js create mode 100644 services/sync/tests/unit/test_util_tracebacks.log.expected diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index c66975b79bb9..8a82c6aae9f4 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -204,19 +204,37 @@ let Utils = { exceptionStr: function Weave_exceptionStr(e) { let message = e.message? e.message : e; - let location = e.location? " (" + e.location + ")" : ""; + let location = ""; + + if (e.location) + // It's a wrapped nsIException. + location = e.location; + else if (e.fileName && e.lineNumber) + // It's a standard JS exception. + location = "file '" + e.fileName + "', line " + e.lineNumber; + + if (location) + location = " (" + location + ")"; return message + location; }, - stackTrace: function Weave_stackTrace(stackFrame, str) { - if (stackFrame.caller) - str = Utils.stackTrace(stackFrame.caller, str); + stackTrace: function Weave_stackTrace(e) { + let output = ""; + if (e.location) { + // It's a wrapped nsIException. + let frame = e.location; + while (frame) { + output += frame + "\n"; + frame = frame.caller; + } + } else if (e.stack) + // It's a standard JS exception + output += e.stack; + else + // It's some other thrown object, e.g. a bare string. + output += "No traceback available.\n"; - if (!str) - str = ""; - str = stackFrame + "\n" + str; - - return str; + return output; }, checkStatus: function Weave_checkStatus(code, msg, ranges) { diff --git a/services/sync/tests/unit/test_util_tracebacks.js b/services/sync/tests/unit/test_util_tracebacks.js new file mode 100644 index 000000000000..c304768173ca --- /dev/null +++ b/services/sync/tests/unit/test_util_tracebacks.js @@ -0,0 +1,36 @@ +Cu.import("resource://weave/util.js"); + +function _reportException(e) { + dump("Exception caught.\n"); + dump("Exception: " + Utils.exceptionStr(e) + "\n"); + dump("Traceback:\n\n" + Utils.stackTrace(e) + "\n"); +} + +function test_stackTrace_works_with_error_object() { + try { + dump("Throwing new Error object.\n"); + throw new Error("Error!"); + } catch (e) { + _reportException(e); + } +} + +function test_stackTrace_works_with_bare_string() { + try { + dump("Throwing bare string.\n"); + throw "Error!"; + } catch (e) { + _reportException(e); + } +} + +function test_stackTrace_works_with_nsIException() { + try { + dump("Throwing a wrapped nsIException.\n"); + let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + dirSvc.get("nonexistentPropertyName"); + } catch (e) { + _reportException(e); + } +} diff --git a/services/sync/tests/unit/test_util_tracebacks.log.expected b/services/sync/tests/unit/test_util_tracebacks.log.expected new file mode 100644 index 000000000000..ad816f85b6b9 --- /dev/null +++ b/services/sync/tests/unit/test_util_tracebacks.log.expected @@ -0,0 +1,36 @@ +*** test pending +Running test: test_stackTrace_works_with_error_object +Throwing new Error object. +Exception caught. +Exception: Error! (file 'test_util_tracebacks.js', line 12) +Traceback: + +Error("Error!")@:0 +test_stackTrace_works_with_error_object()@test_util_tracebacks.js:12 +_find_and_run_tests()@../harness/head.js:106 +_execute_test(_find_and_run_tests)@../harness/head.js:144 +@../harness/tail.js:38 + +Running test: test_stackTrace_works_with_bare_string +Throwing bare string. +Exception caught. +Exception: Error! +Traceback: + +No traceback available. + +Running test: test_stackTrace_works_with_nsIException +Throwing a wrapped nsIException. +Exception caught. +Exception: Not enough arguments [nsIProperties.get] (JS frame :: test_util_tracebacks.js :: test_stackTrace_works_with_nsIException :: line 32) +Traceback: + +JS frame :: test_util_tracebacks.js :: test_stackTrace_works_with_nsIException :: line 32 +JS frame :: ../harness/head.js :: _find_and_run_tests :: line 106 +JS frame :: ../harness/head.js :: _execute_test :: line 144 +JS frame :: ../harness/tail.js :: :: line 38 + +3 of 3 tests passed. +*** test finished +*** exiting +*** PASS *** From db86cb17891fcb127098fee6fe41b8a60ff2a791 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Wed, 25 Jun 2008 00:22:53 -0700 Subject: [PATCH 0465/1860] Split account verification out from login(), make wizard work for using an existing account, related code cleanup/simplification --- services/sync/modules/dav.js | 27 +++++++++---- services/sync/modules/service.js | 69 ++++++++++++++------------------ 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 5c70610d12d7..ee06cac87e2e 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -301,21 +301,32 @@ DAVCollection.prototype = { // Login / Logout - checkLogin: function DC_checkLogin() { + checkLogin: function DC_checkLogin(username, password) { let self = yield; - this._log.debug("Checking login"); + this._log.debug("checkLogin called for user " + username); + + let headers = { + 'Content-type' : 'text/plain', + 'Authorization' : 'Basic ' + btoa(username + ":" + password) + }; + let lock = DAVLocks['default']; + if (lock) + headers['If'] = "<" + lock.URL + "> (<" + lock.token + ">)"; // Make a call to make sure it's working - this.GET("", self.cb); + this._makeRequest.async(this, self.cb, "GET", "", headers); let resp = yield; - if (resp.status < 200 || resp.status >= 300) { - self.done(false); - return; - } + this._log.debug("checkLogin got response status " + resp.status); + // XXX would be nice if 404 == invalid username, 401 == invalid password. + let retmsg = ""; + if (resp.status == 401) + retmsg = "invalid username or password"; + else if (resp.status < 200 || resp.status >= 300) + retmsg = "server error"; - self.done(true); + self.done(retmsg); }, // Locking diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 75538e1e3915..f2bb6b0175fa 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -196,10 +196,8 @@ WeaveSvc.prototype = { get userPath() { return ID.get('WeaveID').username; }, - get currentUser() { - if (this._loggedIn) - return this.username; - return null; + get isLoggedIn() { + return this._loggedIn; }, get enabled() { @@ -499,22 +497,36 @@ WeaveSvc.prototype = { // These are global (for all engines) - login: function WeaveSync_login(onComplete, password, passphrase, verifyonly) { - this._localLock(this._notify("login", this._login, - password, passphrase, verifyonly)).async(this, onComplete); + verifyLogin: function WeaveSync_verifyLogin(username, password) { + this._localLock(this._notify("verify-login", this._verifyLogin, username, password)).async(this, null); }, - _login: function WeaveSync__login(password, passphrase, verifyonly) { + + _verifyLogin: function WeaveSync__verifyLogin(username, password) { + let self = yield; + this._log.debug("Verifying login for user " + username); + + DAV.baseURL = Utils.prefs.getCharPref("serverURL"); + DAV.defaultPrefix = "user/" + username; + + DAV.checkLogin.async(DAV, self.cb, username, password); + let resultMsg = yield; + + // If we got an error message, throw it. [need to throw to cause the + // _notify() wrapper to generate an error notification for observers]. + if (resultMsg) { + this._log.debug("Login verification: " + resultMsg); + throw resultMsg; + } + + }, + + login: function WeaveSync_login(onComplete) { + this._localLock(this._notify("login", this._login)).async(this, onComplete); + }, + _login: function WeaveSync__login() { let self = yield; - // cache password & passphrase - // if null, we'll try to get them from the pw manager below - ID.get('WeaveID').setTempPassword(password); - ID.get('WeaveCryptoID').setTempPassword(passphrase); - - if(verifyonly) - this._log.debug("Verifying login"); - else - this._log.debug("Logging in"); + this._log.debug("Logging in user " + this.username); if (!this.username) throw "No username set, login failed"; @@ -524,29 +536,6 @@ WeaveSvc.prototype = { DAV.baseURL = Utils.prefs.getCharPref("serverURL"); DAV.defaultPrefix = "user/" + this.userPath; - DAV.checkLogin.async(DAV, self.cb, this.username, this.password); - let success = yield; - if (!success) { - try { - // FIXME: This code may not be needed any more, due to the way - // that the server is expected to create the user dir for us. - this._checkUserDir.async(this, self.cb); - yield; - } catch (e) { /* FIXME: tmp workaround for services.m.c */ } - DAV.checkLogin.async(DAV, self.cb, this.username, this.password); - let success = yield; - if (!success) - throw "Login failed"; - } - - // If being called from the Wizard to verify credentials, stop here. - if (verifyonly) { - this._log.debug("Login verified"); - self.done(true); - return; - } - // Otherwise, setup the user session. - this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); this._versionCheck.async(this, self.cb); From ddcd7fe65b68bec82d99338a2cde4571d041d91e Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 25 Jun 2008 04:43:07 -0700 Subject: [PATCH 0466/1860] Changed AsyncException so that it dynamically subclasses the exception it's wrapping, and adds an 'asyncStack' property to allow access to the asynchronous call stack. This, along with my previous few commits, makes the processing of stack traces in our code much more streamlined, and also allows our debugging output to be more informative, as stack information is now logged from the point at which an exception was thrown, rather than the point at which it was caught. Also renamed some things in async.js to be more descriptive and easier-to-understand, albeit a bit more verbose. --- services/sync/modules/async.js | 71 ++++++++++--------- services/sync/modules/engines.js | 2 +- services/sync/modules/service.js | 3 +- services/sync/modules/util.js | 5 ++ .../sync/tests/unit/test_async_exceptions.js | 15 +++- 5 files changed, 57 insertions(+), 39 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index a07e1bdb8e18..e204f6d7e5ba 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -75,25 +75,31 @@ let gCurrentId = 0; let gCurrentCbId = 0; let gOutstandingGenerators = new NamableTracker(); -function AsyncException(initFrame, message) { - this.message = message; - this._traceback = initFrame; +// The AsyncException class represents an exception that's been thrown +// by an asynchronous coroutine-based function. For the most part, it +// behaves just like the "real" exception it wraps, only it adds some +// embellishments that provide information about what other +// asynchronous coroutine-based functions our current one was called +// from. +function AsyncException(asyncStack, exceptionToWrap) { + this._exceptionToWrap = exceptionToWrap; + this._asyncStack = asyncStack; + + // Here we'll have our exception instance's prototype be dynamically + // generated; this ultimately allows us to dynamically "subclass" + // the exception we're wrapping at runtime. + this.__proto__ = { + get asyncStack() { + return this._asyncStack; + }, + + addAsyncFrame: function AsyncException_addAsyncFrame(frame) { + this._asyncStack += ((this._asyncStack? "\n" : "") + + formatAsyncFrame(frame)); + } + }; + this.__proto__.__proto__ = this._exceptionToWrap; } -AsyncException.prototype = { - get message() { return this._message; }, - set message(value) { this._message = value; }, - - get traceback() { return this._traceback; }, - set traceback(value) { this._traceback = value; }, - - addFrame: function AsyncException_addFrame(frame) { - this.traceback += (this.traceback? "\n" : "") + formatFrame(frame); - }, - - toString: function AsyncException_toString() { - return this.message; - } -}; function Generator(thisArg, method, onComplete, args) { this._outstandingCbs = 0; @@ -123,7 +129,7 @@ Generator.prototype = { let cbId = gCurrentCbId++; this._outstandingCbs++; this._log.trace(this.name + ": cb-" + cbId + " generated at:\n" + - formatFrame(caller)); + formatAsyncFrame(caller)); let self = this; let cb = function(data) { self._log.trace(self.name + ": cb-" + cbId + " called."); @@ -161,8 +167,9 @@ Generator.prototype = { this._onComplete = value; }, - get traceback() { - return "unknown (async) :: " + this.name + "\n" + traceAsyncFrame(this._initFrame); + get asyncStack() { + return ("unknown (async) :: " + this.name + "\n" + + traceAsyncFrame(this._initFrame)); }, _handleException: function AsyncGen__handleException(e) { @@ -177,17 +184,17 @@ Generator.prototype = { if (e instanceof AsyncException) { // FIXME: attempt to skip repeated frames, which can happen if the // child generator never yielded. Would break for valid repeats (recursion) - if (e.traceback.indexOf(formatFrame(this._initFrame)) == -1) - e.addFrame(this._initFrame); + if (e.asyncStack.indexOf(formatAsyncFrame(this._initFrame)) == -1) + e.addAsyncFrame(this._initFrame); } else { - e = new AsyncException(this.traceback, e); + e = new AsyncException(this.asyncStack, e); } this._exception = e; } else { this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.debug("Stack trace:\n" + (e.traceback? e.traceback : this.traceback)); + this._log.debug("Stack trace:\n" + Utils.stackTrace(e)); } // continue execution of caller. @@ -264,7 +271,7 @@ Generator.prototype = { if (!this._generator) { this._log.error("Async method '" + this.name + "' is missing a 'yield' call " + "(or called done() after being finalized)"); - this._log.trace("Initial stack trace:\n" + this.traceback); + this._log.trace("Initial async stack trace:\n" + this.asyncStack); } else { this._generator.close(); } @@ -282,15 +289,15 @@ Generator.prototype = { this._log.error("Exception caught from onComplete handler of " + this.name + " generator"); this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.trace("Current stack trace:\n" + traceAsyncFrame(Components.stack)); - this._log.trace("Initial stack trace:\n" + this.traceback); + this._log.trace("Current stack trace:\n" + Utils.stackTrace(e)); + this._log.trace("Initial async stack trace:\n" + this.asyncStack); } } gOutstandingGenerators.remove(this); } }; -function formatFrame(frame) { +function formatAsyncFrame(frame) { // FIXME: sort of hackish, might be confusing if there are multiple // extensions with similar filenames let tmp = frame.filename.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); @@ -309,7 +316,7 @@ function traceAsyncFrame(frame, str) { if (frame.caller) str = traceAsyncFrame(frame.caller, str); - str = formatFrame(frame) + (str? "\n" : "") + str; + str = formatAsyncFrame(frame) + (str? "\n" : "") + str; return str; } @@ -350,8 +357,4 @@ Async = { args.unshift(thisArg, this); Async.run.apply(Async, args); }, - - exceptionStr: function Async_exceptionStr(gen, ex) { - return "Exception caught in " + gen.name + ": " + Utils.exceptionStr(ex); - } }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index bb0b6464e0e8..77f18cef3331 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -241,7 +241,7 @@ Engine.prototype = { try { yield this._remote.openSession(self.cb); - } catch (e if e.message.status == 404) { + } catch (e if e.status == 404) { yield this._initialUpload.async(this, self.cb); return; } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 58985fb2eb50..d10955c182bc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -699,8 +699,7 @@ WeaveSvc.prototype = { engine._tracker.resetScore(); } catch(e) { this._log.error(Utils.exceptionStr(e)); - if (e.traceback) - this._log.trace(e.traceback); + this._log.error(Utils.stackTrace(e)); } }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 8a82c6aae9f4..7120d86e5939 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -234,6 +234,11 @@ let Utils = { // It's some other thrown object, e.g. a bare string. output += "No traceback available.\n"; + if (e.asyncStack) { + output += "This exception was raised by an asynchronous coroutine.\n"; + output += "Initial async stack trace:\n" + e.asyncStack; + } + return output; }, diff --git a/services/sync/tests/unit/test_async_exceptions.js b/services/sync/tests/unit/test_async_exceptions.js index 6eebf4ab3448..5cccc980504a 100644 --- a/services/sync/tests/unit/test_async_exceptions.js +++ b/services/sync/tests/unit/test_async_exceptions.js @@ -3,6 +3,17 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; +function _ensureExceptionIsValid(e) { + do_check_eq(e.message, "intentional failure"); + do_check_eq(typeof(e.asyncStack), "string"); + do_check_true(e.asyncStack.indexOf("thirdGen") > -1); + do_check_true(e.asyncStack.indexOf("secondGen") > -1); + do_check_true(e.asyncStack.indexOf("runTestGenerator") > -1); + do_check_true(e.stack.indexOf("thirdGen") > -1); + do_check_eq(e.stack.indexOf("secondGen"), -1); + do_check_eq(e.stack.indexOf("runTestGenerator"), -1); +} + function thirdGen() { let self = yield; @@ -19,7 +30,7 @@ function secondGen() { try { let result = yield; } catch (e) { - do_check_eq(e.message, "Error: intentional failure"); + ensureExceptionIsValid(e); throw e; } @@ -34,7 +45,7 @@ function runTestGenerator() { try { let result = yield; } catch (e) { - do_check_eq(e.message, "Error: intentional failure"); + ensureExceptionIsValid(e); wasCaught = true; } From 9f38d85edf3e269d82ada4d4a85183b139a7ebc4 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Wed, 25 Jun 2008 04:43:13 -0700 Subject: [PATCH 0467/1860] fix login calls, do login and sync on final screen, add try again link to server errors throughout --- services/sync/locales/en-US/wizard.dtd | 174 +++++++++--------- services/sync/locales/en-US/wizard.properties | 111 ++++++----- 2 files changed, 155 insertions(+), 130 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index db9dd5611f97..ad427062bf8f 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,87 +1,91 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index aaede913805d..9be39de1b737 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,52 +1,73 @@ -serverError.label = Server Error -serverTimeoutError.label = Server Error +serverError.label = Server Error +serverTimeoutError.label = Server Error + + +createUsername-progress.label = Checking username +createUsername-error.label = %S is already taken. Please choose another. +createUsername-success.label = %S is available. + +email-progress.label = Checking email +email-unavailable.label = %S is already taken. Please choose another. +email-invalid.label = Please re-enter a valid email address. +email-success.label = %S is available. + + +passwordsUnmatched.label = Passwords do not match +passphrasesUnmatched.label = Passphrases do not match. +samePasswordAndPassphrase.label = Your password and passphrase must be different + + + +missingCaptchaResponse.label = Please enter the words in the Captcha box. +incorrectCaptcha.label = Incorrect Captcha response. Try again. + + +verify-progress.label = Verifying username and password +verify-error.label = Invalid username and password +verify-success.label = Username and password verified + +create-progress.label = Creating your account +create-uid-inuse.label = Username in use +create-uid-missing.label = Username missing +create-uid-invalid.label = Username invalid +create-mail-invalid.label = Email invalid +create-mail-inuse.label = Emali in use +create-captcha-missing.label = Captcha response missing +create-password-missing.label = Password missing +create-password-incorrect.label = Password incorrect +create-success.label = Account for %S created. -createUsername-progress.label = -createUsername-error.label = %S is already taken. Please choose another. -createUsername-success.label = %S is available. +final-pref-value.label = %S +final-account-value.label = Username: %S +final-sync-value.label = [Explain that a sync will happen] +final-success.label = Weave was successfully installed. -email-progress.label = -email-unavailable.label = %S is already taken. Please choose another. -email-invalid.label = Please re-enter a valid email address. -email-success.label = %S is available. +default-name.label = %S's Firefox +default-name-nouser.label = Firefox +bookmarks.label = Bookmarks +history.label = Browsing History +cookies.label = Cookies +passwords.label = Saved Passwords +tabs.label = Tabs +formdata.label = Saved Form Data + +initialLogin-progress.label = Signing you in +initialLogin-error.label = Problem signing in. + +initialPrefs-progress.label = Setting your preferences -passwordsUnmatched.label = Passwords do not match -passphrasesUnmatched.label = Passphrases do not match. -samePasswordAndPassphrase.label = Your password and passphrase must be different - - - -missingCaptchaResponse.label = Please enter the words in the Captcha box. -incorrectCaptcha.label = Incorrect Captcha response. Try again. - - -verify-progress.label = Verifying username and password -verify-error.label = Invalid username and password -verify-success.label = Username and password verified - - - -initialLogin-progress.label = Signing you in... -initialLogin-done.label = Sign-in successful. -initialLogin-error.label = Problem signing in. - -initialPrefs-progress.label = Setting your preferences... -initialPrefs-done.label = Preferences set. - -initialReset-progress.label = Clearing your default bookmarks... -initialReset-done.label = Default bookmarks cleared. - -initialSync-progress.label = Synchronizing your data... -initialSync-done.label = Sync successful. +initialSync-progress.label = Synchronizing your data initialSync-error.label = Problem syncing data. - -tryAgain.text = Try again now. -clickHere.text = Click here - -data-verify.title = Data (Step 2 of 3) -data-create.title = Data (Step 4 of 5) -final-verify.title = Finish (Step 3 of 3) -final-create.title = Finish (Step 5 of 5) +installation-complete.label = Installation complete. + + +tryAgain.text = Try again now. +clickHere.text = Click here + +data-verify.title = Data (Step 2 of 3) +data-create.title = Data (Step 4 of 5) +final-verify.title = Finish (Step 3 of 3) +final-create.title = Finish (Step 5 of 5) From d41c1276f74d297f978ff01e3513e0797d986384 Mon Sep 17 00:00:00 2001 From: Date: Wed, 25 Jun 2008 11:58:04 -0700 Subject: [PATCH 0468/1860] The share/stop sharing menu item is now added only to normal bookmark folders: Not to the main bookmark menu or to magic folders like 'recently tagged' or whatever. --- services/sync/modules/engines/bookmarks.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c6a5177dc171..fd72a5346782 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -266,6 +266,9 @@ BookmarksSharingManager.prototype = { let self = yield; let mounts = this._engine._store.findIncomingShares(); + /* TODO ensure that old contents of incoming shares have been + * properly clobbered. + */ for (let i = 0; i < mounts.length; i++) { try { this._updateIncomingShare.async(this, self.cb, mounts[i]); From c3fc346e7758c75ec002f474e1c6f87d88d9e5f7 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 25 Jun 2008 13:51:32 -0700 Subject: [PATCH 0469/1860] the tabs engine needs a constant --- services/sync/modules/engines/tabs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 893ba5a75473..962ad50f05ff 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -10,6 +10,7 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/constants.js"); Function.prototype.async = Async.sugar; From 810880f2a3272acaae9cff5ba8b142cda9f54fee Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 25 Jun 2008 13:51:39 -0700 Subject: [PATCH 0470/1860] bug 436636: a system for showing various kinds of notifications in one consolidated location --- services/sync/locales/en-US/notification.dtd | 3 + services/sync/locales/en-US/sync.dtd | 5 - services/sync/locales/en-US/sync.properties | 22 ++- services/sync/modules/Observers.js | 100 +++++++++++++ services/sync/modules/notifications.js | 143 +++++++++++++++++++ services/sync/modules/service.js | 3 +- 6 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 services/sync/locales/en-US/notification.dtd create mode 100644 services/sync/modules/Observers.js create mode 100644 services/sync/modules/notifications.js diff --git a/services/sync/locales/en-US/notification.dtd b/services/sync/locales/en-US/notification.dtd new file mode 100644 index 000000000000..5bbdf7793ee2 --- /dev/null +++ b/services/sync/locales/en-US/notification.dtd @@ -0,0 +1,3 @@ + + + diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index e548bbd19a89..6cdd2cf7f872 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -6,10 +6,5 @@ - - - - - diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 90dab966f847..016a9f4ba811 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -1,8 +1,22 @@ # %S is the date and time at which the last sync successfully completed lastSync.label = Last Update: %S + +weaveButtonOffline.label = Sign In +weaveButtonOnline.label = Weave +shareBookmark.menuItem = Share This Folder... +unShareBookmark.menuItem = Stop Sharing This Folder + +status.offline = Sign in + +# The next two are not normally used, as we now display the username +# when the user is logged in. But if for some reason we can't get the username, +# we display these. status.idle = Idle status.active = Working... -status.offline = Offline -status.error = Error -shareBookmark.menuItem = Share This Folder... -unShareBookmark.menuItem = Stop Sharing This Folder \ No newline at end of file + +error.logout.title = Error While Signing Out +error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it (then why are we bugging you with this info?). +error.sync.title = Error While Syncing +error.sync.description = Weave encountered an error while syncing. You might want to try syncing manually to make sure you are up-to-date. +error.sync.tryAgainButton.label = Try Again +error.sync.tryAgainButton.accesskey = T diff --git a/services/sync/modules/Observers.js b/services/sync/modules/Observers.js new file mode 100644 index 000000000000..e98281158ad5 --- /dev/null +++ b/services/sync/modules/Observers.js @@ -0,0 +1,100 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Observers. + * + * The Initial Developer of the Original Code is Daniel Aquino. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Daniel Aquino + * Myk Melez + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let EXPORTED_SYMBOLS = ["Observers"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +let Observers = { + add: function(callback, topic) { + let observer = new Observer(callback); + if (!(topic in Observers._observers)) + Observers._observers[topic] = {}; + Observers._observers[topic][callback] = observer; + Observers._service.addObserver(observer, topic, true); + return observer; + }, + + remove: function(callback, topic) { + let observer = Observers._observers[topic][callback]; + Observers._service.removeObserver(observer, topic); + delete this._observers[topic][callback]; + }, + + notify: function(subject, topic, data) { + Observers._service.notifyObservers(new Subject(subject), topic, data); + }, + + _service: Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService), + + _observers: {} +}; + + +function Observer(callback) { + this._callback = callback; +} + +Observer.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + observe: function(subject, topic, data) { + // Pass the wrappedJSObject for subjects that have one. Otherwise pass + // the subject itself. This way we support both wrapped subjects created + // using this module and those that are real XPCOM components. + if (subject.wrappedJSObject) + this._callback(subject.wrappedJSObject, topic, data); + else + this._callback(subject, topic, data); + } +} + + +function Subject(object) { + this.wrappedJSObject = object; +} + +Subject.prototype = { + QueryInterface: XPCOMUtils.generateQI([]), + getHelperForLanguage: function() {}, + getInterfaces: function() {} +}; diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js new file mode 100644 index 000000000000..a3f59422887e --- /dev/null +++ b/services/sync/modules/notifications.js @@ -0,0 +1,143 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ["Notifications", "Notification", "NotificationButton", + "TabsNotification"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/Observers.js"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); + +let Notifications = { + // Match the referenced values in toolkit/content/widgets/notification.xml. + get PRIORITY_INFO() 1, // PRIORITY_INFO_LOW + get PRIORITY_WARNING() 4, // PRIORITY_WARNING_LOW + get PRIORITY_ERROR() 7, // PRIORITY_CRITICAL_LOW + + // FIXME: instead of making this public, dress the Notifications object + // to behave like an iterator (using generators?) and have callers access + // this array through the Notifications object. + notifications: [], + + _observers: [], + + // XXX Should we have a helper method for adding a simple notification? + // I.e. something like |function notify(title, description, priority)|. + + add: function Notifications_add(notification) { + this.notifications.push(notification); + Observers.notify(notification, "weave:notification:added", null); + }, + + remove: function Notifications_remove(notification) { + let index = this.notifications.indexOf(notification); + + if (index != -1) { + this.notifications.splice(index, 1); + Observers.notify(notification, "weave:notification:removed", null); + } + }, + + /** + * Replace an existing notification. + */ + replace: function Notifications_replace(oldNotification, newNotification) { + let index = this.notifications.indexOf(oldNotification); + + if (index != -1) + this.notifications.splice(index, 1, newNotification); + else { + this.notifications.push(newNotification); + // XXX Should we throw because we didn't find the existing notification? + // XXX Should we notify observers about weave:notification:added? + } + + // XXX Should we notify observers about weave:notification:replaced? + } + +}; + + +/** + * A basic notification. Subclass this to create more complex notifications. + */ +function Notification(title, description, iconURL, priority, buttons) { + this.title = title; + this.description = description; + + if (iconURL) + this.iconURL = iconURL; + + if (priority) + this.priority = priority; + + if (buttons) + this.buttons = buttons; +} + +// We set each prototype property individually instead of redefining +// the entire prototype to avoid blowing away existing properties +// of the prototype like the the "constructor" property, which we use +// to bind notification objects to their XBL representations. +Notification.prototype.priority = Notifications.PRIORITY_INFO; +Notification.prototype.iconURL = null; +Notification.prototype.buttons = []; + +/** + * A button to display in a notification. + */ +function NotificationButton(label, accessKey, callback) { + this.label = label; + this.accessKey = accessKey; + this.callback = callback; +} + +function TabsNotification() { + // Call the base class's constructor to initialize the new instance. + // XXX Can we simply pass null, null for the title, description? + Notification.call(this, "", "", null, Notifications.PRIORITY_INFO, null); +} + +// We set each prototype property individually instead of redefining +// the entire prototype to avoid blowing away existing properties +// of the prototype like the the "constructor" property, which we use +// to bind notification objects to their XBL representations. +TabsNotification.prototype.__proto__ = Notification.prototype; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ecd9fb80bd45..2968d4021ab8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -89,6 +89,7 @@ Cu.import("resource://weave/constants.js", Weave); Cu.import("resource://weave/util.js", Weave); Cu.import("resource://weave/async.js", Weave); Cu.import("resource://weave/crypto.js", Weave); +Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); Cu.import("resource://weave/dav.js", Weave); Cu.import("resource://weave/stores.js", Weave); @@ -260,7 +261,7 @@ WeaveSvc.prototype = { _onSchedule: function WeaveSync__onSchedule() { if (this.enabled) { this._log.info("Running scheduled sync"); - this._notify("sync", this._lock(this._syncAsNeeded)).async(this); + this._notify("syncAsNeeded", this._lock(this._syncAsNeeded)).async(this); } }, From ed8a7a5add6fbe3c0850dd279ee5fecdf3883273 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 25 Jun 2008 14:30:53 -0700 Subject: [PATCH 0471/1860] Added a number of "real" bookmark sync tests; see test_bookmark_syncing.js for information. --- services/sync/tests/unit/bookmark_setup.js | 138 +++++++++++++++++ .../sync/tests/unit/test_bookmark_syncing.js | 49 +++++- .../unit/test_bookmark_syncing.log.expected | 143 +++++++++++++++++- 3 files changed, 324 insertions(+), 6 deletions(-) create mode 100644 services/sync/tests/unit/bookmark_setup.js diff --git a/services/sync/tests/unit/bookmark_setup.js b/services/sync/tests/unit/bookmark_setup.js new file mode 100644 index 000000000000..f7eaf969a194 --- /dev/null +++ b/services/sync/tests/unit/bookmark_setup.js @@ -0,0 +1,138 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +// This file was originally taken from mozilla-central at: +// http://hg.mozilla.org/index.cgi/mozilla-central/file/f171c57e016e/browser/components/places/tests/unit/head_bookmarks.js +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Places. + * + * The Initial Developer of the Original Code is + * Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2005 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Brian Ryner + * Dietrich Ayala + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const NS_APP_USER_PROFILE_50_DIR = "ProfD"; + +function LOG(aMsg) { + aMsg = ("*** PLACES TESTS: " + aMsg); + Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService). + logStringMessage(aMsg); + print(aMsg); +} + +// If there's no location registered for the profile direcotry, register one now. +var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); +var profileDir = null; +try { + profileDir = dirSvc.get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile); +} catch (e) {} +if (!profileDir) { + // Register our own provider for the profile directory. + // It will simply return the current directory. + var provider = { + getFile: function(prop, persistent) { + persistent.value = true; + if (prop == NS_APP_USER_PROFILE_50_DIR) { + return dirSvc.get("CurProcD", Ci.nsIFile); + } + throw Cr.NS_ERROR_FAILURE; + }, + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIDirectoryServiceProvider) || + iid.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + dirSvc.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider); +} + +var XULAppInfo = { + vendor: "Mozilla", + name: "PlacesTest", + ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", + version: "1", + appBuildID: "2007010101", + platformVersion: "", + platformBuildID: "2007010101", + inSafeMode: false, + logConsoleErrors: true, + OS: "XPCShell", + XPCOMABI: "noarch-spidermonkey", + + QueryInterface: function QueryInterface(iid) { + if (iid.equals(Ci.nsIXULAppInfo) || + iid.equals(Ci.nsIXULRuntime) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +var XULAppInfoFactory = { + createInstance: function (outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return XULAppInfo.QueryInterface(iid); + } +}; + +var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"), + "XULAppInfo", "@mozilla.org/xre/app-info;1", + XULAppInfoFactory); + +var updateSvc = Cc["@mozilla.org/updates/update-service;1"]. + getService(Ci.nsISupports); + +var iosvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + +function uri(spec) { + return iosvc.newURI(spec, null, null); +} + +function cleanUp() { + try { + // Delete a previously created sqlite file + var file = dirSvc.get('ProfD', Ci.nsIFile); + file.append("places.sqlite"); + if (file.exists()) + file.remove(false); + + // Delete exported bookmarks html file + file = dirSvc.get('ProfD', Ci.nsIFile); + file.append("bookmarks.exported.html"); + if (file.exists()) + file.remove(false); + } catch(ex) { dump("Exception: " + ex); } +} +cleanUp(); diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/test_bookmark_syncing.js index 4a3319ff2563..1eb6b9d0cc52 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.js +++ b/services/sync/tests/unit/test_bookmark_syncing.js @@ -1,18 +1,65 @@ Cu.import("resource://weave/engines/bookmarks.js"); +load("bookmark_setup.js"); + // ---------------------------------------- // Test Logic // ---------------------------------------- +function FakeMicrosummaryService() { + return {hasMicrosummary: function() { return false; }}; +} + function run_test() { var syncTesting = new SyncTestingInfrastructure(); function freshEngineSync(cb) { let engine = new BookmarksEngine(); + engine._store.__ms = new FakeMicrosummaryService(); engine.sync(cb); }; - syncTesting.runAsyncFunc("initial sync", freshEngineSync); + function resetProfile() { + // Simulate going to another computer by removing stuff from our + // objects. + syncTesting.fakeFilesystem.fakeContents = {}; + bms.removeItem(boogleBm); + bms.removeItem(yoogleBm); + } + + let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + + cleanUp(); + + let boogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, + uri("http://www.boogle.com"), + -1, + "Boogle"); + bms.setItemGUID(boogleBm, "boogle-bookmark-guid"); + + syncTesting.runAsyncFunc("initial sync w/ one bookmark", freshEngineSync); syncTesting.runAsyncFunc("trivial re-sync", freshEngineSync); + + let yoogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, + uri("http://www.yoogle.com"), + -1, + "Yoogle"); + bms.setItemGUID(yoogleBm, "yoogle-bookmark-guid"); + + syncTesting.runAsyncFunc("add bookmark and re-sync", freshEngineSync); + + bms.moveItem(yoogleBm, + bms.bookmarksMenuFolder, + 0); + + syncTesting.runAsyncFunc("swap bookmark order and re-sync", + freshEngineSync); + + resetProfile(); + + syncTesting.runAsyncFunc("re-sync on second computer", freshEngineSync); + + cleanUp(); } diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/test_bookmark_syncing.log.expected index 775c0d80dfa9..841544640336 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.log.expected +++ b/services/sync/tests/unit/test_bookmark_syncing.log.expected @@ -1,6 +1,6 @@ *** test pending Testing INFO ----------------------------------------- -Testing INFO Step 'initial sync' starting. +Testing INFO Step 'initial sync w/ one bookmark' starting. Testing INFO ----------------------------------------- Service.BmkEngine INFO Beginning sync Testing INFO HTTP MKCOL on user-data/bookmarks/deltas @@ -13,16 +13,16 @@ Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/bookmarks/snapshot.json with data: {"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}} +Testing INFO HTTP PUT to user-data/bookmarks/snapshot.json with data: {"boogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":3} +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":4} Service.Resource DEBUG PUT request successful Service.RemoteStore INFO Full upload to server successful Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":0,"GUID":"fake-guid-0","snapshot":{"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} -Testing INFO Step 'initial sync' succeeded. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":0,"GUID":"fake-guid-0","snapshot":{"boogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} +Testing INFO Step 'initial sync w/ one bookmark' succeeded. Testing INFO ----------------------------------------- Testing INFO Step 'trivial re-sync' starting. Testing INFO ----------------------------------------- @@ -42,6 +42,139 @@ Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap vers Service.RemoteStore TRACE Local snapshot version == server maxVersion Service.BmkEngine INFO Sync complete: no changes needed on client or server Testing INFO Step 'trivial re-sync' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 'add bookmark and re-sync' starting. +Testing INFO ----------------------------------------- +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.BmkEngine INFO Local snapshot version: 0 +Service.BmkEngine INFO Server maxVersion: 0 +Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) +Service.RemoteStore TRACE Local snapshot version == server maxVersion +Service.BmkEngine INFO Reconciling client/server updates +Service.BMSync DEBUG Reconciling 1 against 0 commands +Service.BmkEngine INFO Changes for client: 0 +Service.BmkEngine INFO Predicted changes for server: 1 +Service.BmkEngine INFO Client conflicts: 0 +Service.BmkEngine INFO Server conflicts: 0 +Service.BmkEngine INFO Actual changes for server: 1 +Service.BmkEngine DEBUG Actual changes for server: [{"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}}] +Service.BmkEngine INFO Uploading changes to server +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data +Testing INFO HTTP PUT to user-data/bookmarks/deltas/1 with data: [{"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}}] +Service.ResourceSet DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":5} +Service.Resource DEBUG PUT request successful +Service.BmkEngine INFO Successfully updated deltas and status on server +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":1,"GUID":"fake-guid-0","snapshot":{"boogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"yoogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} +Service.BmkEngine INFO Sync complete +Testing INFO Step 'add bookmark and re-sync' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 'swap bookmark order and re-sync' starting. +Testing INFO ----------------------------------------- +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.BmkEngine INFO Local snapshot version: 1 +Service.BmkEngine INFO Server maxVersion: 1 +Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) +Service.RemoteStore TRACE Local snapshot version == server maxVersion +Service.BmkEngine INFO Reconciling client/server updates +Service.BMSync DEBUG Reconciling 2 against 0 commands +Service.BmkEngine INFO Changes for client: 0 +Service.BmkEngine INFO Predicted changes for server: 2 +Service.BmkEngine INFO Client conflicts: 0 +Service.BmkEngine INFO Server conflicts: 0 +Service.BmkEngine INFO Actual changes for server: 2 +Service.BmkEngine DEBUG Actual changes for server: [{"action":"edit","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":1,"type":"bookmark"}},{"action":"edit","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":0,"type":"bookmark"}}] +Service.BmkEngine INFO Uploading changes to server +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data +Testing INFO HTTP PUT to user-data/bookmarks/deltas/2 with data: [{"action":"edit","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":1,"type":"bookmark"}},{"action":"edit","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":0,"type":"bookmark"}}] +Service.ResourceSet DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":5} +Service.Resource DEBUG PUT request successful +Service.BmkEngine INFO Successfully updated deltas and status on server +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} +Service.BmkEngine INFO Sync complete +Testing INFO Step 'swap bookmark order and re-sync' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 're-sync on second computer' starting. +Testing INFO ----------------------------------------- +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.BmkEngine DEBUG Remote/local sync GUIDs do not match. Forcing initial sync. +Service.BmkEngine INFO Local snapshot version: -1 +Service.BmkEngine INFO Server maxVersion: 2 +Service.RemoteStore TRACE Getting latest from snap --> scratch +Service.RemoteStore INFO Downloading all server data from scratch +Testing INFO HTTP GET from user-data/bookmarks/snapshot.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Testing INFO HTTP GET from user-data/bookmarks/deltas/1, returning status 200 +Service.ResourceSet DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Service.SnapStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} +Testing INFO HTTP GET from user-data/bookmarks/deltas/2, returning status 200 +Service.ResourceSet DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Service.SnapStore TRACE Processing command: {"action":"edit","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":1,"type":"bookmark"}} +Service.SnapStore TRACE Processing command: {"action":"edit","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":0,"type":"bookmark"}} +Service.BmkEngine INFO Reconciling client/server updates +Service.BMSync DEBUG Reconciling 3 against 5 commands +Service.BmkEngine INFO Changes for client: 2 +Service.BmkEngine INFO Predicted changes for server: 0 +Service.BmkEngine INFO Client conflicts: 0 +Service.BmkEngine INFO Server conflicts: 0 +Service.BmkEngine INFO Applying changes locally +Service.SnapStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} +Service.SnapStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} +Service.BStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} +Service.BStore DEBUG -> creating bookmark "Boogle" +Service.BStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} +Service.BStore DEBUG -> creating bookmark "Yoogle" +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}}} +Service.BmkEngine INFO Actual changes for server: 0 +Service.BmkEngine DEBUG Actual changes for server: [] +Service.BmkEngine INFO Sync complete +Testing INFO Step 're-sync on second computer' succeeded. *** test finished *** exiting *** PASS *** From 21fb8c2cded2d0cd5d8a2cfb05debd69aa08e5a4 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Wed, 25 Jun 2008 15:44:35 -0700 Subject: [PATCH 0472/1860] fixed interaction details, all login/verify cases work correctly, final screen creates account and syncs correctly --- services/sync/locales/en-US/wizard.dtd | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index ad427062bf8f..cf5861b6e269 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -11,7 +11,7 @@ - + @@ -53,8 +53,6 @@ - - From b10d11835eaaaf1da83a0d96328f97dabcad9fe1 Mon Sep 17 00:00:00 2001 From: Date: Wed, 25 Jun 2008 15:54:33 -0700 Subject: [PATCH 0473/1860] Set up bookmarkEngine._incomingShareOffer to use Myk's new Notification stuff to offer a notification to the user, asking them to accept or reject the incoming share. --- services/sync/modules/engines/bookmarks.js | 59 +++++++++++++--------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index fd72a5346782..7f21943fa9e4 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -134,7 +134,8 @@ BookmarksSharingManager.prototype = { if ( commandWord == "share" ) { bmkSharing._incomingShareOffer(from, serverPath, folderName); } else if ( commandWord == "stop" ) { - bmkSharing._incomingShareWithdrawn(from, serverPath, folderName); + bmkSharing._log.info("User " + user + " withdrew " + folderName); + bmkSharing._stopIncomingShare(user, serverPath, folderName); } } }; @@ -151,33 +152,45 @@ BookmarksSharingManager.prototype = { }, _incomingShareOffer: function BmkSharing__incomingShareOffer(user, - serverPath, - folderName) { + serverPath, + folderName) { /* Called when we receive an offer from another user to share a - folder. - - TODO what should happen is that we add a notification to the queue - telling that the incoming share has been offered; when the offer - is accepted we will call createIncomingShare and then - updateIncomingShare. - - But since we don't have notification in place yet, I'm going to skip - right ahead to creating the incoming share. + folder. Set up a notification telling our user about the share offer + and allowing them to accept or reject it. */ this._log.info("User " + user + " offered to share folder " + folderName); - this._createIncomingShare(user, serverPath, folderName); + + let bmkSharing = this; + let acceptButton = new Weave.NotificationButton( + "Accept Share", + "a", + function() { + // This is what happens when they click the Accept button: + bmkSharing._log.info("Accepted bookmark share from " + user); + bmkSharing._createIncomingShare(user, serverPath, folderName); + bmkSharing._updateAllIncomingShares(); + return false; + } + ); + let rejectButton = new Weave.NotificationButton( + "No Thanks", + "n", + function() {return false;} + ); + + let title = "Bookmark Share Offer From " + user; + let description ="Weave user " + user + + " is offering to share a bookmark folder called " + folderName + + " with you. Do you want to accept it?"; + let notification = Weave.Notification(title, + description, + null, + Weave.Notifications.PRIORITY_INFO, + [acceptButton, rejectButton] + ); + Weave.Notifications.add(notification); }, - _incomingShareWithdrawn: function BmkSharing__incomingShareStop(user, - serverPath, - folderName) { - /* Called when we receive a message telling us that a user who has - already shared a directory with us has chosen to stop sharing - the directory. - */ - this._log.info("User " + user + " stopped sharing folder " + folderName); - this._stopIncomingShare(user, serverPath, folderName); - }, _share: function BmkSharing__share( selectedFolder, username ) { // Return true if success, false if failure. let ret = false; From ac27fb0d44b45841633bac819c70dd3fbfbc1ae7 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 25 Jun 2008 16:34:28 -0700 Subject: [PATCH 0474/1860] Change default server URL to services.mozilla.com --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index f5f888dfada3..1b192679f0ca 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,4 @@ -pref("extensions.weave.serverURL", "https://sm-labs01.mozilla.org:81/"); +pref("extensions.weave.serverURL", "https://services.mozilla.com/0.2/"); pref("extensions.weave.username", "nobody"); pref("extensions.weave.encryption", "aes-256-cbc"); From 69f58ebd2e95b6a4e79bcbcdbb9541cfeecb3838 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Wed, 25 Jun 2008 17:05:20 -0700 Subject: [PATCH 0475/1860] Bookmark sync test now attempts to add a bookmark on the second computer, then re-sync on the first computer and ensure that the bookmark is copied over. --- .../sync/tests/unit/test_bookmark_syncing.js | 54 ++++++++++- .../unit/test_bookmark_syncing.log.expected | 94 +++++++++++++++++++ 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/test_bookmark_syncing.js index 1eb6b9d0cc52..b68ce8fb90ff 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.js +++ b/services/sync/tests/unit/test_bookmark_syncing.js @@ -1,4 +1,8 @@ Cu.import("resource://weave/engines/bookmarks.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; load("bookmark_setup.js"); @@ -20,11 +24,39 @@ function run_test() { }; function resetProfile() { - // Simulate going to another computer by removing stuff from our - // objects. syncTesting.fakeFilesystem.fakeContents = {}; - bms.removeItem(boogleBm); - bms.removeItem(yoogleBm); + let engine = new BookmarksEngine(); + engine._store.wipe(); + } + + function saveClientState() { + return Utils.deepCopy(syncTesting.fakeFilesystem.fakeContents); + } + + function restoreClientState(state, label) { + function _restoreState() { + let self = yield; + + syncTesting.fakeFilesystem.fakeContents = Utils.deepCopy(state); + let engine = new BookmarksEngine(); + engine._store.wipe(); + let originalSnapshot = Utils.deepCopy(engine._store.wrap()); + engine._snapshot.load(); + let snapshot = engine._snapshot.data; + + engine._core.detectUpdates(self.cb, originalSnapshot, snapshot); + let commands = yield; + + engine._store.applyCommands.async(engine._store, self.cb, commands); + yield; + } + + function restoreState(cb) { + _restoreState.async(this, cb); + } + + syncTesting.runAsyncFunc("restore client state of " + label, + restoreState); } let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. @@ -57,9 +89,23 @@ function run_test() { syncTesting.runAsyncFunc("swap bookmark order and re-sync", freshEngineSync); + var firstComputerState = saveClientState(); + resetProfile(); syncTesting.runAsyncFunc("re-sync on second computer", freshEngineSync); + let zoogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, + uri("http://www.zoogle.com"), + -1, + "Zoogle"); + bms.setItemGUID(zoogleBm, "zoogle-bookmark-guid"); + + syncTesting.runAsyncFunc("add bookmark on second computer and resync", + freshEngineSync); + + restoreClientState(firstComputerState, "first computer"); + syncTesting.runAsyncFunc("re-sync on first computer", freshEngineSync); + cleanUp(); } diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/test_bookmark_syncing.log.expected index 841544640336..580da38af1f3 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.log.expected +++ b/services/sync/tests/unit/test_bookmark_syncing.log.expected @@ -175,6 +175,100 @@ Service.BmkEngine INFO Actual changes for server: 0 Service.BmkEngine DEBUG Actual changes for server: [] Service.BmkEngine INFO Sync complete Testing INFO Step 're-sync on second computer' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 'add bookmark on second computer and resync' starting. +Testing INFO ----------------------------------------- +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.BmkEngine INFO Local snapshot version: 2 +Service.BmkEngine INFO Server maxVersion: 2 +Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) +Service.RemoteStore TRACE Local snapshot version == server maxVersion +Service.BmkEngine INFO Reconciling client/server updates +Service.BMSync DEBUG Reconciling 1 against 0 commands +Service.BmkEngine INFO Changes for client: 0 +Service.BmkEngine INFO Predicted changes for server: 1 +Service.BmkEngine INFO Client conflicts: 0 +Service.BmkEngine INFO Server conflicts: 0 +Service.BmkEngine INFO Actual changes for server: 1 +Service.BmkEngine DEBUG Actual changes for server: [{"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}}] +Service.BmkEngine INFO Uploading changes to server +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data +Testing INFO HTTP PUT to user-data/bookmarks/deltas/3 with data: [{"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}}] +Service.ResourceSet DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":3,"snapEncryption":"none","deltasEncryption":"none","itemCount":6} +Service.Resource DEBUG PUT request successful +Service.BmkEngine INFO Successfully updated deltas and status on server +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":3,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} +Service.BmkEngine INFO Sync complete +Testing INFO Step 'add bookmark on second computer and resync' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 'restore client state of first computer' starting. +Testing INFO ----------------------------------------- +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.BStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} +Service.BStore DEBUG -> creating bookmark "Yoogle" +Service.BStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} +Service.BStore DEBUG -> creating bookmark "Boogle" +Testing INFO Step 'restore client state of first computer' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 're-sync on first computer' starting. +Testing INFO ----------------------------------------- +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.BmkEngine INFO Local snapshot version: 2 +Service.BmkEngine INFO Server maxVersion: 3 +Service.RemoteStore DEBUG Using last sync snapshot as starting point for server snapshot +Service.RemoteStore INFO Downloading server deltas +Testing INFO HTTP GET from user-data/bookmarks/deltas/3, returning status 200 +Service.ResourceSet DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Service.SnapStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}} +Service.BmkEngine INFO Reconciling client/server updates +Service.BMSync DEBUG Reconciling 0 against 1 commands +Service.BmkEngine INFO Changes for client: 1 +Service.BmkEngine INFO Predicted changes for server: 0 +Service.BmkEngine INFO Client conflicts: 0 +Service.BmkEngine INFO Server conflicts: 0 +Service.BmkEngine INFO Applying changes locally +Service.SnapStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}} +Service.BStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}} +Service.BStore DEBUG -> creating bookmark "Zoogle" +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":3,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}}} +Service.BmkEngine INFO Actual changes for server: 0 +Service.BmkEngine DEBUG Actual changes for server: [] +Service.BmkEngine INFO Sync complete +Testing INFO Step 're-sync on first computer' succeeded. *** test finished *** exiting *** PASS *** From bffb925a43efe666180268c0acc6966d0ec35208 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 25 Jun 2008 17:09:18 -0700 Subject: [PATCH 0476/1860] Handle removeCommand in FormEngine correctly. (bug 441874, r=thunder) --- services/sync/modules/engines/forms.js | 96 ++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 24411de1ba8c..a0f29c0f0611 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -11,6 +11,58 @@ Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +/* + * Generate GUID from a name,value pair. + * If the concatenated length is less than 40, we just Base64 the JSON. + * Otherwise, we Base64 the JSON of the name and SHA1 of the value. + * The first character of the key determines which method we used: + * '0' for full Base64, '1' for SHA-1'ed val. + */ +function _generateFormGUID(nam, val) { + var key; + var con = nam + val; + + var jso = Cc["@mozilla.org/dom/json;1"]. + createInstance(Ci.nsIJSON); + + if (con.length <= 40) { + key = '0' + btoa(jso.encode([nam, val])); + } else { + val = Utils.sha1(val); + key = '1' + btoa(jso.encode([nam, val])); + } + + return key; +} + +/* + * Unwrap a name,value pair from a GUID. + * Return an array [sha1ed, name, value] + * sha1ed is a boolean determining if the value is SHA-1'ed or not. + */ +function _unwrapFormGUID(guid) { + var jso = Cc["@mozilla.org/dom/json;1"]. + createInstance(Ci.nsIJSON); + + var ret; + var dec = atob(guid.slice(1)); + var obj = jso.decode(dec); + + switch (guid[0]) { + case '0': + ret = [false, obj[0], obj[1]]; + break; + case '1': + ret = [true, obj[0], obj[1]]; + break; + default: + this._log.warn("Unexpected GUID header: " + guid[0] + ", aborting!"); + return false; + } + + return ret; +} + function FormEngine(pbeId) { this._init(pbeId); } @@ -71,7 +123,7 @@ FormSyncCore.prototype = { while (stmnt.executeStep()) { var nam = stmnt.getUTF8String(1); var val = stmnt.getUTF8String(2); - var key = Utils.sha1(nam + val); + var key = _generateFormGUID(nam, val); if (key == GUID) found = true; @@ -115,18 +167,50 @@ FormStore.prototype = { return this.__formHistory; }, + _getValueFromSHA1: function FormStore__getValueFromSHA1(name, sha) { + var query = "SELECT value FROM moz_formhistory WHERE fieldname = '" + name + "'"; + var stmnt = this._formDB.createStatement(query); + var found = false; + + while (stmnt.executeStep()) { + var val = stmnt.getUTF8String(0); + if (Utils.sha1(val) == sha) { + found = val; + break; + } + } + return found; + }, + _createCommand: function FormStore__createCommand(command) { - this._log.info("FormStore got createCommand: " + command ); + this._log.info("FormStore got createCommand: " + command); this._formHistory.addEntry(command.data.name, command.data.value); }, _removeCommand: function FormStore__removeCommand(command) { - this._log.info("FormStore got removeCommand: " + command ); - this._formHistory.removeEntry(command.data.name, command.data.value); + this._log.info("FormStore got removeCommand: " + command); + + var data = _unwrapFormGUID(command.GUID); + if (!data) { + this._log.warn("Invalid GUID found, ignoring remove request."); + return; + } + + var nam = data[1]; + var val = data[2]; + if (data[0]) { + val = _getValueFromSHA1(nam, val); + } + + if (val) { + this._formHistory.removeEntry(nam, val); + } else { + this._log.warn("Form value not found from GUID, ignoring remove request."); + } }, _editCommand: function FormStore__editCommand(command) { - this._log.info("FormStore got editCommand: " + command ); + this._log.info("FormStore got editCommand: " + command); this._log.warn("Form syncs are expected to only be create/remove!"); }, @@ -137,7 +221,7 @@ FormStore.prototype = { while (stmnt.executeStep()) { var nam = stmnt.getUTF8String(1); var val = stmnt.getUTF8String(2); - var key = Utils.sha1(nam + val); + var key = _generateFormGUID(nam, val); items[key] = { name: nam, value: val }; } From 32bc0d39ef0be68de6bdbbd581dd4a4e7e00a053 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 25 Jun 2008 17:10:24 -0700 Subject: [PATCH 0477/1860] Remove runCmd and other openssl related stuff we no longer need. (bug 441898, r=thunder) --- services/sync/modules/util.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 7120d86e5939..eca9476dc1b9 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -307,31 +307,6 @@ let Utils = { Ci.nsIDOMXPathResult.ANY_TYPE, null); }, - runCmd: function Weave_runCmd() { - var binary; - var args = []; - - for (let i = 0; i < arguments.length; ++i) { - args.push(arguments[i]); - } - - if (args[0] instanceof Ci.nsIFile) { - binary = args.shift(); - } else { - binary = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - binary.initWithPath(args.shift()); - } - - var p = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); - p.init(binary); - - let log = Log4Moz.Service.getLogger("Service.Util"); - log.debug("Running command: " + binary.path + " " + args.join(" ")); - - p.run(true, args, args.length); - return p.exitValue; - }, - getTmp: function Weave_getTmp(name) { let ds = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); From 3b4c505dcf7aebb48d4e2b52d98c2dcde2055cba Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Wed, 25 Jun 2008 18:32:59 -0700 Subject: [PATCH 0478/1860] bustage fix: call this.foo(), not foo(). --- services/sync/modules/engines/forms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index a0f29c0f0611..0a3e532dd33a 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -199,7 +199,7 @@ FormStore.prototype = { var nam = data[1]; var val = data[2]; if (data[0]) { - val = _getValueFromSHA1(nam, val); + val = this._getValueFromSHA1(nam, val); } if (val) { From 901240be43671c57eed6f39be71515869cf0d3e2 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 11:07:13 -0700 Subject: [PATCH 0479/1860] Refactored client state-changing functions out of test_bookmark_syncing.js and into head.js, as part of the SyncTestingInfrastructure class, so that other test suites can use them. --- services/sync/tests/unit/head_first.js | 50 ++++++++++++++++- .../sync/tests/unit/test_bookmark_syncing.js | 56 ++++++------------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 1ab0c3db6743..41e98e5e4513 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -274,7 +274,7 @@ function FakeGUIDService() { }; } -function SyncTestingInfrastructure() { +function SyncTestingInfrastructure(engineCtor) { let __fakePasswords = { 'Mozilla Services Password': {foo: "bar"}, 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} @@ -289,6 +289,7 @@ function SyncTestingInfrastructure() { }; Cu.import("resource://weave/identity.js"); + Cu.import("resource://weave/util.js"); ID.set('WeaveID', new Identity('Mozilla Services Encryption Passphrase', 'foo')); @@ -301,6 +302,45 @@ function SyncTestingInfrastructure() { this.fakeFilesystem = new FakeFilesystemService({}); this.fakeGUIDService = new FakeGUIDService(); + this._logger = getTestLogger(); + this._Engine = engineCtor; + this._clientStates = []; + + this.saveClientState = function pushClientState(label) { + let state = Utils.deepCopy(this.fakeFilesystem.fakeContents); + this._clientStates[label] = state; + }; + + this.restoreClientState = function restoreClientState(label) { + let state = this._clientStates[label]; + + function _restoreState() { + let self = yield; + + this.fakeFilesystem.fakeContents = Utils.deepCopy(state); + let engine = new this._Engine(); + engine._store.wipe(); + let originalSnapshot = Utils.deepCopy(engine._store.wrap()); + engine._snapshot.load(); + let snapshot = engine._snapshot.data; + + engine._core.detectUpdates(self.cb, originalSnapshot, snapshot); + let commands = yield; + + engine._store.applyCommands.async(engine._store, self.cb, commands); + yield; + } + + let self = this; + + function restoreState(cb) { + _restoreState.async(self, cb); + } + + this.runAsyncFunc("restore client state of " + label, + restoreState); + }; + this.__makeCallback = function __makeCallback() { this.__callbackCalled = false; let self = this; @@ -310,7 +350,7 @@ function SyncTestingInfrastructure() { }; this.runAsyncFunc = function runAsyncFunc(name, func) { - let logger = getTestLogger(); + let logger = this._logger; logger.info("-----------------------------------------"); logger.info("Step '" + name + "' starting."); @@ -324,4 +364,10 @@ function SyncTestingInfrastructure() { do_check_eq(Async.outstandingGenerators.length, 0); logger.info("Step '" + name + "' succeeded."); }; + + this.resetClientState = function resetClientState() { + this.fakeFilesystem.fakeContents = {}; + let engine = new this._Engine(); + engine._store.wipe(); + }; } diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/test_bookmark_syncing.js index b68ce8fb90ff..cb06a1d73ca3 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.js +++ b/services/sync/tests/unit/test_bookmark_syncing.js @@ -15,7 +15,11 @@ function FakeMicrosummaryService() { } function run_test() { - var syncTesting = new SyncTestingInfrastructure(); + // ----- + // Setup + // ----- + + var syncTesting = new SyncTestingInfrastructure(BookmarksEngine); function freshEngineSync(cb) { let engine = new BookmarksEngine(); @@ -23,47 +27,15 @@ function run_test() { engine.sync(cb); }; - function resetProfile() { - syncTesting.fakeFilesystem.fakeContents = {}; - let engine = new BookmarksEngine(); - engine._store.wipe(); - } - - function saveClientState() { - return Utils.deepCopy(syncTesting.fakeFilesystem.fakeContents); - } - - function restoreClientState(state, label) { - function _restoreState() { - let self = yield; - - syncTesting.fakeFilesystem.fakeContents = Utils.deepCopy(state); - let engine = new BookmarksEngine(); - engine._store.wipe(); - let originalSnapshot = Utils.deepCopy(engine._store.wrap()); - engine._snapshot.load(); - let snapshot = engine._snapshot.data; - - engine._core.detectUpdates(self.cb, originalSnapshot, snapshot); - let commands = yield; - - engine._store.applyCommands.async(engine._store, self.cb, commands); - yield; - } - - function restoreState(cb) { - _restoreState.async(this, cb); - } - - syncTesting.runAsyncFunc("restore client state of " + label, - restoreState); - } - let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); cleanUp(); + // ----------- + // Test Proper + // ----------- + let boogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, uri("http://www.boogle.com"), -1, @@ -89,9 +61,9 @@ function run_test() { syncTesting.runAsyncFunc("swap bookmark order and re-sync", freshEngineSync); - var firstComputerState = saveClientState(); + syncTesting.saveClientState("first computer"); - resetProfile(); + syncTesting.resetClientState(); syncTesting.runAsyncFunc("re-sync on second computer", freshEngineSync); @@ -104,8 +76,12 @@ function run_test() { syncTesting.runAsyncFunc("add bookmark on second computer and resync", freshEngineSync); - restoreClientState(firstComputerState, "first computer"); + syncTesting.restoreClientState("first computer"); syncTesting.runAsyncFunc("re-sync on first computer", freshEngineSync); + // -------- + // Teardown + // -------- + cleanUp(); } From 723b36500fdbd4d8e7ab21c213e527c0192506e5 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 26 Jun 2008 11:15:02 -0700 Subject: [PATCH 0480/1860] Prevent multiple lock requests from being executed (bug 441922, r=thunder) --- services/sync/modules/async.js | 2 ++ services/sync/modules/dav.js | 13 +++++++++---- services/sync/modules/util.js | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index fc57238dcf88..dcfbc5e3ac94 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -192,6 +192,8 @@ Generator.prototype = { this._exception = e; + } else if (e.message && e.message == 'Cannot acquire lock (internal lock)') { + this._log.warn("Exception: " + Utils.exceptionStr(e)); } else { this._log.error("Exception: " + Utils.exceptionStr(e)); this._log.debug("Stack trace:\n" + Utils.stackTrace(e)); diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index ee06cac87e2e..6bd01cfbc53f 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -252,9 +252,6 @@ DAVCollection.prototype = { }, LOCK: function DC_LOCK(path, data, onComplete) { - if (!this._lockAllowed) - throw "Cannot acquire lock (internal lock)"; - let headers = {'Content-type': 'text/xml; charset="utf-8"', 'Depth': 'infinity', 'Timeout': 'Second-600'}; @@ -364,6 +361,9 @@ DAVCollection.prototype = { let self = yield; this._log.trace("Acquiring lock"); + if (!this._lockAllowed) + throw {message: "Cannot acquire lock (internal lock)"}; + this._lockAllowed = false; if (DAVLocks['default']) { this._log.debug("Lock called, but we already hold a token"); @@ -379,8 +379,10 @@ DAVCollection.prototype = { "", self.cb); let resp = yield; - if (resp.status < 200 || resp.status >= 300) + if (resp.status < 200 || resp.status >= 300) { + this._lockAllowed = true; return; + } let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); let token = tokens.iterateNext(); @@ -393,11 +395,14 @@ DAVCollection.prototype = { if (!DAVLocks['default']) { this._log.warn("Could not acquire lock"); + this._lockAllowed = true; self.done(); return; } this._log.trace("Lock acquired"); + this._lockAllowed = true; + self.done(DAVLocks['default']); }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index eca9476dc1b9..52079f0f20da 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -203,7 +203,7 @@ let Utils = { }, exceptionStr: function Weave_exceptionStr(e) { - let message = e.message? e.message : e; + let message = e.message ? e.message : e; let location = ""; if (e.location) From 6f86ef3de17b0b04c5386586d440ad655b38fcf1 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 11:38:40 -0700 Subject: [PATCH 0481/1860] Revalidated log for password sync test, which changed due to Thunder's changes that put deltas for individual versions into their own files on WebDAV. --- .../unit/test_password_syncing.log.expected | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/services/sync/tests/unit/test_password_syncing.log.expected b/services/sync/tests/unit/test_password_syncing.log.expected index 050ab72e4f28..672963945b1f 100644 --- a/services/sync/tests/unit/test_password_syncing.log.expected +++ b/services/sync/tests/unit/test_password_syncing.log.expected @@ -3,7 +3,7 @@ Testing INFO ----------------------------------------- Testing INFO Step 'initial sync' starting. Testing INFO ----------------------------------------- Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/ +Testing INFO HTTP MKCOL on user-data/passwords/deltas Service.RemoteStore DEBUG Downloading status file Testing INFO HTTP GET from user-data/passwords/status.json, returning status 404 Service.PasswordEngine INFO Initial upload to server @@ -16,11 +16,6 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/passwords/snapshot.json with data: {"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [] -Service.Resource DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} Service.Resource DEBUG PUT request successful Service.RemoteStore INFO Full upload to server successful @@ -35,7 +30,7 @@ Testing INFO Opening 'weave/snapshots/passwords.json' for reading. Testing INFO Reading from stream. Service.SnapStore INFO Read saved snapshot from disk Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/ +Testing INFO HTTP MKCOL on user-data/passwords/deltas Service.RemoteStore DEBUG Downloading status file Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 Service.Resource DEBUG GET request successful @@ -54,7 +49,7 @@ Testing INFO Opening 'weave/snapshots/passwords.json' for reading. Testing INFO Reading from stream. Service.SnapStore INFO Read saved snapshot from disk Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/ +Testing INFO HTTP MKCOL on user-data/passwords/deltas Service.RemoteStore DEBUG Downloading status file Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 Service.Resource DEBUG GET request successful @@ -73,16 +68,11 @@ Service.PasswordEngine INFO Server conflicts: 0 Service.PasswordEngine INFO Actual changes for server: 1 Service.PasswordEngine DEBUG Actual changes for server: [{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}] Service.PasswordEngine INFO Uploading changes to server -Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}]] -Service.Resource DEBUG PUT request successful +Testing INFO HTTP PUT to user-data/passwords/deltas/1 with data: [{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}] +Service.ResourceSet DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} Service.Resource DEBUG PUT request successful @@ -99,7 +89,7 @@ Testing INFO Opening 'weave/snapshots/passwords.json' for reading. Testing INFO Reading from stream. Service.SnapStore INFO Read saved snapshot from disk Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/ +Testing INFO HTTP MKCOL on user-data/passwords/deltas Service.RemoteStore DEBUG Downloading status file Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 Service.Resource DEBUG GET request successful @@ -118,16 +108,11 @@ Service.PasswordEngine INFO Server conflicts: 0 Service.PasswordEngine INFO Actual changes for server: 1 Service.PasswordEngine DEBUG Actual changes for server: [{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}] Service.PasswordEngine INFO Uploading changes to server -Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/passwords/deltas.json with data: [[{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}],[{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}]] -Service.Resource DEBUG PUT request successful +Testing INFO HTTP PUT to user-data/passwords/deltas/2 with data: [{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}] +Service.ResourceSet DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} Service.Resource DEBUG PUT request successful @@ -141,7 +126,7 @@ Testing INFO ----------------------------------------- Testing INFO Step 'resync on second computer' starting. Testing INFO ----------------------------------------- Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/ +Testing INFO HTTP MKCOL on user-data/passwords/deltas Service.RemoteStore DEBUG Downloading status file Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 Service.Resource DEBUG GET request successful @@ -152,19 +137,22 @@ Service.PasswordEngine INFO Local snapshot version: -1 Service.PasswordEngine INFO Server maxVersion: 2 Service.RemoteStore TRACE Getting latest from snap --> scratch Service.RemoteStore INFO Downloading all server data from scratch -Service.RemoteStore DEBUG Downloading server snapshot Testing INFO HTTP GET from user-data/passwords/snapshot.json, returning status 200 Service.Resource DEBUG GET request successful Service.CryptoFilter DEBUG Decrypting data Service.Crypto DEBUG NOT decrypting data Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading server deltas -Testing INFO HTTP GET from user-data/passwords/deltas.json, returning status 200 -Service.Resource DEBUG GET request successful +Testing INFO HTTP GET from user-data/passwords/deltas/1, returning status 200 +Service.ResourceSet DEBUG GET request successful Service.CryptoFilter DEBUG Decrypting data Service.Crypto DEBUG NOT decrypting data Service.JsonFilter DEBUG Decoding JSON data Service.SnapStore TRACE Processing command: {"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}} +Testing INFO HTTP GET from user-data/passwords/deltas/2, returning status 200 +Service.ResourceSet DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data +Service.JsonFilter DEBUG Decoding JSON data Service.SnapStore TRACE Processing command: {"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]} Service.PasswordEngine INFO Reconciling client/server updates Service.PasswordSync DEBUG Reconciling 0 against 1 commands From 23e062b4c9ef37c9de2a587611d5e4f2f81ebb45 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 11:40:14 -0700 Subject: [PATCH 0482/1860] Refactored password sync test to use newly-added functionality in the sync testing infrastructure. --- services/sync/tests/unit/fake_login_manager.js | 1 + services/sync/tests/unit/test_password_syncing.js | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/tests/unit/fake_login_manager.js b/services/sync/tests/unit/fake_login_manager.js index 3c01d24479eb..669d7a8e59ac 100644 --- a/services/sync/tests/unit/fake_login_manager.js +++ b/services/sync/tests/unit/fake_login_manager.js @@ -27,6 +27,7 @@ function FakeLoginManager(fakeLogins) { Utils.getLoginManager = function fake_getLoginManager() { // Return a fake nsILoginManager object. return { + removeAllLogins: function() { self.fakeLogins = []; }, getAllLogins: function() { return self.fakeLogins; }, addLogin: function(login) { getTestLogger().info("nsILoginManager.addLogin() called " + diff --git a/services/sync/tests/unit/test_password_syncing.js b/services/sync/tests/unit/test_password_syncing.js index 90af22cb7fbe..db6eae036f04 100644 --- a/services/sync/tests/unit/test_password_syncing.js +++ b/services/sync/tests/unit/test_password_syncing.js @@ -7,7 +7,7 @@ load("fake_login_manager.js"); // ---------------------------------------- function run_test() { - var syncTesting = new SyncTestingInfrastructure(); + var syncTesting = new SyncTestingInfrastructure(PasswordEngine); var fakeLoginManager = new FakeLoginManager(fakeSampleLogins); function freshEngineSync(cb) { @@ -35,8 +35,7 @@ function run_test() { syncTesting.runAsyncFunc("remove user and re-sync", freshEngineSync); - syncTesting.fakeFilesystem.fakeContents = {}; - fakeLoginManager.fakeLogins = []; + syncTesting.resetClientState(); syncTesting.runAsyncFunc("resync on second computer", freshEngineSync); } From 5d03f16ad70b1f3e01474cdc0f1ad2374f9668a4 Mon Sep 17 00:00:00 2001 From: Date: Thu, 26 Jun 2008 12:07:38 -0700 Subject: [PATCH 0483/1860] For developer release, made bookmark sharing and xmpp preferences default to false --- services/sync/services-sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index f5f888dfada3..a164d9020d9a 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -7,7 +7,7 @@ pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.lastsync", "0"); pref("extensions.weave.ui.syncnow", true); -pref("extensions.weave.ui.sharebookmarks", true); +pref("extensions.weave.ui.sharebookmarks", false); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); @@ -34,7 +34,7 @@ pref("extensions.weave.log.logger.service.dav", "Debug"); pref("extensions.weave.log.logger.service.engine", "Debug"); pref("extensions.weave.log.logger.service.main", "Trace"); -pref("extensions.weave.xmpp.enabled", true); +pref("extensions.weave.xmpp.enabled", false); pref("extensions.weave.xmpp.server.url", "https://sm-labs01.mozilla.org:81/xmpp"); pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); From c963673963ae21e40c4845b4093709ee476b9f8b Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Thu, 26 Jun 2008 12:09:28 -0700 Subject: [PATCH 0484/1860] added text (not final), changed background, some code clean-up, todo: more code clean-up, fix large buttons (they are ugly) --- services/sync/locales/en-US/wizard.dtd | 20 ++++++++++--------- services/sync/locales/en-US/wizard.properties | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index cf5861b6e269..bc6b9d94d03a 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,18 +1,16 @@ - - - - - + + + - + @@ -42,7 +40,7 @@ - + @@ -62,7 +60,7 @@ - + @@ -73,7 +71,8 @@ - + + @@ -87,3 +86,6 @@ + + + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 9be39de1b737..20a07d911560 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -40,7 +40,7 @@ create-success.label = Account for %S created. final-pref-value.label = %S final-account-value.label = Username: %S -final-sync-value.label = [Explain that a sync will happen] +final-sync-value.label = Weave will upload your data to the server. final-success.label = Weave was successfully installed. default-name.label = %S's Firefox From 626e98f9fdf026cdf238cb1d699c902c90fc8d67 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 12:21:25 -0700 Subject: [PATCH 0485/1860] disable bookmarks sharing & xmpp for developer release --- services/sync/services-sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 1b192679f0ca..756efe6db186 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -7,7 +7,7 @@ pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.lastsync", "0"); pref("extensions.weave.ui.syncnow", true); -pref("extensions.weave.ui.sharebookmarks", true); +pref("extensions.weave.ui.sharebookmarks", false); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); @@ -34,7 +34,7 @@ pref("extensions.weave.log.logger.service.dav", "Debug"); pref("extensions.weave.log.logger.service.engine", "Debug"); pref("extensions.weave.log.logger.service.main", "Trace"); -pref("extensions.weave.xmpp.enabled", true); +pref("extensions.weave.xmpp.enabled", false); pref("extensions.weave.xmpp.server.url", "https://sm-labs01.mozilla.org:81/xmpp"); pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); From 3d3517219d91e33f566bcf3c2c1025452d0cc4c5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 12:22:19 -0700 Subject: [PATCH 0486/1860] return http status code from dav's checkLogin --- services/sync/modules/dav.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index ee06cac87e2e..b6513afec5c0 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -319,14 +319,7 @@ DAVCollection.prototype = { let resp = yield; this._log.debug("checkLogin got response status " + resp.status); - // XXX would be nice if 404 == invalid username, 401 == invalid password. - let retmsg = ""; - if (resp.status == 401) - retmsg = "invalid username or password"; - else if (resp.status < 200 || resp.status >= 300) - retmsg = "server error"; - - self.done(retmsg); + self.done(resp.status); }, // Locking From 64ebd95f038606706fc773d6f33ccc46354bfa39 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 12:27:54 -0700 Subject: [PATCH 0487/1860] check login status code in service's verifyLogin; attempt to create user directory when it's a 404 --- services/sync/modules/service.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2968d4021ab8..97ade0cc8904 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -509,16 +509,16 @@ WeaveSvc.prototype = { DAV.baseURL = Utils.prefs.getCharPref("serverURL"); DAV.defaultPrefix = "user/" + username; - DAV.checkLogin.async(DAV, self.cb, username, password); - let resultMsg = yield; + this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); - // If we got an error message, throw it. [need to throw to cause the - // _notify() wrapper to generate an error notification for observers]. - if (resultMsg) { - this._log.debug("Login verification: " + resultMsg); - throw resultMsg; + let status = yield DAV.checkLogin.async(DAV, self.cb, username, password); + if (status == 404) { + // create user directory (for self-hosted webdav shares) + // XXX move to initialize? + yield this._checkUserDir.async(this, self.cb); + status = yield DAV.checkLogin.async(DAV, self.cb, username, password); } - + Utils.ensureStatus(status, "Login verification failed"); }, login: function WeaveSync_login(onComplete) { @@ -539,15 +539,12 @@ WeaveSvc.prototype = { this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); - this._versionCheck.async(this, self.cb); - yield; - this._getKeypair.async(this, self.cb); - yield; - - this._loggedIn = true; + yield this._versionCheck.async(this, self.cb); + yield this._getKeypair.async(this, self.cb); this._setSchedule(this.schedule); + this._loggedIn = true; self.done(true); }, From 14d9c199229fbdb0de513ba582218faa884a2c07 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 14:30:38 -0700 Subject: [PATCH 0488/1860] fix comment in verifyLogin --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 97ade0cc8904..5f2aede4e70a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -514,7 +514,7 @@ WeaveSvc.prototype = { let status = yield DAV.checkLogin.async(DAV, self.cb, username, password); if (status == 404) { // create user directory (for self-hosted webdav shares) - // XXX move to initialize? + // XXX do this in login? yield this._checkUserDir.async(this, self.cb); status = yield DAV.checkLogin.async(DAV, self.cb, username, password); } From 6fdbbe05464568c64ee08de89284f4a6eea41728 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 14:34:32 -0700 Subject: [PATCH 0489/1860] tone down tab engine debugging output a bit --- services/sync/modules/engines/tabs.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 962ad50f05ff..a35b558d3df0 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -77,11 +77,11 @@ TabSyncCore.prototype = { // XXX Should we convert both to nsIURIs and then use nsIURI::equals // to compare them? if (GUID in tabs) { - this._log.debug("_itemExists: " + GUID + " exists"); + this._log.trace("_itemExists: " + GUID + " exists"); return true; } - this._log.debug("_itemExists: " + GUID + " doesn't exist"); + this._log.trace("_itemExists: " + GUID + " doesn't exist"); return false; }, @@ -333,7 +333,7 @@ TabStore.prototype = { // (f.e. in the "selectedWindow" and each tab's "index" properties), so we // convert them to and from JavaScript's zero-based indexes as needed. let windowID = i + 1; - this._log.debug("_wrapRealTabs: window " + windowID); + this._log.trace("_wrapRealTabs: window " + windowID); for (let j = 0; j < window.tabs.length; j++) { let tab = window.tabs[j]; @@ -348,7 +348,7 @@ TabStore.prototype = { } let tabID = currentEntry.url; - this._log.debug("_wrapRealTabs: tab " + tabID); + this._log.trace("_wrapRealTabs: tab " + tabID); // The ID property of each entry in the tab, which I think contains // nsISHEntry::ID, changes every time session store restores the tab, From 035a04998ce3d6db550f22adf3f73aea6b566712 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 26 Jun 2008 14:49:01 -0700 Subject: [PATCH 0490/1860] Add license headers to all files which didn't have them. --- services/sync/modules/engines/cookies.js | 36 +++++++++++++++++++ services/sync/modules/engines/forms.js | 36 +++++++++++++++++++ services/sync/modules/engines/history.js | 36 +++++++++++++++++++ services/sync/modules/engines/passwords.js | 36 +++++++++++++++++++ services/sync/modules/engines/tabs.js | 36 +++++++++++++++++++ services/sync/modules/faultTolerance.js | 36 +++++++++++++++++++ services/sync/modules/sharing.js | 36 +++++++++++++++++++ services/sync/modules/trackers.js | 2 +- .../sync/modules/xmpp/authenticationLayer.js | 36 +++++++++++++++++++ services/sync/modules/xmpp/transportLayer.js | 36 +++++++++++++++++++ services/sync/modules/xmpp/xmppClient.js | 36 +++++++++++++++++++ 11 files changed, 361 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 9098755697b1..a1d8c1303483 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono DiCarlo + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = ['CookieEngine', 'CookieTracker', 'CookieStore']; const Cc = Components.classes; diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 0a3e532dd33a..dc244701b98b 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Anant Narayanan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = ['FormEngine']; const Cc = Components.classes; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 679f67d48247..17ee24d7dff0 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = ['HistoryEngine']; const Cc = Components.classes; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 689fa820d742..27e7594c7a09 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Justin Dolske + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = ['PasswordEngine']; const Cu = Components.utils; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index a35b558d3df0..7d440e5b76c2 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = ['TabEngine']; const Cc = Components.classes; diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index cc848b403919..ec355cfd3718 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); diff --git a/services/sync/modules/sharing.js b/services/sync/modules/sharing.js index bbf5c1a3692c..c3100ab22b41 100644 --- a/services/sync/modules/sharing.js +++ b/services/sync/modules/sharing.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + EXPORTED_SYMBOLS = ["Sharing"]; const Cc = Components.classes; diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index cfb70d5eb592..b6b3a02c79a1 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -14,7 +14,7 @@ * The Original Code is Bookmarks Sync. * * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 + * Portions created by the Initial Developer are Copyright (C) 2008 * the Initial Developer. All Rights Reserved. * * Contributor(s): diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js index 5a7cacb42ff0..ab3f1568a16a 100644 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ b/services/sync/modules/xmpp/authenticationLayer.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono DiCarlo + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = [ "PlainAuthenticator", "Md5DigestAuthenticator" ]; var Cc = Components.classes; diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index b48168301895..1148cf935498 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono DiCarlo + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = ['HTTPPollingTransport']; var Cc = Components.classes; diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index d25d315d69e9..9898a92341d0 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -1,3 +1,39 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono DiCarlo + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + const EXPORTED_SYMBOLS = ['XmppClient', 'HTTPPollingTransport', 'PlainAuthenticator', 'Md5DigestAuthenticator']; // See www.xulplanet.com/tutorials/mozsdk/sockets.php From aa501d14906307d47c7dcf2e77df7581517d017c Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 15:01:34 -0700 Subject: [PATCH 0491/1860] Refactored some things in the test framework to make tests easier to write, and to make certain things possible to write. --- services/sync/tests/unit/head_first.js | 35 ++++++++++++++----- .../sync/tests/unit/test_bookmark_syncing.js | 30 ++++++++-------- .../unit/test_bookmark_syncing.log.expected | 6 ---- .../sync/tests/unit/test_password_syncing.js | 18 ++++------ 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 41e98e5e4513..fe555c398e67 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -274,7 +274,7 @@ function FakeGUIDService() { }; } -function SyncTestingInfrastructure(engineCtor) { +function SyncTestingInfrastructure(engineFactory) { let __fakePasswords = { 'Mozilla Services Password': {foo: "bar"}, 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} @@ -303,32 +303,38 @@ function SyncTestingInfrastructure(engineCtor) { this.fakeGUIDService = new FakeGUIDService(); this._logger = getTestLogger(); - this._Engine = engineCtor; + this._engineFactory = engineFactory; this._clientStates = []; this.saveClientState = function pushClientState(label) { let state = Utils.deepCopy(this.fakeFilesystem.fakeContents); - this._clientStates[label] = state; + let currContents = this.fakeFilesystem.fakeContents; + this.fakeFilesystem.fakeContents = []; + let engine = this._engineFactory(); + let snapshot = Utils.deepCopy(engine._store.wrap()); + this._clientStates[label] = {state: state, snapshot: snapshot}; + this.fakeFilesystem.fakeContents = currContents; }; this.restoreClientState = function restoreClientState(label) { - let state = this._clientStates[label]; + let state = this._clientStates[label].state; + let snapshot = this._clientStates[label].snapshot; function _restoreState() { let self = yield; - this.fakeFilesystem.fakeContents = Utils.deepCopy(state); - let engine = new this._Engine(); + this.fakeFilesystem.fakeContents = []; + let engine = this._engineFactory(); engine._store.wipe(); let originalSnapshot = Utils.deepCopy(engine._store.wrap()); - engine._snapshot.load(); - let snapshot = engine._snapshot.data; engine._core.detectUpdates(self.cb, originalSnapshot, snapshot); let commands = yield; engine._store.applyCommands.async(engine._store, self.cb, commands); yield; + + this.fakeFilesystem.fakeContents = Utils.deepCopy(state); } let self = this; @@ -349,6 +355,17 @@ function SyncTestingInfrastructure(engineCtor) { }; }; + this.doSync = function doSync(name) { + let self = this; + + function freshEngineSync(cb) { + let engine = self._engineFactory(); + engine.sync(cb); + } + + this.runAsyncFunc(name, freshEngineSync); + }; + this.runAsyncFunc = function runAsyncFunc(name, func) { let logger = this._logger; @@ -367,7 +384,7 @@ function SyncTestingInfrastructure(engineCtor) { this.resetClientState = function resetClientState() { this.fakeFilesystem.fakeContents = {}; - let engine = new this._Engine(); + let engine = this._engineFactory(); engine._store.wipe(); }; } diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/test_bookmark_syncing.js index cb06a1d73ca3..3c9e6cf30e22 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.js +++ b/services/sync/tests/unit/test_bookmark_syncing.js @@ -14,18 +14,18 @@ function FakeMicrosummaryService() { return {hasMicrosummary: function() { return false; }}; } +function makeBookmarksEngine() { + let engine = new BookmarksEngine(); + engine._store.__ms = new FakeMicrosummaryService(); + return engine; +} + function run_test() { // ----- // Setup // ----- - var syncTesting = new SyncTestingInfrastructure(BookmarksEngine); - - function freshEngineSync(cb) { - let engine = new BookmarksEngine(); - engine._store.__ms = new FakeMicrosummaryService(); - engine.sync(cb); - }; + var syncTesting = new SyncTestingInfrastructure(makeBookmarksEngine); let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); @@ -42,9 +42,9 @@ function run_test() { "Boogle"); bms.setItemGUID(boogleBm, "boogle-bookmark-guid"); - syncTesting.runAsyncFunc("initial sync w/ one bookmark", freshEngineSync); + syncTesting.doSync("initial sync w/ one bookmark"); - syncTesting.runAsyncFunc("trivial re-sync", freshEngineSync); + syncTesting.doSync("trivial re-sync"); let yoogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, uri("http://www.yoogle.com"), @@ -52,20 +52,19 @@ function run_test() { "Yoogle"); bms.setItemGUID(yoogleBm, "yoogle-bookmark-guid"); - syncTesting.runAsyncFunc("add bookmark and re-sync", freshEngineSync); + syncTesting.doSync("add bookmark and re-sync"); bms.moveItem(yoogleBm, bms.bookmarksMenuFolder, 0); - syncTesting.runAsyncFunc("swap bookmark order and re-sync", - freshEngineSync); + syncTesting.doSync("swap bookmark order and re-sync"); syncTesting.saveClientState("first computer"); syncTesting.resetClientState(); - syncTesting.runAsyncFunc("re-sync on second computer", freshEngineSync); + syncTesting.doSync("re-sync on second computer"); let zoogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, uri("http://www.zoogle.com"), @@ -73,11 +72,10 @@ function run_test() { "Zoogle"); bms.setItemGUID(zoogleBm, "zoogle-bookmark-guid"); - syncTesting.runAsyncFunc("add bookmark on second computer and resync", - freshEngineSync); + syncTesting.doSync("add bookmark on second computer and resync"); syncTesting.restoreClientState("first computer"); - syncTesting.runAsyncFunc("re-sync on first computer", freshEngineSync); + syncTesting.doSync("re-sync on first computer"); // -------- // Teardown diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/test_bookmark_syncing.log.expected index 580da38af1f3..9c121beb31e0 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.log.expected +++ b/services/sync/tests/unit/test_bookmark_syncing.log.expected @@ -218,12 +218,6 @@ Testing INFO Step 'add bookmark on second computer and resync' succeeded. Testing INFO ----------------------------------------- Testing INFO Step 'restore client state of first computer' starting. Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk Service.BStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} Service.BStore DEBUG -> creating bookmark "Yoogle" Service.BStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} diff --git a/services/sync/tests/unit/test_password_syncing.js b/services/sync/tests/unit/test_password_syncing.js index db6eae036f04..e65e60ebab86 100644 --- a/services/sync/tests/unit/test_password_syncing.js +++ b/services/sync/tests/unit/test_password_syncing.js @@ -7,17 +7,13 @@ load("fake_login_manager.js"); // ---------------------------------------- function run_test() { - var syncTesting = new SyncTestingInfrastructure(PasswordEngine); + function passwdFactory() { return new PasswordEngine(); } + var syncTesting = new SyncTestingInfrastructure(passwdFactory); var fakeLoginManager = new FakeLoginManager(fakeSampleLogins); - function freshEngineSync(cb) { - let engine = new PasswordEngine(); - engine.sync(cb); - }; + syncTesting.doSync("initial sync"); - syncTesting.runAsyncFunc("initial sync", freshEngineSync); - - syncTesting.runAsyncFunc("trivial re-sync", freshEngineSync); + syncTesting.doSync("trivial re-sync"); fakeLoginManager.fakeLogins.push( {hostname: "www.yoogle.com", @@ -29,13 +25,13 @@ function run_test() { passwordField: "test_password2"} ); - syncTesting.runAsyncFunc("add user and re-sync", freshEngineSync); + syncTesting.doSync("add user and re-sync"); fakeLoginManager.fakeLogins.pop(); - syncTesting.runAsyncFunc("remove user and re-sync", freshEngineSync); + syncTesting.doSync("remove user and re-sync"); syncTesting.resetClientState(); - syncTesting.runAsyncFunc("resync on second computer", freshEngineSync); + syncTesting.doSync("resync on second computer"); } From 890da27b9fe886ffc690d06a4ad3d391f5279f4c Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Thu, 26 Jun 2008 15:22:48 -0700 Subject: [PATCH 0492/1860] Use statically-sized stack buffers to fix building on Windows. --- services/crypto/WeaveCrypto.cpp | 48 ++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index 55d82f01bbb0..50a640042f58 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -50,6 +50,12 @@ #include "keyhi.h" #include "nss.h" +/* + * In a number of places we use stack buffers to hold smallish temporary data. + * 4K is plenty big for the exptected uses, and avoids poking holes in the + * heap for small allocations. (Yes, we still check for overflow.) + */ +#define STACK_BUFFER_SIZE 4096 NS_IMPL_ISUPPORTS1(WeaveCrypto, IWeaveCrypto) @@ -273,9 +279,9 @@ WeaveCrypto::CommonCrypt(const char *input, PRUint32 inputSize, SECItem *ivParam = nsnull; PRUint32 maxOutputSize; - char keyData[aSymmetricKey.Length()]; + char keyData[STACK_BUFFER_SIZE]; PRUint32 keyDataSize = sizeof(keyData); - char ivData[aIV.Length()]; + char ivData[STACK_BUFFER_SIZE]; PRUint32 ivDataSize = sizeof(ivData); rv = DecodeBase64(aSymmetricKey, keyData, &keyDataSize); @@ -468,8 +474,8 @@ WeaveCrypto::DeriveKeyFromPassphrase(const nsACString& aPassphrase, PromiseFlatCString fPass(aPassphrase); SECItem passphrase = {siBuffer, (unsigned char *)fPass.get(), fPass.Length()}; - char saltBytes[aSalt.Length()]; - PRUint32 saltBytesLength = aSalt.Length(); + char saltBytes[STACK_BUFFER_SIZE]; + PRUint32 saltBytesLength = sizeof(saltBytes); rv = DecodeBase64(aSalt, saltBytes, &saltBytesLength); NS_ENSURE_SUCCESS(rv, rv); SECItem salt = {siBuffer, (unsigned char*)saltBytes, saltBytesLength}; @@ -531,7 +537,7 @@ WeaveCrypto::WrapPrivateKey(SECKEYPrivateKey *aPrivateKey, rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); NS_ENSURE_SUCCESS(rv, rv); - char ivData[aIV.Length()]; + char ivData[STACK_BUFFER_SIZE]; PRUint32 ivDataSize = sizeof(ivData); rv = DecodeBase64(aIV, ivData, &ivDataSize); NS_ENSURE_SUCCESS(rv, rv); @@ -554,7 +560,7 @@ WeaveCrypto::WrapPrivateKey(SECKEYPrivateKey *aPrivateKey, // Use a stack buffer to hold the wrapped key. NSS says about 1200 bytes for // a 2048-bit RSA key, so our 4096 byte buffer should be plenty. - unsigned char stackBuffer[4096]; + unsigned char stackBuffer[STACK_BUFFER_SIZE]; SECItem wrappedKey = {siBuffer, stackBuffer, sizeof(stackBuffer)}; s = PK11_WrapPrivKey(aPrivateKey->pkcs11Slot, @@ -608,7 +614,10 @@ WeaveCrypto::GenerateRandomBytes(PRUint32 aByteCount, nsACString& aEncodedBytes) { nsresult rv; - char random[aByteCount]; + char random[STACK_BUFFER_SIZE]; + + if (aByteCount > STACK_BUFFER_SIZE) + return NS_ERROR_OUT_OF_MEMORY; rv = PK11_GenerateRandom((unsigned char *)random, aByteCount); NS_ENSURE_SUCCESS(rv, rv); @@ -631,7 +640,10 @@ WeaveCrypto::GenerateRandomIV(nsACString& aEncodedBytes) CK_MECHANISM_TYPE mech = PK11_AlgtagToMechanism(mAlgorithm); PRUint32 size = PK11_GetIVLength(mech); - char random[size]; + char random[STACK_BUFFER_SIZE]; + + if (size > STACK_BUFFER_SIZE) + return NS_ERROR_OUT_OF_MEMORY; rv = PK11_GenerateRandom((unsigned char *)random, size); NS_ENSURE_SUCCESS(rv, rv); @@ -742,19 +754,19 @@ WeaveCrypto::WrapSymmetricKey(const nsACString& aSymmetricKey, // Step 1. Get rid of the base64 encoding on the inputs. - char publicKeyBuffer[aPublicKey.Length()]; - PRUint32 publicKeyBufferSize = aPublicKey.Length(); + char publicKeyBuffer[STACK_BUFFER_SIZE]; + PRUint32 publicKeyBufferSize = sizeof(publicKeyBuffer); rv = DecodeBase64(aPublicKey, publicKeyBuffer, &publicKeyBufferSize); NS_ENSURE_SUCCESS(rv, rv); SECItem pubKeyData = {siBuffer, (unsigned char *)publicKeyBuffer, publicKeyBufferSize}; - char symKeyBuffer[aSymmetricKey.Length()]; - PRUint32 symKeyBufferSize = aSymmetricKey.Length(); + char symKeyBuffer[STACK_BUFFER_SIZE]; + PRUint32 symKeyBufferSize = sizeof(symKeyBuffer); rv = DecodeBase64(aSymmetricKey, symKeyBuffer, &symKeyBufferSize); NS_ENSURE_SUCCESS(rv, rv); SECItem symKeyData = {siBuffer, (unsigned char *)symKeyBuffer, symKeyBufferSize}; - char wrappedBuffer[4096]; + char wrappedBuffer[STACK_BUFFER_SIZE]; SECItem wrappedKey = {siBuffer, (unsigned char *)wrappedBuffer, sizeof(wrappedBuffer)}; @@ -869,14 +881,14 @@ WeaveCrypto::UnwrapSymmetricKey(const nsACString& aWrappedSymmetricKey, // Step 1. Get rid of the base64 encoding on the inputs. - char privateKeyBuffer[aWrappedPrivateKey.Length()]; - PRUint32 privateKeyBufferSize = aWrappedPrivateKey.Length(); + char privateKeyBuffer[STACK_BUFFER_SIZE]; + PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); NS_ENSURE_SUCCESS(rv, rv); SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; - char wrappedKeyBuffer[aWrappedSymmetricKey.Length()]; - PRUint32 wrappedKeyBufferSize = aWrappedSymmetricKey.Length(); + char wrappedKeyBuffer[STACK_BUFFER_SIZE]; + PRUint32 wrappedKeyBufferSize = sizeof(wrappedKeyBuffer); rv = DecodeBase64(aWrappedSymmetricKey, wrappedKeyBuffer, &wrappedKeyBufferSize); NS_ENSURE_SUCCESS(rv, rv); SECItem wrappedSymKey = {siBuffer, (unsigned char *)wrappedKeyBuffer, wrappedKeyBufferSize}; @@ -886,7 +898,7 @@ WeaveCrypto::UnwrapSymmetricKey(const nsACString& aWrappedSymmetricKey, rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); NS_ENSURE_SUCCESS(rv, rv); - char ivData[aIV.Length()]; + char ivData[STACK_BUFFER_SIZE]; PRUint32 ivDataSize = sizeof(ivData); rv = DecodeBase64(aIV, ivData, &ivDataSize); NS_ENSURE_SUCCESS(rv, rv); From eaf91c8b072252e57b293e71aa589aec0aa7d21e Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Thu, 26 Jun 2008 18:37:05 -0400 Subject: [PATCH 0493/1860] adding EULA that will be displayed and must be agreed to on first run --- services/sync/locales/en-US/wizard.dtd | 176 +++++++++++++------------ 1 file changed, 92 insertions(+), 84 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index bc6b9d94d03a..bf3e03c6bd44 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,91 +1,99 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + From 80e61ae251ae9a18faa7f8cee70c44284a731fa6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 15:37:30 -0700 Subject: [PATCH 0494/1860] disabling password sync by default due to bug 438356 --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 756efe6db186..4728fab6f82f 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -18,7 +18,7 @@ pref("extensions.weave.syncOnQuit.enabled", true); pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.cookies", true ); -pref("extensions.weave.engine.passwords", true ); +pref("extensions.weave.engine.passwords", false); pref("extensions.weave.engine.forms", true ); pref("extensions.weave.engine.tabs", true); From 335d52cd19b94f85acfd880fe5f496d6adef6d10 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 16:09:22 -0700 Subject: [PATCH 0495/1860] fix unit test makefile so it works on windows (no symlinks on windows, boo) --- services/sync/tests/unit/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) mode change 120000 => 100644 services/sync/tests/unit/Makefile diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile deleted file mode 120000 index 615b4be7d757..000000000000 --- a/services/sync/tests/unit/Makefile +++ /dev/null @@ -1 +0,0 @@ -../harness/Makefile \ No newline at end of file diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile new file mode 100644 index 000000000000..386b97952ace --- /dev/null +++ b/services/sync/tests/unit/Makefile @@ -0,0 +1,2 @@ +all: + ${MAKE} -f ../harness/Makefile From 7deb1bfbe5c93a485f12ef13908226d430a3dcb3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 16:26:14 -0700 Subject: [PATCH 0496/1860] when we abort a lock request because we already hold a token, reset _lockAllowed to allow further requests for locks --- services/sync/modules/dav.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index e95b2ec4979a..a4ad645c6045 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -360,6 +360,7 @@ DAVCollection.prototype = { if (DAVLocks['default']) { this._log.debug("Lock called, but we already hold a token"); + this._lockAllowed = true; self.done(); return; } @@ -395,7 +396,7 @@ DAVCollection.prototype = { this._log.trace("Lock acquired"); this._lockAllowed = true; - + self.done(DAVLocks['default']); }, From 44b1c77e372b50370d630c85de545fd0472e2988 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 16:27:54 -0700 Subject: [PATCH 0497/1860] Added an additional check in stores.js to prevent a strict warning from appearing. --- services/sync/modules/stores.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 4ca580efe2f3..a4a0954e9124 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -168,7 +168,8 @@ SnapshotStore.prototype = { delete this._data[oldGUID]; for (let GUID in this._data) { - if (this._data[GUID].parentGUID == oldGUID) + if (("parentGUID" in this._data[GUID]) && + (this._data[GUID].parentGUID == oldGUID)) this._data[GUID].parentGUID = newGUID; } } From c49f892f2a4db79979e67a5e7bc5643bfce45bde Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 16:28:44 -0700 Subject: [PATCH 0498/1860] Added a bookmark sync test that exercises the commandLike() method by adding two identical bookmarks with different GUIDs to different profiles and then syncing. --- .../sync/tests/unit/test_bookmark_syncing.js | 21 +++++ .../unit/test_bookmark_syncing.log.expected | 89 +++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/test_bookmark_syncing.js index 3c9e6cf30e22..60a10162b791 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.js +++ b/services/sync/tests/unit/test_bookmark_syncing.js @@ -74,9 +74,30 @@ function run_test() { syncTesting.doSync("add bookmark on second computer and resync"); + syncTesting.saveClientState("second computer"); + syncTesting.restoreClientState("first computer"); syncTesting.doSync("re-sync on first computer"); + let binkBm1 = bms.insertBookmark(bms.bookmarksMenuFolder, + uri("http://www.bink.com"), + -1, + "Bink"); + bms.setItemGUID(binkBm1, "bink-bookmark-guid-1"); + + syncTesting.doSync("add bookmark 'bink' on first computer and resync"); + syncTesting.restoreClientState("second computer"); + + let binkBm2 = bms.insertBookmark(bms.bookmarksMenuFolder, + uri("http://www.bink.com"), + -1, + "Bink"); + + bms.setItemGUID(binkBm2, "bink-bookmark-guid-2"); + + syncTesting.doSync("Manually add same bookmark 'bink', but with " + + + "different GUID, to second computer and resync"); + // -------- // Teardown // -------- diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/test_bookmark_syncing.log.expected index 9c121beb31e0..e80b91d2e6ba 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.log.expected +++ b/services/sync/tests/unit/test_bookmark_syncing.log.expected @@ -263,6 +263,95 @@ Service.BmkEngine INFO Actual changes for server: 0 Service.BmkEngine DEBUG Actual changes for server: [] Service.BmkEngine INFO Sync complete Testing INFO Step 're-sync on first computer' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 'add bookmark 'bink' on first computer and resync' starting. +Testing INFO ----------------------------------------- +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.BmkEngine INFO Local snapshot version: 3 +Service.BmkEngine INFO Server maxVersion: 3 +Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) +Service.RemoteStore TRACE Local snapshot version == server maxVersion +Service.BmkEngine INFO Reconciling client/server updates +Service.BMSync DEBUG Reconciling 1 against 0 commands +Service.BmkEngine INFO Changes for client: 0 +Service.BmkEngine INFO Predicted changes for server: 1 +Service.BmkEngine INFO Client conflicts: 0 +Service.BmkEngine INFO Server conflicts: 0 +Service.BmkEngine INFO Actual changes for server: 1 +Service.BmkEngine DEBUG Actual changes for server: [{"action":"create","GUID":"bink-bookmark-guid-1","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}}] +Service.BmkEngine INFO Uploading changes to server +Service.JsonFilter DEBUG Encoding data as JSON +Service.CryptoFilter DEBUG Encrypting data +Service.Crypto DEBUG NOT encrypting data +Testing INFO HTTP PUT to user-data/bookmarks/deltas/4 with data: [{"action":"create","GUID":"bink-bookmark-guid-1","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}}] +Service.ResourceSet DEBUG PUT request successful +Service.JsonFilter DEBUG Encoding data as JSON +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":4,"snapEncryption":"none","deltasEncryption":"none","itemCount":7} +Service.Resource DEBUG PUT request successful +Service.BmkEngine INFO Successfully updated deltas and status on server +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":4,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null},"bink-bookmark-guid-1":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} +Service.BmkEngine INFO Sync complete +Testing INFO Step 'add bookmark 'bink' on first computer and resync' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 'restore client state of second computer' starting. +Testing INFO ----------------------------------------- +Service.BStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} +Service.BStore DEBUG -> creating bookmark "Yoogle" +Service.BStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} +Service.BStore DEBUG -> creating bookmark "Boogle" +Service.BStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}} +Service.BStore DEBUG -> creating bookmark "Zoogle" +Testing INFO Step 'restore client state of second computer' succeeded. +Testing INFO ----------------------------------------- +Testing INFO Step 'Manually add same bookmark 'bink', but with NaN' starting. +Testing INFO ----------------------------------------- +Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. +Testing INFO Reading from stream. +Service.SnapStore INFO Read saved snapshot from disk +Service.BmkEngine INFO Beginning sync +Testing INFO HTTP MKCOL on user-data/bookmarks/deltas +Service.RemoteStore DEBUG Downloading status file +Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 +Service.Resource DEBUG GET request successful +Service.JsonFilter DEBUG Decoding JSON data +Service.RemoteStore DEBUG Downloading status file... done +Service.BmkEngine INFO Local snapshot version: 3 +Service.BmkEngine INFO Server maxVersion: 4 +Service.RemoteStore DEBUG Using last sync snapshot as starting point for server snapshot +Service.RemoteStore INFO Downloading server deltas +Testing INFO HTTP GET from user-data/bookmarks/deltas/4, returning status 200 +Service.ResourceSet DEBUG GET request successful +Service.CryptoFilter DEBUG Decrypting data +Service.Crypto DEBUG NOT decrypting data +Service.JsonFilter DEBUG Decoding JSON data +Service.SnapStore TRACE Processing command: {"action":"create","GUID":"bink-bookmark-guid-1","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}} +Service.BmkEngine INFO Reconciling client/server updates +Service.BMSync DEBUG Reconciling 1 against 1 commands +Service.BmkEngine INFO Changes for client: 1 +Service.BmkEngine INFO Predicted changes for server: 0 +Service.BmkEngine INFO Client conflicts: 0 +Service.BmkEngine INFO Server conflicts: 0 +Service.BmkEngine INFO Applying changes locally +Service.SnapStore TRACE Processing command: {"action":"edit","GUID":"bink-bookmark-guid-2","data":{"GUID":"bink-bookmark-guid-1"}} +Service.BStore TRACE Processing command: {"action":"edit","GUID":"bink-bookmark-guid-2","data":{"GUID":"bink-bookmark-guid-1"}} +Service.SnapStore INFO Saving snapshot to disk +Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. +Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":4,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"bink-bookmark-guid-1":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}}} +Service.BmkEngine INFO Actual changes for server: 0 +Service.BmkEngine DEBUG Actual changes for server: [] +Service.BmkEngine INFO Sync complete +Testing INFO Step 'Manually add same bookmark 'bink', but with NaN' succeeded. *** test finished *** exiting *** PASS *** From c4e6ef90a0260386f253f60719853f59546cd238 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 16:37:38 -0700 Subject: [PATCH 0499/1860] don't even try to sync when the local lock is taken --- services/sync/modules/service.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5f2aede4e70a..d58ed458b4e6 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -260,8 +260,12 @@ WeaveSvc.prototype = { _onSchedule: function WeaveSync__onSchedule() { if (this.enabled) { - this._log.info("Running scheduled sync"); - this._notify("syncAsNeeded", this._lock(this._syncAsNeeded)).async(this); + if (!DAV.allowLock) { + this._log.info("Skipping scheduled sync; local operation in progress") + } else { + this._log.info("Running scheduled sync"); + this._notify("syncAsNeeded", this._lock(this._syncAsNeeded)).async(this); + } } }, From 357f07cf80f853b2b374e84fca7aba9779843240 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 16:59:25 -0700 Subject: [PATCH 0500/1860] Fixed a typo, added an explicit check in test_bookmark_syncing. --- services/sync/tests/unit/test_bookmark_syncing.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/test_bookmark_syncing.js index 60a10162b791..c7fb396e6e8b 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.js +++ b/services/sync/tests/unit/test_bookmark_syncing.js @@ -96,7 +96,11 @@ function run_test() { bms.setItemGUID(binkBm2, "bink-bookmark-guid-2"); syncTesting.doSync("Manually add same bookmark 'bink', but with " + - + "different GUID, to second computer and resync"); + "different GUID, to second computer and resync"); + + binkBm2 = bms.getBookmarkIdsForURI(uri("http://www.bink.com"), {})[0]; + + do_check_eq(bms.getItemGUID(binkBm2), "bink-bookmark-guid-1"); // -------- // Teardown From bfc9a1f9eda697ccae16d3a2ef7655fe03a2dd2f Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 17:00:01 -0700 Subject: [PATCH 0501/1860] Oops, forgot to revalidate logs in my last commit. --- services/sync/tests/unit/test_bookmark_syncing.log.expected | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/test_bookmark_syncing.log.expected index e80b91d2e6ba..a98c2ae7205a 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.log.expected +++ b/services/sync/tests/unit/test_bookmark_syncing.log.expected @@ -314,7 +314,7 @@ Service.BStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookm Service.BStore DEBUG -> creating bookmark "Zoogle" Testing INFO Step 'restore client state of second computer' succeeded. Testing INFO ----------------------------------------- -Testing INFO Step 'Manually add same bookmark 'bink', but with NaN' starting. +Testing INFO Step 'Manually add same bookmark 'bink', but with different GUID, to second computer and resync' starting. Testing INFO ----------------------------------------- Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. Testing INFO Reading from stream. @@ -351,7 +351,7 @@ Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"vers Service.BmkEngine INFO Actual changes for server: 0 Service.BmkEngine DEBUG Actual changes for server: [] Service.BmkEngine INFO Sync complete -Testing INFO Step 'Manually add same bookmark 'bink', but with NaN' succeeded. +Testing INFO Step 'Manually add same bookmark 'bink', but with different GUID, to second computer and resync' succeeded. *** test finished *** exiting *** PASS *** From 0e0f9d62d3ad8c9ed6c63477f7501911c84738c1 Mon Sep 17 00:00:00 2001 From: Date: Thu, 26 Jun 2008 17:00:55 -0700 Subject: [PATCH 0502/1860] Fixed several minor bugs with bookmark sharing --- services/sync/modules/engines/bookmarks.js | 52 +++++++++++++++------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 7f21943fa9e4..c7f5bd3390e4 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -63,6 +63,7 @@ Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/xmpp/xmppClient.js"); +Cu.import("resource://weave/notifications.js"); Function.prototype.async = Async.sugar; @@ -82,7 +83,7 @@ BookmarksSharingManager.prototype = { this._engine = engine; this._log = Log4Moz.Service.getLogger("Bookmark Share"); if ( Utils.prefs.getBoolPref( "xmpp.enabled" ) ) { - dump( "Starting XMPP client for bookmark engine..." ); + this._log.info( "Starting XMPP client for bookmark engine..." ); this._startXmppClient.async(this); } }, @@ -95,7 +96,7 @@ BookmarksSharingManager.prototype = { let serverUrl = Utils.prefs.getCharPref( "xmpp.server.url" ); let realm = Utils.prefs.getCharPref( "xmpp.server.realm" ); - /* Username/password for XMPP are the same; as the ones for Weave, + /* Username/password for XMPP are the same as the ones for Weave, so read them from the weave identity: */ let clientName = ID.get('WeaveID').username; let clientPassword = ID.get('WeaveID').password; @@ -161,7 +162,7 @@ BookmarksSharingManager.prototype = { this._log.info("User " + user + " offered to share folder " + folderName); let bmkSharing = this; - let acceptButton = new Weave.NotificationButton( + let acceptButton = new NotificationButton( "Accept Share", "a", function() { @@ -172,7 +173,7 @@ BookmarksSharingManager.prototype = { return false; } ); - let rejectButton = new Weave.NotificationButton( + let rejectButton = new NotificationButton( "No Thanks", "n", function() {return false;} @@ -182,13 +183,13 @@ BookmarksSharingManager.prototype = { let description ="Weave user " + user + " is offering to share a bookmark folder called " + folderName + " with you. Do you want to accept it?"; - let notification = Weave.Notification(title, - description, - null, - Weave.Notifications.PRIORITY_INFO, - [acceptButton, rejectButton] - ); - Weave.Notifications.add(notification); + let notification = new Notification(title, + description, + null, + Notifications.PRIORITY_INFO, + [acceptButton, rejectButton] + ); + Notifications.add(notification); }, _share: function BmkSharing__share( selectedFolder, username ) { @@ -237,8 +238,10 @@ BookmarksSharingManager.prototype = { _stopSharing: function BmkSharing__stopSharing( selectedFolder, username ) { let self = yield; + // TODO FIXME the next line says getAttribute is not a function... let folderName = selectedFolder.getAttribute( "label" ); - let serverPath = this._annoSvc.getItemAnnotation(folderNode, + let folderNode = selectedFolder.node; + let serverPath = this._annoSvc.getItemAnnotation(folderNode.itemId, SERVER_PATH_ANNO); /* LONGTERM TODO: when we move to being able to share one folder with @@ -327,7 +330,7 @@ BookmarksSharingManager.prototype = { let folderGuid = Utils.makeGUID(); /* Create the directory on the server if it does not exist already. */ - let serverPath = "/user/" + myUserName + "/share/" + folderGuid; + let serverPath = "/0.2/user/" + myUserName + "/share/" + folderGuid; DAV.MKCOL(serverPath, self.cb); let ret = yield; if (!ret) { @@ -359,8 +362,8 @@ BookmarksSharingManager.prototype = { /* Get public keys for me and the user I'm sharing with. Each user's public key is stored in /user/username/public/pubkey. */ - let idRSA = ID.get('WeaveCryptoID'); - let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); + let idRSA = ID.get('WeaveCryptoID'); // TODO Can get error "Resource not defined" + let userPubKeyFile = new Resource("/0.2/user/" + username + "/public/pubkey"); userPubKeyFile.get(self.cb); let userPubKey = yield; @@ -447,6 +450,7 @@ BookmarksSharingManager.prototype = { OUTGOING_SHARED_ANNO ); // Delete the share from the server: + // TODO handle error that can happen if these resources do not exist. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.delete(self.cb); yield; @@ -663,8 +667,21 @@ BookmarksEngine.prototype = { this._sharing.updateAllIncomingShares(self.cb); yield; self.done(); - } + }, + _share: function BmkEngine__share(guid, username) { + let self = yield; + this._sharing.share.async( this._sharing, self.cb, guid, username); + yield; + self.done(true); + }, + + _stopSharing: function BmkEngine__stopSharing(guid, username) { + let self = yield; + this._sharing._stopSharing.async( this._sharing, self.cb, guid, username); + yield; + self.done(); + } }; BookmarksEngine.prototype.__proto__ = new Engine(); @@ -1105,11 +1122,12 @@ BookmarksStore.prototype = { node.containerOpen = true; // If folder is an outgoing share, wrap its annotations: if (this._ans.itemHasAnnotation(node.itemId, OUTGOING_SHARED_ANNO)) { - item.serverPathAnno = this._ans.getItemAnnotation(node.itemId, SERVER_PATH_ANNO); item.outgoingSharedAnno = this._ans.getItemAnnotation(node.itemId, OUTGOING_SHARED_ANNO); + // TODO this can throw an error if SERVER_PATH_ANNO doesn't exist + // (which it always should) } for (var i = 0; i < node.childCount; i++) { From 5e8e5bf3c7d67d0e51a895ec76a2b7d344df90a3 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Thu, 26 Jun 2008 17:08:44 -0700 Subject: [PATCH 0503/1860] fixed captcha screen --- services/sync/locales/en-US/wizard.dtd | 133 +++++++++--------- services/sync/locales/en-US/wizard.properties | 8 +- 2 files changed, 72 insertions(+), 69 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index bf3e03c6bd44..2006b0214fe3 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -2,78 +2,79 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 20a07d911560..6f894162c61d 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,3 +1,8 @@ +verify-progress.label = Verifying username and password +verify-error.label = Invalid username and password +verify-success.label = Username and password verified + + serverError.label = Server Error serverTimeoutError.label = Server Error @@ -22,9 +27,6 @@ missingCaptchaResponse.label = Please enter the words in the Captcha box. incorrectCaptcha.label = Incorrect Captcha response. Try again. -verify-progress.label = Verifying username and password -verify-error.label = Invalid username and password -verify-success.label = Username and password verified create-progress.label = Creating your account create-uid-inuse.label = Username in use From c54ff41f4ce28e3256dcfedf5f655951d8b2249b Mon Sep 17 00:00:00 2001 From: Date: Thu, 26 Jun 2008 17:26:21 -0700 Subject: [PATCH 0504/1860] Removed unneeded atob() and btoa() definitions from authenticationLayer. --- .../sync/modules/xmpp/authenticationLayer.js | 96 +++---------------- 1 file changed, 13 insertions(+), 83 deletions(-) diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js index ab3f1568a16a..50dd9eb0d0ad 100644 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ b/services/sync/modules/xmpp/authenticationLayer.js @@ -42,78 +42,8 @@ var Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); -if (typeof(atob) == 'undefined') { - // This code was written by Tyler Akins and has been placed in the - // public domain. It would be nice if you left this header intact. - // Base64 code from Tyler Akins -- http://rumkin.com - - var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - - function btoa(input) { - var output = ""; - var chr1, chr2, chr3; - var enc1, enc2, enc3, enc4; - var i = 0; - - do { - chr1 = input.charCodeAt(i++); - chr2 = input.charCodeAt(i++); - chr3 = input.charCodeAt(i++); - - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; - - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (isNaN(chr3)) { - enc4 = 64; - } - - output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + - keyStr.charAt(enc3) + keyStr.charAt(enc4); - } while (i < input.length); - - return output; - } - - function atob(input) { - var output = ""; - var chr1, chr2, chr3; - var enc1, enc2, enc3, enc4; - var i = 0; - - // remove all characters that are not A-Z, a-z, 0-9, +, /, or = - input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); - - do { - enc1 = keyStr.indexOf(input.charAt(i++)); - enc2 = keyStr.indexOf(input.charAt(i++)); - enc3 = keyStr.indexOf(input.charAt(i++)); - enc4 = keyStr.indexOf(input.charAt(i++)); - - chr1 = (enc1 << 2) | (enc2 >> 4); - chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); - chr3 = ((enc3 & 3) << 6) | enc4; - - output = output + String.fromCharCode(chr1); - - if (enc3 != 64) { - output = output + String.fromCharCode(chr2); - } - if (enc4 != 64) { - output = output + String.fromCharCode(chr3); - } - } while (i < input.length); - - return output; - } -} - - /* Two implementations of SASL authentication: - one using MD5-DIGEST, the other using PLAIN. + one using MD5-DIGEST, the other using PLAIN. Here's the interface that each implementation must obey: @@ -160,7 +90,7 @@ BaseAuthenticator.prototype = { this._errorMsg = "generateResponse() should be overridden by subclass."; return false; }, - + verifyProtocolSupport: function( rootElem, protocolName ) { /* Parses the incoming stream from the server to check whether the server supports the type of authentication we want to do @@ -171,7 +101,7 @@ BaseAuthenticator.prototype = { this._errorMsg = "Expected stream:stream but got " + rootElem.nodeName; return false; } - + dump( "Got response from server...\n" ); dump( "ID is " + rootElem.getAttribute( "id" ) + "\n" ); // TODO: Do I need to do anything with this ID value??? @@ -193,7 +123,7 @@ BaseAuthenticator.prototype = { this._errorMsg = "Expected stream:features but got " + child.nodeName; return false; } - + var protocolSupported = false; var mechanisms = child.getElementsByTagName( "mechanism" ); for ( var x = 0; x < mechanisms.length; x++ ) { @@ -201,7 +131,7 @@ BaseAuthenticator.prototype = { protocolSupported = true; } } - + if ( !protocolSupported ) { this._errorMsg = protocolName + " not supported by server!"; return false; @@ -216,7 +146,7 @@ function Md5DigestAuthenticator( ) { Uses complicated hash of password with nonce and cnonce to obscure password while preventing replay attacks. - + See http://www.faqs.org/rfcs/rfc2831.html "Using Digest Authentication as a SASL mechanism" @@ -241,8 +171,8 @@ Md5DigestAuthenticator.prototype = { return ""; } else if ( this._stepNumber == 1 ) { - - // proceed to SASL step 2: are you asking for a CHALLENGE?!? + + // proceed to SASL step 2: are you asking for a CHALLENGE?!? var challenge = this._unpackChallenge( rootElem.firstChild.nodeValue ); dump( "Nonce is " + challenge.nonce + "\n" ); // eg: @@ -295,7 +225,7 @@ Md5DigestAuthenticator.prototype = { } else if ( this._stepNumber = 2 ) { dump( "Got to step 3!" ); - // At this point the server might reject us with a + // At this point the server might reject us with a // if ( rootElem.nodeName == "failure" ) { this._errorMsg = rootElem.firstChild.nodeName; @@ -337,7 +267,7 @@ function PlainAuthenticator( ) { /* SASL using PLAIN authentication, which sends password in the clear. */ } PlainAuthenticator.prototype = { - + generateResponse: function( rootElem ) { if ( this._stepNumber == 0 ) { if ( this.verifyProtocolSupport( rootElem, "PLAIN" ) == false ) { @@ -385,14 +315,14 @@ PlainAuthenticator.prototype = { // Server hasn't requested either: we're done. return this.COMPLETION_CODE; } - + if ( this._needBinding ) { // Do resource binding: // Tell the server to generate the resource ID for us. this._stepNumber = 3; return ""; - } - + } + this._errorMsg = "Server requested session not binding: can't happen?"; return false; } else if ( this._stepNumber == 3 ) { From c145b287edb1f496c603c63abb50d0c5720e6c80 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Jun 2008 17:28:30 -0700 Subject: [PATCH 0505/1860] bump version to 0.1.32, storage format version (global & engine) to 3 --- services/sync/modules/constants.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 107099dc9ebb..38688ffeeb04 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,9 +42,9 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.31"; -const STORAGE_FORMAT_VERSION = 2; -const ENGINE_STORAGE_FORMAT_VERSION = 2; +const WEAVE_VERSION = "0.1.32"; +const STORAGE_FORMAT_VERSION = 3; +const ENGINE_STORAGE_FORMAT_VERSION = 3; const PREFS_BRANCH = "extensions.weave."; From 6cfe9281f3652768061272ad31151a29c0d5af2b Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 17:31:57 -0700 Subject: [PATCH 0506/1860] Added more assertion checks in test_bookmark_syncing. --- .../sync/tests/unit/test_bookmark_syncing.js | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/test_bookmark_syncing.js index c7fb396e6e8b..26dbf6c090d9 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.js +++ b/services/sync/tests/unit/test_bookmark_syncing.js @@ -30,6 +30,12 @@ function run_test() { let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); + function bmId(url) { + var bookmarks = bms.getBookmarkIdsForURI(uri(url), {}); + do_check_eq(bookmarks.length, 1); + return bookmarks[0]; + } + cleanUp(); // ----------- @@ -62,10 +68,19 @@ function run_test() { syncTesting.saveClientState("first computer"); + do_check_true(bms.isBookmarked(uri("http://www.boogle.com"))); + do_check_true(bms.isBookmarked(uri("http://www.yoogle.com"))); + syncTesting.resetClientState(); + do_check_false(bms.isBookmarked(uri("http://www.boogle.com"))); + do_check_false(bms.isBookmarked(uri("http://www.yoogle.com"))); + syncTesting.doSync("re-sync on second computer"); + do_check_true(bms.isBookmarked(uri("http://www.boogle.com"))); + do_check_true(bms.isBookmarked(uri("http://www.yoogle.com"))); + let zoogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, uri("http://www.zoogle.com"), -1, @@ -76,9 +91,16 @@ function run_test() { syncTesting.saveClientState("second computer"); + do_check_true(bms.isBookmarked(uri("http://www.zoogle.com"))); + syncTesting.restoreClientState("first computer"); + + do_check_false(bms.isBookmarked(uri("http://www.zoogle.com"))); + syncTesting.doSync("re-sync on first computer"); + do_check_true(bms.isBookmarked(uri("http://www.zoogle.com"))); + let binkBm1 = bms.insertBookmark(bms.bookmarksMenuFolder, uri("http://www.bink.com"), -1, @@ -98,9 +120,8 @@ function run_test() { syncTesting.doSync("Manually add same bookmark 'bink', but with " + "different GUID, to second computer and resync"); - binkBm2 = bms.getBookmarkIdsForURI(uri("http://www.bink.com"), {})[0]; - - do_check_eq(bms.getItemGUID(binkBm2), "bink-bookmark-guid-1"); + do_check_eq(bms.getItemGUID(bmId("http://www.bink.com")), + "bink-bookmark-guid-1"); // -------- // Teardown From 0b2c22f68c6a34ac82037946ff32664e0cce018a Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 17:48:39 -0700 Subject: [PATCH 0507/1860] Implemented DELETE and listFiles on fake DAV so that test_service doesn't raise an exception. --- services/sync/tests/unit/head_first.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index fe555c398e67..206c466f2fb0 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -195,6 +195,29 @@ FakeDAVService.prototype = { MKCOL: function fake_MKCOL(path, onComplete) { getTestLogger().info("HTTP MKCOL on " + path); makeFakeAsyncFunc(true).async(this, onComplete); + }, + + DELETE: function fake_DELETE(path, onComplete) { + var result = {status: 404}; + if (path in this.fakeContents) { + result = {status: 200}; + delete this.fakeContents[path]; + } + getTestLogger().info("HTTP DELETE on " + path + ", returning status " + + result.status); + return makeFakeAsyncFunc(result).async(this, onComplete); + }, + + listFiles: function fake_listFiles(path) { + let self = yield; + if (typeof(path) != "undefined") + throw new Error("Not yet implemented!"); + let filenames = []; + for (name in this.fakeContents) { + getTestLogger().info("file " + name); + filenames.push(name); + } + self.done(filenames); } }; From 95ef6a7d07cfde2c9bbf5e297507c8080acdfb0c Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 18:09:53 -0700 Subject: [PATCH 0508/1860] Fixed test_service. --- services/sync/tests/unit/test_service.js | 5 +++-- services/sync/tests/unit/test_service.log.expected | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index 1f027df7889d..b1ae20fa7814 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -12,8 +12,9 @@ let __fakePrefs = { }; let __fakeDAVContents = { - "meta/version" : "2", - "private/privkey" : "fake private key" + "meta/version" : "3", + "private/privkey" : '{"version":1,"algorithm":"RSA"}', + "public/pubkey" : '{"version":1,"algorithm":"RSA"}' }; let __fakePasswords = { diff --git a/services/sync/tests/unit/test_service.log.expected b/services/sync/tests/unit/test_service.log.expected index f41afc67fa11..c42a3ef9e7aa 100644 --- a/services/sync/tests/unit/test_service.log.expected +++ b/services/sync/tests/unit/test_service.log.expected @@ -1,10 +1,12 @@ *** test pending Running test: test_login_works Service.Main INFO Weave Sync Service Initializing -Service.Main DEBUG Logging in +Service.Main DEBUG Logging in user foo Service.Main INFO Using server URL: https://example.com/user/foo Testing INFO HTTP GET from meta/version, returning status 200 +Service.Main TRACE Retrieving keypair from server Testing INFO HTTP GET from private/privkey, returning status 200 +Testing INFO HTTP GET from public/pubkey, returning status 200 Service.Main INFO Weave scheduler disabled 1 of 1 tests passed. *** test finished From a89053bc55275a4039e3dc485fe87a3c41ef996f Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Thu, 26 Jun 2008 18:26:07 -0700 Subject: [PATCH 0509/1860] Revalidated logtest files, just a formatVersion change from 2 to 3. --- .../sync/tests/unit/test_bookmark_syncing.log.expected | 10 +++++----- .../sync/tests/unit/test_password_syncing.log.expected | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/test_bookmark_syncing.log.expected index a98c2ae7205a..ec7daa007aff 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.log.expected +++ b/services/sync/tests/unit/test_bookmark_syncing.log.expected @@ -16,7 +16,7 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/bookmarks/snapshot.json with data: {"boogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":4} +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":4} Service.Resource DEBUG PUT request successful Service.RemoteStore INFO Full upload to server successful Service.SnapStore INFO Saving snapshot to disk @@ -74,7 +74,7 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/bookmarks/deltas/1 with data: [{"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}}] Service.ResourceSet DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":5} +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":5} Service.Resource DEBUG PUT request successful Service.BmkEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk @@ -114,7 +114,7 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/bookmarks/deltas/2 with data: [{"action":"edit","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":1,"type":"bookmark"}},{"action":"edit","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":0,"type":"bookmark"}}] Service.ResourceSet DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":5} +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":5} Service.Resource DEBUG PUT request successful Service.BmkEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk @@ -207,7 +207,7 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/bookmarks/deltas/3 with data: [{"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}}] Service.ResourceSet DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":3,"snapEncryption":"none","deltasEncryption":"none","itemCount":6} +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":3,"snapEncryption":"none","deltasEncryption":"none","itemCount":6} Service.Resource DEBUG PUT request successful Service.BmkEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk @@ -295,7 +295,7 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/bookmarks/deltas/4 with data: [{"action":"create","GUID":"bink-bookmark-guid-1","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}}] Service.ResourceSet DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":4,"snapEncryption":"none","deltasEncryption":"none","itemCount":7} +Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":4,"snapEncryption":"none","deltasEncryption":"none","itemCount":7} Service.Resource DEBUG PUT request successful Service.BmkEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk diff --git a/services/sync/tests/unit/test_password_syncing.log.expected b/services/sync/tests/unit/test_password_syncing.log.expected index 672963945b1f..a8cfb284e6eb 100644 --- a/services/sync/tests/unit/test_password_syncing.log.expected +++ b/services/sync/tests/unit/test_password_syncing.log.expected @@ -16,7 +16,7 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/passwords/snapshot.json with data: {"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} Service.Resource DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} Service.Resource DEBUG PUT request successful Service.RemoteStore INFO Full upload to server successful Service.SnapStore INFO Saving snapshot to disk @@ -74,7 +74,7 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/passwords/deltas/1 with data: [{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}] Service.ResourceSet DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} Service.Resource DEBUG PUT request successful Service.PasswordEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk @@ -114,7 +114,7 @@ Service.Crypto DEBUG NOT encrypting data Testing INFO HTTP PUT to user-data/passwords/deltas/2 with data: [{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}] Service.ResourceSet DEBUG PUT request successful Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":2,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} +Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} Service.Resource DEBUG PUT request successful Service.PasswordEngine INFO Successfully updated deltas and status on server Service.SnapStore INFO Saving snapshot to disk From 35ac222c1b955fdfcd52c4914c83d5c6df8cc790 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Thu, 26 Jun 2008 21:42:43 -0700 Subject: [PATCH 0510/1860] added run wizard option to pref pane, fixed spacing issues --- services/sync/locales/en-US/preferences.dtd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 7354cc3f5bf6..8d43d70e1a5e 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -64,6 +64,8 @@ + + From 72070fb00a6e5f047c1b7b2dd4d3fa354b28ea7b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 27 Jun 2008 14:21:54 -0700 Subject: [PATCH 0511/1860] make log messages that print raw engine json be trace messages --- services/sync/modules/engines.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 77f18cef3331..e25c5b2e8133 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -344,7 +344,7 @@ Engine.prototype = { let diff = yield; if (diff.length != 0) { this._log.warn("Commands did not apply correctly"); - this._log.debug("Diff from snapshot+commands -> " + + this._log.trace("Diff from snapshot+commands -> " + "new snapshot after commands:\n" + this._serializeCommands(diff)); // FIXME: do we really want to revert the snapshot here? @@ -371,7 +371,7 @@ Engine.prototype = { "actual server->client diff (can be ignored in many cases)"); this._log.info("Actual changes for server: " + serverDelta.length); - this._log.debug("Actual changes for server: " + + this._log.trace("Actual changes for server: " + this._serializeCommands(serverDelta)); if (serverDelta.length) { From 444eab5481a8e9bc35aeda0a6948c210d484fc3d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 27 Jun 2008 15:01:40 -0700 Subject: [PATCH 0512/1860] disable form sync by default --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 4728fab6f82f..d85fd51c17cf 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -19,7 +19,7 @@ pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.cookies", true ); pref("extensions.weave.engine.passwords", false); -pref("extensions.weave.engine.forms", true ); +pref("extensions.weave.engine.forms", false); pref("extensions.weave.engine.tabs", true); pref("extensions.weave.log.appender.console", "Warn"); From fcd2857ffce6ddb8aed9e538777fb647df4edf73 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Fri, 27 Jun 2008 16:02:52 -0700 Subject: [PATCH 0513/1860] grayed out form and password data options, update error log messages, fix last screen to prevent accidental advancing during sync --- services/sync/locales/en-US/wizard.dtd | 2 +- services/sync/locales/en-US/wizard.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 2006b0214fe3..51d1b765b595 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -59,7 +59,7 @@ - + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 6f894162c61d..51196f3beba5 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -3,8 +3,8 @@ verify-error.label = Invalid username and password verify-success.label = Username and password verified -serverError.label = Server Error -serverTimeoutError.label = Server Error +serverError.label = Server Error +serverTimeoutError.label = Server Timeout createUsername-progress.label = Checking username From 58abc256ddd82ef12827baebb74783a45817d704 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Fri, 27 Jun 2008 20:16:43 -0700 Subject: [PATCH 0514/1860] General restructure for performance improvements (bug 441907, r=thunder) --- services/sync/modules/engines.js | 15 ++- services/sync/modules/engines/bookmarks.js | 34 ++--- services/sync/modules/engines/cookies.js | 122 +++++------------ services/sync/modules/engines/forms.js | 150 ++++----------------- services/sync/modules/engines/history.js | 28 ++-- services/sync/modules/engines/passwords.js | 92 +++++-------- services/sync/modules/engines/tabs.js | 61 +++++---- services/sync/modules/stores.js | 27 +++- services/sync/modules/syncCores.js | 12 +- 9 files changed, 193 insertions(+), 348 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e25c5b2e8133..09ae794b2263 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -130,13 +130,6 @@ Engine.prototype = { }, // _core, _store and _tracker need to be overridden in subclasses - __core: null, - get _core() { - if (!this.__core) - this.__core = new SyncCore(); - return this.__core; - }, - __store: null, get _store() { if (!this.__store) @@ -144,6 +137,13 @@ Engine.prototype = { return this.__store; }, + __core: null, + get _core() { + if (!this.__core) + this.__core = new SyncCore(this._store); + return this.__core; + }, + __tracker: null, get _tracker() { if (!this.__tracker) @@ -264,6 +264,7 @@ Engine.prototype = { yield this._remote.keys.getKeyAndIV(self.cb, this.engineId); // 1) Fetch server deltas + let server = {}; let serverSnap = yield this._remote.wrap(self.cb, this._snapshot); server.snapshot = serverSnap.data; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c7f5bd3390e4..ff80170bb1d8 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -628,13 +628,6 @@ BookmarksEngine.prototype = { get logName() { return "BmkEngine"; }, get serverPrefix() { return "user-data/bookmarks/"; }, - __core: null, - get _core() { - if (!this.__core) - this.__core = new BookmarksSyncCore(); - return this.__core; - }, - __store: null, get _store() { if (!this.__store) @@ -642,6 +635,13 @@ BookmarksEngine.prototype = { return this.__store; }, + __core: null, + get _core() { + if (!this.__core) + this.__core = new BookmarksSyncCore(this._store); + return this.__core; + }, + __tracker: null, get _tracker() { if (!this.__tracker) @@ -685,23 +685,13 @@ BookmarksEngine.prototype = { }; BookmarksEngine.prototype.__proto__ = new Engine(); -function BookmarksSyncCore() { +function BookmarksSyncCore(store) { + this._store = store; this._init(); } BookmarksSyncCore.prototype = { _logName: "BMSync", - - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - _itemExists: function BSC__itemExists(GUID) { - return this._bms.getItemIdForGUID(GUID) >= 0; - }, + _store: null, _getEdits: function BSC__getEdits(a, b) { // NOTE: we do not increment ret.numProps, as that would cause @@ -788,6 +778,7 @@ function BookmarksStore() { } BookmarksStore.prototype = { _logName: "BStore", + _lookup: null, __bms: null, get _bms() { @@ -850,7 +841,7 @@ BookmarksStore.prototype = { } return null; }, - + _createCommand: function BStore__createCommand(command) { let newId; let parentId = this._getItemIdForGUID(command.data.parentGUID); @@ -1252,6 +1243,7 @@ BookmarksStore.prototype = { this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu"); this._wrap(this._getNode(this._bms.toolbarFolder), items, "toolbar"); this._wrap(this._getNode(this._bms.unfiledBookmarksFolder), items, "unfiled"); + this._lookup = items; return items; }, diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index a1d8c1303483..e9fbdeb08ead 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -54,13 +54,6 @@ CookieEngine.prototype = { get logName() { return "CookieEngine"; }, get serverPrefix() { return "user-data/cookies/"; }, - __core: null, - get _core() { - if (!this.__core) - this.__core = new CookieSyncCore(); - return this.__core; - }, - __store: null, get _store() { if (!this.__store) @@ -68,6 +61,13 @@ CookieEngine.prototype = { return this.__store; }, + __core: null, + get _core() { + if (!this.__core) + this.__core = new CookieSyncCore(this._store); + return this.__core; + }, + __tracker: null, get _tracker() { if (!this.__tracker) @@ -77,61 +77,13 @@ CookieEngine.prototype = { }; CookieEngine.prototype.__proto__ = new Engine(); -function CookieSyncCore() { +function CookieSyncCore(store) { + this._store = store; this._init(); } CookieSyncCore.prototype = { _logName: "CookieSync", - - __cookieManager: null, - get _cookieManager() { - if (!this.__cookieManager) - this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. - getService(Ci.nsICookieManager2); - /* need the 2nd revision of the ICookieManager interface - because it supports add() and the 1st one doesn't. */ - return this.__cookieManager; - }, - - - _itemExists: function CSC__itemExists(GUID) { - /* true if a cookie with the given GUID exists. - The GUID that we are passed should correspond to the keys - that we define in the JSON returned by CookieStore.wrap() - That is, it will be a string of the form - "host:path:name". */ - - /* TODO verify that colons can't normally appear in any of - the fields -- if they did it then we can't rely on .split(":") - to parse correctly.*/ - - let cookieArray = GUID.split( ":" ); - let cookieHost = cookieArray[0]; - let cookiePath = cookieArray[1]; - let cookieName = cookieArray[2]; - - /* alternate implementation would be to instantiate a cookie from - cookieHost, cookiePath, and cookieName, then call - cookieManager.cookieExists(). Maybe that would have better - performance? This implementation seems pretty slow.*/ - let enumerator = this._cookieManager.enumerator; - while (enumerator.hasMoreElements()) - { - let aCookie = enumerator.getNext(); - if (aCookie.host == cookieHost && - aCookie.path == cookiePath && - aCookie.name == cookieName ) { - return true; - } - } - return false; - /* Note: We can't just call cookieManager.cookieExists() with a generic - javascript object with .host, .path, and .name attributes attatched. - cookieExists is implemented in C and does a hard static_cast to an - nsCookie object, so duck typing doesn't work (and in fact makes - Firefox hard-crash as the static_cast returns null and is not checked.) - */ - }, + _store: null, _commandLike: function CSC_commandLike(a, b) { /* Method required to be overridden. @@ -155,7 +107,7 @@ function CookieStore( cookieManagerStub ) { } CookieStore.prototype = { _logName: "CookieStore", - + _lookup: null, // Documentation of the nsICookie interface says: // name ACString The name of the cookie. Read only. @@ -186,7 +138,7 @@ CookieStore.prototype = { // because it supports add() and the 1st one doesn't. return this.__cookieManager; }, - + _createCommand: function CookieStore__createCommand(command) { /* we got a command to create a cookie in the local browser in order to sync with the server. */ @@ -275,39 +227,39 @@ CookieStore.prototype = { /* Return contents of this store, as JSON. A dictionary of cookies where the keys are GUIDs and the values are sub-dictionaries containing all cookie fields. */ - let items = {}; var iter = this._cookieManager.enumerator; - while (iter.hasMoreElements()){ + while (iter.hasMoreElements()) { var cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie )){ - // String used to identify cookies is - // host:path:name - if ( cookie.isSession ) { - /* Skip session-only cookies, sync only persistent cookies. */ - continue; - } + if (cookie.QueryInterface( Ci.nsICookie )) { + // String used to identify cookies is + // host:path:name + if ( cookie.isSession ) { + /* Skip session-only cookies, sync only persistent cookies. */ + continue; + } - let key = cookie.host + ":" + cookie.path + ":" + cookie.name; - items[ key ] = { parentGUID: '', - name: cookie.name, - value: cookie.value, - isDomain: cookie.isDomain, - host: cookie.host, - path: cookie.path, - isSecure: cookie.isSecure, - // nsICookie2 values: - rawHost: cookie.rawHost, - isSession: cookie.isSession, - expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly }; + let key = cookie.host + ":" + cookie.path + ":" + cookie.name; + items[ key ] = { parentGUID: '', + name: cookie.name, + value: cookie.value, + isDomain: cookie.isDomain, + host: cookie.host, + path: cookie.path, + isSecure: cookie.isSecure, + // nsICookie2 values: + rawHost: cookie.rawHost, + isSession: cookie.isSession, + expiry: cookie.expiry, + isHttpOnly: cookie.isHttpOnly }; - /* See http://developer.mozilla.org/en/docs/nsICookie - Note: not syncing "expires", "status", or "policy" - since they're deprecated. */ + /* See http://developer.mozilla.org/en/docs/nsICookie + Note: not syncing "expires", "status", or "policy" + since they're deprecated. */ } } + this._lookup = items; return items; }, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index dc244701b98b..5dd02c1b1199 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -47,58 +47,6 @@ Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -/* - * Generate GUID from a name,value pair. - * If the concatenated length is less than 40, we just Base64 the JSON. - * Otherwise, we Base64 the JSON of the name and SHA1 of the value. - * The first character of the key determines which method we used: - * '0' for full Base64, '1' for SHA-1'ed val. - */ -function _generateFormGUID(nam, val) { - var key; - var con = nam + val; - - var jso = Cc["@mozilla.org/dom/json;1"]. - createInstance(Ci.nsIJSON); - - if (con.length <= 40) { - key = '0' + btoa(jso.encode([nam, val])); - } else { - val = Utils.sha1(val); - key = '1' + btoa(jso.encode([nam, val])); - } - - return key; -} - -/* - * Unwrap a name,value pair from a GUID. - * Return an array [sha1ed, name, value] - * sha1ed is a boolean determining if the value is SHA-1'ed or not. - */ -function _unwrapFormGUID(guid) { - var jso = Cc["@mozilla.org/dom/json;1"]. - createInstance(Ci.nsIJSON); - - var ret; - var dec = atob(guid.slice(1)); - var obj = jso.decode(dec); - - switch (guid[0]) { - case '0': - ret = [false, obj[0], obj[1]]; - break; - case '1': - ret = [true, obj[0], obj[1]]; - break; - default: - this._log.warn("Unexpected GUID header: " + guid[0] + ", aborting!"); - return false; - } - - return ret; -} - function FormEngine(pbeId) { this._init(pbeId); } @@ -107,13 +55,6 @@ FormEngine.prototype = { get logName() { return "FormEngine"; }, get serverPrefix() { return "user-data/forms/"; }, - __core: null, - get _core() { - if (!this.__core) - this.__core = new FormSyncCore(); - return this.__core; - }, - __store: null, get _store() { if (!this.__store) @@ -121,6 +62,13 @@ FormEngine.prototype = { return this.__store; }, + __core: null, + get _core() { + if (!this.__core) + this.__core = new FormSyncCore(this._store); + return this.__core; + }, + __tracker: null, get _tracker() { if (!this.__tracker) @@ -130,43 +78,13 @@ FormEngine.prototype = { }; FormEngine.prototype.__proto__ = new Engine(); -function FormSyncCore() { +function FormSyncCore(store) { + this._store = store; this._init(); } FormSyncCore.prototype = { _logName: "FormSync", - - __formDB: null, - get _formDB() { - if (!this.__formDB) { - var file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("formhistory.sqlite"); - var stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__formDB = stor.openDatabase(file); - } - return this.__formDB; - }, - - _itemExists: function FSC__itemExists(GUID) { - var found = false; - var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); - - /* Same performance restrictions as PasswordSyncCore apply here: - caching required */ - while (stmnt.executeStep()) { - var nam = stmnt.getUTF8String(1); - var val = stmnt.getUTF8String(2); - var key = _generateFormGUID(nam, val); - - if (key == GUID) - found = true; - } - - return found; - }, + _store: null, _commandLike: function FSC_commandLike(a, b) { /* Not required as GUIDs for similar data sets will be the same */ @@ -180,7 +98,8 @@ function FormStore() { } FormStore.prototype = { _logName: "FormStore", - + _lookup: null, + __formDB: null, get _formDB() { if (!this.__formDB) { @@ -203,21 +122,6 @@ FormStore.prototype = { return this.__formHistory; }, - _getValueFromSHA1: function FormStore__getValueFromSHA1(name, sha) { - var query = "SELECT value FROM moz_formhistory WHERE fieldname = '" + name + "'"; - var stmnt = this._formDB.createStatement(query); - var found = false; - - while (stmnt.executeStep()) { - var val = stmnt.getUTF8String(0); - if (Utils.sha1(val) == sha) { - found = val; - break; - } - } - return found; - }, - _createCommand: function FormStore__createCommand(command) { this._log.info("FormStore got createCommand: " + command); this._formHistory.addEntry(command.data.name, command.data.value); @@ -226,23 +130,19 @@ FormStore.prototype = { _removeCommand: function FormStore__removeCommand(command) { this._log.info("FormStore got removeCommand: " + command); - var data = _unwrapFormGUID(command.GUID); - if (!data) { + var data; + if (command.GUID in this._lookup) { + data = this._lookup[command.GUID]; + } else { this._log.warn("Invalid GUID found, ignoring remove request."); return; } - var nam = data[1]; - var val = data[2]; - if (data[0]) { - val = this._getValueFromSHA1(nam, val); - } + var nam = data.name; + var val = data.value; + this._formHistory.removeEntry(nam, val); - if (val) { - this._formHistory.removeEntry(nam, val); - } else { - this._log.warn("Form value not found from GUID, ignoring remove request."); - } + delete this._lookup[command.GUID]; }, _editCommand: function FormStore__editCommand(command) { @@ -251,18 +151,18 @@ FormStore.prototype = { }, wrap: function FormStore_wrap() { - var items = []; + this._lookup = {}; var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); while (stmnt.executeStep()) { var nam = stmnt.getUTF8String(1); var val = stmnt.getUTF8String(2); - var key = _generateFormGUID(nam, val); + var key = Utils.sha1(nam + val); - items[key] = { name: nam, value: val }; + this._lookup[key] = { name: nam, value: val }; } - - return items; + + return this._lookup; }, wipe: function FormStore_wipe() { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 17ee24d7dff0..99899ab1a56c 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -58,13 +58,6 @@ HistoryEngine.prototype = { get logName() { return "HistEngine"; }, get serverPrefix() { return "user-data/history/"; }, - __core: null, - get _core() { - if (!this.__core) - this.__core = new HistorySyncCore(); - return this.__core; - }, - __store: null, get _store() { if (!this.__store) @@ -72,6 +65,13 @@ HistoryEngine.prototype = { return this.__store; }, + __core: null, + get _core() { + if (!this.__core) + this.__core = new HistorySyncCore(this._store); + return this.__core; + }, + __tracker: null, get _tracker() { if (!this.__tracker) @@ -81,16 +81,13 @@ HistoryEngine.prototype = { }; HistoryEngine.prototype.__proto__ = new Engine(); -function HistorySyncCore() { +function HistorySyncCore(store) { + this._store = store; this._init(); } HistorySyncCore.prototype = { _logName: "HistSync", - - _itemExists: function HSC__itemExists(GUID) { - // we don't care about already-existing items; just try to re-add them - return false; - }, + _store: null, _commandLike: function HSC_commandLike(a, b) { // History commands never qualify for likeness. We will always @@ -134,6 +131,11 @@ HistoryStore.prototype = { return this.__hsvc; }, + _itemExists: function HistStore__itemExists(GUID) { + // we don't care about already-existing items; just try to re-add them + return false; + }, + _createCommand: function HistStore__createCommand(command) { this._log.debug(" -> creating history entry: " + command.GUID); try { diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 27e7594c7a09..62e2f35b340c 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -43,27 +43,6 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); -/* - * _hashLoginInfo - * - * nsILoginInfo objects don't have a unique GUID, so we need to generate one - * on the fly. This is done by taking a hash of every field in the object. - * Note that the resulting GUID could potentiually reveal passwords via - * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by - * anyone, so this should generally be safe. - */ -function _hashLoginInfo(aLogin) { - var loginKey = aLogin.hostname + ":" + - aLogin.formSubmitURL + ":" + - aLogin.httpRealm + ":" + - aLogin.username + ":" + - aLogin.password + ":" + - aLogin.usernameField + ":" + - aLogin.passwordField; - - return Utils.sha1(loginKey); -} - function PasswordEngine() { this._init(); } @@ -72,50 +51,29 @@ PasswordEngine.prototype = { get logName() { return "PasswordEngine"; }, get serverPrefix() { return "user-data/passwords/"; }, - __core: null, - get _core() { - if (!this.__core) - this.__core = new PasswordSyncCore(); - return this.__core; - }, - __store: null, get _store() { if (!this.__store) this.__store = new PasswordStore(); return this.__store; + }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new PasswordSyncCore(this._store); + return this.__core; } }; PasswordEngine.prototype.__proto__ = new Engine(); -function PasswordSyncCore() { +function PasswordSyncCore(store) { + this._store = store; this._init(); } PasswordSyncCore.prototype = { _logName: "PasswordSync", - - __loginManager : null, - get _loginManager() { - if (!this.__loginManager) - this.__loginManager = Utils.getLoginManager(); - return this.__loginManager; - }, - - _itemExists: function PSC__itemExists(GUID) { - var found = false; - var logins = this._loginManager.getAllLogins({}); - - // XXX It would be more efficient to compute all the hashes in one shot, - // cache the results, and check the cache here. That would need to happen - // once per sync -- not sure how to invalidate cache after current sync? - for (var i = 0; i < logins.length && !found; i++) { - var hash = _hashLoginInfo(logins[i]); - if (hash == GUID) - found = true;; - } - - return found; - }, + _store: null, _commandLike: function PSC_commandLike(a, b) { // Not used. @@ -129,20 +87,42 @@ function PasswordStore() { } PasswordStore.prototype = { _logName: "PasswordStore", + _lookup: null, - __loginManager : null, + __loginManager: null, get _loginManager() { if (!this.__loginManager) this.__loginManager = Utils.getLoginManager(); return this.__loginManager; }, - __nsLoginInfo : null, + __nsLoginInfo: null, get _nsLoginInfo() { if (!this.__nsLoginInfo) this.__nsLoginInfo = Utils.makeNewLoginInfo(); return this.__nsLoginInfo; }, + + /* + * _hashLoginInfo + * + * nsILoginInfo objects don't have a unique GUID, so we need to generate one + * on the fly. This is done by taking a hash of every field in the object. + * Note that the resulting GUID could potentiually reveal passwords via + * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by + * anyone, so this should generally be safe. + */ + _hashLoginInfo: function PasswordStore__hashLoginInfo(aLogin) { + var loginKey = aLogin.hostname + ":" + + aLogin.formSubmitURL + ":" + + aLogin.httpRealm + ":" + + aLogin.username + ":" + + aLogin.password + ":" + + aLogin.usernameField + ":" + + aLogin.passwordField; + + return Utils.sha1(loginKey); + }, _createCommand: function PasswordStore__createCommand(command) { this._log.info("PasswordStore got createCommand: " + command ); @@ -180,13 +160,12 @@ PasswordStore.prototype = { wrap: function PasswordStore_wrap() { /* Return contents of this store, as JSON. */ var items = {}; - var logins = this._loginManager.getAllLogins({}); for (var i = 0; i < logins.length; i++) { var login = logins[i]; - var key = _hashLoginInfo(login); + var key = this._hashLoginInfo(login); items[key] = { hostname : login.hostname, formSubmitURL : login.formSubmitURL, @@ -197,6 +176,7 @@ PasswordStore.prototype = { passwordField : login.passwordField }; } + this._lookup = items; return items; }, diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 7d440e5b76c2..d1147981fb9b 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -62,17 +62,17 @@ TabEngine.prototype = { get serverPrefix() "user-data/tabs/", get store() this._store, - get _core() { - let core = new TabSyncCore(this); - this.__defineGetter__("_core", function() core); - return this._core; - }, - get _store() { let store = new TabStore(); this.__defineGetter__("_store", function() store); return this._store; }, + + get _core() { + let core = new TabSyncCore(this._store); + this.__defineGetter__("_core", function() core); + return this._core; + }, get _tracker() { let tracker = new TabTracker(this); @@ -82,16 +82,15 @@ TabEngine.prototype = { }; -function TabSyncCore(engine) { - this._engine = engine; +function TabSyncCore(store) { + this._store = store; this._init(); } TabSyncCore.prototype = { __proto__: new SyncCore(), _logName: "TabSync", - - _engine: null, + _store: null, get _sessionStore() { let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. @@ -100,27 +99,6 @@ TabSyncCore.prototype = { return this._sessionStore; }, - _itemExists: function TSC__itemExists(GUID) { - // Note: this method returns true if the tab exists in any window, not just - // the window from which the tab came. In the future, if we care about - // windows, we might need to make this more specific, although in that case - // we'll have to identify tabs by something other than URL, since even - // window-specific tabs look the same when identified by URL. - - // Get the set of all real and virtual tabs. - let tabs = this._engine.store.wrap(); - - // XXX Should we convert both to nsIURIs and then use nsIURI::equals - // to compare them? - if (GUID in tabs) { - this._log.trace("_itemExists: " + GUID + " exists"); - return true; - } - - this._log.trace("_itemExists: " + GUID + " doesn't exist"); - return false; - }, - _commandLike: function TSC_commandLike(a, b) { // Not implemented. return false; @@ -263,6 +241,27 @@ TabStore.prototype = { self.done(); }, + _itemExists: function TabStore__itemExists(GUID) { + // Note: this method returns true if the tab exists in any window, not just + // the window from which the tab came. In the future, if we care about + // windows, we might need to make this more specific, although in that case + // we'll have to identify tabs by something other than URL, since even + // window-specific tabs look the same when identified by URL. + + // Get the set of all real and virtual tabs. + let tabs = this.wrap(); + + // XXX Should we convert both to nsIURIs and then use nsIURI::equals + // to compare them? + if (GUID in tabs) { + this._log.trace("_itemExists: " + GUID + " exists"); + return true; + } + + this._log.trace("_itemExists: " + GUID + " doesn't exist"); + return false; + }, + _createCommand: function TabStore__createCommand(command) { this._log.debug("_createCommand: " + command.GUID); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index a4a0954e9124..d7a8760f129c 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -62,6 +62,9 @@ Store.prototype = { _logName: "Store", _yieldDuringApply: true, + // set this property in child object's wrap()! + _lookup: null, + __json: null, get _json() { if (!this.__json) @@ -102,10 +105,28 @@ Store.prototype = { self.done(); }, + // override only if neccessary + _itemExists: function Store__itemExists(GUID) { + if (GUID in this._lookup) + return true; + else + return false; + }, + // override these in derived objects - wrap: function Store_wrap() {}, - wipe: function Store_wipe() {}, - resetGUIDs: function Store_resetGUIDs() {} + + // wrap MUST save the wrapped store in the _lookup property! + wrap: function Store_wrap() { + throw "wrap needs to be subclassed"; + }, + + wipe: function Store_wipe() { + throw "wipe needs to be subclassed"; + }, + + resetGUIDs: function Store_resetGUIDs() { + throw "resetGUIDs needs to be subclassed"; + } }; function SnapshotStore(name) { diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 26e14d5200ea..b9b3c0399bdc 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -62,7 +62,10 @@ function SyncCore() { } SyncCore.prototype = { _logName: "Sync", - + + // Set this property in child objects! + _store: null, + _init: function SC__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); }, @@ -209,11 +212,6 @@ SyncCore.prototype = { } }, - _itemExists: function SC__itemExists(GUID) { - this._log.error("itemExists needs to be subclassed"); - return false; - }, - _reconcile: function SC__reconcile(listA, listB) { let self = yield; @@ -253,7 +251,7 @@ SyncCore.prototype = { } // watch out for create commands with GUIDs that already exist - if (b.action == "create" && this._itemExists(b.GUID)) { + if (b.action == "create" && this._store._itemExists(b.GUID)) { this._log.error("Remote command has GUID that already exists " + "locally. Dropping command."); return false; // delete b From a3b432587ed4cb3083a7f21617f13192d1ed8fd9 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Sat, 28 Jun 2008 01:13:14 -0700 Subject: [PATCH 0515/1860] Make PasswordStore process removeCommands correctly (bug 442090, r=thunder) --- services/sync/modules/engines/passwords.js | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 62e2f35b340c..d2d39e5be315 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -140,16 +140,20 @@ PasswordStore.prototype = { _removeCommand: function PasswordStore__removeCommand(command) { this._log.info("PasswordStore got removeCommand: " + command ); - - var login = new this._nsLoginInfo(command.data.hostname, - command.data.formSubmitURL, - command.data.httpRealm, - command.data.username, - command.data.password, - command.data.usernameField, - command.data.passwordField); - - this._loginManager.removeLogin(login); + + if (command.GUID in this._lookup) { + var data = this._lookup[command.GUID]; + var login = new this._nsLoginInfo(data.hostname, + data.formSubmitURL, + data.httpRealm, + data.username, + data.password, + data.usernameField, + data.passwordField); + this._loginManager.removeLogin(login); + } else { + this._log.warn("Invalid GUID for remove, ignoring request!"); + } }, _editCommand: function PasswordStore__editCommand(command) { From f3e1be8b1d8149d3f8843b776d975aef0c316397 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 29 Jun 2008 07:00:53 -0700 Subject: [PATCH 0516/1860] make resetguids async --- services/sync/modules/engines.js | 2 +- services/sync/modules/engines/bookmarks.js | 46 +++++++++++++--------- services/sync/modules/engines/cookies.js | 3 +- services/sync/modules/engines/forms.js | 23 +++++------ services/sync/modules/engines/passwords.js | 3 +- services/sync/modules/engines/tabs.js | 3 +- services/sync/modules/service.js | 15 +++---- services/sync/modules/stores.js | 5 ++- 8 files changed, 56 insertions(+), 44 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e25c5b2e8133..1d6ab919c45a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -251,7 +251,7 @@ Engine.prototype = { "Forcing initial sync."); this._log.trace("Remote: " + this._remote.status.data.GUID); this._log.trace("Local: " + this._snapshot.GUID); - this._store.resetGUIDs(); + yield this._store.resetGUIDs(self.cb); this._snapshot.data = {}; this._snapshot.version = -1; this._snapshot.GUID = this._remote.status.data.GUID; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c7f5bd3390e4..2e92364e883b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -1219,20 +1219,6 @@ BookmarksStore.prototype = { return ret; }, - _resetGUIDs: function BSS__resetGUIDs(node) { - if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) - this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); - - if (node.type == node.RESULT_TYPE_FOLDER && - !this._ls.isLivemark(node.itemId)) { - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this._resetGUIDs(node.getChild(i)); - } - } - }, - findIncomingShares: function BStore_findIncomingShares() { /* Returns list of mount data structures, each of which represents one incoming shared-bookmark folder. */ @@ -1261,10 +1247,34 @@ BookmarksStore.prototype = { this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder); }, - resetGUIDs: function BStore_resetGUIDs() { - this._resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder)); - this._resetGUIDs(this._getNode(this._bms.toolbarFolder)); - this._resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); + __resetGUIDs: function BStore___resetGUIDs(node) { + let self = yield; + + if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) + this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); + + if (node.type == node.RESULT_TYPE_FOLDER && + !this._ls.isLivemark(node.itemId)) { + yield Utils.makeTimerForCall(self.cb); // Yield to main loop + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + this.__resetGUIDs(node.getChild(i)); + } + } + }, + + _resetGUIDs: function BStore__resetGUIDs() { + let self = yield; + this._bms.runInBatchMode({ + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryBatchCallback, + Ci.nsISupports]), + runBatched: function BStore_resetGUIDs_cb(userData) { + this.__resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder)); + this.__resetGUIDs(this._getNode(this._bms.toolbarFolder)); + this.__resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); + } + }); } }; BookmarksStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index a1d8c1303483..41ee6d42ba54 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -319,7 +319,8 @@ CookieStore.prototype = { this._cookieManager.removeAll(); }, - resetGUIDs: function CookieStore_resetGUIDs() { + _resetGUIDs: function CookieStore__resetGUIDs() { + let self = yield; /* called in the case where remote/local sync GUIDs do not match. We do need to override this, but since we're deriving GUIDs from the cookie data itself and not generating them, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index dc244701b98b..c7113cae787a 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -57,7 +57,7 @@ Cu.import("resource://weave/trackers.js"); function _generateFormGUID(nam, val) { var key; var con = nam + val; - + var jso = Cc["@mozilla.org/dom/json;1"]. createInstance(Ci.nsIJSON); @@ -67,7 +67,7 @@ function _generateFormGUID(nam, val) { val = Utils.sha1(val); key = '1' + btoa(jso.encode([nam, val])); } - + return key; } @@ -79,11 +79,11 @@ function _generateFormGUID(nam, val) { function _unwrapFormGUID(guid) { var jso = Cc["@mozilla.org/dom/json;1"]. createInstance(Ci.nsIJSON); - + var ret; var dec = atob(guid.slice(1)); var obj = jso.decode(dec); - + switch (guid[0]) { case '0': ret = [false, obj[0], obj[1]]; @@ -95,7 +95,7 @@ function _unwrapFormGUID(guid) { this._log.warn("Unexpected GUID header: " + guid[0] + ", aborting!"); return false; } - + return ret; } @@ -207,7 +207,7 @@ FormStore.prototype = { var query = "SELECT value FROM moz_formhistory WHERE fieldname = '" + name + "'"; var stmnt = this._formDB.createStatement(query); var found = false; - + while (stmnt.executeStep()) { var val = stmnt.getUTF8String(0); if (Utils.sha1(val) == sha) { @@ -217,7 +217,7 @@ FormStore.prototype = { } return found; }, - + _createCommand: function FormStore__createCommand(command) { this._log.info("FormStore got createCommand: " + command); this._formHistory.addEntry(command.data.name, command.data.value); @@ -225,19 +225,19 @@ FormStore.prototype = { _removeCommand: function FormStore__removeCommand(command) { this._log.info("FormStore got removeCommand: " + command); - + var data = _unwrapFormGUID(command.GUID); if (!data) { this._log.warn("Invalid GUID found, ignoring remove request."); return; } - + var nam = data[1]; var val = data[2]; if (data[0]) { val = this._getValueFromSHA1(nam, val); } - + if (val) { this._formHistory.removeEntry(nam, val); } else { @@ -269,7 +269,8 @@ FormStore.prototype = { this._formHistory.removeAllEntries(); }, - resetGUIDs: function FormStore_resetGUIDs() { + _resetGUIDs: function FormStore__resetGUIDs() { + let self = yield; // Not needed. } }; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 27e7594c7a09..c0bd74b3a6e1 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -204,7 +204,8 @@ PasswordStore.prototype = { this._loginManager.removeAllLogins(); }, - resetGUIDs: function PasswordStore_resetGUIDs() { + _resetGUIDs: function PasswordStore__resetGUIDs() { + let self = yield; // Not needed. } }; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 7d440e5b76c2..043e581c06c9 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -419,7 +419,8 @@ TabStore.prototype = { this._saveVirtualTabs(); }, - resetGUIDs: function TabStore_resetGUIDs() { + _resetGUIDs: function TabStore__resetGUIDs() { + let self = yield; // Not needed. } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d58ed458b4e6..9dce8d56930d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -607,19 +607,15 @@ WeaveSvc.prototype = { if (!this._loggedIn) throw "Can't sync: Not logged in"; - this._versionCheck.async(this, self.cb); - yield; - - this._getKeypair.async(this, self.cb); - yield; + yield this._versionCheck.async(this, self.cb); + yield this._getKeypair.async(this, self.cb); let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { if (!engines[i].enabled) continue; - this._notify(engines[i].name + "-engine:sync", - this._syncEngine, engines[i]).async(this, self.cb); - yield; + yield this._notify(engines[i].name + "-engine:sync", + this._syncEngine, engines[i]).async(this, self.cb); } }, @@ -685,8 +681,7 @@ WeaveSvc.prototype = { _syncEngine: function WeaveSvc__syncEngine(engine) { let self = yield; try { - engine.sync(self.cb); - yield; + yield engine.sync(self.cb); engine._tracker.resetScore(); } catch(e) { this._log.error(Utils.exceptionStr(e)); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index a4a0954e9124..097aa3818780 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -105,7 +105,10 @@ Store.prototype = { // override these in derived objects wrap: function Store_wrap() {}, wipe: function Store_wipe() {}, - resetGUIDs: function Store_resetGUIDs() {} + _resetGUIDs: function Store__resetGUIDs() { let self = yield; }, + resetGUIDs: function Store_resetGUIDs(onComplete) { + this._resetGUIDs.async(this, onComplete); + } }; function SnapshotStore(name) { From 85dfbdd6fb77914d5921631eab71f437a43e5160 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 29 Jun 2008 11:44:27 -0700 Subject: [PATCH 0517/1860] fix cooe engine's resetGUIDs method by importing async module; don't call runInBatchMode in bookmark engine's resetGUIDs, it doesn't work atm --- services/sync/modules/engines/bookmarks.js | 15 +++++---------- services/sync/modules/engines/cookies.js | 5 ++++- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index b97e357832b7..56764f49360d 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -52,6 +52,7 @@ const SHARED_BOOKMARK_FILE_NAME = "shared_bookmarks"; const INCOMING_SHARE_ROOT_ANNO = "weave/mounted-shares-folder"; const INCOMING_SHARE_ROOT_NAME = "Shared Folders"; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/util.js"); @@ -841,7 +842,7 @@ BookmarksStore.prototype = { } return null; }, - + _createCommand: function BStore__createCommand(command) { let newId; let parentId = this._getItemIdForGUID(command.data.parentGUID); @@ -1258,15 +1259,9 @@ BookmarksStore.prototype = { _resetGUIDs: function BStore__resetGUIDs() { let self = yield; - this._bms.runInBatchMode({ - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryBatchCallback, - Ci.nsISupports]), - runBatched: function BStore_resetGUIDs_cb(userData) { - this.__resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder)); - this.__resetGUIDs(this._getNode(this._bms.toolbarFolder)); - this.__resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); - } - }); + this.__resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder)); + this.__resetGUIDs(this._getNode(this._bms.toolbarFolder)); + this.__resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); } }; BookmarksStore.prototype.__proto__ = new Store(); diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index d713ab7e009f..f64b765bebdb 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -41,11 +41,14 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Function.prototype.async = Async.sugar; + function CookieEngine(pbeId) { this._init(pbeId); } @@ -138,7 +141,7 @@ CookieStore.prototype = { // because it supports add() and the 1st one doesn't. return this.__cookieManager; }, - + _createCommand: function CookieStore__createCommand(command) { /* we got a command to create a cookie in the local browser in order to sync with the server. */ From 5de3f5334e725c4aca9246b9190af61b7446e16d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 29 Jun 2008 11:54:26 -0700 Subject: [PATCH 0518/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 38688ffeeb04..2c6e67c3f3de 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.32"; +const WEAVE_VERSION = "0.1.33"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From fa8ac795e2e42ac15700537f1aa3a1c586045d1c Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Sun, 29 Jun 2008 16:46:59 -0700 Subject: [PATCH 0519/1860] Partial bustage fix: test was broken, is now less broken but still fails. --- services/sync/tests/unit/test_passwords.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index ee9ca4fda7b3..3a1e7b94381f 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -6,13 +6,15 @@ var loginMgr = new FakeLoginManager(fakeSampleLogins); var passwords = loadInSandbox("resource://weave/engines/passwords.js"); function test_hashLoginInfo_works() { - var fakeUserHash = passwords._hashLoginInfo(fakeSampleLogins[0]); + var pwStore = new passwords.PasswordStore(); + var fakeUserHash = pwStore._hashLoginInfo(fakeSampleLogins[0]); do_check_eq(typeof fakeUserHash, 'string'); do_check_eq(fakeUserHash.length, 40); } function test_synccore_itemexists_works() { - var fakeUserHash = passwords._hashLoginInfo(fakeSampleLogins[0]); + var pwStore = new passwords.PasswordStore(); + var fakeUserHash = pwStore._hashLoginInfo(fakeSampleLogins[0]); var psc = new passwords.PasswordSyncCore(); do_check_false(psc._itemExists("invalid guid")); do_check_true(psc._itemExists(fakeUserHash)); From 89825385056c6230df96c0b519b24db2dc65d48d Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Sun, 29 Jun 2008 16:58:10 -0700 Subject: [PATCH 0520/1860] =?UTF-8?q?Bug=20442257=20=E2=80=93=20Weave=201.?= =?UTF-8?q?32:=20WeaveCrypto=20doesn't=20work=20under=20Linux.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/crypto/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index bebd6e46d3ee..69dd1574db44 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -130,7 +130,7 @@ headers = -I$(sdkdir)/include \ libdirs := $(sdkdir)/lib $(sdkdir)/bin libs := xpcomglue_s xpcom nspr4 \ - crmf smime3 ssl3 nss3 nssutil3 softokn3 \ + crmf smime3 ssl3 nss3 nssutil3 \ plds4 plc4 ifeq ($(os), linux) From 09f877aca8a80414c713a44ef39569c921a769a0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 29 Jun 2008 17:36:13 -0700 Subject: [PATCH 0521/1860] use this._lookup instead of command.data for remove commands --- services/sync/modules/engines/cookies.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index f64b765bebdb..4681f506c453 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -173,10 +173,10 @@ CookieStore.prototype = { http://developer.mozilla.org/en/docs/nsICookieManager the last argument is "always block cookies from this domain?" and the answer is "no". */ - this._cookieManager.remove( command.data.host, - command.data.name, - command.data.path, - false ); + this._cookieManager.remove(this._lookup[command.GUID].host, + this._lookup[command.GUID].name, + this._lookup[command.GUID].path, + false ); }, _editCommand: function CookieStore__editCommand(command) { From 555aec31fb82a5758efd996b7f0aa87d37b1a722 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 29 Jun 2008 17:36:35 -0700 Subject: [PATCH 0522/1860] import async module --- services/sync/modules/engines/forms.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 666ff99107df..eab81b71d789 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -41,12 +41,15 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/async.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Function.prototype.async = Async.sugar; + function FormEngine(pbeId) { this._init(pbeId); } @@ -99,7 +102,7 @@ function FormStore() { FormStore.prototype = { _logName: "FormStore", _lookup: null, - + __formDB: null, get _formDB() { if (!this.__formDB) { @@ -129,7 +132,7 @@ FormStore.prototype = { _removeCommand: function FormStore__removeCommand(command) { this._log.info("FormStore got removeCommand: " + command); - + var data; if (command.GUID in this._lookup) { data = this._lookup[command.GUID]; @@ -137,11 +140,11 @@ FormStore.prototype = { this._log.warn("Invalid GUID found, ignoring remove request."); return; } - + var nam = data.name; var val = data.value; this._formHistory.removeEntry(nam, val); - + delete this._lookup[command.GUID]; }, @@ -161,7 +164,7 @@ FormStore.prototype = { this._lookup[key] = { name: nam, value: val }; } - + return this._lookup; }, From 964458550a0576d62e25ecb8790d45065f42d50c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 29 Jun 2008 17:36:59 -0700 Subject: [PATCH 0523/1860] define _resetGUIDs in store --- services/sync/modules/engines/history.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 99899ab1a56c..56b73c7e044c 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -195,6 +195,11 @@ HistoryStore.prototype = { wipe: function HistStore_wipe() { this._hsvc.removeAllPages(); + }, + + _resetGUIDs: function FormStore__resetGUIDs() { + let self = yield; + // Not needed. } }; HistoryStore.prototype.__proto__ = new Store(); From 3acaf55ad2ba749eaa7f826f11c9ba6e4183e9c3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 29 Jun 2008 17:37:11 -0700 Subject: [PATCH 0524/1860] import async module --- services/sync/modules/engines/passwords.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 7bf4e4331b07..a2c3d1df4f3f 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -39,10 +39,13 @@ const EXPORTED_SYMBOLS = ['PasswordEngine']; const Cu = Components.utils; Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); +Function.prototype.async = Async.sugar; + function PasswordEngine() { this._init(); } @@ -57,7 +60,7 @@ PasswordEngine.prototype = { this.__store = new PasswordStore(); return this.__store; }, - + __core: null, get _core() { if (!this.__core) @@ -102,7 +105,7 @@ PasswordStore.prototype = { this.__nsLoginInfo = Utils.makeNewLoginInfo(); return this.__nsLoginInfo; }, - + /* * _hashLoginInfo * @@ -140,7 +143,7 @@ PasswordStore.prototype = { _removeCommand: function PasswordStore__removeCommand(command) { this._log.info("PasswordStore got removeCommand: " + command ); - + if (command.GUID in this._lookup) { var data = this._lookup[command.GUID]; var login = new this._nsLoginInfo(data.hostname, From 6457b9c445776e938dc0f35171d22f2a3c06f256 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 29 Jun 2008 17:40:49 -0700 Subject: [PATCH 0525/1860] bump version --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 2c6e67c3f3de..c030dc207a15 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.33"; +const WEAVE_VERSION = "0.1.34"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From fb454607a11010c049f23492e9401c6354f6b744 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Sun, 29 Jun 2008 20:51:22 -0700 Subject: [PATCH 0526/1860] Fix crypto component to not mangle certain strings when doing 8/16 bit character conversion. --- services/crypto/IWeaveCrypto.idl | 6 +++--- services/sync/tests/unit/test_crypto_crypt.js | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/services/crypto/IWeaveCrypto.idl b/services/crypto/IWeaveCrypto.idl index 4c4f5da1cdd4..611f4011d67c 100644 --- a/services/crypto/IWeaveCrypto.idl +++ b/services/crypto/IWeaveCrypto.idl @@ -40,7 +40,7 @@ #include "nsISupports.idl" -[scriptable, uuid(d3b0f750-c976-46d0-be20-96b24f4684bc)] +[scriptable, uuid(f4463043-315e-41f3-b779-82e900e6fffa)] interface IWeaveCrypto : nsISupports { /** @@ -80,7 +80,7 @@ interface IWeaveCrypto : nsISupports * A base64-encoded initialization vector * @returns Encrypted data, base64 encoded */ - ACString encrypt(in ACString clearText, + ACString encrypt(in AUTF8String clearText, in ACString symmetricKey, in ACString iv); /** @@ -95,7 +95,7 @@ interface IWeaveCrypto : nsISupports * A base64-encoded initialization vector * @returns Decrypted data (not base64-encoded) */ - ACString decrypt(in ACString cipherText, + AUTF8String decrypt(in ACString cipherText, in ACString symmetricKey, in ACString iv); /** diff --git a/services/sync/tests/unit/test_crypto_crypt.js b/services/sync/tests/unit/test_crypto_crypt.js index 32fd708f1497..6ea0af0574f6 100644 --- a/services/sync/tests/unit/test_crypto_crypt.js +++ b/services/sync/tests/unit/test_crypto_crypt.js @@ -58,6 +58,14 @@ function run_test() { do_check_eq(cipherText, "HeC7miVGDcpxae9RmiIKAw=="); do_check_eq(clearText, mySecret); + // Test non-ascii input + // ("testuser1" using similar-looking glyphs) + mySecret = String.fromCharCode(355, 277, 349, 357, 533, 537, 101, 345, 185); + cipherText = cryptoSvc.encrypt(mySecret, key, iv); + clearText = cryptoSvc.decrypt(cipherText, key, iv); + do_check_eq(cipherText, "Pj4ixByXoH3SU3JkOXaEKPgwRAWplAWFLQZkpJd5Kr4="); + do_check_eq(clearText, mySecret); + // Tests input spanning a block boundary (AES block size is 16 bytes) mySecret = "123456789012345"; cipherText = cryptoSvc.encrypt(mySecret, key, iv); From 6c16ae6635e87895e9550c42062d2be8608d53c2 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 11:18:16 -0700 Subject: [PATCH 0527/1860] Added a test_passphrase_checking test suite to test passphrase checking code; it's not yet complete. --- .../tests/unit/test_passphrase_checking.js | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 services/sync/tests/unit/test_passphrase_checking.js diff --git a/services/sync/tests/unit/test_passphrase_checking.js b/services/sync/tests/unit/test_passphrase_checking.js new file mode 100644 index 000000000000..cbda8d4cc3af --- /dev/null +++ b/services/sync/tests/unit/test_passphrase_checking.js @@ -0,0 +1,87 @@ +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/crypto.js"); + +let __fakePrefs = { + "log.logger.async" : "Debug", + "username" : "foo", + "serverURL" : "https://example.com/", + "encryption" : "aes-256-cbc", + "enabled" : true, + "schedule" : 0 +}; + +let __fakeDAVContents = { + "meta/version" : "3", + // Note that this private key is encrypted with the passphrase + // "passhrase", provided by our testing infrastructure. + "private/privkey" : '{"version":1,"algorithm":"RSA","privkey":"3ytj94K6Wo0mBjAVsiIwjm5x2+ENvpKTDUqLCz19iXbESf8RT6O8PmY7Pqcndpn+adqaQdvmr0T1JQ5bfLEHev0WBfo8oWJb+OS4rKoCWxDNzGwrOlW5hCfxSekw0KrKjqZyDZ0hT1Qt9vn6thlV2v9YWfmyn0OIxNC9hUqGwU3Wb2F2ejM0Tw40+IIW4eLEvFxLGv0vnEXpZvesPt413proL6FGQJe6vyapBg+sdX1JMYGaKZY84PUGIiDPxTbQg7yIWTSe3WlDhJ001khFiyEoTZvPhiAGXfML9ycrCRZUWkHp/cfS7QiusJXs6co0tLjrIk/rTk8h4mHBnyPkFIxh4YrfC7Bwf9npwomhaZCEQ32VK+a8grTDsGYHPZexDm3TcD2+d+hZ/u4lUOHFscQKX4w83tq942yqFtElCD2yQoqEDr1Z9zge5XBnLcYiH9hL0ozfpxBlTtpR1kSH663JHqlYim0qhuk0zrGAPkHna07UMFufxvgQBSd/YUqWCimJFGi+5QeOOFO20Skj882Bh1QDYsmbxZ/JED5ocGNHWSqpaOL2ML1F9nD5rdtffI0BsTe+j9h+HV4GlvzUz0Jd6RRf9xN4RyxqfENb8iGH5Pwbry7Qyk16rfm0s6JgG8pNb/8quKD+87RAtQFybZtdQ9NfGg+gyRiU9pbb6FPuPnGp+KpktaHu/K3HnomrVUoyLQALfCSbPXg2D9ta6dRV0JRqOZz4w52hlHIa62iJO6QecbdBzPYGT0QfOy/vp6ndRDR+2xMD/BmlaQwm3+58cqhIw9SVV5h/Z5PVaXxAOqg5vpU1NjrbF4uIFo5rmR0PyA/6qtxZaBY6w3I4sUWdDkIer8QsyrFrO7MIEdxksvDoFIeIM5eN8BufLu3ymS5ZXBiFr/iRxlYcQVHK2hz0/7syWUYsrz5/l1mj+qbWGx+6daWOk3xt4SH+p0hUpMC4FbJ9m/xr4im+X5m5ZYiajaF1QPOXTTny2plE0vfqMVlwX1HFFTJrAP+E85sZI8LPHAYO80qhSi3tV/LHjxCnC1LHJXaRkG202pQFWF1yVT/o82HBt9OC1xY6TVcy4Uh+6piNIQ9FxXGWrzjz0AUkxwkSN3Foqlfiq+mqJmNwzIdEQTmNAcBBsN3vWngU4elHjYI5qFZBzxJIkH8tfvivOshrOZIZB9TD9GIRhQwIBWc6i4fnqE9GUK2Jle6werdFATiMU4msQg7ClURaMn/p3MOLoxTmsPd1iBYPQkqnJgEAdNfKj3KRqSc6M/x09hGDSzK2d9Y03pyDGPh2sopcMFdCQbMy8VOld2/hEGakMJv6BPoRfhKmJbgGVf5x4B9dWZNa8WCmlcxaZ7KG43UA0zLm1VgfTcDW5qazDFoxIcfhmO5KoRI3q8vNs+Wh+smLC6yFODdF9HzrPimEYSc6OWHWgUcuiIBRjKeo5gBTbExWmri2VG+cn005vQNxK+0s7JVyFB8TzZ96pV3nFjkYy9OUkaiJxGd6OVGcvhbbrcNsKkaZff7OsLqczf6x0bhwh+y8+bLjLkuusGYUdBvdeiuv12IfeRupvwD8Z3aZOgcD7d+8VTyTyd/KX9fu8P7tD5SojJ5joRPjcv4Q8/mhRgtwx1McMIL3YnKHG+U=","privkeyIV":"fZ7CB/KQAUjEhkmrEkns4Q==","privkeySalt":"JFs5h2RKX9m0Op9DlQIcCOSfOH1MuDrrrHxCx+CpCUU="}', + "public/pubkey" : '{"version":1,"algorithm":"RSA","pubkey":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxxwObnXIoYeQKMG9RbvLkgsu/idDo25qOX87jIEiXkgW1wLKp/1D/DBLUEW303tVszNGVt6bTyAXOIj6skpmYoDs9Z48kvU3+g7Vi4QXEw5moSS4fr+yFpKiYd2Kx1+jCFGvGZjBzAAvnjsWmWrSA+LHJSrFlKY6SM3kNg8KrE8dxUi3wztlZnhZgo1ZYe7/VeBOXUfThtoadIl1VdREw2e79eiMQpPa0XLv4grCaMd/wLRs0be1/nPt7li4NyT0fnYFWg75SU3ni/xSaq/zR4NmW/of5vB2EcKyUG+/mvNplQ0CX+v3hRBCdhpCyPmcbHKUluyKzj7Ms9pKyCkwxwIDAQAB"}' +}; + +let Service = loadInSandbox("resource://weave/service.js"); + +function TestService() { + this.__superclassConstructor = Service.WeaveSvc; + this.__superclassConstructor([]); +} + +TestService.prototype = { + _initLogs: function TS__initLogs() { + this._log = Log4Moz.Service.getLogger("Service.Main"); + } +}; +TestService.prototype.__proto__ = Service.WeaveSvc.prototype; + +Cu.import("resource://weave/identity.js"); +Function.prototype.async = Async.sugar; + +function checkpassphrase_coroutine() { + var self = yield; + + var idRSA = ID.get("WeaveCryptoID"); + + //Uncomment this line to cause things to go amiss. + //idRSA.setTempPassword("badpassphrase"); + + var idTemp = new Identity("temp", "temp", null); + // Generate a random symmetric key. + Crypto.randomKeyGen.async(Crypto, self.cb, idTemp); + yield; + + // Encrypt the symmetric key with the user's public key. + Crypto.wrapKey.async(Crypto, self.cb, idTemp.bulkKey, idRSA); + let wrappedKey = yield; + + // Decrypt the symmetric key with the user's private key. + Crypto.unwrapKey.async(Crypto, self.cb, wrappedKey, idRSA); + let unwrappedKey = yield; + + // Ensure that the original symmetric key is identical to + // the decrypted version. + do_check_eq(unwrappedKey, idTemp.bulkKey); + + self.done(true); +} + +function test_passphrase_checking() { + var syncTesting = new SyncTestingInfrastructure(); + + syncTesting.fakeDAVService.fakeContents = __fakeDAVContents; + for (name in __fakePrefs) + syncTesting.fakePrefService.fakeContents[name] = __fakePrefs[name]; + + var testService = new TestService(); + + function login(cb) { + testService.login(cb); + } + + syncTesting.runAsyncFunc("Logging in", login); + + function checkpassphrase(cb) { + checkpassphrase_coroutine.async({}, cb); + } + + syncTesting.runAsyncFunc("Checking passphrase", checkpassphrase); +} From a604acef43fb20b912a61ec89dff3bf63c7b7bbf Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 30 Jun 2008 11:19:07 -0700 Subject: [PATCH 0528/1860] Primitive password tracking support (bug 435320, r=thunder) --- services/sync/modules/engines/passwords.js | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index a2c3d1df4f3f..792e5abeddce 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -198,3 +198,48 @@ PasswordStore.prototype = { }; PasswordStore.prototype.__proto__ = new Store(); +function PasswordTracker() { + this._init(); +} +PasswordTracker.prototype = { + _logName: "PasswordTracker", + + __loginManager : null, + get _loginManager() { + if (!this.__loginManager) + this.__loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + return this.__loginManager; + }, + + /* We use nsILoginManager's countLogins() method, as it is + * the only method that doesn't require the user to enter + * a master password, but still gives us an idea of how much + * info has changed. + * + * FIXME: This does not track edits of passwords, so we + * artificially add 30 to every score. We should move to + * using the LoginManager shim at some point. + * + * Each addition/removal is worth 15 points. + */ + _loginCount: 0, + get score() { + var count = this._loginManager.countLogins("", "", ""); + var score = Math.abs(this._loginCount - count) * 15; + + if (score >= 100) + return 100; + else + return score + 30; + }, + + resetScore: function PasswordTracker_resetScore() { + this._loginCount = this._loginManager.countLogins("", "", ""); + }, + + _init: function PasswordTracker__init() { + this._log = Log4Moz.Service.getLogger("Service." this._logName); + this._loginCount = this._loginManager.countLogins("", "", ""); + } +} From 620dd6414acf5723e7d8587e4f7e0313a498e0d2 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 11:24:58 -0700 Subject: [PATCH 0529/1860] Refactored test_service a bit to use the synctestinginfrastructure. --- services/sync/tests/unit/test_service.js | 33 ++++++------------- .../sync/tests/unit/test_service.log.expected | 4 +++ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index b1ae20fa7814..5db497ed7813 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -17,11 +17,6 @@ let __fakeDAVContents = { "public/pubkey" : '{"version":1,"algorithm":"RSA"}' }; -let __fakePasswords = { - 'Mozilla Services Password': {foo: "bar"}, - 'Mozilla Services Encryption Passphrase': {foo: "passphrase"} -}; - let Service = loadInSandbox("resource://weave/service.js"); function TestService() { @@ -37,25 +32,17 @@ TestService.prototype = { TestService.prototype.__proto__ = Service.WeaveSvc.prototype; function test_login_works() { - var fds = new FakeDAVService(__fakeDAVContents); - var fprefs = new FakePrefService(__fakePrefs); - var fpasses = new FakePasswordService(__fakePasswords); - var fts = new FakeTimerService(); - var logStats = initTestLogging(); + var syncTesting = new SyncTestingInfrastructure(); + + syncTesting.fakeDAVService.fakeContents = __fakeDAVContents; + for (name in __fakePrefs) + syncTesting.fakePrefService.fakeContents[name] = __fakePrefs[name]; + var testService = new TestService(); - var finished = false; - var successful = false; - var onComplete = function(result) { - finished = true; - successful = result; - }; - testService.login(onComplete); + function login(cb) { + testService.login(cb); + } - while (fts.processCallback()) {} - - do_check_true(finished); - do_check_true(successful); - do_check_eq(logStats.errorsLogged, 0); - do_check_eq(Async.outstandingGenerators.length, 0); + syncTesting.runAsyncFunc("Logging in", login); } diff --git a/services/sync/tests/unit/test_service.log.expected b/services/sync/tests/unit/test_service.log.expected index c42a3ef9e7aa..6ab71c065a84 100644 --- a/services/sync/tests/unit/test_service.log.expected +++ b/services/sync/tests/unit/test_service.log.expected @@ -1,6 +1,9 @@ *** test pending Running test: test_login_works Service.Main INFO Weave Sync Service Initializing +Testing INFO ----------------------------------------- +Testing INFO Step 'Logging in' starting. +Testing INFO ----------------------------------------- Service.Main DEBUG Logging in user foo Service.Main INFO Using server URL: https://example.com/user/foo Testing INFO HTTP GET from meta/version, returning status 200 @@ -8,6 +11,7 @@ Service.Main TRACE Retrieving keypair from server Testing INFO HTTP GET from private/privkey, returning status 200 Testing INFO HTTP GET from public/pubkey, returning status 200 Service.Main INFO Weave scheduler disabled +Testing INFO Step 'Logging in' succeeded. 1 of 1 tests passed. *** test finished *** exiting From 9e3bafbdf4e5e4f31425b050361013042bd091a1 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 30 Jun 2008 11:33:25 -0700 Subject: [PATCH 0530/1860] Fix small bug with PasswordTracker returning scores > 100 --- services/sync/modules/engines/passwords.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 792e5abeddce..a2fe5adc6487 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -226,12 +226,12 @@ PasswordTracker.prototype = { _loginCount: 0, get score() { var count = this._loginManager.countLogins("", "", ""); - var score = Math.abs(this._loginCount - count) * 15; + var score = (Math.abs(this._loginCount - count) * 15) + 30; if (score >= 100) return 100; else - return score + 30; + return score; }, resetScore: function PasswordTracker_resetScore() { From e65b08d6835d72610ddd6aebf897e176a78da0ca Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 11:54:10 -0700 Subject: [PATCH 0531/1860] Updated test_passphrase_checking --- services/sync/tests/unit/head_first.js | 2 + .../tests/unit/test_passphrase_checking.js | 61 ++++--------------- 2 files changed, 15 insertions(+), 48 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 206c466f2fb0..ad3310c653f8 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -316,6 +316,8 @@ function SyncTestingInfrastructure(engineFactory) { ID.set('WeaveID', new Identity('Mozilla Services Encryption Passphrase', 'foo')); + ID.set('WeaveCryptoID', + new Identity('Mozilla Services Encryption Passphrase', 'foo')); this.fakePasswordService = new FakePasswordService(__fakePasswords); this.fakePrefService = new FakePrefService(__fakePrefs); diff --git a/services/sync/tests/unit/test_passphrase_checking.js b/services/sync/tests/unit/test_passphrase_checking.js index cbda8d4cc3af..327e597e701d 100644 --- a/services/sync/tests/unit/test_passphrase_checking.js +++ b/services/sync/tests/unit/test_passphrase_checking.js @@ -1,49 +1,22 @@ -Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/crypto.js"); - -let __fakePrefs = { - "log.logger.async" : "Debug", - "username" : "foo", - "serverURL" : "https://example.com/", - "encryption" : "aes-256-cbc", - "enabled" : true, - "schedule" : 0 -}; - -let __fakeDAVContents = { - "meta/version" : "3", - // Note that this private key is encrypted with the passphrase - // "passhrase", provided by our testing infrastructure. - "private/privkey" : '{"version":1,"algorithm":"RSA","privkey":"3ytj94K6Wo0mBjAVsiIwjm5x2+ENvpKTDUqLCz19iXbESf8RT6O8PmY7Pqcndpn+adqaQdvmr0T1JQ5bfLEHev0WBfo8oWJb+OS4rKoCWxDNzGwrOlW5hCfxSekw0KrKjqZyDZ0hT1Qt9vn6thlV2v9YWfmyn0OIxNC9hUqGwU3Wb2F2ejM0Tw40+IIW4eLEvFxLGv0vnEXpZvesPt413proL6FGQJe6vyapBg+sdX1JMYGaKZY84PUGIiDPxTbQg7yIWTSe3WlDhJ001khFiyEoTZvPhiAGXfML9ycrCRZUWkHp/cfS7QiusJXs6co0tLjrIk/rTk8h4mHBnyPkFIxh4YrfC7Bwf9npwomhaZCEQ32VK+a8grTDsGYHPZexDm3TcD2+d+hZ/u4lUOHFscQKX4w83tq942yqFtElCD2yQoqEDr1Z9zge5XBnLcYiH9hL0ozfpxBlTtpR1kSH663JHqlYim0qhuk0zrGAPkHna07UMFufxvgQBSd/YUqWCimJFGi+5QeOOFO20Skj882Bh1QDYsmbxZ/JED5ocGNHWSqpaOL2ML1F9nD5rdtffI0BsTe+j9h+HV4GlvzUz0Jd6RRf9xN4RyxqfENb8iGH5Pwbry7Qyk16rfm0s6JgG8pNb/8quKD+87RAtQFybZtdQ9NfGg+gyRiU9pbb6FPuPnGp+KpktaHu/K3HnomrVUoyLQALfCSbPXg2D9ta6dRV0JRqOZz4w52hlHIa62iJO6QecbdBzPYGT0QfOy/vp6ndRDR+2xMD/BmlaQwm3+58cqhIw9SVV5h/Z5PVaXxAOqg5vpU1NjrbF4uIFo5rmR0PyA/6qtxZaBY6w3I4sUWdDkIer8QsyrFrO7MIEdxksvDoFIeIM5eN8BufLu3ymS5ZXBiFr/iRxlYcQVHK2hz0/7syWUYsrz5/l1mj+qbWGx+6daWOk3xt4SH+p0hUpMC4FbJ9m/xr4im+X5m5ZYiajaF1QPOXTTny2plE0vfqMVlwX1HFFTJrAP+E85sZI8LPHAYO80qhSi3tV/LHjxCnC1LHJXaRkG202pQFWF1yVT/o82HBt9OC1xY6TVcy4Uh+6piNIQ9FxXGWrzjz0AUkxwkSN3Foqlfiq+mqJmNwzIdEQTmNAcBBsN3vWngU4elHjYI5qFZBzxJIkH8tfvivOshrOZIZB9TD9GIRhQwIBWc6i4fnqE9GUK2Jle6werdFATiMU4msQg7ClURaMn/p3MOLoxTmsPd1iBYPQkqnJgEAdNfKj3KRqSc6M/x09hGDSzK2d9Y03pyDGPh2sopcMFdCQbMy8VOld2/hEGakMJv6BPoRfhKmJbgGVf5x4B9dWZNa8WCmlcxaZ7KG43UA0zLm1VgfTcDW5qazDFoxIcfhmO5KoRI3q8vNs+Wh+smLC6yFODdF9HzrPimEYSc6OWHWgUcuiIBRjKeo5gBTbExWmri2VG+cn005vQNxK+0s7JVyFB8TzZ96pV3nFjkYy9OUkaiJxGd6OVGcvhbbrcNsKkaZff7OsLqczf6x0bhwh+y8+bLjLkuusGYUdBvdeiuv12IfeRupvwD8Z3aZOgcD7d+8VTyTyd/KX9fu8P7tD5SojJ5joRPjcv4Q8/mhRgtwx1McMIL3YnKHG+U=","privkeyIV":"fZ7CB/KQAUjEhkmrEkns4Q==","privkeySalt":"JFs5h2RKX9m0Op9DlQIcCOSfOH1MuDrrrHxCx+CpCUU="}', - "public/pubkey" : '{"version":1,"algorithm":"RSA","pubkey":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxxwObnXIoYeQKMG9RbvLkgsu/idDo25qOX87jIEiXkgW1wLKp/1D/DBLUEW303tVszNGVt6bTyAXOIj6skpmYoDs9Z48kvU3+g7Vi4QXEw5moSS4fr+yFpKiYd2Kx1+jCFGvGZjBzAAvnjsWmWrSA+LHJSrFlKY6SM3kNg8KrE8dxUi3wztlZnhZgo1ZYe7/VeBOXUfThtoadIl1VdREw2e79eiMQpPa0XLv4grCaMd/wLRs0be1/nPt7li4NyT0fnYFWg75SU3ni/xSaq/zR4NmW/of5vB2EcKyUG+/mvNplQ0CX+v3hRBCdhpCyPmcbHKUluyKzj7Ms9pKyCkwxwIDAQAB"}' -}; - -let Service = loadInSandbox("resource://weave/service.js"); - -function TestService() { - this.__superclassConstructor = Service.WeaveSvc; - this.__superclassConstructor([]); -} - -TestService.prototype = { - _initLogs: function TS__initLogs() { - this._log = Log4Moz.Service.getLogger("Service.Main"); - } -}; -TestService.prototype.__proto__ = Service.WeaveSvc.prototype; - Cu.import("resource://weave/identity.js"); Function.prototype.async = Async.sugar; +let __fakeCryptoID = { + keypairAlg: "RSA", + privkey: "3ytj94K6Wo0mBjAVsiIwjm5x2+ENvpKTDUqLCz19iXbESf8RT6O8PmY7Pqcndpn+adqaQdvmr0T1JQ5bfLEHev0WBfo8oWJb+OS4rKoCWxDNzGwrOlW5hCfxSekw0KrKjqZyDZ0hT1Qt9vn6thlV2v9YWfmyn0OIxNC9hUqGwU3Wb2F2ejM0Tw40+IIW4eLEvFxLGv0vnEXpZvesPt413proL6FGQJe6vyapBg+sdX1JMYGaKZY84PUGIiDPxTbQg7yIWTSe3WlDhJ001khFiyEoTZvPhiAGXfML9ycrCRZUWkHp/cfS7QiusJXs6co0tLjrIk/rTk8h4mHBnyPkFIxh4YrfC7Bwf9npwomhaZCEQ32VK+a8grTDsGYHPZexDm3TcD2+d+hZ/u4lUOHFscQKX4w83tq942yqFtElCD2yQoqEDr1Z9zge5XBnLcYiH9hL0ozfpxBlTtpR1kSH663JHqlYim0qhuk0zrGAPkHna07UMFufxvgQBSd/YUqWCimJFGi+5QeOOFO20Skj882Bh1QDYsmbxZ/JED5ocGNHWSqpaOL2ML1F9nD5rdtffI0BsTe+j9h+HV4GlvzUz0Jd6RRf9xN4RyxqfENb8iGH5Pwbry7Qyk16rfm0s6JgG8pNb/8quKD+87RAtQFybZtdQ9NfGg+gyRiU9pbb6FPuPnGp+KpktaHu/K3HnomrVUoyLQALfCSbPXg2D9ta6dRV0JRqOZz4w52hlHIa62iJO6QecbdBzPYGT0QfOy/vp6ndRDR+2xMD/BmlaQwm3+58cqhIw9SVV5h/Z5PVaXxAOqg5vpU1NjrbF4uIFo5rmR0PyA/6qtxZaBY6w3I4sUWdDkIer8QsyrFrO7MIEdxksvDoFIeIM5eN8BufLu3ymS5ZXBiFr/iRxlYcQVHK2hz0/7syWUYsrz5/l1mj+qbWGx+6daWOk3xt4SH+p0hUpMC4FbJ9m/xr4im+X5m5ZYiajaF1QPOXTTny2plE0vfqMVlwX1HFFTJrAP+E85sZI8LPHAYO80qhSi3tV/LHjxCnC1LHJXaRkG202pQFWF1yVT/o82HBt9OC1xY6TVcy4Uh+6piNIQ9FxXGWrzjz0AUkxwkSN3Foqlfiq+mqJmNwzIdEQTmNAcBBsN3vWngU4elHjYI5qFZBzxJIkH8tfvivOshrOZIZB9TD9GIRhQwIBWc6i4fnqE9GUK2Jle6werdFATiMU4msQg7ClURaMn/p3MOLoxTmsPd1iBYPQkqnJgEAdNfKj3KRqSc6M/x09hGDSzK2d9Y03pyDGPh2sopcMFdCQbMy8VOld2/hEGakMJv6BPoRfhKmJbgGVf5x4B9dWZNa8WCmlcxaZ7KG43UA0zLm1VgfTcDW5qazDFoxIcfhmO5KoRI3q8vNs+Wh+smLC6yFODdF9HzrPimEYSc6OWHWgUcuiIBRjKeo5gBTbExWmri2VG+cn005vQNxK+0s7JVyFB8TzZ96pV3nFjkYy9OUkaiJxGd6OVGcvhbbrcNsKkaZff7OsLqczf6x0bhwh+y8+bLjLkuusGYUdBvdeiuv12IfeRupvwD8Z3aZOgcD7d+8VTyTyd/KX9fu8P7tD5SojJ5joRPjcv4Q8/mhRgtwx1McMIL3YnKHG+U=", + privkeyWrapIV: "fZ7CB/KQAUjEhkmrEkns4Q==", + passphraseSalt: "JFs5h2RKX9m0Op9DlQIcCOSfOH1MuDrrrHxCx+CpCUU=", + pubkey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxxwObnXIoYeQKMG9RbvLkgsu/idDo25qOX87jIEiXkgW1wLKp/1D/DBLUEW303tVszNGVt6bTyAXOIj6skpmYoDs9Z48kvU3+g7Vi4QXEw5moSS4fr+yFpKiYd2Kx1+jCFGvGZjBzAAvnjsWmWrSA+LHJSrFlKY6SM3kNg8KrE8dxUi3wztlZnhZgo1ZYe7/VeBOXUfThtoadIl1VdREw2e79eiMQpPa0XLv4grCaMd/wLRs0be1/nPt7li4NyT0fnYFWg75SU3ni/xSaq/zR4NmW/of5vB2EcKyUG+/mvNplQ0CX+v3hRBCdhpCyPmcbHKUluyKzj7Ms9pKyCkwxwIDAQAB" + }; + function checkpassphrase_coroutine() { var self = yield; var idRSA = ID.get("WeaveCryptoID"); - //Uncomment this line to cause things to go amiss. - //idRSA.setTempPassword("badpassphrase"); - var idTemp = new Identity("temp", "temp", null); // Generate a random symmetric key. Crypto.randomKeyGen.async(Crypto, self.cb, idTemp); @@ -65,19 +38,11 @@ function checkpassphrase_coroutine() { } function test_passphrase_checking() { - var syncTesting = new SyncTestingInfrastructure(); + let syncTesting = new SyncTestingInfrastructure(); - syncTesting.fakeDAVService.fakeContents = __fakeDAVContents; - for (name in __fakePrefs) - syncTesting.fakePrefService.fakeContents[name] = __fakePrefs[name]; - - var testService = new TestService(); - - function login(cb) { - testService.login(cb); - } - - syncTesting.runAsyncFunc("Logging in", login); + let id = ID.get("WeaveCryptoID"); + for (name in __fakeCryptoID) + id[name] = __fakeCryptoID[name]; function checkpassphrase(cb) { checkpassphrase_coroutine.async({}, cb); From 22946709ea144c42da58e51d0f44d3dd66326523 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 30 Jun 2008 12:26:41 -0700 Subject: [PATCH 0532/1860] Fix syntax errors that were causing unit tests to fail --- services/sync/modules/engines/forms.js | 2 +- services/sync/modules/engines/passwords.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index eab81b71d789..4d49adb1915a 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -245,5 +245,5 @@ FormsTracker.prototype = { this._rowCount = stmnt.getInt32(0); stmnt.reset(); } -} +}; FormsTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index a2fe5adc6487..bcf1a3312f17 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -43,6 +43,7 @@ Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); Function.prototype.async = Async.sugar; @@ -239,7 +240,8 @@ PasswordTracker.prototype = { }, _init: function PasswordTracker__init() { - this._log = Log4Moz.Service.getLogger("Service." this._logName); + this._log = Log4Moz.Service.getLogger("Service." + this._logName); this._loginCount = this._loginManager.countLogins("", "", ""); } -} +}; +PasswordTracker.prototype.__proto__ = new Tracker(); From a7b5c8ee33f57c995e521e209054fde1ba66c9e1 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 30 Jun 2008 12:31:02 -0700 Subject: [PATCH 0533/1860] Update tests to reflect changes in store/syncCore methods --- services/sync/tests/unit/test_passwords.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/test_passwords.js index 3a1e7b94381f..9639aad66d1c 100644 --- a/services/sync/tests/unit/test_passwords.js +++ b/services/sync/tests/unit/test_passwords.js @@ -16,6 +16,8 @@ function test_synccore_itemexists_works() { var pwStore = new passwords.PasswordStore(); var fakeUserHash = pwStore._hashLoginInfo(fakeSampleLogins[0]); var psc = new passwords.PasswordSyncCore(); - do_check_false(psc._itemExists("invalid guid")); - do_check_true(psc._itemExists(fakeUserHash)); + /* wrap needs to be called before _itemExists */ + pwStore.wrap(); + do_check_false(pwStore._itemExists("invalid guid")); + do_check_true(pwStore._itemExists(fakeUserHash)); } From 3c883b5fe3d304ca73d0e21034630f648e5b3c68 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 13:32:13 -0700 Subject: [PATCH 0534/1860] Revalidated test logs, as 'actual changes for server' details are no longer being logged. --- .../sync/tests/unit/test_bookmark_syncing.log.expected | 7 ------- .../sync/tests/unit/test_password_syncing.log.expected | 3 --- 2 files changed, 10 deletions(-) diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/test_bookmark_syncing.log.expected index ec7daa007aff..e8a5d88844ec 100644 --- a/services/sync/tests/unit/test_bookmark_syncing.log.expected +++ b/services/sync/tests/unit/test_bookmark_syncing.log.expected @@ -66,7 +66,6 @@ Service.BmkEngine INFO Predicted changes for server: 1 Service.BmkEngine INFO Client conflicts: 0 Service.BmkEngine INFO Server conflicts: 0 Service.BmkEngine INFO Actual changes for server: 1 -Service.BmkEngine DEBUG Actual changes for server: [{"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}}] Service.BmkEngine INFO Uploading changes to server Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data @@ -106,7 +105,6 @@ Service.BmkEngine INFO Predicted changes for server: 2 Service.BmkEngine INFO Client conflicts: 0 Service.BmkEngine INFO Server conflicts: 0 Service.BmkEngine INFO Actual changes for server: 2 -Service.BmkEngine DEBUG Actual changes for server: [{"action":"edit","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":1,"type":"bookmark"}},{"action":"edit","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":0,"type":"bookmark"}}] Service.BmkEngine INFO Uploading changes to server Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data @@ -172,7 +170,6 @@ Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}}} Service.BmkEngine INFO Actual changes for server: 0 -Service.BmkEngine DEBUG Actual changes for server: [] Service.BmkEngine INFO Sync complete Testing INFO Step 're-sync on second computer' succeeded. Testing INFO ----------------------------------------- @@ -199,7 +196,6 @@ Service.BmkEngine INFO Predicted changes for server: 1 Service.BmkEngine INFO Client conflicts: 0 Service.BmkEngine INFO Server conflicts: 0 Service.BmkEngine INFO Actual changes for server: 1 -Service.BmkEngine DEBUG Actual changes for server: [{"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}}] Service.BmkEngine INFO Uploading changes to server Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data @@ -260,7 +256,6 @@ Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":3,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}}} Service.BmkEngine INFO Actual changes for server: 0 -Service.BmkEngine DEBUG Actual changes for server: [] Service.BmkEngine INFO Sync complete Testing INFO Step 're-sync on first computer' succeeded. Testing INFO ----------------------------------------- @@ -287,7 +282,6 @@ Service.BmkEngine INFO Predicted changes for server: 1 Service.BmkEngine INFO Client conflicts: 0 Service.BmkEngine INFO Server conflicts: 0 Service.BmkEngine INFO Actual changes for server: 1 -Service.BmkEngine DEBUG Actual changes for server: [{"action":"create","GUID":"bink-bookmark-guid-1","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}}] Service.BmkEngine INFO Uploading changes to server Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data @@ -349,7 +343,6 @@ Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":4,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"bink-bookmark-guid-1":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}}} Service.BmkEngine INFO Actual changes for server: 0 -Service.BmkEngine DEBUG Actual changes for server: [] Service.BmkEngine INFO Sync complete Testing INFO Step 'Manually add same bookmark 'bink', but with different GUID, to second computer and resync' succeeded. *** test finished diff --git a/services/sync/tests/unit/test_password_syncing.log.expected b/services/sync/tests/unit/test_password_syncing.log.expected index a8cfb284e6eb..c47fb009ef7b 100644 --- a/services/sync/tests/unit/test_password_syncing.log.expected +++ b/services/sync/tests/unit/test_password_syncing.log.expected @@ -66,7 +66,6 @@ Service.PasswordEngine INFO Predicted changes for server: 1 Service.PasswordEngine INFO Client conflicts: 0 Service.PasswordEngine INFO Server conflicts: 0 Service.PasswordEngine INFO Actual changes for server: 1 -Service.PasswordEngine DEBUG Actual changes for server: [{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}] Service.PasswordEngine INFO Uploading changes to server Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data @@ -106,7 +105,6 @@ Service.PasswordEngine INFO Predicted changes for server: 1 Service.PasswordEngine INFO Client conflicts: 0 Service.PasswordEngine INFO Server conflicts: 0 Service.PasswordEngine INFO Actual changes for server: 1 -Service.PasswordEngine DEBUG Actual changes for server: [{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}] Service.PasswordEngine INFO Uploading changes to server Service.JsonFilter DEBUG Encoding data as JSON Service.CryptoFilter DEBUG Encrypting data @@ -169,7 +167,6 @@ Service.SnapStore INFO Saving snapshot to disk Testing INFO Opening 'weave/snapshots/passwords.json' for writing. Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} Service.PasswordEngine INFO Actual changes for server: 0 -Service.PasswordEngine DEBUG Actual changes for server: [] Service.PasswordEngine INFO Sync complete Testing INFO Step 'resync on second computer' succeeded. *** test finished From 3ffc6150b6a5a693a88797b81ff4168c434158db Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 13:37:31 -0700 Subject: [PATCH 0535/1860] Removed test_loadall.js, as its need is obviated by the auto-generated js module loader tests. --- services/sync/tests/unit/test_loadall.js | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 services/sync/tests/unit/test_loadall.js diff --git a/services/sync/tests/unit/test_loadall.js b/services/sync/tests/unit/test_loadall.js deleted file mode 100644 index 8c354b91b505..000000000000 --- a/services/sync/tests/unit/test_loadall.js +++ /dev/null @@ -1,15 +0,0 @@ -function run_test() { - Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - Components.utils.import("resource://weave/constants.js"); - Components.utils.import("resource://weave/log4moz.js"); - Components.utils.import("resource://weave/util.js"); - Components.utils.import("resource://weave/async.js"); - Components.utils.import("resource://weave/crypto.js"); - Components.utils.import("resource://weave/identity.js"); - Components.utils.import("resource://weave/dav.js"); - Components.utils.import("resource://weave/wrap.js"); - Components.utils.import("resource://weave/stores.js"); - Components.utils.import("resource://weave/syncCores.js"); - Components.utils.import("resource://weave/engines.js"); - Components.utils.import("resource://weave/service.js"); -} From 1e40a6b557ed74d81980e2a1ad7866fdca2d852c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Jun 2008 13:58:42 -0700 Subject: [PATCH 0536/1860] change AsyncException so it implements toString (which returns the original exception) --- services/sync/modules/async.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index dcfbc5e3ac94..a5bfc655a905 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -82,23 +82,27 @@ let gOutstandingGenerators = new NamableTracker(); // asynchronous coroutine-based functions our current one was called // from. function AsyncException(asyncStack, exceptionToWrap) { - this._exceptionToWrap = exceptionToWrap; + this._originalException = exceptionToWrap; this._asyncStack = asyncStack; // Here we'll have our exception instance's prototype be dynamically // generated; this ultimately allows us to dynamically "subclass" // the exception we're wrapping at runtime. this.__proto__ = { - get asyncStack() { - return this._asyncStack; - }, + __proto__: this._originalException, + + get asyncStack() this._asyncStack, + get originalException() this._originalException, addAsyncFrame: function AsyncException_addAsyncFrame(frame) { this._asyncStack += ((this._asyncStack? "\n" : "") + formatAsyncFrame(frame)); + }, + + toString: function AsyncException_toString() { + return this._originalException; } }; - this.__proto__.__proto__ = this._exceptionToWrap; } function Generator(thisArg, method, onComplete, args) { From 90e589f1661df7a3c50be22c8dad40a87887c6ff Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Jun 2008 14:00:06 -0700 Subject: [PATCH 0537/1860] style fixes, js warning fixes --- services/sync/modules/service.js | 102 +++++++++++++------------------ 1 file changed, 41 insertions(+), 61 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9dce8d56930d..7d532cf50dad 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -147,6 +147,7 @@ WeaveSvc.prototype = { _localLock: Wrap.localLock, _osPrefix: "weave:service:", _loggedIn: false, + _syncInProgress: false, __os: null, get _os() { @@ -197,13 +198,8 @@ WeaveSvc.prototype = { get userPath() { return ID.get('WeaveID').username; }, - get isLoggedIn() { - return this._loggedIn; - }, - - get enabled() { - return Utils.prefs.getBoolPref("enabled"); - }, + get isLoggedIn() this._loggedIn, + get enabled() Utils.prefs.getBoolPref("enabled"), get schedule() { if (!this.enabled) @@ -237,7 +233,7 @@ WeaveSvc.prototype = { } }, - _enableSchedule: function WeaveSync__enableSchedule() { + _enableSchedule: function WeaveSvc__enableSchedule() { if (this._scheduleTimer) { this._scheduleTimer.cancel(); this._scheduleTimer = null; @@ -250,7 +246,7 @@ WeaveSvc.prototype = { this._log.info("Weave scheduler enabled"); }, - _disableSchedule: function WeaveSync__disableSchedule() { + _disableSchedule: function WeaveSvc__disableSchedule() { if (this._scheduleTimer) { this._scheduleTimer.cancel(); this._scheduleTimer = null; @@ -258,18 +254,18 @@ WeaveSvc.prototype = { this._log.info("Weave scheduler disabled"); }, - _onSchedule: function WeaveSync__onSchedule() { + _onSchedule: function WeaveSvc__onSchedule() { if (this.enabled) { if (!DAV.allowLock) { this._log.info("Skipping scheduled sync; local operation in progress") } else { this._log.info("Running scheduled sync"); - this._notify("syncAsNeeded", this._lock(this._syncAsNeeded)).async(this); + this._notify("sync-as-needed", this._lock(this._syncAsNeeded)).async(this); } } }, - _initLogs: function WeaveSync__initLogs() { + _initLogs: function WeaveSvc__initLogs() { this._log = Log4Moz.Service.getLogger("Service.Main"); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.main")]; @@ -312,7 +308,7 @@ WeaveSvc.prototype = { this._debugApp.clear(); }, - _uploadVersion: function WeaveSync__uploadVersion() { + _uploadVersion: function WeaveSvc__uploadVersion() { let self = yield; DAV.MKCOL("meta", self.cb); @@ -326,7 +322,7 @@ WeaveSvc.prototype = { }, // force a server wipe when the version is lower than ours (or there is none) - _versionCheck: function WeaveSync__versionCheck() { + _versionCheck: function WeaveSvc__versionCheck() { let self = yield; DAV.GET("meta/version", self.cb); @@ -334,17 +330,13 @@ WeaveSvc.prototype = { if (!Utils.checkStatus(ret.status)) { this._log.info("Server has no version file. Wiping server data."); - this._serverWipe.async(this, self.cb); - yield; - this._uploadVersion.async(this, self.cb); - yield; + yield this._serverWipe.async(this, self.cb); + yield this._uploadVersion.async(this, self.cb); } else if (ret.responseText < STORAGE_FORMAT_VERSION) { this._log.info("Server version too low. Wiping server data."); - this._serverWipe.async(this, self.cb); - yield; - this._uploadVersion.async(this, self.cb); - yield; + yield this._serverWipe.async(this, self.cb); + yield this._uploadVersion.async(this, self.cb); } else if (ret.responseText > STORAGE_FORMAT_VERSION) { // FIXME: should we do something here? @@ -368,7 +360,7 @@ WeaveSvc.prototype = { finally { DAV.defaultPrefix = prefix; } }, - _getKeypair : function WeaveSync__getKeypair() { + _getKeypair : function WeaveSvc__getKeypair() { let self = yield; if ("none" == Utils.prefs.getCharPref("encryption")) @@ -389,9 +381,8 @@ WeaveSvc.prototype = { "Could not get public key from server", [[200,300],404]); if (privkeyResp.status == 404 || pubkeyResp.status == 404) { - this._generateKeys.async(this, self.cb); - yield; - return; + yield this._generateKeys.async(this, self.cb); + return; } let privkeyData = this._json.decode(privkeyResp.responseText); @@ -417,7 +408,7 @@ WeaveSvc.prototype = { // know if the user's passphrase works or not. }, - _generateKeys: function WeaveSync__generateKeys() { + _generateKeys: function WeaveSvc__generateKeys() { let self = yield; this._log.debug("Generating new RSA key"); @@ -452,7 +443,7 @@ WeaveSvc.prototype = { let pubkeyData = { version : 1, algorithm : id.keypairAlg, - pubkey : id.pubkey, + pubkey : id.pubkey }; data = this._json.encode(pubkeyData); @@ -466,7 +457,7 @@ WeaveSvc.prototype = { // nsIObserver - observe: function WeaveSync__observe(subject, topic, data) { + observe: function WeaveSvc__observe(subject, topic, data) { switch (topic) { case "nsPref:changed": switch (data) { @@ -482,7 +473,7 @@ WeaveSvc.prototype = { } }, - _onQuitApplication: function WeaveSync__onQuitApplication() { + _onQuitApplication: function WeaveSvc__onQuitApplication() { if (!this.enabled || !Utils.prefs.getBoolPref("syncOnQuit.enabled") || !this._loggedIn) @@ -502,11 +493,12 @@ WeaveSvc.prototype = { // These are global (for all engines) - verifyLogin: function WeaveSync_verifyLogin(username, password) { - this._localLock(this._notify("verify-login", this._verifyLogin, username, password)).async(this, null); + verifyLogin: function WeaveSvc_verifyLogin(username, password) { + this._localLock(this._notify("verify-login", this._verifyLogin, + username, password)).async(this, null); }, - _verifyLogin: function WeaveSync__verifyLogin(username, password) { + _verifyLogin: function WeaveSvc__verifyLogin(username, password) { let self = yield; this._log.debug("Verifying login for user " + username); @@ -525,10 +517,10 @@ WeaveSvc.prototype = { Utils.ensureStatus(status, "Login verification failed"); }, - login: function WeaveSync_login(onComplete) { + login: function WeaveSvc_login(onComplete) { this._localLock(this._notify("login", this._login)).async(this, onComplete); }, - _login: function WeaveSync__login() { + _login: function WeaveSvc__login() { let self = yield; this._log.debug("Logging in user " + this.username); @@ -552,7 +544,7 @@ WeaveSvc.prototype = { self.done(true); }, - logout: function WeaveSync_logout() { + logout: function WeaveSvc_logout() { this._log.info("Logging out"); this._disableSchedule(); this._loggedIn = false; @@ -597,16 +589,13 @@ WeaveSvc.prototype = { // These are per-engine - sync: function WeaveSync_sync(onComplete) { + sync: function WeaveSvc_sync(onComplete) { this._notify("sync", this._lock(this._sync)).async(this, onComplete); }, - _sync: function WeaveSync__sync() { + _sync: function WeaveSvc__sync() { let self = yield; - if (!this._loggedIn) - throw "Can't sync: Not logged in"; - yield this._versionCheck.async(this, self.cb); yield this._getKeypair.async(this, self.cb); @@ -624,17 +613,11 @@ WeaveSvc.prototype = { // at different rates, so we store them in a hash indexed by engine name. _syncThresholds: {}, - _syncAsNeeded: function WeaveSync__syncAsNeeded() { + _syncAsNeeded: function WeaveSvc__syncAsNeeded() { let self = yield; - if (!this._loggedIn) - throw "Can't sync: Not logged in"; - - this._versionCheck.async(this, self.cb); - yield; - - this._getKeypair.async(this, self.cb); - yield; + yield this._versionCheck.async(this, self.cb); + yield this._getKeypair.async(this, self.cb); let engines = Engines.getAll(); for each (let engine in engines) { @@ -689,16 +672,13 @@ WeaveSvc.prototype = { } }, - resetServer: function WeaveSync_resetServer(onComplete) { + resetServer: function WeaveSvc_resetServer(onComplete) { this._notify("reset-server", this._lock(this._resetServer)).async(this, onComplete); }, - _resetServer: function WeaveSync__resetServer() { + _resetServer: function WeaveSvc__resetServer() { let self = yield; - if (!this._loggedIn) - throw "Can't reset server: Not logged in"; - let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { if (!engines[i].enabled) @@ -708,11 +688,11 @@ WeaveSvc.prototype = { } }, - resetClient: function WeaveSync_resetClient(onComplete) { + resetClient: function WeaveSvc_resetClient(onComplete) { this._localLock(this._notify("reset-client", this._resetClient)).async(this, onComplete); }, - _resetClient: function WeaveSync__resetClient() { + _resetClient: function WeaveSvc__resetClient() { let self = yield; let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { @@ -723,7 +703,7 @@ WeaveSvc.prototype = { } }, - shareData: function WeaveSync_shareData(dataType, + shareData: function WeaveSvc_shareData(dataType, onComplete, guid, username) { @@ -747,7 +727,7 @@ WeaveSvc.prototype = { username)).async(this, onComplete); }, - _shareData: function WeaveSync__shareData(dataType, + _shareData: function WeaveSvc__shareData(dataType, guid, username) { let self = yield; @@ -765,7 +745,7 @@ WeaveSvc.prototype = { /* LONGTERM TODO this is almost duplicated code, maybe just have * one function where we pass in true to share and false to stop * sharing. */ - stopSharingData: function WeaveSync_stopSharingData(dataType, + stopSharingData: function WeaveSvc_stopSharingData(dataType, onComplete, guid, username) { @@ -777,7 +757,7 @@ WeaveSvc.prototype = { username)).async(this, onComplete); }, - _stopSharingData: function WeaveSync__stopSharingData(dataType, + _stopSharingData: function WeaveSvc__stopSharingData(dataType, onComplete, guid, username) { From 9173e6c8fd37f4f2f424c87a05dc7be500a11adb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Jun 2008 14:00:55 -0700 Subject: [PATCH 0538/1860] change onQuit dialog to wait for a running sync in order to start the last sync --- services/sync/modules/wrap.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 90d5696ab17c..7d85ebe2b9c7 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -85,6 +85,7 @@ let Wrap = { try { this._os.notifyObservers(null, this._osPrefix + savedName + ":start", ""); + this._os.notifyObservers(null, this._osPrefix + "global:start", ""); args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); @@ -92,9 +93,11 @@ let Wrap = { ret = yield; this._os.notifyObservers(null, this._osPrefix + savedName + ":success", ""); + this._os.notifyObservers(null, this._osPrefix + "global:success", ""); } catch (e) { this._os.notifyObservers(null, this._osPrefix + savedName + ":error", ""); + this._os.notifyObservers(null, this._osPrefix + "global:error", ""); throw e; } @@ -113,11 +116,18 @@ let Wrap = { let ret; let args = Array.prototype.slice.call(arguments); + if (!this._loggedIn) + throw "Could not acquire lock (not logged in)"; + if (DAV.locked) + throw "Could not acquire lock (lock already held)"; + DAV.lock.async(DAV, self.cb); let locked = yield; if (!locked) throw "Could not acquire lock"; + this._os.notifyObservers(null, this._osPrefix + "lock:acquired", ""); + try { args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); @@ -128,8 +138,8 @@ let Wrap = { throw e; } finally { - DAV.unlock.async(DAV, self.cb); - yield; + yield DAV.unlock.async(DAV, self.cb); + this._os.notifyObservers(null, this._osPrefix + "lock:released", ""); } self.done(ret); @@ -151,14 +161,23 @@ let Wrap = { throw "Could not acquire lock"; DAV.allowLock = false; + this._os.notifyObservers(null, + this._osPrefix + "local-lock:acquired", ""); + try { args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); Async.run.apply(Async, args); ret = yield; + + } catch (e) { + throw e; + + } finally { + DAV.allowLock = true; + this._os.notifyObservers(null, + this._osPrefix + "local-lock:released", ""); } - catch (e) { throw e; } - finally { DAV.allowLock = true; } self.done(ret); }; From 855902b6414bcdb1e29061aa522d400f6be25e2e Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 14:29:26 -0700 Subject: [PATCH 0539/1860] test_passphrase_checking now actually tests real code and shows that it works. Still have to move isPassphraseValid() into a different place though, probably crypto. --- .../tests/unit/test_passphrase_checking.js | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/services/sync/tests/unit/test_passphrase_checking.js b/services/sync/tests/unit/test_passphrase_checking.js index 327e597e701d..db4e9a2ccc88 100644 --- a/services/sync/tests/unit/test_passphrase_checking.js +++ b/services/sync/tests/unit/test_passphrase_checking.js @@ -6,47 +6,71 @@ Function.prototype.async = Async.sugar; let __fakeCryptoID = { keypairAlg: "RSA", + // This private key is encrypted with the passphrase 'passphrase', + // contained in our testing infrastructure. privkey: "3ytj94K6Wo0mBjAVsiIwjm5x2+ENvpKTDUqLCz19iXbESf8RT6O8PmY7Pqcndpn+adqaQdvmr0T1JQ5bfLEHev0WBfo8oWJb+OS4rKoCWxDNzGwrOlW5hCfxSekw0KrKjqZyDZ0hT1Qt9vn6thlV2v9YWfmyn0OIxNC9hUqGwU3Wb2F2ejM0Tw40+IIW4eLEvFxLGv0vnEXpZvesPt413proL6FGQJe6vyapBg+sdX1JMYGaKZY84PUGIiDPxTbQg7yIWTSe3WlDhJ001khFiyEoTZvPhiAGXfML9ycrCRZUWkHp/cfS7QiusJXs6co0tLjrIk/rTk8h4mHBnyPkFIxh4YrfC7Bwf9npwomhaZCEQ32VK+a8grTDsGYHPZexDm3TcD2+d+hZ/u4lUOHFscQKX4w83tq942yqFtElCD2yQoqEDr1Z9zge5XBnLcYiH9hL0ozfpxBlTtpR1kSH663JHqlYim0qhuk0zrGAPkHna07UMFufxvgQBSd/YUqWCimJFGi+5QeOOFO20Skj882Bh1QDYsmbxZ/JED5ocGNHWSqpaOL2ML1F9nD5rdtffI0BsTe+j9h+HV4GlvzUz0Jd6RRf9xN4RyxqfENb8iGH5Pwbry7Qyk16rfm0s6JgG8pNb/8quKD+87RAtQFybZtdQ9NfGg+gyRiU9pbb6FPuPnGp+KpktaHu/K3HnomrVUoyLQALfCSbPXg2D9ta6dRV0JRqOZz4w52hlHIa62iJO6QecbdBzPYGT0QfOy/vp6ndRDR+2xMD/BmlaQwm3+58cqhIw9SVV5h/Z5PVaXxAOqg5vpU1NjrbF4uIFo5rmR0PyA/6qtxZaBY6w3I4sUWdDkIer8QsyrFrO7MIEdxksvDoFIeIM5eN8BufLu3ymS5ZXBiFr/iRxlYcQVHK2hz0/7syWUYsrz5/l1mj+qbWGx+6daWOk3xt4SH+p0hUpMC4FbJ9m/xr4im+X5m5ZYiajaF1QPOXTTny2plE0vfqMVlwX1HFFTJrAP+E85sZI8LPHAYO80qhSi3tV/LHjxCnC1LHJXaRkG202pQFWF1yVT/o82HBt9OC1xY6TVcy4Uh+6piNIQ9FxXGWrzjz0AUkxwkSN3Foqlfiq+mqJmNwzIdEQTmNAcBBsN3vWngU4elHjYI5qFZBzxJIkH8tfvivOshrOZIZB9TD9GIRhQwIBWc6i4fnqE9GUK2Jle6werdFATiMU4msQg7ClURaMn/p3MOLoxTmsPd1iBYPQkqnJgEAdNfKj3KRqSc6M/x09hGDSzK2d9Y03pyDGPh2sopcMFdCQbMy8VOld2/hEGakMJv6BPoRfhKmJbgGVf5x4B9dWZNa8WCmlcxaZ7KG43UA0zLm1VgfTcDW5qazDFoxIcfhmO5KoRI3q8vNs+Wh+smLC6yFODdF9HzrPimEYSc6OWHWgUcuiIBRjKeo5gBTbExWmri2VG+cn005vQNxK+0s7JVyFB8TzZ96pV3nFjkYy9OUkaiJxGd6OVGcvhbbrcNsKkaZff7OsLqczf6x0bhwh+y8+bLjLkuusGYUdBvdeiuv12IfeRupvwD8Z3aZOgcD7d+8VTyTyd/KX9fu8P7tD5SojJ5joRPjcv4Q8/mhRgtwx1McMIL3YnKHG+U=", privkeyWrapIV: "fZ7CB/KQAUjEhkmrEkns4Q==", passphraseSalt: "JFs5h2RKX9m0Op9DlQIcCOSfOH1MuDrrrHxCx+CpCUU=", pubkey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxxwObnXIoYeQKMG9RbvLkgsu/idDo25qOX87jIEiXkgW1wLKp/1D/DBLUEW303tVszNGVt6bTyAXOIj6skpmYoDs9Z48kvU3+g7Vi4QXEw5moSS4fr+yFpKiYd2Kx1+jCFGvGZjBzAAvnjsWmWrSA+LHJSrFlKY6SM3kNg8KrE8dxUi3wztlZnhZgo1ZYe7/VeBOXUfThtoadIl1VdREw2e79eiMQpPa0XLv4grCaMd/wLRs0be1/nPt7li4NyT0fnYFWg75SU3ni/xSaq/zR4NmW/of5vB2EcKyUG+/mvNplQ0CX+v3hRBCdhpCyPmcbHKUluyKzj7Ms9pKyCkwxwIDAQAB" }; -function checkpassphrase_coroutine() { +function isPassphraseValid(identity) { var self = yield; - var idRSA = ID.get("WeaveCryptoID"); - var idTemp = new Identity("temp", "temp", null); // Generate a random symmetric key. Crypto.randomKeyGen.async(Crypto, self.cb, idTemp); yield; // Encrypt the symmetric key with the user's public key. - Crypto.wrapKey.async(Crypto, self.cb, idTemp.bulkKey, idRSA); + Crypto.wrapKey.async(Crypto, self.cb, idTemp.bulkKey, identity); let wrappedKey = yield; + let unwrappedKey; // Decrypt the symmetric key with the user's private key. - Crypto.unwrapKey.async(Crypto, self.cb, wrappedKey, idRSA); - let unwrappedKey = yield; + try { + Crypto.unwrapKey.async(Crypto, self.cb, wrappedKey, identity); + unwrappedKey = yield; + } catch (e) { + self.done(false); + return; + } // Ensure that the original symmetric key is identical to // the decrypted version. - do_check_eq(unwrappedKey, idTemp.bulkKey); + if (unwrappedKey != idTemp.bulkKey) + throw new Error("Unwrapped key is not identical to original key."); self.done(true); } -function test_passphrase_checking() { - let syncTesting = new SyncTestingInfrastructure(); +function testGenerator() { + let self = yield; let id = ID.get("WeaveCryptoID"); for (name in __fakeCryptoID) id[name] = __fakeCryptoID[name]; - function checkpassphrase(cb) { - checkpassphrase_coroutine.async({}, cb); - } + isPassphraseValid.async({}, self.cb, id); + let result = yield; - syncTesting.runAsyncFunc("Checking passphrase", checkpassphrase); + do_check_eq(result, true); + + id.setTempPassword("incorrect passphrase"); + + isPassphraseValid.async({}, self.cb, id); + result = yield; + + do_check_eq(result, false); + + self.done(); +} + +function run_test() { + let syncTesting = new SyncTestingInfrastructure(); + + syncTesting.runAsyncFunc( + "Ensuring isPassphraseValid() works", + function runTest(cb) { testGenerator.async({}, cb); } + ); } From 320143455693efee3aee30982652a19bf5ec3d5c Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 14:40:11 -0700 Subject: [PATCH 0540/1860] Moved isPassphraseValid() out of its test suite and into the crypto module. --- services/sync/modules/crypto.js | 40 +++++++++++++++++++ .../tests/unit/test_passphrase_checking.js | 34 +--------------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 439bf4957ae7..7ce787b476ea 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -229,4 +229,44 @@ CryptoSvc.prototype = { identity.privkeyWrapIV); self.done(ret); }, + + // This function tests to see if the passphrase which encrypts + // the private key for the given identity is valid. + isPassphraseValid: function Crypto_isPassphraseValid(identity) { + var self = yield; + + // We do this in a somewhat roundabout way; an alternative is + // to use a hard-coded symmetric key, but even that is still + // roundabout--ideally, the IWeaveCrypto interface should + // expose some sort of functionality to make this easier, + // or perhaps it should just offer this very function. -AV + + // Generate a temporary fake identity. + var idTemp = {realm: "Temporary passphrase validation"}; + + // Generate a random symmetric key. + this.randomKeyGen.async(Crypto, self.cb, idTemp); + yield; + + // Encrypt the symmetric key with the user's public key. + this.wrapKey.async(Crypto, self.cb, idTemp.bulkKey, identity); + let wrappedKey = yield; + let unwrappedKey; + + // Decrypt the symmetric key with the user's private key. + try { + this.unwrapKey.async(Crypto, self.cb, wrappedKey, identity); + unwrappedKey = yield; + } catch (e) { + self.done(false); + return; + } + + // Ensure that the original symmetric key is identical to + // the decrypted version. + if (unwrappedKey != idTemp.bulkKey) + throw new Error("Unwrapped key is not identical to original key."); + + self.done(true); + } }; diff --git a/services/sync/tests/unit/test_passphrase_checking.js b/services/sync/tests/unit/test_passphrase_checking.js index db4e9a2ccc88..a8ccabb706b4 100644 --- a/services/sync/tests/unit/test_passphrase_checking.js +++ b/services/sync/tests/unit/test_passphrase_checking.js @@ -14,36 +14,6 @@ let __fakeCryptoID = { pubkey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxxwObnXIoYeQKMG9RbvLkgsu/idDo25qOX87jIEiXkgW1wLKp/1D/DBLUEW303tVszNGVt6bTyAXOIj6skpmYoDs9Z48kvU3+g7Vi4QXEw5moSS4fr+yFpKiYd2Kx1+jCFGvGZjBzAAvnjsWmWrSA+LHJSrFlKY6SM3kNg8KrE8dxUi3wztlZnhZgo1ZYe7/VeBOXUfThtoadIl1VdREw2e79eiMQpPa0XLv4grCaMd/wLRs0be1/nPt7li4NyT0fnYFWg75SU3ni/xSaq/zR4NmW/of5vB2EcKyUG+/mvNplQ0CX+v3hRBCdhpCyPmcbHKUluyKzj7Ms9pKyCkwxwIDAQAB" }; -function isPassphraseValid(identity) { - var self = yield; - - var idTemp = new Identity("temp", "temp", null); - // Generate a random symmetric key. - Crypto.randomKeyGen.async(Crypto, self.cb, idTemp); - yield; - - // Encrypt the symmetric key with the user's public key. - Crypto.wrapKey.async(Crypto, self.cb, idTemp.bulkKey, identity); - let wrappedKey = yield; - let unwrappedKey; - - // Decrypt the symmetric key with the user's private key. - try { - Crypto.unwrapKey.async(Crypto, self.cb, wrappedKey, identity); - unwrappedKey = yield; - } catch (e) { - self.done(false); - return; - } - - // Ensure that the original symmetric key is identical to - // the decrypted version. - if (unwrappedKey != idTemp.bulkKey) - throw new Error("Unwrapped key is not identical to original key."); - - self.done(true); -} - function testGenerator() { let self = yield; @@ -51,14 +21,14 @@ function testGenerator() { for (name in __fakeCryptoID) id[name] = __fakeCryptoID[name]; - isPassphraseValid.async({}, self.cb, id); + Crypto.isPassphraseValid.async(Crypto, self.cb, id); let result = yield; do_check_eq(result, true); id.setTempPassword("incorrect passphrase"); - isPassphraseValid.async({}, self.cb, id); + Crypto.isPassphraseValid.async(Crypto, self.cb, id); result = yield; do_check_eq(result, false); From e398b02f6d9a1b7a5ab7b5ed9e3ed0a929816844 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 30 Jun 2008 15:13:07 -0700 Subject: [PATCH 0541/1860] bug 442711: validate virtual tabs to make sure they have the minimal information necessary to recreate them before trying to save them or notify the user about them --- services/sync/modules/engines/tabs.js | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 20a632d17694..68b025dba2b4 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -180,6 +180,29 @@ TabStore.prototype = { this._saveVirtualTabs(); }, + /** + * Make sure a virtual tab entry is valid (i.e. it contains the information + * we need to extract at least its current history entry's URL). + * + * @param tab {Object} the virtual tab to validate + * @returns {Boolean} whether or not the given virtual tab is valid + */ + validateVirtualTab: function TabStore__validateVirtualTab(tab) { + if (!tab.state || !tab.state.entries || !tab.state.index) { + this._log.warn("invalid virtual tab state: " + this._json.encode(tab)); + return false; + } + + let currentEntry = tab.state.entries[tab.state.index - 1]; + + if (!currentEntry || !currentEntry.url) { + this._log.warn("no current entry or no URL: " + this._json.encode(tab)); + return false; + } + + return true; + }, + // The file in which we store the state of virtual tabs. get _file() { let file = this._dirSvc.get("ProfD", Ci.nsILocalFile); @@ -268,6 +291,13 @@ TabStore.prototype = { if (command.GUID in this._virtualTabs || command.GUID in this._wrapRealTabs()) throw "trying to create a tab that already exists; id: " + command.GUID; + // Don't do anything if the command isn't valid (i.e. it doesn't contain + // the minimum information about the tab that is necessary to recreate it). + if (!this.validateVirtualTab(command.data)) { + this._log.warn("could not create command " + command.GUID + "; invalid"); + return; + } + // Cache the tab and notify the UI to prompt the user to open it. this._virtualTabs[command.GUID] = command.data; this._os.notifyObservers(null, "weave:store:tabs:virtual:created", null); @@ -288,6 +318,13 @@ TabStore.prototype = { _editCommand: function TabStore__editCommand(command) { this._log.debug("_editCommand: " + command.GUID); + // Don't do anything if the command isn't valid (i.e. it doesn't contain + // the minimum information about the tab that is necessary to recreate it). + if (!this.validateVirtualTab(command.data)) { + this._log.warn("could not edit command " + command.GUID + "; invalid"); + return; + } + // We don't edit real tabs, because that isn't what the user would expect, // but it's ok to edit virtual tabs, so that if users do open them, they get // the most up-to-date version of them (and also to reduce sync churn). From ffc2402ba5350cdfb2567ce91fa4dd0a684c9fb6 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Mon, 30 Jun 2008 15:35:00 -0700 Subject: [PATCH 0542/1860] updated strings, increased wizard height, updated background with new height and logo --- services/sync/locales/en-US/wizard.dtd | 29 ++++++++++--------- services/sync/locales/en-US/wizard.properties | 4 +-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 51d1b765b595..6b4fd06be1b2 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,7 +1,9 @@ - - + + + + @@ -11,11 +13,11 @@ - - - - - + + + + + @@ -45,11 +47,11 @@ - - - + + + - + @@ -65,7 +67,8 @@ - + + @@ -77,7 +80,7 @@ - + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 51196f3beba5..549d1ff2feb2 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -19,7 +19,7 @@ email-success.label = %S is available. passwordsUnmatched.label = Passwords do not match passphrasesUnmatched.label = Passphrases do not match. -samePasswordAndPassphrase.label = Your password and passphrase must be different +samePasswordAndPassphrase.label = Your password and passphrase must be different. @@ -42,7 +42,7 @@ create-success.label = Account for %S created. final-pref-value.label = %S final-account-value.label = Username: %S -final-sync-value.label = Weave will upload your data to the server. +final-sync-value.label = Backup and synchronization of browser settings and metadata. final-success.label = Weave was successfully installed. default-name.label = %S's Firefox From a8f3fecf95238a581e78301c04a4511b7bfc81ae Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 15:54:15 -0700 Subject: [PATCH 0543/1860] Fixed a potential bug in Service.login() whereby a user's directory wouldn't be created if it didn't exist (though it would in Service.verifyLogin(), which is only called from the setup wizard). --- services/sync/modules/service.js | 13 +++++-------- services/sync/tests/unit/head_first.js | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7d532cf50dad..94eca831aa07 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -494,26 +494,26 @@ WeaveSvc.prototype = { // These are global (for all engines) verifyLogin: function WeaveSvc_verifyLogin(username, password) { + this._log.debug("Verifying login for user " + username); + this._localLock(this._notify("verify-login", this._verifyLogin, username, password)).async(this, null); }, _verifyLogin: function WeaveSvc__verifyLogin(username, password) { let self = yield; - this._log.debug("Verifying login for user " + username); DAV.baseURL = Utils.prefs.getCharPref("serverURL"); DAV.defaultPrefix = "user/" + username; this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); - let status = yield DAV.checkLogin.async(DAV, self.cb, username, password); if (status == 404) { // create user directory (for self-hosted webdav shares) - // XXX do this in login? yield this._checkUserDir.async(this, self.cb); status = yield DAV.checkLogin.async(DAV, self.cb, username, password); } + Utils.ensureStatus(status, "Login verification failed"); }, @@ -530,11 +530,8 @@ WeaveSvc.prototype = { if (!this.password) throw "No password given or found in password manager"; - DAV.baseURL = Utils.prefs.getCharPref("serverURL"); - DAV.defaultPrefix = "user/" + this.userPath; - - this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); - + yield this._verifyLogin.async(this, self.cb, this.username, + this.password); yield this._versionCheck.async(this, self.cb); yield this._getKeypair.async(this, self.cb); diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index ad3310c653f8..1a249564eb8e 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -173,7 +173,7 @@ function FakeDAVService(contents) { this.fakeContents = contents; DAV.__proto__ = this; - this.checkLogin = makeFakeAsyncFunc(true); + this.checkLogin = makeFakeAsyncFunc(200); } FakeDAVService.prototype = { From ab5ad05fb3ef4704a33641a9c892896984bdeb07 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 16:50:19 -0700 Subject: [PATCH 0544/1860] Added a Service.verifyPassphrase() method. Also, Service.login() now checks to ensure that the user's passphrase is valid, and if it's not, it throws an exception. --- services/sync/modules/service.js | 49 +++++++++++++++++++++--- services/sync/tests/unit/test_service.js | 10 +++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 94eca831aa07..4eff5358f223 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -360,25 +360,35 @@ WeaveSvc.prototype = { finally { DAV.defaultPrefix = prefix; } }, - _getKeypair : function WeaveSvc__getKeypair() { + _getKeypair : function WeaveSvc__getKeypair(id, createIfNecessary) { let self = yield; if ("none" == Utils.prefs.getCharPref("encryption")) return; + if (typeof(id) == "undefined") + id = ID.get('WeaveCryptoID'); + + if (typeof(createIfNecessary) == "undefined") + createIfNecessary = true; + this._log.trace("Retrieving keypair from server"); + let statuses = [[200, 300]]; + if (createIfNecessary) + statuses.push(404); + // XXX this kind of replaces _keyCheck // seems like key generation should only happen during setup? DAV.GET("private/privkey", self.cb); let privkeyResp = yield; Utils.ensureStatus(privkeyResp.status, - "Could not get private key from server", [[200,300],404]); + "Could not get private key from server", statuses); DAV.GET("public/pubkey", self.cb); let pubkeyResp = yield; Utils.ensureStatus(pubkeyResp.status, - "Could not get public key from server", [[200,300],404]); + "Could not get public key from server", statuses); if (privkeyResp.status == 404 || pubkeyResp.status == 404) { yield this._generateKeys.async(this, self.cb); @@ -396,7 +406,6 @@ WeaveSvc.prototype = { throw "Only RSA keys currently supported"; - let id = ID.get('WeaveCryptoID'); id.keypairAlg = privkeyData.algorithm; id.privkey = privkeyData.privkey; id.privkeyWrapIV = privkeyData.privkeyIV; @@ -404,8 +413,9 @@ WeaveSvc.prototype = { id.pubkey = pubkeyData.pubkey; - // XXX note that we have not used the private key, so we don't yet - // know if the user's passphrase works or not. + let isValid = yield Crypto.isPassphraseValid.async(Crypto, self.cb, id); + if (!isValid) + throw new Error("Passphrase is not valid."); }, _generateKeys: function WeaveSvc__generateKeys() { @@ -493,6 +503,33 @@ WeaveSvc.prototype = { // These are global (for all engines) + verifyPassphrase: function WeaveSvc_verifyPassphrase(username, password, + passphrase) { + this._localLock(this._notify("verify-passphrase", + this._verifyPassphrase, + username, + password, + passphrase)).async(this, null); + }, + + _verifyPassphrase: function WeaveSvc__verifyPassphrase(username, password, + passphrase) { + let self = yield; + + this._log.debug("Verifying passphrase"); + + yield this._verifyLogin.async(this, self.cb, username, password); + let id = new Identity('Passphrase Verification', username); + id.setTempPassword(passphrase); + // XXX: We're not checking the version of the server here, in part because + // we have no idea what to do if the version is different than we expect + // it to be. + yield this._getKeypair.async(this, self.cb, id, false); + let isValid = yield Crypto.isPassphraseValid.async(Crypto, self.cb, id); + if (!isValid) + throw new Error("Passphrase is not valid."); + }, + verifyLogin: function WeaveSvc_verifyLogin(username, password) { this._log.debug("Verifying login for user " + username); diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/test_service.js index 5db497ed7813..70911bc00737 100644 --- a/services/sync/tests/unit/test_service.js +++ b/services/sync/tests/unit/test_service.js @@ -2,6 +2,8 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/crypto.js"); +Function.prototype.async = Async.sugar; + let __fakePrefs = { "log.logger.async" : "Debug", "username" : "foo", @@ -31,6 +33,14 @@ TestService.prototype = { }; TestService.prototype.__proto__ = Service.WeaveSvc.prototype; +Crypto.isPassphraseValid = function fake_isPassphraseValid(id) { + let self = yield; + + do_check_eq(id.password, "passphrase"); + + self.done(true); +}; + function test_login_works() { var syncTesting = new SyncTestingInfrastructure(); From 22572ba97ff97a5ed4ca043906b1746f794082a0 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 16:55:38 -0700 Subject: [PATCH 0545/1860] Fixed a bug from r5a5113a0a405 that caused some syncing unit tests to fail. --- services/sync/modules/async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index a5bfc655a905..d052df52926a 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -100,7 +100,7 @@ function AsyncException(asyncStack, exceptionToWrap) { }, toString: function AsyncException_toString() { - return this._originalException; + return this._originalException.toString(); } }; } From e990bff6903780956f413ba50bcc0c5a516cab33 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 17:55:48 -0700 Subject: [PATCH 0546/1860] The auto-login at startup no longer unconditionally syncs; if the login fails, the sync is now aborted. --- services/sync/modules/service.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4eff5358f223..b8e413811155 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -210,15 +210,23 @@ WeaveSvc.prototype = { onWindowOpened: function Weave__onWindowOpened() { if (!this._startupFinished) { if (Utils.prefs.getBoolPref("autoconnect") && - this.username && this.username != 'nobody') { - // Login, then sync. - let self = this; - this.login(function() { self.sync(); }); - } + this.username && this.username != 'nobody') + this._initialLoginAndSync.async(this); this._startupFinished = true; } }, + _initialLoginAndSync: function Weave__initialLoginAndSync() { + let self = yield; + + // Any exceptions thrown by the login process will be propagated + // out here, so there's no need to check to see if the login was + // successful; if it wasn't, this coroutine will just abort. + + yield this.login(self.cb); + yield this.sync(self.cb); + }, + _setSchedule: function Weave__setSchedule(schedule) { switch (this.schedule) { case 0: From 17c3c484c5fda86e68a1311d947bdfe20c294111 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Mon, 30 Jun 2008 18:00:55 -0700 Subject: [PATCH 0547/1860] reverted back to original weave logo, increased wizard height for linux problem, added passphrase verification --- services/sync/locales/en-US/wizard.dtd | 8 ++++---- services/sync/locales/en-US/wizard.properties | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 6b4fd06be1b2..03e56e7ec023 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,6 +1,6 @@ - + @@ -14,7 +14,7 @@ - + @@ -55,9 +55,9 @@ - + - + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 549d1ff2feb2..7deabb6b584d 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,7 +1,9 @@ verify-progress.label = Verifying username and password verify-error.label = Invalid username and password verify-success.label = Username and password verified - +passphrase-progress.label = Verifying passphrase +passphrase-success.label = Passphrase verified +passphrase-error.label = Invalid passphrase serverError.label = Server Error serverTimeoutError.label = Server Timeout From fdb29dea62d1a50cae422f8a98b2110ac5461b9a Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 18:09:30 -0700 Subject: [PATCH 0548/1860] Added documentation for Service._getKeypair(). --- services/sync/modules/service.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b8e413811155..7f4713e86b36 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -368,6 +368,18 @@ WeaveSvc.prototype = { finally { DAV.defaultPrefix = prefix; } }, + // Retrieves the keypair for the given Identity object and inserts + // its information into the Identity object. If no Identity object + // is supplied, the 'WeaveCryptoID' identity is used. + // + // If 'createIfNecessary' is true (the default), then this function + // will create a new keypair if none currently exists. + // + // This coroutine assumes the DAV singleton's prefix is set to the + // proper user-specific directory. + // + // If the password associated with the Identity cannot be used to + // decrypt the private key, an exception is raised. _getKeypair : function WeaveSvc__getKeypair(id, createIfNecessary) { let self = yield; From 0b394f961ed0cb9643b967c273da7c23a4f84855 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Jun 2008 18:49:46 -0700 Subject: [PATCH 0549/1860] update URLs to services.mozilla.com/ --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index d85fd51c17cf..a75d25847eab 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,4 @@ -pref("extensions.weave.serverURL", "https://services.mozilla.com/0.2/"); +pref("extensions.weave.serverURL", "https://services.mozilla.com/"); pref("extensions.weave.username", "nobody"); pref("extensions.weave.encryption", "aes-256-cbc"); From 7c592ca352d3c061078fa49019097b35cc564beb Mon Sep 17 00:00:00 2001 From: Date: Mon, 30 Jun 2008 18:50:06 -0700 Subject: [PATCH 0550/1860] Added test_bookmark_sharing to version control; note this doesn't pass right now (and in fact the main test is commented out.) --- services/sync/modules/engines/bookmarks.js | 12 +- .../sync/tests/unit/test_bookmark_sharing.js | 115 ++++++++++++++++++ 2 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 services/sync/tests/unit/test_bookmark_sharing.js diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 56764f49360d..ecfbda9380dc 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -35,7 +35,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['BookmarksEngine']; +const EXPORTED_SYMBOLS = ['BookmarksEngine', 'BookmarksSharingManager']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -65,6 +65,7 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/xmpp/xmppClient.js"); Cu.import("resource://weave/notifications.js"); +Cu.import("resource://weave/sharing.js"); Function.prototype.async = Async.sugar; @@ -203,8 +204,11 @@ BookmarksSharingManager.prototype = { the UI. */ // Create the outgoing share folder on the server + dump("Blah!\n"); this._createOutgoingShare.async( this, self.cb, selectedFolder, username ); + dump( "Gonna yield?\n" ); let serverPath = yield; + dump("in _share: annotated with serverPath = \" + serverPath + \"\n"); this._updateOutgoingShare.async( this, self.cb, selectedFolder ); yield; @@ -477,9 +481,9 @@ BookmarksSharingManager.prototype = { self.done(); }, - _createIncomingShare: function BookmarkEngine__createShare(user, - serverPath, - title) { + _createIncomingShare: function BmkSharing__createShare(user, + serverPath, + title) { /* Creates a new folder in the bookmark menu for the incoming share. user is the weave id of the user who is offering the folder to us; serverPath is the path on the server where the shared data is located, diff --git a/services/sync/tests/unit/test_bookmark_sharing.js b/services/sync/tests/unit/test_bookmark_sharing.js new file mode 100644 index 000000000000..c1a700352519 --- /dev/null +++ b/services/sync/tests/unit/test_bookmark_sharing.js @@ -0,0 +1,115 @@ +Cu.import("resource://weave/engines/bookmarks.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/sharing.js"); + +Function.prototype.async = Async.sugar; + +load("bookmark_setup.js"); + +function FakeMicrosummaryService() { + return {hasMicrosummary: function() { return false; }}; +} + +function FakeAnnotationService() { + this._annotations = {}; +} +FakeAnnotationService.prototype = { + EXPIRE_NEVER: 0, + getItemAnnotation: function (aItemId, aName) { + if (this._annotations[aItemId] != undefined) + if (this._annotations[aItemId][aName]) + return this._annotations[aItemId][aName]; + return null; + }, + setItemAnnotation: function (aItemId, aName, aValue, aFlags, aExpiration) { + if (this._annotations[aItemId] == undefined) + this._annotations[aItemId] = {}; + this._annotations[aItemId][aName] = aValue; + dump( "Annotated item " + aItemId + " with " + aName + " = " + aValue + "\n"); + //ignore flags and expiration + }, + getItemsWithAnnotation: function(aName, resultCount, results) { + var list = []; + for ( var x in this._annotations) { + if (this._annotations[x][aName] != undefined) { + return x; + } + } + return list; + } +} + + +function FakeSharingApi() { +} +FakeSharingApi.prototype = { + shareWithUsers: function FakeSharingApi_shareWith(path, users, onComplete) { + // TODO just set a flag on the fake DAV thing. + } +} +Sharing.Api = FakeSharingApi; + + +var annoSvc = new FakeAnnotationService(); + +function makeBookmarksEngine() { + let engine = new BookmarksEngine(); + engine._store.__ms = new FakeMicrosummaryService(); + let shareManager = engine._sharing; + shareManager.__annoSvc = annoSvc; // use fake annotation service + return engine; +} + +function run_test() { + let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + + + var syncTesting = new SyncTestingInfrastructure( makeBookmarksEngine ); + + let folderName = "Funny Pictures of Manatees and Walruses"; + let folderToShare = bms.createFolder( bms.bookmarksMenuFolder, + folderName, -1 ); + let lolrusBm = bms.insertBookmark(folderToShare, + uri("http://www.lolrus.com"), + -1, "LOLrus" ); + let lolateeBm = bms.insertBookmark(folderToShare, + uri("http://www.lolatee.com"), + -1, "LOLatee" ); + + // Note xmpp.enabled is set to false by the SyncTestingInfrastructure. + + let username = "rusty"; + let engine = makeBookmarksEngine(); + + + /* + // TODO async function can't be called like this, call it from + // an async callback thingy in SyncTestingInfrastructure. + shareManager._share.async( shareManager, null, folderToShare, "jonas" ); + + dump( "folderToShare = " + folderToShare + "\n"); + // Get the server path from folder annotation... + let serverPath = annoSvc.getItemAnnotation( folderToShare, + "weave/shared-server-path" ); + dump( "Shared it to server path " + serverPath + "\n"); + + // get off rusty's computer, switch to Jonas's computer: + syncTesting.saveClientState( "rusty computer 1" ); + syncTesting.resetClientState(); + + // These next two lines simulate what would happen when jonas received + // the xmpp message and clicked "accept". + shareManager._createIncomingShare(username, serverPath, folderName); + shareManager._updateAllIncomingShares(); + + // now look for a bookmark folder with an incoming-share annotation + let a = annoSvc.getItemsWithAnnotation("weave/shared-incoming", + {}); + do_check_eq( a.length, 1); // should be just one + // TODO next look at its children: + */ +} + + From 71e87cbdc2eb84ab3d2377445611173f1b9089cc Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 19:27:39 -0700 Subject: [PATCH 0551/1860] Fixed a todo in test_bookmark_sharing, still more left to do as jono's modifications to the bookmark sharing API change. --- services/sync/tests/unit/test_bookmark_sharing.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/services/sync/tests/unit/test_bookmark_sharing.js b/services/sync/tests/unit/test_bookmark_sharing.js index c1a700352519..4ce15428ec5e 100644 --- a/services/sync/tests/unit/test_bookmark_sharing.js +++ b/services/sync/tests/unit/test_bookmark_sharing.js @@ -82,12 +82,18 @@ function run_test() { let username = "rusty"; let engine = makeBookmarksEngine(); + let shareManager = engine._sharing; + function setupShare(cb) { + // TODO: Passing in folderToShare won't work at the time of writing + // this because folderToShare is expected to be a DOM node, not a + // Places ID. + shareManager._share.async( shareManager, cb, folderToShare, "jonas" ); + } /* - // TODO async function can't be called like this, call it from - // an async callback thingy in SyncTestingInfrastructure. - shareManager._share.async( shareManager, null, folderToShare, "jonas" ); + syncTesting.runAsyncFunc("Share folder with Jonas", setupShare); + dump( "folderToShare = " + folderToShare + "\n"); // Get the server path from folder annotation... @@ -109,7 +115,7 @@ function run_test() { {}); do_check_eq( a.length, 1); // should be just one // TODO next look at its children: - */ + */ } From 730945c1e31753ada29fdfce411a674bd9c21531 Mon Sep 17 00:00:00 2001 From: Maria Emerson Date: Mon, 30 Jun 2008 21:05:18 -0700 Subject: [PATCH 0552/1860] image update --- services/sync/locales/en-US/wizard.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 7deabb6b584d..48943ee6af9e 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -16,7 +16,7 @@ createUsername-success.label = %S is available. email-progress.label = Checking email email-unavailable.label = %S is already taken. Please choose another. email-invalid.label = Please re-enter a valid email address. -email-success.label = %S is available. +email-success.label = passwordsUnmatched.label = Passwords do not match From cb414fcefbf7f73cabd23aa7ae3a4d487a34cd87 Mon Sep 17 00:00:00 2001 From: Date: Mon, 30 Jun 2008 22:30:04 -0700 Subject: [PATCH 0553/1860] Combined shareData and stopSharingData into one function to reuse code; made it so that if it gets called when Weave.DAV is already locked, instead of failing it sets up an observer that will trigger the share to happen as soon as the sync-succeeded or sync-failed message is received. --- services/sync/modules/engines/bookmarks.js | 18 ++++- services/sync/modules/service.js | 93 ++++++++++++---------- 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index ecfbda9380dc..e63f5fba22b7 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -243,9 +243,20 @@ BookmarksSharingManager.prototype = { _stopSharing: function BmkSharing__stopSharing( selectedFolder, username ) { let self = yield; - // TODO FIXME the next line says getAttribute is not a function... + dump( "selectedFolder is of type: " + typeof selectedFolder + "\n"); + dump( "Value is " + selectedFolder + "\n"); let folderName = selectedFolder.getAttribute( "label" ); let folderNode = selectedFolder.node; + // This line is getting a NS_ERROR_NOT_AVAILABLE. Is that a problem + // with folderNode.itemId or with annoSvc? + if ( folderNode.itemId == undefined ) { + dump( "FolderNode.itemId is undefined!!\n" ); + } else { + dump( "folderNode.itemId is " + folderNode.itemId +"\n"); + } + if (!this._annoSvc.itemHasAnnotation(folderNode.itemId, SERVER_PATH_ANNO)){ + dump( "Expected annotation not found!!\n" ); + } let serverPath = this._annoSvc.getItemAnnotation(folderNode.itemId, SERVER_PATH_ANNO); @@ -335,7 +346,7 @@ BookmarksSharingManager.prototype = { let folderGuid = Utils.makeGUID(); /* Create the directory on the server if it does not exist already. */ - let serverPath = "/0.2/user/" + myUserName + "/share/" + folderGuid; + let serverPath = "/user/" + myUserName + "/share/" + folderGuid; DAV.MKCOL(serverPath, self.cb); let ret = yield; if (!ret) { @@ -368,7 +379,7 @@ BookmarksSharingManager.prototype = { /* Get public keys for me and the user I'm sharing with. Each user's public key is stored in /user/username/public/pubkey. */ let idRSA = ID.get('WeaveCryptoID'); // TODO Can get error "Resource not defined" - let userPubKeyFile = new Resource("/0.2/user/" + username + "/public/pubkey"); + let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); userPubKeyFile.get(self.cb); let userPubKey = yield; @@ -683,6 +694,7 @@ BookmarksEngine.prototype = { _stopSharing: function BmkEngine__stopSharing(guid, username) { let self = yield; + dump( "BookmarkEnginge._stopSharing: guid=" + guid + ", username = " + username + "\n"); this._sharing._stopSharing.async( this._sharing, self.cb, guid, username); yield; self.done(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7f4713e86b36..18cf2583987e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -758,9 +758,10 @@ WeaveSvc.prototype = { }, shareData: function WeaveSvc_shareData(dataType, - onComplete, - guid, - username) { + isShareEnabled, + onComplete, + guid, + username) { /* Shares data of the specified datatype (which must correspond to one of the registered engines) with the user specified by username. The data node indicated by guid will be shared, along with all its @@ -768,61 +769,71 @@ WeaveSvc.prototype = { when sharing is done; it takes an argument that will be true or false to indicate whether sharing succeeded or failed. Implementation, as well as the interpretation of what 'guid' means, - is left up to the engine for the specific dataType. */ + is left up to the engine for the specific dataType. + + isShareEnabled: true to start sharing, false to stop sharing.*/ let messageName = "share-" + dataType; /* so for instance, if dataType is "bookmarks" then a message "share-bookmarks" will be sent out to any observers who are listening for it. As far as I know, there aren't currently any listeners for "share-bookmarks" but we'll send it out just in case. */ - this._notify(messageName, this._lock(this._shareData, - dataType, - guid, - username)).async(this, onComplete); + + let self = this; + let saved_dataType = dataType; + let saved_onComplete = onComplete; + let saved_guid = guid; + let saved_username = username; + let saved_isShareEnabled = isShareEnabled; + let successMsg = "weave:service:global:success"; + let errorMsg = "weave:service:global:error"; + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + + let observer = { + observe: function(subject, topic, data) { + if (!Weave.DAV.locked) { + self._notify(messageName, self._lock(self._shareData, + saved_dataType, + saved_isShareEnabled, + saved_guid, + saved_username)).async(self, + saved_onComplete); + os.removeObserver(observer, successMsg); + os.removeObserver(observer, errorMsg); + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]); + }; + + if (Weave.DAV.locked) { + /* then we have to wait until it's not locked. */ + os.addObserver( observer, successMsg, true ); + os.addObserver( observer, errorMsg, true ); + } else { + // Just do it right now! + observer.observe(); + } }, _shareData: function WeaveSvc__shareData(dataType, - guid, - username) { + isShareEnabled, + guid, + username) { let self = yield; let ret; if (Engines.get(dataType).enabled) { - Engines.get(dataType).share(self.cb, guid, username); + if (isShareEnabled) { + Engines.get(dataType).share(self.cb, guid, username); + } else { + Engines.get(dataType).stopSharing(self.cb, guid, username); + } ret = yield; } else { this._log.warn( "Can't share disabled data type: " + dataType ); ret = false; } self.done(ret); - }, - - /* LONGTERM TODO this is almost duplicated code, maybe just have - * one function where we pass in true to share and false to stop - * sharing. */ - stopSharingData: function WeaveSvc_stopSharingData(dataType, - onComplete, - guid, - username) { - let messageName = "stop-sharing-" + dataType; - this._lock(this._notify(messageName, - this._stopSharingData, - dataType, - guid, - username)).async(this, onComplete); - }, - - _stopSharingData: function WeaveSvc__stopSharingData(dataType, - onComplete, - guid, - username) { - let self = yield; - let ret; - if (Engines.get(dataType).enabled) { - Engines.get(dataType).stopSharing(self.cb, guid, username); - ret = yield; - } else { - ret = false; - } - self.done(ret); } + }; From bd89ad87de19704115c0024a9e4af0763dfe02f3 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 30 Jun 2008 22:58:51 -0700 Subject: [PATCH 0554/1860] bug 442849: correct inconsistencies and grammar nits in wizard text --- services/sync/locales/en-US/wizard.dtd | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 03e56e7ec023..01abeb269904 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -3,10 +3,10 @@ - + - + @@ -20,14 +20,14 @@ - + - + @@ -51,7 +51,7 @@ - + @@ -67,7 +67,7 @@ - + @@ -80,20 +80,20 @@ - + - + - + - + From 359428765be6abedd039e277dab6040611e9cb5d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Jun 2008 23:25:15 -0700 Subject: [PATCH 0555/1860] fix a missing yield, split up a couple of lines for clarity --- services/sync/modules/remote.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 49dfa172102d..4e8f2b984707 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -535,7 +535,8 @@ RemoteStore.prototype = { if (lastSyncSnap.version < this.status.data.snapVersion) { this._log.trace("Getting latest from snap --> scratch"); - self.done(yield this._getLatestFromScratch.async(this, self.cb)); + snap = yield this._getLatestFromScratch.async(this, self.cb); + self.done(snap); return; } else if (lastSyncSnap.version >= this.status.data.snapVersion && @@ -547,7 +548,8 @@ RemoteStore.prototype = { let min = lastSyncSnap.version + 1; let max = this.status.data.maxVersion; for (let id = min; id <= max; id++) { - deltas.push(yield this._deltas.get(self.cb, id)); + let delta = yield this._deltas.get(self.cb, id); + deltas.push(delta); } } else if (lastSyncSnap.version == this.status.data.maxVersion) { @@ -563,14 +565,13 @@ RemoteStore.prototype = { try { for (var i = 0; i < deltas.length; i++) { - snap.applyCommands.async(snap, self.cb, deltas[i]); - yield; + yield snap.applyCommands.async(snap, self.cb, deltas[i]); } } catch (e) { this._log.warn("Error applying remote deltas to saved snapshot, attempting a full download"); this._log.debug("Exception: " + Utils.exceptionStr(e)); this._log.trace("Stack:\n" + Utils.stackTrace(e)); - snap = this._getLatestFromScratch.async(this, self.cb); + snap = yield this._getLatestFromScratch.async(this, self.cb); } self.done(snap); From a6d2178f7a10f9acd48a1486f13766dc147c74f6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Jun 2008 23:25:51 -0700 Subject: [PATCH 0556/1860] continue if there is an edit command for an item we don't have (just print a warning) --- services/sync/modules/stores.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 46bc0b056524..cf16ce9dec23 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -112,14 +112,14 @@ Store.prototype = { else return false; }, - + // override these in derived objects - + // wrap MUST save the wrapped store in the _lookup property! wrap: function Store_wrap() { throw "wrap needs to be subclassed"; }, - + wipe: function Store_wipe() { throw "wipe needs to be subclassed"; }, @@ -201,7 +201,10 @@ SnapshotStore.prototype = { for (let prop in command.data) { if (prop == "GUID") continue; - this._data[command.GUID][prop] = command.data[prop]; + if (command.GUID in this._data) + this._data[command.GUID][prop] = command.data[prop]; + else + this._log.warn("Warning! Edit command for unknown item: " + command.GUID); } }, From b86cb8d3daede6cb2b400ddb070c77fca358e25f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Jun 2008 23:26:35 -0700 Subject: [PATCH 0557/1860] fix cookie command logging; skip cookie commands for items we don't have --- services/sync/modules/engines/cookies.js | 26 +++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 4681f506c453..a0ed60a453e6 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -146,7 +146,8 @@ CookieStore.prototype = { /* we got a command to create a cookie in the local browser in order to sync with the server. */ - this._log.info("CookieStore got createCommand: " + command ); + this._log.debug("CookieStore got create command for: " + command.GUID); + // this assumes command.data fits the nsICookie2 interface if ( !command.data.isSession ) { // Add only persistent cookies ( not session cookies ) @@ -167,22 +168,33 @@ CookieStore.prototype = { command.data appears to be equivalent to what wrap() puts in the JSON dictionary. */ - this._log.info("CookieStore got removeCommand: " + command ); + if (!(command.GUID in this._lookup)) { + this._log.warn("Warning! Remove command for unknown item: " + command.GUID); + return; + } + + this._log.debug("CookieStore got remove command for: " + command.GUID); /* I think it goes like this, according to - http://developer.mozilla.org/en/docs/nsICookieManager - the last argument is "always block cookies from this domain?" - and the answer is "no". */ + http://developer.mozilla.org/en/docs/nsICookieManager + the last argument is "always block cookies from this domain?" + and the answer is "no". */ this._cookieManager.remove(this._lookup[command.GUID].host, this._lookup[command.GUID].name, this._lookup[command.GUID].path, - false ); + false); }, _editCommand: function CookieStore__editCommand(command) { /* we got a command to change a cookie in the local browser in order to sync with the server. */ - this._log.info("CookieStore got editCommand: " + command ); + + if (!(command.GUID in this._lookup)) { + this._log.warn("Warning! Edit command for unknown item: " + command.GUID); + return; + } + + this._log.debug("CookieStore got edit command for: " + command.GUID); /* Look up the cookie that matches the one in the command: */ var iter = this._cookieManager.enumerator; From 05a7312dc20b5a8e6f7d2679c9f3f4897156f98f Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 30 Jun 2008 23:29:28 -0700 Subject: [PATCH 0558/1860] fix capitalization --- services/sync/locales/en-US/wizard.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 01abeb269904..8e2e3a20d2eb 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -14,7 +14,7 @@ - + From edbb25effcd6865ddf84a8ca7841225c37f6e82f Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Mon, 30 Jun 2008 23:36:00 -0700 Subject: [PATCH 0559/1860] Modified sharing.js to obey the new sharing api. --- services/sync/modules/sharing.js | 8 +++++++- services/sync/tests/unit/test_sharing.js | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/sharing.js b/services/sync/modules/sharing.js index c3100ab22b41..2e40a5691058 100644 --- a/services/sync/modules/sharing.js +++ b/services/sync/modules/sharing.js @@ -41,6 +41,7 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/identity.js"); Function.prototype.async = Async.sugar; @@ -58,6 +59,7 @@ Api.prototype = { _shareGenerator: function Api__shareGenerator(path, users) { let self = yield; + let id = ID.get(this._dav.identity); this._dav.defaultPrefix = ""; @@ -67,7 +69,11 @@ Api.prototype = { let jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); let json = jsonSvc.encode(cmd); - this._dav.POST("share/", "cmd=" + escape(json), self.cb); + this._dav.POST("share/", + ("cmd=" + escape(json) + + "&uid=" + escape(id.username) + + "&password=" + escape(id.password)), + self.cb); let xhr = yield; let retval; diff --git a/services/sync/tests/unit/test_sharing.js b/services/sync/tests/unit/test_sharing.js index e18f6de3be95..c3944a871688 100644 --- a/services/sync/tests/unit/test_sharing.js +++ b/services/sync/tests/unit/test_sharing.js @@ -1,11 +1,18 @@ Cu.import("resource://weave/sharing.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/identity.js"); function runTestGenerator() { let self = yield; + ID.set("blarg", new Identity("realm", "myusername", "mypass")); let fakeDav = { + identity: "blarg", POST: function fakeDav_POST(url, data, callback) { + do_check_true(data.indexOf("uid=myusername") != -1); + do_check_true(data.indexOf("password=mypass") != -1); + do_check_true(data.indexOf("/fake/dir") != -1); + do_check_true(data.indexOf("johndoe") != -1); let result = {status: 200, responseText: "OK"}; Utils.makeTimerForCall(function() { callback(result); }); } From 4b383affc4d75568f7b38de90f1379b0b3b73a6a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Jun 2008 23:41:47 -0700 Subject: [PATCH 0560/1860] save username/pass in verifyLogin, at least for now --- services/sync/modules/service.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7f4713e86b36..8522a440ecf8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -560,6 +560,9 @@ WeaveSvc.prototype = { _verifyLogin: function WeaveSvc__verifyLogin(username, password) { let self = yield; + this.username = username; + this.password = password; + DAV.baseURL = Utils.prefs.getCharPref("serverURL"); DAV.defaultPrefix = "user/" + username; From 3376e5b6d4eab5550bbf4f0d9d57161796a49694 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 1 Jul 2008 00:04:47 -0700 Subject: [PATCH 0561/1860] version bump (0.2.0) --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index c030dc207a15..505c9c72b170 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.1.34"; +const WEAVE_VERSION = "0.2.0"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From a45ebc879674a3cbecb64ba9aceed4dc36f80eef Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Jul 2008 09:58:00 -0700 Subject: [PATCH 0562/1860] Made _stopSharing and _stopOutgoingShare more fault-tolerant (they will no longer die if the expected annotation is missing. --- services/sync/modules/engines/bookmarks.js | 44 ++++++++++------------ services/sync/modules/service.js | 4 +- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e63f5fba22b7..583c721b9a7e 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -247,18 +247,14 @@ BookmarksSharingManager.prototype = { dump( "Value is " + selectedFolder + "\n"); let folderName = selectedFolder.getAttribute( "label" ); let folderNode = selectedFolder.node; - // This line is getting a NS_ERROR_NOT_AVAILABLE. Is that a problem - // with folderNode.itemId or with annoSvc? - if ( folderNode.itemId == undefined ) { - dump( "FolderNode.itemId is undefined!!\n" ); + + if (this._annoSvc.itemHasAnnotation(folderNode.itemId, SERVER_PATH_ANNO)){ + let serverPath = this._annoSvc.getItemAnnotation(folderNode.itemId, + SERVER_PATH_ANNO); } else { - dump( "folderNode.itemId is " + folderNode.itemId +"\n"); + this._log.warn("The folder you are de-sharing has no path annotation."); + let serverPath = ""; } - if (!this._annoSvc.itemHasAnnotation(folderNode.itemId, SERVER_PATH_ANNO)){ - dump( "Expected annotation not found!!\n" ); - } - let serverPath = this._annoSvc.getItemAnnotation(folderNode.itemId, - SERVER_PATH_ANNO); /* LONGTERM TODO: when we move to being able to share one folder with * multiple people, this needs to be modified so we can stop sharing with @@ -460,22 +456,20 @@ BookmarksSharingManager.prototype = { server, deletes the annotations that mark it as shared, and sends a message to the shar-ee to let them know it's been withdrawn. */ let self = yield; - let serverPath = this._annoSvc.getItemAnnotation( folderNode, + if (this._annoSvc.itemHasAnnotation(folderNode.itemId, SERVER_PATH_ANNO)){ + let serverPath = this._annoSvc.getItemAnnotation( folderNode, SERVER_PATH_ANNO ); - let username = this._annoSvc.getItemAnnotation( folderNode, - OUTGOING_SHARED_ANNO ); - - // Delete the share from the server: - // TODO handle error that can happen if these resources do not exist. - let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); - keyringFile.delete(self.cb); - yield; - let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); - keyringFile.delete(self.cb); - yield; - // TODO this leaves the folder itself in place... is there a way to - // get rid of that, say through DAV? - + // Delete the share from the server: + // TODO handle error that can happen if these resources do not exist. + let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); + keyringFile.delete(self.cb); + yield; + let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); + keyringFile.delete(self.cb); + yield; + // TODO this leaves the folder itself in place... is there a way to + // get rid of that, say through DAV? + } // Remove the annotations from the local folder: this._annoSvc.setItemAnnotation(folderNode, SERVER_PATH_ANNO, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 18cf2583987e..c52dc5c7c77a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -803,15 +803,17 @@ WeaveSvc.prototype = { os.removeObserver(observer, errorMsg); } }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]); + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) }; if (Weave.DAV.locked) { /* then we have to wait until it's not locked. */ + dump( "DAV is locked, gonna set up observer to do it later.\n"); os.addObserver( observer, successMsg, true ); os.addObserver( observer, errorMsg, true ); } else { // Just do it right now! + dump( "DAV not locked, doing it now.\n"); observer.observe(); } }, From d4e1a772a84cb1a0b0157019cc00b0c057ddb43e Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Jul 2008 10:18:35 -0700 Subject: [PATCH 0563/1860] Made _share() and _stopSharing() take bookmark item ID numbers instead of XUL nodes. --- services/sync/modules/engines/bookmarks.js | 107 ++++++++++----------- 1 file changed, 50 insertions(+), 57 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 583c721b9a7e..5c8f53450dfd 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -81,6 +81,14 @@ BookmarksSharingManager.prototype = { return this.__annoSvc; }, + __bms: null, + get _bms() { + if (!this.__bms) + this._bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + return this.__bms; + }, + _init: function SharingManager__init(engine) { this._engine = engine; this._log = Log4Moz.Service.getLogger("Bookmark Share"); @@ -194,7 +202,7 @@ BookmarksSharingManager.prototype = { Notifications.add(notification); }, - _share: function BmkSharing__share( selectedFolder, username ) { + _share: function BmkSharing__share( folderId, username ) { // Return true if success, false if failure. let ret = false; let self = yield; @@ -202,21 +210,19 @@ BookmarksSharingManager.prototype = { /* TODO What should the behavior be if i'm already sharing it with user A and I ask to share it with user B? (This should be prevented by the UI. */ + let folderName = this._bms.getItemTitle(folderId); // Create the outgoing share folder on the server - dump("Blah!\n"); - this._createOutgoingShare.async( this, self.cb, selectedFolder, username ); - dump( "Gonna yield?\n" ); + this._createOutgoingShare.async( this, self.cb, + folderId, folderName, username ); let serverPath = yield; dump("in _share: annotated with serverPath = \" + serverPath + \"\n"); - this._updateOutgoingShare.async( this, self.cb, selectedFolder ); + this._updateOutgoingShare.async( this, self.cb, folderId ); yield; /* Set the annotation on the folder so we know it's an outgoing share: */ - let folderItemId = selectedFolder.node.itemId; - let folderName = selectedFolder.getAttribute( "label" ); - this._annoSvc.setItemAnnotation(folderItemId, + this._annoSvc.setItemAnnotation(folderId, OUTGOING_SHARED_ANNO, username, 0, @@ -241,15 +247,12 @@ BookmarksSharingManager.prototype = { self.done( ret ); }, - _stopSharing: function BmkSharing__stopSharing( selectedFolder, username ) { + _stopSharing: function BmkSharing__stopSharing( folderId, username ) { let self = yield; - dump( "selectedFolder is of type: " + typeof selectedFolder + "\n"); - dump( "Value is " + selectedFolder + "\n"); - let folderName = selectedFolder.getAttribute( "label" ); - let folderNode = selectedFolder.node; + let folderName = this._bms.getItemTitle(folderId); - if (this._annoSvc.itemHasAnnotation(folderNode.itemId, SERVER_PATH_ANNO)){ - let serverPath = this._annoSvc.getItemAnnotation(folderNode.itemId, + if (this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)){ + let serverPath = this._annoSvc.getItemAnnotation(folderId, SERVER_PATH_ANNO); } else { this._log.warn("The folder you are de-sharing has no path annotation."); @@ -262,7 +265,7 @@ BookmarksSharingManager.prototype = { */ // Stop the outgoing share: - this._stopOutgoingShare.async( this, self.cb, selectedFolder); + this._stopOutgoingShare.async(this, self.cb, folderId); yield; // Send message to the share-ee, so they can stop their incoming share: @@ -325,17 +328,21 @@ BookmarksSharingManager.prototype = { self.done(); }, - _createOutgoingShare: function BmkSharing__createOutgoing(folder, username) { - /* To be called asynchronously. Folder is a node indicating the bookmark - folder that is being shared; username is a string indicating the user - that it is to be shared with. This function creates the directory and - keyring on the server in which the shared data will be put, but it - doesn't actually put the bookmark data there (that's done in - _updateOutgoingShare().) */ + _createOutgoingShare: function BmkSharing__createOutgoing(folderId, + folderName, + username) { + /* To be called asynchronously. FolderId is the integer id of the + bookmark folder to be shared and folderName is a string of its + title. username is a string indicating the user that it is to be + shared with. This function creates the directory and keyring on + the server in which the shared data will be put, but it doesn't + actually put the bookmark data there (that's done in + _updateOutgoingShare().) */ + let self = yield; - let myUserName = ID.get('WeaveID').username; - this._log.debug("Sharing bookmarks from " + folder.getAttribute( "label" ) - + " with " + username); + let myUserName = fID.get('WeaveID').username; + this._log.debug("Turning folder " + folderName + " into outgoing share" + + + " with " + username); /* Generate a new GUID to use as the new directory name on the server in which we'll store the shared data. */ @@ -353,7 +360,7 @@ BookmarksSharingManager.prototype = { /* Store the path to the server directory in an annotation on the shared bookmark folder, so we can easily get back to it later. */ - this._annoSvc.setItemAnnotation(folder.node.itemId, + this._annoSvc.setItemAnnotation(folderId, SERVER_PATH_ANNO, serverPath, 0, @@ -406,7 +413,7 @@ BookmarksSharingManager.prototype = { self.done( serverPath ); }, - _updateOutgoingShare: function BmkSharing__updateOutgoing(folderNode) { + _updateOutgoingShare: function BmkSharing__updateOutgoing(folderId) { /* Puts all the bookmark data from the specified bookmark folder, encrypted, onto the shared directory on the server (replacing anything that was already there). @@ -416,7 +423,7 @@ BookmarksSharingManager.prototype = { let myUserName = ID.get('WeaveID').username; // The folder has an annotation specifying the server path to the // directory: - let serverPath = this._annoSvc.getItemAnnotation(folderNode, + let serverPath = this._annoSvc.getItemAnnotation(folderId, SERVER_PATH_ANNO); // TODO the above can throw an exception if the expected anotation isn't // there. @@ -433,7 +440,8 @@ BookmarksSharingManager.prototype = { let bulkIV = keys.bulkIV; // Get the json-wrapped contents of everything in the folder: - let json = this._engine._store._wrapMount( folderNode, myUserName ); + // TODO what exactly does wrapMount expect? is folderId OK? + let json = this._engine._store._wrapMount( folderId, myUserName ); /* TODO what does wrapMount do with this username? Should I be passing in my own or that of the person I share with? */ @@ -451,13 +459,13 @@ BookmarksSharingManager.prototype = { self.done(); }, - _stopOutgoingShare: function BmkSharing__stopOutgoingShare(folderNode) { + _stopOutgoingShare: function BmkSharing__stopOutgoingShare(folderId) { /* Stops sharing the specified folder. Deletes its data from the server, deletes the annotations that mark it as shared, and sends a message to the shar-ee to let them know it's been withdrawn. */ let self = yield; - if (this._annoSvc.itemHasAnnotation(folderNode.itemId, SERVER_PATH_ANNO)){ - let serverPath = this._annoSvc.getItemAnnotation( folderNode, + if (this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)){ + let serverPath = this._annoSvc.getItemAnnotation( folderId, SERVER_PATH_ANNO ); // Delete the share from the server: // TODO handle error that can happen if these resources do not exist. @@ -471,18 +479,8 @@ BookmarksSharingManager.prototype = { // get rid of that, say through DAV? } // Remove the annotations from the local folder: - this._annoSvc.setItemAnnotation(folderNode, - SERVER_PATH_ANNO, - "", - 0, - this._annoSvc.EXPIRE_NEVER); - this._annoSvc.setItemAnnotation(folderNode, - OUTGOING_SHARED_ANNO, - "", - 0, - this._annoSvc.EXPIRE_NEVER); - // TODO is there a way to remove the annotations entirely rather than - // setting it to an empty string?? + this._annoSvc.removeItemAnnotation(folderId, SERVER_PATH_ANNO); + this._annoSvc.removeItemAnnotation(folderId, OUTGOING_SHARED_ANNO); self.done(); }, @@ -497,8 +495,6 @@ BookmarksSharingManager.prototype = { It is safe to call this again for a folder that already exist: this function will exit without doing anything. It won't create a duplicate. */ - let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); /* Get the toolbar "Shared Folders" folder (identified by its annotation). If it doesn't already exist, create it: */ @@ -508,9 +504,9 @@ BookmarksSharingManager.prototype = { if (a.length == 1) root = a[0]; if (!root) { - root = bms.createFolder(bms.toolbarFolder, - INCOMING_SHARE_ROOT_NAME, - bms.DEFAULT_INDEX); + root = this._bms.createFolder(this._bms.toolbarFolder, + INCOMING_SHARE_ROOT_NAME, + this._bms.DEFAULT_INDEX); this._annoSvc.setItemAnnotation(root, INCOMING_SHARE_ROOT_ANNO, true, @@ -532,7 +528,7 @@ BookmarksSharingManager.prototype = { } } if (!itemExists) { - let newId = bms.createFolder(root, title, bms.DEFAULT_INDEX); + let newId = this._bms.createFolder(root, title, this._bms.DEFAULT_INDEX); // Keep track of who shared this folder with us... this._annoSvc.setItemAnnotation(newId, INCOMING_SHARED_ANNO, @@ -607,22 +603,19 @@ BookmarksSharingManager.prototype = { }, _stopIncomingShare: function BmkSharing__stopIncomingShare(user, - serverPath, - folderName) + serverPath, + folderName) { /* Delete the incoming share folder. Since the update of incoming folders * is triggered when the engine spots a folder with a certain annotation on * it, just getting rid of this folder is all we need to do. */ - let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - let a = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, {}); for (let i = 0; i < a.length; i++) { let creator = this._annoSvc.getItemAnnotation(a[i], OUTGOING_SHARED_ANNO); let path = this._annoSvc.getItemAnnotation(a[i], SERVER_PATH_ANNO); if ( creator == user && path == serverPath ) { - bms.removeFolder( a[i]); + this._bms.removeFolder( a[i]); } } } From 465ec2d28da91558e54d55fd123571475fdb4c53 Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Jul 2008 10:24:52 -0700 Subject: [PATCH 0564/1860] Fixed typo in BookmarkSharingManager._bms initialization --- services/sync/modules/engines/bookmarks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5c8f53450dfd..b8198eeef133 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -84,7 +84,7 @@ BookmarksSharingManager.prototype = { __bms: null, get _bms() { if (!this.__bms) - this._bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); return this.__bms; }, From 1eadd8d429ef93cc74d6dd00568313b8ef14c1bf Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Jul 2008 10:26:01 -0700 Subject: [PATCH 0565/1860] Fixed another typo --- services/sync/modules/engines/bookmarks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index b8198eeef133..18bc0ba2bfbe 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -340,7 +340,7 @@ BookmarksSharingManager.prototype = { _updateOutgoingShare().) */ let self = yield; - let myUserName = fID.get('WeaveID').username; + let myUserName = ID.get('WeaveID').username; this._log.debug("Turning folder " + folderName + " into outgoing share" + + " with " + username); From 6a2251ad043edf578fce594382af353677ee2ffd Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Jul 2008 10:27:19 -0700 Subject: [PATCH 0566/1860] Fixed missing import --- services/sync/modules/engines/bookmarks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 18bc0ba2bfbe..715bfc079466 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -66,6 +66,7 @@ Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/xmpp/xmppClient.js"); Cu.import("resource://weave/notifications.js"); Cu.import("resource://weave/sharing.js"); +Cu.import("resource://weave/remote.js"); Function.prototype.async = Async.sugar; From 8618ce84d783ce375cd7f058eeb0af13eb70bd86 Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Jul 2008 10:31:16 -0700 Subject: [PATCH 0567/1860] Fixed typo where undefined value was getting passed into share and stopSharing: should be node.itemId, not node.id --- services/sync/modules/engines/bookmarks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 715bfc079466..0df5e16eb25c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -250,6 +250,7 @@ BookmarksSharingManager.prototype = { _stopSharing: function BmkSharing__stopSharing( folderId, username ) { let self = yield; + dump("folderId is " + folderId + "\n"); let folderName = this._bms.getItemTitle(folderId); if (this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)){ From e5899a4db4db1fdf89e78dc94e21d01a7c94d4ee Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Jul 2008 10:35:15 -0700 Subject: [PATCH 0568/1860] Fixed yet another typo: this._sharing._share, not this._sharing.share. --- services/sync/modules/engines/bookmarks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 0df5e16eb25c..6776284d5b60 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -676,7 +676,7 @@ BookmarksEngine.prototype = { _share: function BmkEngine__share(guid, username) { let self = yield; - this._sharing.share.async( this._sharing, self.cb, guid, username); + this._sharing._share.async( this._sharing, self.cb, guid, username); yield; self.done(true); }, From 5f6f31308ef3b40a584be9699ea06a94f9688489 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 1 Jul 2008 11:12:00 -0700 Subject: [PATCH 0569/1860] Tracebacks for async coroutines now provide a 'best guess' for where the coroutine was at the time that an exception was thrown, by showing the frame at which the generator's last continuation callback was created. Added a 'location' property to RequestException, analogous to the 'location' property of wrapped nsIExceptions, so that tracebacks can be made for the exceptions if needed. --- services/sync/modules/async.js | 8 +++++++- services/sync/modules/remote.js | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index d052df52926a..685aaf691078 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -115,6 +115,7 @@ function Generator(thisArg, method, onComplete, args) { this._id = gCurrentId++; this.onComplete = onComplete; this._args = args; + this._stackAtLastCallbackGen = null; gOutstandingGenerators.add(this); @@ -132,6 +133,7 @@ Generator.prototype = { let caller = Components.stack.caller; let cbId = gCurrentCbId++; this._outstandingCbs++; + this._stackAtLastCallbackGen = caller; this._log.trace(this.name + ": cb-" + cbId + " generated at:\n" + formatAsyncFrame(caller)); let self = this; @@ -172,7 +174,11 @@ Generator.prototype = { }, get asyncStack() { - return ("unknown (async) :: " + this.name + "\n" + + let cbGenText = ""; + if (this._stackAtLastCallbackGen) + cbGenText = (" (last self.cb generated at " + + formatAsyncFrame(this._stackAtLastCallbackGen) + ")"); + return ("unknown (async) :: " + this.name + cbGenText + "\n" + traceAsyncFrame(this._initFrame)); }, diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 4e8f2b984707..e152b83e00ab 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -58,6 +58,7 @@ function RequestException(resource, action, request) { this._resource = resource; this._action = action; this._request = request; + this.location = Components.stack.caller; } RequestException.prototype = { get resource() { return this._resource; }, From bcfc7c394d497812aa03491892e25cb1a4a587d1 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Tue, 1 Jul 2008 11:29:20 -0700 Subject: [PATCH 0570/1860] make sure nsIObserverService notifications have subjects before accessing the subjects' wrappedJSObject properties --- services/sync/modules/Observers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/Observers.js b/services/sync/modules/Observers.js index e98281158ad5..947b986ded51 100644 --- a/services/sync/modules/Observers.js +++ b/services/sync/modules/Observers.js @@ -81,7 +81,7 @@ Observer.prototype = { // Pass the wrappedJSObject for subjects that have one. Otherwise pass // the subject itself. This way we support both wrapped subjects created // using this module and those that are real XPCOM components. - if (subject.wrappedJSObject) + if (subject && subject.wrappedJSObject) this._callback(subject.wrappedJSObject, topic, data); else this._callback(subject, topic, data); From 7621d68c998863e9e64b425ac83e1714df53c5e5 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 1 Jul 2008 12:03:05 -0700 Subject: [PATCH 0571/1860] Refactored the exception-handling code a bit and made traces produced by async look more like they used to. --- services/sync/modules/async.js | 39 ++++++++++++---------------------- services/sync/modules/util.js | 29 +++++++++++++++++++------ 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 685aaf691078..ef7fe4acc7f8 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -119,11 +119,7 @@ function Generator(thisArg, method, onComplete, args) { gOutstandingGenerators.add(this); - this._initFrame = Components.stack.caller; - // skip our frames - // FIXME: we should have a pref for this, for debugging async.js itself - while (this._initFrame.name.match(/^Async(Gen|)_/)) - this._initFrame = this._initFrame.caller; + this._initFrame = skipAsyncFrames(Components.stack.caller); } Generator.prototype = { get name() { return this._method.name + "-" + this._id; }, @@ -178,8 +174,11 @@ Generator.prototype = { if (this._stackAtLastCallbackGen) cbGenText = (" (last self.cb generated at " + formatAsyncFrame(this._stackAtLastCallbackGen) + ")"); + + let frame = skipAsyncFrames(this._initFrame); + return ("unknown (async) :: " + this.name + cbGenText + "\n" + - traceAsyncFrame(this._initFrame)); + Utils.stackTraceFromFrame(frame, formatAsyncFrame)); }, _handleException: function AsyncGen__handleException(e) { @@ -206,7 +205,7 @@ Generator.prototype = { this._log.warn("Exception: " + Utils.exceptionStr(e)); } else { this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.debug("Stack trace:\n" + Utils.stackTrace(e)); + this._log.debug("Stack trace:\n" + Utils.stackTrace(e, formatAsyncFrame)); } // continue execution of caller. @@ -301,7 +300,8 @@ Generator.prototype = { this._log.error("Exception caught from onComplete handler of " + this.name + " generator"); this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.trace("Current stack trace:\n" + Utils.stackTrace(e)); + this._log.trace("Current stack trace:\n" + + Utils.stackTrace(e, formatAsyncFrame)); this._log.trace("Initial async stack trace:\n" + this.asyncStack); } } @@ -309,6 +309,12 @@ Generator.prototype = { } }; +function skipAsyncFrames(frame) { + while (frame.name && frame.name.match(/^Async(Gen|)_/)) + frame = frame.caller; + return frame; +} + function formatAsyncFrame(frame) { // FIXME: sort of hackish, might be confusing if there are multiple // extensions with similar filenames @@ -319,23 +325,6 @@ function formatAsyncFrame(frame) { return tmp; } -function traceAsyncFrame(frame, str) { - if (!str) - str = ""; - - // skip our frames - // FIXME: we should have a pref for this, for debugging async.js itself - while (frame.name && frame.name.match(/^Async(Gen|)_/)) - frame = frame.caller; - - if (frame.caller) - str = traceAsyncFrame(frame.caller, str); - str = formatAsyncFrame(frame) + (str? "\n" : "") + str; - - return str; -} - - Async = { get outstandingGenerators() { return gOutstandingGenerators; }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 52079f0f20da..dcaafa6f39f3 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -218,17 +218,32 @@ let Utils = { return message + location; }, - stackTrace: function Weave_stackTrace(e) { + stackTraceFromFrame: function Weave_stackTraceFromFrame(frame, formatter) { + if (!formatter) + formatter = function defaultFormatter(frame) { return frame; }; + + let output = ""; + + while (frame) { + output += formatter(frame) + "\n"; + frame = frame.caller; + } + + return output; + }, + + stackTrace: function Weave_stackTrace(e, formatter) { let output = ""; if (e.location) { // It's a wrapped nsIException. - let frame = e.location; - while (frame) { - output += frame + "\n"; - frame = frame.caller; - } + output += this.stackTraceFromFrame(e.location, formatter); } else if (e.stack) - // It's a standard JS exception + // It's a standard JS exception. + + // TODO: It would be nice if we could parse this string and + // create a 'fake' nsIStackFrame-like call stack out of it, + // so that we can do things w/ this stack trace like we do + // with nsIException traces. output += e.stack; else // It's some other thrown object, e.g. a bare string. From 0b61dda20334969132ea03f7b87e7896dfb914d1 Mon Sep 17 00:00:00 2001 From: Date: Tue, 1 Jul 2008 14:41:04 -0700 Subject: [PATCH 0572/1860] Updated a couple of comments in bookmark sharing --- services/sync/modules/engines/bookmarks.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 6776284d5b60..49b1257e51a7 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -339,7 +339,9 @@ BookmarksSharingManager.prototype = { shared with. This function creates the directory and keyring on the server in which the shared data will be put, but it doesn't actually put the bookmark data there (that's done in - _updateOutgoingShare().) */ + _updateOutgoingShare().) + Returns a string which is the path on the server to the new share + directory, or false if it failed.*/ let self = yield; let myUserName = ID.get('WeaveID').username; @@ -383,8 +385,8 @@ BookmarksSharingManager.prototype = { /* Get public keys for me and the user I'm sharing with. Each user's public key is stored in /user/username/public/pubkey. */ - let idRSA = ID.get('WeaveCryptoID'); // TODO Can get error "Resource not defined" - let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); + let idRSA = ID.get('WeaveCryptoID'); + let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); // get a 401? userPubKeyFile.get(self.cb); let userPubKey = yield; From a8f8dda2f59873a16684a9ac8a4328861e89d95d Mon Sep 17 00:00:00 2001 From: Date: Thu, 3 Jul 2008 17:57:21 -0700 Subject: [PATCH 0573/1860] Fixed a whole bunch of bugs in bookmark share, mostly related to adding in needed JsonFilters for remote Resources and fixing the server paths which are passed into DAV for the keyring file and encrypted share data files. (Also fixed a minor bug in DAV itself so that mkcol won't quit right away if you pass it something that starts with a slash.) --- services/sync/modules/dav.js | 2 +- services/sync/modules/engines/bookmarks.js | 131 +++++++++++++-------- services/sync/modules/remote.js | 2 +- services/sync/modules/sharing.js | 2 +- 4 files changed, 86 insertions(+), 51 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index a4ad645c6045..556e2e630012 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -183,7 +183,7 @@ DAVCollection.prototype = { // trailing slashes will cause an empty path component at the end if (components[i] == '') - break; + continue; path2 = path2 + components[i]; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 49b1257e51a7..9d8643ac9081 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -217,7 +217,7 @@ BookmarksSharingManager.prototype = { this._createOutgoingShare.async( this, self.cb, folderId, folderName, username ); let serverPath = yield; - dump("in _share: annotated with serverPath = \" + serverPath + \"\n"); + dump("in _share: annotated with serverPath = " + serverPath + "\n"); this._updateOutgoingShare.async( this, self.cb, folderId ); yield; @@ -230,6 +230,7 @@ BookmarksSharingManager.prototype = { this._annoSvc.EXPIRE_NEVER); // Send an xmpp message to the share-ee if ( this._xmppClient ) { + // TODO include my username here: /user/myusername/ + serverPath if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { let msgText = "share " + serverPath + " " + folderName; this._log.debug( "Sending XMPP message: " + msgText ); @@ -330,46 +331,15 @@ BookmarksSharingManager.prototype = { self.done(); }, - _createOutgoingShare: function BmkSharing__createOutgoing(folderId, - folderName, - username) { - /* To be called asynchronously. FolderId is the integer id of the - bookmark folder to be shared and folderName is a string of its - title. username is a string indicating the user that it is to be - shared with. This function creates the directory and keyring on - the server in which the shared data will be put, but it doesn't - actually put the bookmark data there (that's done in - _updateOutgoingShare().) - Returns a string which is the path on the server to the new share - directory, or false if it failed.*/ - + _createKeyChain: function BmkSharing__createKeychain(serverPath, + myUserName, + username){ + /* Creates a new keychain file on the server, inside the directory given + * by serverPath. The keychain file contains keys for both me (the + * current user) and for the friend specified by username (which must + * be a valid weave user id.) + */ let self = yield; - let myUserName = ID.get('WeaveID').username; - this._log.debug("Turning folder " + folderName + " into outgoing share" + - + " with " + username); - - /* Generate a new GUID to use as the new directory name on the server - in which we'll store the shared data. */ - let folderGuid = Utils.makeGUID(); - - /* Create the directory on the server if it does not exist already. */ - let serverPath = "/user/" + myUserName + "/share/" + folderGuid; - DAV.MKCOL(serverPath, self.cb); - let ret = yield; - if (!ret) { - this._log.error("Can't create remote folder for outgoing share."); - self.done(false); - } - // TODO more error handling - - /* Store the path to the server directory in an annotation on the shared - bookmark folder, so we can easily get back to it later. */ - this._annoSvc.setItemAnnotation(folderId, - SERVER_PATH_ANNO, - serverPath, - 0, - this._annoSvc.EXPIRE_NEVER); - // Create a new symmetric key, to be used only for encrypting this share. // XXX HACK. Seems like the engine shouldn't have to be doing any of this, or // should use its own identity here. @@ -386,15 +356,29 @@ BookmarksSharingManager.prototype = { /* Get public keys for me and the user I'm sharing with. Each user's public key is stored in /user/username/public/pubkey. */ let idRSA = ID.get('WeaveCryptoID'); - let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); // get a 401? + let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); + userPubKeyFile.pushFilter( new JsonFilter() ); + // The above can get a 401 if .htaccess file isnot set to give public + //access to the "public" directory. It can also get a 502? userPubKeyFile.get(self.cb); let userPubKey = yield; + userPubKey = userPubKey.pubkey; + + /* 2008-07-03 15:49:41 + * Async.Generator ERROR Exception: Component returned failure + * code: 0x80070057 (NS_ERROR_ILLEGAL_VALUE) [IWeaveCrypto.wrapSymmetricKey] + * (JS frame :: file:///Users/jonathandicarlo/weave/modules/crypto.js :: Crypto_wrapKey :: line 216) + */ /* Create the keyring, containing the sym key encrypted with each of our public keys: */ - Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", pubkey: idRSA.pubkey} ); + dump( "Calling crypto to wrap sym key with my public key.\n" ); + Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", + pubkey: idRSA.pubkey} ); let encryptedForMe = yield; - Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", pubkey: userPubKey} ); + dump( "Calling crypto to wrap sym key with sharee's public key.\n" ); + Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", + pubkey: userPubKey} ); let encryptedForYou = yield; let keys = { ring : { }, @@ -404,15 +388,64 @@ BookmarksSharingManager.prototype = { keys.ring[username] = encryptedForYou; let keyringFile = new Resource( serverPath + "/" + KEYRING_FILE_NAME ); - let jsonService = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - keyringFile.put( self.cb, jsonService.encode( keys ) ); + keyringFile.pushFilter(new JsonFilter()); + keyringFile.put( self.cb, keys); yield; + self.done(); + }, + + _createOutgoingShare: function BmkSharing__createOutgoing(folderId, + folderName, + username) { + /* To be called asynchronously. FolderId is the integer id of the + bookmark folder to be shared and folderName is a string of its + title. username is a string indicating the user that it is to be + shared with. This function creates the directory and keyring on + the server in which the shared data will be put, but it doesn't + actually put the bookmark data there (that's done in + _updateOutgoingShare().) + Returns a string which is the path on the server to the new share + directory, or false if it failed.*/ + + let self = yield; + let myUserName = ID.get('WeaveID').username; + this._log.debug("Turning folder " + folderName + " into outgoing share" + + " with " + username); + + /* Generate a new GUID to use as the new directory name on the server + in which we'll store the shared data. */ + let folderGuid = Utils.makeGUID(); + + /* Create the directory on the server if it does not exist already. */ + let serverPath = "share/" + folderGuid; + dump( "Trying to create " + serverPath + "\n"); + let ret = yield DAV.MKCOL(serverPath, self.cb); + + if (!ret) { + this._log.error("Can't create remote folder for outgoing share."); + self.done(false); + } + // TODO more error handling + + /* Store the path to the server directory in an annotation on the shared + bookmark folder, so we can easily get back to it later. */ + this._annoSvc.setItemAnnotation(folderId, + SERVER_PATH_ANNO, + serverPath, + 0, + this._annoSvc.EXPIRE_NEVER); + + let encryptionTurnedOn = true; + if (encryptionTurnedOn) { + yield this._createKeyChain.async(this, self.cb, serverPath, myUserName, username); + } + // Call Atul's js api for setting htaccess: let sharingApi = new Sharing.Api( DAV ); - sharingApi.shareWithUsers( serverPath, [username], self.cb ); - let result = yield; - + let result = yield sharingApi.shareWithUsers( serverPath, + [username], + self.cb ); // return the server path: self.done( serverPath ); }, @@ -433,7 +466,9 @@ BookmarksSharingManager.prototype = { // there. // From that directory, get the keyring file, and from it, the symmetric // key that we'll use to encrypt. + dump( "in _updateOutgoingShare. serverPath is " + serverPath +"\n"); let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); + keyringFile.pushFilter(new JsonFilter()); keyringFile.get(self.cb); let keys = yield; diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index e152b83e00ab..798dd862d9ca 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Resource', 'RemoteStore']; +const EXPORTED_SYMBOLS = ['Resource', 'RemoteStore', 'JsonFilter']; const Cc = Components.classes; const Ci = Components.interfaces; diff --git a/services/sync/modules/sharing.js b/services/sync/modules/sharing.js index 2e40a5691058..54f02011c50d 100644 --- a/services/sync/modules/sharing.js +++ b/services/sync/modules/sharing.js @@ -69,7 +69,7 @@ Api.prototype = { let jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); let json = jsonSvc.encode(cmd); - this._dav.POST("share/", + this._dav.POST("/api/share/", ("cmd=" + escape(json) + "&uid=" + escape(id.username) + "&password=" + escape(id.password)), From 082cc5dda35596a4367ba1eebd0f03fc5d7f7781 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 7 Jul 2008 22:30:32 -0700 Subject: [PATCH 0574/1860] Bug 443385: unconditionally remove saved local token, regardless of whether the UNLOCK command succeeded; also cleans up some variable naming re: local locks in dav.js; cosmetic changes in wrap.js --- services/sync/modules/dav.js | 50 +++++++++++++++++--------------- services/sync/modules/service.js | 2 +- services/sync/modules/wrap.js | 6 ++-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 556e2e630012..71aab97bb68d 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -103,16 +103,14 @@ DAVCollection.prototype = { }, get locked() { - return !this._lockAllowed || (DAVLocks['default'] && - DAVLocks['default'].token); + return !this._allowLock || (DAVLocks['default'] && + DAVLocks['default'].token); }, - _lockAllowed: true, - get allowLock() { - return this._lockAllowed; - }, + _allowLock: true, + get allowLock() this._allowLock, set allowLock(value) { - this._lockAllowed = value; + this._allowLock = value; }, _makeRequest: function DC__makeRequest(op, path, headers, data) { @@ -354,13 +352,13 @@ DAVCollection.prototype = { let self = yield; this._log.trace("Acquiring lock"); - if (!this._lockAllowed) + if (!this._allowLock) throw {message: "Cannot acquire lock (internal lock)"}; - this._lockAllowed = false; + this._allowLock = false; if (DAVLocks['default']) { this._log.debug("Lock called, but we already hold a token"); - this._lockAllowed = true; + this._allowLock = true; self.done(); return; } @@ -374,7 +372,7 @@ DAVCollection.prototype = { let resp = yield; if (resp.status < 200 || resp.status >= 300) { - this._lockAllowed = true; + this._allowLock = true; return; } @@ -389,13 +387,13 @@ DAVCollection.prototype = { if (!DAVLocks['default']) { this._log.warn("Could not acquire lock"); - this._lockAllowed = true; + this._allowLock = true; self.done(); return; } this._log.trace("Lock acquired"); - this._lockAllowed = true; + this._allowLock = true; self.done(DAVLocks['default']); }, @@ -408,20 +406,24 @@ DAVCollection.prototype = { if (!this.locked) { this._log.debug("Unlock called, but we don't hold a token right now"); self.done(true); - yield; - } - - this.UNLOCK("lock", self.cb); - let resp = yield; - - if (resp.status < 200 || resp.status >= 300) { - self.done(false); - yield; + return; } + // Do this unconditionally, since code that calls unlock() doesn't + // really have much of an option if unlock fails. The only thing + // to do is wait for it to time out (and hope it didn't really + // fail) delete DAVLocks['default']; - this._log.trace("Lock released (or we didn't have one)"); - self.done(true); + + let resp = yield this.UNLOCK("lock", self.cb); + + if (Utils.checkStatus(resp.status)) { + this._log.trace("Lock released"); + self.done(true); + } else { + this._log.trace("Failed to release lock"); + self.done(false); + } }, forceUnlock: function DC_forceUnlock() { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5ee680a7e123..2c13d10ef653 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -264,7 +264,7 @@ WeaveSvc.prototype = { _onSchedule: function WeaveSvc__onSchedule() { if (this.enabled) { - if (!DAV.allowLock) { + if (DAV.locked) { this._log.info("Skipping scheduled sync; local operation in progress") } else { this._log.info("Running scheduled sync"); diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 7d85ebe2b9c7..7d11f5881a32 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -121,8 +121,7 @@ let Wrap = { if (DAV.locked) throw "Could not acquire lock (lock already held)"; - DAV.lock.async(DAV, self.cb); - let locked = yield; + let locked = yield DAV.lock.async(DAV, self.cb); if (!locked) throw "Could not acquire lock"; @@ -131,8 +130,7 @@ let Wrap = { try { args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); - Async.run.apply(Async, args); - ret = yield; + ret = yield Async.run.apply(Async, args); } catch (e) { throw e; From e08cd10cf4bd10deefb57704efdb6be97f740346 Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 01:18:30 -0700 Subject: [PATCH 0575/1860] Fixed a bunch of bugs in bookmark share: stuf related to server-side paths being incorrect (because the defaultPrefix in DAV was getting reset to an empty string or was getting applied wrong). Discovered that updateOutgoingShare and updateIncomingShare were both calling the same _wrapMount() function, which was incorrect, so I broke that up and fixed what parts are meant to be outgoing and which incoming. --- services/sync/modules/dav.js | 6 +++ services/sync/modules/engines/bookmarks.js | 45 +++++++++++++--------- services/sync/modules/sharing.js | 2 - 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 556e2e630012..7d3958278ecd 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -122,7 +122,13 @@ DAVCollection.prototype = { this._log.debug(op + " request for " + (path? path : 'root folder')); if (!path || path[0] != '/') + // if it's a relative path, (no slash), prepend default prefix path = this._defaultPrefix + path; + else + path = path.slice(1); // if absolute path, remove leading slash + // path at this point should have no leading slash. + dump("DefaultPrefix is " + this._defaultPrefix + "\n"); + dump(" In _makeRequest, after fixing the path, it is " + path +"\n"); let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 9d8643ac9081..f06b0e881453 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -469,6 +469,9 @@ BookmarksSharingManager.prototype = { dump( "in _updateOutgoingShare. serverPath is " + serverPath +"\n"); let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.pushFilter(new JsonFilter()); + // TODO request for share/a317b645-2c2f-6946-aea0-d728509091d4/keyring + // fails with a 404 even though THE FILE IS THERE. Maybe it's not prepending this + // with /user/jono like it should be??? put debugging info into DAV.GET? keyringFile.get(self.cb); let keys = yield; @@ -479,10 +482,7 @@ BookmarksSharingManager.prototype = { let bulkIV = keys.bulkIV; // Get the json-wrapped contents of everything in the folder: - // TODO what exactly does wrapMount expect? is folderId OK? - let json = this._engine._store._wrapMount( folderId, myUserName ); - /* TODO what does wrapMount do with this username? Should I be passing - in my own or that of the person I share with? */ + let json = this._engine._store._wrapMountOutgoing(folderId); // Encrypt it with the symkey and put it into the shared-bookmark file. let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); @@ -598,8 +598,7 @@ BookmarksSharingManager.prototype = { let myUserName = ID.get('WeaveID').username; // The folder has an annotation specifying the server path to the // directory: - let serverPath = this._annoSvc.getItemAnnotation(mountData.node, - SERVER_PATH_ANNO); + let serverPath = mountData.serverPath; // From that directory, get the keyring file, and from it, the symmetric // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); @@ -608,6 +607,7 @@ BookmarksSharingManager.prototype = { // Decrypt the contents of the bookmark file with the symmetric key: let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); + bmkFile.pushFilter( new JsonFilter() ); bmkFile.get(self.cb); let cyphertext = yield; let tmpIdentity = { @@ -627,10 +627,15 @@ BookmarksSharingManager.prototype = { json[guid].parentGUID = mountData.rootGUID; } + // TODO check what the inputs to detectUpdates should be. Should I be + // passing in the current subtree of mountData.node? Do I need to wipe + // that subtree before calling diff? I'm trying to create a diff + // here between json and nothing so as to come up with the createCommands + // needed to create all the bookmarks. /* Create diff between the json from server and the current contents; then apply the diff. */ this._log.trace("Got bookmarks from " + user + ", comparing with local copy"); - this._engine._core.detectUpdates(self.cb, mountData.snapshot, snap.data); + this._engine._core.detectUpdates(self.cb, json, {}); let diff = yield; // FIXME: should make sure all GUIDs here live under the mountpoint @@ -1230,27 +1235,25 @@ BookmarksStore.prototype = { return this.__wrap(node, items, null, null, rootName); }, - _wrapMount: function BStore__wrapMount(node, id) { + _wrapMountOutgoing: function BStore__wrapById( itemId ) { if (node.type != node.RESULT_TYPE_FOLDER) throw "Trying to wrap a non-folder mounted share"; - - let GUID = this._bms.getItemGUID(node.itemId); - let ret = {rootGUID: GUID, userid: id, snapshot: {}, folderNode: node}; - + let node = this._getNode(itemId); + let GUID = this._bms.getItemGUID(itemId); + let snapshot = {}; node.QueryInterface(Ci.nsINavHistoryQueryResultNode); node.containerOpen = true; for (var i = 0; i < node.childCount; i++) { - this.__wrap(node.getChild(i), ret.snapshot, GUID, i); + this.__wrap(node.getChild(i), snapshot, GUID, i); } // remove any share mountpoints - for (let guid in ret.snapshot) { + for (let guid in snapshot) { // TODO decide what to do with this... if (ret.snapshot[guid].type == "incoming-share") - delete ret.snapshot[guid]; + delete snapshot[guid]; } - - return ret; + return snapshot; }, findIncomingShares: function BStore_findIncomingShares() { @@ -1261,8 +1264,12 @@ BookmarksStore.prototype = { for (let i = 0; i < a.length; i++) { /* The value of the incoming-shared annotation is the id of the person who has shared it with us. Get that value: */ - let id = this._ans.getItemAnnotation(a[i], INCOMING_SHARED_ANNO); - ret.push(this._wrapMount(this._getNode(a[i]), id)); + let userId = this._ans.getItemAnnotation(a[i], INCOMING_SHARED_ANNO); + let node = this._getNode(a[i]); + let GUID = this._bms.getItemGUID(a[i]); + let path = this._ans.getItemAnnotation(a[i], SERVER_PATH_ANNO); + let dat = {rootGUID: GUID, userid: userId, serverPath: path, node: node}; + ret.push(dat); } return ret; }, diff --git a/services/sync/modules/sharing.js b/services/sync/modules/sharing.js index 54f02011c50d..794febc5d8c8 100644 --- a/services/sync/modules/sharing.js +++ b/services/sync/modules/sharing.js @@ -61,8 +61,6 @@ Api.prototype = { let self = yield; let id = ID.get(this._dav.identity); - this._dav.defaultPrefix = ""; - let cmd = {"version" : 1, "directory" : path, "share_to_users" : users}; From 571ead9e026b8d181ff325a47839cb13dc0ecc62 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 17:44:00 -0700 Subject: [PATCH 0576/1860] keep track of errors during sync and throw after trying all engines --- services/sync/modules/service.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2c13d10ef653..4364fad60a02 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -663,6 +663,11 @@ WeaveSvc.prototype = { yield this._notify(engines[i].name + "-engine:sync", this._syncEngine, engines[i]).async(this, self.cb); } + + if (this._syncError) { + this._syncError = false; + throw "Some engines did not sync correctly"; + } }, // The values that engine scores must meet or exceed before we sync them @@ -716,6 +721,11 @@ WeaveSvc.prototype = { this._syncThresholds[engine.name] = 1; } } + + if (this._syncError) { + this._syncError = false; + throw "Some engines did not sync correctly"; + } }, _syncEngine: function WeaveSvc__syncEngine(engine) { @@ -726,6 +736,7 @@ WeaveSvc.prototype = { } catch(e) { this._log.error(Utils.exceptionStr(e)); this._log.error(Utils.stackTrace(e)); + this._syncError = true; } }, From 79ccb334f4fab7590134c563c3501c486d343fa2 Mon Sep 17 00:00:00 2001 From: Pazu Date: Tue, 8 Jul 2008 15:33:17 -0300 Subject: [PATCH 0577/1860] Performance improvements: keypair is now fetched only once, and cached in memory. --- services/sync/modules/service.js | 37 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4364fad60a02..077ece40f6c8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -103,6 +103,8 @@ Utils.lazy(Weave, 'Service', WeaveSvc); * Main entry point into Weave's sync framework */ +let KeyPair = {}; + function WeaveSvc(engines) { this._startupFinished = false; this._initLogs(); @@ -400,23 +402,32 @@ WeaveSvc.prototype = { // XXX this kind of replaces _keyCheck // seems like key generation should only happen during setup? - DAV.GET("private/privkey", self.cb); - let privkeyResp = yield; - Utils.ensureStatus(privkeyResp.status, - "Could not get private key from server", statuses); - DAV.GET("public/pubkey", self.cb); - let pubkeyResp = yield; - Utils.ensureStatus(pubkeyResp.status, - "Could not get public key from server", statuses); + if (!(KeyPair['private'] && KeyPair['public'])) { + this._log.info("Fetching keypair from server."); - if (privkeyResp.status == 404 || pubkeyResp.status == 404) { - yield this._generateKeys.async(this, self.cb); - return; + DAV.GET("private/privkey", self.cb); + let privkeyResp = yield; + Utils.ensureStatus(privkeyResp.status, + "Could not get private key from server", statuses); + KeyPair['private'] = this._json.decode(privkeyResp.responseText); + + DAV.GET("public/pubkey", self.cb); + let pubkeyResp = yield; + Utils.ensureStatus(pubkeyResp.status, + "Could not get public key from server", statuses); + KeyPair['public'] = this._json.decode(pubkeyResp.responseText); + + if (privkeyResp.status == 404 || pubkeyResp.status == 404) { + yield this._generateKeys.async(this, self.cb); + return; + } + } else { + this._log.info("Using cached keypair"); } - let privkeyData = this._json.decode(privkeyResp.responseText); - let pubkeyData = this._json.decode(pubkeyResp.responseText); + let privkeyData = KeyPair['private'] + let pubkeyData = KeyPair['public']; if (!privkeyData || !pubkeyData) throw "Bad keypair JSON"; From 3af51cfa2fe794c4d8df9e4ebce06ecf51a96e9e Mon Sep 17 00:00:00 2001 From: Pazu Date: Tue, 8 Jul 2008 17:06:21 -0300 Subject: [PATCH 0578/1860] Clear KeyPair cache on server wipe, and make new key generation work again. --- services/sync/modules/service.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 077ece40f6c8..5b4844e39545 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -410,18 +410,19 @@ WeaveSvc.prototype = { let privkeyResp = yield; Utils.ensureStatus(privkeyResp.status, "Could not get private key from server", statuses); - KeyPair['private'] = this._json.decode(privkeyResp.responseText); DAV.GET("public/pubkey", self.cb); let pubkeyResp = yield; Utils.ensureStatus(pubkeyResp.status, "Could not get public key from server", statuses); - KeyPair['public'] = this._json.decode(pubkeyResp.responseText); if (privkeyResp.status == 404 || pubkeyResp.status == 404) { yield this._generateKeys.async(this, self.cb); return; } + + KeyPair['private'] = this._json.decode(privkeyResp.responseText); + KeyPair['public'] = this._json.decode(pubkeyResp.responseText); } else { this._log.info("Using cached keypair"); } @@ -643,6 +644,7 @@ WeaveSvc.prototype = { _serverWipe: function WeaveSvc__serverWipe() { let self = yield; + KeyPair = {}; DAV.listFiles.async(DAV, self.cb); let names = yield; From fbdc3dd5301d38909eb9f03e10c412da413c9fa0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 18:37:14 -0700 Subject: [PATCH 0579/1860] Move KeyPair object (which caches keys) to a property of the service. Clear it on logout --- services/sync/modules/service.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5b4844e39545..0b8a11a6fa44 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -103,8 +103,6 @@ Utils.lazy(Weave, 'Service', WeaveSvc); * Main entry point into Weave's sync framework */ -let KeyPair = {}; - function WeaveSvc(engines) { this._startupFinished = false; this._initLogs(); @@ -175,6 +173,9 @@ WeaveSvc.prototype = { return this.__json; }, + // object for caching public and private keys + _keyPair: {}, + // Timer object for automagically syncing _scheduleTimer: null, @@ -403,7 +404,7 @@ WeaveSvc.prototype = { // XXX this kind of replaces _keyCheck // seems like key generation should only happen during setup? - if (!(KeyPair['private'] && KeyPair['public'])) { + if (!(this._keyPair['private'] && this._keyPair['public'])) { this._log.info("Fetching keypair from server."); DAV.GET("private/privkey", self.cb); @@ -421,14 +422,14 @@ WeaveSvc.prototype = { return; } - KeyPair['private'] = this._json.decode(privkeyResp.responseText); - KeyPair['public'] = this._json.decode(pubkeyResp.responseText); + this._keyPair['private'] = this._json.decode(privkeyResp.responseText); + this._keyPair['public'] = this._json.decode(pubkeyResp.responseText); } else { this._log.info("Using cached keypair"); } - let privkeyData = KeyPair['private'] - let pubkeyData = KeyPair['public']; + let privkeyData = this._keyPair['private'] + let pubkeyData = this._keyPair['public']; if (!privkeyData || !pubkeyData) throw "Bad keypair JSON"; @@ -617,6 +618,7 @@ WeaveSvc.prototype = { this._log.info("Logging out"); this._disableSchedule(); this._loggedIn = false; + this._keyPair = {}; ID.get('WeaveID').setTempPassword(null); // clear cached password ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase this._os.notifyObservers(null, "weave:service:logout:success", ""); @@ -644,7 +646,7 @@ WeaveSvc.prototype = { _serverWipe: function WeaveSvc__serverWipe() { let self = yield; - KeyPair = {}; + this._keyPair = {}; DAV.listFiles.async(DAV, self.cb); let names = yield; From 7a62c282b3f65e6b6ed2e6cdfba9dd4dfbc34bc7 Mon Sep 17 00:00:00 2001 From: Pazu Date: Tue, 8 Jul 2008 15:31:47 -0300 Subject: [PATCH 0580/1860] Sync on quit improvements. A new preference called "waitOnQuit.enabled" was created. This preference controls whether Weave will perform any actions on quit. If false, Weave won't perform any actions on quit, nor wait for any running actions. If true, Weave will wait for any running sync to finish. The old "syncOnQuit.enabled" preference now controls if Weave will actually perform a full sync on quit or not. If false, no sync is started, but we still wait for an active sync to finish, if waitOnQuit.enable is true. Both options default to true. --- services/sync/locales/en-US/status.properties | 1 + services/sync/modules/service.js | 2 +- services/sync/services-sync.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index 41b71288045d..76c5f9ecd670 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -1,3 +1,4 @@ +status.wait = Waiting for current sync to finish... status.active = Syncing with Weave... status.success = Sync succeeded. status.error = Sync failed. diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0b8a11a6fa44..158e620da2d0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -518,7 +518,7 @@ WeaveSvc.prototype = { _onQuitApplication: function WeaveSvc__onQuitApplication() { if (!this.enabled || - !Utils.prefs.getBoolPref("syncOnQuit.enabled") || + !Utils.prefs.getBoolPref("waitOnQuit.enabled") || !this._loggedIn) return; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index a75d25847eab..89fb3734997c 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -13,6 +13,7 @@ pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); pref("extensions.weave.schedule", 1); +pref("extensions.weave.waitOnQuit.enabled", true); pref("extensions.weave.syncOnQuit.enabled", true); pref("extensions.weave.engine.bookmarks", true); From 900be9f405165ad1fa790cab92145f4948bac5a8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 11:58:59 -0700 Subject: [PATCH 0581/1860] check that we have a local lock token before trying to delete it (or 'delete' will fail) --- services/sync/modules/dav.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 71aab97bb68d..d589e1c6ce54 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -413,7 +413,8 @@ DAVCollection.prototype = { // really have much of an option if unlock fails. The only thing // to do is wait for it to time out (and hope it didn't really // fail) - delete DAVLocks['default']; + if (DAVLocks['default']) + delete DAVLocks['default']; let resp = yield this.UNLOCK("lock", self.cb); From aaf401ef0bae683d8fcd0a1ca2232079e29fff79 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 13:56:03 -0700 Subject: [PATCH 0582/1860] sigh, my previous unlock change actually breaks unlock. fix fix fix. --- services/sync/modules/dav.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index e8b031a46865..02080db628d7 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -415,21 +415,27 @@ DAVCollection.prototype = { return; } - // Do this unconditionally, since code that calls unlock() doesn't - // really have much of an option if unlock fails. The only thing - // to do is wait for it to time out (and hope it didn't really - // fail) - if (DAVLocks['default']) - delete DAVLocks['default']; + try { + let resp = yield this.UNLOCK("lock", self.cb); - let resp = yield this.UNLOCK("lock", self.cb); + if (Utils.checkStatus(resp.status)) { + this._log.trace("Lock released"); + self.done(true); + } else { + this._log.trace("Failed to release lock"); + self.done(false); + } - if (Utils.checkStatus(resp.status)) { - this._log.trace("Lock released"); - self.done(true); - } else { - this._log.trace("Failed to release lock"); - self.done(false); + } catch (e) { + throw e; + + } finally { + // Do this unconditionally, since code that calls unlock() doesn't + // really have much of an option if unlock fails. The only thing + // to do is wait for it to time out (and hope it didn't really + // fail) + if (DAVLocks['default']) + delete DAVLocks['default']; } }, From 71e98a8a2dcfb5274f655f544126546d22e5c40b Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 14:11:21 -0700 Subject: [PATCH 0583/1860] Factored the sending of xmpp notifications in bookmarkSharingManager out into a separate function, to save some duplicated code and make it easier to break xmpp stuff into a separate class later. Also made the bookmarkSharingManager cache the current weave username, which it uses a lot. Finally it now prepends /user/this._myUsername/ to server URLs to make them into absolute paths before sending them to the sharing partner over xmpp, which fixes a leftover TODO from a copule days ago. --- services/sync/modules/engines/bookmarks.js | 63 ++++++++++++---------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f06b0e881453..42329a4b7916 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -90,6 +90,13 @@ BookmarksSharingManager.prototype = { return this.__bms; }, + __myUsername: null, + get _myUsername() { + if (!this.__myUsername) + this.__myUsername = ID.get('WeaveID').username; + return this.__myUsername; + }, + _init: function SharingManager__init(engine) { this._engine = engine; this._log = Log4Moz.Service.getLogger("Bookmark Share"); @@ -109,7 +116,7 @@ BookmarksSharingManager.prototype = { /* Username/password for XMPP are the same as the ones for Weave, so read them from the weave identity: */ - let clientName = ID.get('WeaveID').username; + let clientName = this._myUsername; let clientPassword = ID.get('WeaveID').password; let transport = new HTTPPollingTransport( serverUrl, false, 15000 ); @@ -203,6 +210,19 @@ BookmarksSharingManager.prototype = { Notifications.add(notification); }, + _sendXmppNotification: function BmkSharing__sendXmpp(recipient, cmd, path, name) { + // Send an xmpp message to the share-ee + if ( this._xmppClient ) { + if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { + let msgText = "share " + path + " " + name; + this._log.debug( "Sending XMPP message: " + msgText ); + this._xmppClient.sendMessage( recipient, msgText ); + } else { + this._log.warn( "No XMPP connection for share notification." ); + } + } + }, + _share: function BmkSharing__share( folderId, username ) { // Return true if success, false if failure. let ret = false; @@ -228,22 +248,16 @@ BookmarksSharingManager.prototype = { username, 0, this._annoSvc.EXPIRE_NEVER); - // Send an xmpp message to the share-ee - if ( this._xmppClient ) { - // TODO include my username here: /user/myusername/ + serverPath - if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { - let msgText = "share " + serverPath + " " + folderName; - this._log.debug( "Sending XMPP message: " + msgText ); - this._xmppClient.sendMessage( username, msgText ); - } else { - this._log.warn( "No XMPP connection for share notification." ); - } - } - /* LONGTERM TODO: in the future when we allow sharing one folder with many people, the value of the annotation can be a whole list of usernames instead of just one. */ + // The serverPath is relative; prepend it with /user/myusername/ to make + // it absolute. + let abspath = "/user/" + this._myUsername + "/" + serverPath; + this._sendXmppNotification( username, "share", abspath, folderName); + + this._log.info("Shared " + folderName +" with " + username); ret = true; self.done( ret ); @@ -271,16 +285,9 @@ BookmarksSharingManager.prototype = { this._stopOutgoingShare.async(this, self.cb, folderId); yield; - // Send message to the share-ee, so they can stop their incoming share: - if ( this._xmppClient ) { - if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { - let msgText = "stop " + serverPath + " " + folderName; - this._log.debug( "Sending XMPP message: " + msgText ); - this._xmppClient.sendMessage( username, msgText ); - } else { - this._log.warn( "No XMPP connection for share notification." ); - } - } + // Send message to the share-ee, so they can stop their incoming share + let abspath = "/user/" + this._myUsername + "/" + serverPath; + this._sendXmppNotiication( username, "stop", abspath, folderName ); this._log.info("Stopped sharing " + folderName + "with " + username); self.done( true ); @@ -409,7 +416,6 @@ BookmarksSharingManager.prototype = { directory, or false if it failed.*/ let self = yield; - let myUserName = ID.get('WeaveID').username; this._log.debug("Turning folder " + folderName + " into outgoing share" + " with " + username); @@ -438,7 +444,8 @@ BookmarksSharingManager.prototype = { let encryptionTurnedOn = true; if (encryptionTurnedOn) { - yield this._createKeyChain.async(this, self.cb, serverPath, myUserName, username); + yield this._createKeyChain.async(this, self.cb, serverPath, + this._myUsername, username); } // Call Atul's js api for setting htaccess: @@ -457,7 +464,6 @@ BookmarksSharingManager.prototype = { To be called asynchronously. TODO: error handling*/ let self = yield; - let myUserName = ID.get('WeaveID').username; // The folder has an annotation specifying the server path to the // directory: let serverPath = this._annoSvc.getItemAnnotation(folderId, @@ -478,7 +484,7 @@ BookmarksSharingManager.prototype = { // Unwrap (decrypt) the key with the user's private key. let idRSA = ID.get('WeaveCryptoID'); let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, - keys.ring[myUserName], idRSA); + keys.ring[this._myUsername], idRSA); let bulkIV = keys.bulkIV; // Get the json-wrapped contents of everything in the folder: @@ -595,7 +601,6 @@ BookmarksSharingManager.prototype = { let self = yield; let user = mountData.userid; - let myUserName = ID.get('WeaveID').username; // The folder has an annotation specifying the server path to the // directory: let serverPath = mountData.serverPath; @@ -612,7 +617,7 @@ BookmarksSharingManager.prototype = { let cyphertext = yield; let tmpIdentity = { realm : "temp ID", - bulkKey : keys.ring[myUserName], + bulkKey : keys.ring[this._myUsername], bulkIV : keys.bulkIV }; Crypto.decryptData.async( Crypto, self.cb, cyphertext, tmpIdentity ); From 4bd80a118eca1fab0ec452a32122b7547133c852 Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 14:15:55 -0700 Subject: [PATCH 0584/1860] Fixed some trivial bugs in BookmarkStore._wrapMountOutgoing that were preventing the outgoing share from working properly. --- services/sync/modules/engines/bookmarks.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 42329a4b7916..578546a18439 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -1241,9 +1241,10 @@ BookmarksStore.prototype = { }, _wrapMountOutgoing: function BStore__wrapById( itemId ) { + let node = this._getNode(itemId); if (node.type != node.RESULT_TYPE_FOLDER) throw "Trying to wrap a non-folder mounted share"; - let node = this._getNode(itemId); + let GUID = this._bms.getItemGUID(itemId); let snapshot = {}; node.QueryInterface(Ci.nsINavHistoryQueryResultNode); @@ -1255,7 +1256,7 @@ BookmarksStore.prototype = { // remove any share mountpoints for (let guid in snapshot) { // TODO decide what to do with this... - if (ret.snapshot[guid].type == "incoming-share") + if (snapshot[guid].type == "incoming-share") delete snapshot[guid]; } return snapshot; From f94665b4dc534e370d0bc2fe54213be05aa6179e Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 14:19:26 -0700 Subject: [PATCH 0585/1860] Fixed another trivial bug in BookmarkSharingManager._stopSharing that was making it fail at sending out the xmpp notification. --- services/sync/modules/engines/bookmarks.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 578546a18439..e24ed07dfbc7 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -267,13 +267,12 @@ BookmarksSharingManager.prototype = { let self = yield; dump("folderId is " + folderId + "\n"); let folderName = this._bms.getItemTitle(folderId); + let serverPath = ""; if (this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)){ - let serverPath = this._annoSvc.getItemAnnotation(folderId, - SERVER_PATH_ANNO); + serverPath = this._annoSvc.getItemAnnotation(folderId, SERVER_PATH_ANNO); } else { this._log.warn("The folder you are de-sharing has no path annotation."); - let serverPath = ""; } /* LONGTERM TODO: when we move to being able to share one folder with From 9cdc96eebd759185e17cd461f7eee1c3e78c2c38 Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 14:37:51 -0700 Subject: [PATCH 0586/1860] Removed debugging dump statements from dav.js. --- services/sync/modules/dav.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 02080db628d7..a5b8c5f6dfd2 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -125,8 +125,6 @@ DAVCollection.prototype = { else path = path.slice(1); // if absolute path, remove leading slash // path at this point should have no leading slash. - dump("DefaultPrefix is " + this._defaultPrefix + "\n"); - dump(" In _makeRequest, after fixing the path, it is " + path +"\n"); let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); From f1700f64c1840bf79c7286abc7f6860687cc1ff0 Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 14:51:14 -0700 Subject: [PATCH 0587/1860] Fixed a typo in sendXmppNotification call --- services/sync/modules/engines/bookmarks.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e24ed07dfbc7..3036c0d61b42 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -286,7 +286,7 @@ BookmarksSharingManager.prototype = { // Send message to the share-ee, so they can stop their incoming share let abspath = "/user/" + this._myUsername + "/" + serverPath; - this._sendXmppNotiication( username, "stop", abspath, folderName ); + this._sendXmppNotification( username, "stop", abspath, folderName ); this._log.info("Stopped sharing " + folderName + "with " + username); self.done( true ); @@ -487,7 +487,11 @@ BookmarksSharingManager.prototype = { let bulkIV = keys.bulkIV; // Get the json-wrapped contents of everything in the folder: - let json = this._engine._store._wrapMountOutgoing(folderId); + let wrapMount = this._engine._store._wrapMountOutgoing(folderId); + let jsonService = Components.classes["@mozilla.org/dom/json;1"] + .createInstance(Components.interfaces.nsIJSON); + let json = jsonService.encode( wrapMount ); + dump( "Wrapped json before encryption is like this: " + json + "\n" ); // Encrypt it with the symkey and put it into the shared-bookmark file. let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); From d887839ba11a992a2d75af5318fac4f3e1137d50 Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 14:52:31 -0700 Subject: [PATCH 0588/1860] updateOutgoingShare now explicitly encodes data to json before encrypting and uploading. --- services/sync/modules/engines/bookmarks.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 3036c0d61b42..d5e0f085c405 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -370,12 +370,6 @@ BookmarksSharingManager.prototype = { let userPubKey = yield; userPubKey = userPubKey.pubkey; - /* 2008-07-03 15:49:41 - * Async.Generator ERROR Exception: Component returned failure - * code: 0x80070057 (NS_ERROR_ILLEGAL_VALUE) [IWeaveCrypto.wrapSymmetricKey] - * (JS frame :: file:///Users/jonathandicarlo/weave/modules/crypto.js :: Crypto_wrapKey :: line 216) - */ - /* Create the keyring, containing the sym key encrypted with each of our public keys: */ dump( "Calling crypto to wrap sym key with my public key.\n" ); @@ -491,7 +485,6 @@ BookmarksSharingManager.prototype = { let jsonService = Components.classes["@mozilla.org/dom/json;1"] .createInstance(Components.interfaces.nsIJSON); let json = jsonService.encode( wrapMount ); - dump( "Wrapped json before encryption is like this: " + json + "\n" ); // Encrypt it with the symkey and put it into the shared-bookmark file. let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); From 1a846d789d7a4aadff9c238e51843c194318b9d4 Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 15:05:24 -0700 Subject: [PATCH 0589/1860] Removed a bunch of dump statements that were in bookmarkSharingManager for old debugging stuff that is now done; they were just cluttering up the log. --- services/sync/modules/engines/bookmarks.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d5e0f085c405..511456c353ed 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -237,7 +237,6 @@ BookmarksSharingManager.prototype = { this._createOutgoingShare.async( this, self.cb, folderId, folderName, username ); let serverPath = yield; - dump("in _share: annotated with serverPath = " + serverPath + "\n"); this._updateOutgoingShare.async( this, self.cb, folderId ); yield; @@ -265,7 +264,6 @@ BookmarksSharingManager.prototype = { _stopSharing: function BmkSharing__stopSharing( folderId, username ) { let self = yield; - dump("folderId is " + folderId + "\n"); let folderName = this._bms.getItemTitle(folderId); let serverPath = ""; @@ -372,11 +370,9 @@ BookmarksSharingManager.prototype = { /* Create the keyring, containing the sym key encrypted with each of our public keys: */ - dump( "Calling crypto to wrap sym key with my public key.\n" ); Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", pubkey: idRSA.pubkey} ); let encryptedForMe = yield; - dump( "Calling crypto to wrap sym key with sharee's public key.\n" ); Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", pubkey: userPubKey} ); let encryptedForYou = yield; @@ -418,7 +414,6 @@ BookmarksSharingManager.prototype = { /* Create the directory on the server if it does not exist already. */ let serverPath = "share/" + folderGuid; - dump( "Trying to create " + serverPath + "\n"); let ret = yield DAV.MKCOL(serverPath, self.cb); if (!ret) { @@ -465,7 +460,6 @@ BookmarksSharingManager.prototype = { // there. // From that directory, get the keyring file, and from it, the symmetric // key that we'll use to encrypt. - dump( "in _updateOutgoingShare. serverPath is " + serverPath +"\n"); let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.pushFilter(new JsonFilter()); // TODO request for share/a317b645-2c2f-6946-aea0-d728509091d4/keyring @@ -726,7 +720,6 @@ BookmarksEngine.prototype = { _stopSharing: function BmkEngine__stopSharing(guid, username) { let self = yield; - dump( "BookmarkEnginge._stopSharing: guid=" + guid + ", username = " + username + "\n"); this._sharing._stopSharing.async( this._sharing, self.cb, guid, username); yield; self.done(); From cb2092d695bb30f0e015f8e6f137ac050f16562a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 16:34:27 -0700 Subject: [PATCH 0590/1860] Bug 444119: catch errors during a lock and avoid a deadlock --- services/sync/modules/dav.js | 93 +++++++++++++++++------------------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 02080db628d7..0c396aa6be61 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -120,18 +120,14 @@ DAVCollection.prototype = { this._log.debug(op + " request for " + (path? path : 'root folder')); if (!path || path[0] != '/') - // if it's a relative path, (no slash), prepend default prefix - path = this._defaultPrefix + path; + path = this._defaultPrefix + path; // if relative: prepend default prefix else - path = path.slice(1); // if absolute path, remove leading slash + path = path.slice(1); // if absolute: remove leading slash // path at this point should have no leading slash. - dump("DefaultPrefix is " + this._defaultPrefix + "\n"); - dump(" In _makeRequest, after fixing the path, it is " + path +"\n"); - - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); let xhrCb = self.cb; - request.onload = new Utils.EventListener(xhrCb, "load"); request.onerror = new Utils.EventListener(xhrCb, "error"); request.mozBackgroundRequest = true; @@ -356,52 +352,49 @@ DAVCollection.prototype = { lock: function DC_lock() { let self = yield; + let resp; - this._log.trace("Acquiring lock"); - if (!this._allowLock) - throw {message: "Cannot acquire lock (internal lock)"}; - this._allowLock = false; + try { + this._log.trace("Acquiring lock"); - if (DAVLocks['default']) { - this._log.debug("Lock called, but we already hold a token"); + if (this.locked) { + this._log.debug("Lock called, but we are already locked"); + return; + } + this._allowLock = false; + + resp = yield this.LOCK("lock", + "\n" + + "\n" + + " \n" + + " \n" + + "", self.cb); + if (!Utils.checkStatus(resp.status)) + return; + + let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); + let token = tokens.iterateNext(); + if (token) { + DAVLocks['default'] = { + URL: this._baseURL, + token: token.textContent + }; + } + + if (DAVLocks['default']) { + this._log.trace("Lock acquired"); + self.done(DAVLocks['default']); + } + + } catch (e) { + this._log.error("Could not acquire lock"); + if (resp.responseText) + this._log.error("Server response to LOCK:\n" + resp.responseText); + throw e; + + } finally { this._allowLock = true; - self.done(); - return; } - - this.LOCK("lock", - "\n" + - "\n" + - " \n" + - " \n" + - "", self.cb); - let resp = yield; - - if (resp.status < 200 || resp.status >= 300) { - this._allowLock = true; - return; - } - - let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - if (token) { - DAVLocks['default'] = { - URL: this._baseURL, - token: token.textContent - }; - } - - if (!DAVLocks['default']) { - this._log.warn("Could not acquire lock"); - this._allowLock = true; - self.done(); - return; - } - - this._log.trace("Lock acquired"); - this._allowLock = true; - - self.done(DAVLocks['default']); }, unlock: function DC_unlock() { From f5fe6f94ede47b165ba116eddc4e21214744df34 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Tue, 8 Jul 2008 16:50:59 -0700 Subject: [PATCH 0591/1860] If an exception is raised by a notification button callback, it is now logged. Also added a unit test for this new behavior. --- services/sync/modules/notifications.js | 13 +++++++- .../sync/tests/unit/test_notifications.js | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 services/sync/tests/unit/test_notifications.js diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index a3f59422887e..0fe0a264240c 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -125,9 +125,20 @@ Notification.prototype.buttons = []; * A button to display in a notification. */ function NotificationButton(label, accessKey, callback) { + function callbackWrapper() { + try { + callback.apply(this, arguments); + } catch (e) { + let logger = Log4Moz.Service.getLogger("Notifications"); + logger.error("An exception occurred: " + Utils.exceptionStr(e)); + logger.info(Utils.stackTrace(e)); + throw e; + } + } + this.label = label; this.accessKey = accessKey; - this.callback = callback; + this.callback = callbackWrapper; } function TabsNotification() { diff --git a/services/sync/tests/unit/test_notifications.js b/services/sync/tests/unit/test_notifications.js new file mode 100644 index 000000000000..3c27ded47291 --- /dev/null +++ b/services/sync/tests/unit/test_notifications.js @@ -0,0 +1,32 @@ +Cu.import("resource://weave/notifications.js"); + +function run_test() { + var logStats = initTestLogging("Info"); + + var blah = 0; + + function callback(i) { + blah = i; + } + + let button = new NotificationButton("label", "accessKey", callback); + + button.callback(5); + + do_check_eq(blah, 5); + do_check_eq(logStats.errorsLogged, 0); + + function badCallback() { + throw new Error("oops"); + } + + button = new NotificationButton("label", "accessKey", badCallback); + + try { + button.callback(); + } catch (e) { + do_check_eq(e.message, "oops"); + } + + do_check_eq(logStats.errorsLogged, 1); +} From 40b26fe89a24132242b58ec2b2d1baaf852408cd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 19:35:20 -0700 Subject: [PATCH 0592/1860] remove 'waitOnQuit.enabled' pref, always wait for a running sync before quitting --- services/sync/modules/service.js | 4 +--- services/sync/services-sync.js | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 158e620da2d0..ea87356b83b3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -517,9 +517,7 @@ WeaveSvc.prototype = { }, _onQuitApplication: function WeaveSvc__onQuitApplication() { - if (!this.enabled || - !Utils.prefs.getBoolPref("waitOnQuit.enabled") || - !this._loggedIn) + if (!this.enabled || !this._loggedIn) return; let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 89fb3734997c..a75d25847eab 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -13,7 +13,6 @@ pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); pref("extensions.weave.schedule", 1); -pref("extensions.weave.waitOnQuit.enabled", true); pref("extensions.weave.syncOnQuit.enabled", true); pref("extensions.weave.engine.bookmarks", true); From d732428f25b17b2610613c2c35dbc1905637c114 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 19:40:55 -0700 Subject: [PATCH 0593/1860] version bump to 0.2.1 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 505c9c72b170..e850580550a2 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.2.0"; +const WEAVE_VERSION = "0.2.1"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From f7494df5668109e8a01d1a9d2433ab641011db06 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 20:57:05 -0700 Subject: [PATCH 0594/1860] add WINNT-5.1 to makefile --- services/crypto/Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 69dd1574db44..17b27e29b5c8 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -60,7 +60,14 @@ else cxx = cl so = dll else - $(error Sorry, your os is unknown/unsupported: $(sys)) + ifeq ($(sys), MINGW32_NT-5.1) + os = WINNT + compiler = msvc + cxx = cl + so = dll + else + $(error Sorry, your os is unknown/unsupported: $(sys)) + endif endif endif endif From 593b1ab9a841b5a5c35dd29199f0b0b07b825dc7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 21:24:59 -0700 Subject: [PATCH 0595/1860] sync every 5 minutes (up from 1); start the score threshold at 75 (down from 100); decrement threshold by 25 each iteration (up from 5). these changes guarantee even small changes will sync in no more than 15 minutes (down from 20) --- services/sync/modules/service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ea87356b83b3..bc9b8bb8cc5c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -53,16 +53,16 @@ const Cu = Components.utils; // we'll sync it, reset its threshold to the initial value, rinse, and repeat. // How long we wait between sync checks. -const SCHEDULED_SYNC_INTERVAL = 60 * 1000; // one minute +const SCHEDULED_SYNC_INTERVAL = 60 * 1000 * 5; // five minutes // INITIAL_THRESHOLD represents the value an engine's score has to exceed // in order for us to sync it the first time we start up (and the first time // we do a sync check after having synced the engine or reset the threshold). -const INITIAL_THRESHOLD = 100; +const INITIAL_THRESHOLD = 75; // THRESHOLD_DECREMENT_STEP is the amount by which we decrement an engine's // threshold each time we do a sync check and don't sync that engine. -const THRESHOLD_DECREMENT_STEP = 5; +const THRESHOLD_DECREMENT_STEP = 25; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); From b3990dee6c790f8e1efbc941bfb9a79323f62f68 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 8 Jul 2008 21:25:15 -0700 Subject: [PATCH 0596/1860] bump version to 0.2.2 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index e850580550a2..71b3056f0200 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.2.1"; +const WEAVE_VERSION = "0.2.2"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 685f04299a955b8fd15ba38b37c90a4ad1e28ef0 Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 22:39:05 -0700 Subject: [PATCH 0597/1860] I think I have fixed updateIncomingShare now. So everything should work, if I share with a user not myself. --- services/sync/modules/engines/bookmarks.js | 81 ++++++++++++++-------- services/sync/modules/service.js | 12 ++++ 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 511456c353ed..d774c0f24660 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -187,7 +187,7 @@ BookmarksSharingManager.prototype = { // This is what happens when they click the Accept button: bmkSharing._log.info("Accepted bookmark share from " + user); bmkSharing._createIncomingShare(user, serverPath, folderName); - bmkSharing._updateAllIncomingShares(); + bmkSharing.updateAllIncomingShares(); return false; } ); @@ -302,13 +302,12 @@ BookmarksSharingManager.prototype = { to the folder contents are simply wiped out by the latest server contents.) */ let self = yield; - let mounts = this._engine._store.findIncomingShares(); - - /* TODO ensure that old contents of incoming shares have been - * properly clobbered. - */ + /* TODO ensure that old contents of incoming shares have been + * properly clobbered. + */ for (let i = 0; i < mounts.length; i++) { try { + this._log.trace("Update incoming share from " + mounts[i].serverPath); this._updateIncomingShare.async(this, self.cb, mounts[i]); yield; } catch (e) { @@ -533,6 +532,8 @@ BookmarksSharingManager.prototype = { /* Get the toolbar "Shared Folders" folder (identified by its annotation). If it doesn't already exist, create it: */ + dump( "I'm in _createIncomingShare. user= " + user + "path = " + + serverPath + ", title= " + title + "\n" ); let root; let a = this._annoSvc.getItemsWithAnnotation(INCOMING_SHARE_ROOT_ANNO, {}); @@ -586,53 +587,74 @@ BookmarksSharingManager.prototype = { mountData is an object that's expected to have member data: userid: weave id of the user sharing the folder with us, rootGUID: guid in our bookmark store of the share mount point, - node: the bookmark menu node for the share mount point folder, - snapshot: the json-wrapped current contents of the share. */ + node: the bookmark menu node for the share mount point folder */ + // TODO error handling (see what Resource can throw or return...) + /* TODO tons of symmetry between this and _updateOutgoingShare, can + probably factor the symkey decryption stuff into a common helper + function. */ let self = yield; let user = mountData.userid; // The folder has an annotation specifying the server path to the // directory: let serverPath = mountData.serverPath; // From that directory, get the keyring file, and from it, the symmetric - // key that we'll use to encrypt. + // key that we'll use to decrypt. + this._log.trace("UpdateIncomingShare: getting keyring file."); let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); - keyringFile.get(self.cb); - let keys = yield; + keyringFile.pushFilter(new JsonFilter()); + let keys = yield keyringFile.get(self.cb); + + // Unwrap (decrypt) the key with the user's private key. + this._log.trace("UpdateIncomingShare: decrypting sym key."); + let idRSA = ID.get('WeaveCryptoID'); + let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, + keys.ring[this._myUsername], idRSA); + let bulkIV = keys.bulkIV; // Decrypt the contents of the bookmark file with the symmetric key: + this._log.trace("UpdateIncomingShare: getting encrypted bookmark file."); let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); - bmkFile.pushFilter( new JsonFilter() ); - bmkFile.get(self.cb); - let cyphertext = yield; + let cyphertext = yield bmkFile.get(self.cb); let tmpIdentity = { realm : "temp ID", - bulkKey : keys.ring[this._myUsername], - bulkIV : keys.bulkIV + bulkKey : bulkKey, + bulkIV : bulkIV }; + this._log.trace("UpdateIncomingShare: Decrypting."); Crypto.decryptData.async( Crypto, self.cb, cyphertext, tmpIdentity ); let json = yield; - // TODO error handling (see what Resource can throw or return...) + // decrypting that gets us JSON, turn it into an object: + this._log.trace("UpdateIncomingShare: De-JSON-izing."); + let jsonService = Components.classes["@mozilla.org/dom/json;1"] + .createInstance(Components.interfaces.nsIJSON); + let serverContents = jsonService.decode( json ); // prune tree / get what we want - for (let guid in json) { - if (json[guid].type != "bookmark") - delete json[guid]; + this._log.trace("UpdateIncomingShare: Pruning."); + for (let guid in serverContents) { + if (serverContents[guid].type != "bookmark") + delete serverContents[guid]; else - json[guid].parentGUID = mountData.rootGUID; + serverContents[guid].parentGUID = mountData.rootGUID; } - // TODO check what the inputs to detectUpdates should be. Should I be - // passing in the current subtree of mountData.node? Do I need to wipe - // that subtree before calling diff? I'm trying to create a diff - // here between json and nothing so as to come up with the createCommands - // needed to create all the bookmarks. - /* Create diff between the json from server and the current contents; - then apply the diff. */ + /* Wipe old local contents of the folder, starting from the node: */ + this._log.trace("Wiping local contents of incoming share..."); + this._bms.removeFolderChildren( mountData.node ); + + /* Create diff FROM current contents (i.e. nothing) TO the incoming + * data from serverContents. Then apply the diff. */ this._log.trace("Got bookmarks from " + user + ", comparing with local copy"); - this._engine._core.detectUpdates(self.cb, json, {}); + this._engine._core.detectUpdates(self.cb, {}, serverContents); let diff = yield; + /* LONGTERM TODO: The createCommands that are executed in applyCommands + * will fail badly if the GUID of the incoming item collides with a + * GUID of a bookmark already in my store. (This happened to me a lot + * during testing, obviously, since I was sharing bookmarks with myself). + * Need to think about the right way to handle this. */ + // FIXME: should make sure all GUIDs here live under the mountpoint this._log.trace("Applying changes to folder from " + user); this._engine._store.applyCommands.async(this._engine._store, self.cb, diff); @@ -893,6 +915,7 @@ BookmarksStore.prototype = { parentId = this._bms.bookmarksMenuFolder; } + dump( "Processing createCommand for a " + command.data.type + " type.\n"); switch (command.data.type) { case "query": case "bookmark": diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2c13d10ef653..df4fa052f4a1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -839,6 +839,18 @@ WeaveSvc.prototype = { ret = false; } self.done(ret); + }, + + // Just for debugging for now: + checkForIncomingShares: function WeaveSvc_checkIncomingShares() { + // tons of hard-coded goodness. Again, Debugging Only! + let bmkEngine = Engines.get("bookmarks"); + let user = "avarma"; + let serverPath = "/user/avarma/share/982b0210-5064-874c-9f75-44e04c2a0973"; + let folderName = "Ubuntu and Free Software Links"; + if (bmkEngine.enabled) { + bmkEngine._sharing._incomingShareOffer(user, serverPath, folderName); + } } }; From 984a83308655ed55e3d292edee7ce49581854761 Mon Sep 17 00:00:00 2001 From: Date: Tue, 8 Jul 2008 22:40:52 -0700 Subject: [PATCH 0598/1860] Removed a special menu item that I had installed in the main Weave menu just for debugging purposes. --- services/sync/modules/service.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 34eefbb928d3..0a2247ca4774 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -863,18 +863,5 @@ WeaveSvc.prototype = { ret = false; } self.done(ret); - }, - - // Just for debugging for now: - checkForIncomingShares: function WeaveSvc_checkIncomingShares() { - // tons of hard-coded goodness. Again, Debugging Only! - let bmkEngine = Engines.get("bookmarks"); - let user = "avarma"; - let serverPath = "/user/avarma/share/982b0210-5064-874c-9f75-44e04c2a0973"; - let folderName = "Ubuntu and Free Software Links"; - if (bmkEngine.enabled) { - bmkEngine._sharing._incomingShareOffer(user, serverPath, folderName); - } } - }; From d6b92eb194fce613f35c68e389b4418612215fd7 Mon Sep 17 00:00:00 2001 From: Date: Wed, 9 Jul 2008 13:24:49 -0700 Subject: [PATCH 0599/1860] Restored a line in bookmarkSharingManager.js which I took out by accident when removing debugging dumps and stuff, changeset 969. It should not have been taken out; without it you get 'mounts undefined' error. --- services/sync/modules/engines/bookmarks.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d774c0f24660..eed427af0288 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -302,9 +302,7 @@ BookmarksSharingManager.prototype = { to the folder contents are simply wiped out by the latest server contents.) */ let self = yield; - /* TODO ensure that old contents of incoming shares have been - * properly clobbered. - */ + let mounts = this._engine._store.findIncomingShares(); for (let i = 0; i < mounts.length; i++) { try { this._log.trace("Update incoming share from " + mounts[i].serverPath); From d2f0cfa69b2f4515e88d980180e7fbe95dad03f6 Mon Sep 17 00:00:00 2001 From: Date: Wed, 9 Jul 2008 13:45:10 -0700 Subject: [PATCH 0600/1860] Made wrapping of outgoing shares not crash if an outgoing share is missing an exptected annotation. --- services/sync/modules/engines/bookmarks.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index eed427af0288..62a592cd829e 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -1176,12 +1176,12 @@ BookmarksStore.prototype = { node.containerOpen = true; // If folder is an outgoing share, wrap its annotations: if (this._ans.itemHasAnnotation(node.itemId, OUTGOING_SHARED_ANNO)) { - item.serverPathAnno = this._ans.getItemAnnotation(node.itemId, - SERVER_PATH_ANNO); item.outgoingSharedAnno = this._ans.getItemAnnotation(node.itemId, OUTGOING_SHARED_ANNO); - // TODO this can throw an error if SERVER_PATH_ANNO doesn't exist - // (which it always should) + } + if (this._ans.itemHasAnnotation(node.itemId, SERVER_PATH_ANNO)) { + item.serverPathAnno = this._ans.getItemAnnotation(node.itemId, + SERVER_PATH_ANNO); } for (var i = 0; i < node.childCount; i++) { From 6e4632faba5c31a1a18b53f2fbec03dd6cb23cab Mon Sep 17 00:00:00 2001 From: Date: Wed, 9 Jul 2008 16:57:55 -0700 Subject: [PATCH 0601/1860] Made bookmarkSharingManager._updateOutgoingShare tolerant of outgoing shares that are lacking a server path annotation: it will warn that they're invalid and return, rather than dying. --- services/sync/modules/engines/bookmarks.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 62a592cd829e..001d4c9bc8d9 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -449,8 +449,12 @@ BookmarksSharingManager.prototype = { To be called asynchronously. TODO: error handling*/ let self = yield; - // The folder has an annotation specifying the server path to the - // directory: + // The folder should have an annotation specifying the server path to + // the directory: + if (!this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)) { + this._log.warn("Outgoing share is invalid and can't be synced."); + return; + } let serverPath = this._annoSvc.getItemAnnotation(folderId, SERVER_PATH_ANNO); // TODO the above can throw an exception if the expected anotation isn't @@ -459,9 +463,6 @@ BookmarksSharingManager.prototype = { // key that we'll use to encrypt. let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); keyringFile.pushFilter(new JsonFilter()); - // TODO request for share/a317b645-2c2f-6946-aea0-d728509091d4/keyring - // fails with a 404 even though THE FILE IS THERE. Maybe it's not prepending this - // with /user/jono like it should be??? put debugging info into DAV.GET? keyringFile.get(self.cb); let keys = yield; @@ -486,8 +487,7 @@ BookmarksSharingManager.prototype = { }; Crypto.encryptData.async( Crypto, self.cb, json, tmpIdentity ); let cyphertext = yield; - bmkFile.put( self.cb, cyphertext ); - yield; + yield bmkFile.put( self.cb, cyphertext ); self.done(); }, From d7e9bb6c11f872e1e974c6b3420b5e69b9d440e8 Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Wed, 9 Jul 2008 17:17:24 -0700 Subject: [PATCH 0602/1860] * major revision to the login dialog to be more robust, adding error handling, styling and a help link * some minor hacking of the login progress to improve performance * expanded the application exit sync dialog to be a general modal sync dialog that will display whenever appropriate (e.g. during initial setup, on manual "sync now" requests, and on application exit) * added a progress meter and status messages to the modal sync dialog to provide users with a better understanding of what's going on, and to assist in debugging * added ability to cancel a modal sync request, including on application exit. when cancel is clicked on by the user, it will attempt to cancel the sync at the next opportunity (i.e. before the next sync engine is processed) --- services/sync/locales/en-US/login.dtd | 8 +-- services/sync/locales/en-US/login.properties | 3 ++ services/sync/locales/en-US/preferences.dtd | 2 +- services/sync/locales/en-US/status.dtd | 2 +- services/sync/locales/en-US/status.properties | 40 +++++++++++++-- services/sync/locales/en-US/sync.dtd | 2 +- services/sync/locales/en-US/sync.properties | 2 + services/sync/modules/engines.js | 25 +++++++++- services/sync/modules/engines/bookmarks.js | 1 + services/sync/modules/engines/cookies.js | 1 + services/sync/modules/engines/forms.js | 1 + services/sync/modules/engines/history.js | 1 + services/sync/modules/engines/passwords.js | 1 + services/sync/modules/engines/tabs.js | 1 + services/sync/modules/remote.js | 50 +++++++++++++++++++ services/sync/modules/service.js | 21 ++++++-- 16 files changed, 146 insertions(+), 15 deletions(-) diff --git a/services/sync/locales/en-US/login.dtd b/services/sync/locales/en-US/login.dtd index ba5927dea3f9..39f2a7f365bb 100644 --- a/services/sync/locales/en-US/login.dtd +++ b/services/sync/locales/en-US/login.dtd @@ -1,6 +1,8 @@ - - - + + + + + diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties index decb241f8f0d..f3ef17747556 100644 --- a/services/sync/locales/en-US/login.properties +++ b/services/sync/locales/en-US/login.properties @@ -2,3 +2,6 @@ loginFailed.alert = Login failed noPassword.alert = You must enter a password noPassphrase.alert = You must enter a passphrase samePasswordAndPassphrase.alert = Your password and passphrase must be different +loginStart.label = Signing in, please wait... +loginError.label = Invalid username and/or password. +loginSuccess.label = Signed In \ No newline at end of file diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 8d43d70e1a5e..b329e2a84c2a 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -24,7 +24,7 @@ - + diff --git a/services/sync/locales/en-US/status.dtd b/services/sync/locales/en-US/status.dtd index 7680899e0785..6703fe0ff2a2 100644 --- a/services/sync/locales/en-US/status.dtd +++ b/services/sync/locales/en-US/status.dtd @@ -1,2 +1,2 @@ - + diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index 76c5f9ecd670..8b0cbaa184f7 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -1,4 +1,36 @@ -status.wait = Waiting for current sync to finish... -status.active = Syncing with Weave... -status.success = Sync succeeded. -status.error = Sync failed. +status.wait = Waiting for Current Sync to Finish +status.active = Syncing with Weave +status.success = Sync Complete +status.error = Sync Failed +status.cancel = Cancelling Sync, Please Wait +status.cancelled = Sync Cancelled + +status.engine_start = Starting Sync + +status.downloading-status = Downloading Status from Server +status.uploading-status = Updating Status on Server + +status.downloading-snapshot = Downloading Snapshot from Server +status.uploading-snapshot = Uploading Initial Snapshot to Server + +status.generating-random-key = Generating Random Key +status.encrypting-key = Encrypting Key +status.uploading-key = Uploading Key to Server +status.downloading-keyring = Downloading Key Ring from Server +status.decrypting-key = Decrypting Key + +status.downloading-deltas = Downloading Deltas from Server +status.uploading-deltas = Updating Deltas on Server + +status.calculating-differences = Calculating Differences +status.reconciling-updates = Reconciling Differences +status.applying-changes = Applying Required Changes +status.no-changes-required = No Changes Needed + +status.encoding-json = Encoding Data +status.decoding-json = Decoding Data + +status.encrypting = Encrypting Data +status.decrypting = Decrypting Data + + diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 6cdd2cf7f872..dd20b4411d78 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -1,7 +1,7 @@ - + diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 016a9f4ba811..d48e7a82dce3 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -14,6 +14,8 @@ status.offline = Sign in status.idle = Idle status.active = Working... +error.login.title = Error While Signing In +error.login.description = Weave encountered an error while signing you in. Your username/password failed. Please try again. error.logout.title = Error While Signing Out error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it (then why are we bugging you with this info?). error.sync.title = Error While Syncing diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 58bc23074efa..c145622b75a8 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -77,6 +77,14 @@ EngineManagerSvc.prototype = { } return ret; }, + getEnabled: function EngMgr_getEnabled() { + let ret = []; + for (key in this._engines) { + if(this._engines[key].enabled) + ret.push(this._engines[key]); + } + return ret; + }, register: function EngMgr_register(engine) { this._engines[engine.name] = engine; }, @@ -95,6 +103,9 @@ Engine.prototype = { // "default-engine"; get name() { throw "name property must be overridden in subclasses"; }, + // "Default"; + get displayName() { throw "displayName property must be overriden in subclasses"; }, + // "DefaultEngine"; get logName() { throw "logName property must be overridden in subclasses"; }, @@ -238,6 +249,7 @@ Engine.prototype = { let self = yield; this._log.info("Beginning sync"); + this._os.notifyObservers(null, "weave:service:sync:engine:start", this.displayName); try { yield this._remote.openSession(self.cb); @@ -264,7 +276,8 @@ Engine.prototype = { yield this._remote.keys.getKeyAndIV(self.cb, this.engineId); // 1) Fetch server deltas - + + this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); let server = {}; let serverSnap = yield this._remote.wrap(self.cb, this._snapshot); server.snapshot = serverSnap.data; @@ -273,6 +286,7 @@ Engine.prototype = { // 2) Generate local deltas from snapshot -> current client status + this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); let localJson = new SnapshotStore(); localJson.data = this._store.wrap(); this._core.detectUpdates(self.cb, this._snapshot.data, localJson.data); @@ -284,6 +298,7 @@ Engine.prototype = { if (server.updates.length == 0 && localUpdates.length == 0) { this._snapshot.version = this._remote.status.data.maxVersion; + this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); this._log.info("Sync complete: no changes needed on client or server"); self.done(true); return; @@ -291,6 +306,7 @@ Engine.prototype = { // 3) Reconcile client/server deltas and generate new deltas for them. + this._os.notifyObservers(null, "weave:service:sync:status", "status.reconciling-updates"); this._log.info("Reconciling client/server updates"); this._core.reconcile(self.cb, localUpdates, server.updates); let ret = yield; @@ -311,6 +327,7 @@ Engine.prototype = { if (!(clientChanges.length || serverChanges.length || clientConflicts.length || serverConflicts.length)) { + this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); this._log.info("Sync complete: no changes needed on client or server"); this._snapshot.data = localJson.data; this._snapshot.version = this._remote.status.data.maxVersion; @@ -328,8 +345,11 @@ Engine.prototype = { let newSnapshot; // 3.1) Apply server changes to local store + if (clientChanges.length) { this._log.info("Applying changes locally"); + this._os.notifyObservers(null, "weave:service:sync:status", "status.applying-changes"); + // Note that we need to need to apply client changes to the // current tree, not the saved snapshot @@ -361,6 +381,7 @@ Engine.prototype = { // current client snapshot. In the case where there are no // conflicts, it should be the same as what the resolver returned + this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); newSnapshot = this._store.wrap(); this._core.detectUpdates(self.cb, server.snapshot, newSnapshot); let serverDelta = yield; @@ -388,6 +409,8 @@ Engine.prototype = { for (GUID in this._snapshot.data) c++; + this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-deltas"); + this._remote.appendDelta(self.cb, serverDelta, {maxVersion: this._snapshot.version, deltasEncryption: Crypto.defaultAlgorithm, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 001d4c9bc8d9..775e26dbb7fb 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -687,6 +687,7 @@ function BookmarksEngine(pbeId) { } BookmarksEngine.prototype = { get name() { return "bookmarks"; }, + get displayName() { return "Bookmarks"; }, get logName() { return "BmkEngine"; }, get serverPrefix() { return "user-data/bookmarks/"; }, diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index a0ed60a453e6..0c5baab9df86 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -54,6 +54,7 @@ function CookieEngine(pbeId) { } CookieEngine.prototype = { get name() { return "cookies"; }, + get displayName() { return "Cookies"; }, get logName() { return "CookieEngine"; }, get serverPrefix() { return "user-data/cookies/"; }, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 4d49adb1915a..54ccc2693a4b 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -55,6 +55,7 @@ function FormEngine(pbeId) { } FormEngine.prototype = { get name() { return "forms"; }, + get displayName() { return "Saved Form Data"; }, get logName() { return "FormEngine"; }, get serverPrefix() { return "user-data/forms/"; }, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 56b73c7e044c..ce4f3bf41677 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -55,6 +55,7 @@ function HistoryEngine(pbeId) { } HistoryEngine.prototype = { get name() { return "history"; }, + get displayName() { return "Browsing History"; }, get logName() { return "HistEngine"; }, get serverPrefix() { return "user-data/history/"; }, diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index bcf1a3312f17..389aeecee656 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -52,6 +52,7 @@ function PasswordEngine() { } PasswordEngine.prototype = { get name() { return "passwords"; }, + get displayName() { return "Saved Passwords"; }, get logName() { return "PasswordEngine"; }, get serverPrefix() { return "user-data/passwords/"; }, diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 68b025dba2b4..bb1775314197 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -58,6 +58,7 @@ TabEngine.prototype = { __proto__: new Engine(), get name() "tabs", + get displayName() { return "Tabs"; }, get logName() "TabEngine", get serverPrefix() "user-data/tabs/", get store() this._store, diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 798dd862d9ca..fa168ec9cee9 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -93,6 +93,14 @@ Resource.prototype = { this._data = value; }, + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + get lastRequest() { return this._lastRequest; }, get downloaded() { return this._downloaded; }, get dirty() { return this._dirty; }, @@ -296,6 +304,14 @@ function JsonFilter() { JsonFilter.prototype = { __proto__: new ResourceFilter(), + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + get _json() { let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); this.__defineGetter__("_json", function() json); @@ -305,12 +321,14 @@ JsonFilter.prototype = { beforePUT: function JsonFilter_beforePUT(data) { let self = yield; this._log.debug("Encoding data as JSON"); + this._os.notifyObservers(null, "weave:service:sync:status", "stats.encoding-json"); self.done(this._json.encode(data)); }, afterGET: function JsonFilter_afterGET(data) { let self = yield; this._log.debug("Decoding JSON data"); + this._os.notifyObservers(null, "weave:service:sync:status", "stats.decoding-json"); self.done(this._json.decode(data)); } }; @@ -323,9 +341,18 @@ function CryptoFilter(remoteStore, algProp) { CryptoFilter.prototype = { __proto__: new ResourceFilter(), + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + beforePUT: function CryptoFilter_beforePUT(data) { let self = yield; this._log.debug("Encrypting data"); + this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting"); Crypto.encryptData.async(Crypto, self.cb, data, this._remote.engineId); let ret = yield; self.done(ret); @@ -334,6 +361,7 @@ CryptoFilter.prototype = { afterGET: function CryptoFilter_afterGET(data) { let self = yield; this._log.debug("Decrypting data"); + this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting"); if (!this._remote.status.data) throw "Remote status must be initialized before crypto filter can be used" Crypto.decryptData.async(Crypto, self.cb, data, this._remote.engineId); @@ -355,12 +383,14 @@ Keychain.prototype = { _getKeyAndIV: function Keychain__getKeyAndIV(identity) { let self = yield; + this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-keyring"); this.get(self.cb); yield; if (!this.data || !this.data.ring || !this.data.ring[identity.username]) throw "Keyring does not contain a key for this user"; // Unwrap (decrypt) the key with the user's private key. + this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting-key"); let idRSA = ID.get('WeaveCryptoID'); let symkey = yield Crypto.unwrapKey.async(Crypto, self.cb, this.data.ring[identity.username], idRSA); @@ -383,6 +413,14 @@ RemoteStore.prototype = { get serverPrefix() this._engine.serverPrefix, get engineId() this._engine.engineId, + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + get status() { let status = new Resource(this.serverPrefix + "status.json"); status.pushFilter(new JsonFilter()); @@ -428,6 +466,8 @@ RemoteStore.prototype = { throw "Could not create remote folder"; this._log.debug("Downloading status file"); + this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-status"); + this.status.get(self.cb); yield; this._log.debug("Downloading status file... done"); @@ -459,19 +499,24 @@ RemoteStore.prototype = { let wrappedSymkey; if ("none" != Utils.prefs.getCharPref("encryption")) { + this._os.notifyObservers(null, "weave:service:sync:status", "status.generating-random-key"); + Crypto.randomKeyGen.async(Crypto, self.cb, this.engineId); yield; // Wrap (encrypt) this key with the user's public key. let idRSA = ID.get('WeaveCryptoID'); + this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting-key"); wrappedSymkey = yield Crypto.wrapKey.async(Crypto, self.cb, this.engineId.bulkKey, idRSA); } let keys = {ring: {}, bulkIV: this.engineId.bulkIV}; + this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-key"); keys.ring[this.engineId.username] = wrappedSymkey; yield this.keys.put(self.cb, keys); + this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-snapshot"); yield this._snapshot.put(self.cb, snapshot.data); //yield this._deltas.put(self.cb, []); @@ -479,6 +524,7 @@ RemoteStore.prototype = { for (GUID in snapshot.data) c++; + this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-status"); yield this.status.put(self.cb, {GUID: snapshot.GUID, formatVersion: ENGINE_STORAGE_FORMAT_VERSION, @@ -514,6 +560,7 @@ RemoteStore.prototype = { let status = this.status.data; this._log.info("Downloading all server data from scratch"); + this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-snapshot"); let snap = new SnapshotStore(); snap.data = yield this._snapshot.get(self.cb); @@ -545,6 +592,7 @@ RemoteStore.prototype = { this._log.debug("Using last sync snapshot as starting point for server snapshot"); snap.data = Utils.deepCopy(lastSyncSnap.data); this._log.info("Downloading server deltas"); + this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); deltas = []; let min = lastSyncSnap.version + 1; let max = this.status.data.maxVersion; @@ -601,7 +649,9 @@ RemoteStore.prototype = { } let id = this.status.data.maxVersion; // FIXME: we increment maxVersion in Engine + this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-deltas"); yield this._deltas.put(self.cb, id, delta); + this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-status"); yield this.status.put(self.cb, this.status.data); }, appendDelta: function RStore_appendDelta(onComplete, delta, metadata) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0a2247ca4774..2a769a84f124 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -146,6 +146,8 @@ WeaveSvc.prototype = { _lock: Wrap.lock, _localLock: Wrap.localLock, _osPrefix: "weave:service:", + _cancelRequested: false, + _isQuitting: false, _loggedIn: false, _syncInProgress: false, @@ -202,6 +204,13 @@ WeaveSvc.prototype = { get userPath() { return ID.get('WeaveID').username; }, get isLoggedIn() this._loggedIn, + + get isQuitting() this._isQuitting, + set isQuitting(value) { this._isQuitting = value; }, + + get cancelRequested() this._cancelRequested, + set cancelRequested(value) { this._cancelRequested = value; }, + get enabled() Utils.prefs.getBoolPref("enabled"), get schedule() { @@ -520,6 +529,8 @@ WeaveSvc.prototype = { if (!this.enabled || !this._loggedIn) return; + this.isQuitting = true; + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. getService(Ci.nsIWindowWatcher); @@ -527,8 +538,8 @@ WeaveSvc.prototype = { // until the sync finishes and the window closes. let window = ww.openWindow(null, "chrome://weave/content/status.xul", - "Weave:status", - "chrome,centerscreen,modal", + "Weave:Status", + "chrome,centerscreen,modal,close=0", null); }, @@ -603,8 +614,6 @@ WeaveSvc.prototype = { yield this._verifyLogin.async(this, self.cb, this.username, this.password); - yield this._versionCheck.async(this, self.cb); - yield this._getKeypair.async(this, self.cb); this._setSchedule(this.schedule); @@ -671,8 +680,12 @@ WeaveSvc.prototype = { let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { + if (this.cancelRequested) + continue; + if (!engines[i].enabled) continue; + yield this._notify(engines[i].name + "-engine:sync", this._syncEngine, engines[i]).async(this, self.cb); } From e78f6e2e45cb3381dc694f12008080d194183ca5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 9 Jul 2008 17:36:40 -0700 Subject: [PATCH 0603/1860] cosmetic cleanup to yield calls in the reconciler --- services/sync/modules/syncCores.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index b9b3c0399bdc..b04957bce34f 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -62,10 +62,10 @@ function SyncCore() { } SyncCore.prototype = { _logName: "Sync", - + // Set this property in child objects! _store: null, - + _init: function SC__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); }, @@ -224,10 +224,8 @@ SyncCore.prototype = { let guidChanges = []; for (let i = 0; i < listA.length; i++) { let a = listA[i]; - Utils.makeTimerForCall(self.cb); - yield; // Yield to main loop - //this._log.debug("comparing " + i + ", listB length: " + listB.length); + yield Utils.makeTimerForCall(self.cb); // yield to UI let skip = false; listB = listB.filter(function(b) { @@ -266,8 +264,7 @@ SyncCore.prototype = { for (let i = 0; i < listA.length; i++) { for (let j = 0; j < listB.length; j++) { - Utils.makeTimerForCall(self.cb); - yield; // Yield to main loop + yield Utils.makeTimerForCall(self.cb); // yield to UI if (this._conflicts(listA[i], listB[j]) || this._conflicts(listB[j], listA[i])) { @@ -283,8 +280,7 @@ SyncCore.prototype = { this._getPropagations(listA, conflicts[0], propagations[1]); - Utils.makeTimerForCall(self.cb); - yield; // Yield to main loop + yield Utils.makeTimerForCall(self.cb); // yield to UI this._getPropagations(listB, conflicts[1], propagations[0]); ret = {propagations: propagations, conflicts: conflicts}; From d488cb68582f44c8d035dfc5e2fe9c9164b69ca8 Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Thu, 10 Jul 2008 17:03:56 -0700 Subject: [PATCH 0604/1860] * sync on quit is now also skipped when there is a forced restart of the browser (e.g. updates, extension installs, etc.) * formatting tweaks and cleanup for modal sync UI --- services/sync/locales/en-US/status.dtd | 1 - services/sync/locales/en-US/status.properties | 1 + services/sync/modules/service.js | 7 +++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/status.dtd b/services/sync/locales/en-US/status.dtd index 6703fe0ff2a2..7687d45a6a7f 100644 --- a/services/sync/locales/en-US/status.dtd +++ b/services/sync/locales/en-US/status.dtd @@ -1,2 +1 @@ - diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index 8b0cbaa184f7..2b8f9cbd2913 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -4,6 +4,7 @@ status.success = Sync Complete status.error = Sync Failed status.cancel = Cancelling Sync, Please Wait status.cancelled = Sync Cancelled +status.closing = Closing... status.engine_start = Starting Sync diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2a769a84f124..12b0e0d49740 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -529,6 +529,13 @@ WeaveSvc.prototype = { if (!this.enabled || !this._loggedIn) return; + // Don't quit on exit if this is a forced restart due to application update + // or extension install. + var prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + if(prefBranch.getBoolPref("browser.sessionstore.resume_session_once")) + return; + this.isQuitting = true; let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. From b089511ffaf52e101802386ef1929a25e30dde49 Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Fri, 11 Jul 2008 09:38:44 -0700 Subject: [PATCH 0605/1860] - added checks to see if registration is closed, and if so, shows a message to the user on the account creation pane. - added description to 423 locking warnings, as they are not necessarily a bad thing. temporary as we really need more reobust management of locking when multiple clients are potentially syncing at once. - minor clean up of passphrase verification in the wizard. passphrase verification is still a bit buggy though... --- services/sync/locales/en-US/wizard.properties | 57 ++++++++++--------- services/sync/modules/dav.js | 5 +- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 48943ee6af9e..a45dec1123c0 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,10 +1,10 @@ verify-progress.label = Verifying username and password verify-error.label = Invalid username and password verify-success.label = Username and password verified -passphrase-progress.label = Verifying passphrase -passphrase-success.label = Passphrase verified -passphrase-error.label = Invalid passphrase - +passphrase-progress.label = Verifying passphrase +passphrase-success.label = Passphrase verified +passphrase-error.label = Invalid passphrase + serverError.label = Server Error serverTimeoutError.label = Server Timeout @@ -30,26 +30,26 @@ incorrectCaptcha.label = Incorrect Captcha response. Try again. -create-progress.label = Creating your account -create-uid-inuse.label = Username in use -create-uid-missing.label = Username missing -create-uid-invalid.label = Username invalid -create-mail-invalid.label = Email invalid -create-mail-inuse.label = Emali in use -create-captcha-missing.label = Captcha response missing -create-password-missing.label = Password missing -create-password-incorrect.label = Password incorrect - -create-success.label = Account for %S created. - -final-pref-value.label = %S -final-account-value.label = Username: %S -final-sync-value.label = Backup and synchronization of browser settings and metadata. -final-success.label = Weave was successfully installed. - -default-name.label = %S's Firefox -default-name-nouser.label = Firefox - +create-progress.label = Creating your account +create-uid-inuse.label = Username in use +create-uid-missing.label = Username missing +create-uid-invalid.label = Username invalid +create-mail-invalid.label = Email invalid +create-mail-inuse.label = Emali in use +create-captcha-missing.label = Captcha response missing +create-password-missing.label = Password missing +create-password-incorrect.label = Password incorrect + +create-success.label = Account for %S created. + +final-pref-value.label = %S +final-account-value.label = Username: %S +final-sync-value.label = Backup and synchronization of browser settings and metadata. +final-success.label = Weave was successfully installed. + +default-name.label = %S's Firefox +default-name-nouser.label = Firefox + bookmarks.label = Bookmarks history.label = Browsing History cookies.label = Cookies @@ -61,10 +61,10 @@ initialLogin-progress.label = Signing you in initialLogin-error.label = Problem signing in. initialPrefs-progress.label = Setting your preferences - + initialSync-progress.label = Synchronizing your data -initialSync-error.label = Problem syncing data. - +initialSync-error.label = Problem syncing data. + installation-complete.label = Installation complete. @@ -75,3 +75,6 @@ data-verify.title = Data (Step 2 of 3) data-create.title = Data (Step 4 of 5) final-verify.title = Finish (Step 3 of 3) final-create.title = Finish (Step 5 of 5) + +registration-closed.title = Registration Closed +registration-closed.label = Sorry, but we've reached out account limit for this round of alpha testing. Please try again in a few days. diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 0c396aa6be61..63cee604a1ef 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -153,7 +153,10 @@ DAVCollection.prototype = { let event = yield; ret = event.target; - if (ret.status < 200 || ret.status >= 300) + if (ret.status == 423) + this._log.warn("_makeRequest: got status " + ret.status + " (This is not necessarily bad. It could just mean that another Firefox was syncing at the same time.)"); + else + if (ret.status < 200 || ret.status >= 300) this._log.warn("_makeRequest: got status " + ret.status); self.done(ret); From b5c2b8d393956d44114dfdb0841e43a1126eca66 Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Fri, 11 Jul 2008 09:44:17 -0700 Subject: [PATCH 0606/1860] - s/out/our and server timeout from 10s to 15s (from originally 30s, which was causing people to think the app was locked up) --- services/sync/locales/en-US/wizard.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index a45dec1123c0..5eef18245958 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -77,4 +77,4 @@ final-verify.title = Finish (Step 3 of 3) final-create.title = Finish (Step 5 of 5) registration-closed.title = Registration Closed -registration-closed.label = Sorry, but we've reached out account limit for this round of alpha testing. Please try again in a few days. +registration-closed.label = Sorry, but we've reached our account limit for this round of alpha testing. Please try again in a few days. From 2dd8e827956b48dd30972f66b0dd734801875ea1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 11 Jul 2008 13:40:06 -0700 Subject: [PATCH 0607/1860] move formatAsyncFrame to utils, don't print 'regular' stack trace when we have an async exception (it's not useful) --- services/sync/modules/async.js | 26 +++++---------- services/sync/modules/service.js | 9 ++---- services/sync/modules/util.js | 54 ++++++++++++++++---------------- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index ef7fe4acc7f8..32c0fa8606e6 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -96,7 +96,7 @@ function AsyncException(asyncStack, exceptionToWrap) { addAsyncFrame: function AsyncException_addAsyncFrame(frame) { this._asyncStack += ((this._asyncStack? "\n" : "") + - formatAsyncFrame(frame)); + Utils.formatFrame(frame)); }, toString: function AsyncException_toString() { @@ -131,7 +131,7 @@ Generator.prototype = { this._outstandingCbs++; this._stackAtLastCallbackGen = caller; this._log.trace(this.name + ": cb-" + cbId + " generated at:\n" + - formatAsyncFrame(caller)); + Utils.formatFrame(caller)); let self = this; let cb = function(data) { self._log.trace(self.name + ": cb-" + cbId + " called."); @@ -173,12 +173,12 @@ Generator.prototype = { let cbGenText = ""; if (this._stackAtLastCallbackGen) cbGenText = (" (last self.cb generated at " + - formatAsyncFrame(this._stackAtLastCallbackGen) + ")"); + Utils.formatFrame(this._stackAtLastCallbackGen) + ")"); let frame = skipAsyncFrames(this._initFrame); return ("unknown (async) :: " + this.name + cbGenText + "\n" + - Utils.stackTraceFromFrame(frame, formatAsyncFrame)); + Utils.stackTraceFromFrame(frame, Utils.formatFrame)); }, _handleException: function AsyncGen__handleException(e) { @@ -193,7 +193,7 @@ Generator.prototype = { if (e instanceof AsyncException) { // FIXME: attempt to skip repeated frames, which can happen if the // child generator never yielded. Would break for valid repeats (recursion) - if (e.asyncStack.indexOf(formatAsyncFrame(this._initFrame)) == -1) + if (e.asyncStack.indexOf(Utils.formatFrame(this._initFrame)) == -1) e.addAsyncFrame(this._initFrame); } else { e = new AsyncException(this.asyncStack, e); @@ -205,7 +205,7 @@ Generator.prototype = { this._log.warn("Exception: " + Utils.exceptionStr(e)); } else { this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.debug("Stack trace:\n" + Utils.stackTrace(e, formatAsyncFrame)); + this._log.debug("Async stack trace:\n" + Utils.stackTrace(e, Utils.formatFrame)); } // continue execution of caller. @@ -301,7 +301,7 @@ Generator.prototype = { this.name + " generator"); this._log.error("Exception: " + Utils.exceptionStr(e)); this._log.trace("Current stack trace:\n" + - Utils.stackTrace(e, formatAsyncFrame)); + Utils.stackTrace(e, Utils.formatFrame)); this._log.trace("Initial async stack trace:\n" + this.asyncStack); } } @@ -315,16 +315,6 @@ function skipAsyncFrames(frame) { return frame; } -function formatAsyncFrame(frame) { - // FIXME: sort of hackish, might be confusing if there are multiple - // extensions with similar filenames - let tmp = ""; - if (frame.filename) - tmp = frame.filename.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); - tmp += ":" + frame.lineNumber + " :: " + frame.name; - return tmp; -} - Async = { get outstandingGenerators() { return gOutstandingGenerators; }, @@ -359,5 +349,5 @@ Async = { let args = Array.prototype.slice.call(arguments, 1); args.unshift(thisArg, this); Async.run.apply(Async, args); - }, + } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 12b0e0d49740..dfaf1cced573 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -230,12 +230,7 @@ WeaveSvc.prototype = { _initialLoginAndSync: function Weave__initialLoginAndSync() { let self = yield; - - // Any exceptions thrown by the login process will be propagated - // out here, so there's no need to check to see if the login was - // successful; if it wasn't, this coroutine will just abort. - - yield this.login(self.cb); + yield this.login(self.cb); // will throw if login fails yield this.sync(self.cb); }, @@ -280,7 +275,7 @@ WeaveSvc.prototype = { this._log.info("Skipping scheduled sync; local operation in progress") } else { this._log.info("Running scheduled sync"); - this._notify("sync-as-needed", this._lock(this._syncAsNeeded)).async(this); + this._notify("sync", this._lock(this._syncAsNeeded)).async(this); } } }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index dcaafa6f39f3..f11c3603ec6a 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -202,21 +202,35 @@ let Utils = { return ret; }, + // Works on frames or exceptions, munges file:// URIs to shorten the paths + // FIXME: filename munging is sort of hackish, might be confusing if + // there are multiple extensions with similar filenames + formatFrame: function Utils_formatFrame(frame) { + let tmp = ""; + if (frame.filename) + tmp = frame.filename.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); + else if (frame.fileName) + tmp = frame.fileName.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); + if (frame.lineNumber) + tmp += ":" + frame.lineNumber; + if (frame.name) + tmp += " :: " + frame.name; + return tmp; + }, + exceptionStr: function Weave_exceptionStr(e) { let message = e.message ? e.message : e; let location = ""; - if (e.location) - // It's a wrapped nsIException. + if (e.location) // Wrapped nsIException. location = e.location; - else if (e.fileName && e.lineNumber) - // It's a standard JS exception. - location = "file '" + e.fileName + "', line " + e.lineNumber; + else if (e.fileName && e.lineNumber) // Standard JS exception + location = Utils.formatFrame(e); if (location) location = " (" + location + ")"; return message + location; - }, + }, stackTraceFromFrame: function Weave_stackTraceFromFrame(frame, formatter) { if (!formatter) @@ -233,28 +247,14 @@ let Utils = { }, stackTrace: function Weave_stackTrace(e, formatter) { - let output = ""; - if (e.location) { - // It's a wrapped nsIException. - output += this.stackTraceFromFrame(e.location, formatter); - } else if (e.stack) - // It's a standard JS exception. - - // TODO: It would be nice if we could parse this string and - // create a 'fake' nsIStackFrame-like call stack out of it, - // so that we can do things w/ this stack trace like we do - // with nsIException traces. - output += e.stack; + if (e.asyncStack) // AsyncException + return e.asyncStack; + else if (e.location) // Wrapped nsIException + return this.stackTraceFromFrame(e.location, formatter); + else if (e.stack) // Standard JS exception + return e.stack; else - // It's some other thrown object, e.g. a bare string. - output += "No traceback available.\n"; - - if (e.asyncStack) { - output += "This exception was raised by an asynchronous coroutine.\n"; - output += "Initial async stack trace:\n" + e.asyncStack; - } - - return output; + return "No traceback available"; }, checkStatus: function Weave_checkStatus(code, msg, ranges) { From 068531a23ac1e92e7f815be02b1036c4aea589cc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 11 Jul 2008 14:47:15 -0700 Subject: [PATCH 0608/1860] beef up fault tolerance module and hook it up to the service --- services/sync/modules/faultTolerance.js | 45 +++++++++++++------------ services/sync/modules/log4moz.js | 2 +- services/sync/modules/service.js | 13 ++++--- services/sync/modules/wrap.js | 28 +++++++++++++-- 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index ec355cfd3718..f23d3df3a1c7 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -19,6 +19,7 @@ * * Contributor(s): * Atul Varma + * Dan Mills * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -42,38 +43,40 @@ const EXPORTED_SYMBOLS = ["FaultTolerance"]; FaultTolerance = { get Service() { if (!this._Service) - this._Service = new FaultToleranceService(); + this._Service = new FTService(); return this._Service; } }; -function FaultToleranceService() { +function FTService() { + this._log = Log4Moz.Service.getLogger("FaultTolerance"); + this._appender = new FTAppender(this); + Log4Moz.Service.rootLogger.addAppender(this._appender); } - -FaultToleranceService.prototype = { - init: function FTS_init() { - var appender = new Appender(); - - Log4Moz.Service.rootLogger.addAppender(appender); +FTService.prototype = { + onMessage: function FTS_onMessage(message) { + //dump("got a message: " + message + "\n"); + }, + onException: function FTS_onException(exception) { + dump("got an exception: " + exception + "\n"); + throw exception; } }; -function Formatter() { -} -Formatter.prototype = { - format: function FTF_format(message) { - return message; - } +function FTFormatter() {} +FTFormatter.prototype = { + __proto__: new Log4Moz.Formatter(), + format: function FTF_format(message) message }; -Formatter.prototype.__proto__ = new Log4Moz.Formatter(); -function Appender() { - this._name = "FaultToleranceAppender"; - this._formatter = new Formatter(); +function FTAppender(ftService) { + this._ftService = ftService; + this._name = "FTAppender"; + this._formatter = new FTFormatter(); } -Appender.prototype = { +FTAppender.prototype = { + __proto__: new Log4Moz.Appender(), doAppend: function FTA_append(message) { - // TODO: Implement this. + this._ftService.onMessage(message); } }; -Appender.prototype.__proto__ = new Log4Moz.Appender(); diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 26a9f341966c..13be270a12b9 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -116,7 +116,7 @@ LogMessage.prototype = { }, toString: function LogMsg_toString(){ - return "LogMessage [" + this._date + " " + this.level + " " + + return "LogMessage [" + this.time + " " + this.level + " " + this.message + "]"; } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index dfaf1cced573..42562912948b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -69,6 +69,7 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/wrap.js"); +Cu.import("resource://weave/faultTolerance.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/dav.js"); @@ -88,6 +89,7 @@ let Weave = {}; Cu.import("resource://weave/constants.js", Weave); Cu.import("resource://weave/util.js", Weave); Cu.import("resource://weave/async.js", Weave); +Cu.import("resource://weave/faultTolerance.js", Weave); Cu.import("resource://weave/crypto.js", Weave); Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); @@ -134,17 +136,17 @@ function WeaveSvc(engines) { // Other misc startup Utils.prefs.addObserver("", this, false); this._os.addObserver(this, "quit-application", true); + FaultTolerance.Service; // initialize FT service - if (!this.enabled) { + if (!this.enabled) this._log.info("Weave Sync disabled"); - return; - } } WeaveSvc.prototype = { _notify: Wrap.notify, _lock: Wrap.lock, _localLock: Wrap.localLock, + _catchAll: Wrap.catchAll, _osPrefix: "weave:service:", _cancelRequested: false, _isQuitting: false, @@ -602,7 +604,8 @@ WeaveSvc.prototype = { }, login: function WeaveSvc_login(onComplete) { - this._localLock(this._notify("login", this._login)).async(this, onComplete); + this._catchAll(this._localLock(this._notify("login", this._login))). + async(this, onComplete); }, _login: function WeaveSvc__login() { let self = yield; @@ -763,7 +766,7 @@ WeaveSvc.prototype = { engine._tracker.resetScore(); } catch(e) { this._log.error(Utils.exceptionStr(e)); - this._log.error(Utils.stackTrace(e)); + this._log.debug(Utils.stackTrace(e)); this._syncError = true; } }, diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 7d11f5881a32..e77737cd5532 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -45,6 +45,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/faultTolerance.js"); Function.prototype.async = Async.sugar; @@ -165,8 +166,7 @@ let Wrap = { try { args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); - Async.run.apply(Async, args); - ret = yield; + ret = yield Async.run.apply(Async, args); } catch (e) { throw e; @@ -177,6 +177,30 @@ let Wrap = { this._osPrefix + "local-lock:released", ""); } + self.done(ret); + }; + }, + + // NOTE: see notify, this works the same way. they can be + // chained together as well. + // catchAll catches any exceptions and passes them to the fault tolerance svc + catchAll: function WeaveSync_catchAll(method /* , arg1, arg2, ..., argN */) { + let savedMethod = method; + let savedArgs = Array.prototype.slice.call(arguments, 1); + + return function WeaveCatchAllWrapper(/* argN+1, argN+2, ... */) { + let self = yield; + let ret; + let args = Array.prototype.slice.call(arguments); + + try { + args = savedArgs.concat(args); + args.unshift(this, savedMethod, self.cb); + ret = yield Async.run.apply(Async, args); + + } catch (e) { + ret = FaultTolerance.Service.onException(e); + } self.done(ret); }; } From 96f74cd93c26ddb462a2496eabd9e0c1aac43deb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 11 Jul 2008 17:40:52 -0700 Subject: [PATCH 0609/1860] login code refactoring --- services/sync/modules/service.js | 183 +++++++++++------- .../sync/tests/unit/test_fault_tolerance.js | 2 - 2 files changed, 114 insertions(+), 71 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 42562912948b..391be9c4b040 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -206,6 +206,7 @@ WeaveSvc.prototype = { get userPath() { return ID.get('WeaveID').username; }, get isLoggedIn() this._loggedIn, + get isInitialized() this._initialized, get isQuitting() this._isQuitting, set isQuitting(value) { this._isQuitting = value; }, @@ -223,16 +224,16 @@ WeaveSvc.prototype = { onWindowOpened: function Weave__onWindowOpened() { if (!this._startupFinished) { + this._startupFinished = true; if (Utils.prefs.getBoolPref("autoconnect") && this.username && this.username != 'nobody') this._initialLoginAndSync.async(this); - this._startupFinished = true; } }, _initialLoginAndSync: function Weave__initialLoginAndSync() { let self = yield; - yield this.login(self.cb); // will throw if login fails + yield this.loginAndInit(self.cb); // will throw if login fails yield this.sync(self.cb); }, @@ -345,7 +346,7 @@ WeaveSvc.prototype = { DAV.GET("meta/version", self.cb); let ret = yield; - if (!Utils.checkStatus(ret.status)) { + if (ret.status == 404) { this._log.info("Server has no version file. Wiping server data."); yield this._serverWipe.async(this, self.cb); yield this._uploadVersion.async(this, self.cb); @@ -356,7 +357,8 @@ WeaveSvc.prototype = { yield this._uploadVersion.async(this, self.cb); } else if (ret.responseText > STORAGE_FORMAT_VERSION) { - // FIXME: should we do something here? + // XXX should we do something here? + throw "Server version higher than this client understands. Aborting." } }, @@ -381,15 +383,12 @@ WeaveSvc.prototype = { // its information into the Identity object. If no Identity object // is supplied, the 'WeaveCryptoID' identity is used. // - // If 'createIfNecessary' is true (the default), then this function - // will create a new keypair if none currently exists. - // // This coroutine assumes the DAV singleton's prefix is set to the // proper user-specific directory. // // If the password associated with the Identity cannot be used to // decrypt the private key, an exception is raised. - _getKeypair : function WeaveSvc__getKeypair(id, createIfNecessary) { + _getKeypair : function WeaveSvc__getKeypair(id) { let self = yield; if ("none" == Utils.prefs.getCharPref("encryption")) @@ -398,40 +397,21 @@ WeaveSvc.prototype = { if (typeof(id) == "undefined") id = ID.get('WeaveCryptoID'); - if (typeof(createIfNecessary) == "undefined") - createIfNecessary = true; - this._log.trace("Retrieving keypair from server"); - let statuses = [[200, 300]]; - if (createIfNecessary) - statuses.push(404); - - // XXX this kind of replaces _keyCheck - // seems like key generation should only happen during setup? - - if (!(this._keyPair['private'] && this._keyPair['public'])) { + if (this._keyPair['private'] && this._keyPair['public']) + this._log.info("Using cached keypair"); + else { this._log.info("Fetching keypair from server."); - DAV.GET("private/privkey", self.cb); - let privkeyResp = yield; - Utils.ensureStatus(privkeyResp.status, - "Could not get private key from server", statuses); + let privkeyResp = yield DAV.GET("private/privkey", self.cb); + Utils.ensureStatus(privkeyResp.status, "Could not download private key"); - DAV.GET("public/pubkey", self.cb); - let pubkeyResp = yield; - Utils.ensureStatus(pubkeyResp.status, - "Could not get public key from server", statuses); - - if (privkeyResp.status == 404 || pubkeyResp.status == 404) { - yield this._generateKeys.async(this, self.cb); - return; - } + let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); + Utils.ensureStatus(pubkeyResp.status, "Could not download public key"); this._keyPair['private'] = this._json.decode(privkeyResp.responseText); this._keyPair['public'] = this._json.decode(pubkeyResp.responseText); - } else { - this._log.info("Using cached keypair"); } let privkeyData = this._keyPair['private'] @@ -549,13 +529,11 @@ WeaveSvc.prototype = { // These are global (for all engines) - verifyPassphrase: function WeaveSvc_verifyPassphrase(username, password, - passphrase) { - this._localLock(this._notify("verify-passphrase", - this._verifyPassphrase, - username, - password, - passphrase)).async(this, null); + verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, + password, passphrase) { + this._localLock(this._notify("verify-passphrase", this._verifyPassphrase, + username, password, passphrase)). + async(this, onComplete); }, _verifyPassphrase: function WeaveSvc__verifyPassphrase(username, password, @@ -564,65 +542,137 @@ WeaveSvc.prototype = { this._log.debug("Verifying passphrase"); - yield this._verifyLogin.async(this, self.cb, username, password); let id = new Identity('Passphrase Verification', username); id.setTempPassword(passphrase); // XXX: We're not checking the version of the server here, in part because // we have no idea what to do if the version is different than we expect // it to be. - yield this._getKeypair.async(this, self.cb, id, false); - let isValid = yield Crypto.isPassphraseValid.async(Crypto, self.cb, id); - if (!isValid) + + let privkeyResp = yield DAV.GET("private/privkey", self.cb); + Utils.ensureStatus(privkeyResp.status, "Could not download private key"); + + let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); + Utils.ensureStatus(privkeyResp.status, "Could not download public key"); + + let privkey = this._json.decode(privkeyResp.responseText); + let pubkey = this._json.decode(privkeyResp.responseText); + + if (!privkey || !pubkey) + throw "Bad keypair JSON"; + if (privkey.version != 1 || pubkey.version != 1) + throw "Unexpected keypair data version"; + if (privkey.algorithm != "RSA" || pubkey.algorithm != "RSA") + throw "Only RSA keys currently supported"; + + id.keypairAlg = privkey.algorithm; + id.privkey = privkey.privkey; + id.privkeyWrapIV = privkey.privkeyIV; + id.passphraseSalt = privkey.privkeySalt; + id.pubkey = pubkey.pubkey; + + if (!(yield Crypto.isPassphraseValid.async(Crypto, self.cb, id))) throw new Error("Passphrase is not valid."); }, - verifyLogin: function WeaveSvc_verifyLogin(username, password) { - this._log.debug("Verifying login for user " + username); - + verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password) { this._localLock(this._notify("verify-login", this._verifyLogin, - username, password)).async(this, null); + username, password)).async(this, onComplete); }, _verifyLogin: function WeaveSvc__verifyLogin(username, password) { let self = yield; - this.username = username; - this.password = password; + this._log.debug("Verifying login for user " + username); DAV.baseURL = Utils.prefs.getCharPref("serverURL"); DAV.defaultPrefix = "user/" + username; this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); - let status = yield DAV.checkLogin.async(DAV, self.cb, username, password); - if (status == 404) { - // create user directory (for self-hosted webdav shares) - yield this._checkUserDir.async(this, self.cb); - status = yield DAV.checkLogin.async(DAV, self.cb, username, password); - } + let status = yield DAV.checkLogin.async(DAV, self.cb, username, password); Utils.ensureStatus(status, "Login verification failed"); }, - login: function WeaveSvc_login(onComplete) { - this._catchAll(this._localLock(this._notify("login", this._login))). + loginAndInit: function WeaveSvc_loginAndInit(onComplete, + username, password, passphrase) { + this._localLock(this._notify("login", this._loginAndInit, + username, password, passphrase)). async(this, onComplete); }, - _login: function WeaveSvc__login() { + _loginAndInit: function WeaveSvc__loginAndInit(username, password, passphrase) { + let self = yield; + try { + yield this._login.async(this, self.cb, username, password, passphrase); + } catch (e) { + // we might need to initialize before login will work (e.g. to create the + // user directory), so do this and try again... + yield this._initialize.async(this, self.cb); + yield this._login.async(this, self.cb, username, password, passphrase); + } + yield this._initialize.async(this, self.cb); + }, + + login: function WeaveSvc_login(onComplete, username, password, passphrase) { + this._localLock( + this._notify("login", this._login, + username, password, passphrase)).async(this, onComplete); + }, + _login: function WeaveSvc__login(username, password, passphrase) { let self = yield; this._log.debug("Logging in user " + this.username); + if (typeof(username) != 'undefined') + this.username = username; + if (typeof(password) != 'undefined') + ID.get('WeaveID').setTempPassword(password); + if (typeof(passphrase) != 'undefined') + ID.get('WeaveCryptoID').setTempPassword(passphrase); + if (!this.username) throw "No username set, login failed"; if (!this.password) throw "No password given or found in password manager"; - yield this._verifyLogin.async(this, self.cb, this.username, - this.password); + yield this._verifyLogin.async(this, self.cb, this.username, this.password); + + this._loggedIn = true; + self.done(true); + }, + + initialize: function WeaveSvc_initialize() { + this._localLock( + this._notify("initialize", this._initialize)).async(this, onComplete); + }, + + _initialize: function WeaveSvc__initialize() { + let self = yield; + + // create user directory (for self-hosted webdav shares) if it doesn't exist + let status = yield DAV.checkLogin.async(DAV, self.cb, + this.username, this.password); + if (status == 404) { + yield this._checkUserDir.async(this, self.cb); + status = yield DAV.checkLogin.async(DAV, self.cb, + this.username, this.password); + } + Utils.ensureStatus(status, "Cannot initialize server"); + + // wipe the server if it has any old cruft + yield this._versionCheck.async(this, self.cb); + + // create public/private keypair if it doesn't exist + let privkeyResp = yield DAV.GET("private/privkey", self.cb); + let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); + if (privkeyResp.status == 404 || pubkeyResp.status == 404) + yield this._generateKeys.async(this, self.cb); + + // cache keys + yield this._getKeypair.async(this, self.cb); this._setSchedule(this.schedule); - this._loggedIn = true; + this._initialized = true; self.done(true); }, @@ -630,6 +680,7 @@ WeaveSvc.prototype = { this._log.info("Logging out"); this._disableSchedule(); this._loggedIn = false; + this._initialized = false; this._keyPair = {}; ID.get('WeaveID').setTempPassword(null); // clear cached password ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase @@ -680,9 +731,6 @@ WeaveSvc.prototype = { _sync: function WeaveSvc__sync() { let self = yield; - yield this._versionCheck.async(this, self.cb); - yield this._getKeypair.async(this, self.cb); - let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { if (this.cancelRequested) @@ -709,9 +757,6 @@ WeaveSvc.prototype = { _syncAsNeeded: function WeaveSvc__syncAsNeeded() { let self = yield; - yield this._versionCheck.async(this, self.cb); - yield this._getKeypair.async(this, self.cb); - let engines = Engines.getAll(); for each (let engine in engines) { if (!engine.enabled) diff --git a/services/sync/tests/unit/test_fault_tolerance.js b/services/sync/tests/unit/test_fault_tolerance.js index b7dbe57edcbf..e016a9630afa 100644 --- a/services/sync/tests/unit/test_fault_tolerance.js +++ b/services/sync/tests/unit/test_fault_tolerance.js @@ -6,8 +6,6 @@ function run_test() { FaultTolerance.Service._testProperty = "hi"; do_check_eq(FaultTolerance.Service._testProperty, "hi"); - FaultTolerance.Service.init(); - var log = Log4Moz.Service.rootLogger; log.level = Log4Moz.Level.All; log.info("Testing."); From 3644dd3444920ec56d265e2446a0ecd019936026 Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Fri, 11 Jul 2008 18:29:33 -0700 Subject: [PATCH 0610/1860] - in sync error notification changed "try again" to "sync now.." which will pop a modal sync. --- services/sync/locales/en-US/sync.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index d48e7a82dce3..591bd9a17185 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -20,5 +20,5 @@ error.logout.title = Error While Signing Out error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it (then why are we bugging you with this info?). error.sync.title = Error While Syncing error.sync.description = Weave encountered an error while syncing. You might want to try syncing manually to make sure you are up-to-date. -error.sync.tryAgainButton.label = Try Again -error.sync.tryAgainButton.accesskey = T +error.sync.tryAgainButton.label = Sync Now... +error.sync.tryAgainButton.accesskey = S From 14627d2c24e7655dd1da3eff3d7a2b3242c84ada Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Fri, 11 Jul 2008 18:52:04 -0700 Subject: [PATCH 0611/1860] - adding modal sync dialog handling of lock condition --- services/sync/locales/en-US/status.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index 2b8f9cbd2913..33db96c697cd 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -6,6 +6,9 @@ status.cancel = Cancelling Sync, Please Wait status.cancelled = Sync Cancelled status.closing = Closing... +status.locked = Server Busy +status.tryagain = Please try again in a few minutes. + status.engine_start = Starting Sync status.downloading-status = Downloading Status from Server From 2e010d048a3bf699cc687f1743a3d20148fd80bb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 11 Jul 2008 18:55:42 -0700 Subject: [PATCH 0612/1860] add last exception to fault tolerance module --- services/sync/modules/faultTolerance.js | 10 ++++--- services/sync/modules/service.js | 36 +++++++++++++++---------- services/sync/modules/stores.js | 2 +- services/sync/modules/wrap.js | 2 ++ 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index f23d3df3a1c7..7dcf78856dc2 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -54,12 +54,16 @@ function FTService() { Log4Moz.Service.rootLogger.addAppender(this._appender); } FTService.prototype = { + get lastException() this._lastException, onMessage: function FTS_onMessage(message) { - //dump("got a message: " + message + "\n"); + // FIXME: we get all log messages here, and could use them to keep track of + // our current state }, onException: function FTS_onException(exception) { - dump("got an exception: " + exception + "\n"); - throw exception; + this._lastException = exception; + if ("Could not acquire lock" == exception) + return false; // fatal error + return true; // continue } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 391be9c4b040..346d0982a28d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -246,7 +246,7 @@ WeaveSvc.prototype = { this._enableSchedule(); break; default: - this._log.info("Invalid Weave scheduler setting: " + schedule); + this._log.warn("Invalid Weave scheduler setting: " + schedule); break; } }, @@ -261,7 +261,7 @@ WeaveSvc.prototype = { let listener = new Utils.EventListener(Utils.bind2(this, this._onSchedule)); this._scheduleTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL, this._scheduleTimer.TYPE_REPEATING_SLACK); - this._log.info("Weave scheduler enabled"); + this._log.config("Weave scheduler enabled"); }, _disableSchedule: function WeaveSvc__disableSchedule() { @@ -269,7 +269,7 @@ WeaveSvc.prototype = { this._scheduleTimer.cancel(); this._scheduleTimer = null; } - this._log.info("Weave scheduler disabled"); + this._log.config("Weave scheduler disabled"); }, _onSchedule: function WeaveSvc__onSchedule() { @@ -278,7 +278,8 @@ WeaveSvc.prototype = { this._log.info("Skipping scheduled sync; local operation in progress") } else { this._log.info("Running scheduled sync"); - this._notify("sync", this._lock(this._syncAsNeeded)).async(this); + this._notify("sync", + this._catchAll(this._lock(this._syncAsNeeded))).async(this); } } }, @@ -400,9 +401,9 @@ WeaveSvc.prototype = { this._log.trace("Retrieving keypair from server"); if (this._keyPair['private'] && this._keyPair['public']) - this._log.info("Using cached keypair"); + this._log.debug("Using cached keypair"); else { - this._log.info("Fetching keypair from server."); + this._log.debug("Fetching keypair from server"); let privkeyResp = yield DAV.GET("private/privkey", self.cb); Utils.ensureStatus(privkeyResp.status, "Could not download private key"); @@ -587,7 +588,7 @@ WeaveSvc.prototype = { DAV.baseURL = Utils.prefs.getCharPref("serverURL"); DAV.defaultPrefix = "user/" + username; - this._log.info("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); + this._log.config("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); let status = yield DAV.checkLogin.async(DAV, self.cb, username, password); Utils.ensureStatus(status, "Login verification failed"); @@ -648,6 +649,8 @@ WeaveSvc.prototype = { _initialize: function WeaveSvc__initialize() { let self = yield; + this._log.info("Making sure server is initialized..."); + // create user directory (for self-hosted webdav shares) if it doesn't exist let status = yield DAV.checkLogin.async(DAV, self.cb, this.username, this.password); @@ -661,14 +664,18 @@ WeaveSvc.prototype = { // wipe the server if it has any old cruft yield this._versionCheck.async(this, self.cb); - // create public/private keypair if it doesn't exist + // cache keys, create public/private keypair if it doesn't exist + this._log.debug("Caching keys"); let privkeyResp = yield DAV.GET("private/privkey", self.cb); let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); + if (privkeyResp.status == 404 || pubkeyResp.status == 404) yield this._generateKeys.async(this, self.cb); - // cache keys - yield this._getKeypair.async(this, self.cb); + this._keyPair['private'] = this._json.decode(privkeyResp.responseText); + this._keyPair['public'] = this._json.decode(pubkeyResp.responseText); + + yield this._getKeypair.async(this, self.cb); // makes sure passphrase works this._setSchedule(this.schedule); @@ -725,7 +732,8 @@ WeaveSvc.prototype = { // These are per-engine sync: function WeaveSvc_sync(onComplete) { - this._notify("sync", this._lock(this._sync)).async(this, onComplete); + this._notify("sync", + this._catchAll(this._lock(this._sync))).async(this, onComplete); }, _sync: function WeaveSvc__sync() { @@ -810,9 +818,9 @@ WeaveSvc.prototype = { yield engine.sync(self.cb); engine._tracker.resetScore(); } catch(e) { - this._log.error(Utils.exceptionStr(e)); - this._log.debug(Utils.stackTrace(e)); - this._syncError = true; + let ok = FaultTolerance.Service.onException(e); + if (!ok) + this._syncError = true; } }, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index cf16ce9dec23..2e10ed3bdc78 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -238,7 +238,7 @@ SnapshotStore.prototype = { json = this._json.decode(json); if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { - this._log.info("Read saved snapshot from disk"); + this._log.debug("Read saved snapshot from disk"); this.data = json.snapshot; this.version = json.version; this.GUID = json.GUID; diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index e77737cd5532..74dca5366e45 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -200,6 +200,8 @@ let Wrap = { } catch (e) { ret = FaultTolerance.Service.onException(e); + if (!ret) + throw "Unrecoverable error"; } self.done(ret); }; From ac971d4986aea462accfdd0e6394b48d807c868b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 11 Jul 2008 19:01:06 -0700 Subject: [PATCH 0613/1860] rethrow exceptions the fault tolerance module says are bad. catch lock exceptions in the notify wrapper and don't rethrow them (hack) --- services/sync/modules/wrap.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 74dca5366e45..59f3b75e5ef1 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -99,7 +99,8 @@ let Wrap = { } catch (e) { this._os.notifyObservers(null, this._osPrefix + savedName + ":error", ""); this._os.notifyObservers(null, this._osPrefix + "global:error", ""); - throw e; + if (e != "Could not acquire lock") // FIXME HACK + throw e; } self.done(ret); @@ -201,7 +202,7 @@ let Wrap = { } catch (e) { ret = FaultTolerance.Service.onException(e); if (!ret) - throw "Unrecoverable error"; + throw e; } self.done(ret); }; From d79fa80559ba000955bb5e5f97d93bda5aed3902 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 11 Jul 2008 19:01:36 -0700 Subject: [PATCH 0614/1860] version bump to 0.2.3 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 71b3056f0200..84b0e0243d93 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.2.2"; +const WEAVE_VERSION = "0.2.3"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 15fcac3eec57dfb59d807820e54651582f9e393d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 12 Jul 2008 20:31:27 -0700 Subject: [PATCH 0615/1860] fix verifyPassphrase() --- services/crypto/Makefile | 2 +- services/sync/modules/service.js | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 17b27e29b5c8..c12acb3b54a2 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -52,7 +52,7 @@ else compiler = gcc3 cxx = c++ so = dylib - cppflags += -dynamiclib + cppflags += -dynamiclib -DDEBUG else ifeq ($(sys), MINGW32_NT-6.0) os = WINNT diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 346d0982a28d..f70372a0977b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -424,6 +424,10 @@ WeaveSvc.prototype = { throw "Unexpected keypair data version"; if (privkeyData.algorithm != "RSA" || pubkeyData.algorithm != "RSA") throw "Only RSA keys currently supported"; + if (!privkey.privkey) + throw "Private key does not contain private key data!"; + if (!pubkey.pubkey) + throw "Public key does not contain public key data!"; id.keypairAlg = privkeyData.algorithm; @@ -545,9 +549,13 @@ WeaveSvc.prototype = { let id = new Identity('Passphrase Verification', username); id.setTempPassword(passphrase); + + // FIXME: abstract common bits and share code with getKeypair() + // XXX: We're not checking the version of the server here, in part because // we have no idea what to do if the version is different than we expect // it to be. + // XXX check it and ... throw? let privkeyResp = yield DAV.GET("private/privkey", self.cb); Utils.ensureStatus(privkeyResp.status, "Could not download private key"); @@ -556,7 +564,7 @@ WeaveSvc.prototype = { Utils.ensureStatus(privkeyResp.status, "Could not download public key"); let privkey = this._json.decode(privkeyResp.responseText); - let pubkey = this._json.decode(privkeyResp.responseText); + let pubkey = this._json.decode(pubkeyResp.responseText); if (!privkey || !pubkey) throw "Bad keypair JSON"; @@ -564,6 +572,10 @@ WeaveSvc.prototype = { throw "Unexpected keypair data version"; if (privkey.algorithm != "RSA" || pubkey.algorithm != "RSA") throw "Only RSA keys currently supported"; + if (!privkey.privkey) + throw "Private key does not contain private key data!"; + if (!pubkey.pubkey) + throw "Public key does not contain public key data!"; id.keypairAlg = privkey.algorithm; id.privkey = privkey.privkey; From ee31e82214b5dc5266811acc21937944b8a050b6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 12 Jul 2008 20:58:19 -0700 Subject: [PATCH 0616/1860] fix sanity checks in getKeypair, cause key 404s to be interpreted as a success condition in verifyPassphrase, re-fetch keys after generating them in initialize --- services/sync/modules/service.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f70372a0977b..f87c27174f7a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -424,9 +424,9 @@ WeaveSvc.prototype = { throw "Unexpected keypair data version"; if (privkeyData.algorithm != "RSA" || pubkeyData.algorithm != "RSA") throw "Only RSA keys currently supported"; - if (!privkey.privkey) + if (!privkeyData.privkey) throw "Private key does not contain private key data!"; - if (!pubkey.pubkey) + if (!pubkeyData.pubkey) throw "Public key does not contain public key data!"; @@ -558,9 +558,16 @@ WeaveSvc.prototype = { // XXX check it and ... throw? let privkeyResp = yield DAV.GET("private/privkey", self.cb); - Utils.ensureStatus(privkeyResp.status, "Could not download private key"); - let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); + + // FIXME: this will cause 404's to turn into a 'success' + // while technically incorrect, this function currently only gets + // called from the wizard, which will do a loginAndInit, which + // will create the keys if necessary later + if (privkeyResp.status == 404 || pubkeyResp.status == 404) + return; + + Utils.ensureStatus(privkeyResp.status, "Could not download private key"); Utils.ensureStatus(privkeyResp.status, "Could not download public key"); let privkey = this._json.decode(privkeyResp.responseText); @@ -681,8 +688,14 @@ WeaveSvc.prototype = { let privkeyResp = yield DAV.GET("private/privkey", self.cb); let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); - if (privkeyResp.status == 404 || pubkeyResp.status == 404) + if (privkeyResp.status == 404 || pubkeyResp.status == 404) { yield this._generateKeys.async(this, self.cb); + privkeyResp = yield DAV.GET("private/privkey", self.cb); + pubkeyResp = yield DAV.GET("public/pubkey", self.cb); + } + + Utils.ensureStatus(privkeyResp.status, "Cannot initialize privkey"); + Utils.ensureStatus(pubkeyResp.status, "Cannot initialize pubkey"); this._keyPair['private'] = this._json.decode(privkeyResp.responseText); this._keyPair['public'] = this._json.decode(pubkeyResp.responseText); From 47c8c6189040233b989449cfe8768a17d66c7b10 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 12 Jul 2008 21:08:36 -0700 Subject: [PATCH 0617/1860] version bump to 0.2.4 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 84b0e0243d93..1960eb882a86 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.2.3"; +const WEAVE_VERSION = "0.2.4"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 8a6192806f49ff8de9d0ed79be93c74837b24c81 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 13 Jul 2008 04:06:44 -0700 Subject: [PATCH 0618/1860] Bug 442679: Add support for compiling on 64-bit linux systems. Patch by Mark Wilkinson --- services/crypto/Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index c12acb3b54a2..ed87fb0971d3 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -89,8 +89,12 @@ else ifeq ($(machine), arm) arch = arm else - # FIXME: x86_64, ia64, sparc, Alpha - $(error Sorry, your arch is unknown/unsupported: $(machine)) + ifeq ($(machine), x86_64) + arch = x86_64 + else + # FIXME: ia64, sparc, Alpha + $(error Sorry, your arch is unknown/unsupported: $(machine)) + endif endif endif endif From 6839322778be563676513e6f56da7c08a2a8fbaa Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 16 Jul 2008 19:33:07 -0700 Subject: [PATCH 0619/1860] fix a strict warning when onComplete is null --- services/sync/modules/async.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index 32c0fa8606e6..bec128da6f37 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -186,7 +186,8 @@ Generator.prototype = { this._log.trace(this.name + ": End of coroutine reached."); // skip to calling done() - } else if (this.onComplete.parentGenerator instanceof Generator) { + } else if (this.onComplete && this.onComplete.parentGenerator && + this.onComplete.parentGenerator instanceof Generator) { this._log.trace("[" + this.name + "] Saving exception and stack trace"); this._log.trace("Exception: " + Utils.exceptionStr(e)); From c1f860de117b4429e3f73850e471f5fa89b24bc9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 16 Jul 2008 19:34:35 -0700 Subject: [PATCH 0620/1860] during version check don't attempt to wipe the server when the version file isn't found (that was a workaround for old clients, before the version file existed) --- services/sync/modules/service.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f87c27174f7a..498f432a0a0c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -347,10 +347,10 @@ WeaveSvc.prototype = { DAV.GET("meta/version", self.cb); let ret = yield; - if (ret.status == 404) { - this._log.info("Server has no version file. Wiping server data."); - yield this._serverWipe.async(this, self.cb); - yield this._uploadVersion.async(this, self.cb); + if (Utils.checkStatus(ret.status)) { + this._log.debug("Could not get version file from server"); + self.done(false); + return; } else if (ret.responseText < STORAGE_FORMAT_VERSION) { this._log.info("Server version too low. Wiping server data."); @@ -361,6 +361,7 @@ WeaveSvc.prototype = { // XXX should we do something here? throw "Server version higher than this client understands. Aborting." } + self.done(true); }, _checkUserDir: function WeaveSvc__checkUserDir() { From 472d843d1b136d8e03b700e20e30c6b455ff0098 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 16 Jul 2008 19:36:06 -0700 Subject: [PATCH 0621/1860] allow deep copies of objects to optionally copy object properties in alphabetical order. This is useful to guarantee the order in which they would be serialized as json (which may depend on the order in which properties are added) --- services/sync/modules/util.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index f11c3603ec6a..e82eb9597609 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -183,7 +183,7 @@ let Utils = { return true; }, - deepCopy: function Weave_deepCopy(thing) { + deepCopy: function Weave_deepCopy(thing, sort) { if (typeof(thing) != "object" || thing == null) return thing; let ret; @@ -191,12 +191,14 @@ let Utils = { if ("Array" == thing.constructor.name) { ret = []; for (let i = 0; i < thing.length; i++) - ret.push(Utils.deepCopy(thing[i])); + ret.push(Utils.deepCopy(thing[i]), sort); } else { ret = {}; - for (let key in thing) - ret[key] = Utils.deepCopy(thing[key]); + let props = [p for (p in thing)]; + if (sort) + props = props.sort(); + props.forEach(function(k) ret[k] = Utils.deepCopy(thing[k], sort)); } return ret; From 4e879fab9000fe20ae643226f034d6c18d9b0b8c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 16 Jul 2008 20:47:16 -0700 Subject: [PATCH 0622/1860] Bug 442931: Create a universal (x86/PPC) component on mac. Patch by Godwin Chan , with tweaks by me --- services/crypto/Makefile | 297 ++++++++++++++++++++++++--------------- 1 file changed, 182 insertions(+), 115 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index ed87fb0971d3..1b5ab5d65eb1 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -21,6 +21,7 @@ # # Contributor(s): # Dan Mills (original author) +# Godwin Chan (Darwin Universal Binary) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -36,10 +37,17 @@ # # ***** END LICENSE BLOCK ***** -# Platform detection +# OS detection sys := $(shell uname -s) +ifeq ($(sys), Darwin) + os = Darwin + compiler = gcc3 + cxx = c++ + so = dylib + cppflags += -dynamiclib -DDEBUG +else ifeq ($(sys), Linux) os = Linux compiler = gcc3 @@ -47,68 +55,83 @@ ifeq ($(sys), Linux) so = so cppflags += -shared else - ifeq ($(sys), Darwin) - os = Darwin - compiler = gcc3 - cxx = c++ - so = dylib - cppflags += -dynamiclib -DDEBUG - else - ifeq ($(sys), MINGW32_NT-6.0) - os = WINNT - compiler = msvc - cxx = cl - so = dll - else - ifeq ($(sys), MINGW32_NT-5.1) - os = WINNT - compiler = msvc - cxx = cl - so = dll - else - $(error Sorry, your os is unknown/unsupported: $(sys)) - endif - endif - endif +ifeq ($(sys), MINGW32_NT-6.0) + os = WINNT + compiler = msvc + cxx = cl + so = dll +else +ifeq ($(sys), MINGW32_NT-5.1) + os = WINNT + compiler = msvc + cxx = cl + so = dll +else + $(error Sorry, your os is unknown/unsupported: $(sys)) endif +endif +endif +endif + +# Arch detection machine := $(shell uname -m) +ifeq ($(machine), arm) + arch = arm +else ifeq ($(machine), i386) arch = x86 else - ifeq ($(machine), i586) - arch = x86 - else - ifeq ($(machine), i686) - arch = x86 - else - ifeq ($(machine), ppc) # FIXME: verify - arch = ppc - else - ifeq ($(machine), arm) - arch = arm - else - ifeq ($(machine), x86_64) - arch = x86_64 - else - # FIXME: ia64, sparc, Alpha - $(error Sorry, your arch is unknown/unsupported: $(machine)) - endif - endif - endif - endif - endif +ifeq ($(machine), i586) + arch = x86 +else +ifeq ($(machine), i686) + arch = x86 +else +ifeq ($(machine), Power Macintosh) + arch = ppc +else +ifeq ($(machine), x86_64) + arch = x86_64 +else + $(error: Sorry, your architecture is unknown/unsupported: $(machine)) +endif +endif +endif +endif +endif endif -platform = $(os)_$(arch)-$(compiler) +# Universal binary so no need for $(arch) for Darwin -###################################################################### +ifeq ($(sys), Darwin) + platform = $(os)-$(compiler) +else + platform = $(os)_$(arch)-$(compiler) +endif +################################################################### +# Target and objects + +ifeq ($(sys), Darwin) + target = WeaveCrypto + target_i386 = WeaveCrypto.i386 + target_ppc = WeaveCrypto.ppc + so_target = $(target:=.$(so)) + so_target_i386 = $(target_i386:=.$(so)) + so_target_ppc = $(target_ppc:=.$(so)) + cpp_objects_i386 = $(cpp_sources:.cpp=.oi386) + cpp_objects_ppc = $(cpp_sources:.cpp=.oppc) +else + target = WeaveCrypto + so_target = $(target:=.$(so)) + cpp_objects = $(cpp_sources:.cpp=.o) +endif + +# source and path configurations idl = IWeaveCrypto.idl cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp -# will have .so / .dylib / .dll appended -target = WeaveCrypto sdkdir ?= ${MOZSDKDIR} destdir = .. @@ -139,6 +162,7 @@ headers = -I$(sdkdir)/include \ -I$(sdkdir)/include/nspr \ -I$(sdkdir)/sdk/include +# libraries libdirs := $(sdkdir)/lib $(sdkdir)/bin libs := xpcomglue_s xpcom nspr4 \ crmf smime3 ssl3 nss3 nssutil3 \ @@ -148,49 +172,80 @@ ifeq ($(os), linux) libs := xpcom_core $(libs) endif -ifeq ($(compiler),msvc) -libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) -libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) -cppflags += -c -nologo -O1 -GR- -TP -MT -Zc:wchar_t- -W3 -Gy $(headers) \ --DNDEBUG -DTRIMMED -D_CRT_SECURE_NO_DEPRECATE=1 \ --D_CRT_NONSTDC_NO_DEPRECATE=1 -DWINVER=0x500 -D_WIN32_WINNT=0x500 \ --D_WIN32_IE=0x0500 -DX_DISPLAY_MISSING=1 -DMOZILLA_VERSION=\"1.9pre\" \ --DMOZILLA_VERSION_U=1.9pre -DHAVE_SNPRINTF=1 -D_WINDOWS=1 -D_WIN32=1 \ --DWIN32=1 -DXP_WIN=1 -DXP_WIN32=1 -DHW_THREADS=1 -DSTDC_HEADERS=1 \ --DWIN32_LEAN_AND_MEAN=1 -DNO_X11=1 -DHAVE_MMINTRIN_H=1 \ --DHAVE_OLEACC_IDL=1 -DHAVE_ATLBASE_H=1 -DHAVE_WPCAPI_H=1 -D_X86_=1 \ --DD_INO=d_ino -ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ - $(libdirs) $(libs) \ - kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib -rcflags := -r $(headers) +# compiler and Linker Flags + +ifeq ($(os), Darwin) + libdirs := $(patsubst %,-L%,$(libdirs)) + libs := $(patsubst %,-l%,$(libs)) + cppflags_i386 += -c -pipe -Os -arch i386 \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -fshort-wchar -fpascal-strings -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) \ + -isysroot /Developer/SDKs/MacOSX10.4u.sdk + ldflags_i386 += -pthread -pipe -bundle -arch i386 \ + -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + $(libdirs) $(libs) + cppflags_ppc += -c -pipe -Os -arch ppc \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -fshort-wchar -fpascal-strings -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) \ + -isysroot /Developer/SDKs/MacOSX10.4u.sdk \ + -force_cpusubtype_ALL + ldflags_ppc += -pthread -pipe -bundle -arch ppc \ + -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + -force_cpusubtype_ALL \ + $(libdirs) $(libs) else +ifeq ($(os), Linux) libdirs := $(patsubst %,-L%,$(libdirs)) libs := $(patsubst %,-l%,$(libs)) cppflags += -pipe -Os \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -fshort-wchar -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) - ifeq ($(os), Linux) - ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,-z,defs -Wl,-h,WeaveCrypto.so \ - -Wl,-rpath-link,$(sdkdir)/bin \ - $(sdkdir)/lib/libxpcomglue_s.a \ - $(libdirs) $(libs) - else - cppflags += -fpascal-strings -c - ldflags += -pthread -pipe -bundle \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - $(libdirs) $(libs) - endif + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -fshort-wchar -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) + ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,-z,defs -Wl,-h,WeaveCrypto.so \ + -Wl,-rpath-link,$(sdkdir)/bin \ + $(sdkdir)/lib/libxpcomglue_s.a \ + $(libdirs) $(libs) +else +ifeq ($(os), WINNT) + libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) + libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) + cppflags += -c -nologo -O1 -GR- -TP -MT -Zc:wchar_t- -W3 -Gy $(headers) \ + -DNDEBUG -DTRIMMED -D_CRT_SECURE_NO_DEPRECATE=1 \ + -D_CRT_NONSTDC_NO_DEPRECATE=1 -DWINVER=0x500 -D_WIN32_WINNT=0x500 \ + -D_WIN32_IE=0x0500 -DX_DISPLAY_MISSING=1 -DMOZILLA_VERSION=\"1.9pre\" \ + -DMOZILLA_VERSION_U=1.9pre -DHAVE_SNPRINTF=1 -D_WINDOWS=1 -D_WIN32=1 \ + -DWIN32=1 -DXP_WIN=1 -DXP_WIN32=1 -DHW_THREADS=1 -DSTDC_HEADERS=1 \ + -DWIN32_LEAN_AND_MEAN=1 -DNO_X11=1 -DHAVE_MMINTRIN_H=1 \ + -DHAVE_OLEACC_IDL=1 -DHAVE_ATLBASE_H=1 -DHAVE_WPCAPI_H=1 -D_X86_=1 \ + -DD_INO=d_ino + ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ + $(libdirs) $(libs) \ + kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib + rcflags := -r $(headers) +endif +endif endif ###################################################################### @@ -216,43 +271,55 @@ test-install: install rm -f $(sdkdir)/bin/components/xpti.dat clean: - rm -f $(so_target) $(cpp_objects) $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp + rm -f $(so_target) $(so_target_i386) $(so_target_ppc) \ + $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ + $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp # rules to build the c headers and .xpt from idl - $(idl_headers): $(idl) $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) $(idl_typelib): $(idl) $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) -# "main" (internal) rules, build sources and link the component +# build and link rules +ifeq ($(os), Darwin) + $(so_target): $(so_target_i386) $(so_target_ppc) + lipo -create -output $(so_target) -arch ppc $(so_target_ppc) \ + -arch i386 $(so_target_i386) + chmod +x $(so_target) -$(cpp_objects): $(cpp_sources) -ifeq ($(cxx),cl) - $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) -endif -ifeq ($(os),Linux) - # don't need object files, but we need to satisfy make - touch $(cpp_objects) + #i386 + $(cpp_objects_i386): $(cpp_sources) + $(cxx) -o $@ $(cppflags_i386) $(@:.oi386=.cpp) + + $(so_target_i386): $(idl_headers) $(cpp_objects_i386) + $(cxx) -o $@ $(ldflags_i386) $(cpp_objects_i386) + chmod +x $(so_target_i386) + + #ppc + $(cpp_objects_ppc): $(cpp_sources) + $(cxx) -o $@ $(cppflags_ppc) $(@:.oppc=.cpp) + + $(so_target_ppc): $(idl_headers) $(cpp_objects_ppc) + $(cxx) -o $@ $(ldflags_ppc) $(cpp_objects_ppc) + chmod +x $(so_target_ppc) else - $(cxx) -o $@ $(cppflags) $(@:.o=.cpp) -endif - -ifeq ($(compiler),msvc) -$(target:=.res): $(target:=.rc) +ifeq ($(os), Linux) + $(so_target): $(idl_headers) + $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) + chmod +x $@ +else +ifeq ($(os), WINNT) + $(target:=.res): $(target:=.rc) rc -Fo$@ $(rcflags) $(target:=.rc) -$(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) + $(cpp_objects): $(cpp_sources) + $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) + + $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) -else -ifeq ($(os),Linux) -$(so_target): $(idl_headers) $(cpp_objects) - $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) -else -$(so_target): $(idl_headers) $(cpp_objects) - $(cxx) -o $@ $(ldflags) $(cpp_objects) -endif -endif chmod +x $@ -# strip $@ +endif +endif +endif From d1368450383aeba5fbc4b55fb22aa78a2cb9cf8e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 16 Jul 2008 20:51:25 -0700 Subject: [PATCH 0623/1860] Bug 443489: Use nsICookie2 interface instead of nsICookie. Patch by fabrice@bellet.info --- services/sync/modules/engines/cookies.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 0c5baab9df86..86d5642832fb 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -202,7 +202,7 @@ CookieStore.prototype = { var matchingCookie = null; while (iter.hasMoreElements()){ let cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie ) ){ + if (cookie.QueryInterface( Ci.nsICookie2 ) ){ // see if host:path:name of cookie matches GUID given in command let key = cookie.host + ":" + cookie.path + ":" + cookie.name; if (key == command.GUID) { @@ -247,7 +247,7 @@ CookieStore.prototype = { var iter = this._cookieManager.enumerator; while (iter.hasMoreElements()) { var cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie )) { + if (cookie.QueryInterface( Ci.nsICookie2 )) { // String used to identify cookies is // host:path:name if ( cookie.isSession ) { From 4d4556a241fab3c427f7a6d5d684ec6ea65c53f4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 16 Jul 2008 21:11:18 -0700 Subject: [PATCH 0624/1860] rollback last commit (1843a139184a), it causes cookie unit test to fail --- services/sync/modules/engines/cookies.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 86d5642832fb..0c5baab9df86 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -202,7 +202,7 @@ CookieStore.prototype = { var matchingCookie = null; while (iter.hasMoreElements()){ let cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie2 ) ){ + if (cookie.QueryInterface( Ci.nsICookie ) ){ // see if host:path:name of cookie matches GUID given in command let key = cookie.host + ":" + cookie.path + ":" + cookie.name; if (key == command.GUID) { @@ -247,7 +247,7 @@ CookieStore.prototype = { var iter = this._cookieManager.enumerator; while (iter.hasMoreElements()) { var cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie2 )) { + if (cookie.QueryInterface( Ci.nsICookie )) { // String used to identify cookies is // host:path:name if ( cookie.isSession ) { From 4e2377978dc197c23859b19f8743858608e2a60d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 17 Jul 2008 20:38:42 -0700 Subject: [PATCH 0625/1860] use just 'Darwin' for the platform dir on OSX. 'Darwin-gcc3' does not appear to work correctly --- services/crypto/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 1b5ab5d65eb1..91bd999e3c05 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -106,7 +106,7 @@ endif # Universal binary so no need for $(arch) for Darwin ifeq ($(sys), Darwin) - platform = $(os)-$(compiler) + platform = $(os) else platform = $(os)_$(arch)-$(compiler) endif From 3bb40f50bf6dadc49e7c6be920bbacadffd49070 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 17 Jul 2008 20:39:55 -0700 Subject: [PATCH 0626/1860] check at startup if the crypto module appears to be working and alert the user if not --- services/sync/modules/crypto.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 7ce787b476ea..07f4aff5d194 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -99,11 +99,7 @@ CryptoSvc.prototype = { observe: function Sync_observe(subject, topic, data) { switch (topic) { case "extensions.weave.encryption": { - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - - let cur = branch.getCharPref("extensions.weave.encryption"); - if (cur == data) + if (Utils.pref.getCharPref("encryption") == data) return; switch (data) { @@ -124,6 +120,21 @@ CryptoSvc.prototype = { } }, + checkModule: function Crypto_checkModule() { + let ok = false; + + try { + let svc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + createInstance(Ci.IWeaveCrypto); + let iv = svc.generateRandomIV(); + if (iv.length == 24) + ok = true; + + } catch (e) {} + + return ok; + }, + // Crypto encryptData: function Crypto_encryptData(data, identity) { From 2812cac48f8a81d1cb16df901199b30e3d5f11d3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 17 Jul 2008 20:40:29 -0700 Subject: [PATCH 0627/1860] time out requests after 30 seconds --- services/sync/modules/dav.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 63cee604a1ef..9f04146db946 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -133,6 +133,12 @@ DAVCollection.prototype = { request.mozBackgroundRequest = true; request.open(op, this._baseURL + path, true); + // time out requests after 30 seconds + let cb = function() { request.abort(); }; + let listener = new Utils.EventListener(cb); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(listener, 30000, timer.TYPE_ONE_SHOT); + // Force cache validation let channel = request.channel; channel = channel.QueryInterface(Ci.nsIRequest); @@ -156,7 +162,7 @@ DAVCollection.prototype = { if (ret.status == 423) this._log.warn("_makeRequest: got status " + ret.status + " (This is not necessarily bad. It could just mean that another Firefox was syncing at the same time.)"); else - if (ret.status < 200 || ret.status >= 300) + if (ret.status < 200 || ret.status >= 300) this._log.warn("_makeRequest: got status " + ret.status); self.done(ret); From f4f8162b3fd2ed54e63145e929c4c38a6ab28b5a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 17 Jul 2008 20:41:10 -0700 Subject: [PATCH 0628/1860] when changing passwords, do not allow the new one to be the same as the passphrase --- services/sync/locales/en-US/preferences.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index 596a1fea8021..4c2fa3386fe8 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -12,6 +12,7 @@ reset.lock.warning = This will reset the write lock on the Weave server for your change.password.status.active = Changing your password... change.password.status.success = Your password has been changed. change.password.status.error = There was an error changing your password. +change.password.status.passwordSameAsPassphrase = The password cannot be the same as the passphrase. change.password.status.passwordsDoNotMatch = The passwords you entered do not match. change.password.status.noOldPassword = You didn't enter your current password. change.password.status.noNewPassword = You didn't enter a new password. From e9f631c5f31e2e9d3665cb89b77d6033f62087a4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 17 Jul 2008 21:27:01 -0700 Subject: [PATCH 0629/1860] fixes to request timeout code --- services/sync/modules/dav.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 9f04146db946..5b6aa1d4994d 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -134,8 +134,7 @@ DAVCollection.prototype = { request.open(op, this._baseURL + path, true); // time out requests after 30 seconds - let cb = function() { request.abort(); }; - let listener = new Utils.EventListener(cb); + let listener = new Utils.EventListener(this._timeoutCb(request, xhrCb)); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); timer.initWithCallback(listener, 30000, timer.TYPE_ONE_SHOT); @@ -159,15 +158,21 @@ DAVCollection.prototype = { let event = yield; ret = event.target; - if (ret.status == 423) - this._log.warn("_makeRequest: got status " + ret.status + " (This is not necessarily bad. It could just mean that another Firefox was syncing at the same time.)"); - else - if (ret.status < 200 || ret.status >= 300) - this._log.warn("_makeRequest: got status " + ret.status); - self.done(ret); }, + _timeoutCb: function DC__timeoutCb(request, callback) { + return function() { + try { + let test = request.status; + } catch (e) { + this._log.warn("Connection timed out, aborting..."); + request.abort(); + callback({target:{status:-1}}); + } + }; + }, + get _defaultHeaders() { let h = {'Content-type': 'text/plain'}, id = ID.get(this.identity), From 2aba65861ce2d6301bb163553ccbd027c8e364a1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 17 Jul 2008 21:27:50 -0700 Subject: [PATCH 0630/1860] version bump to 0.2.5 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 1960eb882a86..3064001d3ce0 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -42,7 +42,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; -const WEAVE_VERSION = "0.2.4"; +const WEAVE_VERSION = "0.2.5"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 7a8e48138d2b6ef1a3ba9a2241fd9c4de66ab6c2 Mon Sep 17 00:00:00 2001 From: Dan Mosedale Date: Mon, 21 Jul 2008 18:12:27 -0700 Subject: [PATCH 0631/1860] Minor refactoring to allow for multiple application embeddings of Weave. Also adds a Thunderbird embedding of weave and a fix to set the username/password during verifyPassphrase (bug 446444), r=thunder@mozilla.com. --- services/sync/modules/dav.js | 2 +- services/sync/modules/service.js | 35 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 5b6aa1d4994d..85ecff0e3703 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -52,7 +52,7 @@ Function.prototype.async = Async.sugar; Utils.lazy(this, 'DAV', DAVCollection); -let DAVLocks = {}; +let DAVLocks = {default: null}; /* * DAV object diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 498f432a0a0c..5ac1987a5fde 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -98,6 +98,13 @@ Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/syncCores.js", Weave); Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/service.js", Weave); +Cu.import("resource://weave/engines/cookies.js", Weave); +Cu.import("resource://weave/engines/passwords.js", Weave); +Cu.import("resource://weave/engines/bookmarks.js", Weave); +Cu.import("resource://weave/engines/history.js", Weave); +Cu.import("resource://weave/engines/forms.js", Weave); +Cu.import("resource://weave/engines/tabs.js", Weave); + Utils.lazy(Weave, 'Service', WeaveSvc); /* @@ -105,7 +112,7 @@ Utils.lazy(Weave, 'Service', WeaveSvc); * Main entry point into Weave's sync framework */ -function WeaveSvc(engines) { +function WeaveSvc() { this._startupFinished = false; this._initLogs(); this._log.info("Weave Sync Service Initializing"); @@ -119,20 +126,6 @@ function WeaveSvc(engines) { ID.setAlias('WeaveID', 'DAV:default'); ID.setAlias('WeaveCryptoID', 'Engine:PBE:default'); - if (typeof engines == "undefined") - engines = [ - new BookmarksEngine(), - new HistoryEngine(), - new CookieEngine(), - new PasswordEngine(), - new FormEngine(), - new TabEngine() - ]; - - // Register engines - for (let i = 0; i < engines.length; i++) - Engines.register(engines[i]); - // Other misc startup Utils.prefs.addObserver("", this, false); this._os.addObserver(this, "quit-application", true); @@ -516,9 +509,12 @@ WeaveSvc.prototype = { // or extension install. var prefBranch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch); - if(prefBranch.getBoolPref("browser.sessionstore.resume_session_once")) - return; - + // non browser apps may throw + try { + if(prefBranch.getBoolPref("browser.sessionstore.resume_session_once")) + return; + } catch (ex) {} + this.isQuitting = true; let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. @@ -558,6 +554,9 @@ WeaveSvc.prototype = { // it to be. // XXX check it and ... throw? + this.username = username; + ID.get('WeaveID').setTempPassword(password); + let privkeyResp = yield DAV.GET("private/privkey", self.cb); let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); From 836ce63e21af1bfcf50b74dc30b58d8c0926055a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 21 Jul 2008 18:23:51 -0700 Subject: [PATCH 0632/1860] Fix network timeouts, make them take into account progress; re-upload new snapshot after 25 deltas --- services/sync/modules/constants.js | 6 +++- services/sync/modules/dav.js | 41 ++++++++++++++++++---------- services/sync/modules/engines.js | 4 +-- services/sync/modules/remote.js | 44 ++++++++++++++++++++++-------- 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 3064001d3ce0..46505d76a797 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -40,7 +40,8 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', - 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE']; + 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', + 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; const WEAVE_VERSION = "0.2.5"; const STORAGE_FORMAT_VERSION = 3; @@ -62,3 +63,6 @@ const ONE_BYTE = 1; const ONE_KILOBYTE = 1024 * ONE_BYTE; const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; +const CONNECTION_TIMEOUT = 30000; + +const KEEP_DELTAS = 25; \ No newline at end of file diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index 5b6aa1d4994d..485fe9c41c22 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -125,18 +125,27 @@ DAVCollection.prototype = { path = path.slice(1); // if absolute: remove leading slash // path at this point should have no leading slash. + if (this._lastProgress) + throw "Request already in progress"; + else + this._lastProgress = Date.now(); + + let xhrCb = self.cb; let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. createInstance(Ci.nsIXMLHttpRequest); - let xhrCb = self.cb; - request.onload = new Utils.EventListener(xhrCb, "load"); - request.onerror = new Utils.EventListener(xhrCb, "error"); + + // check for stalled connections + let listener = new Utils.EventListener(this._timeoutCb(request, xhrCb)); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(listener, CONNECTION_TIMEOUT, + timer.TYPE_REPEATING_SLACK); + + request.onload = xhrCb; + request.onerror = xhrCb; + request.onprogress =Utils.bind2(this, this._onProgress); request.mozBackgroundRequest = true; request.open(op, this._baseURL + path, true); - // time out requests after 30 seconds - let listener = new Utils.EventListener(this._timeoutCb(request, xhrCb)); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(listener, 30000, timer.TYPE_ONE_SHOT); // Force cache validation let channel = request.channel; @@ -154,19 +163,21 @@ DAVCollection.prototype = { request.setRequestHeader(key, headers[key]); } - request.send(data); - let event = yield; - ret = event.target; + let event = yield request.send(data); - self.done(ret); + timer.cancel(); + this._lastProgress = null; + self.done(event.target); + }, + + _onProgress: function DC__onProgress(event) { + this._lastProgress = Date.now(); }, _timeoutCb: function DC__timeoutCb(request, callback) { return function() { - try { - let test = request.status; - } catch (e) { - this._log.warn("Connection timed out, aborting..."); + if (Date.now() - this._lastProgress > CONNECTION_TIMEOUT) { + this._log.warn("Connection timed out"); request.abort(); callback({target:{status:-1}}); } diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c145622b75a8..1fa198870ecd 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -82,7 +82,7 @@ EngineManagerSvc.prototype = { for (key in this._engines) { if(this._engines[key].enabled) ret.push(this._engines[key]); - } + } return ret; }, register: function EngMgr_register(engine) { @@ -411,7 +411,7 @@ Engine.prototype = { this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-deltas"); - this._remote.appendDelta(self.cb, serverDelta, + this._remote.appendDelta(self.cb, this._snapshot, serverDelta, {maxVersion: this._snapshot.version, deltasEncryption: Crypto.defaultAlgorithm, itemCount: c}); diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index fa168ec9cee9..8d593001ca22 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -160,8 +160,7 @@ Resource.prototype = { if ("PUT" == action) { for each (let filter in this._filters) { - filter.beforePUT.async(filter, self.cb, data); - data = yield; + data = yield filter.beforePUT.async(filter, self.cb, data); } } @@ -209,8 +208,8 @@ Resource.prototype = { listener = new Utils.EventListener(self.cb); timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); } - timer.initWithCallback(listener, iter * 100, timer.TYPE_ONE_SHOT); - yield; + yield timer.initWithCallback(listener, iter * iter * 1000, + timer.TYPE_ONE_SHOT); iter++; } } @@ -218,8 +217,7 @@ Resource.prototype = { if ("GET" == action) { let filters = this._filters.slice(); // reverse() mutates, so we copy for each (let filter in filters.reverse()) { - filter.afterGET.async(filter, self.cb, this._data); - this._data = yield; + this._data = yield filter.afterGET.async(filter, self.cb, this._data); } } @@ -640,7 +638,7 @@ RemoteStore.prototype = { }, // Adds a new set of changes (a delta) to this store - _appendDelta: function RStore__appendDelta(delta, metadata) { + _appendDelta: function RStore__appendDelta(snapshot, delta, metadata) { let self = yield; if (metadata) { @@ -648,13 +646,35 @@ RemoteStore.prototype = { this.status.data[key] = metadata[key]; } - let id = this.status.data.maxVersion; // FIXME: we increment maxVersion in Engine - this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-deltas"); + // FIXME: we should increment maxVersion here instead of in Engine + let id = this.status.data.maxVersion; + + // upload the delta even if we upload a new snapshot, so other clients + // can be spared of a full re-download + this._os.notifyObservers(null, "weave:service:sync:status", + "status.uploading-deltas"); yield this._deltas.put(self.cb, id, delta); - this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-status"); + + // if we have more than KEEP_DELTAS, then upload a new snapshot + // this allows us to remove old deltas + if ((id - this.status.data.snapVersion) > KEEP_DELTAS) { + this._os.notifyObservers(null, "weave:service:sync:status", + "status.uploading-snapshot"); + yield this.snapshot.put(self.cb, snapshot.data); + this.status.data.snapVersion = id; + } + + // XXX we could define another constant here + // (e.g. KEEP_MAX_DELTAS) to define when to actually start + // deleting deltas from the server. However, we can do this more + // efficiently server-side + + // finally, upload a new status file + this._os.notifyObservers(null, "weave:service:sync:status", + "status.uploading-status"); yield this.status.put(self.cb, this.status.data); }, - appendDelta: function RStore_appendDelta(onComplete, delta, metadata) { - this._appendDelta.async(this, onComplete, delta, metadata); + appendDelta: function RStore_appendDelta(onComplete, snapshot, delta, metadata) { + this._appendDelta.async(this, onComplete, snapshot, delta, metadata); } }; From 362a50169cbe171334d44717b5a3b4d71946e4a5 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 21 Jul 2008 20:53:30 -0700 Subject: [PATCH 0633/1860] Fix forgot password link (bug #442956) --- services/sync/locales/en-US/wizard.dtd | 78 +++++++++++++------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 8e2e3a20d2eb..e5ad58575725 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,17 +1,17 @@ - - - - - - + + + + + + - - - - - + + + + + @@ -24,7 +24,7 @@ - + @@ -45,7 +45,7 @@ - + @@ -64,10 +64,10 @@ - + - + @@ -77,27 +77,27 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + From f4acf041a70603c38c7712762f199f48cd6eb981 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 23 Jul 2008 15:46:13 -0700 Subject: [PATCH 0634/1860] only wrap the first 10 entries in a tab --- services/sync/modules/engines/tabs.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index bb1775314197..e715c5c6911e 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -68,7 +68,7 @@ TabEngine.prototype = { this.__defineGetter__("_store", function() store); return this._store; }, - + get _core() { let core = new TabSyncCore(this._store); this.__defineGetter__("_core", function() core); @@ -413,7 +413,6 @@ TabStore.prototype = { // The session history entry for the page currently loaded in the tab. // We use the URL of the current page as the ID for the tab. let currentEntry = tab.entries[tab.index - 1]; - if (!currentEntry || !currentEntry.url) { this._log.warn("_wrapRealTabs: no current entry or no URL, can't " + "identify " + this._json.encode(tab)); @@ -427,8 +426,14 @@ TabStore.prototype = { // nsISHEntry::ID, changes every time session store restores the tab, // so we can't sync them, or we would generate edit commands on every // restart (even though nothing has actually changed). - for each (let entry in tab.entries) - delete entry.ID; + // Also, only sync up to 10 entries, otherwise we can end up with some + // insanely large snapshots. + for (let k = 0; k < tab.entries.length; k++) { + if (k > 10) + delete tab.entries[k]; + else + delete tab.entries[k].ID; + } items[tabID] = { // Identify this item as a tab in case we start serializing windows From 25db0ae36ad224c4c3348734bfa73d77a2d4cac1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 23 Jul 2008 15:46:48 -0700 Subject: [PATCH 0635/1860] when making deep copies of an object, default to alphabetically sorting the properties for adding --- services/sync/modules/util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index e82eb9597609..5487130efb75 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -183,7 +183,7 @@ let Utils = { return true; }, - deepCopy: function Weave_deepCopy(thing, sort) { + deepCopy: function Weave_deepCopy(thing, noSort) { if (typeof(thing) != "object" || thing == null) return thing; let ret; @@ -191,12 +191,12 @@ let Utils = { if ("Array" == thing.constructor.name) { ret = []; for (let i = 0; i < thing.length; i++) - ret.push(Utils.deepCopy(thing[i]), sort); + ret.push(Utils.deepCopy(thing[i]), noSort); } else { ret = {}; let props = [p for (p in thing)]; - if (sort) + if (!noSort) props = props.sort(); props.forEach(function(k) ret[k] = Utils.deepCopy(thing[k], sort)); } From ea149e2bdd235ef9a2064394418fa71bcae90f50 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 24 Jul 2008 19:29:11 -0700 Subject: [PATCH 0636/1860] Default to not doing anything for resetGUIDs. Define an observer for subclasses to use --- services/sync/modules/stores.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 2e10ed3bdc78..1510b2a6d2a2 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -65,6 +65,14 @@ Store.prototype = { // set this property in child object's wrap()! _lookup: null, + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + __json: null, get _json() { if (!this.__json) @@ -126,7 +134,7 @@ Store.prototype = { _resetGUIDs: function Store__resetGUIDs() { let self = yield; - throw "_resetGUIDs needs to be subclassed"; + // default to do nothing }, resetGUIDs: function Store_resetGUIDs(onComplete) { this._resetGUIDs.async(this, onComplete); From 737712ca43f53e0c236f78eead15c8b72bfb14df Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 25 Jul 2008 01:06:23 -0700 Subject: [PATCH 0637/1860] split Engine into SyncEngine and FileEngine --- services/sync/modules/engines.js | 357 ++++++++++++++++----- services/sync/modules/engines/bookmarks.js | 3 +- services/sync/modules/engines/cookies.js | 3 +- services/sync/modules/engines/forms.js | 3 +- services/sync/modules/engines/history.js | 3 +- services/sync/modules/engines/passwords.js | 5 +- services/sync/modules/engines/tabs.js | 2 +- services/sync/modules/remote.js | 11 +- 8 files changed, 283 insertions(+), 104 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 1fa198870ecd..b21acfa0d805 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,8 +35,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Engines', - 'Engine']; +const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine', 'FileEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -112,64 +111,47 @@ Engine.prototype = { // "user-data/default-engine/"; get serverPrefix() { throw "serverPrefix property must be overridden in subclasses"; }, - get snapshot() this._snapshot, - get _remote() { - if (!this.__remote) - this.__remote = new RemoteStore(this); - return this.__remote; + let remote = new RemoteStore(this); + this.__defineGetter__("_remote", function() remote); + return remote; }, get enabled() { return Utils.prefs.getBoolPref("engine." + this.name); }, - __os: null, get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + this.__defineGetter__("_os", function() os); + return os; }, - __json: null, get _json() { - if (!this.__json) - this.__json = Cc["@mozilla.org/dom/json;1"]. - createInstance(Ci.nsIJSON); - return this.__json; + let json = Cc["@mozilla.org/dom/json;1"]. + createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return json; }, // _core, _store and _tracker need to be overridden in subclasses - __store: null, get _store() { - if (!this.__store) - this.__store = new Store(); - return this.__store; + let store = new Store(); + this.__defineGetter__("_store", function() store); + return store; }, - __core: null, get _core() { - if (!this.__core) - this.__core = new SyncCore(this._store); - return this.__core; + let core = new SyncCore(this._store); + this.__defineGetter__("_core", function() core); + return core; }, - __tracker: null, get _tracker() { - if (!this.__tracker) - this.__tracker = new Tracker(); - return this.__tracker; - }, - - __snapshot: null, - get _snapshot() { - if (!this.__snapshot) - this.__snapshot = new SnapshotStore(this.name); - return this.__snapshot; - }, - set _snapshot(value) { - this.__snapshot = value; + let tracker = new tracker(); + this.__defineGetter__("_tracker", function() tracker); + return tracker; }, get engineId() { @@ -217,6 +199,68 @@ Engine.prototype = { this._log.debug("Client reset completed successfully"); }, + _sync: function Engine__sync() { + let self = yield; + throw "_sync needs to be subclassed"; + }, + + _initialUpload: function Engine__initialUpload() { + let self = yield; + this._log.info("Initial upload to server"); + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + yield this._remote.initialize(self.cb, this._snapshot); + this._snapshot.save(); + }, + + _share: function Engine__share(guid, username) { + let self = yield; + /* This should be overridden by the engine subclass for each datatype. + Implementation should share the data node identified by guid, + and all its children, if any, with the user identified by username. */ + self.done(); + }, + + _stopSharing: function Engine__stopSharing(guid, username) { + let self = yield; + /* This should be overridden by the engine subclass for each datatype. + Stop sharing the data node identified by guid with the user identified + by username.*/ + self.done(); + }, + + sync: function Engine_sync(onComplete) { + return this._sync.async(this, onComplete); + }, + + share: function Engine_share(onComplete, guid, username) { + return this._share.async(this, onComplete, guid, username); + }, + + stopSharing: function Engine_share(onComplete, guid, username) { + return this._stopSharing.async(this, onComplete, guid, username); + }, + + resetServer: function Engimne_resetServer(onComplete) { + this._notify("reset-server", this._resetServer).async(this, onComplete); + }, + + resetClient: function Engine_resetClient(onComplete) { + this._notify("reset-client", this._resetClient).async(this, onComplete); + } +}; + +function SyncEngine() {} +SyncEngine.prototype = { + __proto__: new Engine(), + + get snapshot() { + let snap = new SnapshotStore(this.name); + this.__defineGetter__("_snapshot", function() snapshot); + return snap; + } + // original // / \ // A / \ B @@ -245,14 +289,14 @@ Engine.prototype = { // 3.1) Apply local delta with server changes ("D") // 3.2) Append server delta to the delta file and upload ("C") - _sync: function Engine__sync() { + _sync: function SyncEng__sync() { let self = yield; this._log.info("Beginning sync"); this._os.notifyObservers(null, "weave:service:sync:engine:start", this.displayName); try { - yield this._remote.openSession(self.cb); + yield this._remote.openSession(self.cb, this._snapshot); } catch (e if e.status == 404) { yield this._initialUpload.async(this, self.cb); return; @@ -423,51 +467,190 @@ Engine.prototype = { this._log.info("Sync complete"); self.done(true); - }, - - _initialUpload: function Engine__initialUpload() { - let self = yield; - this._log.info("Initial upload to server"); - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - yield this._remote.initialize(self.cb, this._snapshot); - this._snapshot.save(); - }, - - _share: function Engine__share(guid, username) { - let self = yield; - /* This should be overridden by the engine subclass for each datatype. - Implementation should share the data node identified by guid, - and all its children, if any, with the user identified by username. */ - self.done(); - }, - - _stopSharing: function Engine__stopSharing(guid, username) { - let self = yield; - /* This should be overridden by the engine subclass for each datatype. - Stop sharing the data node identified by guid with the user identified - by username.*/ - self.done(); - }, - - sync: function Engine_sync(onComplete) { - return this._sync.async(this, onComplete); - }, - - share: function Engine_share(onComplete, guid, username) { - return this._share.async(this, onComplete, guid, username); - }, - - stopSharing: function Engine_share(onComplete, guid, username) { - return this._stopSharing.async(this, onComplete, guid, username); - }, - - resetServer: function Engimne_resetServer(onComplete) { - this._notify("reset-server", this._resetServer).async(this, onComplete); - }, - - resetClient: function Engine_resetClient(onComplete) { - this._notify("reset-client", this._resetClient).async(this, onComplete); } }; + +function FileEngine() {} +FileEngine.prototype = { + __proto__: new Engine(), + + _sync: function FileEng__sync() { + let self = yield; + + this._log.info("Beginning sync"); + this._os.notifyObservers(null, "weave:service:sync:engine:start", this.displayName); + + try { + yield this._remote.openSession(self.cb, this._snapshot); + } catch (e if e.status == 404) { + yield this._initialUpload.async(this, self.cb); + return; + } + + if (this._remote.status.data.GUID != this._snapshot.GUID) { + this._log.debug("Remote/local sync GUIDs do not match. " + + "Forcing initial sync."); + this._log.trace("Remote: " + this._remote.status.data.GUID); + this._log.trace("Local: " + this._snapshot.GUID); + yield this._store.resetGUIDs(self.cb); + this._snapshot.data = {}; + this._snapshot.version = -1; + this._snapshot.GUID = this._remote.status.data.GUID; + } + + this._log.info("Local snapshot version: " + this._snapshot.version); + this._log.info("Server maxVersion: " + this._remote.status.data.maxVersion); + + if ("none" != Utils.prefs.getCharPref("encryption")) + yield this._remote.keys.getKeyAndIV(self.cb, this.engineId); + + // 1) Fetch server deltas + + this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); + let server = {}; + let serverSnap = yield this._remote.wrap(self.cb, this._snapshot); + server.snapshot = serverSnap.data; + this._core.detectUpdates(self.cb, this._snapshot.data, server.snapshot); + server.updates = yield; + + // 2) Generate local deltas from snapshot -> current client status + + this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); + let localJson = new SnapshotStore(); + localJson.data = this._store.wrap(); + this._core.detectUpdates(self.cb, this._snapshot.data, localJson.data); + let localUpdates = yield; + + this._log.trace("local json:\n" + localJson.serialize()); + this._log.trace("Local updates: " + this._serializeCommands(localUpdates)); + this._log.trace("Server updates: " + this._serializeCommands(server.updates)); + + if (server.updates.length == 0 && localUpdates.length == 0) { + this._snapshot.version = this._remote.status.data.maxVersion; + this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); + this._log.info("Sync complete: no changes needed on client or server"); + self.done(true); + return; + } + + // 3) Reconcile client/server deltas and generate new deltas for them. + + this._os.notifyObservers(null, "weave:service:sync:status", "status.reconciling-updates"); + this._log.info("Reconciling client/server updates"); + this._core.reconcile(self.cb, localUpdates, server.updates); + let ret = yield; + + let clientChanges = ret.propagations[0]; + let serverChanges = ret.propagations[1]; + let clientConflicts = ret.conflicts[0]; + let serverConflicts = ret.conflicts[1]; + + this._log.info("Changes for client: " + clientChanges.length); + this._log.info("Predicted changes for server: " + serverChanges.length); + this._log.info("Client conflicts: " + clientConflicts.length); + this._log.info("Server conflicts: " + serverConflicts.length); + this._log.trace("Changes for client: " + this._serializeCommands(clientChanges)); + this._log.trace("Predicted changes for server: " + this._serializeCommands(serverChanges)); + this._log.trace("Client conflicts: " + this._serializeConflicts(clientConflicts)); + this._log.trace("Server conflicts: " + this._serializeConflicts(serverConflicts)); + + if (!(clientChanges.length || serverChanges.length || + clientConflicts.length || serverConflicts.length)) { + this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); + this._log.info("Sync complete: no changes needed on client or server"); + this._snapshot.data = localJson.data; + this._snapshot.version = this._remote.status.data.maxVersion; + this._snapshot.save(); + self.done(true); + return; + } + + if (clientConflicts.length || serverConflicts.length) { + this._log.warn("Conflicts found! Discarding server changes"); + } + + let savedSnap = Utils.deepCopy(this._snapshot.data); + let savedVersion = this._snapshot.version; + let newSnapshot; + + // 3.1) Apply server changes to local store + + if (clientChanges.length) { + this._log.info("Applying changes locally"); + this._os.notifyObservers(null, "weave:service:sync:status", "status.applying-changes"); + + // Note that we need to need to apply client changes to the + // current tree, not the saved snapshot + + localJson.applyCommands.async(localJson, self.cb, clientChanges); + yield; + this._snapshot.data = localJson.data; + this._snapshot.version = this._remote.status.data.maxVersion; + this._store.applyCommands.async(this._store, self.cb, clientChanges); + yield; + newSnapshot = this._store.wrap(); + + this._core.detectUpdates(self.cb, this._snapshot.data, newSnapshot); + let diff = yield; + if (diff.length != 0) { + this._log.warn("Commands did not apply correctly"); + this._log.trace("Diff from snapshot+commands -> " + + "new snapshot after commands:\n" + + this._serializeCommands(diff)); + // FIXME: do we really want to revert the snapshot here? + this._snapshot.data = Utils.deepCopy(savedSnap); + this._snapshot.version = savedVersion; + } + this._snapshot.save(); + } + + // 3.2) Append server delta to the delta file and upload + + // Generate a new diff, from the current server snapshot to the + // current client snapshot. In the case where there are no + // conflicts, it should be the same as what the resolver returned + + this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); + newSnapshot = this._store.wrap(); + this._core.detectUpdates(self.cb, server.snapshot, newSnapshot); + let serverDelta = yield; + + // Log an error if not the same + if (!(serverConflicts.length || + Utils.deepEquals(serverChanges, serverDelta))) + this._log.warn("Predicted server changes differ from " + + "actual server->client diff (can be ignored in many cases)"); + + this._log.info("Actual changes for server: " + serverDelta.length); + this._log.trace("Actual changes for server: " + + this._serializeCommands(serverDelta)); + + if (serverDelta.length) { + this._log.info("Uploading changes to server"); + this._snapshot.data = newSnapshot; + this._snapshot.version = ++this._remote.status.data.maxVersion; + + // XXX don't append delta if we do a full upload? + if (this._remote.status.data.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) + yield this._remote.initialize(self.cb, this._snapshot); + + let c = 0; + for (GUID in this._snapshot.data) + c++; + + this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-deltas"); + + this._remote.appendDelta(self.cb, this._snapshot, serverDelta, + {maxVersion: this._snapshot.version, + deltasEncryption: Crypto.defaultAlgorithm, + itemCount: c}); + yield; + + this._log.info("Successfully updated deltas and status on server"); + this._snapshot.save(); + } + + this._log.info("Sync complete"); + self.done(true); + } +}; \ No newline at end of file diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 775e26dbb7fb..623de6d9d0a8 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -686,6 +686,8 @@ function BookmarksEngine(pbeId) { this._init(pbeId); } BookmarksEngine.prototype = { + __proto__: new SyncEngine(), + get name() { return "bookmarks"; }, get displayName() { return "Bookmarks"; }, get logName() { return "BmkEngine"; }, @@ -746,7 +748,6 @@ BookmarksEngine.prototype = { self.done(); } }; -BookmarksEngine.prototype.__proto__ = new Engine(); function BookmarksSyncCore(store) { this._store = store; diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 0c5baab9df86..7f6d73fa9b5d 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -53,6 +53,8 @@ function CookieEngine(pbeId) { this._init(pbeId); } CookieEngine.prototype = { + __proto__: new SyncEngine(), + get name() { return "cookies"; }, get displayName() { return "Cookies"; }, get logName() { return "CookieEngine"; }, @@ -79,7 +81,6 @@ CookieEngine.prototype = { return this.__tracker; } }; -CookieEngine.prototype.__proto__ = new Engine(); function CookieSyncCore(store) { this._store = store; diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 54ccc2693a4b..903bf504e9ab 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -54,6 +54,8 @@ function FormEngine(pbeId) { this._init(pbeId); } FormEngine.prototype = { + __proto__: new SyncEngine(), + get name() { return "forms"; }, get displayName() { return "Saved Form Data"; }, get logName() { return "FormEngine"; }, @@ -80,7 +82,6 @@ FormEngine.prototype = { return this.__tracker; } }; -FormEngine.prototype.__proto__ = new Engine(); function FormSyncCore(store) { this._store = store; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index ce4f3bf41677..b611192f4fd0 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -54,6 +54,8 @@ function HistoryEngine(pbeId) { this._init(pbeId); } HistoryEngine.prototype = { + __proto__: new SyncEngine(), + get name() { return "history"; }, get displayName() { return "Browsing History"; }, get logName() { return "HistEngine"; }, @@ -80,7 +82,6 @@ HistoryEngine.prototype = { return this.__tracker; } }; -HistoryEngine.prototype.__proto__ = new Engine(); function HistorySyncCore(store) { this._store = store; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 389aeecee656..61b471dc3bfa 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -51,6 +51,8 @@ function PasswordEngine() { this._init(); } PasswordEngine.prototype = { + __proto__: new SyncEngine(), + get name() { return "passwords"; }, get displayName() { return "Saved Passwords"; }, get logName() { return "PasswordEngine"; }, @@ -70,7 +72,6 @@ PasswordEngine.prototype = { return this.__core; } }; -PasswordEngine.prototype.__proto__ = new Engine(); function PasswordSyncCore(store) { this._store = store; @@ -234,7 +235,7 @@ PasswordTracker.prototype = { return 100; else return score; - }, + }, resetScore: function PasswordTracker_resetScore() { this._loginCount = this._loginManager.countLogins("", "", ""); diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index e715c5c6911e..18376aa02702 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -55,7 +55,7 @@ function TabEngine(pbeId) { } TabEngine.prototype = { - __proto__: new Engine(), + __proto__: new SyncEngine(), get name() "tabs", get displayName() { return "Tabs"; }, diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 8d593001ca22..7bd6d733e71c 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -411,14 +411,6 @@ RemoteStore.prototype = { get serverPrefix() this._engine.serverPrefix, get engineId() this._engine.engineId, - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - get status() { let status = new Resource(this.serverPrefix + "status.json"); status.pushFilter(new JsonFilter()); @@ -466,8 +458,7 @@ RemoteStore.prototype = { this._log.debug("Downloading status file"); this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-status"); - this.status.get(self.cb); - yield; + yield this.status.get(self.cb); this._log.debug("Downloading status file... done"); // Bail out if the server has a newer format version than we can parse From 7bc53e7eae9e85f5d1feb0e3eaf85d4e70a05e1b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 25 Jul 2008 16:54:37 -0700 Subject: [PATCH 0638/1860] print exceptions that come from engines --- services/sync/modules/service.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5ac1987a5fde..4ca01ec4a36c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -514,7 +514,7 @@ WeaveSvc.prototype = { if(prefBranch.getBoolPref("browser.sessionstore.resume_session_once")) return; } catch (ex) {} - + this.isQuitting = true; let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. @@ -556,7 +556,7 @@ WeaveSvc.prototype = { this.username = username; ID.get('WeaveID').setTempPassword(password); - + let privkeyResp = yield DAV.GET("private/privkey", self.cb); let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); @@ -843,6 +843,8 @@ WeaveSvc.prototype = { yield engine.sync(self.cb); engine._tracker.resetScore(); } catch(e) { + // FIXME: FT module is not printing out exceptions - it should be + this._log.warn("Engine exception: " + e); let ok = FaultTolerance.Service.onException(e); if (!ok) this._syncError = true; From 19aea65dc4a62020b53b798741d5bb434a581ea8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 25 Jul 2008 17:02:43 -0700 Subject: [PATCH 0639/1860] Implement FileEngine --- services/sync/modules/engines.js | 248 +++++++------------------- services/sync/modules/engines/tabs.js | 17 +- 2 files changed, 69 insertions(+), 196 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b21acfa0d805..e06e388344f4 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -167,11 +167,14 @@ Engine.prototype = { }, _init: function Engine__init() { + let levelPref = "log.logger.service.engine." + this.name; + let level = "Debug"; + try { level = Utils.prefs.getCharPref(levelPref); } + catch (e) { /* ignore unset prefs */ } + this._log = Log4Moz.Service.getLogger("Service." + this.logName); - this._log.level = - Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.engine")]; + this._log.level = Log4Moz.Level[level]; this._osPrefix = "weave:" + this.name + ":"; - this._snapshot.load(); }, _serializeCommands: function Engine__serializeCommands(commands) { @@ -194,7 +197,6 @@ Engine.prototype = { _resetClient: function Engine__resetClient() { let self = yield; this._log.debug("Resetting client state"); - this._snapshot.wipe(); this._store.wipe(); this._log.debug("Client reset completed successfully"); }, @@ -206,12 +208,7 @@ Engine.prototype = { _initialUpload: function Engine__initialUpload() { let self = yield; - this._log.info("Initial upload to server"); - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - yield this._remote.initialize(self.cb, this._snapshot); - this._snapshot.save(); + throw "_initialUpload needs to be subclassed"; }, _share: function Engine__share(guid, username) { @@ -259,7 +256,25 @@ SyncEngine.prototype = { let snap = new SnapshotStore(this.name); this.__defineGetter__("_snapshot", function() snapshot); return snap; - } + }, + + _resetClient: function SyncEngine__resetClient() { + let self = yield; + this._log.debug("Resetting client state"); + this._snapshot.wipe(); + this._store.wipe(); + this._log.debug("Client reset completed successfully"); + }, + + _initialUpload: function Engine__initialUpload() { + let self = yield; + this._log.info("Initial upload to server"); + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + yield this._remote.initialize(self.cb, this._snapshot); + this._snapshot.save(); + }, // original // / \ @@ -289,12 +304,14 @@ SyncEngine.prototype = { // 3.1) Apply local delta with server changes ("D") // 3.2) Append server delta to the delta file and upload ("C") - _sync: function SyncEng__sync() { + _sync: function SyncEngine__sync() { let self = yield; this._log.info("Beginning sync"); this._os.notifyObservers(null, "weave:service:sync:engine:start", this.displayName); + this._snapshot.load(); + try { yield this._remote.openSession(self.cb, this._snapshot); } catch (e if e.status == 404) { @@ -470,185 +487,54 @@ SyncEngine.prototype = { } }; -function FileEngine() {} +function FileEngine() { + // subclasses should call _init + // we don't call it here because it requires serverPrefix to be set +} FileEngine.prototype = { __proto__: new Engine(), - _sync: function FileEng__sync() { + get _profileID() { + return "cheese"; // FIXME! + }, + + _init: function FileEngine__init() { + // FIXME meep? + this.__proto__.__proto__.__proto__.__proto__._init.call(this); + this._file = new Resource(this.serverPrefix + "data"); + this._file.pushFilter(new JsonFilter()); + //this._file.pushFilter(new CryptoFilter(this, "dataEncryption")); + }, + + // NOTE: Assumes this._file has latest server data + // this method is separate from _sync so it's easy to override in subclasses + _merge: function FileEngine__merge() { + let self = yield; + this._file.data[this._profileID] = this._store.wrap(); + }, + + // This engine is very simple: + // 1) Get the latest server data + // 2) Merge with our local data store + // 3) Upload new merged data + // NOTE: a version file will be needed in the future to optimize the case + // where there are no changes + _sync: function FileEngine__sync() { let self = yield; this._log.info("Beginning sync"); - this._os.notifyObservers(null, "weave:service:sync:engine:start", this.displayName); + + if (!(yield DAV.MKCOL(this.serverPrefix, self.cb))) + throw "Could not create remote folder"; try { - yield this._remote.openSession(self.cb, this._snapshot); + yield this._file.get(self.cb); } catch (e if e.status == 404) { - yield this._initialUpload.async(this, self.cb); - return; - } - - if (this._remote.status.data.GUID != this._snapshot.GUID) { - this._log.debug("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._log.trace("Remote: " + this._remote.status.data.GUID); - this._log.trace("Local: " + this._snapshot.GUID); - yield this._store.resetGUIDs(self.cb); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.GUID = this._remote.status.data.GUID; - } - - this._log.info("Local snapshot version: " + this._snapshot.version); - this._log.info("Server maxVersion: " + this._remote.status.data.maxVersion); - - if ("none" != Utils.prefs.getCharPref("encryption")) - yield this._remote.keys.getKeyAndIV(self.cb, this.engineId); - - // 1) Fetch server deltas - - this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); - let server = {}; - let serverSnap = yield this._remote.wrap(self.cb, this._snapshot); - server.snapshot = serverSnap.data; - this._core.detectUpdates(self.cb, this._snapshot.data, server.snapshot); - server.updates = yield; - - // 2) Generate local deltas from snapshot -> current client status - - this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); - let localJson = new SnapshotStore(); - localJson.data = this._store.wrap(); - this._core.detectUpdates(self.cb, this._snapshot.data, localJson.data); - let localUpdates = yield; - - this._log.trace("local json:\n" + localJson.serialize()); - this._log.trace("Local updates: " + this._serializeCommands(localUpdates)); - this._log.trace("Server updates: " + this._serializeCommands(server.updates)); - - if (server.updates.length == 0 && localUpdates.length == 0) { - this._snapshot.version = this._remote.status.data.maxVersion; - this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); - this._log.info("Sync complete: no changes needed on client or server"); - self.done(true); - return; - } - - // 3) Reconcile client/server deltas and generate new deltas for them. - - this._os.notifyObservers(null, "weave:service:sync:status", "status.reconciling-updates"); - this._log.info("Reconciling client/server updates"); - this._core.reconcile(self.cb, localUpdates, server.updates); - let ret = yield; - - let clientChanges = ret.propagations[0]; - let serverChanges = ret.propagations[1]; - let clientConflicts = ret.conflicts[0]; - let serverConflicts = ret.conflicts[1]; - - this._log.info("Changes for client: " + clientChanges.length); - this._log.info("Predicted changes for server: " + serverChanges.length); - this._log.info("Client conflicts: " + clientConflicts.length); - this._log.info("Server conflicts: " + serverConflicts.length); - this._log.trace("Changes for client: " + this._serializeCommands(clientChanges)); - this._log.trace("Predicted changes for server: " + this._serializeCommands(serverChanges)); - this._log.trace("Client conflicts: " + this._serializeConflicts(clientConflicts)); - this._log.trace("Server conflicts: " + this._serializeConflicts(serverConflicts)); - - if (!(clientChanges.length || serverChanges.length || - clientConflicts.length || serverConflicts.length)) { - this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); - this._log.info("Sync complete: no changes needed on client or server"); - this._snapshot.data = localJson.data; - this._snapshot.version = this._remote.status.data.maxVersion; - this._snapshot.save(); - self.done(true); - return; - } - - if (clientConflicts.length || serverConflicts.length) { - this._log.warn("Conflicts found! Discarding server changes"); - } - - let savedSnap = Utils.deepCopy(this._snapshot.data); - let savedVersion = this._snapshot.version; - let newSnapshot; - - // 3.1) Apply server changes to local store - - if (clientChanges.length) { - this._log.info("Applying changes locally"); - this._os.notifyObservers(null, "weave:service:sync:status", "status.applying-changes"); - - // Note that we need to need to apply client changes to the - // current tree, not the saved snapshot - - localJson.applyCommands.async(localJson, self.cb, clientChanges); - yield; - this._snapshot.data = localJson.data; - this._snapshot.version = this._remote.status.data.maxVersion; - this._store.applyCommands.async(this._store, self.cb, clientChanges); - yield; - newSnapshot = this._store.wrap(); - - this._core.detectUpdates(self.cb, this._snapshot.data, newSnapshot); - let diff = yield; - if (diff.length != 0) { - this._log.warn("Commands did not apply correctly"); - this._log.trace("Diff from snapshot+commands -> " + - "new snapshot after commands:\n" + - this._serializeCommands(diff)); - // FIXME: do we really want to revert the snapshot here? - this._snapshot.data = Utils.deepCopy(savedSnap); - this._snapshot.version = savedVersion; - } - this._snapshot.save(); - } - - // 3.2) Append server delta to the delta file and upload - - // Generate a new diff, from the current server snapshot to the - // current client snapshot. In the case where there are no - // conflicts, it should be the same as what the resolver returned - - this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); - newSnapshot = this._store.wrap(); - this._core.detectUpdates(self.cb, server.snapshot, newSnapshot); - let serverDelta = yield; - - // Log an error if not the same - if (!(serverConflicts.length || - Utils.deepEquals(serverChanges, serverDelta))) - this._log.warn("Predicted server changes differ from " + - "actual server->client diff (can be ignored in many cases)"); - - this._log.info("Actual changes for server: " + serverDelta.length); - this._log.trace("Actual changes for server: " + - this._serializeCommands(serverDelta)); - - if (serverDelta.length) { - this._log.info("Uploading changes to server"); - this._snapshot.data = newSnapshot; - this._snapshot.version = ++this._remote.status.data.maxVersion; - - // XXX don't append delta if we do a full upload? - if (this._remote.status.data.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) - yield this._remote.initialize(self.cb, this._snapshot); - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-deltas"); - - this._remote.appendDelta(self.cb, this._snapshot, serverDelta, - {maxVersion: this._snapshot.version, - deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c}); - yield; - - this._log.info("Successfully updated deltas and status on server"); - this._snapshot.save(); + this._log.info("Initial upload to server"); + this._file.data = {}; } + yield this._merge.async(this, self.cb); + yield this._file.put(self.cb, this._file.data); this._log.info("Sync complete"); self.done(true); diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 18376aa02702..7a60f80e2e53 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -55,7 +55,7 @@ function TabEngine(pbeId) { } TabEngine.prototype = { - __proto__: new SyncEngine(), + __proto__: new FileEngine(), get name() "tabs", get displayName() { return "Tabs"; }, @@ -93,13 +93,6 @@ TabSyncCore.prototype = { _logName: "TabSync", _store: null, - get _sessionStore() { - let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. - getService(Ci.nsISessionStore); - this.__defineGetter__("_sessionStore", function() sessionStore); - return this._sessionStore; - }, - _commandLike: function TSC_commandLike(a, b) { // Not implemented. return false; @@ -129,13 +122,6 @@ TabStore.prototype = { return this._windowMediator; }, - get _os() { - let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - this.__defineGetter__("_os", function() os); - return this._os; - }, - get _dirSvc() { let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); @@ -344,6 +330,7 @@ TabStore.prototype = { * running at the same time. */ wrap: function TabStore_wrap() { + return this._wrapRealTabs(); let items; let virtualTabs = this._wrapVirtualTabs(); From 629e408d7a01c72c9a91858d2fd67e9f465dd505 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 11:03:06 -0700 Subject: [PATCH 0640/1860] initial try to get crypto working with FileEngine --- services/sync/modules/engines.js | 20 +++++++++++++---- services/sync/modules/remote.js | 38 ++++++++++++++------------------ 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e06e388344f4..b871370a8cca 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -501,9 +501,18 @@ FileEngine.prototype = { _init: function FileEngine__init() { // FIXME meep? this.__proto__.__proto__.__proto__.__proto__._init.call(this); + this._keys = new Keychain(this.serverPrefix); this._file = new Resource(this.serverPrefix + "data"); this._file.pushFilter(new JsonFilter()); - //this._file.pushFilter(new CryptoFilter(this, "dataEncryption")); + this._file.pushFilter(new CryptoFilter(this.engineId)); + }, + + _initialUpload: function FileEngine__initialUpload() { + let self = yield; + this._file.data = {}; + yield this._merge.async(this, self.cb); + yield this._file.put(self.cb, this._file.data); + // put keychain }, // NOTE: Assumes this._file has latest server data @@ -527,14 +536,17 @@ FileEngine.prototype = { if (!(yield DAV.MKCOL(this.serverPrefix, self.cb))) throw "Could not create remote folder"; + if ("none" != Utils.prefs.getCharPref("encryption")) + yield this._keys.getKeyAndIV(self.cb, this.engineId); + try { yield this._file.get(self.cb); + yield this._merge.async(this, self.cb); + yield this._file.put(self.cb, this._file.data); } catch (e if e.status == 404) { + this._initialUpload.async(this, self.cb); this._log.info("Initial upload to server"); - this._file.data = {}; } - yield this._merge.async(this, self.cb); - yield this._file.put(self.cb, this._file.data); this._log.info("Sync complete"); self.done(true); diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 7bd6d733e71c..37532e0a1806 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -34,7 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Resource', 'RemoteStore', 'JsonFilter']; +const EXPORTED_SYMBOLS = ['Resource', 'JsonFilter', 'CryptoFilter', + 'Keychain', 'RemoteStore']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -331,27 +332,25 @@ JsonFilter.prototype = { } }; -function CryptoFilter(remoteStore, algProp) { - this._remote = remoteStore; - this._algProp = algProp; // hackish, but we don't know if it's a delta or snap +function CryptoFilter(identity) { + this._identity = identity; this._log = Log4Moz.Service.getLogger("Service.CryptoFilter"); } CryptoFilter.prototype = { __proto__: new ResourceFilter(), - __os: null, get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + this.__defineGetter__("_os", function() os); + return os; }, beforePUT: function CryptoFilter_beforePUT(data) { let self = yield; this._log.debug("Encrypting data"); this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting"); - Crypto.encryptData.async(Crypto, self.cb, data, this._remote.engineId); + Crypto.encryptData.async(Crypto, self.cb, data, this._identity); let ret = yield; self.done(ret); }, @@ -360,22 +359,19 @@ CryptoFilter.prototype = { let self = yield; this._log.debug("Decrypting data"); this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting"); - if (!this._remote.status.data) - throw "Remote status must be initialized before crypto filter can be used" - Crypto.decryptData.async(Crypto, self.cb, data, this._remote.engineId); + Crypto.decryptData.async(Crypto, self.cb, data, this.identity); let ret = yield; self.done(ret); } }; -function Keychain(remoteStore) { - this._init(remoteStore); +function Keychain(prefix) { + this._init(prefix); } Keychain.prototype = { __proto__: new Resource(), - _init: function Keychain__init(remoteStore) { - this._remote = remoteStore; - this.__proto__.__proto__._init.call(this, this._remote.serverPrefix + "keys.json"); + _init: function Keychain__init(prefix) { + this.__proto__.__proto__._init.call(this, prefix + "keys.json"); this.pushFilter(new JsonFilter()); }, _getKeyAndIV: function Keychain__getKeyAndIV(identity) { @@ -419,7 +415,7 @@ RemoteStore.prototype = { }, get keys() { - let keys = new Keychain(this); + let keys = new Keychain(this.serverPrefix); this.__defineGetter__("keys", function() keys); return keys; }, @@ -427,7 +423,7 @@ RemoteStore.prototype = { get _snapshot() { let snapshot = new Resource(this.serverPrefix + "snapshot.json"); snapshot.pushFilter(new JsonFilter()); - snapshot.pushFilter(new CryptoFilter(this, "snapshotEncryption")); + snapshot.pushFilter(new CryptoFilter(this._engine.engineId)); this.__defineGetter__("_snapshot", function() snapshot); return snapshot; }, @@ -435,7 +431,7 @@ RemoteStore.prototype = { get _deltas() { let deltas = new ResourceSet(this.serverPrefix + "deltas/"); deltas.pushFilter(new JsonFilter()); - deltas.pushFilter(new CryptoFilter(this, "deltasEncryption")); + deltas.pushFilter(new CryptoFilter(this._engine.engineId)); this.__defineGetter__("_deltas", function() deltas); return deltas; }, From 2a1f10f47b59e56704805caf000a5a88ff2d6e6a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 12:04:41 -0700 Subject: [PATCH 0641/1860] get crypto working with FileEngine --- services/sync/modules/engines.js | 10 +++--- services/sync/modules/remote.js | 57 +++++++++++++++++--------------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b871370a8cca..e6f32da2857f 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -509,10 +509,10 @@ FileEngine.prototype = { _initialUpload: function FileEngine__initialUpload() { let self = yield; + yield this._keys.initialize(self.cb, this.engineId); this._file.data = {}; yield this._merge.async(this, self.cb); yield this._file.put(self.cb, this._file.data); - // put keychain }, // NOTE: Assumes this._file has latest server data @@ -536,16 +536,16 @@ FileEngine.prototype = { if (!(yield DAV.MKCOL(this.serverPrefix, self.cb))) throw "Could not create remote folder"; - if ("none" != Utils.prefs.getCharPref("encryption")) - yield this._keys.getKeyAndIV(self.cb, this.engineId); - try { + if ("none" != Utils.prefs.getCharPref("encryption")) + yield this._keys.getKeyAndIV(self.cb, this.engineId); yield this._file.get(self.cb); yield this._merge.async(this, self.cb); yield this._file.put(self.cb, this._file.data); + } catch (e if e.status == 404) { - this._initialUpload.async(this, self.cb); this._log.info("Initial upload to server"); + yield this._initialUpload.async(this, self.cb); } this._log.info("Sync complete"); diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 37532e0a1806..b720930450a0 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -350,8 +350,7 @@ CryptoFilter.prototype = { let self = yield; this._log.debug("Encrypting data"); this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting"); - Crypto.encryptData.async(Crypto, self.cb, data, this._identity); - let ret = yield; + let ret = yield Crypto.encryptData.async(Crypto, self.cb, data, this._identity); self.done(ret); }, @@ -359,8 +358,7 @@ CryptoFilter.prototype = { let self = yield; this._log.debug("Decrypting data"); this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting"); - Crypto.decryptData.async(Crypto, self.cb, data, this.identity); - let ret = yield; + let ret = yield Crypto.decryptData.async(Crypto, self.cb, data, this._identity); self.done(ret); } }; @@ -374,12 +372,37 @@ Keychain.prototype = { this.__proto__.__proto__._init.call(this, prefix + "keys.json"); this.pushFilter(new JsonFilter()); }, + _initialize: function Keychain__initialize(identity) { + let self = yield; + let wrappedSymkey; + + if ("none" != Utils.prefs.getCharPref("encryption")) { + this._os.notifyObservers(null, "weave:service:sync:status", "status.generating-random-key"); + + yield Crypto.randomKeyGen.async(Crypto, self.cb, identity); + + // Wrap (encrypt) this key with the user's public key. + let idRSA = ID.get('WeaveCryptoID'); + this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting-key"); + wrappedSymkey = yield Crypto.wrapKey.async(Crypto, self.cb, + identity.bulkKey, idRSA); + } + + let keys = {ring: {}, bulkIV: identity.bulkIV}; + this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-key"); + keys.ring[identity.username] = wrappedSymkey; + yield this.put(self.cb, keys); + }, + initialize: function Keychain_initialize(onComplete, identity) { + this._initialize.async(this, onComplete, identity); + }, _getKeyAndIV: function Keychain__getKeyAndIV(identity) { let self = yield; this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-keyring"); - this.get(self.cb); - yield; + + yield this.get(self.cb); + if (!this.data || !this.data.ring || !this.data.ring[identity.username]) throw "Keyring does not contain a key for this user"; @@ -415,7 +438,7 @@ RemoteStore.prototype = { }, get keys() { - let keys = new Keychain(this.serverPrefix); + let keys = new Keychain(this.serverPrefix, this.engineId); this.__defineGetter__("keys", function() keys); return keys; }, @@ -481,26 +504,8 @@ RemoteStore.prototype = { // FIXME: add 'metadata' arg here like appendDelta's _initialize: function RStore__initialize(snapshot) { let self = yield; - let wrappedSymkey; - - if ("none" != Utils.prefs.getCharPref("encryption")) { - this._os.notifyObservers(null, "weave:service:sync:status", "status.generating-random-key"); - - Crypto.randomKeyGen.async(Crypto, self.cb, this.engineId); - yield; - - // Wrap (encrypt) this key with the user's public key. - let idRSA = ID.get('WeaveCryptoID'); - this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting-key"); - wrappedSymkey = yield Crypto.wrapKey.async(Crypto, self.cb, - this.engineId.bulkKey, idRSA); - } - - let keys = {ring: {}, bulkIV: this.engineId.bulkIV}; - this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-key"); - keys.ring[this.engineId.username] = wrappedSymkey; - yield this.keys.put(self.cb, keys); + yield this.keys.initialize(self.cb, this.engineId); this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-snapshot"); yield this._snapshot.put(self.cb, snapshot.data); //yield this._deltas.put(self.cb, []); From cec76f0d7c070f7a637c143bcf3b690f31e0be69 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 15:37:59 -0700 Subject: [PATCH 0642/1860] sync the *latest* 10 back-button entries per tab (not the earliest ones) --- services/sync/modules/engines/tabs.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 7a60f80e2e53..9825087aecf9 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -409,16 +409,15 @@ TabStore.prototype = { let tabID = currentEntry.url; this._log.trace("_wrapRealTabs: tab " + tabID); + // Only sync up to 10 back-button entries, otherwise we can end up with + // some insanely large snapshots. + tab.entries = tab.entries.slice(tab.entries.length - 10); + // The ID property of each entry in the tab, which I think contains // nsISHEntry::ID, changes every time session store restores the tab, // so we can't sync them, or we would generate edit commands on every // restart (even though nothing has actually changed). - // Also, only sync up to 10 entries, otherwise we can end up with some - // insanely large snapshots. for (let k = 0; k < tab.entries.length; k++) { - if (k > 10) - delete tab.entries[k]; - else delete tab.entries[k].ID; } From 4c3b29381fa40a215b5d251c68dee327e6600266 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 15:39:09 -0700 Subject: [PATCH 0643/1860] Keychain's constructor doesn't need an identity anymore --- services/sync/modules/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index b720930450a0..7627902896c0 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -438,7 +438,7 @@ RemoteStore.prototype = { }, get keys() { - let keys = new Keychain(this.serverPrefix, this.engineId); + let keys = new Keychain(this.serverPrefix); this.__defineGetter__("keys", function() keys); return keys; }, From 870045119ff99fceb7f6705e4664f01f77913657 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 15:44:36 -0700 Subject: [PATCH 0644/1860] initial client data module from chris+me --- services/sync/modules/clientData.js | 103 ++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 services/sync/modules/clientData.js diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js new file mode 100644 index 000000000000..a429f2086f02 --- /dev/null +++ b/services/sync/modules/clientData.js @@ -0,0 +1,103 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Beard + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['ClientData']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/dav.js"); +Cu.import("resource://weave/remote.js"); +Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; + +Utils.lazy(this, 'ClientData', ClientDataSvc); + +function ClientDataSvc() { + this._log = Log4Moz.Service.getLogger("Service.ClientData"); +} +ClientDataSvc.prototype = { + getClientData: function ClientData_getClientData() { + let self = yield; + + DAV.MKCOL("meta", self.cb); + let ret = yield; + if(!ret) + throw "Could not create meta information directory"; + + DAV.GET("meta/clients", self.cb); + let ret = yield; + + if (Utils.checkStatus(ret.status)) { + this._log.debug("Could not get clients file from server"); + self.done(false); + return; + } + + this._ClientData = this._json.decode(ret.responseText); + + this._log.debug("Successfully downloaded clients file from server"); + self.done(true); + }, + + uploadClientData: function ClientData_uploadClientData() { + let self = yield; + let json = this._json.encode(this._ClientData); + + DAV.MKCOL("meta", self.cb); + let ret = yield; + if(!ret) + throw "Could not create meta information directory"; + + DAV.PUT("meta/clients", json, self.cb); + let ret = yield; + + if(Utils.checkStatus(ret.status)) { + this._log.debug("Could not upload clients file from server"); + self.done(false); + return; + } + + this._log.debug("Successfully uploaded clients file to server"); + self.done(true); + } +}; From d67d98c24265a07f7da4c9e1b1ecae451429798e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 17:33:53 -0700 Subject: [PATCH 0645/1860] allow Resource.put to have no data arg (in which case it will use its internal _data property) --- services/sync/modules/remote.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 7627902896c0..d564a439306e 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -140,7 +140,7 @@ Resource.prototype = { // it. Otherwise do nothing (don't try to get it every time) if (this.dirty) { - this.put(self.cb, this.data); + this.put(self.cb); ret = yield; } else if (!this.downloaded) { @@ -230,6 +230,8 @@ Resource.prototype = { }, put: function Res_put(onComplete, data) { + if ("undefined" == typeof(data)) + data = this._data; this._request.async(this, onComplete, "PUT", data); }, @@ -664,7 +666,7 @@ RemoteStore.prototype = { // finally, upload a new status file this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-status"); - yield this.status.put(self.cb, this.status.data); + yield this.status.put(self.cb); }, appendDelta: function RStore_appendDelta(onComplete, snapshot, delta, metadata) { this._appendDelta.async(this, onComplete, snapshot, delta, metadata); From f9e72470553a892c3085c2e5629620c215415ff0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 17:34:21 -0700 Subject: [PATCH 0646/1860] mostly finish up ClientData impl --- services/sync/modules/clientData.js | 96 ++++++++++++++++++----------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index a429f2086f02..ee8c1b1126ff 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -53,51 +53,75 @@ Function.prototype.async = Async.sugar; Utils.lazy(this, 'ClientData', ClientDataSvc); function ClientDataSvc() { - this._log = Log4Moz.Service.getLogger("Service.ClientData"); + this._init(); } ClientDataSvc.prototype = { - getClientData: function ClientData_getClientData() { - let self = yield; - - DAV.MKCOL("meta", self.cb); - let ret = yield; - if(!ret) - throw "Could not create meta information directory"; - - DAV.GET("meta/clients", self.cb); - let ret = yield; - - if (Utils.checkStatus(ret.status)) { - this._log.debug("Could not get clients file from server"); - self.done(false); - return; + _getCharPref: function ClientData__getCharPref(pref, defaultCb) { + let value; + try { + value = Utils.prefs.getCharPref(pref); + } catch (e) { + value = defaultCb(); + Utils.prefs.setCharPref(pref, value); } - - this._ClientData = this._json.decode(ret.responseText); - - this._log.debug("Successfully downloaded clients file from server"); - self.done(true); + return value; }, - uploadClientData: function ClientData_uploadClientData() { - let self = yield; - let json = this._json.encode(this._ClientData); + get GUID() { + return this._getCharPref("client.GUID", + function() { return Utils.makeGUID(); }); + }, + set GUID(value) { + Utils.prefs.setCharPref("client.GUID", value); + }, - DAV.MKCOL("meta", self.cb); - let ret = yield; + get name() { + return this._getCharPref("client.name", + function() { return "cheese"; }); + }, + set GUID(value) { + Utils.prefs.setCharPref("client.name", value); + }, + + get type() { + return this._getCharPref("client.type", + function() { return "gruyere"; }); + }, + set GUID(value) { + Utils.prefs.setCharPref("client.type", value); + }, + + _init: function ClientData__init() { + this._log = Log4Moz.Service.getLogger("Service.ClientData"); + this._remoteFile = new Resource("meta/clients"); + this._remote.pushFilter(new JsonFilter()); + }, + + _wrap: function ClientData__wrap() { + return { + GUID: this.GUID, + name: this.name, + type: this.type + }; + }, + + _refresh: function ClientData__refresh() { + let self = yield; + + let ret = yield DAV.MKCOL("meta", self.cb); if(!ret) throw "Could not create meta information directory"; - DAV.PUT("meta/clients", json, self.cb); - let ret = yield; - - if(Utils.checkStatus(ret.status)) { - this._log.debug("Could not upload clients file from server"); - self.done(false); - return; + try { + yield this._remote.get(self.cb); + } catch(e if e.status == 404) { + this._remote.data = {}; } - - this._log.debug("Successfully uploaded clients file to server"); - self.done(true); + this._remote.data[this.GUID] = this._wrap(); + yield this._remote.put(self.cb); + this._log.debug("Successfully downloaded clients file from server"); + }, + refresh: function ClientData_refresh() { + this._refresh.async(this, onComplete); } }; From 8502113901a509ca468fd7852a1da176107edd43 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 17:35:10 -0700 Subject: [PATCH 0647/1860] Remote.put doesn't require the data arg anymore --- services/sync/modules/engines.js | 4 ++-- services/sync/modules/service.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e6f32da2857f..bc114dfa7929 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -512,7 +512,7 @@ FileEngine.prototype = { yield this._keys.initialize(self.cb, this.engineId); this._file.data = {}; yield this._merge.async(this, self.cb); - yield this._file.put(self.cb, this._file.data); + yield this._file.put(self.cb); }, // NOTE: Assumes this._file has latest server data @@ -541,7 +541,7 @@ FileEngine.prototype = { yield this._keys.getKeyAndIV(self.cb, this.engineId); yield this._file.get(self.cb); yield this._merge.async(this, self.cb); - yield this._file.put(self.cb, this._file.data); + yield this._file.put(self.cb); } catch (e if e.status == 404) { this._log.info("Initial upload to server"); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4ca01ec4a36c..46187023aa27 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -349,6 +349,7 @@ WeaveSvc.prototype = { this._log.info("Server version too low. Wiping server data."); yield this._serverWipe.async(this, self.cb); yield this._uploadVersion.async(this, self.cb); + yield ClientData.upload } else if (ret.responseText > STORAGE_FORMAT_VERSION) { // XXX should we do something here? From 8260413f38ce36567a4a4a3219226d7b49d1c75f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 18:15:13 -0700 Subject: [PATCH 0648/1860] ClientData fixes; export to chrome as well --- services/sync/modules/clientData.js | 51 ++++++++++++++--------------- services/sync/modules/service.js | 2 ++ 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index ee8c1b1126ff..997503ae1596 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -42,10 +42,10 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/remote.js"); -Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; @@ -56,6 +56,27 @@ function ClientDataSvc() { this._init(); } ClientDataSvc.prototype = { + get GUID() { + return this._getCharPref("client.GUID", function() Utils.makeGUID()); + }, + set GUID(value) { + Utils.prefs.setCharPref("client.GUID", value); + }, + + get name() { + return this._getCharPref("client.name", function() "cheese"); + }, + set GUID(value) { + Utils.prefs.setCharPref("client.name", value); + }, + + get type() { + return this._getCharPref("client.type", function() "gruyere"); + }, + set GUID(value) { + Utils.prefs.setCharPref("client.type", value); + }, + _getCharPref: function ClientData__getCharPref(pref, defaultCb) { let value; try { @@ -67,33 +88,9 @@ ClientDataSvc.prototype = { return value; }, - get GUID() { - return this._getCharPref("client.GUID", - function() { return Utils.makeGUID(); }); - }, - set GUID(value) { - Utils.prefs.setCharPref("client.GUID", value); - }, - - get name() { - return this._getCharPref("client.name", - function() { return "cheese"; }); - }, - set GUID(value) { - Utils.prefs.setCharPref("client.name", value); - }, - - get type() { - return this._getCharPref("client.type", - function() { return "gruyere"; }); - }, - set GUID(value) { - Utils.prefs.setCharPref("client.type", value); - }, - _init: function ClientData__init() { this._log = Log4Moz.Service.getLogger("Service.ClientData"); - this._remoteFile = new Resource("meta/clients"); + this._remote = new Resource("meta/clients"); this._remote.pushFilter(new JsonFilter()); }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 46187023aa27..5a02ef32bb4d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -75,6 +75,7 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/clientData.js"); Cu.import("resource://weave/engines/cookies.js"); Cu.import("resource://weave/engines/bookmarks.js"); Cu.import("resource://weave/engines/history.js"); @@ -93,6 +94,7 @@ Cu.import("resource://weave/faultTolerance.js", Weave); Cu.import("resource://weave/crypto.js", Weave); Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); +Cu.import("resource://weave/clientData.js", Weave); Cu.import("resource://weave/dav.js", Weave); Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/syncCores.js", Weave); From 962200b45af806cb26e24250deec3880f00202df Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 18:34:10 -0700 Subject: [PATCH 0649/1860] some more ClientData fixes; *do* wipe the server when there is no version file; upload the version file when needed; refresh ClientData on login --- services/sync/modules/clientData.js | 8 ++++---- services/sync/modules/service.js | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index 997503ae1596..f747cd465f5a 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -109,16 +109,16 @@ ClientDataSvc.prototype = { if(!ret) throw "Could not create meta information directory"; - try { - yield this._remote.get(self.cb); - } catch(e if e.status == 404) { + try { yield this._remote.get(self.cb); } + catch (e if e.status == 404) { this._remote.data = {}; } + this._remote.data[this.GUID] = this._wrap(); yield this._remote.put(self.cb); this._log.debug("Successfully downloaded clients file from server"); }, - refresh: function ClientData_refresh() { + refresh: function ClientData_refresh(onComplete) { this._refresh.async(this, onComplete); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5a02ef32bb4d..0a89043b0041 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -339,10 +339,14 @@ WeaveSvc.prototype = { _versionCheck: function WeaveSvc__versionCheck() { let self = yield; - DAV.GET("meta/version", self.cb); - let ret = yield; + let ret = yield DAV.GET("meta/version", self.cb); - if (Utils.checkStatus(ret.status)) { + if (ret.status == 404) { + this._log.info("Could not get version file. Wiping server data."); + yield this._serverWipe.async(this, self.cb); + yield this._uploadVersion.async(this, self.cb); + + } else if (!Utils.checkStatus(ret.status)) { this._log.debug("Could not get version file from server"); self.done(false); return; @@ -351,7 +355,6 @@ WeaveSvc.prototype = { this._log.info("Server version too low. Wiping server data."); yield this._serverWipe.async(this, self.cb); yield this._uploadVersion.async(this, self.cb); - yield ClientData.upload } else if (ret.responseText > STORAGE_FORMAT_VERSION) { // XXX should we do something here? @@ -686,6 +689,9 @@ WeaveSvc.prototype = { // wipe the server if it has any old cruft yield this._versionCheck.async(this, self.cb); + // get info on the clients that are syncing with this store + yield ClientData.refresh(self.cb); + // cache keys, create public/private keypair if it doesn't exist this._log.debug("Caching keys"); let privkeyResp = yield DAV.GET("private/privkey", self.cb); From df50b43e3a8adc452d087e58802bbea5c992cd5a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 29 Jul 2008 18:38:58 -0700 Subject: [PATCH 0650/1860] use client GUID (from ClientData) for FileEngine --- services/sync/modules/engines.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index bc114dfa7929..807abcdefe0e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -50,6 +50,7 @@ Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/remote.js"); +Cu.import("resource://weave/clientData.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/syncCores.js"); @@ -495,7 +496,7 @@ FileEngine.prototype = { __proto__: new Engine(), get _profileID() { - return "cheese"; // FIXME! + return ClientData.GUID; }, _init: function FileEngine__init() { From e88f46568c4a0fc815dee3bee71a7ab4b14d2d2a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 30 Jul 2008 00:05:09 -0700 Subject: [PATCH 0651/1860] get tab sync halfway working again, with fileengine --- services/sync/modules/engines/tabs.js | 297 ++------------------------ services/sync/modules/remote.js | 4 +- 2 files changed, 17 insertions(+), 284 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 9825087aecf9..e68c523fc81a 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -61,7 +61,19 @@ TabEngine.prototype = { get displayName() { return "Tabs"; }, get logName() "TabEngine", get serverPrefix() "user-data/tabs/", - get store() this._store, + + get virtualTabs() { + let virtualTabs = {}; + let realTabs = this._store.wrap(); + + for each (let tabset in this._file.data) { + for (let guid in tabset) { + if (!(guid in realTabs) && !(guid in virtualTabs)) + virtualTabs[guid] = tabset[guid]; + } + } + return virtualTabs; + }, get _store() { let store = new TabStore(); @@ -69,12 +81,6 @@ TabEngine.prototype = { return this._store; }, - get _core() { - let core = new TabSyncCore(this._store); - this.__defineGetter__("_core", function() core); - return this._core; - }, - get _tracker() { let tracker = new TabTracker(this); this.__defineGetter__("_tracker", function() tracker); @@ -83,29 +89,11 @@ TabEngine.prototype = { }; -function TabSyncCore(store) { - this._store = store; - this._init(); -} -TabSyncCore.prototype = { - __proto__: new SyncCore(), - - _logName: "TabSync", - _store: null, - - _commandLike: function TSC_commandLike(a, b) { - // Not implemented. - return false; - } -}; - function TabStore() { - this._virtualTabs = {}; this._init(); } TabStore.prototype = { __proto__: new Store(), - _logName: "TabStore", get _sessionStore() { @@ -115,163 +103,6 @@ TabStore.prototype = { return this._sessionStore; }, - get _windowMediator() { - let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. - getService(Ci.nsIWindowMediator); - this.__defineGetter__("_windowMediator", function() windowMediator); - return this._windowMediator; - }, - - get _dirSvc() { - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - this.__defineGetter__("_dirSvc", function() dirSvc); - return this._dirSvc; - }, - - /** - * A cache of "virtual" tabs from other devices synced to the server - * that the user hasn't opened locally. Unlike other stores, we don't - * immediately apply create commands, which would be jarring to users. - * Instead, we store them in this cache and prompt the user to pick - * which ones she wants to open. - * - * We also persist this cache on disk and include it in the list of tabs - * we generate in this.wrap to reduce ping-pong updates between clients - * running simultaneously and to maintain a consistent state across restarts. - */ - _virtualTabs: null, - - get virtualTabs() { - // Make sure the list of virtual tabs is completely up-to-date (the user - // might have independently opened some of these virtual tabs since the last - // time we synced). - let realTabs = this._wrapRealTabs(); - let virtualTabsChanged = false; - for (let id in this._virtualTabs) { - if (id in realTabs) { - this._log.warn("get virtualTabs: both real and virtual tabs exist for " - + id + "; removing virtual one"); - delete this._virtualTabs[id]; - virtualTabsChanged = true; - } - } - if (virtualTabsChanged) - this._saveVirtualTabs(); - - return this._virtualTabs; - }, - - set virtualTabs(newValue) { - this._virtualTabs = newValue; - this._saveVirtualTabs(); - }, - - /** - * Make sure a virtual tab entry is valid (i.e. it contains the information - * we need to extract at least its current history entry's URL). - * - * @param tab {Object} the virtual tab to validate - * @returns {Boolean} whether or not the given virtual tab is valid - */ - validateVirtualTab: function TabStore__validateVirtualTab(tab) { - if (!tab.state || !tab.state.entries || !tab.state.index) { - this._log.warn("invalid virtual tab state: " + this._json.encode(tab)); - return false; - } - - let currentEntry = tab.state.entries[tab.state.index - 1]; - - if (!currentEntry || !currentEntry.url) { - this._log.warn("no current entry or no URL: " + this._json.encode(tab)); - return false; - } - - return true; - }, - - // The file in which we store the state of virtual tabs. - get _file() { - let file = this._dirSvc.get("ProfD", Ci.nsILocalFile); - file.append("weave"); - file.append("store"); - file.append("tabs"); - file.append("virtual.json"); - this.__defineGetter__("_file", function() file); - return this._file; - }, - - _saveVirtualTabs: function TabStore__saveVirtualTabs() { - try { - if (!this._file.exists()) - this._file.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE); - let out = this._json.encode(this._virtualTabs); - let [fos] = Utils.open(this._file, ">"); - fos.writeString(out); - fos.close(); - } - catch(ex) { - this._log.warn("could not serialize virtual tabs to disk: " + ex); - } - }, - - _restoreVirtualTabs: function TabStore__restoreVirtualTabs() { - try { - if (this._file.exists()) { - let [is] = Utils.open(this._file, "<"); - let json = Utils.readStream(is); - is.close(); - this._virtualTabs = this._json.decode(json); - } - } - catch (ex) { - this._log.warn("could not parse virtual tabs from disk: " + ex); - } - }, - - _init: function TabStore__init() { - this._restoreVirtualTabs(); - - this.__proto__.__proto__._init.call(this); - }, - - /** - * Apply commands generated by a diff during a sync operation. This method - * overrides the one in its superclass so it can save a copy of the latest set - * of virtual tabs to disk so they can be restored on startup. - */ - applyCommands: function TabStore_applyCommands(commandList) { - let self = yield; - - this.__proto__.__proto__.applyCommands.async(this, self.cb, commandList); - yield; - - this._saveVirtualTabs(); - - self.done(); - }, - - _itemExists: function TabStore__itemExists(GUID) { - // Note: this method returns true if the tab exists in any window, not just - // the window from which the tab came. In the future, if we care about - // windows, we might need to make this more specific, although in that case - // we'll have to identify tabs by something other than URL, since even - // window-specific tabs look the same when identified by URL. - - // Get the set of all real and virtual tabs. - let tabs = this.wrap(); - - // XXX Should we convert both to nsIURIs and then use nsIURI::equals - // to compare them? - if (GUID in tabs) { - this._log.trace("_itemExists: " + GUID + " exists"); - return true; - } - - this._log.trace("_itemExists: " + GUID + " doesn't exist"); - return false; - }, - _createCommand: function TabStore__createCommand(command) { this._log.debug("_createCommand: " + command.GUID); @@ -290,99 +121,10 @@ TabStore.prototype = { this._os.notifyObservers(null, "weave:store:tabs:virtual:created", null); }, - _removeCommand: function TabStore__removeCommand(command) { - this._log.debug("_removeCommand: " + command.GUID); - - // If this is a virtual tab, it's ok to remove it, since it was never really - // added to this session in the first place. But we don't remove it if it's - // a real tab, since that would be unexpected, unpleasant, and unwanted. - if (command.GUID in this._virtualTabs) { - delete this._virtualTabs[command.GUID]; - this._os.notifyObservers(null, "weave:store:tabs:virtual:removed", null); - } - }, - - _editCommand: function TabStore__editCommand(command) { - this._log.debug("_editCommand: " + command.GUID); - - // Don't do anything if the command isn't valid (i.e. it doesn't contain - // the minimum information about the tab that is necessary to recreate it). - if (!this.validateVirtualTab(command.data)) { - this._log.warn("could not edit command " + command.GUID + "; invalid"); - return; - } - - // We don't edit real tabs, because that isn't what the user would expect, - // but it's ok to edit virtual tabs, so that if users do open them, they get - // the most up-to-date version of them (and also to reduce sync churn). - - if (this._virtualTabs[command.GUID]) - this._virtualTabs[command.GUID] = command.data; - }, - /** * Serialize the current state of tabs. - * - * Note: the state includes both tabs on this device and those on others. - * We get the former from the session store. The latter we retrieved from - * the Weave server and stored in this._virtualTabs. Including virtual tabs - * in the serialized state prevents ping-pong deletes between two clients - * running at the same time. */ wrap: function TabStore_wrap() { - return this._wrapRealTabs(); - let items; - - let virtualTabs = this._wrapVirtualTabs(); - let realTabs = this._wrapRealTabs(); - - // Real tabs override virtual ones, which means ping-pong edits when two - // clients have the same URL loaded with different history/attributes. - // We could fix that by overriding real tabs with virtual ones, but then - // we'd have stale tab metadata in same cases. - items = virtualTabs; - let virtualTabsChanged = false; - for (let id in realTabs) { - // Since virtual tabs can sometimes get out of sync with real tabs - // (the user could have independently opened a new tab that exists - // in the virtual tabs cache since the last time we updated the cache), - // we sync them up in the process of merging them here. - if (this._virtualTabs[id]) { - this._log.warn("wrap: both real and virtual tabs exist for " + id + - "; removing virtual one"); - delete this._virtualTabs[id]; - virtualTabsChanged = true; - } - - items[id] = realTabs[id]; - } - if (virtualTabsChanged) - this._saveVirtualTabs(); - - return items; - }, - - _wrapVirtualTabs: function TabStore__wrapVirtualTabs() { - let items = {}; - - for (let id in this._virtualTabs) { - let virtualTab = this._virtualTabs[id]; - - // Copy the virtual tab without private properties (those that begin - // with an underscore character) so that we don't sync data private to - // this particular Weave client (like the _disposed flag). - let item = {}; - for (let property in virtualTab) - if (property[0] != "_") - item[property] = virtualTab[property]; - - items[id] = item; - } - - return items; - }, - - _wrapRealTabs: function TabStore__wrapRealTabs() { let items = {}; let session = this._json.decode(this._sessionStore.getBrowserState()); @@ -393,7 +135,7 @@ TabStore.prototype = { // (f.e. in the "selectedWindow" and each tab's "index" properties), so we // convert them to and from JavaScript's zero-based indexes as needed. let windowID = i + 1; - this._log.trace("_wrapRealTabs: window " + windowID); + for (let j = 0; j < window.tabs.length; j++) { let tab = window.tabs[j]; @@ -407,7 +149,6 @@ TabStore.prototype = { } let tabID = currentEntry.url; - this._log.trace("_wrapRealTabs: tab " + tabID); // Only sync up to 10 back-button entries, otherwise we can end up with // some insanely large snapshots. @@ -442,16 +183,8 @@ TabStore.prototype = { wipe: function TabStore_wipe() { // We're not going to close tabs, since that's probably not what - // the user wants, but we'll clear the cache of virtual tabs. - this._virtualTabs = {}; - this._saveVirtualTabs(); - }, - - _resetGUIDs: function TabStore__resetGUIDs() { - let self = yield; - // Not needed. + // the user wants } - }; function TabTracker(engine) { diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index d564a439306e..ed9a9b727e5d 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -192,8 +192,8 @@ Resource.prototype = { this._dirty = false; if (action == "GET") this._data = this._lastRequest.responseText; - else if (action == "PUT") - this._data = data; + //else if (action == "PUT") + // this._data = data; // wrong! (because of filters) break; } else if (action == "GET" && this._lastRequest.status == 404) { From 0b5ec318a8a2736a1aa2eb362066e3cf5ff69788 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 30 Jul 2008 21:58:13 -0700 Subject: [PATCH 0652/1860] more tab sync fixes, only disposing is left to fix; also allow the notify wrapper to pass along an optional data payload --- services/sync/modules/engines.js | 8 +++++--- services/sync/modules/engines/tabs.js | 7 +++++-- services/sync/modules/service.js | 29 ++++++++++++++------------- services/sync/modules/wrap.js | 17 ++++++++-------- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 807abcdefe0e..b20bce4f2bc3 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -241,11 +241,11 @@ Engine.prototype = { }, resetServer: function Engimne_resetServer(onComplete) { - this._notify("reset-server", this._resetServer).async(this, onComplete); + this._notify("reset-server", "", this._resetServer).async(this, onComplete); }, resetClient: function Engine_resetClient(onComplete) { - this._notify("reset-client", this._resetClient).async(this, onComplete); + this._notify("reset-client", "", this._resetClient).async(this, onComplete); } }; @@ -533,6 +533,7 @@ FileEngine.prototype = { let self = yield; this._log.info("Beginning sync"); + this._os.notifyObservers(null, "weave:service:sync:engine:start", this.name); if (!(yield DAV.MKCOL(this.serverPrefix, self.cb))) throw "Could not create remote folder"; @@ -550,6 +551,7 @@ FileEngine.prototype = { } this._log.info("Sync complete"); + this._os.notifyObservers(null, "weave:service:sync:engine:end", this.name); self.done(true); } -}; \ No newline at end of file +}; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index e68c523fc81a..0f6a2682de08 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -66,10 +66,13 @@ TabEngine.prototype = { let virtualTabs = {}; let realTabs = this._store.wrap(); - for each (let tabset in this._file.data) { + for (let profileId in this._file.data) { + let tabset = this._file.data[profileId]; for (let guid in tabset) { - if (!(guid in realTabs) && !(guid in virtualTabs)) + if (!(guid in realTabs) && !(guid in virtualTabs)) { virtualTabs[guid] = tabset[guid]; + virtualTabs[guid].profileId = profileId; + } } } return virtualTabs; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0a89043b0041..1ed1f36b34d8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -273,7 +273,7 @@ WeaveSvc.prototype = { this._log.info("Skipping scheduled sync; local operation in progress") } else { this._log.info("Running scheduled sync"); - this._notify("sync", + this._notify("sync", "", this._catchAll(this._lock(this._syncAsNeeded))).async(this); } } @@ -539,7 +539,7 @@ WeaveSvc.prototype = { verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, password, passphrase) { - this._localLock(this._notify("verify-passphrase", this._verifyPassphrase, + this._localLock(this._notify("verify-passphrase", "", this._verifyPassphrase, username, password, passphrase)). async(this, onComplete); }, @@ -601,7 +601,7 @@ WeaveSvc.prototype = { }, verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password) { - this._localLock(this._notify("verify-login", this._verifyLogin, + this._localLock(this._notify("verify-login", "", this._verifyLogin, username, password)).async(this, onComplete); }, @@ -621,7 +621,7 @@ WeaveSvc.prototype = { loginAndInit: function WeaveSvc_loginAndInit(onComplete, username, password, passphrase) { - this._localLock(this._notify("login", this._loginAndInit, + this._localLock(this._notify("login", "", this._loginAndInit, username, password, passphrase)). async(this, onComplete); }, @@ -640,7 +640,7 @@ WeaveSvc.prototype = { login: function WeaveSvc_login(onComplete, username, password, passphrase) { this._localLock( - this._notify("login", this._login, + this._notify("login", "", this._login, username, password, passphrase)).async(this, onComplete); }, _login: function WeaveSvc__login(username, password, passphrase) { @@ -668,7 +668,7 @@ WeaveSvc.prototype = { initialize: function WeaveSvc_initialize() { this._localLock( - this._notify("initialize", this._initialize)).async(this, onComplete); + this._notify("initialize", "", this._initialize)).async(this, onComplete); }, _initialize: function WeaveSvc__initialize() { @@ -729,7 +729,8 @@ WeaveSvc.prototype = { }, resetLock: function WeaveSvc_resetLock(onComplete) { - this._notify("reset-server-lock", this._resetLock).async(this, onComplete); + this._notify("reset-server-lock", "", + this._resetLock).async(this, onComplete); }, _resetLock: function WeaveSvc__resetLock() { let self = yield; @@ -745,7 +746,7 @@ WeaveSvc.prototype = { this.logout(); self.done(); }; - this._notify("server-wipe", this._lock(cb)).async(this, onComplete); + this._notify("server-wipe", "", this._lock(cb)).async(this, onComplete); }, _serverWipe: function WeaveSvc__serverWipe() { let self = yield; @@ -766,7 +767,7 @@ WeaveSvc.prototype = { // These are per-engine sync: function WeaveSvc_sync(onComplete) { - this._notify("sync", + this._notify("sync", "", this._catchAll(this._lock(this._sync))).async(this, onComplete); }, @@ -781,7 +782,7 @@ WeaveSvc.prototype = { if (!engines[i].enabled) continue; - yield this._notify(engines[i].name + "-engine:sync", + yield this._notify(engines[i].name + "-engine:sync", "", this._syncEngine, engines[i]).async(this, self.cb); } @@ -812,7 +813,7 @@ WeaveSvc.prototype = { this._log.debug(engine.name + " score " + score + " reaches threshold " + this._syncThresholds[engine.name] + "; syncing"); - this._notify(engine.name + "-engine:sync", + this._notify(engine.name + "-engine:sync", "", this._syncEngine, engine).async(this, self.cb); yield; @@ -861,7 +862,7 @@ WeaveSvc.prototype = { }, resetServer: function WeaveSvc_resetServer(onComplete) { - this._notify("reset-server", + this._notify("reset-server", "", this._lock(this._resetServer)).async(this, onComplete); }, _resetServer: function WeaveSvc__resetServer() { @@ -877,7 +878,7 @@ WeaveSvc.prototype = { }, resetClient: function WeaveSvc_resetClient(onComplete) { - this._localLock(this._notify("reset-client", + this._localLock(this._notify("reset-client", "", this._resetClient)).async(this, onComplete); }, _resetClient: function WeaveSvc__resetClient() { @@ -927,7 +928,7 @@ WeaveSvc.prototype = { let observer = { observe: function(subject, topic, data) { if (!Weave.DAV.locked) { - self._notify(messageName, self._lock(self._shareData, + self._notify(messageName, "", self._lock(self._shareData, saved_dataType, saved_isShareEnabled, saved_guid, diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 59f3b75e5ef1..1396c2f0ad9b 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -74,10 +74,11 @@ let Wrap = { // ... // } // }; - notify: function Weave_notify(name, method /* , arg1, arg2, ..., argN */) { + notify: function Weave_notify(name, payload, method /* , arg1, arg2, ..., argN */) { let savedName = name; + let savedPayload = payload; let savedMethod = method; - let savedArgs = Array.prototype.slice.call(arguments, 2); + let savedArgs = Array.prototype.slice.call(arguments, 3); return function WeaveNotifyWrapper(/* argN+1, argN+2, ... */) { let self = yield; @@ -85,20 +86,20 @@ let Wrap = { let args = Array.prototype.slice.call(arguments); try { - this._os.notifyObservers(null, this._osPrefix + savedName + ":start", ""); - this._os.notifyObservers(null, this._osPrefix + "global:start", ""); + this._os.notifyObservers(null, this._osPrefix + savedName + ":start", savedPayload); + this._os.notifyObservers(null, this._osPrefix + "global:start", savedPayload); args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); Async.run.apply(Async, args); ret = yield; - this._os.notifyObservers(null, this._osPrefix + savedName + ":success", ""); - this._os.notifyObservers(null, this._osPrefix + "global:success", ""); + this._os.notifyObservers(null, this._osPrefix + savedName + ":success", savedPayload); + this._os.notifyObservers(null, this._osPrefix + "global:success", savedPayload); } catch (e) { - this._os.notifyObservers(null, this._osPrefix + savedName + ":error", ""); - this._os.notifyObservers(null, this._osPrefix + "global:error", ""); + this._os.notifyObservers(null, this._osPrefix + savedName + ":error", savedPayload); + this._os.notifyObservers(null, this._osPrefix + "global:error", savedPayload); if (e != "Could not acquire lock") // FIXME HACK throw e; } From 3b5d133f71dd499a9e1d46a6cabd86d016909f9b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 30 Jul 2008 22:52:49 -0700 Subject: [PATCH 0653/1860] fix snapshot typos in SyncEngine --- services/sync/modules/engines.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b20bce4f2bc3..cbbf9c0c5446 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -253,9 +253,9 @@ function SyncEngine() {} SyncEngine.prototype = { __proto__: new Engine(), - get snapshot() { + get _snapshot() { let snap = new SnapshotStore(this.name); - this.__defineGetter__("_snapshot", function() snapshot); + this.__defineGetter__("_snapshot", function() snap); return snap; }, From 47911802647bb77b59ae96b335a0a24f8ada0e0a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 30 Jul 2008 22:53:11 -0700 Subject: [PATCH 0654/1860] have RemoteStore inherit from Store --- services/sync/modules/remote.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index ed9a9b727e5d..81c43d079f8c 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -429,6 +429,7 @@ function RemoteStore(engine) { this._log = Log4Moz.Service.getLogger("Service.RemoteStore"); } RemoteStore.prototype = { + __proto__: new Store(), get serverPrefix() this._engine.serverPrefix, get engineId() this._engine.engineId, From 4728070898fb9687b6b93e347b32363dd905a743 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 30 Jul 2008 22:53:27 -0700 Subject: [PATCH 0655/1860] do log some stack traces --- services/sync/modules/faultTolerance.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index 7dcf78856dc2..7581e540e155 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -37,6 +37,7 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); const EXPORTED_SYMBOLS = ["FaultTolerance"]; @@ -60,10 +61,15 @@ FTService.prototype = { // our current state }, onException: function FTS_onException(exception) { + let continueSync = true; this._lastException = exception; + if ("Could not acquire lock" == exception) - return false; // fatal error - return true; // continue + continueSync = false; + else + this._log.debug(Utils.stackTrace(exception)); + + return continueSync; } }; From 56b365f18ef8786086da25aef338144aa1a70754 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 30 Jul 2008 23:37:21 -0700 Subject: [PATCH 0656/1860] fix typo in utils deepCopy --- services/sync/modules/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 5487130efb75..05dd6a8089da 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -198,7 +198,7 @@ let Utils = { let props = [p for (p in thing)]; if (!noSort) props = props.sort(); - props.forEach(function(k) ret[k] = Utils.deepCopy(thing[k], sort)); + props.forEach(function(k) ret[k] = Utils.deepCopy(thing[k], noSort)); } return ret; From 3fc1d16308ba86500cc74390cb3c1dbd99b2e20d Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Wed, 30 Jul 2008 23:48:33 -0700 Subject: [PATCH 0657/1860] adding UI to add/edit client name and types --- services/sync/locales/en-US/preferences.dtd | 4 ++++ services/sync/services-sync.js | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index b329e2a84c2a..e76aba0889bd 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -22,6 +22,10 @@ + + + + diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index a75d25847eab..a19032c29a0f 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -35,6 +35,5 @@ pref("extensions.weave.log.logger.service.engine", "Debug"); pref("extensions.weave.log.logger.service.main", "Trace"); pref("extensions.weave.xmpp.enabled", false); -pref("extensions.weave.xmpp.server.url", - "https://sm-labs01.mozilla.org:81/xmpp"); +pref("extensions.weave.xmpp.server.url", "https://sm-labs01.mozilla.org:81/xmpp"); pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); From 5cd319dd6d21a2de93c66ece23ae62caf8970c46 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 31 Jul 2008 00:20:55 -0700 Subject: [PATCH 0658/1860] add a hack that supresses a strange error (this._remote.status.data is null, even though it was downloaded correctly) --- services/sync/modules/engines.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cbbf9c0c5446..b4f4062f1e7c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -314,7 +314,9 @@ SyncEngine.prototype = { this._snapshot.load(); try { + this._remote.status.data; // FIXME - otherwise we get an error... yield this._remote.openSession(self.cb, this._snapshot); + } catch (e if e.status == 404) { yield this._initialUpload.async(this, self.cb); return; From 0b68bdbc95d5344dce2658a34dc86e90d41d5fe7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 31 Jul 2008 00:21:22 -0700 Subject: [PATCH 0659/1860] filter out invalid tags when applying edit commands --- services/sync/modules/engines/bookmarks.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 623de6d9d0a8..dcc98798d7ea 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -1089,9 +1089,11 @@ BookmarksStore.prototype = { itemId, this._getItemIdForGUID(command.data.parentGUID), index); } break; case "tags": { + // filter out null/undefined/empty tags + let tags = command.data.tags.filter(function(t) t); let tagsURI = this._bms.getBookmarkURI(itemId); this._ts.untagURI(tagsURI, null); - this._ts.tagURI(tagsURI, command.data.tags); + this._ts.tagURI(tagsURI, tags); } break; case "keyword": this._bms.setKeywordForBookmark(itemId, command.data.keyword); From 0595ad6457e729bf454228b59dde9703ff2150c2 Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Thu, 31 Jul 2008 00:39:57 -0700 Subject: [PATCH 0660/1860] UI for clients --- services/sync/locales/en-US/preferences.dtd | 1 + services/sync/modules/clientData.js | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index e76aba0889bd..3634efa0ae76 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -1,6 +1,7 @@ + diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index f747cd465f5a..a1809f420150 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -64,19 +64,23 @@ ClientDataSvc.prototype = { }, get name() { - return this._getCharPref("client.name", function() "cheese"); + return this._getCharPref("client.name", function() "Firefox"); }, set GUID(value) { Utils.prefs.setCharPref("client.name", value); }, get type() { - return this._getCharPref("client.type", function() "gruyere"); + return this._getCharPref("client.type", function() "desktop"); }, set GUID(value) { Utils.prefs.setCharPref("client.type", value); }, + clients: function ClientData__clients() { + return this._remote.data; + }, + _getCharPref: function ClientData__getCharPref(pref, defaultCb) { let value; try { From 6e36a9b99c6794137e83ed8a8d5796cf45d5e9f0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 31 Jul 2008 01:02:41 -0700 Subject: [PATCH 0661/1860] sync client data when client prefs change --- services/sync/modules/clientData.js | 13 +++++++++++++ services/sync/modules/service.js | 2 ++ 2 files changed, 15 insertions(+) diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index a1809f420150..fefd1728e265 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -42,6 +42,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/dav.js"); @@ -96,6 +97,7 @@ ClientDataSvc.prototype = { this._log = Log4Moz.Service.getLogger("Service.ClientData"); this._remote = new Resource("meta/clients"); this._remote.pushFilter(new JsonFilter()); + Utils.prefs.addObserver("client", this, false); }, _wrap: function ClientData__wrap() { @@ -124,5 +126,16 @@ ClientDataSvc.prototype = { }, refresh: function ClientData_refresh(onComplete) { this._refresh.async(this, onComplete); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + observe: function WeaveSvc__observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + ClientData.refresh(self.cb); + break; + } } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1ed1f36b34d8..63645dc0dea0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -774,6 +774,8 @@ WeaveSvc.prototype = { _sync: function WeaveSvc__sync() { let self = yield; + yield ClientData.refresh(self.cb); + let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { if (this.cancelRequested) From 60530239f108d108723004f4ecce1b50b6fd12a7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 31 Jul 2008 01:59:03 -0700 Subject: [PATCH 0662/1860] refresh clients on a timer when changed via the prefpane, otherwise just wait until next sync --- services/sync/modules/clientData.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index fefd1728e265..34b3f8e4d470 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -97,7 +97,6 @@ ClientDataSvc.prototype = { this._log = Log4Moz.Service.getLogger("Service.ClientData"); this._remote = new Resource("meta/clients"); this._remote.pushFilter(new JsonFilter()); - Utils.prefs.addObserver("client", this, false); }, _wrap: function ClientData__wrap() { @@ -126,16 +125,5 @@ ClientDataSvc.prototype = { }, refresh: function ClientData_refresh(onComplete) { this._refresh.async(this, onComplete); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - - observe: function WeaveSvc__observe(subject, topic, data) { - switch (topic) { - case "nsPref:changed": - ClientData.refresh(self.cb); - break; - } } }; From 866e77912e1f4094e795c0cb211b9bfab01d140a Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 4 Aug 2008 17:23:23 -0700 Subject: [PATCH 0663/1860] Asynchronous bookmark sharing (bug 449113, r=thunder) --- services/sync/modules/dav.js | 8 ++++++ services/sync/modules/engines/bookmarks.js | 33 +++++++++++++++++++++- services/sync/modules/sharing.js | 29 +++++++++++++++---- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js index ceb2f2860fed..bec181c789c2 100644 --- a/services/sync/modules/dav.js +++ b/services/sync/modules/dav.js @@ -253,6 +253,14 @@ DAVCollection.prototype = { return this._makeRequest.async(this, onComplete, "POST", path, this._defaultHeaders, data); }, + + formPost: function DC_formPOST(path, data, onComplete) { + let headers = {'Content-type': 'application/x-www-form-urlencoded'}; + headers.__proto__ = this._defaultHeaders; + + return this._makeRequest.async(this, onComplete, "POST", path, + headers, data); + }, PUT: function DC_PUT(path, data, onComplete) { return this._makeRequest.async(this, onComplete, "PUT", path, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 623de6d9d0a8..a9733873b2fb 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -20,6 +20,7 @@ * Contributor(s): * Dan Mills * Jono DiCarlo + * Anant Narayanan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -290,6 +291,34 @@ BookmarksSharingManager.prototype = { self.done( true ); }, + /* FIXME! Gets all shares, not just the new ones. Doesn't impact + functionality because _incomingShareOffer does not create + duplicates, but annoys the user by showing notification of ALL + shares on EVERY sync :( + */ + getNewShares: function BmkSharing_getNewShares(onComplete) { + this._getNewShares.async(this, onComplete); + }, + _getNewShares: function BmkSharing__getNewShares() { + let self = yield; + + let sharingApi = new Sharing.Api( DAV ); + let result = yield sharingApi.getShares(self.cb); + + this._log.info("Got Shares: " + result); + let shares = result.split(','); + if (shares.length > 1) { + this._log.info('Found shares'); + for (var i = 0; i < shares.length - 1; i++) { + let share = shares[i].split(':'); + let name = share[0]; + let user = share[1]; + let path = share[2]; + this._incomingShareOffer(user, '/user/' + user + '/' + path, name); + } + } + }, + updateAllIncomingShares: function BmkSharing_updateAllIncoming(onComplete) { this._updateAllIncomingShares.async(this, onComplete); }, @@ -436,8 +465,9 @@ BookmarksSharingManager.prototype = { // Call Atul's js api for setting htaccess: let sharingApi = new Sharing.Api( DAV ); let result = yield sharingApi.shareWithUsers( serverPath, - [username], + [username], folderName, self.cb ); + this._log.info(result.errorText); // return the server path: self.done( serverPath ); }, @@ -725,6 +755,7 @@ BookmarksEngine.prototype = { /* After syncing the regular bookmark folder contents, * also update both the incoming and outgoing shared folders. */ let self = yield; + let ret = yield this._sharing.getNewShares(self.cb); this.__proto__.__proto__._sync.async(this, self.cb ); yield; this._sharing.updateAllOutgoingShares(self.cb); diff --git a/services/sync/modules/sharing.js b/services/sync/modules/sharing.js index 794febc5d8c8..787deff99a64 100644 --- a/services/sync/modules/sharing.js +++ b/services/sync/modules/sharing.js @@ -19,6 +19,7 @@ * * Contributor(s): * Atul Varma + * Anant Narayanan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -50,14 +51,29 @@ function Api(dav) { } Api.prototype = { - shareWithUsers: function Api_shareWithUsers(path, users, onComplete) { - this._shareGenerator.async(this, + shareWithUsers: function Api_shareWithUsers(path, users, folder, onComplete) { + return this._shareGenerator.async(this, onComplete, path, - users); + users, folder); }, - _shareGenerator: function Api__shareGenerator(path, users) { + getShares: function Api_getShares(onComplete) { + return this._getShareGenerator.async(this, onComplete); + }, + + _getShareGenerator: function Api__getShareGenerator() { + let self = yield; + let id = ID.get(this._dav.identity); + + this._dav.formPost("/api/share/get.php", ("uid=" + escape(id.username) + + "&password=" + escape(id.password)), + self.cb); + let xhr = yield; + self.done(xhr.responseText); + }, + + _shareGenerator: function Api__shareGenerator(path, users, folder) { let self = yield; let id = ID.get(this._dav.identity); @@ -67,10 +83,11 @@ Api.prototype = { let jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); let json = jsonSvc.encode(cmd); - this._dav.POST("/api/share/", + this._dav.formPost("/api/share/", ("cmd=" + escape(json) + "&uid=" + escape(id.username) + - "&password=" + escape(id.password)), + "&password=" + escape(id.password) + + "&name=" + escape(folder)), self.cb); let xhr = yield; From 1e469753a7a8ab526656a5069cc13fdf0b55a260 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 4 Aug 2008 17:34:21 -0700 Subject: [PATCH 0664/1860] Client-side OAuth support (bug 444528, r=thunder) --- services/sync/locales/en-US/oauth.dtd | 18 +++ services/sync/locales/en-US/oauth.properties | 10 ++ services/sync/modules/oauth.js | 156 +++++++++++++++++++ services/sync/modules/remote.js | 24 ++- services/sync/modules/service.js | 5 + 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 services/sync/locales/en-US/oauth.dtd create mode 100644 services/sync/locales/en-US/oauth.properties create mode 100644 services/sync/modules/oauth.js diff --git a/services/sync/locales/en-US/oauth.dtd b/services/sync/locales/en-US/oauth.dtd new file mode 100644 index 000000000000..87b9843cc37a --- /dev/null +++ b/services/sync/locales/en-US/oauth.dtd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/sync/locales/en-US/oauth.properties b/services/sync/locales/en-US/oauth.properties new file mode 100644 index 000000000000..bb2f02fb718c --- /dev/null +++ b/services/sync/locales/en-US/oauth.properties @@ -0,0 +1,10 @@ +intro.uidmsg = You are logged in as %S +conf.conmsg = A third party identifying itself as %S is requesting access to your Weave Data. +conf.error = Sorry, but the third party's request was invalid: %S +conf.error1 = a request token was not issued. +conf.error2 = the third party is unregistered. +conf.error3 = the request token has expired. +conf.error4 = your account details could not be verified. +final.step1 = Unwrapping symmetric key... +final.step2 = Adding third party to your keyring... +final.step3 = Done! \ No newline at end of file diff --git a/services/sync/modules/oauth.js b/services/sync/modules/oauth.js new file mode 100644 index 000000000000..8621f1ffb2bf --- /dev/null +++ b/services/sync/modules/oauth.js @@ -0,0 +1,156 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008. + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Anant Narayanan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['OAuth']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/engines.js"); + +Function.prototype.async = Async.sugar; + +Utils.lazy(this, 'OAuth', OAuthSvc); + +function OAuthSvc() { + this._init(); +} +OAuthSvc.prototype = { + _logName: "OAuth", + _keyring: null, + _consKey: null, + _bulkID: null, + _rsaKey: null, + _token: null, + _cback: null, + _uid: null, + _pwd: null, + _pas: null, + _cb1: null, + _cb2: null, + + _init: function OAuth__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log.level = "Debug"; + this._log.info("OAuth Module Initialized"); + }, + + setToken: function OAuth_setToken(token, cback) { + this._token = token; + this._cback = cback; + }, + + setUser: function OAuth_setUser(username, password, passphrase) { + this._uid = username; + this._pwd = password; + this._pas = passphrase; + }, + + validate: function OAuth_getName(obj, cb) { + if (!this._token || !this._uid || !this._pwd) + cb(obj, false); + + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + var key = btoa(this._uid + ":" + this._pwd); + + req.onreadystatechange = function(e) { + if (req.readyState == 4) { + if (req.status == 200) { + var fields = req.responseText.split(','); + cb(obj, fields[0], fields[1], fields[2]); + } else { + cb(obj, req.responseText); + } + } + }; + req.open('GET', 'https://services.mozilla.com/api/oauth/info.php?token=' + this._token + '&key=' + key); + req.send(null); + }, + + finalize: function OAuth_finalize(cb1, cb2, bundle) { + this._cb1 = cb1; + this._cb2 = cb2; + this._bundle = bundle; + + var bmkEngine = Engines.get('bookmarks'); + var bmkRstore = bmkEngine._remote; + + this._keyring = bmkRstore.keys; + this._keyring.getKeyAndIV(Utils.bind2(this, this._gotBulkKey), ID.get('WeaveID')); + }, + + _gotBulkKey: function OAuth_gotBulkKey() { + let consID = new Identity(); + consID.pubkey = this._rsaKey; + consID.username = this._consKey; + this._cb1(this._bundle); + this._log.info("Updating keyring for 3rd party access"); + this._keyring.setKey(Utils.bind2(this, this._done), ID.get('WeaveID'), consID); + }, + + _done: function OAuth__done() { + var cb = this._cb2; + var bu = this._bundle; + + if (!this._token || !this._uid || !this._pwd) + cb(this._bundle, false); + + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + var key = btoa(this._uid + ":" + this._pwd); + + req.onreadystatechange = function(e) { + if (req.readyState == 4) { + if (req.status == 200 && req.responseText == "1") { + cb(bu, true); + } else { + cb(bu, false); + } + } + }; + req.open('GET', 'https://services.mozilla.com/api/oauth/update.php?token=' + this._token + '&key=' + key); + req.send(null); + } +}; diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 81c43d079f8c..f06f16189bc5 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -418,10 +418,32 @@ Keychain.prototype = { identity.bulkKey = symkey; identity.bulkIV = iv; }, + _setKey: function KeyChain__setKey(bulkID, newID) { + /* FIXME!: It's possible that the keyring is changed on the server + after we do a GET. Then we're just uploading this new local keyring, + thereby losing any changes made on the server keyring since this GET. + + Also, if this.data was not instantiated properly (i.e. you're + using KeyChain directly instead of getting it from the engine), + you run the risk of wiping the server-side keychain. + */ + let self = yield; + + this.get(self.cb); + yield; + + let wrappedKey = yield Crypto.wrapKey.async(Crypto, self.cb, + bulkID.bulkKey, newID); + this.data.ring[newID.username] = wrappedKey; + this.put(self.cb, this.data); + yield; + }, getKeyAndIV: function Keychain_getKeyAndIV(onComplete, identity) { this._getKeyAndIV.async(this, onComplete, identity); + }, + setKey: function Keychain_setKey(onComplete, bulkID, newID) { + this._setKey.async(this, onComplete, bulkID, newID); } - // FIXME: implement setKey() }; function RemoteStore(engine) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 63645dc0dea0..b2eee3a1e105 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -72,6 +72,7 @@ Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/faultTolerance.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/oauth.js"); Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); @@ -99,6 +100,7 @@ Cu.import("resource://weave/dav.js", Weave); Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/syncCores.js", Weave); Cu.import("resource://weave/engines.js", Weave); +Cu.import("resource://weave/oauth.js", Weave); Cu.import("resource://weave/service.js", Weave); Cu.import("resource://weave/engines/cookies.js", Weave); Cu.import("resource://weave/engines/passwords.js", Weave); @@ -550,6 +552,9 @@ WeaveSvc.prototype = { this._log.debug("Verifying passphrase"); + this.username = username; + ID.get('WeaveID').setTempPassword(password); + let id = new Identity('Passphrase Verification', username); id.setTempPassword(passphrase); From 9fac04eccf4ab83242f9d9e0f3be8a68643e5f45 Mon Sep 17 00:00:00 2001 From: Dan Mosedale Date: Wed, 6 Aug 2008 14:51:41 -0700 Subject: [PATCH 0665/1860] Fix a typo that could cause an error in appendDelta() --- services/sync/modules/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index f06f16189bc5..1e56cc40d137 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -677,7 +677,7 @@ RemoteStore.prototype = { if ((id - this.status.data.snapVersion) > KEEP_DELTAS) { this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-snapshot"); - yield this.snapshot.put(self.cb, snapshot.data); + yield this._snapshot.put(self.cb, snapshot.data); this.status.data.snapVersion = id; } From 602a4eb53bfc12a6289745ce3681b06e91220e8d Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 7 Aug 2008 20:00:35 -0700 Subject: [PATCH 0666/1860] Syncing for Awesome bar (bug 437133, r=thunder) --- services/sync/locales/en-US/preferences.dtd | 1 + services/sync/modules/engines/history.js | 51 +++- services/sync/modules/engines/input.js | 295 ++++++++++++++++++++ services/sync/modules/service.js | 2 + services/sync/services-sync.js | 1 + 5 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 services/sync/modules/engines/input.js diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 3634efa0ae76..ea5a87ad2c4e 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -37,6 +37,7 @@ + diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index b611192f4fd0..4004f2103a11 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -121,6 +121,7 @@ function HistoryStore() { } HistoryStore.prototype = { _logName: "HistStore", + _lookup: null, __hsvc: null, get _hsvc() { @@ -133,6 +134,20 @@ HistoryStore.prototype = { return this.__hsvc; }, + __histDB: null, + get _histDB() { + if (!this.__histDB) { + let file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("places.sqlite"); + let stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__histDB = stor.openDatabase(file); + } + return this.__histDB; + }, + _itemExists: function HistStore__itemExists(GUID) { // we don't care about already-existing items; just try to re-add them return false; @@ -142,8 +157,12 @@ HistoryStore.prototype = { this._log.debug(" -> creating history entry: " + command.GUID); try { let uri = Utils.makeURI(command.data.URI); + let redirect = false; + if (command.data.transition == 5 || command.data.transition == 6) + redirect = true; + this._hsvc.addVisit(uri, command.data.time, null, - this._hsvc.TRANSITION_TYPED, false, null); + command.data.transition, redirect, 0); this._hsvc.setPageTitle(uri, command.data.title); } catch (e) { this._log.error("Exception caught: " + (e.message? e.message : e)); @@ -178,20 +197,44 @@ HistoryStore.prototype = { return root; }, + /* UGLY, UGLY way of syncing visit type ! + We'll just have to wait for bug #320831 */ + _getVisitType: function HistStore__getVisitType(uri) { + let visitStmnt = this._histDB.createStatement("SELECT visit_type FROM moz_historyvisits WHERE place_id = ?1"); + let pidStmnt = this._histDB.createStatement("SELECT id FROM moz_places WHERE url = ?1"); + + pidStmnt.bindUTF8StringParameter(0, uri); + + let placeID = null; + if (pidStmnt.executeStep()) { + placeID = pidStmnt.getInt32(0); + } + + if (placeID) { + visitStmnt.bindInt32Parameter(0, placeID); + if (visitStmnt.executeStep()) + return visitStmnt.getInt32(0); + } + return null; + }, + wrap: function HistStore_wrap() { let root = this._historyRoot(); root.containerOpen = true; let items = {}; for (let i = 0; i < root.childCount; i++) { let item = root.getChild(i); - let guid = item.time + ":" + item.uri + let guid = item.time + ":" + item.uri; + let vType = this._getVisitType(item.uri); items[guid] = {parentGUID: '', title: item.title, URI: item.uri, - time: item.time + time: item.time, + transition: vType }; - // FIXME: sync transition type - requires FULL_VISITs } + + this._lookup = items; return items; }, diff --git a/services/sync/modules/engines/input.js b/services/sync/modules/engines/input.js new file mode 100644 index 000000000000..14d981445fe0 --- /dev/null +++ b/services/sync/modules/engines/input.js @@ -0,0 +1,295 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Anant Narayanan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['InputEngine']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/syncCores.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); + +Function.prototype.async = Async.sugar; + +function InputEngine(pbeId) { + this._init(pbeId); +} +InputEngine.prototype = { + __proto__: new SyncEngine(), + + get name() { return "input"; }, + get displayName() { return "Input History"; }, + get logName() { return "InputEngine"; }, + get serverPrefix() { return "user-data/input/"; }, + + __store: null, + get _store() { + if (!this.__store) + this.__store = new InputStore(); + return this.__store; + }, + + __core: null, + get _core() { + if (!this.__core) + this.__core = new InputSyncCore(this._store); + return this.__core; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new InputTracker(); + return this.__tracker; + } +}; + +function InputSyncCore(store) { + this._store = store; + this._init(); +} +InputSyncCore.prototype = { + _logName: "InputSync", + _store: null, + + _commandLike: function FSC_commandLike(a, b) { + /* Not required as GUIDs for similar data sets will be the same */ + return false; + } +}; +InputSyncCore.prototype.__proto__ = new SyncCore(); + +function InputStore() { + this._init(); +} +InputStore.prototype = { + _logName: "InputStore", + _lookup: null, + + __placeDB: null, + get _placeDB() { + if (!this.__placeDB) { + let file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("places.sqlite"); + let stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__histDB = stor.openDatabase(file); + } + return this.__histDB; + }, + + _getIDfromURI: function InputStore__getIDfromURI(uri) { + let pidStmnt = this._placeDB.createStatement("SELECT id FROM moz_places WHERE url = ?1"); + pidStmnt.bindUTF8StringParameter(0, uri); + if (pidStmnt.executeStep()) + return pidStmnt.getInt32(0); + else + return null; + }, + + _getInputHistory: function InputStore__getInputHistory(id) { + let ipStmnt = this._placeDB.createStatement("SELECT input, use_count FROM moz_inputhistory WHERE place_id = ?1"); + ipStmnt.bindInt32Parameter(0, id); + + let input = []; + while (ipStmnt.executeStep()) { + let ip = ipStmnt.getUTF8String(0); + let cnt = ipStmnt.getInt32(1); + input[input.length] = {'input': ip, 'count': cnt}; + } + + return input; + }, + + _createCommand: function InputStore__createCommand(command) { + this._log.info("InputStore got createCommand: " + command); + + let placeID = this._getIDfromURI(command.GUID); + if (placeID) { + let createStmnt = this._placeDB.createStatement("INSERT INTO moz_inputhistory (?1, ?2, ?3)"); + createStmnt.bindInt32Parameter(0, placeID); + createStmnt.bindUTF8StringParameter(1, command.data.input); + createStmnt.bindInt32Parameter(2, command.data.count); + + createStmnt.execute(); + } + }, + + _removeCommand: function InputStore__removeCommand(command) { + this._log.info("InputStore got removeCommand: " + command); + + if (!(command.GUID in this._lookup)) { + this._log.warn("Invalid GUID found, ignoring remove request."); + return; + } + + let placeID = this._getIDfromURI(command.GUID); + let remStmnt = this._placeDB.createStatement("DELETE FROM moz_inputhistory WHERE place_id = ?1 AND input = ?2"); + + remStmnt.bindInt32Parameter(0, placeID); + remStmnt.bindUTF8StringParameter(1, command.data.input); + remStmnt.execute(); + + delete this._lookup[command.GUID]; + }, + + _editCommand: function InputStore__editCommand(command) { + this._log.info("InputStore got editCommand: " + command); + + if (!(command.GUID in this._lookup)) { + this._log.warn("Invalid GUID found, ignoring remove request."); + return; + } + + let placeID = this._getIDfromURI(command.GUID); + let editStmnt = this._placeDB.createStatement("UPDATE moz_inputhistory SET input = ?1, use_count = ?2 WHERE place_id = ?3"); + + if ('input' in command.data) { + editStmnt.bindUTF8StringParameter(0, command.data.input); + } else { + editStmnt.bindUTF8StringParameter(0, this._lookup[command.GUID].input); + } + + if ('count' in command.data) { + editStmnt.bindInt32Parameter(1, command.data.count); + } else { + editStmnt.bindInt32Parameter(1, this._lookup[command.GUID].count); + } + + editStmnt.bindInt32Parameter(2, placeID); + editStmnt.execute(); + }, + + wrap: function InputStore_wrap() { + this._lookup = {}; + let stmnt = this._placeDB.createStatement("SELECT * FROM moz_inputhistory"); + + while (stmnt.executeStep()) { + let pid = stmnt.getInt32(0); + let inp = stmnt.getUTF8String(1); + let cnt = stmnt.getInt32(2); + + let idStmnt = this._placeDB.createStatement("SELECT url FROM moz_places WHERE id = ?1"); + idStmnt.bindInt32Parameter(0, pid); + if (idStmnt.executeStep()) { + let key = idStmnt.getUTF8String(0); + this._lookup[key] = { 'input': inp, 'count': cnt }; + } + } + + return this._lookup; + }, + + wipe: function InputStore_wipe() { + var stmnt = this._placeDB.createStatement("DELETE FROM moz_inputhistory"); + stmnt.execute(); + }, + + _resetGUIDs: function InputStore__resetGUIDs() { + let self = yield; + // Not needed. + } +}; +InputStore.prototype.__proto__ = new Store(); + +function InputTracker() { + this._init(); +} +InputTracker.prototype = { + _logName: "InputTracker", + + __placeDB: null, + get _placeDB() { + if (!this.__placeDB) { + let file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("places.sqlite"); + let stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + this.__histDB = stor.openDatabase(file); + } + return this.__histDB; + }, + + /* + * To calculate scores, we just count the changes in + * the database since the last time we were asked. + * + * Each change is worth 5 points. + */ + _rowCount: 0, + get score() { + var stmnt = this._placeDB.createStatement("SELECT COUNT(place_id) FROM moz_inputhistory"); + stmnt.executeStep(); + var count = stmnt.getInt32(0); + stmnt.reset(); + + this._score = Math.abs(this._rowCount - count) * 5; + + if (this._score >= 100) + return 100; + else + return this._score; + }, + + resetScore: function InputTracker_resetScore() { + var stmnt = this._placeDB.createStatement("SELECT COUNT(place_id) FROM moz_inputhistory"); + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); + stmnt.reset(); + this._score = 0; + }, + + _init: function InputTracker__init() { + this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._score = 0; + + var stmnt = this._placeDB.createStatement("SELECT COUNT(place_id) FROM moz_inputhistory"); + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); + stmnt.reset(); + } +}; +InputTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b2eee3a1e105..78d10388232e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -83,6 +83,7 @@ Cu.import("resource://weave/engines/history.js"); Cu.import("resource://weave/engines/passwords.js"); Cu.import("resource://weave/engines/forms.js"); Cu.import("resource://weave/engines/tabs.js"); +Cu.import("resource://weave/engines/input.js"); Function.prototype.async = Async.sugar; @@ -108,6 +109,7 @@ Cu.import("resource://weave/engines/bookmarks.js", Weave); Cu.import("resource://weave/engines/history.js", Weave); Cu.import("resource://weave/engines/forms.js", Weave); Cu.import("resource://weave/engines/tabs.js", Weave); +Cu.import("resource://weave/engines/input.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index a19032c29a0f..eb1fd3ea59d1 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -21,6 +21,7 @@ pref("extensions.weave.engine.cookies", true ); pref("extensions.weave.engine.passwords", false); pref("extensions.weave.engine.forms", false); pref("extensions.weave.engine.tabs", true); +pref("extensions.weave.engine.input", false); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From ecbf85dc0e2d5372ca819c9f984d20fa2f1e5925 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 8 Aug 2008 12:34:01 -0700 Subject: [PATCH 0667/1860] whitespace fix --- services/sync/modules/stores.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 1510b2a6d2a2..702d3ace62db 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -195,7 +195,7 @@ SnapshotStore.prototype = { if ("GUID" in command.data) { // special-case guid changes let newGUID = command.data.GUID, - oldGUID = command.GUID; + oldGUID = command.GUID; this._data[newGUID] = this._data[oldGUID]; delete this._data[oldGUID]; From 6006a426be66988c8d215884971bae46a13ac76b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 8 Aug 2008 14:40:52 -0700 Subject: [PATCH 0668/1860] small fix in _fixParents; change reconcile to 1) not require a deepEquals to drop an incoming/outgoing command pair, merely a GUID match (we can do this because we know we'll drop server changes and use client ones, and because we know we'll actually do a fresh diff at the end to send commands to the server), and 2) check for creates of existing guids after the first pass (since we might find command pairs that go away and thus not hit that error) --- services/sync/modules/syncCores.js | 36 +++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index b04957bce34f..834939dcefd6 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -180,7 +180,8 @@ SyncCore.prototype = { for (let i = 0; i < list.length; i++) { if (!list[i]) continue; - if (list[i].data && list[i].data.parentGUID == oldGUID) + if (list[i].data && list[i].data.parentGUID && + list[i].data.parentGUID == oldGUID) list[i].data.parentGUID = newGUID; for (let j = 0; j < list[i].parents.length; j++) { if (list[i].parents[j] == oldGUID) @@ -222,6 +223,7 @@ SyncCore.prototype = { " against " + listB.length + " commands"); let guidChanges = []; + let edits = []; for (let i = 0; i < listA.length; i++) { let a = listA[i]; @@ -233,32 +235,40 @@ SyncCore.prototype = { if (skip) return true; - if (Utils.deepEquals(a, b)) { - delete listA[i]; // a + if (a.GUID == b.GUID) { + // delete both commands + // XXX this relies on the fact that we actually dump + // outgoing commands and generate new ones by doing a fresh + // diff after applying local changes skip = true; - return false; // b + delete listA[i]; // delete a + return false; // delete b } else if (this._commandLike(a, b)) { this._fixParents(listA, a.GUID, b.GUID); guidChanges.push({action: "edit", GUID: a.GUID, data: {GUID: b.GUID}}); - delete listA[i]; // a skip = true; - return false; // b, but we add it back from guidChanges - } - - // watch out for create commands with GUIDs that already exist - if (b.action == "create" && this._store._itemExists(b.GUID)) { - this._log.error("Remote command has GUID that already exists " + - "locally. Dropping command."); + delete listA[i]; // delete a return false; // delete b } return true; // keep b }, this); } - listA = listA.filter(function(elt) { return elt }); + listB = listB.filter(function(b) { + // watch out for create commands with GUIDs that already exist + if (b.action == "create" && this._store._itemExists(b.GUID)) { + this._log.error("Remote command has GUID that already exists " + + "locally. Dropping command."); + return false; // delete b + } + return true; // keep b + }, this); + + listA = listA.filter(function(elt) { return elt }); // removes any holes + listA = listA.concat(edits); listB = guidChanges.concat(listB); for (let i = 0; i < listA.length; i++) { From ea759ef00e1886a6571085f27b5c0a096d5f77c8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 8 Aug 2008 14:42:57 -0700 Subject: [PATCH 0669/1860] move some code from the engine to remote.js; rename FileEngine to BlobEngine (since it doesn't actually sync complete files); clean up SyncEngine's sync method --- services/sync/modules/engines.js | 168 +++++++++++++------------- services/sync/modules/engines/tabs.js | 2 +- services/sync/modules/remote.js | 89 +++++++++----- 3 files changed, 143 insertions(+), 116 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b4f4062f1e7c..f89112b32889 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,7 +35,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine', 'FileEngine']; +const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine', 'BlobEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -322,48 +322,30 @@ SyncEngine.prototype = { return; } - if (this._remote.status.data.GUID != this._snapshot.GUID) { - this._log.debug("Remote/local sync GUIDs do not match. " + - "Forcing initial sync."); - this._log.trace("Remote: " + this._remote.status.data.GUID); - this._log.trace("Local: " + this._snapshot.GUID); - yield this._store.resetGUIDs(self.cb); - this._snapshot.data = {}; - this._snapshot.version = -1; - this._snapshot.GUID = this._remote.status.data.GUID; - } - - this._log.info("Local snapshot version: " + this._snapshot.version); - this._log.info("Server maxVersion: " + this._remote.status.data.maxVersion); - - if ("none" != Utils.prefs.getCharPref("encryption")) - yield this._remote.keys.getKeyAndIV(self.cb, this.engineId); - // 1) Fetch server deltas this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); - let server = {}; - let serverSnap = yield this._remote.wrap(self.cb, this._snapshot); - server.snapshot = serverSnap.data; - this._core.detectUpdates(self.cb, this._snapshot.data, server.snapshot); - server.updates = yield; + let serverSnap = yield this._remote.wrap(self.cb); + let serverUpdates = yield this._core.detectUpdates(self.cb, + this._snapshot.data, serverSnap); // 2) Generate local deltas from snapshot -> current client status this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); - let localJson = new SnapshotStore(); - localJson.data = this._store.wrap(); - this._core.detectUpdates(self.cb, this._snapshot.data, localJson.data); + let localSnap = new SnapshotStore(); + localSnap.data = this._store.wrap(); + this._core.detectUpdates(self.cb, this._snapshot.data, localSnap.data); let localUpdates = yield; - this._log.trace("local json:\n" + localJson.serialize()); + this._log.trace("local json:\n" + localSnap.serialize()); this._log.trace("Local updates: " + this._serializeCommands(localUpdates)); - this._log.trace("Server updates: " + this._serializeCommands(server.updates)); + this._log.trace("Server updates: " + this._serializeCommands(serverUpdates)); - if (server.updates.length == 0 && localUpdates.length == 0) { - this._snapshot.version = this._remote.status.data.maxVersion; + if (serverUpdates.length == 0 && localUpdates.length == 0) { this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); this._log.info("Sync complete: no changes needed on client or server"); + this._snapshot.version = this._remote.status.data.maxVersion; + this._snapshot.save(); self.done(true); return; } @@ -372,8 +354,7 @@ SyncEngine.prototype = { this._os.notifyObservers(null, "weave:service:sync:status", "status.reconciling-updates"); this._log.info("Reconciling client/server updates"); - this._core.reconcile(self.cb, localUpdates, server.updates); - let ret = yield; + let ret = yield this._core.reconcile(self.cb, localUpdates, serverUpdates); let clientChanges = ret.propagations[0]; let serverChanges = ret.propagations[1]; @@ -393,20 +374,15 @@ SyncEngine.prototype = { clientConflicts.length || serverConflicts.length)) { this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); this._log.info("Sync complete: no changes needed on client or server"); - this._snapshot.data = localJson.data; + this._snapshot.data = localSnap.data; this._snapshot.version = this._remote.status.data.maxVersion; this._snapshot.save(); self.done(true); return; } - if (clientConflicts.length || serverConflicts.length) { + if (clientConflicts.length || serverConflicts.length) this._log.warn("Conflicts found! Discarding server changes"); - } - - let savedSnap = Utils.deepCopy(this._snapshot.data); - let savedVersion = this._snapshot.version; - let newSnapshot; // 3.1) Apply server changes to local store @@ -414,29 +390,27 @@ SyncEngine.prototype = { this._log.info("Applying changes locally"); this._os.notifyObservers(null, "weave:service:sync:status", "status.applying-changes"); - // Note that we need to need to apply client changes to the - // current tree, not the saved snapshot + // apply to real store + yield this._store.applyCommands.async(this._store, self.cb, clientChanges); - localJson.applyCommands.async(localJson, self.cb, clientChanges); - yield; - this._snapshot.data = localJson.data; - this._snapshot.version = this._remote.status.data.maxVersion; - this._store.applyCommands.async(this._store, self.cb, clientChanges); - yield; - newSnapshot = this._store.wrap(); + // get the current state + let newSnap = new SnapshotStore(); + newSnap.data = this._store.wrap(); - this._core.detectUpdates(self.cb, this._snapshot.data, newSnapshot); - let diff = yield; + // apply to the snapshot we got in step 1, and compare with current state + yield localSnap.applyCommands.async(localSnap, self.cb, clientChanges); + let diff = yield this._core.detectUpdates(self.cb, + localSnap.data, newSnap.data); if (diff.length != 0) { this._log.warn("Commands did not apply correctly"); this._log.trace("Diff from snapshot+commands -> " + "new snapshot after commands:\n" + this._serializeCommands(diff)); - // FIXME: do we really want to revert the snapshot here? - this._snapshot.data = Utils.deepCopy(savedSnap); - this._snapshot.version = savedVersion; } - this._snapshot.save(); + + // update the local snap to the current state, we'll use it below + localSnap.data = newSnap.data; + localSnap.version = this._remote.status.data.maxVersion; } // 3.2) Append server delta to the delta file and upload @@ -445,10 +419,10 @@ SyncEngine.prototype = { // current client snapshot. In the case where there are no // conflicts, it should be the same as what the resolver returned - this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); - newSnapshot = this._store.wrap(); - this._core.detectUpdates(self.cb, server.snapshot, newSnapshot); - let serverDelta = yield; + this._os.notifyObservers(null, "weave:service:sync:status", + "status.calculating-differences"); + let serverDelta = yield this._core.detectUpdates(self.cb, + serverSnap, localSnap.data); // Log an error if not the same if (!(serverConflicts.length || @@ -462,46 +436,38 @@ SyncEngine.prototype = { if (serverDelta.length) { this._log.info("Uploading changes to server"); - this._snapshot.data = newSnapshot; - this._snapshot.version = ++this._remote.status.data.maxVersion; + this._os.notifyObservers(null, "weave:service:sync:status", + "status.uploading-deltas"); - // XXX don't append delta if we do a full upload? - if (this._remote.status.data.formatVersion != ENGINE_STORAGE_FORMAT_VERSION) - yield this._remote.initialize(self.cb, this._snapshot); - - let c = 0; - for (GUID in this._snapshot.data) - c++; - - this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-deltas"); - - this._remote.appendDelta(self.cb, this._snapshot, serverDelta, - {maxVersion: this._snapshot.version, - deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c}); - yield; + yield this._remote.appendDelta(self.cb, localSnap, serverDelta, + {maxVersion: this._snapshot.version, + deltasEncryption: Crypto.defaultAlgorithm}); + localSnap.version = this._remote.status.data.maxVersion; this._log.info("Successfully updated deltas and status on server"); - this._snapshot.save(); } + this._snapshot.data = localSnap.data; + this._snapshot.version = localSnap.version; + this._snapshot.save(); + this._log.info("Sync complete"); self.done(true); } }; -function FileEngine() { +function BlobEngine() { // subclasses should call _init // we don't call it here because it requires serverPrefix to be set } -FileEngine.prototype = { +BlobEngine.prototype = { __proto__: new Engine(), get _profileID() { return ClientData.GUID; }, - _init: function FileEngine__init() { + _init: function BlobEngine__init() { // FIXME meep? this.__proto__.__proto__.__proto__.__proto__._init.call(this); this._keys = new Keychain(this.serverPrefix); @@ -510,8 +476,9 @@ FileEngine.prototype = { this._file.pushFilter(new CryptoFilter(this.engineId)); }, - _initialUpload: function FileEngine__initialUpload() { + _initialUpload: function BlobEngine__initialUpload() { let self = yield; + this._log.info("Initial upload to server"); yield this._keys.initialize(self.cb, this.engineId); this._file.data = {}; yield this._merge.async(this, self.cb); @@ -520,7 +487,7 @@ FileEngine.prototype = { // NOTE: Assumes this._file has latest server data // this method is separate from _sync so it's easy to override in subclasses - _merge: function FileEngine__merge() { + _merge: function BlobEngine__merge() { let self = yield; this._file.data[this._profileID] = this._store.wrap(); }, @@ -531,7 +498,7 @@ FileEngine.prototype = { // 3) Upload new merged data // NOTE: a version file will be needed in the future to optimize the case // where there are no changes - _sync: function FileEngine__sync() { + _sync: function BlobEngine__sync() { let self = yield; this._log.info("Beginning sync"); @@ -548,12 +515,45 @@ FileEngine.prototype = { yield this._file.put(self.cb); } catch (e if e.status == 404) { - this._log.info("Initial upload to server"); yield this._initialUpload.async(this, self.cb); } this._log.info("Sync complete"); - this._os.notifyObservers(null, "weave:service:sync:engine:end", this.name); + this._os.notifyObservers(null, "weave:service:sync:engine:success", this.name); self.done(true); } }; + +function HeuristicEngine() { +} +HeuristicEngine.prototype = { + __proto__: new Engine(), + + get _snapshot() { + let snap = new SnapshotStore(this.name); + this.__defineGetter__("_snapshot", function() snap); + return snap; + }, + + _resetClient: function SyncEngine__resetClient() { + let self = yield; + this._log.debug("Resetting client state"); + this._snapshot.wipe(); + this._store.wipe(); + this._log.debug("Client reset completed successfully"); + }, + + _initialUpload: function HeuristicEngine__initialUpload() { + let self = yield; + this._log.info("Initial upload to server"); + this._snapshot.data = this._store.wrap(); + this._snapshot.version = 0; + this._snapshot.GUID = null; // in case there are other snapshots out there + yield this._remote.initialize(self.cb, this._snapshot); + this._snapshot.save(); + }, + + _sync: function HeuristicEngine__sync() { + let self = yield; + } +}; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0f6a2682de08..25c22a17c23c 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -55,7 +55,7 @@ function TabEngine(pbeId) { } TabEngine.prototype = { - __proto__: new FileEngine(), + __proto__: new BlobEngine(), get name() "tabs", get displayName() { return "Tabs"; }, diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js index 81c43d079f8c..37f5bf61dea0 100644 --- a/services/sync/modules/remote.js +++ b/services/sync/modules/remote.js @@ -429,10 +429,17 @@ function RemoteStore(engine) { this._log = Log4Moz.Service.getLogger("Service.RemoteStore"); } RemoteStore.prototype = { - __proto__: new Store(), get serverPrefix() this._engine.serverPrefix, get engineId() this._engine.engineId, + __os: null, + get _os() { + if (!this.__os) + this.__os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + return this.__os; + }, + get status() { let status = new Resource(this.serverPrefix + "status.json"); status.pushFilter(new JsonFilter()); @@ -462,7 +469,7 @@ RemoteStore.prototype = { return deltas; }, - _openSession: function RStore__openSession() { + _openSession: function RStore__openSession(lastSyncSnap) { let self = yield; if (!this.serverPrefix || !this.engineId) @@ -472,6 +479,7 @@ RemoteStore.prototype = { this.keys.data = null; this._snapshot.data = null; this._deltas.data = null; + this._lastSyncSnap = lastSyncSnap; let ret = yield DAV.MKCOL(this.serverPrefix + "deltas", self.cb); if (!ret) @@ -491,9 +499,24 @@ RemoteStore.prototype = { ENGINE_STORAGE_FORMAT_VERSION); throw "Incompatible remote store format"; } + + if (this.status.data.GUID != lastSyncSnap.GUID) { + this._log.trace("Remote GUID: " + this.status.data.GUID); + this._log.trace("Local GUID: " + lastSyncSnap.GUID); + this._log.debug("Server wipe since last sync, resetting last sync snapshot"); + lastSyncSnap.wipe(); + lastSyncSnap.GUID = this.status.data.GUID; + // yield this._store.resetGUIDs(self.cb); // XXX not sure if this is really needed (and it needs to be done from the engine if so) + } + + this._log.info("Last sync snapshot version: " + lastSyncSnap.version); + this._log.info("Server maxVersion: " + this.status.data.maxVersion); + + if ("none" != Utils.prefs.getCharPref("encryption")) + yield this.keys.getKeyAndIV(self.cb, this.engineId); }, - openSession: function RStore_openSession(onComplete) { - this._openSession.async(this, onComplete); + openSession: function RStore_openSession(onComplete, lastSyncSnap) { + this._openSession.async(this, onComplete, lastSyncSnap); }, closeSession: function RStore_closeSession() { @@ -501,6 +524,7 @@ RemoteStore.prototype = { this.keys.data = null; this._snapshot.data = null; this._deltas.data = null; + this._lastSyncSnap = null; }, // Does a fresh upload of the given snapshot to a new store @@ -511,7 +535,6 @@ RemoteStore.prototype = { yield this.keys.initialize(self.cb, this.engineId); this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-snapshot"); yield this._snapshot.put(self.cb, snapshot.data); - //yield this._deltas.put(self.cb, []); let c = 0; for (GUID in snapshot.data) @@ -533,6 +556,7 @@ RemoteStore.prototype = { }, // Removes server files - you may want to run initialize() after this + // FIXME: might want to do a PROPFIND instead (to catch all deltas in one go) _wipe: function Engine__wipe() { let self = yield; this._log.debug("Deleting remote store data"); @@ -550,57 +574,58 @@ RemoteStore.prototype = { // (snapshot + deltas) _getLatestFromScratch: function RStore__getLatestFromScratch() { let self = yield; - let status = this.status.data; this._log.info("Downloading all server data from scratch"); this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-snapshot"); let snap = new SnapshotStore(); snap.data = yield this._snapshot.get(self.cb); - snap.version = status.maxVersion; + this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); + let status = this.status.data; for (let id = status.snapVersion + 1; id <= status.maxVersion; id++) { let delta = yield this._deltas.get(self.cb, id); yield snap.applyCommands.async(snap, self.cb, delta); } - self.done(snap); + self.done(snap.data); }, // Gets the latest server snapshot by downloading only the necessary // deltas from the given snapshot (but may fall back to a full download) - _getLatestFromSnap: function RStore__getLatestFromSnap(lastSyncSnap) { + _getLatestFromSnap: function RStore__getLatestFromSnap() { let self = yield; let deltas, snap = new SnapshotStore(); snap.version = this.status.data.maxVersion; - if (lastSyncSnap.version < this.status.data.snapVersion) { - this._log.trace("Getting latest from snap --> scratch"); - snap = yield this._getLatestFromScratch.async(this, self.cb); - self.done(snap); + if (!this._lastSyncSnap || + this._lastSyncSnap.version < this.status.data.snapVersion) { + this._log.trace("Getting latest from scratch (last sync snap too old)"); + snap.data = yield this._getLatestFromScratch.async(this, self.cb); + self.done(snap.data); return; - } else if (lastSyncSnap.version >= this.status.data.snapVersion && - lastSyncSnap.version < this.status.data.maxVersion) { + } else if (this._lastSyncSnap.version >= this.status.data.snapVersion && + this._lastSyncSnap.version < this.status.data.maxVersion) { this._log.debug("Using last sync snapshot as starting point for server snapshot"); - snap.data = Utils.deepCopy(lastSyncSnap.data); + snap.data = Utils.deepCopy(this._lastSyncSnap.data); this._log.info("Downloading server deltas"); this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); deltas = []; - let min = lastSyncSnap.version + 1; + let min = this._lastSyncSnap.version + 1; let max = this.status.data.maxVersion; for (let id = min; id <= max; id++) { let delta = yield this._deltas.get(self.cb, id); deltas.push(delta); } - } else if (lastSyncSnap.version == this.status.data.maxVersion) { + } else if (this._lastSyncSnap.version == this.status.data.maxVersion) { this._log.debug("Using last sync snapshot as server snapshot (snap version == max version)"); this._log.trace("Local snapshot version == server maxVersion"); - snap.data = Utils.deepCopy(lastSyncSnap.data); + snap.data = Utils.deepCopy(this._lastSyncSnap.data); deltas = []; - } else { // lastSyncSnap.version > this.status.data.maxVersion + } else { // this._lastSyncSnap.version > this.status.data.maxVersion this._log.error("Server snapshot is older than local snapshot"); throw "Server snapshot is older than local snapshot"; } @@ -613,23 +638,21 @@ RemoteStore.prototype = { this._log.warn("Error applying remote deltas to saved snapshot, attempting a full download"); this._log.debug("Exception: " + Utils.exceptionStr(e)); this._log.trace("Stack:\n" + Utils.stackTrace(e)); - snap = yield this._getLatestFromScratch.async(this, self.cb); + snap.data = yield this._getLatestFromScratch.async(this, self.cb); } - self.done(snap); + self.done(snap.data); }, // get the latest server snapshot. If a snapshot is given, try to // download only the necessary deltas to get to the latest - _wrap: function RStore__wrap(snapshot) { + _wrap: function RStore__wrap() { let self = yield; - if (snapshot) - self.done(yield this._getLatestFromSnap.async(this, self.cb, snapshot)); - else - self.done(yield this._getLatestFromScratch.async(this, self.cb)); + let ret = yield this._getLatestFromSnap.async(this, self.cb); + self.done(ret); }, - wrap: function RStore_wrap(onComplete, snapshot) { - this._wrap.async(this, onComplete, snapshot); + wrap: function RStore_wrap(onComplete) { + this._wrap.async(this, onComplete); }, // Adds a new set of changes (a delta) to this store @@ -641,8 +664,12 @@ RemoteStore.prototype = { this.status.data[key] = metadata[key]; } - // FIXME: we should increment maxVersion here instead of in Engine - let id = this.status.data.maxVersion; + let c = 0; + for (item in snapshot.data) + c++; + this.status.data.itemCount = c; + + let id = ++this.status.data.maxVersion; // upload the delta even if we upload a new snapshot, so other clients // can be spared of a full re-download From 27412904b78a97d3d12951e25c03f03bf649ba07 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 8 Aug 2008 15:14:04 -0700 Subject: [PATCH 0670/1860] bump version to 0.2.6 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 46505d76a797..43d7163a2d03 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,7 +43,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; -const WEAVE_VERSION = "0.2.5"; +const WEAVE_VERSION = "0.2.6"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 9a48f80d67a0f72a1fa98610501a2745fbc115d6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 13 Aug 2008 15:19:56 -0700 Subject: [PATCH 0671/1860] Bug 450396: password engine does not define a tracker --- services/sync/modules/engines/passwords.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 61b471dc3bfa..5e0245e3acca 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -70,6 +70,13 @@ PasswordEngine.prototype = { if (!this.__core) this.__core = new PasswordSyncCore(this._store); return this.__core; + }, + + __tracker: null, + get _tracker() { + if (!this.__tracker) + this.__tracker = new PasswordTracker(); + return this.__tracker; } }; From 0f47a003b52f48392904c335712a8bc1b7a268f2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 13 Aug 2008 15:23:15 -0700 Subject: [PATCH 0672/1860] catch exceptions from microsummaries that don't have a static title set --- services/sync/modules/engines/bookmarks.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 3f9f35e8f7ae..d65a426e8a65 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -318,7 +318,7 @@ BookmarksSharingManager.prototype = { } } }, - + updateAllIncomingShares: function BmkSharing_updateAllIncoming(onComplete) { this._updateAllIncomingShares.async(this, onComplete); }, @@ -1232,7 +1232,11 @@ BookmarksStore.prototype = { item.type = "microsummary"; let micsum = this._ms.getMicrosummary(node.itemId); item.generatorURI = micsum.generator.uri.spec; // breaks local generators - item.staticTitle = this._ans.getItemAnnotation(node.itemId, "bookmarks/staticTitle"); + item.staticTitle = ""; + try { + item.staticTitle = this._ans.getItemAnnotation(node.itemId, + "bookmarks/staticTitle"); + } catch (e) {} } else if (node.type == node.RESULT_TYPE_QUERY) { item.type = "query"; item.title = node.title; From cb86792c2deeff1ab4f39ebaac88559cda2d83fc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 14 Aug 2008 17:23:11 -0700 Subject: [PATCH 0673/1860] Bug 450526: fix tag sync bug created by a typo in deepCopy. Fix by Mishail --- services/sync/modules/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 05dd6a8089da..8ccb0c05cc52 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -191,7 +191,7 @@ let Utils = { if ("Array" == thing.constructor.name) { ret = []; for (let i = 0; i < thing.length; i++) - ret.push(Utils.deepCopy(thing[i]), noSort); + ret.push(Utils.deepCopy(thing[i], noSort)); } else { ret = {}; From e5739464cad03b8bbe047a6e3d5b64c9a3b03252 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 19 Aug 2008 17:28:02 -0700 Subject: [PATCH 0674/1860] Add some logging helpers to log4moz, written by myk --- services/sync/modules/log4moz.js | 40 +++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 13be270a12b9..1fe2f0f187a9 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -92,7 +92,45 @@ let Log4Moz = { get DumpAppender() { return DumpAppender; }, get ConsoleAppender() { return ConsoleAppender; }, get FileAppender() { return FileAppender; }, - get RotatingFileAppender() { return RotatingFileAppender; } + get RotatingFileAppender() { return RotatingFileAppender; }, + + // Logging helper: + // let logger = Log4Moz.Service.getLogger("foo"); + // logger.info(Log4Moz.enumerateInterfaces(someObject).join(",")); + enumerateInterfaces: function(aObject) { + let interfaces = []; + + for (i in Ci) { + try { + aObject.QueryInterface(Ci[i]); + interfaces.push(i); + } + catch(ex) {} + } + + return interfaces; + }, + + // Logging helper: + // let logger = Log4Moz.Service.getLogger("foo"); + // logger.info(Log4Moz.enumerateProperties(someObject).join(",")); + enumerateProperties: function(aObject, aExcludeComplexTypes) { + let properties = []; + + for (p in aObject) { + try { + if (aExcludeComplexTypes && + (typeof aObject[p] == "object" || typeof aObject[p] == "function")) + continue; + properties.push(p + " = " + aObject[p]); + } + catch(ex) { + properties.push(p + " = " + ex); + } + } + + return properties; + } }; From 79f2a8a3d5db0376dd56c38da0a43bc95a8c3e62 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 29 Sep 2008 13:17:49 -0700 Subject: [PATCH 0675/1860] add platform-specific binaries so they don't need to be built when making a fresh checkout --- services/sync/IWeaveCrypto.xpt | Bin 0 -> 529 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/sync/IWeaveCrypto.xpt diff --git a/services/sync/IWeaveCrypto.xpt b/services/sync/IWeaveCrypto.xpt new file mode 100644 index 0000000000000000000000000000000000000000..0962806b3443265ac2ff92deac91a390b442f80a GIT binary patch literal 529 zcmazDaQ64*3aKne^~p@)<&t7#VqjumU=n0tU{C_$$Oa$*1_vON8<1jzFc}!WxEVMb z#yNi8UfJ}L;o1LRKyhBEoK#-1XK-mjL4Hw5F@tA#YGPTcb5UhMNj^wBPXkcZL&gL) z1~vyE>osEokOdO^3KL^w1d9QM7<0~?4h$^zm6`P(6USyl+YV@C&6 zg)SpZtv#F-z|_D76HQ@8Hm{Toq8~2C0<@$9WYruXV=9QT1ISniV(bDk)`A#&8WMBT z^NTV|GIALZtnAdvg2c=sr%a%qk=dzv$soToq@+UV^whl6qQsI^Z-{yZ7(Xa6FC{-0 zD2gWH8HOg{R9TW*%ursGSP)#9o10ovl$i|DRhow( Date: Thu, 16 Oct 2008 12:17:42 -0700 Subject: [PATCH 0676/1860] remove eula screen, replace it with shorter text on the intro page of the wizard --- services/sync/locales/en-US/wizard.dtd | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index e5ad58575725..ce186b426435 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,17 +1,15 @@ - - - + - - - - - - - + + + + + + + From fbbf6b4c42a3edc80876638755b0156422cffbd9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 16 Oct 2008 12:29:21 -0700 Subject: [PATCH 0677/1860] Bug 443489: use the correct cookie xpcom interface --- services/sync/modules/engines/cookies.js | 4 ++-- services/sync/tests/unit/test_cookie_store.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 7f6d73fa9b5d..c344d97c42b2 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -203,7 +203,7 @@ CookieStore.prototype = { var matchingCookie = null; while (iter.hasMoreElements()){ let cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie ) ){ + if (cookie.QueryInterface( Ci.nsICookie2 ) ){ // see if host:path:name of cookie matches GUID given in command let key = cookie.host + ":" + cookie.path + ":" + cookie.name; if (key == command.GUID) { @@ -248,7 +248,7 @@ CookieStore.prototype = { var iter = this._cookieManager.enumerator; while (iter.hasMoreElements()) { var cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie )) { + if (cookie.QueryInterface( Ci.nsICookie2 )) { // String used to identify cookies is // host:path:name if ( cookie.isSession ) { diff --git a/services/sync/tests/unit/test_cookie_store.js b/services/sync/tests/unit/test_cookie_store.js index 4131ac29a4f1..691eb3817f56 100644 --- a/services/sync/tests/unit/test_cookie_store.js +++ b/services/sync/tests/unit/test_cookie_store.js @@ -18,7 +18,7 @@ FakeCookie.prototype = { }, QueryInterface: function( aIID ) { - if ( !aIID.equals( Components.interfaces.nsICookie ) ) { + if ( !aIID.equals( Components.interfaces.nsICookie2 ) ) { throw Components.results.NS_ERROR_NO_INTERFACE; } return this; From 0943d47ac7c50d1a5ae8578f05652d5caa820646 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 16 Oct 2008 12:30:30 -0700 Subject: [PATCH 0678/1860] disable bookmark sharing hooks --- services/sync/modules/engines/bookmarks.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d65a426e8a65..dbae49595606 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -755,13 +755,13 @@ BookmarksEngine.prototype = { /* After syncing the regular bookmark folder contents, * also update both the incoming and outgoing shared folders. */ let self = yield; - let ret = yield this._sharing.getNewShares(self.cb); + //let ret = yield this._sharing.getNewShares(self.cb); this.__proto__.__proto__._sync.async(this, self.cb ); yield; - this._sharing.updateAllOutgoingShares(self.cb); - yield; - this._sharing.updateAllIncomingShares(self.cb); - yield; + //this._sharing.updateAllOutgoingShares(self.cb); + //yield; + //this._sharing.updateAllIncomingShares(self.cb); + //yield; self.done(); }, From 43f9da58cbf27ec0dcbfffbbdec5cf6f9db38dc1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 16 Oct 2008 12:31:27 -0700 Subject: [PATCH 0679/1860] disable sharing test --- .../sync/tests/unit/{test_sharing.js => test_sharing.js.disabled} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename services/sync/tests/unit/{test_sharing.js => test_sharing.js.disabled} (100%) diff --git a/services/sync/tests/unit/test_sharing.js b/services/sync/tests/unit/test_sharing.js.disabled similarity index 100% rename from services/sync/tests/unit/test_sharing.js rename to services/sync/tests/unit/test_sharing.js.disabled From 7c9c3cff1839b81675ce8ab07cbece87b17b01ad Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 16 Oct 2008 12:32:40 -0700 Subject: [PATCH 0680/1860] bump version to 0.2.7 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 43d7163a2d03..bcc3530eca53 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,7 +43,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; -const WEAVE_VERSION = "0.2.6"; +const WEAVE_VERSION = "0.2.7"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From a1d15db228706de25035eac8f4da4c263cf508e4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 16 Oct 2008 12:40:54 -0700 Subject: [PATCH 0681/1860] fix terms link --- services/sync/locales/en-US/wizard.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index ce186b426435..46776bd5ba88 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -6,7 +6,7 @@ - + From 5899efcd87b2d7b948c3b51eda0c908c4b76b1de Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:36:29 -0800 Subject: [PATCH 0682/1860] updates to resource class, use a new Auth service with pluggable 'authenticator' objects --- services/sync/modules/auth.js | 98 ++++++++++ services/sync/modules/resource.js | 295 ++++++++++++++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 services/sync/modules/auth.js create mode 100644 services/sync/modules/resource.js diff --git a/services/sync/modules/auth.js b/services/sync/modules/auth.js new file mode 100644 index 000000000000..802d3d473673 --- /dev/null +++ b/services/sync/modules/auth.js @@ -0,0 +1,98 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Auth', 'BasicAuthenticator', 'NoOpAuthenticator']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/Observers.js"); +Cu.import("resource://weave/Preferences.js"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; + +Utils.lazy(this, 'Auth', AuthMgr); + +// XXX: the authenticator api will probably need to be changed to support +// other methods (digest, oauth, etc) + +function NoOpAuthenticator() {} +NoOpAuthenticator.prototype = { + onRequest: function NoOpAuth_onRequest(headers) { + return headers; + } +}; + +function BasicAuthenticator(identity) { + this._id = identity; +} +BasicAuthenticator.prototype = { + onRequest: function BasicAuth_onRequest(headers) { + headers['Authorization'] = 'Basic ' + + btoa(this._id.username + ':' + this._id.password); + return headers; + } +}; + +function AuthMgr() { + this._init(); +} +AuthMgr.prototype = { + defaultAuthenticator: null, + + _init: function AuthMgr__init() { + this._authenticators = {}; + this.defaultAuthenticator = new NoOpAuthenticator(); + }, + + registerAuthenticator: function AuthMgr_register(match, authenticator) { + this._authenticators[match] = authenticator; + }, + + lookupAuthenticator: function AuthMgr_lookup(uri) { + for (let match in this._authenticators) { + if (uri.match(match)) + return this._authenticators[match]; + } + return this.defaultAuthenticator; + } +}; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js new file mode 100644 index 000000000000..2d737da00619 --- /dev/null +++ b/services/sync/modules/resource.js @@ -0,0 +1,295 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Resource', 'JsonFilter']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/Observers.js"); +Cu.import("resource://weave/Preferences.js"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/auth.js"); + +Function.prototype.async = Async.sugar; + +function RequestException(resource, action, request) { + this._resource = resource; + this._action = action; + this._request = request; + this.location = Components.stack.caller; +} +RequestException.prototype = { + get resource() { return this._resource; }, + get action() { return this._action; }, + get request() { return this._request; }, + get status() { return this._request.status; }, + toString: function ReqEx_toString() { + return "Could not " + this._action + " resource " + this._resource.path + + " (" + this._request.status + ")"; + } +}; + +function Resource(uri, authenticator) { + this._init(uri, authenticator); +} +Resource.prototype = { + _logName: "Net.Resource", + + get authenticator() { + if (this._authenticator) + return this._authenticator; + else + return Auth.lookupAuthenticator(this.spec); + }, + set authenticator(value) { + this._authenticator = value; + }, + + get headers() { + return this.authenticator.onRequest(this._headers); + }, + set headers(value) { + this._headers = value; + }, + + get uri() { + return this._uri; + }, + set uri(value) { + this._dirty = true; + this._downloaded = false; + this._uri = value; + }, + + get spec() { + return this._uri.spec; + }, + set spec(value) { + this._dirty = true; + this._downloaded = false; + this._uri.spec = value; + }, + + get data() { + return this._data; + }, + set data(value) { + this._dirty = true; + this._data = value; + }, + + get lastRequest() { return this._lastRequest; }, + get downloaded() { return this._downloaded; }, + get dirty() { return this._dirty; }, + + pushFilter: function Res_pushFilter(filter) { + this._filters.push(filter); + }, + + popFilter: function Res_popFilter() { + return this._filters.pop(); + }, + + clearFilters: function Res_clearFilters() { + this._filters = []; + }, + + _init: function Res__init(uri, authenticator) { + if (typeof uri == 'string') + uri = Utils.makeURI(uri); + this._uri = uri; + this._authenticator = authenticator; + this._headers = {'Content-type': 'text/plain'}; + this._data = null; + this._downloaded = false; + this._dirty = false; + this._filters = []; + this._lastRequest = null; + this._log = Log4Moz.repository.getLogger(this._logName); + }, + + _createRequest: function Res__createRequest(op, onRequestFinished) { + let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + request.onload = onRequestFinished; + request.onerror = onRequestFinished; + request.onprogress = Utils.bind2(this, this._onProgress); + request.mozBackgroundRequest = true; + request.open(op, this.spec, true); + + let headers = this.headers; // avoid calling the authorizer more than once + for (let key in headers) { + if (key == 'Authorization') + this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); + else + this._log.trace("HTTP Header " + key + ": " + headers[key]); + request.setRequestHeader(key, headers[key]); + } + return request; + }, + + _onProgress: function Res__onProgress(event) { + this._lastProgress = Date.now(); + }, + + _setupTimeout: function Res__setupTimeout(request, callback) { + let _request = request; + let _callback = callback; + let onTimer = function() { + if (Date.now() - this._lastProgress > CONNECTION_TIMEOUT) { + this._log.warn("Connection timed out"); + _request.abort(); + _callback({target:{status:-1}}); + } + }; + let listener = new Utils.EventListener(onTimer); + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(listener, CONNECTION_TIMEOUT, + timer.TYPE_REPEATING_SLACK); + this._lastProgress = Date.now(); + return timer; + }, + + _request: function Res__request(action, data) { + let self = yield; + let listener, wait_timer; + let iter = 0; + + this._log.debug(action + " request for " + this.spec); + + if ("PUT" == action) { + for each (let filter in this._filters) { + data = yield filter.beforePUT.async(filter, self.cb, data); + } + } + + while (iter < Preferences.get(PREFS_BRANCH + "network.numRetries")) { + let cb = self.cb; // to avoid warning because we use it twice + let request = this._createRequest(action, cb); + let timeout_timer = this._setupTimeout(request, cb); + let event = yield request.send(data); + timeout_timer.cancel(); + this._lastRequest = event.target; + + if (action == "DELETE" && + Utils.checkStatus(this._lastRequest.status, null, [[200,300],404])) { + this._dirty = false; + this._data = null; + break; + + } else if (Utils.checkStatus(this._lastRequest.status)) { + this._log.debug(action + " request successful"); + this._dirty = false; + + if ("GET" == action) { + this._data = this._lastRequest.responseText; + let filters = this._filters.slice(); // reverse() mutates, so we copy + for each (let filter in filters.reverse()) { + this._data = yield filter.afterGET.async(filter, self.cb, this._data); + } + } + break; + + // FIXME: this should really check a variety of permanent errors, rather than just 404s + } else if (action == "GET" && this._lastRequest.status == 404) { + this._log.debug(action + " request failed (404)"); + throw new RequestException(this, action, this._lastRequest); + + } else { + // wait for a bit and try again + this._log.debug(action + " request failed, retrying..."); + listener = new Utils.EventListener(self.cb); + wait_timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + yield wait_timer.initWithCallback(listener, iter * iter * 1000, + wait_timer.TYPE_ONE_SHOT); + iter++; + } + } + + if (iter >= Preferences.get(PREFS_BRANCH + "network.numRetries")) { + this._log.debug(action + " request failed (too many errors)"); + throw new RequestException(this, action, this._lastRequest); + } + + self.done(this._data); + }, + + get: function Res_get(onComplete) { + this._request.async(this, onComplete, "GET"); + }, + + put: function Res_put(onComplete, data) { + if ("undefined" == typeof(data)) + data = this._data; + this._request.async(this, onComplete, "PUT", data); + }, + + delete: function Res_delete(onComplete) { + this._request.async(this, onComplete, "DELETE"); + } +}; + + +function JsonFilter() { + this._log = Log4Moz.repository.getLogger("Service.JsonFilter"); +} +JsonFilter.prototype = { + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return this._json; + }, + + beforePUT: function JsonFilter_beforePUT(data) { + let self = yield; + this._log.trace("Encoding data as JSON"); + Observers.notify(null, "weave:service:sync:status", "stats.encoding-json"); + self.done(this._json.encode(data)); + }, + + afterGET: function JsonFilter_afterGET(data) { + let self = yield; + this._log.trace("Decoding JSON data"); + Observers.notify(null, "weave:service:sync:status", "stats.decoding-json"); + self.done(this._json.decode(data)); + } +}; From 27bbba78c44a5909103bfc8fb5ad7f2fb0167089 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:37:51 -0800 Subject: [PATCH 0683/1860] add base record types for the new weave server api --- services/sync/modules/base_records/crypto.js | 148 ++++++++++++ services/sync/modules/base_records/keys.js | 241 +++++++++++++++++++ services/sync/modules/base_records/wbo.js | 61 +++++ 3 files changed, 450 insertions(+) create mode 100644 services/sync/modules/base_records/crypto.js create mode 100644 services/sync/modules/base_records/keys.js create mode 100644 services/sync/modules/base_records/wbo.js diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js new file mode 100644 index 000000000000..675ab8b181d6 --- /dev/null +++ b/services/sync/modules/base_records/crypto.js @@ -0,0 +1,148 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['CryptoWrapper']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/Observers.js"); +Cu.import("resource://weave/Preferences.js"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/keys.js"); + +Function.prototype.async = Async.sugar; + +function CryptoWrapper(uri, authenticator) { + this._CryptoWrap_init(uri, authenticator); +} +CryptoWrapper.prototype = { + __proto__: WBORecord.prototype, + _logName: "Record.CryptoWrapper", + + _CryptoWrap_init: function CryptoWrap_init(uri, authenticator) { + this._WBORec_init(uri, authenticator); + }, + + _encrypt: function CryptoWrap__encrypt(passphrase) { + let self = yield; + + let pubkey = yield PubKeys.getDefaultKey(self.cb); + let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); + + let meta = new CryptoMeta(this.data.encryption); // FIXME: cache! + yield meta.get(self.cb); + let symkey = meta.getKey(pubkey, privkey, passphrase); + + let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + createInstance(Ci.IWeaveCrypto); + this.data.payload = crypto.encrypt(this.cleartext, symkey, meta.iv); + + self.done(); + }, + encrypt: function CryptoWrap_encrypt(onComplete, passphrase) { + this._encrypt.async(this, onComplete, passphrase); + }, + + _decrypt: function CryptoWrap__decrypt(passphrase) { + let self = yield; + + let pubkey = yield PubKeys.getDefaultKey(self.cb); + let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); + + let meta = new CryptoMeta(this.data.encryption); // FIXME: cache! + yield meta.get(self.cb); + let symkey = meta.getKey(pubkey, privkey, passphrase); + + let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + createInstance(Ci.IWeaveCrypto); + this.cleartext = crypto.decrypt(this.data.payload, symkey, meta.iv); + + self.done(this.cleartext); + }, + decrypt: function CryptoWrap_decrypt(onComplete, passphrase) { + this._decrypt.async(this, onComplete, passphrase); + } +}; + +function CryptoMeta(uri, authenticator) { + this._CryptoMeta_init(uri, authenticator); +} +CryptoMeta.prototype = { + __proto__: WBORecord.prototype, + _logName: "Record.CryptoMeta", + + _CryptoMeta_init: function CryptoMeta_init(uri, authenticator) { + this._WBORec_init(uri, authenticator); + }, + + get salt() this.data.payload.salt, + set salt(value) { this.data.payload.salt = value; }, + + get iv() this.data.payload.iv, + set iv(value) { this.data.payload.iv = value; }, + + getKey: function getKey(pubKey, privKey, passphrase) { + let wrappedKey; + + // get the full uri to our public key + let pubKeyUri; + if (typeof pubKey == 'string') + pubKeyUri = this.uri.resolve(pubKey); + else + pubKeyUri = pubKey.spec; + + // each hash key is a relative uri, resolve those and match against ours + for (let relUri in this.data.payload.keyring) { + if (pubKeyUri == this.uri.resolve(relUri)) + wrappedKey = this.data.payload.keyring[relUri]; + } + if (!wrappedKey) + throw "keyring doesn't contain a key for " + pubKeyUrl; + + let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + createInstance(Ci.IWeaveCrypto); + return crypto.unwrapSymmetricKey(wrappedKey, privKey.keyData, + passphrase, this.salt, this.iv); + } +}; \ No newline at end of file diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js new file mode 100644 index 000000000000..193f234e947e --- /dev/null +++ b/services/sync/modules/base_records/keys.js @@ -0,0 +1,241 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['PubKey', 'PrivKey', + 'PubKeys', 'PrivKeys']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/Observers.js"); +Cu.import("resource://weave/Preferences.js"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/base_records/wbo.js"); + +Function.prototype.async = Async.sugar; + +Utils.lazy(this, 'PubKeys', PubKeyManager); +Utils.lazy(this, 'PrivKeys', PrivKeyManager); + +function PubKey(uri, authenticator) { + this._PubKey_init(uri, authenticator); +} +PubKey.prototype = { + __proto__: WBORecord.prototype, + + _PubKey_init: function PubKey_init(uri, authenticator) { + this._WBORec_init(uri, authenticator); + }, + + get keyData() { + if (!this.data) + return null; + return this.data.payload.key_data; + }, + + get privateKeyUri() { + if (!this.data) + return null; + // resolve + let uri = this.uri.resolve(this.data.payload.private_key); + if (uri) + return Utils.makeURI(uri); + // does not resolve, return raw (this uri type might not be able to resolve) + return Utils.makeURI(this.data.payload.private_key); + } +}; + +function PrivKey(uri, authenticator) { + this._PrivKey_init(uri, authenticator); +} +PrivKey.prototype = { + __proto__: WBORecord.prototype, + + _PrivKey_init: function PrivKey_init(uri, authenticator) { + this._WBORec_init(uri, authenticator); + }, + + get keyData() { + if (!this.data) + return null; + return this.data.payload.key_data; + }, + + get privateKeyUri() { + throw "attempted to get private key url from a private key!"; + }, + + get publicKeyUri() { + if (!this.data) + return null; + // resolve + let uri = this.uri.resolve(this.data.payload.public_key); + if (uri) + return Utils.makeURI(uri); + // does not resolve, return raw (this uri type might not be able to resolve) + return Utils.makeURI(this.data.payload.public_key); + } +}; + +function SymKey(keyData, wrapped) { + this._init(keyData, wrapped); +} +SymKey.prototype = { + get wrapped() { + return this._wrapped; + }, + + _init: function SymKey__init(keyData, wrapped) { + this._data = keyData; + this._wrapped = wrapped; + }, + + unwrap: function SymKey_unwrap(privkey, passphrase, meta_record) { + let svc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + createInstance(Ci.IWeaveCrypto); + this._data = svc.unwrapSymmetricKey(this._data, + privkey.keyData, + passphrase, + identity.passphraseSalt, + identity.privkeyWrapIV); + } +}; + +function PubKeyManager() { + this._init(); +} +PubKeyManager.prototype = { + _keyType: PubKey, + _logName: "PubKeyManager", + + get defaultKeyUrl() this._defaultKeyUrl, + set defaultKeyUrl(value) { this._defaultKeyUrl = value; }, + + _getDefaultKey: function KeyMgr__getDefaultKey() { + let self = yield; + let ret = yield this.get(self.cb, this.defaultKeyUrl); + self.done(ret); + }, + getDefaultKey: function KeyMgr_getDefaultKey(onComplete) { + return this._getDefaultKey.async(this, onComplete); + }, + + _init: function KeyMgr__init() { + this._log = Log4Moz.repository.getLogger(this._logName); + this._keys = {}; + this._aliases = {}; + }, + + _import: function KeyMgr__import(url) { + let self = yield; + let ret = null; + + this._log.trace("Importing key: " + (url.spec? url.spec : url)); + + try { + let key = new this._keyType(url); + let foo = yield key.get(self.cb); + this.set(url, key); + ret = key; + } catch (e) { + this._log.debug("Failed to import key: " + e); + // don't do anything else, we'll just return null + } + + self.done(ret); + }, + import: function KeyMgr_import(onComplete, url) { + this._import.async(this, onComplete, url); + }, + + _get: function KeyMgr__get(url) { + let self = yield; + + let key = null; + if (url in this._aliases) + url = this._aliases[url]; + if (url in this._keys) + key = this._keys[url]; + + if (!key) + key = yield this.import(self.cb, url); + + self.done(key); + }, + get: function KeyMgr_get(onComplete, url) { + this._get.async(this, onComplete, url); + }, + + set: function KeyMgr_set(url, key) { + this._keys[url] = key; + return key; + }, + + contains: function KeyMgr_contains(url) { + let key = null; + if (url in this._aliases) + url = this._aliases[url]; + if (url in this._keys) + return true; + return false; + }, + + del: function KeyMgr_del(url) { + delete this._keys[url]; + }, + getAlias: function KeyMgr_getAlias(alias) { + return this._aliases[alias]; + }, + setAlias: function KeyMgr_setAlias(url, alias) { + this._aliases[alias] = url; + }, + delAlias: function KeyMgr_delAlias(alias) { + delete this._aliases[alias]; + } +}; + +function PrivKeyManager() { this._init(); } +PrivKeyManager.prototype = { + __proto__: PubKeyManager.prototype, + _keyType: PrivKey, + _logName: "PrivKeyManager" +}; diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js new file mode 100644 index 000000000000..2715899022a7 --- /dev/null +++ b/services/sync/modules/base_records/wbo.js @@ -0,0 +1,61 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['WBORecord']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; + +function WBORecord(uri, authenticator) { + this._WBORec_init(uri, authenticator); +} +WBORecord.prototype = { + __proto__: Resource.prototype, + _logName: "Record.WBO", + + _WBORec_init: function WBORec_init(uri, authenticator) { + this._init(uri, authenticator); + this.pushFilter(new JsonFilter()); + } +}; From b096bede6da333573c60353f9b28132266a28e68 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:38:34 -0800 Subject: [PATCH 0684/1860] remove dav.js (gone) and remote.js (resources now in resource.js) --- services/sync/modules/dav.js | 493 ---------------------- services/sync/modules/remote.js | 724 -------------------------------- 2 files changed, 1217 deletions(-) delete mode 100644 services/sync/modules/dav.js delete mode 100644 services/sync/modules/remote.js diff --git a/services/sync/modules/dav.js b/services/sync/modules/dav.js deleted file mode 100644 index bec181c789c2..000000000000 --- a/services/sync/modules/dav.js +++ /dev/null @@ -1,493 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['DAV', 'DAVCollection']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; - -Utils.lazy(this, 'DAV', DAVCollection); - -let DAVLocks = {default: null}; - -/* - * DAV object - * Abstracts the raw DAV commands - */ - -function DAVCollection(baseURL, defaultPrefix) { - this.baseURL = baseURL; - this.defaultPrefix = defaultPrefix; - this._identity = 'DAV:default'; - this._log = Log4Moz.Service.getLogger("Service.DAV"); - this._log.level = - Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.dav")]; -} -DAVCollection.prototype = { - - __dp: null, - get _dp() { - if (!this.__dp) - this.__dp = Cc["@mozilla.org/xmlextras/domparser;1"]. - createInstance(Ci.nsIDOMParser); - return this.__dp; - }, - - get identity() { return this._identity; }, - set identity(value) { this._identity = value; }, - - get baseURL() { - return this._baseURL; - }, - set baseURL(value) { - if (value && value[value.length-1] != '/') - value = value + '/'; - this._baseURL = value; - }, - - get defaultPrefix() { - return this._defaultPrefix; - }, - set defaultPrefix(value) { - if (value && value[value.length-1] != '/') - value = value + '/'; - if (value && value[0] == '/') - value = value.slice(1); - if (!value) - value = ''; - this._defaultPrefix = value; - }, - - get locked() { - return !this._allowLock || (DAVLocks['default'] && - DAVLocks['default'].token); - }, - - _allowLock: true, - get allowLock() this._allowLock, - set allowLock(value) { - this._allowLock = value; - }, - - _makeRequest: function DC__makeRequest(op, path, headers, data) { - let self = yield; - let ret; - - this._log.debug(op + " request for " + (path? path : 'root folder')); - - if (!path || path[0] != '/') - path = this._defaultPrefix + path; // if relative: prepend default prefix - else - path = path.slice(1); // if absolute: remove leading slash - // path at this point should have no leading slash. - - if (this._lastProgress) - throw "Request already in progress"; - else - this._lastProgress = Date.now(); - - let xhrCb = self.cb; - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(Ci.nsIXMLHttpRequest); - - // check for stalled connections - let listener = new Utils.EventListener(this._timeoutCb(request, xhrCb)); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(listener, CONNECTION_TIMEOUT, - timer.TYPE_REPEATING_SLACK); - - request.onload = xhrCb; - request.onerror = xhrCb; - request.onprogress =Utils.bind2(this, this._onProgress); - request.mozBackgroundRequest = true; - request.open(op, this._baseURL + path, true); - - - // Force cache validation - let channel = request.channel; - channel = channel.QueryInterface(Ci.nsIRequest); - let loadFlags = channel.loadFlags; - loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; - channel.loadFlags = loadFlags; - - let key; - for (key in headers) { - if (key == 'Authorization') - this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); - else - this._log.trace("HTTP Header " + key + ": " + headers[key]); - request.setRequestHeader(key, headers[key]); - } - - let event = yield request.send(data); - - timer.cancel(); - this._lastProgress = null; - self.done(event.target); - }, - - _onProgress: function DC__onProgress(event) { - this._lastProgress = Date.now(); - }, - - _timeoutCb: function DC__timeoutCb(request, callback) { - return function() { - if (Date.now() - this._lastProgress > CONNECTION_TIMEOUT) { - this._log.warn("Connection timed out"); - request.abort(); - callback({target:{status:-1}}); - } - }; - }, - - get _defaultHeaders() { - let h = {'Content-type': 'text/plain'}, - id = ID.get(this.identity), - lock = DAVLocks['default']; - if (id) - h['Authorization'] = 'Basic ' + btoa(id.username + ":" + id.password); - if (lock) - h['If'] = "<" + lock.URL + "> (<" + lock.token + ">)"; - return h; - }, - - // mkdir -p - _mkcol: function DC__mkcol(path) { - let self = yield; - let ok = true; - - try { - let components = path.split('/'); - let path2 = ''; - - for (let i = 0; i < components.length; i++) { - - // trailing slashes will cause an empty path component at the end - if (components[i] == '') - continue; - - path2 = path2 + components[i]; - - // check if it exists first - this._makeRequest.async(this, self.cb, "GET", path2 + "/", this._defaultHeaders); - let ret = yield; - if (ret.status != 404) { - this._log.trace("Skipping creation of path " + path2 + - " (got status " + ret.status + ")"); - } else { - this._log.debug("Creating path: " + path2); - this._makeRequest.async(this, self.cb, "MKCOL", path2, - this._defaultHeaders); - ret = yield; - - if (ret.status != 201) { - this._log.debug(ret.responseText); - throw 'request failed: ' + ret.status; - } - } - - // add slash *after* the request, trailing slashes cause a 412! - path2 = path2 + "/"; - } - - } catch (e) { - this._log.error("Could not create directory on server"); - this._log.error("Exception caught: " + (e.message? e.message : e) + - " - " + (e.location? e.location : "")); - ok = false; - } - - self.done(ok); - }, - - GET: function DC_GET(path, onComplete) { - return this._makeRequest.async(this, onComplete, "GET", path, - this._defaultHeaders); - }, - - POST: function DC_POST(path, data, onComplete) { - return this._makeRequest.async(this, onComplete, "POST", path, - this._defaultHeaders, data); - }, - - formPost: function DC_formPOST(path, data, onComplete) { - let headers = {'Content-type': 'application/x-www-form-urlencoded'}; - headers.__proto__ = this._defaultHeaders; - - return this._makeRequest.async(this, onComplete, "POST", path, - headers, data); - }, - - PUT: function DC_PUT(path, data, onComplete) { - return this._makeRequest.async(this, onComplete, "PUT", path, - this._defaultHeaders, data); - }, - - DELETE: function DC_DELETE(path, onComplete) { - return this._makeRequest.async(this, onComplete, "DELETE", path, - this._defaultHeaders); - }, - - MKCOL: function DC_MKCOL(path, onComplete) { - return this._mkcol.async(this, onComplete, path); - }, - - PROPFIND: function DC_PROPFIND(path, data, onComplete) { - let headers = {'Content-type': 'text/xml; charset="utf-8"', - 'Depth': '0'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "PROPFIND", path, - headers, data); - }, - - LOCK: function DC_LOCK(path, data, onComplete) { - let headers = {'Content-type': 'text/xml; charset="utf-8"', - 'Depth': 'infinity', - 'Timeout': 'Second-600'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "LOCK", path, headers, data); - }, - - UNLOCK: function DC_UNLOCK(path, onComplete) { - let headers = {'Lock-Token': '<' + DAVLocks['default'].token + '>'}; - headers.__proto__ = this._defaultHeaders; - return this._makeRequest.async(this, onComplete, "UNLOCK", path, headers); - }, - - // Get all files - listFiles: function DC_listFiles(path) { - let self = yield; - - if (!path) - path = ""; - - let headers = {'Content-type': 'text/xml; charset="utf-8"', - 'Depth': '1'}; - headers.__proto__ = this._defaultHeaders; - - this._makeRequest.async(this, self.cb, "PROPFIND", path, headers, - "" + - ""); - let resp = yield; - Utils.ensureStatus(resp.status, "propfind failed"); - - let ret = []; - try { - let elts = Utils.xpath(resp.responseXML, '//D:href'); - // FIXME: shouldn't depend on the first one being the root - let root = elts.iterateNext(); - root = root.textContent; - let elt; - while (elt = elts.iterateNext()) - ret.push(elt.textContent.replace(root, '')); - } catch (e) {} - - self.done(ret); - }, - - // Login / Logout - - checkLogin: function DC_checkLogin(username, password) { - let self = yield; - - this._log.debug("checkLogin called for user " + username); - - let headers = { - 'Content-type' : 'text/plain', - 'Authorization' : 'Basic ' + btoa(username + ":" + password) - }; - let lock = DAVLocks['default']; - if (lock) - headers['If'] = "<" + lock.URL + "> (<" + lock.token + ">)"; - - // Make a call to make sure it's working - this._makeRequest.async(this, self.cb, "GET", "", headers); - let resp = yield; - - this._log.debug("checkLogin got response status " + resp.status); - self.done(resp.status); - }, - - // Locking - - _getActiveLock: function DC__getActiveLock() { - let self = yield; - let ret = null; - - this._log.debug("Getting active lock token"); - this.PROPFIND("lock", - "" + - "" + - " " + - "", self.cb); - let resp = yield; - - if (resp.status < 200 || resp.status >= 300) { - self.done(false); - yield; - } - - let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - if (token) - ret = token.textContent; - - if (ret) - this._log.trace("Found an active lock token"); - else - this._log.trace("No active lock token found"); - self.done({URL: this._baseURL, token: ret}); - }, - - lock: function DC_lock() { - let self = yield; - let resp; - - try { - this._log.trace("Acquiring lock"); - - if (this.locked) { - this._log.debug("Lock called, but we are already locked"); - return; - } - this._allowLock = false; - - resp = yield this.LOCK("lock", - "\n" + - "\n" + - " \n" + - " \n" + - "", self.cb); - if (!Utils.checkStatus(resp.status)) - return; - - let tokens = Utils.xpath(resp.responseXML, '//D:locktoken/D:href'); - let token = tokens.iterateNext(); - if (token) { - DAVLocks['default'] = { - URL: this._baseURL, - token: token.textContent - }; - } - - if (DAVLocks['default']) { - this._log.trace("Lock acquired"); - self.done(DAVLocks['default']); - } - - } catch (e) { - this._log.error("Could not acquire lock"); - if (resp.responseText) - this._log.error("Server response to LOCK:\n" + resp.responseText); - throw e; - - } finally { - this._allowLock = true; - } - }, - - unlock: function DC_unlock() { - let self = yield; - - this._log.trace("Releasing lock"); - - if (!this.locked) { - this._log.debug("Unlock called, but we don't hold a token right now"); - self.done(true); - return; - } - - try { - let resp = yield this.UNLOCK("lock", self.cb); - - if (Utils.checkStatus(resp.status)) { - this._log.trace("Lock released"); - self.done(true); - } else { - this._log.trace("Failed to release lock"); - self.done(false); - } - - } catch (e) { - throw e; - - } finally { - // Do this unconditionally, since code that calls unlock() doesn't - // really have much of an option if unlock fails. The only thing - // to do is wait for it to time out (and hope it didn't really - // fail) - if (DAVLocks['default']) - delete DAVLocks['default']; - } - }, - - forceUnlock: function DC_forceUnlock() { - let self = yield; - let unlocked = true; - - this._log.debug("Forcibly releasing any server locks"); - - this._getActiveLock.async(this, self.cb); - DAVLocks['default'] = yield; - - if (!DAVLocks['default']) { - this._log.debug("No server lock found"); - self.done(true); - yield; - } - - this._log.trace("Server lock found, unlocking"); - this.unlock.async(this, self.cb); - unlocked = yield; - - if (unlocked) - this._log.trace("Lock released"); - else - this._log.trace("No lock released"); - self.done(unlocked); - } -}; diff --git a/services/sync/modules/remote.js b/services/sync/modules/remote.js deleted file mode 100644 index b6f31d2156bc..000000000000 --- a/services/sync/modules/remote.js +++ /dev/null @@ -1,724 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['Resource', 'JsonFilter', 'CryptoFilter', - 'Keychain', 'RemoteStore']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/crypto.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/dav.js"); -Cu.import("resource://weave/stores.js"); - -Function.prototype.async = Async.sugar; - - -function RequestException(resource, action, request) { - this._resource = resource; - this._action = action; - this._request = request; - this.location = Components.stack.caller; -} -RequestException.prototype = { - get resource() { return this._resource; }, - get action() { return this._action; }, - get request() { return this._request; }, - get status() { return this._request.status; }, - toString: function ReqEx_toString() { - return "Could not " + this._action + " resource " + this._resource.path + - " (" + this._request.status + ")"; - } -}; - -function Resource(path) { - this._init(path); -} -Resource.prototype = { - get identity() { return this._identity; }, - set identity(value) { this._identity = value; }, - - get dav() { return this._dav; }, - set dav(value) { this._dav = value; }, - - get path() { return this._path; }, - set path(value) { - this._dirty = true; - this._path = value; - }, - - get data() { return this._data; }, - set data(value) { - this._dirty = true; - this._data = value; - }, - - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - get lastRequest() { return this._lastRequest; }, - get downloaded() { return this._downloaded; }, - get dirty() { return this._dirty; }, - - pushFilter: function Res_pushFilter(filter) { - this._filters.push(filter); - }, - - popFilter: function Res_popFilter() { - return this._filters.pop(); - }, - - clearFilters: function Res_clearFilters() { - this._filters = []; - }, - - _init: function Res__init(path) { - this._identity = null; // unused - this._dav = null; // unused - this._path = path; - this._data = null; - this._downloaded = false; - this._dirty = false; - this._filters = []; - this._lastRequest = null; - this._log = Log4Moz.Service.getLogger("Service.Resource"); - }, - - // note: this is unused, and it's not clear whether it's useful or not - _sync: function Res__sync() { - let self = yield; - let ret; - - // If we've set the locally stored value, upload it. If we - // haven't, and we haven't yet downloaded this resource, then get - // it. Otherwise do nothing (don't try to get it every time) - - if (this.dirty) { - this.put(self.cb); - ret = yield; - - } else if (!this.downloaded) { - this.get(self.cb); - ret = yield; - } - - self.done(ret); - }, - sync: function Res_sync(onComplete) { - this._sync.async(this, onComplete); - }, - - _request: function Res__request(action, data) { - let self = yield; - let listener, timer; - let iter = 0; - - if ("PUT" == action) { - for each (let filter in this._filters) { - data = yield filter.beforePUT.async(filter, self.cb, data); - } - } - - while (true) { - switch (action) { - case "GET": - DAV.GET(this.path, self.cb); - break; - case "PUT": - DAV.PUT(this.path, data, self.cb); - break; - case "DELETE": - DAV.DELETE(this.path, self.cb); - break; - default: - throw "Unknown request action for Resource"; - } - this._lastRequest = yield; - - if (action == "DELETE" && - Utils.checkStatus(this._lastRequest.status, null, [[200,300],404])) { - this._dirty = false; - this._data = null; - break; - - } else if (Utils.checkStatus(this._lastRequest.status)) { - this._log.debug(action + " request successful"); - this._dirty = false; - if (action == "GET") - this._data = this._lastRequest.responseText; - //else if (action == "PUT") - // this._data = data; // wrong! (because of filters) - break; - - } else if (action == "GET" && this._lastRequest.status == 404) { - throw new RequestException(this, action, this._lastRequest); - - } else if (iter >= 10) { - // iter too big? bail - throw new RequestException(this, action, this._lastRequest); - - } else { - // wait for a bit and try again - if (!timer) { - listener = new Utils.EventListener(self.cb); - timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - } - yield timer.initWithCallback(listener, iter * iter * 1000, - timer.TYPE_ONE_SHOT); - iter++; - } - } - - if ("GET" == action) { - let filters = this._filters.slice(); // reverse() mutates, so we copy - for each (let filter in filters.reverse()) { - this._data = yield filter.afterGET.async(filter, self.cb, this._data); - } - } - - self.done(this._data); - }, - - get: function Res_get(onComplete) { - this._request.async(this, onComplete, "GET"); - }, - - put: function Res_put(onComplete, data) { - if ("undefined" == typeof(data)) - data = this._data; - this._request.async(this, onComplete, "PUT", data); - }, - - delete: function Res_delete(onComplete) { - this._request.async(this, onComplete, "DELETE"); - } -}; - -function ResourceSet(basePath) { - this._init(basePath); -} -ResourceSet.prototype = { - __proto__: new Resource(), - _init: function ResSet__init(basePath) { - this.__proto__.__proto__._init.call(this); - this._basePath = basePath; - this._log = Log4Moz.Service.getLogger("Service.ResourceSet"); - }, - - _hack: function ResSet__hack(action, id, data) { - let self = yield; - let savedData = this._data; - - if ("PUT" == action) - this._data = data; - - this._path = this._basePath + id; - yield this._request.async(this, self.cb, action, data); - - let newData = this._data; - this._data = savedData; - if (this._data == null) - this._data = {}; - this._data[id] = newData; - - self.done(this._data[id]); - }, - - get: function ResSet_get(onComplete, id) { - this._hack.async(this, onComplete, "GET", id); - }, - - put: function ResSet_put(onComplete, id, data) { - this._hack.async(this, onComplete, "PUT", id, data); - }, - - delete: function ResSet_delete(onComplete, id) { - this._hack.async(this, onComplete, "DELETE", id); - } -}; - -function ResourceFilter() { - this._log = Log4Moz.Service.getLogger("Service.ResourceFilter"); -} -ResourceFilter.prototype = { - beforePUT: function ResFilter_beforePUT(data) { - let self = yield; - this._log.debug("Doing absolutely nothing") - self.done(data); - }, - afterGET: function ResFilter_afterGET(data) { - let self = yield; - this._log.debug("Doing absolutely nothing") - self.done(data); - } -}; - -function JsonFilter() { - this._log = Log4Moz.Service.getLogger("Service.JsonFilter"); -} -JsonFilter.prototype = { - __proto__: new ResourceFilter(), - - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return this._json; - }, - - beforePUT: function JsonFilter_beforePUT(data) { - let self = yield; - this._log.debug("Encoding data as JSON"); - this._os.notifyObservers(null, "weave:service:sync:status", "stats.encoding-json"); - self.done(this._json.encode(data)); - }, - - afterGET: function JsonFilter_afterGET(data) { - let self = yield; - this._log.debug("Decoding JSON data"); - this._os.notifyObservers(null, "weave:service:sync:status", "stats.decoding-json"); - self.done(this._json.decode(data)); - } -}; - -function CryptoFilter(identity) { - this._identity = identity; - this._log = Log4Moz.Service.getLogger("Service.CryptoFilter"); -} -CryptoFilter.prototype = { - __proto__: new ResourceFilter(), - - get _os() { - let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - this.__defineGetter__("_os", function() os); - return os; - }, - - beforePUT: function CryptoFilter_beforePUT(data) { - let self = yield; - this._log.debug("Encrypting data"); - this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting"); - let ret = yield Crypto.encryptData.async(Crypto, self.cb, data, this._identity); - self.done(ret); - }, - - afterGET: function CryptoFilter_afterGET(data) { - let self = yield; - this._log.debug("Decrypting data"); - this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting"); - let ret = yield Crypto.decryptData.async(Crypto, self.cb, data, this._identity); - self.done(ret); - } -}; - -function Keychain(prefix) { - this._init(prefix); -} -Keychain.prototype = { - __proto__: new Resource(), - _init: function Keychain__init(prefix) { - this.__proto__.__proto__._init.call(this, prefix + "keys.json"); - this.pushFilter(new JsonFilter()); - }, - _initialize: function Keychain__initialize(identity) { - let self = yield; - let wrappedSymkey; - - if ("none" != Utils.prefs.getCharPref("encryption")) { - this._os.notifyObservers(null, "weave:service:sync:status", "status.generating-random-key"); - - yield Crypto.randomKeyGen.async(Crypto, self.cb, identity); - - // Wrap (encrypt) this key with the user's public key. - let idRSA = ID.get('WeaveCryptoID'); - this._os.notifyObservers(null, "weave:service:sync:status", "status.encrypting-key"); - wrappedSymkey = yield Crypto.wrapKey.async(Crypto, self.cb, - identity.bulkKey, idRSA); - } - - let keys = {ring: {}, bulkIV: identity.bulkIV}; - this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-key"); - keys.ring[identity.username] = wrappedSymkey; - yield this.put(self.cb, keys); - }, - initialize: function Keychain_initialize(onComplete, identity) { - this._initialize.async(this, onComplete, identity); - }, - _getKeyAndIV: function Keychain__getKeyAndIV(identity) { - let self = yield; - - this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-keyring"); - - yield this.get(self.cb); - - if (!this.data || !this.data.ring || !this.data.ring[identity.username]) - throw "Keyring does not contain a key for this user"; - - // Unwrap (decrypt) the key with the user's private key. - this._os.notifyObservers(null, "weave:service:sync:status", "status.decrypting-key"); - let idRSA = ID.get('WeaveCryptoID'); - let symkey = yield Crypto.unwrapKey.async(Crypto, self.cb, - this.data.ring[identity.username], idRSA); - let iv = this.data.bulkIV; - - identity.bulkKey = symkey; - identity.bulkIV = iv; - }, - _setKey: function KeyChain__setKey(bulkID, newID) { - /* FIXME!: It's possible that the keyring is changed on the server - after we do a GET. Then we're just uploading this new local keyring, - thereby losing any changes made on the server keyring since this GET. - - Also, if this.data was not instantiated properly (i.e. you're - using KeyChain directly instead of getting it from the engine), - you run the risk of wiping the server-side keychain. - */ - let self = yield; - - this.get(self.cb); - yield; - - let wrappedKey = yield Crypto.wrapKey.async(Crypto, self.cb, - bulkID.bulkKey, newID); - this.data.ring[newID.username] = wrappedKey; - this.put(self.cb, this.data); - yield; - }, - getKeyAndIV: function Keychain_getKeyAndIV(onComplete, identity) { - this._getKeyAndIV.async(this, onComplete, identity); - }, - setKey: function Keychain_setKey(onComplete, bulkID, newID) { - this._setKey.async(this, onComplete, bulkID, newID); - } -}; - -function RemoteStore(engine) { - this._engine = engine; - this._log = Log4Moz.Service.getLogger("Service.RemoteStore"); -} -RemoteStore.prototype = { - get serverPrefix() this._engine.serverPrefix, - get engineId() this._engine.engineId, - - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - get status() { - let status = new Resource(this.serverPrefix + "status.json"); - status.pushFilter(new JsonFilter()); - this.__defineGetter__("status", function() status); - return status; - }, - - get keys() { - let keys = new Keychain(this.serverPrefix); - this.__defineGetter__("keys", function() keys); - return keys; - }, - - get _snapshot() { - let snapshot = new Resource(this.serverPrefix + "snapshot.json"); - snapshot.pushFilter(new JsonFilter()); - snapshot.pushFilter(new CryptoFilter(this._engine.engineId)); - this.__defineGetter__("_snapshot", function() snapshot); - return snapshot; - }, - - get _deltas() { - let deltas = new ResourceSet(this.serverPrefix + "deltas/"); - deltas.pushFilter(new JsonFilter()); - deltas.pushFilter(new CryptoFilter(this._engine.engineId)); - this.__defineGetter__("_deltas", function() deltas); - return deltas; - }, - - _openSession: function RStore__openSession(lastSyncSnap) { - let self = yield; - - if (!this.serverPrefix || !this.engineId) - throw "Cannot initialize RemoteStore: engine has no server prefix or crypto ID"; - - this.status.data = null; - this.keys.data = null; - this._snapshot.data = null; - this._deltas.data = null; - this._lastSyncSnap = lastSyncSnap; - - let ret = yield DAV.MKCOL(this.serverPrefix + "deltas", self.cb); - if (!ret) - throw "Could not create remote folder"; - - this._log.debug("Downloading status file"); - this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-status"); - - yield this.status.get(self.cb); - this._log.debug("Downloading status file... done"); - - // Bail out if the server has a newer format version than we can parse - if (this.status.data.formatVersion > ENGINE_STORAGE_FORMAT_VERSION) { - this._log.error("Server uses storage format v" + - this.status.data.formatVersion + - ", this client understands up to v" + - ENGINE_STORAGE_FORMAT_VERSION); - throw "Incompatible remote store format"; - } - - if (this.status.data.GUID != lastSyncSnap.GUID) { - this._log.trace("Remote GUID: " + this.status.data.GUID); - this._log.trace("Local GUID: " + lastSyncSnap.GUID); - this._log.debug("Server wipe since last sync, resetting last sync snapshot"); - lastSyncSnap.wipe(); - lastSyncSnap.GUID = this.status.data.GUID; - // yield this._store.resetGUIDs(self.cb); // XXX not sure if this is really needed (and it needs to be done from the engine if so) - } - - this._log.info("Last sync snapshot version: " + lastSyncSnap.version); - this._log.info("Server maxVersion: " + this.status.data.maxVersion); - - if ("none" != Utils.prefs.getCharPref("encryption")) - yield this.keys.getKeyAndIV(self.cb, this.engineId); - }, - openSession: function RStore_openSession(onComplete, lastSyncSnap) { - this._openSession.async(this, onComplete, lastSyncSnap); - }, - - closeSession: function RStore_closeSession() { - this.status.data = null; - this.keys.data = null; - this._snapshot.data = null; - this._deltas.data = null; - this._lastSyncSnap = null; - }, - - // Does a fresh upload of the given snapshot to a new store - // FIXME: add 'metadata' arg here like appendDelta's - _initialize: function RStore__initialize(snapshot) { - let self = yield; - - yield this.keys.initialize(self.cb, this.engineId); - this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-snapshot"); - yield this._snapshot.put(self.cb, snapshot.data); - - let c = 0; - for (GUID in snapshot.data) - c++; - - this._os.notifyObservers(null, "weave:service:sync:status", "status.uploading-status"); - yield this.status.put(self.cb, - {GUID: snapshot.GUID, - formatVersion: ENGINE_STORAGE_FORMAT_VERSION, - snapVersion: snapshot.version, - maxVersion: snapshot.version, - snapEncryption: Crypto.defaultAlgorithm, - deltasEncryption: Crypto.defaultAlgorithm, - itemCount: c}); - this._log.info("Full upload to server successful"); - }, - initialize: function RStore_initialize(onComplete, snapshot) { - this._initialize.async(this, onComplete, snapshot); - }, - - // Removes server files - you may want to run initialize() after this - // FIXME: might want to do a PROPFIND instead (to catch all deltas in one go) - _wipe: function Engine__wipe() { - let self = yield; - this._log.debug("Deleting remote store data"); - yield this.status.delete(self.cb); - yield this.keys.delete(self.cb); - yield this._snapshot.delete(self.cb); - //yield this._deltas.delete(self.cb); - this._log.debug("Server files deleted"); - }, - wipe: function Engine_wipe(onComplete) { - this._wipe.async(this, onComplete) - }, - - // Gets the latest server snapshot by downloading all server files - // (snapshot + deltas) - _getLatestFromScratch: function RStore__getLatestFromScratch() { - let self = yield; - - this._log.info("Downloading all server data from scratch"); - this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-snapshot"); - - let snap = new SnapshotStore(); - snap.data = yield this._snapshot.get(self.cb); - - this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); - let status = this.status.data; - for (let id = status.snapVersion + 1; id <= status.maxVersion; id++) { - let delta = yield this._deltas.get(self.cb, id); - yield snap.applyCommands.async(snap, self.cb, delta); - } - - self.done(snap.data); - }, - - // Gets the latest server snapshot by downloading only the necessary - // deltas from the given snapshot (but may fall back to a full download) - _getLatestFromSnap: function RStore__getLatestFromSnap() { - let self = yield; - let deltas, snap = new SnapshotStore(); - snap.version = this.status.data.maxVersion; - - if (!this._lastSyncSnap || - this._lastSyncSnap.version < this.status.data.snapVersion) { - this._log.trace("Getting latest from scratch (last sync snap too old)"); - snap.data = yield this._getLatestFromScratch.async(this, self.cb); - self.done(snap.data); - return; - - } else if (this._lastSyncSnap.version >= this.status.data.snapVersion && - this._lastSyncSnap.version < this.status.data.maxVersion) { - this._log.debug("Using last sync snapshot as starting point for server snapshot"); - snap.data = Utils.deepCopy(this._lastSyncSnap.data); - this._log.info("Downloading server deltas"); - this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); - deltas = []; - let min = this._lastSyncSnap.version + 1; - let max = this.status.data.maxVersion; - for (let id = min; id <= max; id++) { - let delta = yield this._deltas.get(self.cb, id); - deltas.push(delta); - } - - } else if (this._lastSyncSnap.version == this.status.data.maxVersion) { - this._log.debug("Using last sync snapshot as server snapshot (snap version == max version)"); - this._log.trace("Local snapshot version == server maxVersion"); - snap.data = Utils.deepCopy(this._lastSyncSnap.data); - deltas = []; - - } else { // this._lastSyncSnap.version > this.status.data.maxVersion - this._log.error("Server snapshot is older than local snapshot"); - throw "Server snapshot is older than local snapshot"; - } - - try { - for (var i = 0; i < deltas.length; i++) { - yield snap.applyCommands.async(snap, self.cb, deltas[i]); - } - } catch (e) { - this._log.warn("Error applying remote deltas to saved snapshot, attempting a full download"); - this._log.debug("Exception: " + Utils.exceptionStr(e)); - this._log.trace("Stack:\n" + Utils.stackTrace(e)); - snap.data = yield this._getLatestFromScratch.async(this, self.cb); - } - - self.done(snap.data); - }, - - // get the latest server snapshot. If a snapshot is given, try to - // download only the necessary deltas to get to the latest - _wrap: function RStore__wrap() { - let self = yield; - let ret = yield this._getLatestFromSnap.async(this, self.cb); - self.done(ret); - }, - wrap: function RStore_wrap(onComplete) { - this._wrap.async(this, onComplete); - }, - - // Adds a new set of changes (a delta) to this store - _appendDelta: function RStore__appendDelta(snapshot, delta, metadata) { - let self = yield; - - if (metadata) { - for (let key in metadata) - this.status.data[key] = metadata[key]; - } - - let c = 0; - for (item in snapshot.data) - c++; - this.status.data.itemCount = c; - - let id = ++this.status.data.maxVersion; - - // upload the delta even if we upload a new snapshot, so other clients - // can be spared of a full re-download - this._os.notifyObservers(null, "weave:service:sync:status", - "status.uploading-deltas"); - yield this._deltas.put(self.cb, id, delta); - - // if we have more than KEEP_DELTAS, then upload a new snapshot - // this allows us to remove old deltas - if ((id - this.status.data.snapVersion) > KEEP_DELTAS) { - this._os.notifyObservers(null, "weave:service:sync:status", - "status.uploading-snapshot"); - yield this._snapshot.put(self.cb, snapshot.data); - this.status.data.snapVersion = id; - } - - // XXX we could define another constant here - // (e.g. KEEP_MAX_DELTAS) to define when to actually start - // deleting deltas from the server. However, we can do this more - // efficiently server-side - - // finally, upload a new status file - this._os.notifyObservers(null, "weave:service:sync:status", - "status.uploading-status"); - yield this.status.put(self.cb); - }, - appendDelta: function RStore_appendDelta(onComplete, snapshot, delta, metadata) { - this._appendDelta.async(this, onComplete, snapshot, delta, metadata); - } -}; From a430e54c0c6f2de8cc837434adb979fefd3be294 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:39:40 -0800 Subject: [PATCH 0685/1860] commit platform-dependent binaries so that it's easier to get started to hack on weave --- services/sync/IWeaveCrypto.xpt | Bin 0 -> 529 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/sync/IWeaveCrypto.xpt diff --git a/services/sync/IWeaveCrypto.xpt b/services/sync/IWeaveCrypto.xpt new file mode 100644 index 0000000000000000000000000000000000000000..0962806b3443265ac2ff92deac91a390b442f80a GIT binary patch literal 529 zcmazDaQ64*3aKne^~p@)<&t7#VqjumU=n0tU{C_$$Oa$*1_vON8<1jzFc}!WxEVMb z#yNi8UfJ}L;o1LRKyhBEoK#-1XK-mjL4Hw5F@tA#YGPTcb5UhMNj^wBPXkcZL&gL) z1~vyE>osEokOdO^3KL^w1d9QM7<0~?4h$^zm6`P(6USyl+YV@C&6 zg)SpZtv#F-z|_D76HQ@8Hm{Toq8~2C0<@$9WYruXV=9QT1ISniV(bDk)`A#&8WMBT z^NTV|GIALZtnAdvg2c=sr%a%qk=dzv$soToq@+UV^whl6qQsI^Z-{yZ7(Xa6FC{-0 zD2gWH8HOg{R9TW*%ursGSP)#9o10ovl$i|DRhow( Date: Mon, 3 Nov 2008 14:40:09 -0800 Subject: [PATCH 0686/1860] add prefs js module --- services/sync/modules/Preferences.js | 153 +++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 services/sync/modules/Preferences.js diff --git a/services/sync/modules/Preferences.js b/services/sync/modules/Preferences.js new file mode 100644 index 000000000000..40c8f91c8111 --- /dev/null +++ b/services/sync/modules/Preferences.js @@ -0,0 +1,153 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Preferences. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez + * Daniel Aquino + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let EXPORTED_SYMBOLS = ["Preferences"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +function Preferences(prefBranch) { + if (prefBranch) + this._prefBranch = prefBranch; +} + +Preferences.prototype = { + _prefBranch: "", + + // Preferences Service + + get _prefSvc() { + let prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(this._prefBranch). + QueryInterface(Ci.nsIPrefBranch2); + this.__defineGetter__("_prefSvc", function() prefSvc); + return this._prefSvc; + }, + + /** + * Get the value of a pref, if any; otherwise return the default value. + * + * @param prefName the name of the pref to get + * @param defaultValue the default value, if any + * + * @returns the value of the pref, if any; otherwise the default value + */ + get: function(prefName, defaultValue) { + // We can't check for |prefName.constructor == Array| here, since we have + // a different global object, so we check the constructor name instead. + if (typeof prefName == "object" && prefName.constructor.name == Array.name) + return prefName.map(function(v) this.get(v), this); + + switch (this._prefSvc.getPrefType(prefName)) { + case Ci.nsIPrefBranch.PREF_STRING: + return this._prefSvc.getCharPref(prefName); + + case Ci.nsIPrefBranch.PREF_INT: + return this._prefSvc.getIntPref(prefName); + + case Ci.nsIPrefBranch.PREF_BOOL: + return this._prefSvc.getBoolPref(prefName); + + case Ci.nsIPrefBranch.PREF_INVALID: + default: + return defaultValue; + } + }, + + set: function(prefName, prefValue) { + // We can't check for |prefName.constructor == Object| here, since we have + // a different global object, so we check the constructor name instead. + if (typeof prefName == "object" && prefName.constructor.name == Object.name) + for (let [name, value] in Iterator(prefName)) + this.set(name, value); + else { + switch (typeof prefValue) { + case "number": + this._prefSvc.setIntPref(prefName, prefValue); + break; + + case "boolean": + this._prefSvc.setBoolPref(prefName, prefValue); + break; + + case "string": + default: + this._prefSvc.setCharPref(prefName, prefValue); + break; + } + } + }, + + // FIXME: make the methods below accept an array of pref names. + + has: function(prefName) { + return (this._prefSvc.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID); + }, + + modified: function(prefName) { + return (this.has(prefName) && this._prefSvc.prefHasUserValue(prefName)); + }, + + locked: function(prefName) { + return this._prefSvc.isLocked(prefName); + }, + + lock: function(prefName) { + this._prefSvc.lockPref(prefName); + }, + + unlock: function(prefName) { + this._prefSvc.unlockPref(prefName); + }, + + reset: function(prefName) { + this._prefSvc.clearUserPref(prefName); + }, + + resetBranch: function(prefBranch) { + this._prefSvc.resetBranch(prefBranch); + } + +}; + +// Give the constructor the same prototype as its instances, so users can access +// preferences directly via the constructor without having to create an instance +// first. +Preferences.__proto__ = Preferences.prototype; From d922f60dd2bd65eb7d550842f009314ab612ed73 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:40:28 -0800 Subject: [PATCH 0687/1860] Update Observers js module to latest version --- services/sync/modules/Observers.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/Observers.js b/services/sync/modules/Observers.js index 947b986ded51..d557576e45eb 100644 --- a/services/sync/modules/Observers.js +++ b/services/sync/modules/Observers.js @@ -56,8 +56,10 @@ let Observers = { remove: function(callback, topic) { let observer = Observers._observers[topic][callback]; - Observers._service.removeObserver(observer, topic); - delete this._observers[topic][callback]; + if (observer) { + Observers._service.removeObserver(observer, topic); + delete this._observers[topic][callback]; + } }, notify: function(subject, topic, data) { @@ -67,6 +69,8 @@ let Observers = { _service: Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService), + // Observers indexed by callback. This lets us get the observer + // to remove when a caller calls |remove|, passing it a callback. _observers: {} }; @@ -81,10 +85,12 @@ Observer.prototype = { // Pass the wrappedJSObject for subjects that have one. Otherwise pass // the subject itself. This way we support both wrapped subjects created // using this module and those that are real XPCOM components. - if (subject && subject.wrappedJSObject) - this._callback(subject.wrappedJSObject, topic, data); + let unwrappedSubject = subject.wrappedJSObject || subject; + + if (typeof this._callback == "function") + this._callback(unwrappedSubject, topic, data); else - this._callback(subject, topic, data); + this._callback.observe(unwrappedSubject, topic, data); } } From 79895a713762b734c1f9a0939a321ffa6295a17c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:41:39 -0800 Subject: [PATCH 0688/1860] add http js server for unit tests; add tests for resources, auth manager, base weave record types (WBOs, keys, crypto wrappers/crypto meta) --- services/sync/tests/unit/Makefile | 3 +- services/sync/tests/unit/head_first.js | 5 +- services/sync/tests/unit/head_http_server.js | 25 +++++ services/sync/tests/unit/test_auth_manager.js | 59 ++++++++++ .../sync/tests/unit/test_fault_tolerance.js | 4 +- services/sync/tests/unit/test_log4moz.js | 2 +- .../sync/tests/unit/test_records_crypto.js | 105 ++++++++++++++++++ services/sync/tests/unit/test_records_keys.js | 64 +++++++++++ services/sync/tests/unit/test_records_wbo.js | 44 ++++++++ services/sync/tests/unit/test_resource.js | 104 +++++++++++++++++ 10 files changed, 408 insertions(+), 7 deletions(-) create mode 100644 services/sync/tests/unit/head_http_server.js create mode 100644 services/sync/tests/unit/test_auth_manager.js create mode 100644 services/sync/tests/unit/test_records_crypto.js create mode 100644 services/sync/tests/unit/test_records_keys.js create mode 100644 services/sync/tests/unit/test_records_wbo.js create mode 100644 services/sync/tests/unit/test_resource.js diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile index 386b97952ace..77ff6d1ef587 100644 --- a/services/sync/tests/unit/Makefile +++ b/services/sync/tests/unit/Makefile @@ -1,2 +1 @@ -all: - ${MAKE} -f ../harness/Makefile +include ../harness/Makefile diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 1a249564eb8e..cee5b43e63b2 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -32,6 +32,7 @@ let provider = { ds.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider); do_bind_resource(do_get_file("modules"), "weave"); +do_bind_resource(do_get_file("tests"), "tests"); function loadInSandbox(aUri) { var sandbox = Components.utils.Sandbox(this); @@ -74,7 +75,7 @@ function FakeTimerService() { }; function getTestLogger(component) { - return Log4Moz.Service.getLogger("Testing"); + return Log4Moz.repository.getLogger("Testing"); } function initTestLogging(level) { @@ -93,7 +94,7 @@ function initTestLogging(level) { }; LogStats.prototype.__proto__ = new Log4Moz.Formatter(); - var log = Log4Moz.Service.rootLogger; + var log = Log4Moz.repository.rootLogger; var logStats = new LogStats(); var appender = new Log4Moz.DumpAppender(logStats); diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js new file mode 100644 index 000000000000..bf56be42e15f --- /dev/null +++ b/services/sync/tests/unit/head_http_server.js @@ -0,0 +1,25 @@ +let Httpd = {}; +Cu.import("resource://tests/lib/httpd.js", Httpd); + +function httpd_setup (handlers) { + let server = new Httpd.nsHttpServer(); + for (let path in handlers) { + server.registerPathHandler(path, handlers[path]); + } + server.start(8080); + return server; +} + +function httpd_basic_auth_handler(body, metadata, response) { + // no btoa() in xpcshell. it's guest:guest + if (metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == "Basic Z3Vlc3Q6Z3Vlc3Q=") { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + } else { + body = "This path exists and is protected - failed"; + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + } + response.bodyOutputStream.write(body, body.length); +} diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js new file mode 100644 index 000000000000..0059a9cefa9e --- /dev/null +++ b/services/sync/tests/unit/test_auth_manager.js @@ -0,0 +1,59 @@ +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/auth.js"); +Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/resource.js"); + +Function.prototype.async = Async.sugar; + +let logger; +let Httpd = {}; +Cu.import("resource://tests/lib/httpd.js", Httpd); + +function server_handler(metadata, response) { + let body; + + // no btoa() in xpcshell. it's guest:guest + if (metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == "Basic Z3Vlc3Q6Z3Vlc3Q=") { + body = "This path exists and is protected"; + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + } else { + body = "This path exists and is protected - failed"; + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + } + + response.bodyOutputStream.write(body, body.length); +} + +function async_test() { + let self = yield; + + logger = Log4Moz.repository.getLogger('Test'); + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + + let server = new Httpd.nsHttpServer(); + server.registerPathHandler("/foo", server_handler); + server.start(8080); + + let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); + Auth.defaultAuthenticator = auth; + + let res = new Resource("http://localhost:8080/foo"); // no authenticator + let content = yield res.get(self.cb); + do_check_eq(content, "This path exists and is protected"); + do_check_eq(res.lastRequest.status, 200); + + do_test_finished(); + server.stop(); + self.done(); +} + +function run_test() { + async_test.async(this); + do_test_pending(); +} diff --git a/services/sync/tests/unit/test_fault_tolerance.js b/services/sync/tests/unit/test_fault_tolerance.js index e016a9630afa..8cdb62b444ea 100644 --- a/services/sync/tests/unit/test_fault_tolerance.js +++ b/services/sync/tests/unit/test_fault_tolerance.js @@ -6,8 +6,8 @@ function run_test() { FaultTolerance.Service._testProperty = "hi"; do_check_eq(FaultTolerance.Service._testProperty, "hi"); - var log = Log4Moz.Service.rootLogger; + var log = Log4Moz.repository.rootLogger; log.level = Log4Moz.Level.All; log.info("Testing."); - do_check_eq(Log4Moz.Service.rootLogger.appenders.length, 1); + do_check_eq(Log4Moz.repository.rootLogger.appenders.length, 1); } diff --git a/services/sync/tests/unit/test_log4moz.js b/services/sync/tests/unit/test_log4moz.js index ee4072ef4a55..43302bcd2dd2 100644 --- a/services/sync/tests/unit/test_log4moz.js +++ b/services/sync/tests/unit/test_log4moz.js @@ -12,7 +12,7 @@ MockAppender.prototype = { MockAppender.prototype.__proto__ = new Log4Moz.Appender(); function run_test() { - var log = Log4Moz.Service.rootLogger; + var log = Log4Moz.repository.rootLogger; var appender = new MockAppender(new Log4Moz.BasicFormatter()); log.level = Log4Moz.Level.Debug; diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js new file mode 100644 index 000000000000..78c1d1b08c30 --- /dev/null +++ b/services/sync/tests/unit/test_records_crypto.js @@ -0,0 +1,105 @@ +try { + Cu.import("resource://weave/log4moz.js"); + Cu.import("resource://weave/util.js"); + Cu.import("resource://weave/async.js"); + Cu.import("resource://weave/auth.js"); + Cu.import("resource://weave/identity.js"); + Cu.import("resource://weave/base_records/keys.js"); + Cu.import("resource://weave/base_records/crypto.js"); +} catch (e) { + do_throw(e); +} +Function.prototype.async = Async.sugar; + +let jsonSvc, cryptoSvc, salt, iv, symKey, wrappedKey, pubKey, privKey; + +function pubkey_handler(metadata, response) { + let obj = {modified: "2454725.98283", + payload: {type: "pubkey", + private_key: "http://localhost:8080/privkey", + key_data: pubKey}}; + return httpd_basic_auth_handler(jsonSvc.encode(obj), metadata, response); +} + +function privkey_handler(metadata, response) { + let obj = {modified: "2454725.98283", + payload: {type: "privkey", + public_key: "http://localhost:8080/pubkey", + key_data: privKey}}; + return httpd_basic_auth_handler(jsonSvc.encode(obj), metadata, response); +} + +function crypted_resource_handler(metadata, response) { + let obj = {modified: "2454725.98283", + encryption: "http://localhost:8080/crypto-meta", + payload: cryptoSvc.encrypt("my payload here", symKey, iv)}; + return httpd_basic_auth_handler(jsonSvc.encode(obj), metadata, response); +} + +function crypto_meta_handler(metadata, response) { + let obj = {modified: "2454725.98283", + payload: {type: "crypto-meta", + salt: salt, + iv: iv, + keyring: { + "pubkey": wrappedKey + }}}; + return httpd_basic_auth_handler(jsonSvc.encode(obj), metadata, response); +} + +function async_test() { + let self = yield; + let server; + + try { + let log = Log4Moz.repository.getLogger(); + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + + jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + + let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); + Auth.defaultAuthenticator = auth; + PubKeys.defaultKeyUrl = "http://localhost:8080/pubkey"; + + server = httpd_setup({"/pubkey": pubkey_handler, + "/privkey": privkey_handler, + "/crypted-resource": crypted_resource_handler, + "/crypto-meta": crypto_meta_handler}); + + salt = cryptoSvc.generateRandomBytes(16); + iv = cryptoSvc.generateRandomIV(); + + let pubOut = {}, privOut = {}; + cryptoSvc.generateKeypair("my passphrase", salt, iv, pubOut, privOut); + pubKey = pubOut.value; + privKey = privOut.value; + + symKey = cryptoSvc.generateRandomKey(); + wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey); + + let wrap = new CryptoWrapper("http://localhost:8080/crypted-resource", auth); + yield wrap.get(self.cb); + + let payload = yield wrap.decrypt(self.cb, "my passphrase"); + do_check_eq(payload, "my payload here"); + do_check_neq(payload, wrap.data.payload); // wrap.data.payload is the encrypted one + + wrap.cleartext = "another payload"; + yield wrap.encrypt(self.cb, "my passphrase"); + payload = yield wrap.decrypt(self.cb, "my passphrase"); + do_check_eq(payload, "another payload"); + + do_test_finished(); + } + catch (e) { do_throw(e); } + finally { server.stop(); } + + self.done(); +} + +function run_test() { + async_test.async(this); + do_test_pending(); +} diff --git a/services/sync/tests/unit/test_records_keys.js b/services/sync/tests/unit/test_records_keys.js new file mode 100644 index 000000000000..8d822b5e8c3b --- /dev/null +++ b/services/sync/tests/unit/test_records_keys.js @@ -0,0 +1,64 @@ +try { + Cu.import("resource://weave/log4moz.js"); + Cu.import("resource://weave/util.js"); + Cu.import("resource://weave/async.js"); + Cu.import("resource://weave/auth.js"); + Cu.import("resource://weave/identity.js"); + Cu.import("resource://weave/base_records/keys.js"); +} catch (e) { + do_throw(e); +} +Function.prototype.async = Async.sugar; + +let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + +function pubkey_handler(metadata, response) { + let obj = {modified: "2454725.98283", + payload: {type: "pubkey", + private_key: "http://localhost:8080/privkey", + key_data: "asdfasdfasf..."}}; + return httpd_basic_auth_handler(json.encode(obj), metadata, response); +} + +function privkey_handler(metadata, response) { + let obj = {modified: "2454725.98283", + payload: {type: "privkey", + public_key: "http://localhost:8080/pubkey", + key_data: "asdfasdfasf..."}}; + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + return httpd_basic_auth_handler(json.encode(obj), metadata, response); +} + +function async_test() { + let self = yield; + let server; + + try { + let log = Log4Moz.repository.getLogger(); + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + + let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); + Auth.defaultAuthenticator = auth; + server = httpd_setup({"/pubkey": pubkey_handler, + "/privkey": privkey_handler}); + + let pubkey = yield PubKeys.get(self.cb, "http://localhost:8080/pubkey"); + do_check_eq(pubkey.data.payload.type, "pubkey"); + do_check_eq(pubkey.lastRequest.status, 200); + + let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); + do_check_eq(privkey.data.payload.type, "privkey"); + do_check_eq(privkey.lastRequest.status, 200); + + do_test_finished(); + } + catch (e) { do_throw(e); } + finally { server.stop(); } + + self.done(); +} + +function run_test() { + async_test.async(this); + do_test_pending(); +} diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js new file mode 100644 index 000000000000..c7bc9f63d037 --- /dev/null +++ b/services/sync/tests/unit/test_records_wbo.js @@ -0,0 +1,44 @@ +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/auth.js"); +Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/base_records/wbo.js"); + +Function.prototype.async = Async.sugar; + +let logger; +let Httpd = {}; +Cu.import("resource://tests/lib/httpd.js", Httpd); + +function server_handler(metadata, response) { + let body = '{"guid": "asdf-1234-asdf-1234", "type": ["object"]}'; + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function async_test() { + let self = yield; + + logger = Log4Moz.repository.getLogger('Test'); + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + + let server = new Httpd.nsHttpServer(); + server.registerPathHandler("/record", server_handler); + server.start(8080); + + let res = new WBORecord("http://localhost:8080/record"); + let rec = yield res.get(self.cb); + do_check_eq(rec.guid, "asdf-1234-asdf-1234"); + do_check_eq(rec.type[0], "object"); + do_check_eq(res.lastRequest.status, 200); + + do_test_finished(); + server.stop(); + + self.done(); +} + +function run_test() { + async_test.async(this); + do_test_pending(); +} diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js new file mode 100644 index 000000000000..70e7efdee7f4 --- /dev/null +++ b/services/sync/tests/unit/test_resource.js @@ -0,0 +1,104 @@ +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/auth.js"); +Cu.import("resource://weave/identity.js"); + +Function.prototype.async = Async.sugar; + +let logger; +let Httpd = {}; +Cu.import("resource://tests/lib/httpd.js", Httpd); + +function server_open(metadata, response) { + let body = "This path exists"; + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function server_protected(metadata, response) { + let body; + + // no btoa() in xpcshell. it's guest:guest + if (metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == "Basic Z3Vlc3Q6Z3Vlc3Q=") { + body = "This path exists and is protected"; + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + } else { + body = "This path exists and is protected - failed"; + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + } + + response.bodyOutputStream.write(body, body.length); +} + +function server_404(metadata, response) { + let body = "File not found"; + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + response.bodyOutputStream.write(body, body.length); +} + +function async_test() { + let self = yield; + + logger = Log4Moz.repository.getLogger('Test'); + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + + let server = new Httpd.nsHttpServer(); + server.registerPathHandler("/open", server_open); + server.registerPathHandler("/protected", server_protected); + server.registerPathHandler("/404", server_404); + server.start(8080); + + Utils.prefs.setIntPref("network.numRetries", 1); // speed up test + + // 1. A regular non-password-protected resource + + let res = new Resource("http://localhost:8080/open"); + let content = yield res.get(self.cb); + do_check_eq(content, "This path exists"); + do_check_eq(res.lastRequest.status, 200); + + // 2. A password protected resource (test that it'll fail w/o pass) + let res2 = new Resource("http://localhost:8080/protected"); + try { + content = yield res2.get(self.cb); + do_check_true(false); // unreachable, get() above must fail + } catch (e) {} + + // 3. A password protected resource + + let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); + let res3 = new Resource("http://localhost:8080/protected", auth); + content = yield res3.get(self.cb); + do_check_eq(content, "This path exists and is protected"); + do_check_eq(res3.lastRequest.status, 200); + + // 4. A non-existent resource (test that it'll fail) + + let res4 = new Resource("http://localhost:8080/404"); + try { + let content = yield res4.get(self.cb); + do_check_true(false); // unreachable, get() above must fail + } catch (e) {} + do_check_eq(res4.lastRequest.responseText, "File not found"); + do_check_eq(res4.lastRequest.status, 404); + + // FIXME: additional coverage needed: + // * PUT requests + // * DELETE requests + // * JsonFilter + + do_test_finished(); + server.stop(); + self.done(); +} + +function run_test() { + async_test.async(this); + do_test_pending(); +} From 3dcc6fa0b501ad98f99b68753144e0ab937a228f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:48:53 -0800 Subject: [PATCH 0689/1860] log4moz api change: Log4Moz.Service is gone. There is now a Log4Moz.repository which will auto-instantiate to a LoggerRepository (but can also be set if one wishes to use a different one). Also includes some other minor changes, e.g. formatters are now optional (and default to a BasicFormatter if not provided) --- services/sync/modules/log4moz.js | 95 +++++++++++++++++--------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 1fe2f0f187a9..4a172f80d5e0 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -80,14 +80,23 @@ let Log4Moz = { } }, - get Service() { - delete Log4Moz.Service; - Log4Moz.Service = new Log4MozService(); - return Log4Moz.Service; + get repository() { + delete Log4Moz.repository; + Log4Moz.repository = new LoggerRepository(); + return Log4Moz.repository; }, + set repository(value) { + delete Log4Moz.repository; + Log4Moz.repository = value; + }, + + get LogMessage() { return LogMessage; }, + get Logger() { return Logger; }, + get LoggerRepository() { return LoggerRepository; }, get Formatter() { return Formatter; }, get BasicFormatter() { return BasicFormatter; }, + get Appender() { return Appender; }, get DumpAppender() { return DumpAppender; }, get ConsoleAppender() { return ConsoleAppender; }, @@ -95,9 +104,9 @@ let Log4Moz = { get RotatingFileAppender() { return RotatingFileAppender; }, // Logging helper: - // let logger = Log4Moz.Service.getLogger("foo"); + // let logger = Log4Moz.repository.getLogger("foo"); // logger.info(Log4Moz.enumerateInterfaces(someObject).join(",")); - enumerateInterfaces: function(aObject) { + enumerateInterfaces: function Log4Moz_enumerateInterfaces(aObject) { let interfaces = []; for (i in Ci) { @@ -112,9 +121,10 @@ let Log4Moz = { }, // Logging helper: - // let logger = Log4Moz.Service.getLogger("foo"); + // let logger = Log4Moz.repository.getLogger("foo"); // logger.info(Log4Moz.enumerateProperties(someObject).join(",")); - enumerateProperties: function(aObject, aExcludeComplexTypes) { + enumerateProperties: function Log4Moz_enumerateProps(aObject, + aExcludeComplexTypes) { let properties = []; for (p in aObject) { @@ -165,15 +175,25 @@ LogMessage.prototype = { */ function Logger(name, repository) { - this._name = name; - this._repository = repository; - this._appenders = []; + this._init(name, repository); } Logger.prototype = { + _init: function Logger__init(name, repository) { + if (!repository) + repository = Log4Moz.repository; + this._name = name; + this._appenders = []; + this._repository = repository; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), parent: null, + get name() { + return this._name; + }, + _level: null, get level() { if (this._level != null) @@ -286,10 +306,12 @@ LoggerRepository.prototype = { }, getLogger: function LogRep_getLogger(name) { - if (!(name in this._loggers)) { - this._loggers[name] = new Logger(name, this); - this._updateParents(name); - } + if (!name) + name = this.getLogger.caller.name; + if (name in this._loggers) + return this._loggers[name]; + this._loggers[name] = new Logger(name, this); + this._updateParents(name); return this._loggers[name]; } }; @@ -313,6 +335,8 @@ function BasicFormatter(dateFormat) { this.dateFormat = dateFormat; } BasicFormatter.prototype = { + __proto__: Formatter.prototype, + _dateFormat: null, get dateFormat() { @@ -332,7 +356,6 @@ BasicFormatter.prototype = { message.message + "\n"; } }; -BasicFormatter.prototype.__proto__ = new Formatter(); /* * Appenders @@ -369,14 +392,15 @@ Appender.prototype = { function DumpAppender(formatter) { this._name = "DumpAppender"; - this._formatter = formatter; + this._formatter = formatter? formatter : new BasicFormatter(); } DumpAppender.prototype = { + __proto__: Appender.prototype, + doAppend: function DApp_doAppend(message) { dump(message); } }; -DumpAppender.prototype.__proto__ = new Appender(); /* * ConsoleAppender @@ -388,6 +412,8 @@ function ConsoleAppender(formatter) { this._formatter = formatter; } ConsoleAppender.prototype = { + __proto__: Appender.prototype, + doAppend: function CApp_doAppend(message) { if (message.level > Log4Moz.Level.Warn) { Cu.reportError(message); @@ -397,7 +423,6 @@ ConsoleAppender.prototype = { getService(Ci.nsIConsoleService).logStringMessage(message); } }; -ConsoleAppender.prototype.__proto__ = new Appender(); /* * FileAppender @@ -407,9 +432,11 @@ ConsoleAppender.prototype.__proto__ = new Appender(); function FileAppender(file, formatter) { this._name = "FileAppender"; this._file = file; // nsIFile - this._formatter = formatter; + this._formatter = formatter? formatter : new BasicFormatter(); } FileAppender.prototype = { + __proto__: Appender.prototype, + __fos: null, get _fos() { if (!this.__fos) @@ -450,7 +477,6 @@ FileAppender.prototype = { this._file.remove(false); } }; -FileAppender.prototype.__proto__ = new Appender(); /* * RotatingFileAppender @@ -466,11 +492,13 @@ function RotatingFileAppender(file, formatter, maxSize, maxBackups) { this._name = "RotatingFileAppender"; this._file = file; // nsIFile - this._formatter = formatter; + this._formatter = formatter? formatter : new BasicFormatter(); this._maxSize = maxSize; this._maxBackups = maxBackups; } RotatingFileAppender.prototype = { + __proto__: FileAppender.prototype, + doAppend: function RFApp_doAppend(message) { if (message === null || message.length <= 0) return; @@ -502,26 +530,3 @@ RotatingFileAppender.prototype = { // Note: this._file still points to the same file } }; -RotatingFileAppender.prototype.__proto__ = new FileAppender(); - -/* - * LoggingService - */ - -function Log4MozService() { - this._repository = new LoggerRepository(); -} -Log4MozService.prototype = { - //classDescription: "Log4moz Logging Service", - //contractID: "@mozilla.org/log4moz/service;1", - //classID: Components.ID("{a60e50d7-90b8-4a12-ad0c-79e6a1896978}"), - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), - - get rootLogger() { - return this._repository.rootLogger; - }, - - getLogger: function LogSvc_getLogger(name) { - return this._repository.getLogger(name); - } -}; From c857fa4dfddf15808b092d208aea1abf122f3fa2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:53:33 -0800 Subject: [PATCH 0690/1860] wizard eula screen changes --- services/sync/locales/en-US/wizard.dtd | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index e5ad58575725..46776bd5ba88 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,17 +1,15 @@ - - - + - - - - - - - + + + + + + + From 18f710016b2d5f4f1010b79b7b225697315ef0be Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:54:21 -0800 Subject: [PATCH 0691/1860] add max retries config setting that resources support now --- services/sync/services-sync.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index eb1fd3ea59d1..3481da461a26 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -38,3 +38,5 @@ pref("extensions.weave.log.logger.service.main", "Trace"); pref("extensions.weave.xmpp.enabled", false); pref("extensions.weave.xmpp.server.url", "https://sm-labs01.mozilla.org:81/xmpp"); pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); + +pref("extensions.weave.network.numRetries", 4); From f38ca3b38f12fb1867a0ee7d25893016ed40fd4b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:56:56 -0800 Subject: [PATCH 0692/1860] makeUri: don't throw on invalid uris, return null instead --- services/sync/modules/util.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 8ccb0c05cc52..f2c3123d9921 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -273,7 +273,7 @@ let Utils = { } if (msg) { - let log = Log4Moz.Service.getLogger("Service.Util"); + let log = Log4Moz.repository.getLogger("Service.Util"); log.error(msg + " Error code: " + code); } @@ -312,7 +312,13 @@ let Utils = { return null; let ioservice = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); - return ioservice.newURI(URIString, null, null); + try { + return ioservice.newURI(URIString, null, null); + } catch (e) { + let log = Log4Moz.repository.getLogger("Service.Util"); + log.debug("Could not create URI: " + e); + return null; + } }, xpath: function Weave_xpath(xmlDoc, xpathString) { @@ -435,7 +441,7 @@ let Utils = { EventListener: function Weave_EventListener(handler, eventName) { this._handler = handler; this._eventName = eventName; - this._log = Log4Moz.Service.getLogger("Async.EventHandler"); + this._log = Log4Moz.repository.getLogger("Async.EventHandler"); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; } From d7ebd3c6886553fe2423cd160f8a30369fcd1f09 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:57:59 -0800 Subject: [PATCH 0693/1860] remove server lock wrapper; change local lock to work with service.js local locking api --- services/sync/modules/wrap.js | 46 +++-------------------------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 1396c2f0ad9b..b6501cbff150 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -43,7 +43,6 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/faultTolerance.js"); @@ -108,45 +107,6 @@ let Wrap = { }; }, - // NOTE: see notify, this works the same way. they can be - // chained together as well. - lock: function WeaveSync_lock(method /* , arg1, arg2, ..., argN */) { - let savedMethod = method; - let savedArgs = Array.prototype.slice.call(arguments, 1); - - return function WeaveLockWrapper( /* argN+1, argN+2, ... */) { - let self = yield; - let ret; - let args = Array.prototype.slice.call(arguments); - - if (!this._loggedIn) - throw "Could not acquire lock (not logged in)"; - if (DAV.locked) - throw "Could not acquire lock (lock already held)"; - - let locked = yield DAV.lock.async(DAV, self.cb); - if (!locked) - throw "Could not acquire lock"; - - this._os.notifyObservers(null, this._osPrefix + "lock:acquired", ""); - - try { - args = savedArgs.concat(args); - args.unshift(this, savedMethod, self.cb); - ret = yield Async.run.apply(Async, args); - - } catch (e) { - throw e; - - } finally { - yield DAV.unlock.async(DAV, self.cb); - this._os.notifyObservers(null, this._osPrefix + "lock:released", ""); - } - - self.done(ret); - }; - }, - // NOTE: see notify, this works the same way. they can be // chained together as well. localLock: function WeaveSync_localLock(method /* , arg1, arg2, ..., argN */) { @@ -158,9 +118,9 @@ let Wrap = { let ret; let args = Array.prototype.slice.call(arguments); - if (DAV.locked) + let ret = this.lock(); + if (!ret) throw "Could not acquire lock"; - DAV.allowLock = false; this._os.notifyObservers(null, this._osPrefix + "local-lock:acquired", ""); @@ -174,7 +134,7 @@ let Wrap = { throw e; } finally { - DAV.allowLock = true; + this.unlock(); this._os.notifyObservers(null, this._osPrefix + "local-lock:released", ""); } From 311e7f80fb4525b9c4f9c753e7a4a3c3fe259800 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 14:59:45 -0800 Subject: [PATCH 0694/1860] remove lots of code that will not be needed with new server --- services/sync/modules/service.js | 248 ++++--------------------------- 1 file changed, 32 insertions(+), 216 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 78d10388232e..ef686d6a276c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -73,17 +73,16 @@ Cu.import("resource://weave/faultTolerance.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/oauth.js"); -Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/clientData.js"); -Cu.import("resource://weave/engines/cookies.js"); Cu.import("resource://weave/engines/bookmarks.js"); +/*Cu.import("resource://weave/engines/cookies.js"); Cu.import("resource://weave/engines/history.js"); Cu.import("resource://weave/engines/passwords.js"); Cu.import("resource://weave/engines/forms.js"); Cu.import("resource://weave/engines/tabs.js"); -Cu.import("resource://weave/engines/input.js"); +Cu.import("resource://weave/engines/input.js");*/ Function.prototype.async = Async.sugar; @@ -97,19 +96,18 @@ Cu.import("resource://weave/crypto.js", Weave); Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); Cu.import("resource://weave/clientData.js", Weave); -Cu.import("resource://weave/dav.js", Weave); Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/syncCores.js", Weave); Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/oauth.js", Weave); Cu.import("resource://weave/service.js", Weave); -Cu.import("resource://weave/engines/cookies.js", Weave); -Cu.import("resource://weave/engines/passwords.js", Weave); Cu.import("resource://weave/engines/bookmarks.js", Weave); +/*Cu.import("resource://weave/engines/cookies.js", Weave); +Cu.import("resource://weave/engines/passwords.js", Weave); Cu.import("resource://weave/engines/history.js", Weave); Cu.import("resource://weave/engines/forms.js", Weave); Cu.import("resource://weave/engines/tabs.js", Weave); -Cu.import("resource://weave/engines/input.js", Weave); +Cu.import("resource://weave/engines/input.js", Weave);*/ Utils.lazy(Weave, 'Service', WeaveSvc); @@ -119,7 +117,6 @@ Utils.lazy(Weave, 'Service', WeaveSvc); */ function WeaveSvc() { - this._startupFinished = false; this._initLogs(); this._log.info("Weave Sync Service Initializing"); @@ -128,10 +125,6 @@ function WeaveSvc() { ID.set('WeaveCryptoID', new Identity('Mozilla Services Encryption Passphrase', this.username)); - // Set up aliases for other modules to use our IDs - ID.setAlias('WeaveID', 'DAV:default'); - ID.setAlias('WeaveCryptoID', 'Engine:PBE:default'); - // Other misc startup Utils.prefs.addObserver("", this, false); this._os.addObserver(this, "quit-application", true); @@ -143,7 +136,6 @@ function WeaveSvc() { WeaveSvc.prototype = { _notify: Wrap.notify, - _lock: Wrap.lock, _localLock: Wrap.localLock, _catchAll: Wrap.catchAll, _osPrefix: "weave:service:", @@ -222,12 +214,20 @@ WeaveSvc.prototype = { }, onWindowOpened: function Weave__onWindowOpened() { - if (!this._startupFinished) { - this._startupFinished = true; - if (Utils.prefs.getBoolPref("autoconnect") && - this.username && this.username != 'nobody') - this._initialLoginAndSync.async(this); - } + if (Utils.prefs.getBoolPref("autoconnect") && + this.username && this.username != 'nobody') + this._initialLoginAndSync.async(this); + }, + + get locked() this._locked, + lock: function Svc_lock() { + if (this._locked) + return false; + this._locked = true; + return true; + }, + unlock: function Svc_unlock() { + this._locked = false; }, _initialLoginAndSync: function Weave__initialLoginAndSync() { @@ -273,23 +273,23 @@ WeaveSvc.prototype = { _onSchedule: function WeaveSvc__onSchedule() { if (this.enabled) { - if (DAV.locked) { + if (this.locked) { this._log.info("Skipping scheduled sync; local operation in progress") } else { this._log.info("Running scheduled sync"); this._notify("sync", "", - this._catchAll(this._lock(this._syncAsNeeded))).async(this); + this._catchAll(this._localLock(this._syncAsNeeded))).async(this); } } }, _initLogs: function WeaveSvc__initLogs() { - this._log = Log4Moz.Service.getLogger("Service.Main"); + this._log = Log4Moz.repository.getLogger("Service.Main"); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.main")]; let formatter = new Log4Moz.BasicFormatter(); - let root = Log4Moz.Service.rootLogger; + let root = Log4Moz.repository.rootLogger; root.level = Log4Moz.Level[Utils.prefs.getCharPref("log.rootLogger")]; let capp = new Log4Moz.ConsoleAppender(formatter); @@ -326,170 +326,12 @@ WeaveSvc.prototype = { this._debugApp.clear(); }, - _uploadVersion: function WeaveSvc__uploadVersion() { - let self = yield; - - DAV.MKCOL("meta", self.cb); - let ret = yield; - if (!ret) - throw "Could not create meta information directory"; - - DAV.PUT("meta/version", STORAGE_FORMAT_VERSION, self.cb); - ret = yield; - Utils.ensureStatus(ret.status, "Could not upload server version file"); - }, - - // force a server wipe when the version is lower than ours (or there is none) - _versionCheck: function WeaveSvc__versionCheck() { - let self = yield; - - let ret = yield DAV.GET("meta/version", self.cb); - - if (ret.status == 404) { - this._log.info("Could not get version file. Wiping server data."); - yield this._serverWipe.async(this, self.cb); - yield this._uploadVersion.async(this, self.cb); - - } else if (!Utils.checkStatus(ret.status)) { - this._log.debug("Could not get version file from server"); - self.done(false); - return; - - } else if (ret.responseText < STORAGE_FORMAT_VERSION) { - this._log.info("Server version too low. Wiping server data."); - yield this._serverWipe.async(this, self.cb); - yield this._uploadVersion.async(this, self.cb); - - } else if (ret.responseText > STORAGE_FORMAT_VERSION) { - // XXX should we do something here? - throw "Server version higher than this client understands. Aborting." - } - self.done(true); - }, - - _checkUserDir: function WeaveSvc__checkUserDir() { - let self = yield; - let prefix = DAV.defaultPrefix; - - this._log.trace("Checking user directory exists"); - - try { - DAV.defaultPrefix = ''; - DAV.MKCOL("user/" + this.userPath, self.cb); - let ret = yield; - if (!ret) - throw "Could not create user directory"; - } - catch (e) { throw e; } - finally { DAV.defaultPrefix = prefix; } - }, - - // Retrieves the keypair for the given Identity object and inserts - // its information into the Identity object. If no Identity object - // is supplied, the 'WeaveCryptoID' identity is used. - // - // This coroutine assumes the DAV singleton's prefix is set to the - // proper user-specific directory. - // - // If the password associated with the Identity cannot be used to - // decrypt the private key, an exception is raised. - _getKeypair : function WeaveSvc__getKeypair(id) { - let self = yield; - - if ("none" == Utils.prefs.getCharPref("encryption")) - return; - - if (typeof(id) == "undefined") - id = ID.get('WeaveCryptoID'); - - this._log.trace("Retrieving keypair from server"); - - if (this._keyPair['private'] && this._keyPair['public']) - this._log.debug("Using cached keypair"); - else { - this._log.debug("Fetching keypair from server"); - - let privkeyResp = yield DAV.GET("private/privkey", self.cb); - Utils.ensureStatus(privkeyResp.status, "Could not download private key"); - - let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); - Utils.ensureStatus(pubkeyResp.status, "Could not download public key"); - - this._keyPair['private'] = this._json.decode(privkeyResp.responseText); - this._keyPair['public'] = this._json.decode(pubkeyResp.responseText); - } - - let privkeyData = this._keyPair['private'] - let pubkeyData = this._keyPair['public']; - - if (!privkeyData || !pubkeyData) - throw "Bad keypair JSON"; - if (privkeyData.version != 1 || pubkeyData.version != 1) - throw "Unexpected keypair data version"; - if (privkeyData.algorithm != "RSA" || pubkeyData.algorithm != "RSA") - throw "Only RSA keys currently supported"; - if (!privkeyData.privkey) - throw "Private key does not contain private key data!"; - if (!pubkeyData.pubkey) - throw "Public key does not contain public key data!"; - - - id.keypairAlg = privkeyData.algorithm; - id.privkey = privkeyData.privkey; - id.privkeyWrapIV = privkeyData.privkeyIV; - id.passphraseSalt = privkeyData.privkeySalt; - - id.pubkey = pubkeyData.pubkey; - - let isValid = yield Crypto.isPassphraseValid.async(Crypto, self.cb, id); - if (!isValid) - throw new Error("Passphrase is not valid."); - }, - - _generateKeys: function WeaveSvc__generateKeys() { - let self = yield; - - this._log.debug("Generating new RSA key"); - - // RSAkeygen will set the needed |id| properties. - let id = ID.get('WeaveCryptoID'); - Crypto.RSAkeygen.async(Crypto, self.cb, id); - yield; - - DAV.MKCOL("private/", self.cb); - let ret = yield; - if (!ret) - throw "Could not create private key directory"; - - DAV.MKCOL("public/", self.cb); - ret = yield; - if (!ret) - throw "Could not create public key directory"; - - let privkeyData = { version : 1, - algorithm : id.keypairAlg, - privkey : id.privkey, - privkeyIV : id.privkeyWrapIV, - privkeySalt : id.passphraseSalt - }; - let data = this._json.encode(privkeyData); - - DAV.PUT("private/privkey", data, self.cb); - ret = yield; - Utils.ensureStatus(ret.status, "Could not upload private key"); - - - let pubkeyData = { version : 1, - algorithm : id.keypairAlg, - pubkey : id.pubkey - }; - data = this._json.encode(pubkeyData); - - DAV.PUT("public/pubkey", data, self.cb); - ret = yield; - Utils.ensureStatus(ret.status, "Could not upload public key"); - }, - +/* + * deleted: + * server version checks + * server user dir check (existence) + * caching keypair (not needed?) + */ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), @@ -683,22 +525,6 @@ WeaveSvc.prototype = { this._log.info("Making sure server is initialized..."); - // create user directory (for self-hosted webdav shares) if it doesn't exist - let status = yield DAV.checkLogin.async(DAV, self.cb, - this.username, this.password); - if (status == 404) { - yield this._checkUserDir.async(this, self.cb); - status = yield DAV.checkLogin.async(DAV, self.cb, - this.username, this.password); - } - Utils.ensureStatus(status, "Cannot initialize server"); - - // wipe the server if it has any old cruft - yield this._versionCheck.async(this, self.cb); - - // get info on the clients that are syncing with this store - yield ClientData.refresh(self.cb); - // cache keys, create public/private keypair if it doesn't exist this._log.debug("Caching keys"); let privkeyResp = yield DAV.GET("private/privkey", self.cb); @@ -735,16 +561,6 @@ WeaveSvc.prototype = { this._os.notifyObservers(null, "weave:service:logout:success", ""); }, - resetLock: function WeaveSvc_resetLock(onComplete) { - this._notify("reset-server-lock", "", - this._resetLock).async(this, onComplete); - }, - _resetLock: function WeaveSvc__resetLock() { - let self = yield; - DAV.forceUnlock.async(DAV, self.cb); - yield; - }, - serverWipe: function WeaveSvc_serverWipe(onComplete) { let cb = function WeaveSvc_serverWipeCb() { let self = yield; @@ -753,7 +569,7 @@ WeaveSvc.prototype = { this.logout(); self.done(); }; - this._notify("server-wipe", "", this._lock(cb)).async(this, onComplete); + this._notify("server-wipe", "", this._localLock(cb)).async(this, onComplete); }, _serverWipe: function WeaveSvc__serverWipe() { let self = yield; @@ -775,7 +591,7 @@ WeaveSvc.prototype = { sync: function WeaveSvc_sync(onComplete) { this._notify("sync", "", - this._catchAll(this._lock(this._sync))).async(this, onComplete); + this._catchAll(this._localLock(this._sync))).async(this, onComplete); }, _sync: function WeaveSvc__sync() { @@ -872,7 +688,7 @@ WeaveSvc.prototype = { resetServer: function WeaveSvc_resetServer(onComplete) { this._notify("reset-server", "", - this._lock(this._resetServer)).async(this, onComplete); + this._localLock(this._resetServer)).async(this, onComplete); }, _resetServer: function WeaveSvc__resetServer() { let self = yield; From cedb5a95645937b583dc3e2c5f68926ae9902cc4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 3 Nov 2008 15:00:38 -0800 Subject: [PATCH 0695/1860] fix log4moz calls (new api), fix module imports for renamed modules --- services/sync/modules/async.js | 2 +- services/sync/modules/clientData.js | 5 ++--- services/sync/modules/crypto.js | 2 +- services/sync/modules/engines.js | 10 +++++----- services/sync/modules/engines/bookmarks.js | 17 +++++++++-------- services/sync/modules/faultTolerance.js | 4 ++-- services/sync/modules/notifications.js | 2 +- services/sync/modules/oauth.js | 2 +- services/sync/modules/stores.js | 4 ++-- services/sync/modules/syncCores.js | 2 +- services/sync/modules/trackers.js | 2 +- services/sync/modules/xmpp/transportLayer.js | 2 +- services/sync/modules/xmpp/xmppClient.js | 2 +- 13 files changed, 28 insertions(+), 28 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index bec128da6f37..cadbcd740b8d 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -107,7 +107,7 @@ function AsyncException(asyncStack, exceptionToWrap) { function Generator(thisArg, method, onComplete, args) { this._outstandingCbs = 0; - this._log = Log4Moz.Service.getLogger("Async.Generator"); + this._log = Log4Moz.repository.getLogger("Async.Generator"); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; this._thisArg = thisArg; diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index 34b3f8e4d470..5d7988762b0e 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -45,8 +45,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/dav.js"); -Cu.import("resource://weave/remote.js"); +Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; @@ -94,7 +93,7 @@ ClientDataSvc.prototype = { }, _init: function ClientData__init() { - this._log = Log4Moz.Service.getLogger("Service.ClientData"); + this._log = Log4Moz.repository.getLogger("Service.ClientData"); this._remote = new Resource("meta/clients"); this._remote.pushFilter(new JsonFilter()); }, diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 07f4aff5d194..25f85bf1cc7a 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -84,7 +84,7 @@ CryptoSvc.prototype = { }, _init: function Crypto__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log = Log4Moz.repository.getLogger("Service." + this._logName); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.crypto")]; let branch = Cc["@mozilla.org/preferences-service;1"] diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f89112b32889..5dcd4f651ec0 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -48,8 +48,7 @@ Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/crypto.js"); -Cu.import("resource://weave/dav.js"); -Cu.import("resource://weave/remote.js"); +Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/clientData.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); @@ -173,7 +172,7 @@ Engine.prototype = { try { level = Utils.prefs.getCharPref(levelPref); } catch (e) { /* ignore unset prefs */ } - this._log = Log4Moz.Service.getLogger("Service." + this.logName); + this._log = Log4Moz.repository.getLogger("Service." + this.logName); this._log.level = Log4Moz.Level[level]; this._osPrefix = "weave:" + this.name + ":"; }, @@ -504,8 +503,9 @@ BlobEngine.prototype = { this._log.info("Beginning sync"); this._os.notifyObservers(null, "weave:service:sync:engine:start", this.name); - if (!(yield DAV.MKCOL(this.serverPrefix, self.cb))) - throw "Could not create remote folder"; + // FIXME + //if (!(yield DAV.MKCOL(this.serverPrefix, self.cb))) + // throw "Could not create remote folder"; try { if ("none" != Utils.prefs.getCharPref("encryption")) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d65a426e8a65..d7904016e35c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -55,7 +55,6 @@ const INCOMING_SHARE_ROOT_NAME = "Shared Folders"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/dav.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/async.js"); @@ -67,7 +66,7 @@ Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/xmpp/xmppClient.js"); Cu.import("resource://weave/notifications.js"); Cu.import("resource://weave/sharing.js"); -Cu.import("resource://weave/remote.js"); +Cu.import("resource://weave/resource.js"); Function.prototype.async = Async.sugar; @@ -100,7 +99,7 @@ BookmarksSharingManager.prototype = { _init: function SharingManager__init(engine) { this._engine = engine; - this._log = Log4Moz.Service.getLogger("Bookmark Share"); + this._log = Log4Moz.repository.getLogger("Bookmark Share"); if ( Utils.prefs.getBoolPref( "xmpp.enabled" ) ) { this._log.info( "Starting XMPP client for bookmark engine..." ); this._startXmppClient.async(this); @@ -301,8 +300,8 @@ BookmarksSharingManager.prototype = { }, _getNewShares: function BmkSharing__getNewShares() { let self = yield; - - let sharingApi = new Sharing.Api( DAV ); +// FIXME +// let sharingApi = new Sharing.Api( DAV ); let result = yield sharingApi.getShares(self.cb); this._log.info("Got Shares: " + result); @@ -440,7 +439,8 @@ BookmarksSharingManager.prototype = { /* Create the directory on the server if it does not exist already. */ let serverPath = "share/" + folderGuid; - let ret = yield DAV.MKCOL(serverPath, self.cb); +// FIXME +// let ret = yield DAV.MKCOL(serverPath, self.cb); if (!ret) { this._log.error("Can't create remote folder for outgoing share."); @@ -463,7 +463,8 @@ BookmarksSharingManager.prototype = { } // Call Atul's js api for setting htaccess: - let sharingApi = new Sharing.Api( DAV ); +// FIXME +// let sharingApi = new Sharing.Api( DAV ); let result = yield sharingApi.shareWithUsers( serverPath, [username], folderName, self.cb ); @@ -1414,7 +1415,7 @@ BookmarksTracker.prototype = { }, _init: function BMT__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log = Log4Moz.repository.getLogger("Service." + this._logName); this._score = 0; Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index 7581e540e155..bdf400bc5ece 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -50,9 +50,9 @@ FaultTolerance = { }; function FTService() { - this._log = Log4Moz.Service.getLogger("FaultTolerance"); + this._log = Log4Moz.repository.getLogger("FaultTolerance"); this._appender = new FTAppender(this); - Log4Moz.Service.rootLogger.addAppender(this._appender); + Log4Moz.repository.rootLogger.addAppender(this._appender); } FTService.prototype = { get lastException() this._lastException, diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index 0fe0a264240c..823aa0ba22f8 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -129,7 +129,7 @@ function NotificationButton(label, accessKey, callback) { try { callback.apply(this, arguments); } catch (e) { - let logger = Log4Moz.Service.getLogger("Notifications"); + let logger = Log4Moz.repository.getLogger("Notifications"); logger.error("An exception occurred: " + Utils.exceptionStr(e)); logger.info(Utils.stackTrace(e)); throw e; diff --git a/services/sync/modules/oauth.js b/services/sync/modules/oauth.js index 8621f1ffb2bf..ecadefd46094 100644 --- a/services/sync/modules/oauth.js +++ b/services/sync/modules/oauth.js @@ -71,7 +71,7 @@ OAuthSvc.prototype = { _cb2: null, _init: function OAuth__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log = Log4Moz.repository.getLogger("Service." + this._logName); this._log.level = "Debug"; this._log.info("OAuth Module Initialized"); }, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 702d3ace62db..f64816ac873e 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -82,7 +82,7 @@ Store.prototype = { }, _init: function Store__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log = Log4Moz.repository.getLogger("Service." + this._logName); }, applyCommands: function Store_applyCommands(commandList) { @@ -180,7 +180,7 @@ SnapshotStore.prototype = { _init: function SStore__init(name) { this.filename = name; - this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log = Log4Moz.repository.getLogger("Service." + this._logName); }, _createCommand: function SStore__createCommand(command) { diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index 834939dcefd6..f1d391001629 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -67,7 +67,7 @@ SyncCore.prototype = { _store: null, _init: function SC__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log = Log4Moz.repository.getLogger("Service." + this._logName); }, // FIXME: this won't work for deep objects, or objects with optional diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index b6b3a02c79a1..ef7a075bd220 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -71,7 +71,7 @@ Tracker.prototype = { _score: 0, _init: function T__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); + this._log = Log4Moz.repository.getLogger("Service." + this._logName); this._score = 0; }, diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js index 1148cf935498..43356dc5c6ce 100644 --- a/services/sync/modules/xmpp/transportLayer.js +++ b/services/sync/modules/xmpp/transportLayer.js @@ -198,7 +198,7 @@ function HTTPPollingTransport( serverUrl, useKeys, interval ) { } HTTPPollingTransport.prototype = { _init: function( serverUrl, useKeys, interval ) { - this._log = Log4Moz.Service.getLogger("Service.XmppTransportLayer"); + this._log = Log4Moz.repository.getLogger("Service.XmppTransportLayer"); this._log.info("Initializing transport: serverUrl=" + serverUrl + ", useKeys=" + useKeys + ", interval=" + interval); this._serverUrl = serverUrl this._n = 0; diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js index 9898a92341d0..7b398293739a 100644 --- a/services/sync/modules/xmpp/xmppClient.js +++ b/services/sync/modules/xmpp/xmppClient.js @@ -71,7 +71,7 @@ XmppClient.prototype = { IQ_ERROR: -1, _init: function( clientName, realm, clientPassword, transport, authenticator ) { - this._log = Log4Moz.Service.getLogger("Service.XmppClient"); + this._log = Log4Moz.repository.getLogger("Service.XmppClient"); this._myName = clientName; this._realm = realm; this._fullName = clientName + "@" + realm; From 2be659c49918018ea592b51dc4cd1dcbe04e3f86 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 17:32:33 -0800 Subject: [PATCH 0696/1860] use util module's stack formatter --- services/sync/modules/async.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js index cadbcd740b8d..f8a69c3a2d6f 100644 --- a/services/sync/modules/async.js +++ b/services/sync/modules/async.js @@ -172,13 +172,9 @@ Generator.prototype = { get asyncStack() { let cbGenText = ""; if (this._stackAtLastCallbackGen) - cbGenText = (" (last self.cb generated at " + - Utils.formatFrame(this._stackAtLastCallbackGen) + ")"); - - let frame = skipAsyncFrames(this._initFrame); - - return ("unknown (async) :: " + this.name + cbGenText + "\n" + - Utils.stackTraceFromFrame(frame, Utils.formatFrame)); + cbGenText = "Last callback created at " + + Utils.formatFrame(this._stackAtLastCallbackGen); + return Utils.stackTraceFromFrame(this._initFrame, Utils.formatFrame) + cbGenText; }, _handleException: function AsyncGen__handleException(e) { From fa6ae0cfedd23af7e1e8c19e13465ea17d88b3b7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 17:34:34 -0800 Subject: [PATCH 0697/1860] print exception in async stack traces, skip async frames in async stack traces --- services/sync/modules/util.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index f2c3123d9921..926b08844ef8 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -209,14 +209,21 @@ let Utils = { // there are multiple extensions with similar filenames formatFrame: function Utils_formatFrame(frame) { let tmp = ""; + if (frame.filename) tmp = frame.filename.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); else if (frame.fileName) tmp = frame.fileName.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); + + // skip async.js frames + if (tmp == "module:async.js") + return null; + if (frame.lineNumber) tmp += ":" + frame.lineNumber; if (frame.name) tmp += " :: " + frame.name; + return tmp; }, @@ -241,7 +248,9 @@ let Utils = { let output = ""; while (frame) { - output += formatter(frame) + "\n"; + let str = formatter(frame); + if (str) + output += str + "\n"; frame = frame.caller; } @@ -250,11 +259,12 @@ let Utils = { stackTrace: function Weave_stackTrace(e, formatter) { if (e.asyncStack) // AsyncException - return e.asyncStack; + return "Original exception: " + Utils.exceptionStr(e.originalException) + "\n" + + "Async stack trace:\n" + e.asyncStack; else if (e.location) // Wrapped nsIException - return this.stackTraceFromFrame(e.location, formatter); + return "Stack trace:\n" + this.stackTraceFromFrame(e.location, formatter); else if (e.stack) // Standard JS exception - return e.stack; + return "JS Stack trace:\n" + e.stack; else return "No traceback available"; }, From ec5b35ea4dfa13c684fdbff063c8aff7374d6e6f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 17:36:19 -0800 Subject: [PATCH 0698/1860] url -> uri --- services/sync/modules/base_records/keys.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 193f234e947e..43db95056322 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -147,12 +147,12 @@ PubKeyManager.prototype = { _keyType: PubKey, _logName: "PubKeyManager", - get defaultKeyUrl() this._defaultKeyUrl, - set defaultKeyUrl(value) { this._defaultKeyUrl = value; }, + get defaultKeyUri() this._defaultKeyUrl, + set defaultKeyUri(value) { this._defaultKeyUri = value; }, _getDefaultKey: function KeyMgr__getDefaultKey() { let self = yield; - let ret = yield this.get(self.cb, this.defaultKeyUrl); + let ret = yield this.get(self.cb, this.defaultKeyUri); self.done(ret); }, getDefaultKey: function KeyMgr_getDefaultKey(onComplete) { From 717672c2ab7c7cc2772ea491f6bbb0193a97e689 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 17:36:41 -0800 Subject: [PATCH 0699/1860] onException cleanup --- services/sync/modules/faultTolerance.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index bdf400bc5ece..42ca263c02d1 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -61,15 +61,9 @@ FTService.prototype = { // our current state }, onException: function FTS_onException(exception) { - let continueSync = true; this._lastException = exception; - - if ("Could not acquire lock" == exception) - continueSync = false; - else - this._log.debug(Utils.stackTrace(exception)); - - return continueSync; + this._log.debug("\n" + Utils.stackTrace(exception)); + return true; // continue sync if thrown by a sync engine } }; From 74ed4e7aa38515f6493831409205b47b2c53e2c4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 17:37:17 -0800 Subject: [PATCH 0700/1860] lots o' fixes --- services/sync/modules/service.js | 313 +++++++++++-------------------- 1 file changed, 108 insertions(+), 205 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ef686d6a276c..26e28c907bc5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -70,7 +70,9 @@ Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/faultTolerance.js"); -Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/auth.js"); +Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/oauth.js"); Cu.import("resource://weave/identity.js"); @@ -92,7 +94,9 @@ Cu.import("resource://weave/constants.js", Weave); Cu.import("resource://weave/util.js", Weave); Cu.import("resource://weave/async.js", Weave); Cu.import("resource://weave/faultTolerance.js", Weave); -Cu.import("resource://weave/crypto.js", Weave); +Cu.import("resource://weave/auth.js", Weave); +Cu.import("resource://weave/resource.js", Weave); +Cu.import("resource://weave/base_records/keys.js", Weave); Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); Cu.import("resource://weave/clientData.js", Weave); @@ -116,23 +120,7 @@ Utils.lazy(Weave, 'Service', WeaveSvc); * Main entry point into Weave's sync framework */ -function WeaveSvc() { - this._initLogs(); - this._log.info("Weave Sync Service Initializing"); - - // Create Weave identities (for logging in, and for encryption) - ID.set('WeaveID', new Identity('Mozilla Services Password', this.username)); - ID.set('WeaveCryptoID', - new Identity('Mozilla Services Encryption Passphrase', this.username)); - - // Other misc startup - Utils.prefs.addObserver("", this, false); - this._os.addObserver(this, "quit-application", true); - FaultTolerance.Service; // initialize FT service - - if (!this.enabled) - this._log.info("Weave Sync disabled"); -} +function WeaveSvc() {} WeaveSvc.prototype = { _notify: Wrap.notify, @@ -194,10 +182,19 @@ WeaveSvc.prototype = { get passphrase() { return ID.get('WeaveCryptoID').password; }, set passphrase(value) { ID.get('WeaveCryptoID').password = value; }, + get baseURL() { + let url = Utils.prefs.getCharPref("serverURL"); + if (url && url[url.length-1] != '/') + url = url + '/'; + return url; + }, + set baseURL(value) { + Utils.prefs.setCharPref("serverURL", value); + }, + get userPath() { return ID.get('WeaveID').username; }, get isLoggedIn() this._loggedIn, - get isInitialized() this._initialized, get isQuitting() this._isQuitting, set isQuitting(value) { this._isQuitting = value; }, @@ -214,9 +211,6 @@ WeaveSvc.prototype = { }, onWindowOpened: function Weave__onWindowOpened() { - if (Utils.prefs.getBoolPref("autoconnect") && - this.username && this.username != 'nobody') - this._initialLoginAndSync.async(this); }, get locked() this._locked, @@ -230,12 +224,6 @@ WeaveSvc.prototype = { this._locked = false; }, - _initialLoginAndSync: function Weave__initialLoginAndSync() { - let self = yield; - yield this.loginAndInit(self.cb); // will throw if login fails - yield this.sync(self.cb); - }, - _setSchedule: function Weave__setSchedule(schedule) { switch (this.schedule) { case 0: @@ -274,7 +262,7 @@ WeaveSvc.prototype = { _onSchedule: function WeaveSvc__onSchedule() { if (this.enabled) { if (this.locked) { - this._log.info("Skipping scheduled sync; local operation in progress") + this._log.info("Skipping scheduled sync; local operation in progress"); } else { this._log.info("Running scheduled sync"); this._notify("sync", "", @@ -283,6 +271,45 @@ WeaveSvc.prototype = { } }, + // one-time initialization like setting up observers and the like + // xxx we might need to split some of this out into something we can call + // again when username/server/etc changes + _onStartup: function WeaveSvc__onStartup() { + let self = yield; + this._initLogs(); + this._log.info("Weave Service Initializing"); + + Utils.prefs.addObserver("", this, false); + this._os.addObserver(this, "quit-application", true); + FaultTolerance.Service; // initialize FT service + + if (!this.enabled) + this._log.info("Weave Sync disabled"); + + this._setSchedule(this.schedule); + + // Create Weave identities (for logging in, and for encryption) + ID.set('WeaveID', new Identity('Mozilla Services Password', this.username)); + Auth.defaultAuthenticator = new BasicAuthenticator(ID.get('WeaveID')); + + ID.set('WeaveCryptoID', + new Identity('Mozilla Services Encryption Passphrase', this.username)); + + let url = this.baseURL + this.username; + PubKeys.defaultKeyUri = url + "keys/pubkey"; + + if (Utils.prefs.getBoolPref("autoconnect") && + this.username && this.username != 'nobody') + try { + yield this.login(self.cb); + yield this.sync(self.cb); + } catch (e) {} + self.done(); + }, + onStartup: function WeaveSvc_onStartup() { + this._onStartup.async(this); + }, + _initLogs: function WeaveSvc__initLogs() { this._log = Log4Moz.repository.getLogger("Service.Main"); this._log.level = @@ -326,12 +353,6 @@ WeaveSvc.prototype = { this._debugApp.clear(); }, -/* - * deleted: - * server version checks - * server user dir check (existence) - * caching keypair (not needed?) - */ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), @@ -354,6 +375,7 @@ WeaveSvc.prototype = { }, _onQuitApplication: function WeaveSvc__onQuitApplication() { +/* if (!this.enabled || !this._loggedIn) return; @@ -379,15 +401,20 @@ WeaveSvc.prototype = { "Weave:Status", "chrome,centerscreen,modal,close=0", null); +*/ }, // These are global (for all engines) - verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, - password, passphrase) { - this._localLock(this._notify("verify-passphrase", "", this._verifyPassphrase, - username, password, passphrase)). - async(this, onComplete); + _verifyLogin: function WeaveSvc__verifyLogin(username, password) { + let self = yield; + this._log.debug("Verifying login for user " + username); + let res = new Resource(this.baseURL + username); + yield res.get(self.cb); + }, + verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password) { + this._localLock(this._notify("verify-login", "", this._verifyLogin, + username, password)).async(this, onComplete); }, _verifyPassphrase: function WeaveSvc__verifyPassphrase(username, password, @@ -402,96 +429,18 @@ WeaveSvc.prototype = { let id = new Identity('Passphrase Verification', username); id.setTempPassword(passphrase); - // FIXME: abstract common bits and share code with getKeypair() + let pubkey = yield PubKeys.getDefaultKey(self.cb); + let privkey = yield PrivKeys.get(self.cb, pubkey.PrivKeyUri); - // XXX: We're not checking the version of the server here, in part because - // we have no idea what to do if the version is different than we expect - // it to be. - // XXX check it and ... throw? - - this.username = username; - ID.get('WeaveID').setTempPassword(password); - - let privkeyResp = yield DAV.GET("private/privkey", self.cb); - let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); - - // FIXME: this will cause 404's to turn into a 'success' - // while technically incorrect, this function currently only gets - // called from the wizard, which will do a loginAndInit, which - // will create the keys if necessary later - if (privkeyResp.status == 404 || pubkeyResp.status == 404) - return; - - Utils.ensureStatus(privkeyResp.status, "Could not download private key"); - Utils.ensureStatus(privkeyResp.status, "Could not download public key"); - - let privkey = this._json.decode(privkeyResp.responseText); - let pubkey = this._json.decode(pubkeyResp.responseText); - - if (!privkey || !pubkey) - throw "Bad keypair JSON"; - if (privkey.version != 1 || pubkey.version != 1) - throw "Unexpected keypair data version"; - if (privkey.algorithm != "RSA" || pubkey.algorithm != "RSA") - throw "Only RSA keys currently supported"; - if (!privkey.privkey) - throw "Private key does not contain private key data!"; - if (!pubkey.pubkey) - throw "Public key does not contain public key data!"; - - id.keypairAlg = privkey.algorithm; - id.privkey = privkey.privkey; - id.privkeyWrapIV = privkey.privkeyIV; - id.passphraseSalt = privkey.privkeySalt; - id.pubkey = pubkey.pubkey; - - if (!(yield Crypto.isPassphraseValid.async(Crypto, self.cb, id))) - throw new Error("Passphrase is not valid."); + // fixme: decrypt something here }, - - verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password) { - this._localLock(this._notify("verify-login", "", this._verifyLogin, - username, password)).async(this, onComplete); - }, - - _verifyLogin: function WeaveSvc__verifyLogin(username, password) { - let self = yield; - - this._log.debug("Verifying login for user " + username); - - DAV.baseURL = Utils.prefs.getCharPref("serverURL"); - DAV.defaultPrefix = "user/" + username; - - this._log.config("Using server URL: " + DAV.baseURL + DAV.defaultPrefix); - - let status = yield DAV.checkLogin.async(DAV, self.cb, username, password); - Utils.ensureStatus(status, "Login verification failed"); - }, - - loginAndInit: function WeaveSvc_loginAndInit(onComplete, - username, password, passphrase) { - this._localLock(this._notify("login", "", this._loginAndInit, + verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, + password, passphrase) { + this._localLock(this._notify("verify-passphrase", "", this._verifyPassphrase, username, password, passphrase)). async(this, onComplete); }, - _loginAndInit: function WeaveSvc__loginAndInit(username, password, passphrase) { - let self = yield; - try { - yield this._login.async(this, self.cb, username, password, passphrase); - } catch (e) { - // we might need to initialize before login will work (e.g. to create the - // user directory), so do this and try again... - yield this._initialize.async(this, self.cb); - yield this._login.async(this, self.cb, username, password, passphrase); - } - yield this._initialize.async(this, self.cb); - }, - login: function WeaveSvc_login(onComplete, username, password, passphrase) { - this._localLock( - this._notify("login", "", this._login, - username, password, passphrase)).async(this, onComplete); - }, _login: function WeaveSvc__login(username, password, passphrase) { let self = yield; @@ -514,47 +463,16 @@ WeaveSvc.prototype = { this._loggedIn = true; self.done(true); }, - - initialize: function WeaveSvc_initialize() { + login: function WeaveSvc_login(onComplete, username, password, passphrase) { this._localLock( - this._notify("initialize", "", this._initialize)).async(this, onComplete); - }, - - _initialize: function WeaveSvc__initialize() { - let self = yield; - - this._log.info("Making sure server is initialized..."); - - // cache keys, create public/private keypair if it doesn't exist - this._log.debug("Caching keys"); - let privkeyResp = yield DAV.GET("private/privkey", self.cb); - let pubkeyResp = yield DAV.GET("public/pubkey", self.cb); - - if (privkeyResp.status == 404 || pubkeyResp.status == 404) { - yield this._generateKeys.async(this, self.cb); - privkeyResp = yield DAV.GET("private/privkey", self.cb); - pubkeyResp = yield DAV.GET("public/pubkey", self.cb); - } - - Utils.ensureStatus(privkeyResp.status, "Cannot initialize privkey"); - Utils.ensureStatus(pubkeyResp.status, "Cannot initialize pubkey"); - - this._keyPair['private'] = this._json.decode(privkeyResp.responseText); - this._keyPair['public'] = this._json.decode(pubkeyResp.responseText); - - yield this._getKeypair.async(this, self.cb); // makes sure passphrase works - - this._setSchedule(this.schedule); - - this._initialized = true; - self.done(true); + this._notify("login", "", this._login, + username, password, passphrase)).async(this, onComplete); }, logout: function WeaveSvc_logout() { this._log.info("Logging out"); this._disableSchedule(); this._loggedIn = false; - this._initialized = false; this._keyPair = {}; ID.get('WeaveID').setTempPassword(null); // clear cached password ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase @@ -564,40 +482,19 @@ WeaveSvc.prototype = { serverWipe: function WeaveSvc_serverWipe(onComplete) { let cb = function WeaveSvc_serverWipeCb() { let self = yield; - this._serverWipe.async(this, self.cb); - yield; + this._log.error("Server wipe not supported"); this.logout(); - self.done(); }; this._notify("server-wipe", "", this._localLock(cb)).async(this, onComplete); }, - _serverWipe: function WeaveSvc__serverWipe() { - let self = yield; - - this._keyPair = {}; - DAV.listFiles.async(DAV, self.cb); - let names = yield; - - for (let i = 0; i < names.length; i++) { - if (names[i].match(/\.htaccess$/)) - continue; - DAV.DELETE(names[i], self.cb); - let resp = yield; - this._log.debug(resp.status); - } - }, // These are per-engine - sync: function WeaveSvc_sync(onComplete) { - this._notify("sync", "", - this._catchAll(this._localLock(this._sync))).async(this, onComplete); - }, - _sync: function WeaveSvc__sync() { let self = yield; - yield ClientData.refresh(self.cb); + //this._log.debug("Refreshing client list"); + //yield ClientData.refresh(self.cb); let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { @@ -607,6 +504,7 @@ WeaveSvc.prototype = { if (!engines[i].enabled) continue; + this._log.debug("Syncing engine " + engines[i].name); yield this._notify(engines[i].name + "-engine:sync", "", this._syncEngine, engines[i]).async(this, self.cb); } @@ -616,6 +514,10 @@ WeaveSvc.prototype = { throw "Some engines did not sync correctly"; } }, + sync: function WeaveSvc_sync(onComplete) { + this._notify("sync", "", + this._catchAll(this._localLock(this._sync))).async(this, onComplete); + }, // The values that engine scores must meet or exceed before we sync them // as needed. These are engine-specific, as different kinds of data change @@ -686,10 +588,6 @@ WeaveSvc.prototype = { } }, - resetServer: function WeaveSvc_resetServer(onComplete) { - this._notify("reset-server", "", - this._localLock(this._resetServer)).async(this, onComplete); - }, _resetServer: function WeaveSvc__resetServer() { let self = yield; @@ -701,11 +599,11 @@ WeaveSvc.prototype = { yield; } }, - - resetClient: function WeaveSvc_resetClient(onComplete) { - this._localLock(this._notify("reset-client", "", - this._resetClient)).async(this, onComplete); + resetServer: function WeaveSvc_resetServer(onComplete) { + this._notify("reset-server", "", + this._localLock(this._resetServer)).async(this, onComplete); }, + _resetClient: function WeaveSvc__resetClient() { let self = yield; let engines = Engines.getAll(); @@ -716,28 +614,32 @@ WeaveSvc.prototype = { yield; } }, + resetClient: function WeaveSvc_resetClient(onComplete) { + this._localLock(this._notify("reset-client", "", + this._resetClient)).async(this, onComplete); + } /*, shareData: function WeaveSvc_shareData(dataType, isShareEnabled, onComplete, guid, username) { - /* Shares data of the specified datatype (which must correspond to - one of the registered engines) with the user specified by username. - The data node indicated by guid will be shared, along with all its - children, if it has any. onComplete is a function that will be called - when sharing is done; it takes an argument that will be true or false - to indicate whether sharing succeeded or failed. - Implementation, as well as the interpretation of what 'guid' means, - is left up to the engine for the specific dataType. + // Shares data of the specified datatype (which must correspond to + // one of the registered engines) with the user specified by username. + // The data node indicated by guid will be shared, along with all its + // children, if it has any. onComplete is a function that will be called + // when sharing is done; it takes an argument that will be true or false + // to indicate whether sharing succeeded or failed. + // Implementation, as well as the interpretation of what 'guid' means, + // is left up to the engine for the specific dataType. - isShareEnabled: true to start sharing, false to stop sharing.*/ + // isShareEnabled: true to start sharing, false to stop sharing. let messageName = "share-" + dataType; - /* so for instance, if dataType is "bookmarks" then a message - "share-bookmarks" will be sent out to any observers who are listening - for it. As far as I know, there aren't currently any listeners for - "share-bookmarks" but we'll send it out just in case. */ + // so for instance, if dataType is "bookmarks" then a message + // "share-bookmarks" will be sent out to any observers who are listening + // for it. As far as I know, there aren't currently any listeners for + // "share-bookmarks" but we'll send it out just in case. let self = this; let saved_dataType = dataType; @@ -767,7 +669,7 @@ WeaveSvc.prototype = { }; if (Weave.DAV.locked) { - /* then we have to wait until it's not locked. */ + // then we have to wait until it's not locked. dump( "DAV is locked, gonna set up observer to do it later.\n"); os.addObserver( observer, successMsg, true ); os.addObserver( observer, errorMsg, true ); @@ -797,4 +699,5 @@ WeaveSvc.prototype = { } self.done(ret); } +*/ }; From 9d508ecdd757f10c82c6397b5a3c75b3b2e25f4c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 17:37:48 -0800 Subject: [PATCH 0701/1860] small module to get an event at startup --- services/sync/Weave.js | 67 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 services/sync/Weave.js diff --git a/services/sync/Weave.js b/services/sync/Weave.js new file mode 100644 index 000000000000..a03b37b6f456 --- /dev/null +++ b/services/sync/Weave.js @@ -0,0 +1,67 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function WeaveService() {} +WeaveService.prototype = { + classDescription: "Weave Service", + contractID: "@mozilla.org/weave/service;1", + classID: Components.ID("{74b89fb0-f200-4ae8-a3ec-dd164117f6de}"), + _xpcom_categories: [{ category: "app-startup", service: true }], + + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver, + Components.interfaces.nsISupportsWeakReference]), + + observe: function BSS__observe(subject, topic, data) { + switch (topic) { + case "app-startup": + let os = Components.classes["@mozilla.org/observer-service;1"]. + getService(Components.interfaces.nsIObserverService); + os.addObserver(this, "profile-after-change", true); + break; + case "profile-after-change": + Components.utils.import("resource://weave/service.js"); + Weave.Service.onStartup(); + break; + } + } +}; + +function NSGetModule(compMgr, fileSpec) { + return XPCOMUtils.generateModule([WeaveService]); +} + From 3a19ebd9b31a2bed70df1a7a123bca8463df4be7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 17:38:35 -0800 Subject: [PATCH 0702/1860] url -> uri --- services/sync/tests/unit/test_records_crypto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 78c1d1b08c30..db6940835943 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -61,7 +61,7 @@ function async_test() { let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); Auth.defaultAuthenticator = auth; - PubKeys.defaultKeyUrl = "http://localhost:8080/pubkey"; + PubKeys.defaultKeyUri = "http://localhost:8080/pubkey"; server = httpd_setup({"/pubkey": pubkey_handler, "/privkey": privkey_handler, From 09b5d6ba7c319d9554ae68907d343f3a488be6bb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 19:18:07 -0800 Subject: [PATCH 0703/1860] add a temp NewEngine class that will talk to the 0.3 server --- services/sync/modules/engines.js | 42 ++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5dcd4f651ec0..7567319f6a1f 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,7 +35,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine', 'BlobEngine']; +const EXPORTED_SYMBOLS = ['Engines', 'NewEngine', 'Engine', 'SyncEngine', 'BlobEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -111,12 +111,6 @@ Engine.prototype = { // "user-data/default-engine/"; get serverPrefix() { throw "serverPrefix property must be overridden in subclasses"; }, - get _remote() { - let remote = new RemoteStore(this); - this.__defineGetter__("_remote", function() remote); - return remote; - }, - get enabled() { return Utils.prefs.getBoolPref("engine." + this.name); }, @@ -191,7 +185,7 @@ Engine.prototype = { _resetServer: function Engine__resetServer() { let self = yield; - yield this._remote.wipe(self.cb); + throw "_resetServer needs to be subclassed"; }, _resetClient: function Engine__resetClient() { @@ -248,16 +242,37 @@ Engine.prototype = { } }; +function NewEngine() {} +NewEngine.prototype = { + __proto__: Engine.prototype, + + _sync: function NewEngine__sync() { + let self = yield; + self.done(); + } +}; + function SyncEngine() {} SyncEngine.prototype = { __proto__: new Engine(), + get _remote() { + let remote = new RemoteStore(this); + this.__defineGetter__("_remote", function() remote); + return remote; + }, + get _snapshot() { let snap = new SnapshotStore(this.name); this.__defineGetter__("_snapshot", function() snap); return snap; }, + _resetServer: function SyncEngine__resetServer() { + let self = yield; + yield this._remote.wipe(self.cb); + }, + _resetClient: function SyncEngine__resetClient() { let self = yield; this._log.debug("Resetting client state"); @@ -529,12 +544,23 @@ function HeuristicEngine() { HeuristicEngine.prototype = { __proto__: new Engine(), + get _remote() { + let remote = new RemoteStore(this); + this.__defineGetter__("_remote", function() remote); + return remote; + }, + get _snapshot() { let snap = new SnapshotStore(this.name); this.__defineGetter__("_snapshot", function() snap); return snap; }, + _resetServer: function SyncEngine__resetServer() { + let self = yield; + yield this._remote.wipe(self.cb); + }, + _resetClient: function SyncEngine__resetClient() { let self = yield; this._log.debug("Resetting client state"); From fa9dadc4a8393cf29caf324ee8514f2c10f8b6fb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 19:18:46 -0800 Subject: [PATCH 0704/1860] remove some sharing code, derive from NewEngine --- services/sync/modules/engines/bookmarks.js | 49 ++++------------------ 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 2cf050850393..0d169e3af526 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -717,7 +717,8 @@ function BookmarksEngine(pbeId) { this._init(pbeId); } BookmarksEngine.prototype = { - __proto__: new SyncEngine(), + __proto__: NewEngine.prototype, + get _super() NewEngine.prototype, get name() { return "bookmarks"; }, get displayName() { return "Bookmarks"; }, @@ -743,42 +744,10 @@ BookmarksEngine.prototype = { if (!this.__tracker) this.__tracker = new BookmarksTracker(); return this.__tracker; - }, - - __sharing: null, - get _sharing() { - if (!this.__sharing) - this.__sharing = new BookmarksSharingManager(this); - return this.__sharing; - }, - - _sync: function BmkEngine__sync() { - /* After syncing the regular bookmark folder contents, - * also update both the incoming and outgoing shared folders. */ - let self = yield; - //let ret = yield this._sharing.getNewShares(self.cb); - this.__proto__.__proto__._sync.async(this, self.cb ); - yield; - //this._sharing.updateAllOutgoingShares(self.cb); - //yield; - //this._sharing.updateAllIncomingShares(self.cb); - //yield; - self.done(); - }, - - _share: function BmkEngine__share(guid, username) { - let self = yield; - this._sharing._share.async( this._sharing, self.cb, guid, username); - yield; - self.done(true); - }, - - _stopSharing: function BmkEngine__stopSharing(guid, username) { - let self = yield; - this._sharing._stopSharing.async( this._sharing, self.cb, guid, username); - yield; - self.done(); } + + // XXX for sharing, will need to re-add code to get new shares before syncing, + // and updating incoming/outgoing shared folders after syncing }; function BookmarksSyncCore(store) { @@ -786,6 +755,7 @@ function BookmarksSyncCore(store) { this._init(); } BookmarksSyncCore.prototype = { + __proto__: SyncCore.prototype, _logName: "BMSync", _store: null, @@ -867,12 +837,12 @@ BookmarksSyncCore.prototype = { } } }; -BookmarksSyncCore.prototype.__proto__ = new SyncCore(); function BookmarksStore() { this._init(); } BookmarksStore.prototype = { + __proto__: Store.prototype, _logName: "BStore", _lookup: null, @@ -1369,7 +1339,6 @@ BookmarksStore.prototype = { this.__resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); } }; -BookmarksStore.prototype.__proto__ = new Store(); /* * Tracker objects for each engine may need to subclass the @@ -1386,6 +1355,7 @@ function BookmarksTracker() { this._init(); } BookmarksTracker.prototype = { + __proto__: Tracker.prototype, _logName: "BMTracker", /* We don't care about the first three */ @@ -1422,5 +1392,4 @@ BookmarksTracker.prototype = { getService(Ci.nsINavBookmarksService). addObserver(this, false); } -} -BookmarksTracker.prototype.__proto__ = new Tracker(); +}; From 6156541a0a828b3a896c45cd6e8e6003f2e7bbfa Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 19:19:32 -0800 Subject: [PATCH 0705/1860] say sync complete when it is! --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 26e28c907bc5..d28d12630dae 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -513,6 +513,8 @@ WeaveSvc.prototype = { this._syncError = false; throw "Some engines did not sync correctly"; } + + this._log.debug("Sync complete"); }, sync: function WeaveSvc_sync(onComplete) { this._notify("sync", "", From 8cd1779ecbf89ffa64dda18560167cd5ea2f36bf Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 6 Nov 2008 23:23:35 -0800 Subject: [PATCH 0706/1860] make sure records always have a data field that represents their state (e.g. for serialization); add some pki routines (createKeypair); add incoming/outgoing queue to NewEngine --- services/sync/modules/base_records/crypto.js | 8 ++- services/sync/modules/base_records/keys.js | 66 ++++++++++++++------ services/sync/modules/base_records/wbo.js | 14 +++++ services/sync/modules/clientData.js | 4 +- services/sync/modules/engines.js | 31 ++++++++- services/sync/modules/resource.js | 32 +++++----- 6 files changed, 115 insertions(+), 40 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 675ab8b181d6..672533a12b4d 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -62,6 +62,7 @@ CryptoWrapper.prototype = { _CryptoWrap_init: function CryptoWrap_init(uri, authenticator) { this._WBORec_init(uri, authenticator); + this.data.payload = ""; }, _encrypt: function CryptoWrap__encrypt(passphrase) { @@ -114,6 +115,11 @@ CryptoMeta.prototype = { _CryptoMeta_init: function CryptoMeta_init(uri, authenticator) { this._WBORec_init(uri, authenticator); + this.data.payload = { + salt: null, + iv: null, + keyring: {} + }; }, get salt() this.data.payload.salt, @@ -138,7 +144,7 @@ CryptoMeta.prototype = { wrappedKey = this.data.payload.keyring[relUri]; } if (!wrappedKey) - throw "keyring doesn't contain a key for " + pubKeyUrl; + throw "keyring doesn't contain a key for " + pubKeyUri; let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. createInstance(Ci.IWeaveCrypto); diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 43db95056322..a581f5312746 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -61,26 +61,35 @@ function PubKey(uri, authenticator) { } PubKey.prototype = { __proto__: WBORecord.prototype, + _logName: "Record.PubKey", _PubKey_init: function PubKey_init(uri, authenticator) { this._WBORec_init(uri, authenticator); + this.data.payload = { + type: "pubkey", + key_data: null, + private_key: null + }; }, - get keyData() { - if (!this.data) - return null; - return this.data.payload.key_data; + get keyData() this.data.payload.key_data, + set keyData(value) { + this.data.payload.key_data = value; + }, + get _privateKeyUri() this.data.payload.private_key, + set _privateKeyUri(value) { + this.data.payload.private_key = value; }, get privateKeyUri() { if (!this.data) return null; // resolve - let uri = this.uri.resolve(this.data.payload.private_key); + let uri = this.uri.resolve(this._privateKeyUri); if (uri) return Utils.makeURI(uri); // does not resolve, return raw (this uri type might not be able to resolve) - return Utils.makeURI(this.data.payload.private_key); + return Utils.makeURI(this._privateKeyUri); } }; @@ -89,15 +98,24 @@ function PrivKey(uri, authenticator) { } PrivKey.prototype = { __proto__: WBORecord.prototype, + _logName: "Record.PrivKey", _PrivKey_init: function PrivKey_init(uri, authenticator) { this._WBORec_init(uri, authenticator); + this.data.payload = { + type: "privkey", + key_data: null, + private_key: null + }; }, - get keyData() { - if (!this.data) - return null; - return this.data.payload.key_data; + get keyData() this.data.payload.key_data, + set keyData(value) { + this.data.payload.key_data = value; + }, + get _publicKeyUri() this.payload.public_key, + set _publicKeyUri(value) { + this.data.payload.public_key = value; }, get privateKeyUri() { @@ -108,14 +126,15 @@ PrivKey.prototype = { if (!this.data) return null; // resolve - let uri = this.uri.resolve(this.data.payload.public_key); + let uri = this.uri.resolve(this._publicKeyUri); if (uri) return Utils.makeURI(uri); // does not resolve, return raw (this uri type might not be able to resolve) - return Utils.makeURI(this.data.payload.public_key); + return Utils.makeURI(this._publicKeyUri); } }; +// XXX unused/unfinished function SymKey(keyData, wrapped) { this._init(keyData, wrapped); } @@ -147,7 +166,7 @@ PubKeyManager.prototype = { _keyType: PubKey, _logName: "PubKeyManager", - get defaultKeyUri() this._defaultKeyUrl, + get defaultKeyUri() this._defaultKeyUri, set defaultKeyUri(value) { this._defaultKeyUri = value; }, _getDefaultKey: function KeyMgr__getDefaultKey() { @@ -159,6 +178,18 @@ PubKeyManager.prototype = { return this._getDefaultKey.async(this, onComplete); }, + createKeypair: function KeyMgr_createKeypair() { + let pubOut = {}, privOut = {}; + let cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + cryptoSvc.generateKeypair("my passphrase", salt, iv, pubOut, privOut); + let pubkey = new PubKey(); + let privkey = new PrivKey(); + pubkey.keyData = pubOut.value; + privkey.keyData = privOut.value; + return {pubkey: pubkey, privkey: privkey}; + }, + _init: function KeyMgr__init() { this._log = Log4Moz.repository.getLogger(this._logName); this._keys = {}; @@ -167,21 +198,18 @@ PubKeyManager.prototype = { _import: function KeyMgr__import(url) { let self = yield; - let ret = null; this._log.trace("Importing key: " + (url.spec? url.spec : url)); try { let key = new this._keyType(url); - let foo = yield key.get(self.cb); + yield key.get(self.cb); this.set(url, key); - ret = key; + self.done(key); } catch (e) { this._log.debug("Failed to import key: " + e); - // don't do anything else, we'll just return null + self.done(null); } - - self.done(ret); }, import: function KeyMgr_import(onComplete, url) { this._import.async(this, onComplete, url); diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 2715899022a7..c9c882b66a40 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -57,5 +57,19 @@ WBORecord.prototype = { _WBORec_init: function WBORec_init(uri, authenticator) { this._init(uri, authenticator); this.pushFilter(new JsonFilter()); + this.data = { + modified: "2454725.98283", // FIXME + payload: {} + }; + }, + + get modified() this.data.modified, + set modified(value) { + this.data.modified = value; + }, + + get payload() this.data.payload, + set payload(value) { + this.data.payload = value; } }; diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index 5d7988762b0e..9aad2d84330d 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -11,10 +11,10 @@ * for the specific language governing rights and limitations under the * License. * - * The Original Code is Bookmarks Sync. + * The Original Code is Weave. * * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 + * Portions created by the Initial Developer are Copyright (C) 2008 * the Initial Developer. All Rights Reserved. * * Contributor(s): diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 7567319f6a1f..181e1b9170dd 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -246,8 +246,37 @@ function NewEngine() {} NewEngine.prototype = { __proto__: Engine.prototype, + get lastSync() Utils.prefs.getCharPref(this.name + ".lastSync"), + set lastSync(value) { + Utils.prefs.setCharPref(this.name + ".lastSync", value); + }, + + _incoming: null, + get incoming() { + if (!this._incoming) + this._incoming = []; + return this._incoming; + }, + + _outgoing: null, + get outgoing() { + if (!this._outgoing) + this._outgoing = []; + return this._outgoing; + }, + _sync: function NewEngine__sync() { let self = yield; + // find new items from server, place into incoming queue + + // if snapshot-based, generate outgoing queue + + // apply incoming queue one by one + + // upload new/changed items from our outgoing queue + foreach (this.outgoing) { + // ... + } self.done(); } }; @@ -475,7 +504,7 @@ function BlobEngine() { // we don't call it here because it requires serverPrefix to be set } BlobEngine.prototype = { - __proto__: new Engine(), + __proto__: Engine.prototype, get _profileID() { return ClientData.GUID; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 2d737da00619..2691ef5d69f5 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -109,42 +109,40 @@ Resource.prototype = { this._uri.spec = value; }, - get data() { - return this._data; - }, + _data: null, + get data() this._data, set data(value) { this._dirty = true; this._data = value; }, - get lastRequest() { return this._lastRequest; }, - get downloaded() { return this._downloaded; }, - get dirty() { return this._dirty; }, + _lastRequest: null, + _downloaded: false, + _dirty: false, + get lastRequest() this._lastRequest, + get downloaded() this._downloaded, + get dirty() this._dirty, + _filters: null, pushFilter: function Res_pushFilter(filter) { + if (!this._filters) + this._filters = []; this._filters.push(filter); }, - popFilter: function Res_popFilter() { return this._filters.pop(); }, - clearFilters: function Res_clearFilters() { this._filters = []; }, _init: function Res__init(uri, authenticator) { + this._log = Log4Moz.repository.getLogger(this._logName); if (typeof uri == 'string') uri = Utils.makeURI(uri); this._uri = uri; this._authenticator = authenticator; this._headers = {'Content-type': 'text/plain'}; - this._data = null; - this._downloaded = false; - this._dirty = false; - this._filters = []; - this._lastRequest = null; - this._log = Log4Moz.repository.getLogger(this._logName); }, _createRequest: function Res__createRequest(op, onRequestFinished) { @@ -198,7 +196,7 @@ Resource.prototype = { if ("PUT" == action) { for each (let filter in this._filters) { - data = yield filter.beforePUT.async(filter, self.cb, data); + data = yield filter.beforePUT.async(filter, self.cb, data, this); } } @@ -224,7 +222,7 @@ Resource.prototype = { this._data = this._lastRequest.responseText; let filters = this._filters.slice(); // reverse() mutates, so we copy for each (let filter in filters.reverse()) { - this._data = yield filter.afterGET.async(filter, self.cb, this._data); + this._data = yield filter.afterGET.async(filter, self.cb, this._data, this); } } break; @@ -270,7 +268,7 @@ Resource.prototype = { function JsonFilter() { - this._log = Log4Moz.repository.getLogger("Service.JsonFilter"); + this._log = Log4Moz.repository.getLogger("Net.JsonFilter"); } JsonFilter.prototype = { get _json() { From e7e90a9a3e5a644b80072c5616a3dacc6cf2fe7e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 8 Nov 2008 02:00:33 -0800 Subject: [PATCH 0707/1860] crypto changes: private keys get their own iv and salt, crypto meta records get a separate iv and no salt. various fixes in crypto code and other wbo related objects. fix crypto tests to match. --- services/sync/modules/base_records/crypto.js | 186 ++++++++++++++---- services/sync/modules/base_records/keys.js | 85 +++++--- services/sync/modules/base_records/wbo.js | 19 ++ services/sync/modules/engines.js | 27 ++- services/sync/modules/resource.js | 19 +- services/sync/modules/service.js | 5 +- .../sync/tests/unit/test_records_crypto.js | 74 +++---- 7 files changed, 295 insertions(+), 120 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 672533a12b4d..2ead946c3444 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['CryptoWrapper']; +const EXPORTED_SYMBOLS = ['CryptoWrapper', 'CryptoMeta']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -53,6 +53,10 @@ Cu.import("resource://weave/base_records/keys.js"); Function.prototype.async = Async.sugar; +// fixme: global, ugh +let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); +let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"].createInstance(Ci.IWeaveCrypto); + function CryptoWrapper(uri, authenticator) { this._CryptoWrap_init(uri, authenticator); } @@ -61,23 +65,33 @@ CryptoWrapper.prototype = { _logName: "Record.CryptoWrapper", _CryptoWrap_init: function CryptoWrap_init(uri, authenticator) { + // FIXME: this will add a json filter, meaning our payloads will be json + // encoded, even though they are already a string this._WBORec_init(uri, authenticator); + this.data.encryption = ""; this.data.payload = ""; }, + // FIXME: we make no attempt to ensure cleartext is in sync + // with the encrypted payload + cleartext: null, + + get encryption() this.data.encryption, + set encryption(value) { + this.data.encryption = value; + }, + _encrypt: function CryptoWrap__encrypt(passphrase) { let self = yield; let pubkey = yield PubKeys.getDefaultKey(self.cb); let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); - let meta = new CryptoMeta(this.data.encryption); // FIXME: cache! + let meta = new CryptoMeta(this.encryption); // FIXME: cache! yield meta.get(self.cb); - let symkey = meta.getKey(pubkey, privkey, passphrase); + let symkey = yield meta.getKey(self.cb, privkey, passphrase); - let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - createInstance(Ci.IWeaveCrypto); - this.data.payload = crypto.encrypt(this.cleartext, symkey, meta.iv); + this.payload = crypto.encrypt(json.encode([this.cleartext]), symkey, meta.bulkIV); self.done(); }, @@ -91,13 +105,11 @@ CryptoWrapper.prototype = { let pubkey = yield PubKeys.getDefaultKey(self.cb); let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); - let meta = new CryptoMeta(this.data.encryption); // FIXME: cache! + let meta = new CryptoMeta(this.encryption); // FIXME: cache! yield meta.get(self.cb); - let symkey = meta.getKey(pubkey, privkey, passphrase); + let symkey = yield meta.getKey(self.cb, privkey, passphrase); - let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - createInstance(Ci.IWeaveCrypto); - this.cleartext = crypto.decrypt(this.data.payload, symkey, meta.iv); + this.cleartext = json.decode(crypto.decrypt(this.payload, symkey, meta.bulkIV))[0]; self.done(this.cleartext); }, @@ -116,39 +128,143 @@ CryptoMeta.prototype = { _CryptoMeta_init: function CryptoMeta_init(uri, authenticator) { this._WBORec_init(uri, authenticator); this.data.payload = { - salt: null, - iv: null, + bulkIV: null, keyring: {} }; }, - get salt() this.data.payload.salt, - set salt(value) { this.data.payload.salt = value; }, + generateIV: function CryptoMeta_generateIV() { + this.bulkIV = crypto.generateRandomIV(); + }, - get iv() this.data.payload.iv, - set iv(value) { this.data.payload.iv = value; }, + get bulkIV() this.data.payload.bulkIV, + set bulkIV(value) { this.data.payload.bulkIV = value; }, - getKey: function getKey(pubKey, privKey, passphrase) { - let wrappedKey; + _getKey: function CryptoMeta__getKey(privkey, passphrase) { + let self = yield; + let wrapped_key; - // get the full uri to our public key - let pubKeyUri; - if (typeof pubKey == 'string') - pubKeyUri = this.uri.resolve(pubKey); - else - pubKeyUri = pubKey.spec; + // get the uri to our public key + let pubkeyUri = privkey.publicKeyUri.spec; // each hash key is a relative uri, resolve those and match against ours - for (let relUri in this.data.payload.keyring) { - if (pubKeyUri == this.uri.resolve(relUri)) - wrappedKey = this.data.payload.keyring[relUri]; + for (let relUri in this.payload.keyring) { + if (pubkeyUri == this.uri.resolve(relUri)) + wrapped_key = this.payload.keyring[relUri]; } - if (!wrappedKey) - throw "keyring doesn't contain a key for " + pubKeyUri; + if (!wrapped_key) + throw "keyring doesn't contain a key for " + pubkeyUri; - let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - createInstance(Ci.IWeaveCrypto); - return crypto.unwrapSymmetricKey(wrappedKey, privKey.keyData, - passphrase, this.salt, this.iv); + let ret = crypto.unwrapSymmetricKey(wrapped_key, privkey.keyData, + passphrase, privkey.salt, privkey.iv); + self.done(ret); + }, + getKey: function CryptoMeta_getKey(onComplete, privkey, passphrase) { + this._getKey.async(this, onComplete, privkey, passphrase); + }, + + _addKey: function CryptoMeta__addKey(new_pubkey, privkey, passphrase) { + let self = yield; + let symkey = yield this.getKey(self.cb, privkey, passphrase); + yield this.addUnwrappedKey(self.cb, new_pubkey, symkey); + }, + addKey: function CryptoMeta_addKey(onComplete, new_pubkey, privkey, passphrase) { + this._addKey.async(this, onComplete, new_pubkey, privkey, passphrase); + }, + + _addUnwrappedKey: function CryptoMeta__addUnwrappedKey(new_pubkey, symkey) { + let self = yield; + + // get the new public key + if (typeof new_pubkey == 'string') + new_pubkey = PubKeys.get(self.cb, new_pubkey); + + // each hash key is a relative uri, resolve those and + // if we find the one we're about to add, remove it + for (let relUri in this.payload.keyring) { + if (pubkeyUri == this.uri.resolve(relUri)) + delete this.payload.keyring[relUri]; + } + + this.payload.keyring[new_pubkey.uri.spec] = + crypto.wrapSymmetricKey(symkey, new_pubkey.keyData); + }, + addUnwrappedKey: function CryptoMeta_addUnwrappedKey(onComplete, new_pubkey, symkey) { + this._addUnwrappedKey.async(this, onComplete, new_pubkey, symkey); } -}; \ No newline at end of file +}; + +function CryptoMetaManager() { + this._init(); +} +CryptoMetaManager.prototype = { + _init: function CryptoMetaMgr__init() { + this._log = Log4Moz.repository.getLogger("CryptoMetaMgr"); + this._records = {}; + this._aliases = {}; + }, + + _import: function CryptoMetaMgr__import(url) { + let self = yield; + + this._log.trace("Importing record: " + (url.spec? url.spec : url)); + + try { + let rec = new CryptoMeta(url); + yield rec.get(self.cb); + this.set(url, rec); + self.done(rec); + } catch (e) { + this._log.debug("Failed to import record: " + e); + self.done(null); + } + }, + import: function CryptoMetaMgr_import(onComplete, url) { + this._import.async(this, onComplete, url); + }, + + _get: function CryptoMetaMgr__get(url) { + let self = yield; + + let rec = null; + if (url in this._aliases) + url = this._aliases[url]; + if (url in this._records) + rec = this._keys[url]; + + if (!key) + rec = yield this.import(self.cb, url); + + self.done(rec); + }, + get: function KeyMgr_get(onComplete, url) { + this._get.async(this, onComplete, url); + }, + + set: function KeyMgr_set(url, key) { + this._keys[url] = key; + return key; + }, + + contains: function KeyMgr_contains(url) { + let key = null; + if (url in this._aliases) + url = this._aliases[url]; + if (url in this._keys) + return true; + return false; + }, + + del: function KeyMgr_del(url) { + delete this._keys[url]; + }, + getAlias: function KeyMgr_getAlias(alias) { + return this._aliases[alias]; + }, + setAlias: function KeyMgr_setAlias(url, alias) { + this._aliases[alias] = url; + }, + delAlias: function KeyMgr_delAlias(alias) { + delete this._aliases[alias]; + } +}; diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index a581f5312746..07ce1b696dc5 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -77,10 +77,6 @@ PubKey.prototype = { this.data.payload.key_data = value; }, get _privateKeyUri() this.data.payload.private_key, - set _privateKeyUri(value) { - this.data.payload.private_key = value; - }, - get privateKeyUri() { if (!this.data) return null; @@ -90,6 +86,13 @@ PubKey.prototype = { return Utils.makeURI(uri); // does not resolve, return raw (this uri type might not be able to resolve) return Utils.makeURI(this._privateKeyUri); + }, + set privateKeyUri(value) { + this.data.payload.private_key = value; + }, + + get publicKeyUri() { + throw "attempted to get public key url from a public key!"; } }; @@ -104,33 +107,42 @@ PrivKey.prototype = { this._WBORec_init(uri, authenticator); this.data.payload = { type: "privkey", + salt: null, + iv: null, key_data: null, - private_key: null + public_key: null }; }, + get salt() this.data.payload.salt, + set salt(value) { + this.data.payload.salt = value; + }, + get iv() this.data.payload.iv, + set iv(value) { + this.data.payload.iv = value; + }, get keyData() this.data.payload.key_data, set keyData(value) { this.data.payload.key_data = value; }, - get _publicKeyUri() this.payload.public_key, - set _publicKeyUri(value) { - this.data.payload.public_key = value; - }, - - get privateKeyUri() { - throw "attempted to get private key url from a private key!"; - }, get publicKeyUri() { if (!this.data) return null; // resolve - let uri = this.uri.resolve(this._publicKeyUri); + let uri = this.uri.resolve(this.payload.public_key); if (uri) return Utils.makeURI(uri); // does not resolve, return raw (this uri type might not be able to resolve) - return Utils.makeURI(this._publicKeyUri); + return Utils.makeURI(this.payload.public_key); + }, + set publicKeyUri(value) { + this.payload.public_key = value; + }, + + get privateKeyUri() { + throw "attempted to get private key url from a private key!"; } }; @@ -166,6 +178,19 @@ PubKeyManager.prototype = { _keyType: PubKey, _logName: "PubKeyManager", + _init: function KeyMgr__init() { + this._log = Log4Moz.repository.getLogger(this._logName); + this._keys = {}; + this._aliases = {}; + }, + + get _crypto() { + let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + this.__defineGetter__("_crypto", function() crypto); + return crypto; + }, + get defaultKeyUri() this._defaultKeyUri, set defaultKeyUri(value) { this._defaultKeyUri = value; }, @@ -178,22 +203,26 @@ PubKeyManager.prototype = { return this._getDefaultKey.async(this, onComplete); }, - createKeypair: function KeyMgr_createKeypair() { - let pubOut = {}, privOut = {}; - let cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - getService(Ci.IWeaveCrypto); - cryptoSvc.generateKeypair("my passphrase", salt, iv, pubOut, privOut); + createKeypair: function KeyMgr_createKeypair(passphrase, pubkeyUri, privkeyUri) { let pubkey = new PubKey(); let privkey = new PrivKey(); - pubkey.keyData = pubOut.value; - privkey.keyData = privOut.value; - return {pubkey: pubkey, privkey: privkey}; - }, + privkey.salt = this._crypto.generateRandomBytes(16); + privkey.iv = this._crypto.generateRandomIV(); - _init: function KeyMgr__init() { - this._log = Log4Moz.repository.getLogger(this._logName); - this._keys = {}; - this._aliases = {}; + let pub = {}, priv = {}; + this._crypto.generateKeypair(passphrase, privkey.salt, privkey.iv, pub, priv); + [pubkey.keyData, privkey.keyData] = [pub.value, priv.value]; + + if (pubkeyUri) { + pubkey.uri = pubkeyUri; + privkey.publicKeyUri = pubkeyUri; + } + if (privkeyUri) { + privkey.uri = privkeyUri; + pubkey.privateKeyUri = privkeyUri; + } + + return {pubkey: pubkey, privkey: privkey}; }, _import: function KeyMgr__import(url) { diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index c9c882b66a40..dc5babfc02d2 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -58,18 +58,37 @@ WBORecord.prototype = { this._init(uri, authenticator); this.pushFilter(new JsonFilter()); this.data = { + id: "", modified: "2454725.98283", // FIXME payload: {} }; }, + get id() this.data.id, + set id(value) { + this.data.id = value; + }, + + get parentid() this.data.parentid, + set parentid(value) { + this.data.parentid = value; + }, + get modified() this.data.modified, set modified(value) { this.data.modified = value; }, + get encoding() this.data.encoding, + set encoding(value) { + this.data.encoding = value; + }, + get payload() this.data.payload, set payload(value) { this.data.payload = value; } + + // note: encryption is part of the CryptoWrapper object, which uses + // a string (encrypted) payload instead of an object }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 181e1b9170dd..b13454a92b20 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -56,6 +56,9 @@ Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); + Function.prototype.async = Async.sugar; // Singleton service, holds registered engines @@ -271,12 +274,30 @@ NewEngine.prototype = { // if snapshot-based, generate outgoing queue - // apply incoming queue one by one + // tmp- generate all items + let all = this._store.wrap(); + for (let key in all) { + let record = new CryptoWrapper(); + record.id = key; + record.cleartext = all[key]; + this.outgoing.push(record); + } + + + // remove from incoming queue any items also in outgoing queue // upload new/changed items from our outgoing queue - foreach (this.outgoing) { - // ... + // FIXME: roll these up into a single POST! + for each (record in this.outgoing) { + + this._log.info(uneval(record.payload)); + //record.put(self.cb); } + + // apply incoming queue one by one + for each (record in this.outgoing) { + } + self.done(); } }; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 2691ef5d69f5..8cdbfb55c098 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -97,11 +97,16 @@ Resource.prototype = { set uri(value) { this._dirty = true; this._downloaded = false; - this._uri = value; + if (typeof value == 'string') + this._uri = Utils.makeURI(value); + else + this._uri = value; }, get spec() { - return this._uri.spec; + if (this._uri) + return this._uri.spec; + return null; }, set spec(value) { this._dirty = true; @@ -125,8 +130,6 @@ Resource.prototype = { _filters: null, pushFilter: function Res_pushFilter(filter) { - if (!this._filters) - this._filters = []; this._filters.push(filter); }, popFilter: function Res_popFilter() { @@ -138,11 +141,10 @@ Resource.prototype = { _init: function Res__init(uri, authenticator) { this._log = Log4Moz.repository.getLogger(this._logName); - if (typeof uri == 'string') - uri = Utils.makeURI(uri); - this._uri = uri; + this.uri = uri; this._authenticator = authenticator; this._headers = {'Content-type': 'text/plain'}; + this._filters = []; }, _createRequest: function Res__createRequest(op, onRequestFinished) { @@ -234,7 +236,8 @@ Resource.prototype = { } else { // wait for a bit and try again - this._log.debug(action + " request failed, retrying..."); + this._log.debug(action + " request failed (" + + this._lastRequest.status + "), retrying..."); listener = new Utils.EventListener(self.cb); wait_timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); yield wait_timer.initWithCallback(listener, iter * iter * 1000, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d28d12630dae..dbf9b7f795e8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -465,8 +465,9 @@ WeaveSvc.prototype = { }, login: function WeaveSvc_login(onComplete, username, password, passphrase) { this._localLock( - this._notify("login", "", this._login, - username, password, passphrase)).async(this, onComplete); + this._notify("login", "", + this._catchAll(this._login, username, password, passphrase))). + async(this, onComplete); }, logout: function WeaveSvc_logout() { diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index db6940835943..5ea23dd18a11 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -11,40 +11,29 @@ try { } Function.prototype.async = Async.sugar; -let jsonSvc, cryptoSvc, salt, iv, symKey, wrappedKey, pubKey, privKey; +let jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); +let cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); +let keys, cryptoMeta, cryptoWrap; function pubkey_handler(metadata, response) { - let obj = {modified: "2454725.98283", - payload: {type: "pubkey", - private_key: "http://localhost:8080/privkey", - key_data: pubKey}}; - return httpd_basic_auth_handler(jsonSvc.encode(obj), metadata, response); + return httpd_basic_auth_handler(jsonSvc.encode(keys.pubkey.data), + metadata, response); } function privkey_handler(metadata, response) { - let obj = {modified: "2454725.98283", - payload: {type: "privkey", - public_key: "http://localhost:8080/pubkey", - key_data: privKey}}; - return httpd_basic_auth_handler(jsonSvc.encode(obj), metadata, response); + return httpd_basic_auth_handler(jsonSvc.encode(keys.privkey.data), + metadata, response); } function crypted_resource_handler(metadata, response) { - let obj = {modified: "2454725.98283", - encryption: "http://localhost:8080/crypto-meta", - payload: cryptoSvc.encrypt("my payload here", symKey, iv)}; - return httpd_basic_auth_handler(jsonSvc.encode(obj), metadata, response); + return httpd_basic_auth_handler(jsonSvc.encode(cryptoWrap.data), + metadata, response); } function crypto_meta_handler(metadata, response) { - let obj = {modified: "2454725.98283", - payload: {type: "crypto-meta", - salt: salt, - iv: iv, - keyring: { - "pubkey": wrappedKey - }}}; - return httpd_basic_auth_handler(jsonSvc.encode(obj), metadata, response); + return httpd_basic_auth_handler(jsonSvc.encode(cryptoMeta.data), + metadata, response); } function async_test() { @@ -55,40 +44,37 @@ function async_test() { let log = Log4Moz.repository.getLogger(); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); - jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - getService(Ci.IWeaveCrypto); - let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); Auth.defaultAuthenticator = auth; - PubKeys.defaultKeyUri = "http://localhost:8080/pubkey"; server = httpd_setup({"/pubkey": pubkey_handler, "/privkey": privkey_handler, "/crypted-resource": crypted_resource_handler, "/crypto-meta": crypto_meta_handler}); - salt = cryptoSvc.generateRandomBytes(16); - iv = cryptoSvc.generateRandomIV(); + PubKeys.defaultKeyUri = "http://localhost:8080/pubkey"; + keys = PubKeys.createKeypair("my passphrase", + "http://localhost:8080/pubkey", + "http://localhost:8080/privkey"); + keys.symkey = cryptoSvc.generateRandomKey(); + keys.wrappedkey = cryptoSvc.wrapSymmetricKey(keys.symkey, keys.pubkey.keyData); - let pubOut = {}, privOut = {}; - cryptoSvc.generateKeypair("my passphrase", salt, iv, pubOut, privOut); - pubKey = pubOut.value; - privKey = privOut.value; + cryptoMeta = new CryptoMeta("http://localhost:8080/crypto-meta", auth); + cryptoMeta.generateIV(); + yield cryptoMeta.addUnwrappedKey(self.cb, keys.pubkey, keys.symkey); - symKey = cryptoSvc.generateRandomKey(); - wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey); + cryptoWrap = new CryptoWrapper("http://localhost:8080/crypted-resource", auth); + cryptoWrap.encryption = "http://localhost:8080/crypto-meta"; + cryptoWrap.cleartext = "my payload here"; + yield cryptoWrap.encrypt(self.cb, "my passphrase"); - let wrap = new CryptoWrapper("http://localhost:8080/crypted-resource", auth); - yield wrap.get(self.cb); - - let payload = yield wrap.decrypt(self.cb, "my passphrase"); + let payload = yield cryptoWrap.decrypt(self.cb, "my passphrase"); do_check_eq(payload, "my payload here"); - do_check_neq(payload, wrap.data.payload); // wrap.data.payload is the encrypted one + do_check_neq(payload, cryptoWrap.payload); // wrap.data.payload is the encrypted one - wrap.cleartext = "another payload"; - yield wrap.encrypt(self.cb, "my passphrase"); - payload = yield wrap.decrypt(self.cb, "my passphrase"); + cryptoWrap.cleartext = "another payload"; + yield cryptoWrap.encrypt(self.cb, "my passphrase"); + payload = yield cryptoWrap.decrypt(self.cb, "my passphrase"); do_check_eq(payload, "another payload"); do_test_finished(); From a80da25de7a419017920e0dd5d950e27907ae588 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 8 Nov 2008 20:24:12 -0800 Subject: [PATCH 0708/1860] cache crypto meta objects --- services/sync/modules/base_records/crypto.js | 59 +++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 2ead946c3444..c8436445494d 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['CryptoWrapper', 'CryptoMeta']; +const EXPORTED_SYMBOLS = ['CryptoWrapper', 'CryptoMeta', 'CryptoMetas']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -53,6 +53,8 @@ Cu.import("resource://weave/base_records/keys.js"); Function.prototype.async = Async.sugar; +Utils.lazy(this, 'CryptoMetas', RecordManager); + // fixme: global, ugh let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"].createInstance(Ci.IWeaveCrypto); @@ -87,10 +89,11 @@ CryptoWrapper.prototype = { let pubkey = yield PubKeys.getDefaultKey(self.cb); let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); - let meta = new CryptoMeta(this.encryption); // FIXME: cache! - yield meta.get(self.cb); + let meta = yield CryptoMetas.get(self.cb, this.encryption); let symkey = yield meta.getKey(self.cb, privkey, passphrase); + // note: we wrap the cleartext payload in an array because + // when it's a simple string nsIJSON returns null this.payload = crypto.encrypt(json.encode([this.cleartext]), symkey, meta.bulkIV); self.done(); @@ -105,10 +108,10 @@ CryptoWrapper.prototype = { let pubkey = yield PubKeys.getDefaultKey(self.cb); let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); - let meta = new CryptoMeta(this.encryption); // FIXME: cache! - yield meta.get(self.cb); + let meta = yield CryptoMetas.get(self.cb, this.encryption); let symkey = yield meta.getKey(self.cb, privkey, passphrase); + // note: payload is wrapped in an array, see _encrypt this.cleartext = json.decode(crypto.decrypt(this.payload, symkey, meta.bulkIV))[0]; self.done(this.cleartext); @@ -194,23 +197,26 @@ CryptoMeta.prototype = { } }; -function CryptoMetaManager() { +function RecordManager() { this._init(); } -CryptoMetaManager.prototype = { - _init: function CryptoMetaMgr__init() { - this._log = Log4Moz.repository.getLogger("CryptoMetaMgr"); +RecordManager.prototype = { + _recordType: CryptoMeta, + _logName: "RecordMgr", + + _init: function RegordMgr__init() { + this._log = Log4Moz.repository.getLogger(this._logName); this._records = {}; this._aliases = {}; }, - _import: function CryptoMetaMgr__import(url) { + _import: function RegordMgr__import(url) { let self = yield; this._log.trace("Importing record: " + (url.spec? url.spec : url)); try { - let rec = new CryptoMeta(url); + let rec = new this._recordType(url); yield rec.get(self.cb); this.set(url, rec); self.done(rec); @@ -219,52 +225,51 @@ CryptoMetaManager.prototype = { self.done(null); } }, - import: function CryptoMetaMgr_import(onComplete, url) { + import: function RegordMgr_import(onComplete, url) { this._import.async(this, onComplete, url); }, - _get: function CryptoMetaMgr__get(url) { + _get: function RegordMgr__get(url) { let self = yield; let rec = null; if (url in this._aliases) url = this._aliases[url]; if (url in this._records) - rec = this._keys[url]; + rec = this._records[url]; - if (!key) + if (!rec) rec = yield this.import(self.cb, url); self.done(rec); }, - get: function KeyMgr_get(onComplete, url) { + get: function RegordMgr_get(onComplete, url) { this._get.async(this, onComplete, url); }, - set: function KeyMgr_set(url, key) { - this._keys[url] = key; - return key; + set: function RegordMgr_set(url, record) { + this._records[url] = record; }, - contains: function KeyMgr_contains(url) { - let key = null; + contains: function RegordMgr_contains(url) { + let record = null; if (url in this._aliases) url = this._aliases[url]; - if (url in this._keys) + if (url in this._records) return true; return false; }, - del: function KeyMgr_del(url) { - delete this._keys[url]; + del: function RegordMgr_del(url) { + delete this._records[url]; }, - getAlias: function KeyMgr_getAlias(alias) { + getAlias: function RegordMgr_getAlias(alias) { return this._aliases[alias]; }, - setAlias: function KeyMgr_setAlias(url, alias) { + setAlias: function RegordMgr_setAlias(url, alias) { this._aliases[alias] = url; }, - delAlias: function KeyMgr_delAlias(alias) { + delAlias: function RegordMgr_delAlias(alias) { delete this._aliases[alias]; } }; From 80607eec713b0355fa13f3af872795d11985058a Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 13 Nov 2008 01:51:24 +0100 Subject: [PATCH 0709/1860] Fix redeclaration (bug #463870) --- services/sync/modules/wrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index b6501cbff150..72491ce8b85f 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -118,7 +118,7 @@ let Wrap = { let ret; let args = Array.prototype.slice.call(arguments); - let ret = this.lock(); + ret = this.lock(); if (!ret) throw "Could not acquire lock"; From ffdc62e078ea98b9725732e0491b52ff6df4bdfd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:13:06 +0900 Subject: [PATCH 0710/1860] add resource logger pref; lower numretries to 2 --- services/sync/services-sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 3481da461a26..8a27f85c6461 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -34,9 +34,10 @@ pref("extensions.weave.log.logger.service.crypto", "Debug"); pref("extensions.weave.log.logger.service.dav", "Debug"); pref("extensions.weave.log.logger.service.engine", "Debug"); pref("extensions.weave.log.logger.service.main", "Trace"); +pref("extensions.weave.log.logger.network.resources", "Debug"); pref("extensions.weave.xmpp.enabled", false); pref("extensions.weave.xmpp.server.url", "https://sm-labs01.mozilla.org:81/xmpp"); pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); -pref("extensions.weave.network.numRetries", 4); +pref("extensions.weave.network.numRetries", 2); From 950f2c038f57349f29f6a3f3476ae6618cb17e80 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:14:42 +0900 Subject: [PATCH 0711/1860] record import (cache) fix --- services/sync/modules/base_records/crypto.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index c8436445494d..a831564411ec 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -212,18 +212,19 @@ RecordManager.prototype = { _import: function RegordMgr__import(url) { let self = yield; + let rec; this._log.trace("Importing record: " + (url.spec? url.spec : url)); try { - let rec = new this._recordType(url); + rec = new this._recordType(url); yield rec.get(self.cb); this.set(url, rec); - self.done(rec); } catch (e) { this._log.debug("Failed to import record: " + e); - self.done(null); + rec = null; } + self.done(rec); }, import: function RegordMgr_import(onComplete, url) { this._import.async(this, onComplete, url); From 57795df82f993b40340bf6c325cd92de3bc36326 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:15:16 +0900 Subject: [PATCH 0712/1860] add rsa gen debug messages --- services/sync/modules/base_records/keys.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 07ce1b696dc5..bd75aa286642 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -204,6 +204,7 @@ PubKeyManager.prototype = { }, createKeypair: function KeyMgr_createKeypair(passphrase, pubkeyUri, privkeyUri) { + this._log.debug("Generating RSA keypair"); let pubkey = new PubKey(); let privkey = new PrivKey(); privkey.salt = this._crypto.generateRandomBytes(16); @@ -222,6 +223,7 @@ PubKeyManager.prototype = { pubkey.privateKeyUri = privkeyUri; } + this._log.debug("Generating RSA keypair... done"); return {pubkey: pubkey, privkey: privkey}; }, From c76adde18e85a398589e245ac700e9c6188208c9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:15:34 +0900 Subject: [PATCH 0713/1860] collection class --- .../sync/modules/base_records/collection.js | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 services/sync/modules/base_records/collection.js diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js new file mode 100644 index 000000000000..ad8c55c57cac --- /dev/null +++ b/services/sync/modules/base_records/collection.js @@ -0,0 +1,152 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Collection']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/Observers.js"); +Cu.import("resource://weave/Preferences.js"); +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/crypto.js"); +Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/keys.js"); + +Function.prototype.async = Async.sugar; + +function Collection(uri, authenticator) { + this._Coll_init(uri, authenticator); +} +Collection.prototype = { + __proto__: Resource.prototype, + _logName: "Collection", + + _Coll_init: function Coll_init(uri, authenticator) { + this._init(uri, authenticator); + this.pushFilter(new JsonFilter()); + this._full = true; + this._modified = 0; + this._data = []; + }, + + _rebuildURL: function Coll__rebuildURL() { + // XXX should consider what happens if it's not a URL... + this.uri.QueryInterface(Ci.nsIURL); + + let args = []; + if (this.modified) + args.push('modified=' + this.modified); + if (this.full) + args.push('full=1'); + + this.uri.query = (args.length > 0)? '?' + args.join('&') : ''; + }, + + // get full items + get full() { return this._full; }, + set full(value) { + this._full = value; + this._rebuildURL(); + }, + + // get only items modified since some date + get modified() { return this._modified; }, + set modified(value) { + this._modified = value; + this._rebuildURL(); + }, + + get iter() { + if (!this._iter) + this._iter = new CollectionIterator(this); + return this._iter; + }, + + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return this._json; + }, + + pushRecord: function Coll_pushRecord(onComplete, record) { + let fn = function(record) { + let self = yield; + yield record.filterUpload(self.cb); // XXX EEK + this._data.push(this._json.decode(record.data)); // HACK HACK HACK + self.done(); + }; + fn.async(this, onComplete, record); + }, + + clearRecords: function Coll_clearRecords() { + this._data = []; + } +}; + +// FIXME: iterator should return WBOs / CryptoWrappers / Keys ? +// FIXME: need to reset iterator after a get/put/post +function CollectionIterator(coll) { + this._init(coll); +} +CollectionIterator.prototype = { + _init: function CollIter__init(coll) { + this._coll = coll; + this._idx = 0; + }, + + get count() { return this._coll.data.length; }, + + next: function CollIter_next() { + if (this._idx >= this.count) + return null; + let item = this._coll.data[this._idx++]; + let wrap = new CryptoWrapper(this._coll.uri.resolve(item.id)); + wrap.data = item; + return wrap; + }, + + reset: function CollIter_reset() { + this._idx = 0; + } +}; \ No newline at end of file From 5e35b818370bd60d9fc17df658a5d9939d27f284 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:16:08 +0900 Subject: [PATCH 0714/1860] add filter to wbo to set their id automatically based on uri --- services/sync/modules/base_records/wbo.js | 29 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index dc5babfc02d2..98dde5125050 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -56,17 +56,20 @@ WBORecord.prototype = { _WBORec_init: function WBORec_init(uri, authenticator) { this._init(uri, authenticator); + this.pushFilter(new WBOFilter()); this.pushFilter(new JsonFilter()); this.data = { - id: "", - modified: "2454725.98283", // FIXME + // modified: "2454725.98283", // FIXME payload: {} }; }, - get id() this.data.id, - set id(value) { - this.data.id = value; + // id is special because it's based on the uri + get id() { + if (this.data.id) + return decodeURI(this.data.id); + let foo = this.uri.spec.split('/'); + return decodeURI(foo[foo.length-1]); }, get parentid() this.data.parentid, @@ -92,3 +95,19 @@ WBORecord.prototype = { // note: encryption is part of the CryptoWrapper object, which uses // a string (encrypted) payload instead of an object }; + +function WBOFilter() {} +WBOFilter.prototype = { + beforePUT: function(data, wbo) { + let self = yield; + let foo = wbo.uri.spec.split('/'); + data.id = decodeURI(foo[foo.length-1]); + self.done(data); + }, + afterGET: function(data, wbo) { + let self = yield; + let foo = wbo.uri.spec.split('/'); + data.id = decodeURI(foo[foo.length-1]); + self.done(data); + } +}; From 138030e5e677e55fe2648827d0ead5170ca9b89e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:17:15 +0900 Subject: [PATCH 0715/1860] remove unused fields, add half-baked impl to get password from ui --- services/sync/modules/identity.js | 37 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 23fbd2f66054..b2d60087f35b 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -44,6 +44,9 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); + +Function.prototype.async = Async.sugar; Utils.lazy(this, 'ID', IDManager); @@ -94,16 +97,6 @@ function Identity(realm, username, password) { Identity.prototype = { realm : null, - // Only the "WeaveCryptoID" realm uses these: - privkey : null, - pubkey : null, - passphraseSalt : null, - privkeyWrapIV : null, - - // Only the per-engine identity uses these: - bulkKey : null, - bulkIV : null, - username : null, get userHash() { return Utils.sha1(this.username); }, @@ -119,5 +112,29 @@ Identity.prototype = { setTempPassword: function Id_setTempPassword(value) { this._password = value; + }, + + // we'll call this if set to call out to the ui to prompt the user + // note:the ui is expected to set the password/setTempPassword + // note2: callback must be an async.js style generator function + onGetPassword: null, + + // Attempts to get the password from the user if not set + _getPassword: function Id__getPassword() { + let self = yield; + let pass; + + if (this.password) + pass = this.password; + else { + if (this.onGetPassword) { + yield Async.run(this, this.onGetPassword, self.cb, this); + pass = this.password; // retry after ui callback + } + } + self.done(pass); + }, + getPassword: function Id_getPassword(onComplete) { + this._getPassword.async(this, onComplete); } }; From 7090dab4a6c942c924c05f9d3c1b7eaf7357265e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:18:35 +0900 Subject: [PATCH 0716/1860] attempt at fixing login/server setup, needs more work still --- services/sync/modules/service.js | 56 +++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index dbf9b7f795e8..41e53544dcc4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -182,6 +182,14 @@ WeaveSvc.prototype = { get passphrase() { return ID.get('WeaveCryptoID').password; }, set passphrase(value) { ID.get('WeaveCryptoID').password = value; }, + // chrome-provided callbacks for when the service needs a password/passphrase + set onGetPassword(value) { + ID.get('WeaveID').onGetPassword = value; + }, + set onGetPassphrase(value) { + ID.get('WeaveCryptoID').onGetPassword = value; + }, + get baseURL() { let url = Utils.prefs.getCharPref("serverURL"); if (url && url[url.length-1] != '/') @@ -296,7 +304,8 @@ WeaveSvc.prototype = { new Identity('Mozilla Services Encryption Passphrase', this.username)); let url = this.baseURL + this.username; - PubKeys.defaultKeyUri = url + "keys/pubkey"; + PubKeys.defaultKeyUri = url + "/keys/pubkey"; + PrivKeys.defaultKeyUri = url + "/keys/privkey"; if (Utils.prefs.getBoolPref("autoconnect") && this.username && this.username != 'nobody') @@ -489,11 +498,56 @@ WeaveSvc.prototype = { this._notify("server-wipe", "", this._localLock(cb)).async(this, onComplete); }, + // stuff we need to to after login, before we can really do + // anything (e.g. key setup) + _remoteSetup: function WeaveSvc__remoteSetup() { + let self = yield; + let ret = false; // false to abort sync + + // FIXME: too easy to generate a keypair. we should be absolutely certain + // that the keys are not there (404) and that it's not some other + // transient network problem instead + let needKeys = true; + let pubkey = yield PubKeys.getDefaultKey(self.cb); + if (pubkey) { + // make sure we have a matching privkey + let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); + if (privkey) { + needKeys = false; + ret = true; + } + } + + if (needKeys) { + let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb); + if (pass) { + let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, + PrivKeys.defaultKeyUri); + try { + yield keys.pubkey.put(self.cb); + yield keys.privkey.put(self.cb); + ret = true; + } catch (e) { + this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); + this._log.error(keys.pubkey.lastRequest.responseText); + } + } else { + this._log.warn("Could not get encryption passphrase"); + } + } + + self.done(ret); + }, + // These are per-engine _sync: function WeaveSvc__sync() { let self = yield; + if (!(yield this._remoteSetup.async(this, self.cb))) { + throw "aborting sync, remote setup failed"; + } + //this._log.debug("Refreshing client list"); //yield ClientData.refresh(self.cb); From a3f43b3134c02a68ee98fe29c46feab4f3c0c327 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:19:51 +0900 Subject: [PATCH 0717/1860] make it so filters can be called (to e.g. manually copy resource data out --- services/sync/modules/resource.js | 66 +++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 8cdbfb55c098..02f5d410bb1e 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -63,7 +63,7 @@ RequestException.prototype = { get request() { return this._request; }, get status() { return this._request.status; }, toString: function ReqEx_toString() { - return "Could not " + this._action + " resource " + this._resource.path + + return "Could not " + this._action + " resource " + this._resource.spec + " (" + this._request.status + ")"; } }; @@ -108,11 +108,6 @@ Resource.prototype = { return this._uri.spec; return null; }, - set spec(value) { - this._dirty = true; - this._downloaded = false; - this._uri.spec = value; - }, _data: null, get data() this._data, @@ -141,6 +136,8 @@ Resource.prototype = { _init: function Res__init(uri, authenticator) { this._log = Log4Moz.repository.getLogger(this._logName); + this._log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; this.uri = uri; this._authenticator = authenticator; this._headers = {'Content-type': 'text/plain'}; @@ -189,24 +186,47 @@ Resource.prototype = { return timer; }, + filterUpload: function Res_filterUpload(onComplete) { + let fn = function() { + let self = yield; + for each (let filter in this._filters) { + this._data = yield filter.beforePUT.async(filter, self.cb, this._data, this); + } + }; + fn.async(this, onComplete); + }, + + filterDownload: function Res_filterUpload(onComplete) { + let fn = function() { + let self = yield; + let filters = this._filters.slice(); // reverse() mutates, so we copy + for each (let filter in filters.reverse()) { + this._data = yield filter.afterGET.async(filter, self.cb, this._data, this); + } + }; + fn.async(this, onComplete); + }, + _request: function Res__request(action, data) { let self = yield; let listener, wait_timer; let iter = 0; + if ("undefined" != typeof(data)) + this._data = data; + this._log.debug(action + " request for " + this.spec); - if ("PUT" == action) { - for each (let filter in this._filters) { - data = yield filter.beforePUT.async(filter, self.cb, data, this); - } + if ("PUT" == action || "POST" == action) { + yield this.filterUpload(self.cb); + this._log.trace(action + " Body:\n" + this._data); } while (iter < Preferences.get(PREFS_BRANCH + "network.numRetries")) { let cb = self.cb; // to avoid warning because we use it twice let request = this._createRequest(action, cb); let timeout_timer = this._setupTimeout(request, cb); - let event = yield request.send(data); + let event = yield request.send(this._data); timeout_timer.cancel(); this._lastRequest = event.target; @@ -220,18 +240,18 @@ Resource.prototype = { this._log.debug(action + " request successful"); this._dirty = false; - if ("GET" == action) { + if ("GET" == action || "POST" == action) { this._data = this._lastRequest.responseText; - let filters = this._filters.slice(); // reverse() mutates, so we copy - for each (let filter in filters.reverse()) { - this._data = yield filter.afterGET.async(filter, self.cb, this._data, this); - } + this._log.trace(action + " Body:\n" + this._data); + yield this.filterDownload(self.cb); } break; - // FIXME: this should really check a variety of permanent errors, rather than just 404s - } else if (action == "GET" && this._lastRequest.status == 404) { - this._log.debug(action + " request failed (404)"); + // FIXME: this should really check a variety of permanent errors, rather than just >= 400 + } else if (this._lastRequest.status >= 400) { + this._log.debug(action + " request failed (" + this._lastRequest.status + ")"); + if (this._lastRequest.responseText) + this._log.debug("Error response: \n" + this._lastRequest.responseText); throw new RequestException(this, action, this._lastRequest); } else { @@ -248,6 +268,8 @@ Resource.prototype = { if (iter >= Preferences.get(PREFS_BRANCH + "network.numRetries")) { this._log.debug(action + " request failed (too many errors)"); + if (this._lastRequest && this._lastRequest.responseText) + this._log.debug("Error response: \n" + this._lastRequest.responseText); throw new RequestException(this, action, this._lastRequest); } @@ -259,11 +281,13 @@ Resource.prototype = { }, put: function Res_put(onComplete, data) { - if ("undefined" == typeof(data)) - data = this._data; this._request.async(this, onComplete, "PUT", data); }, + post: function Res_post(onComplete, data) { + this._request.async(this, onComplete, "POST", data); + }, + delete: function Res_delete(onComplete) { this._request.async(this, onComplete, "DELETE"); } From 8b8d09a4640c3f792c4a998d9ce2922f7aa846a9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:20:25 +0900 Subject: [PATCH 0718/1860] more NewEngine impl --- services/sync/modules/engines.js | 163 +++++++++++++++++++++++++++---- 1 file changed, 145 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b13454a92b20..b6d44de05d13 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -57,7 +57,9 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/collection.js"); Function.prototype.async = Async.sugar; @@ -249,7 +251,19 @@ function NewEngine() {} NewEngine.prototype = { __proto__: Engine.prototype, - get lastSync() Utils.prefs.getCharPref(this.name + ".lastSync"), + get _snapshot() { + let snap = new SnapshotStore(this.name); + this.__defineGetter__("_snapshot", function() snap); + return snap; + }, + + get lastSync() { + try { + return Utils.prefs.getCharPref(this.name + ".lastSync"); + } catch (e) { + return 0; + } + }, set lastSync(value) { Utils.prefs.setCharPref(this.name + ".lastSync", value); }, @@ -268,37 +282,150 @@ NewEngine.prototype = { return this._outgoing; }, + get baseURL() { + let url = Utils.prefs.getCharPref("serverURL"); + if (url && url[url.length-1] != '/') + url = url + '/'; + return url; + }, + + get engineURL() { + return this.baseURL + ID.get('WeaveID').username + '/' + this.name + '/'; + }, + + get cryptoMetaURL() { + return this.baseURL + ID.get('WeaveID').username + '/crypto/' + this.name; + }, + + _remoteSetup: function NewEngine__remoteSetup() { + let self = yield; + + let meta = yield CryptoMetas.get(self.cb, this.cryptoMetaURL); + if (!meta) { + let cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + let symkey = cryptoSvc.generateRandomKey(); + let pubkey = yield PubKeys.getDefaultKey(self.cb); + meta = new CryptoMeta(this.cryptoMetaURL); + meta.generateIV(); + yield meta.addUnwrappedKey(self.cb, pubkey, symkey); + yield meta.put(self.cb); + } + }, + + _createRecord: function NewEngine__newCryptoWrapper(id, payload, encrypt) { + let self = yield; + + let record = new CryptoWrapper(); + record.uri = this.engineURL + id; + record.encryption = this.cryptoMetaURL; + record.cleartext = payload; + if (encrypt || encrypt == undefined) + yield record.encrypt(self.cb, ID.get('WeaveCryptoID').password); + + self.done(record); + }, + _sync: function NewEngine__sync() { let self = yield; - // find new items from server, place into incoming queue - // if snapshot-based, generate outgoing queue + yield this._remoteSetup.async(this, self.cb); - // tmp- generate all items - let all = this._store.wrap(); - for (let key in all) { - let record = new CryptoWrapper(); - record.id = key; - record.cleartext = all[key]; - this.outgoing.push(record); + // first sync case: sync all items up + // otherwise we expect any new items to be already in the outgoing queue + // otherwise we use snapshots to generate the outgoing queue + if (!this.lastSync) { + let all = this._store.wrap(); + for (let key in all) { + let record = yield this._createRecord.async(this, self.cb, key, all[key]); + this.outgoing.push(record); + } + this._snapshot.data = all; + + } else { + this._snapshot.load(); + let newsnap = this._store.wrap(); + let updates = yield this._core.detectUpdates(self.cb, + this._snapshot.data, newsnap); + for each (let cmd in updates) { + let data = ""; + if (cmd.action == "create" || cmd.action == "edit") + data = newsnap[cmd.GUID]; + let record = yield this._createRecord.async(this, self.cb, cmd.GUID, data); + this.outgoing.push(record); + } + this._snapshot.data = newsnap; } + // find new items from server, place into incoming queue + let newitems = new Collection(this.engineURL); + newitems.modified = this.lastSync; + newitems.full = true; + yield newitems.get(self.cb); + + let item; + while ((item = newitems.iter.next())) { + // server returns items with modified==this.lastSync, so skip those + if (item.modified > this.lastSync) + this.incoming.push(item); + } // remove from incoming queue any items also in outgoing queue - - // upload new/changed items from our outgoing queue - // FIXME: roll these up into a single POST! - for each (record in this.outgoing) { - - this._log.info(uneval(record.payload)); - //record.put(self.cb); + let conflicts = []; + for (let i = 0; i < this.incoming.length; i++) { + for each (let out in this.outgoing) { + if (this.incoming[i].id == out.id) { + conflicts.push({in: this.incoming[i], out: out}); + delete this.incoming[i]; + break; + } + } } + if (conflicts.length) + this._log.warn("Conflicts found. Conflicting server changes discarded"); // apply incoming queue one by one - for each (record in this.outgoing) { + // XXX may need to sort here (e.g. create parents first) + let inc; + while ((inc = this.incoming.pop())) { + yield this._store.applyIncoming(self.cb, inc); + if (inc.modified > this.lastSync) + this.lastSync = inc.modified; } + // upload new/changed items from our outgoing queue + /* re-enable this block after POST to server is working + let up = new Collection(this.engineURL); + let out; + while ((out = this.outgoing.pop())) { + yield up.pushRecord(self.cb, out); + } + yield up.post(self.cb); + */ + + // remove below block once POST is working + let out; + while ((out = this.outgoing.pop())) { + yield out.put(self.cb); + } + + // FIXME: hack to get the last modified timestamp. race condition alert! + yield newitems.get(self.cb); + newitems.iter.reset(); + while ((item = newitems.iter.next())) { + if (item.modified > this.lastSync) + this.lastSync = item.modified; + } + + this._snapshot.save(); + self.done(); + }, + + _resetServer: function NewEngine__resetServer() { + let self = yield; + let all = new Resource(this.engineURL); + yield all.delete(self.cb); } }; From 6af63f8e72249553c3f07a8e3c64c4c87c0a1093 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 09:21:12 +0900 Subject: [PATCH 0719/1860] work with NewEngine --- services/sync/modules/engines/bookmarks.js | 106 ++++++++++++++------- 1 file changed, 73 insertions(+), 33 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 0d169e3af526..ee1cf7b7ce51 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -742,7 +742,7 @@ BookmarksEngine.prototype = { __tracker: null, get _tracker() { if (!this.__tracker) - this.__tracker = new BookmarksTracker(); + this.__tracker = new BookmarksTracker(this); return this.__tracker; } @@ -908,6 +908,30 @@ BookmarksStore.prototype = { return null; }, + _applyIncoming: function BStore__applyIncoming(record) { + let self = yield; + + if (!this._lookup) + this.wrap(); + + yield record.decrypt(self.cb, ID.get('WeaveCryptoID').password); + + this._log.trace("RECORD: " + record.id + " -> " + uneval(record.cleartext)); + + if (!this._lookup[record.id]) + this._createCommand({GUID: record.id, data: record.cleartext}); + else if (record.cleartext == "") + this._removeCommand({GUID: record.id}); + else { + let data = Utils.deepCopy(record.cleartext); + delete data.GUID; + this._editCommand({GUID: record.id, data: data}); + } + }, + applyIncoming: function BStore_applyIncoming(onComplete, record) { + this._applyIncoming.async(this, onComplete, record); + }, + _createCommand: function BStore__createCommand(command) { let newId; let parentId = this._getItemIdForGUID(command.data.parentGUID); @@ -1351,45 +1375,61 @@ BookmarksStore.prototype = { * because the observer methods take care of updating _score which * getScore returns by default. */ -function BookmarksTracker() { - this._init(); +function BookmarksTracker(engine) { + this._init(engine); } BookmarksTracker.prototype = { __proto__: Tracker.prototype, _logName: "BMTracker", - /* We don't care about the first three */ - onBeginUpdateBatch: function BMT_onBeginUpdateBatch() { + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]), - }, - onEndUpdateBatch: function BMT_onEndUpdateBatch() { - - }, - onItemVisited: function BMT_onItemVisited() { - - }, - - /* Every add or remove is worth 4 points, - * on the basis that adding or removing 20 bookmarks - * means its time to sync? - */ - onItemAdded: function BMT_onEndUpdateBatch() { - this._score += 4; - }, - onItemRemoved: function BMT_onItemRemoved() { - this._score += 4; - }, - /* Changes are worth 2 points? */ - onItemChanged: function BMT_onItemChanged() { - this._score += 2; - }, - - _init: function BMT__init() { + _init: function BMT__init(engine) { + this._engine = engine; this._log = Log4Moz.repository.getLogger("Service." + this._logName); this._score = 0; - Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService). - addObserver(this, false); - } + getService(Ci.nsINavBookmarksService). + addObserver(this, false); + }, + + // FIXME: not getting any events whatsoever! + + /* Every add/remove/change is worth 10 points */ + + onItemAdded: function BMT_onEndUpdateBatch(itemId) { + this._log.debug("Adding item to queue: " + itemId); + this._score += 10; + let all = this._engine._store.wrap(); + let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); + this._engine.outgoing.push(record); + }, + + onItemRemoved: function BMT_onItemRemoved(itemId) { + this._log.debug("Adding item to queue: " + itemId); + this._score += 10; + let all = this._engine._store.wrap(); + let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); + this._engine.outgoing.push(record); + }, + + onItemChanged: function BMT_onItemChanged(itemId) { + this._log.debug("Adding item to queue: " + itemId); + this._score += 10; + let all = this._engine._store.wrap(); + let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); + this._engine.outgoing.push(record); + }, + + onItemMoved: function BMT_onItemMoved(itemId) { + this._log.debug("Adding item to queue: " + itemId); + this._score += 10; + let all = this._engine._store.wrap(); + let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); + this._engine.outgoing.push(record); + }, + + onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, + onEndUpdateBatch: function BMT_onEndUpdateBatch() {}, + onItemVisited: function BMT_onItemVisited() {} }; From 0b826e13cee511b4d8908475573ecafb0dcdf14e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Nov 2008 10:38:53 +0900 Subject: [PATCH 0720/1860] enable batch upload, yay! --- services/sync/modules/engines.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b6d44de05d13..85276f1d80cd 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -394,20 +394,12 @@ NewEngine.prototype = { } // upload new/changed items from our outgoing queue - /* re-enable this block after POST to server is working let up = new Collection(this.engineURL); let out; while ((out = this.outgoing.pop())) { yield up.pushRecord(self.cb, out); } yield up.post(self.cb); - */ - - // remove below block once POST is working - let out; - while ((out = this.outgoing.pop())) { - yield out.put(self.cb); - } // FIXME: hack to get the last modified timestamp. race condition alert! yield newitems.get(self.cb); From 9081c499df0e4a00febef8d2180dc2af1fb25d41 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 01:03:01 +0900 Subject: [PATCH 0721/1860] add tmp hack to work around server payload json decoding bug --- services/sync/modules/base_records/wbo.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 98dde5125050..9392c82935ae 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -96,18 +96,24 @@ WBORecord.prototype = { // a string (encrypted) payload instead of an object }; +// fixme: global, ugh +let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + function WBOFilter() {} WBOFilter.prototype = { beforePUT: function(data, wbo) { let self = yield; let foo = wbo.uri.spec.split('/'); data.id = decodeURI(foo[foo.length-1]); + data.payload = json.encode([data.payload]); self.done(data); }, afterGET: function(data, wbo) { let self = yield; let foo = wbo.uri.spec.split('/'); data.id = decodeURI(foo[foo.length-1]); + // data.payload = json.decode(data.payload)[0]; // fixme: server is decoding for us! + data.payload = data.payload[0]; // remove when above gets fixed self.done(data); } }; From eda85def96b8327d5f7f0f66aa238c2d0ff65ce5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 01:04:14 +0900 Subject: [PATCH 0722/1860] clean up sync function, get last timestamp properly from post result, save correct snapshot at the end, only attempt to post changes to server if there are any changes at all --- services/sync/modules/engines.js | 59 ++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 85276f1d80cd..bc5ae114382c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -329,20 +329,21 @@ NewEngine.prototype = { _sync: function NewEngine__sync() { let self = yield; + // STEP 0: Get our crypto records in order + yield this._remoteSetup.async(this, self.cb); - // first sync case: sync all items up - // otherwise we expect any new items to be already in the outgoing queue - // otherwise we use snapshots to generate the outgoing queue + // STEP 1: Generate outgoing items + if (!this.lastSync) { + // first sync: upload all items let all = this._store.wrap(); for (let key in all) { let record = yield this._createRecord.async(this, self.cb, key, all[key]); this.outgoing.push(record); } - this._snapshot.data = all; - } else { + // we've synced before: use snapshot to upload changes only this._snapshot.load(); let newsnap = this._store.wrap(); let updates = yield this._core.detectUpdates(self.cb, @@ -354,10 +355,10 @@ NewEngine.prototype = { let record = yield this._createRecord.async(this, self.cb, cmd.GUID, data); this.outgoing.push(record); } - this._snapshot.data = newsnap; } - // find new items from server, place into incoming queue + // STEP 2: Find new items from server, place into incoming queue + let newitems = new Collection(this.engineURL); newitems.modified = this.lastSync; newitems.full = true; @@ -365,12 +366,16 @@ NewEngine.prototype = { let item; while ((item = newitems.iter.next())) { - // server returns items with modified==this.lastSync, so skip those - if (item.modified > this.lastSync) - this.incoming.push(item); + this.incoming.push(item); } + // FIXME: need to sort incoming queue here (e.g. create parents first) + + // STEP 3: Reconcile + // remove from incoming queue any items also in outgoing queue + // FIXME: should attempt to match same items with different IDs here (same + // item created on two different machines before syncing either one) let conflicts = []; for (let i = 0; i < this.incoming.length; i++) { for each (let out in this.outgoing) { @@ -384,8 +389,8 @@ NewEngine.prototype = { if (conflicts.length) this._log.warn("Conflicts found. Conflicting server changes discarded"); - // apply incoming queue one by one - // XXX may need to sort here (e.g. create parents first) + // STEP 4: Apply incoming items + let inc; while ((inc = this.incoming.pop())) { yield this._store.applyIncoming(self.cb, inc); @@ -393,22 +398,26 @@ NewEngine.prototype = { this.lastSync = inc.modified; } - // upload new/changed items from our outgoing queue - let up = new Collection(this.engineURL); - let out; - while ((out = this.outgoing.pop())) { - yield up.pushRecord(self.cb, out); - } - yield up.post(self.cb); + // STEP 5: Upload outgoing items - // FIXME: hack to get the last modified timestamp. race condition alert! - yield newitems.get(self.cb); - newitems.iter.reset(); - while ((item = newitems.iter.next())) { - if (item.modified > this.lastSync) - this.lastSync = item.modified; + if (this.outgoing.length) { + let up = new Collection(this.engineURL); + let out; + while ((out = this.outgoing.pop())) { + yield up.pushRecord(self.cb, out); + } + yield up.post(self.cb); + + // up.data now contains the post result, get the last timestamp from there + for each (let ts in up.data.success) { + if (ts > this.lastSync) + this.lastSync = ts; + } } + // STEP 6: Save the current snapshot so as to calculate changes at next sync + + this._snapshot.data = this._store.wrap(); this._snapshot.save(); self.done(); From 541beae025b195dd50bddbdfd4d86793eb1aba02 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 01:48:38 +0900 Subject: [PATCH 0723/1860] add some debug chatter --- services/sync/modules/engines.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index bc5ae114382c..9132a55c6b66 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -330,10 +330,12 @@ NewEngine.prototype = { let self = yield; // STEP 0: Get our crypto records in order + this._log.debug("Ensuring server crypto records are there"); yield this._remoteSetup.async(this, self.cb); // STEP 1: Generate outgoing items + this._log.debug("Calculating client changes"); if (!this.lastSync) { // first sync: upload all items @@ -358,6 +360,7 @@ NewEngine.prototype = { } // STEP 2: Find new items from server, place into incoming queue + this._log.debug("Downloading server changes"); let newitems = new Collection(this.engineURL); newitems.modified = this.lastSync; @@ -372,6 +375,7 @@ NewEngine.prototype = { // FIXME: need to sort incoming queue here (e.g. create parents first) // STEP 3: Reconcile + this._log.debug("Reconciling server/client changes"); // remove from incoming queue any items also in outgoing queue // FIXME: should attempt to match same items with different IDs here (same @@ -386,10 +390,12 @@ NewEngine.prototype = { } } } + this._incoming = this.incoming.filter(function(i) i); // removes any holes if (conflicts.length) this._log.warn("Conflicts found. Conflicting server changes discarded"); // STEP 4: Apply incoming items + this._log.debug("Applying server changes"); let inc; while ((inc = this.incoming.pop())) { @@ -399,6 +405,7 @@ NewEngine.prototype = { } // STEP 5: Upload outgoing items + this._log.debug("Uploading client changes"); if (this.outgoing.length) { let up = new Collection(this.engineURL); @@ -416,6 +423,7 @@ NewEngine.prototype = { } // STEP 6: Save the current snapshot so as to calculate changes at next sync + this._log.debug("Saving snapshot for next sync"); this._snapshot.data = this._store.wrap(); this._snapshot.save(); From a323766c623c003889cef92d0620709f1278912c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 01:49:02 +0900 Subject: [PATCH 0724/1860] fix bookmark deletion sync --- services/sync/modules/engines/bookmarks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index ee1cf7b7ce51..67b42f8e048b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -918,10 +918,10 @@ BookmarksStore.prototype = { this._log.trace("RECORD: " + record.id + " -> " + uneval(record.cleartext)); - if (!this._lookup[record.id]) - this._createCommand({GUID: record.id, data: record.cleartext}); - else if (record.cleartext == "") + if (record.cleartext == "") this._removeCommand({GUID: record.id}); + else if (!this._lookup[record.id]) + this._createCommand({GUID: record.id, data: record.cleartext}); else { let data = Utils.deepCopy(record.cleartext); delete data.GUID; From 6ee568669b47a3fa710c09651716a34ce48e2e45 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 14:46:18 +0900 Subject: [PATCH 0725/1860] collection iterator is now async, it needs to run the new record's download filter --- .../sync/modules/base_records/collection.js | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index ad8c55c57cac..b388c0a17a83 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -137,13 +137,20 @@ CollectionIterator.prototype = { get count() { return this._coll.data.length; }, - next: function CollIter_next() { - if (this._idx >= this.count) - return null; - let item = this._coll.data[this._idx++]; - let wrap = new CryptoWrapper(this._coll.uri.resolve(item.id)); - wrap.data = item; - return wrap; + next: function CollIter_next(onComplete) { + let fn = function CollIter__next() { + let self = yield; + + if (this._idx >= this.count) + return; + let item = this._coll.data[this._idx++]; + let wrap = new CryptoWrapper(this._coll.uri.resolve(item.id)); + wrap.data = this._coll._json.encode(item); // HACK HACK HACK + yield wrap.filterDownload(self.cb); // XXX EEK + + self.done(wrap); + }; + fn.async(this, onComplete); }, reset: function CollIter_reset() { From 124b1251cd565220b90acced9c8f2813c964b60a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 14:47:43 +0900 Subject: [PATCH 0726/1860] server payload decoding problem is fixed, so remove hack. we now always decode the payload after downloading. --- services/sync/modules/base_records/wbo.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 9392c82935ae..849508810c9b 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -112,8 +112,7 @@ WBOFilter.prototype = { let self = yield; let foo = wbo.uri.spec.split('/'); data.id = decodeURI(foo[foo.length-1]); - // data.payload = json.decode(data.payload)[0]; // fixme: server is decoding for us! - data.payload = data.payload[0]; // remove when above gets fixed + data.payload = json.decode(data.payload)[0]; self.done(data); } }; From 2d16e8cca35d10f9848de1330e0d2785fca13f4b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 14:48:22 +0900 Subject: [PATCH 0727/1860] use the new modified property returned after a server POST --- services/sync/modules/engines.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9132a55c6b66..a5a93491c194 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -368,7 +368,7 @@ NewEngine.prototype = { yield newitems.get(self.cb); let item; - while ((item = newitems.iter.next())) { + while ((item = yield newitems.iter.next(self.cb))) { this.incoming.push(item); } @@ -414,12 +414,8 @@ NewEngine.prototype = { yield up.pushRecord(self.cb, out); } yield up.post(self.cb); - - // up.data now contains the post result, get the last timestamp from there - for each (let ts in up.data.success) { - if (ts > this.lastSync) - this.lastSync = ts; - } + if (up.data.modified > this.lastSync) + this.lastSync = up.data.modified; } // STEP 6: Save the current snapshot so as to calculate changes at next sync From 72d7ce7fa7e68dde7947a247b99737a139fdf82a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 14:49:18 +0900 Subject: [PATCH 0728/1860] query the bookmarks service to find out if we have an item (instead of our snapshot cache) --- services/sync/modules/engines/bookmarks.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 67b42f8e048b..11b8e4f92a79 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -920,7 +920,7 @@ BookmarksStore.prototype = { if (record.cleartext == "") this._removeCommand({GUID: record.id}); - else if (!this._lookup[record.id]) + else if (this._getItemIdForGUID(record.id) < 0) this._createCommand({GUID: record.id, data: record.cleartext}); else { let data = Utils.deepCopy(record.cleartext); @@ -941,7 +941,6 @@ BookmarksStore.prototype = { parentId = this._bms.bookmarksMenuFolder; } - dump( "Processing createCommand for a " + command.data.type + " type.\n"); switch (command.data.type) { case "query": case "bookmark": From 61b795089f27c0394b5399642a73ef9f887fa036 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 16:12:00 +0900 Subject: [PATCH 0729/1860] sort incoming records by depth, so that folders are created before their containing items --- services/sync/modules/engines.js | 39 ++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a5a93491c194..3a63bfb852b7 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -320,12 +320,29 @@ NewEngine.prototype = { record.uri = this.engineURL + id; record.encryption = this.cryptoMetaURL; record.cleartext = payload; + if (payload.parentGUID) // FIXME: should be parentid + record.parentid = payload.parentGUID; if (encrypt || encrypt == undefined) yield record.encrypt(self.cb, ID.get('WeaveCryptoID').password); self.done(record); }, + _recDepth: function NewEngine__recDepth(rec) { + if (rec.depth) + return rec.depth; + if (!rec.parentid) + return 0; + for each (let inc in this.incoming) { + if (inc.id == rec.parentid) { + rec.depth = this._recDepth(inc) + 1; + return rec.depth; + } + } + this._log.warn("Couldn't calculate depth of item " + rec.id); + return 0; + }, + _sync: function NewEngine__sync() { let self = yield; @@ -372,7 +389,25 @@ NewEngine.prototype = { this.incoming.push(item); } - // FIXME: need to sort incoming queue here (e.g. create parents first) + // STEP 2.5: Analyze incoming records and sort them + for each (let inc in this.incoming) { + this._recDepth(inc); + } + this.incoming.sort(function(a, b) { + if ((typeof(a.depth) == "number" && typeof(b.depth) == "undefined") || + (typeof(a.depth) == "number" && b.depth == null) || + (a.depth > b.depth)) + return 1; + if ((typeof(a.depth) == "undefined" && typeof(b.depth) == "number") || + (a.depth == null && typeof(b.depth) == "number") || + (a.depth < b.depth)) + return -1; + //if (a.index > b.index) + // return -1; + //if (a.index < b.index) + // return 1; + return 0; + }); // STEP 3: Reconcile this._log.debug("Reconciling server/client changes"); @@ -398,7 +433,7 @@ NewEngine.prototype = { this._log.debug("Applying server changes"); let inc; - while ((inc = this.incoming.pop())) { + while ((inc = this.incoming.shift())) { yield this._store.applyIncoming(self.cb, inc); if (inc.modified > this.lastSync) this.lastSync = inc.modified; From 1155a300ba815eddf65a60844d3c3e08caa6e180 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 25 Nov 2008 16:37:19 +0900 Subject: [PATCH 0730/1860] comment depth function for clarity, remove incorrect warning --- services/sync/modules/engines.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3a63bfb852b7..a643a28e7ba2 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -329,17 +329,23 @@ NewEngine.prototype = { }, _recDepth: function NewEngine__recDepth(rec) { + // we've calculated depth for this record already if (rec.depth) return rec.depth; + + // record has no parent if (!rec.parentid) return 0; + + // search for the record's parent and calculate its depth, then add one for each (let inc in this.incoming) { if (inc.id == rec.parentid) { rec.depth = this._recDepth(inc) + 1; return rec.depth; } } - this._log.warn("Couldn't calculate depth of item " + rec.id); + + // we couldn't find the record's parent, so it's an orphan return 0; }, From 20fdad35925158ef7a5ea897a391e3e6c2e8069d Mon Sep 17 00:00:00 2001 From: Daniel Brooks Date: Tue, 25 Nov 2008 12:27:08 -0600 Subject: [PATCH 0731/1860] get started on the Fennec ui by setting up the list of prefs --- services/sync/locales/en-US/preferences.dtd | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index ea5a87ad2c4e..9a4e904bcc7d 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -76,3 +76,17 @@ + + + + + + + + + + + + + + From b09eb8fbaf8efe39983a997f821d6734683162b9 Mon Sep 17 00:00:00 2001 From: Daniel Brooks Date: Tue, 25 Nov 2008 13:20:19 -0600 Subject: [PATCH 0732/1860] use consistent entity names in the Fennec prefs --- services/sync/locales/en-US/preferences.dtd | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 9a4e904bcc7d..3af6595768e4 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -77,16 +77,15 @@ - - + - + - + - + - + - + From 339a5a4c983afdf01b063979fff7f63301aa09cc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Nov 2008 14:07:18 +0900 Subject: [PATCH 0733/1860] decrypt incoming items all in one go so as to sort them by index (which is inside the payload); don't attempt to change the index of an item if we're also changing its folder; only change index/parent if it's different from current value --- services/sync/modules/engines.js | 15 +++++---- services/sync/modules/engines/bookmarks.js | 37 +++++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a643a28e7ba2..79f801dae9f8 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -382,7 +382,7 @@ NewEngine.prototype = { } } - // STEP 2: Find new items from server, place into incoming queue + // STEP 2.1: Find new items from server, place into incoming queue this._log.debug("Downloading server changes"); let newitems = new Collection(this.engineURL); @@ -395,9 +395,10 @@ NewEngine.prototype = { this.incoming.push(item); } - // STEP 2.5: Analyze incoming records and sort them + // STEP 2.2: Decrypt items, then analyze incoming records and sort them for each (let inc in this.incoming) { - this._recDepth(inc); + yield inc.decrypt(self.cb, ID.get('WeaveCryptoID').password); + this._recDepth(inc); // note: doesn't need access to payload } this.incoming.sort(function(a, b) { if ((typeof(a.depth) == "number" && typeof(b.depth) == "undefined") || @@ -408,10 +409,10 @@ NewEngine.prototype = { (a.depth == null && typeof(b.depth) == "number") || (a.depth < b.depth)) return -1; - //if (a.index > b.index) - // return -1; - //if (a.index < b.index) - // return 1; + if (a.cleartext.index > b.cleartext.index) + return 1; + if (a.cleartext.index < b.cleartext.index) + return -1; return 0; }); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 11b8e4f92a79..7661047fad08 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -914,8 +914,6 @@ BookmarksStore.prototype = { if (!this._lookup) this.wrap(); - yield record.decrypt(self.cb, ID.get('WeaveCryptoID').password); - this._log.trace("RECORD: " + record.id + " -> " + uneval(record.cleartext)); if (record.cleartext == "") @@ -1071,16 +1069,17 @@ BookmarksStore.prototype = { if (command.GUID == "menu" || command.GUID == "toolbar" || command.GUID == "unfiled") { - this._log.warn("Attempted to edit root node (" + command.GUID + - "). Skipping command."); + this._log.debug("Attempted to edit root node (" + command.GUID + + "). Skipping command."); return; } var itemId = this._bms.getItemIdForGUID(command.GUID); if (itemId < 0) { - this._log.warn("Item for GUID " + command.GUID + " not found. Skipping."); + this._log.debug("Item for GUID " + command.GUID + " not found. Skipping."); return; } + this._log.trace("Editing item " + itemId); for (let key in command.data) { switch (key) { @@ -1103,15 +1102,29 @@ BookmarksStore.prototype = { this._bms.changeBookmarkURI(itemId, Utils.makeURI(command.data.URI)); break; case "index": - this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), - command.data.index); + let curIdx = this._bms.getItemIndex(itemId); + if (curIdx != command.data.index) { + // ignore index if we're going to move the item to another folder altogether + if (command.data.parentGUID && + (this._bms.getFolderIdForItem(itemId) != + this._getItemIdForGUID(command.data.parentGUID))) + break; + this._log.trace("Moving item (changing index)"); + this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), + command.data.index); + } break; case "parentGUID": { - let index = -1; - if (command.data.index && command.data.index >= 0) - index = command.data.index; - this._bms.moveItem( - itemId, this._getItemIdForGUID(command.data.parentGUID), index); + if (command.data.parentGUID && + (this._bms.getFolderIdForItem(itemId) != + this._getItemIdForGUID(command.data.parentGUID))) { + this._log.trace("Moving item (changing folder)"); + let index = -1; + if (command.data.index && command.data.index >= 0) + index = command.data.index; + this._bms.moveItem(itemId, + this._getItemIdForGUID(command.data.parentGUID), index); + } } break; case "tags": { // filter out null/undefined/empty tags From 4e810622b6a1f09cee296fefd4d760fd88c0a85b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 27 Nov 2008 00:23:25 +0900 Subject: [PATCH 0734/1860] sync engines unconditionally on timer (for now, heuristic sync is broken somehow) --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 41e53544dcc4..6193d91b02c1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -274,7 +274,7 @@ WeaveSvc.prototype = { } else { this._log.info("Running scheduled sync"); this._notify("sync", "", - this._catchAll(this._localLock(this._syncAsNeeded))).async(this); + this._catchAll(this._localLock(this._sync))).async(this); } } }, From fee2818007780da035cf322fe8c3da446e6e8595 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 27 Nov 2008 00:25:28 +0900 Subject: [PATCH 0735/1860] add logic to detect when the same item is in both incoming & outgoing queues, but with different IDs - change the local ID in that case --- services/sync/modules/engines.js | 80 ++++++++++++++++++---- services/sync/modules/engines/bookmarks.js | 45 +++++++++--- 2 files changed, 103 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 79f801dae9f8..5b8f481b4201 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -349,6 +349,38 @@ NewEngine.prototype = { return 0; }, + _recordLike: function NewEngine__recordLike(a, b) { + // Check that all other properties are the same + if (!Utils.deepEquals(a.parentid, b.parentid)) + return false; + for (let key in a.cleartext) { + if (key == "parentGUID") + continue; // FIXME: bookmarks-specific + if (!Utils.deepEquals(a.cleartext[key], b.cleartext[key])) + return false; + } + for (key in b.cleartext) { + if (key == "parentGUID") + continue; // FIXME: bookmarks-specific + if (!Utils.deepEquals(a.cleartext[key], b.cleartext[key])) + return false; + } + return true; + }, + + _changeRecordRefs: function NewEngine__changeRecordRefs(oldID, newID) { + let self = yield; + for each (let rec in this.outgoing) { + if (rec.parentid == oldID) + rec.parentid = newID; + } + }, + + _changeRecordID: function NewEngine__changeRecordID(oldID, newID) { + let self = yield; + throw "_changeRecordID must be overridden in a subclass"; + }, + _sync: function NewEngine__sync() { let self = yield; @@ -419,9 +451,8 @@ NewEngine.prototype = { // STEP 3: Reconcile this._log.debug("Reconciling server/client changes"); - // remove from incoming queue any items also in outgoing queue - // FIXME: should attempt to match same items with different IDs here (same - // item created on two different machines before syncing either one) + // STEP 3.1: Check for the same item (same ID) on both incoming & outgoing + // queues. Client one wins in this case. let conflicts = []; for (let i = 0; i < this.incoming.length; i++) { for each (let out in this.outgoing) { @@ -434,22 +465,44 @@ NewEngine.prototype = { } this._incoming = this.incoming.filter(function(i) i); // removes any holes if (conflicts.length) - this._log.warn("Conflicts found. Conflicting server changes discarded"); + this._log.debug("Conflicts found. Conflicting server changes discarded"); + + // STEP 3.2: Check if any incoming & outgoing items are really the same but + // with different IDs + for (let i = 0; i < this.incoming.length; i++) { + for (let o = 0; o < this.outgoing.length; o++) { + if (this._recordLike(this.incoming[i], this.outgoing[o])) { + // change refs in outgoing queue + yield this._changeRecordRefs.async(this, self.cb, + this.outgoing[o].id, + this.incoming[i].id); + // change actual id of item + yield this._changeRecordID.async(this, self.cb, + this.outgoing[o].id, + this.incoming[i].id); + delete this.incoming[i]; + delete this.outgoing[o]; + break; + } + } + this._outgoing = this.outgoing.filter(function(i) i); // removes any holes + } + this._incoming = this.incoming.filter(function(i) i); // removes any holes // STEP 4: Apply incoming items - this._log.debug("Applying server changes"); - - let inc; - while ((inc = this.incoming.shift())) { - yield this._store.applyIncoming(self.cb, inc); - if (inc.modified > this.lastSync) - this.lastSync = inc.modified; + if (this.incoming.length) { + this._log.debug("Applying server changes"); + let inc; + while ((inc = this.incoming.shift())) { + yield this._store.applyIncoming(self.cb, inc); + if (inc.modified > this.lastSync) + this.lastSync = inc.modified; + } } // STEP 5: Upload outgoing items - this._log.debug("Uploading client changes"); - if (this.outgoing.length) { + this._log.debug("Uploading client changes"); let up = new Collection(this.engineURL); let out; while ((out = this.outgoing.pop())) { @@ -462,7 +515,6 @@ NewEngine.prototype = { // STEP 6: Save the current snapshot so as to calculate changes at next sync this._log.debug("Saving snapshot for next sync"); - this._snapshot.data = this._store.wrap(); this._snapshot.save(); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 7661047fad08..4fb1cd271767 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -744,6 +744,22 @@ BookmarksEngine.prototype = { if (!this.__tracker) this.__tracker = new BookmarksTracker(this); return this.__tracker; + }, + + _changeRecordRefs: function NewEngine__changeRecordRefs(oldID, newID) { + let self = yield; + for each (let rec in this.outgoing) { + if (rec.parentid == oldID) { + rec.parentid = newID; + rec.cleartext.parentGUID = newID; + yield rec.encrypt(self.cb, ID.get('WeaveCryptoID').password); + } + } + }, + + _changeRecordID: function BSS__changeRecordID(oldID, newID) { + let self = yield; + yield this._store._changeRecordID.async(this._store, self.cb, oldID, newID); } // XXX for sharing, will need to re-add code to get new shares before syncing, @@ -1087,14 +1103,6 @@ BookmarksStore.prototype = { // all commands have this to help in reconciliation, but it makes // no sense to edit it break; - case "GUID": - var existing = this._getItemIdForGUID(command.data.GUID); - if (existing < 0) - this._bms.setItemGUID(itemId, command.data.GUID); - else - this._log.warn("Can't change GUID " + command.GUID + - " to " + command.data.GUID + ": GUID already exists."); - break; case "title": this._bms.setItemTitle(itemId, command.data.title); break; @@ -1177,6 +1185,27 @@ BookmarksStore.prototype = { } }, + _changeRecordID: function BSS__changeRecordID(oldID, newID) { + let self = yield; + + var itemId = this._bms.getItemIdForGUID(oldID); + if (itemId < 0) { + this._log.warn("Can't change GUID " + oldID + " to " + + newID + ": Item does not exist"); + return; + } + + var collision = this._getItemIdForGUID(newID); + if (collision >= 0) { + this._log.warn("Can't change GUID " + oldID + " to " + + newID + ": new ID already in use"); + return; + } + + this._log.debug("Changing GUID " + oldID + " to " + newID); + this._bms.setItemGUID(itemId, newID); + }, + _getNode: function BSS__getNode(folder) { let query = this._hsvc.getNewQuery(); query.setFolders([folder], 1); From fd586a198e9a74468cb99e65203c995855b4f0b9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 27 Nov 2008 23:07:15 +0900 Subject: [PATCH 0736/1860] bump version to 0.2.90 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index bcc3530eca53..60a3efa2b8df 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,7 +43,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; -const WEAVE_VERSION = "0.2.7"; +const WEAVE_VERSION = "0.2.90"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 12333d432422e3a49e5f199e1fba069cb5cd60db Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 28 Nov 2008 00:33:53 +0900 Subject: [PATCH 0737/1860] Bug 465974: fix firefox crash at startup due to weave using nss without initializing it --- services/crypto/WeaveCrypto.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index 50a640042f58..fd50fa87edf3 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -63,6 +63,8 @@ WeaveCrypto::WeaveCrypto() : mAlgorithm(SEC_OID_AES_256_CBC), mKeypairBits(2048) { + // Ensure that PSM (and thus NSS) is initialized. + nsCOMPtr psm(nsGetServiceByContractID("@mozilla.org/psm;1")); } WeaveCrypto::~WeaveCrypto() From 28ab20f05e2673fa3738bec9c6c381ff14027086 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 14:17:44 -0800 Subject: [PATCH 0738/1860] fix 'logging in' debug string so it prints the actual username being used --- services/sync/modules/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6193d91b02c1..8d88fc7e7cf1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -453,8 +453,6 @@ WeaveSvc.prototype = { _login: function WeaveSvc__login(username, password, passphrase) { let self = yield; - this._log.debug("Logging in user " + this.username); - if (typeof(username) != 'undefined') this.username = username; if (typeof(password) != 'undefined') @@ -467,6 +465,8 @@ WeaveSvc.prototype = { if (!this.password) throw "No password given or found in password manager"; + this._log.debug("Logging in user " + this.username); + yield this._verifyLogin.async(this, self.cb, this.username, this.password); this._loggedIn = true; From fdd317f72e802e7b9a71643c320bf965ab7359b4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 14:18:12 -0800 Subject: [PATCH 0739/1860] print http status, even for successful requests --- services/sync/modules/resource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 02f5d410bb1e..4336c15dd820 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -237,7 +237,7 @@ Resource.prototype = { break; } else if (Utils.checkStatus(this._lastRequest.status)) { - this._log.debug(action + " request successful"); + this._log.debug(action + " request successful (" + this._lastRequest.status + ")"); this._dirty = false; if ("GET" == action || "POST" == action) { From ef35dcab5ca192a1366e85a0f971af0d08a6d960 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 18:07:07 -0800 Subject: [PATCH 0740/1860] server api changes: encoding is gone (payload guaranteed to be utf-8 now), payload is guaranteed to be json so no need to wrap in an array to encode; change crypto object to place encrypted data in a 'cyphertext' property inside the payload, instead of replacing the payload --- services/sync/modules/base_records/crypto.js | 27 ++++++++++++++------ services/sync/modules/base_records/wbo.js | 13 ++-------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index a831564411ec..1d8d6edfa77c 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -70,17 +70,30 @@ CryptoWrapper.prototype = { // FIXME: this will add a json filter, meaning our payloads will be json // encoded, even though they are already a string this._WBORec_init(uri, authenticator); - this.data.encryption = ""; - this.data.payload = ""; + this.data.payload = { + encryption: "", + cleartext: null, + ciphertext: null + }; }, // FIXME: we make no attempt to ensure cleartext is in sync // with the encrypted payload cleartext: null, - get encryption() this.data.encryption, + get encryption() this.payload.encryption, set encryption(value) { - this.data.encryption = value; + this.payload.encryption = value; + }, + + get cleartext() this.payload.cleartext, + set cleartext(value) { + this.payload.cleartext = value; + }, + + get ciphertext() this.payload.ciphertext, + set ciphertext(value) { + this.payload.ciphertext = value; }, _encrypt: function CryptoWrap__encrypt(passphrase) { @@ -92,9 +105,7 @@ CryptoWrapper.prototype = { let meta = yield CryptoMetas.get(self.cb, this.encryption); let symkey = yield meta.getKey(self.cb, privkey, passphrase); - // note: we wrap the cleartext payload in an array because - // when it's a simple string nsIJSON returns null - this.payload = crypto.encrypt(json.encode([this.cleartext]), symkey, meta.bulkIV); + this.ciphertext = crypto.encrypt(json.encode([this.cleartext]), symkey, meta.bulkIV); self.done(); }, @@ -112,7 +123,7 @@ CryptoWrapper.prototype = { let symkey = yield meta.getKey(self.cb, privkey, passphrase); // note: payload is wrapped in an array, see _encrypt - this.cleartext = json.decode(crypto.decrypt(this.payload, symkey, meta.bulkIV))[0]; + this.cleartext = json.decode(crypto.decrypt(this.ciphertext, symkey, meta.bulkIV))[0]; self.done(this.cleartext); }, diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 849508810c9b..6b5ed1969262 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -59,7 +59,6 @@ WBORecord.prototype = { this.pushFilter(new WBOFilter()); this.pushFilter(new JsonFilter()); this.data = { - // modified: "2454725.98283", // FIXME payload: {} }; }, @@ -82,18 +81,10 @@ WBORecord.prototype = { this.data.modified = value; }, - get encoding() this.data.encoding, - set encoding(value) { - this.data.encoding = value; - }, - get payload() this.data.payload, set payload(value) { this.data.payload = value; } - - // note: encryption is part of the CryptoWrapper object, which uses - // a string (encrypted) payload instead of an object }; // fixme: global, ugh @@ -105,14 +96,14 @@ WBOFilter.prototype = { let self = yield; let foo = wbo.uri.spec.split('/'); data.id = decodeURI(foo[foo.length-1]); - data.payload = json.encode([data.payload]); + data.payload = json.encode(data.payload); self.done(data); }, afterGET: function(data, wbo) { let self = yield; let foo = wbo.uri.spec.split('/'); data.id = decodeURI(foo[foo.length-1]); - data.payload = json.decode(data.payload)[0]; + data.payload = json.decode(data.payload); self.done(data); } }; From 119efac69fe8e3fad9c0c1699a98a2e7a1f07e53 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 18:08:59 -0800 Subject: [PATCH 0741/1860] remove incorrect getter/setter for cleartext - it should not be stored inside the payload! --- services/sync/modules/base_records/crypto.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 1d8d6edfa77c..6fdb8dec4600 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -86,11 +86,6 @@ CryptoWrapper.prototype = { this.payload.encryption = value; }, - get cleartext() this.payload.cleartext, - set cleartext(value) { - this.payload.cleartext = value; - }, - get ciphertext() this.payload.ciphertext, set ciphertext(value) { this.payload.ciphertext = value; @@ -106,6 +101,7 @@ CryptoWrapper.prototype = { let symkey = yield meta.getKey(self.cb, privkey, passphrase); this.ciphertext = crypto.encrypt(json.encode([this.cleartext]), symkey, meta.bulkIV); + this.cleartext = null; self.done(); }, @@ -124,6 +120,7 @@ CryptoWrapper.prototype = { // note: payload is wrapped in an array, see _encrypt this.cleartext = json.decode(crypto.decrypt(this.ciphertext, symkey, meta.bulkIV))[0]; + this.ciphertext = null; self.done(this.cleartext); }, From b526cb909f87e66c7920d2a09c66b576519d3e05 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 18:43:43 -0800 Subject: [PATCH 0742/1860] reset default key URLs whenever username is set via the service --- services/sync/modules/service.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8d88fc7e7cf1..68b78c334cfc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -174,6 +174,8 @@ WeaveSvc.prototype = { // fixme - need to loop over all Identity objects - needs some rethinking... ID.get('WeaveID').username = value; ID.get('WeaveCryptoID').username = value; + + this._genKeyURLs(); }, get password() { return ID.get('WeaveID').password; }, @@ -198,6 +200,7 @@ WeaveSvc.prototype = { }, set baseURL(value) { Utils.prefs.setCharPref("serverURL", value); + this._genKeyURLs(); }, get userPath() { return ID.get('WeaveID').username; }, @@ -279,6 +282,12 @@ WeaveSvc.prototype = { } }, + _genKeyURLs: function WeaveSvc__genKeyURLs() { + let url = this.baseURL + this.username; + PubKeys.defaultKeyUri = url + "/keys/pubkey"; + PrivKeys.defaultKeyUri = url + "/keys/privkey"; + }, + // one-time initialization like setting up observers and the like // xxx we might need to split some of this out into something we can call // again when username/server/etc changes @@ -303,9 +312,7 @@ WeaveSvc.prototype = { ID.set('WeaveCryptoID', new Identity('Mozilla Services Encryption Passphrase', this.username)); - let url = this.baseURL + this.username; - PubKeys.defaultKeyUri = url + "/keys/pubkey"; - PrivKeys.defaultKeyUri = url + "/keys/privkey"; + this._genKeyURLs(); if (Utils.prefs.getBoolPref("autoconnect") && this.username && this.username != 'nobody') From 3adbef56bfb7cf67d1b3ec13453d68489a8d2f6a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 18:58:28 -0800 Subject: [PATCH 0743/1860] remove unused 'cleartext' payload property; add fixme comment to set a pref listener --- services/sync/modules/base_records/crypto.js | 1 - services/sync/modules/service.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 6fdb8dec4600..8639c1d45073 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -72,7 +72,6 @@ CryptoWrapper.prototype = { this._WBORec_init(uri, authenticator); this.data.payload = { encryption: "", - cleartext: null, ciphertext: null }; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 68b78c334cfc..ccfa33d6aab3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -175,6 +175,7 @@ WeaveSvc.prototype = { ID.get('WeaveID').username = value; ID.get('WeaveCryptoID').username = value; + // FIXME: need to also call this whenever the username pref changes this._genKeyURLs(); }, From 1295c1902e7df821d09d80585967593530db8cd1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 19:04:49 -0800 Subject: [PATCH 0744/1860] change chrome url for weave passwords in login manager to chrome://weave --- services/sync/modules/util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 926b08844ef8..32b041a93a99 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -105,7 +105,7 @@ let Utils = { let password; let lm = Cc["@mozilla.org/login-manager;1"] .getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://sync', null, realm); + let logins = lm.findLogins({}, 'chrome://weave', null, realm); for (let i = 0; i < logins.length; i++) { if (logins[i].username == username) { @@ -120,7 +120,7 @@ let Utils = { // cleanup any existing passwords let lm = Cc["@mozilla.org/login-manager;1"] .getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://sync', null, realm); + let logins = lm.findLogins({}, 'chrome://weave', null, realm); for (let i = 0; i < logins.length; i++) lm.removeLogin(logins[i]); @@ -130,7 +130,7 @@ let Utils = { // save the new one let nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new nsLoginInfo('chrome://sync', null, realm, + let login = new nsLoginInfo('chrome://weave', null, realm, username, password, "", ""); lm.addLogin(login); }, From b151a50866645dcd39e5659fdd1db1e6501a4756 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 20:01:12 -0800 Subject: [PATCH 0745/1860] add debug for when setting password --- services/sync/modules/util.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 32b041a93a99..17ff7d96874e 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -127,6 +127,9 @@ let Utils = { if (!password) return; + let log = Log4Moz.repository.getLogger("Service.Util"); + log.trace("Setting '" + realm + "' password for user " + username); + // save the new one let nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); From 4bedc4d9457e7db7c92b7e2f5dd6f4b6ec51ccd2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 20:01:41 -0800 Subject: [PATCH 0746/1860] re-set password in login manager when setting username/realm --- services/sync/modules/identity.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index b2d60087f35b..b3defe41959f 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -95,12 +95,32 @@ function Identity(realm, username, password) { this._password = password; } Identity.prototype = { - realm : null, + get realm() this._realm, + set realm(value) { + let current = Utils.findPassword(this.realm, this.username); + if (current) + this.password = null; + + this._realm = value; + + if (current) + this.password = current; + }, + + get username() this._username, + set username(value) { + let current = Utils.findPassword(this.realm, this.username); + if (current) + this.password = null; + + this._username = value; + + if (current) + this.password = current; + }, - username : null, get userHash() { return Utils.sha1(this.username); }, - _password: null, get password() { if (!this._password) return Utils.findPassword(this.realm, this.username); From 170f0afc0c2e27303d564a5516d1216007dddb82 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 1 Dec 2008 20:02:01 -0800 Subject: [PATCH 0747/1860] bump version to 0.2.91 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 60a3efa2b8df..33320188dcb2 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,7 +43,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; -const WEAVE_VERSION = "0.2.90"; +const WEAVE_VERSION = "0.2.91"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 314f6e2d77c7b1d8f0921268be8ce61a7d962fb0 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Tue, 2 Dec 2008 21:42:20 +0530 Subject: [PATCH 0748/1860] bug 467085 log4moz updateParents messed up, doesn't support multilevel parenting properly, r=thunder --- services/sync/modules/log4moz.js | 10 ++++++---- services/sync/tests/unit/test_log4moz.js | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 4a172f80d5e0..9dfb4ceb909a 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -283,7 +283,9 @@ LoggerRepository.prototype = { let cur, parent; // find the closest parent - for (let i = 0; i < pieces.length; i++) { + // don't test for the logger name itself, as there's a chance it's already + // there in this._loggers + for (let i = 0; i < pieces.length - 1; i++) { if (cur) cur += '.' + pieces[i]; else @@ -292,15 +294,15 @@ LoggerRepository.prototype = { parent = cur; } - // if they are the same it has no parent - if (parent == name) + // if we didn't assign a parent above, there is no parent + if (!parent) this._loggers[name].parent = this.rootLogger; else this._loggers[name].parent = this._loggers[parent]; // trigger updates for any possible descendants of this logger for (let logger in this._loggers) { - if (logger != name && name.indexOf(logger) == 0) + if (logger != name && logger.indexOf(name) == 0) this._updateParents(logger); } }, diff --git a/services/sync/tests/unit/test_log4moz.js b/services/sync/tests/unit/test_log4moz.js index 43302bcd2dd2..1c5feb864ab0 100644 --- a/services/sync/tests/unit/test_log4moz.js +++ b/services/sync/tests/unit/test_log4moz.js @@ -24,4 +24,22 @@ function run_test() { do_check_eq(appender.messages.length, 1); do_check_true(appender.messages[0].indexOf("info test") > 0); do_check_true(appender.messages[0].indexOf("INFO") > 0); + + // Test - check whether parenting is correct + let grandparentLog = Log4Moz.repository.getLogger("grandparent"); + let childLog = Log4Moz.repository.getLogger("grandparent.parent.child"); + do_check_eq(childLog.parent.name, "grandparent"); + + let parentLog = Log4Moz.repository.getLogger("grandparent.parent"); + do_check_eq(childLog.parent.name, "grandparent.parent"); + + // Test - check that appends are exactly in scope + let gpAppender = new MockAppender(new Log4Moz.BasicFormatter()); + gpAppender.level = Log4Moz.Level.Info; + grandparentLog.addAppender(gpAppender); + childLog.info("child info test"); + log.info("this shouldn't show up in gpAppender"); + + do_check_eq(gpAppender.messages.length, 1); + do_check_true(gpAppender.messages[0].indexOf("child info test") > 0); } From 7b1a57417c953a829336242a56a460cbaf709fe4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 2 Dec 2008 14:26:18 -0800 Subject: [PATCH 0749/1860] some small key record fixes; avoid causing indirect login manager queries in the Identity constructor; fix wbo, keys, crypto record unit tests --- services/sync/modules/base_records/keys.js | 24 +++---- services/sync/modules/identity.js | 4 +- .../sync/tests/unit/test_records_crypto.js | 49 ++++++++++----- services/sync/tests/unit/test_records_keys.js | 36 ++++++----- services/sync/tests/unit/test_records_wbo.js | 62 ++++++++++++------- 5 files changed, 109 insertions(+), 66 deletions(-) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index bd75aa286642..fa3d1f4bfc39 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -65,18 +65,18 @@ PubKey.prototype = { _PubKey_init: function PubKey_init(uri, authenticator) { this._WBORec_init(uri, authenticator); - this.data.payload = { + this.payload = { type: "pubkey", key_data: null, private_key: null }; }, - get keyData() this.data.payload.key_data, + get keyData() this.payload.key_data, set keyData(value) { - this.data.payload.key_data = value; + this.payload.key_data = value; }, - get _privateKeyUri() this.data.payload.private_key, + get _privateKeyUri() this.payload.private_key, get privateKeyUri() { if (!this.data) return null; @@ -88,7 +88,7 @@ PubKey.prototype = { return Utils.makeURI(this._privateKeyUri); }, set privateKeyUri(value) { - this.data.payload.private_key = value; + this.payload.private_key = value; }, get publicKeyUri() { @@ -105,7 +105,7 @@ PrivKey.prototype = { _PrivKey_init: function PrivKey_init(uri, authenticator) { this._WBORec_init(uri, authenticator); - this.data.payload = { + this.payload = { type: "privkey", salt: null, iv: null, @@ -114,17 +114,17 @@ PrivKey.prototype = { }; }, - get salt() this.data.payload.salt, + get salt() this.payload.salt, set salt(value) { - this.data.payload.salt = value; + this.payload.salt = value; }, - get iv() this.data.payload.iv, + get iv() this.payload.iv, set iv(value) { - this.data.payload.iv = value; + this.payload.iv = value; }, - get keyData() this.data.payload.key_data, + get keyData() this.payload.key_data, set keyData(value) { - this.data.payload.key_data = value; + this.payload.key_data = value; }, get publicKeyUri() { diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index b3defe41959f..7c039c0206f6 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -90,8 +90,8 @@ IDManager.prototype = { */ function Identity(realm, username, password) { - this.realm = realm; - this.username = username; + this._realm = realm; + this._username = username; this._password = password; } Identity.prototype = { diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 5ea23dd18a11..9be994ac2a7c 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -11,29 +11,37 @@ try { } Function.prototype.async = Async.sugar; -let jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); -let cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. +let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); +let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. getService(Ci.IWeaveCrypto); let keys, cryptoMeta, cryptoWrap; function pubkey_handler(metadata, response) { - return httpd_basic_auth_handler(jsonSvc.encode(keys.pubkey.data), - metadata, response); + let obj = {id: "ignore-me", + modified: keys.pubkey.modified, + payload: json.encode(keys.pubkey.payload)}; + return httpd_basic_auth_handler(json.encode(obj), metadata, response); } function privkey_handler(metadata, response) { - return httpd_basic_auth_handler(jsonSvc.encode(keys.privkey.data), - metadata, response); + let obj = {id: "ignore-me-2", + modified: keys.privkey.modified, + payload: json.encode(keys.privkey.payload)}; + return httpd_basic_auth_handler(json.encode(obj), metadata, response); } function crypted_resource_handler(metadata, response) { - return httpd_basic_auth_handler(jsonSvc.encode(cryptoWrap.data), - metadata, response); + let obj = {id: "ignore-me-3", + modified: cryptoWrap.modified, + payload: json.encode(cryptoWrap.payload)}; + return httpd_basic_auth_handler(json.encode(obj), metadata, response); } function crypto_meta_handler(metadata, response) { - return httpd_basic_auth_handler(jsonSvc.encode(cryptoMeta.data), - metadata, response); + let obj = {id: "ignore-me-4", + modified: cryptoMeta.modified, + payload: json.encode(cryptoMeta.payload)}; + return httpd_basic_auth_handler(json.encode(obj), metadata, response); } function async_test() { @@ -44,39 +52,52 @@ function async_test() { let log = Log4Moz.repository.getLogger(); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); - let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); - Auth.defaultAuthenticator = auth; + log.info("Setting up server and authenticator"); server = httpd_setup({"/pubkey": pubkey_handler, "/privkey": privkey_handler, "/crypted-resource": crypted_resource_handler, "/crypto-meta": crypto_meta_handler}); + let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); + Auth.defaultAuthenticator = auth; + + log.info("Generating keypair + symmetric key"); + PubKeys.defaultKeyUri = "http://localhost:8080/pubkey"; keys = PubKeys.createKeypair("my passphrase", "http://localhost:8080/pubkey", "http://localhost:8080/privkey"); - keys.symkey = cryptoSvc.generateRandomKey(); - keys.wrappedkey = cryptoSvc.wrapSymmetricKey(keys.symkey, keys.pubkey.keyData); + keys.symkey = crypto.generateRandomKey(); + keys.wrappedkey = crypto.wrapSymmetricKey(keys.symkey, keys.pubkey.keyData); + + log.info("Setting up keyring"); cryptoMeta = new CryptoMeta("http://localhost:8080/crypto-meta", auth); cryptoMeta.generateIV(); yield cryptoMeta.addUnwrappedKey(self.cb, keys.pubkey, keys.symkey); + log.info("Creating and encrypting a record"); + cryptoWrap = new CryptoWrapper("http://localhost:8080/crypted-resource", auth); cryptoWrap.encryption = "http://localhost:8080/crypto-meta"; cryptoWrap.cleartext = "my payload here"; yield cryptoWrap.encrypt(self.cb, "my passphrase"); + log.info("Decrypting the record"); + let payload = yield cryptoWrap.decrypt(self.cb, "my passphrase"); do_check_eq(payload, "my payload here"); do_check_neq(payload, cryptoWrap.payload); // wrap.data.payload is the encrypted one + log.info("Re-encrypting the record with alternate payload"); + cryptoWrap.cleartext = "another payload"; yield cryptoWrap.encrypt(self.cb, "my passphrase"); payload = yield cryptoWrap.decrypt(self.cb, "my passphrase"); do_check_eq(payload, "another payload"); + log.info("Done!"); do_test_finished(); } catch (e) { do_throw(e); } diff --git a/services/sync/tests/unit/test_records_keys.js b/services/sync/tests/unit/test_records_keys.js index 8d822b5e8c3b..2b905a68409e 100644 --- a/services/sync/tests/unit/test_records_keys.js +++ b/services/sync/tests/unit/test_records_keys.js @@ -5,27 +5,27 @@ try { Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/base_records/keys.js"); -} catch (e) { - do_throw(e); -} +} catch (e) { do_throw(e); } + Function.prototype.async = Async.sugar; let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); function pubkey_handler(metadata, response) { - let obj = {modified: "2454725.98283", - payload: {type: "pubkey", - private_key: "http://localhost:8080/privkey", - key_data: "asdfasdfasf..."}}; + let obj = {id: "asdf-1234-asdf-1234", + modified: "2454725.98283", + payload: json.encode({type: "pubkey", + private_key: "http://localhost:8080/privkey", + key_data: "asdfasdfasf..."})}; return httpd_basic_auth_handler(json.encode(obj), metadata, response); } function privkey_handler(metadata, response) { - let obj = {modified: "2454725.98283", - payload: {type: "privkey", - public_key: "http://localhost:8080/pubkey", - key_data: "asdfasdfasf..."}}; - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + let obj = {id: "asdf-1234-asdf-1234-2", + modified: "2454725.98283", + payload: json.encode({type: "privkey", + public_key: "http://localhost:8080/pubkey", + key_data: "asdfasdfasf..."})}; return httpd_basic_auth_handler(json.encode(obj), metadata, response); } @@ -37,19 +37,27 @@ function async_test() { let log = Log4Moz.repository.getLogger(); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); - let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); - Auth.defaultAuthenticator = auth; + log.info("Setting up server and authenticator"); + server = httpd_setup({"/pubkey": pubkey_handler, "/privkey": privkey_handler}); + let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); + Auth.defaultAuthenticator = auth; + + log.info("Getting a public key"); + let pubkey = yield PubKeys.get(self.cb, "http://localhost:8080/pubkey"); do_check_eq(pubkey.data.payload.type, "pubkey"); do_check_eq(pubkey.lastRequest.status, 200); + log.info("Getting a private key"); + let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); do_check_eq(privkey.data.payload.type, "privkey"); do_check_eq(privkey.lastRequest.status, 200); + log.info("Done!"); do_test_finished(); } catch (e) { do_throw(e); } diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index c7bc9f63d037..7a7215972225 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -1,39 +1,53 @@ -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/auth.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/base_records/wbo.js"); +try { + Cu.import("resource://weave/log4moz.js"); + Cu.import("resource://weave/util.js"); + Cu.import("resource://weave/async.js"); + Cu.import("resource://weave/auth.js"); + Cu.import("resource://weave/identity.js"); + Cu.import("resource://weave/base_records/wbo.js"); +} catch (e) { do_throw(e); } Function.prototype.async = Async.sugar; -let logger; -let Httpd = {}; -Cu.import("resource://tests/lib/httpd.js", Httpd); +let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); -function server_handler(metadata, response) { - let body = '{"guid": "asdf-1234-asdf-1234", "type": ["object"]}'; - response.setStatusLine(metadata.httpVersion, 200, "OK"); - response.bodyOutputStream.write(body, body.length); +function record_handler(metadata, response) { + let obj = {id: "asdf-1234-asdf-1234", + modified: "2454725.98283", + payload: json.encode({cheese: "roquefort"})}; + return httpd_basic_auth_handler(json.encode(obj), metadata, response); } function async_test() { let self = yield; + let server; - logger = Log4Moz.repository.getLogger('Test'); - Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + try { + let log = Log4Moz.repository.getLogger('Test'); + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); - let server = new Httpd.nsHttpServer(); - server.registerPathHandler("/record", server_handler); - server.start(8080); + log.info("Setting up server and authenticator"); - let res = new WBORecord("http://localhost:8080/record"); - let rec = yield res.get(self.cb); - do_check_eq(rec.guid, "asdf-1234-asdf-1234"); - do_check_eq(rec.type[0], "object"); - do_check_eq(res.lastRequest.status, 200); + server = httpd_setup({"/record": record_handler}); - do_test_finished(); - server.stop(); + let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); + Auth.defaultAuthenticator = auth; + + log.info("Getting a WBO record"); + + let res = new WBORecord("http://localhost:8080/record"); + let rec = yield res.get(self.cb); + do_check_eq(rec.id, "record"); // NOT "asdf-1234-asdf-1234"! + do_check_eq(rec.modified, 2454725.98283); + do_check_eq(typeof(rec.payload), "object"); + do_check_eq(rec.payload.cheese, "roquefort"); + do_check_eq(res.lastRequest.status, 200); + + log.info("Done!"); + do_test_finished(); + } + catch (e) { do_throw(e); } + finally { server.stop(); } self.done(); } From 0b747688242473e9b02fc66cbb3b394c1a32b48f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 2 Dec 2008 16:46:24 -0800 Subject: [PATCH 0750/1860] fix bookmarks tracker by removing yield calls (which of course don't work as callbacks for nsIBookmarksObserver) --- services/sync/modules/engines/bookmarks.js | 33 ++++++++-------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 4fb1cd271767..de8dd03150b7 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -1438,39 +1438,30 @@ BookmarksTracker.prototype = { /* Every add/remove/change is worth 10 points */ - onItemAdded: function BMT_onEndUpdateBatch(itemId) { - this._log.debug("Adding item to queue: " + itemId); + onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { + this._log.debug("Item " + itemId + " added, adding to queue"); this._score += 10; - let all = this._engine._store.wrap(); - let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); - this._engine.outgoing.push(record); + //let all = this._engine._store.wrap(); + //let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); + //this._engine.outgoing.push(record); }, - onItemRemoved: function BMT_onItemRemoved(itemId) { - this._log.debug("Adding item to queue: " + itemId); + onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { + this._log.debug("Item " + itemId + " removed, adding to queue"); this._score += 10; - let all = this._engine._store.wrap(); - let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); - this._engine.outgoing.push(record); }, - onItemChanged: function BMT_onItemChanged(itemId) { - this._log.debug("Adding item to queue: " + itemId); + onItemChanged: function BMT_onItemChanged(itemId, property, isAnnotationProperty, value) { + this._log.debug("Item " + itemId + " changed, adding to queue"); this._score += 10; - let all = this._engine._store.wrap(); - let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); - this._engine.outgoing.push(record); }, - onItemMoved: function BMT_onItemMoved(itemId) { - this._log.debug("Adding item to queue: " + itemId); + onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { + this._log.debug("Item " + itemId + " moved, adding to queue"); this._score += 10; - let all = this._engine._store.wrap(); - let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); - this._engine.outgoing.push(record); }, onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, onEndUpdateBatch: function BMT_onEndUpdateBatch() {}, - onItemVisited: function BMT_onItemVisited() {} + onItemVisited: function BMT_onItemVisited(itemId, aVisitID, time) {} }; From 1fa964e605a2f34375aed6a27d9780d4fcde92ea Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 2 Dec 2008 16:48:07 -0800 Subject: [PATCH 0751/1860] move non-working unit tests into subfolders so that 'make' works again. the non-working tests will need some major re-writing due to the move away from webdav --- services/sync/tests/unit/{ => attic}/test_bookmark_sharing.js | 0 .../unit/{test_sharing.js.disabled => attic/test_sharing.js} | 0 services/sync/tests/unit/{ => attic}/test_xmpp.js | 0 services/sync/tests/unit/{ => attic}/test_xmpp_simple.js | 0 services/sync/tests/unit/{ => attic}/test_xmpp_transport_http.js | 0 services/sync/tests/unit/{ => need-work}/test_bookmark_syncing.js | 0 .../tests/unit/{ => need-work}/test_bookmark_syncing.log.expected | 0 services/sync/tests/unit/{ => need-work}/test_cookie_store.js | 0 .../sync/tests/unit/{ => need-work}/test_passphrase_checking.js | 0 services/sync/tests/unit/{ => need-work}/test_password_syncing.js | 0 .../tests/unit/{ => need-work}/test_password_syncing.log.expected | 0 services/sync/tests/unit/{ => need-work}/test_service.js | 0 .../sync/tests/unit/{ => need-work}/test_service.log.expected | 0 services/sync/tests/unit/{ => need-work}/test_util_tracebacks.js | 0 .../tests/unit/{ => need-work}/test_util_tracebacks.log.expected | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename services/sync/tests/unit/{ => attic}/test_bookmark_sharing.js (100%) rename services/sync/tests/unit/{test_sharing.js.disabled => attic/test_sharing.js} (100%) rename services/sync/tests/unit/{ => attic}/test_xmpp.js (100%) rename services/sync/tests/unit/{ => attic}/test_xmpp_simple.js (100%) rename services/sync/tests/unit/{ => attic}/test_xmpp_transport_http.js (100%) rename services/sync/tests/unit/{ => need-work}/test_bookmark_syncing.js (100%) rename services/sync/tests/unit/{ => need-work}/test_bookmark_syncing.log.expected (100%) rename services/sync/tests/unit/{ => need-work}/test_cookie_store.js (100%) rename services/sync/tests/unit/{ => need-work}/test_passphrase_checking.js (100%) rename services/sync/tests/unit/{ => need-work}/test_password_syncing.js (100%) rename services/sync/tests/unit/{ => need-work}/test_password_syncing.log.expected (100%) rename services/sync/tests/unit/{ => need-work}/test_service.js (100%) rename services/sync/tests/unit/{ => need-work}/test_service.log.expected (100%) rename services/sync/tests/unit/{ => need-work}/test_util_tracebacks.js (100%) rename services/sync/tests/unit/{ => need-work}/test_util_tracebacks.log.expected (100%) diff --git a/services/sync/tests/unit/test_bookmark_sharing.js b/services/sync/tests/unit/attic/test_bookmark_sharing.js similarity index 100% rename from services/sync/tests/unit/test_bookmark_sharing.js rename to services/sync/tests/unit/attic/test_bookmark_sharing.js diff --git a/services/sync/tests/unit/test_sharing.js.disabled b/services/sync/tests/unit/attic/test_sharing.js similarity index 100% rename from services/sync/tests/unit/test_sharing.js.disabled rename to services/sync/tests/unit/attic/test_sharing.js diff --git a/services/sync/tests/unit/test_xmpp.js b/services/sync/tests/unit/attic/test_xmpp.js similarity index 100% rename from services/sync/tests/unit/test_xmpp.js rename to services/sync/tests/unit/attic/test_xmpp.js diff --git a/services/sync/tests/unit/test_xmpp_simple.js b/services/sync/tests/unit/attic/test_xmpp_simple.js similarity index 100% rename from services/sync/tests/unit/test_xmpp_simple.js rename to services/sync/tests/unit/attic/test_xmpp_simple.js diff --git a/services/sync/tests/unit/test_xmpp_transport_http.js b/services/sync/tests/unit/attic/test_xmpp_transport_http.js similarity index 100% rename from services/sync/tests/unit/test_xmpp_transport_http.js rename to services/sync/tests/unit/attic/test_xmpp_transport_http.js diff --git a/services/sync/tests/unit/test_bookmark_syncing.js b/services/sync/tests/unit/need-work/test_bookmark_syncing.js similarity index 100% rename from services/sync/tests/unit/test_bookmark_syncing.js rename to services/sync/tests/unit/need-work/test_bookmark_syncing.js diff --git a/services/sync/tests/unit/test_bookmark_syncing.log.expected b/services/sync/tests/unit/need-work/test_bookmark_syncing.log.expected similarity index 100% rename from services/sync/tests/unit/test_bookmark_syncing.log.expected rename to services/sync/tests/unit/need-work/test_bookmark_syncing.log.expected diff --git a/services/sync/tests/unit/test_cookie_store.js b/services/sync/tests/unit/need-work/test_cookie_store.js similarity index 100% rename from services/sync/tests/unit/test_cookie_store.js rename to services/sync/tests/unit/need-work/test_cookie_store.js diff --git a/services/sync/tests/unit/test_passphrase_checking.js b/services/sync/tests/unit/need-work/test_passphrase_checking.js similarity index 100% rename from services/sync/tests/unit/test_passphrase_checking.js rename to services/sync/tests/unit/need-work/test_passphrase_checking.js diff --git a/services/sync/tests/unit/test_password_syncing.js b/services/sync/tests/unit/need-work/test_password_syncing.js similarity index 100% rename from services/sync/tests/unit/test_password_syncing.js rename to services/sync/tests/unit/need-work/test_password_syncing.js diff --git a/services/sync/tests/unit/test_password_syncing.log.expected b/services/sync/tests/unit/need-work/test_password_syncing.log.expected similarity index 100% rename from services/sync/tests/unit/test_password_syncing.log.expected rename to services/sync/tests/unit/need-work/test_password_syncing.log.expected diff --git a/services/sync/tests/unit/test_service.js b/services/sync/tests/unit/need-work/test_service.js similarity index 100% rename from services/sync/tests/unit/test_service.js rename to services/sync/tests/unit/need-work/test_service.js diff --git a/services/sync/tests/unit/test_service.log.expected b/services/sync/tests/unit/need-work/test_service.log.expected similarity index 100% rename from services/sync/tests/unit/test_service.log.expected rename to services/sync/tests/unit/need-work/test_service.log.expected diff --git a/services/sync/tests/unit/test_util_tracebacks.js b/services/sync/tests/unit/need-work/test_util_tracebacks.js similarity index 100% rename from services/sync/tests/unit/test_util_tracebacks.js rename to services/sync/tests/unit/need-work/test_util_tracebacks.js diff --git a/services/sync/tests/unit/test_util_tracebacks.log.expected b/services/sync/tests/unit/need-work/test_util_tracebacks.log.expected similarity index 100% rename from services/sync/tests/unit/test_util_tracebacks.log.expected rename to services/sync/tests/unit/need-work/test_util_tracebacks.log.expected From bf0b649065c55160cab9c33f6bb12f714e536c23 Mon Sep 17 00:00:00 2001 From: Date: Thu, 4 Dec 2008 11:00:47 -0800 Subject: [PATCH 0752/1860] Added Linux-ARM processor compiled version of crypto module to repository. Slight modification of makefile to enable building this on ARM. --- services/crypto/Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 91bd999e3c05..ac93264562ce 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -215,11 +215,15 @@ ifeq ($(os), Linux) libs := $(patsubst %,-l%,$(libs)) cppflags += -pipe -Os \ -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -fshort-wchar -pthread \ + -fno-common -pthread \ -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ -Wno-long-long \ -include xpcom-config.h $(headers) +ifeq ($(arch), x86) + cppflags += -fshort-wchar +else +endif ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ -Wl,-dead_strip \ -Wl,-exported_symbol \ From bf22ae95edc57f1e8d68cf7e9cf351694a751921 Mon Sep 17 00:00:00 2001 From: Date: Thu, 4 Dec 2008 15:57:29 -0800 Subject: [PATCH 0753/1860] Moved the ARM version of WeaveCrypto.so to the /components directory (where every other platform will try to laod it and fail, while ARM will try to load it and succeed) --- services/sync/WeaveCrypto.so | Bin 0 -> 56986 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 services/sync/WeaveCrypto.so diff --git a/services/sync/WeaveCrypto.so b/services/sync/WeaveCrypto.so new file mode 100755 index 0000000000000000000000000000000000000000..0259efa9c6626898ec69c1abcc980b723f688f0b GIT binary patch literal 56986 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 LT7qN0kP!bL_4K;I literal 0 HcmV?d00001 From 2c02cfc4d938e5f6f50555b692c1fec647390513 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Dec 2008 00:39:54 -0800 Subject: [PATCH 0754/1860] use callbacks instead of snapshot diffs to figure out what changes to send to the server --- services/sync/modules/engines.js | 542 ++++++++------------- services/sync/modules/engines/bookmarks.js | 169 ++++--- services/sync/modules/service.js | 8 +- services/sync/modules/trackers.js | 132 ++++- 4 files changed, 418 insertions(+), 433 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5b8f481b4201..86ef2c0f2e7e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -100,7 +100,7 @@ EngineManagerSvc.prototype = { } }; -function Engine() {} +function Engine() { /* subclasses should call this._init() */} Engine.prototype = { _notify: Wrap.notify, @@ -134,7 +134,9 @@ Engine.prototype = { return json; }, - // _core, _store and _tracker need to be overridden in subclasses + get score() this._tracker.score, + + // _core, _store, and tracker need to be overridden in subclasses get _store() { let store = new Store(); this.__defineGetter__("_store", function() store); @@ -148,7 +150,7 @@ Engine.prototype = { }, get _tracker() { - let tracker = new tracker(); + let tracker = new Tracker(); this.__defineGetter__("_tracker", function() tracker); return tracker; }, @@ -174,6 +176,10 @@ Engine.prototype = { this._log = Log4Moz.repository.getLogger("Service." + this.logName); this._log.level = Log4Moz.Level[level]; this._osPrefix = "weave:" + this.name + ":"; + + this._tracker; // initialize tracker to load previously changed IDs + + this._log.debug("Engine initialized"); }, _serializeCommands: function Engine__serializeCommands(commands) { @@ -205,11 +211,6 @@ Engine.prototype = { throw "_sync needs to be subclassed"; }, - _initialUpload: function Engine__initialUpload() { - let self = yield; - throw "_initialUpload needs to be subclassed"; - }, - _share: function Engine__share(guid, username) { let self = yield; /* This should be overridden by the engine subclass for each datatype. @@ -247,41 +248,10 @@ Engine.prototype = { } }; -function NewEngine() {} -NewEngine.prototype = { +function SyncEngine() { /* subclasses should call this._init() */ } +SyncEngine.prototype = { __proto__: Engine.prototype, - get _snapshot() { - let snap = new SnapshotStore(this.name); - this.__defineGetter__("_snapshot", function() snap); - return snap; - }, - - get lastSync() { - try { - return Utils.prefs.getCharPref(this.name + ".lastSync"); - } catch (e) { - return 0; - } - }, - set lastSync(value) { - Utils.prefs.setCharPref(this.name + ".lastSync", value); - }, - - _incoming: null, - get incoming() { - if (!this._incoming) - this._incoming = []; - return this._incoming; - }, - - _outgoing: null, - get outgoing() { - if (!this._outgoing) - this._outgoing = []; - return this._outgoing; - }, - get baseURL() { let url = Utils.prefs.getCharPref("serverURL"); if (url && url[url.length-1] != '/') @@ -297,38 +267,92 @@ NewEngine.prototype = { return this.baseURL + ID.get('WeaveID').username + '/crypto/' + this.name; }, - _remoteSetup: function NewEngine__remoteSetup() { - let self = yield; - - let meta = yield CryptoMetas.get(self.cb, this.cryptoMetaURL); - if (!meta) { - let cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - getService(Ci.IWeaveCrypto); - let symkey = cryptoSvc.generateRandomKey(); - let pubkey = yield PubKeys.getDefaultKey(self.cb); - meta = new CryptoMeta(this.cryptoMetaURL); - meta.generateIV(); - yield meta.addUnwrappedKey(self.cb, pubkey, symkey); - yield meta.put(self.cb); + get lastSync() { + try { + return Utils.prefs.getCharPref(this.name + ".lastSync"); + } catch (e) { + return 0; } }, + set lastSync(value) { + Utils.prefs.setCharPref(this.name + ".lastSync", value); + }, - _createRecord: function NewEngine__newCryptoWrapper(id, payload, encrypt) { + // XXX these two should perhaps just be a variable inside sync(), but we have + // one or two other methods that use it + + get incoming() { + if (!this._incoming) + this._incoming = []; + return this._incoming; + }, + + get outgoing() { + if (!this._outgoing) + this._outgoing = []; + return this._outgoing; + }, + + // Create a new record starting from an ID + // Calls _serializeItem to get the actual item, but sometimes needs to be + // overridden anyway (to alter parentid or other properties outside the payload) + _createRecord: function SyncEngine__newCryptoWrapper(id, encrypt) { let self = yield; let record = new CryptoWrapper(); record.uri = this.engineURL + id; record.encryption = this.cryptoMetaURL; - record.cleartext = payload; - if (payload.parentGUID) // FIXME: should be parentid - record.parentid = payload.parentGUID; + record.cleartext = yield this._serializeItem.async(this, self.cb, id); + + if (record.cleartext) { + if (record.cleartext.parentid) + record.parentid = record.cleartext.parentid; + else if (record.cleartext.parentGUID) // FIXME: bookmarks-specific + record.parentid = record.cleartext.parentGUID; + } + if (encrypt || encrypt == undefined) yield record.encrypt(self.cb, ID.get('WeaveCryptoID').password); self.done(record); }, - _recDepth: function NewEngine__recDepth(rec) { + // Serialize an item. This will become the encrypted field in the payload of + // a record. Needs to be overridden in a subclass + _serializeItem: function SyncEngine__serializeItem(id) { + let self = yield; + self.done({}); + }, + + _getAllIDs: function SyncEngine__getAllIDs() { + let self = yield; + self.done({}); + }, + + // Check if a record is "like" another one, even though the IDs are different, + // in that case, we'll change the ID of the local item to match + // Probably needs to be overridden in a subclass, to change which criteria + // make two records "the same one" + _recordLike: function SyncEngine__recordLike(a, b) { + if (a.parentid != b.parentid) + return false; + return Utils.deepEquals(a.cleartext, b.cleartext); + }, + + _changeRecordRefs: function SyncEngine__changeRecordRefs(oldID, newID) { + let self = yield; + for each (let rec in this.outgoing) { + if (rec.parentid == oldID) + rec.parentid = newID; + } + }, + + _changeRecordID: function SyncEngine__changeRecordID(oldID, newID) { + let self = yield; + throw "_changeRecordID must be overridden in a subclass"; + }, + + _recDepth: function SyncEngine__recDepth(rec) { // we've calculated depth for this record already if (rec.depth) return rec.depth; @@ -349,72 +373,53 @@ NewEngine.prototype = { return 0; }, - _recordLike: function NewEngine__recordLike(a, b) { - // Check that all other properties are the same - if (!Utils.deepEquals(a.parentid, b.parentid)) - return false; - for (let key in a.cleartext) { - if (key == "parentGUID") - continue; // FIXME: bookmarks-specific - if (!Utils.deepEquals(a.cleartext[key], b.cleartext[key])) - return false; - } - for (key in b.cleartext) { - if (key == "parentGUID") - continue; // FIXME: bookmarks-specific - if (!Utils.deepEquals(a.cleartext[key], b.cleartext[key])) - return false; - } - return true; - }, - - _changeRecordRefs: function NewEngine__changeRecordRefs(oldID, newID) { - let self = yield; - for each (let rec in this.outgoing) { - if (rec.parentid == oldID) - rec.parentid = newID; - } - }, - - _changeRecordID: function NewEngine__changeRecordID(oldID, newID) { - let self = yield; - throw "_changeRecordID must be overridden in a subclass"; - }, - - _sync: function NewEngine__sync() { + // Any setup that needs to happen at the beginning of each sync. + // Makes sure crypto records and keys are all set-up + _syncStartup: function SyncEngine__syncStartup() { let self = yield; - // STEP 0: Get our crypto records in order this._log.debug("Ensuring server crypto records are there"); - yield this._remoteSetup.async(this, self.cb); + let meta = yield CryptoMetas.get(self.cb, this.cryptoMetaURL); + if (!meta) { + let cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + let symkey = cryptoSvc.generateRandomKey(); + let pubkey = yield PubKeys.getDefaultKey(self.cb); + meta = new CryptoMeta(this.cryptoMetaURL); + meta.generateIV(); + yield meta.addUnwrappedKey(self.cb, pubkey, symkey); + yield meta.put(self.cb); + } + }, + + // Generate outgoing records + _generateOutgoing: function SyncEngine__generateOutgoing() { + let self = yield; - // STEP 1: Generate outgoing items this._log.debug("Calculating client changes"); + // first sync special case: upload all items if (!this.lastSync) { - // first sync: upload all items - let all = this._store.wrap(); - for (let key in all) { - let record = yield this._createRecord.async(this, self.cb, key, all[key]); - this.outgoing.push(record); - } - } else { - // we've synced before: use snapshot to upload changes only - this._snapshot.load(); - let newsnap = this._store.wrap(); - let updates = yield this._core.detectUpdates(self.cb, - this._snapshot.data, newsnap); - for each (let cmd in updates) { - let data = ""; - if (cmd.action == "create" || cmd.action == "edit") - data = newsnap[cmd.GUID]; - let record = yield this._createRecord.async(this, self.cb, cmd.GUID, data); - this.outgoing.push(record); - } + this._log.info("First sync, uploading all items"); + let all = yield this._getAllIDs.async(this, self.cb); + for (let key in all) + this._tracker.addChangedID(key); } - // STEP 2.1: Find new items from server, place into incoming queue + // generate queue from changed items list + // NOTE we want changed items -> outgoing -> server to be as atomic as + // possible, so we clear the changed IDs after we upload the changed records + // NOTE2 don't encrypt, we'll do that before uploading instead + for (let id in this._tracker.changedIDs) { + this.outgoing.push(yield this._createRecord.async(this, self.cb, id, false)); + } + }, + + // Generate outgoing records + _fetchIncoming: function SyncEngine__fetchIncoming() { + let self = yield; + this._log.debug("Downloading server changes"); let newitems = new Collection(this.engineURL); @@ -426,8 +431,15 @@ NewEngine.prototype = { while ((item = yield newitems.iter.next(self.cb))) { this.incoming.push(item); } + }, + + // Process incoming records to get them ready for reconciliation and applying later + // i.e., decrypt them, and sort them + _processIncoming: function SyncEngine__processIncoming() { + let self = yield; + + this._log.debug("Decrypting and sorting incoming changes"); - // STEP 2.2: Decrypt items, then analyze incoming records and sort them for each (let inc in this.incoming) { yield inc.decrypt(self.cb, ID.get('WeaveCryptoID').password); this._recDepth(inc); // note: doesn't need access to payload @@ -441,18 +453,34 @@ NewEngine.prototype = { (a.depth == null && typeof(b.depth) == "number") || (a.depth < b.depth)) return -1; - if (a.cleartext.index > b.cleartext.index) - return 1; - if (a.cleartext.index < b.cleartext.index) - return -1; + if (a.cleartext && b.cleartext) { + if (a.cleartext.index > b.cleartext.index) + return 1; + if (a.cleartext.index < b.cleartext.index) + return -1; + } return 0; }); + }, + + // Reconciliation has two steps: + // 1) Check for the same item (same ID) on both the incoming and outgoing + // queues. This means the same item was modified on this profile and another + // at the same time. In this case, this client wins (which really means, the + // last profile you sync wins). + // 2) Check if any incoming & outgoing items are actually the same, even + // though they have different IDs. This happens when the same item is added + // on two different machines at the same time. For example, when a profile + // is synced for the first time after having (manually or otherwise) imported + // bookmarks imported, every bookmark will match this condition. + // When two items with different IDs are "the same" we change the local ID to + // match the remote one. + _reconcile: function SyncEngine__reconcile() { + let self = yield; - // STEP 3: Reconcile this._log.debug("Reconciling server/client changes"); - // STEP 3.1: Check for the same item (same ID) on both incoming & outgoing - // queues. Client one wins in this case. + // Check for the same item (same ID) on both incoming & outgoing queues let conflicts = []; for (let i = 0; i < this.incoming.length; i++) { for each (let out in this.outgoing) { @@ -463,12 +491,11 @@ NewEngine.prototype = { } } } - this._incoming = this.incoming.filter(function(i) i); // removes any holes + this._incoming = this.incoming.filter(function(n) n); // removes any holes if (conflicts.length) this._log.debug("Conflicts found. Conflicting server changes discarded"); - // STEP 3.2: Check if any incoming & outgoing items are really the same but - // with different IDs + // Check for items with different IDs which we think are the same one for (let i = 0; i < this.incoming.length; i++) { for (let o = 0; o < this.outgoing.length; o++) { if (this._recordLike(this.incoming[i], this.outgoing[o])) { @@ -477,272 +504,85 @@ NewEngine.prototype = { this.outgoing[o].id, this.incoming[i].id); // change actual id of item - yield this._changeRecordID.async(this, self.cb, - this.outgoing[o].id, - this.incoming[i].id); + yield this._changeItemID.async(this, self.cb, + this.outgoing[o].id, + this.incoming[i].id); delete this.incoming[i]; delete this.outgoing[o]; break; } } - this._outgoing = this.outgoing.filter(function(i) i); // removes any holes + this._outgoing = this.outgoing.filter(function(n) n); // removes any holes } - this._incoming = this.incoming.filter(function(i) i); // removes any holes + this._incoming = this.incoming.filter(function(n) n); // removes any holes + }, - // STEP 4: Apply incoming items + // Apply incoming records + _applyIncoming: function SyncEngine__applyIncoming() { + let self = yield; if (this.incoming.length) { this._log.debug("Applying server changes"); + this._tracker.disable(); let inc; while ((inc = this.incoming.shift())) { yield this._store.applyIncoming(self.cb, inc); if (inc.modified > this.lastSync) this.lastSync = inc.modified; } + this._tracker.enable(); } + }, - // STEP 5: Upload outgoing items + // Upload outgoing records + _uploadOutgoing: function SyncEngine__uploadOutgoing() { + let self = yield; if (this.outgoing.length) { this._log.debug("Uploading client changes"); let up = new Collection(this.engineURL); let out; while ((out = this.outgoing.pop())) { + yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); yield up.pushRecord(self.cb, out); } yield up.post(self.cb); if (up.data.modified > this.lastSync) this.lastSync = up.data.modified; } - - // STEP 6: Save the current snapshot so as to calculate changes at next sync - this._log.debug("Saving snapshot for next sync"); - this._snapshot.data = this._store.wrap(); - this._snapshot.save(); - - self.done(); + this._tracker.clearChangedIDs(); }, - _resetServer: function NewEngine__resetServer() { + // Any cleanup necessary. + // Save the current snapshot so as to calculate changes at next sync + _syncFinish: function SyncEngine__syncFinish() { let self = yield; - let all = new Resource(this.engineURL); - yield all.delete(self.cb); - } -}; - -function SyncEngine() {} -SyncEngine.prototype = { - __proto__: new Engine(), - - get _remote() { - let remote = new RemoteStore(this); - this.__defineGetter__("_remote", function() remote); - return remote; + this._log.debug("Finishing up sync"); + this._tracker.resetScore(); }, - get _snapshot() { - let snap = new SnapshotStore(this.name); - this.__defineGetter__("_snapshot", function() snap); - return snap; - }, - - _resetServer: function SyncEngine__resetServer() { - let self = yield; - yield this._remote.wipe(self.cb); - }, - - _resetClient: function SyncEngine__resetClient() { - let self = yield; - this._log.debug("Resetting client state"); - this._snapshot.wipe(); - this._store.wipe(); - this._log.debug("Client reset completed successfully"); - }, - - _initialUpload: function Engine__initialUpload() { - let self = yield; - this._log.info("Initial upload to server"); - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - yield this._remote.initialize(self.cb, this._snapshot); - this._snapshot.save(); - }, - - // original - // / \ - // A / \ B - // / \ - // client --C-> server - // \ / - // D \ / C - // \ / - // final - - // If we have a saved snapshot, original == snapshot. Otherwise, - // it's the empty set {}. - - // C is really the diff between server -> final, so if we determine - // D we can calculate C from that. In the case where A and B have - // no conflicts, C == A and D == B. - - // Sync flow: - // 1) Fetch server deltas - // 1.1) Construct current server status from snapshot + server deltas - // 1.2) Generate single delta from snapshot -> current server status ("B") - // 2) Generate local deltas from snapshot -> current client status ("A") - // 3) Reconcile client/server deltas and generate new deltas for them. - // Reconciliation won't generate C directly, we will simply diff - // server->final after step 3.1. - // 3.1) Apply local delta with server changes ("D") - // 3.2) Append server delta to the delta file and upload ("C") - _sync: function SyncEngine__sync() { let self = yield; - this._log.info("Beginning sync"); - this._os.notifyObservers(null, "weave:service:sync:engine:start", this.displayName); + yield this._syncStartup.async(this, self.cb); - this._snapshot.load(); + // Populate incoming and outgoing queues + yield this._generateOutgoing.async(this, self.cb); + yield this._fetchIncoming.async(this, self.cb); - try { - this._remote.status.data; // FIXME - otherwise we get an error... - yield this._remote.openSession(self.cb, this._snapshot); + // Decrypt and sort incoming records, then reconcile + yield this._processIncoming.async(this, self.cb); + yield this._reconcile.async(this, self.cb); - } catch (e if e.status == 404) { - yield this._initialUpload.async(this, self.cb); - return; - } + // Apply incoming records, upload outgoing records + yield this._applyIncoming.async(this, self.cb); + yield this._uploadOutgoing.async(this, self.cb); - // 1) Fetch server deltas + yield this._syncFinish.async(this, self.cb); + }, - this._os.notifyObservers(null, "weave:service:sync:status", "status.downloading-deltas"); - let serverSnap = yield this._remote.wrap(self.cb); - let serverUpdates = yield this._core.detectUpdates(self.cb, - this._snapshot.data, serverSnap); - - // 2) Generate local deltas from snapshot -> current client status - - this._os.notifyObservers(null, "weave:service:sync:status", "status.calculating-differences"); - let localSnap = new SnapshotStore(); - localSnap.data = this._store.wrap(); - this._core.detectUpdates(self.cb, this._snapshot.data, localSnap.data); - let localUpdates = yield; - - this._log.trace("local json:\n" + localSnap.serialize()); - this._log.trace("Local updates: " + this._serializeCommands(localUpdates)); - this._log.trace("Server updates: " + this._serializeCommands(serverUpdates)); - - if (serverUpdates.length == 0 && localUpdates.length == 0) { - this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); - this._log.info("Sync complete: no changes needed on client or server"); - this._snapshot.version = this._remote.status.data.maxVersion; - this._snapshot.save(); - self.done(true); - return; - } - - // 3) Reconcile client/server deltas and generate new deltas for them. - - this._os.notifyObservers(null, "weave:service:sync:status", "status.reconciling-updates"); - this._log.info("Reconciling client/server updates"); - let ret = yield this._core.reconcile(self.cb, localUpdates, serverUpdates); - - let clientChanges = ret.propagations[0]; - let serverChanges = ret.propagations[1]; - let clientConflicts = ret.conflicts[0]; - let serverConflicts = ret.conflicts[1]; - - this._log.info("Changes for client: " + clientChanges.length); - this._log.info("Predicted changes for server: " + serverChanges.length); - this._log.info("Client conflicts: " + clientConflicts.length); - this._log.info("Server conflicts: " + serverConflicts.length); - this._log.trace("Changes for client: " + this._serializeCommands(clientChanges)); - this._log.trace("Predicted changes for server: " + this._serializeCommands(serverChanges)); - this._log.trace("Client conflicts: " + this._serializeConflicts(clientConflicts)); - this._log.trace("Server conflicts: " + this._serializeConflicts(serverConflicts)); - - if (!(clientChanges.length || serverChanges.length || - clientConflicts.length || serverConflicts.length)) { - this._os.notifyObservers(null, "weave:service:sync:status", "status.no-changes-required"); - this._log.info("Sync complete: no changes needed on client or server"); - this._snapshot.data = localSnap.data; - this._snapshot.version = this._remote.status.data.maxVersion; - this._snapshot.save(); - self.done(true); - return; - } - - if (clientConflicts.length || serverConflicts.length) - this._log.warn("Conflicts found! Discarding server changes"); - - // 3.1) Apply server changes to local store - - if (clientChanges.length) { - this._log.info("Applying changes locally"); - this._os.notifyObservers(null, "weave:service:sync:status", "status.applying-changes"); - - // apply to real store - yield this._store.applyCommands.async(this._store, self.cb, clientChanges); - - // get the current state - let newSnap = new SnapshotStore(); - newSnap.data = this._store.wrap(); - - // apply to the snapshot we got in step 1, and compare with current state - yield localSnap.applyCommands.async(localSnap, self.cb, clientChanges); - let diff = yield this._core.detectUpdates(self.cb, - localSnap.data, newSnap.data); - if (diff.length != 0) { - this._log.warn("Commands did not apply correctly"); - this._log.trace("Diff from snapshot+commands -> " + - "new snapshot after commands:\n" + - this._serializeCommands(diff)); - } - - // update the local snap to the current state, we'll use it below - localSnap.data = newSnap.data; - localSnap.version = this._remote.status.data.maxVersion; - } - - // 3.2) Append server delta to the delta file and upload - - // Generate a new diff, from the current server snapshot to the - // current client snapshot. In the case where there are no - // conflicts, it should be the same as what the resolver returned - - this._os.notifyObservers(null, "weave:service:sync:status", - "status.calculating-differences"); - let serverDelta = yield this._core.detectUpdates(self.cb, - serverSnap, localSnap.data); - - // Log an error if not the same - if (!(serverConflicts.length || - Utils.deepEquals(serverChanges, serverDelta))) - this._log.warn("Predicted server changes differ from " + - "actual server->client diff (can be ignored in many cases)"); - - this._log.info("Actual changes for server: " + serverDelta.length); - this._log.trace("Actual changes for server: " + - this._serializeCommands(serverDelta)); - - if (serverDelta.length) { - this._log.info("Uploading changes to server"); - this._os.notifyObservers(null, "weave:service:sync:status", - "status.uploading-deltas"); - - yield this._remote.appendDelta(self.cb, localSnap, serverDelta, - {maxVersion: this._snapshot.version, - deltasEncryption: Crypto.defaultAlgorithm}); - localSnap.version = this._remote.status.data.maxVersion; - - this._log.info("Successfully updated deltas and status on server"); - } - - this._snapshot.data = localSnap.data; - this._snapshot.version = localSnap.version; - this._snapshot.save(); - - this._log.info("Sync complete"); - self.done(true); + _resetServer: function SyncEngine__resetServer() { + let self = yield; + let all = new Resource(this.engineURL); + yield all.delete(self.cb); } }; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index de8dd03150b7..3be9fe9f8a71 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -665,7 +665,7 @@ BookmarksSharingManager.prototype = { if (serverContents[guid].type != "bookmark") delete serverContents[guid]; else - serverContents[guid].parentGUID = mountData.rootGUID; + serverContents[guid].parentid = mountData.rootGUID; } /* Wipe old local contents of the folder, starting from the node: */ @@ -711,53 +711,81 @@ BookmarksSharingManager.prototype = { } } - - function BookmarksEngine(pbeId) { this._init(pbeId); } BookmarksEngine.prototype = { - __proto__: NewEngine.prototype, - get _super() NewEngine.prototype, + __proto__: SyncEngine.prototype, + get _super() SyncEngine.prototype, - get name() { return "bookmarks"; }, - get displayName() { return "Bookmarks"; }, - get logName() { return "BmkEngine"; }, - get serverPrefix() { return "user-data/bookmarks/"; }, + get name() "bookmarks", + get displayName() "Bookmarks", + get logName() "BmkEngine", + get serverPrefix() "user-data/bookmarks/", - __store: null, get _store() { - if (!this.__store) - this.__store = new BookmarksStore(); - return this.__store; + let store = new BookmarksStore(); + this.__defineGetter__("_store", function() store); + return store; }, - __core: null, get _core() { - if (!this.__core) - this.__core = new BookmarksSyncCore(this._store); - return this.__core; + let core = new BookmarksSyncCore(); + this.__defineGetter__("_core", function() core); + return core; }, - __tracker: null, get _tracker() { - if (!this.__tracker) - this.__tracker = new BookmarksTracker(this); - return this.__tracker; + let tracker = new BookmarksTracker(); + this.__defineGetter__("_tracker", function() tracker); + return tracker; }, - _changeRecordRefs: function NewEngine__changeRecordRefs(oldID, newID) { + _getAllIDs: function BmkEngine__getAllIDs() { + let self = yield; + let all = this._store.wrap(); // FIXME: using store is an inefficient hack... + delete all["unfiled"]; + delete all["toolbar"]; + delete all["menu"]; + self.done(all); + }, + + _serializeItem: function BmkEngine__serializeItem(id) { + let self = yield; + let all = this._store.wrap(); // FIXME OMG SO INEFFICIENT + self.done(all[id]); + }, + + _recordLike: function SyncEngine__recordLike(a, b) { + if (a.parentid != b.parentid) + return false; + for (let key in a.cleartext) { + if (key == "index") + continue; + if (!Utils.deepEquals(a.cleartext[key], b.cleartext[key])) + return false; + } + for (key in b.cleartext) { + if (key == "index") + continue; + if (!Utils.deepEquals(a.cleartext[key], b.cleartext[key])) + return false; + } + return true; + }, + + _changeRecordRefs: function BmkEngine__changeRecordRefs(oldID, newID) { let self = yield; for each (let rec in this.outgoing) { if (rec.parentid == oldID) { rec.parentid = newID; - rec.cleartext.parentGUID = newID; + rec.cleartext.parentid = newID; yield rec.encrypt(self.cb, ID.get('WeaveCryptoID').password); } } }, - _changeRecordID: function BSS__changeRecordID(oldID, newID) { + _changeRecordID: function BmkEngine__changeRecordID(oldID, newID) { let self = yield; yield this._store._changeRecordID.async(this._store, self.cb, oldID, newID); } @@ -809,7 +837,7 @@ BookmarksSyncCore.prototype = { a.action != b.action || a.action != "create" || a.data.type != b.data.type || - a.data.parentGUID != b.data.parentGUID || + a.data.parentid != b.data.parentid || a.GUID == b.GUID) return false; @@ -932,7 +960,7 @@ BookmarksStore.prototype = { this._log.trace("RECORD: " + record.id + " -> " + uneval(record.cleartext)); - if (record.cleartext == "") + if (!record.cleartext) this._removeCommand({GUID: record.id}); else if (this._getItemIdForGUID(record.id) < 0) this._createCommand({GUID: record.id, data: record.cleartext}); @@ -948,7 +976,7 @@ BookmarksStore.prototype = { _createCommand: function BStore__createCommand(command) { let newId; - let parentId = this._getItemIdForGUID(command.data.parentGUID); + let parentId = this._getItemIdForGUID(command.data.parentid); if (parentId < 0) { this._log.warn("Creating node with unknown parent -> reparenting to root"); @@ -1041,8 +1069,13 @@ BookmarksStore.prototype = { this._log.error("_createCommand: Unknown item type: " + command.data.type); break; } - if (newId) + if (newId) { + this._log.trace("Setting GUID of new item " + newId + " to " + command.GUID); this._bms.setItemGUID(newId, command.GUID); + let foo = this._bms.getItemGUID(command.GUID); + if (foo == newId) + this._log.debug("OK!"); + } }, _removeCommand: function BStore__removeCommand(command) { @@ -1055,6 +1088,7 @@ BookmarksStore.prototype = { } var itemId = this._bms.getItemIdForGUID(command.GUID); + this._log.debug("woo: " + itemId); if (itemId < 0) { this._log.warn("Attempted to remove item " + command.GUID + ", but it does not exist. Skipping."); @@ -1090,7 +1124,7 @@ BookmarksStore.prototype = { return; } - var itemId = this._bms.getItemIdForGUID(command.GUID); + var itemId = this._getItemIdForGUID(command.GUID); if (itemId < 0) { this._log.debug("Item for GUID " + command.GUID + " not found. Skipping."); return; @@ -1113,25 +1147,25 @@ BookmarksStore.prototype = { let curIdx = this._bms.getItemIndex(itemId); if (curIdx != command.data.index) { // ignore index if we're going to move the item to another folder altogether - if (command.data.parentGUID && + if (command.data.parentid && (this._bms.getFolderIdForItem(itemId) != - this._getItemIdForGUID(command.data.parentGUID))) + this._getItemIdForGUID(command.data.parentid))) break; this._log.trace("Moving item (changing index)"); this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), command.data.index); } break; - case "parentGUID": { - if (command.data.parentGUID && + case "parentid": { + if (command.data.parentid && (this._bms.getFolderIdForItem(itemId) != - this._getItemIdForGUID(command.data.parentGUID))) { + this._getItemIdForGUID(command.data.parentid))) { this._log.trace("Moving item (changing folder)"); let index = -1; if (command.data.index && command.data.index >= 0) index = command.data.index; this._bms.moveItem(itemId, - this._getItemIdForGUID(command.data.parentGUID), index); + this._getItemIdForGUID(command.data.parentid), index); } } break; case "tags": { @@ -1185,10 +1219,12 @@ BookmarksStore.prototype = { } }, - _changeRecordID: function BSS__changeRecordID(oldID, newID) { + _changeItemID: function BSS__changeItemID(oldID, newID) { let self = yield; - var itemId = this._bms.getItemIdForGUID(oldID); + var itemId = this._getItemIdForGUID(oldID); + if (itemId == null) // toplevel folder + return; if (itemId < 0) { this._log.warn("Can't change GUID " + oldID + " to " + newID + ": Item does not exist"); @@ -1212,7 +1248,7 @@ BookmarksStore.prototype = { return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; }, - __wrap: function BSS___wrap(node, items, parentGUID, index, guidOverride) { + __wrap: function BSS___wrap(node, items, parentid, index, guidOverride) { let GUID, item; // we override the guid for the root items, "menu", "toolbar", etc. @@ -1221,7 +1257,7 @@ BookmarksStore.prototype = { item = {}; } else { GUID = this._bms.getItemGUID(node.itemId); - item = {parentGUID: parentGUID, index: index}; + item = {parentid: parentid, index: index}; } if (node.type == node.RESULT_TYPE_FOLDER) { @@ -1411,27 +1447,27 @@ BookmarksStore.prototype = { * engine. How the engine decides to set the score is upto it, * as long as the value between 0 and 100 actually corresponds * to its urgency to sync. - * - * Here's an example BookmarksTracker. We don't subclass getScore - * because the observer methods take care of updating _score which - * getScore returns by default. */ -function BookmarksTracker(engine) { - this._init(engine); +function BookmarksTracker() { + this._init(); } BookmarksTracker.prototype = { __proto__: Tracker.prototype, - _logName: "BMTracker", + _logName: "BmkTracker", + file: "bookmarks", + + get _bms() { + let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + this.__defineGetter__("_bms", function() bms); + return bms; + }, QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]), - _init: function BMT__init(engine) { - this._engine = engine; - this._log = Log4Moz.repository.getLogger("Service." + this._logName); - this._score = 0; - Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService). - addObserver(this, false); + _init: function BMT__init() { + this.__proto__.__proto__._init.call(this); + this._bms.addObserver(this, false); }, // FIXME: not getting any events whatsoever! @@ -1439,25 +1475,38 @@ BookmarksTracker.prototype = { /* Every add/remove/change is worth 10 points */ onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { - this._log.debug("Item " + itemId + " added, adding to queue"); + if (!this.enabled) + return; + let guid = this._bms.getItemGUID(itemId); + this._log.debug("Item " + guid + " added, adding to queue"); + this.addChangedID(guid); this._score += 10; - //let all = this._engine._store.wrap(); - //let record = yield this._engine._createRecord.async(this, self.cb, key, all[itemId]); - //this._engine.outgoing.push(record); }, onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { - this._log.debug("Item " + itemId + " removed, adding to queue"); + if (!this.enabled) + return; + let guid = this._bms.getItemGUID(itemId); + this._log.debug("Item " + guid + " removed, adding to queue"); + this.addChangedID(guid); this._score += 10; }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnnotationProperty, value) { - this._log.debug("Item " + itemId + " changed, adding to queue"); + if (!this.enabled) + return; + let guid = this._bms.getItemGUID(itemId); + this._log.debug("Item " + guid + " changed, adding to queue"); + this.addChangedID(guid); this._score += 10; }, onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { - this._log.debug("Item " + itemId + " moved, adding to queue"); + if (!this.enabled) + return; + let guid = this._bms.getItemGUID(itemId); + this._log.debug("Item " + guid + " moved, adding to queue"); + this.addChangedID(guid); this._score += 10; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ccfa33d6aab3..98048342436b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -600,7 +600,7 @@ WeaveSvc.prototype = { if (!(engine.name in this._syncThresholds)) this._syncThresholds[engine.name] = INITIAL_THRESHOLD; - let score = engine._tracker.score; + let score = engine.score; if (score >= this._syncThresholds[engine.name]) { this._log.debug(engine.name + " score " + score + " reaches threshold " + @@ -641,10 +641,8 @@ WeaveSvc.prototype = { _syncEngine: function WeaveSvc__syncEngine(engine) { let self = yield; - try { - yield engine.sync(self.cb); - engine._tracker.resetScore(); - } catch(e) { + try { yield engine.sync(self.cb); } + catch(e) { // FIXME: FT module is not printing out exceptions - it should be this._log.warn("Engine exception: " + e); let ok = FaultTolerance.Service.onException(e); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index ef7a075bd220..0026b13bf7cf 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -51,32 +51,56 @@ Function.prototype.async = Async.sugar; /* * Trackers are associated with a single engine and deal with - * listening for changes to their particular data type + * listening for changes to their particular data type. + * + * There are two things they keep track of: + * 1) A score, indicating how urgently the engine wants to sync + * 2) A list of IDs for all the changed items that need to be synced * and updating their 'score', indicating how urgently they * want to sync. * - * 'score's range from 0 (Nothing's changed) - * to 100 (I need to sync now!) - * -1 is also a valid score - * (don't sync me unless the user specifically requests it) - * - * Setting a score outside of this range will raise an exception. - * Well not yet, but it will :) */ function Tracker() { this._init(); } Tracker.prototype = { _logName: "Tracker", - _score: 0, + file: "none", - _init: function T__init() { - this._log = Log4Moz.repository.getLogger("Service." + this._logName); - this._score = 0; + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() json); + return json; }, - /* Should be called by service periodically - * before deciding which engines to sync + _init: function T__init() { + this._log = Log4Moz.repository.getLogger(this._logName); + this._score = 0; + this.loadChangedIDs(); + this.enable(); + }, + + get enabled() { + return this._enabled; + }, + + enable: function T_enable() { + this._enabled = true; + }, + + disable: function T_disable() { + this._enabled = false; + }, + + /* + * Score can be called as often as desired to decide which engines to sync + * + * Valid values for score: + * -1: Do not sync unless the user specifically requests it (almost disabled) + * 0: Nothing has changed + * 100: Please sync me ASAP! + * + * Setting it to other values should (but doesn't currently) throw an exception */ get score() { if (this._score >= 100) @@ -85,10 +109,84 @@ Tracker.prototype = { return this._score; }, - /* Should be called by service everytime a sync - * has been done for an engine - */ + // Should be called by service everytime a sync has been done for an engine resetScore: function T_resetScore() { this._score = 0; + }, + + /* + * Changed IDs are in an object (hash) to make it easy to check if + * one is already set or not. + * Note that it would be nice to make these methods asynchronous so + * as to not block when writing to disk. However, these will often + * get called from observer callbacks, and so it is better to make + * them synchronous. + */ + + get changedIDs() { + let items = {}; + this.__defineGetter__("changedIDs", function() items); + return items; + }, + + saveChangedIDs: function T_saveChangedIDs() { + this._log.trace("Saving changed IDs to disk"); + + let file = Utils.getProfileFile( + {path: "weave/changes/" + this.file + ".json", + autoCreate: true}); + let out = this._json.encode(this.changedIDs); + let [fos] = Utils.open(file, ">"); + fos.writeString(out); + fos.close(); + }, + + loadChangedIDs: function T_loadChangedIDs() { + let file = Utils.getProfileFile("weave/changes/" + this.file + ".json"); + if (!file.exists()) + return; + + this._log.trace("Loading previously changed IDs from disk"); + + try { + let [is] = Utils.open(file, "<"); + let json = Utils.readStream(is); + is.close(); + + let ids = this._json.decode(json); + for (let id in ids) { + this.changedIDs[id] = 1; + } + } catch (e) { + this._log.warn("Could not load changed IDs from previous session"); + this._log.debug("Exception: " + e); + } + }, + + addChangedID: function T_addChangedID(id) { + if (!this.enabled) + return; + if (!this.changedIDs[id]) { + this.changedIDs[id] = true; + this.saveChangedIDs(); + } + }, + + removeChangedID: function T_removeChangedID(id) { + if (!this.enabled) + return; + if (this.changedIDs[id]) { + delete this.changedIDs[id]; + this.saveChangedIDs(); + } + }, + + clearChangedIDs: function T_clearChangedIDs() { + if (!this.enabled) + return; + for (let id in this.changedIDs) { + delete this.changedIDs[id]; + } + this.saveChangedIDs(); } }; From d0359084300ff0b8b3109dea4b143394fcd0b09e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Dec 2008 00:55:19 -0800 Subject: [PATCH 0755/1860] disable tracker for entire sync run. make sure tracker is re-enabled at the end --- services/sync/modules/engines.js | 37 +++++++++++++--------- services/sync/modules/engines/bookmarks.js | 4 +-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 86ef2c0f2e7e..434fba5bc71a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -347,7 +347,7 @@ SyncEngine.prototype = { } }, - _changeRecordID: function SyncEngine__changeRecordID(oldID, newID) { + _changeItemID: function SyncEngine__changeItemID(oldID, newID) { let self = yield; throw "_changeRecordID must be overridden in a subclass"; }, @@ -390,6 +390,8 @@ SyncEngine.prototype = { meta.generateIV(); yield meta.addUnwrappedKey(self.cb, pubkey, symkey); yield meta.put(self.cb); + + this._tracker.disable(); } }, @@ -522,14 +524,12 @@ SyncEngine.prototype = { let self = yield; if (this.incoming.length) { this._log.debug("Applying server changes"); - this._tracker.disable(); let inc; while ((inc = this.incoming.shift())) { yield this._store.applyIncoming(self.cb, inc); if (inc.modified > this.lastSync) this.lastSync = inc.modified; } - this._tracker.enable(); } }, @@ -557,26 +557,33 @@ SyncEngine.prototype = { let self = yield; this._log.debug("Finishing up sync"); this._tracker.resetScore(); + this._tracker.enable(); }, _sync: function SyncEngine__sync() { let self = yield; - yield this._syncStartup.async(this, self.cb); + try { + yield this._syncStartup.async(this, self.cb); - // Populate incoming and outgoing queues - yield this._generateOutgoing.async(this, self.cb); - yield this._fetchIncoming.async(this, self.cb); + // Populate incoming and outgoing queues + yield this._generateOutgoing.async(this, self.cb); + yield this._fetchIncoming.async(this, self.cb); - // Decrypt and sort incoming records, then reconcile - yield this._processIncoming.async(this, self.cb); - yield this._reconcile.async(this, self.cb); + // Decrypt and sort incoming records, then reconcile + yield this._processIncoming.async(this, self.cb); + yield this._reconcile.async(this, self.cb); - // Apply incoming records, upload outgoing records - yield this._applyIncoming.async(this, self.cb); - yield this._uploadOutgoing.async(this, self.cb); - - yield this._syncFinish.async(this, self.cb); + // Apply incoming records, upload outgoing records + yield this._applyIncoming.async(this, self.cb); + yield this._uploadOutgoing.async(this, self.cb); + } + catch (e) { + throw e; + } + finally { + yield this._syncFinish.async(this, self.cb); + } }, _resetServer: function SyncEngine__resetServer() { diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 3be9fe9f8a71..583e7741548c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -785,9 +785,9 @@ BookmarksEngine.prototype = { } }, - _changeRecordID: function BmkEngine__changeRecordID(oldID, newID) { + _changeItemID: function BmkEngine__changeRecordID(oldID, newID) { let self = yield; - yield this._store._changeRecordID.async(this._store, self.cb, oldID, newID); + yield this._store._changeItemID.async(this._store, self.cb, oldID, newID); } // XXX for sharing, will need to re-add code to get new shares before syncing, From baa577ea4fdb5196b52de71d1ed9e6e1c5cbeb1f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Dec 2008 00:58:25 -0800 Subject: [PATCH 0756/1860] oops, always disable tracker during sync --- services/sync/modules/engines.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 434fba5bc71a..40e6d078f30b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -390,9 +390,8 @@ SyncEngine.prototype = { meta.generateIV(); yield meta.addUnwrappedKey(self.cb, pubkey, symkey); yield meta.put(self.cb); - - this._tracker.disable(); } + this._tracker.disable(); }, // Generate outgoing records From 5be5a82d779faba6e3d25cc151f49a6e20689d76 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Dec 2008 03:28:17 -0800 Subject: [PATCH 0757/1860] re-enable tracker in the first-sync case just to add all the guids; add some debugging messages; when reconciling throw out records which are identical on the client and server (even if we thought they had changed) --- services/sync/modules/engines.js | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 40e6d078f30b..d26f3dc333df 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -404,8 +404,11 @@ SyncEngine.prototype = { if (!this.lastSync) { this._log.info("First sync, uploading all items"); let all = yield this._getAllIDs.async(this, self.cb); - for (let key in all) + this._tracker.enable(); + for (let key in all) { this._tracker.addChangedID(key); + } + this._tracker.disable(); } // generate queue from changed items list @@ -481,16 +484,26 @@ SyncEngine.prototype = { this._log.debug("Reconciling server/client changes"); + this._log.debug(this.incoming.length + " items coming in, " + + this.outgoing.length + " items going out"); + // Check for the same item (same ID) on both incoming & outgoing queues let conflicts = []; for (let i = 0; i < this.incoming.length; i++) { - for each (let out in this.outgoing) { - if (this.incoming[i].id == out.id) { - conflicts.push({in: this.incoming[i], out: out}); + for (let o = 0; o < this.outgoing.length; o++) { + if (this.incoming[i].id == this.outgoing[o].id) { + // Only consider it a conflict if there are actual differences + // otherwise, just remove the outgoing record as well + if (!Utils.deepEquals(this.incoming[i].cleartext, + this.outgoing[o].cleartext)) + conflicts.push({in: this.incoming[i], out: this.outgoing[o]}); + else + delete this.outgoing[o]; delete this.incoming[i]; break; } } + this._outgoing = this.outgoing.filter(function(n) n); // removes any holes } this._incoming = this.incoming.filter(function(n) n); // removes any holes if (conflicts.length) @@ -516,6 +529,10 @@ SyncEngine.prototype = { this._outgoing = this.outgoing.filter(function(n) n); // removes any holes } this._incoming = this.incoming.filter(function(n) n); // removes any holes + + this._log.debug("Reconciliation complete"); + this._log.debug(this.incoming.length + " items coming in, " + + this.outgoing.length + " items going out"); }, // Apply incoming records @@ -552,7 +569,7 @@ SyncEngine.prototype = { // Any cleanup necessary. // Save the current snapshot so as to calculate changes at next sync - _syncFinish: function SyncEngine__syncFinish() { + _syncFinish: function SyncEngine__syncFinish(error) { let self = yield; this._log.debug("Finishing up sync"); this._tracker.resetScore(); @@ -576,12 +593,15 @@ SyncEngine.prototype = { // Apply incoming records, upload outgoing records yield this._applyIncoming.async(this, self.cb); yield this._uploadOutgoing.async(this, self.cb); + + yield this._syncFinish.async(this, self.cb); } catch (e) { + this._log.warn("Sync failed"); throw e; } finally { - yield this._syncFinish.async(this, self.cb); + this._tracker.enable(); } }, From 4ef8fe6f9bd79ae1e09d09792c4913a7275351f6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Dec 2008 03:36:27 -0800 Subject: [PATCH 0758/1860] parentGUID -> parentid --- services/sync/modules/engines.js | 6 +----- services/sync/modules/engines/cookies.js | 24 ++++++++++++------------ services/sync/modules/engines/history.js | 11 +++++------ services/sync/modules/stores.js | 8 ++++---- services/sync/modules/syncCores.js | 12 ++++++------ 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index d26f3dc333df..d59ed1dfe4c1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -304,12 +304,8 @@ SyncEngine.prototype = { record.encryption = this.cryptoMetaURL; record.cleartext = yield this._serializeItem.async(this, self.cb, id); - if (record.cleartext) { - if (record.cleartext.parentid) + if (record.cleartext && record.cleartext.parentid) record.parentid = record.cleartext.parentid; - else if (record.cleartext.parentGUID) // FIXME: bookmarks-specific - record.parentid = record.cleartext.parentGUID; - } if (encrypt || encrypt == undefined) yield record.encrypt(self.cb, ID.get('WeaveCryptoID').password); diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index c344d97c42b2..78c53ed3e6eb 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -257,18 +257,18 @@ CookieStore.prototype = { } let key = cookie.host + ":" + cookie.path + ":" + cookie.name; - items[ key ] = { parentGUID: '', - name: cookie.name, - value: cookie.value, - isDomain: cookie.isDomain, - host: cookie.host, - path: cookie.path, - isSecure: cookie.isSecure, - // nsICookie2 values: - rawHost: cookie.rawHost, - isSession: cookie.isSession, - expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly }; + items[ key ] = { parentid: '', + name: cookie.name, + value: cookie.value, + isDomain: cookie.isDomain, + host: cookie.host, + path: cookie.path, + isSecure: cookie.isSecure, + // nsICookie2 values: + rawHost: cookie.rawHost, + isSession: cookie.isSession, + expiry: cookie.expiry, + isHttpOnly: cookie.isHttpOnly }; /* See http://developer.mozilla.org/en/docs/nsICookie Note: not syncing "expires", "status", or "policy" diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 4004f2103a11..62a1f4d31191 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -226,12 +226,11 @@ HistoryStore.prototype = { let item = root.getChild(i); let guid = item.time + ":" + item.uri; let vType = this._getVisitType(item.uri); - items[guid] = {parentGUID: '', - title: item.title, - URI: item.uri, - time: item.time, - transition: vType - }; + items[guid] = {parentid: '', + title: item.title, + URI: item.uri, + time: item.time, + transition: vType}; } this._lookup = items; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index f64816ac873e..813693ceb967 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -201,9 +201,9 @@ SnapshotStore.prototype = { delete this._data[oldGUID]; for (let GUID in this._data) { - if (("parentGUID" in this._data[GUID]) && - (this._data[GUID].parentGUID == oldGUID)) - this._data[GUID].parentGUID = newGUID; + if (("parentid" in this._data[GUID]) && + (this._data[GUID].parentid == oldGUID)) + this._data[GUID].parentid = newGUID; } } for (let prop in command.data) { @@ -261,7 +261,7 @@ SnapshotStore.prototype = { let json = this._json.encode(this.data); json = json.replace(/:{type/g, ":\n\t{type"); json = json.replace(/}, /g, "},\n "); - json = json.replace(/, parentGUID/g, ",\n\t parentGUID"); + json = json.replace(/, parentid/g, ",\n\t parentid"); json = json.replace(/, index/g, ",\n\t index"); json = json.replace(/, title/g, ",\n\t title"); json = json.replace(/, URI/g, ",\n\t URI"); diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js index f1d391001629..1afc8f17a4f4 100644 --- a/services/sync/modules/syncCores.js +++ b/services/sync/modules/syncCores.js @@ -88,10 +88,10 @@ SyncCore.prototype = { }, _nodeParentsInt: function SC__nodeParentsInt(GUID, tree, parents) { - if (!tree[GUID] || !tree[GUID].parentGUID) + if (!tree[GUID] || !tree[GUID].parentid) return parents; - parents.push(tree[GUID].parentGUID); - return this._nodeParentsInt(tree[GUID].parentGUID, tree, parents); + parents.push(tree[GUID].parentid); + return this._nodeParentsInt(tree[GUID].parentid, tree, parents); }, _detectUpdates: function SC__detectUpdates(a, b) { @@ -180,9 +180,9 @@ SyncCore.prototype = { for (let i = 0; i < list.length; i++) { if (!list[i]) continue; - if (list[i].data && list[i].data.parentGUID && - list[i].data.parentGUID == oldGUID) - list[i].data.parentGUID = newGUID; + if (list[i].data && list[i].data.parentid && + list[i].data.parentid == oldGUID) + list[i].data.parentid = newGUID; for (let j = 0; j < list[i].parents.length; j++) { if (list[i].parents[j] == oldGUID) list[i].parents[j] = newGUID; From c60d9a3569c8705f19b71b6e2336786b753dbed5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 6 Dec 2008 00:08:54 -0800 Subject: [PATCH 0759/1860] clear changed IDs before adding all current ones in first-sync case; edit the ID list directly instead of enabling/disabling the tracker --- services/sync/modules/engines.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index d59ed1dfe4c1..3c41383b935d 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -397,14 +397,19 @@ SyncEngine.prototype = { this._log.debug("Calculating client changes"); // first sync special case: upload all items + // note that we use a backdoor (of sorts) to the tracker and it + // won't save to disk this list if (!this.lastSync) { this._log.info("First sync, uploading all items"); + + // remove any old ones first + this._tracker.clearChangedIDs(); + + // now add all current ones let all = yield this._getAllIDs.async(this, self.cb); - this._tracker.enable(); - for (let key in all) { - this._tracker.addChangedID(key); + for (let id in all) { + this._tracker.changedIDs[id] = true; } - this._tracker.disable(); } // generate queue from changed items list From 8038d65adda613443996a9b00d6146d69f5e0105 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 6 Dec 2008 00:11:16 -0800 Subject: [PATCH 0760/1860] more/better logging; allow changed IDs list to be cleared even when tracker is disabled --- services/sync/modules/trackers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 0026b13bf7cf..8bd6c05ad5c9 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -130,7 +130,7 @@ Tracker.prototype = { }, saveChangedIDs: function T_saveChangedIDs() { - this._log.trace("Saving changed IDs to disk"); + this._log.debug("Saving changed IDs to disk"); let file = Utils.getProfileFile( {path: "weave/changes/" + this.file + ".json", @@ -146,7 +146,7 @@ Tracker.prototype = { if (!file.exists()) return; - this._log.trace("Loading previously changed IDs from disk"); + this._log.debug("Loading previously changed IDs from disk"); try { let [is] = Utils.open(file, "<"); @@ -166,6 +166,7 @@ Tracker.prototype = { addChangedID: function T_addChangedID(id) { if (!this.enabled) return; + this._log.debug("Adding changed ID " + id); if (!this.changedIDs[id]) { this.changedIDs[id] = true; this.saveChangedIDs(); @@ -175,6 +176,7 @@ Tracker.prototype = { removeChangedID: function T_removeChangedID(id) { if (!this.enabled) return; + this._log.debug("Removing changed ID " + id); if (this.changedIDs[id]) { delete this.changedIDs[id]; this.saveChangedIDs(); @@ -182,8 +184,7 @@ Tracker.prototype = { }, clearChangedIDs: function T_clearChangedIDs() { - if (!this.enabled) - return; + this._log.debug("Clearing changed ID list"); for (let id in this.changedIDs) { delete this.changedIDs[id]; } From 1e8af46a757d6410ffafe2bbceaaf2ee59def3f7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 6 Dec 2008 00:12:40 -0800 Subject: [PATCH 0761/1860] tracker now caches places id -> guid mappings, since otherwise it can't retrieve the correct guid on item removal (because the callback happens after the item is removed) --- services/sync/modules/engines/bookmarks.js | 119 ++++++++++++--------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 583e7741548c..93d69b21a17a 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -952,26 +952,17 @@ BookmarksStore.prototype = { return null; }, - _applyIncoming: function BStore__applyIncoming(record) { - let self = yield; - - if (!this._lookup) - this.wrap(); - - this._log.trace("RECORD: " + record.id + " -> " + uneval(record.cleartext)); - - if (!record.cleartext) - this._removeCommand({GUID: record.id}); - else if (this._getItemIdForGUID(record.id) < 0) - this._createCommand({GUID: record.id, data: record.cleartext}); - else { - let data = Utils.deepCopy(record.cleartext); - delete data.GUID; - this._editCommand({GUID: record.id, data: data}); - } - }, applyIncoming: function BStore_applyIncoming(onComplete, record) { - this._applyIncoming.async(this, onComplete, record); + let fn = function(rec) { + let self = yield; + if (!record.cleartext) + this._removeCommand({GUID: record.id}); + else if (this._getItemIdForGUID(record.id) < 0) + this._createCommand({GUID: record.id, data: record.cleartext}); + else + this._editCommand({GUID: record.id, data: record.cleartext}); + }; + fn.async(this, onComplete, record); }, _createCommand: function BStore__createCommand(command) { @@ -1071,10 +1062,13 @@ BookmarksStore.prototype = { } if (newId) { this._log.trace("Setting GUID of new item " + newId + " to " + command.GUID); - this._bms.setItemGUID(newId, command.GUID); - let foo = this._bms.getItemGUID(command.GUID); - if (foo == newId) - this._log.debug("OK!"); + let cur = this._bms.getItemGUID(newId); + if (cur == command.GUID) + this._log.warn("Item " + newId + " already has GUID " + command.GUID); + else { + this._bms.setItemGUID(newId, command.GUID); + Engines.get("bookmarks")._tracker._all[newId] = command.GUID; // HACK - see tracker + } } }, @@ -1088,10 +1082,8 @@ BookmarksStore.prototype = { } var itemId = this._bms.getItemIdForGUID(command.GUID); - this._log.debug("woo: " + itemId); if (itemId < 0) { - this._log.warn("Attempted to remove item " + command.GUID + - ", but it does not exist. Skipping."); + this._log.debug("Item " + command.GUID + "already removed"); return; } var type = this._bms.getItemType(itemId); @@ -1240,6 +1232,7 @@ BookmarksStore.prototype = { this._log.debug("Changing GUID " + oldID + " to " + newID); this._bms.setItemGUID(itemId, newID); + Engines.get("bookmarks")._tracker._all[itemId] = newID; // HACK - see tracker }, _getNode: function BSS__getNode(folder) { @@ -1467,47 +1460,73 @@ BookmarksTracker.prototype = { _init: function BMT__init() { this.__proto__.__proto__._init.call(this); + + // NOTE: since the callbacks give us item IDs (not GUIDs), we use + // getItemGUID to get it within the callback. For removals, however, + // that doesn't work because the item is already gone! (and worse, Places + // has a bug where it will generate a new one instead of throwing). + // Our solution: cache item IDs -> GUIDs + + // FIXME: very roundabout way of getting id -> guid mapping! + let store = new BookmarksStore(); + let all = store.wrap(); + this._all = {}; + for (let guid in all) { + this._all[this._bms.getItemIdForGUID(guid)] = guid; + } + this._bms.addObserver(this, false); }, - // FIXME: not getting any events whatsoever! - /* Every add/remove/change is worth 10 points */ - - onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { + _upScore: function BMT__upScore() { if (!this.enabled) return; - let guid = this._bms.getItemGUID(itemId); - this._log.debug("Item " + guid + " added, adding to queue"); - this.addChangedID(guid); this._score += 10; }, + onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { + this._log.trace("onItemAdded: " + itemId); + + this._all[itemId] = this._bms.getItemGUID(itemId); + this.addChangedID(this._all[itemId]); + + this._upScore(); + }, + onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { - if (!this.enabled) - return; - let guid = this._bms.getItemGUID(itemId); - this._log.debug("Item " + guid + " removed, adding to queue"); - this.addChangedID(guid); - this._score += 10; + this._log.trace("onItemRemoved: " + itemId); + + this.addChangedID(this._all[itemId]); + delete this._all[itemId]; + + this._upScore(); }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnnotationProperty, value) { - if (!this.enabled) - return; + this._log.trace("onItemChanged: " + itemId + ", property: " + property + + ", isAnno: " + isAnnotationProperty + ", value: " + value); + + // NOTE: we get onItemChanged too much, when adding an item, changing its + // GUID, before removal... it makes removals break, because trying + // to update our cache before removals makes it so we think the + // temporary guid was removed, instead of the real one. + // Therefore, we ignore all guid changes. When *Weave* changes the + // GUID, we take care to update the tracker's cache. If anyone else + // changes the GUID, that will case breakage. let guid = this._bms.getItemGUID(itemId); - this._log.debug("Item " + guid + " changed, adding to queue"); - this.addChangedID(guid); - this._score += 10; + if (guid != this._all[itemId]) + this._log.trace("GUID change, ignoring"); + else + this.addChangedID(this._all[itemId]); // some other change + + this._upScore(); }, onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { - if (!this.enabled) - return; - let guid = this._bms.getItemGUID(itemId); - this._log.debug("Item " + guid + " moved, adding to queue"); - this.addChangedID(guid); - this._score += 10; + this._log.trace("onItemMoved: " + itemId); + this.addChangedID(this._all[itemId]); + this._upScore(); }, onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, From a74ce0aa1ec88d69b04c42568badf3940fcfcfcf Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 6 Dec 2008 00:13:19 -0800 Subject: [PATCH 0762/1860] bump version to 0.2.92 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 33320188dcb2..1bf89182be5b 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,7 +43,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; -const WEAVE_VERSION = "0.2.91"; +const WEAVE_VERSION = "0.2.92"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From cd02841900cccad10274f206e3503c6649279a91 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 8 Dec 2008 09:53:32 -0800 Subject: [PATCH 0763/1860] remove sync cores, they are no longer used; remove sharing code from bookmarks (we can bring it back when we need it again, needs work); have the engine give hints to the store about when to wrap all items and cache them (to make subsequent calls to wrap one item fast); move serializeItem/getAllIDs into the store --- services/sync/modules/engines.js | 37 +- services/sync/modules/engines/bookmarks.js | 772 +-------------------- services/sync/modules/stores.js | 46 +- 3 files changed, 60 insertions(+), 795 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3c41383b935d..4f5969972965 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -52,7 +52,6 @@ Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/clientData.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/async.js"); @@ -136,19 +135,13 @@ Engine.prototype = { get score() this._tracker.score, - // _core, _store, and tracker need to be overridden in subclasses + // _store, and tracker need to be overridden in subclasses get _store() { let store = new Store(); this.__defineGetter__("_store", function() store); return store; }, - get _core() { - let core = new SyncCore(this._store); - this.__defineGetter__("_core", function() core); - return core; - }, - get _tracker() { let tracker = new Tracker(); this.__defineGetter__("_tracker", function() tracker); @@ -294,15 +287,15 @@ SyncEngine.prototype = { }, // Create a new record starting from an ID - // Calls _serializeItem to get the actual item, but sometimes needs to be + // Calls _store.wrapItem to get the actual item, but sometimes needs to be // overridden anyway (to alter parentid or other properties outside the payload) - _createRecord: function SyncEngine__newCryptoWrapper(id, encrypt) { + _createRecord: function SyncEngine__createRecord(id, encrypt) { let self = yield; let record = new CryptoWrapper(); record.uri = this.engineURL + id; record.encryption = this.cryptoMetaURL; - record.cleartext = yield this._serializeItem.async(this, self.cb, id); + record.cleartext = this._store.wrapItem(id); if (record.cleartext && record.cleartext.parentid) record.parentid = record.cleartext.parentid; @@ -313,18 +306,6 @@ SyncEngine.prototype = { self.done(record); }, - // Serialize an item. This will become the encrypted field in the payload of - // a record. Needs to be overridden in a subclass - _serializeItem: function SyncEngine__serializeItem(id) { - let self = yield; - self.done({}); - }, - - _getAllIDs: function SyncEngine__getAllIDs() { - let self = yield; - self.done({}); - }, - // Check if a record is "like" another one, even though the IDs are different, // in that case, we'll change the ID of the local item to match // Probably needs to be overridden in a subclass, to change which criteria @@ -406,19 +387,27 @@ SyncEngine.prototype = { this._tracker.clearChangedIDs(); // now add all current ones - let all = yield this._getAllIDs.async(this, self.cb); + let all = this._store.getAllIDs(); for (let id in all) { this._tracker.changedIDs[id] = true; } } // generate queue from changed items list + + // XXX should have a heuristic like this, but then we need to be able to + // serialize each item by itself, something our stores can't currently do + //if (this._tracker.changedIDs.length >= 30) + this._store.cacheItemsHint(); + // NOTE we want changed items -> outgoing -> server to be as atomic as // possible, so we clear the changed IDs after we upload the changed records // NOTE2 don't encrypt, we'll do that before uploading instead for (let id in this._tracker.changedIDs) { this.outgoing.push(yield this._createRecord.async(this, self.cb, id, false)); } + + this._store.clearItemCacheHint(); }, // Generate outgoing records diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 93d69b21a17a..93ae7c228e9b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -59,658 +59,14 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/xmpp/xmppClient.js"); Cu.import("resource://weave/notifications.js"); -Cu.import("resource://weave/sharing.js"); Cu.import("resource://weave/resource.js"); Function.prototype.async = Async.sugar; -function BookmarksSharingManager(engine) { - this._init(engine); -} -BookmarksSharingManager.prototype = { - __annoSvc: null, - get _annoSvc() { - if (!this.__anoSvc) - this.__annoSvc = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - return this.__annoSvc; - }, - - __bms: null, - get _bms() { - if (!this.__bms) - this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - return this.__bms; - }, - - __myUsername: null, - get _myUsername() { - if (!this.__myUsername) - this.__myUsername = ID.get('WeaveID').username; - return this.__myUsername; - }, - - _init: function SharingManager__init(engine) { - this._engine = engine; - this._log = Log4Moz.repository.getLogger("Bookmark Share"); - if ( Utils.prefs.getBoolPref( "xmpp.enabled" ) ) { - this._log.info( "Starting XMPP client for bookmark engine..." ); - this._startXmppClient.async(this); - } - }, - - _startXmppClient: function BmkSharing__startXmppClient() { - // To be called asynchronously. - let self = yield; - - // Get serverUrl and realm of the jabber server from preferences: - let serverUrl = Utils.prefs.getCharPref( "xmpp.server.url" ); - let realm = Utils.prefs.getCharPref( "xmpp.server.realm" ); - - /* Username/password for XMPP are the same as the ones for Weave, - so read them from the weave identity: */ - let clientName = this._myUsername; - let clientPassword = ID.get('WeaveID').password; - - let transport = new HTTPPollingTransport( serverUrl, false, 15000 ); - let auth = new PlainAuthenticator(); - /* LONGTERM TODO would prefer to use MD5Authenticator instead, - once we get it working, but since we are connecting over SSL, the - Plain auth is probably fine for now. */ - this._xmppClient = new XmppClient( clientName, - realm, - clientPassword, - transport, - auth ); - let bmkSharing = this; - let messageHandler = { - handle: function ( messageText, from ) { - /* The callback function for incoming xmpp messages. - We expect message text to be in the form of: - . - Where command is one of: - "share" (sender offers to share directory dir with us) - or "stop" (sender has stopped sharing directory dir with us.) - or "accept" (sharee has accepted our offer to share our dir.) - or "decline" (sharee has declined our offer to share our dir.) - - Folder name is the human-readable name of the bookmark folder - being shared (it can contain spaces). serverpath is the path - on the server to the directory where the data is stored: - only the machine seese this, and it can't have spaces. - */ - let words = messageText.split(" "); - let commandWord = words[0]; - let serverPath = words[1]; - let directoryName = words.slice(2).join(" "); - if ( commandWord == "share" ) { - bmkSharing._incomingShareOffer(from, serverPath, folderName); - } else if ( commandWord == "stop" ) { - bmkSharing._log.info("User " + user + " withdrew " + folderName); - bmkSharing._stopIncomingShare(user, serverPath, folderName); - } - } - }; - - this._xmppClient.registerMessageHandler( messageHandler ); - this._xmppClient.connect( realm, self.cb ); - yield; - if ( this._xmppClient._connectionStatus == this._xmppClient.FAILED ) { - this._log.warn( "Weave can't log in to xmpp server: xmpp disabled." ); - } else if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { - this._log.info( "Weave logged into xmpp OK." ); - } - self.done(); - }, - - _incomingShareOffer: function BmkSharing__incomingShareOffer(user, - serverPath, - folderName) { - /* Called when we receive an offer from another user to share a - folder. Set up a notification telling our user about the share offer - and allowing them to accept or reject it. - */ - this._log.info("User " + user + " offered to share folder " + folderName); - - let bmkSharing = this; - let acceptButton = new NotificationButton( - "Accept Share", - "a", - function() { - // This is what happens when they click the Accept button: - bmkSharing._log.info("Accepted bookmark share from " + user); - bmkSharing._createIncomingShare(user, serverPath, folderName); - bmkSharing.updateAllIncomingShares(); - return false; - } - ); - let rejectButton = new NotificationButton( - "No Thanks", - "n", - function() {return false;} - ); - - let title = "Bookmark Share Offer From " + user; - let description ="Weave user " + user + - " is offering to share a bookmark folder called " + folderName + - " with you. Do you want to accept it?"; - let notification = new Notification(title, - description, - null, - Notifications.PRIORITY_INFO, - [acceptButton, rejectButton] - ); - Notifications.add(notification); - }, - - _sendXmppNotification: function BmkSharing__sendXmpp(recipient, cmd, path, name) { - // Send an xmpp message to the share-ee - if ( this._xmppClient ) { - if ( this._xmppClient._connectionStatus == this._xmppClient.CONNECTED ) { - let msgText = "share " + path + " " + name; - this._log.debug( "Sending XMPP message: " + msgText ); - this._xmppClient.sendMessage( recipient, msgText ); - } else { - this._log.warn( "No XMPP connection for share notification." ); - } - } - }, - - _share: function BmkSharing__share( folderId, username ) { - // Return true if success, false if failure. - let ret = false; - let self = yield; - - /* TODO What should the behavior be if i'm already sharing it with user - A and I ask to share it with user B? (This should be prevented by - the UI. */ - let folderName = this._bms.getItemTitle(folderId); - - // Create the outgoing share folder on the server - this._createOutgoingShare.async( this, self.cb, - folderId, folderName, username ); - let serverPath = yield; - this._updateOutgoingShare.async( this, self.cb, folderId ); - yield; - - /* Set the annotation on the folder so we know - it's an outgoing share: */ - this._annoSvc.setItemAnnotation(folderId, - OUTGOING_SHARED_ANNO, - username, - 0, - this._annoSvc.EXPIRE_NEVER); - /* LONGTERM TODO: in the future when we allow sharing one folder - with many people, the value of the annotation can be a whole list - of usernames instead of just one. */ - - // The serverPath is relative; prepend it with /user/myusername/ to make - // it absolute. - let abspath = "/user/" + this._myUsername + "/" + serverPath; - this._sendXmppNotification( username, "share", abspath, folderName); - - - this._log.info("Shared " + folderName +" with " + username); - ret = true; - self.done( ret ); - }, - - _stopSharing: function BmkSharing__stopSharing( folderId, username ) { - let self = yield; - let folderName = this._bms.getItemTitle(folderId); - let serverPath = ""; - - if (this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)){ - serverPath = this._annoSvc.getItemAnnotation(folderId, SERVER_PATH_ANNO); - } else { - this._log.warn("The folder you are de-sharing has no path annotation."); - } - - /* LONGTERM TODO: when we move to being able to share one folder with - * multiple people, this needs to be modified so we can stop sharing with - * one person but keep sharing with others. - */ - - // Stop the outgoing share: - this._stopOutgoingShare.async(this, self.cb, folderId); - yield; - - // Send message to the share-ee, so they can stop their incoming share - let abspath = "/user/" + this._myUsername + "/" + serverPath; - this._sendXmppNotification( username, "stop", abspath, folderName ); - - this._log.info("Stopped sharing " + folderName + "with " + username); - self.done( true ); - }, - - /* FIXME! Gets all shares, not just the new ones. Doesn't impact - functionality because _incomingShareOffer does not create - duplicates, but annoys the user by showing notification of ALL - shares on EVERY sync :( - */ - getNewShares: function BmkSharing_getNewShares(onComplete) { - this._getNewShares.async(this, onComplete); - }, - _getNewShares: function BmkSharing__getNewShares() { - let self = yield; -// FIXME -// let sharingApi = new Sharing.Api( DAV ); - let result = yield sharingApi.getShares(self.cb); - - this._log.info("Got Shares: " + result); - let shares = result.split(','); - if (shares.length > 1) { - this._log.info('Found shares'); - for (var i = 0; i < shares.length - 1; i++) { - let share = shares[i].split(':'); - let name = share[0]; - let user = share[1]; - let path = share[2]; - this._incomingShareOffer(user, '/user/' + user + '/' + path, name); - } - } - }, - - updateAllIncomingShares: function BmkSharing_updateAllIncoming(onComplete) { - this._updateAllIncomingShares.async(this, onComplete); - }, - _updateAllIncomingShares: function BmkSharing__updateAllIncoming() { - /* For every bookmark folder in my tree that has the annotation - marking it as an incoming shared folder, pull down its latest - contents from its owner's account on the server. (This is - a one-way data transfer because I can't modify bookmarks that - are owned by someone else but shared to me; any changes I make - to the folder contents are simply wiped out by the latest - server contents.) */ - let self = yield; - let mounts = this._engine._store.findIncomingShares(); - for (let i = 0; i < mounts.length; i++) { - try { - this._log.trace("Update incoming share from " + mounts[i].serverPath); - this._updateIncomingShare.async(this, self.cb, mounts[i]); - yield; - } catch (e) { - this._log.warn("Could not sync shared folder from " + mounts[i].userid); - this._log.trace(Utils.stackTrace(e)); - } - } - }, - - updateAllOutgoingShares: function BmkSharing_updateAllOutgoing(onComplete) { - this._updateAllOutgoingShares.async(this, onComplete); - }, - _updateAllOutgoingShares: function BmkSharing__updateAllOutgoing() { - let self = yield; - let shares = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, - {}); - for ( let i=0; i < shares.length; i++ ) { - /* TODO only update the shares that have changed. Perhaps we can - do this by checking whether there's a corresponding entry in the - diff produced by the latest sync. */ - this._updateOutgoingShare.async(this, self.cb, shares[i]); - yield; - } - self.done(); - }, - - _createKeyChain: function BmkSharing__createKeychain(serverPath, - myUserName, - username){ - /* Creates a new keychain file on the server, inside the directory given - * by serverPath. The keychain file contains keys for both me (the - * current user) and for the friend specified by username (which must - * be a valid weave user id.) - */ - let self = yield; - // Create a new symmetric key, to be used only for encrypting this share. - // XXX HACK. Seems like the engine shouldn't have to be doing any of this, or - // should use its own identity here. - let tmpIdentity = { - realm : "temp ID", - bulkKey : null, - bulkIV : null - }; - Crypto.randomKeyGen.async(Crypto, self.cb, tmpIdentity); - yield; - let bulkKey = tmpIdentity.bulkKey; - let bulkIV = tmpIdentity.bulkIV; - - /* Get public keys for me and the user I'm sharing with. - Each user's public key is stored in /user/username/public/pubkey. */ - let idRSA = ID.get('WeaveCryptoID'); - let userPubKeyFile = new Resource("/user/" + username + "/public/pubkey"); - userPubKeyFile.pushFilter( new JsonFilter() ); - // The above can get a 401 if .htaccess file isnot set to give public - //access to the "public" directory. It can also get a 502? - userPubKeyFile.get(self.cb); - let userPubKey = yield; - userPubKey = userPubKey.pubkey; - - /* Create the keyring, containing the sym key encrypted with each - of our public keys: */ - Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", - pubkey: idRSA.pubkey} ); - let encryptedForMe = yield; - Crypto.wrapKey.async(Crypto, self.cb, bulkKey, {realm : "tmpWrapID", - pubkey: userPubKey} ); - let encryptedForYou = yield; - let keys = { - ring : { }, - bulkIV : bulkIV - }; - keys.ring[myUserName] = encryptedForMe; - keys.ring[username] = encryptedForYou; - - let keyringFile = new Resource( serverPath + "/" + KEYRING_FILE_NAME ); - keyringFile.pushFilter(new JsonFilter()); - keyringFile.put( self.cb, keys); - yield; - - self.done(); - }, - - _createOutgoingShare: function BmkSharing__createOutgoing(folderId, - folderName, - username) { - /* To be called asynchronously. FolderId is the integer id of the - bookmark folder to be shared and folderName is a string of its - title. username is a string indicating the user that it is to be - shared with. This function creates the directory and keyring on - the server in which the shared data will be put, but it doesn't - actually put the bookmark data there (that's done in - _updateOutgoingShare().) - Returns a string which is the path on the server to the new share - directory, or false if it failed.*/ - - let self = yield; - this._log.debug("Turning folder " + folderName + " into outgoing share" - + " with " + username); - - /* Generate a new GUID to use as the new directory name on the server - in which we'll store the shared data. */ - let folderGuid = Utils.makeGUID(); - - /* Create the directory on the server if it does not exist already. */ - let serverPath = "share/" + folderGuid; -// FIXME -// let ret = yield DAV.MKCOL(serverPath, self.cb); - - if (!ret) { - this._log.error("Can't create remote folder for outgoing share."); - self.done(false); - } - // TODO more error handling - - /* Store the path to the server directory in an annotation on the shared - bookmark folder, so we can easily get back to it later. */ - this._annoSvc.setItemAnnotation(folderId, - SERVER_PATH_ANNO, - serverPath, - 0, - this._annoSvc.EXPIRE_NEVER); - - let encryptionTurnedOn = true; - if (encryptionTurnedOn) { - yield this._createKeyChain.async(this, self.cb, serverPath, - this._myUsername, username); - } - - // Call Atul's js api for setting htaccess: -// FIXME -// let sharingApi = new Sharing.Api( DAV ); - let result = yield sharingApi.shareWithUsers( serverPath, - [username], folderName, - self.cb ); - this._log.info(result.errorText); - // return the server path: - self.done( serverPath ); - }, - - _updateOutgoingShare: function BmkSharing__updateOutgoing(folderId) { - /* Puts all the bookmark data from the specified bookmark folder, - encrypted, onto the shared directory on the server (replacing - anything that was already there). - To be called asynchronously. - TODO: error handling*/ - let self = yield; - // The folder should have an annotation specifying the server path to - // the directory: - if (!this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)) { - this._log.warn("Outgoing share is invalid and can't be synced."); - return; - } - let serverPath = this._annoSvc.getItemAnnotation(folderId, - SERVER_PATH_ANNO); - // TODO the above can throw an exception if the expected anotation isn't - // there. - // From that directory, get the keyring file, and from it, the symmetric - // key that we'll use to encrypt. - let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); - keyringFile.pushFilter(new JsonFilter()); - keyringFile.get(self.cb); - let keys = yield; - - // Unwrap (decrypt) the key with the user's private key. - let idRSA = ID.get('WeaveCryptoID'); - let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, - keys.ring[this._myUsername], idRSA); - let bulkIV = keys.bulkIV; - - // Get the json-wrapped contents of everything in the folder: - let wrapMount = this._engine._store._wrapMountOutgoing(folderId); - let jsonService = Components.classes["@mozilla.org/dom/json;1"] - .createInstance(Components.interfaces.nsIJSON); - let json = jsonService.encode( wrapMount ); - - // Encrypt it with the symkey and put it into the shared-bookmark file. - let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); - let tmpIdentity = { - realm : "temp ID", - bulkKey : bulkKey, - bulkIV : bulkIV - }; - Crypto.encryptData.async( Crypto, self.cb, json, tmpIdentity ); - let cyphertext = yield; - yield bmkFile.put( self.cb, cyphertext ); - self.done(); - }, - - _stopOutgoingShare: function BmkSharing__stopOutgoingShare(folderId) { - /* Stops sharing the specified folder. Deletes its data from the - server, deletes the annotations that mark it as shared, and sends - a message to the shar-ee to let them know it's been withdrawn. */ - let self = yield; - if (this._annoSvc.itemHasAnnotation(folderId, SERVER_PATH_ANNO)){ - let serverPath = this._annoSvc.getItemAnnotation( folderId, - SERVER_PATH_ANNO ); - // Delete the share from the server: - // TODO handle error that can happen if these resources do not exist. - let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); - keyringFile.delete(self.cb); - yield; - let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); - keyringFile.delete(self.cb); - yield; - // TODO this leaves the folder itself in place... is there a way to - // get rid of that, say through DAV? - } - // Remove the annotations from the local folder: - this._annoSvc.removeItemAnnotation(folderId, SERVER_PATH_ANNO); - this._annoSvc.removeItemAnnotation(folderId, OUTGOING_SHARED_ANNO); - self.done(); - }, - - _createIncomingShare: function BmkSharing__createShare(user, - serverPath, - title) { - /* Creates a new folder in the bookmark menu for the incoming share. - user is the weave id of the user who is offering the folder to us; - serverPath is the path on the server where the shared data is located, - and title is the human-readable title to use for the new folder. - - It is safe to call this again for a folder that already exist: this - function will exit without doing anything. It won't create a duplicate. - */ - - /* Get the toolbar "Shared Folders" folder (identified by its annotation). - If it doesn't already exist, create it: */ - dump( "I'm in _createIncomingShare. user= " + user + "path = " + - serverPath + ", title= " + title + "\n" ); - let root; - let a = this._annoSvc.getItemsWithAnnotation(INCOMING_SHARE_ROOT_ANNO, - {}); - if (a.length == 1) - root = a[0]; - if (!root) { - root = this._bms.createFolder(this._bms.toolbarFolder, - INCOMING_SHARE_ROOT_NAME, - this._bms.DEFAULT_INDEX); - this._annoSvc.setItemAnnotation(root, - INCOMING_SHARE_ROOT_ANNO, - true, - 0, - this._annoSvc.EXPIRE_NEVER); - } - /* Inside "Shared Folders", create a new folder annotated with - the originating user and the directory path specified by the incoming - share offer. Unless a folder with these exact annotations already - exists, in which case do nothing. */ - let itemExists = false; - a = this._annoSvc.getItemsWithAnnotation(INCOMING_SHARED_ANNO, {}); - for (let i = 0; i < a.length; i++) { - let creator = this._annoSvc.getItemAnnotation(a[i], INCOMING_SHARED_ANNO); - let path = this._annoSvc.getItemAnnotation(a[i], SERVER_PATH_ANNO); - if ( creator == user && path == serverPath ) { - itemExists = true; - break; - } - } - if (!itemExists) { - let newId = this._bms.createFolder(root, title, this._bms.DEFAULT_INDEX); - // Keep track of who shared this folder with us... - this._annoSvc.setItemAnnotation(newId, - INCOMING_SHARED_ANNO, - user, - 0, - this._annoSvc.EXPIRE_NEVER); - // and what the path to the shared data on the server is... - this._annoSvc.setItemAnnotation(newId, - SERVER_PATH_ANNO, - serverPath, - 0, - this._annoSvc.EXPIRE_NEVER); - } - }, - - _updateIncomingShare: function BmkSharing__updateIncomingShare(mountData) { - /* Pull down bookmarks from the server for a single incoming - shared folder, obliterating whatever was in that folder before. - - mountData is an object that's expected to have member data: - userid: weave id of the user sharing the folder with us, - rootGUID: guid in our bookmark store of the share mount point, - node: the bookmark menu node for the share mount point folder */ - - // TODO error handling (see what Resource can throw or return...) - /* TODO tons of symmetry between this and _updateOutgoingShare, can - probably factor the symkey decryption stuff into a common helper - function. */ - let self = yield; - let user = mountData.userid; - // The folder has an annotation specifying the server path to the - // directory: - let serverPath = mountData.serverPath; - // From that directory, get the keyring file, and from it, the symmetric - // key that we'll use to decrypt. - this._log.trace("UpdateIncomingShare: getting keyring file."); - let keyringFile = new Resource(serverPath + "/" + KEYRING_FILE_NAME); - keyringFile.pushFilter(new JsonFilter()); - let keys = yield keyringFile.get(self.cb); - - // Unwrap (decrypt) the key with the user's private key. - this._log.trace("UpdateIncomingShare: decrypting sym key."); - let idRSA = ID.get('WeaveCryptoID'); - let bulkKey = yield Crypto.unwrapKey.async(Crypto, self.cb, - keys.ring[this._myUsername], idRSA); - let bulkIV = keys.bulkIV; - - // Decrypt the contents of the bookmark file with the symmetric key: - this._log.trace("UpdateIncomingShare: getting encrypted bookmark file."); - let bmkFile = new Resource(serverPath + "/" + SHARED_BOOKMARK_FILE_NAME); - let cyphertext = yield bmkFile.get(self.cb); - let tmpIdentity = { - realm : "temp ID", - bulkKey : bulkKey, - bulkIV : bulkIV - }; - this._log.trace("UpdateIncomingShare: Decrypting."); - Crypto.decryptData.async( Crypto, self.cb, cyphertext, tmpIdentity ); - let json = yield; - // decrypting that gets us JSON, turn it into an object: - this._log.trace("UpdateIncomingShare: De-JSON-izing."); - let jsonService = Components.classes["@mozilla.org/dom/json;1"] - .createInstance(Components.interfaces.nsIJSON); - let serverContents = jsonService.decode( json ); - - // prune tree / get what we want - this._log.trace("UpdateIncomingShare: Pruning."); - for (let guid in serverContents) { - if (serverContents[guid].type != "bookmark") - delete serverContents[guid]; - else - serverContents[guid].parentid = mountData.rootGUID; - } - - /* Wipe old local contents of the folder, starting from the node: */ - this._log.trace("Wiping local contents of incoming share..."); - this._bms.removeFolderChildren( mountData.node ); - - /* Create diff FROM current contents (i.e. nothing) TO the incoming - * data from serverContents. Then apply the diff. */ - this._log.trace("Got bookmarks from " + user + ", comparing with local copy"); - this._engine._core.detectUpdates(self.cb, {}, serverContents); - let diff = yield; - - /* LONGTERM TODO: The createCommands that are executed in applyCommands - * will fail badly if the GUID of the incoming item collides with a - * GUID of a bookmark already in my store. (This happened to me a lot - * during testing, obviously, since I was sharing bookmarks with myself). - * Need to think about the right way to handle this. */ - - // FIXME: should make sure all GUIDs here live under the mountpoint - this._log.trace("Applying changes to folder from " + user); - this._engine._store.applyCommands.async(this._engine._store, self.cb, diff); - yield; - - this._log.trace("Shared folder from " + user + " successfully synced!"); - }, - - _stopIncomingShare: function BmkSharing__stopIncomingShare(user, - serverPath, - folderName) - { - /* Delete the incoming share folder. Since the update of incoming folders - * is triggered when the engine spots a folder with a certain annotation on - * it, just getting rid of this folder is all we need to do. - */ - let a = this._annoSvc.getItemsWithAnnotation(OUTGOING_SHARED_ANNO, {}); - for (let i = 0; i < a.length; i++) { - let creator = this._annoSvc.getItemAnnotation(a[i], OUTGOING_SHARED_ANNO); - let path = this._annoSvc.getItemAnnotation(a[i], SERVER_PATH_ANNO); - if ( creator == user && path == serverPath ) { - this._bms.removeFolder( a[i]); - } - } - } -} - function BookmarksEngine(pbeId) { this._init(pbeId); } @@ -729,33 +85,12 @@ BookmarksEngine.prototype = { return store; }, - get _core() { - let core = new BookmarksSyncCore(); - this.__defineGetter__("_core", function() core); - return core; - }, - get _tracker() { let tracker = new BookmarksTracker(); this.__defineGetter__("_tracker", function() tracker); return tracker; }, - _getAllIDs: function BmkEngine__getAllIDs() { - let self = yield; - let all = this._store.wrap(); // FIXME: using store is an inefficient hack... - delete all["unfiled"]; - delete all["toolbar"]; - delete all["menu"]; - self.done(all); - }, - - _serializeItem: function BmkEngine__serializeItem(id) { - let self = yield; - let all = this._store.wrap(); // FIXME OMG SO INEFFICIENT - self.done(all[id]); - }, - _recordLike: function SyncEngine__recordLike(a, b) { if (a.parentid != b.parentid) return false; @@ -794,94 +129,6 @@ BookmarksEngine.prototype = { // and updating incoming/outgoing shared folders after syncing }; -function BookmarksSyncCore(store) { - this._store = store; - this._init(); -} -BookmarksSyncCore.prototype = { - __proto__: SyncCore.prototype, - _logName: "BMSync", - _store: null, - - _getEdits: function BSC__getEdits(a, b) { - // NOTE: we do not increment ret.numProps, as that would cause - // edit commands to always get generated - let ret = SyncCore.prototype._getEdits.call(this, a, b); - ret.props.type = a.type; - return ret; - }, - - // compares properties - // returns true if the property is not set in either object - // returns true if the property is set and equal in both objects - // returns false otherwise - _comp: function BSC__comp(a, b, prop) { - return (!a.data[prop] && !b.data[prop]) || - (a.data[prop] && b.data[prop] && (a.data[prop] == b.data[prop])); - }, - - _commandLike: function BSC__commandLike(a, b) { - // Check that neither command is null, that their actions, types, - // and parents are the same, and that they don't have the same - // GUID. - // * Items with the same GUID do not qualify for 'likeness' because - // we already consider them to be the same object, and therefore - // we need to process any edits. - // * Remove or edit commands don't qualify for likeness either, - // since remove or edit commands with different GUIDs are - // guaranteed to refer to two different items - // * The parent GUID check works because reconcile() fixes up the - // parent GUIDs as it runs, and the command list is sorted by - // depth - if (!a || !b || - a.action != b.action || - a.action != "create" || - a.data.type != b.data.type || - a.data.parentid != b.data.parentid || - a.GUID == b.GUID) - return false; - - // Bookmarks and folders are allowed to be in a different index as long as - // they are in the same folder. Separators must be at - // the same index to qualify for 'likeness'. - switch (a.data.type) { - case "bookmark": - if (this._comp(a, b, 'URI') && - this._comp(a, b, 'title')) - return true; - return false; - case "query": - if (this._comp(a, b, 'URI') && - this._comp(a, b, 'title')) - return true; - return false; - case "microsummary": - if (this._comp(a, b, 'URI') && - this._comp(a, b, 'generatorURI')) - return true; - return false; - case "folder": - if (this._comp(a, b, 'title')) - return true; - return false; - case "livemark": - if (this._comp(a, b, 'title') && - this._comp(a, b, 'siteURI') && - this._comp(a, b, 'feedURI')) - return true; - return false; - case "separator": - if (this._comp(a, b, 'index')) - return true; - return false; - default: - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this._log.error("commandLike: Unknown item type: " + json.encode(a)); - return false; - } - } -}; - function BookmarksStore() { this._init(); } @@ -1403,6 +650,25 @@ BookmarksStore.prototype = { return items; }, + // FIXME: the fast path here is not so bad, specially since the engine always + // gives the cache hint atm. but wrapping all items to return just one + // (the slow path) is pretty bad + wrapItem: function BStore_wrapItem(id) { + if (this._itemCache) + return this._itemCache[id]; + let all = this.wrap(); + return all[id]; + }, + + // XXX need a better way to query Places for all GUIDs + getAllIDs: function BStore_getAllIDs() { + let all = this.wrap(); + delete all["unfiled"]; + delete all["toolbar"]; + delete all["menu"]; + return all; + }, + wipe: function BStore_wipe() { this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder); this._bms.removeFolderChildren(this._bms.toolbarFolder); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 813693ceb967..aeef6cfc3bef 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -59,8 +59,7 @@ function Store() { this._init(); } Store.prototype = { - _logName: "Store", - _yieldDuringApply: true, + _logName: "BaseClass", // set this property in child object's wrap()! _lookup: null, @@ -82,19 +81,21 @@ Store.prototype = { }, _init: function Store__init() { - this._log = Log4Moz.repository.getLogger("Service." + this._logName); + this._log = Log4Moz.repository.getLogger("Store." + this._logName); }, - applyCommands: function Store_applyCommands(commandList) { + _applyCommands: function Store__applyCommands(commandList) { let self = yield; for (var i = 0; i < commandList.length; i++) { - if (this._yieldDuringApply) { + if (i % 5) { Utils.makeTimerForCall(self.cb); yield; // Yield to main loop } + var command = commandList[i]; - this._log.trace("Processing command: " + this._json.encode(command)); + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace("Processing command: " + this._json.encode(command)); switch (command["action"]) { case "create": this._createCommand(command); @@ -110,7 +111,9 @@ Store.prototype = { break; } } - self.done(); + }, + applyCommands: function Store_applyCommands(onComplete, commandList) { + this._applyCommands.async(this, onComplete, commandList); }, // override only if neccessary @@ -121,23 +124,30 @@ Store.prototype = { return false; }, + cacheItemsHint: function Store_cacheItemsHint() { + this._itemCache = this.wrap(); + }, + + clearItemCacheHint: function Store_clearItemCacheHint() { + this._itemCache = null; + }, + // override these in derived objects - // wrap MUST save the wrapped store in the _lookup property! wrap: function Store_wrap() { - throw "wrap needs to be subclassed"; + throw "override wrap in a subclass"; + }, + + wrapItem: function Store_wrapItem() { + throw "override wrapItem in a subclass"; + }, + + getAllIDs: function Store_getAllIDs() { + throw "override getAllIDs in a subclass"; }, wipe: function Store_wipe() { - throw "wipe needs to be subclassed"; - }, - - _resetGUIDs: function Store__resetGUIDs() { - let self = yield; - // default to do nothing - }, - resetGUIDs: function Store_resetGUIDs(onComplete) { - this._resetGUIDs.async(this, onComplete); + throw "override wipe in a subclass"; } }; From 1f456ebaa686351492c36470e91134a638aace63 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 8 Dec 2008 13:21:25 -0800 Subject: [PATCH 0764/1860] make changeItemID exclusively a store method --- services/sync/modules/engines.js | 10 ++-------- services/sync/modules/engines/bookmarks.js | 9 +-------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 4f5969972965..69f864167827 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -324,11 +324,6 @@ SyncEngine.prototype = { } }, - _changeItemID: function SyncEngine__changeItemID(oldID, newID) { - let self = yield; - throw "_changeRecordID must be overridden in a subclass"; - }, - _recDepth: function SyncEngine__recDepth(rec) { // we've calculated depth for this record already if (rec.depth) @@ -508,9 +503,8 @@ SyncEngine.prototype = { this.outgoing[o].id, this.incoming[i].id); // change actual id of item - yield this._changeItemID.async(this, self.cb, - this.outgoing[o].id, - this.incoming[i].id); + this._store.changeItemID(this.outgoing[o].id, + this.incoming[i].id); delete this.incoming[i]; delete this.outgoing[o]; break; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 93ae7c228e9b..3d03b706f088 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -118,11 +118,6 @@ BookmarksEngine.prototype = { yield rec.encrypt(self.cb, ID.get('WeaveCryptoID').password); } } - }, - - _changeItemID: function BmkEngine__changeRecordID(oldID, newID) { - let self = yield; - yield this._store._changeItemID.async(this._store, self.cb, oldID, newID); } // XXX for sharing, will need to re-add code to get new shares before syncing, @@ -458,9 +453,7 @@ BookmarksStore.prototype = { } }, - _changeItemID: function BSS__changeItemID(oldID, newID) { - let self = yield; - + changeItemID: function BStore_changeItemID(oldID, newID) { var itemId = this._getItemIdForGUID(oldID); if (itemId == null) // toplevel folder return; From 47154935e42fa0c900389949722d3950a683a71c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 9 Dec 2008 12:26:14 -0800 Subject: [PATCH 0765/1860] enable history sync (not working yet) --- services/sync/modules/engines.js | 2 +- services/sync/modules/engines/bookmarks.js | 6 +- services/sync/modules/engines/history.js | 138 +++++++-------------- services/sync/modules/service.js | 26 +--- services/sync/modules/stores.js | 4 + 5 files changed, 62 insertions(+), 114 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 69f864167827..792eac576405 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -166,7 +166,7 @@ Engine.prototype = { try { level = Utils.prefs.getCharPref(levelPref); } catch (e) { /* ignore unset prefs */ } - this._log = Log4Moz.repository.getLogger("Service." + this.logName); + this._log = Log4Moz.repository.getLogger("Engine." + this.logName); this._log.level = Log4Moz.Level[level]; this._osPrefix = "weave:" + this.name + ":"; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 3d03b706f088..e259ede86a40 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -67,8 +67,8 @@ Cu.import("resource://weave/resource.js"); Function.prototype.async = Async.sugar; -function BookmarksEngine(pbeId) { - this._init(pbeId); +function BookmarksEngine() { + this._init(); } BookmarksEngine.prototype = { __proto__: SyncEngine.prototype, @@ -76,7 +76,7 @@ BookmarksEngine.prototype = { get name() "bookmarks", get displayName() "Bookmarks", - get logName() "BmkEngine", + get logName() "Bookmarks", get serverPrefix() "user-data/bookmarks/", get _store() { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 62a1f4d31191..2017c592774e 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -11,7 +11,7 @@ * for the specific language governing rights and limitations under the * License. * - * The Original Code is Bookmarks Sync. + * The Original Code is Weave * * The Initial Developer of the Original Code is Mozilla. * Portions created by the Initial Developer are Copyright (C) 2008 @@ -50,102 +50,58 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; -function HistoryEngine(pbeId) { - this._init(pbeId); -} -HistoryEngine.prototype = { - __proto__: new SyncEngine(), - - get name() { return "history"; }, - get displayName() { return "Browsing History"; }, - get logName() { return "HistEngine"; }, - get serverPrefix() { return "user-data/history/"; }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new HistoryStore(); - return this.__store; - }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new HistorySyncCore(this._store); - return this.__core; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new HistoryTracker(); - return this.__tracker; - } -}; - -function HistorySyncCore(store) { - this._store = store; +function HistoryEngine() { this._init(); } -HistorySyncCore.prototype = { - _logName: "HistSync", - _store: null, +HistoryEngine.prototype = { + __proto__: SyncEngine.prototype, + get _super() SyncEngine.prototype, - _commandLike: function HSC_commandLike(a, b) { - // History commands never qualify for likeness. We will always - // take the union of all client/server items. We use the URL as - // the GUID, so the same sites will map to the same item (same - // GUID), without our intervention. - return false; + get name() "history", + get displayName() "History", + get logName() "History", + get serverPrefix() "user-data/history/", + + get _store() { + let store = new HistoryStore(); + this.__defineGetter__("_store", function() store); + return store; }, - /** - * Determine the differences between two snapshots. This method overrides - * the one in its superclass so it can ignore removes, since removes don't - * matter for history (and would cause deltas to grow too large too fast). - */ - _detectUpdates: function HSC__detectUpdates(a, b) { - let self = yield; - - this.__proto__.__proto__._detectUpdates.async(this, self.cb, a, b); - let cmds = yield; - cmds = cmds.filter(function (v) v.action != "remove"); - - self.done(cmds); + get _tracker() { + let tracker = new HistoryTracker(); + this.__defineGetter__("_tracker", function() tracker); + return tracker; } }; -HistorySyncCore.prototype.__proto__ = new SyncCore(); function HistoryStore() { this._init(); } HistoryStore.prototype = { + __proto__: Store.prototype, _logName: "HistStore", _lookup: null, - __hsvc: null, get _hsvc() { - if (!this.__hsvc) { - this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - this.__hsvc.QueryInterface(Ci.nsIGlobalHistory2); - this.__hsvc.QueryInterface(Ci.nsIBrowserHistory); - } - return this.__hsvc; + let hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + hsvc.QueryInterface(Ci.nsIGlobalHistory2); + hsvc.QueryInterface(Ci.nsIBrowserHistory); + this.__defineGetter__("_hsvc", function() hsvc); + return hsvc; }, - __histDB: null, get _histDB() { - if (!this.__histDB) { - let file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("places.sqlite"); - let stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__histDB = stor.openDatabase(file); - } - return this.__histDB; + let file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("places.sqlite"); + let stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + let db = stor.openDatabase(file); + this.__defineGetter__("_histDB", function() db); + return db; }, _itemExists: function HistStore__itemExists(GUID) { @@ -246,14 +202,26 @@ HistoryStore.prototype = { // Not needed. } }; -HistoryStore.prototype.__proto__ = new Store(); function HistoryTracker() { this._init(); } HistoryTracker.prototype = { + __proto__: Tracker.prototype, _logName: "HistoryTracker", + get _hsvc() { + let hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + this.__defineGetter__("_hsvc", function() hsvc); + return hsvc; + }, + + _init: function HT__init() { + this.__proto__.__proto__._init.call(this); + this._hsvc.addObserver(this, false); + }, + /* We don't care about the first four */ onBeginUpdateBatch: function HT_onBeginUpdateBatch() { @@ -284,15 +252,5 @@ HistoryTracker.prototype = { }, onClearHistory: function HT_onClearHistory() { this._score += 50; - }, - - _init: function HT__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - - Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService). - addObserver(this, false); } -} -HistoryTracker.prototype.__proto__ = new Tracker(); +}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 98048342436b..df0b5af4d1ec 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -78,13 +78,6 @@ Cu.import("resource://weave/oauth.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/clientData.js"); -Cu.import("resource://weave/engines/bookmarks.js"); -/*Cu.import("resource://weave/engines/cookies.js"); -Cu.import("resource://weave/engines/history.js"); -Cu.import("resource://weave/engines/passwords.js"); -Cu.import("resource://weave/engines/forms.js"); -Cu.import("resource://weave/engines/tabs.js"); -Cu.import("resource://weave/engines/input.js");*/ Function.prototype.async = Async.sugar; @@ -104,14 +97,7 @@ Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/syncCores.js", Weave); Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/oauth.js", Weave); -Cu.import("resource://weave/service.js", Weave); -Cu.import("resource://weave/engines/bookmarks.js", Weave); -/*Cu.import("resource://weave/engines/cookies.js", Weave); -Cu.import("resource://weave/engines/passwords.js", Weave); -Cu.import("resource://weave/engines/history.js", Weave); -Cu.import("resource://weave/engines/forms.js", Weave); -Cu.import("resource://weave/engines/tabs.js", Weave); -Cu.import("resource://weave/engines/input.js", Weave);*/ +Cu.import("resource://weave/service.js", Weave); // ?? Utils.lazy(Weave, 'Service', WeaveSvc); @@ -560,16 +546,16 @@ WeaveSvc.prototype = { //yield ClientData.refresh(self.cb); let engines = Engines.getAll(); - for (let i = 0; i < engines.length; i++) { + for each (let engine in engines) { if (this.cancelRequested) continue; - if (!engines[i].enabled) + if (!engine.enabled) continue; - this._log.debug("Syncing engine " + engines[i].name); - yield this._notify(engines[i].name + "-engine:sync", "", - this._syncEngine, engines[i]).async(this, self.cb); + this._log.debug("Syncing engine " + engine.name); + yield this._notify(engine.name + "-engine:sync", "", + this._syncEngine, engine).async(this, self.cb); } if (this._syncError) { diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index aeef6cfc3bef..d7d86ffefb32 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -142,6 +142,10 @@ Store.prototype = { throw "override wrapItem in a subclass"; }, + changeItemID: function Store_changeItemID(oldID, newID) { + throw "override changeItemID in a subclass"; + }, + getAllIDs: function Store_getAllIDs() { throw "override getAllIDs in a subclass"; }, From 60c22cd76bfd2a3c6c9dfeb6e0f3de48d731c92e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 10 Dec 2008 00:57:27 -0800 Subject: [PATCH 0766/1860] closer to history working --- services/sync/modules/engines/history.js | 148 ++++++++++++++++------- 1 file changed, 101 insertions(+), 47 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 2017c592774e..307e1a11d544 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -88,11 +88,23 @@ HistoryStore.prototype = { getService(Ci.nsINavHistoryService); hsvc.QueryInterface(Ci.nsIGlobalHistory2); hsvc.QueryInterface(Ci.nsIBrowserHistory); + hsvc.QueryInterface(Ci.nsPIPlacesDatabase); this.__defineGetter__("_hsvc", function() hsvc); return hsvc; }, - get _histDB() { + get _anno() { + let anno = Cc["@mozilla.org/browser/annotation-service;1"]. + getService(Ci.nsIAnnotationService); + this.__defineGetter__("_ans", function() anno); + return anno; + }, + + get _histDB_31() { + return this._hsvc.DBConnection; + }, + + get _histDB_30() { let file = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties). get("ProfD", Ci.nsIFile); @@ -100,13 +112,44 @@ HistoryStore.prototype = { let stor = Cc["@mozilla.org/storage/service;1"]. getService(Ci.mozIStorageService); let db = stor.openDatabase(file); - this.__defineGetter__("_histDB", function() db); + this.__defineGetter__("_histDB_30", function() db); return db; }, - _itemExists: function HistStore__itemExists(GUID) { - // we don't care about already-existing items; just try to re-add them - return false; + get _db() { + // FIXME + //if (fx3.0) + // return this._histDB_30; + //else + return this._histDB_31; + }, + + get _visitStm() { + let stmt = this._db.createStatement( + "SELECT visit_type FROM moz_historyvisits WHERE place_id = ?1"); + this.__defineGetter__("_visitStm", function() stmt); + return stmt; + }, + + get _pidStm() { + let stmt = this._db.createStatement( + "SELECT id FROM moz_places WHERE url = ?1"); + this.__defineGetter__("_pidStm", function() stmt); + return stmt; + }, + + get _urlStm() { + let stmt = this._db.createStatement( + "SELECT url FROM moz_places WHERE id = ?1"); + this.__defineGetter__("_urlStm", function() stmt); + return stmt; + }, + + get _findPidByAnnoStm() { + let stmt = this._db.createStatement( + "SELECT place_id FROM moz_annos WHERE type = ?1 AND content = ?2"); + this.__defineGetter__("_findPidByAnnoStm", function() stmt); + return stmt; }, _createCommand: function HistStore__createCommand(command) { @@ -138,68 +181,79 @@ HistoryStore.prototype = { // FIXME: implement! }, - _historyRoot: function HistStore__historyRoot() { + // See bug 320831 for why we use SQL here + _getVisitType: function HistStore__getVisitType(uri) { + this._pidStm.bindUTF8StringParameter(0, uri); + if (this._pidStm.executeStep()) { + let placeId = this._pidStm.getInt32(0); + this._visitStm.bindInt32Parameter(0, placeId); + if (this._visitStm.executeStep()) + return this._visitStm.getInt32(0); + } + return null; + }, + + _getGUID: function HistStore__getAnno(uri) { + try { + return this._anno.getPageAnnotation(uri, "weave/guid"); + } catch (e) { + // FIXME + // if (e != NS_ERROR_NOT_AVAILABLE) + // throw e; + } + let guid = Utils.makeGUID(); + this._anno.setPageAnnotation(uri, "weave/guid", guid, 0, 0); + return guid; + }, + + // See bug 468732 for why we use SQL here + _findURLByGUID: function HistStore__findByGUID(guid) { + this._findPidByAnnoStm.bindUTF8Parameter(0, "weave/guid"); + this._findPidByAnnoStm.bindUTF8Parameter(1, guid); + if (this._findPidByAnnoStm.executeStep()) { + let placeId = this._findPidByAnnoStm.getInt32(0); + this._urlStm.bindInt32Parameter(0, placeId); + if (this._urlStm.executeStep()) { + return this._urlStm.getString(0); + } + } + return null; + }, + + wrap: function HistStore_wrap() { let query = this._hsvc.getNewQuery(), options = this._hsvc.getNewQueryOptions(); query.minVisits = 1; - options.maxResults = 1000; - options.resultType = options.RESULTS_AS_VISIT; // FULL_VISIT does not work - options.sortingMode = options.SORT_BY_DATE_DESCENDING; - options.queryType = options.QUERY_TYPE_HISTORY; let root = this._hsvc.executeQuery(query, options).root; root.QueryInterface(Ci.nsINavHistoryQueryResultNode); - return root; - }, - - /* UGLY, UGLY way of syncing visit type ! - We'll just have to wait for bug #320831 */ - _getVisitType: function HistStore__getVisitType(uri) { - let visitStmnt = this._histDB.createStatement("SELECT visit_type FROM moz_historyvisits WHERE place_id = ?1"); - let pidStmnt = this._histDB.createStatement("SELECT id FROM moz_places WHERE url = ?1"); - - pidStmnt.bindUTF8StringParameter(0, uri); - - let placeID = null; - if (pidStmnt.executeStep()) { - placeID = pidStmnt.getInt32(0); - } - - if (placeID) { - visitStmnt.bindInt32Parameter(0, placeID); - if (visitStmnt.executeStep()) - return visitStmnt.getInt32(0); - } - return null; - }, - - wrap: function HistStore_wrap() { - let root = this._historyRoot(); root.containerOpen = true; + let items = {}; for (let i = 0; i < root.childCount; i++) { let item = root.getChild(i); - let guid = item.time + ":" + item.uri; + let guid = this._getGUID(item.uri); let vType = this._getVisitType(item.uri); - items[guid] = {parentid: '', - title: item.title, + items[guid] = {title: item.title, URI: item.uri, - time: item.time, transition: vType}; + // FIXME: get last 10 visit times (& transitions ?) } - - this._lookup = items; + return items; }, + wrapItem: function HistStore_wrapItem(id) { + // FIXME: use findURLByGUID! (not so important b/c of cache hints though) + if (this._itemCache) + return this._itemCache[id]; + let all = this._wrap(); + return all[id]; + }, + wipe: function HistStore_wipe() { this._hsvc.removeAllPages(); - }, - - _resetGUIDs: function FormStore__resetGUIDs() { - let self = yield; - // Not needed. } }; From fd9fd5f7159451fd0c7c7dffb4af555febbed7e3 Mon Sep 17 00:00:00 2001 From: Date: Wed, 10 Dec 2008 18:13:05 -0800 Subject: [PATCH 0767/1860] Added UI to set username/password/passphrase for Weave client on Fennec, and made it work. Following my UI proposal for now, it uses html pages loaded at chrome URLs, which is not ideal. --- services/sync/locales/en-US/preferences.dtd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 3af6595768e4..0def1558f8c5 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -89,3 +89,6 @@ + + + From 01186f326645193f8bc516f9490a37a1a334971b Mon Sep 17 00:00:00 2001 From: Date: Thu, 11 Dec 2008 14:26:20 -0800 Subject: [PATCH 0768/1860] On Fennec there is no microsummaries service, so I made the BookmarksEngine catch the exception if the service is undefined, and skip commands that would require it to create a microsummary, logging warnings instead. --- services/sync/modules/engines/bookmarks.js | 36 ++++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e259ede86a40..30e4fcb0d488 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -53,6 +53,8 @@ const SHARED_BOOKMARK_FILE_NAME = "shared_bookmarks"; const INCOMING_SHARE_ROOT_ANNO = "weave/mounted-shares-folder"; const INCOMING_SHARE_ROOT_NAME = "Shared Folders"; +const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); @@ -159,8 +161,13 @@ BookmarksStore.prototype = { __ms: null, get _ms() { if (!this.__ms) - this.__ms = Cc["@mozilla.org/microsummary/service;1"]. - getService(Ci.nsIMicrosummaryService); + try { + this.__ms = Cc["@mozilla.org/microsummary/service;1"]. + getService(Ci.nsIMicrosummaryService); + } catch( e ) { + this.__ms = SERVICE_NOT_SUPPORTED; + this._log.warn("There is no Microsummary service."); + } return this.__ms; }, @@ -240,11 +247,15 @@ BookmarksStore.prototype = { this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", command.data.staticTitle || "", 0, this._ans.EXPIRE_NEVER); let genURI = Utils.makeURI(command.data.generatorURI); - try { - let micsum = this._ms.createMicrosummary(URI, genURI); - this._ms.setMicrosummary(newId, micsum); - } - catch(ex) { /* ignore "missing local generator" exceptions */ } + if (this._ms == SERVICE_NOT_SUPPORTED) { + this._log.warn("Can't create microsummary -- not supported."); + } else { + try { + let micsum = this._ms.createMicrosummary(URI, genURI); + this._ms.setMicrosummary(newId, micsum); + } + catch(ex) { /* ignore "missing local generator" exceptions */ } + } } } break; case "folder": @@ -422,8 +433,12 @@ BookmarksStore.prototype = { case "generatorURI": { let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); let genURI = Utils.makeURI(command.data.generatorURI); - let micsum = this._ms.createMicrosummary(micsumURI, genURI); - this._ms.setMicrosummary(itemId, micsum); + if (this._ms == SERVICE_NOT_SUPPORTED) { + this._log.warn("Can't create microsummary -- not supported."); + } else { + let micsum = this._ms.createMicrosummary(micsumURI, genURI); + this._ms.setMicrosummary(itemId, micsum); + } } break; case "siteURI": this._ls.setSiteURI(itemId, Utils.makeURI(command.data.siteURI)); @@ -533,7 +548,8 @@ BookmarksStore.prototype = { } else if (node.type == node.RESULT_TYPE_URI || node.type == node.RESULT_TYPE_QUERY) { - if (this._ms.hasMicrosummary(node.itemId)) { + if (this._ms != SERVICE_NOT_SUPPORTED && + this._ms.hasMicrosummary(node.itemId)) { item.type = "microsummary"; let micsum = this._ms.getMicrosummary(node.itemId); item.generatorURI = micsum.generator.uri.spec; // breaks local generators From 59fc745d80968376d4747fb18cc6af81a38dd5dd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Dec 2008 13:53:48 -0800 Subject: [PATCH 0769/1860] don't stop applying changes when one fails to do so --- services/sync/modules/engines.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 792eac576405..b8c9781dcf27 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -526,9 +526,14 @@ SyncEngine.prototype = { this._log.debug("Applying server changes"); let inc; while ((inc = this.incoming.shift())) { - yield this._store.applyIncoming(self.cb, inc); - if (inc.modified > this.lastSync) - this.lastSync = inc.modified; + try { + yield this._store.applyIncoming(self.cb, inc); + if (inc.modified > this.lastSync) + this.lastSync = inc.modified; + } catch (e) { + this._log.warn("Error while applying incoming record: " + + (e.message? e.message : e)); + } } } }, From 4122beacf338badb6aa363b48b64fc9911360066 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Dec 2008 13:54:19 -0800 Subject: [PATCH 0770/1860] remove slightly misleading comment --- services/sync/modules/engines/bookmarks.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e259ede86a40..ab8ab9bb9638 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -693,13 +693,6 @@ BookmarksStore.prototype = { } }; -/* - * Tracker objects for each engine may need to subclass the - * getScore routine, which returns the current 'score' for that - * engine. How the engine decides to set the score is upto it, - * as long as the value between 0 and 100 actually corresponds - * to its urgency to sync. - */ function BookmarksTracker() { this._init(); } From cca578a461df58ae3ffacd9fff3e6e0ed8f7762a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Dec 2008 13:55:26 -0800 Subject: [PATCH 0771/1860] add applyImcoming to store base class --- services/sync/modules/stores.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index d7d86ffefb32..4a5d5e6452cb 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -84,6 +84,20 @@ Store.prototype = { this._log = Log4Moz.repository.getLogger("Store." + this._logName); }, + applyIncoming: function BStore_applyIncoming(onComplete, record) { + let fn = function(rec) { + let self = yield; + if (!record.cleartext) + this.remove(record); + else if (!this.itemExists(record.id)) + this.create(record); + else + this.update(record); + }; + fn.async(this, onComplete, record); + }, + + // FIXME unused now _applyCommands: function Store__applyCommands(commandList) { let self = yield; From 1952d9409593055ded5623ed733e57e702a4def9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 12 Dec 2008 13:55:58 -0800 Subject: [PATCH 0772/1860] closer to history somewhat working. need to rethink approach because of performance problems --- services/sync/modules/engines/history.js | 115 +++++++++++++---------- 1 file changed, 66 insertions(+), 49 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 307e1a11d544..91b1b565f924 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -126,14 +126,14 @@ HistoryStore.prototype = { get _visitStm() { let stmt = this._db.createStatement( - "SELECT visit_type FROM moz_historyvisits WHERE place_id = ?1"); + "SELECT visit_type AS type FROM moz_historyvisits WHERE place_id = :placeid"); this.__defineGetter__("_visitStm", function() stmt); return stmt; }, get _pidStm() { let stmt = this._db.createStatement( - "SELECT id FROM moz_places WHERE url = ?1"); + "SELECT id FROM moz_places WHERE url = :url"); this.__defineGetter__("_pidStm", function() stmt); return stmt; }, @@ -152,48 +152,50 @@ HistoryStore.prototype = { return stmt; }, - _createCommand: function HistStore__createCommand(command) { - this._log.debug(" -> creating history entry: " + command.GUID); - try { - let uri = Utils.makeURI(command.data.URI); - let redirect = false; - if (command.data.transition == 5 || command.data.transition == 6) - redirect = true; + create: function HistStore_create(record) { + this._log.debug(" -> creating history entry: " + record.id); + let uri = Utils.makeURI(record.cleartext.URI); + let redirect = false; + if (record.cleartext.transition == 5 || record.cleartext.transition == 6) + redirect = true; - this._hsvc.addVisit(uri, command.data.time, null, - command.data.transition, redirect, 0); - this._hsvc.setPageTitle(uri, command.data.title); - } catch (e) { - this._log.error("Exception caught: " + (e.message? e.message : e)); - } + this._hsvc.addVisit(uri, record.cleartext.time, null, + record.cleartext.transition, redirect, 0); + this._hsvc.setPageTitle(uri, record.cleartext.title); }, - _removeCommand: function HistStore__removeCommand(command) { - this._log.trace(" -> NOT removing history entry: " + command.GUID); - // we can't remove because we only sync the last 1000 items, not - // the whole store. So we don't know if remove commands were - // generated due to the user removing an entry or because it - // dropped past the 1000 item mark. + remove: function HistStore_remove(record) { + this._log.trace(" -> NOT removing history entry: " + record.id); + // FIXME: implement! }, - _editCommand: function HistStore__editCommand(command) { - this._log.trace(" -> FIXME: NOT editing history entry: " + command.GUID); + update: function HistStore_update(record) { + this._log.trace(" -> FIXME: NOT editing history entry: " + record.id); // FIXME: implement! }, // See bug 320831 for why we use SQL here _getVisitType: function HistStore__getVisitType(uri) { - this._pidStm.bindUTF8StringParameter(0, uri); - if (this._pidStm.executeStep()) { - let placeId = this._pidStm.getInt32(0); - this._visitStm.bindInt32Parameter(0, placeId); - if (this._visitStm.executeStep()) - return this._visitStm.getInt32(0); + if (typeof(uri) != "string") + uri = uri.spec; + try { + this._pidStm.params.url = uri; + if (this._pidStm.step()) { + let placeId = this._pidStm.row.id; + this._visitStm.params.placeid = placeId; + if (this._visitStm.step()) + return this._visitStm.row.type; + } + } finally { + this._pidStm.reset(); + this._visitStm.reset(); } return null; }, - _getGUID: function HistStore__getAnno(uri) { + _getGUID: function HistStore__getGUID(uri) { + if (typeof(uri) == "string") + uri = Utils.makeURI(uri); try { return this._anno.getPageAnnotation(uri, "weave/guid"); } catch (e) { @@ -220,6 +222,12 @@ HistoryStore.prototype = { return null; }, + // XXX need a better way to query Places for all GUIDs + getAllIDs: function BStore_getAllIDs() { + let all = this.wrap(); + return all; + }, + wrap: function HistStore_wrap() { let query = this._hsvc.getNewQuery(), options = this._hsvc.getNewQueryOptions(); @@ -271,40 +279,49 @@ HistoryTracker.prototype = { return hsvc; }, + // FIXME: hack! + get _store() { + let store = new HistoryStore(); + this.__defineGetter__("_store", function() store); + return store; + }, + _init: function HT__init() { this.__proto__.__proto__._init.call(this); this._hsvc.addObserver(this, false); }, - /* We don't care about the first four */ - onBeginUpdateBatch: function HT_onBeginUpdateBatch() { - - }, - onEndUpdateBatch: function HT_onEndUpdateBatch() { - - }, - onPageChanged: function HT_onPageChanged() { - - }, - onTitleChanged: function HT_onTitleChanged() { - - }, + onBeginUpdateBatch: function HT_onBeginUpdateBatch() {}, + onEndUpdateBatch: function HT_onEndUpdateBatch() {}, + onPageChanged: function HT_onPageChanged() {}, + onTitleChanged: function HT_onTitleChanged() {}, /* Every add or remove is worth 1 point. - * Clearing the whole history is worth 50 points, - * to ensure we're above the cutoff for syncing - * ASAP. + * Clearing the whole history is worth 50 points (see below) */ - onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { + _upScore: function BMT__upScore() { + if (!this.enabled) + return; this._score += 1; }, + + onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { + this._log.trace("onVisit: " + itemId); + this.addChangedID(this._store._getGUID(uri)); + this._upScore(); + }, onPageExpired: function HT_onPageExpired(uri, time, entry) { - this._score += 1; + this._log.trace("onPageExpired: " + itemId); + this.addChangedID(this._store._getGUID(uri)); // XXX eek ? + this._upScore(); }, onDeleteURI: function HT_onDeleteURI(uri) { - this._score += 1; + this._log.trace("onDeleteURI: " + itemId); + this.addChangedID(this._store._getGUID(uri)); // FIXME eek + this._upScore(); }, onClearHistory: function HT_onClearHistory() { + this._log.trace("onClearHistory"); this._score += 50; } }; From 510497e07e35c47e3639a39ba3c9d7914f43908b Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Sat, 13 Dec 2008 16:33:04 +0100 Subject: [PATCH 0773/1860] Make Form history sync work again with new engine --- services/sync/modules/engines/forms.js | 208 ++++++++++++------------- services/sync/modules/stores.js | 2 +- 2 files changed, 102 insertions(+), 108 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 903bf504e9ab..d39ffec29143 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -50,156 +50,149 @@ Cu.import("resource://weave/trackers.js"); Function.prototype.async = Async.sugar; -function FormEngine(pbeId) { - this._init(pbeId); -} -FormEngine.prototype = { - __proto__: new SyncEngine(), - - get name() { return "forms"; }, - get displayName() { return "Saved Form Data"; }, - get logName() { return "FormEngine"; }, - get serverPrefix() { return "user-data/forms/"; }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new FormStore(); - return this.__store; - }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new FormSyncCore(this._store); - return this.__core; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new FormsTracker(); - return this.__tracker; - } -}; - -function FormSyncCore(store) { - this._store = store; +function FormEngine() { this._init(); } -FormSyncCore.prototype = { - _logName: "FormSync", - _store: null, +FormEngine.prototype = { + __proto__: SyncEngine.prototype, + get _super() SyncEngine.prototype, + + get name() "forms", + get displayName() "Forms", + get logName() "Forms", + get serverPrefix() "user-data/forms/", - _commandLike: function FSC_commandLike(a, b) { - /* Not required as GUIDs for similar data sets will be the same */ - return false; + get _store() { + let store = new FormStore(); + this.__defineGetter__("_store", function() store); + return store; + }, + + get _tracker() { + let tracker = new FormsTracker(); + this.__defineGetter__("_tracker", function() tracker); + return tracker; } }; -FormSyncCore.prototype.__proto__ = new SyncCore(); + function FormStore() { this._init(); } FormStore.prototype = { + __proto__: Store.prototype, _logName: "FormStore", _lookup: null, - __formDB: null, get _formDB() { - if (!this.__formDB) { - var file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("formhistory.sqlite"); - var stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__formDB = stor.openDatabase(file); - } - return this.__formDB; + let file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsIFile); + file.append("formhistory.sqlite"); + let stor = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService); + let formDB = stor.openDatabase(file); + + this.__defineGetter__("_formDB", function() formDB); + return formDB; }, - __formHistory: null, get _formHistory() { - if (!this.__formHistory) - this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"]. - getService(Ci.nsIFormHistory2); - return this.__formHistory; + let formHistory = Cc["@mozilla.org/satchel/form-history;1"]. + getService(Ci.nsIFormHistory2); + this.__defineGetter__("_formHistory", function() formHistory); + return formHistory; + }, + + get _formStatement() { + let stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); + this.__defineGetter__("_formStatement", function() stmnt); + return stmnt; }, - _createCommand: function FormStore__createCommand(command) { - this._log.info("FormStore got createCommand: " + command); - this._formHistory.addEntry(command.data.name, command.data.value); + create: function FormStore_create(record) { + this._log.debug("Got create record: " + record.id); + this._formHistory.addEntry(record.cleartext.name, record.cleartext.value); }, - _removeCommand: function FormStore__removeCommand(command) { - this._log.info("FormStore got removeCommand: " + command); - - var data; - if (command.GUID in this._lookup) { - data = this._lookup[command.GUID]; + remove: function FormStore_remove(record) { + this._log.debug("Got remove record: " + record.id); + + if (record.id in this._lookup) { + let data = this._lookup[record.id]; } else { this._log.warn("Invalid GUID found, ignoring remove request."); return; } - var nam = data.name; - var val = data.value; - this._formHistory.removeEntry(nam, val); - - delete this._lookup[command.GUID]; + this._formHistory.removeEntry(data.name, data.value); + delete this._lookup[record.id]; }, - _editCommand: function FormStore__editCommand(command) { - this._log.info("FormStore got editCommand: " + command); - this._log.warn("Form syncs are expected to only be create/remove!"); + update: function FormStore__editCommand(record) { + this._log.debug("Got update record: " + record.id); + this._log.warn("Ignoring update request"); }, wrap: function FormStore_wrap() { this._lookup = {}; - var stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); + let stmnt = this._formStatement; while (stmnt.executeStep()) { - var nam = stmnt.getUTF8String(1); - var val = stmnt.getUTF8String(2); - var key = Utils.sha1(nam + val); + let nam = stmnt.getUTF8String(1); + let val = stmnt.getUTF8String(2); + let key = Utils.sha1(nam + val); this._lookup[key] = { name: nam, value: val }; } - + stmnt.reset(); + return this._lookup; }, - wipe: function FormStore_wipe() { - this._formHistory.removeAllEntries(); + wrapItem: function FormStore_wrapItem(id) { + if (!this._lookup) + this._lookup = this.wrap(); + return this._lookup[id]; }, - _resetGUIDs: function FormStore__resetGUIDs() { - let self = yield; - // Not needed. + getAllIDs: function FormStore_getAllIDs() { + if (!this._lookup) + this._lookup = this.wrap(); + return this._lookup; + }, + + wipe: function FormStore_wipe() { + this._formHistory.removeAllEntries(); } }; -FormStore.prototype.__proto__ = new Store(); function FormsTracker() { this._init(); } FormsTracker.prototype = { + __proto__: Tracker.prototype, _logName: "FormsTracker", - __formDB: null, get _formDB() { - if (!this.__formDB) { - var file = Cc["@mozilla.org/file/directory_service;1"]. + let file = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties). get("ProfD", Ci.nsIFile); - file.append("formhistory.sqlite"); - var stor = Cc["@mozilla.org/storage/service;1"]. + + file.append("formhistory.sqlite"); + let stor = Cc["@mozilla.org/storage/service;1"]. getService(Ci.mozIStorageService); - this.__formDB = stor.openDatabase(file); - } - - return this.__formDB; + + let formDB = stor.openDatabase(file); + this.__defineGetter__("_formDB", function() formDB); + return formDB; + }, + + get _formStatement() { + let stmnt = this._formDB. + createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + this.__defineGetter__("_formStatement", function() stmnt); + return stmnt; }, /* nsIFormSubmitObserver is not available in JS. @@ -217,13 +210,13 @@ FormsTracker.prototype = { */ _rowCount: 0, get score() { - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + let stmnt = this._formStatement; stmnt.executeStep(); - var count = stmnt.getInt32(0); - stmnt.reset(); - + + let count = stmnt.getInt32(0); this._score = Math.abs(this._rowCount - count) * 2; - + stmnt.reset(); + if (this._score >= 100) return 100; else @@ -231,21 +224,22 @@ FormsTracker.prototype = { }, resetScore: function FormsTracker_resetScore() { - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); - stmnt.executeStep(); + let stmnt = this._formStatement; + stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); - stmnt.reset(); this._score = 0; + stmnt.reset(); }, _init: function FormsTracker__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; + this.__proto__.__proto__._init.call(this); - var stmnt = this._formDB.createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); + let stmnt = this._formStatement; stmnt.executeStep(); + this._rowCount = stmnt.getInt32(0); stmnt.reset(); } }; -FormsTracker.prototype.__proto__ = new Tracker(); + diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 4a5d5e6452cb..008ab87d430f 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -89,7 +89,7 @@ Store.prototype = { let self = yield; if (!record.cleartext) this.remove(record); - else if (!this.itemExists(record.id)) + else if (!this._itemExists(record.id)) this.create(record); else this.update(record); From 783b6e79970539e1393d9aa4294817e6db5dbb5e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 16 Dec 2008 17:06:45 -0800 Subject: [PATCH 0774/1860] api change for stores, deal with records not 'commands' --- services/sync/modules/engines/bookmarks.js | 46 ++++++++++------------ services/sync/modules/engines/history.js | 6 +-- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 9bb318a57eaf..576ac26c9215 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -201,21 +201,13 @@ BookmarksStore.prototype = { return null; }, - applyIncoming: function BStore_applyIncoming(onComplete, record) { - let fn = function(rec) { - let self = yield; - if (!record.cleartext) - this._removeCommand({GUID: record.id}); - else if (this._getItemIdForGUID(record.id) < 0) - this._createCommand({GUID: record.id, data: record.cleartext}); - else - this._editCommand({GUID: record.id, data: record.cleartext}); - }; - fn.async(this, onComplete, record); + itemExists: function BStore_itemExists(id) { + return this._getItemIdForGUID(record.id) >= 0; }, - _createCommand: function BStore__createCommand(command) { + create: function BStore_create(record) { let newId; + let command = {GUID: record.id, data: record.cleartext}; let parentId = this._getItemIdForGUID(command.data.parentid); if (parentId < 0) { @@ -239,7 +231,7 @@ BookmarksStore.prototype = { if (command.data.description) { this._ans.setItemAnnotation(newId, "bookmarkProperties/description", command.data.description, 0, - this._ans.EXPIRE_NEVER); + this._ans.EXPIRE_NEVER); } if (command.data.type == "microsummary") { @@ -325,42 +317,44 @@ BookmarksStore.prototype = { } }, - _removeCommand: function BStore__removeCommand(command) { - if (command.GUID == "menu" || - command.GUID == "toolbar" || - command.GUID == "unfiled") { - this._log.warn("Attempted to remove root node (" + command.GUID + - "). Skipping command."); + remove: function BStore_remove(record) { + if (record.id == "menu" || + record.id == "toolbar" || + record.id == "unfiled") { + this._log.warn("Attempted to remove root node (" + record.id + + "). Skipping record removal."); return; } - var itemId = this._bms.getItemIdForGUID(command.GUID); + var itemId = this._bms.getItemIdForGUID(record.id); if (itemId < 0) { - this._log.debug("Item " + command.GUID + "already removed"); + this._log.debug("Item " + record.id + "already removed"); return; } var type = this._bms.getItemType(itemId); switch (type) { case this._bms.TYPE_BOOKMARK: - this._log.debug(" -> removing bookmark " + command.GUID); + this._log.debug(" -> removing bookmark " + record.id); this._bms.removeItem(itemId); break; case this._bms.TYPE_FOLDER: - this._log.debug(" -> removing folder " + command.GUID); + this._log.debug(" -> removing folder " + record.id); this._bms.removeFolder(itemId); break; case this._bms.TYPE_SEPARATOR: - this._log.debug(" -> removing separator " + command.GUID); + this._log.debug(" -> removing separator " + record.id); this._bms.removeItem(itemId); break; default: - this._log.error("removeCommand: Unknown item type: " + type); + this._log.error("remove: Unknown item type: " + type); break; } }, - _editCommand: function BStore__editCommand(command) { + update: function BStore_update(record) { + let command = {GUID: record.id, data: record.cleartext}; + if (command.GUID == "menu" || command.GUID == "toolbar" || command.GUID == "unfiled") { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 91b1b565f924..7aa58c5c2b9b 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -100,10 +100,6 @@ HistoryStore.prototype = { return anno; }, - get _histDB_31() { - return this._hsvc.DBConnection; - }, - get _histDB_30() { let file = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties). @@ -121,7 +117,7 @@ HistoryStore.prototype = { //if (fx3.0) // return this._histDB_30; //else - return this._histDB_31; + return this._hsvc.DBConnection; }, get _visitStm() { From c180978924960fc448d5562f865269a061c2de02 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 16 Dec 2008 17:08:57 -0800 Subject: [PATCH 0775/1860] remove applyCommands from store; make itemExists not a 'private' method --- services/sync/modules/stores.js | 37 ++------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 008ab87d430f..577a497c8515 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -89,7 +89,7 @@ Store.prototype = { let self = yield; if (!record.cleartext) this.remove(record); - else if (!this._itemExists(record.id)) + else if (!this.itemExists(record.id)) this.create(record); else this.update(record); @@ -97,41 +97,8 @@ Store.prototype = { fn.async(this, onComplete, record); }, - // FIXME unused now - _applyCommands: function Store__applyCommands(commandList) { - let self = yield; - - for (var i = 0; i < commandList.length; i++) { - if (i % 5) { - Utils.makeTimerForCall(self.cb); - yield; // Yield to main loop - } - - var command = commandList[i]; - if (this._log.level <= Log4Moz.Level.Trace) - this._log.trace("Processing command: " + this._json.encode(command)); - switch (command["action"]) { - case "create": - this._createCommand(command); - break; - case "remove": - this._removeCommand(command); - break; - case "edit": - this._editCommand(command); - break; - default: - this._log.error("unknown action in command: " + command["action"]); - break; - } - } - }, - applyCommands: function Store_applyCommands(onComplete, commandList) { - this._applyCommands.async(this, onComplete, commandList); - }, - // override only if neccessary - _itemExists: function Store__itemExists(GUID) { + itemExists: function Store_itemExists(GUID) { if (GUID in this._lookup) return true; else From bc8df604f0230bd21699c5e0dcb08300a2aa611b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 17 Dec 2008 02:29:28 -0800 Subject: [PATCH 0776/1860] override _itemExists, itemExists (no _) checks for cached items, then calls that --- services/sync/modules/engines/bookmarks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 576ac26c9215..5a6898628640 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -201,7 +201,7 @@ BookmarksStore.prototype = { return null; }, - itemExists: function BStore_itemExists(id) { + _itemExists: function BStore__itemExists(id) { return this._getItemIdForGUID(record.id) >= 0; }, From e84c711b856fe6482778068bb578131a46993f0d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 17 Dec 2008 02:30:11 -0800 Subject: [PATCH 0777/1860] change itemExists to check for the cache, then call _itemExists, that way subclasses can override just that without having to care about the cache --- services/sync/modules/stores.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 577a497c8515..87d192152b84 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -97,14 +97,21 @@ Store.prototype = { fn.async(this, onComplete, record); }, - // override only if neccessary - itemExists: function Store_itemExists(GUID) { - if (GUID in this._lookup) + itemExists: function Store_itemExists(id) { + if (!this._itemCache) + return this._itemExists(id); + + if (id in this._itemCache) return true; else return false; }, + // subclasses probably want to override this one + _itemExists: function Store__itemExists(id) { + return false; + }, + cacheItemsHint: function Store_cacheItemsHint() { this._itemCache = this.wrap(); }, From 457e439c0f033e871fa79fbc8d3d18e95807b5bc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 17 Dec 2008 02:32:00 -0800 Subject: [PATCH 0778/1860] sync only the last 150 history items (pending some special sauce to sync high-scoring items); various sql fixes. history is syncing now, though there are bugs here and there --- services/sync/modules/engines/history.js | 61 ++++++++++++++++++------ 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 7aa58c5c2b9b..9576ae6135e5 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -40,6 +40,8 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); @@ -136,14 +138,21 @@ HistoryStore.prototype = { get _urlStm() { let stmt = this._db.createStatement( - "SELECT url FROM moz_places WHERE id = ?1"); + "SELECT url FROM moz_places WHERE id = :id"); this.__defineGetter__("_urlStm", function() stmt); return stmt; }, + get _annoAttrIdStm() { + let stmt = this._db.createStatement( + "SELECT id from moz_anno_attributes WHERE name = :name"); + this.__defineGetter__("_annoAttrIdStm", function() stmt); + return stmt; + }, + get _findPidByAnnoStm() { let stmt = this._db.createStatement( - "SELECT place_id FROM moz_annos WHERE type = ?1 AND content = ?2"); + "SELECT place_id AS id FROM moz_annos WHERE anno_attribute_id = :attr AND content = :content"); this.__defineGetter__("_findPidByAnnoStm", function() stmt); return stmt; }, @@ -200,22 +209,39 @@ HistoryStore.prototype = { // throw e; } let guid = Utils.makeGUID(); - this._anno.setPageAnnotation(uri, "weave/guid", guid, 0, 0); + this._anno.setPageAnnotation(uri, "weave/guid", guid, 0, this._anno.EXPIRE_WITH_HISTORY); return guid; }, // See bug 468732 for why we use SQL here _findURLByGUID: function HistStore__findByGUID(guid) { - this._findPidByAnnoStm.bindUTF8Parameter(0, "weave/guid"); - this._findPidByAnnoStm.bindUTF8Parameter(1, guid); - if (this._findPidByAnnoStm.executeStep()) { - let placeId = this._findPidByAnnoStm.getInt32(0); - this._urlStm.bindInt32Parameter(0, placeId); - if (this._urlStm.executeStep()) { - return this._urlStm.getString(0); - } + try { + this._annoAttrIdStm.params.name = "weave/guid"; + if (!this._annoAttrIdStm.step()) + return null; + let annoId = this._annoAttrIdStm.row.id; + + this._findPidByAnnoStm.params.attr = annoId; + this._findPidByAnnoStm.params.content = guid; + if (!this._findPidByAnnoStm.step()) + return null; + let placeId = this._findPidByAnnoStm.row.id; + + this._urlStm.params.id = placeId; + if (!this._urlStm.step()) + return null; + + return this._urlStm.row.url; + } finally { + this._annoAttrIdStm.reset(); + this._findPidByAnnoStm.reset(); + this._urlStm.reset(); } - return null; + }, + + changeItemID: function HStore_changeItemID(oldID, newID) { + let uri = Utils.makeURI(this._findURLByGUID(oldID)); + this._anno.setPageAnnotation(uri, "weave/guid", newID, 0, 0); }, // XXX need a better way to query Places for all GUIDs @@ -229,6 +255,9 @@ HistoryStore.prototype = { options = this._hsvc.getNewQueryOptions(); query.minVisits = 1; + options.maxResults = 150; + options.sortingMode = options.SORT_BY_DATE_DESCENDING; + options.queryType = options.QUERY_TYPE_HISTORY; let root = this._hsvc.executeQuery(query, options).root; root.QueryInterface(Ci.nsINavHistoryQueryResultNode); @@ -268,6 +297,8 @@ HistoryTracker.prototype = { __proto__: Tracker.prototype, _logName: "HistoryTracker", + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]), + get _hsvc() { let hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. getService(Ci.nsINavHistoryService); @@ -302,17 +333,17 @@ HistoryTracker.prototype = { }, onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { - this._log.trace("onVisit: " + itemId); + this._log.trace("onVisit: " + uri.spec); this.addChangedID(this._store._getGUID(uri)); this._upScore(); }, onPageExpired: function HT_onPageExpired(uri, time, entry) { - this._log.trace("onPageExpired: " + itemId); + this._log.trace("onPageExpired: " + uri.spec); this.addChangedID(this._store._getGUID(uri)); // XXX eek ? this._upScore(); }, onDeleteURI: function HT_onDeleteURI(uri) { - this._log.trace("onDeleteURI: " + itemId); + this._log.trace("onDeleteURI: " + uri.spec); this.addChangedID(this._store._getGUID(uri)); // FIXME eek this._upScore(); }, From 3183390fa98171f4c32ec01c0b12a050228e8b54 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 17 Dec 2008 16:04:03 -0800 Subject: [PATCH 0779/1860] sync visit times and transition types for history --- services/sync/modules/engines/history.js | 143 ++++++++++++++--------- 1 file changed, 85 insertions(+), 58 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 9576ae6135e5..8834f5019102 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -122,80 +122,85 @@ HistoryStore.prototype = { return this._hsvc.DBConnection; }, + _fetchRow: function HistStore__fetchRow(stm, params, retparams) { + try { + for (let key in params) { + stm.params[key] = params[key]; + } + if (!stm.step()) + return null; + if (retparams.length == 1) + return stm.row[retparams[0]]; + let ret = {}; + for each (let key in retparams) { + ret[key] = stm.row[key]; + } + return ret; + } finally { + stm.reset(); + } + }, + + _defineStm: function HistStore__defineStm(prop, sql) { + let stm = this._db.createStatement(sql); + this.__defineGetter__(prop, function() stm); + return stm; + }, + get _visitStm() { - let stmt = this._db.createStatement( - "SELECT visit_type AS type FROM moz_historyvisits WHERE place_id = :placeid"); - this.__defineGetter__("_visitStm", function() stmt); - return stmt; + let stm = this._defineStm( + "_visitStm", + "SELECT visit_type AS type, visit_date AS date " + + "FROM moz_historyvisits " + + "WHERE place_id = :placeid " + + "ORDER BY visit_date DESC"); + return stm; }, get _pidStm() { - let stmt = this._db.createStatement( - "SELECT id FROM moz_places WHERE url = :url"); - this.__defineGetter__("_pidStm", function() stmt); - return stmt; + let stm = this._defineStm( + "_pidStm", "SELECT id FROM moz_places WHERE url = :url"); + return stm; }, get _urlStm() { - let stmt = this._db.createStatement( - "SELECT url FROM moz_places WHERE id = :id"); - this.__defineGetter__("_urlStm", function() stmt); - return stmt; + let stm = this._defineStm( + "_urlStm", "SELECT url FROM moz_places WHERE id = :id"); + return stm; }, get _annoAttrIdStm() { - let stmt = this._db.createStatement( + let stm = this._defineStm( + "_annoAttrIdStm", "SELECT id from moz_anno_attributes WHERE name = :name"); - this.__defineGetter__("_annoAttrIdStm", function() stmt); - return stmt; + return stm; }, get _findPidByAnnoStm() { - let stmt = this._db.createStatement( - "SELECT place_id AS id FROM moz_annos WHERE anno_attribute_id = :attr AND content = :content"); - this.__defineGetter__("_findPidByAnnoStm", function() stmt); - return stmt; - }, - - create: function HistStore_create(record) { - this._log.debug(" -> creating history entry: " + record.id); - let uri = Utils.makeURI(record.cleartext.URI); - let redirect = false; - if (record.cleartext.transition == 5 || record.cleartext.transition == 6) - redirect = true; - - this._hsvc.addVisit(uri, record.cleartext.time, null, - record.cleartext.transition, redirect, 0); - this._hsvc.setPageTitle(uri, record.cleartext.title); - }, - - remove: function HistStore_remove(record) { - this._log.trace(" -> NOT removing history entry: " + record.id); - // FIXME: implement! - }, - - update: function HistStore_update(record) { - this._log.trace(" -> FIXME: NOT editing history entry: " + record.id); - // FIXME: implement! + let stm = this._defineStm( + "_findPidByAnnoStm", + "SELECT place_id AS id FROM moz_annos " + + "WHERE anno_attribute_id = :attr AND content = :content"); + return stm; }, // See bug 320831 for why we use SQL here - _getVisitType: function HistStore__getVisitType(uri) { + _getVisits: function HistStore__getVisits(uri) { if (typeof(uri) != "string") uri = uri.spec; - try { - this._pidStm.params.url = uri; - if (this._pidStm.step()) { - let placeId = this._pidStm.row.id; - this._visitStm.params.placeid = placeId; - if (this._visitStm.step()) - return this._visitStm.row.type; - } - } finally { - this._pidStm.reset(); - this._visitStm.reset(); + let placeid = this._fetchRow(this._pidStm, {url: uri}, ['id']); + + this._visitStm.params.placeid = placeid; + let i = 0; + let visits = []; + while (this._visitStm.step()) { + visits.push({date: this._visitStm.row.date, + type: this._visitStm.row.type}); + if (++i > 10) + break; } - return null; + this._visitStm.reset(); + return visits; }, _getGUID: function HistStore__getGUID(uri) { @@ -250,12 +255,36 @@ HistoryStore.prototype = { return all; }, + create: function HistStore_create(record) { + this._log.debug(" -> creating history entry: " + record.cleartext.URI); + + let uri = Utils.makeURI(record.cleartext.URI); + + let visit; + while ((visit = record.cleartext.visits.pop())) { + this._log.debug(" visit " + visit.time); + this._hsvc.addVisit(uri, visit.time, null, visit.type, + (visit.type == 5 || visit.type == 6), 0); + } + this._hsvc.setPageTitle(uri, record.cleartext.title); + }, + + remove: function HistStore_remove(record) { + this._log.trace(" -> NOT removing history entry: " + record.id); + // FIXME: implement! + }, + + update: function HistStore_update(record) { + this._log.trace(" -> FIXME: NOT editing history entry: " + record.id); + // FIXME: implement! + }, + wrap: function HistStore_wrap() { let query = this._hsvc.getNewQuery(), options = this._hsvc.getNewQueryOptions(); query.minVisits = 1; - options.maxResults = 150; + options.maxResults = 100; options.sortingMode = options.SORT_BY_DATE_DESCENDING; options.queryType = options.QUERY_TYPE_HISTORY; @@ -267,11 +296,9 @@ HistoryStore.prototype = { for (let i = 0; i < root.childCount; i++) { let item = root.getChild(i); let guid = this._getGUID(item.uri); - let vType = this._getVisitType(item.uri); items[guid] = {title: item.title, URI: item.uri, - transition: vType}; - // FIXME: get last 10 visit times (& transitions ?) + visits: this._getVisits(item.uri)}; } return items; From de6a6d5ac349dffd9880ce8f1ffa9bbc62db21ae Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 18 Dec 2008 01:09:17 -0800 Subject: [PATCH 0780/1860] typo fix --- services/sync/modules/engines/bookmarks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5a6898628640..fb2a22f68f11 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -202,7 +202,7 @@ BookmarksStore.prototype = { }, _itemExists: function BStore__itemExists(id) { - return this._getItemIdForGUID(record.id) >= 0; + return this._getItemIdForGUID(id) >= 0; }, create: function BStore_create(record) { From 76ed90d792b62fda840cea642036424b19ee3dfe Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 18 Dec 2008 01:11:03 -0800 Subject: [PATCH 0781/1860] fix some variable references to use a local var instead of a closure --- services/sync/modules/stores.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 87d192152b84..146a2e37814c 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -87,12 +87,12 @@ Store.prototype = { applyIncoming: function BStore_applyIncoming(onComplete, record) { let fn = function(rec) { let self = yield; - if (!record.cleartext) - this.remove(record); - else if (!this.itemExists(record.id)) - this.create(record); + if (!rec.cleartext) + this.remove(rec); + else if (!this.itemExists(rec.id)) + this.create(rec); else - this.update(record); + this.update(rec); }; fn.async(this, onComplete, record); }, From 9ad442b36d50d681945eecc54188ae9c0ad29617 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 18 Dec 2008 15:10:33 -0800 Subject: [PATCH 0782/1860] move startup code to happen after the UI comes up --- services/sync/Weave.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index a03b37b6f456..91403d7cb253 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -51,9 +51,9 @@ WeaveService.prototype = { case "app-startup": let os = Components.classes["@mozilla.org/observer-service;1"]. getService(Components.interfaces.nsIObserverService); - os.addObserver(this, "profile-after-change", true); + os.addObserver(this, "sessionstore-windows-restored", true); break; - case "profile-after-change": + case "sessionstore-windows-restored": Components.utils.import("resource://weave/service.js"); Weave.Service.onStartup(); break; From fac48597b05ab9402f8d4bb23d6f49bd9c5028fc Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Fri, 19 Dec 2008 00:42:12 +0100 Subject: [PATCH 0783/1860] Use stream based communication instead of XHR --- services/sync/modules/resource.js | 153 +++++++++++++++--------------- 1 file changed, 75 insertions(+), 78 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 4336c15dd820..2cd2bbd07651 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -19,6 +19,7 @@ * * Contributor(s): * Dan Mills + * Anant Narayanan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -64,7 +65,7 @@ RequestException.prototype = { get status() { return this._request.status; }, toString: function ReqEx_toString() { return "Could not " + this._action + " resource " + this._resource.spec + - " (" + this._request.status + ")"; + " (" + this._request.responseStatus + ")"; } }; @@ -144,14 +145,11 @@ Resource.prototype = { this._filters = []; }, - _createRequest: function Res__createRequest(op, onRequestFinished) { - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(Ci.nsIXMLHttpRequest); - request.onload = onRequestFinished; - request.onerror = onRequestFinished; - request.onprogress = Utils.bind2(this, this._onProgress); - request.mozBackgroundRequest = true; - request.open(op, this.spec, true); + _createRequest: function Res__createRequest() { + let ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let channel = ios.newChannel(this.spec, null, null). + QueryInterface(Ci.nsIHttpChannel); let headers = this.headers; // avoid calling the authorizer more than once for (let key in headers) { @@ -159,33 +157,15 @@ Resource.prototype = { this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); else this._log.trace("HTTP Header " + key + ": " + headers[key]); - request.setRequestHeader(key, headers[key]); + channel.setRequestHeader(key, headers[key], true); } - return request; + return channel; }, _onProgress: function Res__onProgress(event) { this._lastProgress = Date.now(); }, - _setupTimeout: function Res__setupTimeout(request, callback) { - let _request = request; - let _callback = callback; - let onTimer = function() { - if (Date.now() - this._lastProgress > CONNECTION_TIMEOUT) { - this._log.warn("Connection timed out"); - _request.abort(); - _callback({target:{status:-1}}); - } - }; - let listener = new Utils.EventListener(onTimer); - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(listener, CONNECTION_TIMEOUT, - timer.TYPE_REPEATING_SLACK); - this._lastProgress = Date.now(); - return timer; - }, - filterUpload: function Res_filterUpload(onComplete) { let fn = function() { let self = yield; @@ -209,70 +189,58 @@ Resource.prototype = { _request: function Res__request(action, data) { let self = yield; - let listener, wait_timer; let iter = 0; - + + let cb = self.cb; + let channel = this._createRequest(); + if ("undefined" != typeof(data)) this._data = data; - this._log.debug(action + " request for " + this.spec); - if ("PUT" == action || "POST" == action) { yield this.filterUpload(self.cb); this._log.trace(action + " Body:\n" + this._data); + + let upload = channel.QueryInterface(Ci.nsIUploadChannel); + let iStream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + iStream.setData(this._data, this._data.length); + + upload.setUploadStream(iStream, 'text/plain', this._data.length); } - while (iter < Preferences.get(PREFS_BRANCH + "network.numRetries")) { - let cb = self.cb; // to avoid warning because we use it twice - let request = this._createRequest(action, cb); - let timeout_timer = this._setupTimeout(request, cb); - let event = yield request.send(this._data); - timeout_timer.cancel(); - this._lastRequest = event.target; + let listener = new ChannelListener(cb, this._onProgress, this._log); + channel.requestMethod = action; + this._data = yield channel.asyncOpen(listener, null); - if (action == "DELETE" && - Utils.checkStatus(this._lastRequest.status, null, [[200,300],404])) { - this._dirty = false; - this._data = null; - break; + // FIXME: this should really check a variety of permanent errors, + // rather than just >= 400 + if (channel.responseStatus >= 400) { + this._log.debug(action + " request failed (" + + channel.responseStatus + ")"); + if (this._data) + this._log.debug("Error response: \n" + this._data); + throw new RequestException(this, action, channel); + } else { + this._log.debug(channel.requestMethod + " request successful (" + + channel.responseStatus + ")"); - } else if (Utils.checkStatus(this._lastRequest.status)) { - this._log.debug(action + " request successful (" + this._lastRequest.status + ")"); - this._dirty = false; - - if ("GET" == action || "POST" == action) { - this._data = this._lastRequest.responseText; - this._log.trace(action + " Body:\n" + this._data); - yield this.filterDownload(self.cb); + switch (action) { + case "DELETE": + if (Utils.checkStatus(channel.responseStatus, null, [[200,300],404])){ + this._dirty = false; + this._data = null; } break; - - // FIXME: this should really check a variety of permanent errors, rather than just >= 400 - } else if (this._lastRequest.status >= 400) { - this._log.debug(action + " request failed (" + this._lastRequest.status + ")"); - if (this._lastRequest.responseText) - this._log.debug("Error response: \n" + this._lastRequest.responseText); - throw new RequestException(this, action, this._lastRequest); - - } else { - // wait for a bit and try again - this._log.debug(action + " request failed (" + - this._lastRequest.status + "), retrying..."); - listener = new Utils.EventListener(self.cb); - wait_timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - yield wait_timer.initWithCallback(listener, iter * iter * 1000, - wait_timer.TYPE_ONE_SHOT); - iter++; + case "GET": + case "PUT": + case "POST": + this._log.trace(action + " Body:\n" + this._data); + yield this.filterDownload(self.cb); + break; } } - - if (iter >= Preferences.get(PREFS_BRANCH + "network.numRetries")) { - this._log.debug(action + " request failed (too many errors)"); - if (this._lastRequest && this._lastRequest.responseText) - this._log.debug("Error response: \n" + this._lastRequest.responseText); - throw new RequestException(this, action, this._lastRequest); - } - + self.done(this._data); }, @@ -293,6 +261,35 @@ Resource.prototype = { } }; +function ChannelListener(onComplete, onProgress, logger) { + this._onComplete = onComplete; + this._onProgress = onProgress; + this._log = logger; +} +ChannelListener.prototype = { + onStartRequest: function Channel_onStartRequest(channel) { + this._log.debug(channel.requestMethod + " request for " + + channel.URI.spec); + this._data = ''; + }, + + onStopRequest: function Channel_onStopRequest(channel, ctx, time) { + if (this._data == '') + this._data = null; + + this._onComplete(this._data); + }, + + onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) { + this._onProgress(); + + let siStream = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + siStream.init(stream); + + this._data += siStream.read(count); + } +}; function JsonFilter() { this._log = Log4Moz.repository.getLogger("Net.JsonFilter"); From 10e7e970e7537c9871dae069b6564a762fd2ea6e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 18 Dec 2008 16:53:25 -0800 Subject: [PATCH 0784/1860] Bug 470208: Fix log4moz typo in FileAppender --- services/sync/modules/log4moz.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 9dfb4ceb909a..77a3ecc4c116 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -468,7 +468,7 @@ FileAppender.prototype = { if (message === null || message.length <= 0) return; try { - this._fos().write(message, message.length); + this._fos.write(message, message.length); } catch(e) { dump("Error writing file:\n" + e); } From 9428f99441a21015fef81b869c515d39c58a0f0e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 18 Dec 2008 22:39:32 -0800 Subject: [PATCH 0785/1860] implement the awe (or tears) inducing places-style sql queries -- history sync works pretty well now --- services/sync/modules/engines.js | 2 + services/sync/modules/engines/history.js | 53 +++++++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b8c9781dcf27..92dadabf97ca 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -526,6 +526,7 @@ SyncEngine.prototype = { this._log.debug("Applying server changes"); let inc; while ((inc = this.incoming.shift())) { + this._log.trace("Incoming record: " + this._json.encode(inc.cleartext)); try { yield this._store.applyIncoming(self.cb, inc); if (inc.modified > this.lastSync) @@ -546,6 +547,7 @@ SyncEngine.prototype = { let up = new Collection(this.engineURL); let out; while ((out = this.outgoing.pop())) { + this._log.trace("Outgoing record: " + this._json.encode(out.cleartext)); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); yield up.pushRecord(self.cb, out); } diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 8834f5019102..46eceac43efd 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -150,22 +150,53 @@ HistoryStore.prototype = { get _visitStm() { let stm = this._defineStm( "_visitStm", - "SELECT visit_type AS type, visit_date AS date " + + "SELECT * FROM ( " + + "SELECT visit_type AS type, visit_date AS date " + + "FROM moz_historyvisits_temp " + + "WHERE place_id = :placeid " + + "ORDER BY visit_date DESC " + + "LIMIT 10 " + + ") " + + "UNION ALL " + + "SELECT * FROM ( " + + "SELECT visit_type AS type, visit_date AS date " + "FROM moz_historyvisits " + "WHERE place_id = :placeid " + - "ORDER BY visit_date DESC"); + "AND id NOT IN (SELECT id FROM moz_historyvisits_temp) " + + "ORDER BY visit_date DESC " + + "LIMIT 10 " + + ") " + + "ORDER BY 2 DESC LIMIT 10"); /* 2 is visit_date */ return stm; }, get _pidStm() { let stm = this._defineStm( - "_pidStm", "SELECT id FROM moz_places WHERE url = :url"); + "_pidStm", + "SELECT * FROM " + + "(SELECT id FROM moz_places_temp WHERE url = :url LIMIT 1) " + + "UNION ALL " + + "SELECT * FROM ( " + + "SELECT id FROM moz_places WHERE url = :url " + + "AND id NOT IN (SELECT id from moz_places_temp) " + + "LIMIT 1 " + + ") " + + "LIMIT 1"); return stm; }, get _urlStm() { let stm = this._defineStm( - "_urlStm", "SELECT url FROM moz_places WHERE id = :id"); + "_urlStm", + "SELECT * FROM " + + "(SELECT url FROM moz_places_temp WHERE id = :id LIMIT 1) " + + "UNION ALL " + + "SELECT * FROM ( " + + "SELECT url FROM moz_places WHERE ud = :id " + + "AND id NOT IN (SELECT id from moz_places_temp) " + + "LIMIT 1 " + + ") " + + "LIMIT 1"); return stm; }, @@ -186,18 +217,20 @@ HistoryStore.prototype = { // See bug 320831 for why we use SQL here _getVisits: function HistStore__getVisits(uri) { + let visits = []; if (typeof(uri) != "string") uri = uri.spec; + let placeid = this._fetchRow(this._pidStm, {url: uri}, ['id']); + if (!placeid) { + this._log.debug("Could not find place ID for history URL: " + placeid); + return visits; + } this._visitStm.params.placeid = placeid; - let i = 0; - let visits = []; while (this._visitStm.step()) { visits.push({date: this._visitStm.row.date, type: this._visitStm.row.type}); - if (++i > 10) - break; } this._visitStm.reset(); return visits; @@ -262,8 +295,8 @@ HistoryStore.prototype = { let visit; while ((visit = record.cleartext.visits.pop())) { - this._log.debug(" visit " + visit.time); - this._hsvc.addVisit(uri, visit.time, null, visit.type, + this._log.debug(" visit " + visit.date); + this._hsvc.addVisit(uri, visit.date, null, visit.type, (visit.type == 5 || visit.type == 6), 0); } this._hsvc.setPageTitle(uri, record.cleartext.title); From 9e02e6bc0e2e3244627df40dbea6b605ecb81dad Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 19 Dec 2008 11:48:09 -0800 Subject: [PATCH 0786/1860] change engine to process (download, reconcile, apply changes) incoming records one by one --- .../sync/modules/base_records/collection.js | 12 ++ services/sync/modules/engines.js | 153 +++++++----------- services/sync/modules/engines/bookmarks.js | 20 ++- 3 files changed, 83 insertions(+), 102 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index b388c0a17a83..b6282580a37d 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -79,6 +79,8 @@ Collection.prototype = { args.push('modified=' + this.modified); if (this.full) args.push('full=1'); + if (this.sort) + args.push('sort=' + this.sort); this.uri.query = (args.length > 0)? '?' + args.join('&') : ''; }, @@ -97,6 +99,16 @@ Collection.prototype = { this._rebuildURL(); }, + // get items sorted by some criteria + // date + // index + // depthindex + get sort() { return this._sort; }, + set sort(value) { + this._sort = value; + this._rebuildURL(); + }, + get iter() { if (!this._iter) this._iter = new CollectionIterator(this); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 92dadabf97ca..505a01f2eff3 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -406,50 +406,31 @@ SyncEngine.prototype = { }, // Generate outgoing records - _fetchIncoming: function SyncEngine__fetchIncoming() { + _processIncoming: function SyncEngine__processIncoming() { let self = yield; - this._log.debug("Downloading server changes"); + this._log.debug("Downloading & applying server changes"); let newitems = new Collection(this.engineURL); newitems.modified = this.lastSync; newitems.full = true; + newitems.sort = "depthindex"; yield newitems.get(self.cb); + this._lastSyncTmp = 0; let item; while ((item = yield newitems.iter.next(self.cb))) { - this.incoming.push(item); + yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); + if (yield this._reconcile.async(this, self.cb, item)) + yield this._applyIncoming.async(this, self.cb, item); + else + this._log.debug("Skipping reconciled incoming item"); } - }, + if (this.lastSync < this._lastSyncTmp) + this.lastSync = this._lastSyncTmp; - // Process incoming records to get them ready for reconciliation and applying later - // i.e., decrypt them, and sort them - _processIncoming: function SyncEngine__processIncoming() { - let self = yield; - - this._log.debug("Decrypting and sorting incoming changes"); - - for each (let inc in this.incoming) { - yield inc.decrypt(self.cb, ID.get('WeaveCryptoID').password); - this._recDepth(inc); // note: doesn't need access to payload - } - this.incoming.sort(function(a, b) { - if ((typeof(a.depth) == "number" && typeof(b.depth) == "undefined") || - (typeof(a.depth) == "number" && b.depth == null) || - (a.depth > b.depth)) - return 1; - if ((typeof(a.depth) == "undefined" && typeof(b.depth) == "number") || - (a.depth == null && typeof(b.depth) == "number") || - (a.depth < b.depth)) - return -1; - if (a.cleartext && b.cleartext) { - if (a.cleartext.index > b.cleartext.index) - return 1; - if (a.cleartext.index < b.cleartext.index) - return -1; - } - return 0; - }); + // removes any holes caused by reconciliation above: + this._outgoing = this.outgoing.filter(function(n) n); }, // Reconciliation has two steps: @@ -464,78 +445,65 @@ SyncEngine.prototype = { // bookmarks imported, every bookmark will match this condition. // When two items with different IDs are "the same" we change the local ID to // match the remote one. - _reconcile: function SyncEngine__reconcile() { + _reconcile: function SyncEngine__reconcile(item) { let self = yield; + let ret = true; - this._log.debug("Reconciling server/client changes"); - - this._log.debug(this.incoming.length + " items coming in, " + - this.outgoing.length + " items going out"); + this._log.debug("Reconciling incoming item"); // Check for the same item (same ID) on both incoming & outgoing queues let conflicts = []; - for (let i = 0; i < this.incoming.length; i++) { - for (let o = 0; o < this.outgoing.length; o++) { - if (this.incoming[i].id == this.outgoing[o].id) { - // Only consider it a conflict if there are actual differences - // otherwise, just remove the outgoing record as well - if (!Utils.deepEquals(this.incoming[i].cleartext, - this.outgoing[o].cleartext)) - conflicts.push({in: this.incoming[i], out: this.outgoing[o]}); - else - delete this.outgoing[o]; - delete this.incoming[i]; - break; - } + for (let o = 0; o < this.outgoing.length; o++) { + if (!this.outgoing[o]) + continue; // skip previously removed items + if (item.id == this.outgoing[o].id) { + // Only consider it a conflict if there are actual differences + // otherwise, just ignore the outgoing record as well + if (!Utils.deepEquals(item.cleartext, this.outgoing[o].cleartext)) + conflicts.push({in: item, out: this.outgoing[o]}); + else + delete this.outgoing[o]; + + self.done(false); + return; } - this._outgoing = this.outgoing.filter(function(n) n); // removes any holes } - this._incoming = this.incoming.filter(function(n) n); // removes any holes if (conflicts.length) this._log.debug("Conflicts found. Conflicting server changes discarded"); // Check for items with different IDs which we think are the same one - for (let i = 0; i < this.incoming.length; i++) { - for (let o = 0; o < this.outgoing.length; o++) { - if (this._recordLike(this.incoming[i], this.outgoing[o])) { - // change refs in outgoing queue - yield this._changeRecordRefs.async(this, self.cb, - this.outgoing[o].id, - this.incoming[i].id); - // change actual id of item - this._store.changeItemID(this.outgoing[o].id, - this.incoming[i].id); - delete this.incoming[i]; - delete this.outgoing[o]; - break; - } - } - this._outgoing = this.outgoing.filter(function(n) n); // removes any holes - } - this._incoming = this.incoming.filter(function(n) n); // removes any holes + for (let o = 0; o < this.outgoing.length; o++) { + if (!this.outgoing[o]) + continue; // skip previously removed items - this._log.debug("Reconciliation complete"); - this._log.debug(this.incoming.length + " items coming in, " + - this.outgoing.length + " items going out"); + if (this._recordLike(item, this.outgoing[o])) { + // change refs in outgoing queue + yield this._changeRecordRefs.async(this, self.cb, + this.outgoing[o].id, + item.id); + // change actual id of item + this._store.changeItemID(this.outgoing[o].id, + item.id); + delete this.outgoing[o]; + self.done(false); + return; + } + } + self.done(true); }, // Apply incoming records - _applyIncoming: function SyncEngine__applyIncoming() { + _applyIncoming: function SyncEngine__applyIncoming(item) { let self = yield; - if (this.incoming.length) { - this._log.debug("Applying server changes"); - let inc; - while ((inc = this.incoming.shift())) { - this._log.trace("Incoming record: " + this._json.encode(inc.cleartext)); - try { - yield this._store.applyIncoming(self.cb, inc); - if (inc.modified > this.lastSync) - this.lastSync = inc.modified; - } catch (e) { - this._log.warn("Error while applying incoming record: " + - (e.message? e.message : e)); - } - } + this._log.debug("Applying incoming record"); + this._log.trace("Incoming record: " + this._json.encode(item.cleartext)); + try { + yield this._store.applyIncoming(self.cb, item); + if (item.modified > this._lastSyncTmp) + this._lastSyncTmp = item.modified; + } catch (e) { + this._log.warn("Error while applying incoming record: " + + (e.message? e.message : e)); } }, @@ -573,16 +541,13 @@ SyncEngine.prototype = { try { yield this._syncStartup.async(this, self.cb); - // Populate incoming and outgoing queues + // Populate outgoing queue yield this._generateOutgoing.async(this, self.cb); - yield this._fetchIncoming.async(this, self.cb); - // Decrypt and sort incoming records, then reconcile + // Fetch incoming records and apply them yield this._processIncoming.async(this, self.cb); - yield this._reconcile.async(this, self.cb); - // Apply incoming records, upload outgoing records - yield this._applyIncoming.async(this, self.cb); + // Upload outgoing records yield this._uploadOutgoing.async(this, self.cb); yield this._syncFinish.async(this, self.cb); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index fb2a22f68f11..4ba116fb1011 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -328,7 +328,7 @@ BookmarksStore.prototype = { var itemId = this._bms.getItemIdForGUID(record.id); if (itemId < 0) { - this._log.debug("Item " + record.id + "already removed"); + this._log.debug("Item " + record.id + " already removed"); return; } var type = this._bms.getItemType(itemId); @@ -748,24 +748,26 @@ BookmarksTracker.prototype = { }, onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { - this._log.trace("onItemAdded: " + itemId); - this._all[itemId] = this._bms.getItemGUID(itemId); + //if (!this.enabled) + //return; + this._log.trace("onItemAdded: " + itemId); this.addChangedID(this._all[itemId]); - this._upScore(); }, onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { - this._log.trace("onItemRemoved: " + itemId); - - this.addChangedID(this._all[itemId]); delete this._all[itemId]; - + if (!this.enabled) + return; + this._log.trace("onItemRemoved: " + itemId); + this.addChangedID(this._all[itemId]); this._upScore(); }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnnotationProperty, value) { + if (!this.enabled) + return; this._log.trace("onItemChanged: " + itemId + ", property: " + property + ", isAnno: " + isAnnotationProperty + ", value: " + value); @@ -786,6 +788,8 @@ BookmarksTracker.prototype = { }, onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { + if (!this.enabled) + return; this._log.trace("onItemMoved: " + itemId); this.addChangedID(this._all[itemId]); this._upScore(); From c18e054dccddbbfceecd9be8707974517b280c90 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 19 Dec 2008 15:24:37 -0800 Subject: [PATCH 0787/1860] don't filter downloads on a PUT; fail permanently on http status 400-499, 501, 505 --- services/sync/modules/resource.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 2cd2bbd07651..899eb306e691 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -190,8 +190,6 @@ Resource.prototype = { _request: function Res__request(action, data) { let self = yield; let iter = 0; - - let cb = self.cb; let channel = this._createRequest(); if ("undefined" != typeof(data)) @@ -209,15 +207,12 @@ Resource.prototype = { upload.setUploadStream(iStream, 'text/plain', this._data.length); } - let listener = new ChannelListener(cb, this._onProgress, this._log); + let listener = new ChannelListener(self.cb, this._onProgress, this._log); channel.requestMethod = action; this._data = yield channel.asyncOpen(listener, null); - // FIXME: this should really check a variety of permanent errors, - // rather than just >= 400 - if (channel.responseStatus >= 400) { - this._log.debug(action + " request failed (" + - channel.responseStatus + ")"); + if (Utils.checkStatus(channel.responseStatus, null, [[400,499],501,505])) { + this._log.debug(action + " request failed (" + channel.responseStatus + ")"); if (this._data) this._log.debug("Error response: \n" + this._data); throw new RequestException(this, action, channel); @@ -233,7 +228,6 @@ Resource.prototype = { } break; case "GET": - case "PUT": case "POST": this._log.trace(action + " Body:\n" + this._data); yield this.filterDownload(self.cb); From 4d9c5713dfe8c0015ae2cbed1cb0886da86a31e8 Mon Sep 17 00:00:00 2001 From: Date: Fri, 19 Dec 2008 15:48:40 -0800 Subject: [PATCH 0788/1860] Major rehaul/simplification of the Fennec UI for connecting to your Weave account --- services/sync/modules/clientData.js | 16 ++++++++++++++-- services/sync/modules/service.js | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index 9aad2d84330d..9066915d12e1 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -109,12 +109,24 @@ ClientDataSvc.prototype = { _refresh: function ClientData__refresh() { let self = yield; - let ret = yield DAV.MKCOL("meta", self.cb); + // No more such thing as DAV. I think this is making a directory. + // Will the directory now be handled automatically? (YES) + /* let ret = yield DAV.MKCOL("meta", self.cb); if(!ret) - throw "Could not create meta information directory"; + throw "Could not create meta information directory";*/ + // This fails horribly because this._remote._uri.spec is null. What's + // that mean? + // Spec is supposed to be just a string? + + // Probably problem has to do with Resource getting intialized by + // relative, not absolute, path? + // Use WBORecord (Weave Basic Object) from wbo.js? + this._log.debug("The URI is " + this._remote._uri); + this._log.debug("The URI.spec is " + this._remote._uri.spec); try { yield this._remote.get(self.cb); } catch (e if e.status == 404) { + this._log.debug("404ed. Using empty for remote data."); this._remote.data = {}; } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index df0b5af4d1ec..b413a34b010e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -428,7 +428,7 @@ WeaveSvc.prototype = { this.username = username; ID.get('WeaveID').setTempPassword(password); - + let id = new Identity('Passphrase Verification', username); id.setTempPassword(passphrase); @@ -541,7 +541,7 @@ WeaveSvc.prototype = { if (!(yield this._remoteSetup.async(this, self.cb))) { throw "aborting sync, remote setup failed"; } - + // TODO This right here. Make sure it works. //this._log.debug("Refreshing client list"); //yield ClientData.refresh(self.cb); From 11e62511ab3a78960973e80d103e417f20250590 Mon Sep 17 00:00:00 2001 From: Date: Fri, 19 Dec 2008 16:13:00 -0800 Subject: [PATCH 0789/1860] Changed components/Weave.js to start up the service on the 'profile-after-change' event instead of the 'sessionstore-windows-restored' event, since the latter doesn't exist on Fennec. --- services/sync/Weave.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 91403d7cb253..a03b37b6f456 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -51,9 +51,9 @@ WeaveService.prototype = { case "app-startup": let os = Components.classes["@mozilla.org/observer-service;1"]. getService(Components.interfaces.nsIObserverService); - os.addObserver(this, "sessionstore-windows-restored", true); + os.addObserver(this, "profile-after-change", true); break; - case "sessionstore-windows-restored": + case "profile-after-change": Components.utils.import("resource://weave/service.js"); Weave.Service.onStartup(); break; From 5023daa33801eb2bf2f1c74c5cc99f09118cd6f8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 19 Dec 2008 17:00:12 -0800 Subject: [PATCH 0790/1860] fix typo in history sql queries, clarify valid sort order values for collections --- .../sync/modules/base_records/collection.js | 5 +- services/sync/modules/engines/history.js | 51 ++++++++++--------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index b6282580a37d..9efab67b0737 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -99,8 +99,9 @@ Collection.prototype = { this._rebuildURL(); }, - // get items sorted by some criteria - // date + // get items sorted by some criteria. valid values: + // oldest (oldest first) + // newest (newest first) // index // depthindex get sort() { return this._sort; }, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 46eceac43efd..bcdf9f690704 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -141,15 +141,9 @@ HistoryStore.prototype = { } }, - _defineStm: function HistStore__defineStm(prop, sql) { - let stm = this._db.createStatement(sql); - this.__defineGetter__(prop, function() stm); - return stm; - }, - get _visitStm() { - let stm = this._defineStm( - "_visitStm", + this._log.trace("Creating SQL statement: _visitStm"); + let stm = this._db.createStatement( "SELECT * FROM ( " + "SELECT visit_type AS type, visit_date AS date " + "FROM moz_historyvisits_temp " + @@ -167,12 +161,13 @@ HistoryStore.prototype = { "LIMIT 10 " + ") " + "ORDER BY 2 DESC LIMIT 10"); /* 2 is visit_date */ + this.__defineGetter__("_visitStm", function() stm); return stm; }, get _pidStm() { - let stm = this._defineStm( - "_pidStm", + this._log.trace("Creating SQL statement: _pidStm"); + let stm = this._db.createStatement( "SELECT * FROM " + "(SELECT id FROM moz_places_temp WHERE url = :url LIMIT 1) " + "UNION ALL " + @@ -182,36 +177,42 @@ HistoryStore.prototype = { "LIMIT 1 " + ") " + "LIMIT 1"); + this.__defineGetter__("_pidStm", function() stm); return stm; }, get _urlStm() { - let stm = this._defineStm( - "_urlStm", + this._log.trace("Creating SQL statement: _urlStm"); + let stm = this._db.createStatement( "SELECT * FROM " + "(SELECT url FROM moz_places_temp WHERE id = :id LIMIT 1) " + "UNION ALL " + "SELECT * FROM ( " + - "SELECT url FROM moz_places WHERE ud = :id " + + "SELECT url FROM moz_places WHERE id = :id " + "AND id NOT IN (SELECT id from moz_places_temp) " + "LIMIT 1 " + ") " + "LIMIT 1"); + this._log.trace("_urlStm 1"); + this.__defineGetter__("_urlStm", function() stm); + this._log.trace("_urlStm 2"); return stm; }, get _annoAttrIdStm() { - let stm = this._defineStm( - "_annoAttrIdStm", + this._log.trace("Creating SQL statement: _annoAttrIdStm"); + let stm = this._db.createStatement( "SELECT id from moz_anno_attributes WHERE name = :name"); + this.__defineGetter__("_annoAttrIdStm", function() stm); return stm; }, get _findPidByAnnoStm() { - let stm = this._defineStm( - "_findPidByAnnoStm", + this._log.trace("Creating SQL statement: _findPidByAnnoStm"); + let stm = this._db.createStatement( "SELECT place_id AS id FROM moz_annos " + "WHERE anno_attribute_id = :attr AND content = :content"); + this.__defineGetter__("_findPidByAnnoStm", function() stm); return stm; }, @@ -227,12 +228,16 @@ HistoryStore.prototype = { return visits; } - this._visitStm.params.placeid = placeid; - while (this._visitStm.step()) { - visits.push({date: this._visitStm.row.date, - type: this._visitStm.row.type}); + try { + this._visitStm.params.placeid = placeid; + while (this._visitStm.step()) { + visits.push({date: this._visitStm.row.date, + type: this._visitStm.row.type}); + } + } finally { + this._visitStm.reset(); } - this._visitStm.reset(); + return visits; }, @@ -252,7 +257,7 @@ HistoryStore.prototype = { }, // See bug 468732 for why we use SQL here - _findURLByGUID: function HistStore__findByGUID(guid) { + _findURLByGUID: function HistStore__findURLByGUID(guid) { try { this._annoAttrIdStm.params.name = "weave/guid"; if (!this._annoAttrIdStm.step()) From 24f2ae4140a75c1f9f90a6dee8c28eaf95ed457d Mon Sep 17 00:00:00 2001 From: Date: Fri, 19 Dec 2008 18:35:39 -0800 Subject: [PATCH 0791/1860] Changed tiny check box for large toggle icon for hiding/showing password/passphrase on the Fennec account-connect screen. Note: this icon was drawn by me and is excessively crappy. Please replace with a professional icon. --- services/sync/Weave.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index a03b37b6f456..8416c1ef9124 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -53,6 +53,8 @@ WeaveService.prototype = { getService(Components.interfaces.nsIObserverService); os.addObserver(this, "profile-after-change", true); break; + /* The events "final-ui-startup" and "sessionstore-windows-restored" + * would be better but unfortunately neither one exists in Fennec. */ case "profile-after-change": Components.utils.import("resource://weave/service.js"); Weave.Service.onStartup(); From 60c23cd2337103c3b23f61c7bb5150368640fe7b Mon Sep 17 00:00:00 2001 From: Date: Fri, 19 Dec 2008 18:43:36 -0800 Subject: [PATCH 0792/1860] Set Weave component back to using the 'sessionstore-windows-restored' event. Will use a different way of intializing on Fennec. --- services/sync/Weave.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 8416c1ef9124..2ce339f48d77 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -53,9 +53,10 @@ WeaveService.prototype = { getService(Components.interfaces.nsIObserverService); os.addObserver(this, "profile-after-change", true); break; - /* The events "final-ui-startup" and "sessionstore-windows-restored" - * would be better but unfortunately neither one exists in Fennec. */ - case "profile-after-change": + /* The following event doesn't exist on Fennec; for Fennec loading, see + * fennec-weave-overlay.js. + */ + case "sessionstore-windows-restored": Components.utils.import("resource://weave/service.js"); Weave.Service.onStartup(); break; From bee16c53fd85ce1e9b012b72fe2e21ca2d1c5039 Mon Sep 17 00:00:00 2001 From: Date: Sun, 21 Dec 2008 14:08:33 -0800 Subject: [PATCH 0793/1860] Wrote some scaffolding for the Fennec-weave UI -- doesn't do anything yet. --- services/sync/modules/service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b413a34b010e..36ea1d640d52 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -512,6 +512,7 @@ WeaveSvc.prototype = { } } + // TODO: do not try the following if we're on Fennec: if (needKeys) { let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb); if (pass) { From 85b974343374b34f345b351278ef3181a7f5ebb6 Mon Sep 17 00:00:00 2001 From: Date: Sun, 21 Dec 2008 14:41:17 -0800 Subject: [PATCH 0794/1860] Weave on Fennec will no longer attempt to generate SSH keys -- for this release, you need to have a Weave account already and then connect Fennec to it. --- services/sync/modules/service.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 36ea1d640d52..8997af2c3cce 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -117,6 +117,7 @@ WeaveSvc.prototype = { _isQuitting: false, _loggedIn: false, _syncInProgress: false, + _keyGenEnabled: true, __os: null, get _os() { @@ -200,6 +201,9 @@ WeaveSvc.prototype = { get cancelRequested() this._cancelRequested, set cancelRequested(value) { this._cancelRequested = value; }, + get keyGenEnabled() this._keyGenEnabled, + set keyGenEnabled(value) { this._keyGenEnabled = value; }, + get enabled() Utils.prefs.getBoolPref("enabled"), get schedule() { @@ -512,8 +516,14 @@ WeaveSvc.prototype = { } } + if (this._keyGenEnabled) { // TODO: do not try the following if we're on Fennec: - if (needKeys) { + this._log.warn("Key generation is enabled."); + } else { + this._log.warn("Key generation is disabled."); + } + + if (needKeys && this._keyGenEnabled) { let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb); if (pass) { let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, @@ -531,6 +541,10 @@ WeaveSvc.prototype = { } } + if (needKeys && !this._keyGenEnabled) { + this._log.warn("Can't get keys from server and local keygen disabled."); + } + self.done(ret); }, From 00c3dc8efd7cae0ebd853dd5a11e1f9c945deb30 Mon Sep 17 00:00:00 2001 From: Date: Sun, 21 Dec 2008 14:59:33 -0800 Subject: [PATCH 0795/1860] Removed extra debug code from service.js --- services/sync/modules/service.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8997af2c3cce..03169b9ce8d7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -516,13 +516,6 @@ WeaveSvc.prototype = { } } - if (this._keyGenEnabled) { - // TODO: do not try the following if we're on Fennec: - this._log.warn("Key generation is enabled."); - } else { - this._log.warn("Key generation is disabled."); - } - if (needKeys && this._keyGenEnabled) { let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb); if (pass) { From f26a61dc45e74405f27c895077f6ba73927c546d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 23 Dec 2008 11:18:37 -0800 Subject: [PATCH 0796/1860] register for the right event on firefox --- services/sync/Weave.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 2ce339f48d77..bc117581e38a 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -51,7 +51,7 @@ WeaveService.prototype = { case "app-startup": let os = Components.classes["@mozilla.org/observer-service;1"]. getService(Components.interfaces.nsIObserverService); - os.addObserver(this, "profile-after-change", true); + os.addObserver(this, "sessionstore-windows-restored", true); break; /* The following event doesn't exist on Fennec; for Fennec loading, see * fennec-weave-overlay.js. From 1dc3c758be520ff9ab92f90bf006f3fbed03031a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 23 Dec 2008 11:19:33 -0800 Subject: [PATCH 0797/1860] add a method to push a depth-only record into a collection --- services/sync/modules/base_records/collection.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 9efab67b0737..3d9e651cc58a 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -132,6 +132,10 @@ Collection.prototype = { fn.async(this, onComplete, record); }, + pushDepthRecord: function Coll_pushDepthRecord(record) { + this._data.push(this._json.encode(record)); + }, + clearRecords: function Coll_clearRecords() { this._data = []; } From c2718e647e37eeee2089712b5ee0f6492ea61f6a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 23 Dec 2008 11:20:20 -0800 Subject: [PATCH 0798/1860] add depth and sortindex getters, and a toString for nicer printing --- services/sync/modules/base_records/wbo.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 6b5ed1969262..9b2fd868d29e 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -81,9 +81,25 @@ WBORecord.prototype = { this.data.modified = value; }, + get depth() this.data.depth, + set depth(value) { + this.data.depth = value; + }, + + get sortindex() this.data.sortindex, + set sortindex(value) { + this.data.sortindex = value; + }, + get payload() this.data.payload, set payload(value) { this.data.payload = value; + }, + + toString: function WBORec_toString() { + return "{id: " + this.id + ", depth: " + this.depth + + ", sortindex: " + this.sortindex + ",\nmodified: " + this.modified + + ", payload: " + json.encode(this.cleartext) + "}"; } }; From f9418ee8ffa1b023bafbced6049914d040c65150 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 23 Dec 2008 11:22:29 -0800 Subject: [PATCH 0799/1860] add a method to wrap a depth-only record (just depth and guid, nothing else) --- services/sync/modules/stores.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 146a2e37814c..84bab445ef9b 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -130,6 +130,18 @@ Store.prototype = { throw "override wrapItem in a subclass"; }, + wrapDepth: function BStore_wrapDepth(guid, items) { + if (typeof(items) == "undefined") + items = {}; + for (let childguid in this._itemCache) { + if (this._itemCache[childguid].parentid == guid) { + items[childguid] = this._itemCache[childguid].depth; + this.wrapDepth(childguid, items); + } + } + return items; + }, + changeItemID: function Store_changeItemID(oldID, newID) { throw "override changeItemID in a subclass"; }, From 9d1e4350e93ae758034fdb4a2792cd81bfac24fa Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 23 Dec 2008 11:23:10 -0800 Subject: [PATCH 0800/1860] remove accidentally committed logging calls --- services/sync/modules/engines/history.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index bcdf9f690704..0b6ae45f7f54 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -193,9 +193,7 @@ HistoryStore.prototype = { "LIMIT 1 " + ") " + "LIMIT 1"); - this._log.trace("_urlStm 1"); this.__defineGetter__("_urlStm", function() stm); - this._log.trace("_urlStm 2"); return stm; }, From bce8f611e9fef3a7d2b62e3e9243ef6ed1574d82 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 23 Dec 2008 11:30:31 -0800 Subject: [PATCH 0801/1860] Various engine/bookmark changes: * Rely on the server to sort incoming records, remove all sorting code client-side. * Streamline sync to be able to process incoming records one at a time, as soon as they are downloaded. This changes reconciliation to be able to process a single incoming record. * Engine base class will automatically convert parentid, depth, and sortindex into toplevel WBO objects. This is good for now but kinda broken, engine subclasses should really be generating WBO records themselves. * Since index is now a toplevel WBO property, there is no need for the bookmarks sync code to subclass recordLike. * Refactor bookmarks store to be a little cleaner and work directly with records instead of "commands". --- services/sync/modules/engines.js | 76 ++++--- services/sync/modules/engines/bookmarks.js | 243 ++++++--------------- 2 files changed, 119 insertions(+), 200 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 505a01f2eff3..3d5b8acb93b3 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -297,8 +297,21 @@ SyncEngine.prototype = { record.encryption = this.cryptoMetaURL; record.cleartext = this._store.wrapItem(id); - if (record.cleartext && record.cleartext.parentid) + // XXX subclasses might not expect us to delete the payload fields + if (record.cleartext) { + if (record.cleartext.parentid) { record.parentid = record.cleartext.parentid; + delete record.cleartext.parentid; + } + if (typeof(record.cleartext.depth) != "undefined") { + record.depth = record.cleartext.depth; + delete record.cleartext.depth; + } + if (typeof(record.cleartext.sortindex) != "undefined") { + record.sortindex = record.cleartext.sortindex; + delete record.cleartext.sortindex; + } + } if (encrypt || encrypt == undefined) yield record.encrypt(self.cb, ID.get('WeaveCryptoID').password); @@ -313,6 +326,9 @@ SyncEngine.prototype = { _recordLike: function SyncEngine__recordLike(a, b) { if (a.parentid != b.parentid) return false; + if (a.depth != b.depth) + return false; + // note: sortindex ignored return Utils.deepEquals(a.cleartext, b.cleartext); }, @@ -324,27 +340,6 @@ SyncEngine.prototype = { } }, - _recDepth: function SyncEngine__recDepth(rec) { - // we've calculated depth for this record already - if (rec.depth) - return rec.depth; - - // record has no parent - if (!rec.parentid) - return 0; - - // search for the record's parent and calculate its depth, then add one - for each (let inc in this.incoming) { - if (inc.id == rec.parentid) { - rec.depth = this._recDepth(inc) + 1; - return rec.depth; - } - } - - // we couldn't find the record's parent, so it's an orphan - return 0; - }, - // Any setup that needs to happen at the beginning of each sync. // Makes sure crypto records and keys are all set-up _syncStartup: function SyncEngine__syncStartup() { @@ -426,6 +421,8 @@ SyncEngine.prototype = { else this._log.debug("Skipping reconciled incoming item"); } + if (typeof(this._lastSyncTmp) == "string") + this._lastSyncTmp = parseInt(this._lastSyncTmp); if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; @@ -496,10 +493,10 @@ SyncEngine.prototype = { _applyIncoming: function SyncEngine__applyIncoming(item) { let self = yield; this._log.debug("Applying incoming record"); - this._log.trace("Incoming record: " + this._json.encode(item.cleartext)); + this._log.trace("Incoming:\n" + item); try { yield this._store.applyIncoming(self.cb, item); - if (item.modified > this._lastSyncTmp) + if (this._lastSyncTmp < item.modified) this._lastSyncTmp = item.modified; } catch (e) { this._log.warn("Error while applying incoming record: " + @@ -510,18 +507,41 @@ SyncEngine.prototype = { // Upload outgoing records _uploadOutgoing: function SyncEngine__uploadOutgoing() { let self = yield; + if (this.outgoing.length) { - this._log.debug("Uploading client changes"); + this._log.debug("Uploading client changes (" + this.outgoing.length + ")"); + + // collection we'll upload let up = new Collection(this.engineURL); + + // regen the store cache so we can get item depths + this._store.cacheItemsHint(); + let depth = {}; + let out; while ((out = this.outgoing.pop())) { - this._log.trace("Outgoing record: " + this._json.encode(out.cleartext)); + this._log.trace("Outgoing:\n" + out); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); yield up.pushRecord(self.cb, out); + this._store.wrapDepth(out.id, depth); } + + // now add short depth-only records + this._log.trace(depth.length + "outgoing depth records"); + for (let id in depth) { + up.pushDepthRecord({id: id, depth: depth[id]}); + } + this._store.clearItemCacheHint(); + + // do the upload yield up.post(self.cb); - if (up.data.modified > this.lastSync) - this.lastSync = up.data.modified; + + // save last modified date + let mod = up.data.modified; + if (typeof(mod) == "string") + mod = parseInt(mod); + if (mod > this.lastSync) + this.lastSync = mod; } this._tracker.clearChangedIDs(); }, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 4ba116fb1011..d0dff2b8b2a8 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -91,37 +91,7 @@ BookmarksEngine.prototype = { let tracker = new BookmarksTracker(); this.__defineGetter__("_tracker", function() tracker); return tracker; - }, - - _recordLike: function SyncEngine__recordLike(a, b) { - if (a.parentid != b.parentid) - return false; - for (let key in a.cleartext) { - if (key == "index") - continue; - if (!Utils.deepEquals(a.cleartext[key], b.cleartext[key])) - return false; - } - for (key in b.cleartext) { - if (key == "index") - continue; - if (!Utils.deepEquals(a.cleartext[key], b.cleartext[key])) - return false; - } - return true; - }, - - _changeRecordRefs: function BmkEngine__changeRecordRefs(oldID, newID) { - let self = yield; - for each (let rec in this.outgoing) { - if (rec.parentid == oldID) { - rec.parentid = newID; - rec.cleartext.parentid = newID; - yield rec.encrypt(self.cb, ID.get('WeaveCryptoID').password); - } - } } - // XXX for sharing, will need to re-add code to get new shares before syncing, // and updating incoming/outgoing shared folders after syncing }; @@ -207,38 +177,35 @@ BookmarksStore.prototype = { create: function BStore_create(record) { let newId; - let command = {GUID: record.id, data: record.cleartext}; - let parentId = this._getItemIdForGUID(command.data.parentid); + let parentId = this._getItemIdForGUID(record.parentid); if (parentId < 0) { this._log.warn("Creating node with unknown parent -> reparenting to root"); parentId = this._bms.bookmarksMenuFolder; } - switch (command.data.type) { + switch (record.cleartext.type) { case "query": case "bookmark": case "microsummary": { - this._log.debug(" -> creating bookmark \"" + command.data.title + "\""); - let URI = Utils.makeURI(command.data.URI); - newId = this._bms.insertBookmark(parentId, - URI, - command.data.index, - command.data.title); + this._log.debug(" -> creating bookmark \"" + record.cleartext.title + "\""); + let URI = Utils.makeURI(record.cleartext.URI); + newId = this._bms.insertBookmark(parentId, URI, record.sortindex, + record.cleartext.title); this._ts.untagURI(URI, null); - this._ts.tagURI(URI, command.data.tags); - this._bms.setKeywordForBookmark(newId, command.data.keyword); - if (command.data.description) { + this._ts.tagURI(URI, record.cleartext.tags); + this._bms.setKeywordForBookmark(newId, record.cleartext.keyword); + if (record.cleartext.description) { this._ans.setItemAnnotation(newId, "bookmarkProperties/description", - command.data.description, 0, + record.cleartext.description, 0, this._ans.EXPIRE_NEVER); } - if (command.data.type == "microsummary") { + if (record.cleartext.type == "microsummary") { this._log.debug(" \-> is a microsummary"); this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", - command.data.staticTitle || "", 0, this._ans.EXPIRE_NEVER); - let genURI = Utils.makeURI(command.data.generatorURI); + record.cleartext.staticTitle || "", 0, this._ans.EXPIRE_NEVER); + let genURI = Utils.makeURI(record.cleartext.generatorURI); if (this._ms == SERVICE_NOT_SUPPORTED) { this._log.warn("Can't create microsummary -- not supported."); } else { @@ -251,68 +218,68 @@ BookmarksStore.prototype = { } } break; case "folder": - this._log.debug(" -> creating folder \"" + command.data.title + "\""); + this._log.debug(" -> creating folder \"" + record.cleartext.title + "\""); newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); + record.cleartext.title, + record.sortindex); // If folder is an outgoing share, put the annotations on it: - if ( command.data.outgoingSharedAnno != undefined ) { + if ( record.cleartext.outgoingSharedAnno != undefined ) { this._ans.setItemAnnotation(newId, OUTGOING_SHARED_ANNO, - command.data.outgoingSharedAnno, + record.cleartext.outgoingSharedAnno, 0, this._ans.EXPIRE_NEVER); this._ans.setItemAnnotation(newId, SERVER_PATH_ANNO, - command.data.serverPathAnno, + record.cleartext.serverPathAnno, 0, this._ans.EXPIRE_NEVER); } break; case "livemark": - this._log.debug(" -> creating livemark \"" + command.data.title + "\""); + this._log.debug(" -> creating livemark \"" + record.cleartext.title + "\""); newId = this._ls.createLivemark(parentId, - command.data.title, - Utils.makeURI(command.data.siteURI), - Utils.makeURI(command.data.feedURI), - command.data.index); + record.cleartext.title, + Utils.makeURI(record.cleartext.siteURI), + Utils.makeURI(record.cleartext.feedURI), + record.sortindex); break; case "incoming-share": /* even though incoming shares are folders according to the * bookmarkService, _wrap() wraps them as type=incoming-share, so we * handle them separately, like so: */ - this._log.debug(" -> creating incoming-share \"" + command.data.title + "\""); + this._log.debug(" -> creating incoming-share \"" + record.cleartext.title + "\""); newId = this._bms.createFolder(parentId, - command.data.title, - command.data.index); + record.cleartext.title, + record.sortindex); this._ans.setItemAnnotation(newId, INCOMING_SHARED_ANNO, - command.data.incomingSharedAnno, + record.cleartext.incomingSharedAnno, 0, this._ans.EXPIRE_NEVER); this._ans.setItemAnnotation(newId, SERVER_PATH_ANNO, - command.data.serverPathAnno, + record.cleartext.serverPathAnno, 0, this._ans.EXPIRE_NEVER); break; case "separator": this._log.debug(" -> creating separator"); - newId = this._bms.insertSeparator(parentId, command.data.index); + newId = this._bms.insertSeparator(parentId, record.sortindex); break; default: - this._log.error("_createCommand: Unknown item type: " + command.data.type); + this._log.error("_create: Unknown item type: " + record.cleartext.type); break; } if (newId) { - this._log.trace("Setting GUID of new item " + newId + " to " + command.GUID); + this._log.trace("Setting GUID of new item " + newId + " to " + record.id); let cur = this._bms.getItemGUID(newId); - if (cur == command.GUID) - this._log.warn("Item " + newId + " already has GUID " + command.GUID); + if (cur == record.id) + this._log.warn("Item " + newId + " already has GUID " + record.id); else { - this._bms.setItemGUID(newId, command.GUID); - Engines.get("bookmarks")._tracker._all[newId] = command.GUID; // HACK - see tracker + this._bms.setItemGUID(newId, record.id); + Engines.get("bookmarks")._tracker._all[newId] = record.id; // HACK - see tracker } } }, @@ -353,80 +320,55 @@ BookmarksStore.prototype = { }, update: function BStore_update(record) { - let command = {GUID: record.id, data: record.cleartext}; + let itemId = this._getItemIdForGUID(record.id); - if (command.GUID == "menu" || - command.GUID == "toolbar" || - command.GUID == "unfiled") { - this._log.debug("Attempted to edit root node (" + command.GUID + - "). Skipping command."); + if (record.id == "menu" || + record.id == "toolbar" || + record.id == "unfiled") { + this._log.debug("Skipping update for root node."); return; } - - var itemId = this._getItemIdForGUID(command.GUID); if (itemId < 0) { - this._log.debug("Item for GUID " + command.GUID + " not found. Skipping."); + this._log.debug("Skipping update for unknown item: " + record.id); return; } - this._log.trace("Editing item " + itemId); - for (let key in command.data) { + this._log.trace("Updating " + record.id + " (" + itemId + ")"); + + if ((this._bms.getItemIndex(itemId) != record.sortindex) || + (this._bms.getFolderIdForItem(itemId) != + this._getItemIdForGUID(record.parentid))) { + this._log.trace("Moving item (changing folder/index)"); + let parentid = this._getItemIdForGUID(record.parentid); + this._bms.moveItem(itemId, parentid, record.sortindex); + } + + for (let key in record.cleartext) { switch (key) { - case "type": - // all commands have this to help in reconciliation, but it makes - // no sense to edit it - break; case "title": - this._bms.setItemTitle(itemId, command.data.title); + this._bms.setItemTitle(itemId, record.cleartext.title); break; case "URI": - this._bms.changeBookmarkURI(itemId, Utils.makeURI(command.data.URI)); + this._bms.changeBookmarkURI(itemId, Utils.makeURI(record.cleartext.URI)); break; - case "index": - let curIdx = this._bms.getItemIndex(itemId); - if (curIdx != command.data.index) { - // ignore index if we're going to move the item to another folder altogether - if (command.data.parentid && - (this._bms.getFolderIdForItem(itemId) != - this._getItemIdForGUID(command.data.parentid))) - break; - this._log.trace("Moving item (changing index)"); - this._bms.moveItem(itemId, this._bms.getFolderIdForItem(itemId), - command.data.index); - } - break; - case "parentid": { - if (command.data.parentid && - (this._bms.getFolderIdForItem(itemId) != - this._getItemIdForGUID(command.data.parentid))) { - this._log.trace("Moving item (changing folder)"); - let index = -1; - if (command.data.index && command.data.index >= 0) - index = command.data.index; - this._bms.moveItem(itemId, - this._getItemIdForGUID(command.data.parentid), index); - } - } break; case "tags": { // filter out null/undefined/empty tags - let tags = command.data.tags.filter(function(t) t); + let tags = record.cleartext.tags.filter(function(t) t); let tagsURI = this._bms.getBookmarkURI(itemId); this._ts.untagURI(tagsURI, null); this._ts.tagURI(tagsURI, tags); } break; case "keyword": - this._bms.setKeywordForBookmark(itemId, command.data.keyword); + this._bms.setKeywordForBookmark(itemId, record.cleartext.keyword); break; case "description": - if (command.data.description) { - this._ans.setItemAnnotation(itemId, "bookmarkProperties/description", - command.data.description, 0, - this._ans.EXPIRE_NEVER); - } + this._ans.setItemAnnotation(itemId, "bookmarkProperties/description", + record.cleartext.description, 0, + this._ans.EXPIRE_NEVER); break; case "generatorURI": { let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); - let genURI = Utils.makeURI(command.data.generatorURI); + let genURI = Utils.makeURI(record.cleartext.generatorURI); if (this._ms == SERVICE_NOT_SUPPORTED) { this._log.warn("Can't create microsummary -- not supported."); } else { @@ -435,29 +377,26 @@ BookmarksStore.prototype = { } } break; case "siteURI": - this._ls.setSiteURI(itemId, Utils.makeURI(command.data.siteURI)); + this._ls.setSiteURI(itemId, Utils.makeURI(record.cleartext.siteURI)); break; case "feedURI": - this._ls.setFeedURI(itemId, Utils.makeURI(command.data.feedURI)); + this._ls.setFeedURI(itemId, Utils.makeURI(record.cleartext.feedURI)); break; case "outgoingSharedAnno": this._ans.setItemAnnotation(itemId, OUTGOING_SHARED_ANNO, - command.data.outgoingSharedAnno, 0, + record.cleartext.outgoingSharedAnno, 0, this._ans.EXPIRE_NEVER); break; case "incomingSharedAnno": this._ans.setItemAnnotation(itemId, INCOMING_SHARED_ANNO, - command.data.incomingSharedAnno, 0, + record.cleartext.incomingSharedAnno, 0, this._ans.EXPIRE_NEVER); break; case "serverPathAnno": this._ans.setItemAnnotation(itemId, SERVER_PATH_ANNO, - command.data.serverPathAnno, 0, + record.cleartext.serverPathAnno, 0, this._ans.EXPIRE_NEVER); break; - default: - this._log.warn("Can't change item property: " + key); - break; } } }, @@ -490,16 +429,16 @@ BookmarksStore.prototype = { return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; }, - __wrap: function BSS___wrap(node, items, parentid, index, guidOverride) { + __wrap: function BSS___wrap(node, items, parentid, index, depth, guidOverride) { let GUID, item; // we override the guid for the root items, "menu", "toolbar", etc. if (guidOverride) { GUID = guidOverride; - item = {}; + item = {sortindex: index, depth: depth}; } else { GUID = this._bms.getItemGUID(node.itemId); - item = {parentid: parentid, index: index}; + item = {parentid: parentid, sortindex: index, depth: depth}; } if (node.type == node.RESULT_TYPE_FOLDER) { @@ -534,7 +473,7 @@ BookmarksStore.prototype = { } for (var i = 0; i < node.childCount; i++) { - this.__wrap(node.getChild(i), items, GUID, i); + this.__wrap(node.getChild(i), items, GUID, i, depth + 1); } } if (!guidOverride) @@ -601,47 +540,7 @@ BookmarksStore.prototype = { // helper _wrap: function BStore__wrap(node, items, rootName) { - return this.__wrap(node, items, null, null, rootName); - }, - - _wrapMountOutgoing: function BStore__wrapById( itemId ) { - let node = this._getNode(itemId); - if (node.type != node.RESULT_TYPE_FOLDER) - throw "Trying to wrap a non-folder mounted share"; - - let GUID = this._bms.getItemGUID(itemId); - let snapshot = {}; - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this.__wrap(node.getChild(i), snapshot, GUID, i); - } - - // remove any share mountpoints - for (let guid in snapshot) { - // TODO decide what to do with this... - if (snapshot[guid].type == "incoming-share") - delete snapshot[guid]; - } - return snapshot; - }, - - findIncomingShares: function BStore_findIncomingShares() { - /* Returns list of mount data structures, each of which - represents one incoming shared-bookmark folder. */ - let ret = []; - let a = this._ans.getItemsWithAnnotation(INCOMING_SHARED_ANNO, {}); - for (let i = 0; i < a.length; i++) { - /* The value of the incoming-shared annotation is the id of the - person who has shared it with us. Get that value: */ - let userId = this._ans.getItemAnnotation(a[i], INCOMING_SHARED_ANNO); - let node = this._getNode(a[i]); - let GUID = this._bms.getItemGUID(a[i]); - let path = this._ans.getItemAnnotation(a[i], SERVER_PATH_ANNO); - let dat = {rootGUID: GUID, userid: userId, serverPath: path, node: node}; - ret.push(dat); - } - return ret; + return this.__wrap(node, items, null, 0, 0, rootName); }, wrap: function BStore_wrap() { From 82a9092f7693f81594516d82d9ef7ba93c0bdd30 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 23 Dec 2008 12:17:40 -0800 Subject: [PATCH 0802/1860] bump version to 0.2.93 --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 1bf89182be5b..dd5d7a5c58dd 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,7 +43,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; -const WEAVE_VERSION = "0.2.92"; +const WEAVE_VERSION = "0.2.93"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 0ce571341ba6538f7c311be6f94f74c680df0635 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 23 Dec 2008 13:51:30 -0800 Subject: [PATCH 0803/1860] change serverURL to use 0.3 url schema --- services/sync/modules/engines.js | 3 ++- services/sync/modules/service.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3d5b8acb93b3..1eb74f4c5e8d 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -248,7 +248,8 @@ SyncEngine.prototype = { get baseURL() { let url = Utils.prefs.getCharPref("serverURL"); if (url && url[url.length-1] != '/') - url = url + '/'; + url += '/'; + url += "0.3/user/"; return url; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 03169b9ce8d7..65f6e1fcc5c0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -183,7 +183,8 @@ WeaveSvc.prototype = { get baseURL() { let url = Utils.prefs.getCharPref("serverURL"); if (url && url[url.length-1] != '/') - url = url + '/'; + url += '/'; + url += "0.3/user/"; return url; }, set baseURL(value) { From b32036fd84ed0a406d731281833cba8f9ef37bfa Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 26 Dec 2008 16:08:55 -0800 Subject: [PATCH 0804/1860] Bug 471076: explicitly check login succeeded --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 65f6e1fcc5c0..dc339e23ed43 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -419,6 +419,8 @@ WeaveSvc.prototype = { this._log.debug("Verifying login for user " + username); let res = new Resource(this.baseURL + username); yield res.get(self.cb); + if (res.data != "1") + throw "Login failed"; }, verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password) { this._localLock(this._notify("verify-login", "", this._verifyLogin, From 214da15ad399d85285e999fc5b2124191726713e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 26 Dec 2008 21:49:17 -0800 Subject: [PATCH 0805/1860] force GC, makes sync of many items possible on fennec --- services/sync/modules/engines.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 1eb74f4c5e8d..f36a3c2ca2a1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -413,9 +413,18 @@ SyncEngine.prototype = { newitems.sort = "depthindex"; yield newitems.get(self.cb); + let mem = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); this._lastSyncTmp = 0; let item; while ((item = yield newitems.iter.next(self.cb))) { + if (mem.isLowMemory()) { + this._log.warn("Low memory, forcing GC"); + Cu.forceGC(); + if (mem.isLowMemory()) { + this._log.warn("Low memory, aborting sync!"); + throw "Low memory"; + } + } yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); if (yield this._reconcile.async(this, self.cb, item)) yield this._applyIncoming.async(this, self.cb, item); From 0f6b8220a3763fbd09f432db966019aa0c31d493 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 26 Dec 2008 21:50:07 -0800 Subject: [PATCH 0806/1860] remove commented-out code --- services/sync/modules/engines/history.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 0b6ae45f7f54..65aecd708265 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -115,11 +115,7 @@ HistoryStore.prototype = { }, get _db() { - // FIXME - //if (fx3.0) - // return this._histDB_30; - //else - return this._hsvc.DBConnection; + return this._hsvc.DBConnection; }, _fetchRow: function HistStore__fetchRow(stm, params, retparams) { From 8ec19cbc2850352dd85b4597000ceb7370e16ea0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 27 Dec 2008 00:11:41 -0800 Subject: [PATCH 0807/1860] add some makefile magic for creating release & snapshot builds more easily --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index dd5d7a5c58dd..536ddbea9d6e 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,7 +43,7 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; -const WEAVE_VERSION = "0.2.93"; +const WEAVE_VERSION = "@weave_version@"; const STORAGE_FORMAT_VERSION = 3; const ENGINE_STORAGE_FORMAT_VERSION = 3; From 3ef26eff3a49ff214d8c05c87ddbeb0a77656d35 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 27 Dec 2008 12:15:04 -0800 Subject: [PATCH 0808/1860] print observer notifications to debug log --- services/sync/modules/wrap.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 72491ce8b85f..9ab894e3b89d 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -85,6 +85,7 @@ let Wrap = { let args = Array.prototype.slice.call(arguments); try { + this._log.debug("Event: " + this._osPrefix + savedName + ":start"); this._os.notifyObservers(null, this._osPrefix + savedName + ":start", savedPayload); this._os.notifyObservers(null, this._osPrefix + "global:start", savedPayload); @@ -93,10 +94,12 @@ let Wrap = { Async.run.apply(Async, args); ret = yield; + this._log.debug("Event: " + this._osPrefix + savedName + ":success"); this._os.notifyObservers(null, this._osPrefix + savedName + ":success", savedPayload); this._os.notifyObservers(null, this._osPrefix + "global:success", savedPayload); } catch (e) { + this._log.debug("Event: " + this._osPrefix + savedName + ":error"); this._os.notifyObservers(null, this._osPrefix + savedName + ":error", savedPayload); this._os.notifyObservers(null, this._osPrefix + "global:error", savedPayload); if (e != "Could not acquire lock") // FIXME HACK From f46b11e6d83d19ce3d52166d6231e25fe8d72d7a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 27 Dec 2008 12:15:26 -0800 Subject: [PATCH 0809/1860] half baked replace by title method --- services/sync/modules/notifications.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index 823aa0ba22f8..66816255f181 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -91,6 +91,16 @@ let Notifications = { } // XXX Should we notify observers about weave:notification:replaced? + }, + + // replaces all existing notifications with the same title as the new one + // FIXME not working? + replaceTitle: function Notifications_replaceTitle(notification) { + for each (let old in this.notifications) { + if (old.title == notification.title) + this.remove(old); + } + this.add(notification); } }; From cc6cf6bdd30604055612b6b65e0b824ba633237f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 27 Dec 2008 12:15:45 -0800 Subject: [PATCH 0810/1860] really actually fix login errors this time --- services/sync/modules/service.js | 77 +++++++++++++++++--------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index dc339e23ed43..8ad11901c3af 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -414,17 +414,18 @@ WeaveSvc.prototype = { // These are global (for all engines) - _verifyLogin: function WeaveSvc__verifyLogin(username, password) { - let self = yield; - this._log.debug("Verifying login for user " + username); - let res = new Resource(this.baseURL + username); - yield res.get(self.cb); - if (res.data != "1") - throw "Login failed"; - }, verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password) { - this._localLock(this._notify("verify-login", "", this._verifyLogin, - username, password)).async(this, onComplete); + let user = username, pass = password; + + let fn = function WeaveSvc__verifyLogin() { + let self = yield; + this._log.debug("Verifying login for user " + user); + let res = new Resource(this.baseURL + user); + yield res.get(self.cb); + if (res.data != "\"1\"") + throw "Login failed"; + }; + this._notify("verify-login", "", fn).async(this, onComplete); }, _verifyPassphrase: function WeaveSvc__verifyPassphrase(username, password, @@ -451,33 +452,36 @@ WeaveSvc.prototype = { async(this, onComplete); }, - _login: function WeaveSvc__login(username, password, passphrase) { - let self = yield; - - if (typeof(username) != 'undefined') - this.username = username; - if (typeof(password) != 'undefined') - ID.get('WeaveID').setTempPassword(password); - if (typeof(passphrase) != 'undefined') - ID.get('WeaveCryptoID').setTempPassword(passphrase); - - if (!this.username) - throw "No username set, login failed"; - if (!this.password) - throw "No password given or found in password manager"; - - this._log.debug("Logging in user " + this.username); - - yield this._verifyLogin.async(this, self.cb, this.username, this.password); - - this._loggedIn = true; - self.done(true); - }, login: function WeaveSvc_login(onComplete, username, password, passphrase) { + let user = username, pass = password, passp = passphrase; + + let fn = function WeaveSvc__login() { + let self = yield; + + this._loggedIn = false; + + if (typeof(user) != 'undefined') + this.username = user; + if (typeof(pass) != 'undefined') + ID.get('WeaveID').setTempPassword(pass); + if (typeof(passp) != 'undefined') + ID.get('WeaveCryptoID').setTempPassword(passp); + + if (!this.username) + throw "No username set, login failed"; + if (!this.password) + throw "No password given or found in password manager"; + + this._log.debug("Logging in user " + this.username); + + yield this.verifyLogin(self.cb, this.username, this.password); + + this._loggedIn = true; + self.done(true); + }; this._localLock( - this._notify("login", "", - this._catchAll(this._login, username, password, passphrase))). - async(this, onComplete); + this._catchAll( + this._notify("login", "", fn))).async(this, onComplete); }, logout: function WeaveSvc_logout() { @@ -549,6 +553,9 @@ WeaveSvc.prototype = { _sync: function WeaveSvc__sync() { let self = yield; + if (!this._loggedIn) + throw "aborting sync, not logged in"; + if (!(yield this._remoteSetup.async(this, self.cb))) { throw "aborting sync, remote setup failed"; } From 9b2e58c19098ea798f4bb04a1a54dabf93e3a085 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 28 Dec 2008 19:59:44 -0800 Subject: [PATCH 0811/1860] switch to generating individual bookmark records directly from the places store, without using a cache (to improve memory performance); create objects for bookmark types; temporarily disable history sync --- services/sync/modules/base_records/crypto.js | 1 - services/sync/modules/base_records/keys.js | 1 - services/sync/modules/base_records/wbo.js | 20 +- services/sync/modules/engines.js | 157 +-------- services/sync/modules/engines/bookmarks.js | 312 +++++++++--------- .../sync/modules/type_records/bookmark.js | 199 +++++++++++ 6 files changed, 381 insertions(+), 309 deletions(-) create mode 100644 services/sync/modules/type_records/bookmark.js diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 8639c1d45073..ce0aa07301bf 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -47,7 +47,6 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/keys.js"); diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index fa3d1f4bfc39..ff001fffead7 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -48,7 +48,6 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/base_records/wbo.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 9b2fd868d29e..243d703180fa 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -81,12 +81,20 @@ WBORecord.prototype = { this.data.modified = value; }, - get depth() this.data.depth, + get depth() { + if (this.data.depth) + return this.data.depth; + return 0; + }, set depth(value) { this.data.depth = value; }, - get sortindex() this.data.sortindex, + get sortindex() { + if (this.data.sortindex) + return this.data.sortindex; + return 0; + }, set sortindex(value) { this.data.sortindex = value; }, @@ -97,9 +105,11 @@ WBORecord.prototype = { }, toString: function WBORec_toString() { - return "{id: " + this.id + ", depth: " + this.depth + - ", sortindex: " + this.sortindex + ",\nmodified: " + this.modified + - ", payload: " + json.encode(this.cleartext) + "}"; + return "id: " + this.id + "\n" + + "parent: " + this.parentid + "\n" + + "depth: " + this.depth + ", index: " + this.sortindex + "\n" + + "modified: " + this.modified + "\n" + + "payload: " + json.encode(this.cleartext); } }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f36a3c2ca2a1..015e7e22ee22 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -287,37 +287,12 @@ SyncEngine.prototype = { return this._outgoing; }, - // Create a new record starting from an ID - // Calls _store.wrapItem to get the actual item, but sometimes needs to be - // overridden anyway (to alter parentid or other properties outside the payload) - _createRecord: function SyncEngine__createRecord(id, encrypt) { - let self = yield; - - let record = new CryptoWrapper(); + // Create a new record by querying the store, and add the engine metadata + _createRecord: function SyncEngine__createRecord(id) { + let record = this._store.createRecord(id); record.uri = this.engineURL + id; record.encryption = this.cryptoMetaURL; - record.cleartext = this._store.wrapItem(id); - - // XXX subclasses might not expect us to delete the payload fields - if (record.cleartext) { - if (record.cleartext.parentid) { - record.parentid = record.cleartext.parentid; - delete record.cleartext.parentid; - } - if (typeof(record.cleartext.depth) != "undefined") { - record.depth = record.cleartext.depth; - delete record.cleartext.depth; - } - if (typeof(record.cleartext.sortindex) != "undefined") { - record.sortindex = record.cleartext.sortindex; - delete record.cleartext.sortindex; - } - } - - if (encrypt || encrypt == undefined) - yield record.encrypt(self.cb, ID.get('WeaveCryptoID').password); - - self.done(record); + return record; }, // Check if a record is "like" another one, even though the IDs are different, @@ -389,16 +364,16 @@ SyncEngine.prototype = { // XXX should have a heuristic like this, but then we need to be able to // serialize each item by itself, something our stores can't currently do //if (this._tracker.changedIDs.length >= 30) - this._store.cacheItemsHint(); + //this._store.cacheItemsHint(); // NOTE we want changed items -> outgoing -> server to be as atomic as // possible, so we clear the changed IDs after we upload the changed records // NOTE2 don't encrypt, we'll do that before uploading instead for (let id in this._tracker.changedIDs) { - this.outgoing.push(yield this._createRecord.async(this, self.cb, id, false)); + this.outgoing.push(this._createRecord(id)); } - this._store.clearItemCacheHint(); + //this._store.clearItemCacheHint(); }, // Generate outgoing records @@ -525,7 +500,7 @@ SyncEngine.prototype = { let up = new Collection(this.engineURL); // regen the store cache so we can get item depths - this._store.cacheItemsHint(); + //this._store.cacheItemsHint(); let depth = {}; let out; @@ -541,7 +516,7 @@ SyncEngine.prototype = { for (let id in depth) { up.pushDepthRecord({id: id, depth: depth[id]}); } - this._store.clearItemCacheHint(); + //this._store.clearItemCacheHint(); // do the upload yield up.post(self.cb); @@ -597,117 +572,3 @@ SyncEngine.prototype = { yield all.delete(self.cb); } }; - -function BlobEngine() { - // subclasses should call _init - // we don't call it here because it requires serverPrefix to be set -} -BlobEngine.prototype = { - __proto__: Engine.prototype, - - get _profileID() { - return ClientData.GUID; - }, - - _init: function BlobEngine__init() { - // FIXME meep? - this.__proto__.__proto__.__proto__.__proto__._init.call(this); - this._keys = new Keychain(this.serverPrefix); - this._file = new Resource(this.serverPrefix + "data"); - this._file.pushFilter(new JsonFilter()); - this._file.pushFilter(new CryptoFilter(this.engineId)); - }, - - _initialUpload: function BlobEngine__initialUpload() { - let self = yield; - this._log.info("Initial upload to server"); - yield this._keys.initialize(self.cb, this.engineId); - this._file.data = {}; - yield this._merge.async(this, self.cb); - yield this._file.put(self.cb); - }, - - // NOTE: Assumes this._file has latest server data - // this method is separate from _sync so it's easy to override in subclasses - _merge: function BlobEngine__merge() { - let self = yield; - this._file.data[this._profileID] = this._store.wrap(); - }, - - // This engine is very simple: - // 1) Get the latest server data - // 2) Merge with our local data store - // 3) Upload new merged data - // NOTE: a version file will be needed in the future to optimize the case - // where there are no changes - _sync: function BlobEngine__sync() { - let self = yield; - - this._log.info("Beginning sync"); - this._os.notifyObservers(null, "weave:service:sync:engine:start", this.name); - - // FIXME - //if (!(yield DAV.MKCOL(this.serverPrefix, self.cb))) - // throw "Could not create remote folder"; - - try { - if ("none" != Utils.prefs.getCharPref("encryption")) - yield this._keys.getKeyAndIV(self.cb, this.engineId); - yield this._file.get(self.cb); - yield this._merge.async(this, self.cb); - yield this._file.put(self.cb); - - } catch (e if e.status == 404) { - yield this._initialUpload.async(this, self.cb); - } - - this._log.info("Sync complete"); - this._os.notifyObservers(null, "weave:service:sync:engine:success", this.name); - self.done(true); - } -}; - -function HeuristicEngine() { -} -HeuristicEngine.prototype = { - __proto__: new Engine(), - - get _remote() { - let remote = new RemoteStore(this); - this.__defineGetter__("_remote", function() remote); - return remote; - }, - - get _snapshot() { - let snap = new SnapshotStore(this.name); - this.__defineGetter__("_snapshot", function() snap); - return snap; - }, - - _resetServer: function SyncEngine__resetServer() { - let self = yield; - yield this._remote.wipe(self.cb); - }, - - _resetClient: function SyncEngine__resetClient() { - let self = yield; - this._log.debug("Resetting client state"); - this._snapshot.wipe(); - this._store.wipe(); - this._log.debug("Client reset completed successfully"); - }, - - _initialUpload: function HeuristicEngine__initialUpload() { - let self = yield; - this._log.info("Initial upload to server"); - this._snapshot.data = this._store.wrap(); - this._snapshot.version = 0; - this._snapshot.GUID = null; // in case there are other snapshots out there - yield this._remote.initialize(self.cb, this._snapshot); - this._snapshot.save(); - }, - - _sync: function HeuristicEngine__sync() { - let self = yield; - } -}; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d0dff2b8b2a8..b404ebecc5f4 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -66,6 +66,7 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/notifications.js"); Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/type_records/bookmark.js"); Function.prototype.async = Async.sugar; @@ -128,17 +129,18 @@ BookmarksStore.prototype = { return this.__ls; }, - __ms: null, get _ms() { - if (!this.__ms) - try { - this.__ms = Cc["@mozilla.org/microsummary/service;1"]. - getService(Ci.nsIMicrosummaryService); - } catch( e ) { - this.__ms = SERVICE_NOT_SUPPORTED; - this._log.warn("There is no Microsummary service."); - } - return this.__ms; + let ms; + try { + ms = Cc["@mozilla.org/microsummary/service;1"]. + getService(Ci.nsIMicrosummaryService); + } catch (e) { + ms = null; + this._log.warn("Could not load microsummary service"); + this._log.debug(e); + } + this.__defineGetter__("_ms", function() ms); + return ms; }, __ts: null, @@ -171,6 +173,28 @@ BookmarksStore.prototype = { return null; }, + _getWeaveIdForItem: function BStore__getWeaveIdForItem(placeId) { + if (placeId == this._bms.bookmarksMenuFolder) + return "menu"; + if (placeId == this._bms.toolbarFolder) + return "toolbar"; + if (placeId == this._bms.unfiledBookmarksFolder) + return "unfiled"; + return this._bms.getItemGUID(placeId); + }, + + _isToplevel: function BStore__isToplevel(placeId) { + if (placeId == this._bms.bookmarksMenuFolder) + return true; + if (placeId == this._bms.toolbarFolder) + return true; + if (placeId == this._bms.unfiledBookmarksFolder) + return true; + if (this._bms.getFolderIdForItem(placeId) < 0) + return true; + return false; + }, + _itemExists: function BStore__itemExists(id) { return this._getItemIdForGUID(id) >= 0; }, @@ -189,11 +213,11 @@ BookmarksStore.prototype = { case "bookmark": case "microsummary": { this._log.debug(" -> creating bookmark \"" + record.cleartext.title + "\""); - let URI = Utils.makeURI(record.cleartext.URI); - newId = this._bms.insertBookmark(parentId, URI, record.sortindex, + let uri = Utils.makeURI(record.cleartext.uri); + newId = this._bms.insertBookmark(parentId, uri, record.sortindex, record.cleartext.title); - this._ts.untagURI(URI, null); - this._ts.tagURI(URI, record.cleartext.tags); + this._ts.untagURI(uri, null); + this._ts.tagURI(uri, record.cleartext.tags); this._bms.setKeywordForBookmark(newId, record.cleartext.keyword); if (record.cleartext.description) { this._ans.setItemAnnotation(newId, "bookmarkProperties/description", @@ -210,7 +234,7 @@ BookmarksStore.prototype = { this._log.warn("Can't create microsummary -- not supported."); } else { try { - let micsum = this._ms.createMicrosummary(URI, genURI); + let micsum = this._ms.createMicrosummary(uri, genURI); this._ms.setMicrosummary(newId, micsum); } catch(ex) { /* ignore "missing local generator" exceptions */ } @@ -348,8 +372,8 @@ BookmarksStore.prototype = { case "title": this._bms.setItemTitle(itemId, record.cleartext.title); break; - case "URI": - this._bms.changeBookmarkURI(itemId, Utils.makeURI(record.cleartext.URI)); + case "uri": + this._bms.changeBookmarkURI(itemId, Utils.makeURI(record.cleartext.uri)); break; case "tags": { // filter out null/undefined/empty tags @@ -423,154 +447,134 @@ BookmarksStore.prototype = { Engines.get("bookmarks")._tracker._all[itemId] = newID; // HACK - see tracker }, - _getNode: function BSS__getNode(folder) { + _getNode: function BStore__getNode(folder) { let query = this._hsvc.getNewQuery(); query.setFolders([folder], 1); return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; }, - __wrap: function BSS___wrap(node, items, parentid, index, depth, guidOverride) { - let GUID, item; - - // we override the guid for the root items, "menu", "toolbar", etc. - if (guidOverride) { - GUID = guidOverride; - item = {sortindex: index, depth: depth}; - } else { - GUID = this._bms.getItemGUID(node.itemId); - item = {parentid: parentid, sortindex: index, depth: depth}; - } - - if (node.type == node.RESULT_TYPE_FOLDER) { - if (this._ls.isLivemark(node.itemId)) { - item.type = "livemark"; - let siteURI = this._ls.getSiteURI(node.itemId); - let feedURI = this._ls.getFeedURI(node.itemId); - item.siteURI = siteURI? siteURI.spec : ""; - item.feedURI = feedURI? feedURI.spec : ""; - } else if (this._ans.itemHasAnnotation(node.itemId, INCOMING_SHARED_ANNO)){ - /* When there's an incoming share, we just sync the folder itself - and the values of its annotations: NOT any of its contents. So - we'll wrap it as type=incoming-share, not as a "folder". */ - item.type = "incoming-share"; - item.title = node.title; - item.serverPathAnno = this._ans.getItemAnnotation(node.itemId, - SERVER_PATH_ANNO); - item.incomingSharedAnno = this._ans.getItemAnnotation(node.itemId, - INCOMING_SHARED_ANNO); - } else { - item.type = "folder"; - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - // If folder is an outgoing share, wrap its annotations: - if (this._ans.itemHasAnnotation(node.itemId, OUTGOING_SHARED_ANNO)) { - item.outgoingSharedAnno = this._ans.getItemAnnotation(node.itemId, - OUTGOING_SHARED_ANNO); - } - if (this._ans.itemHasAnnotation(node.itemId, SERVER_PATH_ANNO)) { - item.serverPathAnno = this._ans.getItemAnnotation(node.itemId, - SERVER_PATH_ANNO); - } - - for (var i = 0; i < node.childCount; i++) { - this.__wrap(node.getChild(i), items, GUID, i, depth + 1); - } - } - if (!guidOverride) - item.title = node.title; // no titles for root nodes - - } else if (node.type == node.RESULT_TYPE_URI || - node.type == node.RESULT_TYPE_QUERY) { - if (this._ms != SERVICE_NOT_SUPPORTED && - this._ms.hasMicrosummary(node.itemId)) { - item.type = "microsummary"; - let micsum = this._ms.getMicrosummary(node.itemId); - item.generatorURI = micsum.generator.uri.spec; // breaks local generators - item.staticTitle = ""; - try { - item.staticTitle = this._ans.getItemAnnotation(node.itemId, - "bookmarks/staticTitle"); - } catch (e) {} - } else if (node.type == node.RESULT_TYPE_QUERY) { - item.type = "query"; - item.title = node.title; - } else { - item.type = "bookmark"; - item.title = node.title; - } - - try { - item.description = - this._ans.getItemAnnotation(node.itemId, "bookmarkProperties/description"); - } catch (e) { - item.description = undefined; - } - - item.URI = node.uri; - - // This will throw if makeURI can't make an nsIURI object out of the - // node.uri string (or return null if node.uri is null), in which case - // we won't be able to get tags for the bookmark (but we'll still sync - // the rest of the record). - let uri; - try { - uri = Utils.makeURI(node.uri); - } - catch(e) { - this._log.error("error parsing URI string <" + node.uri + "> " + - "for item " + node.itemId + " (" + node.title + "): " + - e); - } - - if (uri) - item.tags = this._ts.getTagsForURI(uri, {}); - - item.keyword = this._bms.getKeywordForBookmark(node.itemId); - - } else if (node.type == node.RESULT_TYPE_SEPARATOR) { - item.type = "separator"; - - } else { - this._log.warn("Warning: unknown item type, cannot serialize: " + node.type); - return; - } - - items[GUID] = item; + // XXX a little inefficient - isToplevel calls getFolderIdForItem too + _itemDepth: function BStore__itemDepth(id) { + if (this._isToplevel(id)) + return 0; + return this._itemDepth(this._bms.getFolderIdForItem(id)) + 1; }, - // helper - _wrap: function BStore__wrap(node, items, rootName) { - return this.__wrap(node, items, null, 0, 0, rootName); + _getTags: function BStore__getTags(uri) { + try { + if (typeof(uri) == "string") + uri = Utils.makeURI(uri); + } catch(e) { + this._log.warn("Could not parse URI \"" + uri + "\": " + e); + } + return this._ts.getTagsForURI(uri, {}); }, - wrap: function BStore_wrap() { - var items = {}; - this._wrap(this._getNode(this._bms.bookmarksMenuFolder), items, "menu"); - this._wrap(this._getNode(this._bms.toolbarFolder), items, "toolbar"); - this._wrap(this._getNode(this._bms.unfiledBookmarksFolder), items, "unfiled"); - this._lookup = items; + _getDescription: function BStore__getDescription(id) { + try { + return this._ans.getItemAnnotation(id, "bookmarkProperties/description"); + } catch (e) { + return undefined; + } + }, + + _getStaticTitle: function BStore__getStaticTitle(id) { + try { + return this._ans.getItemAnnotation(id, "bookmarks/staticTitle"); + } catch (e) { + return ""; + } + }, + + // Create a record starting from the weave id (places guid) + // NOTE: the record id will not be set, because WBOs generate it from + // the URL, which we don't have here. The engine sets it. + createRecord: function BStore_createRecord(guid) { + let record; + + let placeId = this._bms.getItemIdForGUID(guid); + if (placeId < 0) { + record = new PlacesItem(); + record.cleartext = null; // deleted item + return record; + } + + switch (this._bms.getItemType(placeId)) { + case this._bms.TYPE_BOOKMARK: + if (this._ms && this._ms.hasMicrosummary(placeId)) { + record = new BookmarkMicsum(); + let micsum = this._ms.getMicrosummary(placeId); + record.generatorURI = micsum.generator.uri; // breaks local generators + record.staticTitle = this._getStaticTitle(placeId); + + } else { + record = new Bookmark(); + record.title = this._bms.getItemTitle(placeId); + } + + record.bmkUri = this._bms.getBookmarkURI(placeId); + record.tags = this._getTags(record.bmkUri); + record.keyword = this._bms.getKeywordForBookmark(placeId); + record.description = this._getDescription(placeId); + break; + + case this._bms.TYPE_FOLDER: + if (this._ls.isLivemark(placeId)) { + record = new Livemark(); + record.siteURI = this._ls.getSiteURI(placeId); + record.feedURI = this._ls.getFeedURI(placeId); + + } else { + record = new BookmarkFolder(); + record.title = this._bms.getItemTitle(placeId); + } + break; + + case this._bms.TYPE_SEPARATOR: + record = new BookmarkSeparator(); + break; + + case this._bms.TYPE_DYNAMIC_CONTAINER: + record = new PlacesItem(); + this._log.warn("Don't know how to serialize dynamic containers yet"); + break; + + default: + record = new PlacesItem(); + this._log.warn("Unknown item type, cannot serialize: " + + this._bms.getItemType(placeId)); + } + + record.parentid = this._getWeaveIdForItem(this._bms.getFolderIdForItem(placeId)); + record.depth = this._itemDepth(placeId); + record.sortindex = this._bms.getItemIndex(placeId); + + return record; + }, + + getAllIDs: function BStore_getAllIDs(node, items) { + if (!node) { + let items = {}; + this.getAllIDs(this._getNode(this._bms.bookmarksMenuFolder), items); + this.getAllIDs(this._getNode(this._bms.toolbarFolder), items); + this.getAllIDs(this._getNode(this._bms.unfiledBookmarksFolder), items); + return items; + } + + if (node.type == node.RESULT_TYPE_FOLDER && + !this._ls.isLivemark(node.itemId)) { + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); + node.containerOpen = true; + for (var i = 0; i < node.childCount; i++) { + let child = node.getChild(i); + items[this._bms.getItemGUID(child.itemId)] = {placesId: child.itemId}; + this.getAllIDs(child, items); + } + } + return items; }, - // FIXME: the fast path here is not so bad, specially since the engine always - // gives the cache hint atm. but wrapping all items to return just one - // (the slow path) is pretty bad - wrapItem: function BStore_wrapItem(id) { - if (this._itemCache) - return this._itemCache[id]; - let all = this.wrap(); - return all[id]; - }, - - // XXX need a better way to query Places for all GUIDs - getAllIDs: function BStore_getAllIDs() { - let all = this.wrap(); - delete all["unfiled"]; - delete all["toolbar"]; - delete all["menu"]; - return all; - }, - wipe: function BStore_wipe() { this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder); this._bms.removeFolderChildren(this._bms.toolbarFolder); @@ -630,7 +634,7 @@ BookmarksTracker.prototype = { // FIXME: very roundabout way of getting id -> guid mapping! let store = new BookmarksStore(); - let all = store.wrap(); + let all = store.getAllIDs(); this._all = {}; for (let guid in all) { this._all[this._bms.getItemIdForGUID(guid)] = guid; diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js new file mode 100644 index 000000000000..cc04adca9d59 --- /dev/null +++ b/services/sync/modules/type_records/bookmark.js @@ -0,0 +1,199 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['PlacesItem', 'Bookmark', 'BookmarkFolder', 'BookmarkMicsum', + 'Livemark', 'BookmarkSeparator']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/keys.js"); + +Function.prototype.async = Async.sugar; + +function PlacesItem(uri, authenticator) { + this._PlacesItem_init(uri, authenticator); +} +PlacesItem.prototype = { + __proto__: CryptoWrapper.prototype, + _logName: "Record.PlacesItem", + + _PlacesItem_init: function BmkItemRec_init(uri, authenticator) { + this._CryptoWrap_init(uri, authenticator); + this.cleartext = { + }; + }, + + get type() this.cleartext.type, + set type(value) { + // XXX check type is valid? + this.cleartext.type = value; + } +}; + +function Bookmark(uri, authenticator) { + this._Bookmark_init(uri, authenticator); +} +Bookmark.prototype = { + __proto__: PlacesItem.prototype, + _logName: "Record.Bookmark", + + _Bookmark_init: function BmkRec_init(uri, authenticator) { + this._PlacesItem_init(uri, authenticator); + this.cleartext.type = "bookmark"; + }, + + get title() this.cleartext.title, + set title(value) { + this.cleartext.title = value; + }, + + get bmkUri() this.cleartext.uri, + set bmkUri(value) { + if (typeof(value) == "string") + this.cleartext.uri = value; + else + this.cleartext.uri = value.spec; + }, + + get description() this.cleartext.description, + set description(value) { + this.cleartext.description = value; + }, + + get tags() this.cleartext.tags, + set tags(value) { + this.cleartext.tags = value; + }, + + get keyword() this.cleartext.keyword, + set keyword(value) { + this.cleartext.keyword = value; + } +}; + +function BookmarkMicsum(uri, authenticator) { + this._BookmarkMicsum_init(uri, authenticator); +} +BookmarkMicsum.prototype = { + __proto__: Bookmark.prototype, + _logName: "Record.BookmarkMicsum", + + _BookmarkMicsum_init: function BmkMicsumRec_init(uri, authenticator) { + this._Bookmark_init(uri, authenticator); + this.cleartext.type = "microsummary"; + }, + + get generatorURI() this.cleartext.generatorURI, + set generatorURI(value) { + if (typeof(value) == "string") + this.cleartext.generatorURI = value; + else + this.cleartext.generatorURI = value? value.spec : ""; + }, + + get staticTitle() this.cleartext.staticTitle, + set staticTitle(value) { + this.cleartext.staticTitle = value; + } +}; + +function BookmarkFolder(uri, authenticator) { + this._BookmarkFolder_init(uri, authenticator); +} +BookmarkFolder.prototype = { + __proto__: PlacesItem.prototype, + _logName: "Record.Folder", + + _BookmarkFolder_init: function FolderRec_init(uri, authenticator) { + this._PlacesItem_init(uri, authenticator); + this.cleartext.type = "folder"; + }, + + get title() this.cleartext.title, + set title(value) { + this.cleartext.title = value; + } +}; + +function Livemark(uri, authenticator) { + this._Livemark_init(uri, authenticator); +} +Livemark.prototype = { + __proto__: BookmarkFolder.prototype, + _logName: "Record.Livemark", + + _Livemark_init: function LvmkRec_init(uri, authenticator) { + this._BookmarkFolder_init(uri, authenticator); + this.cleartext.type = "livemark"; + }, + + get siteURI() this.cleartext.siteURI, + set siteURI(value) { + if (typeof(value) == "string") + this.cleartext.siteURI = value; + else + this.cleartext.siteURI = value? value.spec : ""; + }, + + get feedURI() this.cleartext.feedURI, + set feedURI(value) { + if (typeof(value) == "string") + this.cleartext.feedURI = value; + else + this.cleartext.feedURI = value? value.spec : ""; + } +}; + +function BookmarkSeparator(uri, authenticator) { + this._BookmarkSeparator_init(uri, authenticator); +} +BookmarkSeparator.prototype = { + __proto__: PlacesItem.prototype, + _logName: "Record.Separator", + + _BookmarkSeparator_init: function SepRec_init(uri, authenticator) { + this._PlacesItem_init(uri, authenticator); + this.cleartext.type = "separator"; + } +}; From aa2f5af5c796b7b0af5298bf63c9e90221ecc1e0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 29 Dec 2008 23:28:17 -0800 Subject: [PATCH 0812/1860] make engine keep less records in memory by limiting the outgoing queue to a maximum of 100 records, and fetch the rest from the store each time --- services/sync/modules/engines.js | 130 +++++++-------------- services/sync/modules/engines/bookmarks.js | 8 +- services/sync/modules/stores.js | 23 +--- 3 files changed, 48 insertions(+), 113 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 015e7e22ee22..185089f1c4d6 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -175,18 +175,6 @@ Engine.prototype = { this._log.debug("Engine initialized"); }, - _serializeCommands: function Engine__serializeCommands(commands) { - let json = this._json.encode(commands); - //json = json.replace(/ {action/g, "\n {action"); - return json; - }, - - _serializeConflicts: function Engine__serializeConflicts(conflicts) { - let json = this._json.encode(conflicts); - //json = json.replace(/ {action/g, "\n {action"); - return json; - }, - _resetServer: function Engine__resetServer() { let self = yield; throw "_resetServer needs to be subclassed"; @@ -272,18 +260,9 @@ SyncEngine.prototype = { Utils.prefs.setCharPref(this.name + ".lastSync", value); }, - // XXX these two should perhaps just be a variable inside sync(), but we have - // one or two other methods that use it - - get incoming() { - if (!this._incoming) - this._incoming = []; - return this._incoming; - }, - get outgoing() { if (!this._outgoing) - this._outgoing = []; + this._outgoing = {}; return this._outgoing; }, @@ -334,14 +313,6 @@ SyncEngine.prototype = { yield meta.addUnwrappedKey(self.cb, pubkey, symkey); yield meta.put(self.cb); } - this._tracker.disable(); - }, - - // Generate outgoing records - _generateOutgoing: function SyncEngine__generateOutgoing() { - let self = yield; - - this._log.debug("Calculating client changes"); // first sync special case: upload all items // note that we use a backdoor (of sorts) to the tracker and it @@ -359,21 +330,7 @@ SyncEngine.prototype = { } } - // generate queue from changed items list - - // XXX should have a heuristic like this, but then we need to be able to - // serialize each item by itself, something our stores can't currently do - //if (this._tracker.changedIDs.length >= 30) - //this._store.cacheItemsHint(); - - // NOTE we want changed items -> outgoing -> server to be as atomic as - // possible, so we clear the changed IDs after we upload the changed records - // NOTE2 don't encrypt, we'll do that before uploading instead - for (let id in this._tracker.changedIDs) { - this.outgoing.push(this._createRecord(id)); - } - - //this._store.clearItemCacheHint(); + this._tracker.disable(); // FIXME: need finer-grained ignoring }, // Generate outgoing records @@ -403,16 +360,14 @@ SyncEngine.prototype = { yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); if (yield this._reconcile.async(this, self.cb, item)) yield this._applyIncoming.async(this, self.cb, item); - else - this._log.debug("Skipping reconciled incoming item"); + else { + this._log.debug("Skipping reconciled incoming item " + item.id); + if (this._lastSyncTmp < item.modified) + this._lastSyncTmp = item.modified; + } } - if (typeof(this._lastSyncTmp) == "string") - this._lastSyncTmp = parseInt(this._lastSyncTmp); if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; - - // removes any holes caused by reconciliation above: - this._outgoing = this.outgoing.filter(function(n) n); }, // Reconciliation has two steps: @@ -431,42 +386,45 @@ SyncEngine.prototype = { let self = yield; let ret = true; - this._log.debug("Reconciling incoming item"); - // Check for the same item (same ID) on both incoming & outgoing queues - let conflicts = []; - for (let o = 0; o < this.outgoing.length; o++) { - if (!this.outgoing[o]) - continue; // skip previously removed items - if (item.id == this.outgoing[o].id) { - // Only consider it a conflict if there are actual differences - // otherwise, just ignore the outgoing record as well - if (!Utils.deepEquals(item.cleartext, this.outgoing[o].cleartext)) - conflicts.push({in: item, out: this.outgoing[o]}); - else - delete this.outgoing[o]; - - self.done(false); - return; + if (item.id in this._tracker.changedIDs) { + // Check to see if client and server were changed in the same way + let out = this._createRecord(item.id); + if (Utils.deepEquals(item.cleartext, out.cleartext)) { + this._tracker.removeChangedID(item.id); + delete this.outgoing[item.id]; + } else { + this._log.debug("Discarding server change due to conflict with local change"); } + self.done(false); + return; + } + + // Check for the incoming item's ID otherwise existing locally + if (this._store.itemExists(item.id)) { + self.done(true); + return; } - if (conflicts.length) - this._log.debug("Conflicts found. Conflicting server changes discarded"); // Check for items with different IDs which we think are the same one - for (let o = 0; o < this.outgoing.length; o++) { - if (!this.outgoing[o]) - continue; // skip previously removed items + for (let id in this._tracker.changedIDs) { + // Generate outgoing record or used a cached one + let out = (id in this.outgoing)? + this.outgoing[id] : this._createRecord(id); + + // cache the first 100, after that we will throw them away - slower but less memory hungry + if ([i for (i in this.outgoing)].length <= 100) + this.outgoing[id] = out; + + if (this._recordLike(item, out)) { + // change refs in outgoing queue, then actual id of local item + // XXX might it be better to just clear the outgoing queue? + yield this._changeRecordRefs.async(this, self.cb, id, item.id); + this._store.changeItemID(id, item.id); + + this._tracker.removeChangedID(item.id); + delete this.outgoing[item.id]; - if (this._recordLike(item, this.outgoing[o])) { - // change refs in outgoing queue - yield this._changeRecordRefs.async(this, self.cb, - this.outgoing[o].id, - item.id); - // change actual id of item - this._store.changeItemID(this.outgoing[o].id, - item.id); - delete this.outgoing[o]; self.done(false); return; } @@ -545,16 +503,8 @@ SyncEngine.prototype = { try { yield this._syncStartup.async(this, self.cb); - - // Populate outgoing queue - yield this._generateOutgoing.async(this, self.cb); - - // Fetch incoming records and apply them yield this._processIncoming.async(this, self.cb); - - // Upload outgoing records yield this._uploadOutgoing.async(this, self.cb); - yield this._syncFinish.async(this, self.cb); } catch (e) { diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index b404ebecc5f4..591370855038 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -195,7 +195,7 @@ BookmarksStore.prototype = { return false; }, - _itemExists: function BStore__itemExists(id) { + itemExists: function BStore_itemExists(id) { return this._getItemIdForGUID(id) >= 0; }, @@ -230,14 +230,14 @@ BookmarksStore.prototype = { this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", record.cleartext.staticTitle || "", 0, this._ans.EXPIRE_NEVER); let genURI = Utils.makeURI(record.cleartext.generatorURI); - if (this._ms == SERVICE_NOT_SUPPORTED) { - this._log.warn("Can't create microsummary -- not supported."); - } else { + if (this._ms) { try { let micsum = this._ms.createMicrosummary(uri, genURI); this._ms.setMicrosummary(newId, micsum); } catch(ex) { /* ignore "missing local generator" exceptions */ } + } else { + this._log.warn("Can't create microsummary -- not supported."); } } } break; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 84bab445ef9b..8890cf3cfa28 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -97,21 +97,6 @@ Store.prototype = { fn.async(this, onComplete, record); }, - itemExists: function Store_itemExists(id) { - if (!this._itemCache) - return this._itemExists(id); - - if (id in this._itemCache) - return true; - else - return false; - }, - - // subclasses probably want to override this one - _itemExists: function Store__itemExists(id) { - return false; - }, - cacheItemsHint: function Store_cacheItemsHint() { this._itemCache = this.wrap(); }, @@ -122,12 +107,12 @@ Store.prototype = { // override these in derived objects - wrap: function Store_wrap() { - throw "override wrap in a subclass"; + itemExists: function Store_itemExists(id) { + throw "override itemExists in a subclass"; }, - wrapItem: function Store_wrapItem() { - throw "override wrapItem in a subclass"; + createRecord: function Store_createRecord() { + throw "override createRecord in a subclass"; }, wrapDepth: function BStore_wrapDepth(guid, items) { From b247fa249cb3317d7149440b9b50d1fe058bbfcd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 30 Dec 2008 23:52:20 -0800 Subject: [PATCH 0813/1860] add a generic cache class for storing records during reconciliation. cache has 100 item limit, and is cleared before and after reconciliation so the OS can reclaim memory after GC --- services/sync/modules/engines.js | 59 +++++++++++-------- services/sync/modules/engines/bookmarks.js | 5 +- services/sync/modules/notifications.js | 8 +-- services/sync/modules/stores.js | 67 +++++++++++++++++++--- 4 files changed, 101 insertions(+), 38 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 185089f1c4d6..e1ad745ad935 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -233,6 +233,12 @@ function SyncEngine() { /* subclasses should call this._init() */ } SyncEngine.prototype = { __proto__: Engine.prototype, + get _memory() { + let mem = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); + this.__defineGetter__("_memory" function() mem); + return mem; + }, + get baseURL() { let url = Utils.prefs.getCharPref("serverURL"); if (url && url[url.length-1] != '/') @@ -295,6 +301,17 @@ SyncEngine.prototype = { } }, + _lowMemCheck: function SyncEngine__lowMemCheck() { + if (mem.isLowMemory()) { + this._log.warn("Low memory, forcing GC"); + Cu.forceGC(); + if (mem.isLowMemory()) { + this._log.warn("Low memory, aborting sync!"); + throw "Low memory"; + } + } + }, + // Any setup that needs to happen at the beginning of each sync. // Makes sure crypto records and keys are all set-up _syncStartup: function SyncEngine__syncStartup() { @@ -339,24 +356,23 @@ SyncEngine.prototype = { this._log.debug("Downloading & applying server changes"); + // enable cache, and keep only the first few items. Otherwise (when + // we have more outgoing items than can fit in the cache), we will + // keep rotating items in and out, perpetually getting cache misses + this._store.cache.enabled = true; + this._store.cache.fifo = false; // filo + this._store.cache.clear(); + let newitems = new Collection(this.engineURL); newitems.modified = this.lastSync; newitems.full = true; newitems.sort = "depthindex"; yield newitems.get(self.cb); - let mem = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); - this._lastSyncTmp = 0; let item; + this._lastSyncTmp = 0; while ((item = yield newitems.iter.next(self.cb))) { - if (mem.isLowMemory()) { - this._log.warn("Low memory, forcing GC"); - Cu.forceGC(); - if (mem.isLowMemory()) { - this._log.warn("Low memory, aborting sync!"); - throw "Low memory"; - } - } + this._lowMemCheck(); yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); if (yield this._reconcile.async(this, self.cb, item)) yield this._applyIncoming.async(this, self.cb, item); @@ -368,6 +384,8 @@ SyncEngine.prototype = { } if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; + + this._store.cache.clear(); // free some memory }, // Reconciliation has two steps: @@ -408,13 +426,7 @@ SyncEngine.prototype = { // Check for items with different IDs which we think are the same one for (let id in this._tracker.changedIDs) { - // Generate outgoing record or used a cached one - let out = (id in this.outgoing)? - this.outgoing[id] : this._createRecord(id); - - // cache the first 100, after that we will throw them away - slower but less memory hungry - if ([i for (i in this.outgoing)].length <= 100) - this.outgoing[id] = out; + let out = this._createRecord(id); if (this._recordLike(item, out)) { // change refs in outgoing queue, then actual id of local item @@ -456,25 +468,26 @@ SyncEngine.prototype = { // collection we'll upload let up = new Collection(this.engineURL); - - // regen the store cache so we can get item depths - //this._store.cacheItemsHint(); let depth = {}; - let out; - while ((out = this.outgoing.pop())) { + // don't cache the outgoing items, we won't need them later + this._store.cache.enabled = false; + + for (let id in this._tracker.changedIDs) { + let out = this._createRecord(id); this._log.trace("Outgoing:\n" + out); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); yield up.pushRecord(self.cb, out); this._store.wrapDepth(out.id, depth); } + this._store.cache.enabled = true; + // now add short depth-only records this._log.trace(depth.length + "outgoing depth records"); for (let id in depth) { up.pushDepthRecord({id: id, depth: depth[id]}); } - //this._store.clearItemCacheHint(); // do the upload yield up.post(self.cb); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 591370855038..6cab778eb004 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -490,7 +490,9 @@ BookmarksStore.prototype = { // NOTE: the record id will not be set, because WBOs generate it from // the URL, which we don't have here. The engine sets it. createRecord: function BStore_createRecord(guid) { - let record; + let record = this.cache.get(guid); + if (record) + return record; let placeId = this._bms.getItemIdForGUID(guid); if (placeId < 0) { @@ -549,6 +551,7 @@ BookmarksStore.prototype = { record.depth = this._itemDepth(placeId); record.sortindex = this._bms.getItemIndex(placeId); + this.cache.put(guid, record); return record; }, diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index 66816255f181..c3cda4fb6ab6 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -94,15 +94,11 @@ let Notifications = { }, // replaces all existing notifications with the same title as the new one - // FIXME not working? replaceTitle: function Notifications_replaceTitle(notification) { - for each (let old in this.notifications) { - if (old.title == notification.title) - this.remove(old); - } + this.notifications.filter(function(old) old.title == notification.title) + .forEach(function(old) this.remove(old), this); this.add(notification); } - }; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 8890cf3cfa28..eeb071414033 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -80,6 +80,12 @@ Store.prototype = { return this.__json; }, + get cache() { + let cache = new RecordCache(); + this.__defineGetter__("cache", function() cache); + return cache; + }, + _init: function Store__init() { this._log = Log4Moz.repository.getLogger("Store." + this._logName); }, @@ -97,14 +103,6 @@ Store.prototype = { fn.async(this, onComplete, record); }, - cacheItemsHint: function Store_cacheItemsHint() { - this._itemCache = this.wrap(); - }, - - clearItemCacheHint: function Store_clearItemCacheHint() { - this._itemCache = null; - }, - // override these in derived objects itemExists: function Store_itemExists(id) { @@ -140,6 +138,59 @@ Store.prototype = { } }; +function Cache() { + this.count = 0; + this.maxItems = 100; + this.fifo = true; + this.enabled = true; + this._head = this._tail = null; + this._items = {}; +} +Cache.prototype = { + _pop: function Cache__pop() { + if (this.count <= 0) + return; + if (this.count == 1) + this.clear(); + else { + delete this._items[this._tail.id]; + this._tail = this._tail.prev; + this.count--; + } + }, + put: function Cache_put(id, item) { + if (!this.enabled) + return; + let wrapper = {id: id, prev: null, next: this._head, item: item}; + this._items[wrapper.id] = wrapper; + + if (this.fifo) { + if (this._head) + this._head.prev = wrapper; + this._head = wrapper; + } else { + if (this._tail) + this._tail.next = wrapper; + this._tail = wrapper; + } + + this.count++; + if (this.count >= this.maxItems) + this._pop(); + }, + get: function Cache_get(id) { + if (id in this._items) + return this._items[id].item; + return undefined; + }, + clear: function Cache_clear() { + this.count = 0; + this._head = null; + this._tail = null; + this._items = {}; + } +}; + function SnapshotStore(name) { this._init(name); } From 6d41b1cbc917180ff56373feeecea8076afb7a71 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 30 Dec 2008 23:56:53 -0800 Subject: [PATCH 0814/1860] typo fix --- services/sync/modules/engines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e1ad745ad935..bf0e5adc0154 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -235,7 +235,7 @@ SyncEngine.prototype = { get _memory() { let mem = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); - this.__defineGetter__("_memory" function() mem); + this.__defineGetter__("_memory", function() mem); return mem; }, From a44b550606d20d843778aaa15a9d34c064fec891 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 13:20:19 -0800 Subject: [PATCH 0815/1860] improve WBORecord's toString() --- services/sync/modules/base_records/wbo.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 243d703180fa..bb3d5e5b5953 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -105,11 +105,11 @@ WBORecord.prototype = { }, toString: function WBORec_toString() { - return "id: " + this.id + "\n" + - "parent: " + this.parentid + "\n" + - "depth: " + this.depth + ", index: " + this.sortindex + "\n" + - "modified: " + this.modified + "\n" + - "payload: " + json.encode(this.cleartext); + return "{ id: " + this.id + "\n" + + " parent: " + this.parentid + "\n" + + " depth: " + this.depth + ", index: " + this.sortindex + "\n" + + " modified: " + this.modified + "\n" + + " payload: " + json.encode(this.cleartext) + " }"; } }; From cc31abe0738ae7f8616653333378eecbc68230c4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 13:35:40 -0800 Subject: [PATCH 0816/1860] limit json filter's debug output --- services/sync/modules/resource.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 899eb306e691..db0701da3fd3 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -287,6 +287,7 @@ ChannelListener.prototype = { function JsonFilter() { this._log = Log4Moz.repository.getLogger("Net.JsonFilter"); + this._log.level = Log4Moz.Level["Debug"]; } JsonFilter.prototype = { get _json() { From 4d37e0b259c67f5711da22d2453f4cd1a74b5168 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 13:36:28 -0800 Subject: [PATCH 0817/1860] fix cache typo. remove wrapDepth, it will be replaces with a different method --- services/sync/modules/stores.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index eeb071414033..b8d2516f3576 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -81,7 +81,7 @@ Store.prototype = { }, get cache() { - let cache = new RecordCache(); + let cache = new Cache(); this.__defineGetter__("cache", function() cache); return cache; }, @@ -113,18 +113,6 @@ Store.prototype = { throw "override createRecord in a subclass"; }, - wrapDepth: function BStore_wrapDepth(guid, items) { - if (typeof(items) == "undefined") - items = {}; - for (let childguid in this._itemCache) { - if (this._itemCache[childguid].parentid == guid) { - items[childguid] = this._itemCache[childguid].depth; - this.wrapDepth(childguid, items); - } - } - return items; - }, - changeItemID: function Store_changeItemID(oldID, newID) { throw "override changeItemID in a subclass"; }, From f579f793aa125701172570f09aa0a2478baf3b79 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 13:46:55 -0800 Subject: [PATCH 0818/1860] change pushDepthRecord to pushLiteral (takes an object instead of a resource; omits the resource upload filters step) --- services/sync/modules/base_records/collection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 3d9e651cc58a..506574215d74 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -132,8 +132,8 @@ Collection.prototype = { fn.async(this, onComplete, record); }, - pushDepthRecord: function Coll_pushDepthRecord(record) { - this._data.push(this._json.encode(record)); + pushLiteral: function Coll_pushLiteral(object) { + this._data.push(this._json.encode(object)); }, clearRecords: function Coll_clearRecords() { From e8341bb29a991c92082dc38cb43bfdb941c29484 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 13:49:19 -0800 Subject: [PATCH 0819/1860] add a _getWeaveParentIdForItem method; fix getAllIDs; add a createMetadataRecord method --- services/sync/modules/engines/bookmarks.js | 36 ++++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 6cab778eb004..df91b5114e4f 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -495,9 +495,9 @@ BookmarksStore.prototype = { return record; let placeId = this._bms.getItemIdForGUID(guid); - if (placeId < 0) { + if (placeId < 0) { // deleted item record = new PlacesItem(); - record.cleartext = null; // deleted item + record.cleartext = null; return record; } @@ -547,7 +547,7 @@ BookmarksStore.prototype = { this._bms.getItemType(placeId)); } - record.parentid = this._getWeaveIdForItem(this._bms.getFolderIdForItem(placeId)); + record.parentid = this._getWeaveParentIdForItem(placeId); record.depth = this._itemDepth(placeId); record.sortindex = this._bms.getItemIndex(placeId); @@ -555,12 +555,17 @@ BookmarksStore.prototype = { return record; }, - getAllIDs: function BStore_getAllIDs(node, items) { + _getWeaveParentIdForItem: function BStore__getWeaveParentIdForItem(itemId) { + return this._getWeaveIdForItem(this._bms.getFolderIdForItem(itemId)); + }, + + getAllIDs: function BStore_getAllIDs(node, depthIndex, items) { + if (typeof(items) == "undefined") + items = {}; if (!node) { - let items = {}; - this.getAllIDs(this._getNode(this._bms.bookmarksMenuFolder), items); - this.getAllIDs(this._getNode(this._bms.toolbarFolder), items); - this.getAllIDs(this._getNode(this._bms.unfiledBookmarksFolder), items); + this.getAllIDs(this._getNode(this._bms.bookmarksMenuFolder), depthIndex, items); + this.getAllIDs(this._getNode(this._bms.toolbarFolder), depthIndex, items); + this.getAllIDs(this._getNode(this._bms.unfiledBookmarksFolder), depthIndex, items); return items; } @@ -570,14 +575,25 @@ BookmarksStore.prototype = { node.containerOpen = true; for (var i = 0; i < node.childCount; i++) { let child = node.getChild(i); - items[this._bms.getItemGUID(child.itemId)] = {placesId: child.itemId}; - this.getAllIDs(child, items); + let foo = {id: this._bms.getItemGUID(child.itemId)}; + if (depthIndex) { + foo.depth = this._itemDepth(child.itemId); + foo.sortindex = this._bms.getItemIndex(child.itemId); + } + items[foo.id] = foo; + this.getAllIDs(child, depthIndex, items); } } return items; }, + createMetaRecords: function BStore_createMetaRecords(guid, items) { + let node = this._getNode(this._bms.getItemIdForGUID(guid)); + this.getAllIDs(node, true, items); + return items; + }, + wipe: function BStore_wipe() { this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder); this._bms.removeFolderChildren(this._bms.toolbarFolder); From 8025a250214a82eb8abb25a9d811611f618138a4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 13:51:38 -0800 Subject: [PATCH 0820/1860] remove 'outgoing' record cache (generate outgoing records at send time); clear the store cache and force a GC after reconciliation; push depth+index (metadata) records after regular (full) records; simplify + better comment reconciliation function; fix low mem check --- services/sync/modules/engines.js | 111 +++++++++++++------------------ 1 file changed, 46 insertions(+), 65 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index bf0e5adc0154..103b5b45c2fd 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -266,12 +266,6 @@ SyncEngine.prototype = { Utils.prefs.setCharPref(this.name + ".lastSync", value); }, - get outgoing() { - if (!this._outgoing) - this._outgoing = {}; - return this._outgoing; - }, - // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { let record = this._store.createRecord(id); @@ -293,19 +287,11 @@ SyncEngine.prototype = { return Utils.deepEquals(a.cleartext, b.cleartext); }, - _changeRecordRefs: function SyncEngine__changeRecordRefs(oldID, newID) { - let self = yield; - for each (let rec in this.outgoing) { - if (rec.parentid == oldID) - rec.parentid = newID; - } - }, - _lowMemCheck: function SyncEngine__lowMemCheck() { - if (mem.isLowMemory()) { + if (this._memory.isLowMemory()) { this._log.warn("Low memory, forcing GC"); Cu.forceGC(); - if (mem.isLowMemory()) { + if (this._memory.isLowMemory()) { this._log.warn("Low memory, aborting sync!"); throw "Low memory"; } @@ -332,21 +318,18 @@ SyncEngine.prototype = { } // first sync special case: upload all items - // note that we use a backdoor (of sorts) to the tracker and it - // won't save to disk this list + // NOTE: we use a backdoor (of sorts) to the tracker so it + // won't save to disk this list over and over if (!this.lastSync) { this._log.info("First sync, uploading all items"); - - // remove any old ones first this._tracker.clearChangedIDs(); - - // now add all current ones - let all = this._store.getAllIDs(); - for (let id in all) { - this._tracker.changedIDs[id] = true; - } + [i for (i in this._store.getAllIDs())] + .forEach(function(id) this._tracker.changedIDs[id] = true, this); } + let outnum = [i for (i in this._tracker.changedIDs)].length; + this._log.info(outnum + " outgoing items pre-reconciliation"); + this._tracker.disable(); // FIXME: need finer-grained ignoring }, @@ -377,7 +360,7 @@ SyncEngine.prototype = { if (yield this._reconcile.async(this, self.cb, item)) yield this._applyIncoming.async(this, self.cb, item); else { - this._log.debug("Skipping reconciled incoming item " + item.id); + this._log.trace("Skipping reconciled incoming item " + item.id); if (this._lastSyncTmp < item.modified) this._lastSyncTmp = item.modified; } @@ -385,58 +368,52 @@ SyncEngine.prototype = { if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; - this._store.cache.clear(); // free some memory + // try to free some memory + this._store.cache.clear(); + Cu.forceGC(); }, - // Reconciliation has two steps: + // Reconciliation has three steps: // 1) Check for the same item (same ID) on both the incoming and outgoing - // queues. This means the same item was modified on this profile and another - // at the same time. In this case, this client wins (which really means, the - // last profile you sync wins). - // 2) Check if any incoming & outgoing items are actually the same, even - // though they have different IDs. This happens when the same item is added - // on two different machines at the same time. For example, when a profile - // is synced for the first time after having (manually or otherwise) imported - // bookmarks imported, every bookmark will match this condition. - // When two items with different IDs are "the same" we change the local ID to - // match the remote one. + // queues. This means the same item was modified on this profile and + // another at the same time. In this case, this client wins (which really + // means, the last profile you sync wins). + // 2) Check if the incoming item's ID exists locally. In that case it's an + // update and we should not try a similarity check (step 3) + // 3) Check if any incoming & outgoing items are actually the same, even + // though they have different IDs. This happens when the same item is + // added on two different machines at the same time. It's also the common + // case when syncing for the first time two machines that already have the + // same bookmarks. In this case we change the IDs to match. _reconcile: function SyncEngine__reconcile(item) { let self = yield; let ret = true; - // Check for the same item (same ID) on both incoming & outgoing queues + // Step 1: check for conflicts if (item.id in this._tracker.changedIDs) { // Check to see if client and server were changed in the same way let out = this._createRecord(item.id); - if (Utils.deepEquals(item.cleartext, out.cleartext)) { + if (Utils.deepEquals(item.cleartext, out.cleartext)) this._tracker.removeChangedID(item.id); - delete this.outgoing[item.id]; - } else { + else this._log.debug("Discarding server change due to conflict with local change"); - } self.done(false); return; } - // Check for the incoming item's ID otherwise existing locally + // Step 2: check for updates if (this._store.itemExists(item.id)) { self.done(true); return; } - // Check for items with different IDs which we think are the same one + // Step 3: check for similar items for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); - if (this._recordLike(item, out)) { - // change refs in outgoing queue, then actual id of local item - // XXX might it be better to just clear the outgoing queue? - yield this._changeRecordRefs.async(this, self.cb, id, item.id); this._store.changeItemID(id, item.id); - this._tracker.removeChangedID(item.id); - delete this.outgoing[item.id]; - + this._store.cache.clear(); // because parentid refs will be wrong self.done(false); return; } @@ -447,7 +424,6 @@ SyncEngine.prototype = { // Apply incoming records _applyIncoming: function SyncEngine__applyIncoming(item) { let self = yield; - this._log.debug("Applying incoming record"); this._log.trace("Incoming:\n" + item); try { yield this._store.applyIncoming(self.cb, item); @@ -463,12 +439,12 @@ SyncEngine.prototype = { _uploadOutgoing: function SyncEngine__uploadOutgoing() { let self = yield; - if (this.outgoing.length) { - this._log.debug("Uploading client changes (" + this.outgoing.length + ")"); - + let outnum = [i for (i in this._tracker.changedIDs)].length; + this._log.debug("Preparing " + outnum + " outgoing records"); + if (outnum) { // collection we'll upload let up = new Collection(this.engineURL); - let depth = {}; + let meta = {}; // don't cache the outgoing items, we won't need them later this._store.cache.enabled = false; @@ -478,24 +454,29 @@ SyncEngine.prototype = { this._log.trace("Outgoing:\n" + out); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); yield up.pushRecord(self.cb, out); - this._store.wrapDepth(out.id, depth); + this._store.createMetaRecords(out.id, meta); } this._store.cache.enabled = true; - // now add short depth-only records - this._log.trace(depth.length + "outgoing depth records"); - for (let id in depth) { - up.pushDepthRecord({id: id, depth: depth[id]}); + // now add short depth-and-index-only records, except the ones we're + // sending as full records + let count = 0; + for each (let obj in meta) { + if (!obj.id in this._tracker.changedIDs) { + up.pushLiteral.push(obj); + count++; + } } + this._log.debug("Uploading client changes (" + outnum + ")"); + this._log.debug(count + " outgoing depth+index records"); + // do the upload yield up.post(self.cb); // save last modified date let mod = up.data.modified; - if (typeof(mod) == "string") - mod = parseInt(mod); if (mod > this.lastSync) this.lastSync = mod; } From 097cea491639535f1dec20da0137064c593860c2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 15:51:35 -0800 Subject: [PATCH 0821/1860] fix/further simplify reconciliation --- services/sync/modules/engines.js | 39 +++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 103b5b45c2fd..64ce638bd822 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -269,7 +269,10 @@ SyncEngine.prototype = { // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { let record = this._store.createRecord(id); + this._log.debug("setting uri to " + this.engineURL + id); record.uri = this.engineURL + id; + this._log.debug("uri set to " + record.uri); + this._log.debug("record id set to " + record.id); record.encryption = this.cryptoMetaURL; return record; }, @@ -373,6 +376,19 @@ SyncEngine.prototype = { Cu.forceGC(); }, + _isEqual: function SyncEngine__isEqual(item) { + let local = this._createRecord(item.id); + this._log.trace("Local record: \n" + local); + if (item.parentid == local.parentid && + Utils.deepEquals(item.cleartext, local.cleartext)) { + this._log.debug("Local record is the same"); + return true; + } else { + this._log.debug("Local record is different"); + return false; + } + }, + // Reconciliation has three steps: // 1) Check for the same item (same ID) on both the incoming and outgoing // queues. This means the same item was modified on this profile and @@ -389,25 +405,23 @@ SyncEngine.prototype = { let self = yield; let ret = true; - // Step 1: check for conflicts + // Step 1: Check for conflicts + // If same as local record, do not upload if (item.id in this._tracker.changedIDs) { - // Check to see if client and server were changed in the same way - let out = this._createRecord(item.id); - if (Utils.deepEquals(item.cleartext, out.cleartext)) + if (this._isEqual(item)) this._tracker.removeChangedID(item.id); - else - this._log.debug("Discarding server change due to conflict with local change"); self.done(false); return; } - // Step 2: check for updates + // Step 2: Check for updates + // If different from local record, apply server update if (this._store.itemExists(item.id)) { - self.done(true); + self.done(!this._isEqual(item)); return; } - // Step 3: check for similar items + // Step 3: Check for similar items for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); if (this._recordLike(item, out)) { @@ -454,7 +468,8 @@ SyncEngine.prototype = { this._log.trace("Outgoing:\n" + out); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); yield up.pushRecord(self.cb, out); - this._store.createMetaRecords(out.id, meta); + if (out.cleartext) // skip deleted records + this._store.createMetaRecords(out.id, meta); } this._store.cache.enabled = true; @@ -469,9 +484,7 @@ SyncEngine.prototype = { } } - this._log.debug("Uploading client changes (" + outnum + ")"); - this._log.debug(count + " outgoing depth+index records"); - + this._log.debug("Uploading " + outnum + " records + " + count + " index/depth records)"); // do the upload yield up.post(self.cb); From 0ac1218fb2dea348990d643e12bdebe4baf803d8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 16:16:38 -0800 Subject: [PATCH 0822/1860] fix tracker's onItemRemoved --- services/sync/modules/engines/bookmarks.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index df91b5114e4f..bddbb6fb5918 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -679,11 +679,12 @@ BookmarksTracker.prototype = { }, onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { + let guid = this._all[itemId]; delete this._all[itemId]; if (!this.enabled) return; this._log.trace("onItemRemoved: " + itemId); - this.addChangedID(this._all[itemId]); + this.addChangedID(guid); this._upScore(); }, From 16999e0a7d81fb76af08d4135693037ecbea98ac Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 16:17:09 -0800 Subject: [PATCH 0823/1860] warn when trying to add an undefined guid to the tracker's changes list --- services/sync/modules/trackers.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 8bd6c05ad5c9..56b5b1f84ad1 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -166,6 +166,10 @@ Tracker.prototype = { addChangedID: function T_addChangedID(id) { if (!this.enabled) return; + if (!id) { + this._log.warn("Attempted to add undefined ID to tracker"); + return; + } this._log.debug("Adding changed ID " + id); if (!this.changedIDs[id]) { this.changedIDs[id] = true; @@ -176,6 +180,10 @@ Tracker.prototype = { removeChangedID: function T_removeChangedID(id) { if (!this.enabled) return; + if (!id) { + this._log.warn("Attempted to remove undefined ID from tracker"); + return; + } this._log.debug("Removing changed ID " + id); if (this.changedIDs[id]) { delete this.changedIDs[id]; From 2ab4d0b90aa7851eabfa4e70a5a98989a0aa3ae1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 16:17:36 -0800 Subject: [PATCH 0824/1860] remove extra debug code --- services/sync/modules/engines.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 64ce638bd822..ad94a0e7e35e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -269,10 +269,7 @@ SyncEngine.prototype = { // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { let record = this._store.createRecord(id); - this._log.debug("setting uri to " + this.engineURL + id); record.uri = this.engineURL + id; - this._log.debug("uri set to " + record.uri); - this._log.debug("record id set to " + record.id); record.encryption = this.cryptoMetaURL; return record; }, From 91ddcc1c4d437a849e0430dcf4e7a3e9781e6554 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 17:27:45 -0800 Subject: [PATCH 0825/1860] consider sortindex when checking if a remote record is the same as a local one --- services/sync/modules/engines.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index ad94a0e7e35e..67fce1297e37 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -377,6 +377,7 @@ SyncEngine.prototype = { let local = this._createRecord(item.id); this._log.trace("Local record: \n" + local); if (item.parentid == local.parentid && + item.sortindex == local.sortindex && Utils.deepEquals(item.cleartext, local.cleartext)) { this._log.debug("Local record is the same"); return true; From 3ccdc7e05c03278906bb5a10b6b76f4db8add793 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 17:35:47 -0800 Subject: [PATCH 0826/1860] drop _isEqual debug statements to trace level --- services/sync/modules/engines.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 67fce1297e37..9300f472f1ee 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -379,10 +379,10 @@ SyncEngine.prototype = { if (item.parentid == local.parentid && item.sortindex == local.sortindex && Utils.deepEquals(item.cleartext, local.cleartext)) { - this._log.debug("Local record is the same"); + this._log.trace("Local record is the same"); return true; } else { - this._log.debug("Local record is different"); + this._log.trace("Local record is different"); return false; } }, From 80c51b0f0d523ef194bd4ce88797f40154f562be Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 19:35:23 -0800 Subject: [PATCH 0827/1860] extra debugging to catch getFolderIdForItem errors --- services/sync/modules/engines/bookmarks.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index bddbb6fb5918..766b54f4872f 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -190,8 +190,13 @@ BookmarksStore.prototype = { return true; if (placeId == this._bms.unfiledBookmarksFolder) return true; - if (this._bms.getFolderIdForItem(placeId) < 0) - return true; + try { + if (this._bms.getFolderIdForItem(placeId) < 0) + return true; + } catch (e) { + this._log.debug("Oops! Failed for place ID: " + placeId); + throw e; + } return false; }, From 11acc0e0bf71a77289c0e8e85e1f3fce29ee75c2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 2 Jan 2009 21:13:32 -0800 Subject: [PATCH 0828/1860] fix depth/index record generation --- services/sync/modules/engines.js | 8 ++-- services/sync/modules/engines/bookmarks.js | 47 +++++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9300f472f1ee..aa498873f6a9 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -464,10 +464,10 @@ SyncEngine.prototype = { for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); this._log.trace("Outgoing:\n" + out); - yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); - yield up.pushRecord(self.cb, out); if (out.cleartext) // skip deleted records this._store.createMetaRecords(out.id, meta); + yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); + yield up.pushRecord(self.cb, out); } this._store.cache.enabled = true; @@ -476,8 +476,8 @@ SyncEngine.prototype = { // sending as full records let count = 0; for each (let obj in meta) { - if (!obj.id in this._tracker.changedIDs) { - up.pushLiteral.push(obj); + if (!(obj.id in this._tracker.changedIDs)) { + up.pushLiteral(obj); count++; } } diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 766b54f4872f..dcd48209b285 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -564,15 +564,12 @@ BookmarksStore.prototype = { return this._getWeaveIdForItem(this._bms.getFolderIdForItem(itemId)); }, - getAllIDs: function BStore_getAllIDs(node, depthIndex, items) { + _getChildren: function BStore_getChildren(guid, depthIndex, items) { if (typeof(items) == "undefined") items = {}; - if (!node) { - this.getAllIDs(this._getNode(this._bms.bookmarksMenuFolder), depthIndex, items); - this.getAllIDs(this._getNode(this._bms.toolbarFolder), depthIndex, items); - this.getAllIDs(this._getNode(this._bms.unfiledBookmarksFolder), depthIndex, items); - return items; - } + let node = guid; // the recursion case + if (typeof(node) == "string") // callers will give us the guid as the first arg + node = this._getNode(this._getItemIdForGUID(guid)); if (node.type == node.RESULT_TYPE_FOLDER && !this._ls.isLivemark(node.itemId)) { @@ -586,16 +583,46 @@ BookmarksStore.prototype = { foo.sortindex = this._bms.getItemIndex(child.itemId); } items[foo.id] = foo; - this.getAllIDs(child, depthIndex, items); + this._getChildren(child, depthIndex, items); } } return items; }, + _getSiblings: function BStore__getSiblings(guid, depthIndex, items) { + if (typeof(items) == "undefined") + items = {}; + + let parentid = this._bms.getFolderIdForItem(this._getItemIdForGUID(guid)); + let parent = this._getNode(parentid); + parent.QueryInterface(Ci.nsINavHistoryQueryResultNode); + parent.containerOpen = true; + + for (var i = 0; i < parent.childCount; i++) { + let child = parent.getChild(i); + let foo = {id: this._bms.getItemGUID(child.itemId)}; + if (depthIndex) { + foo.depth = this._itemDepth(child.itemId); + foo.sortindex = this._bms.getItemIndex(child.itemId); + } + items[foo.id] = foo; + } + + return items; + }, + + getAllIDs: function BStore_getAllIDs() { + let items = {}; + this._getChildren(this._getNode(this._bms.bookmarksMenuFolder), true, items); + this._getChildren(this._getNode(this._bms.toolbarFolder), true, items); + this._getChildren(this._getNode(this._bms.unfiledBookmarksFolder), true, items); + return items; + }, + createMetaRecords: function BStore_createMetaRecords(guid, items) { - let node = this._getNode(this._bms.getItemIdForGUID(guid)); - this.getAllIDs(node, true, items); + this._getChildren(guid, true, items); + this._getSiblings(guid, true, items); return items; }, From 67882d690a4e593fc3d459b6d0a9a030d4d37548 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 3 Jan 2009 01:04:17 -0800 Subject: [PATCH 0829/1860] update history engine to work again with latest engine changes, and re-enable history sync --- services/sync/modules/engines/history.js | 118 +++++++++++------- services/sync/modules/type_records/history.js | 83 ++++++++++++ 2 files changed, 156 insertions(+), 45 deletions(-) create mode 100644 services/sync/modules/type_records/history.js diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 65aecd708265..d6ce1071d6d7 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -49,6 +49,7 @@ Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/type_records/history.js"); Function.prototype.async = Async.sugar; @@ -181,10 +182,10 @@ HistoryStore.prototype = { this._log.trace("Creating SQL statement: _urlStm"); let stm = this._db.createStatement( "SELECT * FROM " + - "(SELECT url FROM moz_places_temp WHERE id = :id LIMIT 1) " + + "(SELECT url,title FROM moz_places_temp WHERE id = :id LIMIT 1) " + "UNION ALL " + "SELECT * FROM ( " + - "SELECT url FROM moz_places WHERE id = :id " + + "SELECT url,title FROM moz_places WHERE id = :id " + "AND id NOT IN (SELECT id from moz_places_temp) " + "LIMIT 1 " + ") " + @@ -268,7 +269,8 @@ HistoryStore.prototype = { if (!this._urlStm.step()) return null; - return this._urlStm.row.url; + return {url: this._urlStm.row.url, + title: this._urlStm.row.title}; } finally { this._annoAttrIdStm.reset(); this._findPidByAnnoStm.reset(); @@ -277,41 +279,12 @@ HistoryStore.prototype = { }, changeItemID: function HStore_changeItemID(oldID, newID) { - let uri = Utils.makeURI(this._findURLByGUID(oldID)); + let uri = Utils.makeURI(this._findURLByGUID(oldID).uri); this._anno.setPageAnnotation(uri, "weave/guid", newID, 0, 0); }, - // XXX need a better way to query Places for all GUIDs - getAllIDs: function BStore_getAllIDs() { - let all = this.wrap(); - return all; - }, - create: function HistStore_create(record) { - this._log.debug(" -> creating history entry: " + record.cleartext.URI); - - let uri = Utils.makeURI(record.cleartext.URI); - - let visit; - while ((visit = record.cleartext.visits.pop())) { - this._log.debug(" visit " + visit.date); - this._hsvc.addVisit(uri, visit.date, null, visit.type, - (visit.type == 5 || visit.type == 6), 0); - } - this._hsvc.setPageTitle(uri, record.cleartext.title); - }, - - remove: function HistStore_remove(record) { - this._log.trace(" -> NOT removing history entry: " + record.id); - // FIXME: implement! - }, - - update: function HistStore_update(record) { - this._log.trace(" -> FIXME: NOT editing history entry: " + record.id); - // FIXME: implement! - }, - - wrap: function HistStore_wrap() { + getAllIDs: function HistStore_getAllIDs() { let query = this._hsvc.getNewQuery(), options = this._hsvc.getNewQueryOptions(); @@ -328,20 +301,65 @@ HistoryStore.prototype = { for (let i = 0; i < root.childCount; i++) { let item = root.getChild(i); let guid = this._getGUID(item.uri); - items[guid] = {title: item.title, - URI: item.uri, - visits: this._getVisits(item.uri)}; + items[guid] = item.uri; } - return items; }, - wrapItem: function HistStore_wrapItem(id) { - // FIXME: use findURLByGUID! (not so important b/c of cache hints though) - if (this._itemCache) - return this._itemCache[id]; - let all = this._wrap(); - return all[id]; + create: function HistStore_create(record) { + this._log.debug(" -> creating history entry: " + record.cleartext.uri); + + let uri = Utils.makeURI(record.cleartext.uri); + let visit; + while ((visit = record.cleartext.visits.pop())) { + this._log.debug(" visit " + visit.date); + this._hsvc.addVisit(uri, visit.date, null, visit.type, + (visit.type == 5 || visit.type == 6), 0); + } + this._hsvc.setPageTitle(uri, record.cleartext.title); + }, + + remove: function HistStore_remove(record) { + this._log.trace(" -> NOT removing history entry: " + record.id); + // FIXME: implement! + }, + + // FIXME: skip already-existing visits, places will add duplicates... + update: function HistStore_update(record) { + this._log.trace(" -> editing history entry: " + record.cleartext.uri); + let uri = Utils.makeURI(record.cleartext.uri); + let visit; + while ((visit = record.cleartext.visits.pop())) { + this._log.debug(" visit " + visit.date); + this._hsvc.addVisit(uri, visit.date, null, visit.type, + (visit.type == 5 || visit.type == 6), 0); + } + this._hsvc.setPageTitle(uri, record.cleartext.title); + }, + + itemExists: function HistStore_itemExists(id) { + if (this._findURLByGUID(id)) + return true; + return false; + }, + + createRecord: function HistStore_createRecord(guid) { + let foo = this._findURLByGUID(guid); + let record = new HistoryRec(); + if (foo) { + record.histUri = foo.url; + record.title = foo.title; + record.visits = this._getVisits(record.histUri); + } else { + record.cleartext = null; // deleted item + } + this.cache.put(guid, record); + return record; + }, + + // no depth or index for history + createMetaRecords: function HistStore_createMetaRecords(guid, items) { + return {}; }, wipe: function HistStore_wipe() { @@ -374,6 +392,16 @@ HistoryTracker.prototype = { _init: function HT__init() { this.__proto__.__proto__._init.call(this); + + // FIXME: very roundabout way of getting url -> guid mapping! + // FIXME2: not updated after startup + let store = new HistoryStore(); + let all = store.getAllIDs(); + this._all = {}; + for (let guid in all) { + this._all[all[guid]] = guid; + } + this._hsvc.addObserver(this, false); }, @@ -403,7 +431,7 @@ HistoryTracker.prototype = { }, onDeleteURI: function HT_onDeleteURI(uri) { this._log.trace("onDeleteURI: " + uri.spec); - this.addChangedID(this._store._getGUID(uri)); // FIXME eek + this.addChangedID(this._all[uri]); this._upScore(); }, onClearHistory: function HT_onClearHistory() { diff --git a/services/sync/modules/type_records/history.js b/services/sync/modules/type_records/history.js new file mode 100644 index 000000000000..7d8c4abd2fbd --- /dev/null +++ b/services/sync/modules/type_records/history.js @@ -0,0 +1,83 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['HistoryRec']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/keys.js"); + +Function.prototype.async = Async.sugar; + +function HistoryRec(uri, authenticator) { + this._HistoryRec_init(uri, authenticator); +} +HistoryRec.prototype = { + __proto__: CryptoWrapper.prototype, + _logName: "Record.History", + + _HistoryRec_init: function HistItem_init(uri, authenticator) { + this._CryptoWrap_init(uri, authenticator); + this.cleartext = { + }; + }, + + get histUri() this.cleartext.uri, + set histUri(value) { + if (typeof(value) == "string") + this.cleartext.uri = value; + else + this.cleartext.uri = value.spec; + }, + + get title() this.cleartext.title, + set title(value) { + this.cleartext.title = value; + }, + + get visits() this.cleartext.visits, + set visits(value) { + this.cleartext.visits = value; + } +}; From 7d7d9e80a4c5c64f2af615c3a6ab3a38cd854ee4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 3 Jan 2009 01:40:22 -0800 Subject: [PATCH 0830/1860] partially re-enable client data (guid,name,type) --- services/sync/modules/clientData.js | 86 +++++++++---------- services/sync/modules/service.js | 6 +- services/sync/modules/type_records/clients.js | 71 +++++++++++++++ 3 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 services/sync/modules/type_records/clients.js diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js index 9066915d12e1..904440022e37 100644 --- a/services/sync/modules/clientData.js +++ b/services/sync/modules/clientData.js @@ -46,7 +46,9 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/type_records/clientData.js"); Function.prototype.async = Async.sugar; @@ -56,6 +58,24 @@ function ClientDataSvc() { this._init(); } ClientDataSvc.prototype = { + name: "clients", + + _init: function ClientData__init() { + this._log = Log4Moz.repository.getLogger("Service.ClientData"); + }, + + get baseURL() { + let url = Utils.prefs.getCharPref("serverURL"); + if (url && url[url.length-1] != '/') + url += '/'; + url += "0.3/user/"; + return url; + }, + + get engineURL() { + return this.baseURL + ID.get('WeaveID').username + '/' + this.name + '/'; + }, + get GUID() { return this._getCharPref("client.GUID", function() Utils.makeGUID()); }, @@ -77,10 +97,6 @@ ClientDataSvc.prototype = { Utils.prefs.setCharPref("client.type", value); }, - clients: function ClientData__clients() { - return this._remote.data; - }, - _getCharPref: function ClientData__getCharPref(pref, defaultCb) { let value; try { @@ -92,49 +108,25 @@ ClientDataSvc.prototype = { return value; }, - _init: function ClientData__init() { - this._log = Log4Moz.repository.getLogger("Service.ClientData"); - this._remote = new Resource("meta/clients"); - this._remote.pushFilter(new JsonFilter()); - }, - - _wrap: function ClientData__wrap() { - return { - GUID: this.GUID, - name: this.name, - type: this.type - }; - }, - - _refresh: function ClientData__refresh() { - let self = yield; - - // No more such thing as DAV. I think this is making a directory. - // Will the directory now be handled automatically? (YES) - /* let ret = yield DAV.MKCOL("meta", self.cb); - if(!ret) - throw "Could not create meta information directory";*/ - - // This fails horribly because this._remote._uri.spec is null. What's - // that mean? - // Spec is supposed to be just a string? - - // Probably problem has to do with Resource getting intialized by - // relative, not absolute, path? - // Use WBORecord (Weave Basic Object) from wbo.js? - this._log.debug("The URI is " + this._remote._uri); - this._log.debug("The URI.spec is " + this._remote._uri.spec); - try { yield this._remote.get(self.cb); } - catch (e if e.status == 404) { - this._log.debug("404ed. Using empty for remote data."); - this._remote.data = {}; - } - - this._remote.data[this.GUID] = this._wrap(); - yield this._remote.put(self.cb); - this._log.debug("Successfully downloaded clients file from server"); - }, refresh: function ClientData_refresh(onComplete) { - this._refresh.async(this, onComplete); + let fn = function() { + let self = yield; + + let record = new ClientRec(); + record.uri = this.engineURL + this.GUID; + try { + yield record.get(self.cb); + } catch (e) { + // error? first-time upload? + // payload will be undefined, reset it so put below will work + record = new ClientRec(); + record.uri = this.engineURL + this.GUID; + } + + record.name = this.name; + record.type = this.type; + yield record.put(self.cb); + }; + fn.async(this, onComplete); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8ad11901c3af..2877891c4c96 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -559,9 +559,9 @@ WeaveSvc.prototype = { if (!(yield this._remoteSetup.async(this, self.cb))) { throw "aborting sync, remote setup failed"; } - // TODO This right here. Make sure it works. - //this._log.debug("Refreshing client list"); - //yield ClientData.refresh(self.cb); + + this._log.debug("Refreshing client list"); + yield ClientData.refresh(self.cb); let engines = Engines.getAll(); for each (let engine in engines) { diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js new file mode 100644 index 000000000000..afe000669eda --- /dev/null +++ b/services/sync/modules/type_records/clients.js @@ -0,0 +1,71 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['ClientRec']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/wbo.js"); + +Function.prototype.async = Async.sugar; + +function ClientRec(uri, authenticator) { + this._ClientRec_init(uri, authenticator); +} +ClientRec.prototype = { + __proto__: WBORecord.prototype, + _logName: "Record.Client", + + _ClientRec_init: function ClientRec_init(uri, authenticator) { + this._WBORec_init(uri, authenticator); + }, + + get name() this.payload.name, + set name(value) { + this.payload.name = value; + }, + + get type() this.payload.type, + set type(value) { + this.payload.type = value; + } +}; From b89469944f6ba25bc34c66e7520b5a27b772f537 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 6 Jan 2009 13:54:18 -0800 Subject: [PATCH 0831/1860] add clients sync (list of clients with names and types); make it simpler to create Engine subclasses; remove the 2nd 'authenticator' parameter for Resources; resetServer is now wipeServer; consider 0 an invalid place ID (fixes bug when calculating item depths); add a Svc global exported from utils.js where commonly used services can live --- .../sync/modules/base_records/collection.js | 11 +- services/sync/modules/base_records/crypto.js | 16 +- services/sync/modules/base_records/keys.js | 16 +- services/sync/modules/base_records/wbo.js | 8 +- services/sync/modules/clientData.js | 132 ----------- services/sync/modules/engines.js | 120 +++------- services/sync/modules/engines/bookmarks.js | 73 ++---- services/sync/modules/engines/clients.js | 208 ++++++++++++++++++ services/sync/modules/engines/history.js | 30 +-- services/sync/modules/faultTolerance.js | 2 + services/sync/modules/resource.js | 12 +- services/sync/modules/service.js | 161 ++++++++------ services/sync/modules/stores.js | 7 + .../sync/modules/type_records/bookmark.js | 48 ++-- services/sync/modules/type_records/clients.js | 22 +- services/sync/modules/type_records/history.js | 8 +- services/sync/modules/util.js | 39 +++- services/sync/tests/unit/test_auth_manager.js | 2 +- 18 files changed, 475 insertions(+), 440 deletions(-) delete mode 100644 services/sync/modules/clientData.js create mode 100644 services/sync/modules/engines/clients.js diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 506574215d74..3f69b594ff5c 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -55,15 +55,16 @@ Cu.import("resource://weave/base_records/keys.js"); Function.prototype.async = Async.sugar; -function Collection(uri, authenticator) { - this._Coll_init(uri, authenticator); +function Collection(uri, recordObj) { + this._Coll_init(uri); + this._recordObj = recordObj; } Collection.prototype = { __proto__: Resource.prototype, _logName: "Collection", - _Coll_init: function Coll_init(uri, authenticator) { - this._init(uri, authenticator); + _Coll_init: function Coll_init(uri) { + this._init(uri); this.pushFilter(new JsonFilter()); this._full = true; this._modified = 0; @@ -161,7 +162,7 @@ CollectionIterator.prototype = { if (this._idx >= this.count) return; let item = this._coll.data[this._idx++]; - let wrap = new CryptoWrapper(this._coll.uri.resolve(item.id)); + let wrap = new this._coll._recordObj(this._coll.uri.resolve(item.id)); wrap.data = this._coll._json.encode(item); // HACK HACK HACK yield wrap.filterDownload(self.cb); // XXX EEK diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index ce0aa07301bf..ba0fe43a3aae 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -58,17 +58,17 @@ Utils.lazy(this, 'CryptoMetas', RecordManager); let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"].createInstance(Ci.IWeaveCrypto); -function CryptoWrapper(uri, authenticator) { - this._CryptoWrap_init(uri, authenticator); +function CryptoWrapper(uri) { + this._CryptoWrap_init(uri); } CryptoWrapper.prototype = { __proto__: WBORecord.prototype, _logName: "Record.CryptoWrapper", - _CryptoWrap_init: function CryptoWrap_init(uri, authenticator) { + _CryptoWrap_init: function CryptoWrap_init(uri) { // FIXME: this will add a json filter, meaning our payloads will be json // encoded, even though they are already a string - this._WBORec_init(uri, authenticator); + this._WBORec_init(uri); this.data.payload = { encryption: "", ciphertext: null @@ -127,15 +127,15 @@ CryptoWrapper.prototype = { } }; -function CryptoMeta(uri, authenticator) { - this._CryptoMeta_init(uri, authenticator); +function CryptoMeta(uri) { + this._CryptoMeta_init(uri); } CryptoMeta.prototype = { __proto__: WBORecord.prototype, _logName: "Record.CryptoMeta", - _CryptoMeta_init: function CryptoMeta_init(uri, authenticator) { - this._WBORec_init(uri, authenticator); + _CryptoMeta_init: function CryptoMeta_init(uri) { + this._WBORec_init(uri); this.data.payload = { bulkIV: null, keyring: {} diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index ff001fffead7..a627812e44d5 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -55,15 +55,15 @@ Function.prototype.async = Async.sugar; Utils.lazy(this, 'PubKeys', PubKeyManager); Utils.lazy(this, 'PrivKeys', PrivKeyManager); -function PubKey(uri, authenticator) { - this._PubKey_init(uri, authenticator); +function PubKey(uri) { + this._PubKey_init(uri); } PubKey.prototype = { __proto__: WBORecord.prototype, _logName: "Record.PubKey", - _PubKey_init: function PubKey_init(uri, authenticator) { - this._WBORec_init(uri, authenticator); + _PubKey_init: function PubKey_init(uri) { + this._WBORec_init(uri); this.payload = { type: "pubkey", key_data: null, @@ -95,15 +95,15 @@ PubKey.prototype = { } }; -function PrivKey(uri, authenticator) { - this._PrivKey_init(uri, authenticator); +function PrivKey(uri) { + this._PrivKey_init(uri); } PrivKey.prototype = { __proto__: WBORecord.prototype, _logName: "Record.PrivKey", - _PrivKey_init: function PrivKey_init(uri, authenticator) { - this._WBORec_init(uri, authenticator); + _PrivKey_init: function PrivKey_init(uri) { + this._WBORec_init(uri); this.payload = { type: "privkey", salt: null, diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index bb3d5e5b5953..e2046c8eaeac 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -47,15 +47,15 @@ Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; -function WBORecord(uri, authenticator) { - this._WBORec_init(uri, authenticator); +function WBORecord(uri) { + this._WBORec_init(uri); } WBORecord.prototype = { __proto__: Resource.prototype, _logName: "Record.WBO", - _WBORec_init: function WBORec_init(uri, authenticator) { - this._init(uri, authenticator); + _WBORec_init: function WBORec_init(uri) { + this._init(uri); this.pushFilter(new WBOFilter()); this.pushFilter(new JsonFilter()); this.data = { diff --git a/services/sync/modules/clientData.js b/services/sync/modules/clientData.js deleted file mode 100644 index 904440022e37..000000000000 --- a/services/sync/modules/clientData.js +++ /dev/null @@ -1,132 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Weave. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Chris Beard - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['ClientData']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/type_records/clientData.js"); - -Function.prototype.async = Async.sugar; - -Utils.lazy(this, 'ClientData', ClientDataSvc); - -function ClientDataSvc() { - this._init(); -} -ClientDataSvc.prototype = { - name: "clients", - - _init: function ClientData__init() { - this._log = Log4Moz.repository.getLogger("Service.ClientData"); - }, - - get baseURL() { - let url = Utils.prefs.getCharPref("serverURL"); - if (url && url[url.length-1] != '/') - url += '/'; - url += "0.3/user/"; - return url; - }, - - get engineURL() { - return this.baseURL + ID.get('WeaveID').username + '/' + this.name + '/'; - }, - - get GUID() { - return this._getCharPref("client.GUID", function() Utils.makeGUID()); - }, - set GUID(value) { - Utils.prefs.setCharPref("client.GUID", value); - }, - - get name() { - return this._getCharPref("client.name", function() "Firefox"); - }, - set GUID(value) { - Utils.prefs.setCharPref("client.name", value); - }, - - get type() { - return this._getCharPref("client.type", function() "desktop"); - }, - set GUID(value) { - Utils.prefs.setCharPref("client.type", value); - }, - - _getCharPref: function ClientData__getCharPref(pref, defaultCb) { - let value; - try { - value = Utils.prefs.getCharPref(pref); - } catch (e) { - value = defaultCb(); - Utils.prefs.setCharPref(pref, value); - } - return value; - }, - - refresh: function ClientData_refresh(onComplete) { - let fn = function() { - let self = yield; - - let record = new ClientRec(); - record.uri = this.engineURL + this.GUID; - try { - yield record.get(self.cb); - } catch (e) { - // error? first-time upload? - // payload will be undefined, reset it so put below will work - record = new ClientRec(); - record.uri = this.engineURL + this.GUID; - } - - record.name = this.name; - record.type = this.type; - yield record.put(self.cb); - }; - fn.async(this, onComplete); - } -}; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index aa498873f6a9..7487fdde748e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -35,7 +35,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Engines', 'NewEngine', 'Engine', 'SyncEngine', 'BlobEngine']; +const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -47,9 +47,7 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/wrap.js"); -Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/clientData.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); @@ -99,25 +97,21 @@ EngineManagerSvc.prototype = { } }; -function Engine() { /* subclasses should call this._init() */} +function Engine() { this._init(); } Engine.prototype = { _notify: Wrap.notify, - // "default-engine"; - get name() { throw "name property must be overridden in subclasses"; }, + name: "engine", + displayName: "Boring Engine", + logName: "Engine", - // "Default"; - get displayName() { throw "displayName property must be overriden in subclasses"; }, + // _storeObj, and _trackerObj should to be overridden in subclasses - // "DefaultEngine"; - get logName() { throw "logName property must be overridden in subclasses"; }, + _storeObj: Store, + _trackerObj: Tracker, - // "user-data/default-engine/"; - get serverPrefix() { throw "serverPrefix property must be overridden in subclasses"; }, - - get enabled() { - return Utils.prefs.getBoolPref("engine." + this.name); - }, + get enabled() Utils.prefs.getBoolPref("engine." + this.name), + get score() this._tracker.score, get _os() { let os = Cc["@mozilla.org/observer-service;1"]. @@ -126,40 +120,18 @@ Engine.prototype = { return os; }, - get _json() { - let json = Cc["@mozilla.org/dom/json;1"]. - createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return json; - }, - - get score() this._tracker.score, - - // _store, and tracker need to be overridden in subclasses get _store() { - let store = new Store(); + let store = new this._storeObj(); this.__defineGetter__("_store", function() store); return store; }, get _tracker() { - let tracker = new Tracker(); + let tracker = new this._trackerObj(); this.__defineGetter__("_tracker", function() tracker); return tracker; }, - get engineId() { - let id = ID.get('Engine:' + this.name); - if (!id) { - // Copy the service login from WeaveID - let masterID = ID.get('WeaveID'); - - id = new Identity(this.logName, masterID.username, masterID.password); - ID.set('Engine:' + this.name, id); - } - return id; - }, - _init: function Engine__init() { let levelPref = "log.logger.service.engine." + this.name; let level = "Debug"; @@ -175,64 +147,34 @@ Engine.prototype = { this._log.debug("Engine initialized"); }, - _resetServer: function Engine__resetServer() { - let self = yield; - throw "_resetServer needs to be subclassed"; - }, - - _resetClient: function Engine__resetClient() { - let self = yield; - this._log.debug("Resetting client state"); - this._store.wipe(); - this._log.debug("Client reset completed successfully"); - }, - - _sync: function Engine__sync() { - let self = yield; - throw "_sync needs to be subclassed"; - }, - - _share: function Engine__share(guid, username) { - let self = yield; - /* This should be overridden by the engine subclass for each datatype. - Implementation should share the data node identified by guid, - and all its children, if any, with the user identified by username. */ - self.done(); - }, - - _stopSharing: function Engine__stopSharing(guid, username) { - let self = yield; - /* This should be overridden by the engine subclass for each datatype. - Stop sharing the data node identified by guid with the user identified - by username.*/ - self.done(); - }, - sync: function Engine_sync(onComplete) { - return this._sync.async(this, onComplete); + if (!this._sync) + throw "engine does not implement _sync method"; + this._notify("sync", "", this._sync).async(this, onComplete); }, - share: function Engine_share(onComplete, guid, username) { - return this._share.async(this, onComplete, guid, username); + wipeServer: function Engimne_wipeServer(onComplete) { + if (!this._wipeServer) + throw "engine does not implement _wipeServer method"; + this._notify("wipe-server", "", this._wipeServer).async(this, onComplete); }, - stopSharing: function Engine_share(onComplete, guid, username) { - return this._stopSharing.async(this, onComplete, guid, username); + _wipeClient: function Engine__wipeClient() { + let self = yield; + this._log.debug("Deleting all local data"); + this._store.wipe(); }, - - resetServer: function Engimne_resetServer(onComplete) { - this._notify("reset-server", "", this._resetServer).async(this, onComplete); - }, - - resetClient: function Engine_resetClient(onComplete) { - this._notify("reset-client", "", this._resetClient).async(this, onComplete); + wipeClient: function Engine_wipeClient(onComplete) { + this._notify("wipe-client", "", this._wipeClient).async(this, onComplete); } }; -function SyncEngine() { /* subclasses should call this._init() */ } +function SyncEngine() { this._init(); } SyncEngine.prototype = { __proto__: Engine.prototype, + _recordObj: CryptoWrapper, + get _memory() { let mem = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); this.__defineGetter__("_memory", function() mem); @@ -346,7 +288,7 @@ SyncEngine.prototype = { this._store.cache.fifo = false; // filo this._store.cache.clear(); - let newitems = new Collection(this.engineURL); + let newitems = new Collection(this.engineURL, this._recordObj); newitems.modified = this.lastSync; newitems.full = true; newitems.sort = "depthindex"; @@ -521,9 +463,11 @@ SyncEngine.prototype = { } }, - _resetServer: function SyncEngine__resetServer() { + _wipeServer: function SyncEngine__wipeServer() { let self = yield; let all = new Resource(this.engineURL); yield all.delete(self.cb); + let crypto = new Resource(this.cryptoMetaURL); + yield crypto.delete(self.cb); } }; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index dcd48209b285..ddea72c45c66 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -75,26 +75,11 @@ function BookmarksEngine() { } BookmarksEngine.prototype = { __proto__: SyncEngine.prototype, - get _super() SyncEngine.prototype, - - get name() "bookmarks", - get displayName() "Bookmarks", - get logName() "Bookmarks", - get serverPrefix() "user-data/bookmarks/", - - get _store() { - let store = new BookmarksStore(); - this.__defineGetter__("_store", function() store); - return store; - }, - - get _tracker() { - let tracker = new BookmarksTracker(); - this.__defineGetter__("_tracker", function() tracker); - return tracker; - } - // XXX for sharing, will need to re-add code to get new shares before syncing, - // and updating incoming/outgoing shared folders after syncing + name: "bookmarks", + displayName: "Bookmarks", + logName: "Bookmarks", + _storeObj: BookmarksStore, + _trackerObj: BookmarksTracker }; function BookmarksStore() { @@ -103,7 +88,6 @@ function BookmarksStore() { BookmarksStore.prototype = { __proto__: Store.prototype, _logName: "BStore", - _lookup: null, __bms: null, get _bms() { @@ -190,25 +174,20 @@ BookmarksStore.prototype = { return true; if (placeId == this._bms.unfiledBookmarksFolder) return true; - try { - if (this._bms.getFolderIdForItem(placeId) < 0) - return true; - } catch (e) { - this._log.debug("Oops! Failed for place ID: " + placeId); - throw e; - } + if (this._bms.getFolderIdForItem(placeId) <= 0) + return true; return false; }, itemExists: function BStore_itemExists(id) { - return this._getItemIdForGUID(id) >= 0; + return this._getItemIdForGUID(id) > 0; }, create: function BStore_create(record) { let newId; let parentId = this._getItemIdForGUID(record.parentid); - if (parentId < 0) { + if (parentId <= 0) { this._log.warn("Creating node with unknown parent -> reparenting to root"); parentId = this._bms.bookmarksMenuFolder; } @@ -323,7 +302,7 @@ BookmarksStore.prototype = { } var itemId = this._bms.getItemIdForGUID(record.id); - if (itemId < 0) { + if (itemId <= 0) { this._log.debug("Item " + record.id + " already removed"); return; } @@ -357,7 +336,7 @@ BookmarksStore.prototype = { this._log.debug("Skipping update for root node."); return; } - if (itemId < 0) { + if (itemId <= 0) { this._log.debug("Skipping update for unknown item: " + record.id); return; } @@ -434,14 +413,14 @@ BookmarksStore.prototype = { var itemId = this._getItemIdForGUID(oldID); if (itemId == null) // toplevel folder return; - if (itemId < 0) { + if (itemId <= 0) { this._log.warn("Can't change GUID " + oldID + " to " + newID + ": Item does not exist"); return; } var collision = this._getItemIdForGUID(newID); - if (collision >= 0) { + if (collision > 0) { this._log.warn("Can't change GUID " + oldID + " to " + newID + ": new ID already in use"); return; @@ -500,7 +479,7 @@ BookmarksStore.prototype = { return record; let placeId = this._bms.getItemIdForGUID(guid); - if (placeId < 0) { // deleted item + if (placeId <= 0) { // deleted item record = new PlacesItem(); record.cleartext = null; return record; @@ -630,30 +609,6 @@ BookmarksStore.prototype = { this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder); this._bms.removeFolderChildren(this._bms.toolbarFolder); this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder); - }, - - __resetGUIDs: function BStore___resetGUIDs(node) { - let self = yield; - - if (this._ans.itemHasAnnotation(node.itemId, "placesInternal/GUID")) - this._ans.removeItemAnnotation(node.itemId, "placesInternal/GUID"); - - if (node.type == node.RESULT_TYPE_FOLDER && - !this._ls.isLivemark(node.itemId)) { - yield Utils.makeTimerForCall(self.cb); // Yield to main loop - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - for (var i = 0; i < node.childCount; i++) { - this.__resetGUIDs(node.getChild(i)); - } - } - }, - - _resetGUIDs: function BStore__resetGUIDs() { - let self = yield; - this.__resetGUIDs(this._getNode(this._bms.bookmarksMenuFolder)); - this.__resetGUIDs(this._getNode(this._bms.toolbarFolder)); - this.__resetGUIDs(this._getNode(this._bms.unfiledBookmarksFolder)); } }; diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js new file mode 100644 index 000000000000..d39d628e9a05 --- /dev/null +++ b/services/sync/modules/engines/clients.js @@ -0,0 +1,208 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Clients']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/type_records/clientData.js"); + +Function.prototype.async = Async.sugar; + +Utils.lazy(this, 'Clients', ClientEngine); + +function ClientEngine() { + this._init(); +} +ClientEngine.prototype = { + __proto__: SyncEngine.prototype, + name: "clients", + displayName: "Clients", + logName: "Clients", + _storeObj: ClientStore, + _trackerObj: ClientTracker, + _recordObj: ClientRecord +}; + +function ClientStore() { + this._ClientStore_init(); +} +ClientStore.prototype = { + __proto__: Store.prototype, + _logName: "Clients.Store", + + get GUID() this._getCharPref("client.GUID", function() Utils.makeGUID()), + set GUID(value) Utils.prefs.setCharPref("client.GUID", value), + + get name() this._getCharPref("client.name", function() "Firefox"), + set name(value) Utils.prefs.setCharPref("client.name", value), + + get type() this._getCharPref("client.type", function() "desktop"), + set type(value) Utils.prefs.setCharPref("client.type", value), + + // FIXME: use Preferences module instead + _getCharPref: function ClientData__getCharPref(pref, defaultCb) { + let value; + try { + value = Utils.prefs.getCharPref(pref); + } catch (e) { + value = defaultCb(); + Utils.prefs.setCharPref(pref, value); + } + return value; + }, + + _ClientStore_init: function ClientStore__init() { + this.__proto__.__proto__._init.call(this); + this.clients = {}; + this.loadSnapshot(); + }, + + saveSnapshot: function ClientStore_saveSnapshot() { + this._log.debug("Saving client list to disk"); + let file = Utils.getProfileFile( + {path: "weave/meta/clients.json", autoCreate: true}); + let out = Svc.Json.encode(this.clients); + let [fos] = Utils.open(file, ">"); + fos.writeString(out); + fos.close(); + }, + + loadSnapshot: function ClientStore_loadSnapshot() { + let file = Utils.getProfileFile("weave/meta/clients.json"); + if (!file.exists()) + return; + this._log.debug("Loading client list from disk"); + try { + let [is] = Utils.open(file, "<"); + let json = Utils.readStream(is); + is.close(); + this.clients = Svc.Json.decode(json); + } catch (e) { + this._log.debug("Failed to load saved client list" + e); + } + }, + + // methods to apply changes: create, remove, update, changeItemID, wipe + + create: function ClientStore_create(record) { + this.update(record); + }, + + remove: function ClientStore_remove(record) { + delete this.clients[record.id]; + this.saveSnapshot(); + }, + + update: function ClientStore_update(record) { + this.clients[record.id] = record.payload; + this.saveSnapshot(); + }, + + changeItemID: function ClientStore_changeItemID(oldID, newID) { + this.clients[newID] = this.clients[oldID]; + delete this.clients[oldID]; + this.saveSnapshot(); + }, + + wipe: function ClientStore_wipe() { + this.clients = {}; + this.saveSnapshot(); + }, + + // methods to query local data: getAllIDs, itemExists, createRecord + + getAllIDs: function ClientStore_getAllIDs() { + // make sure we always return at least our GUID (e.g., before syncing + // when the client list is empty); + this.clients[this.GUID] = true; + return this.clients; + }, + + itemExists: function ClientStore_itemExists(id) { + return id in this.clients; + }, + + createRecord: function ClientStore_createRecord(guid) { + let record = new ClientRecord(); + if (guid == this.GUID) + record.payload = {name: this.name, type: this.type}; + else + record.payload = this.clients[guid]; + return record; + } +}; + +function ClientTracker() { + this._init(); +} +ClientTracker.prototype = { + __proto__: Tracker.prototype, + _logName: "ClientTracker", + + // we always want to sync, but are not dying to do so + get score() "75", + + // Override Tracker's _init to disable loading changed IDs from disk + _init: function ClientTracker__init() { + this._log = Log4Moz.repository.getLogger(this._logName); + this._store = new ClientStore(); // FIXME: hack + this.enable(); + }, + + // always upload our record + // FIXME: we should compare against the store's snapshot instead + get changedIDs() { + let items = {}; + items[this._store.GUID] = true; + return items; + }, + + // clobber these just in case anything tries to call them + addChangedID: function(id) {}, + removeChangedID: function(id) {}, + clearChangedIDs: function() {} +}; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index d6ce1071d6d7..a2423cff5680 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -45,7 +45,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/async.js"); @@ -58,24 +57,11 @@ function HistoryEngine() { } HistoryEngine.prototype = { __proto__: SyncEngine.prototype, - get _super() SyncEngine.prototype, - - get name() "history", - get displayName() "History", - get logName() "History", - get serverPrefix() "user-data/history/", - - get _store() { - let store = new HistoryStore(); - this.__defineGetter__("_store", function() store); - return store; - }, - - get _tracker() { - let tracker = new HistoryTracker(); - this.__defineGetter__("_tracker", function() tracker); - return tracker; - } + name: "history", + displayName: "History", + logName: "History", + _storeObj: HistoryStore, + _trackerObj: HistoryTracker }; function HistoryStore() { @@ -84,7 +70,6 @@ function HistoryStore() { HistoryStore.prototype = { __proto__: Store.prototype, _logName: "HistStore", - _lookup: null, get _hsvc() { let hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. @@ -357,11 +342,6 @@ HistoryStore.prototype = { return record; }, - // no depth or index for history - createMetaRecords: function HistStore_createMetaRecords(guid, items) { - return {}; - }, - wipe: function HistStore_wipe() { this._hsvc.removeAllPages(); } diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index 42ca263c02d1..124f43c36d3d 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -51,6 +51,7 @@ FaultTolerance = { function FTService() { this._log = Log4Moz.repository.getLogger("FaultTolerance"); + this._log.level = Log4Moz.Level["Debug"]; this._appender = new FTAppender(this); Log4Moz.repository.rootLogger.addAppender(this._appender); } @@ -60,6 +61,7 @@ FTService.prototype = { // FIXME: we get all log messages here, and could use them to keep track of // our current state }, + onException: function FTS_onException(exception) { this._lastException = exception; this._log.debug("\n" + Utils.stackTrace(exception)); diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index db0701da3fd3..f28ca6cbb8ff 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -69,8 +69,8 @@ RequestException.prototype = { } }; -function Resource(uri, authenticator) { - this._init(uri, authenticator); +function Resource(uri) { + this._init(uri); } Resource.prototype = { _logName: "Net.Resource", @@ -135,12 +135,11 @@ Resource.prototype = { this._filters = []; }, - _init: function Res__init(uri, authenticator) { + _init: function Res__init(uri) { this._log = Log4Moz.repository.getLogger(this._logName); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; this.uri = uri; - this._authenticator = authenticator; this._headers = {'Content-type': 'text/plain'}; this._filters = []; }, @@ -286,8 +285,11 @@ ChannelListener.prototype = { }; function JsonFilter() { + let level = "Debug"; + try { level = Utils.prefs.getCharPref("log.logger.network.jsonFilter"); } + catch (e) { /* ignore unset prefs */ } this._log = Log4Moz.repository.getLogger("Net.JsonFilter"); - this._log.level = Log4Moz.Level["Debug"]; + this._log.level = Log4Moz.Level[level]; } JsonFilter.prototype = { get _json() { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2877891c4c96..a8f27c4e2916 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -77,7 +77,7 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/oauth.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/clientData.js"); +Cu.import("resource://weave/engines/clientData.js"); Function.prototype.async = Async.sugar; @@ -92,12 +92,12 @@ Cu.import("resource://weave/resource.js", Weave); Cu.import("resource://weave/base_records/keys.js", Weave); Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); -Cu.import("resource://weave/clientData.js", Weave); Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/syncCores.js", Weave); Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/oauth.js", Weave); Cu.import("resource://weave/service.js", Weave); // ?? +Cu.import("resource://weave/engines/clientData.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); @@ -561,27 +561,38 @@ WeaveSvc.prototype = { } this._log.debug("Refreshing client list"); - yield ClientData.refresh(self.cb); + yield Clients.sync(self.cb); - let engines = Engines.getAll(); - for each (let engine in engines) { - if (this.cancelRequested) - continue; + try { + let engines = Engines.getAll(); + for each (let engine in engines) { + if (!engine.enabled) + continue; - if (!engine.enabled) - continue; + this._log.debug("Syncing engine " + engine.name); + yield this._notify(engine.name + "-engine:sync", "", + this._syncEngine, engine).async(this, self.cb); - this._log.debug("Syncing engine " + engine.name); - yield this._notify(engine.name + "-engine:sync", "", - this._syncEngine, engine).async(this, self.cb); - } + if (this._cancelRequested) { + this._log.info("Cancel requested, aborting sync"); + break; + } + if (this.abortSync) { + this._log.error("Aborting sync"); + break; + } + } - if (this._syncError) { + if (this._syncError) + this._log.warn("Some engines did not sync correctly"); + else + this._log.info("Sync completed successfully"); + + } finally { + this._cancelRequested = false; + this._abortSync = false; this._syncError = false; - throw "Some engines did not sync correctly"; } - - this._log.debug("Sync complete"); }, sync: function WeaveSvc_sync(onComplete) { this._notify("sync", "", @@ -596,50 +607,65 @@ WeaveSvc.prototype = { _syncAsNeeded: function WeaveSvc__syncAsNeeded() { let self = yield; - let engines = Engines.getAll(); - for each (let engine in engines) { - if (!engine.enabled) - continue; + try { + let engines = Engines.getAll(); + for each (let engine in engines) { + if (!engine.enabled) + continue; - if (!(engine.name in this._syncThresholds)) - this._syncThresholds[engine.name] = INITIAL_THRESHOLD; + if (!(engine.name in this._syncThresholds)) + this._syncThresholds[engine.name] = INITIAL_THRESHOLD; - let score = engine.score; - if (score >= this._syncThresholds[engine.name]) { - this._log.debug(engine.name + " score " + score + - " reaches threshold " + - this._syncThresholds[engine.name] + "; syncing"); - this._notify(engine.name + "-engine:sync", "", - this._syncEngine, engine).async(this, self.cb); - yield; + let score = engine.score; + if (score >= this._syncThresholds[engine.name]) { + this._log.debug(engine.name + " score " + score + + " reaches threshold " + + this._syncThresholds[engine.name] + "; syncing"); + yield this._notify(engine.name + "-engine:sync", "", + this._syncEngine, engine).async(this, self.cb); - // Reset the engine's threshold to the initial value. - // Note: we do this after syncing the engine so that we'll try again - // next time around if syncing fails for some reason. The upside - // of this approach is that we'll sync again as soon as possible; - // but the downside is that if the error is caused by the server being - // overloaded, we'll contribute to the problem by trying to sync - // repeatedly at the maximum rate. - this._syncThresholds[engine.name] = INITIAL_THRESHOLD; + // Reset the engine's threshold to the initial value. + // Note: we do this after syncing the engine so that we'll try again + // next time around if syncing fails for some reason. The upside + // of this approach is that we'll sync again as soon as possible; + // but the downside is that if the error is caused by the server being + // overloaded, we'll contribute to the problem by trying to sync + // repeatedly at the maximum rate. + this._syncThresholds[engine.name] = INITIAL_THRESHOLD; + } + else { + this._log.debug(engine.name + " score " + score + + " does not reach threshold " + + this._syncThresholds[engine.name] + "; not syncing"); + + // Decrement the threshold by the standard amount, and if this puts it + // at or below zero, then set it to 1, the lowest possible value, where + // it'll stay until there's something to sync (whereupon we'll sync it, + // reset the threshold to the initial value, and start over again). + this._syncThresholds[engine.name] -= THRESHOLD_DECREMENT_STEP; + if (this._syncThresholds[engine.name] <= 0) + this._syncThresholds[engine.name] = 1; + } + + if (this._cancelRequested) { + this._log.info("Cancel requested, aborting sync"); + break; + } + if (this.abortSync) { + this._log.error("Aborting sync"); + break; + } } - else { - this._log.debug(engine.name + " score " + score + - " does not reach threshold " + - this._syncThresholds[engine.name] + "; not syncing"); - // Decrement the threshold by the standard amount, and if this puts it - // at or below zero, then set it to 1, the lowest possible value, where - // it'll stay until there's something to sync (whereupon we'll sync it, - // reset the threshold to the initial value, and start over again). - this._syncThresholds[engine.name] -= THRESHOLD_DECREMENT_STEP; - if (this._syncThresholds[engine.name] <= 0) - this._syncThresholds[engine.name] = 1; - } - } + if (this._syncError) + this._log.warn("Some engines did not sync correctly"); + else + this._log.info("Sync completed successfully"); - if (this._syncError) { + } finally { + this._cancelRequested = false; + this._abortSync = false; this._syncError = false; - throw "Some engines did not sync correctly"; } }, @@ -647,43 +673,40 @@ WeaveSvc.prototype = { let self = yield; try { yield engine.sync(self.cb); } catch(e) { - // FIXME: FT module is not printing out exceptions - it should be - this._log.warn("Engine exception: " + e); - let ok = FaultTolerance.Service.onException(e); - if (!ok) - this._syncError = true; + this._syncError = true; + this._abortSync = !FaultTolerance.Service.onException(e); } }, - _resetServer: function WeaveSvc__resetServer() { + _wipeServer: function WeaveSvc__wipeServer() { let self = yield; let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { if (!engines[i].enabled) continue; - engines[i].resetServer(self.cb); + engines[i].wipeServer(self.cb); yield; } }, - resetServer: function WeaveSvc_resetServer(onComplete) { - this._notify("reset-server", "", - this._localLock(this._resetServer)).async(this, onComplete); + wipeServer: function WeaveSvc_wipeServer(onComplete) { + this._notify("wipe-server", "", + this._localLock(this._wipeServer)).async(this, onComplete); }, - _resetClient: function WeaveSvc__resetClient() { + _wipeClient: function WeaveSvc__wipeClient() { let self = yield; let engines = Engines.getAll(); for (let i = 0; i < engines.length; i++) { if (!engines[i].enabled) continue; - engines[i].resetClient(self.cb); + engines[i].wipeClient(self.cb); yield; } }, - resetClient: function WeaveSvc_resetClient(onComplete) { - this._localLock(this._notify("reset-client", "", - this._resetClient)).async(this, onComplete); + wipeClient: function WeaveSvc_wipeClient(onComplete) { + this._localLock(this._notify("wipe-client", "", + this._wipeClient)).async(this, onComplete); } /*, shareData: function WeaveSvc_shareData(dataType, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index b8d2516f3576..90caca6feaf2 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -113,6 +113,13 @@ Store.prototype = { throw "override createRecord in a subclass"; }, + // override if depth and/or depthindex are used + // should return all objects potentially affected by change of guid, in + // short form (no full content, only id, depth, sortindex) + createMetaRecords: function Store_createMetaRecords(guid, items) { + return {}; + }, + changeItemID: function Store_changeItemID(oldID, newID) { throw "override changeItemID in a subclass"; }, diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index cc04adca9d59..7cdfbce98ced 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -51,15 +51,15 @@ Cu.import("resource://weave/base_records/keys.js"); Function.prototype.async = Async.sugar; -function PlacesItem(uri, authenticator) { - this._PlacesItem_init(uri, authenticator); +function PlacesItem(uri) { + this._PlacesItem_init(uri); } PlacesItem.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Record.PlacesItem", - _PlacesItem_init: function BmkItemRec_init(uri, authenticator) { - this._CryptoWrap_init(uri, authenticator); + _PlacesItem_init: function BmkItemRec_init(uri) { + this._CryptoWrap_init(uri); this.cleartext = { }; }, @@ -71,15 +71,15 @@ PlacesItem.prototype = { } }; -function Bookmark(uri, authenticator) { - this._Bookmark_init(uri, authenticator); +function Bookmark(uri) { + this._Bookmark_init(uri); } Bookmark.prototype = { __proto__: PlacesItem.prototype, _logName: "Record.Bookmark", - _Bookmark_init: function BmkRec_init(uri, authenticator) { - this._PlacesItem_init(uri, authenticator); + _Bookmark_init: function BmkRec_init(uri) { + this._PlacesItem_init(uri); this.cleartext.type = "bookmark"; }, @@ -112,15 +112,15 @@ Bookmark.prototype = { } }; -function BookmarkMicsum(uri, authenticator) { - this._BookmarkMicsum_init(uri, authenticator); +function BookmarkMicsum(uri) { + this._BookmarkMicsum_init(uri); } BookmarkMicsum.prototype = { __proto__: Bookmark.prototype, _logName: "Record.BookmarkMicsum", - _BookmarkMicsum_init: function BmkMicsumRec_init(uri, authenticator) { - this._Bookmark_init(uri, authenticator); + _BookmarkMicsum_init: function BmkMicsumRec_init(uri) { + this._Bookmark_init(uri); this.cleartext.type = "microsummary"; }, @@ -138,15 +138,15 @@ BookmarkMicsum.prototype = { } }; -function BookmarkFolder(uri, authenticator) { - this._BookmarkFolder_init(uri, authenticator); +function BookmarkFolder(uri) { + this._BookmarkFolder_init(uri); } BookmarkFolder.prototype = { __proto__: PlacesItem.prototype, _logName: "Record.Folder", - _BookmarkFolder_init: function FolderRec_init(uri, authenticator) { - this._PlacesItem_init(uri, authenticator); + _BookmarkFolder_init: function FolderRec_init(uri) { + this._PlacesItem_init(uri); this.cleartext.type = "folder"; }, @@ -156,15 +156,15 @@ BookmarkFolder.prototype = { } }; -function Livemark(uri, authenticator) { - this._Livemark_init(uri, authenticator); +function Livemark(uri) { + this._Livemark_init(uri); } Livemark.prototype = { __proto__: BookmarkFolder.prototype, _logName: "Record.Livemark", - _Livemark_init: function LvmkRec_init(uri, authenticator) { - this._BookmarkFolder_init(uri, authenticator); + _Livemark_init: function LvmkRec_init(uri) { + this._BookmarkFolder_init(uri); this.cleartext.type = "livemark"; }, @@ -185,15 +185,15 @@ Livemark.prototype = { } }; -function BookmarkSeparator(uri, authenticator) { - this._BookmarkSeparator_init(uri, authenticator); +function BookmarkSeparator(uri) { + this._BookmarkSeparator_init(uri); } BookmarkSeparator.prototype = { __proto__: PlacesItem.prototype, _logName: "Record.Separator", - _BookmarkSeparator_init: function SepRec_init(uri, authenticator) { - this._PlacesItem_init(uri, authenticator); + _BookmarkSeparator_init: function SepRec_init(uri) { + this._PlacesItem_init(uri); this.cleartext.type = "separator"; } }; diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index afe000669eda..c62f0ecd23d7 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['ClientRec']; +const EXPORTED_SYMBOLS = ['ClientRecord']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -48,15 +48,15 @@ Cu.import("resource://weave/base_records/wbo.js"); Function.prototype.async = Async.sugar; -function ClientRec(uri, authenticator) { - this._ClientRec_init(uri, authenticator); +function ClientRecord(uri) { + this._ClientRec_init(uri); } -ClientRec.prototype = { +ClientRecord.prototype = { __proto__: WBORecord.prototype, _logName: "Record.Client", - _ClientRec_init: function ClientRec_init(uri, authenticator) { - this._WBORec_init(uri, authenticator); + _ClientRec_init: function ClientRec_init(uri) { + this._WBORec_init(uri); }, get name() this.payload.name, @@ -67,5 +67,15 @@ ClientRec.prototype = { get type() this.payload.type, set type(value) { this.payload.type = value; + }, + + // FIXME: engines.js calls encrypt/decrypt for all records, so define these: + encrypt: function ClientRec_encrypt(onComplete) { + let fn = function ClientRec__encrypt() {let self = yield;}; + fn.async(this, onComplete); + }, + decrypt: function ClientRec_decrypt(onComplete) { + let fn = function ClientRec__decrypt() {let self = yield;}; + fn.async(this, onComplete); } }; diff --git a/services/sync/modules/type_records/history.js b/services/sync/modules/type_records/history.js index 7d8c4abd2fbd..28e83083fdb4 100644 --- a/services/sync/modules/type_records/history.js +++ b/services/sync/modules/type_records/history.js @@ -50,15 +50,15 @@ Cu.import("resource://weave/base_records/keys.js"); Function.prototype.async = Async.sugar; -function HistoryRec(uri, authenticator) { - this._HistoryRec_init(uri, authenticator); +function HistoryRec(uri) { + this._HistoryRec_init(uri); } HistoryRec.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Record.History", - _HistoryRec_init: function HistItem_init(uri, authenticator) { - this._CryptoWrap_init(uri, authenticator); + _HistoryRec_init: function HistItem_init(uri) { + this._CryptoWrap_init(uri); this.cleartext = { }; }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 17ff7d96874e..5fb1d8e788f4 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Utils']; +const EXPORTED_SYMBOLS = ['Utils', 'Svc']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -149,6 +149,34 @@ let Utils = { dest.__defineGetter__(prop, getter); }, + // like lazy, but rather than new'ing the 3rd arg we use its return value + lazy2: function Weave_lazy2(dest, prop, fn) { + let getter = function() { + delete dest[prop]; + dest[prop] = ctr(); + return dest[prop]; + }; + dest.__defineGetter__(prop, getter); + }, + + lazySvc: function Weave_lazySvc(dest, prop, cid, iface) { + let getter = function() { + delete dest[prop]; + dest[prop] = Cc[cid].getService(iface); + return dest[prop]; + }; + dest.__defineGetter__(prop, getter); + }, + + lazyInstance: function Weave_lazyInstance(dest, prop, cid, iface) { + let getter = function() { + delete dest[prop]; + dest[prop] = Cc[cid].createInstance(iface); + return dest[prop]; + }; + dest.__defineGetter__(prop, getter); + }, + deepEquals: function Weave_deepEquals(a, b) { if (!a && !b) return true; @@ -474,4 +502,11 @@ Utils.EventListener.prototype = { //this._log.trace("Timer fired"); this._handler(timer); } -} +}; + +/* + * Commonly-used services + */ + +let Svc = {}; +Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js index 0059a9cefa9e..a31c21e3a643 100644 --- a/services/sync/tests/unit/test_auth_manager.js +++ b/services/sync/tests/unit/test_auth_manager.js @@ -43,7 +43,7 @@ function async_test() { let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); Auth.defaultAuthenticator = auth; - let res = new Resource("http://localhost:8080/foo"); // no authenticator + let res = new Resource("http://localhost:8080/foo"); let content = yield res.get(self.cb); do_check_eq(content, "This path exists and is protected"); do_check_eq(res.lastRequest.status, 200); From 57cf66d8705a39dc8586161ed2f31ca5ec287647 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 7 Jan 2009 13:05:05 -0800 Subject: [PATCH 0832/1860] fail recordLike if either record is null (deleted) --- services/sync/modules/engines.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 7487fdde748e..e62bc0aa92e9 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -226,6 +226,9 @@ SyncEngine.prototype = { if (a.depth != b.depth) return false; // note: sortindex ignored + if (a.cleartext == null || + b.cleartext == null) + return false; return Utils.deepEquals(a.cleartext, b.cleartext); }, From 7e8513e187a92d0307f12fa33d285600a549c9b6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 7 Jan 2009 14:22:02 -0800 Subject: [PATCH 0833/1860] catch undefined URIs in makeURI; use Svc global to cache IO service --- services/sync/modules/util.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 5fb1d8e788f4..2da0b6c92666 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -349,12 +349,10 @@ let Utils = { }, makeURI: function Weave_makeURI(URIString) { - if (URIString === null || URIString == "") + if (!URIString) return null; - let ioservice = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); try { - return ioservice.newURI(URIString, null, null); + return Svc.IO.newURI(URIString, null, null); } catch (e) { let log = Log4Moz.repository.getLogger("Service.Util"); log.debug("Could not create URI: " + e); @@ -510,3 +508,4 @@ Utils.EventListener.prototype = { let Svc = {}; Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); +Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); From 16164eff8a2551a1a5e659063bec8839be7eef4a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 7 Jan 2009 15:01:12 -0800 Subject: [PATCH 0834/1860] Bug 472480: catch errors when changing history item IDs --- services/sync/modules/engines/history.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index a2423cff5680..6375e28a2422 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -264,8 +264,12 @@ HistoryStore.prototype = { }, changeItemID: function HStore_changeItemID(oldID, newID) { - let uri = Utils.makeURI(this._findURLByGUID(oldID).uri); - this._anno.setPageAnnotation(uri, "weave/guid", newID, 0, 0); + try { + let uri = Utils.makeURI(this._findURLByGUID(oldID).uri); + this._anno.setPageAnnotation(uri, "weave/guid", newID, 0, 0); + } catch (e) { + this._log.debug("Could not change item ID: " + e); + } }, From ae589fa960cc5a811ff0cbe93ce8f1ac7bcffff6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 7 Jan 2009 16:43:49 -0800 Subject: [PATCH 0835/1860] correctly maintain linked list pointers in cache --- services/sync/modules/stores.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 90caca6feaf2..205588388d10 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -162,11 +162,15 @@ Cache.prototype = { if (this.fifo) { if (this._head) this._head.prev = wrapper; + let next = this._head; this._head = wrapper; + this._head.next = next; } else { if (this._tail) this._tail.next = wrapper; + let prev = this._tail; this._tail = wrapper; + this._tail.prev = prev; } this.count++; From f5226519876d8312b70e422451a0594f5f298ff0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 7 Jan 2009 17:45:17 -0800 Subject: [PATCH 0836/1860] fix Cache --- services/sync/modules/stores.js | 58 +++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 205588388d10..9214ea7b0f76 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -142,40 +142,56 @@ function Cache() { this._items = {}; } Cache.prototype = { - _pop: function Cache__pop() { - if (this.count <= 0) - return; - if (this.count == 1) + remove: function Cache_remove(item) { + if (this.count <= 0 || this.count == 1) { this.clear(); - else { - delete this._items[this._tail.id]; - this._tail = this._tail.prev; - this.count--; + return; } + + item.next.prev = item.prev; + item.prev.next = item.next; + + if (item == this._head) + this._head = item.next; + if (item == this._tail) + this._tail = item.prev; + + item.next = null; + item.prev = null; + + delete this._items[item.id]; + this.count--; + }, + pop: function Cache_pop() { + if (this.fifo) + this.remove(this._tail); + else + this.remove(this._head); }, put: function Cache_put(id, item) { if (!this.enabled) return; - let wrapper = {id: id, prev: null, next: this._head, item: item}; - this._items[wrapper.id] = wrapper; - if (this.fifo) { - if (this._head) - this._head.prev = wrapper; - let next = this._head; + let wrapper = {id: id, item: item}; + + if (this._head === null) { + wrapper.next = wrapper; + wrapper.prev = wrapper; this._head = wrapper; - this._head.next = next; - } else { - if (this._tail) - this._tail.next = wrapper; - let prev = this._tail; this._tail = wrapper; - this._tail.prev = prev; + } else { + wrapper.next = this._tail; + wrapper.prev = this._head; + this._head.prev = wrapper; + this._tail.next = wrapper; + this._tail = wrapper; } + this._items[wrapper.id] = wrapper; this.count++; + if (this.count >= this.maxItems) - this._pop(); + this.pop(); }, get: function Cache_get(id) { if (id in this._items) From 4ec9acd63ea047f47ed922a4b1eebee1879a94f4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 7 Jan 2009 17:46:31 -0800 Subject: [PATCH 0837/1860] set guid when creating history entries --- services/sync/modules/engines/history.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 6375e28a2422..9a51954a751b 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -306,6 +306,10 @@ HistoryStore.prototype = { (visit.type == 5 || visit.type == 6), 0); } this._hsvc.setPageTitle(uri, record.cleartext.title); + + let guid = this._getGUID(record.cleartext.uri); + if (guid != record.id) + this.changeItemID(guid, record.id); }, remove: function HistStore_remove(record) { From 4f965c7b193462bab968daf2e9ee49557dc48795 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 8 Jan 2009 06:29:55 +0100 Subject: [PATCH 0838/1860] Basic record parsing in place --- services/sync/modules/resource.js | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index f28ca6cbb8ff..306aac5e0c0d 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -284,6 +284,51 @@ ChannelListener.prototype = { } }; +/* Parses out single WBOs from a full dump */ +function RecordParser(data) { + this._data = data; +} +RecordParser.prototype = { + _parse: function RecordParse__parse(data) { + let start; + let bCount = 0; + let done = false; + + for (let i = 1; i < this._data.length; i++) { + if (data[i] == '{') { + if (bCount == 0) + start = i; + bCount++; + } else if (data[i] == '}') { + bCount--; + if (bCount == 0) + done = true; + } + + if (done) + return [start, i]; + } + + return false; + }, + + getRecords: function RecordParse_getRecords() { + let off; + let ret = []; + let data = this._data; + + while (off = this._parse(data)) { + ret[ret.length] = data.substring(off[0], off[1] + 1); + data = data.substring(off[1] + 1); + } + + if (ret.length) + return ret; + else + return false; + } +}; + function JsonFilter() { let level = "Debug"; try { level = Utils.prefs.getCharPref("log.logger.network.jsonFilter"); } From c32815d0e86e46d13b797070acabd73ae7369b7d Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 8 Jan 2009 06:39:39 +0100 Subject: [PATCH 0839/1860] Make RecordParser interface little easier to use --- services/sync/modules/resource.js | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 306aac5e0c0d..628c4bf26e40 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -289,43 +289,34 @@ function RecordParser(data) { this._data = data; } RecordParser.prototype = { - _parse: function RecordParse__parse(data) { + getNextRecord: function RecordParser_getNextRecord() { let start; let bCount = 0; let done = false; for (let i = 1; i < this._data.length; i++) { - if (data[i] == '{') { + if (this._data[i] == '{') { if (bCount == 0) start = i; bCount++; - } else if (data[i] == '}') { + } else if (this._data[i] == '}') { bCount--; if (bCount == 0) done = true; } - if (done) - return [start, i]; + if (done) { + let ret = this._data.substring(start, i + 1); + this._data = this._data.substring(i + 1); + return ret; + } } return false; }, - getRecords: function RecordParse_getRecords() { - let off; - let ret = []; - let data = this._data; - - while (off = this._parse(data)) { - ret[ret.length] = data.substring(off[0], off[1] + 1); - data = data.substring(off[1] + 1); - } - - if (ret.length) - return ret; - else - return false; + append: function RecordParser_append(data) { + this._data += data; } }; From e3b95e318bba54b4ae44c28789be155b030fe495 Mon Sep 17 00:00:00 2001 From: Date: Thu, 8 Jan 2009 16:37:36 -0800 Subject: [PATCH 0840/1860] The Weave check box on the Fennec preferences screen now logs you in/out when you click it in addition to enabling/disabling the scheduler. Improved the fennec-connect page by making the text fields auto-select when you click in them and turning the 'accept TOS' check box into a button. --- services/sync/modules/engines/history.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 9a51954a751b..582a555960a8 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -150,7 +150,7 @@ HistoryStore.prototype = { get _pidStm() { this._log.trace("Creating SQL statement: _pidStm"); let stm = this._db.createStatement( - "SELECT * FROM " + + "SELECT * FROM " + "(SELECT id FROM moz_places_temp WHERE url = :url LIMIT 1) " + "UNION ALL " + "SELECT * FROM ( " + @@ -165,7 +165,8 @@ HistoryStore.prototype = { get _urlStm() { this._log.trace("Creating SQL statement: _urlStm"); - let stm = this._db.createStatement( + try { + let stm = this._db.createStatement( "SELECT * FROM " + "(SELECT url,title FROM moz_places_temp WHERE id = :id LIMIT 1) " + "UNION ALL " + @@ -175,6 +176,14 @@ HistoryStore.prototype = { "LIMIT 1 " + ") " + "LIMIT 1"); + } catch (e) { + // On Fennec, this gets an error that there is no such table + // as moz_places_temp in existence + /*this._log.warn("moz_places_view exists?"); + this._log.warn( this._db.tableExists("moz_places_view"));*/ + this._log.warn(this._db.lastErrorString); + throw(e); + } this.__defineGetter__("_urlStm", function() stm); return stm; }, From 1e6f180c29d86ff0644684e16ecf58724ac6addf Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 8 Jan 2009 16:55:30 -0800 Subject: [PATCH 0841/1860] make sure we return false if veryfyLogin fails --- services/sync/modules/service.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a8f27c4e2916..a0e84d794b6f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -474,10 +474,14 @@ WeaveSvc.prototype = { this._log.debug("Logging in user " + this.username); - yield this.verifyLogin(self.cb, this.username, this.password); - - this._loggedIn = true; - self.done(true); + try { + yield this.verifyLogin(self.cb, this.username, this.password); + this._loggedIn = true; + self.done(true); + } catch (e) { + this._loggedIn = false; + self.done(false); + } }; this._localLock( this._catchAll( From 6e14db07bc6ba4d4fb83de051ef9435cb3020906 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 8 Jan 2009 16:57:35 -0800 Subject: [PATCH 0842/1860] commit so hg will let me merge --- services/sync/modules/engines/clients.js | 2 +- services/sync/modules/engines/tabs.js | 35 ++++++++---------------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index d39d628e9a05..30d0d5068412 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -96,7 +96,7 @@ ClientStore.prototype = { }, _ClientStore_init: function ClientStore__init() { - this.__proto__.__proto__._init.call(this); + this._init.call(this); this.clients = {}; this.loadSnapshot(); }, diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 25c22a17c23c..cb3c5afdb9f1 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -50,17 +50,17 @@ Cu.import("resource://weave/constants.js"); Function.prototype.async = Async.sugar; -function TabEngine(pbeId) { - this._init(pbeId); +function TabEngine() { + this._init(); } - TabEngine.prototype = { - __proto__: new BlobEngine(), - - get name() "tabs", - get displayName() { return "Tabs"; }, - get logName() "TabEngine", - get serverPrefix() "user-data/tabs/", + __proto__: SyncEngine.prototype, + name: "tabs", + displayName: "Tabs", + logName: "Tabs", + _storeObj: TabStore, + _trackerObj: TabTracker, + _recordObj: ClientTabsRecord get virtualTabs() { let virtualTabs = {}; @@ -76,28 +76,15 @@ TabEngine.prototype = { } } return virtualTabs; - }, - - get _store() { - let store = new TabStore(); - this.__defineGetter__("_store", function() store); - return this._store; - }, - - get _tracker() { - let tracker = new TabTracker(this); - this.__defineGetter__("_tracker", function() tracker); - return this._tracker; } - }; function TabStore() { - this._init(); + this._TabStore_init(); } TabStore.prototype = { __proto__: new Store(), - _logName: "TabStore", + _logName: "Tabs.Store", get _sessionStore() { let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. From f5ced0f6a7ff018ee174c8685939d3e222d8c72d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 8 Jan 2009 21:33:37 -0800 Subject: [PATCH 0843/1860] skip part of reconciliation for incoming deleted records --- services/sync/modules/engines.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e62bc0aa92e9..c24f1cbb691b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -364,6 +364,12 @@ SyncEngine.prototype = { return; } + // If the incoming item has been deleted, skip step 3 + if (item.cleartext === null) { + self.done(true); + return; + } + // Step 3: Check for similar items for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); From d14983295921f549cdc456ea1b62068378beacde Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 8 Jan 2009 21:33:59 -0800 Subject: [PATCH 0844/1860] when an orphan bookmark is found, reparent it to the unfiled bookmarks folder --- services/sync/modules/engines/bookmarks.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index ddea72c45c66..d46ffd29bf3d 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -540,7 +540,13 @@ BookmarksStore.prototype = { }, _getWeaveParentIdForItem: function BStore__getWeaveParentIdForItem(itemId) { - return this._getWeaveIdForItem(this._bms.getFolderIdForItem(itemId)); + let parentid = this._bms.getFolderIdForItem(itemId); + if (parentid == -1) { + this._log.debug("Found orphan bookmark, reparenting to unfiled"); + parentid = this._bms.unfiledBookmarksFolder; + this._bms.moveItem(itemId, parentid, -1); + } + return this._getWeaveIdForItem(parentid); }, _getChildren: function BStore_getChildren(guid, depthIndex, items) { From e5ea25ce9673e4517cbed4b11dd850c0b2e43f21 Mon Sep 17 00:00:00 2001 From: Date: Fri, 9 Jan 2009 15:44:27 -0800 Subject: [PATCH 0845/1860] History sync now works in Fennec, by simply skipping all the temp tables that don't exist in Fennec. This should also fix bug 472853 for weave in Firefox. --- services/sync/modules/engines/history.js | 96 +++++++++++++++--------- 1 file changed, 59 insertions(+), 37 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 582a555960a8..5d0213207636 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -125,48 +125,73 @@ HistoryStore.prototype = { get _visitStm() { this._log.trace("Creating SQL statement: _visitStm"); - let stm = this._db.createStatement( - "SELECT * FROM ( " + - "SELECT visit_type AS type, visit_date AS date " + - "FROM moz_historyvisits_temp " + - "WHERE place_id = :placeid " + - "ORDER BY visit_date DESC " + - "LIMIT 10 " + - ") " + - "UNION ALL " + - "SELECT * FROM ( " + - "SELECT visit_type AS type, visit_date AS date " + - "FROM moz_historyvisits " + - "WHERE place_id = :placeid " + - "AND id NOT IN (SELECT id FROM moz_historyvisits_temp) " + - "ORDER BY visit_date DESC " + - "LIMIT 10 " + - ") " + - "ORDER BY 2 DESC LIMIT 10"); /* 2 is visit_date */ + let stm; + if (this._db.tableExists("moz_historyvisits_temp")) { + stm = this._db.createStatement( + "SELECT * FROM ( " + + "SELECT visit_type AS type, visit_date AS date " + + "FROM moz_historyvisits_temp " + + "WHERE place_id = :placeid " + + "ORDER BY visit_date DESC " + + "LIMIT 10 " + + ") " + + "UNION ALL " + + "SELECT * FROM ( " + + "SELECT visit_type AS type, visit_date AS date " + + "FROM moz_historyvisits " + + "WHERE place_id = :placeid " + + "AND id NOT IN (SELECT id FROM moz_historyvisits_temp) " + + "ORDER BY visit_date DESC " + + "LIMIT 10 " + + ") " + + "ORDER BY 2 DESC LIMIT 10"); /* 2 is visit_date */ + } else { + stm = this._db.createStatement( + "SELECT visit_type AS type, visit_date AS date " + + "FROM moz_historyvisits " + + "WHERE place_id = :placeid " + + "ORDER BY visit_date DESC LIMIT 10" + ); + } this.__defineGetter__("_visitStm", function() stm); return stm; }, get _pidStm() { this._log.trace("Creating SQL statement: _pidStm"); - let stm = this._db.createStatement( - "SELECT * FROM " + - "(SELECT id FROM moz_places_temp WHERE url = :url LIMIT 1) " + - "UNION ALL " + - "SELECT * FROM ( " + - "SELECT id FROM moz_places WHERE url = :url " + - "AND id NOT IN (SELECT id from moz_places_temp) " + - "LIMIT 1 " + - ") " + - "LIMIT 1"); + let stm; + // See comment in get _urlStm() + if (this._db.tableExists("moz_places_temp")) { + stm = this._db.createStatement( + "SELECT * FROM " + + "(SELECT id FROM moz_places_temp WHERE url = :url LIMIT 1) " + + "UNION ALL " + + "SELECT * FROM ( " + + "SELECT id FROM moz_places WHERE url = :url " + + "AND id NOT IN (SELECT id from moz_places_temp) " + + "LIMIT 1 " + + ") " + + "LIMIT 1"); + } else { + stm = this._db.createStatement( + "SELECT id FROM moz_places WHERE url = :url LIMIT 1" + ); + } this.__defineGetter__("_pidStm", function() stm); return stm; }, get _urlStm() { this._log.trace("Creating SQL statement: _urlStm"); - try { - let stm = this._db.createStatement( + /* On Fennec, there is no table called moz_places_temp. + * (We think there should be; we're not sure why there's not.) + * As a workaround until we figure out why, we'll check if the table + * exists first, and if it doesn't we'll use a simpler query without + * that table. + */ + let stm; + if (this._db.tableExists("moz_places_temp")) { + stm = this._db.createStatement( "SELECT * FROM " + "(SELECT url,title FROM moz_places_temp WHERE id = :id LIMIT 1) " + "UNION ALL " + @@ -176,13 +201,10 @@ HistoryStore.prototype = { "LIMIT 1 " + ") " + "LIMIT 1"); - } catch (e) { - // On Fennec, this gets an error that there is no such table - // as moz_places_temp in existence - /*this._log.warn("moz_places_view exists?"); - this._log.warn( this._db.tableExists("moz_places_view"));*/ - this._log.warn(this._db.lastErrorString); - throw(e); + } else { + stm = this._db.createStatement( + "SELECT url,title FROM moz_places WHERE id = :id LIMIT 1" + ); } this.__defineGetter__("_urlStm", function() stm); return stm; From 4c79b05d75cdc2f62514bb797b24a3b796b046cb Mon Sep 17 00:00:00 2001 From: Date: Mon, 12 Jan 2009 17:52:06 -0800 Subject: [PATCH 0846/1860] Made the label inside the Weave pref on the main Fennec preferences screen into a dynamic status indicator --- services/sync/locales/en-US/preferences.dtd | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 0def1558f8c5..9494e55be24a 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -90,5 +90,7 @@ - - + + + + From 281dab48d9d629a4e18d4109f3fc032e9166d4e1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 13 Jan 2009 13:40:40 -0800 Subject: [PATCH 0847/1860] small getchildren/getsiblings cleanup --- services/sync/modules/engines/bookmarks.js | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d46ffd29bf3d..fc7b59f5b9bf 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -539,6 +539,15 @@ BookmarksStore.prototype = { return record; }, + _createMiniRecord: function BStore__createMiniRecord(placesId, depthIndex) { + let foo = {id: this._bms.getItemGUID(placesId)}; + if (depthIndex) { + foo.depth = this._itemDepth(placesId); + foo.sortindex = this._bms.getItemIndex(placesId); + } + return foo; + }, + _getWeaveParentIdForItem: function BStore__getWeaveParentIdForItem(itemId) { let parentid = this._bms.getFolderIdForItem(itemId); if (parentid == -1) { @@ -562,11 +571,7 @@ BookmarksStore.prototype = { node.containerOpen = true; for (var i = 0; i < node.childCount; i++) { let child = node.getChild(i); - let foo = {id: this._bms.getItemGUID(child.itemId)}; - if (depthIndex) { - foo.depth = this._itemDepth(child.itemId); - foo.sortindex = this._bms.getItemIndex(child.itemId); - } + let foo = this._createMiniRecord(child.itemId); items[foo.id] = foo; this._getChildren(child, depthIndex, items); } @@ -586,11 +591,7 @@ BookmarksStore.prototype = { for (var i = 0; i < parent.childCount; i++) { let child = parent.getChild(i); - let foo = {id: this._bms.getItemGUID(child.itemId)}; - if (depthIndex) { - foo.depth = this._itemDepth(child.itemId); - foo.sortindex = this._bms.getItemIndex(child.itemId); - } + let foo = this._createMiniRecord(child.itemId); items[foo.id] = foo; } @@ -599,9 +600,9 @@ BookmarksStore.prototype = { getAllIDs: function BStore_getAllIDs() { let items = {}; - this._getChildren(this._getNode(this._bms.bookmarksMenuFolder), true, items); - this._getChildren(this._getNode(this._bms.toolbarFolder), true, items); - this._getChildren(this._getNode(this._bms.unfiledBookmarksFolder), true, items); + this._getChildren("menu", true, items); + this._getChildren("toolbar", true, items); + this._getChildren("unfiled", true, items); return items; }, From 3b49a353f23afe20de33249c9595ba27314f0fae Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 13 Jan 2009 14:43:21 -0800 Subject: [PATCH 0848/1860] ignore/unignore specific weave IDs instead of a blanket enable/disable of the tracker during sync. --- services/sync/modules/engines.js | 9 ++--- services/sync/modules/engines/bookmarks.js | 25 +++++++------- services/sync/modules/trackers.js | 38 ++++++++++++---------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c24f1cbb691b..069e0405a043 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -274,8 +274,6 @@ SyncEngine.prototype = { let outnum = [i for (i in this._tracker.changedIDs)].length; this._log.info(outnum + " outgoing items pre-reconciliation"); - - this._tracker.disable(); // FIXME: need finer-grained ignoring }, // Generate outgoing records @@ -389,12 +387,15 @@ SyncEngine.prototype = { let self = yield; this._log.trace("Incoming:\n" + item); try { + this._tracker.ignoreID(item.id); yield this._store.applyIncoming(self.cb, item); if (this._lastSyncTmp < item.modified) this._lastSyncTmp = item.modified; } catch (e) { this._log.warn("Error while applying incoming record: " + (e.message? e.message : e)); + } finally { + this._tracker.unignoreID(item.id); } }, @@ -451,7 +452,6 @@ SyncEngine.prototype = { let self = yield; this._log.debug("Finishing up sync"); this._tracker.resetScore(); - this._tracker.enable(); }, _sync: function SyncEngine__sync() { @@ -467,9 +467,6 @@ SyncEngine.prototype = { this._log.warn("Sync failed"); throw e; } - finally { - this._tracker.enable(); - } }, _wipeServer: function SyncEngine__wipeServer() { diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index fc7b59f5b9bf..127f266baf38 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -653,38 +653,39 @@ BookmarksTracker.prototype = { this._all[this._bms.getItemIdForGUID(guid)] = guid; } + // ignore changes to the three roots + // we use special names for them, so ignore their "real" places guid + // as well as ours, just in case + this.ignoreID("menu"); + this.ignoreID("toolbar"); + this.ignoreID("unfiled"); + this.ignoreID(this._all[this._bms.bookmarksMenuFolder]); + this.ignoreID(this._all[this._bms.toolbarFolder]); + this.ignoreID(this._all[this._bms.unfiledBookmarksFolder]); + this._bms.addObserver(this, false); }, /* Every add/remove/change is worth 10 points */ _upScore: function BMT__upScore() { - if (!this.enabled) - return; this._score += 10; }, onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { this._all[itemId] = this._bms.getItemGUID(itemId); - //if (!this.enabled) - //return; this._log.trace("onItemAdded: " + itemId); this.addChangedID(this._all[itemId]); this._upScore(); }, onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { - let guid = this._all[itemId]; - delete this._all[itemId]; - if (!this.enabled) - return; this._log.trace("onItemRemoved: " + itemId); - this.addChangedID(guid); + this.addChangedID(this._all[itemId]); + delete this._all[itemId]; this._upScore(); }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnnotationProperty, value) { - if (!this.enabled) - return; this._log.trace("onItemChanged: " + itemId + ", property: " + property + ", isAnno: " + isAnnotationProperty + ", value: " + value); @@ -705,8 +706,6 @@ BookmarksTracker.prototype = { }, onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { - if (!this.enabled) - return; this._log.trace("onItemMoved: " + itemId); this.addChangedID(this._all[itemId]); this._upScore(); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 56b5b1f84ad1..29d15ace2285 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -76,22 +76,11 @@ Tracker.prototype = { _init: function T__init() { this._log = Log4Moz.repository.getLogger(this._logName); this._score = 0; + this._ignored = []; this.loadChangedIDs(); this.enable(); }, - get enabled() { - return this._enabled; - }, - - enable: function T_enable() { - this._enabled = true; - }, - - disable: function T_disable() { - this._enabled = false; - }, - /* * Score can be called as often as desired to decide which engines to sync * @@ -163,13 +152,28 @@ Tracker.prototype = { } }, + // ignore/unignore specific IDs. Useful for ignoring items that are + // being processed, or that shouldn't be synced. + // But note: not persisted to disk + + ignoreID: function T_ignoreID(id) { + this.unignoreID(id); + this._ignored.push(id); + }, + + unignoreID: function T_unignoreID(id) { + let index = this._ignored.indexOf(id); + if (index != -1) + this._ignored.splice(index, 1); + }, + addChangedID: function T_addChangedID(id) { - if (!this.enabled) - return; if (!id) { this._log.warn("Attempted to add undefined ID to tracker"); return; } + if (id in this._ignored) + return; this._log.debug("Adding changed ID " + id); if (!this.changedIDs[id]) { this.changedIDs[id] = true; @@ -178,12 +182,12 @@ Tracker.prototype = { }, removeChangedID: function T_removeChangedID(id) { - if (!this.enabled) - return; if (!id) { - this._log.warn("Attempted to remove undefined ID from tracker"); + this._log.warn("Attempted to remove undefined ID to tracker"); return; } + if (id in this._ignored) + return; this._log.debug("Removing changed ID " + id); if (this.changedIDs[id]) { delete this.changedIDs[id]; From 816657c26ee4dea580cd4c86a92663c22c6a383b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 13 Jan 2009 15:55:35 -0800 Subject: [PATCH 0849/1860] make tracker return true/false when adding a changed ID to indicate if it was a valid add or not; change bookmarks & history trackers to match; fix some problems in bookmarks tracker --- services/sync/modules/engines/bookmarks.js | 43 +++++++++------------- services/sync/modules/engines/history.js | 14 +++---- services/sync/modules/trackers.js | 14 ++++--- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 127f266baf38..2bb6537e9eee 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -672,43 +672,36 @@ BookmarksTracker.prototype = { }, onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { - this._all[itemId] = this._bms.getItemGUID(itemId); this._log.trace("onItemAdded: " + itemId); - this.addChangedID(this._all[itemId]); - this._upScore(); + this._all[itemId] = this._bms.getItemGUID(itemId); + if (this.addChangedID(this._all[itemId])) + this._upScore(); }, onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { this._log.trace("onItemRemoved: " + itemId); - this.addChangedID(this._all[itemId]); + if (this.addChangedID(this._all[itemId])) + this._upScore(); delete this._all[itemId]; - this._upScore(); }, - onItemChanged: function BMT_onItemChanged(itemId, property, isAnnotationProperty, value) { - this._log.trace("onItemChanged: " + itemId + ", property: " + property + - ", isAnno: " + isAnnotationProperty + ", value: " + value); - - // NOTE: we get onItemChanged too much, when adding an item, changing its - // GUID, before removal... it makes removals break, because trying - // to update our cache before removals makes it so we think the - // temporary guid was removed, instead of the real one. - // Therefore, we ignore all guid changes. When *Weave* changes the - // GUID, we take care to update the tracker's cache. If anyone else - // changes the GUID, that will case breakage. - let guid = this._bms.getItemGUID(itemId); - if (guid != this._all[itemId]) - this._log.trace("GUID change, ignoring"); - else - this.addChangedID(this._all[itemId]); // some other change - - this._upScore(); + onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { + this._log.trace("onItemChanged: " + itemId + + ", \"" + property + (isAnno? " (anno)" : "") + "\"" + + " = \"" + value + "\""); + // 1) notifications for already-deleted items are ignored + // 2) note that engine/store are responsible for manually updating the + // tracker's placesId->weaveId cache + if ((itemId in this._all) && + (this._bms.getItemGUID(itemId) != this._all[itemId]) && + this.addChangedID(this._all[itemId])) + this._upScore(); }, onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { this._log.trace("onItemMoved: " + itemId); - this.addChangedID(this._all[itemId]); - this._upScore(); + if (this.addChangedID(this._all[itemId])) + this._upScore(); }, onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 5d0213207636..fff12323de78 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -433,25 +433,23 @@ HistoryTracker.prototype = { * Clearing the whole history is worth 50 points (see below) */ _upScore: function BMT__upScore() { - if (!this.enabled) - return; this._score += 1; }, onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { this._log.trace("onVisit: " + uri.spec); - this.addChangedID(this._store._getGUID(uri)); - this._upScore(); + if (this.addChangedID(this._store._getGUID(uri))) + this._upScore(); }, onPageExpired: function HT_onPageExpired(uri, time, entry) { this._log.trace("onPageExpired: " + uri.spec); - this.addChangedID(this._store._getGUID(uri)); // XXX eek ? - this._upScore(); + if (this.addChangedID(this._store._getGUID(uri))) // XXX eek ? + this._upScore(); }, onDeleteURI: function HT_onDeleteURI(uri) { this._log.trace("onDeleteURI: " + uri.spec); - this.addChangedID(this._all[uri]); - this._upScore(); + if (this.addChangedID(this._all[uri])) + this._upScore(); }, onClearHistory: function HT_onClearHistory() { this._log.trace("onClearHistory"); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 29d15ace2285..0390ff375b87 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -170,29 +170,31 @@ Tracker.prototype = { addChangedID: function T_addChangedID(id) { if (!id) { this._log.warn("Attempted to add undefined ID to tracker"); - return; + return false; } if (id in this._ignored) - return; - this._log.debug("Adding changed ID " + id); + return false; if (!this.changedIDs[id]) { + this._log.debug("Adding changed ID " + id); this.changedIDs[id] = true; this.saveChangedIDs(); } + return true; }, removeChangedID: function T_removeChangedID(id) { if (!id) { this._log.warn("Attempted to remove undefined ID to tracker"); - return; + return false; } if (id in this._ignored) - return; - this._log.debug("Removing changed ID " + id); + return false; if (this.changedIDs[id]) { + this._log.debug("Removing changed ID " + id); delete this.changedIDs[id]; this.saveChangedIDs(); } + return true; }, clearChangedIDs: function T_clearChangedIDs() { From d2f4303219fe660b6f08b042c9ce731348192a2e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 13 Jan 2009 16:11:31 -0800 Subject: [PATCH 0850/1860] syncCores are gone --- services/sync/modules/service.js | 1 - services/sync/modules/syncCores.js | 310 ----------------------------- 2 files changed, 311 deletions(-) delete mode 100644 services/sync/modules/syncCores.js diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a0e84d794b6f..e17fa623a305 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -93,7 +93,6 @@ Cu.import("resource://weave/base_records/keys.js", Weave); Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); Cu.import("resource://weave/stores.js", Weave); -Cu.import("resource://weave/syncCores.js", Weave); Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/oauth.js", Weave); Cu.import("resource://weave/service.js", Weave); // ?? diff --git a/services/sync/modules/syncCores.js b/services/sync/modules/syncCores.js deleted file mode 100644 index 1afc8f17a4f4..000000000000 --- a/services/sync/modules/syncCores.js +++ /dev/null @@ -1,310 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['SyncCore']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; - -/* - * SyncCore objects - * Sync cores deal with diff creation and conflict resolution. - * Tree data structures where all nodes have GUIDs only need to be - * subclassed for each data type to implement commandLike and - * itemExists. - */ - -function SyncCore() { - this._init(); -} -SyncCore.prototype = { - _logName: "Sync", - - // Set this property in child objects! - _store: null, - - _init: function SC__init() { - this._log = Log4Moz.repository.getLogger("Service." + this._logName); - }, - - // FIXME: this won't work for deep objects, or objects with optional - // properties - _getEdits: function SC__getEdits(a, b) { - let ret = {numProps: 0, props: {}}; - for (prop in a) { - if (!Utils.deepEquals(a[prop], b[prop])) { - ret.numProps++; - ret.props[prop] = b[prop]; - } - } - return ret; - }, - - _nodeParents: function SC__nodeParents(GUID, tree) { - return this._nodeParentsInt(GUID, tree, []); - }, - - _nodeParentsInt: function SC__nodeParentsInt(GUID, tree, parents) { - if (!tree[GUID] || !tree[GUID].parentid) - return parents; - parents.push(tree[GUID].parentid); - return this._nodeParentsInt(tree[GUID].parentid, tree, parents); - }, - - _detectUpdates: function SC__detectUpdates(a, b) { - let self = yield; - - let cmds = []; - - try { - for (let GUID in a) { - - Utils.makeTimerForCall(self.cb); - yield; // Yield to main loop - - if (GUID in b) { - let edits = this._getEdits(a[GUID], b[GUID]); - if (edits.numProps == 0) // no changes - skip - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "edit", GUID: GUID, - depth: parents.length, parents: parents, - data: edits.props}); - } else { - let parents = this._nodeParents(GUID, a); // ??? - cmds.push({action: "remove", GUID: GUID, - depth: parents.length, parents: parents}); - } - } - - for (GUID in b) { - - Utils.makeTimerForCall(self.cb); - yield; // Yield to main loop - - if (GUID in a) - continue; - let parents = this._nodeParents(GUID, b); - cmds.push({action: "create", GUID: GUID, - depth: parents.length, parents: parents, - data: b[GUID]}); - } - cmds.sort(function(a, b) { - if (a.depth > b.depth) - return 1; - if (a.depth < b.depth) - return -1; - if (a.index > b.index) - return -1; - if (a.index < b.index) - return 1; - return 0; // should never happen, but not a big deal if it does - }); - - } catch (e) { - throw e; - - } finally { - self.done(cmds); - } - }, - - _commandLike: function SC__commandLike(a, b) { - this._log.error("commandLike needs to be subclassed"); - - // Check that neither command is null, and verify that the GUIDs - // are different (otherwise we need to check for edits) - if (!a || !b || a.GUID == b.GUID) - return false; - - // Check that all other properties are the same - // FIXME: could be optimized... - for (let key in a) { - if (key != "GUID" && !Utils.deepEquals(a[key], b[key])) - return false; - } - for (key in b) { - if (key != "GUID" && !Utils.deepEquals(a[key], b[key])) - return false; - } - return true; - }, - - // When we change the GUID of a local item (because we detect it as - // being the same item as a remote one), we need to fix any other - // local items that have it as their parent - _fixParents: function SC__fixParents(list, oldGUID, newGUID) { - for (let i = 0; i < list.length; i++) { - if (!list[i]) - continue; - if (list[i].data && list[i].data.parentid && - list[i].data.parentid == oldGUID) - list[i].data.parentid = newGUID; - for (let j = 0; j < list[i].parents.length; j++) { - if (list[i].parents[j] == oldGUID) - list[i].parents[j] = newGUID; - } - } - }, - - _conflicts: function SC__conflicts(a, b) { - if ((a.GUID == b.GUID) && !Utils.deepEquals(a, b)) - return true; - return false; - }, - - _getPropagations: function SC__getPropagations(commands, conflicts, propagations) { - for (let i = 0; i < commands.length; i++) { - let alsoConflicts = function(elt) { - return (elt.action == "create" || elt.action == "remove") && - commands[i].parents.indexOf(elt.GUID) >= 0; - }; - if (conflicts.some(alsoConflicts)) - conflicts.push(commands[i]); - - let cmdConflicts = function(elt) { - return elt.GUID == commands[i].GUID; - }; - if (!conflicts.some(cmdConflicts)) - propagations.push(commands[i]); - } - }, - - _reconcile: function SC__reconcile(listA, listB) { - let self = yield; - - let propagations = [[], []]; - let conflicts = [[], []]; - let ret = {propagations: propagations, conflicts: conflicts}; - this._log.debug("Reconciling " + listA.length + - " against " + listB.length + " commands"); - - let guidChanges = []; - let edits = []; - for (let i = 0; i < listA.length; i++) { - let a = listA[i]; - - yield Utils.makeTimerForCall(self.cb); // yield to UI - - let skip = false; - listB = listB.filter(function(b) { - // fast path for when we already found a matching command - if (skip) - return true; - - if (a.GUID == b.GUID) { - // delete both commands - // XXX this relies on the fact that we actually dump - // outgoing commands and generate new ones by doing a fresh - // diff after applying local changes - skip = true; - delete listA[i]; // delete a - return false; // delete b - - } else if (this._commandLike(a, b)) { - this._fixParents(listA, a.GUID, b.GUID); - guidChanges.push({action: "edit", - GUID: a.GUID, - data: {GUID: b.GUID}}); - skip = true; - delete listA[i]; // delete a - return false; // delete b - } - return true; // keep b - }, this); - } - - listB = listB.filter(function(b) { - // watch out for create commands with GUIDs that already exist - if (b.action == "create" && this._store._itemExists(b.GUID)) { - this._log.error("Remote command has GUID that already exists " + - "locally. Dropping command."); - return false; // delete b - } - return true; // keep b - }, this); - - listA = listA.filter(function(elt) { return elt }); // removes any holes - listA = listA.concat(edits); - listB = guidChanges.concat(listB); - - for (let i = 0; i < listA.length; i++) { - for (let j = 0; j < listB.length; j++) { - - yield Utils.makeTimerForCall(self.cb); // yield to UI - - if (this._conflicts(listA[i], listB[j]) || - this._conflicts(listB[j], listA[i])) { - if (!conflicts[0].some( - function(elt) { return elt.GUID == listA[i].GUID })) - conflicts[0].push(listA[i]); - if (!conflicts[1].some( - function(elt) { return elt.GUID == listB[j].GUID })) - conflicts[1].push(listB[j]); - } - } - } - - this._getPropagations(listA, conflicts[0], propagations[1]); - - yield Utils.makeTimerForCall(self.cb); // yield to UI - - this._getPropagations(listB, conflicts[1], propagations[0]); - ret = {propagations: propagations, conflicts: conflicts}; - - self.done(ret); - }, - - // Public methods - - detectUpdates: function SC_detectUpdates(onComplete, a, b) { - return this._detectUpdates.async(this, onComplete, a, b); - }, - - reconcile: function SC_reconcile(onComplete, listA, listB) { - return this._reconcile.async(this, onComplete, listA, listB); - } -}; From 86c79ad0e86be542855c9800f4e9efc75a30a9a7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 13 Jan 2009 16:55:51 -0800 Subject: [PATCH 0851/1860] disable unused engines; fix trackers trying to call enable which is gone now; improve bookmarks tracker logging --- services/sync/modules/engines/bookmarks.js | 4 +- services/sync/modules/engines/clients.js | 3 +- services/sync/modules/sharing.js | 114 --------------------- services/sync/modules/trackers.js | 1 - 4 files changed, 4 insertions(+), 118 deletions(-) delete mode 100644 services/sync/modules/sharing.js diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 2bb6537e9eee..93516285e65c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -687,8 +687,8 @@ BookmarksTracker.prototype = { onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { this._log.trace("onItemChanged: " + itemId + - ", \"" + property + (isAnno? " (anno)" : "") + "\"" + - " = \"" + value + "\""); + (", " + property + (isAnno? " (anno)" : "")) + + (value? (" = \"" + value + "\"") : "")); // 1) notifications for already-deleted items are ignored // 2) note that engine/store are responsible for manually updating the // tracker's placesId->weaveId cache diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 30d0d5068412..a2d12918e581 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -189,8 +189,9 @@ ClientTracker.prototype = { // Override Tracker's _init to disable loading changed IDs from disk _init: function ClientTracker__init() { this._log = Log4Moz.repository.getLogger(this._logName); + this._score = 0; + this._ignored = []; this._store = new ClientStore(); // FIXME: hack - this.enable(); }, // always upload our record diff --git a/services/sync/modules/sharing.js b/services/sync/modules/sharing.js deleted file mode 100644 index 787deff99a64..000000000000 --- a/services/sync/modules/sharing.js +++ /dev/null @@ -1,114 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Atul Varma - * Anant Narayanan - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -EXPORTED_SYMBOLS = ["Sharing"]; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/identity.js"); - -Function.prototype.async = Async.sugar; - -function Api(dav) { - this._dav = dav; -} - -Api.prototype = { - shareWithUsers: function Api_shareWithUsers(path, users, folder, onComplete) { - return this._shareGenerator.async(this, - onComplete, - path, - users, folder); - }, - - getShares: function Api_getShares(onComplete) { - return this._getShareGenerator.async(this, onComplete); - }, - - _getShareGenerator: function Api__getShareGenerator() { - let self = yield; - let id = ID.get(this._dav.identity); - - this._dav.formPost("/api/share/get.php", ("uid=" + escape(id.username) + - "&password=" + escape(id.password)), - self.cb); - let xhr = yield; - self.done(xhr.responseText); - }, - - _shareGenerator: function Api__shareGenerator(path, users, folder) { - let self = yield; - let id = ID.get(this._dav.identity); - - let cmd = {"version" : 1, - "directory" : path, - "share_to_users" : users}; - let jsonSvc = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - let json = jsonSvc.encode(cmd); - - this._dav.formPost("/api/share/", - ("cmd=" + escape(json) + - "&uid=" + escape(id.username) + - "&password=" + escape(id.password) + - "&name=" + escape(folder)), - self.cb); - let xhr = yield; - - let retval; - - if (xhr.status == 200) { - if (xhr.responseText == "OK") { - retval = {wasSuccessful: true}; - } else { - retval = {wasSuccessful: false, - errorText: xhr.responseText}; - } - } else { - retval = {wasSuccessful: false, - errorText: "Server returned status " + xhr.status}; - } - - self.done(retval); - } -}; - -Sharing = { - Api: Api -}; diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 0390ff375b87..a6a7de8fb7e3 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -78,7 +78,6 @@ Tracker.prototype = { this._score = 0; this._ignored = []; this.loadChangedIDs(); - this.enable(); }, /* From e7706a253820336f43524aeac4be1268380190da Mon Sep 17 00:00:00 2001 From: Date: Wed, 14 Jan 2009 11:23:08 -0800 Subject: [PATCH 0852/1860] Added callback to Weave.Service.onStartup, which the Fennec UI uses to notify user that Weave has connected, that it ran into an error, or that it's awaiting configuration. Started moving fennec preferences from HTML to XUL. --- services/sync/modules/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e17fa623a305..d873ba2585ee 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -313,8 +313,8 @@ WeaveSvc.prototype = { } catch (e) {} self.done(); }, - onStartup: function WeaveSvc_onStartup() { - this._onStartup.async(this); + onStartup: function WeaveSvc_onStartup(callback) { + this._onStartup.async(this, callback); }, _initLogs: function WeaveSvc__initLogs() { From f4752789b191f09cedc05395215df0e3d4cc0af4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 14 Jan 2009 22:01:04 -0800 Subject: [PATCH 0853/1860] add some extra log info to engine; have tracker ignore all changes when applying a change to prevent the tracker from generating new guids for new items before the store has a chance to set the right one --- services/sync/modules/engines.js | 16 +++++++++++----- services/sync/modules/engines/history.js | 3 +-- services/sync/modules/trackers.js | 5 +++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 069e0405a043..89767eec33c9 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -296,13 +296,16 @@ SyncEngine.prototype = { yield newitems.get(self.cb); let item; + let count = {applied: 0, reconciled: 0}; this._lastSyncTmp = 0; while ((item = yield newitems.iter.next(self.cb))) { this._lowMemCheck(); yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); - if (yield this._reconcile.async(this, self.cb, item)) + if (yield this._reconcile.async(this, self.cb, item)) { + count.applied++; yield this._applyIncoming.async(this, self.cb, item); - else { + } else { + count.reconciled++; this._log.trace("Skipping reconciled incoming item " + item.id); if (this._lastSyncTmp < item.modified) this._lastSyncTmp = item.modified; @@ -311,6 +314,9 @@ SyncEngine.prototype = { if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; + this._log.info("Applied " + count.applied + " records, reconciled " + + count.reconciled + " records"); + // try to free some memory this._store.cache.clear(); Cu.forceGC(); @@ -387,7 +393,7 @@ SyncEngine.prototype = { let self = yield; this._log.trace("Incoming:\n" + item); try { - this._tracker.ignoreID(item.id); + this._tracker.ignoreAll = true; yield this._store.applyIncoming(self.cb, item); if (this._lastSyncTmp < item.modified) this._lastSyncTmp = item.modified; @@ -395,7 +401,7 @@ SyncEngine.prototype = { this._log.warn("Error while applying incoming record: " + (e.message? e.message : e)); } finally { - this._tracker.unignoreID(item.id); + this._tracker.ignoreAll = false; } }, @@ -434,7 +440,7 @@ SyncEngine.prototype = { } } - this._log.debug("Uploading " + outnum + " records + " + count + " index/depth records)"); + this._log.info("Uploading " + outnum + " records + " + count + " index/depth records)"); // do the upload yield up.post(self.cb); diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index fff12323de78..1df9938996e0 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -414,8 +414,7 @@ HistoryTracker.prototype = { // FIXME: very roundabout way of getting url -> guid mapping! // FIXME2: not updated after startup - let store = new HistoryStore(); - let all = store.getAllIDs(); + let all = this._store.getAllIDs(); this._all = {}; for (let guid in all) { this._all[all[guid]] = guid; diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index a6a7de8fb7e3..0bd738d4fbe2 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -77,6 +77,7 @@ Tracker.prototype = { this._log = Log4Moz.repository.getLogger(this._logName); this._score = 0; this._ignored = []; + this.ignoreAll = false; this.loadChangedIDs(); }, @@ -171,7 +172,7 @@ Tracker.prototype = { this._log.warn("Attempted to add undefined ID to tracker"); return false; } - if (id in this._ignored) + if (this.ignoreAll || (id in this._ignored)) return false; if (!this.changedIDs[id]) { this._log.debug("Adding changed ID " + id); @@ -186,7 +187,7 @@ Tracker.prototype = { this._log.warn("Attempted to remove undefined ID to tracker"); return false; } - if (id in this._ignored) + if (this.ignoreAll || (id in this._ignored)) return false; if (this.changedIDs[id]) { this._log.debug("Removing changed ID " + id); From 5dd61aa3db30dea86aec262af4e8404a415427de Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 15 Jan 2009 13:58:59 -0800 Subject: [PATCH 0854/1860] Bug 468671: Don't synchronize livemarks. Based on patch by Jorge Alves --- services/sync/modules/engines/bookmarks.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 93516285e65c..e0bbc8a456ca 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -634,6 +634,13 @@ BookmarksTracker.prototype = { return bms; }, + get _ls() { + let ls = Cc["@mozilla.org/browser/livemark-service;2"]. + getService(Ci.nsILivemarkService); + this.__defineGetter__("_ls", function() ls); + return ls; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]), _init: function BMT__init() { @@ -672,6 +679,8 @@ BookmarksTracker.prototype = { }, onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { + if (this._ls.isLivemark(folder)) + return; this._log.trace("onItemAdded: " + itemId); this._all[itemId] = this._bms.getItemGUID(itemId); if (this.addChangedID(this._all[itemId])) @@ -679,6 +688,8 @@ BookmarksTracker.prototype = { }, onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { + if (this._ls.isLivemark(folder)) + return; this._log.trace("onItemRemoved: " + itemId); if (this.addChangedID(this._all[itemId])) this._upScore(); @@ -686,6 +697,8 @@ BookmarksTracker.prototype = { }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { + if (this._ls.isLivemark(this._bms.getFolderIdForItem(itemId)) + return; this._log.trace("onItemChanged: " + itemId + (", " + property + (isAnno? " (anno)" : "")) + (value? (" = \"" + value + "\"") : "")); From 2f10695341a0e9653fb2a1223da43b7b48fb8411 Mon Sep 17 00:00:00 2001 From: Date: Thu, 15 Jan 2009 14:05:50 -0800 Subject: [PATCH 0855/1860] Added a method to historyStore called tempTableExists which does a SELECT (rather than using tableExists) to correctly tell whether a temp table is there or not, so that historyEngine will work correctly on both Fennec and Firefox (See bug 472963) --- services/sync/modules/engines.js | 2 +- services/sync/modules/engines/history.js | 40 +++++++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 069e0405a043..1a29d9f31330 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -143,7 +143,7 @@ Engine.prototype = { this._osPrefix = "weave:" + this.name + ":"; this._tracker; // initialize tracker to load previously changed IDs - + dump(this.name + "engine initialized.\n"); this._log.debug("Engine initialized"); }, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index fff12323de78..7914aa250f05 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -61,7 +61,16 @@ HistoryEngine.prototype = { displayName: "History", logName: "History", _storeObj: HistoryStore, - _trackerObj: HistoryTracker + _trackerObj: HistoryTracker, + // TODO the following overridden method has been defined just to facilitate + // debugging of tempTableExists. Once that debugging is done, this should + // be deleted. + _syncFinish: function HistoryEngine__syncFinish(error) { + let self = yield; + this._log.debug("Finishing up sync"); + this._log.debug(this._store._pidStm); + this._tracker.resetScore(); + } }; function HistoryStore() { @@ -123,10 +132,33 @@ HistoryStore.prototype = { } }, + tempTableExists: function HistStore__tempTableExists(tableName) { + // Check for table existance manually + // (just using this._db.tableExists() gives us false negatives on + // Firefox for temp tables.) + let statement = this._db.createStatement( + "SELECT count(*) as count FROM sqlite_temp_master WHERE type='table' " + + "AND name='" + tableName + "'" + ); + dump("Checking if temp table " + tableName + " exists.\n"); + this._log.debug("Checking if temp table " + tableName + " exists."); + statement.step(); + let num = statement.row["count"]; + if (num == 0) { + this._log.debug("No: the table does not exist."); + dump("No: the table does not exist.\n"); + return false; + } else { + this._log.debug("Yes: the table exists."); + dump("Yes: the table exists.\n"); + return true; + } + }, + get _visitStm() { this._log.trace("Creating SQL statement: _visitStm"); let stm; - if (this._db.tableExists("moz_historyvisits_temp")) { + if (this.tempTableExists("moz_historyvisits_temp")) { stm = this._db.createStatement( "SELECT * FROM ( " + "SELECT visit_type AS type, visit_date AS date " + @@ -161,7 +193,7 @@ HistoryStore.prototype = { this._log.trace("Creating SQL statement: _pidStm"); let stm; // See comment in get _urlStm() - if (this._db.tableExists("moz_places_temp")) { + if (this.tempTableExists("moz_places_temp")) { stm = this._db.createStatement( "SELECT * FROM " + "(SELECT id FROM moz_places_temp WHERE url = :url LIMIT 1) " + @@ -190,7 +222,7 @@ HistoryStore.prototype = { * that table. */ let stm; - if (this._db.tableExists("moz_places_temp")) { + if (this.tempTableExists("moz_places_temp")) { stm = this._db.createStatement( "SELECT * FROM " + "(SELECT url,title FROM moz_places_temp WHERE id = :id LIMIT 1) " + From ed18a6d7737bef99f88379ad3c16833328a177cb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 15 Jan 2009 15:53:34 -0800 Subject: [PATCH 0856/1860] fix typo --- services/sync/modules/engines/bookmarks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e0bbc8a456ca..bd87dbac32aa 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -697,7 +697,7 @@ BookmarksTracker.prototype = { }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { - if (this._ls.isLivemark(this._bms.getFolderIdForItem(itemId)) + if (this._ls.isLivemark(this._bms.getFolderIdForItem(itemId))) return; this._log.trace("onItemChanged: " + itemId + (", " + property + (isAnno? " (anno)" : "")) + From 14627daf17b5f44ddd3ca247ad95fcdfd0085f15 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 15 Jan 2009 18:43:44 -0800 Subject: [PATCH 0857/1860] fix url vs uri typo --- services/sync/modules/engines/history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 461807d13d81..6206c957f226 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -328,7 +328,7 @@ HistoryStore.prototype = { changeItemID: function HStore_changeItemID(oldID, newID) { try { - let uri = Utils.makeURI(this._findURLByGUID(oldID).uri); + let uri = Utils.makeURI(this._findURLByGUID(oldID).url); this._anno.setPageAnnotation(uri, "weave/guid", newID, 0, 0); } catch (e) { this._log.debug("Could not change item ID: " + e); From 79b8f6430ab05b6c373001418ceca175b493d43f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 20 Jan 2009 13:13:31 -0800 Subject: [PATCH 0858/1860] simpler reconciliation for history --- services/sync/modules/engines/history.js | 106 +++++++++++++---------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 6206c957f226..3557bb81888b 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -62,14 +62,39 @@ HistoryEngine.prototype = { logName: "History", _storeObj: HistoryStore, _trackerObj: HistoryTracker, - // TODO the following overridden method has been defined just to facilitate - // debugging of tempTableExists. Once that debugging is done, this should - // be deleted. - _syncFinish: function HistoryEngine__syncFinish(error) { + + // History reconciliation is simpler than the default one from SyncEngine, + // because we have the advantage that we can use the URI as a definitive + // check for local existence of incoming items. The steps are as follows: + // + // 1) Check for the same item in the locally modified list. In that case, + // local trumps remote. This step is unchanged from our superclass. + // 2) Check if the incoming item was deleted. Skip if so. + // 3) Apply new record/update. + // + // Note that we don't attempt to equalize the IDs, the history store does that + // as part of update() + _reconcile: function HistEngine__reconcile(item) { let self = yield; - this._log.debug("Finishing up sync"); - this._log.debug(this._store._pidStm); - this._tracker.resetScore(); + let ret = true; + + // Step 1: Check for conflicts + // If same as local record, do not upload + if (item.id in this._tracker.changedIDs) { + if (this._isEqual(item)) + this._tracker.removeChangedID(item.id); + self.done(false); + return; + } + + // Step 2: Check if the item is deleted - we don't support that (yet?) + if (item.cleartext == null) { + self.done(false); + return; + } + + // Step 3: Apply update/new record + self.done(true); } }; @@ -132,33 +157,21 @@ HistoryStore.prototype = { } }, - tempTableExists: function HistStore__tempTableExists(tableName) { - // Check for table existance manually - // (just using this._db.tableExists() gives us false negatives on - // Firefox for temp tables.) + // Check for table existance manually because + // mozIStorageConnection.tableExists() gives us false negatives + tableExists: function HistStore__tableExists(tableName) { let statement = this._db.createStatement( "SELECT count(*) as count FROM sqlite_temp_master WHERE type='table' " + - "AND name='" + tableName + "'" - ); - dump("Checking if temp table " + tableName + " exists.\n"); - this._log.debug("Checking if temp table " + tableName + " exists."); + "AND name='" + tableName + "'"); statement.step(); let num = statement.row["count"]; - if (num == 0) { - this._log.debug("No: the table does not exist."); - dump("No: the table does not exist.\n"); - return false; - } else { - this._log.debug("Yes: the table exists."); - dump("Yes: the table exists.\n"); - return true; - } + return num != 0; }, get _visitStm() { this._log.trace("Creating SQL statement: _visitStm"); let stm; - if (this.tempTableExists("moz_historyvisits_temp")) { + if (this.tableExists("moz_historyvisits_temp")) { stm = this._db.createStatement( "SELECT * FROM ( " + "SELECT visit_type AS type, visit_date AS date " + @@ -193,7 +206,7 @@ HistoryStore.prototype = { this._log.trace("Creating SQL statement: _pidStm"); let stm; // See comment in get _urlStm() - if (this.tempTableExists("moz_places_temp")) { + if (this.tableExists("moz_places_temp")) { stm = this._db.createStatement( "SELECT * FROM " + "(SELECT id FROM moz_places_temp WHERE url = :url LIMIT 1) " + @@ -222,7 +235,7 @@ HistoryStore.prototype = { * that table. */ let stm; - if (this.tempTableExists("moz_places_temp")) { + if (this.tableExists("moz_places_temp")) { stm = this._db.createStatement( "SELECT * FROM " + "(SELECT url,title FROM moz_places_temp WHERE id = :id LIMIT 1) " + @@ -359,38 +372,37 @@ HistoryStore.prototype = { }, create: function HistStore_create(record) { - this._log.debug(" -> creating history entry: " + record.cleartext.uri); - - let uri = Utils.makeURI(record.cleartext.uri); - let visit; - while ((visit = record.cleartext.visits.pop())) { - this._log.debug(" visit " + visit.date); - this._hsvc.addVisit(uri, visit.date, null, visit.type, - (visit.type == 5 || visit.type == 6), 0); - } - this._hsvc.setPageTitle(uri, record.cleartext.title); - - let guid = this._getGUID(record.cleartext.uri); - if (guid != record.id) - this.changeItemID(guid, record.id); + this.update(record); }, remove: function HistStore_remove(record) { - this._log.trace(" -> NOT removing history entry: " + record.id); + //this._log.trace(" -> NOT removing history entry: " + record.id); // FIXME: implement! }, - // FIXME: skip already-existing visits, places will add duplicates... update: function HistStore_update(record) { - this._log.trace(" -> editing history entry: " + record.cleartext.uri); + this._log.trace(" -> processing history entry: " + record.cleartext.uri); + let uri = Utils.makeURI(record.cleartext.uri); + let curvisits = []; + if (this.urlExists(uri)) + curvisits = this._getVisits(record.cleartext.uri); + let visit; while ((visit = record.cleartext.visits.pop())) { + if (curvisits.filter(function(i) i.date == visit.date).length) + continue; this._log.debug(" visit " + visit.date); this._hsvc.addVisit(uri, visit.date, null, visit.type, (visit.type == 5 || visit.type == 6), 0); } this._hsvc.setPageTitle(uri, record.cleartext.title); + + // Equalize IDs + let localId = this._getGUID(record.cleartext.uri); + if (localId != record.id) + this.changeItemID(localId, record.id); + }, itemExists: function HistStore_itemExists(id) { @@ -399,6 +411,12 @@ HistoryStore.prototype = { return false; }, + urlExists: function HistStore_urlExists(url) { + if (typeof(url) == "string") + url = Utils.makeURI(url); + return this._hsvc.isVisited(url); + }, + createRecord: function HistStore_createRecord(guid) { let foo = this._findURLByGUID(guid); let record = new HistoryRec(); From 43bb576a4e6e1f09b87513813d54555ab8c02bed Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 21 Jan 2009 19:02:38 -0800 Subject: [PATCH 0859/1860] cast modified timestamp to an int if it's a string --- services/sync/modules/base_records/wbo.js | 6 +++++- services/sync/modules/engines.js | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index e2046c8eaeac..fa9bd5e18cec 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -76,7 +76,11 @@ WBORecord.prototype = { this.data.parentid = value; }, - get modified() this.data.modified, + get modified() { + if (typeof(this.data.modified) == "string") + this.data.modified = parseInt(this.data.modified); + return this.data.modified; + }, set modified(value) { this.data.modified = value; }, diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 6ba75a96ab89..9a244ef2353b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -446,6 +446,8 @@ SyncEngine.prototype = { // save last modified date let mod = up.data.modified; + if (typeof(mod) == "string") + mod = parseInt(mod); if (mod > this.lastSync) this.lastSync = mod; } From dda2da0b67b824a2e19cc01d339c78c2194db707 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 21 Jan 2009 19:04:13 -0800 Subject: [PATCH 0860/1860] wrap microsummary creation code in try/catch; catch unknown items during onItemMoved (should not happen, but...) --- services/sync/modules/engines/bookmarks.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index bd87dbac32aa..e2e308ddd3b7 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -375,14 +375,18 @@ BookmarksStore.prototype = { this._ans.EXPIRE_NEVER); break; case "generatorURI": { - let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); - let genURI = Utils.makeURI(record.cleartext.generatorURI); - if (this._ms == SERVICE_NOT_SUPPORTED) { - this._log.warn("Can't create microsummary -- not supported."); - } else { - let micsum = this._ms.createMicrosummary(micsumURI, genURI); - this._ms.setMicrosummary(itemId, micsum); - } + try { + let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); + let genURI = Utils.makeURI(record.cleartext.generatorURI); + if (this._ms == SERVICE_NOT_SUPPORTED) { + this._log.warn("Can't create microsummary -- not supported."); + } else { + let micsum = this._ms.createMicrosummary(micsumURI, genURI); + this._ms.setMicrosummary(itemId, micsum); + } + } catch (e) { + this._log.debug("Could not set microsummary generator URI: " + e); + } } break; case "siteURI": this._ls.setSiteURI(itemId, Utils.makeURI(record.cleartext.siteURI)); @@ -713,6 +717,8 @@ BookmarksTracker.prototype = { onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { this._log.trace("onItemMoved: " + itemId); + if (!this._all[itemId]) + this._all[itemId] = this._bms.itemGUID(itemId); if (this.addChangedID(this._all[itemId])) this._upScore(); }, From b2b7f84119e1a7bc227550994c86e64c5964ece1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 22 Jan 2009 11:48:36 -0800 Subject: [PATCH 0861/1860] move Observers.js and Preferences.js into an ext/ directory, to make it clear they are externally-mantained libraries --- services/sync/modules/auth.js | 2 -- .../sync/modules/base_records/collection.js | 2 -- services/sync/modules/base_records/crypto.js | 2 -- services/sync/modules/base_records/keys.js | 2 -- services/sync/modules/{ => ext}/Observers.js | 0 .../sync/modules/{ => ext}/Preferences.js | 0 services/sync/modules/notifications.js | 2 +- services/sync/modules/resource.js | 20 +++++++++---------- 8 files changed, 11 insertions(+), 19 deletions(-) rename services/sync/modules/{ => ext}/Observers.js (100%) rename services/sync/modules/{ => ext}/Preferences.js (100%) diff --git a/services/sync/modules/auth.js b/services/sync/modules/auth.js index 802d3d473673..a66063c1cf8e 100644 --- a/services/sync/modules/auth.js +++ b/services/sync/modules/auth.js @@ -41,8 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/Observers.js"); -Cu.import("resource://weave/Preferences.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 3f69b594ff5c..45c9df9fca2c 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -41,8 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/Observers.js"); -Cu.import("resource://weave/Preferences.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index ba0fe43a3aae..1fd0848a958b 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -41,8 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/Observers.js"); -Cu.import("resource://weave/Preferences.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index a627812e44d5..d42e587497ce 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -42,8 +42,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/Observers.js"); -Cu.import("resource://weave/Preferences.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/modules/Observers.js b/services/sync/modules/ext/Observers.js similarity index 100% rename from services/sync/modules/Observers.js rename to services/sync/modules/ext/Observers.js diff --git a/services/sync/modules/Preferences.js b/services/sync/modules/ext/Preferences.js similarity index 100% rename from services/sync/modules/Preferences.js rename to services/sync/modules/ext/Preferences.js diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index c3cda4fb6ab6..17bfdff0b393 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -42,7 +42,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/Observers.js"); +Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 628c4bf26e40..893113a21d8b 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -42,8 +42,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/Observers.js"); -Cu.import("resource://weave/Preferences.js"); +Cu.import("resource://weave/ext/Observers.js"); +Cu.import("resource://weave/ext/Preferences.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -190,14 +190,14 @@ Resource.prototype = { let self = yield; let iter = 0; let channel = this._createRequest(); - + if ("undefined" != typeof(data)) this._data = data; if ("PUT" == action || "POST" == action) { yield this.filterUpload(self.cb); this._log.trace(action + " Body:\n" + this._data); - + let upload = channel.QueryInterface(Ci.nsIUploadChannel); let iStream = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); @@ -216,7 +216,7 @@ Resource.prototype = { this._log.debug("Error response: \n" + this._data); throw new RequestException(this, action, channel); } else { - this._log.debug(channel.requestMethod + " request successful (" + + this._log.debug(channel.requestMethod + " request successful (" + channel.responseStatus + ")"); switch (action) { @@ -233,7 +233,7 @@ Resource.prototype = { break; } } - + self.done(this._data); }, @@ -293,7 +293,7 @@ RecordParser.prototype = { let start; let bCount = 0; let done = false; - + for (let i = 1; i < this._data.length; i++) { if (this._data[i] == '{') { if (bCount == 0) @@ -304,17 +304,17 @@ RecordParser.prototype = { if (bCount == 0) done = true; } - + if (done) { let ret = this._data.substring(start, i + 1); this._data = this._data.substring(i + 1); return ret; } } - + return false; }, - + append: function RecordParser_append(data) { this._data += data; } From 27685f5d0a04d7290bc2ecc3261f80c2c596a290 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 23 Jan 2009 15:08:12 -0800 Subject: [PATCH 0862/1860] WBO toString will serialize 'payload', CryptoWrapper overrides that and will serialize 'cleartext' --- services/sync/modules/base_records/crypto.js | 8 ++++++++ services/sync/modules/base_records/wbo.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 1fd0848a958b..230ccb920d98 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -122,6 +122,14 @@ CryptoWrapper.prototype = { }, decrypt: function CryptoWrap_decrypt(onComplete, passphrase) { this._decrypt.async(this, onComplete, passphrase); + }, + + toString: function WBORec_toString() { + return "{ id: " + this.id + "\n" + + " parent: " + this.parentid + "\n" + + " depth: " + this.depth + ", index: " + this.sortindex + "\n" + + " modified: " + this.modified + "\n" + + " payload: " + json.encode(this.cleartext) + " }"; } }; diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index fa9bd5e18cec..6f0e5d8cd9e9 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -113,7 +113,7 @@ WBORecord.prototype = { " parent: " + this.parentid + "\n" + " depth: " + this.depth + ", index: " + this.sortindex + "\n" + " modified: " + this.modified + "\n" + - " payload: " + json.encode(this.cleartext) + " }"; + " payload: " + json.encode(this.payload) + " }"; } }; From cba18395327c10c6c29e992d1411d4aca76d4153 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 23 Jan 2009 15:09:21 -0800 Subject: [PATCH 0863/1860] make it so clients list can be modified from any client, and so it doesn't upload client info on every sync; clients list no longer stores data (except the local client's guid) in the firefox prefs --- services/sync/modules/engines/clients.js | 138 +++++++++++++---------- 1 file changed, 79 insertions(+), 59 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index a2d12918e581..c34f31eaf0f3 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -42,7 +42,9 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/ext/Preferences.js"); Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); @@ -55,7 +57,7 @@ Function.prototype.async = Async.sugar; Utils.lazy(this, 'Clients', ClientEngine); function ClientEngine() { - this._init(); + this._ClientEngine_init(); } ClientEngine.prototype = { __proto__: SyncEngine.prototype, @@ -64,7 +66,60 @@ ClientEngine.prototype = { logName: "Clients", _storeObj: ClientStore, _trackerObj: ClientTracker, - _recordObj: ClientRecord + _recordObj: ClientRecord, + + _ClientEngine_init: function ClientEngine__init() { + this._init(); + if (!this.getInfo(this.clientID)) + this.setInfo(this.clientID, {name: "Firefox", type: "desktop"}); + }, + + // Override SyncEngine's to not set encryption URL + _createRecord: function SyncEngine__createRecord(id) { + let record = this._store.createRecord(id); + record.uri = this.engineURL + id; + return record; + }, + + // get and set info for clients + + // FIXME: callers must use the setInfo interface or changes won't get synced, + // which is unintuitive + + getClients: function ClientEngine_getClients() { + return this._store.clients; + }, + + getInfo: function ClientEngine_getInfo(id) { + return this._store.getInfo(id); + }, + + setInfo: function ClientEngine_setInfo(id, info) { + this._store.setInfo(id, info); + this._tracker.addChangedID(id); + }, + + // helpers for getting/setting this client's info directly + + get clientID() { + if (!Preferences.get(PREFS_BRANCH + "client.GUID")) + Preferences.set(PREFS_BRANCH + "client.GUID", Utils.makeGUID()); + return Preferences.get(PREFS_BRANCH + "client.GUID"); + }, + + get clientName() { return this.getInfo(this.clientID).name; }, + set clientName(value) { + let info = this.getInfo(this.clientID); + info.name = value; + this.setInfo(this.clientID, info); + }, + + get clientType() { return this.getInfo(this.clientID).type; }, + set clientType(value) { + let info = this.getInfo(this.clientID); + info.type = value; + this.setInfo(this.clientID, info); + } }; function ClientStore() { @@ -74,33 +129,26 @@ ClientStore.prototype = { __proto__: Store.prototype, _logName: "Clients.Store", - get GUID() this._getCharPref("client.GUID", function() Utils.makeGUID()), - set GUID(value) Utils.prefs.setCharPref("client.GUID", value), - - get name() this._getCharPref("client.name", function() "Firefox"), - set name(value) Utils.prefs.setCharPref("client.name", value), - - get type() this._getCharPref("client.type", function() "desktop"), - set type(value) Utils.prefs.setCharPref("client.type", value), - - // FIXME: use Preferences module instead - _getCharPref: function ClientData__getCharPref(pref, defaultCb) { - let value; - try { - value = Utils.prefs.getCharPref(pref); - } catch (e) { - value = defaultCb(); - Utils.prefs.setCharPref(pref, value); - } - return value; - }, - _ClientStore_init: function ClientStore__init() { this._init.call(this); this.clients = {}; this.loadSnapshot(); }, + // get/set client info; doesn't use records like the methods below + // NOTE: setInfo will not update tracker. Use Engine.setInfo for that + + getInfo: function ClientStore_getInfo(id) { + return this.clients[id]; + }, + + setInfo: function ClientStore_setInfo(id, info) { + this.clients[id] = info; + this.saveSnapshot(); + }, + + // load/save clients list from/to disk + saveSnapshot: function ClientStore_saveSnapshot() { this._log.debug("Saving client list to disk"); let file = Utils.getProfileFile( @@ -132,13 +180,13 @@ ClientStore.prototype = { this.update(record); }, - remove: function ClientStore_remove(record) { - delete this.clients[record.id]; + update: function ClientStore_update(record) { + this.clients[record.id] = record.payload; this.saveSnapshot(); }, - update: function ClientStore_update(record) { - this.clients[record.id] = record.payload; + remove: function ClientStore_remove(record) { + delete this.clients[record.id]; this.saveSnapshot(); }, @@ -156,9 +204,6 @@ ClientStore.prototype = { // methods to query local data: getAllIDs, itemExists, createRecord getAllIDs: function ClientStore_getAllIDs() { - // make sure we always return at least our GUID (e.g., before syncing - // when the client list is empty); - this.clients[this.GUID] = true; return this.clients; }, @@ -166,12 +211,9 @@ ClientStore.prototype = { return id in this.clients; }, - createRecord: function ClientStore_createRecord(guid) { + createRecord: function ClientStore_createRecord(id) { let record = new ClientRecord(); - if (guid == this.GUID) - record.payload = {name: this.name, type: this.type}; - else - record.payload = this.clients[guid]; + record.payload = this.clients[id]; return record; } }; @@ -182,28 +224,6 @@ function ClientTracker() { ClientTracker.prototype = { __proto__: Tracker.prototype, _logName: "ClientTracker", - - // we always want to sync, but are not dying to do so - get score() "75", - - // Override Tracker's _init to disable loading changed IDs from disk - _init: function ClientTracker__init() { - this._log = Log4Moz.repository.getLogger(this._logName); - this._score = 0; - this._ignored = []; - this._store = new ClientStore(); // FIXME: hack - }, - - // always upload our record - // FIXME: we should compare against the store's snapshot instead - get changedIDs() { - let items = {}; - items[this._store.GUID] = true; - return items; - }, - - // clobber these just in case anything tries to call them - addChangedID: function(id) {}, - removeChangedID: function(id) {}, - clearChangedIDs: function() {} + file: "clients", + get score() "75" // we always want to sync, but are not dying to do so }; From 622ea47d49d13202c16282d4d151f77642f34ac3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sat, 24 Jan 2009 16:49:23 -0800 Subject: [PATCH 0864/1860] Bug 471076: make notify wrapper catch exceptions; don't use catchAll wrapper in service, and check return value of login/verifyLogin --- services/sync/modules/service.js | 26 ++++++++++---------------- services/sync/modules/wrap.js | 3 +-- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d873ba2585ee..7379c423668a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -268,7 +268,7 @@ WeaveSvc.prototype = { } else { this._log.info("Running scheduled sync"); this._notify("sync", "", - this._catchAll(this._localLock(this._sync))).async(this); + this._localLock(this._sync)).async(this); } } }, @@ -308,8 +308,8 @@ WeaveSvc.prototype = { if (Utils.prefs.getBoolPref("autoconnect") && this.username && this.username != 'nobody') try { - yield this.login(self.cb); - yield this.sync(self.cb); + if (yield this.login(self.cb)) + yield this.sync(self.cb); } catch (e) {} self.done(); }, @@ -423,6 +423,7 @@ WeaveSvc.prototype = { yield res.get(self.cb); if (res.data != "\"1\"") throw "Login failed"; + self.done(true); }; this._notify("verify-login", "", fn).async(this, onComplete); }, @@ -473,18 +474,12 @@ WeaveSvc.prototype = { this._log.debug("Logging in user " + this.username); - try { - yield this.verifyLogin(self.cb, this.username, this.password); - this._loggedIn = true; - self.done(true); - } catch (e) { - this._loggedIn = false; - self.done(false); - } + if (!(yield this.verifyLogin(self.cb, this.username, this.password))) + throw "Login failed"; + this._loggedIn = true; + self.done(true); }; - this._localLock( - this._catchAll( - this._notify("login", "", fn))).async(this, onComplete); + this._localLock(this._notify("login", "", fn)).async(this, onComplete); }, logout: function WeaveSvc_logout() { @@ -598,8 +593,7 @@ WeaveSvc.prototype = { } }, sync: function WeaveSvc_sync(onComplete) { - this._notify("sync", "", - this._catchAll(this._localLock(this._sync))).async(this, onComplete); + this._notify("sync", "", this._localLock(this._sync)).async(this, onComplete); }, // The values that engine scores must meet or exceed before we sync them diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 9ab894e3b89d..2e4f4d7cf5d7 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -102,8 +102,7 @@ let Wrap = { this._log.debug("Event: " + this._osPrefix + savedName + ":error"); this._os.notifyObservers(null, this._osPrefix + savedName + ":error", savedPayload); this._os.notifyObservers(null, this._osPrefix + "global:error", savedPayload); - if (e != "Could not acquire lock") // FIXME HACK - throw e; + ret = undefined; } self.done(ret); From c7b10edc87d9903b91ccbffe0d8e9715c7232cb8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 26 Jan 2009 10:00:00 -0800 Subject: [PATCH 0865/1860] add more flexible descriptions for login errors --- services/sync/locales/en-US/sync.properties | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 591bd9a17185..976961ce87f4 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -15,7 +15,9 @@ status.idle = Idle status.active = Working... error.login.title = Error While Signing In -error.login.description = Weave encountered an error while signing you in. Your username/password failed. Please try again. +error.login.description = Weave encountered an error while signing you in: %1. Please try again. +error.login.reason.password = Your username/password failed +error.login.reason.unknown = Unknown error error.logout.title = Error While Signing Out error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it (then why are we bugging you with this info?). error.sync.title = Error While Syncing From 977010f53db5b7410ee6d790b1589f0d5ab95098 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 27 Jan 2009 13:35:10 -0800 Subject: [PATCH 0866/1860] Make records not extend Resource, instead they have a serialize() method that can be used to create a Resource for uploading if necessary. Use global service instances under Svc.* Consolidate various record managers (caches/convenience factories) to extend the same base object Log exceptions caught by notify wrapper --- .../sync/modules/base_records/collection.js | 28 +--- services/sync/modules/base_records/crypto.js | 102 ++----------- services/sync/modules/base_records/keys.js | 119 ++++----------- services/sync/modules/base_records/wbo.js | 142 ++++++++++++++---- services/sync/modules/engines.js | 18 ++- services/sync/modules/engines/bookmarks.js | 3 +- services/sync/modules/resource.js | 24 +-- services/sync/modules/service.js | 3 +- services/sync/modules/type_records/clients.js | 12 +- services/sync/modules/util.js | 1 + services/sync/modules/wrap.js | 2 + 11 files changed, 188 insertions(+), 266 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 45c9df9fca2c..34837d60d41e 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -115,24 +115,8 @@ Collection.prototype = { return this._iter; }, - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return this._json; - }, - - pushRecord: function Coll_pushRecord(onComplete, record) { - let fn = function(record) { - let self = yield; - yield record.filterUpload(self.cb); // XXX EEK - this._data.push(this._json.decode(record.data)); // HACK HACK HACK - self.done(); - }; - fn.async(this, onComplete, record); - }, - - pushLiteral: function Coll_pushLiteral(object) { - this._data.push(this._json.encode(object)); + pushData: function Coll_pushData(data) { + this._data.push(data); }, clearRecords: function Coll_clearRecords() { @@ -160,11 +144,11 @@ CollectionIterator.prototype = { if (this._idx >= this.count) return; let item = this._coll.data[this._idx++]; - let wrap = new this._coll._recordObj(this._coll.uri.resolve(item.id)); - wrap.data = this._coll._json.encode(item); // HACK HACK HACK - yield wrap.filterDownload(self.cb); // XXX EEK + let record = new this._coll._recordObj(); + record.deserialize(Svc.Json.encode(item)); // FIXME: inefficient + record.baseUri = this._coll.uri; - self.done(wrap); + self.done(record); }; fn.async(this, onComplete); }, diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 230ccb920d98..47f78b1bbe13 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -50,12 +50,6 @@ Cu.import("resource://weave/base_records/keys.js"); Function.prototype.async = Async.sugar; -Utils.lazy(this, 'CryptoMetas', RecordManager); - -// fixme: global, ugh -let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); -let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"].createInstance(Ci.IWeaveCrypto); - function CryptoWrapper(uri) { this._CryptoWrap_init(uri); } @@ -96,7 +90,8 @@ CryptoWrapper.prototype = { let meta = yield CryptoMetas.get(self.cb, this.encryption); let symkey = yield meta.getKey(self.cb, privkey, passphrase); - this.ciphertext = crypto.encrypt(json.encode([this.cleartext]), symkey, meta.bulkIV); + this.ciphertext = Svc.Crypto.encrypt(Svc.Json.encode([this.cleartext]), + symkey, meta.bulkIV); this.cleartext = null; self.done(); @@ -115,7 +110,8 @@ CryptoWrapper.prototype = { let symkey = yield meta.getKey(self.cb, privkey, passphrase); // note: payload is wrapped in an array, see _encrypt - this.cleartext = json.decode(crypto.decrypt(this.ciphertext, symkey, meta.bulkIV))[0]; + this.cleartext = Svc.Json.decode(Svc.Crypto.decrypt(this.ciphertext, + symkey, meta.bulkIV))[0]; this.ciphertext = null; self.done(this.cleartext); @@ -129,7 +125,7 @@ CryptoWrapper.prototype = { " parent: " + this.parentid + "\n" + " depth: " + this.depth + ", index: " + this.sortindex + "\n" + " modified: " + this.modified + "\n" + - " payload: " + json.encode(this.cleartext) + " }"; + " payload: " + Svc.Json.encode(this.cleartext) + " }"; } }; @@ -149,7 +145,7 @@ CryptoMeta.prototype = { }, generateIV: function CryptoMeta_generateIV() { - this.bulkIV = crypto.generateRandomIV(); + this.bulkIV = Svc.Crypto.generateRandomIV(); }, get bulkIV() this.data.payload.bulkIV, @@ -170,7 +166,7 @@ CryptoMeta.prototype = { if (!wrapped_key) throw "keyring doesn't contain a key for " + pubkeyUri; - let ret = crypto.unwrapSymmetricKey(wrapped_key, privkey.keyData, + let ret = Svc.Crypto.unwrapSymmetricKey(wrapped_key, privkey.keyData, passphrase, privkey.salt, privkey.iv); self.done(ret); }, @@ -202,87 +198,17 @@ CryptoMeta.prototype = { } this.payload.keyring[new_pubkey.uri.spec] = - crypto.wrapSymmetricKey(symkey, new_pubkey.keyData); + Svc.Crypto.wrapSymmetricKey(symkey, new_pubkey.keyData); }, addUnwrappedKey: function CryptoMeta_addUnwrappedKey(onComplete, new_pubkey, symkey) { this._addUnwrappedKey.async(this, onComplete, new_pubkey, symkey); } }; -function RecordManager() { - this._init(); -} -RecordManager.prototype = { - _recordType: CryptoMeta, - _logName: "RecordMgr", +Utils.lazy(this, 'CryptoMetas', CryptoRecordManager); - _init: function RegordMgr__init() { - this._log = Log4Moz.repository.getLogger(this._logName); - this._records = {}; - this._aliases = {}; - }, - - _import: function RegordMgr__import(url) { - let self = yield; - let rec; - - this._log.trace("Importing record: " + (url.spec? url.spec : url)); - - try { - rec = new this._recordType(url); - yield rec.get(self.cb); - this.set(url, rec); - } catch (e) { - this._log.debug("Failed to import record: " + e); - rec = null; - } - self.done(rec); - }, - import: function RegordMgr_import(onComplete, url) { - this._import.async(this, onComplete, url); - }, - - _get: function RegordMgr__get(url) { - let self = yield; - - let rec = null; - if (url in this._aliases) - url = this._aliases[url]; - if (url in this._records) - rec = this._records[url]; - - if (!rec) - rec = yield this.import(self.cb, url); - - self.done(rec); - }, - get: function RegordMgr_get(onComplete, url) { - this._get.async(this, onComplete, url); - }, - - set: function RegordMgr_set(url, record) { - this._records[url] = record; - }, - - contains: function RegordMgr_contains(url) { - let record = null; - if (url in this._aliases) - url = this._aliases[url]; - if (url in this._records) - return true; - return false; - }, - - del: function RegordMgr_del(url) { - delete this._records[url]; - }, - getAlias: function RegordMgr_getAlias(alias) { - return this._aliases[alias]; - }, - setAlias: function RegordMgr_setAlias(url, alias) { - this._aliases[alias] = url; - }, - delAlias: function RegordMgr_delAlias(alias) { - delete this._aliases[alias]; - } -}; +function CryptoRecordManager() { this._init(); } +CryptoRecordManager.prototype = { + __proto__: RecordManager.prototype, + _recordType: CryptoMeta +}; \ No newline at end of file diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index d42e587497ce..e0bb738d7422 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -46,13 +46,11 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); Function.prototype.async = Async.sugar; -Utils.lazy(this, 'PubKeys', PubKeyManager); -Utils.lazy(this, 'PrivKeys', PrivKeyManager); - function PubKey(uri) { this._PubKey_init(uri); } @@ -168,47 +166,35 @@ SymKey.prototype = { } }; -function PubKeyManager() { - this._init(); -} +Utils.lazy(this, 'PubKeys', PubKeyManager); + +function PubKeyManager() { this._init(); } PubKeyManager.prototype = { - _keyType: PubKey, + __proto__: RecordManager.prototype, + _recordType: PubKey, _logName: "PubKeyManager", - _init: function KeyMgr__init() { - this._log = Log4Moz.repository.getLogger(this._logName); - this._keys = {}; - this._aliases = {}; - }, - - get _crypto() { - let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - getService(Ci.IWeaveCrypto); - this.__defineGetter__("_crypto", function() crypto); - return crypto; - }, - get defaultKeyUri() this._defaultKeyUri, set defaultKeyUri(value) { this._defaultKeyUri = value; }, - _getDefaultKey: function KeyMgr__getDefaultKey() { - let self = yield; - let ret = yield this.get(self.cb, this.defaultKeyUri); - self.done(ret); - }, getDefaultKey: function KeyMgr_getDefaultKey(onComplete) { - return this._getDefaultKey.async(this, onComplete); + let fn = function KeyMgr__getDefaultKey() { + let self = yield; + let ret = yield this.get(self.cb, this.defaultKeyUri); + self.done(ret); + }; + fn.async(this, onComplete); }, createKeypair: function KeyMgr_createKeypair(passphrase, pubkeyUri, privkeyUri) { this._log.debug("Generating RSA keypair"); let pubkey = new PubKey(); let privkey = new PrivKey(); - privkey.salt = this._crypto.generateRandomBytes(16); - privkey.iv = this._crypto.generateRandomIV(); + privkey.salt = Svc.Crypto.generateRandomBytes(16); + privkey.iv = Svc.Crypto.generateRandomIV(); let pub = {}, priv = {}; - this._crypto.generateKeypair(passphrase, privkey.salt, privkey.iv, pub, priv); + Svc.Crypto.generateKeypair(passphrase, privkey.salt, privkey.iv, pub, priv); [pubkey.keyData, privkey.keyData] = [pub.value, priv.value]; if (pubkeyUri) { @@ -224,74 +210,23 @@ PubKeyManager.prototype = { return {pubkey: pubkey, privkey: privkey}; }, - _import: function KeyMgr__import(url) { - let self = yield; - - this._log.trace("Importing key: " + (url.spec? url.spec : url)); - - try { - let key = new this._keyType(url); - yield key.get(self.cb); - this.set(url, key); - self.done(key); - } catch (e) { - this._log.debug("Failed to import key: " + e); - self.done(null); - } - }, - import: function KeyMgr_import(onComplete, url) { - this._import.async(this, onComplete, url); - }, - - _get: function KeyMgr__get(url) { - let self = yield; - - let key = null; - if (url in this._aliases) - url = this._aliases[url]; - if (url in this._keys) - key = this._keys[url]; - - if (!key) - key = yield this.import(self.cb, url); - - self.done(key); - }, - get: function KeyMgr_get(onComplete, url) { - this._get.async(this, onComplete, url); - }, - - set: function KeyMgr_set(url, key) { - this._keys[url] = key; - return key; - }, - - contains: function KeyMgr_contains(url) { - let key = null; - if (url in this._aliases) - url = this._aliases[url]; - if (url in this._keys) - return true; - return false; - }, - - del: function KeyMgr_del(url) { - delete this._keys[url]; - }, - getAlias: function KeyMgr_getAlias(alias) { - return this._aliases[alias]; - }, - setAlias: function KeyMgr_setAlias(url, alias) { - this._aliases[alias] = url; - }, - delAlias: function KeyMgr_delAlias(alias) { - delete this._aliases[alias]; + uploadKeypair: function KeyMgr_uploadKeypair(onComplete, keys) { + let fn = function KeyMgr__uploadKeypair(keys) { + let self = yield; + for each (let key in keys) { + let res = new Resource(key.uri); + yield res.put(self.cb, key.serialize()); + } + }; + fn.async(this, onComplete, keys); } }; +Utils.lazy(this, 'PrivKeys', PrivKeyManager); + function PrivKeyManager() { this._init(); } PrivKeyManager.prototype = { __proto__: PubKeyManager.prototype, - _keyType: PrivKey, + _recordType: PrivKey, _logName: "PrivKeyManager" }; diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 6f0e5d8cd9e9..3d55ca6ca13b 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['WBORecord']; +const EXPORTED_SYMBOLS = ['WBORecord', 'RecordManager']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -43,6 +43,7 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; @@ -51,27 +52,32 @@ function WBORecord(uri) { this._WBORec_init(uri); } WBORecord.prototype = { - __proto__: Resource.prototype, _logName: "Record.WBO", _WBORec_init: function WBORec_init(uri) { - this._init(uri); - this.pushFilter(new WBOFilter()); - this.pushFilter(new JsonFilter()); + this.baseUri = (typeof(uri) == "string")? Utils.makeURI(uri) : uri; this.data = { payload: {} }; }, - // id is special because it's based on the uri - get id() { - if (this.data.id) - return decodeURI(this.data.id); - let foo = this.uri.spec.split('/'); - return decodeURI(foo[foo.length-1]); + get id() { return decodeURI(this.data.id); }, + set id(value) { + this.data.id = encodeURI(value); }, - get parentid() this.data.parentid, + // NOTE: baseUri must have a trailing slash, or baseUri.resolve() will omit + // the collection name + get uri() { return Utils.makeURI(this.baseUri.resolve(this.id)); }, + set uri(value) { + if (typeof(value) != "string") + value = value.spec; + let foo = value.split('/'); + this.data.id = foo.pop(); + this.baseUri = Utils.makeURI(foo.join('/') + '/'); + }, + + get parentid() { return this.data.parentid; }, set parentid(value) { this.data.parentid = value; }, @@ -103,37 +109,113 @@ WBORecord.prototype = { this.data.sortindex = value; }, - get payload() this.data.payload, + get payload() { return this.data.payload; }, set payload(value) { this.data.payload = value; }, + // payload is encoded twice in serialized form, because the + // server expects a string + serialize: function WBORec_serialize() { + this.payload = Svc.Json.encode(this.payload); + let ret = Svc.Json.encode(this.data); + this.payload = Svc.Json.decode(this.payload); + return ret; + }, + + deserialize: function WBORec_deserialize(json) { + this.data = Svc.Json.decode(json); + this.payload = Svc.Json.decode(this.payload); + }, + toString: function WBORec_toString() { return "{ id: " + this.id + "\n" + " parent: " + this.parentid + "\n" + " depth: " + this.depth + ", index: " + this.sortindex + "\n" + " modified: " + this.modified + "\n" + - " payload: " + json.encode(this.payload) + " }"; + " payload: " + Svc.Json.encode(this.payload) + " }"; } }; -// fixme: global, ugh -let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); +function RecordManager() { + this._init(); +} +RecordManager.prototype = { + _recordType: WBORecord, + _logName: "RecordMgr", -function WBOFilter() {} -WBOFilter.prototype = { - beforePUT: function(data, wbo) { - let self = yield; - let foo = wbo.uri.spec.split('/'); - data.id = decodeURI(foo[foo.length-1]); - data.payload = json.encode(data.payload); - self.done(data); + _init: function RegordMgr__init() { + this._log = Log4Moz.repository.getLogger(this._logName); + this._records = {}; + this._aliases = {}; }, - afterGET: function(data, wbo) { - let self = yield; - let foo = wbo.uri.spec.split('/'); - data.id = decodeURI(foo[foo.length-1]); - data.payload = json.decode(data.payload); - self.done(data); + + import: function RegordMgr_import(onComplete, url) { + let fn = function RegordMgr__import(url) { + let self = yield; + let record; + + this._log.trace("Importing record: " + (url.spec? url.spec : url)); + + try { + let rsrc = new Resource(url); + yield rsrc.get(self.cb); + + record = new this._recordType(); + record.deserialize(rsrc.data); + record.uri = url; // NOTE: may override id in rsrc.data + + this.set(url, record); + } catch (e) { + this._log.debug("Failed to import record: " + e); + record = null; + } + self.done(record); + }; + fn.async(this, onComplete, url); + }, + + get: function RegordMgr_get(onComplete, url) { + let fn = function RegordMgr__get(url) { + let self = yield; + + let record = null; + if (url in this._aliases) + url = this._aliases[url]; + if (url in this._records) + record = this._records[url]; + + if (!record) + record = yield this.import(self.cb, url); + + self.done(record); + }; + fn.async(this, onComplete, url); + }, + + set: function RegordMgr_set(url, record) { + this._records[url] = record; + }, + + contains: function RegordMgr_contains(url) { + let record = null; + if (url in this._aliases) + url = this._aliases[url]; + if (url in this._records) + return true; + return false; + }, + + del: function RegordMgr_del(url) { + delete this._records[url]; + }, + getAlias: function RegordMgr_getAlias(alias) { + return this._aliases[alias]; + }, + setAlias: function RegordMgr_setAlias(url, alias) { + this._aliases[alias] = url; + }, + delAlias: function RegordMgr_delAlias(alias) { + delete this._aliases[alias]; } }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9a244ef2353b..18ff2c96e6d5 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -211,7 +211,7 @@ SyncEngine.prototype = { // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { let record = this._store.createRecord(id); - record.uri = this.engineURL + id; + record.id = id; // XXX not needed record.encryption = this.cryptoMetaURL; return record; }, @@ -252,14 +252,13 @@ SyncEngine.prototype = { let meta = yield CryptoMetas.get(self.cb, this.cryptoMetaURL); if (!meta) { - let cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - getService(Ci.IWeaveCrypto); - let symkey = cryptoSvc.generateRandomKey(); + let symkey = Svc.Crypto.generateRandomKey(); let pubkey = yield PubKeys.getDefaultKey(self.cb); meta = new CryptoMeta(this.cryptoMetaURL); meta.generateIV(); yield meta.addUnwrappedKey(self.cb, pubkey, symkey); - yield meta.put(self.cb); + let res = new Resource(meta.uri); + yield res.put(self.cb, meta.serialize()); } // first sync special case: upload all items @@ -300,7 +299,12 @@ SyncEngine.prototype = { this._lastSyncTmp = 0; while ((item = yield newitems.iter.next(self.cb))) { this._lowMemCheck(); + try { yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); + } catch (e) { + this._log.error("Could not decrypt incoming record: " + + Utils.exceptionStr(e)); + } if (yield this._reconcile.async(this, self.cb, item)) { count.applied++; yield this._applyIncoming.async(this, self.cb, item); @@ -425,7 +429,7 @@ SyncEngine.prototype = { if (out.cleartext) // skip deleted records this._store.createMetaRecords(out.id, meta); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); - yield up.pushRecord(self.cb, out); + up.pushData(Svc.Json.decode(out.serialize())); // FIXME: inefficient } this._store.cache.enabled = true; @@ -435,7 +439,7 @@ SyncEngine.prototype = { let count = 0; for each (let obj in meta) { if (!(obj.id in this._tracker.changedIDs)) { - up.pushLiteral(obj); + up.pushData(obj); count++; } } diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e2e308ddd3b7..5562c84de08c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -475,8 +475,7 @@ BookmarksStore.prototype = { }, // Create a record starting from the weave id (places guid) - // NOTE: the record id will not be set, because WBOs generate it from - // the URL, which we don't have here. The engine sets it. + // FIXME: set id here createRecord: function BStore_createRecord(guid) { let record = this.cache.get(guid); if (record) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 893113a21d8b..e85990e1bfb8 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -117,10 +117,10 @@ Resource.prototype = { this._data = value; }, - _lastRequest: null, + _lastChannel: null, _downloaded: false, _dirty: false, - get lastRequest() this._lastRequest, + get lastChannel() this._lastChannel, get downloaded() this._downloaded, get dirty() this._dirty, @@ -147,7 +147,7 @@ Resource.prototype = { _createRequest: function Res__createRequest() { let ios = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); - let channel = ios.newChannel(this.spec, null, null). + this._lastChannel = ios.newChannel(this.spec, null, null). QueryInterface(Ci.nsIHttpChannel); let headers = this.headers; // avoid calling the authorizer more than once @@ -156,9 +156,9 @@ Resource.prototype = { this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); else this._log.trace("HTTP Header " + key + ": " + headers[key]); - channel.setRequestHeader(key, headers[key], true); + this._lastChannel.setRequestHeader(key, headers[key], true); } - return channel; + return this._lastChannel; }, _onProgress: function Res__onProgress(event) { @@ -198,26 +198,26 @@ Resource.prototype = { yield this.filterUpload(self.cb); this._log.trace(action + " Body:\n" + this._data); - let upload = channel.QueryInterface(Ci.nsIUploadChannel); - let iStream = Cc["@mozilla.org/io/string-input-stream;1"]. + let stream = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); - iStream.setData(this._data, this._data.length); + stream.setData(this._data, this._data.length); - upload.setUploadStream(iStream, 'text/plain', this._data.length); + channel.QueryInterface(Ci.nsIUploadChannel); + channel.setUploadStream(stream, 'text/plain', this._data.length); } let listener = new ChannelListener(self.cb, this._onProgress, this._log); channel.requestMethod = action; this._data = yield channel.asyncOpen(listener, null); - if (Utils.checkStatus(channel.responseStatus, null, [[400,499],501,505])) { + if (!channel.requestSucceeded) { this._log.debug(action + " request failed (" + channel.responseStatus + ")"); if (this._data) this._log.debug("Error response: \n" + this._data); throw new RequestException(this, action, channel); + } else { - this._log.debug(channel.requestMethod + " request successful (" + - channel.responseStatus + ")"); + this._log.debug(action + " request successful (" + channel.responseStatus + ")"); switch (action) { case "DELETE": diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7379c423668a..64842776bde5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -527,8 +527,7 @@ WeaveSvc.prototype = { let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, PrivKeys.defaultKeyUri); try { - yield keys.pubkey.put(self.cb); - yield keys.privkey.put(self.cb); + yield PubKeys.uploadKeypair(self.cb, keys); ret = true; } catch (e) { this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index c62f0ecd23d7..ee4ed9d71a16 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -59,17 +59,7 @@ ClientRecord.prototype = { this._WBORec_init(uri); }, - get name() this.payload.name, - set name(value) { - this.payload.name = value; - }, - - get type() this.payload.type, - set type(value) { - this.payload.type = value; - }, - - // FIXME: engines.js calls encrypt/decrypt for all records, so define these: + // XXX engines.js calls encrypt/decrypt for all records, so define these: encrypt: function ClientRec_encrypt(onComplete) { let fn = function ClientRec__encrypt() {let self = yield;}; fn.async(this, onComplete); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 2da0b6c92666..036615c8f2e6 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -509,3 +509,4 @@ Utils.EventListener.prototype = { let Svc = {}; Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); +Utils.lazySvc(Svc, 'Crypto', "@labs.mozilla.com/Weave/Crypto;1", Ci.IWeaveCrypto); diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 2e4f4d7cf5d7..6196e007a031 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -44,6 +44,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/util.js"); Cu.import("resource://weave/faultTolerance.js"); Function.prototype.async = Async.sugar; @@ -100,6 +101,7 @@ let Wrap = { } catch (e) { this._log.debug("Event: " + this._osPrefix + savedName + ":error"); + this._log.debug("Caught exception: " + Utils.exceptionStr(e)); this._os.notifyObservers(null, this._osPrefix + savedName + ":error", savedPayload); this._os.notifyObservers(null, this._osPrefix + "global:error", savedPayload); ret = undefined; From 45394e921d391380a9ce261c668cb7a51461a4cd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 27 Jan 2009 15:12:35 -0800 Subject: [PATCH 0867/1860] initialize wbo records' uri property correctly --- services/sync/modules/base_records/wbo.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 3d55ca6ca13b..3dfaeeb890ef 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -55,10 +55,11 @@ WBORecord.prototype = { _logName: "Record.WBO", _WBORec_init: function WBORec_init(uri) { - this.baseUri = (typeof(uri) == "string")? Utils.makeURI(uri) : uri; this.data = { payload: {} }; + if (uri) + this.uri = uri; }, get id() { return decodeURI(this.data.id); }, From 7cc53dd26092ff2978016a9bac08ab623958a0d7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 27 Jan 2009 15:25:16 -0800 Subject: [PATCH 0868/1860] don't encrypt empty payloads --- services/sync/modules/base_records/crypto.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 47f78b1bbe13..b620746c0ad9 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -84,6 +84,13 @@ CryptoWrapper.prototype = { _encrypt: function CryptoWrap__encrypt(passphrase) { let self = yield; + // Don't encrypt empty payloads + if (!this.cleartext) { + this.ciphertext = this.cleartext; + self.done(); + return; + } + let pubkey = yield PubKeys.getDefaultKey(self.cb); let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); @@ -103,6 +110,13 @@ CryptoWrapper.prototype = { _decrypt: function CryptoWrap__decrypt(passphrase) { let self = yield; + // Empty payloads aren't encrypted + if (!this.ciphertext) { + this.cleartext = this.ciphertext; + self.done(); + return; + } + let pubkey = yield PubKeys.getDefaultKey(self.cb); let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); From e4d5b67df29b374ad2b8e7fc04dd78c1831391ad Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 27 Jan 2009 16:36:00 -0800 Subject: [PATCH 0869/1860] only url-encode weave IDs when constructing a URL with them --- services/sync/modules/base_records/wbo.js | 10 +++++----- services/sync/modules/engines.js | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 3dfaeeb890ef..99993eebe8ca 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -62,14 +62,14 @@ WBORecord.prototype = { this.uri = uri; }, - get id() { return decodeURI(this.data.id); }, - set id(value) { - this.data.id = encodeURI(value); - }, + get id() { return this.data.id; }, + set id(value) { this.data.id = value; }, // NOTE: baseUri must have a trailing slash, or baseUri.resolve() will omit // the collection name - get uri() { return Utils.makeURI(this.baseUri.resolve(this.id)); }, + get uri() { + return Utils.makeURI(this.baseUri.resolve(encodeURI(this.id))); + }, set uri(value) { if (typeof(value) != "string") value = value.spec; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 18ff2c96e6d5..63dee826b662 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -143,7 +143,6 @@ Engine.prototype = { this._osPrefix = "weave:" + this.name + ":"; this._tracker; // initialize tracker to load previously changed IDs - dump(this.name + "engine initialized.\n"); this._log.debug("Engine initialized"); }, From 8901413e7118ba9a9bf47eeb2f76dc8197137065 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 27 Jan 2009 16:54:54 -0800 Subject: [PATCH 0870/1860] set record IDs inside the store; fix bookmarks tracker, it was ignoring change events (e.g., changed title and such) --- services/sync/modules/engines.js | 1 - services/sync/modules/engines/bookmarks.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 63dee826b662..4c920a78cc9f 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -210,7 +210,6 @@ SyncEngine.prototype = { // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { let record = this._store.createRecord(id); - record.id = id; // XXX not needed record.encryption = this.cryptoMetaURL; return record; }, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5562c84de08c..60e2b491f5ef 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -475,7 +475,6 @@ BookmarksStore.prototype = { }, // Create a record starting from the weave id (places guid) - // FIXME: set id here createRecord: function BStore_createRecord(guid) { let record = this.cache.get(guid); if (record) @@ -534,6 +533,7 @@ BookmarksStore.prototype = { this._bms.getItemType(placeId)); } + record.id = guid; record.parentid = this._getWeaveParentIdForItem(placeId); record.depth = this._itemDepth(placeId); record.sortindex = this._bms.getItemIndex(placeId); @@ -709,7 +709,7 @@ BookmarksTracker.prototype = { // 2) note that engine/store are responsible for manually updating the // tracker's placesId->weaveId cache if ((itemId in this._all) && - (this._bms.getItemGUID(itemId) != this._all[itemId]) && + (this._bms.getItemGUID(itemId) == this._all[itemId]) && this.addChangedID(this._all[itemId])) this._upScore(); }, From 100157eb9cddfce1eada03f75f8ade3289ac61e9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 27 Jan 2009 17:08:47 -0800 Subject: [PATCH 0871/1860] set record ID in history store when creating a record --- services/sync/modules/engines/history.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 3557bb81888b..fdbf0c88e392 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -66,12 +66,12 @@ HistoryEngine.prototype = { // History reconciliation is simpler than the default one from SyncEngine, // because we have the advantage that we can use the URI as a definitive // check for local existence of incoming items. The steps are as follows: - // + // // 1) Check for the same item in the locally modified list. In that case, // local trumps remote. This step is unchanged from our superclass. // 2) Check if the incoming item was deleted. Skip if so. // 3) Apply new record/update. - // + // // Note that we don't attempt to equalize the IDs, the history store does that // as part of update() _reconcile: function HistEngine__reconcile(item) { @@ -398,7 +398,7 @@ HistoryStore.prototype = { } this._hsvc.setPageTitle(uri, record.cleartext.title); - // Equalize IDs + // Equalize IDs let localId = this._getGUID(record.cleartext.uri); if (localId != record.id) this.changeItemID(localId, record.id); @@ -420,6 +420,7 @@ HistoryStore.prototype = { createRecord: function HistStore_createRecord(guid) { let foo = this._findURLByGUID(guid); let record = new HistoryRec(); + record.id = guid; if (foo) { record.histUri = foo.url; record.title = foo.title; From e5c740ae5c7e51a97086e6482ddec33d5da69538 Mon Sep 17 00:00:00 2001 From: Date: Tue, 27 Jan 2009 17:23:18 -0800 Subject: [PATCH 0872/1860] Added id argument to definition of createRecord in base Store class -- all subclasses must override this and must take the id argument. --- services/sync/modules/stores.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 9214ea7b0f76..82e9c7ec30f1 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -109,7 +109,7 @@ Store.prototype = { throw "override itemExists in a subclass"; }, - createRecord: function Store_createRecord() { + createRecord: function Store_createRecord(id) { throw "override createRecord in a subclass"; }, @@ -186,7 +186,7 @@ Cache.prototype = { this._tail.next = wrapper; this._tail = wrapper; } - + this._items[wrapper.id] = wrapper; this.count++; From 02ee222e7abd2a33b938bd1d91f72a1d1e16de62 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 27 Jan 2009 17:23:23 -0800 Subject: [PATCH 0873/1860] set record ID for bookmark deletes too --- services/sync/modules/engines/bookmarks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 60e2b491f5ef..fd5288386cab 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -483,6 +483,7 @@ BookmarksStore.prototype = { let placeId = this._bms.getItemIdForGUID(guid); if (placeId <= 0) { // deleted item record = new PlacesItem(); + record.id = guid; record.cleartext = null; return record; } From bb424a74139fadd8154f5672442c05961151043d Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 28 Jan 2009 17:51:23 -0800 Subject: [PATCH 0874/1860] bug 475855: make the test_auth_manager and test_resource tests work again with recent changes to the Resource object --- services/sync/tests/unit/test_auth_manager.js | 2 +- services/sync/tests/unit/test_resource.js | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js index a31c21e3a643..9d2132893e9b 100644 --- a/services/sync/tests/unit/test_auth_manager.js +++ b/services/sync/tests/unit/test_auth_manager.js @@ -46,7 +46,7 @@ function async_test() { let res = new Resource("http://localhost:8080/foo"); let content = yield res.get(self.cb); do_check_eq(content, "This path exists and is protected"); - do_check_eq(res.lastRequest.status, 200); + do_check_eq(res.lastChannel.responseStatus, 200); do_test_finished(); server.stop(); diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index 70e7efdee7f4..c8b887a97f0b 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -61,7 +61,7 @@ function async_test() { let res = new Resource("http://localhost:8080/open"); let content = yield res.get(self.cb); do_check_eq(content, "This path exists"); - do_check_eq(res.lastRequest.status, 200); + do_check_eq(res.lastChannel.responseStatus, 200); // 2. A password protected resource (test that it'll fail w/o pass) let res2 = new Resource("http://localhost:8080/protected"); @@ -73,10 +73,11 @@ function async_test() { // 3. A password protected resource let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); - let res3 = new Resource("http://localhost:8080/protected", auth); + let res3 = new Resource("http://localhost:8080/protected"); + res3.authenticator = auth; content = yield res3.get(self.cb); do_check_eq(content, "This path exists and is protected"); - do_check_eq(res3.lastRequest.status, 200); + do_check_eq(res3.lastChannel.responseStatus, 200); // 4. A non-existent resource (test that it'll fail) @@ -85,8 +86,8 @@ function async_test() { let content = yield res4.get(self.cb); do_check_true(false); // unreachable, get() above must fail } catch (e) {} - do_check_eq(res4.lastRequest.responseText, "File not found"); - do_check_eq(res4.lastRequest.status, 404); + do_check_eq(res4.lastChannel.responseStatusText, "Not Found"); + do_check_eq(res4.lastChannel.responseStatus, 404); // FIXME: additional coverage needed: // * PUT requests From 1ce8d1f2bf1ade1da745f82a2cc4598bf29d2e5c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 2 Feb 2009 11:43:06 -0800 Subject: [PATCH 0875/1860] ignore all events when tracker.ignoreAll is true; ignore most bookmark annotations (except for the ones we sync) --- services/sync/modules/engines/bookmarks.js | 25 ++++++++++++++++++++-- services/sync/modules/engines/history.js | 2 ++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index fd5288386cab..6fd710a8813a 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -683,26 +683,43 @@ BookmarksTracker.prototype = { }, onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { - if (this._ls.isLivemark(folder)) + if (this.ignoreAll || + this._ls.isLivemark(folder)) return; + this._log.trace("onItemAdded: " + itemId); + this._all[itemId] = this._bms.getItemGUID(itemId); if (this.addChangedID(this._all[itemId])) this._upScore(); }, onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { - if (this._ls.isLivemark(folder)) + if (this.ignoreAll || + this._ls.isLivemark(folder)) return; + this._log.trace("onItemRemoved: " + itemId); + if (this.addChangedID(this._all[itemId])) this._upScore(); delete this._all[itemId]; }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { + if (this.ignoreAll) + return; + + // ignore annotations except for the ones that we sync + if (isAnno && (property != "livemark/feedURI" || + property != "livemark/siteURI" || + property != "microsummary/generatorURI")) + return; + + // ignore if parent is a livemark if (this._ls.isLivemark(this._bms.getFolderIdForItem(itemId))) return; + this._log.trace("onItemChanged: " + itemId + (", " + property + (isAnno? " (anno)" : "")) + (value? (" = \"" + value + "\"") : "")); @@ -716,7 +733,11 @@ BookmarksTracker.prototype = { }, onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { + if (this.ignoreAll) + return; + this._log.trace("onItemMoved: " + itemId); + if (!this._all[itemId]) this._all[itemId] = this._bms.itemGUID(itemId); if (this.addChangedID(this._all[itemId])) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index fdbf0c88e392..5a63a34c7a22 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -487,6 +487,8 @@ HistoryTracker.prototype = { }, onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { + if (this.ignoreAll) + return; this._log.trace("onVisit: " + uri.spec); if (this.addChangedID(this._store._getGUID(uri))) this._upScore(); From 1264d072e55bf625ea69efd06490311465015b3d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 2 Feb 2009 11:44:07 -0800 Subject: [PATCH 0876/1860] server returns a json object for user root now, modify checkLogin() accordingly --- services/sync/modules/service.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 64842776bde5..6b581b0fdcfa 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -421,8 +421,7 @@ WeaveSvc.prototype = { this._log.debug("Verifying login for user " + user); let res = new Resource(this.baseURL + user); yield res.get(self.cb); - if (res.data != "\"1\"") - throw "Login failed"; + Svc.Json.decode(res.data); // will throw if not json self.done(true); }; this._notify("verify-login", "", fn).async(this, onComplete); From 94a61b07da642a70e1a38d36a40b2e459c0d0b36 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 3 Feb 2009 15:50:41 -0800 Subject: [PATCH 0877/1860] disable scheduled sync when login fails --- services/sync/modules/service.js | 51 ++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6b581b0fdcfa..a51372886a10 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -294,8 +294,6 @@ WeaveSvc.prototype = { if (!this.enabled) this._log.info("Weave Sync disabled"); - this._setSchedule(this.schedule); - // Create Weave identities (for logging in, and for encryption) ID.set('WeaveID', new Identity('Mozilla Services Password', this.username)); Auth.defaultAuthenticator = new BasicAuthenticator(ID.get('WeaveID')); @@ -421,7 +419,7 @@ WeaveSvc.prototype = { this._log.debug("Verifying login for user " + user); let res = new Resource(this.baseURL + user); yield res.get(self.cb); - Svc.Json.decode(res.data); // will throw if not json + //Svc.Json.decode(res.data); // will throw if not json self.done(true); }; this._notify("verify-login", "", fn).async(this, onComplete); @@ -459,24 +457,31 @@ WeaveSvc.prototype = { this._loggedIn = false; - if (typeof(user) != 'undefined') - this.username = user; - if (typeof(pass) != 'undefined') - ID.get('WeaveID').setTempPassword(pass); - if (typeof(passp) != 'undefined') - ID.get('WeaveCryptoID').setTempPassword(passp); + try { + if (typeof(user) != 'undefined') + this.username = user; + if (typeof(pass) != 'undefined') + ID.get('WeaveID').setTempPassword(pass); + if (typeof(passp) != 'undefined') + ID.get('WeaveCryptoID').setTempPassword(passp); - if (!this.username) - throw "No username set, login failed"; - if (!this.password) - throw "No password given or found in password manager"; + if (!this.username) + throw "No username set, login failed"; + if (!this.password) + throw "No password given or found in password manager"; - this._log.debug("Logging in user " + this.username); + this._log.debug("Logging in user " + this.username); - if (!(yield this.verifyLogin(self.cb, this.username, this.password))) - throw "Login failed"; - this._loggedIn = true; - self.done(true); + if (!(yield this.verifyLogin(self.cb, this.username, this.password))) + throw "Login failed"; + this._loggedIn = true; + this._setSchedule(this.schedule); + self.done(true); + + } catch (e) { + this._disableSchedule(); + throw e; + } }; this._localLock(this._notify("login", "", fn)).async(this, onComplete); }, @@ -549,8 +554,10 @@ WeaveSvc.prototype = { _sync: function WeaveSvc__sync() { let self = yield; - if (!this._loggedIn) + if (!this._loggedIn) { + this._disableSchedule(); throw "aborting sync, not logged in"; + } if (!(yield this._remoteSetup.async(this, self.cb))) { throw "aborting sync, remote setup failed"; @@ -603,6 +610,12 @@ WeaveSvc.prototype = { let self = yield; try { + + if (!this._loggedIn) { + this._disableSchedule(); + throw "aborting sync, not logged in"; + } + let engines = Engines.getAll(); for each (let engine in engines) { if (!engine.enabled) From 9c6a733a9b1aa1c0626a6475c0abd23019d15d6c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 3 Feb 2009 15:54:30 -0800 Subject: [PATCH 0878/1860] collection searches for records older than a certain date now use the 'older' parameter instead of 'modified', since the latter is ambiguous depending on the verb used --- services/sync/modules/base_records/collection.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 34837d60d41e..20d642280bcf 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -65,7 +65,7 @@ Collection.prototype = { this._init(uri); this.pushFilter(new JsonFilter()); this._full = true; - this._modified = 0; + this._older = 0; this._data = []; }, @@ -74,8 +74,8 @@ Collection.prototype = { this.uri.QueryInterface(Ci.nsIURL); let args = []; - if (this.modified) - args.push('modified=' + this.modified); + if (this.older) + args.push('older=' + this.older); if (this.full) args.push('full=1'); if (this.sort) @@ -92,9 +92,9 @@ Collection.prototype = { }, // get only items modified since some date - get modified() { return this._modified; }, - set modified(value) { - this._modified = value; + get older() { return this._older; }, + set older(value) { + this._older = value; this._rebuildURL(); }, From 3f02cc46edf07a9ac9a9cedaa3105fab78f90bb6 Mon Sep 17 00:00:00 2001 From: Date: Wed, 4 Feb 2009 19:51:20 -0800 Subject: [PATCH 0879/1860] Rewrote tabs engine to work with 0.3 API. (Not yet tested). --- services/sync/modules/engines/bookmarks.js | 4 + services/sync/modules/engines/history.js | 16 + services/sync/modules/engines/tabs.js | 322 +++++++++++---------- services/sync/modules/type_records/tabs.js | 92 ++++++ 4 files changed, 275 insertions(+), 159 deletions(-) create mode 100644 services/sync/modules/type_records/tabs.js diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 6fd710a8813a..33c496cf7daa 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -198,6 +198,10 @@ BookmarksStore.prototype = { case "microsummary": { this._log.debug(" -> creating bookmark \"" + record.cleartext.title + "\""); let uri = Utils.makeURI(record.cleartext.uri); + this._log.debug(" -> -> ParentID is " + parentId); + this._log.debug(" -> -> uri is " + record.cleartext.uri); + this._log.debug(" -> -> sortindex is " + record.sortindex); + this._log.debug(" -> -> title is " + record.cleartext.title); newId = this._bms.insertBookmark(parentId, uri, record.sortindex, record.cleartext.title); this._ts.untagURI(uri, null); diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 5a63a34c7a22..aa8000161e4c 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -355,6 +355,22 @@ HistoryStore.prototype = { query.minVisits = 1; options.maxResults = 100; + /* + dump("SORT_BY_DATE_DESCENDING is " + options.SORT_BY_DATE_DESCENDING + "\n"); + dump("SORT_BY_ANNOTATIOn_DESCENDING is " + options.SORT_BY_ANNOTATION_DESCENDING + "\n"); + dump("BEFORE SETTING, options.sortingMode is " + options.sortingMode + "\n"); + + for ( let z = -1; z < 20; z++) { + try { + options.sortingMode = z; + dump(" -> Can set to " + z + " OK...\n"); + } catch (e) { + dump(" -> Setting to " + z + " raises exception.\n"); + } + } + dump("AFTER SETTING, options.sortingMode is " + options.sortingMode + "\n"); + */ + // TODO the following line throws exception on Fennec; see above. options.sortingMode = options.SORT_BY_DATE_DESCENDING; options.queryType = options.QUERY_TYPE_HISTORY; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index cb3c5afdb9f1..74b07d33f4e1 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -19,6 +19,7 @@ * * Contributor(s): * Myk Melez + * Jono DiCarlo * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -36,6 +37,8 @@ const EXPORTED_SYMBOLS = ['TabEngine']; +const SESSION_STORE_KEY = "weave-tab-sync-id"; + const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; @@ -43,10 +46,10 @@ const Cu = Components.utils; Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/type_records/tabs.js"); Function.prototype.async = Async.sugar; @@ -60,65 +63,96 @@ TabEngine.prototype = { logName: "Tabs", _storeObj: TabStore, _trackerObj: TabTracker, - _recordObj: ClientTabsRecord + _recordObj: TabSetRecord, - get virtualTabs() { - let virtualTabs = {}; - let realTabs = this._store.wrap(); + // API for use by Weave UI code to give user choices of tabs to open: + getAllClients: function TabEngine_getAllClients() { + return this._store._remoteClients; + }, - for (let profileId in this._file.data) { - let tabset = this._file.data[profileId]; - for (let guid in tabset) { - if (!(guid in realTabs) && !(guid in virtualTabs)) { - virtualTabs[guid] = tabset[guid]; - virtualTabs[guid].profileId = profileId; - } - } - } - return virtualTabs; + getClientById: function TabEngine_getClientById(id) { + return this._store._remoteClients[id]; } + }; + function TabStore() { this._TabStore_init(); } TabStore.prototype = { - __proto__: new Store(), + __proto__: Store.prototype, _logName: "Tabs.Store", + _filePath: "weave/meta/tabSets.json", + _remoteClients: {}, + + _TabStore_init: function TabStore__init() { + this._init(); + this._readFromFile(); + }, + + get _localClientGUID() { + return Engines.get("clients").clientID; + }, + + get _localClientName() { + return Engines.get("clients").clientName; + }, + + _writeToFile: function TabStore_writeToFile() { + // use JSON service to serialize the records... + let file = Utils.getProfileFile( + {path: this._filePath, autoCreate: true}); + let jsonObj = {}; + for (let id in this._remoteClients) { + jsonObj[id] = this._remoteClients[id].toJson(); + } + let [fos] = Utils.open(file, ">"); + fos.writeString(this._json.encode(jsonObj)); + fos.close(); + }, + + _readFromFile: function TabStore_readFromFile() { + // use JSON service to un-serialize the records... + // call on initialization. + // Put stuff into remoteClients. + let file = Utils.getProfileFile(this._filePath); + if (!file.exists()) + return; + try { + let [is] = Utils.open(file, "<"); + let json = Utils.readStream(is); + is.close(); + let jsonObj = this._json.decode(json); + for (let id in jsonObj) { + this._remoteClients[id] = new TabSetRecord(); + this._remoteClients[id].fromJson(jsonObj[id]); + this._remoteClients[id].id = id; + } + } catch (e) { + this._log.warn("Failed to load saved tabs file" + e); + } + }, get _sessionStore() { let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. getService(Ci.nsISessionStore); - this.__defineGetter__("_sessionStore", function() sessionStore); + this.__defineGetter__("_sessionStore", function() { return sessionStore;}); return this._sessionStore; }, - _createCommand: function TabStore__createCommand(command) { - this._log.debug("_createCommand: " + command.GUID); - - if (command.GUID in this._virtualTabs || command.GUID in this._wrapRealTabs()) - throw "trying to create a tab that already exists; id: " + command.GUID; - - // Don't do anything if the command isn't valid (i.e. it doesn't contain - // the minimum information about the tab that is necessary to recreate it). - if (!this.validateVirtualTab(command.data)) { - this._log.warn("could not create command " + command.GUID + "; invalid"); - return; - } - - // Cache the tab and notify the UI to prompt the user to open it. - this._virtualTabs[command.GUID] = command.data; - this._os.notifyObservers(null, "weave:store:tabs:virtual:created", null); + get _json() { + let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); + this.__defineGetter__("_json", function() {return json;}); + return this._json; }, - /** - * Serialize the current state of tabs. - */ - wrap: function TabStore_wrap() { - let items = {}; - + _createLocalClientTabSetRecord: function TabStore__createLocalTabSet() { let session = this._json.decode(this._sessionStore.getBrowserState()); + let record = new TabSetRecord(); + record.setClientName( this._localClientName ); + for (let i = 0; i < session.windows.length; i++) { let window = session.windows[i]; // For some reason, session store uses one-based array index references, @@ -128,144 +162,114 @@ TabStore.prototype = { for (let j = 0; j < window.tabs.length; j++) { let tab = window.tabs[j]; - - // The session history entry for the page currently loaded in the tab. - // We use the URL of the current page as the ID for the tab. - let currentEntry = tab.entries[tab.index - 1]; - if (!currentEntry || !currentEntry.url) { - this._log.warn("_wrapRealTabs: no current entry or no URL, can't " + - "identify " + this._json.encode(tab)); - continue; + let title = tab.contentDocument.title.innerHtml; // will this work? + let urlHistory = []; + let entries = tab.entries.slice(tab.entries.length - 10); + for (let entry in entries) { + urlHistory.push( entry.url ); } - - let tabID = currentEntry.url; - - // Only sync up to 10 back-button entries, otherwise we can end up with - // some insanely large snapshots. - tab.entries = tab.entries.slice(tab.entries.length - 10); - - // The ID property of each entry in the tab, which I think contains - // nsISHEntry::ID, changes every time session store restores the tab, - // so we can't sync them, or we would generate edit commands on every - // restart (even though nothing has actually changed). - for (let k = 0; k < tab.entries.length; k++) { - delete tab.entries[k].ID; - } - - items[tabID] = { - // Identify this item as a tab in case we start serializing windows - // in the future. - type: "tab", - - // The position of this tab relative to other tabs in the window. - // For consistency with session store data, we make this one-based. - position: j + 1, - - windowID: windowID, - - state: tab - }; + record.addTab(title, urlHistory); } } + return record; + }, + itemExists: function TabStore_itemExists(id) { + if (id == this._localClientGUID) { + return true; + } else if (this._remoteClients[id]) { + return true; + } else { + return false; + } + }, + + createRecord: function TabStore_createRecord(id) { + let record; + if (id == this._localClientGUID) { + record = this._createLocalClientTabSetRecord(); + } else { + record = this._remoteClients[id]; + } + record.id = id; + return record; + }, + + changeItemId: function TabStore_changeItemId(oldId, newId) { + if (this._remoteClients[oldId]) { + let record = this._remoteClients[oldId]; + record.id = newId; + delete this._remoteClients[oldId]; + this._remoteClients[newId] = record; + } + }, + + getAllIds: function TabStore_getAllIds() { + let items = {}; + items[ this._localClientGUID ] = true; + for (let id in this._remoteClients) { + items[id] = true; + } return items; }, wipe: function TabStore_wipe() { - // We're not going to close tabs, since that's probably not what - // the user wants + this._log.debug("Wipe called. Clearing cache of remote client tabs."); + this._remoteClients = {}; + this._writeToFile(); + }, + + create: function TabStore_create(record) { + if (record.id == this._localClientGUID) + return; // can't happen? + this._log.debug("Create called. Adding remote client record for "); + this._log.debug(record.getClientName()); + this._remoteClients[record.id] = record; + this._writeToFile(); + // TODO writing to file after every change is inefficient. How do we + // make sure to do it (or at least flush it) only after sync is done? + // override syncFinished + }, + + update: function TabStore_update(record) { + if (record.id == this._localClientGUID) + return; // can't happen? + this._log.debug("Update called. Updating remote client record for"); + this._log.debug(record.getClientName()); + this._remoteClients[record.id] = record; + this._writeToFile(); + }, + + remove: function TabStore_remove(record) { + if (record.id == this._localClientGUID) + return; // can't happen? + this._log.debug("Remove called. Deleting record with id " + record.id); + delete this._remoteClients[record.id]; + this._writeToFile(); } + }; -function TabTracker(engine) { - this._engine = engine; +function TabTracker() { this._init(); } TabTracker.prototype = { - __proto__: new Tracker(), - + __proto__: Tracker.prototype, _logName: "TabTracker", + file: "tab_tracker", - _engine: null, + _init: function TabTracker__init() { + this.__proto__.__proto__.init.call(this); - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return this._json; + // Register me as an observer!! Listen for tabs opening and closing: + let container = gBrowser.tabContainer; + container.addEventListener("TabOpen", this.onTabChanged, false); + container.addEventListener("TabClose", this.onTabChanged, false); + // TODO: remove these event listeners when not needed? + // as in container.removeEventListener("TabOpen", this.onTabChanged, false); }, - /** - * There are two ways we could calculate the score. We could calculate it - * incrementally by using the window mediator to watch for windows opening/ - * closing and FUEL (or some other API) to watch for tabs opening/closing - * and changing location. - * - * Or we could calculate it on demand by comparing the state of tabs - * according to the session store with the state according to the snapshot. - * - * It's hard to say which is better. The incremental approach is less - * accurate if it simply increments the score whenever there's a change, - * but it might be more performant. The on-demand approach is more accurate, - * but it might be less performant depending on how often it's called. - * - * In this case we've decided to go with the on-demand approach, and we - * calculate the score as the percent difference between the snapshot set - * and the current tab set, where tabs that only exist in one set are - * completely different, while tabs that exist in both sets but whose data - * doesn't match (f.e. because of variations in history) are considered - * "half different". - * - * So if the sets don't match at all, we return 100; - * if they completely match, we return 0; - * if half the tabs match, and their data is the same, we return 50; - * and if half the tabs match, but their data is all different, we return 75. - */ - get score() { - // The snapshot data is a singleton that we can't modify, so we have to - // copy its unique items to a new hash. - let snapshotData = this._engine.snapshot.data; - let a = {}; - - // The wrapped current state is a unique instance we can munge all we want. - let b = this._engine.store.wrap(); - - // An array that counts the number of intersecting IDs between a and b - // (represented as the length of c) and whether or not their values match - // (represented by the boolean value of each item in c). - let c = []; - - // Generate c and update a and b to contain only unique items. - for (id in snapshotData) { - if (id in b) { - c.push(this._json.encode(snapshotData[id]) == this._json.encode(b[id])); - delete b[id]; - } - else { - a[id] = snapshotData[id]; - } - } - - let numShared = c.length; - let numUnique = [true for (id in a)].length + [true for (id in b)].length; - let numTotal = numShared + numUnique; - - // We're going to divide by the total later, so make sure we don't try - // to divide by zero, even though we should never be in a state where there - // are no tabs in either set. - if (numTotal == 0) - return 0; - - // The number of shared items whose data is different. - let numChanged = c.filter(function(v) !v).length; - - let fractionSimilar = (numShared - (numChanged / 2)) / numTotal; - let fractionDissimilar = 1 - fractionSimilar; - let percentDissimilar = Math.round(fractionDissimilar * 100); - - return percentDissimilar; - }, - - resetScore: function FormsTracker_resetScore() { - // Not implemented, since we calculate the score on demand. + onTabChanged: function TabTracker_onTabChanged(event) { + this._score += 10; // meh? meh. } } diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js new file mode 100644 index 000000000000..7cf0194a53a1 --- /dev/null +++ b/services/sync/modules/type_records/tabs.js @@ -0,0 +1,92 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono DiCarlo + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['TabSetRecord']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/keys.js"); + +Function.prototype.async = Async.sugar; + +function TabSetRecord(uri) { + this._TabSetRecord_init(uri); +} +TabSetRecord.prototype = { + __proto__: CryptoWrapper.prototype, + _logName: "Record.Tabs", + + _TabSetRecord_init: function TabSetRecord__init(uri) { + this._CryptoWrap_init(uri); + this.cleartext = { + }; + }, + + addTab: function TabSetRecord_addTab(title, urlHistory) { + if (!this.cleartext.tabs) + this.cleartext.tabs = []; + this.cleartext.tabs.push( { title: title, + urlHistory: urlHistory }); + }, + + getAllTabs: function TabSetRecord_getAllTabs() { + return this.cleartext.tabs; + }, + + setClientName: function TabSetRecord_setClientName(value) { + this.cleartext.clientName = value; + }, + + getClientName: function TabSetRecord_getClientName() { + return this.cleartext.clientName; + }, + + toJson: function TabSetRecord_toJson() { + return this.cleartext; + }, + + fromJson: function TabSetRecord_fromJson(json) { + this.cleartext = json; + } +}; From 1f777d68202ffaee8e3393907bbd8618f26e2bd6 Mon Sep 17 00:00:00 2001 From: Date: Fri, 6 Feb 2009 11:19:50 -0800 Subject: [PATCH 0880/1860] Added debugging info to TabEngine. Instantiated and registered TabEngine in both Firefox weave glue and Fennec weave glue. --- services/sync/modules/engines/tabs.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 74b07d33f4e1..30f50d021211 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -87,6 +87,7 @@ TabStore.prototype = { _remoteClients: {}, _TabStore_init: function TabStore__init() { + dump("Initializing TabStore!!\n"); this._init(); this._readFromFile(); }, @@ -101,6 +102,7 @@ TabStore.prototype = { _writeToFile: function TabStore_writeToFile() { // use JSON service to serialize the records... + this._log.debug("Writing out to file..."); let file = Utils.getProfileFile( {path: this._filePath, autoCreate: true}); let jsonObj = {}; @@ -116,6 +118,7 @@ TabStore.prototype = { // use JSON service to un-serialize the records... // call on initialization. // Put stuff into remoteClients. + this._log.debug("Reading in from file..."); let file = Utils.getProfileFile(this._filePath); if (!file.exists()) return; @@ -128,7 +131,7 @@ TabStore.prototype = { this._remoteClients[id] = new TabSetRecord(); this._remoteClients[id].fromJson(jsonObj[id]); this._remoteClients[id].id = id; - } + } } catch (e) { this._log.warn("Failed to load saved tabs file" + e); } @@ -163,6 +166,7 @@ TabStore.prototype = { for (let j = 0; j < window.tabs.length; j++) { let tab = window.tabs[j]; let title = tab.contentDocument.title.innerHtml; // will this work? + this._log.debug("Wrapping a tab with title " + title); let urlHistory = []; let entries = tab.entries.slice(tab.entries.length - 10); for (let entry in entries) { @@ -175,20 +179,27 @@ TabStore.prototype = { }, itemExists: function TabStore_itemExists(id) { + this._log.debug("ItemExists called."); if (id == this._localClientGUID) { + this._log.debug("It's me."); return true; } else if (this._remoteClients[id]) { + this._log.debug("It's somebody else."); return true; } else { + this._log.debug("It doesn't exist!"); return false; } }, createRecord: function TabStore_createRecord(id) { + this._log.debug("CreateRecord called for id " + id ); let record; if (id == this._localClientGUID) { + this._log.debug("That's Me!"); record = this._createLocalClientTabSetRecord(); } else { + this._log.debug("That's Somebody Else."); record = this._remoteClients[id]; } record.id = id; @@ -196,6 +207,7 @@ TabStore.prototype = { }, changeItemId: function TabStore_changeItemId(oldId, newId) { + this._log.debug("changeItemId called."); if (this._remoteClients[oldId]) { let record = this._remoteClients[oldId]; record.id = newId; @@ -205,6 +217,7 @@ TabStore.prototype = { }, getAllIds: function TabStore_getAllIds() { + this._log.debug("getAllIds called."); let items = {}; items[ this._localClientGUID ] = true; for (let id in this._remoteClients) { From 9fdef82eed75c5cb16614d52761bf36599dd99bf Mon Sep 17 00:00:00 2001 From: Date: Fri, 6 Feb 2009 17:50:12 -0800 Subject: [PATCH 0881/1860] Switched TabTracker to register listeners on window open and remove them at window close -- need to find an alternate way to make this work on Fennec, where windows don't have tabContainers. --- services/sync/modules/engines/tabs.js | 35 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 30f50d021211..92f76e4c5ff2 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -43,6 +43,7 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); @@ -264,22 +265,40 @@ TabStore.prototype = { }; function TabTracker() { - this._init(); + this._TabTracker_init(); } TabTracker.prototype = { __proto__: Tracker.prototype, _logName: "TabTracker", file: "tab_tracker", - _init: function TabTracker__init() { - this.__proto__.__proto__.init.call(this); + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + _TabTracker_init: function TabTracker__init() { + this._init(); // Register me as an observer!! Listen for tabs opening and closing: - let container = gBrowser.tabContainer; - container.addEventListener("TabOpen", this.onTabChanged, false); - container.addEventListener("TabClose", this.onTabChanged, false); - // TODO: remove these event listeners when not needed? - // as in container.removeEventListener("TabOpen", this.onTabChanged, false); + // TODO We need to also register with any windows that are ALREDY + // open. + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Ci.nsIWindowWatcher); + dump("Initialized TabTracker\n"); + ww.registerNotification(this); + }, + + observe: function TabTracker_observe(aSubject, aTopic, aData) { + dump("TabTracker spotted window open/close...\n"); + let window = aSubject.QueryInterface(Ci.nsIDOMWindow); + // TODO: Not all windows have tabContainers. Fennec windows don't, + // for instance. + let container = window.getBrowser().tabContainer; + if (aTopic == "domwindowopened") { + container.addEventListener("TabOpen", this.onTabChanged, false); + container.addEventListener("TabClose", this.onTabChanged, false); + } else if (aTopic == "domwindowclosed") { + container.removeEventListener("TabOpen", this.onTabChanged, false); + container.removeEventListener("TabClose", this.onTabChanged, false); + } }, onTabChanged: function TabTracker_onTabChanged(event) { From de4eb6f43c28c5b5693cdea5dc46fd05a82838eb Mon Sep 17 00:00:00 2001 From: Date: Mon, 9 Feb 2009 20:23:42 -0800 Subject: [PATCH 0882/1860] Fixed some minor problems with TabEngine; identified places where TabEngine will need to use alternate methods to work on Fennec. --- services/sync/modules/engines/tabs.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 92f76e4c5ff2..e3f0bbef1c29 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -37,8 +37,6 @@ const EXPORTED_SYMBOLS = ['TabEngine']; -const SESSION_STORE_KEY = "weave-tab-sync-id"; - const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; @@ -51,6 +49,7 @@ Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/type_records/tabs.js"); +Cu.import("resource://weave/engines/clientData.js"); Function.prototype.async = Async.sugar; @@ -94,11 +93,11 @@ TabStore.prototype = { }, get _localClientGUID() { - return Engines.get("clients").clientID; + return Clients.clientID; }, get _localClientName() { - return Engines.get("clients").clientName; + return Clients.clientName; }, _writeToFile: function TabStore_writeToFile() { @@ -139,6 +138,7 @@ TabStore.prototype = { }, get _sessionStore() { + // TODO: sessionStore seems to not exist on Fennec? let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. getService(Ci.nsISessionStore); this.__defineGetter__("_sessionStore", function() { return sessionStore;}); @@ -217,7 +217,7 @@ TabStore.prototype = { } }, - getAllIds: function TabStore_getAllIds() { + getAllIDs: function TabStore_getAllIds() { this._log.debug("getAllIds called."); let items = {}; items[ this._localClientGUID ] = true; @@ -291,7 +291,12 @@ TabTracker.prototype = { let window = aSubject.QueryInterface(Ci.nsIDOMWindow); // TODO: Not all windows have tabContainers. Fennec windows don't, // for instance. - let container = window.getBrowser().tabContainer; + if (! window.getBrowser) + return; + let browser = window.getBrowser(); + if (! browser.tabContainer) + return; + let container = browser.tabContainer; if (aTopic == "domwindowopened") { container.addEventListener("TabOpen", this.onTabChanged, false); container.addEventListener("TabClose", this.onTabChanged, false); From a8e8c866168750f31abc0eb7c9d66a73b0634a6f Mon Sep 17 00:00:00 2001 From: Date: Mon, 9 Feb 2009 21:16:18 -0800 Subject: [PATCH 0883/1860] SessionStore not available on Fennec, so now using alternate methods to get the list of tabs. --- services/sync/modules/engines/tabs.js | 56 ++++++++++++++++++--------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index e3f0bbef1c29..0d3e76b6355b 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -100,6 +100,13 @@ TabStore.prototype = { return Clients.clientName; }, + get _fennecTabs() { + let wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + let browserWindow = wm.getMostRecentWindow("navigator:browser"); + return browserWindow.Browser._tabs; + }, + _writeToFile: function TabStore_writeToFile() { // use JSON service to serialize the records... this._log.debug("Writing out to file..."); @@ -138,7 +145,6 @@ TabStore.prototype = { }, get _sessionStore() { - // TODO: sessionStore seems to not exist on Fennec? let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. getService(Ci.nsISessionStore); this.__defineGetter__("_sessionStore", function() { return sessionStore;}); @@ -151,29 +157,40 @@ TabStore.prototype = { return this._json; }, - _createLocalClientTabSetRecord: function TabStore__createLocalTabSet() { - let session = this._json.decode(this._sessionStore.getBrowserState()); + _addTabToRecord: function( tab, record ) { + // TODO contentDocument not defined for fennec's tab objects + let title = tab.contentDocument.title.innerHtml; // will this work? + this._log.debug("Wrapping a tab with title " + title); + let urlHistory = []; + let entries = tab.entries.slice(tab.entries.length - 10); + for (let entry in entries) { + urlHistory.push( entry.url ); + } + record.addTab(title, urlHistory); + }, + _createLocalClientTabSetRecord: function TabStore__createLocalTabSet() { + // Test for existence of sessionStore. If it doesn't exist, then + // use get _fennecTabs instead. let record = new TabSetRecord(); record.setClientName( this._localClientName ); - for (let i = 0; i < session.windows.length; i++) { - let window = session.windows[i]; - // For some reason, session store uses one-based array index references, - // (f.e. in the "selectedWindow" and each tab's "index" properties), so we - // convert them to and from JavaScript's zero-based indexes as needed. - let windowID = i + 1; + if (Cc["@mozilla.org/browser/sessionstore;1"]) { + let session = this._json.decode(this._sessionStore.getBrowserState()); + for (let i = 0; i < session.windows.length; i++) { + let window = session.windows[i]; + /* For some reason, session store uses one-based array index references, + (f.e. in the "selectedWindow" and each tab's "index" properties), so we + convert them to and from JavaScript's zero-based indexes as needed. */ + let windowID = i + 1; - for (let j = 0; j < window.tabs.length; j++) { - let tab = window.tabs[j]; - let title = tab.contentDocument.title.innerHtml; // will this work? - this._log.debug("Wrapping a tab with title " + title); - let urlHistory = []; - let entries = tab.entries.slice(tab.entries.length - 10); - for (let entry in entries) { - urlHistory.push( entry.url ); + for (let j = 0; j < window.tabs.length; j++) { + this._addTabToRecord(window.tabs[j], record); } - record.addTab(title, urlHistory); + } + } else { + for each ( let tab in this._fennecTabs) { + this._addTabToRecord(tab, record); } } return record; @@ -279,7 +296,8 @@ TabTracker.prototype = { // Register me as an observer!! Listen for tabs opening and closing: // TODO We need to also register with any windows that are ALREDY - // open. + // open. On Fennec maybe try to get this from getBrowser(), which is + // defined differently but should still exist... var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] .getService(Ci.nsIWindowWatcher); dump("Initialized TabTracker\n"); From 7112bfbeef0689afbc83146270039a350074ef36 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 00:12:11 -0800 Subject: [PATCH 0884/1860] add support for newer filter to collections --- services/sync/modules/base_records/collection.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 20d642280bcf..ac2918ac1f4f 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -66,6 +66,7 @@ Collection.prototype = { this.pushFilter(new JsonFilter()); this._full = true; this._older = 0; + this._newer = 0; this._data = []; }, @@ -76,6 +77,8 @@ Collection.prototype = { let args = []; if (this.older) args.push('older=' + this.older); + else if (this.newer) + args.push('newer=' + this.newer); if (this.full) args.push('full=1'); if (this.sort) @@ -91,13 +94,20 @@ Collection.prototype = { this._rebuildURL(); }, - // get only items modified since some date + // get only items modified before some date get older() { return this._older; }, set older(value) { this._older = value; this._rebuildURL(); }, + // get only items modified since some date + get newer() { return this._newer; }, + set newer(value) { + this._newer = value; + this._rebuildURL(); + }, + // get items sorted by some criteria. valid values: // oldest (oldest first) // newest (newest first) From a10f735dc5a1e844967c5b0dd6eecde61ba51e0e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 00:51:06 -0800 Subject: [PATCH 0885/1860] use -fshort-wchar on x86_64 too (only exclude it form arm) --- services/crypto/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index ac93264562ce..3984ae413601 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -220,7 +220,7 @@ ifeq ($(os), Linux) -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ -Wno-long-long \ -include xpcom-config.h $(headers) -ifeq ($(arch), x86) +ifneq ($(arch), arm) cppflags += -fshort-wchar else endif From f426a67dd88fdf68b6c090b3da0cf3645bfeb0ca Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 00:52:05 -0800 Subject: [PATCH 0886/1860] put prefs branch into Svc.Prefs --- services/sync/modules/util.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 036615c8f2e6..92f85854b920 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -42,6 +42,8 @@ const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/ext/Preferences.js"); +Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/log4moz.js"); @@ -507,6 +509,7 @@ Utils.EventListener.prototype = { */ let Svc = {}; +Svc.Prefs = new Preferences(PREFS_BRANCH); Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); Utils.lazySvc(Svc, 'Crypto', "@labs.mozilla.com/Weave/Crypto;1", Ci.IWeaveCrypto); From 3d7c6aeb652aeb23123c4a1be832b120565dc363 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 00:52:48 -0800 Subject: [PATCH 0887/1860] temporarily send the 'modified' header as well as 'newer' so we continue to support older servers --- services/sync/modules/base_records/collection.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index ac2918ac1f4f..fdaad8afc0f0 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -77,8 +77,10 @@ Collection.prototype = { let args = []; if (this.older) args.push('older=' + this.older); - else if (this.newer) + else if (this.newer) { args.push('newer=' + this.newer); + args.push('modified=' + this.newer); // tmp hack for older servers + } if (this.full) args.push('full=1'); if (this.sort) From 4b02e27e68d875d95db709c6b5d4101864093018 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 00:53:23 -0800 Subject: [PATCH 0888/1860] don't set any username by default --- services/sync/services-sync.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 8a27f85c6461..1125355624a2 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,5 +1,4 @@ pref("extensions.weave.serverURL", "https://services.mozilla.com/"); -pref("extensions.weave.username", "nobody"); pref("extensions.weave.encryption", "aes-256-cbc"); From 8f1df7a0ddfb79c92d32d61033ce545c5b8e40c1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 00:56:42 -0800 Subject: [PATCH 0889/1860] remove unused code; add multi-cluster support; use Svc.Prefs branch for prefs; move some code from the window overlay into onStartup (print weave version and user agent string); automatically disable weave if crypto module is not working --- services/sync/modules/service.js | 232 +++++++++++++------------------ 1 file changed, 96 insertions(+), 136 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a51372886a10..f5aa1a86557e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -149,13 +149,13 @@ WeaveSvc.prototype = { _scheduleTimer: null, get username() { - return Utils.prefs.getCharPref("username"); + return Svc.Prefs.get("username"); }, set username(value) { if (value) - Utils.prefs.setCharPref("username", value); + Svc.Prefs.set("username", value); else - Utils.prefs.clearUserPref("username"); + Svc.Prefs.reset("username"); // fixme - need to loop over all Identity objects - needs some rethinking... ID.get('WeaveID').username = value; @@ -180,14 +180,32 @@ WeaveSvc.prototype = { }, get baseURL() { - let url = Utils.prefs.getCharPref("serverURL"); - if (url && url[url.length-1] != '/') + let url = Svc.Prefs.get("serverURL"); + if (!url) + throw "No server URL set"; + if (url[url.length-1] != '/') + url += '/'; + url += "0.3/"; + return url; + }, + set baseURL(value) { + Svc.Prefs.set("serverURL", value); + }, + + get clusterURL() { + let url = Svc.Prefs.get("clusterURL"); + if (!url) + return null; + // XXX tmp hack for sm-weave-proxy01 + if (url == "https://sm-weave-proxy01.services.mozilla.com/") + return "https://sm-weave-proxy01.services.mozilla.com/weave/0.3/"; + if (url[url.length-1] != '/') url += '/'; url += "0.3/user/"; return url; }, - set baseURL(value) { - Utils.prefs.setCharPref("serverURL", value); + set clusterURL(value) { + Svc.Prefs.set("clusterURL", value); this._genKeyURLs(); }, @@ -204,15 +222,12 @@ WeaveSvc.prototype = { get keyGenEnabled() this._keyGenEnabled, set keyGenEnabled(value) { this._keyGenEnabled = value; }, - get enabled() Utils.prefs.getBoolPref("enabled"), + get enabled() Svc.Prefs.get("enabled"), get schedule() { if (!this.enabled) return 0; // manual/off - return Utils.prefs.getIntPref("schedule"); - }, - - onWindowOpened: function Weave__onWindowOpened() { + return Svc.Prefs.get("schedule"); }, get locked() this._locked, @@ -274,18 +289,46 @@ WeaveSvc.prototype = { }, _genKeyURLs: function WeaveSvc__genKeyURLs() { - let url = this.baseURL + this.username; + let url = this.clusterURL + this.username; PubKeys.defaultKeyUri = url + "/keys/pubkey"; PrivKeys.defaultKeyUri = url + "/keys/privkey"; }, + _checkCrypto: function WeaveSvc__checkCrypto() { + let ok = false; + + try { + let svc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + createInstance(Ci.IWeaveCrypto); + let iv = svc.generateRandomIV(); + if (iv.length == 24) + ok = true; + + } catch (e) {} + + return ok; + }, + + onWindowOpened: function Weave__onWindowOpened() { + }, + // one-time initialization like setting up observers and the like // xxx we might need to split some of this out into something we can call // again when username/server/etc changes _onStartup: function WeaveSvc__onStartup() { let self = yield; this._initLogs(); - this._log.info("Weave Service Initializing"); + this._log.info("Weave " + WEAVE_VERSION + " initializing"); + + let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. + getService(Ci.nsIHttpProtocolHandler).userAgent; + this._log.info(ua); + + if (!this._checkCrypto()) { + this.enabled = false; + this._log.error("Could not load the Weave crypto component. Disabling " + + "Weave, since it will not work correctly."); + } Utils.prefs.addObserver("", this, false); this._os.addObserver(this, "quit-application", true); @@ -303,12 +346,12 @@ WeaveSvc.prototype = { this._genKeyURLs(); - if (Utils.prefs.getBoolPref("autoconnect") && - this.username && this.username != 'nobody') + if (Svc.Prefs.get("autoconnect") && this.username) { try { if (yield this.login(self.cb)) yield this.sync(self.cb); } catch (e) {} + } self.done(); }, onStartup: function WeaveSvc_onStartup(callback) { @@ -318,18 +361,18 @@ WeaveSvc.prototype = { _initLogs: function WeaveSvc__initLogs() { this._log = Log4Moz.repository.getLogger("Service.Main"); this._log.level = - Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.main")]; + Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")]; let formatter = new Log4Moz.BasicFormatter(); let root = Log4Moz.repository.rootLogger; - root.level = Log4Moz.Level[Utils.prefs.getCharPref("log.rootLogger")]; + root.level = Log4Moz.Level[Svc.Prefs.get("log.rootLogger")]; let capp = new Log4Moz.ConsoleAppender(formatter); - capp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.console")]; + capp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.console")]; root.addAppender(capp); let dapp = new Log4Moz.DumpAppender(formatter); - dapp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.dump")]; + dapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.dump")]; root.addAppender(dapp); let brief = this._dirSvc.get("ProfD", Ci.nsIFile); @@ -346,10 +389,10 @@ WeaveSvc.prototype = { verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); this._briefApp = new Log4Moz.RotatingFileAppender(brief, formatter); - this._briefApp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.briefLog")]; + this._briefApp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.briefLog")]; root.addAppender(this._briefApp); this._debugApp = new Log4Moz.RotatingFileAppender(verbose, formatter); - this._debugApp.level = Log4Moz.Level[Utils.prefs.getCharPref("log.appender.debugLog")]; + this._debugApp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.debugLog")]; root.addAppender(this._debugApp); }, @@ -380,46 +423,44 @@ WeaveSvc.prototype = { }, _onQuitApplication: function WeaveSvc__onQuitApplication() { -/* - if (!this.enabled || !this._loggedIn) - return; - - // Don't quit on exit if this is a forced restart due to application update - // or extension install. - var prefBranch = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch); - // non browser apps may throw - try { - if(prefBranch.getBoolPref("browser.sessionstore.resume_session_once")) - return; - } catch (ex) {} - - this.isQuitting = true; - - let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. - getService(Ci.nsIWindowWatcher); - - // This window has to be modal to prevent the application from quitting - // until the sync finishes and the window closes. - let window = ww.openWindow(null, - "chrome://weave/content/status.xul", - "Weave:Status", - "chrome,centerscreen,modal,close=0", - null); -*/ }, // These are global (for all engines) - verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password) { + // gets cluster from central LDAP server and sets this.clusterURL + findCluster: function WeaveSvc_findCluster(onComplete, username) { + let fn = function WeaveSvc__findCluster() { + let self = yield; + this._log.debug("Finding cluster for user " + username); + let res = new Resource(this.baseURL + "api/register/chknode/" + username); + yield res.get(self.cb); + if (res.lastChannel.responseStatus != 200) { + self.done(false); + return; + } + this.clusterURL = 'https://' + res.data + '/'; + self.done(true); + }; + fn.async(this, onComplete); + }, + + verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password, isLogin) { let user = username, pass = password; let fn = function WeaveSvc__verifyLogin() { let self = yield; this._log.debug("Verifying login for user " + user); - let res = new Resource(this.baseURL + user); + + let cluster = this.clusterURL; + yield this.findCluster(self.cb, username); + + let res = new Resource(this.clusterURL + user); yield res.get(self.cb); - //Svc.Json.decode(res.data); // will throw if not json + + if (!isLogin) // restore cluster so verifyLogin has no impact + this.clusterURL = cluster; + + //Svc.Json.decode(res.data); // throws if not json self.done(true); }; this._notify("verify-login", "", fn).async(this, onComplete); @@ -472,7 +513,7 @@ WeaveSvc.prototype = { this._log.debug("Logging in user " + this.username); - if (!(yield this.verifyLogin(self.cb, this.username, this.password))) + if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) throw "Login failed"; this._loggedIn = true; this._setSchedule(this.schedule); @@ -535,7 +576,8 @@ WeaveSvc.prototype = { ret = true; } catch (e) { this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); - this._log.error(keys.pubkey.lastRequest.responseText); + // FIXME no lastRequest anymore + //this._log.error(keys.pubkey.lastRequest.responseText); } } else { this._log.warn("Could not get encryption passphrase"); @@ -715,87 +757,5 @@ WeaveSvc.prototype = { wipeClient: function WeaveSvc_wipeClient(onComplete) { this._localLock(this._notify("wipe-client", "", this._wipeClient)).async(this, onComplete); - } /*, - - shareData: function WeaveSvc_shareData(dataType, - isShareEnabled, - onComplete, - guid, - username) { - // Shares data of the specified datatype (which must correspond to - // one of the registered engines) with the user specified by username. - // The data node indicated by guid will be shared, along with all its - // children, if it has any. onComplete is a function that will be called - // when sharing is done; it takes an argument that will be true or false - // to indicate whether sharing succeeded or failed. - // Implementation, as well as the interpretation of what 'guid' means, - // is left up to the engine for the specific dataType. - - // isShareEnabled: true to start sharing, false to stop sharing. - - let messageName = "share-" + dataType; - // so for instance, if dataType is "bookmarks" then a message - // "share-bookmarks" will be sent out to any observers who are listening - // for it. As far as I know, there aren't currently any listeners for - // "share-bookmarks" but we'll send it out just in case. - - let self = this; - let saved_dataType = dataType; - let saved_onComplete = onComplete; - let saved_guid = guid; - let saved_username = username; - let saved_isShareEnabled = isShareEnabled; - let successMsg = "weave:service:global:success"; - let errorMsg = "weave:service:global:error"; - let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - - let observer = { - observe: function(subject, topic, data) { - if (!Weave.DAV.locked) { - self._notify(messageName, "", self._lock(self._shareData, - saved_dataType, - saved_isShareEnabled, - saved_guid, - saved_username)).async(self, - saved_onComplete); - os.removeObserver(observer, successMsg); - os.removeObserver(observer, errorMsg); - } - }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) - }; - - if (Weave.DAV.locked) { - // then we have to wait until it's not locked. - dump( "DAV is locked, gonna set up observer to do it later.\n"); - os.addObserver( observer, successMsg, true ); - os.addObserver( observer, errorMsg, true ); - } else { - // Just do it right now! - dump( "DAV not locked, doing it now.\n"); - observer.observe(); - } - }, - - _shareData: function WeaveSvc__shareData(dataType, - isShareEnabled, - guid, - username) { - let self = yield; - let ret; - if (Engines.get(dataType).enabled) { - if (isShareEnabled) { - Engines.get(dataType).share(self.cb, guid, username); - } else { - Engines.get(dataType).stopSharing(self.cb, guid, username); - } - ret = yield; - } else { - this._log.warn( "Can't share disabled data type: " + dataType ); - ret = false; - } - self.done(ret); } -*/ }; From fc40e04c0f13c200c43fa6fc00ef190644d87de6 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 00:57:16 -0800 Subject: [PATCH 0890/1860] multi-cluster support; use new 'newer' flag instead of 'modified' for collection searches --- services/sync/modules/engines.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 4c920a78cc9f..f2408cf5a85c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -181,8 +181,13 @@ SyncEngine.prototype = { }, get baseURL() { - let url = Utils.prefs.getCharPref("serverURL"); - if (url && url[url.length-1] != '/') + let url = Svc.Prefs.get("clusterURL"); + if (!url) + return null; + // XXX tmp hack for sm-weave-proxy01 + if (url == "https://sm-weave-proxy01.services.mozilla.com/") + return "https://sm-weave-proxy01.services.mozilla.com/weave/0.3/"; + if (url[url.length-1] != '/') url += '/'; url += "0.3/user/"; return url; @@ -287,7 +292,7 @@ SyncEngine.prototype = { this._store.cache.clear(); let newitems = new Collection(this.engineURL, this._recordObj); - newitems.modified = this.lastSync; + newitems.newer = this.lastSync; newitems.full = true; newitems.sort = "depthindex"; yield newitems.get(self.cb); From c369be505ba7cd2761a78d3c6c1da15b6e41b183 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 13:29:04 -0800 Subject: [PATCH 0891/1860] allow multi-cluster support to be turned off --- services/sync/modules/service.js | 6 ++++++ services/sync/services-sync.js | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f5aa1a86557e..f5d185f2e531 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -431,6 +431,12 @@ WeaveSvc.prototype = { findCluster: function WeaveSvc_findCluster(onComplete, username) { let fn = function WeaveSvc__findCluster() { let self = yield; + if (Svc.Prefs.get("independentNode")) { + this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); + this.clusterURL = Svc.Prefs.get("serverURL"); + self.done(true); + return; + } this._log.debug("Finding cluster for user " + username); let res = new Resource(this.baseURL + "api/register/chknode/" + username); yield res.get(self.cb); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 1125355624a2..e682be03ad4c 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,5 @@ pref("extensions.weave.serverURL", "https://services.mozilla.com/"); +pref("extensions.weave.independentNode", false); pref("extensions.weave.encryption", "aes-256-cbc"); @@ -16,7 +17,7 @@ pref("extensions.weave.syncOnQuit.enabled", true); pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); -pref("extensions.weave.engine.cookies", true ); +pref("extensions.weave.engine.cookies", false); pref("extensions.weave.engine.passwords", false); pref("extensions.weave.engine.forms", false); pref("extensions.weave.engine.tabs", true); From f2e66e7193e41cc011bef27e66f52f28fb75b4e0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 15:56:37 -0800 Subject: [PATCH 0892/1860] automatically try to detect if the server supports the multi-cluster call (hack hack hack) --- services/sync/modules/service.js | 23 ++++++++++++----------- services/sync/services-sync.js | 1 - 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f5d185f2e531..b63f95d567d0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -431,21 +431,22 @@ WeaveSvc.prototype = { findCluster: function WeaveSvc_findCluster(onComplete, username) { let fn = function WeaveSvc__findCluster() { let self = yield; - if (Svc.Prefs.get("independentNode")) { - this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); - this.clusterURL = Svc.Prefs.get("serverURL"); - self.done(true); - return; - } + let ret = false; + this._log.debug("Finding cluster for user " + username); let res = new Resource(this.baseURL + "api/register/chknode/" + username); yield res.get(self.cb); - if (res.lastChannel.responseStatus != 200) { - self.done(false); - return; + + if (res.lastChannel.responseStatus == 404) { + this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); + this.clusterURL = Svc.Prefs.get("serverURL"); + ret = true; + + } else if (res.lastChannel.responseStatus == 200) { + this.clusterURL = 'https://' + res.data + '/'; + ret = true; } - this.clusterURL = 'https://' + res.data + '/'; - self.done(true); + self.done(ret); }; fn.async(this, onComplete); }, diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index e682be03ad4c..35b073a8f525 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,5 +1,4 @@ pref("extensions.weave.serverURL", "https://services.mozilla.com/"); -pref("extensions.weave.independentNode", false); pref("extensions.weave.encryption", "aes-256-cbc"); From a3360ff87f873e58b14adc2d9fa4e120cd5c12f5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 17:50:40 -0800 Subject: [PATCH 0893/1860] catch exceptions when trying to find out user's cluster --- services/sync/modules/service.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b63f95d567d0..08c59108deee 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -434,8 +434,10 @@ WeaveSvc.prototype = { let ret = false; this._log.debug("Finding cluster for user " + username); - let res = new Resource(this.baseURL + "api/register/chknode/" + username); - yield res.get(self.cb); + try { + let res = new Resource(this.baseURL + "api/register/chknode/" + username); + yield res.get(self.cb); + } catch (e) { /* we check status below */ } if (res.lastChannel.responseStatus == 404) { this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); From b67a32c6889efa09c98f5af47e8fca3e14822106 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Feb 2009 17:57:29 -0800 Subject: [PATCH 0894/1860] oops fix syntax error --- services/sync/modules/service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 08c59108deee..d4243b9d028e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -434,8 +434,9 @@ WeaveSvc.prototype = { let ret = false; this._log.debug("Finding cluster for user " + username); + + let res = new Resource(this.baseURL + "api/register/chknode/" + username); try { - let res = new Resource(this.baseURL + "api/register/chknode/" + username); yield res.get(self.cb); } catch (e) { /* we check status below */ } From f5ae596ec5bda68fd1e8dd9aa3c4898d8b1edbe9 Mon Sep 17 00:00:00 2001 From: Date: Tue, 10 Feb 2009 19:23:06 -0800 Subject: [PATCH 0895/1860] Fennec tabs and Firefox tabs now handled in different functions, and the Fennec one at least works. --- services/sync/modules/engines/tabs.js | 73 ++++++++++++++------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0d3e76b6355b..638199b8086c 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -100,13 +100,6 @@ TabStore.prototype = { return Clients.clientName; }, - get _fennecTabs() { - let wm = Cc["@mozilla.org/appshell/window-mediator;1"] - .getService(Ci.nsIWindowMediator); - let browserWindow = wm.getMostRecentWindow("navigator:browser"); - return browserWindow.Browser._tabs; - }, - _writeToFile: function TabStore_writeToFile() { // use JSON service to serialize the records... this._log.debug("Writing out to file..."); @@ -157,18 +150,6 @@ TabStore.prototype = { return this._json; }, - _addTabToRecord: function( tab, record ) { - // TODO contentDocument not defined for fennec's tab objects - let title = tab.contentDocument.title.innerHtml; // will this work? - this._log.debug("Wrapping a tab with title " + title); - let urlHistory = []; - let entries = tab.entries.slice(tab.entries.length - 10); - for (let entry in entries) { - urlHistory.push( entry.url ); - } - record.addTab(title, urlHistory); - }, - _createLocalClientTabSetRecord: function TabStore__createLocalTabSet() { // Test for existence of sessionStore. If it doesn't exist, then // use get _fennecTabs instead. @@ -176,26 +157,50 @@ TabStore.prototype = { record.setClientName( this._localClientName ); if (Cc["@mozilla.org/browser/sessionstore;1"]) { - let session = this._json.decode(this._sessionStore.getBrowserState()); - for (let i = 0; i < session.windows.length; i++) { - let window = session.windows[i]; - /* For some reason, session store uses one-based array index references, - (f.e. in the "selectedWindow" and each tab's "index" properties), so we - convert them to and from JavaScript's zero-based indexes as needed. */ - let windowID = i + 1; - - for (let j = 0; j < window.tabs.length; j++) { - this._addTabToRecord(window.tabs[j], record); - } - } + this._addFirefoxTabsToRecord(record); } else { - for each ( let tab in this._fennecTabs) { - this._addTabToRecord(tab, record); - } + this._addFennecTabsToRecord(record); } return record; }, + _addFirefoxTabsToRecord: function TabStore__addFirefoxTabs(record) { + let session = this._json.decode(this._sessionStore.getBrowserState()); + for (let i = 0; i < session.windows.length; i++) { + let window = session.windows[i]; + /* For some reason, session store uses one-based array index references, + (f.e. in the "selectedWindow" and each tab's "index" properties), so we + convert them to and from JavaScript's zero-based indexes as needed. */ + let windowID = i + 1; + + for (let j = 0; j < window.tabs.length; j++) { + let tab = window.tabs[j]; + let title = tab.contentDocument.title.innerHtml; // will this work? + this._log.debug("Wrapping a tab with title " + title); + let urlHistory = []; + let entries = tab.entries.slice(tab.entries.length - 10); + for (let entry in entries) { + urlHistory.push( entry.url ); + } + record.addTab(title, urlHistory); + } + } + }, + + _addFennecTabsToRecord: function TabStore__addFennecTabs(record) { + let wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + let browserWindow = wm.getMostRecentWindow("navigator:browser"); + for each (let tab in browserWindow.Browser._tabs ) { + let title = tab.browser.contentDocument.title; + let url = tab.browser.contentWindow.location; + let urlHistory = [url]; + // TODO how to get older entries in urlHistory? + dump("Making tab with title = " + title + ", url = " + url + "\n"); + record.addTab(title, urlHistory); + } + }, + itemExists: function TabStore_itemExists(id) { this._log.debug("ItemExists called."); if (id == this._localClientGUID) { From d9c24d3aa99ef5e13220e549130a29c18fd20e1b Mon Sep 17 00:00:00 2001 From: Date: Tue, 10 Feb 2009 21:54:11 -0800 Subject: [PATCH 0896/1860] Fixed tab encoding bug on Fennec --- services/sync/modules/engines/tabs.js | 2 +- services/sync/modules/type_records/tabs.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 638199b8086c..df28739d9ff2 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -193,7 +193,7 @@ TabStore.prototype = { let browserWindow = wm.getMostRecentWindow("navigator:browser"); for each (let tab in browserWindow.Browser._tabs ) { let title = tab.browser.contentDocument.title; - let url = tab.browser.contentWindow.location; + let url = tab.browser.contentWindow.location.toString(); let urlHistory = [url]; // TODO how to get older entries in urlHistory? dump("Making tab with title = " + title + ", url = " + url + "\n"); diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js index 7cf0194a53a1..5ad165d375bd 100644 --- a/services/sync/modules/type_records/tabs.js +++ b/services/sync/modules/type_records/tabs.js @@ -66,8 +66,7 @@ TabSetRecord.prototype = { addTab: function TabSetRecord_addTab(title, urlHistory) { if (!this.cleartext.tabs) this.cleartext.tabs = []; - this.cleartext.tabs.push( { title: title, - urlHistory: urlHistory }); + this.cleartext.tabs.push( {title: title, urlHistory: urlHistory }); }, getAllTabs: function TabSetRecord_getAllTabs() { From 674b4ed1b4b85514d8bb2120701a6760187c5758 Mon Sep 17 00:00:00 2001 From: Date: Tue, 10 Feb 2009 22:15:25 -0800 Subject: [PATCH 0897/1860] Fixed bug in tab encoding in Firefox. --- services/sync/modules/engines/tabs.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index df28739d9ff2..79ac9f458c0f 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -175,14 +175,17 @@ TabStore.prototype = { for (let j = 0; j < window.tabs.length; j++) { let tab = window.tabs[j]; - let title = tab.contentDocument.title.innerHtml; // will this work? - this._log.debug("Wrapping a tab with title " + title); + let currentPage = tab.entries[tab.entries.length - 1]; + /* TODO not always accurate -- if you've hit Back in this tab, then the current + * page might not be the last entry. Deal with this later. + */ + this._log.debug("Wrapping a tab with title " + currentPage.title); let urlHistory = []; let entries = tab.entries.slice(tab.entries.length - 10); for (let entry in entries) { urlHistory.push( entry.url ); } - record.addTab(title, urlHistory); + record.addTab(currentPage.title, urlHistory); } } }, From 91fa3aa73645d6d4ecb3365c9c5305c3350033ee Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 11 Feb 2009 18:11:18 +0100 Subject: [PATCH 0898/1860] Ignore bad HTTPS certificates (bug #476758) --- services/sync/modules/resource.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index e85990e1bfb8..3850edc308b5 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -149,7 +149,8 @@ Resource.prototype = { getService(Ci.nsIIOService); this._lastChannel = ios.newChannel(this.spec, null, null). QueryInterface(Ci.nsIHttpChannel); - + this._lastChannel.notificationCallbacks = new badCertListener(); + let headers = this.headers; // avoid calling the authorizer more than once for (let key in headers) { if (key == 'Authorization') @@ -348,3 +349,30 @@ JsonFilter.prototype = { self.done(this._json.decode(data)); } }; + +function badCertListener() { +} +badCertListener.prototype = { + getInterface: function(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Components.interfaces.nsIBadCertListener2) || + aIID.equals(Components.interfaces.nsIInterfaceRequestor) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + notifyCertProblem: function certProblem(socketInfo, sslStatus, targetHost) { + // Silently ignore? + let log = Log4Moz.repository.getLogger("Service.CertListener"); + log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; + log.debug("Invalid HTTPS certificate encountered, ignoring!"); + + return true; + } +}; From cd10914746494394789e3cf121cdc005c36074b5 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 11 Feb 2009 18:16:03 +0100 Subject: [PATCH 0899/1860] Remove & ignore binary files in repository --- services/sync/WeaveCrypto.so | Bin 56986 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 services/sync/WeaveCrypto.so diff --git a/services/sync/WeaveCrypto.so b/services/sync/WeaveCrypto.so deleted file mode 100755 index 0259efa9c6626898ec69c1abcc980b723f688f0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56986 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 LT7qN0kP!bL_4K;I From 8082140276740bc2cb0f929d285bd06def328864 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 11:24:16 -0800 Subject: [PATCH 0900/1860] add missing setter for the weave enabled pref --- services/sync/modules/service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d4243b9d028e..b02073353347 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -223,6 +223,7 @@ WeaveSvc.prototype = { set keyGenEnabled(value) { this._keyGenEnabled = value; }, get enabled() Svc.Prefs.get("enabled"), + set enabled(value) Svc.Prefs.set("enabled", value), get schedule() { if (!this.enabled) From 4262dabffc4d3e07578aa1a9b209c0505689da24 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 13:38:50 -0800 Subject: [PATCH 0901/1860] ignore sync if weave is disabled --- services/sync/modules/service.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b02073353347..acd5b130143a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -299,13 +299,13 @@ WeaveSvc.prototype = { let ok = false; try { - let svc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - createInstance(Ci.IWeaveCrypto); - let iv = svc.generateRandomIV(); + let iv = Svc.Crypto.generateRandomIV(); if (iv.length == 24) ok = true; - } catch (e) {} + } catch (e) { + this._log.debug("Crypto check failed: " + e); + } return ok; }, @@ -607,6 +607,9 @@ WeaveSvc.prototype = { _sync: function WeaveSvc__sync() { let self = yield; + if (!this.enabled) + return; + if (!this._loggedIn) { this._disableSchedule(); throw "aborting sync, not logged in"; @@ -662,6 +665,9 @@ WeaveSvc.prototype = { _syncAsNeeded: function WeaveSvc__syncAsNeeded() { let self = yield; + if (!this.enabled) + return; + try { if (!this._loggedIn) { From 03e94dbd361b154989a19d355983cb766a47fd84 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 13:39:15 -0800 Subject: [PATCH 0902/1860] Backed out changeset fbdf53ade028 (remove & ignore binaries) --- services/sync/WeaveCrypto.so | Bin 0 -> 56986 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 services/sync/WeaveCrypto.so diff --git a/services/sync/WeaveCrypto.so b/services/sync/WeaveCrypto.so new file mode 100755 index 0000000000000000000000000000000000000000..0259efa9c6626898ec69c1abcc980b723f688f0b GIT binary patch literal 56986 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 LT7qN0kP!bL_4K;I literal 0 HcmV?d00001 From 8aad4428fd61c7f4908d4daf00b285e42a2e9279 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 13:47:51 -0800 Subject: [PATCH 0903/1860] remove sm-weave-proxy01 hack --- services/sync/modules/engines.js | 3 --- services/sync/modules/service.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f2408cf5a85c..ab878339bd95 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -184,9 +184,6 @@ SyncEngine.prototype = { let url = Svc.Prefs.get("clusterURL"); if (!url) return null; - // XXX tmp hack for sm-weave-proxy01 - if (url == "https://sm-weave-proxy01.services.mozilla.com/") - return "https://sm-weave-proxy01.services.mozilla.com/weave/0.3/"; if (url[url.length-1] != '/') url += '/'; url += "0.3/user/"; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index acd5b130143a..aee97c41514f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -196,9 +196,6 @@ WeaveSvc.prototype = { let url = Svc.Prefs.get("clusterURL"); if (!url) return null; - // XXX tmp hack for sm-weave-proxy01 - if (url == "https://sm-weave-proxy01.services.mozilla.com/") - return "https://sm-weave-proxy01.services.mozilla.com/weave/0.3/"; if (url[url.length-1] != '/') url += '/'; url += "0.3/user/"; From 4581c20d38a41df174b8b15a57bea6499463b4dc Mon Sep 17 00:00:00 2001 From: Date: Wed, 11 Feb 2009 19:14:25 -0800 Subject: [PATCH 0904/1860] Tab sync works, and has UI in firefox (though not yet in Fennec) --- services/sync/modules/engines/tabs.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 79ac9f458c0f..96ef9c8da05c 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -334,5 +334,17 @@ TabTracker.prototype = { onTabChanged: function TabTracker_onTabChanged(event) { this._score += 10; // meh? meh. + }, + + get changedIDs() { + let obj = {}; + obj[Clients.clientID] = true; + return obj; + }, + + // TODO this hard-coded score is a hack; replace with maybe +25 or +35 + // per tab open event. + get score() { + return 100; } } From 9e3a5f1cb2d1a864367b11c353313bd75372d73f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 19:16:15 -0800 Subject: [PATCH 0905/1860] ldap chknode returns full url now --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index aee97c41514f..64ddaa24fdd5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -444,7 +444,7 @@ WeaveSvc.prototype = { ret = true; } else if (res.lastChannel.responseStatus == 200) { - this.clusterURL = 'https://' + res.data + '/'; + this.clusterURL = res.data; ret = true; } self.done(ret); From bc834eb679d870b728d9838d6d469b4f3e698821 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 19:19:04 -0800 Subject: [PATCH 0906/1860] add makeURL function (returns an nsIURL), and add Svc.Memory (for memory service) --- services/sync/modules/util.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 92f85854b920..7b1a17e81f46 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -362,6 +362,12 @@ let Utils = { } }, + makeURL: function Weave_makeURL(URIString) { + let url = Utils.makeURI(URIString); + url.QueryInterface(Ci.nsIURL); + return url; + }, + xpath: function Weave_xpath(xmlDoc, xpathString) { let root = xmlDoc.ownerDocument == null ? xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement; @@ -513,3 +519,4 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); Utils.lazySvc(Svc, 'Crypto', "@labs.mozilla.com/Weave/Crypto;1", Ci.IWeaveCrypto); +Utils.lazySvc(Svc, 'Memory', "@mozilla.org/xpcom/memory-service;1", Ci.nsIMemory); From b6349ef097f48ae418c45612bc0f9beab8b5ba71 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 19:21:06 -0800 Subject: [PATCH 0907/1860] ...chknode is returning a hostname again --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 64ddaa24fdd5..aee97c41514f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -444,7 +444,7 @@ WeaveSvc.prototype = { ret = true; } else if (res.lastChannel.responseStatus == 200) { - this.clusterURL = res.data; + this.clusterURL = 'https://' + res.data + '/'; ret = true; } self.done(ret); From 0f91b91f7ca567ba9035cbd67633d07252df4735 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 21:49:16 -0800 Subject: [PATCH 0908/1860] resolve using the baseUri, it's cheaper that way --- services/sync/modules/base_records/crypto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index b620746c0ad9..4da26fa28efd 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -174,7 +174,7 @@ CryptoMeta.prototype = { // each hash key is a relative uri, resolve those and match against ours for (let relUri in this.payload.keyring) { - if (pubkeyUri == this.uri.resolve(relUri)) + if (pubkeyUri == this.baseUri.resolve(relUri)) wrapped_key = this.payload.keyring[relUri]; } if (!wrapped_key) From d4a6db5dba2fd5d4130d2f920967032861935ea5 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 21:50:36 -0800 Subject: [PATCH 0909/1860] notify wrapper doesn't eat exceptions; catchAll wrapper doesn't use the FT service --- services/sync/modules/wrap.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 6196e007a031..108eb39c969b 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -101,10 +101,9 @@ let Wrap = { } catch (e) { this._log.debug("Event: " + this._osPrefix + savedName + ":error"); - this._log.debug("Caught exception: " + Utils.exceptionStr(e)); this._os.notifyObservers(null, this._osPrefix + savedName + ":error", savedPayload); this._os.notifyObservers(null, this._osPrefix + "global:error", savedPayload); - ret = undefined; + throw e; } self.done(ret); @@ -149,7 +148,7 @@ let Wrap = { // NOTE: see notify, this works the same way. they can be // chained together as well. - // catchAll catches any exceptions and passes them to the fault tolerance svc + // catchAll catches any exceptions and prints a stack trace for them catchAll: function WeaveSync_catchAll(method /* , arg1, arg2, ..., argN */) { let savedMethod = method; let savedArgs = Array.prototype.slice.call(arguments, 1); @@ -165,9 +164,7 @@ let Wrap = { ret = yield Async.run.apply(Async, args); } catch (e) { - ret = FaultTolerance.Service.onException(e); - if (!ret) - throw e; + this._log.debug("Caught exception: " + Utils.exceptionStr(e)); } self.done(ret); }; From 78e90924d18a935e0f5e9e5e6aa47f1994ec2f8d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 21:51:24 -0800 Subject: [PATCH 0910/1860] add '-engine' to engine name in observer notifications --- services/sync/modules/engines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index ab878339bd95..c050cce4b71d 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -140,7 +140,7 @@ Engine.prototype = { this._log = Log4Moz.repository.getLogger("Engine." + this.logName); this._log.level = Log4Moz.Level[level]; - this._osPrefix = "weave:" + this.name + ":"; + this._osPrefix = "weave:" + this.name + "-engine:"; this._tracker; // initialize tracker to load previously changed IDs this._log.debug("Engine initialized"); From 51e17d6ea6b9d86ea992d9d3a417074c077e8cc1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 22:08:56 -0800 Subject: [PATCH 0911/1860] use catchAll wrapper, remove wipeClient from the service --- services/sync/modules/service.js | 110 +++++++++++++------------------ 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index aee97c41514f..3e85b4ac61a1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -112,7 +112,6 @@ WeaveSvc.prototype = { _localLock: Wrap.localLock, _catchAll: Wrap.catchAll, _osPrefix: "weave:service:", - _cancelRequested: false, _isQuitting: false, _loggedIn: false, _syncInProgress: false, @@ -208,19 +207,19 @@ WeaveSvc.prototype = { get userPath() { return ID.get('WeaveID').username; }, - get isLoggedIn() this._loggedIn, + get isLoggedIn() { return this._loggedIn; }, - get isQuitting() this._isQuitting, + get isQuitting() { return this._isQuitting; }, set isQuitting(value) { this._isQuitting = value; }, - get cancelRequested() this._cancelRequested, - set cancelRequested(value) { this._cancelRequested = value; }, + get cancelRequested() { return Engines.cancelRequested; }, + set cancelRequested(value) { Engines.cancelRequested = value; }, - get keyGenEnabled() this._keyGenEnabled, + get keyGenEnabled() { return this._keyGenEnabled; }, set keyGenEnabled(value) { this._keyGenEnabled = value; }, - get enabled() Svc.Prefs.get("enabled"), - set enabled(value) Svc.Prefs.set("enabled", value), + get enabled() { return Svc.Prefs.get("enabled"); }, + set enabled(value) { Svc.Prefs.set("enabled", value); }, get schedule() { if (!this.enabled) @@ -228,7 +227,7 @@ WeaveSvc.prototype = { return Svc.Prefs.get("schedule"); }, - get locked() this._locked, + get locked() { return this._locked; }, lock: function Svc_lock() { if (this._locked) return false; @@ -277,11 +276,12 @@ WeaveSvc.prototype = { _onSchedule: function WeaveSvc__onSchedule() { if (this.enabled) { if (this.locked) { - this._log.info("Skipping scheduled sync; local operation in progress"); + this._log.trace("Skipping scheduled sync; local operation in progress"); } else { this._log.info("Running scheduled sync"); - this._notify("sync", "", - this._localLock(this._sync)).async(this); + this._catchAll( + this._notify("sync", "", + this._localLock(this._sync))).async(this); } } }, @@ -471,7 +471,7 @@ WeaveSvc.prototype = { //Svc.Json.decode(res.data); // throws if not json self.done(true); }; - this._notify("verify-login", "", fn).async(this, onComplete); + this._catchAll(this._notify("verify-login", "", fn)).async(this, onComplete); }, _verifyPassphrase: function WeaveSvc__verifyPassphrase(username, password, @@ -493,9 +493,10 @@ WeaveSvc.prototype = { }, verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, password, passphrase) { - this._localLock(this._notify("verify-passphrase", "", this._verifyPassphrase, - username, password, passphrase)). - async(this, onComplete); + this._catchAll( + this._localLock( + this._notify("verify-passphrase", "", this._verifyPassphrase, + username, password, passphrase))).async(this, onComplete); }, login: function WeaveSvc_login(onComplete, username, password, passphrase) { @@ -532,7 +533,9 @@ WeaveSvc.prototype = { throw e; } }; - this._localLock(this._notify("login", "", fn)).async(this, onComplete); + this._catchAll( + this._localLock( + this._notify("login", "", fn))).async(this, onComplete); }, logout: function WeaveSvc_logout() { @@ -551,7 +554,8 @@ WeaveSvc.prototype = { this._log.error("Server wipe not supported"); this.logout(); }; - this._notify("server-wipe", "", this._localLock(cb)).async(this, onComplete); + this._catchAll( + this._notify("server-wipe", "", this._localLock(cb))).async(this, onComplete); }, // stuff we need to to after login, before we can really do @@ -625,18 +629,10 @@ WeaveSvc.prototype = { if (!engine.enabled) continue; - this._log.debug("Syncing engine " + engine.name); - yield this._notify(engine.name + "-engine:sync", "", - this._syncEngine, engine).async(this, self.cb); - - if (this._cancelRequested) { - this._log.info("Cancel requested, aborting sync"); - break; - } - if (this.abortSync) { - this._log.error("Aborting sync"); - break; - } + if (!(yield this._syncEngine.async(this, self.cb, engine))) { + this._log.info("Aborting sync"); + break; + } } if (this._syncError) @@ -645,13 +641,14 @@ WeaveSvc.prototype = { this._log.info("Sync completed successfully"); } finally { - this._cancelRequested = false; - this._abortSync = false; + this.cancelRequested = false; this._syncError = false; } }, sync: function WeaveSvc_sync(onComplete) { - this._notify("sync", "", this._localLock(this._sync)).async(this, onComplete); + this._catchAll( + this._notify("sync", "", + this._localLock(this._sync))).async(this, onComplete); }, // The values that engine scores must meet or exceed before we sync them @@ -685,8 +682,10 @@ WeaveSvc.prototype = { this._log.debug(engine.name + " score " + score + " reaches threshold " + this._syncThresholds[engine.name] + "; syncing"); - yield this._notify(engine.name + "-engine:sync", "", - this._syncEngine, engine).async(this, self.cb); + if (!(yield this._syncEngine.async(this, self.cb, engine))) { + this._log.info("Aborting sync"); + break; + } // Reset the engine's threshold to the initial value. // Note: we do this after syncing the engine so that we'll try again @@ -710,15 +709,6 @@ WeaveSvc.prototype = { if (this._syncThresholds[engine.name] <= 0) this._syncThresholds[engine.name] = 1; } - - if (this._cancelRequested) { - this._log.info("Cancel requested, aborting sync"); - break; - } - if (this.abortSync) { - this._log.error("Aborting sync"); - break; - } } if (this._syncError) @@ -728,17 +718,23 @@ WeaveSvc.prototype = { } finally { this._cancelRequested = false; - this._abortSync = false; this._syncError = false; } }, + // returns true if sync should proceed + // false / no return value means sync should be aborted _syncEngine: function WeaveSvc__syncEngine(engine) { let self = yield; - try { yield engine.sync(self.cb); } + try { + yield engine.sync(self.cb); + if (!this.cancelRequested) + self.done(true); + } catch(e) { this._syncError = true; - this._abortSync = !FaultTolerance.Service.onException(e); + if (FaultTolerance.Service.onException(e)) + self.done(true); } }, @@ -754,22 +750,8 @@ WeaveSvc.prototype = { } }, wipeServer: function WeaveSvc_wipeServer(onComplete) { - this._notify("wipe-server", "", - this._localLock(this._wipeServer)).async(this, onComplete); - }, - - _wipeClient: function WeaveSvc__wipeClient() { - let self = yield; - let engines = Engines.getAll(); - for (let i = 0; i < engines.length; i++) { - if (!engines[i].enabled) - continue; - engines[i].wipeClient(self.cb); - yield; - } - }, - wipeClient: function WeaveSvc_wipeClient(onComplete) { - this._localLock(this._notify("wipe-client", "", - this._wipeClient)).async(this, onComplete); + this._catchAll( + this._notify("wipe-server", "", + this._localLock(this._wipeServer))).async(this, onComplete); } }; From 1a5b79768700e863cbc22578be156539932dacde Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 23:52:44 -0800 Subject: [PATCH 0912/1860] remove old storage format constants, add min_server_format_version --- services/sync/modules/constants.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 536ddbea9d6e..2a8233350524 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -34,8 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', - 'ENGINE_STORAGE_FORMAT_VERSION', +const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'MIN_SERVER_STORAGE_VERSION', 'PREFS_BRANCH', 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', @@ -44,8 +43,11 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'STORAGE_FORMAT_VERSION', 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; const WEAVE_VERSION = "@weave_version@"; -const STORAGE_FORMAT_VERSION = 3; -const ENGINE_STORAGE_FORMAT_VERSION = 3; + +// last client version's server storage this version supports +// e.g. if set to the current version, this client will wipe the server +// data stored by any older client +const MIN_SERVER_STORAGE_VERSION = "@weave_version@"; const PREFS_BRANCH = "extensions.weave."; From 833e0e2824d862b55313e22b832104ed92359d77 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 23:53:37 -0800 Subject: [PATCH 0913/1860] add a generic WBO record manager --- services/sync/modules/base_records/wbo.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 99993eebe8ca..7efe8a66ee4f 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['WBORecord', 'RecordManager']; +const EXPORTED_SYMBOLS = ['WBORecord', 'RecordManager', 'Records']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -138,6 +138,8 @@ WBORecord.prototype = { } }; +Utils.lazy(this, 'Records', RecordManager); + function RecordManager() { this._init(); } @@ -159,12 +161,12 @@ RecordManager.prototype = { this._log.trace("Importing record: " + (url.spec? url.spec : url)); try { - let rsrc = new Resource(url); - yield rsrc.get(self.cb); + this.lastResource = new Resource(url); + yield this.lastResource.get(self.cb); record = new this._recordType(); - record.deserialize(rsrc.data); - record.uri = url; // NOTE: may override id in rsrc.data + record.deserialize(this.lastResource.data); + record.uri = url; // NOTE: may override id in this.lastResource.data this.set(url, record); } catch (e) { From ac0179e65cfec4126b1c207f1863ed83ad9cd35f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Feb 2009 23:54:20 -0800 Subject: [PATCH 0914/1860] add version checking at startup using a global metadata record; wipe server on incompatible versions --- services/sync/modules/service.js | 78 +++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3e85b4ac61a1..80734a40d7b0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -72,6 +72,7 @@ Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/faultTolerance.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/oauth.js"); @@ -564,6 +565,44 @@ WeaveSvc.prototype = { let self = yield; let ret = false; // false to abort sync + this._log.debug("Fetching global metadata record"); + let meta = yield Records.import(self.cb, this.clusterURL + + this.username + "/meta/global"); + if (meta) { + // FIXME: version is a string, and we don't have any version + // parsing function so we can implement greater-than. So we + // just check for != which is wrong! + if (meta.payload.storageVersion != MIN_SERVER_STORAGE_VERSION) { + this._log.debug("Last upload client version too old, wiping server data" + + "to ensure consistency"); + yield this._wipeServer.async(this, self.cb); + + this._log.debug("Uploading new metadata record"); + meta.payload.storageVersion = WEAVE_VERSION; + let res = new Resource(meta.uri); + yield res.put(self.cb, meta.serialize()); + } + + } else { + if (Records.lastResource.lastChannel.responseStatus == 404) { + this._log.debug("Metadata record not found, deleting all server " + + "data to ensure consistency"); + yield this._wipeServer.async(this, self.cb); + + this._log.debug("Uploading new metadata record"); + meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); + meta.payload.storageVersion = WEAVE_VERSION; + let res = new Resource(meta.uri); + yield res.put(self.cb, meta.serialize()); + + } else { + this._log.debug("Unknown error while downloading metadata record. " + + "Aborting sync."); + self.done(false); + return; + } + } + // FIXME: too easy to generate a keypair. we should be absolutely certain // that the keys are not there (404) and that it's not some other // transient network problem instead @@ -578,7 +617,25 @@ WeaveSvc.prototype = { } } - if (needKeys && this._keyGenEnabled) { + if (needKeys) { + if (PubKeys.lastResource.lastChannel.responseStatus != 404 && + PrivKeys.lastResource.lastChannel.responseStatus != 404) { + this._log.warn("Couldn't download keys from server, aborting sync"); + this._log.debug("PubKey HTTP response status: " + + PubKeys.lastResource.lastChannel.responseStatus); + this._log.debug("PrivKey HTTP response status: " + + PrivKeys.lastResource.lastChannel.responseStatus); + self.done(false); + return; + } + + if (!this._keyGenEnabled) { + this._log.warn("Couldn't download keys from server, and key generation" + + "is disabled. Aborting sync"); + self.done(false); + return; + } + let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb); if (pass) { let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, @@ -596,10 +653,6 @@ WeaveSvc.prototype = { } } - if (needKeys && !this._keyGenEnabled) { - this._log.warn("Can't get keys from server and local keygen disabled."); - } - self.done(ret); }, @@ -741,12 +794,15 @@ WeaveSvc.prototype = { _wipeServer: function WeaveSvc__wipeServer() { let self = yield; - let engines = Engines.getAll(); - for (let i = 0; i < engines.length; i++) { - if (!engines[i].enabled) - continue; - engines[i].wipeServer(self.cb); - yield; + // tmp, delete all known collections + for each (let coll in ["keys", "crypto", "clients", + "bookmarks", "history", "tabs"]) { + let res = new Resource(this.clusterURL + this.username + "/" + coll + "/"); + try { + yield res.delete(self.cb); + } catch (e) { + this._log.debug("Exception on delete: " + Utils.exceptionStr(e)); + } } }, wipeServer: function WeaveSvc_wipeServer(onComplete) { From 3f3160870a5d709f94d7f9f482c2d23030082f44 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 17 Feb 2009 13:20:02 -0800 Subject: [PATCH 0915/1860] add a method for resetting the engine's lastSync pref --- services/sync/modules/engines.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c050cce4b71d..01f6453eadd0 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -199,14 +199,15 @@ SyncEngine.prototype = { }, get lastSync() { - try { - return Utils.prefs.getCharPref(this.name + ".lastSync"); - } catch (e) { - return 0; - } + return Svc.Prefs.get(this.name + ".lastSync", 0); }, set lastSync(value) { - Utils.prefs.setCharPref(this.name + ".lastSync", value); + Svc.Prefs.set(this.name + ".lastSync", value); + }, + resetLastSync: function SyncEngine_resetLastSync() { + this._log.debug("Resetting " + this.name + " last sync time"); + Svc.Prefs.reset(this.name + ".lastSync"); + Svc.Prefs.set(this.name + ".lastSync", 0); }, // Create a new record by querying the store, and add the engine metadata From 86cb93d7f78655b737fd4f3f36cebceeeb393cb4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 17 Feb 2009 13:21:14 -0800 Subject: [PATCH 0916/1860] reset engine lastSync prefs when wiping the server --- services/sync/modules/service.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 80734a40d7b0..1fd857946f9b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -603,9 +603,6 @@ WeaveSvc.prototype = { } } - // FIXME: too easy to generate a keypair. we should be absolutely certain - // that the keys are not there (404) and that it's not some other - // transient network problem instead let needKeys = true; let pubkey = yield PubKeys.getDefaultKey(self.cb); if (pubkey) { @@ -616,7 +613,6 @@ WeaveSvc.prototype = { ret = true; } } - if (needKeys) { if (PubKeys.lastResource.lastChannel.responseStatus != 404 && PrivKeys.lastResource.lastChannel.responseStatus != 404) { @@ -791,18 +787,23 @@ WeaveSvc.prototype = { } }, + // XXX deletes all known collections; we should have a way to delete + // everything on the server by querying it to get all collections _wipeServer: function WeaveSvc__wipeServer() { let self = yield; - // tmp, delete all known collections - for each (let coll in ["keys", "crypto", "clients", - "bookmarks", "history", "tabs"]) { - let res = new Resource(this.clusterURL + this.username + "/" + coll + "/"); + let engines = Engines.getAll(); + engines.push(Clients, {name: "keys"}, {name: "crypto"}); + for each (let engine in engines) { + let url = this.clusterURL + this.username + "/" + engine.name + "/"; + let res = new Resource(url); try { yield res.delete(self.cb); } catch (e) { this._log.debug("Exception on delete: " + Utils.exceptionStr(e)); } + if (engine.resetLastSync) + engine.resetLastSync(); } }, wipeServer: function WeaveSvc_wipeServer(onComplete) { From 38761d6541fec6d178c6bac712bc80f35c819172 Mon Sep 17 00:00:00 2001 From: Date: Tue, 17 Feb 2009 16:28:54 -0800 Subject: [PATCH 0917/1860] First pass at Fennec tab UI. There's now a button that opens a fullscren panel from the left, which contains the names of all remote tabs. They're not clickable yet. --- services/sync/modules/engines/tabs.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 96ef9c8da05c..871707e4741f 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -315,8 +315,9 @@ TabTracker.prototype = { observe: function TabTracker_observe(aSubject, aTopic, aData) { dump("TabTracker spotted window open/close...\n"); let window = aSubject.QueryInterface(Ci.nsIDOMWindow); - // TODO: Not all windows have tabContainers. Fennec windows don't, - // for instance. + // Ignore windows that don't have tabContainers. + // TODO: Fennec windows don't have tabContainers, but we still want + // to register an observer in them. if (! window.getBrowser) return; let browser = window.getBrowser(); @@ -330,6 +331,7 @@ TabTracker.prototype = { container.removeEventListener("TabOpen", this.onTabChanged, false); container.removeEventListener("TabClose", this.onTabChanged, false); } + // TODO }, onTabChanged: function TabTracker_onTabChanged(event) { @@ -337,6 +339,7 @@ TabTracker.prototype = { }, get changedIDs() { + // The record for my own client is always the only changed record. let obj = {}; obj[Clients.clientID] = true; return obj; From fe4644498e0433c5e3f04db8ba9b2ce93eac5818 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 18 Feb 2009 12:03:12 -0800 Subject: [PATCH 0918/1860] try/catch exceptions on Preferences.reset() --- services/sync/modules/ext/Preferences.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/ext/Preferences.js b/services/sync/modules/ext/Preferences.js index 40c8f91c8111..e96a8c80f788 100644 --- a/services/sync/modules/ext/Preferences.js +++ b/services/sync/modules/ext/Preferences.js @@ -138,11 +138,15 @@ Preferences.prototype = { }, reset: function(prefName) { - this._prefSvc.clearUserPref(prefName); + try { + this._prefSvc.clearUserPref(prefName); + } catch (e) {} }, resetBranch: function(prefBranch) { - this._prefSvc.resetBranch(prefBranch); + try { + this._prefSvc.resetBranch(prefBranch); + } catch (e) {} } }; From c51cd4654cfda4e326e08ceeb0245e67b10bd67a Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Feb 2009 18:18:50 -0800 Subject: [PATCH 0919/1860] Fixed a bug in how Firefox gets URL history to save when syncing tabs up. Also some more work on the Fennec tabs UI. --- services/sync/modules/engines/tabs.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 871707e4741f..0801c3b0ed1a 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -87,6 +87,7 @@ TabStore.prototype = { _remoteClients: {}, _TabStore_init: function TabStore__init() { + // TODO: This gets dumped twice. Is TabStore initialized twice? dump("Initializing TabStore!!\n"); this._init(); this._readFromFile(); @@ -181,10 +182,15 @@ TabStore.prototype = { */ this._log.debug("Wrapping a tab with title " + currentPage.title); let urlHistory = []; - let entries = tab.entries.slice(tab.entries.length - 10); - for (let entry in entries) { - urlHistory.push( entry.url ); - } + // Include URLs in reverse order; max out at 10, and skip nulls. + for (let i = tab.entries.length -1; i >= 0; i++) { + let entry = tab.entries[i]; + if (entry && entry.url) + urlHistory.push(entry.url); + if (urlHistory.length >= 10) + break; + } + this._log.debug("This tab's url history is " + urlHistory + "\n"); record.addTab(currentPage.title, urlHistory); } } @@ -308,7 +314,6 @@ TabTracker.prototype = { // defined differently but should still exist... var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] .getService(Ci.nsIWindowWatcher); - dump("Initialized TabTracker\n"); ww.registerNotification(this); }, From df776b08bb8aa659c0f4717d669fbe591b163199 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 18 Feb 2009 20:17:30 -0800 Subject: [PATCH 0920/1860] make sure to only save the last sync timestamp as a number --- services/sync/modules/engines.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 01f6453eadd0..a11f4c44c5bc 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -202,6 +202,8 @@ SyncEngine.prototype = { return Svc.Prefs.get(this.name + ".lastSync", 0); }, set lastSync(value) { + if (typeof(value) == "string") + value = parseInt(value); Svc.Prefs.set(this.name + ".lastSync", value); }, resetLastSync: function SyncEngine_resetLastSync() { @@ -451,8 +453,6 @@ SyncEngine.prototype = { // save last modified date let mod = up.data.modified; - if (typeof(mod) == "string") - mod = parseInt(mod); if (mod > this.lastSync) this.lastSync = mod; } From db114950fe057ab7d265ea9536c673d725469d03 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Feb 2009 22:23:48 -0800 Subject: [PATCH 0921/1860] Fixed a bug with my fix -- I had inadvertently created an infinite loop. --- services/sync/modules/engines/tabs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0801c3b0ed1a..30c91de6a262 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -183,7 +183,7 @@ TabStore.prototype = { this._log.debug("Wrapping a tab with title " + currentPage.title); let urlHistory = []; // Include URLs in reverse order; max out at 10, and skip nulls. - for (let i = tab.entries.length -1; i >= 0; i++) { + for (let i = tab.entries.length -1; i >= 0; i--) { let entry = tab.entries[i]; if (entry && entry.url) urlHistory.push(entry.url); From 5914a48b30fb5848017a0f0270c102b01c640433 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 19 Feb 2009 00:36:55 -0800 Subject: [PATCH 0922/1860] pass the cryptoMetaURL to the store's createRecord --- services/sync/modules/engines.js | 4 +--- services/sync/modules/engines/bookmarks.js | 3 ++- services/sync/modules/engines/clients.js | 9 +-------- services/sync/modules/engines/history.js | 3 ++- services/sync/modules/engines/tabs.js | 3 ++- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a11f4c44c5bc..0fa5fa16578b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -214,9 +214,7 @@ SyncEngine.prototype = { // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { - let record = this._store.createRecord(id); - record.encryption = this.cryptoMetaURL; - return record; + return this._store.createRecord(id, this.cryptoMetaURL); }, // Check if a record is "like" another one, even though the IDs are different, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 33c496cf7daa..44319bb7d58b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -479,7 +479,7 @@ BookmarksStore.prototype = { }, // Create a record starting from the weave id (places guid) - createRecord: function BStore_createRecord(guid) { + createRecord: function BStore_createRecord(guid, cryptoMetaURL) { let record = this.cache.get(guid); if (record) return record; @@ -542,6 +542,7 @@ BookmarksStore.prototype = { record.parentid = this._getWeaveParentIdForItem(placeId); record.depth = this._itemDepth(placeId); record.sortindex = this._bms.getItemIndex(placeId); + record.encryption = cryptoMetaURL; this.cache.put(guid, record); return record; diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index c34f31eaf0f3..d3302405b210 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -74,13 +74,6 @@ ClientEngine.prototype = { this.setInfo(this.clientID, {name: "Firefox", type: "desktop"}); }, - // Override SyncEngine's to not set encryption URL - _createRecord: function SyncEngine__createRecord(id) { - let record = this._store.createRecord(id); - record.uri = this.engineURL + id; - return record; - }, - // get and set info for clients // FIXME: callers must use the setInfo interface or changes won't get synced, @@ -225,5 +218,5 @@ ClientTracker.prototype = { __proto__: Tracker.prototype, _logName: "ClientTracker", file: "clients", - get score() "75" // we always want to sync, but are not dying to do so + get score() 100 // always sync }; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index aa8000161e4c..9ceb35fea0a2 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -433,7 +433,7 @@ HistoryStore.prototype = { return this._hsvc.isVisited(url); }, - createRecord: function HistStore_createRecord(guid) { + createRecord: function HistStore_createRecord(guid, cryptoMetaURL) { let foo = this._findURLByGUID(guid); let record = new HistoryRec(); record.id = guid; @@ -441,6 +441,7 @@ HistoryStore.prototype = { record.histUri = foo.url; record.title = foo.title; record.visits = this._getVisits(record.histUri); + record.encryption = cryptoMetaURL; } else { record.cleartext = null; // deleted item } diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 30c91de6a262..cf0517978ba9 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -224,7 +224,7 @@ TabStore.prototype = { } }, - createRecord: function TabStore_createRecord(id) { + createRecord: function TabStore_createRecord(id, cryptoMetaURL) { this._log.debug("CreateRecord called for id " + id ); let record; if (id == this._localClientGUID) { @@ -235,6 +235,7 @@ TabStore.prototype = { record = this._remoteClients[id]; } record.id = id; + record.encryption = cryptoMetaURL; return record; }, From 7a3b7604435e053dfcd0a3ec18186060597540ef Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 19 Feb 2009 04:06:08 -0800 Subject: [PATCH 0923/1860] add a clearCache method to the record manager --- services/sync/modules/base_records/wbo.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 7efe8a66ee4f..1b60b3fbb4ba 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -209,6 +209,10 @@ RecordManager.prototype = { return false; }, + clearCache: function recordMgr_clearCache() { + this._records = {}; + }, + del: function RegordMgr_del(url) { delete this._records[url]; }, From 88251cc731d902e7b5056488631920c6c7bb0c9c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 19 Feb 2009 04:07:23 -0800 Subject: [PATCH 0924/1860] go back to storing the local client name and type as prefs. also store a 'syncID' (gets reset upon a server wipe) --- services/sync/modules/engines/clients.js | 41 +++++++++++++----------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index d3302405b210..853ce5bf29e6 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -42,7 +42,6 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/ext/Preferences.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -70,8 +69,6 @@ ClientEngine.prototype = { _ClientEngine_init: function ClientEngine__init() { this._init(); - if (!this.getInfo(this.clientID)) - this.setInfo(this.clientID, {name: "Firefox", type: "desktop"}); }, // get and set info for clients @@ -95,24 +92,28 @@ ClientEngine.prototype = { // helpers for getting/setting this client's info directly get clientID() { - if (!Preferences.get(PREFS_BRANCH + "client.GUID")) - Preferences.set(PREFS_BRANCH + "client.GUID", Utils.makeGUID()); - return Preferences.get(PREFS_BRANCH + "client.GUID"); + if (!Svc.Prefs.get("client.GUID")) + Svc.Prefs.set("client.GUID", Utils.makeGUID()); + return Svc.Prefs.get("client.GUID"); }, - get clientName() { return this.getInfo(this.clientID).name; }, - set clientName(value) { - let info = this.getInfo(this.clientID); - info.name = value; - this.setInfo(this.clientID, info); + get syncID() { + if (!Svc.Prefs.get("client.syncID")) + Svc.Prefs.set("client.syncID", Utils.makeGUID()); + return Svc.Prefs.get("client.syncID"); + }, + set syncID(value) { + Svc.Prefs.set("client.syncID", value); + }, + resetSyncID: function ClientEngine_resetSyncID() { + Svc.Prefs.reset("client.syncID"); }, - get clientType() { return this.getInfo(this.clientID).type; }, - set clientType(value) { - let info = this.getInfo(this.clientID); - info.type = value; - this.setInfo(this.clientID, info); - } + get clientName() { return Svc.Prefs.get("client.name", "Firefox"); }, + set clientName(value) { Svc.Prefs.set("client.name", value); }, + + get clientType() { return Svc.Prefs.get("client.type", "desktop"); }, + set clientType(value) { Svc.Prefs.set("client.type", value); } }; function ClientStore() { @@ -197,6 +198,7 @@ ClientStore.prototype = { // methods to query local data: getAllIDs, itemExists, createRecord getAllIDs: function ClientStore_getAllIDs() { + this.clients[Clients.clientID] = this.createRecord(Clients.clientID); return this.clients; }, @@ -206,7 +208,10 @@ ClientStore.prototype = { createRecord: function ClientStore_createRecord(id) { let record = new ClientRecord(); - record.payload = this.clients[id]; + if (id == Clients.clientID) + record.payload = {name: Clients.clientName, type: Clients.clientType}; + else + record.payload = this.clients[id]; return record; } }; From 93b70c004949e0f81cba2ecfe535f49ffb070fdd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 19 Feb 2009 04:09:55 -0800 Subject: [PATCH 0925/1860] make a more concerted attempt at doing a fresh start on various server conditions, such as missing keys, etc. clear local caches (makes it so Fx doesn't need to be restarted after a server wipe). set the lastsync pref here instead of having the window do it --- services/sync/modules/service.js | 103 +++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1fd857946f9b..44f2d62c921b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -73,6 +73,7 @@ Cu.import("resource://weave/faultTolerance.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/oauth.js"); @@ -319,6 +320,9 @@ WeaveSvc.prototype = { this._initLogs(); this._log.info("Weave " + WEAVE_VERSION + " initializing"); + if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) + this._wipeClientMetadata(); + let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. getService(Ci.nsIHttpProtocolHandler).userAgent; this._log.info(ua); @@ -564,6 +568,7 @@ WeaveSvc.prototype = { _remoteSetup: function WeaveSvc__remoteSetup() { let self = yield; let ret = false; // false to abort sync + let reset = false; this._log.debug("Fetching global metadata record"); let meta = yield Records.import(self.cb, this.clusterURL + @@ -573,27 +578,26 @@ WeaveSvc.prototype = { // parsing function so we can implement greater-than. So we // just check for != which is wrong! if (meta.payload.storageVersion != MIN_SERVER_STORAGE_VERSION) { - this._log.debug("Last upload client version too old, wiping server data" + - "to ensure consistency"); - yield this._wipeServer.async(this, self.cb); + reset = true; + yield this._freshStart.async(this, self.cb); + this._log.info("Server data wiped to ensure consistency after client upgrade"); - this._log.debug("Uploading new metadata record"); - meta.payload.storageVersion = WEAVE_VERSION; - let res = new Resource(meta.uri); - yield res.put(self.cb, meta.serialize()); + } else if (!meta.payload.syncID) { + reset = true; + yield this._freshStart.async(this, self.cb); + this._log.info("Server data wiped to ensure consistency after client upgrade"); + + } else if (meta.payload.syncID != Clients.syncID) { + this._wipeClientMetadata(); + Clients.syncID = meta.payload.syncID; + this._log.info("Cleared local caches after server wipe was detected"); } } else { if (Records.lastResource.lastChannel.responseStatus == 404) { - this._log.debug("Metadata record not found, deleting all server " + - "data to ensure consistency"); - yield this._wipeServer.async(this, self.cb); - - this._log.debug("Uploading new metadata record"); - meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); - meta.payload.storageVersion = WEAVE_VERSION; - let res = new Resource(meta.uri); - yield res.put(self.cb, meta.serialize()); + reset = true; + yield this._freshStart.async(this, self.cb); + this._log.info("Metadata record not found, server wiped to ensure consistency"); } else { this._log.debug("Unknown error while downloading metadata record. " + @@ -632,6 +636,11 @@ WeaveSvc.prototype = { return; } + if (!reset) { + yield this._freshStart.async(this, self.cb); + this._log.info("Server data wiped to ensure consistency due to missing keys"); + } + let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb); if (pass) { let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, @@ -684,10 +693,12 @@ WeaveSvc.prototype = { } } - if (this._syncError) + if (!this._syncError) { + Svc.Prefs.reset("lastsync"); + Svc.Prefs.set("lastsync", Date.now()); + this._log.info("Sync completed successfully"); + } else this._log.warn("Some engines did not sync correctly"); - else - this._log.info("Sync completed successfully"); } finally { this.cancelRequested = false; @@ -760,10 +771,12 @@ WeaveSvc.prototype = { } } - if (this._syncError) - this._log.warn("Some engines did not sync correctly"); - else + if (!this._syncError) { + Svc.Prefs.reset("lastsync"); + Svc.Prefs.set("lastsync", Date.now()); this._log.info("Sync completed successfully"); + } else + this._log.warn("Some engines did not sync correctly"); } finally { this._cancelRequested = false; @@ -787,11 +800,28 @@ WeaveSvc.prototype = { } }, + _freshStart: function WeaveSvc__freshStart() { + let self = yield; + + this._wipeClientMetadata(); + this._log.info("Client metadata wiped, deleting server data"); + yield this._wipeServer.async(this, self.cb); + + this._log.debug("Uploading new metadata record"); + meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); + meta.payload.storageVersion = WEAVE_VERSION; + meta.payload.syncID = Clients.syncID; + let res = new Resource(meta.uri); + yield res.put(self.cb, meta.serialize()); + }, + // XXX deletes all known collections; we should have a way to delete // everything on the server by querying it to get all collections _wipeServer: function WeaveSvc__wipeServer() { let self = yield; + Clients.resetSyncID(); + let engines = Engines.getAll(); engines.push(Clients, {name: "keys"}, {name: "crypto"}); for each (let engine in engines) { @@ -806,9 +836,30 @@ WeaveSvc.prototype = { engine.resetLastSync(); } }, - wipeServer: function WeaveSvc_wipeServer(onComplete) { - this._catchAll( - this._notify("wipe-server", "", - this._localLock(this._wipeServer))).async(this, onComplete); + + _wipeClientMetadata: function WeaveSvc__wipeClientMetadata() { + this.clearLogs(); + this._log.info("Logs reinitialized"); + + PubKeys.clearCache(); + PrivKeys.clearCache(); + CryptoMetas.clearCache(); + Records.clearCache(); + + Clients._store.wipe(); + Engines.get("tabs")._store.wipe(); + + try { + let cruft = this._dirSvc.get("ProfD", Ci.nsIFile); + cruft.QueryInterface(Ci.nsILocalFile); + cruft.append("weave"); + cruft.append("snapshots"); + if (cruft.exists()) + cruft.remove(true); + } catch (e) { + this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); + } } }; + + From 5e83915d131afb7deeb43beba8945ad8f9677047 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 19 Feb 2009 04:10:52 -0800 Subject: [PATCH 0926/1860] make lastsync pref an int; don't set it in sync.js --- services/sync/modules/wrap.js | 1 + services/sync/services-sync.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 108eb39c969b..71333aeb82dd 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -165,6 +165,7 @@ let Wrap = { } catch (e) { this._log.debug("Caught exception: " + Utils.exceptionStr(e)); + this._log.debug("\n" + Utils.stackTrace(e)); } self.done(ret); }; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 35b073a8f525..dcddef0a13d7 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -3,7 +3,7 @@ pref("extensions.weave.serverURL", "https://services.mozilla.com/"); pref("extensions.weave.encryption", "aes-256-cbc"); pref("extensions.weave.lastversion", "firstrun"); -pref("extensions.weave.lastsync", "0"); +pref("extensions.weave.lastsync", 0); pref("extensions.weave.ui.syncnow", true); pref("extensions.weave.ui.sharebookmarks", false); From d372e94239cafbe38fed924aecd2330086d2655b Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Thu, 19 Feb 2009 07:50:04 -0800 Subject: [PATCH 0927/1860] bug 479248: integrate new version of Preferences.js module --- services/sync/modules/ext/Preferences.js | 93 +++++++++++++++++------- 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/services/sync/modules/ext/Preferences.js b/services/sync/modules/ext/Preferences.js index e96a8c80f788..937a6451650e 100644 --- a/services/sync/modules/ext/Preferences.js +++ b/services/sync/modules/ext/Preferences.js @@ -70,14 +70,12 @@ Preferences.prototype = { * @returns the value of the pref, if any; otherwise the default value */ get: function(prefName, defaultValue) { - // We can't check for |prefName.constructor == Array| here, since we have - // a different global object, so we check the constructor name instead. - if (typeof prefName == "object" && prefName.constructor.name == Array.name) + if (isArray(prefName)) return prefName.map(function(v) this.get(v), this); switch (this._prefSvc.getPrefType(prefName)) { case Ci.nsIPrefBranch.PREF_STRING: - return this._prefSvc.getCharPref(prefName); + return this._prefSvc.getComplexValue(prefName, Ci.nsISupportsString).data; case Ci.nsIPrefBranch.PREF_INT: return this._prefSvc.getIntPref(prefName); @@ -92,29 +90,58 @@ Preferences.prototype = { }, set: function(prefName, prefValue) { - // We can't check for |prefName.constructor == Object| here, since we have - // a different global object, so we check the constructor name instead. - if (typeof prefName == "object" && prefName.constructor.name == Object.name) + if (isObject(prefName)) { for (let [name, value] in Iterator(prefName)) this.set(name, value); - else { - switch (typeof prefValue) { - case "number": - this._prefSvc.setIntPref(prefName, prefValue); - break; + return; + } - case "boolean": - this._prefSvc.setBoolPref(prefName, prefValue); - break; + switch (typeof prefValue) { + case "number": + this._prefSvc.setIntPref(prefName, prefValue); + if (prefValue % 1 != 0) + Cu.reportError("WARNING: setting " + prefName + " pref to non-integer number " + + prefValue + " converts it to integer number " + this.get(prefName) + + "; to retain precision, store non-integer numbers as strings"); + break; - case "string": - default: - this._prefSvc.setCharPref(prefName, prefValue); - break; + case "boolean": + this._prefSvc.setBoolPref(prefName, prefValue); + break; + + case "string": + default: { + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = prefValue; + this._prefSvc.setComplexValue(prefName, Ci.nsISupportsString, string); + break; } } }, + reset: function(prefName) { + if (isArray(prefName)) { + prefName.map(function(v) this.reset(v), this); + return; + } + + try { + this._prefSvc.clearUserPref(prefName); + } + catch(ex) { + // The pref service throws NS_ERROR_UNEXPECTED when the caller tries + // to reset a pref that doesn't exist or is already set to its default + // value. This interface fails silently in those cases, so callers + // can unconditionally reset a pref without having to check if it needs + // resetting first or trap exceptions after the fact. It passes through + // other exceptions, however, so callers know about them, since we don't + // know what other exceptions might be thrown and what they might mean. + if (ex.result != Cr.NS_ERROR_UNEXPECTED) + throw ex; + } + }, + // FIXME: make the methods below accept an array of pref names. has: function(prefName) { @@ -137,16 +164,18 @@ Preferences.prototype = { this._prefSvc.unlockPref(prefName); }, - reset: function(prefName) { - try { - this._prefSvc.clearUserPref(prefName); - } catch (e) {} - }, - resetBranch: function(prefBranch) { try { this._prefSvc.resetBranch(prefBranch); - } catch (e) {} + } + catch(ex) { + // The current implementation of nsIPrefBranch in Mozilla + // doesn't implement resetBranch, so we do it ourselves. + if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) + this.reset(this._prefSvc.getChildList(prefBranch, [])); + else + throw ex; + } } }; @@ -155,3 +184,15 @@ Preferences.prototype = { // preferences directly via the constructor without having to create an instance // first. Preferences.__proto__ = Preferences.prototype; + +function isArray(val) { + // We can't check for |val.constructor == Array| here, since we might have + // a different global object, so we check the constructor name instead. + return (typeof val == "object" && val.constructor.name == Array.name); +} + +function isObject(val) { + // We can't check for |val.constructor == Object| here, since we might have + // a different global object, so we check the constructor name instead. + return (typeof val == "object" && val.constructor.name == Object.name); +} From 3d5d36ce197598f569da7c4e69a98ca99e0564fb Mon Sep 17 00:00:00 2001 From: Date: Thu, 19 Feb 2009 11:20:23 -0800 Subject: [PATCH 0928/1860] Tab sync now skips empty tabs. This fixes bug 479216. --- services/sync/modules/engines/tabs.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index cf0517978ba9..831b2d4efa5d 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -176,6 +176,9 @@ TabStore.prototype = { for (let j = 0; j < window.tabs.length; j++) { let tab = window.tabs[j]; + // Skip empty (i.e. just-opened, no history yet) tabs: + if (tab.entries.length == 0) + continue; let currentPage = tab.entries[tab.entries.length - 1]; /* TODO not always accurate -- if you've hit Back in this tab, then the current * page might not be the last entry. Deal with this later. @@ -190,7 +193,6 @@ TabStore.prototype = { if (urlHistory.length >= 10) break; } - this._log.debug("This tab's url history is " + urlHistory + "\n"); record.addTab(currentPage.title, urlHistory); } } From 8a04a636a812f36d3fba665556c63481a93219f8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 19 Feb 2009 16:41:53 -0800 Subject: [PATCH 0929/1860] Bug 479232: don't wipe tabs engine when it hasn't loaded --- services/sync/modules/service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 44f2d62c921b..796dbcd49de0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -847,7 +847,8 @@ WeaveSvc.prototype = { Records.clearCache(); Clients._store.wipe(); - Engines.get("tabs")._store.wipe(); + if (Engines.get("tabs")) + Engines.get("tabs")._store.wipe(); try { let cruft = this._dirSvc.get("ProfD", Ci.nsIFile); From 5dee51d24bab90fc403145218aebe47871a11f87 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 20 Feb 2009 01:49:00 -0800 Subject: [PATCH 0930/1860] don't load old crypto module --- services/sync/modules/base_records/collection.js | 1 - services/sync/modules/engines/bookmarks.js | 1 - 2 files changed, 2 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index fdaad8afc0f0..53cefe5417bf 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -45,7 +45,6 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 44319bb7d58b..5a088cef82d5 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -58,7 +58,6 @@ const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/crypto.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); From 51a1f264eeb02b1b8028aca05a5ffcdb3e50b7db Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 20 Feb 2009 01:50:26 -0800 Subject: [PATCH 0931/1860] upgrade to latest Observers.js version --- services/sync/modules/ext/Observers.js | 127 ++++++++++++++++++++----- services/sync/modules/notifications.js | 4 +- services/sync/modules/resource.js | 12 +-- 3 files changed, 110 insertions(+), 33 deletions(-) diff --git a/services/sync/modules/ext/Observers.js b/services/sync/modules/ext/Observers.js index d557576e45eb..0700115e5728 100644 --- a/services/sync/modules/ext/Observers.js +++ b/services/sync/modules/ext/Observers.js @@ -44,59 +44,136 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +/** + * A service for adding, removing and notifying observers of notifications. + * Wraps the nsIObserverService interface. + * + * @version 0.2 + */ let Observers = { - add: function(callback, topic) { - let observer = new Observer(callback); - if (!(topic in Observers._observers)) - Observers._observers[topic] = {}; - Observers._observers[topic][callback] = observer; - Observers._service.addObserver(observer, topic, true); + /** + * Register the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic to observe + * + * @param callback {Object} + * the callback; an Object that implements nsIObserver or a Function + * that gets called when the notification occurs + * + * @param thisObject {Object} [optional] + * the object to use as |this| when calling a Function callback + * + * @returns the observer + */ + add: function(topic, callback, thisObject) { + let observer = new Observer(topic, callback, thisObject); + this._cache.push(observer); + this._service.addObserver(observer, topic, true); + return observer; }, - remove: function(callback, topic) { - let observer = Observers._observers[topic][callback]; + /** + * Unregister the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic being observed + * + * @param callback {Object} + * the callback doing the observing + * + * @param thisObject {Object} [optional] + * the object being used as |this| when calling a Function callback + */ + remove: function(topic, callback, thisObject) { + // This seems fairly inefficient, but I'm not sure how much better + // we can make it. We could index by topic, but we can't index by callback + // or thisObject, as far as I know, since the keys to JavaScript hashes + // (a.k.a. objects) can apparently only be primitive values. + let [observer] = this._cache.filter(function(v) v.topic == topic && + v.callback == callback && + v.thisObject == thisObject); if (observer) { - Observers._service.removeObserver(observer, topic); - delete this._observers[topic][callback]; + this._service.removeObserver(observer, topic); + this._cache.splice(this._cache.indexOf(observer), 1); } }, - notify: function(subject, topic, data) { - Observers._service.notifyObservers(new Subject(subject), topic, data); + /** + * Notify observers about something. + * + * @param topic {String} + * the topic to notify observers about + * + * @param subject {Object} [optional] + * some information about the topic; can be any JS object or primitive + * + * @param data {String} [optional] [deprecated] + * some more information about the topic; deprecated as the subject + * is sufficient to pass all needed information to the JS observers + * that this module targets; if you have multiple values to pass to + * the observer, wrap them in an object and pass them via the subject + * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) + */ + notify: function(topic, subject, data) { + subject = (typeof subject == "undefined") ? null : new Subject(subject); + data = (typeof data == "undefined") ? null : data; + this._service.notifyObservers(subject, topic, data); }, _service: Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService), - // Observers indexed by callback. This lets us get the observer - // to remove when a caller calls |remove|, passing it a callback. - _observers: {} + /** + * A cache of observers that have been added. + * + * We use this to remove observers when a caller calls |remove|. + * + * XXX This might result in reference cycles, causing memory leaks, + * if we hold a reference to an observer that holds a reference to us. + * Could we fix that by making this an independent top-level object + * rather than a property of this object? + */ + _cache: [] }; -function Observer(callback) { - this._callback = callback; +function Observer(topic, callback, thisObject) { + this.topic = topic; + this.callback = callback; + this.thisObject = thisObject; } Observer.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), observe: function(subject, topic, data) { - // Pass the wrappedJSObject for subjects that have one. Otherwise pass - // the subject itself. This way we support both wrapped subjects created + // Extract the wrapped object for subjects that are one of our wrappers + // around a JS object. This way we support both wrapped subjects created // using this module and those that are real XPCOM components. - let unwrappedSubject = subject.wrappedJSObject || subject; + if (subject && typeof subject == "object" && + ("wrappedJSObject" in subject) && + ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) + subject = subject.wrappedJSObject.object; - if (typeof this._callback == "function") - this._callback(unwrappedSubject, topic, data); - else - this._callback.observe(unwrappedSubject, topic, data); + if (typeof this.callback == "function") { + if (this.thisObject) + this.callback.call(this.thisObject, subject, data); + else + this.callback(subject, data); + } + else // typeof this.callback == "object" (nsIObserver) + this.callback.observe(subject, topic, data); } } function Subject(object) { - this.wrappedJSObject = object; + // Double-wrap the object and set a property identifying the wrappedJSObject + // as one of our wrappers to distinguish between subjects that are one of our + // wrappers (which we should unwrap when notifying our observers) and those + // that are real JS XPCOM components (which we should pass through unaltered). + this.wrappedJSObject = { observersModuleSubjectWrapper: true, object: object }; } Subject.prototype = { diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index 17bfdff0b393..0b6af897b7da 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -64,7 +64,7 @@ let Notifications = { add: function Notifications_add(notification) { this.notifications.push(notification); - Observers.notify(notification, "weave:notification:added", null); + Observers.notify("weave:notification:added", notification, null); }, remove: function Notifications_remove(notification) { @@ -72,7 +72,7 @@ let Notifications = { if (index != -1) { this.notifications.splice(index, 1); - Observers.notify(notification, "weave:notification:removed", null); + Observers.notify("weave:notification:removed", notification, null); } }, diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 3850edc308b5..912827409e47 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -150,7 +150,7 @@ Resource.prototype = { this._lastChannel = ios.newChannel(this.spec, null, null). QueryInterface(Ci.nsIHttpChannel); this._lastChannel.notificationCallbacks = new badCertListener(); - + let headers = this.headers; // avoid calling the authorizer more than once for (let key in headers) { if (key == 'Authorization') @@ -338,14 +338,14 @@ JsonFilter.prototype = { beforePUT: function JsonFilter_beforePUT(data) { let self = yield; this._log.trace("Encoding data as JSON"); - Observers.notify(null, "weave:service:sync:status", "stats.encoding-json"); + Observers.notify("weave:service:sync:status", null, "stats.encoding-json"); self.done(this._json.encode(data)); }, afterGET: function JsonFilter_afterGET(data) { let self = yield; this._log.trace("Decoding JSON data"); - Observers.notify(null, "weave:service:sync:status", "stats.decoding-json"); + Observers.notify("weave:service:sync:status", null, "stats.decoding-json"); self.done(this._json.decode(data)); } }; @@ -356,16 +356,16 @@ badCertListener.prototype = { getInterface: function(aIID) { return this.QueryInterface(aIID); }, - + QueryInterface: function(aIID) { if (aIID.equals(Components.interfaces.nsIBadCertListener2) || aIID.equals(Components.interfaces.nsIInterfaceRequestor) || aIID.equals(Components.interfaces.nsISupports)) return this; - + throw Components.results.NS_ERROR_NO_INTERFACE; }, - + notifyCertProblem: function certProblem(socketInfo, sslStatus, targetHost) { // Silently ignore? let log = Log4Moz.repository.getLogger("Service.CertListener"); From bd2abf266835ff665face6e039ff6937e454c991 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 20 Feb 2009 01:51:20 -0800 Subject: [PATCH 0932/1860] shorten engine logging pref names, set defaults for them --- services/sync/modules/engines.js | 2 +- services/sync/services-sync.js | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 0fa5fa16578b..b55cf7cdc0cf 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -133,7 +133,7 @@ Engine.prototype = { }, _init: function Engine__init() { - let levelPref = "log.logger.service.engine." + this.name; + let levelPref = "log.logger.engine." + this.name; let level = "Debug"; try { level = Utils.prefs.getCharPref(levelPref); } catch (e) { /* ignore unset prefs */ } diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index dcddef0a13d7..79daada4d8b2 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -28,15 +28,12 @@ pref("extensions.weave.log.appender.briefLog", "Info"); pref("extensions.weave.log.appender.debugLog", "Trace"); pref("extensions.weave.log.rootLogger", "Trace"); -pref("extensions.weave.log.logger.async", "Debug"); -pref("extensions.weave.log.logger.service.crypto", "Debug"); -pref("extensions.weave.log.logger.service.dav", "Debug"); -pref("extensions.weave.log.logger.service.engine", "Debug"); pref("extensions.weave.log.logger.service.main", "Trace"); +pref("extensions.weave.log.logger.async", "Debug"); pref("extensions.weave.log.logger.network.resources", "Debug"); - -pref("extensions.weave.xmpp.enabled", false); -pref("extensions.weave.xmpp.server.url", "https://sm-labs01.mozilla.org:81/xmpp"); -pref("extensions.weave.xmpp.server.realm", "sm-labs01.mozilla.org"); +pref("extensions.weave.log.logger.engine.bookmarks", "Debug"); +pref("extensions.weave.log.logger.engine.history", "Debug"); +pref("extensions.weave.log.logger.engine.tabs", "Debug"); +pref("extensions.weave.log.logger.engine.clients", "Debug"); pref("extensions.weave.network.numRetries", 2); From 4665710651c620c4be06852771848927764dc0a8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 20 Feb 2009 01:52:07 -0800 Subject: [PATCH 0933/1860] random fixes, actually syncs client data now :) --- services/sync/modules/engines/clients.js | 47 +++++++++++++++++------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 853ce5bf29e6..5e58a4758321 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -125,19 +125,24 @@ ClientStore.prototype = { _ClientStore_init: function ClientStore__init() { this._init.call(this); - this.clients = {}; + this._clients = {}; this.loadSnapshot(); }, + get clients() { + this._clients[Clients.clientID] = this.createRecord(Clients.clientID).payload; + return this._clients; + }, + // get/set client info; doesn't use records like the methods below // NOTE: setInfo will not update tracker. Use Engine.setInfo for that getInfo: function ClientStore_getInfo(id) { - return this.clients[id]; + return this._clients[id]; }, setInfo: function ClientStore_setInfo(id, info) { - this.clients[id] = info; + this._clients[id] = info; this.saveSnapshot(); }, @@ -147,7 +152,7 @@ ClientStore.prototype = { this._log.debug("Saving client list to disk"); let file = Utils.getProfileFile( {path: "weave/meta/clients.json", autoCreate: true}); - let out = Svc.Json.encode(this.clients); + let out = Svc.Json.encode(this._clients); let [fos] = Utils.open(file, ">"); fos.writeString(out); fos.close(); @@ -162,7 +167,7 @@ ClientStore.prototype = { let [is] = Utils.open(file, "<"); let json = Utils.readStream(is); is.close(); - this.clients = Svc.Json.decode(json); + this._clients = Svc.Json.decode(json); } catch (e) { this._log.debug("Failed to load saved client list" + e); } @@ -170,48 +175,64 @@ ClientStore.prototype = { // methods to apply changes: create, remove, update, changeItemID, wipe + applyIncoming: function ClientStore_applyIncoming(onComplete, record) { + let fn = function(rec) { + let self = yield; + if (!rec.payload) + this.remove(rec); + else if (!this.itemExists(rec.id)) + this.create(rec); + else + this.update(rec); + }; + fn.async(this, onComplete, record); + }, + create: function ClientStore_create(record) { this.update(record); }, update: function ClientStore_update(record) { - this.clients[record.id] = record.payload; + this._log.debug("Updating client " + record.id); + this._clients[record.id] = record.payload; this.saveSnapshot(); }, remove: function ClientStore_remove(record) { - delete this.clients[record.id]; + this._log.debug("Removing client " + record.id); + delete this._clients[record.id]; this.saveSnapshot(); }, changeItemID: function ClientStore_changeItemID(oldID, newID) { - this.clients[newID] = this.clients[oldID]; - delete this.clients[oldID]; + this._clients[newID] = this._clients[oldID]; + delete this._clients[oldID]; this.saveSnapshot(); }, wipe: function ClientStore_wipe() { - this.clients = {}; + this._log.debug("Wiping local clients store"); + this._clients = {}; this.saveSnapshot(); }, // methods to query local data: getAllIDs, itemExists, createRecord getAllIDs: function ClientStore_getAllIDs() { - this.clients[Clients.clientID] = this.createRecord(Clients.clientID); return this.clients; }, itemExists: function ClientStore_itemExists(id) { - return id in this.clients; + return id in this._clients; }, createRecord: function ClientStore_createRecord(id) { let record = new ClientRecord(); + record.id = id; if (id == Clients.clientID) record.payload = {name: Clients.clientName, type: Clients.clientType}; else - record.payload = this.clients[id]; + record.payload = this._clients[id]; return record; } }; From 086a0e1b2b80cbe63b616961585b1cfd19cf8b01 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 20 Feb 2009 01:54:45 -0800 Subject: [PATCH 0934/1860] Add method call for removing an appender. Patch by kixx --- services/sync/modules/log4moz.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 77a3ecc4c116..a2477e211f18 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -222,6 +222,15 @@ Logger.prototype = { this._appenders.push(appender); }, + removeAppender: function Logger_removeAppender(appender) { + let newAppenders = []; + for (let i = 0; i < this._appenders.length; i++) { + if (this._appenders[i] != appender) + newAppenders.push(this._appenders[i]); + } + this._appenders = newAppenders; + }, + log: function Logger_log(message) { if (this.level > message.level) return; From 815d23f2230b068b610586f2bb6c44fed7ec6b3d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 20 Feb 2009 12:18:43 -0800 Subject: [PATCH 0935/1860] use auth.services.mozilla.com as default server url --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 79daada4d8b2..9b674f2da454 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,4 @@ -pref("extensions.weave.serverURL", "https://services.mozilla.com/"); +pref("extensions.weave.serverURL", "https://auth.services.mozilla.com/"); pref("extensions.weave.encryption", "aes-256-cbc"); From c4b76ada2a18443741b32eb706cddb17937ea424 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 22 Feb 2009 00:04:58 -0800 Subject: [PATCH 0936/1860] Wrap.notify now takes the topic prefix as an argument, instead of requiring this._osPrefix to be set; use Observers.js in several places (sync.js, status.js, etc); some event topics have changed, beware; fix up status window, now prints some useful status as sync progresses --- services/sync/locales/en-US/status.properties | 4 +- services/sync/modules/engines.js | 13 ++-- services/sync/modules/resource.js | 2 - services/sync/modules/service.js | 8 +-- services/sync/modules/wrap.js | 61 +++++++++---------- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index 33db96c697cd..5ba2b500178e 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -9,7 +9,9 @@ status.closing = Closing... status.locked = Server Busy status.tryagain = Please try again in a few minutes. -status.engine_start = Starting Sync +status.engine.start = Starting Sync +status.engine.process-incoming = Processing Incoming Items +status.engine.upload-outgoing = Uploading Outgoing Items status.downloading-status = Downloading Status from Server status.uploading-status = Updating Status on Server diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b55cf7cdc0cf..c161f95bc4ce 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -43,6 +43,7 @@ const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -99,8 +100,6 @@ EngineManagerSvc.prototype = { function Engine() { this._init(); } Engine.prototype = { - _notify: Wrap.notify, - name: "engine", displayName: "Boring Engine", logName: "Engine", @@ -138,9 +137,9 @@ Engine.prototype = { try { level = Utils.prefs.getCharPref(levelPref); } catch (e) { /* ignore unset prefs */ } + this._notify = Wrap.notify("weave:engine:"); this._log = Log4Moz.repository.getLogger("Engine." + this.logName); this._log.level = Log4Moz.Level[level]; - this._osPrefix = "weave:" + this.name + "-engine:"; this._tracker; // initialize tracker to load previously changed IDs this._log.debug("Engine initialized"); @@ -149,13 +148,13 @@ Engine.prototype = { sync: function Engine_sync(onComplete) { if (!this._sync) throw "engine does not implement _sync method"; - this._notify("sync", "", this._sync).async(this, onComplete); + this._notify("sync", this.name, this._sync).async(this, onComplete); }, wipeServer: function Engimne_wipeServer(onComplete) { if (!this._wipeServer) throw "engine does not implement _wipeServer method"; - this._notify("wipe-server", "", this._wipeServer).async(this, onComplete); + this._notify("wipe-server", this.name, this._wipeServer).async(this, onComplete); }, _wipeClient: function Engine__wipeClient() { @@ -164,7 +163,7 @@ Engine.prototype = { this._store.wipe(); }, wipeClient: function Engine_wipeClient(onComplete) { - this._notify("wipe-client", "", this._wipeClient).async(this, onComplete); + this._notify("wipe-client", this.name, this._wipeClient).async(this, onComplete); } }; @@ -470,7 +469,9 @@ SyncEngine.prototype = { try { yield this._syncStartup.async(this, self.cb); + Observers.notify("weave:engine:sync:status", "process-incoming"); yield this._processIncoming.async(this, self.cb); + Observers.notify("weave:engine:sync:status", "upload-outgoing"); yield this._uploadOutgoing.async(this, self.cb); yield this._syncFinish.async(this, self.cb); } diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 912827409e47..5d094522a110 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -338,14 +338,12 @@ JsonFilter.prototype = { beforePUT: function JsonFilter_beforePUT(data) { let self = yield; this._log.trace("Encoding data as JSON"); - Observers.notify("weave:service:sync:status", null, "stats.encoding-json"); self.done(this._json.encode(data)); }, afterGET: function JsonFilter_afterGET(data) { let self = yield; this._log.trace("Decoding JSON data"); - Observers.notify("weave:service:sync:status", null, "stats.decoding-json"); self.done(this._json.decode(data)); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 796dbcd49de0..e34311c97989 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -107,13 +107,13 @@ Utils.lazy(Weave, 'Service', WeaveSvc); * Main entry point into Weave's sync framework */ -function WeaveSvc() {} +function WeaveSvc() { + this._notify = Wrap.notify("weave:service:"); +} WeaveSvc.prototype = { - _notify: Wrap.notify, _localLock: Wrap.localLock, _catchAll: Wrap.catchAll, - _osPrefix: "weave:service:", _isQuitting: false, _loggedIn: false, _syncInProgress: false, @@ -550,7 +550,7 @@ WeaveSvc.prototype = { this._keyPair = {}; ID.get('WeaveID').setTempPassword(null); // clear cached password ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase - this._os.notifyObservers(null, "weave:service:logout:success", ""); + this._os.notifyObservers(null, "weave:service:logout:finish", ""); }, serverWipe: function WeaveSvc_serverWipe(onComplete) { diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 71333aeb82dd..f91764a446da 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -42,6 +42,7 @@ const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/util.js"); @@ -59,54 +60,50 @@ Function.prototype.async = Async.sugar; let Wrap = { - // NOTE: requires _osPrefix string property in your object - // NOTE2: copy this function over to your objects, use like this: + // NOTE: copy this function over to your objects, use like this: // + // function MyObj() { + // this._notify = Wrap.notify("some:prefix:"); + // } // MyObj.prototype = { - // _notify: Wrap.notify, // ... // method: function MyMethod() { // let self = yield; // ... // // doFoo is assumed to be an asynchronous method - // this._notify("foo", this._doFoo, arg1, arg2).async(this, self.cb); + // this._notify("foo", "", this._doFoo, arg1, arg2).async(this, self.cb); // let ret = yield; // ... // } // }; - notify: function Weave_notify(name, payload, method /* , arg1, arg2, ..., argN */) { - let savedName = name; - let savedPayload = payload; - let savedMethod = method; - let savedArgs = Array.prototype.slice.call(arguments, 3); + notify: function WeaveWrap_notify(prefix) { + return function NotifyWrapMaker(name, subject, method) { + let savedArgs = Array.prototype.slice.call(arguments, 4); + return function NotifyWrap() { + let self = yield; + let ret; + let args = Array.prototype.slice.call(arguments); - return function WeaveNotifyWrapper(/* argN+1, argN+2, ... */) { - let self = yield; - let ret; - let args = Array.prototype.slice.call(arguments); + try { + this._log.debug("Event: " + prefix + name + ":start"); + Observers.notify(prefix + name + ":start", subject); - try { - this._log.debug("Event: " + this._osPrefix + savedName + ":start"); - this._os.notifyObservers(null, this._osPrefix + savedName + ":start", savedPayload); - this._os.notifyObservers(null, this._osPrefix + "global:start", savedPayload); + args = savedArgs.concat(args); + args.unshift(this, method, self.cb); + Async.run.apply(Async, args); + ret = yield; - args = savedArgs.concat(args); - args.unshift(this, savedMethod, self.cb); - Async.run.apply(Async, args); - ret = yield; + this._log.debug("Event: " + prefix + name + ":finish"); + let foo = Observers.notify(prefix + name + ":finish", subject); - this._log.debug("Event: " + this._osPrefix + savedName + ":success"); - this._os.notifyObservers(null, this._osPrefix + savedName + ":success", savedPayload); - this._os.notifyObservers(null, this._osPrefix + "global:success", savedPayload); + } catch (e) { + this._log.debug("Event: " + prefix + name + ":error"); + Observers.notify(prefix + name + ":error", subject); + throw e; + } - } catch (e) { - this._log.debug("Event: " + this._osPrefix + savedName + ":error"); - this._os.notifyObservers(null, this._osPrefix + savedName + ":error", savedPayload); - this._os.notifyObservers(null, this._osPrefix + "global:error", savedPayload); - throw e; - } - - self.done(ret); + self.done(ret); + }; }; }, From 6b7ef429ba61647bc210e752dbc73328795909da Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 22 Feb 2009 00:21:15 -0800 Subject: [PATCH 0937/1860] fix 'last sync' date in weave menu --- services/sync/modules/service.js | 2 +- services/sync/services-sync.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e34311c97989..223df9a6184e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -695,7 +695,7 @@ WeaveSvc.prototype = { if (!this._syncError) { Svc.Prefs.reset("lastsync"); - Svc.Prefs.set("lastsync", Date.now()); + Svc.Prefs.set("lastsync", new Date().toString()); this._log.info("Sync completed successfully"); } else this._log.warn("Some engines did not sync correctly"); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9b674f2da454..03eff5790cb9 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -3,7 +3,6 @@ pref("extensions.weave.serverURL", "https://auth.services.mozilla.com/"); pref("extensions.weave.encryption", "aes-256-cbc"); pref("extensions.weave.lastversion", "firstrun"); -pref("extensions.weave.lastsync", 0); pref("extensions.weave.ui.syncnow", true); pref("extensions.weave.ui.sharebookmarks", false); From 96aab61e2b5ae96713f5414d4a355cbccaa11974 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 23 Feb 2009 12:36:55 -0800 Subject: [PATCH 0938/1860] use Gecko's version comparison API, don't clobber the server if the remote version > local client version --- services/sync/modules/service.js | 51 +++++++++++++++----------------- services/sync/modules/util.js | 2 ++ 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 223df9a6184e..519a925fbe07 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -573,38 +573,35 @@ WeaveSvc.prototype = { this._log.debug("Fetching global metadata record"); let meta = yield Records.import(self.cb, this.clusterURL + this.username + "/meta/global"); - if (meta) { - // FIXME: version is a string, and we don't have any version - // parsing function so we can implement greater-than. So we - // just check for != which is wrong! - if (meta.payload.storageVersion != MIN_SERVER_STORAGE_VERSION) { - reset = true; - yield this._freshStart.async(this, self.cb); - this._log.info("Server data wiped to ensure consistency after client upgrade"); - } else if (!meta.payload.syncID) { - reset = true; - yield this._freshStart.async(this, self.cb); - this._log.info("Server data wiped to ensure consistency after client upgrade"); - - } else if (meta.payload.syncID != Clients.syncID) { - this._wipeClientMetadata(); - Clients.syncID = meta.payload.syncID; - this._log.info("Cleared local caches after server wipe was detected"); - } - - } else { - if (Records.lastResource.lastChannel.responseStatus == 404) { - reset = true; - yield this._freshStart.async(this, self.cb); - this._log.info("Metadata record not found, server wiped to ensure consistency"); - - } else { - this._log.debug("Unknown error while downloading metadata record. " + + if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || + Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, + meta.payload.storageVersion) > 0) { + // make sure the meta record is either not there (404), or that + // we got it ok (200), otherwise something went wrong and we abort + if (Records.lastResource.lastChannel.responseStatus != 404 || + Records.lastResource.lastChannel.responseStatus != 200) { + this._log.warn("Unknown error while downloading metadata record. " + "Aborting sync."); self.done(false); return; } + reset = true; + yield this._freshStart.async(this, self.cb); + this._log.info("Server data wiped to ensure consistency after client " + + "upgrade (or possible first-run)"); + + } else if (Svc.Version.compare(meta.payload.storageVersion, + WEAVE_VERSION) > 0) { + this._log.warn("Server data is of a newer Weave version, this client " + + "needs to be upgraded. Aborting sync."); + self.done(false); + return; + + } else if (meta.payload.syncID != Clients.syncID) { + this._wipeClientMetadata(); + Clients.syncID = meta.payload.syncID; + this._log.info("Cleared local caches after server wipe was detected"); } let needKeys = true; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 7b1a17e81f46..03ad0e075613 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -520,3 +520,5 @@ Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); Utils.lazySvc(Svc, 'Crypto', "@labs.mozilla.com/Weave/Crypto;1", Ci.IWeaveCrypto); Utils.lazySvc(Svc, 'Memory', "@mozilla.org/xpcom/memory-service;1", Ci.nsIMemory); +Utils.lazySvc(Svc, 'Version', + "@mozilla.org/xpcom/version-comparator;1", Ci.nsIVersionComparator); From d171bcbf672c78037c4d9e21d4fe68930b62b6e9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 23 Feb 2009 16:27:41 -0800 Subject: [PATCH 0939/1860] fix bad logic when checking metadata record --- services/sync/modules/service.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 519a925fbe07..2c53fa79b6ec 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -577,10 +577,9 @@ WeaveSvc.prototype = { if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, meta.payload.storageVersion) > 0) { - // make sure the meta record is either not there (404), or that - // we got it ok (200), otherwise something went wrong and we abort - if (Records.lastResource.lastChannel.responseStatus != 404 || - Records.lastResource.lastChannel.responseStatus != 200) { + // abort the server wipe if the GET status was anything other than 404 or 200 + let status = Records.lastResource.lastChannel.responseStatus; + if (status != 200 && status != 404) { this._log.warn("Unknown error while downloading metadata record. " + "Aborting sync."); self.done(false); From a6c071a5b309a72a5e35c367a6bffd49bc0f9f04 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 23 Feb 2009 16:55:41 -0800 Subject: [PATCH 0940/1860] try/catch in FileAppender.remove(), sometimes that fails --- services/sync/modules/log4moz.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index a2477e211f18..bbc0005cb374 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -485,7 +485,11 @@ FileAppender.prototype = { clear: function FApp_clear() { this.closeStream(); - this._file.remove(false); + try { + this._file.remove(false); + } catch (e) { + // XXX do something? + } } }; From 14b18432cdab00dc980eea99ab8389f4ed8ab30c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 23 Feb 2009 16:56:23 -0800 Subject: [PATCH 0941/1860] better logging about why the server is wiped when it does --- services/sync/modules/service.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2c53fa79b6ec..e2c4d7ce3fdb 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -585,10 +585,17 @@ WeaveSvc.prototype = { self.done(false); return; } + reset = true; yield this._freshStart.async(this, self.cb); - this._log.info("Server data wiped to ensure consistency after client " + - "upgrade (or possible first-run)"); + + if (status == 404) + this._log.info("Metadata record not found, server wiped to ensure " + + "consistency."); + else // 200 + this._log.info("Server data wiped to ensure consistency after client " + + "upgrade (" + meta.paylaod.storageVersion + " -> " + + WEAVE_VERSION + ")"); } else if (Svc.Version.compare(meta.payload.storageVersion, WEAVE_VERSION) > 0) { From 8177379ef27a52c5e2ba41a384f757a84c582c0f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 23 Feb 2009 17:59:14 -0800 Subject: [PATCH 0942/1860] improve pref pane; clean up first tab and make 'create account' load the services homepage, also temporarily hide the change password form; move client name setting to clients tab --- services/sync/locales/en-US/preferences.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 9494e55be24a..003ef3f75b1e 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -21,7 +21,7 @@ - + From 0725f2d4bf1b75c0e1493bc42fb38a39aeb80bff Mon Sep 17 00:00:00 2001 From: Chris Beard Date: Mon, 23 Feb 2009 18:37:13 -0800 Subject: [PATCH 0943/1860] cleaning up of pref pane, adding default label to sync status --- services/sync/locales/en-US/preferences.dtd | 14 ++++++++------ services/sync/locales/en-US/status.dtd | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 003ef3f75b1e..ec7fd250a551 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -23,12 +23,8 @@ - - - - - - + + @@ -43,6 +39,12 @@ + + + + + + diff --git a/services/sync/locales/en-US/status.dtd b/services/sync/locales/en-US/status.dtd index 7687d45a6a7f..42744bfe7e44 100644 --- a/services/sync/locales/en-US/status.dtd +++ b/services/sync/locales/en-US/status.dtd @@ -1 +1,2 @@ + From 860a78e5da47a4c2b8714033a1e03617993d11a4 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 23 Feb 2009 18:47:36 -0800 Subject: [PATCH 0944/1860] add a "what's this?" link to the login box so users can find out what the encryption passphrase is --- services/sync/locales/en-US/login.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/login.dtd b/services/sync/locales/en-US/login.dtd index 39f2a7f365bb..26112bed1c84 100644 --- a/services/sync/locales/en-US/login.dtd +++ b/services/sync/locales/en-US/login.dtd @@ -1,7 +1,7 @@ - + From 993265dfb863e0b6dfb7013d939c6f2c22d52c0d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 23 Feb 2009 19:33:40 -0800 Subject: [PATCH 0945/1860] reset lastSync pref before setting it, as some clients have mismatched types saved --- services/sync/modules/engines.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c161f95bc4ce..f3f3a25e5205 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -201,6 +201,7 @@ SyncEngine.prototype = { return Svc.Prefs.get(this.name + ".lastSync", 0); }, set lastSync(value) { + Svc.Prefs.reset(this.name + ".lastSync"); if (typeof(value) == "string") value = parseInt(value); Svc.Prefs.set(this.name + ".lastSync", value); From 4097c3368d2194c7d2d0bf32a2ffb4976e77d29c Mon Sep 17 00:00:00 2001 From: Date: Wed, 25 Feb 2009 00:56:46 -0800 Subject: [PATCH 0946/1860] Changed how firefox populates tabs-from-other-computers menu and how it opens a tab picked from that menu, so that it can restore the history of the tab and not just the latest URL. --- services/sync/modules/engines/tabs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 831b2d4efa5d..36981ff4d063 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -176,6 +176,7 @@ TabStore.prototype = { for (let j = 0; j < window.tabs.length; j++) { let tab = window.tabs[j]; + this._log.debug( "tab state: " + uneval(tab));//this._sessionStore.getTabState(tab)); // Skip empty (i.e. just-opened, no history yet) tabs: if (tab.entries.length == 0) continue; From d975ea8c9edafd8344caa3f15d0e927e3e973875 Mon Sep 17 00:00:00 2001 From: Date: Wed, 25 Feb 2009 15:06:28 -0800 Subject: [PATCH 0947/1860] Fixed a typo in service.js ('paylaod' for 'payload') --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e2c4d7ce3fdb..ab6f5619236c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -594,7 +594,7 @@ WeaveSvc.prototype = { "consistency."); else // 200 this._log.info("Server data wiped to ensure consistency after client " + - "upgrade (" + meta.paylaod.storageVersion + " -> " + + "upgrade (" + meta.payload.storageVersion + " -> " + WEAVE_VERSION + ")"); } else if (Svc.Version.compare(meta.payload.storageVersion, From af386814f3980b832105955e2d95c786f4505440 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 26 Feb 2009 00:47:30 -0800 Subject: [PATCH 0948/1860] Bug 480271 - On first sign in, username is initialized with "undefined". r=thunder --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ab6f5619236c..bfa74bb0a3cc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -150,7 +150,7 @@ WeaveSvc.prototype = { _scheduleTimer: null, get username() { - return Svc.Prefs.get("username"); + return Svc.Prefs.get("username", ""); }, set username(value) { if (value) From bf9d8937c94291ffa1101f4a72a2d55ef8023e08 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 26 Feb 2009 12:34:54 -0800 Subject: [PATCH 0949/1860] Bug 480381 - "lastsync" pref is sometimes a number and sometimes a string --- services/sync/modules/service.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bfa74bb0a3cc..8c7dd0c82b4b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -697,8 +697,7 @@ WeaveSvc.prototype = { } if (!this._syncError) { - Svc.Prefs.reset("lastsync"); - Svc.Prefs.set("lastsync", new Date().toString()); + Svc.Prefs.set("lastSync", new Date().toString()); this._log.info("Sync completed successfully"); } else this._log.warn("Some engines did not sync correctly"); @@ -775,8 +774,7 @@ WeaveSvc.prototype = { } if (!this._syncError) { - Svc.Prefs.reset("lastsync"); - Svc.Prefs.set("lastsync", Date.now()); + Svc.Prefs.set("lastSync", new Date().toString()); this._log.info("Sync completed successfully"); } else this._log.warn("Some engines did not sync correctly"); From 2b6f7beee26b3f51e30f082a4e1d27d50dc10ec9 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Feb 2009 13:52:12 -0800 Subject: [PATCH 0950/1860] remove unused observer service getter --- services/sync/modules/engines.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f3f3a25e5205..b34eca6847bd 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -112,13 +112,6 @@ Engine.prototype = { get enabled() Utils.prefs.getBoolPref("engine." + this.name), get score() this._tracker.score, - get _os() { - let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - this.__defineGetter__("_os", function() os); - return os; - }, - get _store() { let store = new this._storeObj(); this.__defineGetter__("_store", function() store); From cfa95b6a253612cabc42cc61c6cb04dc80de0058 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Feb 2009 15:06:57 -0800 Subject: [PATCH 0951/1860] track local client data changes so we can upload it as necessary --- services/sync/modules/engines/clients.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 5e58a4758321..86a19ba9dfbf 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -69,6 +69,7 @@ ClientEngine.prototype = { _ClientEngine_init: function ClientEngine__init() { this._init(); + Utils.prefs.addObserver("", this, false); }, // get and set info for clients @@ -113,7 +114,20 @@ ClientEngine.prototype = { set clientName(value) { Svc.Prefs.set("client.name", value); }, get clientType() { return Svc.Prefs.get("client.type", "desktop"); }, - set clientType(value) { Svc.Prefs.set("client.type", value); } + set clientType(value) { Svc.Prefs.set("client.type", value); }, + + observe: function ClientEngine_observe() { + switch (topic) { + case "nsPref:changed": + switch (data) { + case "client.name": + case "client.type": + this._tracker.addChangedID(this.clientID); + break; + } + break; + } + } }; function ClientStore() { From d8933c524a3c7438c3024522e9a1cb0b0dee3704 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Feb 2009 17:31:06 -0800 Subject: [PATCH 0952/1860] Bug 480461: skip items that cannot be decrypted --- services/sync/modules/engines.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b34eca6847bd..0e7d4a6f00fa 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -291,23 +291,24 @@ SyncEngine.prototype = { let item; let count = {applied: 0, reconciled: 0}; this._lastSyncTmp = 0; + while ((item = yield newitems.iter.next(self.cb))) { this._lowMemCheck(); try { - yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); + yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); + if (yield this._reconcile.async(this, self.cb, item)) { + count.applied++; + yield this._applyIncoming.async(this, self.cb, item); + } else { + count.reconciled++; + this._log.trace("Skipping reconciled incoming item " + item.id); + if (this._lastSyncTmp < item.modified) + this._lastSyncTmp = item.modified; + } } catch (e) { - this._log.error("Could not decrypt incoming record: " + + this._log.error("Could not process incoming record: " + Utils.exceptionStr(e)); } - if (yield this._reconcile.async(this, self.cb, item)) { - count.applied++; - yield this._applyIncoming.async(this, self.cb, item); - } else { - count.reconciled++; - this._log.trace("Skipping reconciled incoming item " + item.id); - if (this._lastSyncTmp < item.modified) - this._lastSyncTmp = item.modified; - } } if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; From 128073c2fe22bc5ba12df9e49e119ece5e95d40a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Feb 2009 17:37:49 -0800 Subject: [PATCH 0953/1860] fix args list for observe() --- services/sync/modules/engines/clients.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 86a19ba9dfbf..eeb2b36ab460 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -116,7 +116,7 @@ ClientEngine.prototype = { get clientType() { return Svc.Prefs.get("client.type", "desktop"); }, set clientType(value) { Svc.Prefs.set("client.type", value); }, - observe: function ClientEngine_observe() { + observe: function ClientEngine_observe(subject, topic, data) { switch (topic) { case "nsPref:changed": switch (data) { From c072e643adf226f6288313daa458114a5ab5620a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 26 Feb 2009 20:52:56 -0800 Subject: [PATCH 0954/1860] Bug 480480 - Finding cluster with api/register/chknode has trailing newline --- services/sync/modules/service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8c7dd0c82b4b..2f761589e36e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -449,7 +449,8 @@ WeaveSvc.prototype = { ret = true; } else if (res.lastChannel.responseStatus == 200) { - this.clusterURL = 'https://' + res.data + '/'; + // XXX Bug 480480 Work around the server sending a trailing newline + this.clusterURL = 'https://' + res.data.trim() + '/'; ret = true; } self.done(ret); From 5d2fea258c1a9818273fe22593d78309b93be37c Mon Sep 17 00:00:00 2001 From: Date: Fri, 27 Feb 2009 18:28:26 -0800 Subject: [PATCH 0955/1860] Resource.js now always validates the cache (VALIDATE_ALWAYS) when downloading stuff. This seems to fix bug 480270 on Firefox but not on Fennec. --- services/sync/modules/engines/tabs.js | 12 +----------- services/sync/modules/resource.js | 8 +++++++- services/sync/modules/service.js | 5 ++++- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 36981ff4d063..e4ad3935f5be 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -176,7 +176,7 @@ TabStore.prototype = { for (let j = 0; j < window.tabs.length; j++) { let tab = window.tabs[j]; - this._log.debug( "tab state: " + uneval(tab));//this._sessionStore.getTabState(tab)); + //this._sessionStore.getTabState(tab)); // Skip empty (i.e. just-opened, no history yet) tabs: if (tab.entries.length == 0) continue; @@ -214,27 +214,20 @@ TabStore.prototype = { }, itemExists: function TabStore_itemExists(id) { - this._log.debug("ItemExists called."); if (id == this._localClientGUID) { - this._log.debug("It's me."); return true; } else if (this._remoteClients[id]) { - this._log.debug("It's somebody else."); return true; } else { - this._log.debug("It doesn't exist!"); return false; } }, createRecord: function TabStore_createRecord(id, cryptoMetaURL) { - this._log.debug("CreateRecord called for id " + id ); let record; if (id == this._localClientGUID) { - this._log.debug("That's Me!"); record = this._createLocalClientTabSetRecord(); } else { - this._log.debug("That's Somebody Else."); record = this._remoteClients[id]; } record.id = id; @@ -243,7 +236,6 @@ TabStore.prototype = { }, changeItemId: function TabStore_changeItemId(oldId, newId) { - this._log.debug("changeItemId called."); if (this._remoteClients[oldId]) { let record = this._remoteClients[oldId]; record.id = newId; @@ -253,7 +245,6 @@ TabStore.prototype = { }, getAllIDs: function TabStore_getAllIds() { - this._log.debug("getAllIds called."); let items = {}; items[ this._localClientGUID ] = true; for (let id in this._remoteClients) { @@ -265,7 +256,6 @@ TabStore.prototype = { wipe: function TabStore_wipe() { this._log.debug("Wipe called. Clearing cache of remote client tabs."); this._remoteClients = {}; - this._writeToFile(); }, create: function TabStore_create(record) { diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 5d094522a110..83b7bd73d1a6 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -148,7 +148,13 @@ Resource.prototype = { let ios = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); this._lastChannel = ios.newChannel(this.spec, null, null). - QueryInterface(Ci.nsIHttpChannel); + QueryInterface(Ci.nsIRequest); + // Always validate the cache: + let loadFlags = this._lastChannel.loadFlags; + loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; + this._lastChannel.loadFlags = loadFlags; + this._lastChannel = this._lastChannel.QueryInterface(Ci.nsIHttpChannel); + this._lastChannel.notificationCallbacks = new badCertListener(); let headers = this.headers; // avoid calling the authorizer more than once diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2f761589e36e..9c830cabafb3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -574,7 +574,9 @@ WeaveSvc.prototype = { this._log.debug("Fetching global metadata record"); let meta = yield Records.import(self.cb, this.clusterURL + this.username + "/meta/global"); - + + this._log.debug("Min server storage version is " + MIN_SERVER_STORAGE_VERSION); + this._log.debug("payload storage version is " + meta.payload.storageVersion); if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, meta.payload.storageVersion) > 0) { @@ -811,6 +813,7 @@ WeaveSvc.prototype = { this._log.debug("Uploading new metadata record"); meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); + this._log.debug("Setting meta payload storage version to " + WEAVE_VERSION); meta.payload.storageVersion = WEAVE_VERSION; meta.payload.syncID = Clients.syncID; let res = new Resource(meta.uri); From fffb6cfab28cc89201ec0bcc98cbf218a41a0048 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 28 Feb 2009 11:11:36 -0800 Subject: [PATCH 0956/1860] Bug 480528 - Color Weave logs to help find errors, engine problems, etc. --- services/sync/modules/log4moz.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index bbc0005cb374..aa12029b44b8 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -362,8 +362,13 @@ BasicFormatter.prototype = { format: function BF_format(message) { let date = new Date(message.time); + + // Pad a string to a certain length (20) with a character (space) + let pad = function BF__pad(str, len, chr) str + + new Array(Math.max((len || 20) - str.length + 1, 0)).join(chr || " "); + return date.toLocaleFormat(this.dateFormat) + "\t" + - message.loggerName + "\t" + message.levelDesc + "\t" + + pad(message.loggerName) + " " + message.levelDesc + "\t" + message.message + "\n"; } }; From 4201c41d7ed049a5eae4815ff24f081b9505f44d Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 2 Mar 2009 19:25:50 +0100 Subject: [PATCH 0957/1860] Print meta information only if present --- services/sync/modules/service.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9c830cabafb3..a81c0646703d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -576,7 +576,12 @@ WeaveSvc.prototype = { this.username + "/meta/global"); this._log.debug("Min server storage version is " + MIN_SERVER_STORAGE_VERSION); - this._log.debug("payload storage version is " + meta.payload.storageVersion); + + if (meta) { + this._log.debug("payload storage version is " + + meta.payload.storageVersion); + } + if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, meta.payload.storageVersion) > 0) { From 3da0d54fdccc73b665de07b364e3ae4de0392120 Mon Sep 17 00:00:00 2001 From: Date: Mon, 2 Mar 2009 14:15:46 -0800 Subject: [PATCH 0958/1860] Added favicons to Firefox tabs-from-other-computers menu. --- services/sync/modules/service.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a81c0646703d..ab519617febd 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -320,8 +320,10 @@ WeaveSvc.prototype = { this._initLogs(); this._log.info("Weave " + WEAVE_VERSION + " initializing"); - if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) + if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) { + this._log.warn("Wiping client from _onStartup."); this._wipeClientMetadata(); + } let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. getService(Ci.nsIHttpProtocolHandler).userAgent; @@ -574,17 +576,26 @@ WeaveSvc.prototype = { this._log.debug("Fetching global metadata record"); let meta = yield Records.import(self.cb, this.clusterURL + this.username + "/meta/global"); - + + // DEBUG: Just for now, i'm turning off wiping of server based on version + // mismatch. Don't commit this line: + meta.payload.storageVersion = MIN_SERVER_STORAGE_VERSION; // this this._log.debug("Min server storage version is " + MIN_SERVER_STORAGE_VERSION); - if (meta) { - this._log.debug("payload storage version is " + + this._log.debug("payload storage version is " + meta.payload.storageVersion); } - + if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, meta.payload.storageVersion) > 0) { + if (Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, + meta.payload.storageVersion) > 0) { + this._log.warn("Version " + meta.payload.storageVersion + " does not match version " + MIN_SERVER_STORAGE_VERSION); + } + if (!meta.payload.syncID) { + this._log.warn("No sync id."); + } // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.lastResource.lastChannel.responseStatus; if (status != 200 && status != 404) { @@ -595,6 +606,7 @@ WeaveSvc.prototype = { } reset = true; + this._log.warn("Calling freshStart from the first case."); yield this._freshStart.async(this, self.cb); if (status == 404) @@ -613,6 +625,7 @@ WeaveSvc.prototype = { return; } else if (meta.payload.syncID != Clients.syncID) { + this._log.warn("Wiping client because of syncID mismatch."); this._wipeClientMetadata(); Clients.syncID = meta.payload.syncID; this._log.info("Cleared local caches after server wipe was detected"); @@ -648,6 +661,7 @@ WeaveSvc.prototype = { } if (!reset) { + this._log.warn("Calling freshStart from !reset case."); yield this._freshStart.async(this, self.cb); this._log.info("Server data wiped to ensure consistency due to missing keys"); } @@ -811,7 +825,7 @@ WeaveSvc.prototype = { _freshStart: function WeaveSvc__freshStart() { let self = yield; - + this._log.warn("Wiping client data from freshStart."); this._wipeClientMetadata(); this._log.info("Client metadata wiped, deleting server data"); yield this._wipeServer.async(this, self.cb); From 7de0c26e43b047a3798ef8ebde2a7f2550e48f7a Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 3 Mar 2009 00:42:57 +0100 Subject: [PATCH 0959/1860] Password sync for 0.3 (bug #468697) --- services/sync/modules/engines/passwords.js | 341 +++++++++--------- .../sync/modules/type_records/passwords.js | 100 +++++ services/sync/modules/util.js | 10 +- services/sync/services-sync.js | 2 +- 4 files changed, 275 insertions(+), 178 deletions(-) create mode 100644 services/sync/modules/type_records/passwords.js diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 5e0245e3acca..6cd9e474e20a 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -19,6 +19,7 @@ * * Contributor(s): * Justin Dolske + * Anant Narayanan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -37,13 +38,17 @@ const EXPORTED_SYMBOLS = ['PasswordEngine']; const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; +Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/ext/Observers.js"); +Cu.import("resource://weave/type_records/passwords.js"); Function.prototype.async = Async.sugar; @@ -51,206 +56,206 @@ function PasswordEngine() { this._init(); } PasswordEngine.prototype = { - __proto__: new SyncEngine(), - - get name() { return "passwords"; }, - get displayName() { return "Saved Passwords"; }, - get logName() { return "PasswordEngine"; }, - get serverPrefix() { return "user-data/passwords/"; }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new PasswordStore(); - return this.__store; + __proto__: SyncEngine.prototype, + name: "passwords", + displayName: "Passwords", + logName: "Passwords", + _storeObj: PasswordStore, + _trackerObj: PasswordTracker, + _recordObj: LoginRec, + + /* We override syncStartup & syncFinish to populate/reset our local cache + of loginInfo items. We can remove this when the interface to query + LoginInfo items by GUID is ready + */ + _syncStartup: function PasswordStore__syncStartup() { + let self = yield; + this._store._cacheLogins(); + yield SyncEngine.prototype._syncStartup.async(this, self.cb); }, - - __core: null, - get _core() { - if (!this.__core) - this.__core = new PasswordSyncCore(this._store); - return this.__core; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new PasswordTracker(); - return this.__tracker; + + _syncFinish: function PasswordStore__syncFinish() { + let self = yield; + this._store._clearLoginCache(); + yield SyncEngine.prototype._syncFinish.async(this, self.cb); } }; -function PasswordSyncCore(store) { - this._store = store; - this._init(); -} -PasswordSyncCore.prototype = { - _logName: "PasswordSync", - _store: null, - - _commandLike: function PSC_commandLike(a, b) { - // Not used. - return false; - } -}; -PasswordSyncCore.prototype.__proto__ = new SyncCore(); - function PasswordStore() { this._init(); } PasswordStore.prototype = { + __proto__: Store.prototype, _logName: "PasswordStore", - _lookup: null, - __loginManager: null, get _loginManager() { - if (!this.__loginManager) - this.__loginManager = Utils.getLoginManager(); - return this.__loginManager; + let loginManager = Utils.getLoginManager(); + this.__defineGetter__("_loginManager", function() loginManager); + return loginManager; }, - __nsLoginInfo: null, - get _nsLoginInfo() { - if (!this.__nsLoginInfo) - this.__nsLoginInfo = Utils.makeNewLoginInfo(); - return this.__nsLoginInfo; - }, - - /* - * _hashLoginInfo - * - * nsILoginInfo objects don't have a unique GUID, so we need to generate one - * on the fly. This is done by taking a hash of every field in the object. - * Note that the resulting GUID could potentiually reveal passwords via - * dictionary attacks or brute force. But GUIDs shouldn't be obtainable by - * anyone, so this should generally be safe. - */ - _hashLoginInfo: function PasswordStore__hashLoginInfo(aLogin) { - var loginKey = aLogin.hostname + ":" + - aLogin.formSubmitURL + ":" + - aLogin.httpRealm + ":" + - aLogin.username + ":" + - aLogin.password + ":" + - aLogin.usernameField + ":" + - aLogin.passwordField; - - return Utils.sha1(loginKey); - }, - - _createCommand: function PasswordStore__createCommand(command) { - this._log.info("PasswordStore got createCommand: " + command ); - - var login = new this._nsLoginInfo(command.data.hostname, - command.data.formSubmitURL, - command.data.httpRealm, - command.data.username, - command.data.password, - command.data.usernameField, - command.data.passwordField); - - this._loginManager.addLogin(login); - }, - - _removeCommand: function PasswordStore__removeCommand(command) { - this._log.info("PasswordStore got removeCommand: " + command ); - - if (command.GUID in this._lookup) { - var data = this._lookup[command.GUID]; - var login = new this._nsLoginInfo(data.hostname, - data.formSubmitURL, - data.httpRealm, - data.username, - data.password, - data.usernameField, - data.passwordField); - this._loginManager.removeLogin(login); - } else { - this._log.warn("Invalid GUID for remove, ignoring request!"); + get _loginItems() { + let loginItems = {}; + let logins = this._loginManager.getAllLogins({}); + for (let i = 0; i < logins.length; i++) { + let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); + loginItems[metaInfo.guid] = logins[i]; } + + this.__defineGetter__("_loginItems", function() loginItems); + return loginItems; + }, + + _nsLoginInfo: null, + _init: function PasswordStore_init() { + Store.prototype._init.call(this); + this._nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" + ); + }, + + _cacheLogins: function PasswordStore__cacheLogins() { + /* Force the getter to populate the property + Also, this way, we don't fail if the store is created twice? + */ + return this._loginItems; + }, + + _clearLoginCache: function PasswordStore__clearLoginCache() { + this.__loginItems = null; + }, + + _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) { + return new this._nsLoginInfo(record.hostname, + record.formSubmitURL, + record.httpRealm, + record.username, + record.password, + record.usernameField, + record.passwordField); }, - _editCommand: function PasswordStore__editCommand(command) { - this._log.info("PasswordStore got editCommand: " + command ); - throw "Password syncs are expected to only be create/remove!"; - }, - - wrap: function PasswordStore_wrap() { - /* Return contents of this store, as JSON. */ - var items = {}; - var logins = this._loginManager.getAllLogins({}); - - for (var i = 0; i < logins.length; i++) { - var login = logins[i]; - - var key = this._hashLoginInfo(login); - - items[key] = { hostname : login.hostname, - formSubmitURL : login.formSubmitURL, - httpRealm : login.httpRealm, - username : login.username, - password : login.password, - usernameField : login.usernameField, - passwordField : login.passwordField }; + getAllIDs: function PasswordStore__getAllIDs() { + let items = {}; + let logins = this._loginManager.getAllLogins({}); + + for (let i = 0; i < logins.length; i++) { + let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); + items[metaInfo.guid] = logins[i].hostname; } - - this._lookup = items; + return items; }, + changeItemID: function PasswordStore__changeItemID(oldID, newID) { + if (!(oldID in this._loginItems)) { + this._log.warn("Can't change GUID " + oldID + " to " + + newID + ": Item does not exist"); + return; + } + let info = this._loginItems[oldID]; + + if (newID in this._loginItems) { + this._log.warn("Can't change GUID " + oldID + " to " + + newID + ": new ID already in use"); + return; + } + + this._log.debug("Changing GUID " + oldID + " to " + newID); + + let prop = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag2); + prop.setPropertyAsAUTF8String("guid", newID); + + this._loginManager.modifyLogin(info, prop); + }, + + itemExists: function PasswordStore__itemExists(id) { + return ((id in this._loginItems) == true); + }, + + createRecord: function PasswordStore__createRecord(guid, cryptoMetaURL) { + let record = new LoginRec(); + record.id = guid; + if (guid in this._loginItems) { + let login = this._loginItems[guid]; + record.encryption = cryptoMetaURL; + 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; + } else { + /* Deleted item */ + record.cleartext = null; + } + return record; + }, + + create: function PasswordStore__create(record) { + this._loginManager.addLogin(this._nsLoginInfoFromRecord(record)); + }, + + remove: function PasswordStore__remove(record) { + if (record.id in this._loginItems) { + this._loginManager.removeLogin(this._loginItems[record.id]); + return; + } + + this._log.debug("Asked to remove record that doesn't exist, ignoring!"); + }, + + update: function PasswordStore__update(record) { + if (!(record.id in this._loginItems)) { + this._log.debug("Skipping update for unknown item: " + record.id); + return; + } + let login = this._loginItems[record.id]; + this._log.trace("Updating " + record.id + " (" + itemId + ")"); + + let newinfo = this._nsLoginInfoFromRecord(record); + this._loginManager.modifyLogin(login, newinfo); + }, + wipe: function PasswordStore_wipe() { this._loginManager.removeAllLogins(); - }, - - _resetGUIDs: function PasswordStore__resetGUIDs() { - let self = yield; - // Not needed. } }; -PasswordStore.prototype.__proto__ = new Store(); function PasswordTracker() { this._init(); } PasswordTracker.prototype = { + __proto__: Tracker.prototype, _logName: "PasswordTracker", - __loginManager : null, - get _loginManager() { - if (!this.__loginManager) - this.__loginManager = Cc["@mozilla.org/login-manager;1"]. - getService(Ci.nsILoginManager); - return this.__loginManager; + _init: function PasswordTracker_init() { + Tracker.prototype._init.call(this); + Observers.add("passwordmgr-storage-changed", this); }, - /* We use nsILoginManager's countLogins() method, as it is - * the only method that doesn't require the user to enter - * a master password, but still gives us an idea of how much - * info has changed. - * - * FIXME: This does not track edits of passwords, so we - * artificially add 30 to every score. We should move to - * using the LoginManager shim at some point. - * - * Each addition/removal is worth 15 points. - */ - _loginCount: 0, - get score() { - var count = this._loginManager.countLogins("", "", ""); - var score = (Math.abs(this._loginCount - count) * 15) + 30; + /* A single add, remove or change is 15 points, all items removed is 50 */ + observe: function PasswordTracker_observe(aSubject, aTopic, aData) { + if (this.ignoreAll) + return; - if (score >= 100) - return 100; - else - return score; - }, + this._log.debug("Received notification " + aData); - resetScore: function PasswordTracker_resetScore() { - this._loginCount = this._loginManager.countLogins("", "", ""); - }, - - _init: function PasswordTracker__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._loginCount = this._loginManager.countLogins("", "", ""); + switch (aData) { + case 'addLogin': + case 'modifyLogin': + case 'removeLogin': + let metaInfo = aSubject.QueryInterface(Ci.nsILoginMetaInfo); + this._score += 15; + this.addChangedID(metaInfo.guid); + break; + case 'removeAllLogins': + this._score += 50; + break; + } } }; -PasswordTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/type_records/passwords.js b/services/sync/modules/type_records/passwords.js new file mode 100644 index 000000000000..fe81b4ba01ff --- /dev/null +++ b/services/sync/modules/type_records/passwords.js @@ -0,0 +1,100 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Anant Narayanan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['LoginRec']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/keys.js"); + +Function.prototype.async = Async.sugar; + +function LoginRec(uri) { + this._LoginRec_init(uri); +} +LoginRec.prototype = { + __proto__: CryptoWrapper.prototype, + _logName: "Record.Login", + + _LoginRec_init: function LoginItem_init(uri) { + this._CryptoWrap_init(uri); + this.cleartext = { + }; + }, + + get hostname() this.cleartext.hostname, + set hostname(value) { + this.cleartext.hostname = value; + }, + + get formSubmitURL() this.cleartext.formSubmitURL, + set formSubmitURL(value) { + this.cleartext.formSubmitURL = value; + }, + + get httpRealm() this.cleartext.httpRealm, + set httpRealm(value) { + this.cleartext.httpRealm = value; + }, + + get username() this.cleartext.username, + set username(value) { + this.cleartext.username = value; + }, + + get password() this.cleartext.password, + set password(value) { + this.cleartext.password = value; + }, + + get usernameField() this.cleartext.usernameField, + set usernameField(value) { + this.cleartext.usernameField = value; + }, + + get passwordField() this.cleartext.passwordField, + set passwordField(value) { + this.cleartext.passwordField = value; + } +}; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 03ad0e075613..d71a2155dc19 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -93,15 +93,7 @@ let Utils = { return Cc["@mozilla.org/login-manager;1"]. getService(Ci.nsILoginManager); }, - - makeNewLoginInfo: function getNewLoginInfo() { - return new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", - Ci.nsILoginInfo, - "init" - ); - }, - + findPassword: function findPassword(realm, username) { // fixme: make a request and get the realm ? let password; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 03eff5790cb9..53b22293fdc0 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -16,7 +16,7 @@ pref("extensions.weave.syncOnQuit.enabled", true); pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.cookies", false); -pref("extensions.weave.engine.passwords", false); +pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.forms", false); pref("extensions.weave.engine.tabs", true); pref("extensions.weave.engine.input", false); From 1e9f5997e57ceb3c3125a66667633096f4e2f32d Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 3 Mar 2009 00:57:37 +0100 Subject: [PATCH 0960/1860] Rename methods to be less confusing --- services/sync/modules/engines/passwords.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 6cd9e474e20a..ee2e88bd4b9b 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -68,13 +68,13 @@ PasswordEngine.prototype = { of loginInfo items. We can remove this when the interface to query LoginInfo items by GUID is ready */ - _syncStartup: function PasswordStore__syncStartup() { + _syncStartup: function PasswordEngine__syncStartup() { let self = yield; this._store._cacheLogins(); yield SyncEngine.prototype._syncStartup.async(this, self.cb); }, - _syncFinish: function PasswordStore__syncFinish() { + _syncFinish: function PasswordEngine__syncFinish() { let self = yield; this._store._clearLoginCache(); yield SyncEngine.prototype._syncFinish.async(this, self.cb); From 07d879f28198273b565b19c8f4af860d548e8716 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 3 Mar 2009 01:15:48 +0100 Subject: [PATCH 0961/1860] Remove getter that fixes the 'two-store' problem --- services/sync/modules/engines.js | 7 ++-- services/sync/modules/engines/passwords.js | 40 +++++++--------------- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 0e7d4a6f00fa..d54e089deb80 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -112,10 +112,11 @@ Engine.prototype = { get enabled() Utils.prefs.getBoolPref("engine." + this.name), get score() this._tracker.score, + __store: null, get _store() { - let store = new this._storeObj(); - this.__defineGetter__("_store", function() store); - return store; + if (!this.__store) + this.__store = new this._storeObj(); + return this.__store; }, get _tracker() { diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index ee2e88bd4b9b..449acaba5ca4 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -64,16 +64,7 @@ PasswordEngine.prototype = { _trackerObj: PasswordTracker, _recordObj: LoginRec, - /* We override syncStartup & syncFinish to populate/reset our local cache - of loginInfo items. We can remove this when the interface to query - LoginInfo items by GUID is ready - */ - _syncStartup: function PasswordEngine__syncStartup() { - let self = yield; - this._store._cacheLogins(); - yield SyncEngine.prototype._syncStartup.async(this, self.cb); - }, - + /* Wipe cache when sync finishes */ _syncFinish: function PasswordEngine__syncFinish() { let self = yield; this._store._clearLoginCache(); @@ -93,17 +84,19 @@ PasswordStore.prototype = { this.__defineGetter__("_loginManager", function() loginManager); return loginManager; }, - + + __loginItems: null, get _loginItems() { - let loginItems = {}; - let logins = this._loginManager.getAllLogins({}); - for (let i = 0; i < logins.length; i++) { - let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); - loginItems[metaInfo.guid] = logins[i]; + if (!this.__loginItems) { + this.__loginItems = {}; + let logins = this._loginManager.getAllLogins({}); + for (let i = 0; i < logins.length; i++) { + let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); + this.__loginItems[metaInfo.guid] = logins[i]; + } } - - this.__defineGetter__("_loginItems", function() loginItems); - return loginItems; + + return this.__loginItems; }, _nsLoginInfo: null, @@ -113,14 +106,7 @@ PasswordStore.prototype = { "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init" - ); - }, - - _cacheLogins: function PasswordStore__cacheLogins() { - /* Force the getter to populate the property - Also, this way, we don't fail if the store is created twice? - */ - return this._loginItems; + ); }, _clearLoginCache: function PasswordStore__clearLoginCache() { From 136b000cd9373737c670694f5f805a5588720fa0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 2 Mar 2009 18:55:26 -0800 Subject: [PATCH 0962/1860] Switch away from __defineGetter__ due to bug 481104; explicitly cache logins in password engine; whitespace fixes --- services/sync/modules/engines.js | 17 +++----- services/sync/modules/engines/passwords.js | 50 ++++++++++------------ services/sync/modules/util.js | 2 +- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index d54e089deb80..422ff25f6fb1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -112,7 +112,6 @@ Engine.prototype = { get enabled() Utils.prefs.getBoolPref("engine." + this.name), get score() this._tracker.score, - __store: null, get _store() { if (!this.__store) this.__store = new this._storeObj(); @@ -120,9 +119,9 @@ Engine.prototype = { }, get _tracker() { - let tracker = new this._trackerObj(); - this.__defineGetter__("_tracker", function() tracker); - return tracker; + if (!this.__tracker) + this.__tracker = new this._trackerObj(); + return this.__tracker; }, _init: function Engine__init() { @@ -167,12 +166,6 @@ SyncEngine.prototype = { _recordObj: CryptoWrapper, - get _memory() { - let mem = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); - this.__defineGetter__("_memory", function() mem); - return mem; - }, - get baseURL() { let url = Svc.Prefs.get("clusterURL"); if (!url) @@ -228,10 +221,10 @@ SyncEngine.prototype = { }, _lowMemCheck: function SyncEngine__lowMemCheck() { - if (this._memory.isLowMemory()) { + if (Svc.Memory.isLowMemory()) { this._log.warn("Low memory, forcing GC"); Cu.forceGC(); - if (this._memory.isLowMemory()) { + if (Svc.Memory.isLowMemory()) { this._log.warn("Low memory, aborting sync!"); throw "Low memory"; } diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 449acaba5ca4..f15e8d77256a 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -63,11 +63,17 @@ PasswordEngine.prototype = { _storeObj: PasswordStore, _trackerObj: PasswordTracker, _recordObj: LoginRec, - + + _syncStartup: function PasswordEngine__syncStartup() { + let self = yield; + this._store.cacheLogins(); + yield SyncEngine.prototype._syncStartup.async(this, self.cb); + }, + /* Wipe cache when sync finishes */ _syncFinish: function PasswordEngine__syncFinish() { let self = yield; - this._store._clearLoginCache(); + this._store.clearLoginCache(); yield SyncEngine.prototype._syncFinish.async(this, self.cb); } }; @@ -84,21 +90,7 @@ PasswordStore.prototype = { this.__defineGetter__("_loginManager", function() loginManager); return loginManager; }, - - __loginItems: null, - get _loginItems() { - if (!this.__loginItems) { - this.__loginItems = {}; - let logins = this._loginManager.getAllLogins({}); - for (let i = 0; i < logins.length; i++) { - let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); - this.__loginItems[metaInfo.guid] = logins[i]; - } - } - return this.__loginItems; - }, - _nsLoginInfo: null, _init: function PasswordStore_init() { Store.prototype._init.call(this); @@ -106,13 +98,9 @@ PasswordStore.prototype = { "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init" - ); + ); }, - - _clearLoginCache: function PasswordStore__clearLoginCache() { - this.__loginItems = null; - }, - + _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) { return new this._nsLoginInfo(record.hostname, record.formSubmitURL, @@ -123,15 +111,23 @@ PasswordStore.prototype = { record.passwordField); }, + cacheLogins: function PasswordStore_cacheLogins() { + this._loginItems = this.getAllIDs(); + }, + + clearLoginCache: function PasswordStore_clearLoginCache() { + this._loginItems = null; + }, + getAllIDs: function PasswordStore__getAllIDs() { let items = {}; let logins = this._loginManager.getAllLogins({}); - + for (let i = 0; i < logins.length; i++) { let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); items[metaInfo.guid] = logins[i].hostname; } - + return items; }, @@ -142,13 +138,13 @@ PasswordStore.prototype = { return; } let info = this._loginItems[oldID]; - + if (newID in this._loginItems) { this._log.warn("Can't change GUID " + oldID + " to " + newID + ": new ID already in use"); return; } - + this._log.debug("Changing GUID " + oldID + " to " + newID); let prop = Cc["@mozilla.org/hash-property-bag;1"]. @@ -191,7 +187,7 @@ PasswordStore.prototype = { this._loginManager.removeLogin(this._loginItems[record.id]); return; } - + this._log.debug("Asked to remove record that doesn't exist, ignoring!"); }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index d71a2155dc19..f73024816b55 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -93,7 +93,7 @@ let Utils = { return Cc["@mozilla.org/login-manager;1"]. getService(Ci.nsILoginManager); }, - + findPassword: function findPassword(realm, username) { // fixme: make a request and get the realm ? let password; From af93847398c09397c62e92c3c12aab491d0ac503 Mon Sep 17 00:00:00 2001 From: Date: Mon, 2 Mar 2009 23:55:05 -0800 Subject: [PATCH 0963/1860] Added more logging to service.js to help me figure out the caching problem. --- services/sync/modules/service.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ab519617febd..8a80f3660414 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -577,9 +577,6 @@ WeaveSvc.prototype = { let meta = yield Records.import(self.cb, this.clusterURL + this.username + "/meta/global"); - // DEBUG: Just for now, i'm turning off wiping of server based on version - // mismatch. Don't commit this line: - meta.payload.storageVersion = MIN_SERVER_STORAGE_VERSION; // this this._log.debug("Min server storage version is " + MIN_SERVER_STORAGE_VERSION); if (meta) { this._log.debug("payload storage version is " + From 3d51ead93395bd14a942d06e18737710a94c0037 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 3 Mar 2009 01:29:35 -0800 Subject: [PATCH 0964/1860] add some trace-level debugging to reconciler; ignore old (changed) IDs while reconciling --- services/sync/modules/engines.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 422ff25f6fb1..99a05606e7d4 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -347,6 +347,7 @@ SyncEngine.prototype = { // Step 1: Check for conflicts // If same as local record, do not upload + this._log.trace("Reconcile step 1"); if (item.id in this._tracker.changedIDs) { if (this._isEqual(item)) this._tracker.removeChangedID(item.id); @@ -356,22 +357,26 @@ SyncEngine.prototype = { // Step 2: Check for updates // If different from local record, apply server update + this._log.trace("Reconcile step 2"); if (this._store.itemExists(item.id)) { self.done(!this._isEqual(item)); return; } // If the incoming item has been deleted, skip step 3 + this._log.trace("Reconcile step 2.5"); if (item.cleartext === null) { self.done(true); return; } // Step 3: Check for similar items + this._log.trace("Reconcile step 3"); for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); if (this._recordLike(item, out)) { this._store.changeItemID(id, item.id); + this._tracker.removeChangedID(id); this._tracker.removeChangedID(item.id); this._store.cache.clear(); // because parentid refs will be wrong self.done(false); From d17a833ca1f3db361e60816a8e70539a262556b0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 3 Mar 2009 01:32:10 -0800 Subject: [PATCH 0965/1860] override _recordLike for passwords; add logging calls; make getAllIDs return full nsLoginInfos --- services/sync/modules/engines/passwords.js | 44 +++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index f15e8d77256a..30506ace02bc 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -75,6 +75,20 @@ PasswordEngine.prototype = { let self = yield; this._store.clearLoginCache(); yield SyncEngine.prototype._syncFinish.async(this, self.cb); + }, + + _recordLike: function SyncEngine__recordLike(a, b) { + if (a.cleartext == null || b.cleartext == null) + return false; + if (a.cleartext.hostname == b.cleartext.hostname) { + } + if (a.cleartext.hostname != b.cleartext.hostname || + a.cleartext.httpRealm != b.cleartext.httpRealm || + a.cleartext.username != b.cleartext.username) + return false; + if (!a.cleartext.formSubmitURL || !b.cleartext.formSubmitURL) + return true; + return a.cleartext.formSubmitURL == b.cleartext.formSubmitURL; } }; @@ -112,10 +126,12 @@ PasswordStore.prototype = { }, cacheLogins: function PasswordStore_cacheLogins() { + this._log.debug("Caching all logins"); this._loginItems = this.getAllIDs(); }, clearLoginCache: function PasswordStore_clearLoginCache() { + this._log.debug("Clearing login cache"); this._loginItems = null; }, @@ -125,37 +141,33 @@ PasswordStore.prototype = { for (let i = 0; i < logins.length; i++) { let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); - items[metaInfo.guid] = logins[i].hostname; + items[metaInfo.guid] = metaInfo; } return items; }, changeItemID: function PasswordStore__changeItemID(oldID, newID) { + this._log.debug("Changing item ID: " + oldID + " to " + newID); + if (!(oldID in this._loginItems)) { - this._log.warn("Can't change GUID " + oldID + " to " + - newID + ": Item does not exist"); + this._log.warn("Can't change item ID: item doesn't exist"); return; } - let info = this._loginItems[oldID]; - if (newID in this._loginItems) { - this._log.warn("Can't change GUID " + oldID + " to " + - newID + ": new ID already in use"); + this._log.warn("Can't change item ID: new ID already in use"); return; } - this._log.debug("Changing GUID " + oldID + " to " + newID); - let prop = Cc["@mozilla.org/hash-property-bag;1"]. - createInstance(Ci.nsIWritablePropertyBag2); + createInstance(Ci.nsIWritablePropertyBag2); prop.setPropertyAsAUTF8String("guid", newID); - this._loginManager.modifyLogin(info, prop); + this._loginManager.modifyLogin(this._loginItems[oldID], prop); }, itemExists: function PasswordStore__itemExists(id) { - return ((id in this._loginItems) == true); + return (id in this._loginItems); }, createRecord: function PasswordStore__createRecord(guid, cryptoMetaURL) { @@ -179,10 +191,12 @@ PasswordStore.prototype = { }, create: function PasswordStore__create(record) { + this._log.debug("Adding login for " + record.hostname); this._loginManager.addLogin(this._nsLoginInfoFromRecord(record)); }, remove: function PasswordStore__remove(record) { + this._log.debug("Removing login " + record.id); if (record.id in this._loginItems) { this._loginManager.removeLogin(this._loginItems[record.id]); return; @@ -192,6 +206,8 @@ PasswordStore.prototype = { }, update: function PasswordStore__update(record) { + this._log.debug("Updating login for " + record.hostname); + if (!(record.id in this._loginItems)) { this._log.debug("Skipping update for unknown item: " + record.id); return; @@ -225,17 +241,17 @@ PasswordTracker.prototype = { if (this.ignoreAll) return; - this._log.debug("Received notification " + aData); - switch (aData) { case 'addLogin': case 'modifyLogin': case 'removeLogin': let metaInfo = aSubject.QueryInterface(Ci.nsILoginMetaInfo); this._score += 15; + this._log.debug(aData + ": " + metaInfo.guid); this.addChangedID(metaInfo.guid); break; case 'removeAllLogins': + this._log.debug(aData); this._score += 50; break; } From 45dbbe523ecb90c90447d53d5e723095dff4fae8 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 3 Mar 2009 01:48:13 -0800 Subject: [PATCH 0966/1860] create new records with correct guid set --- services/sync/modules/engines/passwords.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 30506ace02bc..be1c57c0ea2e 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -116,13 +116,16 @@ PasswordStore.prototype = { }, _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) { - return new this._nsLoginInfo(record.hostname, - record.formSubmitURL, - record.httpRealm, - record.username, - record.password, - record.usernameField, - record.passwordField); + let info = new this._nsLoginInfo(record.hostname, + record.formSubmitURL, + record.httpRealm, + record.username, + record.password, + record.usernameField, + record.passwordField); + info.QueryInterface(Ci.nsILoginMetaInfo); + info.guid = record.id; + return info; }, cacheLogins: function PasswordStore_cacheLogins() { From 30a3526467da6e2f6d1b7c0095b38de7f6434643 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 3 Mar 2009 11:53:56 -0800 Subject: [PATCH 0967/1860] disable recordLike for clients --- services/sync/modules/engines/clients.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index eeb2b36ab460..67f33d3d1543 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -72,6 +72,12 @@ ClientEngine.prototype = { Utils.prefs.addObserver("", this, false); }, + // We never want to change one client id to another, even if + // they look exactly the same + _recordLike: function SyncEngine__recordLike(a, b) { + return false; + }, + // get and set info for clients // FIXME: callers must use the setInfo interface or changes won't get synced, From 81070128f9986f56fe4240bddf382b3d9638c204 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 3 Mar 2009 11:57:30 -0800 Subject: [PATCH 0968/1860] don't fail when client metadata record doesn't exist, or has an empty payload. Reset last sync after a werver wipe on all clients --- services/sync/modules/service.js | 45 ++++++++++++++++---------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8a80f3660414..3cdc01ce719b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -576,34 +576,33 @@ WeaveSvc.prototype = { this._log.debug("Fetching global metadata record"); let meta = yield Records.import(self.cb, this.clusterURL + this.username + "/meta/global"); + let remoteVersion = (meta && meta.payload.storageVersion)? + meta.payload.storageVersion : ""; - this._log.debug("Min server storage version is " + MIN_SERVER_STORAGE_VERSION); - if (meta) { - this._log.debug("payload storage version is " + - meta.payload.storageVersion); - } + this._log.debug("Min supported storage version is " + MIN_SERVER_STORAGE_VERSION); + this._log.debug("Remote storage version is " + remoteVersion); if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || - Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, - meta.payload.storageVersion) > 0) { - if (Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, - meta.payload.storageVersion) > 0) { - this._log.warn("Version " + meta.payload.storageVersion + " does not match version " + MIN_SERVER_STORAGE_VERSION); - } - if (!meta.payload.syncID) { - this._log.warn("No sync id."); - } + Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, remoteVersion) > 0) { + // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.lastResource.lastChannel.responseStatus; if (status != 200 && status != 404) { - this._log.warn("Unknown error while downloading metadata record. " + - "Aborting sync."); + this._log.warn("Unknown error while downloading metadata record. " + + "Aborting sync."); self.done(false); return; } + if (!meta) + this._log.info("No metadata record, server wipe needed"); + if (meta && !meta.payload.syncID) + this._log.warn("No sync id, server wipe needed"); + if (Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, remoteVersion) > 0) + this._log.info("Server storage version no longer supported, server wipe needed"); + reset = true; - this._log.warn("Calling freshStart from the first case."); + this._log.info("Wiping server data"); yield this._freshStart.async(this, self.cb); if (status == 404) @@ -611,18 +610,16 @@ WeaveSvc.prototype = { "consistency."); else // 200 this._log.info("Server data wiped to ensure consistency after client " + - "upgrade (" + meta.payload.storageVersion + " -> " + - WEAVE_VERSION + ")"); + "upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")"); - } else if (Svc.Version.compare(meta.payload.storageVersion, - WEAVE_VERSION) > 0) { + } else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) { this._log.warn("Server data is of a newer Weave version, this client " + "needs to be upgraded. Aborting sync."); self.done(false); return; } else if (meta.payload.syncID != Clients.syncID) { - this._log.warn("Wiping client because of syncID mismatch."); + this._log.info("Resetting client because of syncID mismatch."); this._wipeClientMetadata(); Clients.syncID = meta.payload.syncID; this._log.info("Cleared local caches after server wipe was detected"); @@ -871,6 +868,10 @@ WeaveSvc.prototype = { if (Engines.get("tabs")) Engines.get("tabs")._store.wipe(); + for each (let engine in Engines.getAll().push(Clients)) { + engine.resetLastSync(); + } + try { let cruft = this._dirSvc.get("ProfD", Ci.nsIFile); cruft.QueryInterface(Ci.nsILocalFile); From 89c0b7ae5054a57b5ad3bbd775d0b3563b2b05a3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 26 Feb 2009 22:36:14 -0800 Subject: [PATCH 0969/1860] Bug 480457 - Expose a resetClient API for weave service and engines. r=thunder --- services/sync/modules/engines.js | 20 +++++++ services/sync/modules/engines/clients.js | 6 ++ services/sync/modules/engines/tabs.js | 7 ++- services/sync/modules/service.js | 72 +++++++++++++----------- 4 files changed, 70 insertions(+), 35 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 99a05606e7d4..dbf2e7a98f4e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -150,11 +150,26 @@ Engine.prototype = { this._notify("wipe-server", this.name, this._wipeServer).async(this, onComplete); }, + /** + * Get rid of any local meta-data + */ + resetClient: function Engine_resetClient(onComplete) { + if (!this._resetClient) + throw "engine does not implement _resetClient method"; + + this._notify("reset-client", this.name, this._resetClient). + async(this, onComplete); + }, + _wipeClient: function Engine__wipeClient() { let self = yield; + + yield this.resetClient(this.cb); + this._log.debug("Deleting all local data"); this._store.wipe(); }, + wipeClient: function Engine_wipeClient(onComplete) { this._notify("wipe-client", this.name, this._wipeClient).async(this, onComplete); } @@ -481,5 +496,10 @@ SyncEngine.prototype = { yield all.delete(self.cb); let crypto = new Resource(this.cryptoMetaURL); yield crypto.delete(self.cb); + }, + + _resetClient: function SyncEngine__resetClient() { + let self = yield; + this.resetLastSync(); } }; diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 67f33d3d1543..4dc274931de4 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -133,6 +133,12 @@ ClientEngine.prototype = { } break; } + }, + + _resetClient: function ClientEngine__resetClient() { + let self = yield; + this.resetLastSync(); + this._store.wipe(); } }; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index e4ad3935f5be..53afaef868f2 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -72,8 +72,13 @@ TabEngine.prototype = { getClientById: function TabEngine_getClientById(id) { return this._store._remoteClients[id]; - } + }, + _resetClient: function TabEngine__resetClient() { + let self = yield; + this.resetLastSync(); + this._store.wipe(); + } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3cdc01ce719b..f7a6655f8b60 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -320,9 +320,10 @@ WeaveSvc.prototype = { this._initLogs(); this._log.info("Weave " + WEAVE_VERSION + " initializing"); + // Reset our sync id if we're upgrading, so sync knows to reset local data if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) { - this._log.warn("Wiping client from _onStartup."); - this._wipeClientMetadata(); + this._log.info("Resetting client syncID from _onStartup."); + Clients.resetSyncID(); } let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. @@ -619,10 +620,10 @@ WeaveSvc.prototype = { return; } else if (meta.payload.syncID != Clients.syncID) { - this._log.info("Resetting client because of syncID mismatch."); - this._wipeClientMetadata(); + yield this.resetClient(self.cb); + this._log.info("Reset client because of syncID mismatch."); Clients.syncID = meta.payload.syncID; - this._log.info("Cleared local caches after server wipe was detected"); + this._log.info("Reset the client after a server/client sync ID mismatch"); } let needKeys = true; @@ -819,8 +820,8 @@ WeaveSvc.prototype = { _freshStart: function WeaveSvc__freshStart() { let self = yield; - this._log.warn("Wiping client data from freshStart."); - this._wipeClientMetadata(); + yield this.resetClient(self.cb); + this._log.info("Reset client data from freshStart."); this._log.info("Client metadata wiped, deleting server data"); yield this._wipeServer.async(this, self.cb); @@ -838,8 +839,6 @@ WeaveSvc.prototype = { _wipeServer: function WeaveSvc__wipeServer() { let self = yield; - Clients.resetSyncID(); - let engines = Engines.getAll(); engines.push(Clients, {name: "keys"}, {name: "crypto"}); for each (let engine in engines) { @@ -850,38 +849,43 @@ WeaveSvc.prototype = { } catch (e) { this._log.debug("Exception on delete: " + Utils.exceptionStr(e)); } - if (engine.resetLastSync) - engine.resetLastSync(); } }, - _wipeClientMetadata: function WeaveSvc__wipeClientMetadata() { - this.clearLogs(); - this._log.info("Logs reinitialized"); + /** + * Reset the client by getting rid of any local server data and client data. + */ + resetClient: function WeaveSvc_resetClient(onComplete) { + let fn = function WeaveSvc__resetClient() { + let self = yield; - PubKeys.clearCache(); - PrivKeys.clearCache(); - CryptoMetas.clearCache(); - Records.clearCache(); + // First drop old logs to track client resetting behavior + this.clearLogs(); + this._log.info("Logs reinitialized for client reset"); - Clients._store.wipe(); - if (Engines.get("tabs")) - Engines.get("tabs")._store.wipe(); + // Pretend we've never synced to the server and drop cached data + Clients.resetSyncID(); + Svc.Prefs.reset("lastSync"); + for each (let cache in [PubKeys, PrivKeys, CryptoMetas, Records]) + cache.clearCache(); - for each (let engine in Engines.getAll().push(Clients)) { - engine.resetLastSync(); - } - - try { - let cruft = this._dirSvc.get("ProfD", Ci.nsIFile); - cruft.QueryInterface(Ci.nsILocalFile); - cruft.append("weave"); - cruft.append("snapshots"); - if (cruft.exists()) - cruft.remove(true); - } catch (e) { - this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); + // Have each engine drop any temporary meta data + for each (let engine in [Clients].concat(Engines.getAll())) + yield engine.resetClient(self.cb); + + // XXX Bug 480448: Delete any snapshots from old code + try { + let cruft = this._dirSvc.get("ProfD", Ci.nsIFile); + cruft.QueryInterface(Ci.nsILocalFile); + cruft.append("weave"); + cruft.append("snapshots"); + if (cruft.exists()) + cruft.remove(true); + } catch (e) { + this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); + } } + this._catchAll(this._notify("reset-client", "", fn)).async(this, onComplete); } }; From 500a04a9135083df1c179b7042620a1714a11625 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 26 Feb 2009 22:14:32 -0800 Subject: [PATCH 0970/1860] Bug 480490 - Expose a wipeServer API for weave service. r=thunder --- services/sync/modules/service.js | 48 +++++++++++++++----------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f7a6655f8b60..0172a2632bf3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -557,16 +557,6 @@ WeaveSvc.prototype = { this._os.notifyObservers(null, "weave:service:logout:finish", ""); }, - serverWipe: function WeaveSvc_serverWipe(onComplete) { - let cb = function WeaveSvc_serverWipeCb() { - let self = yield; - this._log.error("Server wipe not supported"); - this.logout(); - }; - this._catchAll( - this._notify("server-wipe", "", this._localLock(cb))).async(this, onComplete); - }, - // stuff we need to to after login, before we can really do // anything (e.g. key setup) _remoteSetup: function WeaveSvc__remoteSetup() { @@ -823,7 +813,7 @@ WeaveSvc.prototype = { yield this.resetClient(self.cb); this._log.info("Reset client data from freshStart."); this._log.info("Client metadata wiped, deleting server data"); - yield this._wipeServer.async(this, self.cb); + yield this.wipeServer(self.cb); this._log.debug("Uploading new metadata record"); meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); @@ -834,22 +824,30 @@ WeaveSvc.prototype = { yield res.put(self.cb, meta.serialize()); }, - // XXX deletes all known collections; we should have a way to delete - // everything on the server by querying it to get all collections - _wipeServer: function WeaveSvc__wipeServer() { - let self = yield; + /** + * Wipe all user data from the server. + */ + wipeServer: function WeaveSvc_wipeServer(onComplete) { + let fn = function WeaveSvc__wipeServer() { + let self = yield; - let engines = Engines.getAll(); - engines.push(Clients, {name: "keys"}, {name: "crypto"}); - for each (let engine in engines) { - let url = this.clusterURL + this.username + "/" + engine.name + "/"; - let res = new Resource(url); - try { - yield res.delete(self.cb); - } catch (e) { - this._log.debug("Exception on delete: " + Utils.exceptionStr(e)); + // Grab all the collections for the user + let userURL = this.clusterURL + this.username + "/"; + let res = new Resource(userURL); + yield res.get(self.cb); + + // Get the array of collections and delete each one + let allCollections = Svc.Json.decode(res.data); + for each (let name in allCollections) { + try { + yield new Resource(userURL + name).delete(self.cb); + } + catch(ex) { + this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); + } } - } + }; + this._catchAll(this._notify("wipe-server", "", fn)).async(this, onComplete); }, /** From 5f1f966bbeba8a470f0bb3cc3d646ee1d11755bb Mon Sep 17 00:00:00 2001 From: Date: Wed, 4 Mar 2009 15:49:58 -0800 Subject: [PATCH 0971/1860] Added a mostRecentError field to Weave.Service, which gets set to a string on any error that the user might need to know about; this is queried by the Fennec UI code to disply better error messages. --- services/sync/modules/service.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0172a2632bf3..19b1346391bc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -118,6 +118,7 @@ WeaveSvc.prototype = { _loggedIn: false, _syncInProgress: false, _keyGenEnabled: true, + _mostRecentError: null, __os: null, get _os() { @@ -223,6 +224,8 @@ WeaveSvc.prototype = { get enabled() { return Svc.Prefs.get("enabled"); }, set enabled(value) { Svc.Prefs.set("enabled", value); }, + get mostRecentError() { return this._mostRecentError; }, + get schedule() { if (!this.enabled) return 0; // manual/off @@ -524,15 +527,23 @@ WeaveSvc.prototype = { if (typeof(passp) != 'undefined') ID.get('WeaveCryptoID').setTempPassword(passp); - if (!this.username) - throw "No username set, login failed"; - if (!this.password) - throw "No password given or found in password manager"; + if (!this.username) { + this._mostRecentError = "No username set."; + throw "No username set, login failed"; + } + + if (!this.password) { + this._mostRecentError = "No password set."; + throw "No password given or found in password manager"; + } this._log.debug("Logging in user " + this.username); - if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) + if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) { + this._mostRecentError = "Login failed. Check your username/password/phrase."; throw "Login failed"; + } + this._loggedIn = true; this._setSchedule(this.schedule); self.done(true); @@ -579,6 +590,7 @@ WeaveSvc.prototype = { // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.lastResource.lastChannel.responseStatus; if (status != 200 && status != 404) { + this._mostRecentError = "Unknown error when downloading metadata."; this._log.warn("Unknown error while downloading metadata record. " + "Aborting sync."); self.done(false); @@ -604,6 +616,7 @@ WeaveSvc.prototype = { "upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")"); } else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) { + this._mostRecentError = "Client needs to be upgraded."; this._log.warn("Server data is of a newer Weave version, this client " + "needs to be upgraded. Aborting sync."); self.done(false); @@ -634,6 +647,7 @@ WeaveSvc.prototype = { PubKeys.lastResource.lastChannel.responseStatus); this._log.debug("PrivKey HTTP response status: " + PrivKeys.lastResource.lastChannel.responseStatus); + this._mostRecentError = "Can't download keys from server."; self.done(false); return; } @@ -641,6 +655,7 @@ WeaveSvc.prototype = { if (!this._keyGenEnabled) { this._log.warn("Couldn't download keys from server, and key generation" + "is disabled. Aborting sync"); + this._mostRecentError = "No keys. Try syncing from desktop first."; self.done(false); return; } @@ -659,11 +674,13 @@ WeaveSvc.prototype = { yield PubKeys.uploadKeypair(self.cb, keys); ret = true; } catch (e) { + this._mostRecentError = "Could not upload keys."; this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); // FIXME no lastRequest anymore //this._log.error(keys.pubkey.lastRequest.responseText); } } else { + this._mostRecentError = "Could not get encryption passphrase."; this._log.warn("Could not get encryption passphrase"); } } @@ -681,6 +698,7 @@ WeaveSvc.prototype = { if (!this._loggedIn) { this._disableSchedule(); + this._mostRecentError = "Can't sync, not logged in."; throw "aborting sync, not logged in"; } @@ -698,6 +716,7 @@ WeaveSvc.prototype = { continue; if (!(yield this._syncEngine.async(this, self.cb, engine))) { + this._mostRecentError = "Failure in " + engine.displayName; this._log.info("Aborting sync"); break; } @@ -752,6 +771,7 @@ WeaveSvc.prototype = { " reaches threshold " + this._syncThresholds[engine.name] + "; syncing"); if (!(yield this._syncEngine.async(this, self.cb, engine))) { + this._mostRecentError = "Failure in " + engine.displayName; this._log.info("Aborting sync"); break; } From 9244e6d4e5a45b91dc5f951d8af9f1c24e9cdf58 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 6 Mar 2009 09:18:50 -0600 Subject: [PATCH 0972/1860] Bug 481873 - Failed login notifications remain after successful login --- services/sync/modules/notifications.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index 0b6af897b7da..536b8a6bc18d 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -93,10 +93,21 @@ let Notifications = { // XXX Should we notify observers about weave:notification:replaced? }, + /** + * Remove all notifications that match a title. If no title is provided, all + * notifications are removed. + * + * @param title [optional] + * Title of notifications to remove; falsy value means remove all + */ + removeAll: function Notifications_removeAll(title) { + this.notifications.filter(function(old) old.title == title || !title). + forEach(function(old) this.remove(old), this); + }, + // replaces all existing notifications with the same title as the new one replaceTitle: function Notifications_replaceTitle(notification) { - this.notifications.filter(function(old) old.title == notification.title) - .forEach(function(old) this.remove(old), this); + this.removeAll(notification.title); this.add(notification); } }; From c6dff7c32d1ab3a47ef2adb687417fcc89a04e76 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 6 Mar 2009 17:58:22 -0800 Subject: [PATCH 0973/1860] integrate latest version of Preferences.js module, which features a variety of fixes for various issue and also adds support for pref observers --- services/sync/modules/ext/Preferences.js | 364 +++++++++++++++++++---- 1 file changed, 306 insertions(+), 58 deletions(-) diff --git a/services/sync/modules/ext/Preferences.js b/services/sync/modules/ext/Preferences.js index 937a6451650e..b23c71e87b7f 100644 --- a/services/sync/modules/ext/Preferences.js +++ b/services/sync/modules/ext/Preferences.js @@ -42,36 +42,35 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +// The minimum and maximum integers that can be set as preferences. +// The range of valid values is narrower than the range of valid JS values +// because the native preferences code treats integers as NSPR PRInt32s, +// which are 32-bit signed integers on all platforms. +const MAX_INT = Math.pow(2, 31) - 1; +const MIN_INT = -MAX_INT; + function Preferences(prefBranch) { if (prefBranch) this._prefBranch = prefBranch; } Preferences.prototype = { - _prefBranch: "", - - // Preferences Service - - get _prefSvc() { - let prefSvc = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefService). - getBranch(this._prefBranch). - QueryInterface(Ci.nsIPrefBranch2); - this.__defineGetter__("_prefSvc", function() prefSvc); - return this._prefSvc; - }, - /** - * Get the value of a pref, if any; otherwise return the default value. - * - * @param prefName the name of the pref to get - * @param defaultValue the default value, if any - * - * @returns the value of the pref, if any; otherwise the default value - */ + * Get the value of a pref, if any; otherwise return the default value. + * + * @param prefName {String|Array} + * the pref to get, or an array of prefs to get + * + * @param defaultValue + * the default value, if any, for prefs that don't have one + * + * @returns the value of the pref, if any; otherwise the default value + */ get: function(prefName, defaultValue) { if (isArray(prefName)) - return prefName.map(function(v) this.get(v), this); + return prefName.map(function(v) this.get(v, defaultValue), this); switch (this._prefSvc.getPrefType(prefName)) { case Ci.nsIPrefBranch.PREF_STRING: @@ -84,11 +83,38 @@ Preferences.prototype = { return this._prefSvc.getBoolPref(prefName); case Ci.nsIPrefBranch.PREF_INVALID: - default: return defaultValue; + + default: + // This should never happen. + throw "Error getting pref " + prefName + "; its value's type is " + + this._prefSvc.getPrefType(prefName) + ", which I don't know " + + "how to handle."; } }, + /** + * Set a preference to a value. + * + * You can set multiple prefs by passing an object as the only parameter. + * In that case, this method will treat the properties of the object + * as preferences to set, where each property name is the name of a pref + * and its corresponding property value is the value of the pref. + * + * @param prefName {String|Object} + * the name of the pref to set; or an object containing a set + * of prefs to set + * + * @param prefValue {String|Number|Boolean} + * the value to which to set the pref + * + * Note: Preferences cannot store non-integer numbers or numbers outside + * the signed 32-bit range -(2^31-1) to 2^31-1, If you have such a number, + * store it as a string by calling toString() on the number before passing + * it to this method, i.e.: + * Preferences.set("pi", 3.14159.toString()) + * Preferences.set("big", Math.pow(2, 31).toString()). + */ set: function(prefName, prefValue) { if (isObject(prefName)) { for (let [name, value] in Iterator(prefName)) @@ -96,30 +122,98 @@ Preferences.prototype = { return; } - switch (typeof prefValue) { - case "number": - this._prefSvc.setIntPref(prefName, prefValue); - if (prefValue % 1 != 0) - Cu.reportError("WARNING: setting " + prefName + " pref to non-integer number " + - prefValue + " converts it to integer number " + this.get(prefName) + - "; to retain precision, store non-integer numbers as strings"); + let prefType; + if (typeof prefValue != "undefined" && prefValue != null) + prefType = prefValue.constructor.name; + + switch (prefType) { + case "String": + { + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = prefValue; + this._prefSvc.setComplexValue(prefName, Ci.nsISupportsString, string); + } break; - case "boolean": + case "Number": + // We throw if the number is outside the range, since the result + // will never be what the consumer wanted to store, but we only warn + // if the number is non-integer, since the consumer might not mind + // the loss of precision. + if (prefValue > MAX_INT || prefValue < MIN_INT) + throw("you cannot set the " + prefName + " pref to the number " + + prefValue + ", as number pref values must be in the signed " + + "32-bit integer range -(2^31-1) to 2^31-1. To store numbers " + + "outside that range, store them as strings."); + this._prefSvc.setIntPref(prefName, prefValue); + if (prefValue % 1 != 0) + Cu.reportError("Warning: setting the " + prefName + " pref to the " + + "non-integer number " + prefValue + " converted it " + + "to the integer number " + this.get(prefName) + + "; to retain fractional precision, store non-integer " + + "numbers as strings."); + break; + + case "Boolean": this._prefSvc.setBoolPref(prefName, prefValue); break; - case "string": - default: { - let string = Cc["@mozilla.org/supports-string;1"]. - createInstance(Ci.nsISupportsString); - string.data = prefValue; - this._prefSvc.setComplexValue(prefName, Ci.nsISupportsString, string); - break; - } + default: + throw "can't set pref " + prefName + " to value '" + prefValue + + "'; it isn't a String, Number, or Boolean"; } }, + /** + * Whether or not the given pref has a value. This is different from isSet + * because it returns true whether the value of the pref is a default value + * or a user-set value, while isSet only returns true if the value + * is a user-set value. + * + * @param prefName {String|Array} + * the pref to check, or an array of prefs to check + * + * @returns {Boolean|Array} + * whether or not the pref has a value; or, if the caller provided + * an array of pref names, an array of booleans indicating whether + * or not the prefs have values + */ + has: function(prefName) { + if (isArray(prefName)) + return prefName.map(this.has, this); + + return (this._prefSvc.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID); + }, + + /** + * Whether or not the given pref has a user-set value. This is different + * from |has| because it returns true only if the value of the pref is a user- + * set value, while |has| returns true if the value of the pref is a default + * value or a user-set value. + * + * @param prefName {String|Array} + * the pref to check, or an array of prefs to check + * + * @returns {Boolean|Array} + * whether or not the pref has a user-set value; or, if the caller + * provided an array of pref names, an array of booleans indicating + * whether or not the prefs have user-set values + */ + isSet: function(prefName) { + if (isArray(prefName)) + return prefName.map(this.isSet, this); + + return (this.has(prefName) && this._prefSvc.prefHasUserValue(prefName)); + }, + + /** + * Whether or not the given pref has a user-set value. Use isSet instead, + * which is equivalent. + * @deprecated + */ + modified: function(prefName) { return this.isSet(prefName) }, + reset: function(prefName) { if (isArray(prefName)) { prefName.map(function(v) this.reset(v), this); @@ -142,28 +236,117 @@ Preferences.prototype = { } }, - // FIXME: make the methods below accept an array of pref names. - - has: function(prefName) { - return (this._prefSvc.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID); - }, - - modified: function(prefName) { - return (this.has(prefName) && this._prefSvc.prefHasUserValue(prefName)); - }, - - locked: function(prefName) { - return this._prefSvc.isLocked(prefName); - }, - + /** + * Lock a pref so it can't be changed. + * + * @param prefName {String|Array} + * the pref to lock, or an array of prefs to lock + */ lock: function(prefName) { + if (isArray(prefName)) + prefName.map(this.lock, this); + this._prefSvc.lockPref(prefName); }, + /** + * Unlock a pref so it can be changed. + * + * @param prefName {String|Array} + * the pref to lock, or an array of prefs to lock + */ unlock: function(prefName) { + if (isArray(prefName)) + prefName.map(this.unlock, this); + this._prefSvc.unlockPref(prefName); }, + /** + * Whether or not the given pref is locked against changes. + * + * @param prefName {String|Array} + * the pref to check, or an array of prefs to check + * + * @returns {Boolean|Array} + * whether or not the pref has a user-set value; or, if the caller + * provided an array of pref names, an array of booleans indicating + * whether or not the prefs have user-set values + */ + locked: function(prefName) { + if (isArray(prefName)) + return prefName.map(this.locked, this); + + return this._prefSvc.prefIsLocked(prefName); + }, + + /** + * Start observing a pref. + * + * The callback can be a function or any object that implements nsIObserver. + * When the callback is a function and thisObject is provided, it gets called + * as a method of thisObject. + * + * @param prefName {String} + * the name of the pref to observe + * + * @param callback {Function|Object} + * the code to notify when the pref changes; + * + * @param thisObject {Object} [optional] + * the object to use as |this| when calling a Function callback; + * + * @returns the wrapped observer + */ + observe: function(prefName, callback, thisObject) { + let fullPrefName = this._prefBranch + (prefName || ""); + + let observer = new PrefObserver(fullPrefName, callback, thisObject); + Preferences._prefSvc.addObserver(fullPrefName, observer, true); + observers.push(observer); + + return observer; + }, + + /** + * Stop observing a pref. + * + * You must call this method with the same prefName, callback, and thisObject + * with which you originally registered the observer. However, you don't have + * to call this method on the same exact instance of Preferences; you can call + * it on any instance. For example, the following code first starts and then + * stops observing the "foo.bar.baz" preference: + * + * let observer = function() {...}; + * Preferences.observe("foo.bar.baz", observer); + * new Preferences("foo.bar.").ignore("baz", observer); + * + * @param prefName {String} + * the name of the pref being observed + * + * @param callback {Function|Object} + * the code being notified when the pref changes + * + * @param thisObject {Object} [optional] + * the object being used as |this| when calling a Function callback + */ + ignore: function(prefName, callback, thisObject) { + let fullPrefName = this._prefBranch + (prefName || ""); + + // This seems fairly inefficient, but I'm not sure how much better we can + // make it. We could index by fullBranch, but we can't index by callback + // or thisObject, as far as I know, since the keys to JavaScript hashes + // (a.k.a. objects) can apparently only be primitive values. + let [observer] = observers.filter(function(v) v.prefName == fullPrefName && + v.callback == callback && + v.thisObject == thisObject); + + if (observer) { + Preferences._prefSvc.removeObserver(fullPrefName, observer); + observers.splice(observers.indexOf(observer), 1); + } + }, + resetBranch: function(prefBranch) { try { this._prefSvc.resetBranch(prefBranch); @@ -176,6 +359,25 @@ Preferences.prototype = { else throw ex; } + }, + + /** + * The branch of the preferences tree to which this instance provides access. + * @private + */ + _prefBranch: "", + + /** + * Preferences Service + * @private + */ + get _prefSvc() { + let prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(this._prefBranch). + QueryInterface(Ci.nsIPrefBranch2); + this.__defineGetter__("_prefSvc", function() prefSvc); + return this._prefSvc; } }; @@ -185,14 +387,60 @@ Preferences.prototype = { // first. Preferences.__proto__ = Preferences.prototype; +/** + * A cache of pref observers. + * + * We use this to remove observers when a caller calls Preferences::ignore. + * + * All Preferences instances share this object, because we want callers to be + * able to remove an observer using a different Preferences object than the one + * with which they added it. That means we have to identify the observers + * in this object by their complete pref name, not just their name relative to + * the root branch of the Preferences object with which they were created. + */ +let observers = []; + +function PrefObserver(prefName, callback, thisObject) { + this.prefName = prefName; + this.callback = callback; + this.thisObject = thisObject; +} + +PrefObserver.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + observe: function(subject, topic, data) { + // The pref service only observes whole branches, but we only observe + // individual preferences, so we check here that the pref that changed + // is the exact one we're observing (and not some sub-pref on the branch). + if (data != this.prefName) + return; + + if (typeof this.callback == "function") { + let prefValue = Preferences.get(this.prefName); + + if (this.thisObject) + this.callback.call(this.thisObject, prefValue); + else + this.callback(prefValue); + } + else // typeof this.callback == "object" (nsIObserver) + this.callback.observe(subject, topic, data); + } +}; + function isArray(val) { - // We can't check for |val.constructor == Array| here, since we might have - // a different global object, so we check the constructor name instead. - return (typeof val == "object" && val.constructor.name == Array.name); + // We can't check for |val.constructor == Array| here, since the value + // might be from a different context whose Array constructor is not the same + // as ours, so instead we match based on the name of the constructor. + return (typeof val != "undefined" && val != null && typeof val == "object" && + val.constructor.name == "Array"); } function isObject(val) { - // We can't check for |val.constructor == Object| here, since we might have - // a different global object, so we check the constructor name instead. - return (typeof val == "object" && val.constructor.name == Object.name); + // We can't check for |val.constructor == Object| here, since the value + // might be from a different context whose Object constructor is not the same + // as ours, so instead we match based on the name of the constructor. + return (typeof val != "undefined" && val != null && typeof val == "object" && + val.constructor.name == "Object"); } From 8b6d0e34625a621cd6e216cd1ed4d155fb9a180b Mon Sep 17 00:00:00 2001 From: Date: Sat, 7 Mar 2009 00:55:47 -0800 Subject: [PATCH 0974/1860] Fennec now launches first run page, and updates extensions.weave.lastversion, so that it's no longer resetting syncId and therefore resetting the server every single time it tries to sync. Also set the download in resource.js to use LOAD_BYPASS_CACHE. Together these changes seem to fix bug 480270. --- services/sync/modules/engines/clients.js | 4 +++- services/sync/modules/resource.js | 2 +- services/sync/modules/service.js | 3 +++ services/sync/services-sync.js | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 4dc274931de4..50169b55a0dc 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -105,7 +105,9 @@ ClientEngine.prototype = { }, get syncID() { - if (!Svc.Prefs.get("client.syncID")) + let oldSyncId = Svc.Prefs.get("client.syncID"); + dump("oldSyncId is " + oldSyncId + "\n"); + if (!oldSyncId) Svc.Prefs.set("client.syncID", Utils.makeGUID()); return Svc.Prefs.get("client.syncID"); }, diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 83b7bd73d1a6..b524ea58bdc8 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -151,7 +151,7 @@ Resource.prototype = { QueryInterface(Ci.nsIRequest); // Always validate the cache: let loadFlags = this._lastChannel.loadFlags; - loadFlags |= Ci.nsIRequest.VALIDATE_ALWAYS; + loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; //VALIDATE_ALWAYS; this._lastChannel.loadFlags = loadFlags; this._lastChannel = this._lastChannel.QueryInterface(Ci.nsIHttpChannel); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 19b1346391bc..256cff5ca235 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -578,6 +578,7 @@ WeaveSvc.prototype = { this._log.debug("Fetching global metadata record"); let meta = yield Records.import(self.cb, this.clusterURL + this.username + "/meta/global"); + let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; @@ -623,6 +624,8 @@ WeaveSvc.prototype = { return; } else if (meta.payload.syncID != Clients.syncID) { + this._log.warn("Meta.payload.syncID is " + meta.payload.syncID); + this._log.warn(", Clients.syncID is " + Clients.syncID); yield this.resetClient(self.cb); this._log.info("Reset client because of syncID mismatch."); Clients.syncID = meta.payload.syncID; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 53b22293fdc0..64a596c23310 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -36,3 +36,4 @@ pref("extensions.weave.log.logger.engine.tabs", "Debug"); pref("extensions.weave.log.logger.engine.clients", "Debug"); pref("extensions.weave.network.numRetries", 2); +pref("extensions.weave.client.syncID", "YOUR MOM"); From 1a080e57d6a473f0a37eb30571b0d260d643b47f Mon Sep 17 00:00:00 2001 From: Date: Sat, 7 Mar 2009 11:04:42 -0800 Subject: [PATCH 0975/1860] Fixed bug 480420 by using url.spec instead of url as key for the cache in RecordManager. --- services/sync/modules/base_records/keys.js | 2 +- services/sync/modules/base_records/wbo.js | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index e0bb738d7422..b033c94e8ebb 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -117,7 +117,7 @@ PrivKey.prototype = { set iv(value) { this.payload.iv = value; }, - get keyData() this.payload.key_data, + get keyData() { return this.payload.key_data; }, set keyData(value) { this.payload.key_data = value; }, diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 1b60b3fbb4ba..83b6ae4046b6 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -183,10 +183,16 @@ RecordManager.prototype = { let self = yield; let record = null; + let spec = url.spec? url.spec : url; + /* Note: using url object directly as key for this._records cache + * does not work because different url objects (even pointing to the + * same place) are different objects and therefore not equal. So + * always use the string, not the object, as a key. TODO: use the + * string as key for this._aliases as well? (Don't know) */ if (url in this._aliases) url = this._aliases[url]; - if (url in this._records) - record = this._records[url]; + if (spec in this._records) + record = this._records[spec]; if (!record) record = yield this.import(self.cb, url); @@ -197,7 +203,8 @@ RecordManager.prototype = { }, set: function RegordMgr_set(url, record) { - this._records[url] = record; + let spec = url.spec ? url.spec : url; + this._records[spec] = record; }, contains: function RegordMgr_contains(url) { From c9a52e0246ba1869799c4570e3aa512cb1fdb287 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 10 Mar 2009 01:20:50 -0700 Subject: [PATCH 0976/1860] remove dump() debug statements --- services/sync/modules/engines/clients.js | 4 +--- services/sync/modules/engines/tabs.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 50169b55a0dc..4dc274931de4 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -105,9 +105,7 @@ ClientEngine.prototype = { }, get syncID() { - let oldSyncId = Svc.Prefs.get("client.syncID"); - dump("oldSyncId is " + oldSyncId + "\n"); - if (!oldSyncId) + if (!Svc.Prefs.get("client.syncID")) Svc.Prefs.set("client.syncID", Utils.makeGUID()); return Svc.Prefs.get("client.syncID"); }, diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 53afaef868f2..629e8fa8ef31 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -92,8 +92,6 @@ TabStore.prototype = { _remoteClients: {}, _TabStore_init: function TabStore__init() { - // TODO: This gets dumped twice. Is TabStore initialized twice? - dump("Initializing TabStore!!\n"); this._init(); this._readFromFile(); }, @@ -317,7 +315,7 @@ TabTracker.prototype = { }, observe: function TabTracker_observe(aSubject, aTopic, aData) { - dump("TabTracker spotted window open/close...\n"); + this._log.trace("Spotted window open/close"); let window = aSubject.QueryInterface(Ci.nsIDOMWindow); // Ignore windows that don't have tabContainers. // TODO: Fennec windows don't have tabContainers, but we still want From 7daec0625a06a724470f9bd5fd93903bdcf10139 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Mar 2009 06:07:24 -0500 Subject: [PATCH 0977/1860] Bug 482003 - Refactor _sync and _syncAsNeeded. r=thunder --- services/sync/modules/service.js | 164 ++++++++++++++----------------- 1 file changed, 73 insertions(+), 91 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 256cff5ca235..e2336ef9840d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -358,7 +358,7 @@ WeaveSvc.prototype = { if (Svc.Prefs.get("autoconnect") && this.username) { try { if (yield this.login(self.cb)) - yield this.sync(self.cb); + yield this.sync(self.cb, true); } catch (e) {} } self.done(); @@ -691,9 +691,21 @@ WeaveSvc.prototype = { self.done(ret); }, - // These are per-engine + /** + * Engine scores must meet or exceed this value before we sync them when + * using thresholds. These are engine-specific, as different kinds of data + * change at different rates, so we store them in a hash by engine name. + */ + _syncThresh: {}, - _sync: function WeaveSvc__sync() { + /** + * Sync up engines with the server. + * + * @param useThresh + * True to use thresholds to determine what engines to sync + * @throw Reason for not syncing + */ + _sync: function WeaveSvc__sync(useThresh) { let self = yield; if (!this.enabled) @@ -713,106 +725,76 @@ WeaveSvc.prototype = { yield Clients.sync(self.cb); try { - let engines = Engines.getAll(); - for each (let engine in engines) { + for each (let engine in Engines.getAll()) { + let name = engine.name; + + // Nothing to do for disabled engines if (!engine.enabled) continue; - if (!(yield this._syncEngine.async(this, self.cb, engine))) { - this._mostRecentError = "Failure in " + engine.displayName; - this._log.info("Aborting sync"); - break; - } + // Conditionally reset the threshold for the current engine + let resetThresh = Utils.bind2(this, function WeaveSvc__resetThresh(cond) + cond ? this._syncThresh[name] = INITIAL_THRESHOLD : undefined); + + // Initialize the threshold if it doesn't exist yet + resetThresh(!(name in this._syncThresh)); + + // Determine if we should sync if using thresholds + if (useThresh) { + let score = engine.score; + let thresh = this._syncThresh[name]; + if (score >= thresh) + this._log.debug("Syncing " + name + "; " + + "score " + score + " >= thresh " + thresh); + else { + this._log.debug("Not syncing " + name + "; " + + "score " + score + " < thresh " + thresh); + + // Decrement the threshold by a standard amount with a lower bound of 1 + this._syncThresh[name] = Math.max(thresh - THRESHOLD_DECREMENT_STEP, 1); + + // No need to sync this engine for now + continue; + } + } + + // If there's any problems with syncing the engine, report the failure + if (!(yield this._syncEngine.async(this, self.cb, engine))) { + this._mostRecentError = "Failure in " + engine.displayName; + this._log.info("Aborting sync"); + break; + } + + // We've successfully synced, so reset the threshold. We do this after + // a successful sync so failures can try again on next sync, but this + // could trigger too many syncs if the server is having problems. + resetThresh(useThresh); } - if (!this._syncError) { - Svc.Prefs.set("lastSync", new Date().toString()); - this._log.info("Sync completed successfully"); - } else + if (this._syncError) this._log.warn("Some engines did not sync correctly"); - + else { + Svc.Prefs.set("lastSync", new Date().toString()); + this._log.info("Sync completed successfully"); + } } finally { this.cancelRequested = false; this._syncError = false; } }, - sync: function WeaveSvc_sync(onComplete) { - this._catchAll( - this._notify("sync", "", - this._localLock(this._sync))).async(this, onComplete); - }, - // The values that engine scores must meet or exceed before we sync them - // as needed. These are engine-specific, as different kinds of data change - // at different rates, so we store them in a hash indexed by engine name. - _syncThresholds: {}, - - _syncAsNeeded: function WeaveSvc__syncAsNeeded() { - let self = yield; - - if (!this.enabled) - return; - - try { - - if (!this._loggedIn) { - this._disableSchedule(); - throw "aborting sync, not logged in"; - } - - let engines = Engines.getAll(); - for each (let engine in engines) { - if (!engine.enabled) - continue; - - if (!(engine.name in this._syncThresholds)) - this._syncThresholds[engine.name] = INITIAL_THRESHOLD; - - let score = engine.score; - if (score >= this._syncThresholds[engine.name]) { - this._log.debug(engine.name + " score " + score + - " reaches threshold " + - this._syncThresholds[engine.name] + "; syncing"); - if (!(yield this._syncEngine.async(this, self.cb, engine))) { - this._mostRecentError = "Failure in " + engine.displayName; - this._log.info("Aborting sync"); - break; - } - - // Reset the engine's threshold to the initial value. - // Note: we do this after syncing the engine so that we'll try again - // next time around if syncing fails for some reason. The upside - // of this approach is that we'll sync again as soon as possible; - // but the downside is that if the error is caused by the server being - // overloaded, we'll contribute to the problem by trying to sync - // repeatedly at the maximum rate. - this._syncThresholds[engine.name] = INITIAL_THRESHOLD; - } - else { - this._log.debug(engine.name + " score " + score + - " does not reach threshold " + - this._syncThresholds[engine.name] + "; not syncing"); - - // Decrement the threshold by the standard amount, and if this puts it - // at or below zero, then set it to 1, the lowest possible value, where - // it'll stay until there's something to sync (whereupon we'll sync it, - // reset the threshold to the initial value, and start over again). - this._syncThresholds[engine.name] -= THRESHOLD_DECREMENT_STEP; - if (this._syncThresholds[engine.name] <= 0) - this._syncThresholds[engine.name] = 1; - } - } - - if (!this._syncError) { - Svc.Prefs.set("lastSync", new Date().toString()); - this._log.info("Sync completed successfully"); - } else - this._log.warn("Some engines did not sync correctly"); - - } finally { - this._cancelRequested = false; - this._syncError = false; - } + /** + * Do a synchronized sync (only one sync at a time). + * + * @param onComplete + * Callback when this method completes + * @param fullSync + * True to unconditionally sync all engines + */ + sync: function WeaveSvc_sync(onComplete, fullSync) { + let useThresh = false; // !fullSync but not doing thresholds yet + this._catchAll(this._notify("sync", "", this._localLock(this._sync))). + async(this, onComplete, useThresh); }, // returns true if sync should proceed From 20b4959b0874dac30e19a4c60967aa23f333b478 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Mar 2009 06:15:52 -0500 Subject: [PATCH 0978/1860] Bug 482007 - Refactor "should sync" and schedule sync logic. r=thunder --- services/sync/modules/service.js | 129 ++++++++++++++----------------- 1 file changed, 57 insertions(+), 72 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e2336ef9840d..528b26be72cd 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -64,6 +64,11 @@ const INITIAL_THRESHOLD = 75; // threshold each time we do a sync check and don't sync that engine. const THRESHOLD_DECREMENT_STEP = 25; +// The following are various error messages for not syncing +const kSyncWeaveDisabled = "Weave is disabled"; +const kSyncNotLoggedIn = "User is not logged in"; +const kSyncNotScheduled = "Not scheduled to do sync"; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); @@ -148,7 +153,7 @@ WeaveSvc.prototype = { _keyPair: {}, // Timer object for automagically syncing - _scheduleTimer: null, + _syncTimer: null, get username() { return Svc.Prefs.get("username", ""); @@ -226,12 +231,6 @@ WeaveSvc.prototype = { get mostRecentError() { return this._mostRecentError; }, - get schedule() { - if (!this.enabled) - return 0; // manual/off - return Svc.Prefs.get("schedule"); - }, - get locked() { return this._locked; }, lock: function Svc_lock() { if (this._locked) @@ -243,54 +242,6 @@ WeaveSvc.prototype = { this._locked = false; }, - _setSchedule: function Weave__setSchedule(schedule) { - switch (this.schedule) { - case 0: - this._disableSchedule(); - break; - case 1: - this._enableSchedule(); - break; - default: - this._log.warn("Invalid Weave scheduler setting: " + schedule); - break; - } - }, - - _enableSchedule: function WeaveSvc__enableSchedule() { - if (this._scheduleTimer) { - this._scheduleTimer.cancel(); - this._scheduleTimer = null; - } - this._scheduleTimer = Cc["@mozilla.org/timer;1"]. - createInstance(Ci.nsITimer); - let listener = new Utils.EventListener(Utils.bind2(this, this._onSchedule)); - this._scheduleTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL, - this._scheduleTimer.TYPE_REPEATING_SLACK); - this._log.config("Weave scheduler enabled"); - }, - - _disableSchedule: function WeaveSvc__disableSchedule() { - if (this._scheduleTimer) { - this._scheduleTimer.cancel(); - this._scheduleTimer = null; - } - this._log.config("Weave scheduler disabled"); - }, - - _onSchedule: function WeaveSvc__onSchedule() { - if (this.enabled) { - if (this.locked) { - this._log.trace("Skipping scheduled sync; local operation in progress"); - } else { - this._log.info("Running scheduled sync"); - this._catchAll( - this._notify("sync", "", - this._localLock(this._sync))).async(this); - } - } - }, - _genKeyURLs: function WeaveSvc__genKeyURLs() { let url = this.clusterURL + this.username; PubKeys.defaultKeyUri = url + "/keys/pubkey"; @@ -419,9 +370,10 @@ WeaveSvc.prototype = { switch (topic) { case "nsPref:changed": switch (data) { - case "enabled": // this works because this.schedule is 0 when disabled + case "enabled": case "schedule": - this._setSchedule(this.schedule); + // Potentially we'll want to reschedule syncs + this._checkSync(); break; } break; @@ -519,7 +471,6 @@ WeaveSvc.prototype = { this._loggedIn = false; - try { if (typeof(user) != 'undefined') this.username = user; if (typeof(pass) != 'undefined') @@ -545,13 +496,7 @@ WeaveSvc.prototype = { } this._loggedIn = true; - this._setSchedule(this.schedule); self.done(true); - - } catch (e) { - this._disableSchedule(); - throw e; - } }; this._catchAll( this._localLock( @@ -560,11 +505,14 @@ WeaveSvc.prototype = { logout: function WeaveSvc_logout() { this._log.info("Logging out"); - this._disableSchedule(); this._loggedIn = false; this._keyPair = {}; ID.get('WeaveID').setTempPassword(null); // clear cached password ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase + + // Cancel the sync timer now that we're logged out + this._checkSync(); + this._os.notifyObservers(null, "weave:service:logout:finish", ""); }, @@ -698,6 +646,42 @@ WeaveSvc.prototype = { */ _syncThresh: {}, + /** + * Determine if a sync should run. If so, schedule a repeating sync; + * otherwise, cancel future syncs and return a reason. + * + * @return Reason for not syncing; not-truthy if sync should run + */ + _checkSync: function Weave__checkSync() { + let reason = ""; + if (!this.enabled) + reason = kSyncWeaveDisabled; + else if (!this._loggedIn) + reason = kSyncNotLoggedIn; + else if (Svc.Prefs.get("schedule", 0) != 1) + reason = kSyncNotScheduled; + + // A truthy reason means we shouldn't continue to sync + if (reason) { + // Cancel any future syncs + if (this._syncTimer) { + this._syncTimer.cancel(); + this._syncTimer = null; + } + this._log.config("Weave scheduler disabled: " + reason); + } + // We're good to sync, so schedule a repeating sync if we haven't yet + else if (!this._syncTimer) { + this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let listener = new Utils.EventListener(Utils.bind2(this, this.sync)); + this._syncTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL, + Ci.nsITimer.TYPE_REPEATING_SLACK); + this._log.config("Weave scheduler enabled"); + } + + return reason; + }, + /** * Sync up engines with the server. * @@ -708,13 +692,14 @@ WeaveSvc.prototype = { _sync: function WeaveSvc__sync(useThresh) { let self = yield; - if (!this.enabled) - return; - - if (!this._loggedIn) { - this._disableSchedule(); - this._mostRecentError = "Can't sync, not logged in."; - throw "aborting sync, not logged in"; + // Make sure we should sync or record why we shouldn't. We always obey the + // reason if we're using thresholds (not a full sync); otherwise, allow + // "not scheduled" as future syncs have already been canceled by checkSync. + let reason = this._checkSync(); + if (reason && (useThresh || reason != kSyncNotScheduled)) { + reason = "Can't sync: " + reason; + this._mostRecentError = reason; + throw reason; } if (!(yield this._remoteSetup.async(this, self.cb))) { From 413d07818a057365409e3dc1903f3d5e46ef9bd7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Mar 2009 06:30:30 -0500 Subject: [PATCH 0979/1860] Bug 482178 - Clean up services -> lazy services. r=thunder --- services/sync/modules/crypto.js | 44 +++++-------------- services/sync/modules/engines/passwords.js | 18 +++----- services/sync/modules/resource.js | 6 +-- services/sync/modules/service.js | 32 ++------------ services/sync/modules/stores.js | 8 ---- services/sync/modules/util.js | 26 ++++------- services/sync/modules/wrap.js | 6 +-- .../sync/tests/unit/fake_login_manager.js | 7 ++- 8 files changed, 37 insertions(+), 110 deletions(-) diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js index 25f85bf1cc7a..778a347230aa 100644 --- a/services/sync/modules/crypto.js +++ b/services/sync/modules/crypto.js @@ -58,23 +58,6 @@ function CryptoSvc() { CryptoSvc.prototype = { _logName: "Crypto", - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - __crypto: null, - get _crypto() { - if (!this.__crypto) - this.__crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - createInstance(Ci.IWeaveCrypto); - return this.__crypto; - }, - - get defaultAlgorithm() { return Utils.prefs.getCharPref("encryption"); }, @@ -113,7 +96,7 @@ CryptoSvc.prototype = { return; // otherwise we'll send the alg changed event twice } // FIXME: listen to this bad boy somewhere - this._os.notifyObservers(null, "weave:encryption:algorithm-changed", ""); + Svc.Observer.notifyObservers(null, "weave:encryption:algorithm-changed", ""); } break; default: this._log.warn("Unknown encryption preference changed - ignoring"); @@ -149,7 +132,7 @@ CryptoSvc.prototype = { } else { let symkey = identity.bulkKey; let iv = identity.bulkIV; - ret = this._crypto.encrypt(data, symkey, iv); + ret = Svc.Crypto.encrypt(data, symkey, iv); } self.done(ret); @@ -167,7 +150,7 @@ CryptoSvc.prototype = { } else { let symkey = identity.bulkKey; let iv = identity.bulkIV; - ret = this._crypto.decrypt(data, symkey, iv); + ret = Svc.Crypto.decrypt(data, symkey, iv); } self.done(ret); @@ -184,8 +167,8 @@ CryptoSvc.prototype = { this._log.trace("randomKeyGen called. [id=" + identity.realm + "]"); - let symkey = this._crypto.generateRandomKey(); - let iv = this._crypto.generateRandomIV(); + let symkey = Svc.Crypto.generateRandomKey(); + let iv = Svc.Crypto.generateRandomIV(); identity.bulkKey = symkey; identity.bulkIV = iv; @@ -206,12 +189,10 @@ CryptoSvc.prototype = { // Generate a blob of random data for salting the passphrase used to // encrypt the private key. - let salt = this._crypto.generateRandomBytes(32); - let iv = this._crypto.generateRandomIV(); + let salt = Svc.Crypto.generateRandomBytes(32); + let iv = Svc.Crypto.generateRandomIV(); - this._crypto.generateKeypair(identity.password, - salt, iv, - pubOut, privOut); + Svc.Crypto.generateKeypair(identity.password, salt, iv, pubOut, privOut); identity.keypairAlg = "RSA"; identity.pubkey = pubOut.value; @@ -224,7 +205,7 @@ CryptoSvc.prototype = { let self = yield; this._log.trace("wrapKey called. [id=" + identity.realm + "]"); - let ret = this._crypto.wrapSymmetricKey(data, identity.pubkey); + let ret = Svc.Crypto.wrapSymmetricKey(data, identity.pubkey); self.done(ret); }, @@ -233,11 +214,8 @@ CryptoSvc.prototype = { let self = yield; this._log.trace("upwrapKey called. [id=" + identity.realm + "]"); - let ret = this._crypto.unwrapSymmetricKey(data, - identity.privkey, - identity.password, - identity.passphraseSalt, - identity.privkeyWrapIV); + let ret = Svc.Crypto.unwrapSymmetricKey(data, identity.privkey, + identity.password, identity.passphraseSalt, identity.privkeyWrapIV); self.done(ret); }, diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index be1c57c0ea2e..6b880a7737ae 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -99,12 +99,6 @@ PasswordStore.prototype = { __proto__: Store.prototype, _logName: "PasswordStore", - get _loginManager() { - let loginManager = Utils.getLoginManager(); - this.__defineGetter__("_loginManager", function() loginManager); - return loginManager; - }, - _nsLoginInfo: null, _init: function PasswordStore_init() { Store.prototype._init.call(this); @@ -140,7 +134,7 @@ PasswordStore.prototype = { getAllIDs: function PasswordStore__getAllIDs() { let items = {}; - let logins = this._loginManager.getAllLogins({}); + let logins = Svc.Login.getAllLogins({}); for (let i = 0; i < logins.length; i++) { let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); @@ -166,7 +160,7 @@ PasswordStore.prototype = { createInstance(Ci.nsIWritablePropertyBag2); prop.setPropertyAsAUTF8String("guid", newID); - this._loginManager.modifyLogin(this._loginItems[oldID], prop); + Svc.Login.modifyLogin(this._loginItems[oldID], prop); }, itemExists: function PasswordStore__itemExists(id) { @@ -195,13 +189,13 @@ PasswordStore.prototype = { create: function PasswordStore__create(record) { this._log.debug("Adding login for " + record.hostname); - this._loginManager.addLogin(this._nsLoginInfoFromRecord(record)); + Svc.Login.addLogin(this._nsLoginInfoFromRecord(record)); }, remove: function PasswordStore__remove(record) { this._log.debug("Removing login " + record.id); if (record.id in this._loginItems) { - this._loginManager.removeLogin(this._loginItems[record.id]); + Svc.Login.removeLogin(this._loginItems[record.id]); return; } @@ -219,11 +213,11 @@ PasswordStore.prototype = { this._log.trace("Updating " + record.id + " (" + itemId + ")"); let newinfo = this._nsLoginInfoFromRecord(record); - this._loginManager.modifyLogin(login, newinfo); + Svc.Login.modifyLogin(login, newinfo); }, wipe: function PasswordStore_wipe() { - this._loginManager.removeAllLogins(); + Svc.Login.removeAllLogins(); } }; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index b524ea58bdc8..8c103d58878c 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -145,9 +145,7 @@ Resource.prototype = { }, _createRequest: function Res__createRequest() { - let ios = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - this._lastChannel = ios.newChannel(this.spec, null, null). + this._lastChannel = Svc.IO.newChannel(this.spec, null, null). QueryInterface(Ci.nsIRequest); // Always validate the cache: let loadFlags = this._lastChannel.loadFlags; @@ -268,6 +266,8 @@ function ChannelListener(onComplete, onProgress, logger) { } ChannelListener.prototype = { onStartRequest: function Channel_onStartRequest(channel) { + // XXX Bug 482179 Some reason xpconnect makes |channel| only nsIRequest + channel.QueryInterface(Ci.nsIHttpChannel); this._log.debug(channel.requestMethod + " request for " + channel.URI.spec); this._data = ''; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 528b26be72cd..d1c715c6ef7a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -125,30 +125,6 @@ WeaveSvc.prototype = { _keyGenEnabled: true, _mostRecentError: null, - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - - __dirSvc: null, - get _dirSvc() { - if (!this.__dirSvc) - this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - return this.__dirSvc; - }, - - __json: null, - get _json() { - if (!this.__json) - this.__json = Cc["@mozilla.org/dom/json;1"]. - createInstance(Ci.nsIJSON); - return this.__json; - }, - // object for caching public and private keys _keyPair: {}, @@ -291,7 +267,7 @@ WeaveSvc.prototype = { } Utils.prefs.addObserver("", this, false); - this._os.addObserver(this, "quit-application", true); + Svc.Observer.addObserver(this, "quit-application", true); FaultTolerance.Service; // initialize FT service if (!this.enabled) @@ -335,7 +311,7 @@ WeaveSvc.prototype = { dapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.dump")]; root.addAppender(dapp); - let brief = this._dirSvc.get("ProfD", Ci.nsIFile); + let brief = Svc.Directory.get("ProfD", Ci.nsIFile); brief.QueryInterface(Ci.nsILocalFile); brief.append("weave"); brief.append("logs"); @@ -513,7 +489,7 @@ WeaveSvc.prototype = { // Cancel the sync timer now that we're logged out this._checkSync(); - this._os.notifyObservers(null, "weave:service:logout:finish", ""); + Svc.Observer.notifyObservers(null, "weave:service:logout:finish", ""); }, // stuff we need to to after login, before we can really do @@ -863,7 +839,7 @@ WeaveSvc.prototype = { // XXX Bug 480448: Delete any snapshots from old code try { - let cruft = this._dirSvc.get("ProfD", Ci.nsIFile); + let cruft = Svc.Directory.get("ProfD", Ci.nsIFile); cruft.QueryInterface(Ci.nsILocalFile); cruft.append("weave"); cruft.append("snapshots"); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 82e9c7ec30f1..19eaba89c0ab 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -64,14 +64,6 @@ Store.prototype = { // set this property in child object's wrap()! _lookup: null, - __os: null, - get _os() { - if (!this.__os) - this.__os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - return this.__os; - }, - __json: null, get _json() { if (!this.__json) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index f73024816b55..fd03d4986791 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -78,9 +78,7 @@ let Utils = { arg = {path: arg}; let pathParts = arg.path.split("/"); - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - let file = dirSvc.get("ProfD", Ci.nsIFile); + let file = Svc.Directory.get("ProfD", Ci.nsIFile); file.QueryInterface(Ci.nsILocalFile); for (let i = 0; i < pathParts.length; i++) file.append(pathParts[i]); @@ -89,17 +87,10 @@ let Utils = { return file; }, - getLoginManager: function getLoginManager() { - return Cc["@mozilla.org/login-manager;1"]. - getService(Ci.nsILoginManager); - }, - findPassword: function findPassword(realm, username) { // fixme: make a request and get the realm ? let password; - let lm = Cc["@mozilla.org/login-manager;1"] - .getService(Ci.nsILoginManager); - let logins = lm.findLogins({}, 'chrome://weave', null, realm); + let logins = Svc.Login.findLogins({}, 'chrome://weave', null, realm); for (let i = 0; i < logins.length; i++) { if (logins[i].username == username) { @@ -112,8 +103,7 @@ let Utils = { setPassword: function setPassword(realm, username, password) { // cleanup any existing passwords - let lm = Cc["@mozilla.org/login-manager;1"] - .getService(Ci.nsILoginManager); + let lm = Svc.Login; let logins = lm.findLogins({}, 'chrome://weave', null, realm); for (let i = 0; i < logins.length; i++) lm.removeLogin(logins[i]); @@ -370,10 +360,7 @@ let Utils = { }, getTmp: function Weave_getTmp(name) { - let ds = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - - let tmp = ds.get("ProfD", Ci.nsIFile); + let tmp = Svc.Directory.get("ProfD", Ci.nsIFile); tmp.QueryInterface(Ci.nsILocalFile); tmp.append("weave"); @@ -509,8 +496,11 @@ Utils.EventListener.prototype = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); -Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); Utils.lazySvc(Svc, 'Crypto', "@labs.mozilla.com/Weave/Crypto;1", Ci.IWeaveCrypto); +Utils.lazySvc(Svc, 'Directory', "@mozilla.org/file/directory_service;1", Ci.nsIProperties); +Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); +Utils.lazySvc(Svc, 'Login', "@mozilla.org/login-manager;1", Ci.nsILoginManager); Utils.lazySvc(Svc, 'Memory', "@mozilla.org/xpcom/memory-service;1", Ci.nsIMemory); +Utils.lazySvc(Svc, 'Observer', "@mozilla.org/observer-service;1", Ci.nsIObserverService); Utils.lazySvc(Svc, 'Version', "@mozilla.org/xpcom/version-comparator;1", Ci.nsIVersionComparator); diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index f91764a446da..476129570a76 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -122,8 +122,7 @@ let Wrap = { if (!ret) throw "Could not acquire lock"; - this._os.notifyObservers(null, - this._osPrefix + "local-lock:acquired", ""); + Svc.Observer.notifyObservers(null, this._osPrefix + "local-lock:acquired", ""); try { args = savedArgs.concat(args); @@ -135,8 +134,7 @@ let Wrap = { } finally { this.unlock(); - this._os.notifyObservers(null, - this._osPrefix + "local-lock:released", ""); + Svc.Observer.notifyObservers(null, this._osPrefix + "local-lock:released", ""); } self.done(ret); diff --git a/services/sync/tests/unit/fake_login_manager.js b/services/sync/tests/unit/fake_login_manager.js index 669d7a8e59ac..ceba356aebb9 100644 --- a/services/sync/tests/unit/fake_login_manager.js +++ b/services/sync/tests/unit/fake_login_manager.js @@ -24,9 +24,9 @@ function FakeLoginManager(fakeLogins) { let self = this; - Utils.getLoginManager = function fake_getLoginManager() { - // Return a fake nsILoginManager object. - return { + // Use a fake nsILoginManager object. + delete Svc.Login; + Svc.Login = { removeAllLogins: function() { self.fakeLogins = []; }, getAllLogins: function() { return self.fakeLogins; }, addLogin: function(login) { @@ -34,6 +34,5 @@ function FakeLoginManager(fakeLogins) { "with hostname '" + login.hostname + "'."); self.fakeLogins.push(login); } - }; }; } From 94bc20d907104f309f5775eac69dc57168be36d2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Mar 2009 06:30:36 -0500 Subject: [PATCH 0980/1860] Bug 481345 - Weave should be disabled while offline or private browsing mode is active. r=thunder --- services/sync/modules/service.js | 18 ++++++++++++++++++ services/sync/modules/util.js | 1 + 2 files changed, 19 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d1c715c6ef7a..056942b4dfbc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -67,6 +67,8 @@ const THRESHOLD_DECREMENT_STEP = 25; // The following are various error messages for not syncing const kSyncWeaveDisabled = "Weave is disabled"; const kSyncNotLoggedIn = "User is not logged in"; +const kSyncNetworkOffline = "Network is offline"; +const kSyncInPrivateBrowsing = "Private browsing is enabled"; const kSyncNotScheduled = "Not scheduled to do sync"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -267,6 +269,8 @@ WeaveSvc.prototype = { } Utils.prefs.addObserver("", this, false); + Svc.Observer.addObserver(this, "network:offline-status-changed", true); + Svc.Observer.addObserver(this, "private-browsing", true); Svc.Observer.addObserver(this, "quit-application", true); FaultTolerance.Service; // initialize FT service @@ -353,6 +357,16 @@ WeaveSvc.prototype = { break; } break; + case "network:offline-status-changed": + // Whether online or offline, we'll reschedule syncs + this._log.debug("Network offline status change: " + data); + this._checkSync(); + break; + case "private-browsing": + // Entering or exiting private browsing? Reschedule syncs + this._log.debug("Private browsing change: " + data); + this._checkSync(); + break; case "quit-application": this._onQuitApplication(); break; @@ -634,6 +648,10 @@ WeaveSvc.prototype = { reason = kSyncWeaveDisabled; else if (!this._loggedIn) reason = kSyncNotLoggedIn; + else if (Svc.IO.offline) + reason = kSyncNetworkOffline; + else if (Svc.Private.privateBrowsingEnabled) + reason = kSyncInPrivateBrowsing; else if (Svc.Prefs.get("schedule", 0) != 1) reason = kSyncNotScheduled; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index fd03d4986791..2cd9a75c7833 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -502,5 +502,6 @@ Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); Utils.lazySvc(Svc, 'Login', "@mozilla.org/login-manager;1", Ci.nsILoginManager); Utils.lazySvc(Svc, 'Memory', "@mozilla.org/xpcom/memory-service;1", Ci.nsIMemory); Utils.lazySvc(Svc, 'Observer', "@mozilla.org/observer-service;1", Ci.nsIObserverService); +Utils.lazySvc(Svc, 'Private', "@mozilla.org/privatebrowsing;1", Ci.nsIPrivateBrowsingService); Utils.lazySvc(Svc, 'Version', "@mozilla.org/xpcom/version-comparator;1", Ci.nsIVersionComparator); From b52b64113e52bab07dc5a3b4fa9cc992f4a40971 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Mar 2009 14:12:05 -0500 Subject: [PATCH 0981/1860] Make method signatures for sync and _sync match up. (followup for bug 482003) --- services/sync/modules/service.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 056942b4dfbc..2c19bdf4efb0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -679,13 +679,16 @@ WeaveSvc.prototype = { /** * Sync up engines with the server. * - * @param useThresh - * True to use thresholds to determine what engines to sync + * @param fullSync + * True to unconditionally sync all engines * @throw Reason for not syncing */ _sync: function WeaveSvc__sync(useThresh) { let self = yield; + // Use thresholds to determine what to sync only if it's not a full sync + let useThresh = !fullSync; + // Make sure we should sync or record why we shouldn't. We always obey the // reason if we're using thresholds (not a full sync); otherwise, allow // "not scheduled" as future syncs have already been canceled by checkSync. @@ -771,9 +774,9 @@ WeaveSvc.prototype = { * True to unconditionally sync all engines */ sync: function WeaveSvc_sync(onComplete, fullSync) { - let useThresh = false; // !fullSync but not doing thresholds yet + fullSync = true; // not doing thresholds yet this._catchAll(this._notify("sync", "", this._localLock(this._sync))). - async(this, onComplete, useThresh); + async(this, onComplete, fullSync); }, // returns true if sync should proceed From b2cfd91bef418dda063931a8a8487940fecf8f55 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Mar 2009 14:13:18 -0500 Subject: [PATCH 0982/1860] Pushed too early :( really match up signatures --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2c19bdf4efb0..bbe4b7cb419a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -683,7 +683,7 @@ WeaveSvc.prototype = { * True to unconditionally sync all engines * @throw Reason for not syncing */ - _sync: function WeaveSvc__sync(useThresh) { + _sync: function WeaveSvc__sync(fullSync) { let self = yield; // Use thresholds to determine what to sync only if it's not a full sync From b3513bc7dfc351ba842e295c3d5d342f7674add0 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Mar 2009 15:33:50 -0500 Subject: [PATCH 0983/1860] Bug 482561 - Remove localLock notifications --- services/sync/modules/wrap.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js index 476129570a76..eb285b3fd214 100644 --- a/services/sync/modules/wrap.js +++ b/services/sync/modules/wrap.js @@ -122,8 +122,6 @@ let Wrap = { if (!ret) throw "Could not acquire lock"; - Svc.Observer.notifyObservers(null, this._osPrefix + "local-lock:acquired", ""); - try { args = savedArgs.concat(args); args.unshift(this, savedMethod, self.cb); @@ -134,7 +132,6 @@ let Wrap = { } finally { this.unlock(); - Svc.Observer.notifyObservers(null, this._osPrefix + "local-lock:released", ""); } self.done(ret); From c57cf919c5a6d235f80b9c3516d782eefc7d027f Mon Sep 17 00:00:00 2001 From: Date: Tue, 10 Mar 2009 17:12:59 -0700 Subject: [PATCH 0984/1860] Was failing on Fennec because private browsing doesn't exist there -- now more gracefully handles missing components. --- services/sync/modules/service.js | 3 ++- services/sync/modules/util.js | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bbe4b7cb419a..adcd05974d4c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -650,7 +650,8 @@ WeaveSvc.prototype = { reason = kSyncNotLoggedIn; else if (Svc.IO.offline) reason = kSyncNetworkOffline; - else if (Svc.Private.privateBrowsingEnabled) + else if (Svc.Private && Svc.Private.privateBrowsingEnabled) + // Svc.Private doesn't exist on Fennec -- don't assume it's there. reason = kSyncInPrivateBrowsing; else if (Svc.Prefs.get("schedule", 0) != 1) reason = kSyncNotScheduled; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 2cd9a75c7833..8370d9d6e129 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -146,8 +146,15 @@ let Utils = { lazySvc: function Weave_lazySvc(dest, prop, cid, iface) { let getter = function() { delete dest[prop]; - dest[prop] = Cc[cid].getService(iface); - return dest[prop]; + if (!Cc[cid]) { + let log = Log4Moz.repository.getLogger("Service.Util"); + log.warn("Component " + cid + " requested, but doesn't exist on " + + "this platform."); + return null; + } else{ + dest[prop] = Cc[cid].getService(iface); + return dest[prop]; + } }; dest.__defineGetter__(prop, getter); }, From dc09b6881352dd2a833d65ca34f28f01ff037529 Mon Sep 17 00:00:00 2001 From: Date: Tue, 10 Mar 2009 17:40:49 -0700 Subject: [PATCH 0985/1860] Removed GUID 'your mom' as the default client.name. Oops! Sorry everybody, that was there for debugging. --- services/sync/services-sync.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 64a596c23310..c4b69cf15aa6 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -35,5 +35,4 @@ pref("extensions.weave.log.logger.engine.history", "Debug"); pref("extensions.weave.log.logger.engine.tabs", "Debug"); pref("extensions.weave.log.logger.engine.clients", "Debug"); -pref("extensions.weave.network.numRetries", 2); -pref("extensions.weave.client.syncID", "YOUR MOM"); +pref("extensions.weave.network.numRetries", 2); \ No newline at end of file From 996a639a6ddc0934c8d0f0f62770566963b69109 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Mar 2009 01:40:04 -0700 Subject: [PATCH 0986/1860] Bug 478327: Send minimal '[null]' payload for deleted records --- services/sync/modules/base_records/crypto.js | 6 ++---- services/sync/modules/base_records/wbo.js | 6 +++--- services/sync/modules/engines.js | 7 +++---- services/sync/modules/engines/bookmarks.js | 3 ++- services/sync/modules/engines/history.js | 2 +- services/sync/modules/engines/passwords.js | 2 +- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 4da26fa28efd..a33eab48f57e 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -85,8 +85,7 @@ CryptoWrapper.prototype = { let self = yield; // Don't encrypt empty payloads - if (!this.cleartext) { - this.ciphertext = this.cleartext; + if (!this.payload) { self.done(); return; } @@ -111,8 +110,7 @@ CryptoWrapper.prototype = { let self = yield; // Empty payloads aren't encrypted - if (!this.ciphertext) { - this.cleartext = this.ciphertext; + if (!this.payload) { self.done(); return; } diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 83b6ae4046b6..965d40423061 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -118,15 +118,15 @@ WBORecord.prototype = { // payload is encoded twice in serialized form, because the // server expects a string serialize: function WBORec_serialize() { - this.payload = Svc.Json.encode(this.payload); + this.payload = Svc.Json.encode([this.payload]); let ret = Svc.Json.encode(this.data); - this.payload = Svc.Json.decode(this.payload); + this.payload = Svc.Json.decode(this.payload)[0]; return ret; }, deserialize: function WBORec_deserialize(json) { this.data = Svc.Json.decode(json); - this.payload = Svc.Json.decode(this.payload); + this.payload = Svc.Json.decode(this.payload)[0]; }, toString: function WBORec_toString() { diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index dbf2e7a98f4e..460017ca0ce0 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -229,8 +229,7 @@ SyncEngine.prototype = { if (a.depth != b.depth) return false; // note: sortindex ignored - if (a.cleartext == null || - b.cleartext == null) + if (a.payload == null || b.payload == null) // deleted items return false; return Utils.deepEquals(a.cleartext, b.cleartext); }, @@ -380,7 +379,7 @@ SyncEngine.prototype = { // If the incoming item has been deleted, skip step 3 this._log.trace("Reconcile step 2.5"); - if (item.cleartext === null) { + if (item.payload === null) { self.done(true); return; } @@ -435,7 +434,7 @@ SyncEngine.prototype = { for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); this._log.trace("Outgoing:\n" + out); - if (out.cleartext) // skip deleted records + if (out.payload) // skip deleted records this._store.createMetaRecords(out.id, meta); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); up.pushData(Svc.Json.decode(out.serialize())); // FIXME: inefficient diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5a088cef82d5..9279be5048b2 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -487,7 +487,8 @@ BookmarksStore.prototype = { if (placeId <= 0) { // deleted item record = new PlacesItem(); record.id = guid; - record.cleartext = null; + record.payload = null; + this.cache.put(guid, record); return record; } diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 9ceb35fea0a2..f27e7b91af68 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -443,7 +443,7 @@ HistoryStore.prototype = { record.visits = this._getVisits(record.histUri); record.encryption = cryptoMetaURL; } else { - record.cleartext = null; // deleted item + record.payload = null; // deleted item } this.cache.put(guid, record); return record; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 6b880a7737ae..f63fd9195d9c 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -182,7 +182,7 @@ PasswordStore.prototype = { record.passwordField = login.passwordField; } else { /* Deleted item */ - record.cleartext = null; + record.payload = null; } return record; }, From a396042def8fbc9a1db3e5a3004ea20b838478bc Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 11 Mar 2009 02:02:58 -0700 Subject: [PATCH 0987/1860] oops, client was not updating depth/index of records --- services/sync/modules/engines/bookmarks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 9279be5048b2..8634eb8dd4a5 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -580,7 +580,7 @@ BookmarksStore.prototype = { node.containerOpen = true; for (var i = 0; i < node.childCount; i++) { let child = node.getChild(i); - let foo = this._createMiniRecord(child.itemId); + let foo = this._createMiniRecord(child.itemId, true); items[foo.id] = foo; this._getChildren(child, depthIndex, items); } @@ -600,7 +600,7 @@ BookmarksStore.prototype = { for (var i = 0; i < parent.childCount; i++) { let child = parent.getChild(i); - let foo = this._createMiniRecord(child.itemId); + let foo = this._createMiniRecord(child.itemId, true); items[foo.id] = foo; } From a4b2d2cc5409a43250013c35a146cc19fcd2ec31 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 11 Mar 2009 10:34:31 -0500 Subject: [PATCH 0988/1860] Bug 482513 - ClientRecord inherits from WBORecord, so it lacks cleartext. r=thunder --- services/sync/modules/type_records/clients.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index ee4ed9d71a16..b2037b3d0592 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -59,6 +59,10 @@ ClientRecord.prototype = { this._WBORec_init(uri); }, + // engines.js uses cleartext to determine if records _isEqual + // XXX Bug 482669 Implement .equals() for SyncEngine to compare records + get cleartext() this.serialize(), + // XXX engines.js calls encrypt/decrypt for all records, so define these: encrypt: function ClientRec_encrypt(onComplete) { let fn = function ClientRec__encrypt() {let self = yield;}; From 9f65f545da8e13bec6a561433f16e7d76d4d9edf Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 11 Mar 2009 14:35:23 -0500 Subject: [PATCH 0989/1860] self.cb not this.cb for engine._wipeClient --- services/sync/modules/engines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 460017ca0ce0..5a18db6ad292 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -164,7 +164,7 @@ Engine.prototype = { _wipeClient: function Engine__wipeClient() { let self = yield; - yield this.resetClient(this.cb); + yield this.resetClient(self.cb); this._log.debug("Deleting all local data"); this._store.wipe(); From 9d70a8d650ee1c50d2394f3d13d9c0bbfcde2e6d Mon Sep 17 00:00:00 2001 From: Date: Wed, 11 Mar 2009 18:27:44 -0700 Subject: [PATCH 0990/1860] Fennec now updates status line to show what engine is syncing and what it's doing. This fixes bug 481323. --- services/sync/modules/service.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index adcd05974d4c..19d16b9a6195 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -461,11 +461,11 @@ WeaveSvc.prototype = { this._loggedIn = false; - if (typeof(user) != 'undefined') + if (typeof(user) != 'undefined') this.username = user; - if (typeof(pass) != 'undefined') + if (typeof(pass) != 'undefined') ID.get('WeaveID').setTempPassword(pass); - if (typeof(passp) != 'undefined') + if (typeof(passp) != 'undefined') ID.get('WeaveCryptoID').setTempPassword(passp); if (!this.username) { @@ -485,8 +485,8 @@ WeaveSvc.prototype = { throw "Login failed"; } - this._loggedIn = true; - self.done(true); + this._loggedIn = true; + self.done(true); }; this._catchAll( this._localLock( From cb383c708b5ab3b2cebda551dcd6f0584ca8fa86 Mon Sep 17 00:00:00 2001 From: Date: Wed, 11 Mar 2009 18:29:18 -0700 Subject: [PATCH 0991/1860] Fixed tabs in services.js. --- services/sync/modules/service.js | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 19d16b9a6195..790df7042e98 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -461,32 +461,32 @@ WeaveSvc.prototype = { this._loggedIn = false; - if (typeof(user) != 'undefined') - this.username = user; - if (typeof(pass) != 'undefined') - ID.get('WeaveID').setTempPassword(pass); - if (typeof(passp) != 'undefined') - ID.get('WeaveCryptoID').setTempPassword(passp); + if (typeof(user) != 'undefined') + this.username = user; + if (typeof(pass) != 'undefined') + ID.get('WeaveID').setTempPassword(pass); + if (typeof(passp) != 'undefined') + ID.get('WeaveCryptoID').setTempPassword(passp); - if (!this.username) { - this._mostRecentError = "No username set."; - throw "No username set, login failed"; - } + if (!this.username) { + this._mostRecentError = "No username set."; + throw "No username set, login failed"; + } - if (!this.password) { - this._mostRecentError = "No password set."; - throw "No password given or found in password manager"; - } + if (!this.password) { + this._mostRecentError = "No password set."; + throw "No password given or found in password manager"; + } - this._log.debug("Logging in user " + this.username); + this._log.debug("Logging in user " + this.username); - if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) { - this._mostRecentError = "Login failed. Check your username/password/phrase."; - throw "Login failed"; - } + if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) { + this._mostRecentError = "Login failed. Check your username/password/phrase."; + throw "Login failed"; + } - this._loggedIn = true; - self.done(true); + this._loggedIn = true; + self.done(true); }; this._catchAll( this._localLock( From faffb3d5bfc510ebe5929dd98df36c5d7df659b4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 11 Mar 2009 23:37:47 -0500 Subject: [PATCH 0992/1860] Bug 482878 - service (_remoteSetup) needs to migrate payload formats. r=thunder --- services/sync/modules/service.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 790df7042e98..adb7401142e5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -517,6 +517,12 @@ WeaveSvc.prototype = { let meta = yield Records.import(self.cb, this.clusterURL + this.username + "/meta/global"); + // XXX Bug 482878 Old payloads weren't array-wrapped, so migrate by wiping + if (meta && meta.payload == null) { + this._log.debug("Migrating to minimal payloads by wiping the server"); + meta = null; + } + let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; From 68fe0cd4ec083f061b2c01173dfb0773b979ffee Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 3 Mar 2009 17:29:35 -0600 Subject: [PATCH 0993/1860] Bug 481266 - Provide a way to perform actions on remote clients. r=thunder --- services/sync/modules/engines/clients.js | 12 ++-- services/sync/modules/service.js | 81 +++++++++++++++++++++++- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 4dc274931de4..5365cd4891de 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -255,10 +255,14 @@ ClientStore.prototype = { createRecord: function ClientStore_createRecord(id) { let record = new ClientRecord(); record.id = id; - if (id == Clients.clientID) - record.payload = {name: Clients.clientName, type: Clients.clientType}; - else - record.payload = this._clients[id]; + record.payload = this._clients[id] || {}; + + // For the local client, update the name and type with the current value + if (id == Clients.clientID) { + record.payload.name = Clients.clientName; + record.payload.type = Clients.clientType; + } + return record; } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index adb7401142e5..6c1a27374bd0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -878,7 +878,82 @@ WeaveSvc.prototype = { } } this._catchAll(this._notify("reset-client", "", fn)).async(this, onComplete); - } + }, + + /** + * 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", 0, "Clear temporary local data for all engines"], + ["resetEngine", 1, "Clear temporary local data for engine"], + ["wipeAll", 0, "Delete all client data for all engines"], + ["wipeEngine", 1, "Delete all client data for engine"], + ].reduce(function WeaveSvc__commands(commands, entry) { + commands[entry[0]] = {}; + for (let [i, attr] in Iterator(["args", "desc"])) + commands[entry[0]][attr] = entry[i + 1]; + return commands; + }, {}), + + /** + * Prepare to send a command to each remote client. Calling this doesn't + * actually sync the command data to the server. If the client already has + * the command/args pair, it won't get a duplicate action. + * + * @param command + * Command to invoke on remote clients + * @param args + * Array of arguments to give to the command + */ + prepCommand: function WeaveSvc_prepCommand(command, args) { + let commandData = this._commands[command]; + // Don't send commands that we don't know about + if (commandData == null) { + this._log.error("Unknown command to send: " + command); + return; + } + // Don't send a command with the wrong number of arguments + else if (args == null || args.length != commandData.args) { + this._log.error("Expected " + commandData.args + " args for '" + + command + "', but got " + args); + return; + } + + // Package the command/args pair into an object + let action = { + command: command, + args: args, + }; + let actionStr = command + "(" + args + ")"; + + // Convert args into a string to simplify array comparisons + let jsonArgs = Svc.Json.encode(args); + let notDupe = function(action) action.command != command || + Svc.Json.encode(action.args) != jsonArgs; + + this._log.info("Sending clients: " + actionStr + "; " + commandData.desc); + + // Add the action to each remote client + for (let guid in Clients.getClients()) { + // Don't send commands to the local client + if (guid == Clients.clientID) + continue; + + let info = Clients.getInfo(guid); + // Set the action to be a new commands array if none exists + if (info.commands == null) + info.commands = [action]; + // Add the new action if there are no duplicates + else if (info.commands.every(notDupe)) + info.commands.push(action); + // Must have been a dupe.. skip! + else + continue; + + Clients.setInfo(guid, info); + this._log.trace("Client " + guid + " got a new action: " + actionStr); + } + }, }; - - From 737f481797072ccf1b6281ae444a6dfe20410634 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Mar 2009 01:33:14 -0500 Subject: [PATCH 0994/1860] Bug 482792 - Provide a wipeClient, wipeRemote, resetService for weave service. r=thunder --- services/sync/modules/service.js | 72 +++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6c1a27374bd0..0f086e878960 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -820,6 +820,9 @@ WeaveSvc.prototype = { /** * Wipe all user data from the server. + * + * @param onComplete + * Callback when this method completes */ wipeServer: function WeaveSvc_wipeServer(onComplete) { let fn = function WeaveSvc__wipeServer() { @@ -845,21 +848,80 @@ WeaveSvc.prototype = { }, /** - * Reset the client by getting rid of any local server data and client data. + * Wipe all local user data. + * + * @param onComplete + * Callback when this method completes */ - resetClient: function WeaveSvc_resetClient(onComplete) { - let fn = function WeaveSvc__resetClient() { + wipeClient: function WeaveSvc_wipeClient(onComplete) { + let fn = function WeaveSvc__wipeClient() { + let self = yield; + + // Clear out any service data + yield this.resetService(self.cb); + + // Fully wipe each engine + for each (let engine in [Clients].concat(Engines.getAll())) + yield engine.wipeClient(self.cb); + }; + this._catchAll(this._notify("wipe-client", "", fn)).async(this, onComplete); + }, + + /** + * Wipe all remote user data by wiping the server then telling each remote + * client to wipe itself. + * + * @param onComplete + * Callback when this method completes + */ + wipeRemote: function WeaveSvc_wipeRemote(onComplete) { + let fn = function WeaveSvc__wipeRemote() { + let self = yield; + + // Clear out any server data + //yield this.wipeServer(self.cb); + + // Tell the remote machines to wipe themselves + this.prepCommand("wipeAll", []); + }; + this._catchAll(this._notify("wipe-remote", "", fn)).async(this, onComplete); + }, + + /** + * Reset local service information like logs, sync times, caches. + * + * @param onComplete + * Callback when this method completes + */ + resetService: function WeaveSvc__resetService(onComplete) { + let fn = function WeaveSvc__resetService() { let self = yield; // First drop old logs to track client resetting behavior this.clearLogs(); - this._log.info("Logs reinitialized for client reset"); + this._log.info("Logs reinitialized for service reset"); // Pretend we've never synced to the server and drop cached data Clients.resetSyncID(); Svc.Prefs.reset("lastSync"); for each (let cache in [PubKeys, PrivKeys, CryptoMetas, Records]) cache.clearCache(); + }; + this._catchAll(this._notify("reset-service", "", fn)).async(this, onComplete); + }, + + /** + * Reset the client by getting rid of any local server data and client data. + * + * @param onComplete + * Callback when this method completes + */ + resetClient: function WeaveSvc_resetClient(onComplete) { + let fn = function WeaveSvc__resetClient() { + let self = yield; + + // Clear out any service data + yield this.resetService(self.cb); // Have each engine drop any temporary meta data for each (let engine in [Clients].concat(Engines.getAll())) @@ -876,7 +938,7 @@ WeaveSvc.prototype = { } catch (e) { this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); } - } + }; this._catchAll(this._notify("reset-client", "", fn)).async(this, onComplete); }, From c9440f1feaa2c04acb0c1b40f1d48fd8eec3d3ac Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Mar 2009 01:33:14 -0500 Subject: [PATCH 0995/1860] Bug 482793 - Process commands sent by remote clients. r=thunder --- services/sync/modules/service.js | 73 ++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0f086e878960..85330c7621df 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -713,6 +713,22 @@ WeaveSvc.prototype = { this._log.debug("Refreshing client list"); yield Clients.sync(self.cb); + // Process the incoming commands if we have any + if (Clients.getClients()[Clients.clientID].commands) { + try { + if (!(yield this.processCommands(self.cb))) + throw "aborting sync, process commands said so"; + + // Repeat remoteSetup in-case the commands forced us to reset + if (!(yield this._remoteSetup.async(this, self.cb))) + throw "aborting sync, remote setup failed after processing commands"; + } + finally { + // Always immediately push back the local client (now without commands) + yield Clients.sync(self.cb); + } + } + try { for each (let engine in Engines.getAll()) { let name = engine.name; @@ -959,6 +975,63 @@ WeaveSvc.prototype = { return commands; }, {}), + /** + * Check if the local client has any remote commands and perform them. + * + * @param onComplete + * Callback when this method completes + * @return False to abort sync + */ + processCommands: function WeaveSvc_processCommands(onComplete) { + let fn = function WeaveSvc__processCommands() { + let self = yield; + let info = Clients.getInfo(Clients.clientID); + let commands = info.commands; + + // Immediately clear out the commands as we've got them locally + delete info.commands; + Clients.setInfo(Clients.clientID, info); + + // Process each command in order + for each ({command: command, args: args} in commands) { + this._log.debug("Processing command: " + command + "(" + args + ")"); + + switch (command) { + case "resetAll": + yield this.resetClient(self.cb); + break; + + case "resetEngine": { + let engine = Engines.get(args[0]); + if (engine != null) + yield engine.resetClient(self.cb); + else + this._log.debug("Cannot reset an unknown engine: " + args[0]); + break; + } + case "wipeAll": + yield this.wipeClient(self.cb); + break; + + case "wipeEngine": { + let engine = Engines.get(args[0]); + if (engine != null) + yield engine.wipeClient(self.cb); + else + this._log.debug("Cannot wipe an unknown engine: " + args[0]); + break; + } + default: + this._log.debug("Received an unknown command: " + command); + break; + } + } + + self.done(true); + }; + this._notify("process-commands", "", fn).async(this, onComplete); + }, + /** * Prepare to send a command to each remote client. Calling this doesn't * actually sync the command data to the server. If the client already has From 5009b5e8e62bf7d058ded23b74fa11b18ff40b16 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Mar 2009 01:33:14 -0500 Subject: [PATCH 0996/1860] Bug 468691 - Need UI for when something is wrong. r=thunder --- services/sync/locales/en-US/preferences.dtd | 9 +++++++++ services/sync/locales/en-US/preferences.properties | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index ec7fd250a551..647f420a5f3e 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -69,6 +69,15 @@ + + + + + + + + + diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index 4c2fa3386fe8..26e23fda181d 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -1,5 +1,13 @@ # %S is the username of the signed in user signedIn.description = Signed in as %S + +erase.local.warning.title = Erase Local Data +erase.local.warning = This will delete all local data.\n\nAre you sure you want to do this? +erase.server.warning.title = Erase Server Data +erase.server.warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? +erase.remote.warning.title = Erase Other Clients +erase.remote.warning = This will delete all remote data.\n\nAre you sure you want to do this? + reset.server.warning.title = Reset Server Data reset.server.warning = This will delete all data on the Weave server.\n\nYou must restart this and any other instances of Firefox you may have running on any computer once you do this.\n\nAre you sure you want to do this? reset.client.warning.title = Reset Client Data From 665a271abcbe15406b41136acb1035f77217da0c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Mar 2009 02:15:30 -0500 Subject: [PATCH 0997/1860] Bug 482903 - Wiping local passwords data prevents syncing. r=thunder --- services/sync/modules/identity.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 7c039c0206f6..e5f15f2bf0a0 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -122,8 +122,9 @@ Identity.prototype = { get userHash() { return Utils.sha1(this.username); }, get password() { + // Look up the password then cache it if (!this._password) - return Utils.findPassword(this.realm, this.username); + return this._password = Utils.findPassword(this.realm, this.username); return this._password; }, set password(value) { From c10c141632a06e693b1cf7056405e05f97e1c1ec Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 12 Mar 2009 00:35:54 -0700 Subject: [PATCH 0998/1860] inhibit caching, since we bypass the cache anyway --- services/sync/modules/resource.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 8c103d58878c..682ef50c41bc 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -149,7 +149,8 @@ Resource.prototype = { QueryInterface(Ci.nsIRequest); // Always validate the cache: let loadFlags = this._lastChannel.loadFlags; - loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; //VALIDATE_ALWAYS; + loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; this._lastChannel.loadFlags = loadFlags; this._lastChannel = this._lastChannel.QueryInterface(Ci.nsIHttpChannel); From 0322011c1a6d75b00ac80c61df6d51bcdd8bc8fd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Mar 2009 10:34:12 -0500 Subject: [PATCH 0999/1860] Fix whitespace in service.js --- services/sync/modules/service.js | 92 ++++++++++++++++---------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 85330c7621df..bc1b6d515d36 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -232,7 +232,7 @@ WeaveSvc.prototype = { try { let iv = Svc.Crypto.generateRandomIV(); if (iv.length == 24) - ok = true; + ok = true; } catch (e) { this._log.debug("Crypto check failed: " + e); @@ -265,7 +265,7 @@ WeaveSvc.prototype = { if (!this._checkCrypto()) { this.enabled = false; this._log.error("Could not load the Weave crypto component. Disabling " + - "Weave, since it will not work correctly."); + "Weave, since it will not work correctly."); } Utils.prefs.addObserver("", this, false); @@ -288,8 +288,8 @@ WeaveSvc.prototype = { if (Svc.Prefs.get("autoconnect") && this.username) { try { - if (yield this.login(self.cb)) - yield this.sync(self.cb, true); + if (yield this.login(self.cb)) + yield this.sync(self.cb, true); } catch (e) {} } self.done(); @@ -388,7 +388,7 @@ WeaveSvc.prototype = { let res = new Resource(this.baseURL + "api/register/chknode/" + username); try { - yield res.get(self.cb); + yield res.get(self.cb); } catch (e) { /* we check status below */ } if (res.lastChannel.responseStatus == 404) { @@ -420,7 +420,7 @@ WeaveSvc.prototype = { yield res.get(self.cb); if (!isLogin) // restore cluster so verifyLogin has no impact - this.clusterURL = cluster; + this.clusterURL = cluster; //Svc.Json.decode(res.data); // throws if not json self.done(true); @@ -447,10 +447,9 @@ WeaveSvc.prototype = { }, verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, password, passphrase) { - this._catchAll( - this._localLock( - this._notify("verify-passphrase", "", this._verifyPassphrase, - username, password, passphrase))).async(this, onComplete); + this._catchAll(this._localLock(this._notify("verify-passphrase", "", + this._verifyPassphrase, username, password, passphrase))). + async(this, onComplete); }, login: function WeaveSvc_login(onComplete, username, password, passphrase) { @@ -488,9 +487,8 @@ WeaveSvc.prototype = { this._loggedIn = true; self.done(true); }; - this._catchAll( - this._localLock( - this._notify("login", "", fn))).async(this, onComplete); + this._catchAll(this._localLock(this._notify("login", "", fn))). + async(this, onComplete); }, logout: function WeaveSvc_logout() { @@ -514,8 +512,8 @@ WeaveSvc.prototype = { let reset = false; this._log.debug("Fetching global metadata record"); - let meta = yield Records.import(self.cb, this.clusterURL + - this.username + "/meta/global"); + let meta = yield Records.import(self.cb, this.clusterURL + this.username + + "/meta/global"); // XXX Bug 482878 Old payloads weren't array-wrapped, so migrate by wiping if (meta && meta.payload == null) { @@ -535,19 +533,19 @@ WeaveSvc.prototype = { // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.lastResource.lastChannel.responseStatus; if (status != 200 && status != 404) { - this._mostRecentError = "Unknown error when downloading metadata."; - this._log.warn("Unknown error while downloading metadata record. " + + this._mostRecentError = "Unknown error when downloading metadata."; + this._log.warn("Unknown error while downloading metadata record. " + "Aborting sync."); - self.done(false); - return; + self.done(false); + return; } if (!meta) - this._log.info("No metadata record, server wipe needed"); + this._log.info("No metadata record, server wipe needed"); if (meta && !meta.payload.syncID) - this._log.warn("No sync id, server wipe needed"); + this._log.warn("No sync id, server wipe needed"); if (Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, remoteVersion) > 0) - this._log.info("Server storage version no longer supported, server wipe needed"); + this._log.info("Server storage version no longer supported, server wipe needed"); reset = true; this._log.info("Wiping server data"); @@ -568,8 +566,8 @@ WeaveSvc.prototype = { return; } else if (meta.payload.syncID != Clients.syncID) { - this._log.warn("Meta.payload.syncID is " + meta.payload.syncID); - this._log.warn(", Clients.syncID is " + Clients.syncID); + this._log.warn("Meta.payload.syncID is " + meta.payload.syncID + + ", Clients.syncID is " + Clients.syncID); yield this.resetClient(self.cb); this._log.info("Reset client because of syncID mismatch."); Clients.syncID = meta.payload.syncID; @@ -588,29 +586,29 @@ WeaveSvc.prototype = { } if (needKeys) { if (PubKeys.lastResource.lastChannel.responseStatus != 404 && - PrivKeys.lastResource.lastChannel.responseStatus != 404) { - this._log.warn("Couldn't download keys from server, aborting sync"); - this._log.debug("PubKey HTTP response status: " + - PubKeys.lastResource.lastChannel.responseStatus); - this._log.debug("PrivKey HTTP response status: " + - PrivKeys.lastResource.lastChannel.responseStatus); - this._mostRecentError = "Can't download keys from server."; - self.done(false); - return; + PrivKeys.lastResource.lastChannel.responseStatus != 404) { + this._log.warn("Couldn't download keys from server, aborting sync"); + this._log.debug("PubKey HTTP response status: " + + PubKeys.lastResource.lastChannel.responseStatus); + this._log.debug("PrivKey HTTP response status: " + + PrivKeys.lastResource.lastChannel.responseStatus); + this._mostRecentError = "Can't download keys from server."; + self.done(false); + return; } if (!this._keyGenEnabled) { - this._log.warn("Couldn't download keys from server, and key generation" + - "is disabled. Aborting sync"); - this._mostRecentError = "No keys. Try syncing from desktop first."; - self.done(false); - return; + this._log.warn("Couldn't download keys from server, and key generation" + + "is disabled. Aborting sync"); + this._mostRecentError = "No keys. Try syncing from desktop first."; + self.done(false); + return; } if (!reset) { - this._log.warn("Calling freshStart from !reset case."); - yield this._freshStart.async(this, self.cb); - this._log.info("Server data wiped to ensure consistency due to missing keys"); + this._log.warn("Calling freshStart from !reset case."); + yield this._freshStart.async(this, self.cb); + this._log.info("Server data wiped to ensure consistency due to missing keys"); } let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb); @@ -618,16 +616,16 @@ WeaveSvc.prototype = { let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, PrivKeys.defaultKeyUri); try { - yield PubKeys.uploadKeypair(self.cb, keys); + yield PubKeys.uploadKeypair(self.cb, keys); ret = true; } catch (e) { - this._mostRecentError = "Could not upload keys."; + this._mostRecentError = "Could not upload keys."; this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); - // FIXME no lastRequest anymore + // FIXME no lastRequest anymore //this._log.error(keys.pubkey.lastRequest.responseText); } } else { - this._mostRecentError = "Could not get encryption passphrase."; + this._mostRecentError = "Could not get encryption passphrase."; this._log.warn("Could not get encryption passphrase"); } } @@ -809,12 +807,12 @@ WeaveSvc.prototype = { try { yield engine.sync(self.cb); if (!this.cancelRequested) - self.done(true); + self.done(true); } catch(e) { this._syncError = true; if (FaultTolerance.Service.onException(e)) - self.done(true); + self.done(true); } }, From d69dcc1750d6f0e1cc92cf0a6162df14cbabf774 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Mar 2009 15:01:04 -0500 Subject: [PATCH 1000/1860] Start sync timer after manually logging in --- services/sync/modules/service.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bc1b6d515d36..9fb4f38d70a5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -484,7 +484,10 @@ WeaveSvc.prototype = { throw "Login failed"; } + // Try starting the sync timer now that we're logged in this._loggedIn = true; + this._checkSync(); + self.done(true); }; this._catchAll(this._localLock(this._notify("login", "", fn))). From c19bc431d34b18e0bac443a238d62a835d9291e0 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Mar 2009 15:41:46 -0500 Subject: [PATCH 1001/1860] Bug 483037 - Need to wrap sync callback for timer callback - scheduled syncs don't work --- services/sync/modules/service.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9fb4f38d70a5..3de33283b9dd 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -241,7 +241,7 @@ WeaveSvc.prototype = { return ok; }, - onWindowOpened: function Weave__onWindowOpened() { + onWindowOpened: function WeaveSvc__onWindowOpened() { }, // one-time initialization like setting up observers and the like @@ -649,7 +649,7 @@ WeaveSvc.prototype = { * * @return Reason for not syncing; not-truthy if sync should run */ - _checkSync: function Weave__checkSync() { + _checkSync: function WeaveSvc__checkSync() { let reason = ""; if (!this.enabled) reason = kSyncWeaveDisabled; @@ -675,7 +675,8 @@ WeaveSvc.prototype = { // We're good to sync, so schedule a repeating sync if we haven't yet else if (!this._syncTimer) { this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let listener = new Utils.EventListener(Utils.bind2(this, this.sync)); + let listener = new Utils.EventListener(Utils.bind2(this, + function WeaveSvc__checkSyncCallback(timer) this.sync(null, false))); this._syncTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK); this._log.config("Weave scheduler enabled"); From f1b0989272d820e57a24191dd2fa34ad2cd09960 Mon Sep 17 00:00:00 2001 From: Date: Thu, 12 Mar 2009 15:54:26 -0700 Subject: [PATCH 1002/1860] The remote-tabs menu (on both firefox and fennec) no longer shows tabs you already have open locally. Fixes bug 480405. --- services/sync/modules/engines/tabs.js | 58 +++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 629e8fa8ef31..540581d86285 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -78,6 +78,48 @@ TabEngine.prototype = { let self = yield; this.resetLastSync(); this._store.wipe(); + }, + + /* The intent is not to show tabs in the menu if they're already + * open locally. There are a couple ways to interpret this: for + * instance, we could do it by removing a tab from the list when + * you open it -- but then if you close it, you can't get back to + * it. So the way I'm doing it here is to not show a tab in the menu + * if you have a tab open to the same URL, even though this means + * that as soon as you navigate anywhere, the original tab will + * reappear in the menu. + */ + locallyOpenTabMatchesURL: function TabEngine_localTabMatches(url) { + // url should be string, not object + /* Some code duplication from _addFirefoxTabsToRecord and + * _addFennecTabsToRecord. Unify? */ + if (Cc["@mozilla.org/browser/sessionstore;1"]) { + let state = this._store._sessionStore.getBrowserState(); + let session = this._store._json.decode(state); + for (let i = 0; i < session.windows.length; i++) { + let window = session.windows[i]; + for (let j = 0; j < window.tabs.length; j++) { + let tab = window.tabs[j]; + if (tab.entries.length > 0) { + let tabUrl = tab.entries[tab.entries.length-1].url; + if (tabUrl == url) { + return true; + } + } + } + } + } else { + let wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + let browserWindow = wm.getMostRecentWindow("navigator:browser"); + for each (let tab in browserWindow.Browser._tabs ) { + let tabUrl = tab.browser.contentWindow.location.toString(); + if (tabUrl == url) { + return true; + } + } + } + return false; } }; @@ -172,9 +214,10 @@ TabStore.prototype = { let session = this._json.decode(this._sessionStore.getBrowserState()); for (let i = 0; i < session.windows.length; i++) { let window = session.windows[i]; - /* For some reason, session store uses one-based array index references, - (f.e. in the "selectedWindow" and each tab's "index" properties), so we - convert them to and from JavaScript's zero-based indexes as needed. */ + /* For some reason, session store uses one-based array index + references, (f.e. in the "selectedWindow" and each tab's + "index" properties), so we convert them to and from + JavaScript's zero-based indexes as needed. */ let windowID = i + 1; for (let j = 0; j < window.tabs.length; j++) { @@ -184,9 +227,10 @@ TabStore.prototype = { if (tab.entries.length == 0) continue; let currentPage = tab.entries[tab.entries.length - 1]; - /* TODO not always accurate -- if you've hit Back in this tab, then the current - * page might not be the last entry. Deal with this later. - */ + /* TODO not always accurate -- if you've hit Back in this tab, + * then the current page might not be the last entry. Deal + * with this later. + */ this._log.debug("Wrapping a tab with title " + currentPage.title); let urlHistory = []; // Include URLs in reverse order; max out at 10, and skip nulls. @@ -210,8 +254,8 @@ TabStore.prototype = { let title = tab.browser.contentDocument.title; let url = tab.browser.contentWindow.location.toString(); let urlHistory = [url]; + this._log.debug("Wrapping a tab with title " + title); // TODO how to get older entries in urlHistory? - dump("Making tab with title = " + title + ", url = " + url + "\n"); record.addTab(title, urlHistory); } }, From 234c184ef05870c9e7decc531301bf44bae394b9 Mon Sep 17 00:00:00 2001 From: Date: Thu, 12 Mar 2009 17:06:41 -0700 Subject: [PATCH 1003/1860] Fennec remote-tab browser now has buttons to sort by date, name, or source client. This mostly fixes bug 481326, but I need to add more metadata to tab records before I can make sort-by-date work. --- services/sync/services-sync.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index c4b69cf15aa6..43fe5fa35958 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -35,4 +35,6 @@ pref("extensions.weave.log.logger.engine.history", "Debug"); pref("extensions.weave.log.logger.engine.tabs", "Debug"); pref("extensions.weave.log.logger.engine.clients", "Debug"); -pref("extensions.weave.network.numRetries", 2); \ No newline at end of file +pref("extensions.weave.network.numRetries", 2); + +pref("extensions.weave.tabs.sortMode", "recency"); \ No newline at end of file From ecbe623cad521cce7b5d311d4d6cebf81d2c7152 Mon Sep 17 00:00:00 2001 From: Date: Mon, 16 Mar 2009 16:49:56 -0700 Subject: [PATCH 1004/1860] If you click the button to show synced tabs before Weave has been configured, it will now take you straight to the sign-in screen. Fixes bug 482818. --- services/sync/modules/engines/tabs.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 540581d86285..b65efbace7f3 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -241,6 +241,7 @@ TabStore.prototype = { if (urlHistory.length >= 10) break; } + // TODO add last-visited date for this tab... but how? record.addTab(currentPage.title, urlHistory); } } @@ -256,7 +257,9 @@ TabStore.prototype = { let urlHistory = [url]; this._log.debug("Wrapping a tab with title " + title); // TODO how to get older entries in urlHistory? + // can we use BrowserUI._getHistory somehow? record.addTab(title, urlHistory); + // TODO add last-visited date for this tab... but how? } }, @@ -336,6 +339,10 @@ TabStore.prototype = { }; +/* TODO let's have TabTracker keep track of open/close switch events + * and maintain most-recently used date of each tab... + */ + function TabTracker() { this._TabTracker_init(); } From 98690ef27e1bddde49b7a60fd2ed0c92552b22f0 Mon Sep 17 00:00:00 2001 From: Date: Mon, 16 Mar 2009 18:31:37 -0700 Subject: [PATCH 1005/1860] Moved al the user-visible strings out of fennec-preferences.xul into chrome/locale/en-US/preferences.dtd. This half-fixes 483076. --- services/sync/locales/en-US/preferences.dtd | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 647f420a5f3e..cd79f33153ec 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -105,3 +105,10 @@ + + + + + + + \ No newline at end of file From fc7834ec47de68c7881e5b25e7e5b469a313374a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 16 Mar 2009 19:37:30 -0700 Subject: [PATCH 1006/1860] Cache unwrapped symmetric keys --- services/sync/modules/base_records/crypto.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index a33eab48f57e..a04109770a2e 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -149,6 +149,7 @@ CryptoMeta.prototype = { _logName: "Record.CryptoMeta", _CryptoMeta_init: function CryptoMeta_init(uri) { + dump("ASDASD\n\n"); this._WBORec_init(uri); this.data.payload = { bulkIV: null, @@ -167,6 +168,12 @@ CryptoMeta.prototype = { let self = yield; let wrapped_key; + // We cache the unwrapped key, as it's expensive to generate + if (this._unwrappedKey) { + self.done(this._unwrappedKey); + return; + } + // get the uri to our public key let pubkeyUri = privkey.publicKeyUri.spec; @@ -178,9 +185,10 @@ CryptoMeta.prototype = { if (!wrapped_key) throw "keyring doesn't contain a key for " + pubkeyUri; - let ret = Svc.Crypto.unwrapSymmetricKey(wrapped_key, privkey.keyData, - passphrase, privkey.salt, privkey.iv); - self.done(ret); + this._unwrappedKey = + Svc.Crypto.unwrapSymmetricKey(wrapped_key, privkey.keyData, passphrase, + privkey.salt, privkey.iv); + self.done(this._unwrappedKey); }, getKey: function CryptoMeta_getKey(onComplete, privkey, passphrase) { this._getKey.async(this, onComplete, privkey, passphrase); From 202f74838ce1b9b805b87a05fe786ac11309bf6f Mon Sep 17 00:00:00 2001 From: Date: Tue, 17 Mar 2009 17:55:27 -0700 Subject: [PATCH 1007/1860] Fixed entity typos that were stopping fennec chrome from overlaying correctly --- services/sync/locales/en-US/preferences.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index cd79f33153ec..6dc3133742ae 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -105,7 +105,7 @@ - + From 84d9d6495d6aeff4e081fa8849da5fd75fd2a1a7 Mon Sep 17 00:00:00 2001 From: Date: Tue, 17 Mar 2009 17:57:53 -0700 Subject: [PATCH 1008/1860] TabTracker now keeps track of when each tab was most recently used; tabStore wraps this data for sync, and fennec UI uses it to sort incoming tabs by most-recently-used date. This fixes 481326. --- services/sync/modules/engines/tabs.js | 162 +++++++++++++++------ services/sync/modules/type_records/tabs.js | 6 +- 2 files changed, 121 insertions(+), 47 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index b65efbace7f3..0b13deebc68f 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -41,6 +41,8 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +const TAB_TIME_ATTR = "weave.tabEngine.lastUsed.timeStamp"; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); @@ -190,6 +192,13 @@ TabStore.prototype = { return this._sessionStore; }, + get _windowMediator() { + let wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + this.__defineGetter__("_windowMediator", function() { return wm;}); + return this._windowMediator; + }, + get _json() { let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); this.__defineGetter__("_json", function() {return json;}); @@ -211,38 +220,46 @@ TabStore.prototype = { }, _addFirefoxTabsToRecord: function TabStore__addFirefoxTabs(record) { - let session = this._json.decode(this._sessionStore.getBrowserState()); - for (let i = 0; i < session.windows.length; i++) { - let window = session.windows[i]; - /* For some reason, session store uses one-based array index - references, (f.e. in the "selectedWindow" and each tab's - "index" properties), so we convert them to and from - JavaScript's zero-based indexes as needed. */ - let windowID = i + 1; - - for (let j = 0; j < window.tabs.length; j++) { - let tab = window.tabs[j]; - //this._sessionStore.getTabState(tab)); + // Iterate through each tab of each window + let enumerator = this._windowMediator.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + let window = enumerator.getNext(); + let tabContainer = window.getBrowser().tabContainer; + for each (let tabChild in tabContainer.childNodes) { + if (!tabChild.QueryInterface) + continue; + let tab = tabChild.QueryInterface(Ci.nsIDOMNode); + if (!tab) + continue; + let tabState = this._json.decode(this._sessionStore.getTabState(tab)); // Skip empty (i.e. just-opened, no history yet) tabs: - if (tab.entries.length == 0) + if (tabState.entries.length == 0) continue; - let currentPage = tab.entries[tab.entries.length - 1]; + + // Get the time the tab was last used + let lastUsedTimestamp = tab.getAttribute(TAB_TIME_ATTR); + + // Get title of current page + let currentPage = tabState.entries[tabState.entries.length - 1]; /* TODO not always accurate -- if you've hit Back in this tab, * then the current page might not be the last entry. Deal - * with this later. - */ - this._log.debug("Wrapping a tab with title " + currentPage.title); + * with this later. */ + + // Get url history let urlHistory = []; // Include URLs in reverse order; max out at 10, and skip nulls. - for (let i = tab.entries.length -1; i >= 0; i--) { - let entry = tab.entries[i]; + for (let i = tabState.entries.length -1; i >= 0; i--) { + let entry = tabState.entries[i]; if (entry && entry.url) urlHistory.push(entry.url); if (urlHistory.length >= 10) break; } - // TODO add last-visited date for this tab... but how? - record.addTab(currentPage.title, urlHistory); + + // add tab to record + this._log.debug("Wrapping a tab with title " + currentPage.title); + this._log.debug("And timestamp " + lastUsedTimestamp); + record.addTab(currentPage.title, urlHistory, lastUsedTimestamp); } } }, @@ -339,9 +356,6 @@ TabStore.prototype = { }; -/* TODO let's have TabTracker keep track of open/close switch events - * and maintain most-recently used date of each tab... - */ function TabTracker() { this._TabTracker_init(); @@ -356,41 +370,96 @@ TabTracker.prototype = { _TabTracker_init: function TabTracker__init() { this._init(); - // Register me as an observer!! Listen for tabs opening and closing: - // TODO We need to also register with any windows that are ALREDY - // open. On Fennec maybe try to get this from getBrowser(), which is - // defined differently but should still exist... + // TODO Figure out how this will work on Fennec. + + // Register as an observer so we can catch windows opening and closing: var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] .getService(Ci.nsIWindowWatcher); ww.registerNotification(this); + + /* Also directly register the listeners for any browser window alread + * open: */ + let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + let enumerator = wm.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + this._registerListenersForWindow(enumerator.getNext()); + } + }, + + _registerListenersForWindow: function TabTracker__registerListen(window) { + if (! window.getBrowser) { + return; + } + let browser = window.getBrowser(); + if (! browser.tabContainer) { + return; + } + //this._log.trace("Registering tab listeners in new window.\n"); + let container = browser.tabContainer; + container.addEventListener("TabOpen", this.onTabOpened, false); + container.addEventListener("TabClose", this.onTabClosed, false); + container.addEventListener("TabSelect", this.onTabSelected, false); + }, + + _unRegisterListenersForWindow: function TabTracker__unregister(window) { + if (! window.getBrowser) { + return; + } + let browser = window.getBrowser(); + if (! browser.tabContainer) { + return; + } + let container = browser.tabContainer; + container.removeEventListener("TabOpen", this.onTabOpened, false); + container.removeEventListener("TabClose", this.onTabClosed, false); + container.removeEventListener("TabSelect", this.onTabSelected, false); }, observe: function TabTracker_observe(aSubject, aTopic, aData) { - this._log.trace("Spotted window open/close"); + /* Called when a window opens or closes. Make sure that every + * window has the appropriate listeners registered. */ let window = aSubject.QueryInterface(Ci.nsIDOMWindow); - // Ignore windows that don't have tabContainers. - // TODO: Fennec windows don't have tabContainers, but we still want - // to register an observer in them. - if (! window.getBrowser) - return; - let browser = window.getBrowser(); - if (! browser.tabContainer) - return; - let container = browser.tabContainer; + // TODO figure out how this will work in Fennec. if (aTopic == "domwindowopened") { - container.addEventListener("TabOpen", this.onTabChanged, false); - container.addEventListener("TabClose", this.onTabChanged, false); + this._registerListenersForWindow(window); } else if (aTopic == "domwindowclosed") { - container.removeEventListener("TabOpen", this.onTabChanged, false); - container.removeEventListener("TabClose", this.onTabChanged, false); + this._unRegisterListenersForWindow(window); } - // TODO }, - onTabChanged: function TabTracker_onTabChanged(event) { - this._score += 10; // meh? meh. + onTabOpened: function TabTracker_onTabOpened(event) { + // Store a timestamp in the tab to track when it was last used + //this._log.trace("Tab opened.\n"); + /*if (Cc["@mozilla.org/browser/sessionstore;1"]) { + let ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore); + ss.setTabValue(event.target, TAB_TIME_ATTR, event.timeStamp); + }*/ + event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); + //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); + this._score += 50; }, + onTabClosed: function TabTracker_onTabSelected(event) { + //this._log.trace("Tab closed.\n"); + this._score += 10; + }, + + onTabSelected: function TabTracker_onTabSelected(event) { + // Update the tab's timestamp + //this._log.trace("Tab selected.\n"); + /*if (Cc["@mozilla.org/browser/sessionstore;1"]) { + let ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore); + ss.setTabValue(event.target, TAB_TIME_ATTR, event.timeStamp); + }*/ + event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); + //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); + this._score += 10; + }, + // TODO: Also listen for tabs loading new content? + get changedIDs() { // The record for my own client is always the only changed record. let obj = {}; @@ -400,6 +469,9 @@ TabTracker.prototype = { // TODO this hard-coded score is a hack; replace with maybe +25 or +35 // per tab open event. + // Actually maybe it should just stay this way? Otherwise we're missing + // something really important which is laod-page-in-tab events, which are + // a powerful motivation to sync get score() { return 100; } diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js index 5ad165d375bd..4b4d1188c1b5 100644 --- a/services/sync/modules/type_records/tabs.js +++ b/services/sync/modules/type_records/tabs.js @@ -63,10 +63,12 @@ TabSetRecord.prototype = { }; }, - addTab: function TabSetRecord_addTab(title, urlHistory) { + addTab: function TabSetRecord_addTab(title, urlHistory, lastUsed) { if (!this.cleartext.tabs) this.cleartext.tabs = []; - this.cleartext.tabs.push( {title: title, urlHistory: urlHistory }); + this.cleartext.tabs.push( {title: title, + urlHistory: urlHistory, + lastUsed: lastUsed}); }, getAllTabs: function TabSetRecord_getAllTabs() { From 8985ca10c1ec0b0bf367d14400450cfd83dd78c8 Mon Sep 17 00:00:00 2001 From: Date: Tue, 17 Mar 2009 18:02:13 -0700 Subject: [PATCH 1009/1860] Removed unused code from tab tracker. --- services/sync/modules/engines/tabs.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0b13deebc68f..e4101c7ea500 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -431,11 +431,6 @@ TabTracker.prototype = { onTabOpened: function TabTracker_onTabOpened(event) { // Store a timestamp in the tab to track when it was last used //this._log.trace("Tab opened.\n"); - /*if (Cc["@mozilla.org/browser/sessionstore;1"]) { - let ss = Cc["@mozilla.org/browser/sessionstore;1"] - .getService(Ci.nsISessionStore); - ss.setTabValue(event.target, TAB_TIME_ATTR, event.timeStamp); - }*/ event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); this._score += 50; @@ -449,11 +444,6 @@ TabTracker.prototype = { onTabSelected: function TabTracker_onTabSelected(event) { // Update the tab's timestamp //this._log.trace("Tab selected.\n"); - /*if (Cc["@mozilla.org/browser/sessionstore;1"]) { - let ss = Cc["@mozilla.org/browser/sessionstore;1"] - .getService(Ci.nsISessionStore); - ss.setTabValue(event.target, TAB_TIME_ATTR, event.timeStamp); - }*/ event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); this._score += 10; @@ -467,11 +457,13 @@ TabTracker.prototype = { return obj; }, - // TODO this hard-coded score is a hack; replace with maybe +25 or +35 - // per tab open event. - // Actually maybe it should just stay this way? Otherwise we're missing - // something really important which is laod-page-in-tab events, which are - // a powerful motivation to sync + /* Score is pegged to 100, which means tabs are always synced. + * Is this the right thing to do? Or should we be using the score + * calculated from tab open/close/select events (see above)? Note that + * we should definitely listen for tabs loading new content if we want to + * go that way. But tabs loading new content happens so often that it + * might be easier to just always sync. + */ get score() { return 100; } From a8685c8bc359362acf8cd8e26e8d44c764826628 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 18 Mar 2009 12:09:26 -0700 Subject: [PATCH 1010/1860] remove accidental debugging spew --- services/sync/modules/base_records/crypto.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index a04109770a2e..9eb3c49f6690 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -149,7 +149,6 @@ CryptoMeta.prototype = { _logName: "Record.CryptoMeta", _CryptoMeta_init: function CryptoMeta_init(uri) { - dump("ASDASD\n\n"); this._WBORec_init(uri); this.data.payload = { bulkIV: null, From 85097cdb94485acab88e7d5db85b4c686d2955f9 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Mar 2009 12:29:14 -0700 Subject: [PATCH 1011/1860] Moved Fennec's dynamic strings to locale/fennec.properties for easier localization. Fixes bug 483076. --- services/sync/locales/en-US/fennec.properties | 33 +++++++++++++++++++ services/sync/locales/en-US/preferences.dtd | 6 +++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 services/sync/locales/en-US/fennec.properties diff --git a/services/sync/locales/en-US/fennec.properties b/services/sync/locales/en-US/fennec.properties new file mode 100644 index 000000000000..51c90e0723c4 --- /dev/null +++ b/services/sync/locales/en-US/fennec.properties @@ -0,0 +1,33 @@ + +fennec.logging-in = Weave is trying to log in... +fennec.logged-in = Weave is logged in. +fennec.turned-off = Weave is turned off. +fennec.login.error = Weave had an error when trying to log in. +fennec.login.error.detail = Login error: %S + +fennec.default.client.name = Fennec +fennec.default.client.type = Mobile + +fennec.sync.complete.time = Sync completed at %S, %S +fennec.sync.error.detail = Error: %S +fennec.sync.error.generic = Weave had an error when trying to sync. +fennec.sync.status = Syncing %S... +fennec.sync.status.detail = Syncing (%S %S)... + +fennec.username.here = Your Username Here +fennec.password.here = Your Password Here +fennec.passphrase.here = Your Passphrase Here + +fennec.turn.weave.off = Turn Weave Off +fennec.turn.weave.on = Turn Weave On + +fennec.username.is = You are user: %S +fenec.no.username = No username set. + +fennec.need.credentials = Weave needs more info from you to get started. +fennec.need.username = You must enter a Weave username. +fennec.need.password = You must enter a Weave password. +fennec.need.passphrase = You must enter a Weave passphrase. + +fennec.quitting = Cannot sync, Weave is quitting. +fennec.no.sync = Cannot sync, Weave is not logged in. \ No newline at end of file diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 6dc3133742ae..a2b161088b37 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -111,4 +111,8 @@ - \ No newline at end of file + + + + + \ No newline at end of file From 5577ad0f4bface0a8ead43c2f828e6cbfaabd6ba Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Mar 2009 12:40:27 -0700 Subject: [PATCH 1012/1860] Experimenting with getting tab last-used-time data on Fennec --- services/sync/modules/engines/tabs.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index e4101c7ea500..e60b69c6e0fe 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -272,10 +272,19 @@ TabStore.prototype = { let title = tab.browser.contentDocument.title; let url = tab.browser.contentWindow.location.toString(); let urlHistory = [url]; - this._log.debug("Wrapping a tab with title " + title); - // TODO how to get older entries in urlHistory? + + // TODO how to get older entries in urlHistory? without session store? // can we use BrowserUI._getHistory somehow? - record.addTab(title, urlHistory); + + // Get the time the tab was last used + /* let lastUsedTimestamp = tab.getAttribute(TAB_TIME_ATTR); + if (!lastUsedTimestamp) */ + // TODO that doesn't work: tab.getAttribute is not a function on Fennec. + let lastUsedTimestamp = "0"; + + this._log.debug("Wrapping a tab with title " + title); + this._log.debug("And timestamp " + lastUsedTimestamp); + record.addTab(title, urlHistory, lastUsedTimestamp); // TODO add last-visited date for this tab... but how? } }, @@ -396,6 +405,7 @@ TabTracker.prototype = { return; } //this._log.trace("Registering tab listeners in new window.\n"); + dump("Tab listeners registered!\n"); let container = browser.tabContainer; container.addEventListener("TabOpen", this.onTabOpened, false); container.addEventListener("TabClose", this.onTabClosed, false); @@ -431,6 +441,7 @@ TabTracker.prototype = { onTabOpened: function TabTracker_onTabOpened(event) { // Store a timestamp in the tab to track when it was last used //this._log.trace("Tab opened.\n"); + dump("Tab opened.\n"); event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); this._score += 50; @@ -443,6 +454,7 @@ TabTracker.prototype = { onTabSelected: function TabTracker_onTabSelected(event) { // Update the tab's timestamp + dump("Tab selected.\n"); //this._log.trace("Tab selected.\n"); event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); From b3045b96af77fac52851ca5f5213f3749d427f50 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Mar 2009 13:22:40 -0700 Subject: [PATCH 1013/1860] Never wipe the server if key generation is disabled; in case of version mismatch, describe the error and abort. This fixes 482062. --- services/sync/modules/service.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3de33283b9dd..68cc790ffc1b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -550,6 +550,13 @@ WeaveSvc.prototype = { if (Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, remoteVersion) > 0) this._log.info("Server storage version no longer supported, server wipe needed"); + if (!this._keyGenEnabled) { + this._log.info("...and key generation is disabled. Not wiping. " + + "Aborting sync."); + this._mostRecentError = "Weave needs updating on your desktop browser."; + self.done(false); + return; + } reset = true; this._log.info("Wiping server data"); yield this._freshStart.async(this, self.cb); From 20eecc1f9f2ae1f148c9767f31dba7446a7850e1 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Mar 2009 21:07:42 -0700 Subject: [PATCH 1014/1860] Made sure that tabs with undefined title or undefined last used date can't break sort ordering. See bug 481326. --- services/sync/modules/type_records/tabs.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js index 4b4d1188c1b5..187b5ef76f5a 100644 --- a/services/sync/modules/type_records/tabs.js +++ b/services/sync/modules/type_records/tabs.js @@ -66,6 +66,15 @@ TabSetRecord.prototype = { addTab: function TabSetRecord_addTab(title, urlHistory, lastUsed) { if (!this.cleartext.tabs) this.cleartext.tabs = []; + if (!title) { + title = ""; + } + if (!lastUsed) { + lastUsed = 0; + } + if (!urlHistory || urlHistory.length == 0) { + return; + } this.cleartext.tabs.push( {title: title, urlHistory: urlHistory, lastUsed: lastUsed}); From 043faf60b3da2d1c883918bed3ac9eebbb8ee2d9 Mon Sep 17 00:00:00 2001 From: Date: Wed, 18 Mar 2009 21:58:21 -0700 Subject: [PATCH 1015/1860] De-uglified visuals of the sort-by date/title/client buttons in Fennec remote-tab view. See bug 481326. --- services/sync/locales/en-US/preferences.dtd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index a2b161088b37..db3e8af8b4b0 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -113,6 +113,7 @@ - - - \ No newline at end of file + + + + \ No newline at end of file From f337772f897133215fc7d96dabb966ecb4ff4701 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 19 Mar 2009 14:00:57 -0700 Subject: [PATCH 1016/1860] More services cleanup; Expose a Utils.openWindow/openDialog --- services/sync/modules/service.js | 1 - services/sync/modules/util.js | 64 +++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 68cc790ffc1b..812945077e01 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -104,7 +104,6 @@ Cu.import("resource://weave/identity.js", Weave); Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/oauth.js", Weave); -Cu.import("resource://weave/service.js", Weave); // ?? Cu.import("resource://weave/engines/clientData.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 8370d9d6e129..b56f11863569 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -442,6 +442,41 @@ let Utils = { return [stream, file]; }, + /** + * Open/reshow a window/dialog based on its name and type. + * + * @param name + * Name of the window/dialog to reshow if already open + * @param type + * Opening behavior: "Window" or "Dialog" + * @param args + * More arguments go here depending on the type + */ + _openWin: function Utils__openWin(name, type /*, args... */) { + // Just re-show the window if it's already open + let openedWindow = Svc.WinMediator.getMostRecentWindow("Weave:" + name); + if (openedWindow) { + openedWindow.focus(); + return; + } + + // Open up the window/dialog! + let win = Svc.WinWatcher; + if (type == "Dialog") + win = win.activeWindow; + win["open" + type].apply(win, this.slice(arguments, 2)); + }, + + openWindow: function Utils_openWindow(name, uri, options, args) { + this._openWin(name, "Window", null, "chrome://weave/content/" + uri, + "", options || "centerscreen,chrome,dialog,resizable=yes", args); + }, + + openDialog: function Utils_openDialog(name, uri, options, args) { + this._openWin(name, "Dialog", "chrome://weave/content/" + uri, "", + options || "centerscreen,chrome,dialog,modal,resizable=yes", args); + }, + // assumes an nsIConverterInputStream readStream: function Weave_readStream(is) { let ret = "", str = {}; @@ -451,6 +486,14 @@ let Utils = { return ret; }, + slice: function Utils_slice(array, start, end) { + let args = [start]; + if (end !== undefined) + args.push(end); + + return Array.prototype.slice.apply(array, args); + }, + bind2: function Async_bind2(object, method) { return function innerBind() { return method.apply(object, arguments); }; }, @@ -503,12 +546,15 @@ Utils.EventListener.prototype = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); -Utils.lazySvc(Svc, 'Crypto', "@labs.mozilla.com/Weave/Crypto;1", Ci.IWeaveCrypto); -Utils.lazySvc(Svc, 'Directory', "@mozilla.org/file/directory_service;1", Ci.nsIProperties); -Utils.lazySvc(Svc, 'IO', "@mozilla.org/network/io-service;1", Ci.nsIIOService); -Utils.lazySvc(Svc, 'Login', "@mozilla.org/login-manager;1", Ci.nsILoginManager); -Utils.lazySvc(Svc, 'Memory', "@mozilla.org/xpcom/memory-service;1", Ci.nsIMemory); -Utils.lazySvc(Svc, 'Observer', "@mozilla.org/observer-service;1", Ci.nsIObserverService); -Utils.lazySvc(Svc, 'Private', "@mozilla.org/privatebrowsing;1", Ci.nsIPrivateBrowsingService); -Utils.lazySvc(Svc, 'Version', - "@mozilla.org/xpcom/version-comparator;1", Ci.nsIVersionComparator); +[["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], + ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], + ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], + ["Login", "@mozilla.org/login-manager;1", "nsILoginManager"], + ["Memory", "@mozilla.org/xpcom/memory-service;1", "nsIMemory"], + ["Observer", "@mozilla.org/observer-service;1", "nsIObserverService"], + ["Private", "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService"], + ["Prompt", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"], + ["Version", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator"], + ["WinMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"], + ["WinWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"], +].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], Ci[lazy[2]])); From da652b2539e63a1c0f3ca4322413c81856b5bcbc Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 19 Mar 2009 14:02:41 -0700 Subject: [PATCH 1017/1860] :( trailing whitespace --- services/sync/modules/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index b56f11863569..dbbf5635686c 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -468,7 +468,7 @@ let Utils = { }, openWindow: function Utils_openWindow(name, uri, options, args) { - this._openWin(name, "Window", null, "chrome://weave/content/" + uri, + this._openWin(name, "Window", null, "chrome://weave/content/" + uri, "", options || "centerscreen,chrome,dialog,resizable=yes", args); }, From 9060aea0770c07fbe5e815cd89270b45e2dd92d1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 19 Mar 2009 14:21:23 -0700 Subject: [PATCH 1018/1860] More window/dialog cleanup for commonly used dialogs --- services/sync/modules/util.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index dbbf5635686c..5b6e18aa14c2 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -464,19 +464,39 @@ let Utils = { let win = Svc.WinWatcher; if (type == "Dialog") win = win.activeWindow; - win["open" + type].apply(win, this.slice(arguments, 2)); - }, - - openWindow: function Utils_openWindow(name, uri, options, args) { - this._openWin(name, "Window", null, "chrome://weave/content/" + uri, - "", options || "centerscreen,chrome,dialog,resizable=yes", args); + win["open" + type].apply(win, Utils.slice(arguments, 2)); }, openDialog: function Utils_openDialog(name, uri, options, args) { - this._openWin(name, "Dialog", "chrome://weave/content/" + uri, "", + Utils._openWin(name, "Dialog", "chrome://weave/content/" + uri, "", options || "centerscreen,chrome,dialog,modal,resizable=yes", args); }, + openLog: function Utils_openLog() { + Utils.openWindow("Log", "log.xul"); + }, + + openLogin: function Utils_openLogin() { + Utils.openDialog("Login", "login.xul"); + }, + + openShare: function Utils_openShare() { + Utils.openDialog("Share", "share.xul"); + }, + + openStatus: function Utils_openStatus() { + Utils.openDialog("Status", "status.xul"); + }, + + openWindow: function Utils_openWindow(name, uri, options, args) { + Utils._openWin(name, "Window", null, "chrome://weave/content/" + uri, + "", options || "centerscreen,chrome,dialog,resizable=yes", args); + }, + + openWizard: function Utils_openWizard() { + Utils.openDialog("Wizard", "wizard.xul"); + }, + // assumes an nsIConverterInputStream readStream: function Weave_readStream(is) { let ret = "", str = {}; From 15122c043474d29035beac3de02e438d907d6ddd Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Thu, 19 Mar 2009 23:55:55 -0700 Subject: [PATCH 1019/1860] Bug 479341 - make WeaveCrypto::GenerateKeypair() call PK11_GenerateKeyPairWithOpFlags(). bacon=thunder --- services/crypto/WeaveCrypto.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index fd50fa87edf3..84714c8b02c1 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -396,6 +396,12 @@ WeaveCrypto::GenerateKeypair(const nsACString& aPassphrase, SECKEYPublicKey *pubKey = nsnull; PK11SlotInfo *slot = nsnull; PK11RSAGenParams rsaParams; + // Attributes for the private key. We're just going to wrap and extract the + // value, so they're not critical. The _PUBLIC attribute just indicates the + // object can be accessed without being logged into the token. + PK11AttrFlags attrFlags = (PK11_ATTR_SESSION | + PK11_ATTR_PUBLIC | + PK11_ATTR_SENSITIVE); rsaParams.keySizeInBits = mKeypairBits; // 1024, 2048, etc. @@ -409,16 +415,10 @@ WeaveCrypto::GenerateKeypair(const nsACString& aPassphrase, } // Generate the keypair. - // XXX isSensitive sets PK11_ATTR_SENSITIVE | PK11_ATTR_PRIVATE - // Might want to use PK11_GenerateKeyPairWithFlags and not set - // CKA_PRIVATE, since that may trigger a master password entry, which is - // kind of pointless for session objects... - privKey = PK11_GenerateKeyPair(slot, + privKey = PK11_GenerateKeyPairWithFlags(slot, CKM_RSA_PKCS_KEY_PAIR_GEN, &rsaParams, &pubKey, - PR_FALSE, // isPerm - PR_TRUE, // isSensitive - nsnull); // wincx + attrFlags, nsnull); if (!privKey) { NS_WARNING("PK11_GenerateKeyPair failed"); From b9ac3bfd09758c94566f4ba6ea025446a2f420cb Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 20 Mar 2009 00:11:04 -0700 Subject: [PATCH 1020/1860] Bug 484144 - Make sync status dialog not modal. r=thunder --- services/sync/locales/en-US/status.properties | 2 ++ services/sync/modules/util.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index 5ba2b500178e..6a9ee2b82a7d 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -1,3 +1,5 @@ +dialog.accept = Hide + status.wait = Waiting for Current Sync to Finish status.active = Syncing with Weave status.success = Sync Complete diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 5b6e18aa14c2..7d1f16694433 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -485,7 +485,7 @@ let Utils = { }, openStatus: function Utils_openStatus() { - Utils.openDialog("Status", "status.xul"); + Utils.openWindow("Status", "status.xul"); }, openWindow: function Utils_openWindow(name, uri, options, args) { From c6d6a07f822b5cf6978f54b8ec0594b42ad0a36a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 20 Mar 2009 00:13:16 -0700 Subject: [PATCH 1021/1860] Bug 482898 - Allow an optional array of engines for wipeRemote, wipeServer, resetClient, etc.. r=thunder --- services/sync/modules/engines.js | 19 +++++++- services/sync/modules/service.js | 83 +++++++++++++++++++++----------- 2 files changed, 72 insertions(+), 30 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5a18db6ad292..b93ae58eb79a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -67,10 +67,27 @@ Utils.lazy(this, 'Engines', EngineManagerSvc); function EngineManagerSvc() { this._engines = {}; + this._log = Log4Moz.repository.getLogger("Service.Engines"); + this._log.level = Log4Moz.Level[Svc.Prefs.get( + "log.logger.service.engines", "Debug")]; } EngineManagerSvc.prototype = { get: function EngMgr_get(name) { - return this._engines[name]; + // Return an array of engines if we have an array of names + if (name.constructor.name == "Array") { + let engines = []; + name.forEach(function(name) { + let engine = this.get(name); + if (engine) + engines.push(engine); + }, this); + return engines; + } + + let engine = this._engines[name]; + if (!engine) + this._log.debug("Could not get engine: " + name); + return engine; }, getAll: function EngMgr_getAll() { let ret = []; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 812945077e01..e498235f07c5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -847,8 +847,10 @@ WeaveSvc.prototype = { * * @param onComplete * Callback when this method completes + * @param engines [optional] + * Array of engine names to wipe. If not given, all engines are used. */ - wipeServer: function WeaveSvc_wipeServer(onComplete) { + wipeServer: function WeaveSvc_wipeServer(onComplete, engines) { let fn = function WeaveSvc__wipeServer() { let self = yield; @@ -861,6 +863,10 @@ WeaveSvc.prototype = { let allCollections = Svc.Json.decode(res.data); for each (let name in allCollections) { try { + // If we have a list of engines, make sure it's one we want + if (engines && engines.indexOf(name) == -1) + continue; + yield new Resource(userURL + name).delete(self.cb); } catch(ex) { @@ -876,16 +882,26 @@ WeaveSvc.prototype = { * * @param onComplete * Callback when this method completes + * @param engines [optional] + * Array of engine names to wipe. If not given, all engines are used. */ - wipeClient: function WeaveSvc_wipeClient(onComplete) { + wipeClient: function WeaveSvc_wipeClient(onComplete, engines) { let fn = function WeaveSvc__wipeClient() { let self = yield; - // Clear out any service data - yield this.resetService(self.cb); + // If we don't have any engines, reset the service and wipe all engines + if (!engines) { + // Clear out any service data + yield this.resetService(self.cb); + + engines = [Clients].concat(Engines.getAll()); + } + // Convert the array of names into engines + else + engines = Engines.get(engines); // Fully wipe each engine - for each (let engine in [Clients].concat(Engines.getAll())) + for each (let engine in engines) yield engine.wipeClient(self.cb); }; this._catchAll(this._notify("wipe-client", "", fn)).async(this, onComplete); @@ -897,13 +913,21 @@ WeaveSvc.prototype = { * * @param onComplete * Callback when this method completes + * @param engines [optional] + * Array of engine names to wipe. If not given, all engines are used. */ - wipeRemote: function WeaveSvc_wipeRemote(onComplete) { + wipeRemote: function WeaveSvc_wipeRemote(onComplete, engines) { let fn = function WeaveSvc__wipeRemote() { let self = yield; // Clear out any server data - //yield this.wipeServer(self.cb); + //yield this.wipeServer(self.cb, engines); + + // Only wipe the engines provided + if (engines) { + engines.forEach(function(e) this.prepCommand("wipeEngine", [e]), this); + return; + } // Tell the remote machines to wipe themselves this.prepCommand("wipeAll", []); @@ -939,16 +963,26 @@ WeaveSvc.prototype = { * * @param onComplete * Callback when this method completes + * @param engines [optional] + * Array of engine names to reset. If not given, all engines are used. */ - resetClient: function WeaveSvc_resetClient(onComplete) { + resetClient: function WeaveSvc_resetClient(onComplete, engines) { let fn = function WeaveSvc__resetClient() { let self = yield; - // Clear out any service data - yield this.resetService(self.cb); + // If we don't have any engines, reset everything including the service + if (!engines) { + // Clear out any service data + yield this.resetService(self.cb); + + engines = [Clients].concat(Engines.getAll()); + } + // Convert the array of names into engines + else + engines = Engines.get(engines); // Have each engine drop any temporary meta data - for each (let engine in [Clients].concat(Engines.getAll())) + for each (let engine in engines) yield engine.resetClient(self.cb); // XXX Bug 480448: Delete any snapshots from old code @@ -1004,31 +1038,22 @@ WeaveSvc.prototype = { for each ({command: command, args: args} in commands) { this._log.debug("Processing command: " + command + "(" + args + ")"); + let engines = [args[0]]; switch (command) { case "resetAll": - yield this.resetClient(self.cb); + engines = null; + // Fallthrough + case "resetEngine": + yield this.resetClient(self.cb, engines); break; - case "resetEngine": { - let engine = Engines.get(args[0]); - if (engine != null) - yield engine.resetClient(self.cb); - else - this._log.debug("Cannot reset an unknown engine: " + args[0]); - break; - } case "wipeAll": - yield this.wipeClient(self.cb); + engines = null; + // Fallthrough + case "wipeEngine": + yield this.wipeClient(self.cb, engines); break; - case "wipeEngine": { - let engine = Engines.get(args[0]); - if (engine != null) - yield engine.wipeClient(self.cb); - else - this._log.debug("Cannot wipe an unknown engine: " + args[0]); - break; - } default: this._log.debug("Received an unknown command: " + command); break; From 71402d7cb4b4c167a89ed78c38c37a2ccb59eb05 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 20 Mar 2009 00:14:21 -0700 Subject: [PATCH 1022/1860] bug 476539: use a XULRunner-based test harness for unit testing; r=thunder --- services/crypto/Makefile | 10 +- services/sync/modules/crypto.js | 261 ------------------ services/sync/tests/unit/Makefile | 49 ++++ services/sync/tests/unit/head_first.js | 8 +- services/sync/tests/unit/head_http_server.js | 2 +- services/sync/tests/unit/test_auth_manager.js | 2 +- .../sync/tests/unit/test_records_crypto.js | 4 +- services/sync/tests/unit/test_resource.js | 2 +- 8 files changed, 57 insertions(+), 281 deletions(-) delete mode 100644 services/sync/modules/crypto.js diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 3984ae413601..1f00d7cdc02b 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -254,7 +254,7 @@ endif ###################################################################### -.PHONY: all build install test-install clean +.PHONY: all build install clean all: build # default target @@ -266,14 +266,6 @@ install: build cp $(idl_typelib) $(destdir)/components cp $(so_target) $(platformdir)/components -# gross hack to get around component registration for xpcshell tests -test-install: install - ln -sf `pwd`/$(destdir)/defaults/preferences/sync.js $(sdkdir)/bin/defaults/pref/sync.js # fixme!! - ln -sf `pwd`/$(destdir)/components/$(idl_typelib) $(sdkdir)/bin/components/$(idl_typelib) - ln -sf `pwd`/$(platformdir)/components/$(so_target) $(sdkdir)/bin/components/$(so_target) - rm -f $(sdkdir)/bin/components/compreg.dat - rm -f $(sdkdir)/bin/components/xpti.dat - clean: rm -f $(so_target) $(so_target_i386) $(so_target_ppc) \ $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ diff --git a/services/sync/modules/crypto.js b/services/sync/modules/crypto.js deleted file mode 100644 index 778a347230aa..000000000000 --- a/services/sync/modules/crypto.js +++ /dev/null @@ -1,261 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * Justin Dolske - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['Crypto']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; - -Utils.lazy(this, 'Crypto', CryptoSvc); - -function CryptoSvc() { - this._init(); -} -CryptoSvc.prototype = { - _logName: "Crypto", - - get defaultAlgorithm() { - return Utils.prefs.getCharPref("encryption"); - }, - set defaultAlgorithm(value) { - if (value != Utils.prefs.getCharPref("encryption")) - Utils.prefs.setCharPref("encryption", value); - }, - - _init: function Crypto__init() { - this._log = Log4Moz.repository.getLogger("Service." + this._logName); - this._log.level = - Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.crypto")]; - let branch = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch2); - branch.addObserver("extensions.weave.encryption", this, false); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), - - // nsIObserver - - observe: function Sync_observe(subject, topic, data) { - switch (topic) { - case "extensions.weave.encryption": { - if (Utils.pref.getCharPref("encryption") == data) - return; - - switch (data) { - case "none": - this._log.info("Encryption disabled"); - break; - - default: - this._log.warn("Unknown encryption algorithm, resetting"); - branch.clearUserPref("extensions.weave.encryption"); - return; // otherwise we'll send the alg changed event twice - } - // FIXME: listen to this bad boy somewhere - Svc.Observer.notifyObservers(null, "weave:encryption:algorithm-changed", ""); - } break; - default: - this._log.warn("Unknown encryption preference changed - ignoring"); - } - }, - - checkModule: function Crypto_checkModule() { - let ok = false; - - try { - let svc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - createInstance(Ci.IWeaveCrypto); - let iv = svc.generateRandomIV(); - if (iv.length == 24) - ok = true; - - } catch (e) {} - - return ok; - }, - - // Crypto - - encryptData: function Crypto_encryptData(data, identity) { - let self = yield; - let ret; - - this._log.trace("encrypt called. [id=" + identity.realm + "]"); - - if ("none" == this.defaultAlgorithm) { - this._log.debug("NOT encrypting data"); - ret = data; - } else { - let symkey = identity.bulkKey; - let iv = identity.bulkIV; - ret = Svc.Crypto.encrypt(data, symkey, iv); - } - - self.done(ret); - }, - - decryptData: function Crypto_decryptData(data, identity) { - let self = yield; - let ret; - - this._log.trace("decrypt called. [id=" + identity.realm + "]"); - - if ("none" == this.defaultAlgorithm) { - this._log.debug("NOT decrypting data"); - ret = data; - } else { - let symkey = identity.bulkKey; - let iv = identity.bulkIV; - ret = Svc.Crypto.decrypt(data, symkey, iv); - } - - self.done(ret); - }, - - /* - * randomKeyGen - * - * Generates a random symmetric key and IV, and puts them in the specified - * identity. - */ - randomKeyGen: function Crypto_randomKeyGen(identity) { - let self = yield; - - this._log.trace("randomKeyGen called. [id=" + identity.realm + "]"); - - let symkey = Svc.Crypto.generateRandomKey(); - let iv = Svc.Crypto.generateRandomIV(); - - identity.bulkKey = symkey; - identity.bulkIV = iv; - }, - - /* - * RSAkeygen - * - * Generates a new RSA keypair, as well as the salt/IV used for protecting - * the private key, and puts them in the specified identity. - */ - RSAkeygen: function Crypto_RSAkeygen(identity) { - let self = yield; - - this._log.trace("RSAkeygen called. [id=" + identity.realm + "]"); - let privOut = {}; - let pubOut = {}; - - // Generate a blob of random data for salting the passphrase used to - // encrypt the private key. - let salt = Svc.Crypto.generateRandomBytes(32); - let iv = Svc.Crypto.generateRandomIV(); - - Svc.Crypto.generateKeypair(identity.password, salt, iv, pubOut, privOut); - - identity.keypairAlg = "RSA"; - identity.pubkey = pubOut.value; - identity.privkey = privOut.value; - identity.passphraseSalt = salt; - identity.privkeyWrapIV = iv; - }, - - wrapKey : function Crypto_wrapKey(data, identity) { - let self = yield; - - this._log.trace("wrapKey called. [id=" + identity.realm + "]"); - let ret = Svc.Crypto.wrapSymmetricKey(data, identity.pubkey); - - self.done(ret); - }, - - unwrapKey: function Crypto_unwrapKey(data, identity) { - let self = yield; - - this._log.trace("upwrapKey called. [id=" + identity.realm + "]"); - let ret = Svc.Crypto.unwrapSymmetricKey(data, identity.privkey, - identity.password, identity.passphraseSalt, identity.privkeyWrapIV); - self.done(ret); - }, - - // This function tests to see if the passphrase which encrypts - // the private key for the given identity is valid. - isPassphraseValid: function Crypto_isPassphraseValid(identity) { - var self = yield; - - // We do this in a somewhat roundabout way; an alternative is - // to use a hard-coded symmetric key, but even that is still - // roundabout--ideally, the IWeaveCrypto interface should - // expose some sort of functionality to make this easier, - // or perhaps it should just offer this very function. -AV - - // Generate a temporary fake identity. - var idTemp = {realm: "Temporary passphrase validation"}; - - // Generate a random symmetric key. - this.randomKeyGen.async(Crypto, self.cb, idTemp); - yield; - - // Encrypt the symmetric key with the user's public key. - this.wrapKey.async(Crypto, self.cb, idTemp.bulkKey, identity); - let wrappedKey = yield; - let unwrappedKey; - - // Decrypt the symmetric key with the user's private key. - try { - this.unwrapKey.async(Crypto, self.cb, wrappedKey, identity); - unwrappedKey = yield; - } catch (e) { - self.done(false); - return; - } - - // Ensure that the original symmetric key is identical to - // the decrypted version. - if (unwrappedKey != idTemp.bulkKey) - throw new Error("Unwrapped key is not identical to original key."); - - self.done(true); - } -}; diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile index 77ff6d1ef587..720637ea7026 100644 --- a/services/sync/tests/unit/Makefile +++ b/services/sync/tests/unit/Makefile @@ -1 +1,50 @@ +# profiledir needs to be an absolute path on Mac OS X (FIXME: file bug). +profiledir = $(abspath ../profile) +sys := $(shell uname -s) + +# OS detection + +ifeq ($(sys), Darwin) + os = Darwin +else +ifeq ($(sys), Linux) + os = Linux +else +ifeq ($(sys), MINGW32_NT-6.0) + os = WINNT +else +ifeq ($(sys), MINGW32_NT-5.1) + os = WINNT +else + $(error Sorry, your os is unknown/unsupported: $(sys)) +endif +endif +endif +endif + +ifeq ($(topsrcdir),) +topsrcdir = ../.. +endif + +ifeq ($(native_topsrcdir),) +ifeq ($(os), WINNT) +native_topsrcdir = ..\.. +else +native_topsrcdir = ../.. +endif +endif + +# The path to the extension, in the native format, as required by the extension +# manager when it installs an extension via a file in the /extensions/ +# directory that contains the path to the extension. +ifeq ($(os), WINNT) +extensiondir = $(subst /,\,$(shell pwd -W))\$(native_topsrcdir) +else +extensiondir = `pwd`/$(topsrcdir) +endif + +# A command that installs the extension into the profile when the harness +# creates a new profile. +configure_profile = echo "$(extensiondir)" > $(profiledir)/extensions/\{340c2bbc-ce74-4362-90b5-7c26312808ef\}; + include ../harness/Makefile diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index cee5b43e63b2..b7c118a17140 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -1,5 +1,3 @@ -version(180); - const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; @@ -31,9 +29,6 @@ let provider = { }; ds.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider); -do_bind_resource(do_get_file("modules"), "weave"); -do_bind_resource(do_get_file("tests"), "tests"); - function loadInSandbox(aUri) { var sandbox = Components.utils.Sandbox(this); var request = Components. @@ -41,8 +36,9 @@ function loadInSandbox(aUri) { createInstance(); request.open("GET", aUri, false); + request.overrideMimeType("application/javascript"); request.send(null); - Components.utils.evalInSandbox(request.responseText, sandbox); + Components.utils.evalInSandbox(request.responseText, sandbox, "1.8"); return sandbox; } diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js index bf56be42e15f..55108fe45088 100644 --- a/services/sync/tests/unit/head_http_server.js +++ b/services/sync/tests/unit/head_http_server.js @@ -1,5 +1,5 @@ let Httpd = {}; -Cu.import("resource://tests/lib/httpd.js", Httpd); +Cu.import("resource://harness/modules/httpd.js", Httpd); function httpd_setup (handlers) { let server = new Httpd.nsHttpServer(); diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js index 9d2132893e9b..e7cb18f210c9 100644 --- a/services/sync/tests/unit/test_auth_manager.js +++ b/services/sync/tests/unit/test_auth_manager.js @@ -9,7 +9,7 @@ Function.prototype.async = Async.sugar; let logger; let Httpd = {}; -Cu.import("resource://tests/lib/httpd.js", Httpd); +Cu.import("resource://harness/modules/httpd.js", Httpd); function server_handler(metadata, response) { let body; diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 9be994ac2a7c..773b9f6b0412 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -12,8 +12,6 @@ try { Function.prototype.async = Async.sugar; let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); -let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - getService(Ci.IWeaveCrypto); let keys, cryptoMeta, cryptoWrap; function pubkey_handler(metadata, response) { @@ -68,6 +66,8 @@ function async_test() { keys = PubKeys.createKeypair("my passphrase", "http://localhost:8080/pubkey", "http://localhost:8080/privkey"); + let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); keys.symkey = crypto.generateRandomKey(); keys.wrappedkey = crypto.wrapSymmetricKey(keys.symkey, keys.pubkey.keyData); diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index c8b887a97f0b..b14b7dedb3cf 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -9,7 +9,7 @@ Function.prototype.async = Async.sugar; let logger; let Httpd = {}; -Cu.import("resource://tests/lib/httpd.js", Httpd); +Cu.import("resource://harness/modules/httpd.js", Httpd); function server_open(metadata, response) { let body = "This path exists"; From b3619d9635946ff823b4f4d4cd47db14ca5a56d5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 20 Mar 2009 02:35:46 -0700 Subject: [PATCH 1023/1860] Bug 482900 - Provide a friendlier UI to choose which ways to sync data. r=thunder --- .../sync/locales/en-US/pick-sync.properties | 15 +++++++++++++++ services/sync/locales/en-US/preferences.dtd | 6 ------ .../sync/locales/en-US/preferences.properties | 4 ---- services/sync/modules/util.js | 17 ++++++----------- 4 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 services/sync/locales/en-US/pick-sync.properties diff --git a/services/sync/locales/en-US/pick-sync.properties b/services/sync/locales/en-US/pick-sync.properties new file mode 100644 index 000000000000..b443d9d7eb9a --- /dev/null +++ b/services/sync/locales/en-US/pick-sync.properties @@ -0,0 +1,15 @@ +dialog.title = Sync with Weave +dialog.accept = Sync + +dir.caption = Direction to sync + +dir.resetClient = Do a fresh, full sync with the server +dir.wipeClient = Erase local data and restart with server data +dir.wipeRemote = Erase remote data and restart with local data + +dir.wipeClient.warning.title = Confirm Erase Local Data +dir.wipeClient.warning = This will erase your local data.\n\nAre you sure you want to do this? +dir.wipeRemote.warning.title = Confirm Erase Remote Data +dir.wipeRemote.warning = This will erase your remote data.\n\nAre you sure you want to do this? + +data.caption = Data to sync diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index db3e8af8b4b0..01fd6702dd07 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -69,14 +69,8 @@ - - - - - - diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index 26e23fda181d..cbe21be60565 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -1,12 +1,8 @@ # %S is the username of the signed in user signedIn.description = Signed in as %S -erase.local.warning.title = Erase Local Data -erase.local.warning = This will delete all local data.\n\nAre you sure you want to do this? erase.server.warning.title = Erase Server Data erase.server.warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? -erase.remote.warning.title = Erase Other Clients -erase.remote.warning = This will delete all remote data.\n\nAre you sure you want to do this? reset.server.warning.title = Reset Server Data reset.server.warning = This will delete all data on the Weave server.\n\nYou must restart this and any other instances of Firefox you may have running on any computer once you do this.\n\nAre you sure you want to do this? diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 7d1f16694433..3c786677b704 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -137,8 +137,7 @@ let Utils = { lazy2: function Weave_lazy2(dest, prop, fn) { let getter = function() { delete dest[prop]; - dest[prop] = ctr(); - return dest[prop]; + return dest[prop] = fn(); }; dest.__defineGetter__(prop, getter); }, @@ -464,7 +463,7 @@ let Utils = { let win = Svc.WinWatcher; if (type == "Dialog") win = win.activeWindow; - win["open" + type].apply(win, Utils.slice(arguments, 2)); + win["open" + type].apply(win, Array.slice(arguments, 2)); }, openDialog: function Utils_openDialog(name, uri, options, args) { @@ -488,6 +487,10 @@ let Utils = { Utils.openWindow("Status", "status.xul"); }, + openSync: function Utils_openSync() { + Utils.openWindow("Sync", "pick-sync.xul"); + }, + openWindow: function Utils_openWindow(name, uri, options, args) { Utils._openWin(name, "Window", null, "chrome://weave/content/" + uri, "", options || "centerscreen,chrome,dialog,resizable=yes", args); @@ -506,14 +509,6 @@ let Utils = { return ret; }, - slice: function Utils_slice(array, start, end) { - let args = [start]; - if (end !== undefined) - args.push(end); - - return Array.prototype.slice.apply(array, args); - }, - bind2: function Async_bind2(object, method) { return function innerBind() { return method.apply(object, arguments); }; }, From 0d69b384e56c3b2fc860bd6aedbb3c1c412e1668 Mon Sep 17 00:00:00 2001 From: Date: Tue, 24 Mar 2009 19:23:53 -0700 Subject: [PATCH 1024/1860] Bug 481319: Weave.Service can now be queried for top-level status (OK, failed, partial success), and also for an object that gives detailed status (what caused sync to abort, server status codes, status of each engine, etc.) --- services/sync/modules/service.js | 85 +++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e498235f07c5..6c09f8be8ec9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -64,13 +64,6 @@ const INITIAL_THRESHOLD = 75; // threshold each time we do a sync check and don't sync that engine. const THRESHOLD_DECREMENT_STEP = 25; -// The following are various error messages for not syncing -const kSyncWeaveDisabled = "Weave is disabled"; -const kSyncNotLoggedIn = "User is not logged in"; -const kSyncNetworkOffline = "Network is offline"; -const kSyncInPrivateBrowsing = "Private browsing is enabled"; -const kSyncNotScheduled = "Not scheduled to do sync"; - Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); @@ -108,6 +101,35 @@ Cu.import("resource://weave/engines/clientData.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); +/* + * Service status query system. See constants defined in constants.js. + */ + +function StatusRecord() { + this._init(); +} +StatusRecord.prototype = { + _init: function() { + this.server = []; + this.sync = null; + this.engines = {}; + }, + + addServerStatus: function( statusCode ) { + this.server.push(statusCode); + }, + + setSyncStatus: function( statusCode ) { + this.sync = statusCode; + }, + + setEngineStatus: function( engineName, statusCode ) { + this.engines[engineName] = statusCode; + } +}; + + + /* * Service singleton * Main entry point into Weave's sync framework @@ -124,7 +146,11 @@ WeaveSvc.prototype = { _loggedIn: false, _syncInProgress: false, _keyGenEnabled: true, - _mostRecentError: null, + + // WEAVE_STATUS_OK, WEAVE_STATUS_FAILED, or WEAVE_STATUS_PARTIAL + _weaveStatusCode: null, + // More detailed status info: + _detailedStatus: null, // object for caching public and private keys _keyPair: {}, @@ -206,7 +232,8 @@ WeaveSvc.prototype = { get enabled() { return Svc.Prefs.get("enabled"); }, set enabled(value) { Svc.Prefs.set("enabled", value); }, - get mostRecentError() { return this._mostRecentError; }, + get statusCode() { return this._weaveStatusCode; }, + get detailedStatus() { return this._detailedStatus; }, get locked() { return this._locked; }, lock: function Svc_lock() { @@ -219,6 +246,11 @@ WeaveSvc.prototype = { this._locked = false; }, + _setSyncFailure: function WeavSvc__setSyncFailure(code) { + this._weaveStatusCode = WEAVE_STATUS_FAILED; + this._detailedStatus.setSyncStatus(code); + }, + _genKeyURLs: function WeaveSvc__genKeyURLs() { let url = this.clusterURL + this.username; PubKeys.defaultKeyUri = url + "/keys/pubkey"; @@ -251,6 +283,8 @@ WeaveSvc.prototype = { this._initLogs(); this._log.info("Weave " + WEAVE_VERSION + " initializing"); + this._detailedStatus = new StatusRecord(); + // Reset our sync id if we're upgrading, so sync knows to reset local data if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) { this._log.info("Resetting client syncID from _onStartup."); @@ -458,6 +492,7 @@ WeaveSvc.prototype = { let self = yield; this._loggedIn = false; + this._detailedStatus = new StatusRecord(); if (typeof(user) != 'undefined') this.username = user; @@ -467,19 +502,19 @@ WeaveSvc.prototype = { ID.get('WeaveCryptoID').setTempPassword(passp); if (!this.username) { - this._mostRecentError = "No username set."; + this._setSyncFailure( LOGIN_FAILED_NO_USERNAME ); throw "No username set, login failed"; } if (!this.password) { - this._mostRecentError = "No password set."; + this._setSyncFailure( LOGIN_FAILED_NO_PASSWORD ); throw "No password given or found in password manager"; } this._log.debug("Logging in user " + this.username); if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) { - this._mostRecentError = "Login failed. Check your username/password/phrase."; + this._setSyncFailure( LOGIN_FAILED_REJECTED ); throw "Login failed"; } @@ -535,7 +570,7 @@ WeaveSvc.prototype = { // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.lastResource.lastChannel.responseStatus; if (status != 200 && status != 404) { - this._mostRecentError = "Unknown error when downloading metadata."; + this._setSyncFailure(METARECORD_DOWNLOAD_FAIL); this._log.warn("Unknown error while downloading metadata record. " + "Aborting sync."); self.done(false); @@ -552,7 +587,7 @@ WeaveSvc.prototype = { if (!this._keyGenEnabled) { this._log.info("...and key generation is disabled. Not wiping. " + "Aborting sync."); - this._mostRecentError = "Weave needs updating on your desktop browser."; + this._setSyncFailure(DESKTOP_VERSION_OUT_OF_DATE); self.done(false); return; } @@ -568,7 +603,7 @@ WeaveSvc.prototype = { "upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")"); } else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) { - this._mostRecentError = "Client needs to be upgraded."; + this._setSyncFailure(VERSION_OUT_OF_DATE); this._log.warn("Server data is of a newer Weave version, this client " + "needs to be upgraded. Aborting sync."); self.done(false); @@ -601,7 +636,7 @@ WeaveSvc.prototype = { PubKeys.lastResource.lastChannel.responseStatus); this._log.debug("PrivKey HTTP response status: " + PrivKeys.lastResource.lastChannel.responseStatus); - this._mostRecentError = "Can't download keys from server."; + this._setSyncFailure(KEYS_DOWNLOAD_FAIL); self.done(false); return; } @@ -609,7 +644,7 @@ WeaveSvc.prototype = { if (!this._keyGenEnabled) { this._log.warn("Couldn't download keys from server, and key generation" + "is disabled. Aborting sync"); - this._mostRecentError = "No keys. Try syncing from desktop first."; + this._setSyncFailure(NO_KEYS_NO_KEYGEN); self.done(false); return; } @@ -628,13 +663,13 @@ WeaveSvc.prototype = { yield PubKeys.uploadKeypair(self.cb, keys); ret = true; } catch (e) { - this._mostRecentError = "Could not upload keys."; + this._setSyncFailure(KEYS_UPLOAD_FAIL); this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); // FIXME no lastRequest anymore //this._log.error(keys.pubkey.lastRequest.responseText); } } else { - this._mostRecentError = "Could not get encryption passphrase."; + this._setSyncFailure(SETUP_FAILED_NO_PASSPHRASE); this._log.warn("Could not get encryption passphrase"); } } @@ -709,8 +744,10 @@ WeaveSvc.prototype = { // "not scheduled" as future syncs have already been canceled by checkSync. let reason = this._checkSync(); if (reason && (useThresh || reason != kSyncNotScheduled)) { + // this is a purposeful abort rather than a failure, so don't set + // WEAVE_STATUS_FAILED; instead, leave it as it was. + this._detailedStatus.setSyncStatus(reason); reason = "Can't sync: " + reason; - this._mostRecentError = reason; throw reason; } @@ -724,8 +761,10 @@ WeaveSvc.prototype = { // Process the incoming commands if we have any if (Clients.getClients()[Clients.clientID].commands) { try { - if (!(yield this.processCommands(self.cb))) + if (!(yield this.processCommands(self.cb))) { + this._detailedStatus.setSyncStatus(ABORT_SYNC_COMMAND); throw "aborting sync, process commands said so"; + } // Repeat remoteSetup in-case the commands forced us to reset if (!(yield this._remoteSetup.async(this, self.cb))) @@ -773,7 +812,6 @@ WeaveSvc.prototype = { // If there's any problems with syncing the engine, report the failure if (!(yield this._syncEngine.async(this, self.cb, engine))) { - this._mostRecentError = "Failure in " + engine.displayName; this._log.info("Aborting sync"); break; } @@ -788,6 +826,7 @@ WeaveSvc.prototype = { this._log.warn("Some engines did not sync correctly"); else { Svc.Prefs.set("lastSync", new Date().toString()); + this._weaveStatusCode = WEAVE_STATUS_OK; this._log.info("Sync completed successfully"); } } finally { @@ -821,6 +860,8 @@ WeaveSvc.prototype = { } catch(e) { this._syncError = true; + this._weaveStatusCode = WEAVE_STATUS_PARTIAL; + this._detailedStatus.setEngineStatus(engine.name, e); if (FaultTolerance.Service.onException(e)) self.done(true); } From ee9eab1a95ed3dabcd8c6c0255bf650f4a798654 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 26 Mar 2009 01:23:50 +0800 Subject: [PATCH 1025/1860] Bug 483671 - Password engine isn't getting correct hostname. r=thunder --- services/sync/modules/stores.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 19eaba89c0ab..b4cd50aa46a9 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -85,7 +85,7 @@ Store.prototype = { applyIncoming: function BStore_applyIncoming(onComplete, record) { let fn = function(rec) { let self = yield; - if (!rec.cleartext) + if (rec.payload == null) this.remove(rec); else if (!this.itemExists(rec.id)) this.create(rec); From 3d98af7a3fab8257bbe35f4f5b07e54844a49602 Mon Sep 17 00:00:00 2001 From: Date: Wed, 25 Mar 2009 17:36:11 -0700 Subject: [PATCH 1026/1860] Fixed name of TabStore.changeItemID so that it correctly overrides the abstract base class method. Whoops. --- services/sync/modules/engines/tabs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index e60b69c6e0fe..32d660bc413f 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -311,7 +311,7 @@ TabStore.prototype = { return record; }, - changeItemId: function TabStore_changeItemId(oldId, newId) { + changeItemID: function TabStore_changeItemId(oldId, newId) { if (this._remoteClients[oldId]) { let record = this._remoteClients[oldId]; record.id = newId; From f45fe7670fb4c130e0f7b5b309ddbd5c7c85b131 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 25 Mar 2009 19:27:24 -0700 Subject: [PATCH 1027/1860] Bug 484848: modifyLogin sends an array of nsILoginMetaInfos to observers --- services/sync/modules/engines/passwords.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index f63fd9195d9c..726b39805347 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -210,7 +210,7 @@ PasswordStore.prototype = { return; } let login = this._loginItems[record.id]; - this._log.trace("Updating " + record.id + " (" + itemId + ")"); + this._log.trace("Updating " + record.id); let newinfo = this._nsLoginInfoFromRecord(record); Svc.Login.modifyLogin(login, newinfo); @@ -239,14 +239,15 @@ PasswordTracker.prototype = { return; switch (aData) { - case 'addLogin': case 'modifyLogin': - case 'removeLogin': + aSubject = aSubject[1]; + case 'addLogin': + case 'removeLogin': { let metaInfo = aSubject.QueryInterface(Ci.nsILoginMetaInfo); this._score += 15; this._log.debug(aData + ": " + metaInfo.guid); this.addChangedID(metaInfo.guid); - break; + } break; case 'removeAllLogins': this._log.debug(aData); this._score += 50; From c9b1bff02af390e1d80547f327af229f3cc4183f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 25 Mar 2009 19:28:46 -0700 Subject: [PATCH 1028/1860] make dialogs non-resizable, split lazy callbacks out --- services/sync/modules/util.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 3c786677b704..adc8199e1641 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -125,21 +125,27 @@ let Utils = { // lazy load objects from a constructor on first access. It will // work with the global object ('this' in the global context). lazy: function Weave_lazy(dest, prop, ctr) { - let getter = function() { + delete dest[prop]; + dest.__defineGetter__(prop, Utils.lazyCb(dest, prop, ctr)); + }, + lazyCb: function Weave_lazyCb(dest, prop, ctr) { + return function() { delete dest[prop]; dest[prop] = new ctr(); return dest[prop]; }; - dest.__defineGetter__(prop, getter); }, // like lazy, but rather than new'ing the 3rd arg we use its return value lazy2: function Weave_lazy2(dest, prop, fn) { - let getter = function() { + delete dest[prop]; + dest.__defineGetter__(prop, Utils.lazyCb2(dest, prop, fn)); + }, + lazyCb2: function Weave_lazyCb2(dest, prop, fn) { + return function() { delete dest[prop]; return dest[prop] = fn(); }; - dest.__defineGetter__(prop, getter); }, lazySvc: function Weave_lazySvc(dest, prop, cid, iface) { @@ -468,7 +474,7 @@ let Utils = { openDialog: function Utils_openDialog(name, uri, options, args) { Utils._openWin(name, "Dialog", "chrome://weave/content/" + uri, "", - options || "centerscreen,chrome,dialog,modal,resizable=yes", args); + options || "centerscreen,chrome,dialog,modal,resizable=no", args); }, openLog: function Utils_openLog() { @@ -497,7 +503,7 @@ let Utils = { }, openWizard: function Utils_openWizard() { - Utils.openDialog("Wizard", "wizard.xul"); + Utils.openWindow("Wizard", "wizard.xul"); }, // assumes an nsIConverterInputStream From c4c17c5a19c146ce1e7bfaf3cb9d12ba551f9c63 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 25 Mar 2009 19:30:16 -0700 Subject: [PATCH 1029/1860] add a setHeader() function so you can set some headers and leave others as they are; set merge to /false/ when setting headers on the channel; set content-type properly when uploading content --- services/sync/modules/resource.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 682ef50c41bc..ca8c9e45c1ef 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -91,6 +91,13 @@ Resource.prototype = { set headers(value) { this._headers = value; }, + setHeader: function Res_setHeader() { + if (arguments.length % 2) + throw "setHeader only accepts arguments in multiples of 2"; + for (let i = 0; i < arguments.length; i += 2) { + this._headers[arguments[i]] = arguments[i + 1]; + } + }, get uri() { return this._uri; @@ -162,7 +169,7 @@ Resource.prototype = { this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); else this._log.trace("HTTP Header " + key + ": " + headers[key]); - this._lastChannel.setRequestHeader(key, headers[key], true); + this._lastChannel.setRequestHeader(key, headers[key], false); } return this._lastChannel; }, @@ -204,12 +211,15 @@ Resource.prototype = { yield this.filterUpload(self.cb); this._log.trace(action + " Body:\n" + this._data); + let type = ('Content-Type' in this._headers)? + this._headers['Content-Type'] : 'text/plain'; + let stream = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); stream.setData(this._data, this._data.length); channel.QueryInterface(Ci.nsIUploadChannel); - channel.setUploadStream(stream, 'text/plain', this._data.length); + channel.setUploadStream(stream, type, this._data.length); } let listener = new ChannelListener(self.cb, this._onProgress, this._log); From 69bdb23a37e6f5ccf75acda8bca874609e0e7439 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 25 Mar 2009 19:30:45 -0700 Subject: [PATCH 1030/1860] add a tmp server url for account reg --- services/sync/services-sync.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 43fe5fa35958..48c82c64d9c8 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,5 @@ pref("extensions.weave.serverURL", "https://auth.services.mozilla.com/"); +pref("extensions.weave.tmpServerURL", "https://services.mozilla.com/"); pref("extensions.weave.encryption", "aes-256-cbc"); From edc3d99323c684aba4495c086b2202376b9de8a1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 25 Mar 2009 19:33:16 -0700 Subject: [PATCH 1031/1860] wizard revamp --- services/sync/locales/en-US/wizard.dtd | 202 +++++++++--------- services/sync/locales/en-US/wizard.properties | 6 +- 2 files changed, 104 insertions(+), 104 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 46776bd5ba88..5245a1f5879d 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -1,101 +1,101 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 5eef18245958..4df96512d8e7 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -52,10 +52,10 @@ default-name-nouser.label = Firefox bookmarks.label = Bookmarks history.label = Browsing History -cookies.label = Cookies -passwords.label = Saved Passwords tabs.label = Tabs -formdata.label = Saved Form Data +passwords.label = Saved Passwords +forms.label = Saved Form Data +cookies.label = Cookies initialLogin-progress.label = Signing you in initialLogin-error.label = Problem signing in. From 4c1e7868bda10be00275da04fe8559634020d57f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 25 Mar 2009 19:47:37 -0700 Subject: [PATCH 1032/1860] Bug 481319: try again --- services/sync/modules/engines/passwords.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 726b39805347..dbf8eca342f9 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -240,7 +240,7 @@ PasswordTracker.prototype = { switch (aData) { case 'modifyLogin': - aSubject = aSubject[1]; + aSubject = aSubject.queryElementAt(1, Ci.nsILoginMetaInfo); case 'addLogin': case 'removeLogin': { let metaInfo = aSubject.QueryInterface(Ci.nsILoginMetaInfo); From 22bd65a69c2b8f7c37f888b428684fe9575d2973 Mon Sep 17 00:00:00 2001 From: Date: Wed, 25 Mar 2009 23:43:14 -0700 Subject: [PATCH 1033/1860] Moved constants from constants.js to constants.js.in (see bug 481319.) --- services/sync/modules/constants.js | 49 ++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 2a8233350524..af753b9839c8 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -40,7 +40,18 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'MIN_SERVER_STORAGE_VERSION', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', - 'CONNECTION_TIMEOUT', 'KEEP_DELTAS']; + 'CONNECTION_TIMEOUT', 'KEEP_DELTAS', + 'WEAVE_STATUS_OK', 'WEAVE_STATUS_FAILED', + 'WEAVE_STATUS_PARTIAL', 'SERVER_LOW_QUOTA', + 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', + 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', + 'LOGIN_FAILED_REJECTED', 'METARECORD_DOWNLOAD_FAIL', + 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', + 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', + 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', + 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', + 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', + 'kSyncNotScheduled']; const WEAVE_VERSION = "@weave_version@"; @@ -67,4 +78,38 @@ const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; const CONNECTION_TIMEOUT = 30000; -const KEEP_DELTAS = 25; \ No newline at end of file +const KEEP_DELTAS = 25; + +// Top-level statuses: +const WEAVE_STATUS_OK = "Sync succeeded."; +const WEAVE_STATUS_FAILED = "Sync failed."; +const WEAVE_STATUS_PARTIAL = "Sync partially succeeded but some data not synced."; + +// Server statuses (Not mutually exclusive): +const SERVER_LOW_QUOTA = "Getting close to your Weave server storage quota."; +const SERVER_DOWNTIME = "Weave server is overloaded, try agian in 30 sec."; +const SERVER_UNREACHABLE = "Weave server is unreachable."; + +// Ways that a sync can fail during setup or login: +const LOGIN_FAILED_NO_USERNAME = "No username set, login failed"; +const LOGIN_FAILED_NO_PASSWORD = "No password given or found in password manager."; +const LOGIN_FAILED_REJECTED = "Login rejected. Check your username/password/passphrase."; +const METARECORD_DOWNLOAD_FAIL = "Can't download metadata record, HTTP error."; +const VERSION_OUT_OF_DATE = "This copy of Weave needs to be updated."; +const DESKTOP_VERSION_OUT_OF_DATE = "Weave needs updating on your desktop browser."; +const KEYS_DOWNLOAD_FAIL = "Can't download keys from server, HTTP error."; +const NO_KEYS_NO_KEYGEN = "No keys present on server and keygen disabled; sync from desktop first."; +const KEYS_UPLOAD_FAIL = "Could not upload keys."; +const SETUP_FAILED_NO_PASSPHRASE = "Could not get encryption passphrase."; + +// Ways that a sync can be disabled +const kSyncWeaveDisabled = "Weave is disabled"; +const kSyncNotLoggedIn = "User is not logged in"; +const kSyncNetworkOffline = "Network is offline"; +const kSyncInPrivateBrowsing = "Private browsing is enabled"; +const kSyncNotScheduled = "Not scheduled to do sync"; +// If one of these happens, leave the top-level status the same! + +// Ways that a sync can be aborted: +const ABORT_SYNC_COMMAND = "aborting sync, process commands said so"; + From a42b0d511c2af4216ba39fc38abdc201a2ae51bb Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 26 Mar 2009 10:58:54 -0700 Subject: [PATCH 1034/1860] Bug 484848: try #3 --- services/sync/modules/engines/passwords.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index dbf8eca342f9..8bfaee5add5f 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -240,14 +240,15 @@ PasswordTracker.prototype = { switch (aData) { case 'modifyLogin': - aSubject = aSubject.queryElementAt(1, Ci.nsILoginMetaInfo); + aSubject = aSubject.QueryInterface(Ci.nsIArray). + queryElementAt(1, Ci.nsILoginMetaInfo); case 'addLogin': - case 'removeLogin': { - let metaInfo = aSubject.QueryInterface(Ci.nsILoginMetaInfo); + case 'removeLogin': + aSubject.QueryInterface(Ci.nsILoginMetaInfo); this._score += 15; - this._log.debug(aData + ": " + metaInfo.guid); - this.addChangedID(metaInfo.guid); - } break; + this._log.debug(aData + ": " + aSubject.guid); + this.addChangedID(aSubject.guid); + break; case 'removeAllLogins': this._log.debug(aData); this._score += 50; From f81bd72e4e9600e339837070f875b1b83a945b8b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 27 Mar 2009 00:46:10 -0700 Subject: [PATCH 1035/1860] style police; split findCluster into two, findCluster (no side-effects) and setCluster (saves it); fix verifyLogin to work with any login, not just the saved one; add a createAccount call which creates an account on the server --- services/sync/modules/service.js | 134 ++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 39 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6c09f8be8ec9..68d9d9d1922b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -115,15 +115,15 @@ StatusRecord.prototype = { this.engines = {}; }, - addServerStatus: function( statusCode ) { + addServerStatus: function(statusCode) { this.server.push(statusCode); }, - setSyncStatus: function( statusCode ) { + setSyncStatus: function(statusCode) { this.sync = statusCode; }, - setEngineStatus: function( engineName, statusCode ) { + setEngineStatus: function(engineName, statusCode) { this.engines[engineName] = statusCode; } }; @@ -411,11 +411,11 @@ WeaveSvc.prototype = { // These are global (for all engines) - // gets cluster from central LDAP server and sets this.clusterURL + // gets cluster from central LDAP server and returns it, or null on error findCluster: function WeaveSvc_findCluster(onComplete, username) { let fn = function WeaveSvc__findCluster() { let self = yield; - let ret = false; + let ret = null; this._log.debug("Finding cluster for user " + username); @@ -426,63 +426,85 @@ WeaveSvc.prototype = { if (res.lastChannel.responseStatus == 404) { this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); - this.clusterURL = Svc.Prefs.get("serverURL"); - ret = true; - + ret = Svc.Prefs.get("serverURL"); } else if (res.lastChannel.responseStatus == 200) { // XXX Bug 480480 Work around the server sending a trailing newline - this.clusterURL = 'https://' + res.data.trim() + '/'; - ret = true; + ret = 'https://' + res.data.trim() + '/'; } + + self.done(ret); + }; + fn.async(this, onComplete); + }, + + // gets cluster from central LDAP server and sets this.clusterURL + setCluster: function WeaveSvc_setCluster(onComplete, username) { + let fn = function WeaveSvc__setCluster() { + let self = yield; + let ret = false; + + let cluster = yield this.findCluster(self.cb, username); + if (cluster) { + this._log.debug("Saving cluster setting"); + this.clusterURL = cluster; + ret = true; + } else + this._log.debug("Error setting cluster for user " + username); + self.done(ret); }; fn.async(this, onComplete); }, verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password, isLogin) { - let user = username, pass = password; - let fn = function WeaveSvc__verifyLogin() { let self = yield; - this._log.debug("Verifying login for user " + user); + this._log.debug("Verifying login for user " + username); - let cluster = this.clusterURL; - yield this.findCluster(self.cb, username); + let url = yield this.findCluster(self.cb, username); + if (isLogin) + this.clusterURL = url; - let res = new Resource(this.clusterURL + user); + if (url[url.length-1] != '/') + url += '/'; + url += "0.3/user/"; + + let res = new Resource(url + username); + res.authenticator = { + onRequest: function(headers) { + headers['Authorization'] = 'Basic ' + btoa(username + ':' + password); + return headers; + } + }; yield res.get(self.cb); - if (!isLogin) // restore cluster so verifyLogin has no impact - this.clusterURL = cluster; - //Svc.Json.decode(res.data); // throws if not json self.done(true); }; this._catchAll(this._notify("verify-login", "", fn)).async(this, onComplete); }, - _verifyPassphrase: function WeaveSvc__verifyPassphrase(username, password, - passphrase) { - let self = yield; - - this._log.debug("Verifying passphrase"); - - this.username = username; - ID.get('WeaveID').setTempPassword(password); - - let id = new Identity('Passphrase Verification', username); - id.setTempPassword(passphrase); - - let pubkey = yield PubKeys.getDefaultKey(self.cb); - let privkey = yield PrivKeys.get(self.cb, pubkey.PrivKeyUri); - - // fixme: decrypt something here - }, verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, password, passphrase) { - this._catchAll(this._localLock(this._notify("verify-passphrase", "", - this._verifyPassphrase, username, password, passphrase))). - async(this, onComplete); + let fn = function WeaveSvc__verifyPassphrase() { + let self = yield; + + this._log.debug("Verifying passphrase"); + + this.username = username; + ID.get('WeaveID').setTempPassword(password); + + let id = new Identity('Passphrase Verification', username); + id.setTempPassword(passphrase); + + let pubkey = yield PubKeys.getDefaultKey(self.cb); + let privkey = yield PrivKeys.get(self.cb, pubkey.PrivKeyUri); + + // fixme: decrypt something here + }; + this._catchAll( + this._localLock( + this._notify("verify-passphrase", "", fn))).async(this, onComplete); }, login: function WeaveSvc_login(onComplete, username, password, passphrase) { @@ -541,6 +563,40 @@ WeaveSvc.prototype = { Svc.Observer.notifyObservers(null, "weave:service:logout:finish", ""); }, + createAccount: function WeaveSvc_createAccount(onComplete, username, password, email, + captchaChallenge, captchaResponse) { + let fn = function WeaveSvc__createAccount() { + function enc(x) encodeURIComponent(x); + let message = "uid=" + enc(username) + "&password=" + enc(password) + + "&mail=" + enc(email) + + "&recaptcha_challenge_field=" + enc(captchaChallenge) + + "&recaptcha_response_field=" + enc(captchaResponse); + + let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/new'; + let res = new Weave.Resource(url); + res.authenticator = new Weave.NoOpAuthenticator(); + res.setHeader("Content-Type", "application/x-www-form-urlencoded", + "Content-Length", message.length); + + // fixme: Resource throws on error - it really shouldn't :-/ + let resp; + try { + resp = yield res.post(self.cb, message); + } catch (e) {} + + if (res.lastChannel.responseStatus != 200 && + res.lastChannel.responseStatus != 201) + this._log.info("Failed to create account. " + + "status: " + res.lastChannel.responseStatus + ", " + + "response: " + resp); + else + this._log.info("Account created: " + resp); + + self.done(res.lastChannel.responseStatus); + }; + fn.async(this, onComplete); + }, + // stuff we need to to after login, before we can really do // anything (e.g. key setup) _remoteSetup: function WeaveSvc__remoteSetup() { From c07fffd1c6a61d88de2f52857fc9f5bcd509e7f1 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 27 Mar 2009 00:46:39 -0700 Subject: [PATCH 1036/1860] fix verify path + general polish --- services/sync/locales/en-US/wizard.dtd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 5245a1f5879d..d6bc0ffd9bdb 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -65,8 +65,7 @@ - - + From 300517d6b1d5e7623ce69474a64c4b142b7e8238 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 27 Mar 2009 00:56:38 -0700 Subject: [PATCH 1037/1860] fix missing yield in createAccount --- services/sync/modules/service.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 68d9d9d1922b..b80ff3d32d28 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -566,6 +566,8 @@ WeaveSvc.prototype = { createAccount: function WeaveSvc_createAccount(onComplete, username, password, email, captchaChallenge, captchaResponse) { let fn = function WeaveSvc__createAccount() { + let self = yield; + function enc(x) encodeURIComponent(x); let message = "uid=" + enc(username) + "&password=" + enc(password) + "&mail=" + enc(email) + @@ -582,7 +584,9 @@ WeaveSvc.prototype = { let resp; try { resp = yield res.post(self.cb, message); - } catch (e) {} + } catch (e) { + this._log.trace("Create account error: " + e); + } if (res.lastChannel.responseStatus != 200 && res.lastChannel.responseStatus != 201) From 04341ccae57165235c843d370c6330379dc64aee Mon Sep 17 00:00:00 2001 From: Date: Fri, 27 Mar 2009 11:06:43 -0700 Subject: [PATCH 1038/1860] Bug 485539 - added guard to the tab engine to handle the case where there are undefined items in tabContainer.childNodes. --- services/sync/modules/engines/tabs.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 32d660bc413f..784de6de6035 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -226,6 +226,10 @@ TabStore.prototype = { let window = enumerator.getNext(); let tabContainer = window.getBrowser().tabContainer; for each (let tabChild in tabContainer.childNodes) { + if (!tabChild) { + this._log.warn("Undefined item in tabContainer.childNodes."); + continue; + } if (!tabChild.QueryInterface) continue; let tab = tabChild.QueryInterface(Ci.nsIDOMNode); From cde76b6de68dc3e09680d61caaeb1cd9eeae183f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 27 Mar 2009 20:17:10 -0700 Subject: [PATCH 1039/1860] fix passphrase help link; remove some unused strings --- services/sync/locales/en-US/wizard.dtd | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index d6bc0ffd9bdb..07781fe0e257 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -37,7 +37,7 @@ - + @@ -61,10 +61,6 @@ - - - - From f9a2ae9deb253a74f9c09cd885ff05af02a3e48a Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 27 Mar 2009 20:22:04 -0700 Subject: [PATCH 1040/1860] remove more unused strings --- services/sync/locales/en-US/wizard.dtd | 5 ----- 1 file changed, 5 deletions(-) diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd index 07781fe0e257..b04ad0406644 100644 --- a/services/sync/locales/en-US/wizard.dtd +++ b/services/sync/locales/en-US/wizard.dtd @@ -83,11 +83,6 @@ - - - - - From 3271a5f9c9402b8b5dda3f7f29cc3296a5b6da5d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 27 Mar 2009 20:57:38 -0700 Subject: [PATCH 1041/1860] shorten status messages --- services/sync/modules/constants.js | 10 +++++----- services/sync/modules/service.js | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index af753b9839c8..82f994c7f941 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -83,7 +83,7 @@ const KEEP_DELTAS = 25; // Top-level statuses: const WEAVE_STATUS_OK = "Sync succeeded."; const WEAVE_STATUS_FAILED = "Sync failed."; -const WEAVE_STATUS_PARTIAL = "Sync partially succeeded but some data not synced."; +const WEAVE_STATUS_PARTIAL = "Sync partially succeeded, some data failed to sync."; // Server statuses (Not mutually exclusive): const SERVER_LOW_QUOTA = "Getting close to your Weave server storage quota."; @@ -91,14 +91,14 @@ const SERVER_DOWNTIME = "Weave server is overloaded, try agian in 30 sec."; const SERVER_UNREACHABLE = "Weave server is unreachable."; // Ways that a sync can fail during setup or login: -const LOGIN_FAILED_NO_USERNAME = "No username set, login failed"; -const LOGIN_FAILED_NO_PASSWORD = "No password given or found in password manager."; -const LOGIN_FAILED_REJECTED = "Login rejected. Check your username/password/passphrase."; +const LOGIN_FAILED_NO_USERNAME = "No username set, login failed."; +const LOGIN_FAILED_NO_PASSWORD = "No password set, login failed."; +const LOGIN_FAILED_REJECTED = "Incorrect username or password."; const METARECORD_DOWNLOAD_FAIL = "Can't download metadata record, HTTP error."; const VERSION_OUT_OF_DATE = "This copy of Weave needs to be updated."; const DESKTOP_VERSION_OUT_OF_DATE = "Weave needs updating on your desktop browser."; const KEYS_DOWNLOAD_FAIL = "Can't download keys from server, HTTP error."; -const NO_KEYS_NO_KEYGEN = "No keys present on server and keygen disabled; sync from desktop first."; +const NO_KEYS_NO_KEYGEN = "Key generation disabled. Sync from the desktop first."; const KEYS_UPLOAD_FAIL = "Could not upload keys."; const SETUP_FAILED_NO_PASSPHRASE = "Could not get encryption passphrase."; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b80ff3d32d28..78d7e2b9cd29 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -524,19 +524,19 @@ WeaveSvc.prototype = { ID.get('WeaveCryptoID').setTempPassword(passp); if (!this.username) { - this._setSyncFailure( LOGIN_FAILED_NO_USERNAME ); + this._setSyncFailure(LOGIN_FAILED_NO_USERNAME); throw "No username set, login failed"; } if (!this.password) { - this._setSyncFailure( LOGIN_FAILED_NO_PASSWORD ); + this._setSyncFailure(LOGIN_FAILED_NO_PASSWORD); throw "No password given or found in password manager"; } this._log.debug("Logging in user " + this.username); if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) { - this._setSyncFailure( LOGIN_FAILED_REJECTED ); + this._setSyncFailure(LOGIN_FAILED_REJECTED); throw "Login failed"; } From 32e984a5b63205865b617ff4bc7606a8a0bdf863 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Mar 2009 15:18:20 -0700 Subject: [PATCH 1042/1860] remove dump() debugging from tabs engine --- services/sync/modules/engines/tabs.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 784de6de6035..106a760b5e97 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -409,7 +409,7 @@ TabTracker.prototype = { return; } //this._log.trace("Registering tab listeners in new window.\n"); - dump("Tab listeners registered!\n"); + //dump("Tab listeners registered!\n"); let container = browser.tabContainer; container.addEventListener("TabOpen", this.onTabOpened, false); container.addEventListener("TabClose", this.onTabClosed, false); @@ -444,8 +444,7 @@ TabTracker.prototype = { onTabOpened: function TabTracker_onTabOpened(event) { // Store a timestamp in the tab to track when it was last used - //this._log.trace("Tab opened.\n"); - dump("Tab opened.\n"); + this._log.trace("Tab opened."); event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); this._score += 50; @@ -458,7 +457,7 @@ TabTracker.prototype = { onTabSelected: function TabTracker_onTabSelected(event) { // Update the tab's timestamp - dump("Tab selected.\n"); + this._log.trace("Tab selected."); //this._log.trace("Tab selected.\n"); event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); From da175609d00828adc5cda82e82b012f49aeab257 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 30 Mar 2009 15:19:00 -0700 Subject: [PATCH 1043/1860] Bug 478328: delete old history data from the server --- services/sync/modules/engines/history.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index f27e7b91af68..d0ad2b04ea8b 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -48,6 +48,7 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/collection.js"); Cu.import("resource://weave/type_records/history.js"); Function.prototype.async = Async.sugar; @@ -95,6 +96,18 @@ HistoryEngine.prototype = { // Step 3: Apply update/new record self.done(true); + }, + + _syncFinish: function HistEngine__syncFinish(error) { + let self = yield; + this._log.debug("Finishing up sync"); + this._tracker.resetScore(); + + // Only leave 1 week's worth of history on the server + let coll = new Collection(this.engineURL, this._recordObj); + coll.older = this.lastSync - 604800; // 1 week + coll.full = 0; + yield coll.delete(self.cb); } }; From ba02dd10b94447b4f15729b678e4f3c0db674739 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 31 Mar 2009 14:09:38 -0500 Subject: [PATCH 1044/1860] Bug 481741 - Switch to JSON from nsIJSON --- .../sync/modules/base_records/collection.js | 4 ++-- services/sync/modules/base_records/crypto.js | 8 ++++---- services/sync/modules/base_records/wbo.js | 12 ++++++------ services/sync/modules/engines.js | 2 +- services/sync/modules/engines/clients.js | 4 ++-- services/sync/modules/engines/tabs.js | 14 ++++---------- services/sync/modules/resource.js | 10 ++-------- services/sync/modules/service.js | 8 ++++---- services/sync/modules/stores.js | 14 +++----------- services/sync/modules/trackers.js | 10 ++-------- services/sync/modules/util.js | 1 - services/sync/tests/unit/test_records_crypto.js | 17 ++++++++--------- services/sync/tests/unit/test_records_keys.js | 10 ++++------ services/sync/tests/unit/test_records_wbo.js | 6 ++---- 14 files changed, 44 insertions(+), 76 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 53cefe5417bf..f0d77088c556 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -156,7 +156,7 @@ CollectionIterator.prototype = { return; let item = this._coll.data[this._idx++]; let record = new this._coll._recordObj(); - record.deserialize(Svc.Json.encode(item)); // FIXME: inefficient + record.deserialize(JSON.stringify(item)); // FIXME: inefficient record.baseUri = this._coll.uri; self.done(record); @@ -167,4 +167,4 @@ CollectionIterator.prototype = { reset: function CollIter_reset() { this._idx = 0; } -}; \ No newline at end of file +}; diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 9eb3c49f6690..6fed91748e1e 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -96,7 +96,7 @@ CryptoWrapper.prototype = { let meta = yield CryptoMetas.get(self.cb, this.encryption); let symkey = yield meta.getKey(self.cb, privkey, passphrase); - this.ciphertext = Svc.Crypto.encrypt(Svc.Json.encode([this.cleartext]), + this.ciphertext = Svc.Crypto.encrypt(JSON.stringify([this.cleartext]), symkey, meta.bulkIV); this.cleartext = null; @@ -122,7 +122,7 @@ CryptoWrapper.prototype = { let symkey = yield meta.getKey(self.cb, privkey, passphrase); // note: payload is wrapped in an array, see _encrypt - this.cleartext = Svc.Json.decode(Svc.Crypto.decrypt(this.ciphertext, + this.cleartext = JSON.parse(Svc.Crypto.decrypt(this.ciphertext, symkey, meta.bulkIV))[0]; this.ciphertext = null; @@ -137,7 +137,7 @@ CryptoWrapper.prototype = { " parent: " + this.parentid + "\n" + " depth: " + this.depth + ", index: " + this.sortindex + "\n" + " modified: " + this.modified + "\n" + - " payload: " + Svc.Json.encode(this.cleartext) + " }"; + " payload: " + JSON.stringify(this.cleartext) + " }"; } }; @@ -230,4 +230,4 @@ function CryptoRecordManager() { this._init(); } CryptoRecordManager.prototype = { __proto__: RecordManager.prototype, _recordType: CryptoMeta -}; \ No newline at end of file +}; diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 965d40423061..b468e6c62228 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -118,15 +118,15 @@ WBORecord.prototype = { // payload is encoded twice in serialized form, because the // server expects a string serialize: function WBORec_serialize() { - this.payload = Svc.Json.encode([this.payload]); - let ret = Svc.Json.encode(this.data); - this.payload = Svc.Json.decode(this.payload)[0]; + this.payload = JSON.stringify([this.payload]); + let ret = JSON.stringify(this.data); + this.payload = JSON.parse(this.payload)[0]; return ret; }, deserialize: function WBORec_deserialize(json) { - this.data = Svc.Json.decode(json); - this.payload = Svc.Json.decode(this.payload)[0]; + this.data = JSON.parse(json); + this.payload = JSON.parse(this.payload)[0]; }, toString: function WBORec_toString() { @@ -134,7 +134,7 @@ WBORecord.prototype = { " parent: " + this.parentid + "\n" + " depth: " + this.depth + ", index: " + this.sortindex + "\n" + " modified: " + this.modified + "\n" + - " payload: " + Svc.Json.encode(this.payload) + " }"; + " payload: " + JSON.stringify(this.payload) + " }"; } }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b93ae58eb79a..5f6df4694c13 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -454,7 +454,7 @@ SyncEngine.prototype = { if (out.payload) // skip deleted records this._store.createMetaRecords(out.id, meta); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); - up.pushData(Svc.Json.decode(out.serialize())); // FIXME: inefficient + up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient } this._store.cache.enabled = true; diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 5365cd4891de..9e1a21b24f2b 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -178,7 +178,7 @@ ClientStore.prototype = { this._log.debug("Saving client list to disk"); let file = Utils.getProfileFile( {path: "weave/meta/clients.json", autoCreate: true}); - let out = Svc.Json.encode(this._clients); + let out = JSON.stringify(this._clients); let [fos] = Utils.open(file, ">"); fos.writeString(out); fos.close(); @@ -193,7 +193,7 @@ ClientStore.prototype = { let [is] = Utils.open(file, "<"); let json = Utils.readStream(is); is.close(); - this._clients = Svc.Json.decode(json); + this._clients = JSON.parse(json); } catch (e) { this._log.debug("Failed to load saved client list" + e); } diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 106a760b5e97..25e141acff9a 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -97,7 +97,7 @@ TabEngine.prototype = { * _addFennecTabsToRecord. Unify? */ if (Cc["@mozilla.org/browser/sessionstore;1"]) { let state = this._store._sessionStore.getBrowserState(); - let session = this._store._json.decode(state); + let session = JSON.parse(state); for (let i = 0; i < session.windows.length; i++) { let window = session.windows[i]; for (let j = 0; j < window.tabs.length; j++) { @@ -158,7 +158,7 @@ TabStore.prototype = { jsonObj[id] = this._remoteClients[id].toJson(); } let [fos] = Utils.open(file, ">"); - fos.writeString(this._json.encode(jsonObj)); + fos.writeString(JSON.stringify(jsonObj)); fos.close(); }, @@ -174,7 +174,7 @@ TabStore.prototype = { let [is] = Utils.open(file, "<"); let json = Utils.readStream(is); is.close(); - let jsonObj = this._json.decode(json); + let jsonObj = JSON.parse(json); for (let id in jsonObj) { this._remoteClients[id] = new TabSetRecord(); this._remoteClients[id].fromJson(jsonObj[id]); @@ -199,12 +199,6 @@ TabStore.prototype = { return this._windowMediator; }, - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() {return json;}); - return this._json; - }, - _createLocalClientTabSetRecord: function TabStore__createLocalTabSet() { // Test for existence of sessionStore. If it doesn't exist, then // use get _fennecTabs instead. @@ -235,7 +229,7 @@ TabStore.prototype = { let tab = tabChild.QueryInterface(Ci.nsIDOMNode); if (!tab) continue; - let tabState = this._json.decode(this._sessionStore.getTabState(tab)); + let tabState = JSON.parse(this._sessionStore.getTabState(tab)); // Skip empty (i.e. just-opened, no history yet) tabs: if (tabState.entries.length == 0) continue; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index ca8c9e45c1ef..b83d2ef52e5b 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -346,22 +346,16 @@ function JsonFilter() { this._log.level = Log4Moz.Level[level]; } JsonFilter.prototype = { - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return this._json; - }, - beforePUT: function JsonFilter_beforePUT(data) { let self = yield; this._log.trace("Encoding data as JSON"); - self.done(this._json.encode(data)); + self.done(JSON.stringify(data)); }, afterGET: function JsonFilter_afterGET(data) { let self = yield; this._log.trace("Decoding JSON data"); - self.done(this._json.decode(data)); + self.done(JSON.parse(data)); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 78d7e2b9cd29..ca0beb2ef57b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -478,7 +478,7 @@ WeaveSvc.prototype = { }; yield res.get(self.cb); - //Svc.Json.decode(res.data); // throws if not json + //JSON.parse(res.data); // throws if not json self.done(true); }; this._catchAll(this._notify("verify-login", "", fn)).async(this, onComplete); @@ -961,7 +961,7 @@ WeaveSvc.prototype = { yield res.get(self.cb); // Get the array of collections and delete each one - let allCollections = Svc.Json.decode(res.data); + let allCollections = JSON.parse(res.data); for each (let name in allCollections) { try { // If we have a list of engines, make sure it's one we want @@ -1198,9 +1198,9 @@ WeaveSvc.prototype = { let actionStr = command + "(" + args + ")"; // Convert args into a string to simplify array comparisons - let jsonArgs = Svc.Json.encode(args); + let jsonArgs = JSON.stringify(args); let notDupe = function(action) action.command != command || - Svc.Json.encode(action.args) != jsonArgs; + JSON.stringify(action.args) != jsonArgs; this._log.info("Sending clients: " + actionStr + "; " + commandData.desc); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index b4cd50aa46a9..db31dfb52830 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -64,14 +64,6 @@ Store.prototype = { // set this property in child object's wrap()! _lookup: null, - __json: null, - get _json() { - if (!this.__json) - this.__json = Cc["@mozilla.org/dom/json;1"]. - createInstance(Ci.nsIJSON); - return this.__json; - }, - get cache() { let cache = new Cache(); this.__defineGetter__("cache", function() cache); @@ -284,7 +276,7 @@ SnapshotStore.prototype = { let out = {version: this.version, GUID: this.GUID, snapshot: this.data}; - out = this._json.encode(out); + out = JSON.stringify(out); let [fos] = Utils.open(file, ">"); fos.writeString(out); @@ -300,7 +292,7 @@ SnapshotStore.prototype = { let [is] = Utils.open(file, "<"); let json = Utils.readStream(is); is.close(); - json = this._json.decode(json); + json = JSON.parse(json); if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { this._log.debug("Read saved snapshot from disk"); @@ -315,7 +307,7 @@ SnapshotStore.prototype = { }, serialize: function SStore_serialize() { - let json = this._json.encode(this.data); + let json = JSON.stringify(this.data); json = json.replace(/:{type/g, ":\n\t{type"); json = json.replace(/}, /g, "},\n "); json = json.replace(/, parentid/g, ",\n\t parentid"); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 0bd738d4fbe2..d1214180ecdd 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -67,12 +67,6 @@ Tracker.prototype = { _logName: "Tracker", file: "none", - get _json() { - let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - this.__defineGetter__("_json", function() json); - return json; - }, - _init: function T__init() { this._log = Log4Moz.repository.getLogger(this._logName); this._score = 0; @@ -124,7 +118,7 @@ Tracker.prototype = { let file = Utils.getProfileFile( {path: "weave/changes/" + this.file + ".json", autoCreate: true}); - let out = this._json.encode(this.changedIDs); + let out = JSON.stringify(this.changedIDs); let [fos] = Utils.open(file, ">"); fos.writeString(out); fos.close(); @@ -142,7 +136,7 @@ Tracker.prototype = { let json = Utils.readStream(is); is.close(); - let ids = this._json.decode(json); + let ids = JSON.parse(json); for (let id in ids) { this.changedIDs[id] = 1; } diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index adc8199e1641..68c4a7376874 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -566,7 +566,6 @@ Utils.EventListener.prototype = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); -Utils.lazyInstance(Svc, 'Json', "@mozilla.org/dom/json;1", Ci.nsIJSON); [["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 773b9f6b0412..b0ce7cf080db 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -11,35 +11,34 @@ try { } Function.prototype.async = Async.sugar; -let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); let keys, cryptoMeta, cryptoWrap; function pubkey_handler(metadata, response) { let obj = {id: "ignore-me", modified: keys.pubkey.modified, - payload: json.encode(keys.pubkey.payload)}; - return httpd_basic_auth_handler(json.encode(obj), metadata, response); + payload: JSON.stringify(keys.pubkey.payload)}; + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } function privkey_handler(metadata, response) { let obj = {id: "ignore-me-2", modified: keys.privkey.modified, - payload: json.encode(keys.privkey.payload)}; - return httpd_basic_auth_handler(json.encode(obj), metadata, response); + payload: JSON.stringify(keys.privkey.payload)}; + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } function crypted_resource_handler(metadata, response) { let obj = {id: "ignore-me-3", modified: cryptoWrap.modified, - payload: json.encode(cryptoWrap.payload)}; - return httpd_basic_auth_handler(json.encode(obj), metadata, response); + payload: JSON.stringify(cryptoWrap.payload)}; + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } function crypto_meta_handler(metadata, response) { let obj = {id: "ignore-me-4", modified: cryptoMeta.modified, - payload: json.encode(cryptoMeta.payload)}; - return httpd_basic_auth_handler(json.encode(obj), metadata, response); + payload: JSON.stringify(cryptoMeta.payload)}; + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } function async_test() { diff --git a/services/sync/tests/unit/test_records_keys.js b/services/sync/tests/unit/test_records_keys.js index 2b905a68409e..91a6c56f05e7 100644 --- a/services/sync/tests/unit/test_records_keys.js +++ b/services/sync/tests/unit/test_records_keys.js @@ -9,24 +9,22 @@ try { Function.prototype.async = Async.sugar; -let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - function pubkey_handler(metadata, response) { let obj = {id: "asdf-1234-asdf-1234", modified: "2454725.98283", - payload: json.encode({type: "pubkey", + payload: JSON.stringify({type: "pubkey", private_key: "http://localhost:8080/privkey", key_data: "asdfasdfasf..."})}; - return httpd_basic_auth_handler(json.encode(obj), metadata, response); + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } function privkey_handler(metadata, response) { let obj = {id: "asdf-1234-asdf-1234-2", modified: "2454725.98283", - payload: json.encode({type: "privkey", + payload: JSON.stringify({type: "privkey", public_key: "http://localhost:8080/pubkey", key_data: "asdfasdfasf..."})}; - return httpd_basic_auth_handler(json.encode(obj), metadata, response); + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } function async_test() { diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index 7a7215972225..501789e00d70 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -9,13 +9,11 @@ try { Function.prototype.async = Async.sugar; -let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - function record_handler(metadata, response) { let obj = {id: "asdf-1234-asdf-1234", modified: "2454725.98283", - payload: json.encode({cheese: "roquefort"})}; - return httpd_basic_auth_handler(json.encode(obj), metadata, response); + payload: JSON.stringify({cheese: "roquefort"})}; + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } function async_test() { From 12c8d58c279cc9d0cdff1dbda844933a8bac3796 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 31 Mar 2009 19:52:16 -0500 Subject: [PATCH 1045/1860] Bug 486230 - Share json disk caching with Utils.json{Load,Save} --- services/sync/modules/engines/clients.js | 23 ++-------- services/sync/modules/engines/tabs.js | 39 +++++------------ services/sync/modules/trackers.js | 31 ++----------- services/sync/modules/util.js | 55 ++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 75 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 9e1a21b24f2b..955462e91773 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -174,29 +174,14 @@ ClientStore.prototype = { // load/save clients list from/to disk + _snapshot: "meta/clients", + saveSnapshot: function ClientStore_saveSnapshot() { - this._log.debug("Saving client list to disk"); - let file = Utils.getProfileFile( - {path: "weave/meta/clients.json", autoCreate: true}); - let out = JSON.stringify(this._clients); - let [fos] = Utils.open(file, ">"); - fos.writeString(out); - fos.close(); + Utils.jsonSave(this._snapshot, this, this._clients); }, loadSnapshot: function ClientStore_loadSnapshot() { - let file = Utils.getProfileFile("weave/meta/clients.json"); - if (!file.exists()) - return; - this._log.debug("Loading client list from disk"); - try { - let [is] = Utils.open(file, "<"); - let json = Utils.readStream(is); - is.close(); - this._clients = JSON.parse(json); - } catch (e) { - this._log.debug("Failed to load saved client list" + e); - } + Utils.jsonLoad(this._snapshot, this, function(json) this._clients = json); }, // methods to apply changes: create, remove, update, changeItemID, wipe diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 25e141acff9a..3f37f97d5e8a 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -132,7 +132,7 @@ function TabStore() { TabStore.prototype = { __proto__: Store.prototype, _logName: "Tabs.Store", - _filePath: "weave/meta/tabSets.json", + _filePath: "meta/tabSets", _remoteClients: {}, _TabStore_init: function TabStore__init() { @@ -149,40 +149,21 @@ TabStore.prototype = { }, _writeToFile: function TabStore_writeToFile() { - // use JSON service to serialize the records... - this._log.debug("Writing out to file..."); - let file = Utils.getProfileFile( - {path: this._filePath, autoCreate: true}); - let jsonObj = {}; - for (let id in this._remoteClients) { - jsonObj[id] = this._remoteClients[id].toJson(); - } - let [fos] = Utils.open(file, ">"); - fos.writeString(JSON.stringify(jsonObj)); - fos.close(); + let json = {}; + for (let [id, val] in Iterator(this._remoteClients)) + json[id] = val.toJson(); + + Utils.jsonSave(this._filePath, this, json); }, _readFromFile: function TabStore_readFromFile() { - // use JSON service to un-serialize the records... - // call on initialization. - // Put stuff into remoteClients. - this._log.debug("Reading in from file..."); - let file = Utils.getProfileFile(this._filePath); - if (!file.exists()) - return; - try { - let [is] = Utils.open(file, "<"); - let json = Utils.readStream(is); - is.close(); - let jsonObj = JSON.parse(json); - for (let id in jsonObj) { + Utils.jsonLoad(this._filePath, this, function(json) { + for (let [id, val] in Iterator(json)) { this._remoteClients[id] = new TabSetRecord(); - this._remoteClients[id].fromJson(jsonObj[id]); + this._remoteClients[id].fromJson(val); this._remoteClients[id].id = id; } - } catch (e) { - this._log.warn("Failed to load saved tabs file" + e); - } + }); }, get _sessionStore() { diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index d1214180ecdd..b53ad9c05a7d 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -113,37 +113,14 @@ Tracker.prototype = { }, saveChangedIDs: function T_saveChangedIDs() { - this._log.debug("Saving changed IDs to disk"); - - let file = Utils.getProfileFile( - {path: "weave/changes/" + this.file + ".json", - autoCreate: true}); - let out = JSON.stringify(this.changedIDs); - let [fos] = Utils.open(file, ">"); - fos.writeString(out); - fos.close(); + Utils.jsonSave("changes/" + this.file, this, this.changedIDs); }, loadChangedIDs: function T_loadChangedIDs() { - let file = Utils.getProfileFile("weave/changes/" + this.file + ".json"); - if (!file.exists()) - return; - - this._log.debug("Loading previously changed IDs from disk"); - - try { - let [is] = Utils.open(file, "<"); - let json = Utils.readStream(is); - is.close(); - - let ids = JSON.parse(json); - for (let id in ids) { + Utils.jsonLoad("changes/" + this.file, this, function(json) { + for (let id in json) this.changedIDs[id] = 1; - } - } catch (e) { - this._log.warn("Could not load changed IDs from previous session"); - this._log.debug("Exception: " + e); - } + }); }, // ignore/unignore specific IDs. Useful for ignoring items that are diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 68c4a7376874..a3f6114a981e 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -386,6 +386,61 @@ let Utils = { return tmp; }, + /** + * Load a json object from disk + * + * @param filePath + * Json file path load from weave/[filePath].json + * @param that + * Object to use for logging and "this" for callback + * @param callback + * Function to process json object as its first parameter + */ + jsonLoad: function Utils_jsonLoad(filePath, that, callback) { + filePath = "weave/" + filePath + ".json"; + if (that._log) + that._log.debug("Loading json from disk: " + filePath); + + let file = Utils.getProfileFile(filePath); + if (!file.exists()) + return; + + try { + let [is] = Utils.open(file, "<"); + let json = Utils.readStream(is); + is.close(); + callback.call(that, JSON.parse(json)); + } + catch (ex) { + if (that._log) + that._log.debug("Failed to load json: " + ex); + } + }, + + /** + * Save a json-able object to disk + * + * @param filePath + * Json file path save to weave/[filePath].json + * @param that + * Object to use for logging and "this" for callback + * @param callback + * Function to provide json-able object to save. If this isn't a + * function, it'll be used as the object to make a json string. + */ + jsonSave: function Utils_jsonSave(filePath, that, callback) { + filePath = "weave/" + filePath + ".json"; + if (that._log) + that._log.debug("Saving json to disk: " + filePath); + + let file = Utils.getProfileFile({ autoCreate: true, path: filePath }); + let json = typeof callback == "function" ? callback.call(that) : callback; + let out = JSON.stringify(json); + let [fos] = Utils.open(file, ">"); + fos.writeString(out); + fos.close(); + }, + // Returns a timer that is scheduled to call the given callback as // soon as possible. makeTimerForCall: function makeTimerForCall(cb) { From 5bce0fe53f87f0a6f188c8c35d1efb1fdfef87de Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 31 Mar 2009 20:07:43 -0500 Subject: [PATCH 1046/1860] Bug 486234 - Give changes json file name for history, passwords --- services/sync/modules/engines/history.js | 1 + services/sync/modules/engines/passwords.js | 1 + 2 files changed, 2 insertions(+) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index d0ad2b04ea8b..14917a0439cb 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -473,6 +473,7 @@ function HistoryTracker() { HistoryTracker.prototype = { __proto__: Tracker.prototype, _logName: "HistoryTracker", + file: "history", QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]), diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 8bfaee5add5f..efdb65e1835b 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -227,6 +227,7 @@ function PasswordTracker() { PasswordTracker.prototype = { __proto__: Tracker.prototype, _logName: "PasswordTracker", + file: "password", _init: function PasswordTracker_init() { Tracker.prototype._init.call(this); From 260cdd20c324318b56e0bec65158ee97833e8382 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 1 Apr 2009 01:56:32 -0500 Subject: [PATCH 1047/1860] Bug 482896 - Unify local client data and remote client data storage Initialize unified client store from disk json and local prefs and update on pref changes + sync. Clean up client engine code (local vs inherited, alphabetical), and create a helper modify() to save snapshots. --- services/sync/modules/engines/clients.js | 183 ++++++++++++----------- services/sync/modules/stores.js | 14 +- 2 files changed, 111 insertions(+), 86 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 955462e91773..a0a4a61e691a 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -122,13 +122,26 @@ ClientEngine.prototype = { get clientType() { return Svc.Prefs.get("client.type", "desktop"); }, set clientType(value) { Svc.Prefs.set("client.type", value); }, + updateLocalInfo: function ClientEngine_updateLocalInfo(info) { + // Grab data from the store if we weren't given something to start with + if (!info) + info = this.getInfo(this.clientID); + + // Overwrite any existing values with the ones from the pref + info.name = this.clientName; + info.type = this.clientType; + + return info; + }, + observe: function ClientEngine_observe(subject, topic, data) { switch (topic) { case "nsPref:changed": switch (data) { case "client.name": case "client.type": - this._tracker.addChangedID(this.clientID); + // Update the store and tracker on pref changes + this.setInfo(this.clientID, this.updateLocalInfo()); break; } break; @@ -143,113 +156,113 @@ ClientEngine.prototype = { }; function ClientStore() { - this._ClientStore_init(); + this.init(); } ClientStore.prototype = { + ////////////////////////////////////////////////////////////////////////////// + // ClientStore Attributes + + clients: {}, + __proto__: Store.prototype, - _logName: "Clients.Store", - - _ClientStore_init: function ClientStore__init() { - this._init.call(this); - this._clients = {}; - this.loadSnapshot(); - }, - - get clients() { - this._clients[Clients.clientID] = this.createRecord(Clients.clientID).payload; - return this._clients; - }, - - // get/set client info; doesn't use records like the methods below - // NOTE: setInfo will not update tracker. Use Engine.setInfo for that - - getInfo: function ClientStore_getInfo(id) { - return this._clients[id]; - }, - - setInfo: function ClientStore_setInfo(id, info) { - this._clients[id] = info; - this.saveSnapshot(); - }, - - // load/save clients list from/to disk _snapshot: "meta/clients", - saveSnapshot: function ClientStore_saveSnapshot() { - Utils.jsonSave(this._snapshot, this, this._clients); + ////////////////////////////////////////////////////////////////////////////// + // ClientStore Methods + + /** + * Get the client by guid + */ + getInfo: function ClientStore_getInfo(id) this.clients[id], + + /** + * Initialize parent class then load client data from disk + */ + init: function ClientStore_init() { + this._init.call(this); + this.loadSnapshot(); + + // Get fresh local client info in case prefs were changed when closed + let id = Clients.clientID; + this.setInfo(id, Clients.updateLocalInfo(this.clients[id] || {})); }, + /** + * Load client data from json disk + */ loadSnapshot: function ClientStore_loadSnapshot() { - Utils.jsonLoad(this._snapshot, this, function(json) this._clients = json); + Utils.jsonLoad(this._snapshot, this, function(json) this.clients = json); }, - // methods to apply changes: create, remove, update, changeItemID, wipe + /** + * Log that we're about to change the client store then save to disk + */ + modify: function ClientStore_modify(message, action) { + this._log.debug(message); + action.call(this); + this.saveSnapshot(); + }, - applyIncoming: function ClientStore_applyIncoming(onComplete, record) { - let fn = function(rec) { - let self = yield; - if (!rec.payload) - this.remove(rec); - else if (!this.itemExists(rec.id)) - this.create(rec); - else - this.update(rec); - }; - fn.async(this, onComplete, record); + /** + * Save client data to json disk + */ + saveSnapshot: function ClientStore_saveSnapshot() { + Utils.jsonSave(this._snapshot, this, this.clients); + }, + + /** + * Set the client data for a guid. Use Engine.setInfo to update tracker. + */ + setInfo: function ClientStore_setInfo(id, info) { + this.modify("Setting client " + id + ": " + JSON.stringify(info), + function() this.clients[id] = info); + }, + + ////////////////////////////////////////////////////////////////////////////// + // Store.prototype Attributes + + _logName: "Clients.Store", + + ////////////////////////////////////////////////////////////////////////////// + // Store.prototype Methods + + changeItemID: function ClientStore_changeItemID(oldID, newID) { + this.modify("Changing id from " + oldId + " to " + newID, function() { + this.clients[newID] = this.clients[oldID]; + delete this.clients[oldID]; + }); }, create: function ClientStore_create(record) { this.update(record); }, - update: function ClientStore_update(record) { - this._log.debug("Updating client " + record.id); - this._clients[record.id] = record.payload; - this.saveSnapshot(); - }, - - remove: function ClientStore_remove(record) { - this._log.debug("Removing client " + record.id); - delete this._clients[record.id]; - this.saveSnapshot(); - }, - - changeItemID: function ClientStore_changeItemID(oldID, newID) { - this._clients[newID] = this._clients[oldID]; - delete this._clients[oldID]; - this.saveSnapshot(); - }, - - wipe: function ClientStore_wipe() { - this._log.debug("Wiping local clients store"); - this._clients = {}; - this.saveSnapshot(); - }, - - // methods to query local data: getAllIDs, itemExists, createRecord - - getAllIDs: function ClientStore_getAllIDs() { - return this.clients; - }, - - itemExists: function ClientStore_itemExists(id) { - return id in this._clients; - }, - createRecord: function ClientStore_createRecord(id) { let record = new ClientRecord(); record.id = id; - record.payload = this._clients[id] || {}; - - // For the local client, update the name and type with the current value - if (id == Clients.clientID) { - record.payload.name = Clients.clientName; - record.payload.type = Clients.clientType; - } + record.payload = this.clients[id]; return record; - } + }, + + getAllIDs: function ClientStore_getAllIDs() this.clients, + + itemExists: function ClientStore_itemExists(id) id in this.clients, + + remove: function ClientStore_remove(record) { + this.modify("Removing client " + record.id, function() + delete this.clients[record.id]); + }, + + update: function ClientStore_update(record) { + this.modify("Updating client " + record.id, function() + this.clients[record.id] = record.payload); + }, + + wipe: function ClientStore_wipe() { + this.modify("Wiping local clients store", function() this.clients = {}); + }, }; function ClientTracker() { diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index db31dfb52830..8b24ff057366 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -74,7 +74,7 @@ Store.prototype = { this._log = Log4Moz.repository.getLogger("Store." + this._logName); }, - applyIncoming: function BStore_applyIncoming(onComplete, record) { + applyIncoming: function Store_applyIncoming(onComplete, record) { let fn = function(rec) { let self = yield; if (rec.payload == null) @@ -89,6 +89,18 @@ Store.prototype = { // override these in derived objects + create: function Store_create(record) { + throw "override create in a subclass"; + }, + + remove: function Store_remove(record) { + throw "override remove in a subclass"; + }, + + update: function Store_update(record) { + throw "override update in a subclass"; + }, + itemExists: function Store_itemExists(id) { throw "override itemExists in a subclass"; }, From baf67a35fb9dad3491c893edf0075cf7ac04f914 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 1 Apr 2009 17:12:08 -0500 Subject: [PATCH 1048/1860] Bug 480929 - RSS names not synced Set the title in the record just like any other bookmark (folder) --- services/sync/modules/engines/bookmarks.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 8634eb8dd4a5..a918d15eddeb 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -519,8 +519,9 @@ BookmarksStore.prototype = { } else { record = new BookmarkFolder(); - record.title = this._bms.getItemTitle(placeId); } + + record.title = this._bms.getItemTitle(placeId); break; case this._bms.TYPE_SEPARATOR: From 795b73fa82240e6e0ced8af15f3430efbf7b2e02 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 1 Apr 2009 19:00:28 -0500 Subject: [PATCH 1049/1860] Bug 445186 - Weave Sign-In Dialog "Cancel" button doesn't actually cancel anything, while I'm signing in Just relabel Cancel to Hide (but keep around the original (localized) label in case we fail) --- services/sync/locales/en-US/login.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties index f3ef17747556..664bf09fedad 100644 --- a/services/sync/locales/en-US/login.properties +++ b/services/sync/locales/en-US/login.properties @@ -4,4 +4,5 @@ noPassphrase.alert = You must enter a passphrase samePasswordAndPassphrase.alert = Your password and passphrase must be different loginStart.label = Signing in, please wait... loginError.label = Invalid username and/or password. -loginSuccess.label = Signed In \ No newline at end of file +loginSuccess.label = Signed In +hide.label = Hide From 4f96e8c986602756cb64e86afeba10ddc1f892d3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 1 Apr 2009 21:21:27 -0700 Subject: [PATCH 1050/1860] fix typo in prefs pane --- services/sync/locales/en-US/preferences.dtd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 01fd6702dd07..252ad06f3334 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -41,7 +41,7 @@ - + @@ -110,4 +110,4 @@ - \ No newline at end of file + From 26a4641aea041d956ee1cb112db09bfc79306492 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 1 Apr 2009 22:53:09 -0700 Subject: [PATCH 1051/1860] really fix pref pane typo --- services/sync/locales/en-US/preferences.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 252ad06f3334..7cd4913d06e9 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -41,7 +41,7 @@ - + From 4b89c24175facbaef5daba12f78f09eddd18d6a2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 1 Apr 2009 23:26:54 -0700 Subject: [PATCH 1052/1860] dos->unix line endings --- services/sync/locales/en-US/wizard.properties | 160 +++++++++--------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 4df96512d8e7..87a1e1664d19 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -1,80 +1,80 @@ -verify-progress.label = Verifying username and password -verify-error.label = Invalid username and password -verify-success.label = Username and password verified -passphrase-progress.label = Verifying passphrase -passphrase-success.label = Passphrase verified -passphrase-error.label = Invalid passphrase - -serverError.label = Server Error -serverTimeoutError.label = Server Timeout - - -createUsername-progress.label = Checking username -createUsername-error.label = %S is already taken. Please choose another. -createUsername-success.label = %S is available. - -email-progress.label = Checking email -email-unavailable.label = %S is already taken. Please choose another. -email-invalid.label = Please re-enter a valid email address. -email-success.label = - - -passwordsUnmatched.label = Passwords do not match -passphrasesUnmatched.label = Passphrases do not match. -samePasswordAndPassphrase.label = Your password and passphrase must be different. - - - -missingCaptchaResponse.label = Please enter the words in the Captcha box. -incorrectCaptcha.label = Incorrect Captcha response. Try again. - - - -create-progress.label = Creating your account -create-uid-inuse.label = Username in use -create-uid-missing.label = Username missing -create-uid-invalid.label = Username invalid -create-mail-invalid.label = Email invalid -create-mail-inuse.label = Emali in use -create-captcha-missing.label = Captcha response missing -create-password-missing.label = Password missing -create-password-incorrect.label = Password incorrect - -create-success.label = Account for %S created. - -final-pref-value.label = %S -final-account-value.label = Username: %S -final-sync-value.label = Backup and synchronization of browser settings and metadata. -final-success.label = Weave was successfully installed. - -default-name.label = %S's Firefox -default-name-nouser.label = Firefox - -bookmarks.label = Bookmarks -history.label = Browsing History -tabs.label = Tabs -passwords.label = Saved Passwords -forms.label = Saved Form Data -cookies.label = Cookies - -initialLogin-progress.label = Signing you in -initialLogin-error.label = Problem signing in. - -initialPrefs-progress.label = Setting your preferences - -initialSync-progress.label = Synchronizing your data -initialSync-error.label = Problem syncing data. - -installation-complete.label = Installation complete. - - -tryAgain.text = Try again now. -clickHere.text = Click here - -data-verify.title = Data (Step 2 of 3) -data-create.title = Data (Step 4 of 5) -final-verify.title = Finish (Step 3 of 3) -final-create.title = Finish (Step 5 of 5) - -registration-closed.title = Registration Closed -registration-closed.label = Sorry, but we've reached our account limit for this round of alpha testing. Please try again in a few days. +verify-progress.label = Verifying username and password +verify-error.label = Invalid username and password +verify-success.label = Username and password verified +passphrase-progress.label = Verifying passphrase +passphrase-success.label = Passphrase verified +passphrase-error.label = Invalid passphrase + +serverError.label = Server Error +serverTimeoutError.label = Server Timeout + + +createUsername-progress.label = Checking username +createUsername-error.label = %S is already taken. Please choose another. +createUsername-success.label = %S is available. + +email-progress.label = Checking email +email-unavailable.label = %S is already taken. Please choose another. +email-invalid.label = Please re-enter a valid email address. +email-success.label = + + +passwordsUnmatched.label = Passwords do not match +passphrasesUnmatched.label = Passphrases do not match. +samePasswordAndPassphrase.label = Your password and passphrase must be different. + + + +missingCaptchaResponse.label = Please enter the words in the Captcha box. +incorrectCaptcha.label = Incorrect Captcha response. Try again. + + + +create-progress.label = Creating your account +create-uid-inuse.label = Username in use +create-uid-missing.label = Username missing +create-uid-invalid.label = Username invalid +create-mail-invalid.label = Email invalid +create-mail-inuse.label = Emali in use +create-captcha-missing.label = Captcha response missing +create-password-missing.label = Password missing +create-password-incorrect.label = Password incorrect + +create-success.label = Account for %S created. + +final-pref-value.label = %S +final-account-value.label = Username: %S +final-sync-value.label = Backup and synchronization of browser settings and metadata. +final-success.label = Weave was successfully installed. + +default-name.label = %S's Firefox +default-name-nouser.label = Firefox + +bookmarks.label = Bookmarks +history.label = Browsing History +tabs.label = Tabs +passwords.label = Saved Passwords +forms.label = Saved Form Data +cookies.label = Cookies + +initialLogin-progress.label = Signing you in +initialLogin-error.label = Problem signing in. + +initialPrefs-progress.label = Setting your preferences + +initialSync-progress.label = Synchronizing your data +initialSync-error.label = Problem syncing data. + +installation-complete.label = Installation complete. + + +tryAgain.text = Try again now. +clickHere.text = Click here + +data-verify.title = Data (Step 2 of 3) +data-create.title = Data (Step 4 of 5) +final-verify.title = Finish (Step 3 of 3) +final-create.title = Finish (Step 5 of 5) + +registration-closed.title = Registration Closed +registration-closed.label = Sorry, but we've reached our account limit for this round of alpha testing. Please try again in a few days. From 06ee61460be7ef4d01a1d166df4285ad64a730e9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 3 Apr 2009 09:26:12 -0500 Subject: [PATCH 1053/1860] Bug 486667 - Clients get amnesia -- forget who they are after a wipe Set the local client info after resetting the clients store hash --- services/sync/modules/engines/clients.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index a0a4a61e691a..ee8c2ec52594 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -261,7 +261,12 @@ ClientStore.prototype = { }, wipe: function ClientStore_wipe() { - this.modify("Wiping local clients store", function() this.clients = {}); + this.modify("Wiping local clients store", function() { + this.clients = {}; + + // Make sure the local client is still here + this.clients[Clients.clientID] = Clients.updateLocalInfo({}); + }); }, }; From 0cb23173e862b81258e7012497f97044355cc8eb Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 3 Apr 2009 12:38:47 -0500 Subject: [PATCH 1054/1860] Bug 482670 - WBORecord should implement a .deleted property Expose a .deleted property that engines can set to true to store an even thinner deleted payload (empty string "" instead of "[null]") on the server. Handle deserializing of deleted records by setting the property. Note: Engines must set their payloads to something JSON-able if it's not a delete record. --- services/sync/modules/base_records/crypto.js | 22 +++++------ services/sync/modules/base_records/wbo.js | 41 ++++++++++++++------ services/sync/modules/engines.js | 6 +-- services/sync/modules/engines/bookmarks.js | 2 +- services/sync/modules/engines/history.js | 6 +-- services/sync/modules/engines/passwords.js | 5 +-- services/sync/modules/stores.js | 2 +- 7 files changed, 50 insertions(+), 34 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 6fed91748e1e..4c0e64b891c9 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -84,8 +84,8 @@ CryptoWrapper.prototype = { _encrypt: function CryptoWrap__encrypt(passphrase) { let self = yield; - // Don't encrypt empty payloads - if (!this.payload) { + // No need to encrypt deleted records + if (this.deleted) { self.done(); return; } @@ -109,8 +109,8 @@ CryptoWrapper.prototype = { _decrypt: function CryptoWrap__decrypt(passphrase) { let self = yield; - // Empty payloads aren't encrypted - if (!this.payload) { + // Deleted records aren't encrypted + if (this.deleted) { self.done(); return; } @@ -132,13 +132,13 @@ CryptoWrapper.prototype = { this._decrypt.async(this, onComplete, passphrase); }, - toString: function WBORec_toString() { - return "{ id: " + this.id + "\n" + - " parent: " + this.parentid + "\n" + - " depth: " + this.depth + ", index: " + this.sortindex + "\n" + - " modified: " + this.modified + "\n" + - " payload: " + JSON.stringify(this.cleartext) + " }"; - } + toString: function CryptoWrap_toString() "{ " + [ + "id: " + this.id, + "parent: " + this.parentid, + "depth: " + this.depth + ", index: " + this.sortindex, + "modified: " + this.modified, + "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.cleartext)) + ].join("\n ") + " }", }; function CryptoMeta(uri) { diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index b468e6c62228..05e95870a39b 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -52,8 +52,16 @@ function WBORecord(uri) { this._WBORec_init(uri); } WBORecord.prototype = { + ////////////////////////////////////////////////////////////////////////////// + // WBORecord Attributes + + deleted: false, + _logName: "Record.WBO", + ////////////////////////////////////////////////////////////////////////////// + // WBORecord Methods + _WBORec_init: function WBORec_init(uri) { this.data = { payload: {} @@ -115,27 +123,36 @@ WBORecord.prototype = { this.data.payload = value; }, - // payload is encoded twice in serialized form, because the - // server expects a string serialize: function WBORec_serialize() { - this.payload = JSON.stringify([this.payload]); + // Convert the payload into a string because the server expects that + let payload = this.payload; + this.payload = this.deleted ? "" : JSON.stringify(payload); + let ret = JSON.stringify(this.data); - this.payload = JSON.parse(this.payload)[0]; + + // Restore the original payload + this.payload = payload; + return ret; }, deserialize: function WBORec_deserialize(json) { this.data = JSON.parse(json); - this.payload = JSON.parse(this.payload)[0]; + + // Empty string payloads are deleted records + if (this.payload === "") + this.deleted = true; + else + this.payload = JSON.parse(this.payload); }, - toString: function WBORec_toString() { - return "{ id: " + this.id + "\n" + - " parent: " + this.parentid + "\n" + - " depth: " + this.depth + ", index: " + this.sortindex + "\n" + - " modified: " + this.modified + "\n" + - " payload: " + JSON.stringify(this.payload) + " }"; - } + toString: function WBORec_toString() "{ " + [ + "id: " + this.id, + "parent: " + this.parentid, + "depth: " + this.depth + ", index: " + this.sortindex, + "modified: " + this.modified, + "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.payload)) + ].join("\n ") + " }", }; Utils.lazy(this, 'Records', RecordManager); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5f6df4694c13..9d8c6bbab8df 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -246,7 +246,7 @@ SyncEngine.prototype = { if (a.depth != b.depth) return false; // note: sortindex ignored - if (a.payload == null || b.payload == null) // deleted items + if (a.deleted || b.deleted) return false; return Utils.deepEquals(a.cleartext, b.cleartext); }, @@ -396,7 +396,7 @@ SyncEngine.prototype = { // If the incoming item has been deleted, skip step 3 this._log.trace("Reconcile step 2.5"); - if (item.payload === null) { + if (item.deleted) { self.done(true); return; } @@ -451,7 +451,7 @@ SyncEngine.prototype = { for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); this._log.trace("Outgoing:\n" + out); - if (out.payload) // skip deleted records + if (!out.deleted) this._store.createMetaRecords(out.id, meta); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index a918d15eddeb..da86313872e8 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -487,7 +487,7 @@ BookmarksStore.prototype = { if (placeId <= 0) { // deleted item record = new PlacesItem(); record.id = guid; - record.payload = null; + record.deleted = true; this.cache.put(guid, record); return record; } diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 14917a0439cb..93cd5ca24f60 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -89,7 +89,7 @@ HistoryEngine.prototype = { } // Step 2: Check if the item is deleted - we don't support that (yet?) - if (item.cleartext == null) { + if (item.deleted) { self.done(false); return; } @@ -455,9 +455,9 @@ HistoryStore.prototype = { record.title = foo.title; record.visits = this._getVisits(record.histUri); record.encryption = cryptoMetaURL; - } else { - record.payload = null; // deleted item } + else + record.deleted = true; this.cache.put(guid, record); return record; }, diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index efdb65e1835b..4594da5dc8d0 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -180,10 +180,9 @@ PasswordStore.prototype = { record.password = login.password; record.usernameField = login.usernameField; record.passwordField = login.passwordField; - } else { - /* Deleted item */ - record.payload = null; } + else + record.deleted = true; return record; }, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 8b24ff057366..7725e5c2aeb1 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -77,7 +77,7 @@ Store.prototype = { applyIncoming: function Store_applyIncoming(onComplete, record) { let fn = function(rec) { let self = yield; - if (rec.payload == null) + if (rec.deleted) this.remove(rec); else if (!this.itemExists(rec.id)) this.create(rec); From 18f94912adcba63a5aee11f6bccdaac205c29427 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 3 Apr 2009 13:49:22 -0500 Subject: [PATCH 1055/1860] Backed out changeset 430ce13b63f3 (bug 482878) Bug 482670 restored un-wrapped payloads, so until a version bump, those using trunk will need to do a manual server wipe. --- services/sync/modules/service.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ca0beb2ef57b..b0d591a6c743 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -612,12 +612,6 @@ WeaveSvc.prototype = { let meta = yield Records.import(self.cb, this.clusterURL + this.username + "/meta/global"); - // XXX Bug 482878 Old payloads weren't array-wrapped, so migrate by wiping - if (meta && meta.payload == null) { - this._log.debug("Migrating to minimal payloads by wiping the server"); - meta = null; - } - let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; From e51b6071894b1fcad70f8317491b4d4f15d5f6ef Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 6 Apr 2009 19:05:16 +0200 Subject: [PATCH 1056/1860] Update form sync engine to work with Weave0.3/FF3.5 --- services/sync/modules/engines/forms.js | 239 ++++++++++---------- services/sync/modules/type_records/forms.js | 75 ++++++ 2 files changed, 196 insertions(+), 118 deletions(-) create mode 100644 services/sync/modules/type_records/forms.js diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index d39ffec29143..e5de6384879b 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -40,13 +40,15 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/type_records/forms.js"); Function.prototype.async = Async.sugar; @@ -55,23 +57,34 @@ function FormEngine() { } FormEngine.prototype = { __proto__: SyncEngine.prototype, - get _super() SyncEngine.prototype, - - get name() "forms", - get displayName() "Forms", - get logName() "Forms", - get serverPrefix() "user-data/forms/", + name: "forms", + displayname: "Forms", + logName: "Forms", + _storeObj: FormStore, + _trackerObj: FormTracker, + _recordObj: FormRec, - get _store() { - let store = new FormStore(); - this.__defineGetter__("_store", function() store); - return store; + _syncStartup: function FormEngine__syncStartup() { + let self = yield; + this._store.cacheFormItems(); + yield SyncEngine.prototype._syncStartup.async(this, self.cb); }, - get _tracker() { - let tracker = new FormsTracker(); - this.__defineGetter__("_tracker", function() tracker); - return tracker; + /* Wipe cache when sync finishes */ + _syncFinish: function FormEngine__syncFinish() { + let self = yield; + this._store.clearFormCache(); + yield SyncEngine.prototype._syncFinish.async(this, self.cb); + }, + + _recordLike: function SyncEngine__recordLike(a, b) { + if (a.cleartext == null || b.cleartext == null) + return false; + if (a.cleartext.name == b.cleartext.name && + a.cleartext.value == b.cleartext.value) { + return true; + } + return false; } }; @@ -82,7 +95,7 @@ function FormStore() { FormStore.prototype = { __proto__: Store.prototype, _logName: "FormStore", - _lookup: null, + _formItems: null, get _formDB() { let file = Cc["@mozilla.org/file/directory_service;1"]. @@ -109,33 +122,19 @@ FormStore.prototype = { this.__defineGetter__("_formStatement", function() stmnt); return stmnt; }, - - create: function FormStore_create(record) { - this._log.debug("Got create record: " + record.id); - this._formHistory.addEntry(record.cleartext.name, record.cleartext.value); + + cacheFormItems: function FormStore_cacheFormItems() { + this._log.debug("Caching all form items"); + this._formItems = this.getAllIDs(); }, - - remove: function FormStore_remove(record) { - this._log.debug("Got remove record: " + record.id); - - if (record.id in this._lookup) { - let data = this._lookup[record.id]; - } else { - this._log.warn("Invalid GUID found, ignoring remove request."); - return; - } - - this._formHistory.removeEntry(data.name, data.value); - delete this._lookup[record.id]; + + clearFormCache: function FormStore_clearFormCache() { + this._log.debug("Clearing form cache"); + this._formItems = null; }, - - update: function FormStore__editCommand(record) { - this._log.debug("Got update record: " + record.id); - this._log.warn("Ignoring update request"); - }, - - wrap: function FormStore_wrap() { - this._lookup = {}; + + getAllIDs: function FormStore_getAllIDs() { + let items = {}; let stmnt = this._formStatement; while (stmnt.executeStep()) { @@ -143,23 +142,56 @@ FormStore.prototype = { let val = stmnt.getUTF8String(2); let key = Utils.sha1(nam + val); - this._lookup[key] = { name: nam, value: val }; + items[key] = { name: nam, value: val }; } stmnt.reset(); + + return items; + }, + + changeItemID: function FormStore_changeItemID(oldID, newID) { + this._log.warn("FormStore IDs are data-dependent, cannot change!"); + }, + + itemExists: function FormStore_itemExists(id) { + return (id in this._formItems); + }, + + createRecord: function FormStore_createRecord(guid, cryptoMetaURL) { + let record = new FormRec(); + record.id = guid; - return this._lookup; + if (guid in this._formItems) { + let item = this._formItems[guid]; + record.encryption = cryptoMetaURL; + record.name = item.name; + record.value = item.value; + } else { + record.deleted = true; + } + + return record; + }, + + create: function FormStore_create(record) { + this._log.debug("Adding form record for " + record.name); + this._formHistory.addEntry(record.name, record.value); }, - wrapItem: function FormStore_wrapItem(id) { - if (!this._lookup) - this._lookup = this.wrap(); - return this._lookup[id]; + remove: function FormStore_remove(record) { + this._log.debug("Removing form record: " + record.id); + + if (record.id in this._formItems) { + let item = this._formItems[record.id]; + this._formHistory.removeEntry(item.name, item.value); + return; + } + + this._log.warn("Invalid GUID found, ignoring remove request."); }, - getAllIDs: function FormStore_getAllIDs() { - if (!this._lookup) - this._lookup = this.wrap(); - return this._lookup; + update: function FormStore_update(record) { + this._log.warn("Ignoring form record update request!"); }, wipe: function FormStore_wipe() { @@ -167,79 +199,50 @@ FormStore.prototype = { } }; -function FormsTracker() { +function FormTracker() { this._init(); } -FormsTracker.prototype = { +FormTracker.prototype = { __proto__: Tracker.prototype, - _logName: "FormsTracker", - - get _formDB() { - let file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - - file.append("formhistory.sqlite"); - let stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - - let formDB = stor.openDatabase(file); - this.__defineGetter__("_formDB", function() formDB); - return formDB; + _logName: "FormTracker", + file: "form", + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), + + __observerService: null, + get _observerService() { + if (!this.__observerService) + this.__observerService = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + return this.__observerService; }, - get _formStatement() { - let stmnt = this._formDB. - createStatement("SELECT COUNT(fieldname) FROM moz_formhistory"); - this.__defineGetter__("_formStatement", function() stmnt); - return stmnt; - }, - - /* nsIFormSubmitObserver is not available in JS. - * To calculate scores, we instead just count the changes in - * the database since the last time we were asked. - * - * FIXME!: Buggy, because changes in a row doesn't result in - * an increment of our score. A possible fix is to do a - * SELECT for each fieldname and compare those instead of the - * whole row count. - * - * Each change is worth 2 points. At some point, we may - * want to differentiate between search-history rows and other - * form items, and assign different scores. - */ - _rowCount: 0, - get score() { - let stmnt = this._formStatement; - stmnt.executeStep(); - - let count = stmnt.getInt32(0); - this._score = Math.abs(this._rowCount - count) * 2; - stmnt.reset(); - - if (this._score >= 100) - return 100; - else - return this._score; - }, - - resetScore: function FormsTracker_resetScore() { - let stmnt = this._formStatement; - stmnt.executeStep(); - - this._rowCount = stmnt.getInt32(0); - this._score = 0; - stmnt.reset(); - }, - - _init: function FormsTracker__init() { + _init: function FormTracker__init() { this.__proto__.__proto__._init.call(this); - - let stmnt = this._formStatement; - stmnt.executeStep(); + this._log.trace("FormTracker initializing!"); + this._observerService.addObserver(this, "earlyformsubmit", false); + }, + + /* 10 points per form element */ + notify: function FormTracker_notify(formElement, aWindow, actionURI) { + if (this.ignoreAll) + return; + + this._log.trace("Form submission notification for " + actionURI.spec); - this._rowCount = stmnt.getInt32(0); - stmnt.reset(); + /* Get number of elements in form, add points and changedIDs */ + let len = formElement.length; + let elements = formElement.elements; + for (let i = 0; i < len; i++) { + let element = elements.item(i); + let inputElement = element.QueryInterface(Ci.nsIDOMHTMLInputElement); + + if (inputElement && inputElement.type == "text") { + this._log.trace("Logging form element: " + inputElement.name + "::" + + inputElement.value); + this.addChangedID(Utils.sha1(inputElement.name + inputElement.value)); + this._score += 10; + } + } } }; - diff --git a/services/sync/modules/type_records/forms.js b/services/sync/modules/type_records/forms.js new file mode 100644 index 000000000000..9a9a85ab8d03 --- /dev/null +++ b/services/sync/modules/type_records/forms.js @@ -0,0 +1,75 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Anant Narayanan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['FormRec']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/keys.js"); + +Function.prototype.async = Async.sugar; + +function FormRec(uri) { + this._FormRec_init(uri); +} +FormRec.prototype = { + __proto__: CryptoWrapper.prototype, + _logName: "Record.Form", + + _FormRec_init: function FormRec_init(uri) { + this._CryptoWrap_init(uri); + this.cleartext = { + }; + }, + + get name() this.cleartext.name, + set name(p) { + this.cleartext.name = p; + }, + + get value() this.cleartext.value, + set value(p) { + this.cleartext.value = p; + } +}; From c04c284502ec921814c9fcff6105d1541fc7ee75 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 6 Apr 2009 19:31:39 +0200 Subject: [PATCH 1057/1860] Fix indentation --- services/sync/modules/base_records/wbo.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 05e95870a39b..bf76ea89c370 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -52,16 +52,9 @@ function WBORecord(uri) { this._WBORec_init(uri); } WBORecord.prototype = { - ////////////////////////////////////////////////////////////////////////////// - // WBORecord Attributes - deleted: false, - _logName: "Record.WBO", - ////////////////////////////////////////////////////////////////////////////// - // WBORecord Methods - _WBORec_init: function WBORec_init(uri) { this.data = { payload: {} @@ -182,8 +175,8 @@ RecordManager.prototype = { yield this.lastResource.get(self.cb); record = new this._recordType(); - record.deserialize(this.lastResource.data); - record.uri = url; // NOTE: may override id in this.lastResource.data + record.deserialize(this.lastResource.data); + record.uri = url; // NOTE: may override id in this.lastResource.data this.set(url, record); } catch (e) { @@ -207,12 +200,12 @@ RecordManager.prototype = { * always use the string, not the object, as a key. TODO: use the * string as key for this._aliases as well? (Don't know) */ if (url in this._aliases) - url = this._aliases[url]; + url = this._aliases[url]; if (spec in this._records) - record = this._records[spec]; + record = this._records[spec]; if (!record) - record = yield this.import(self.cb, url); + record = yield this.import(self.cb, url); self.done(record); }; From 81cc68c73ba43a1ca8f4b36b2b671a69b455626e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 7 Apr 2009 16:45:29 -0500 Subject: [PATCH 1058/1860] Add a Utils.isArray and use it --- services/sync/modules/engines.js | 2 +- services/sync/modules/util.js | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9d8c6bbab8df..82e24f73bbbc 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -74,7 +74,7 @@ function EngineManagerSvc() { EngineManagerSvc.prototype = { get: function EngMgr_get(name) { // Return an array of engines if we have an array of names - if (name.constructor.name == "Array") { + if (Utils.isArray(name)) { let engines = []; name.forEach(function(name) { let engine = this.get(name); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index a3f6114a981e..950a3bf3419d 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -122,6 +122,16 @@ let Utils = { lm.addLogin(login); }, + /** + * Determine if some value is an array + * + * @param val + * Value to check (can be null, undefined, etc.) + * @return True if it's an array; false otherwise + */ + isArray: function Utils_isArray(val) val != null && typeof val == "object" && + val.constructor.name == "Array", + // lazy load objects from a constructor on first access. It will // work with the global object ('this' in the global context). lazy: function Weave_lazy(dest, prop, ctr) { @@ -187,7 +197,8 @@ let Utils = { if (typeof(a) != "object" || typeof(b) != "object") return false; - if ("Array" == a.constructor.name && "Array" == b.constructor.name) { + let isArray = [Utils.isArray(a), Utils.isArray(b)]; + if (isArray[0] && isArray[1]) { if (a.length != b.length) return false; @@ -198,7 +209,7 @@ let Utils = { } else { // check if only one of them is an array - if ("Array" == a.constructor.name || "Array" == b.constructor.name) + if (isArray[0] || isArray[1]) return false; for (let key in a) { @@ -215,7 +226,7 @@ let Utils = { return thing; let ret; - if ("Array" == thing.constructor.name) { + if (Utils.isArray(thing)) { ret = []; for (let i = 0; i < thing.length; i++) ret.push(Utils.deepCopy(thing[i], noSort)); From 9426e0d7b8a0c2b796daff267b32a0c39ba98265 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 7 Apr 2009 16:45:41 -0500 Subject: [PATCH 1059/1860] Bug 487308 - Allow registering of an array of engines Import engines to the Weave global object and use them to register engines, which checks if the arg is an array. To support handling of errors (unused), the engine is returned on register failure. --- services/sync/modules/engines.js | 31 +++++++++++++++++++++++++++++-- services/sync/modules/service.js | 6 ++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 82e24f73bbbc..9e83f4f9a07e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -104,8 +104,35 @@ EngineManagerSvc.prototype = { } return ret; }, - register: function EngMgr_register(engine) { - this._engines[engine.name] = engine; + + /** + * Register an Engine to the service. Alternatively, give an array of engine + * objects to register. + * + * @param engineObject + * Engine object used to get an instance of the engine + * @return The engine object if anything failed + */ + register: function EngMgr_register(engineObject) { + if (Utils.isArray(engineObject)) + return engineObject.map(this.register, this); + + try { + let engine = new engineObject(); + this._engines[engine.name] = engine; + } + catch(ex) { + let mesg = ex.message ? ex.message : ex; + let name = engineObject || ""; + name = name.prototype || ""; + name = name.name || ""; + + let out = "Could not initialize engine '" + name + "': " + mesg; + dump(out); + this._log.error(out); + + return engineObject; + } }, unregister: function EngMgr_unregister(val) { let name = val; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b0d591a6c743..c9520ab16486 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -97,7 +97,13 @@ Cu.import("resource://weave/identity.js", Weave); Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/oauth.js", Weave); + +Cu.import("resource://weave/engines/bookmarks.js", Weave); Cu.import("resource://weave/engines/clientData.js", Weave); +Cu.import("resource://weave/engines/forms.js", Weave); +Cu.import("resource://weave/engines/history.js", Weave); +Cu.import("resource://weave/engines/passwords.js", Weave); +Cu.import("resource://weave/engines/tabs.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); From 650fe8e480ed8feb5c16929de394c74fc4285fa9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 7 Apr 2009 19:22:36 -0500 Subject: [PATCH 1060/1860] Bug 487338 - Track special folder weave GUIDs <-> bookmark id mapping Create a specialIds hash and use it to for getting weaveId/id, determine top level, if a folder is a root, getting all ids, wiping, and tracker ignoring. --- services/sync/modules/engines/bookmarks.js | 75 +++++++++------------- 1 file changed, 31 insertions(+), 44 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index da86313872e8..5138faa49a6c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -83,10 +83,17 @@ BookmarksEngine.prototype = { function BookmarksStore() { this._init(); + + // Initialize the special top level folders + [["menu", "bookmarksMenuFolder"], + ["toolbar", "toolbarFolder"], + ["unfiled", "unfiledBookmarksFolder"], + ].forEach(function(top) this.specialIds[top[0]] = this._bms[top[1]], this); } BookmarksStore.prototype = { __proto__: Store.prototype, _logName: "BStore", + specialIds: {}, __bms: null, get _bms() { @@ -143,36 +150,25 @@ BookmarksStore.prototype = { }, _getItemIdForGUID: function BStore__getItemIdForGUID(GUID) { - switch (GUID) { - case "menu": - return this._bms.bookmarksMenuFolder; - case "toolbar": - return this._bms.toolbarFolder; - case "unfiled": - return this._bms.unfiledBookmarksFolder; - default: - return this._bms.getItemIdForGUID(GUID); - } - return null; + if (GUID in this.specialIds) + return this.specialIds[GUID]; + + return this._bms.getItemIdForGUID(GUID); }, _getWeaveIdForItem: function BStore__getWeaveIdForItem(placeId) { - if (placeId == this._bms.bookmarksMenuFolder) - return "menu"; - if (placeId == this._bms.toolbarFolder) - return "toolbar"; - if (placeId == this._bms.unfiledBookmarksFolder) - return "unfiled"; + for (let [weaveId, id] in Iterator(this.specialIds)) + if (placeId == id) + return weaveId; + return this._bms.getItemGUID(placeId); }, _isToplevel: function BStore__isToplevel(placeId) { - if (placeId == this._bms.bookmarksMenuFolder) - return true; - if (placeId == this._bms.toolbarFolder) - return true; - if (placeId == this._bms.unfiledBookmarksFolder) - return true; + for (let [weaveId, id] in Iterator(this.specialIds)) + if (placeId == id) + return true; + if (this._bms.getFolderIdForItem(placeId) <= 0) return true; return false; @@ -296,9 +292,7 @@ BookmarksStore.prototype = { }, remove: function BStore_remove(record) { - if (record.id == "menu" || - record.id == "toolbar" || - record.id == "unfiled") { + if (record.id in this.specialIds) { this._log.warn("Attempted to remove root node (" + record.id + "). Skipping record removal."); return; @@ -333,9 +327,7 @@ BookmarksStore.prototype = { update: function BStore_update(record) { let itemId = this._getItemIdForGUID(record.id); - if (record.id == "menu" || - record.id == "toolbar" || - record.id == "unfiled") { + if (record.id in this.specialIds) { this._log.debug("Skipping update for root node."); return; } @@ -610,9 +602,8 @@ BookmarksStore.prototype = { getAllIDs: function BStore_getAllIDs() { let items = {}; - this._getChildren("menu", true, items); - this._getChildren("toolbar", true, items); - this._getChildren("unfiled", true, items); + for (let [weaveId, id] in Iterator(this.specialIds)) + this._getChildren(weaveId, true, items); return items; }, @@ -623,9 +614,8 @@ BookmarksStore.prototype = { }, wipe: function BStore_wipe() { - this._bms.removeFolderChildren(this._bms.bookmarksMenuFolder); - this._bms.removeFolderChildren(this._bms.toolbarFolder); - this._bms.removeFolderChildren(this._bms.unfiledBookmarksFolder); + for (let [weaveId, id] in Iterator(this.specialIds)) + this._bms.removeFolderChildren(id); } }; @@ -670,15 +660,12 @@ BookmarksTracker.prototype = { this._all[this._bms.getItemIdForGUID(guid)] = guid; } - // ignore changes to the three roots - // we use special names for them, so ignore their "real" places guid - // as well as ours, just in case - this.ignoreID("menu"); - this.ignoreID("toolbar"); - this.ignoreID("unfiled"); - this.ignoreID(this._all[this._bms.bookmarksMenuFolder]); - this.ignoreID(this._all[this._bms.toolbarFolder]); - this.ignoreID(this._all[this._bms.unfiledBookmarksFolder]); + // Ignore changes to the special roots. We use special names for them, so + // ignore their "real" places guid as well as ours, just in case + for (let [weaveId, id] in Iterator(store.specialIds)) { + this.ignoreID(weaveId); + this.ignoreID(this._all[id]); + } this._bms.addObserver(this, false); }, From 2f234e74c0ef93ca4aa1da4cb94c8bfbf56cbec5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 7 Apr 2009 22:56:04 -0500 Subject: [PATCH 1061/1860] Bug 487363 - Share bookmark tracker ignore logic Share the ignore logic that takes a parent folder and ignore if the engine says so or it's a livemark. --- services/sync/modules/engines/bookmarks.js | 32 +++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5138faa49a6c..5dc1c479f30e 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -675,9 +675,24 @@ BookmarksTracker.prototype = { this._score += 10; }, + /** + * Determine if a change should be ignored: we're ignoring everything or the + * folder is for livemarks + * + * @param folder + * Folder of the item being changed + */ + _ignore: function BMT__ignore(folder) { + // Ignore unconditionally if the engine tells us to + if (this.ignoreAll) + return true; + + // Ignore livemark children + return this._ls.isLivemark(folder); + }, + onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { - if (this.ignoreAll || - this._ls.isLivemark(folder)) + if (this._ignore(folder)) return; this._log.trace("onItemAdded: " + itemId); @@ -688,8 +703,7 @@ BookmarksTracker.prototype = { }, onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { - if (this.ignoreAll || - this._ls.isLivemark(folder)) + if (this._ignore(folder)) return; this._log.trace("onItemRemoved: " + itemId); @@ -700,7 +714,8 @@ BookmarksTracker.prototype = { }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { - if (this.ignoreAll) + let folder = this._bms.getFolderIdForItem(itemId); + if (this._ignore(folder)) return; // ignore annotations except for the ones that we sync @@ -709,10 +724,6 @@ BookmarksTracker.prototype = { property != "microsummary/generatorURI")) return; - // ignore if parent is a livemark - if (this._ls.isLivemark(this._bms.getFolderIdForItem(itemId))) - return; - this._log.trace("onItemChanged: " + itemId + (", " + property + (isAnno? " (anno)" : "")) + (value? (" = \"" + value + "\"") : "")); @@ -726,7 +737,8 @@ BookmarksTracker.prototype = { }, onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { - if (this.ignoreAll) + let folder = this._bms.getFolderIdForItem(itemId); + if (this._ignore(folder)) return; this._log.trace("onItemMoved: " + itemId); From b863f67e7573225790336f3deafeb44bf19066b9 Mon Sep 17 00:00:00 2001 From: Mikhail Stepura Date: Tue, 7 Apr 2009 23:17:40 -0500 Subject: [PATCH 1062/1860] Bug 486481 - Weird behavior when syncing the bookmark with tags Add the remaining special top level folders and ignore them for certain behavior like getAllIDs and wipe. Have the tracker ignore changes to things in the tags folder. --- services/sync/modules/engines/bookmarks.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5dc1c479f30e..c3433d5bd740 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -86,6 +86,8 @@ function BookmarksStore() { // Initialize the special top level folders [["menu", "bookmarksMenuFolder"], + ["places", "placesRoot"], + ["tags", "tagsFolder"], ["toolbar", "toolbarFolder"], ["unfiled", "unfiledBookmarksFolder"], ].forEach(function(top) this.specialIds[top[0]] = this._bms[top[1]], this); @@ -308,6 +310,7 @@ BookmarksStore.prototype = { switch (type) { case this._bms.TYPE_BOOKMARK: this._log.debug(" -> removing bookmark " + record.id); + this._ts.untagURI(this._bms.getBookmarkURI(itemId), null); this._bms.removeItem(itemId); break; case this._bms.TYPE_FOLDER: @@ -603,7 +606,8 @@ BookmarksStore.prototype = { getAllIDs: function BStore_getAllIDs() { let items = {}; for (let [weaveId, id] in Iterator(this.specialIds)) - this._getChildren(weaveId, true, items); + if (weaveId != "places" && weaveId != "tags") + this._getChildren(weaveId, true, items); return items; }, @@ -615,7 +619,8 @@ BookmarksStore.prototype = { wipe: function BStore_wipe() { for (let [weaveId, id] in Iterator(this.specialIds)) - this._bms.removeFolderChildren(id); + if (weaveId != "places") + this._bms.removeFolderChildren(id); } }; @@ -687,6 +692,15 @@ BookmarksTracker.prototype = { if (this.ignoreAll) return true; + let tags = this._bms.tagsFolder; + // Ignore changes to tags (folders under the tags folder) + if (folder == tags) + return true; + + // Ignore tag items (the actual instance of a tag for a bookmark) + if (this._bms.getFolderIdForItem(folder) == tags) + return true; + // Ignore livemark children return this._ls.isLivemark(folder); }, From 0989826f681f22be3ded67fbdf0212af25cebe5a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 8 Apr 2009 02:12:57 -0500 Subject: [PATCH 1063/1860] Bug 487378 - Use the app name as the default client name Use nsIXULAppInfo to get the name and remove Fennec specific bits. --- services/sync/locales/en-US/fennec.properties | 1 - services/sync/modules/engines/clients.js | 2 +- services/sync/modules/util.js | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/fennec.properties b/services/sync/locales/en-US/fennec.properties index 51c90e0723c4..cbd1e9abac7e 100644 --- a/services/sync/locales/en-US/fennec.properties +++ b/services/sync/locales/en-US/fennec.properties @@ -5,7 +5,6 @@ fennec.turned-off = Weave is turned off. fennec.login.error = Weave had an error when trying to log in. fennec.login.error.detail = Login error: %S -fennec.default.client.name = Fennec fennec.default.client.type = Mobile fennec.sync.complete.time = Sync completed at %S, %S diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index ee8c2ec52594..818a0e4ee579 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -116,7 +116,7 @@ ClientEngine.prototype = { Svc.Prefs.reset("client.syncID"); }, - get clientName() { return Svc.Prefs.get("client.name", "Firefox"); }, + get clientName() { return Svc.Prefs.get("client.name", Svc.AppInfo.name); }, set clientName(value) { Svc.Prefs.set("client.name", value); }, get clientType() { return Svc.Prefs.get("client.type", "desktop"); }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 950a3bf3419d..5ca840c89c6c 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -632,7 +632,8 @@ Utils.EventListener.prototype = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); -[["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], +[["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], + ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], ["Login", "@mozilla.org/login-manager;1", "nsILoginManager"], From 1f12f2269eeeeaf3c615b426dcd94f5a37f1719f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 8 Apr 2009 14:39:14 -0500 Subject: [PATCH 1064/1860] Bug 486810 - Engines (and their trackers) are created with every new window. r=thunder Register the built-in engines on service start-up instead of from the overlay, and have Engines.register check if the engine has already been registered. --- services/sync/modules/engines.js | 7 +++++-- services/sync/modules/service.js | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9e83f4f9a07e..07b0754b6770 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -118,8 +118,11 @@ EngineManagerSvc.prototype = { return engineObject.map(this.register, this); try { - let engine = new engineObject(); - this._engines[engine.name] = engine; + let name = engineObject.prototype.name; + if (name in this._engines) + this._log.error("Engine '" + name + "' is already registered!"); + else + this._engines[name] = new engineObject(); } catch(ex) { let mesg = ex.message ? ex.message : ex; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c9520ab16486..3b84fbd3aa1a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -288,7 +288,7 @@ WeaveSvc.prototype = { let self = yield; this._initLogs(); this._log.info("Weave " + WEAVE_VERSION + " initializing"); - + this._registerEngines(); this._detailedStatus = new StatusRecord(); // Reset our sync id if we're upgrading, so sync knows to reset local data @@ -380,6 +380,29 @@ WeaveSvc.prototype = { this._debugApp.clear(); }, + /** + * Register the built-in engines for certain applications + */ + _registerEngines: function WeaveSvc__registerEngines() { + let engines = []; + switch (Svc.AppInfo.name) { + case "Fennec": + engines = ["Bookmarks", "History", "Password", "Tab"]; + break; + + case "Firefox": + engines = ["Bookmarks", "Form", "History", "Password", "Tab"]; + break; + + case "Thunderbird": + engines = ["Cookie", "Password"]; + break; + } + + // Grab the actual engine and register them + Engines.register(engines.map(function(name) Weave[name + "Engine"])); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), From d8596b36abbabd951e4bcf407d9d33dbcf8e19a8 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Wed, 8 Apr 2009 15:00:02 -0700 Subject: [PATCH 1065/1860] switch from --utc to -u flag to enable UTC output for date command so it works on both Linux/Windows (which support --utc in addition to -u) and Mac OS X (which only supports -u) --- services/sync/modules/engines/bookmarks.js | 3 +++ services/sync/modules/engines/history.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c3433d5bd740..2776d527421c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -657,6 +657,7 @@ BookmarksTracker.prototype = { // has a bug where it will generate a new one instead of throwing). // Our solution: cache item IDs -> GUIDs +let before = new Date(); // FIXME: very roundabout way of getting id -> guid mapping! let store = new BookmarksStore(); let all = store.getAllIDs(); @@ -664,6 +665,8 @@ BookmarksTracker.prototype = { for (let guid in all) { this._all[this._bms.getItemIdForGUID(guid)] = guid; } +let after = new Date(); +dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in all)].length + " bookmark items\n"); // Ignore changes to the special roots. We use special names for them, so // ignore their "real" places guid as well as ours, just in case diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 93cd5ca24f60..d937ba7c1c8a 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -494,6 +494,7 @@ HistoryTracker.prototype = { _init: function HT__init() { this.__proto__.__proto__._init.call(this); +let before = new Date(); // FIXME: very roundabout way of getting url -> guid mapping! // FIXME2: not updated after startup let all = this._store.getAllIDs(); @@ -501,6 +502,8 @@ HistoryTracker.prototype = { for (let guid in all) { this._all[all[guid]] = guid; } +let after = new Date(); +dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in all)].length + " history items\n"); this._hsvc.addObserver(this, false); }, From 39b7609edc91917e003e964a6150ecaeed9d50d6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 8 Apr 2009 19:23:14 -0500 Subject: [PATCH 1066/1860] Bug 487523 - Scheduled sync fires while a sync is running Just skip the scheduled sync if we're already syncing --- services/sync/modules/service.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3b84fbd3aa1a..16398b892d2e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -800,7 +800,12 @@ WeaveSvc.prototype = { else if (!this._syncTimer) { this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let listener = new Utils.EventListener(Utils.bind2(this, - function WeaveSvc__checkSyncCallback(timer) this.sync(null, false))); + function WeaveSvc__checkSyncCallback(timer) { + if (this.locked) + this._log.debug("Skipping scheduled sync: already locked for sync"); + else + this.sync(null, false); + })); this._syncTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK); this._log.config("Weave scheduler enabled"); From 5312d216e2dde75c3043971fb5c79c221d753728 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 8 Apr 2009 18:08:45 -0700 Subject: [PATCH 1067/1860] skip getting meta records for records already in the meta list --- services/sync/modules/engines.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 07b0754b6770..aa4a9811012b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -481,7 +481,8 @@ SyncEngine.prototype = { for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); this._log.trace("Outgoing:\n" + out); - if (!out.deleted) + // skip getting siblings of already processed and deleted records + if (!out.deleted && !(out.id in meta)) this._store.createMetaRecords(out.id, meta); yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient From 9a20cd645ec3c079b65ee72246f1c32ed27cccb3 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 8 Apr 2009 18:11:14 -0700 Subject: [PATCH 1068/1860] don't track history deletes at all; don't built id->guid mapping hash for history tracker --- services/sync/modules/engines/history.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index d937ba7c1c8a..2a35722f9293 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -493,18 +493,6 @@ HistoryTracker.prototype = { _init: function HT__init() { this.__proto__.__proto__._init.call(this); - -let before = new Date(); - // FIXME: very roundabout way of getting url -> guid mapping! - // FIXME2: not updated after startup - let all = this._store.getAllIDs(); - this._all = {}; - for (let guid in all) { - this._all[all[guid]] = guid; - } -let after = new Date(); -dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in all)].length + " history items\n"); - this._hsvc.addObserver(this, false); }, @@ -533,9 +521,6 @@ dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in al this._upScore(); }, onDeleteURI: function HT_onDeleteURI(uri) { - this._log.trace("onDeleteURI: " + uri.spec); - if (this.addChangedID(this._all[uri])) - this._upScore(); }, onClearHistory: function HT_onClearHistory() { this._log.trace("onClearHistory"); From 375421cc8da2cfdf6ac9bc41dcfb86219d592454 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 8 Apr 2009 22:48:26 -0500 Subject: [PATCH 1069/1860] Bug 487541 - form submission caused exception 0x80004002 (NS_NOINTERFACE) [nsISupports.QueryInterface] Get rid of the QueryInterface and just use instanceof while copying the logic of nsFormHistory::Notify to avoid divergent logic until satchel provides a notification. --- services/sync/modules/engines/forms.js | 66 ++++++++++++++++++++++---- services/sync/services-sync.js | 1 + 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index e5de6384879b..8e9ee8b50851 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -227,22 +227,68 @@ FormTracker.prototype = { notify: function FormTracker_notify(formElement, aWindow, actionURI) { if (this.ignoreAll) return; - + this._log.trace("Form submission notification for " + actionURI.spec); - + + // XXX Bug 487541 Copy the logic from nsFormHistory::Notify to avoid + // divergent logic, which can lead to security issues, until there's a + // better way to get satchel's results like with a notification. + + // Determine if a dom node has the autocomplete attribute set to "off" + let completeOff = function(domNode) { + let autocomplete = domNode.getAttribute("autocomplete"); + return autocomplete && autocomplete.search(/^off$/i) == 0; + } + + if (completeOff(formElement)) { + this._log.trace("Form autocomplete set to off"); + return; + } + /* Get number of elements in form, add points and changedIDs */ let len = formElement.length; let elements = formElement.elements; for (let i = 0; i < len; i++) { - let element = elements.item(i); - let inputElement = element.QueryInterface(Ci.nsIDOMHTMLInputElement); - - if (inputElement && inputElement.type == "text") { - this._log.trace("Logging form element: " + inputElement.name + "::" + - inputElement.value); - this.addChangedID(Utils.sha1(inputElement.name + inputElement.value)); - this._score += 10; + let el = elements.item(i); + + // Grab the name for debugging, but check if empty when satchel would + let name = el.name; + if (name === "") + name = el.id; + + if (!(el instanceof Ci.nsIDOMHTMLInputElement)) { + this._log.trace(name + " is not a DOMHTMLInputElement: " + el); + continue; } + + if (el.type.search(/^text$/i) != 0) { + this._log.trace(name + "'s type is not 'text': " + el.type); + continue; + } + + if (completeOff(el)) { + this._log.trace(name + "'s autocomplete set to off"); + continue; + } + + if (el.value === "") { + this._log.trace(name + "'s value is empty"); + continue; + } + + if (el.value == el.defaultValue) { + this._log.trace(name + "'s value is the default"); + continue; + } + + if (name === "") { + this._log.trace("Text input element has no name or id"); + continue; + } + + this._log.trace("Logging form element: " + name + " :: " + el.value); + this.addChangedID(Utils.sha1(name + el.value)); + this._score += 10; } } }; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 48c82c64d9c8..9fc36f49c4f4 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -32,6 +32,7 @@ pref("extensions.weave.log.logger.service.main", "Trace"); pref("extensions.weave.log.logger.async", "Debug"); pref("extensions.weave.log.logger.network.resources", "Debug"); pref("extensions.weave.log.logger.engine.bookmarks", "Debug"); +pref("extensions.weave.log.logger.engine.forms", "Debug"); pref("extensions.weave.log.logger.engine.history", "Debug"); pref("extensions.weave.log.logger.engine.tabs", "Debug"); pref("extensions.weave.log.logger.engine.clients", "Debug"); From 96b1f97bd7b190f21956ae2d1d8c74fd9adc811e Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 10 Apr 2009 16:04:13 -0700 Subject: [PATCH 1070/1860] can't get guid on expiration --- services/sync/modules/engines/history.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 2a35722f9293..96c321c560ba 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -516,9 +516,6 @@ HistoryTracker.prototype = { this._upScore(); }, onPageExpired: function HT_onPageExpired(uri, time, entry) { - this._log.trace("onPageExpired: " + uri.spec); - if (this.addChangedID(this._store._getGUID(uri))) // XXX eek ? - this._upScore(); }, onDeleteURI: function HT_onDeleteURI(uri) { }, From 9f15fb45065d40520541e25fcd9ddaebb774cc81 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 10 Apr 2009 16:08:31 -0700 Subject: [PATCH 1071/1860] Remove __proto__.__proto__ it's fail-prone --- services/sync/modules/engines/history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 96c321c560ba..a2fc5fc811e0 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -492,7 +492,7 @@ HistoryTracker.prototype = { }, _init: function HT__init() { - this.__proto__.__proto__._init.call(this); + Tracker._init.call(this); this._hsvc.addObserver(this, false); }, From 717807284eaaebdc08fa57e8b55fd23ac808163f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 10 Apr 2009 17:30:05 -0700 Subject: [PATCH 1072/1860] oops fix missing .prototype --- services/sync/modules/engines/history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index a2fc5fc811e0..197fe216f12c 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -492,7 +492,7 @@ HistoryTracker.prototype = { }, _init: function HT__init() { - Tracker._init.call(this); + Tracker.prototype._init.call(this); this._hsvc.addObserver(this, false); }, From 5898898012e3b0283eda1303dcc4b443db033116 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 13 Apr 2009 14:54:31 -0500 Subject: [PATCH 1073/1860] Bug 488165 - Correct set the record object type when processing records Set _recordObj for Bookmarks/History engines and override PlacesItem.decrypt to switch itself to the right type after CryptoWrapper decrypts the payload. --- services/sync/modules/engines/bookmarks.js | 1 + services/sync/modules/engines/history.js | 1 + .../sync/modules/type_records/bookmark.js | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 2776d527421c..0ea413f7320f 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -77,6 +77,7 @@ BookmarksEngine.prototype = { name: "bookmarks", displayName: "Bookmarks", logName: "Bookmarks", + _recordObj: PlacesItem, _storeObj: BookmarksStore, _trackerObj: BookmarksTracker }; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 197fe216f12c..f38a1ce17d1c 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -61,6 +61,7 @@ HistoryEngine.prototype = { name: "history", displayName: "History", logName: "History", + _recordObj: HistoryRec, _storeObj: HistoryStore, _trackerObj: HistoryTracker, diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 7cdfbce98ced..8744a4cf3fcb 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -55,6 +55,33 @@ function PlacesItem(uri) { this._PlacesItem_init(uri); } PlacesItem.prototype = { + decrypt: function PlacesItem_decrypt(onComplete, passphrase) { + CryptoWrapper.prototype.decrypt.call(this, Utils.bind2(this, function(ret) { + // Convert the abstract places item to the actual object type + if (!this.deleted) + this.__proto__ = this.getTypeObject(this.type).prototype; + + // Give the original callback the result + onComplete(ret); + }), passphrase); + }, + + getTypeObject: function PlacesItem_getTypeObject(type) { + switch (type) { + case "bookmark": + return Bookmark; + case "microsummary": + return BookmarkMicsum; + case "folder": + return BookmarkFolder; + case "livemark": + return Livemark; + case "separator": + return BookmarkSeparator; + } + throw "Unknown places item object type"; + }, + __proto__: CryptoWrapper.prototype, _logName: "Record.PlacesItem", From 3de3a62f999dad05d8e67c9ca79609ac922c0c0b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 13 Apr 2009 16:18:11 -0500 Subject: [PATCH 1074/1860] Bug 488182 - Use an Iterator to grab both key and values from cleartext for processing bookmarks Get rid of extra record.cleartext references within the for/each/switch -- even for unused keys like incomingSharedAnno. --- services/sync/modules/engines/bookmarks.js | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 0ea413f7320f..e76718b32f42 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -350,33 +350,33 @@ BookmarksStore.prototype = { this._bms.moveItem(itemId, parentid, record.sortindex); } - for (let key in record.cleartext) { + for (let [key, val] in Iterator(record.cleartext)) { switch (key) { case "title": - this._bms.setItemTitle(itemId, record.cleartext.title); + this._bms.setItemTitle(itemId, val); break; case "uri": - this._bms.changeBookmarkURI(itemId, Utils.makeURI(record.cleartext.uri)); + this._bms.changeBookmarkURI(itemId, Utils.makeURI(val)); break; case "tags": { // filter out null/undefined/empty tags - let tags = record.cleartext.tags.filter(function(t) t); + let tags = val.filter(function(t) t); let tagsURI = this._bms.getBookmarkURI(itemId); this._ts.untagURI(tagsURI, null); this._ts.tagURI(tagsURI, tags); } break; case "keyword": - this._bms.setKeywordForBookmark(itemId, record.cleartext.keyword); + this._bms.setKeywordForBookmark(itemId, val); break; case "description": this._ans.setItemAnnotation(itemId, "bookmarkProperties/description", - record.cleartext.description, 0, + val, 0, this._ans.EXPIRE_NEVER); break; case "generatorURI": { try { let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); - let genURI = Utils.makeURI(record.cleartext.generatorURI); + let genURI = Utils.makeURI(val); if (this._ms == SERVICE_NOT_SUPPORTED) { this._log.warn("Can't create microsummary -- not supported."); } else { @@ -388,24 +388,24 @@ BookmarksStore.prototype = { } } break; case "siteURI": - this._ls.setSiteURI(itemId, Utils.makeURI(record.cleartext.siteURI)); + this._ls.setSiteURI(itemId, Utils.makeURI(val)); break; case "feedURI": - this._ls.setFeedURI(itemId, Utils.makeURI(record.cleartext.feedURI)); + this._ls.setFeedURI(itemId, Utils.makeURI(val)); break; case "outgoingSharedAnno": this._ans.setItemAnnotation(itemId, OUTGOING_SHARED_ANNO, - record.cleartext.outgoingSharedAnno, 0, + val, 0, this._ans.EXPIRE_NEVER); break; case "incomingSharedAnno": this._ans.setItemAnnotation(itemId, INCOMING_SHARED_ANNO, - record.cleartext.incomingSharedAnno, 0, + val, 0, this._ans.EXPIRE_NEVER); break; case "serverPathAnno": this._ans.setItemAnnotation(itemId, SERVER_PATH_ANNO, - record.cleartext.serverPathAnno, 0, + val, 0, this._ans.EXPIRE_NEVER); break; } From 7691ce98a9ac0a289df821ef01efef7540ce597b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 13 Apr 2009 16:39:29 -0500 Subject: [PATCH 1075/1860] Bug 488142 - Make it easier to add simple deferred cleartext/payload values Add a couple Utils helper to create a pair of simple [gs]etters that use a hash property instead. Apply this to various records: WBORecord, {Priv,Pub}Key, Crypto{Meta,Wrapper}, and every engine's type records. Migrate by making sure key data exists (name change). --- services/sync/modules/base_records/crypto.js | 17 ++--- services/sync/modules/base_records/keys.js | 56 +++++------------ services/sync/modules/base_records/wbo.js | 25 +------- services/sync/modules/engines/bookmarks.js | 6 +- services/sync/modules/engines/history.js | 8 +-- services/sync/modules/service.js | 16 ++++- .../sync/modules/type_records/bookmark.js | 62 ++++--------------- services/sync/modules/type_records/forms.js | 12 +--- services/sync/modules/type_records/history.js | 17 ++--- .../sync/modules/type_records/passwords.js | 38 +----------- services/sync/modules/util.js | 41 ++++++++++++ 11 files changed, 106 insertions(+), 192 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 4c0e64b891c9..4ddb1d0b6401 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -71,16 +71,6 @@ CryptoWrapper.prototype = { // with the encrypted payload cleartext: null, - get encryption() this.payload.encryption, - set encryption(value) { - this.payload.encryption = value; - }, - - get ciphertext() this.payload.ciphertext, - set ciphertext(value) { - this.payload.ciphertext = value; - }, - _encrypt: function CryptoWrap__encrypt(passphrase) { let self = yield; @@ -141,6 +131,8 @@ CryptoWrapper.prototype = { ].join("\n ") + " }", }; +Utils.deferGetSet(CryptoWrapper, "payload", ["encryption", "ciphertext"]); + function CryptoMeta(uri) { this._CryptoMeta_init(uri); } @@ -160,9 +152,6 @@ CryptoMeta.prototype = { this.bulkIV = Svc.Crypto.generateRandomIV(); }, - get bulkIV() this.data.payload.bulkIV, - set bulkIV(value) { this.data.payload.bulkIV = value; }, - _getKey: function CryptoMeta__getKey(privkey, passphrase) { let self = yield; let wrapped_key; @@ -224,6 +213,8 @@ CryptoMeta.prototype = { } }; +Utils.deferGetSet(CryptoMeta, "data.payload", "bulkIV"); + Utils.lazy(this, 'CryptoMetas', CryptoRecordManager); function CryptoRecordManager() { this._init(); } diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index b033c94e8ebb..f766b6819a48 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -62,28 +62,18 @@ PubKey.prototype = { this._WBORec_init(uri); this.payload = { type: "pubkey", - key_data: null, - private_key: null + keyData: null, + privateKeyUri: null }; }, - get keyData() this.payload.key_data, - set keyData(value) { - this.payload.key_data = value; - }, - get _privateKeyUri() this.payload.private_key, get privateKeyUri() { if (!this.data) return null; - // resolve - let uri = this.uri.resolve(this._privateKeyUri); - if (uri) - return Utils.makeURI(uri); - // does not resolve, return raw (this uri type might not be able to resolve) - return Utils.makeURI(this._privateKeyUri); - }, - set privateKeyUri(value) { - this.payload.private_key = value; + + // Use the uri if it resolves, otherwise return raw (uri type unresolvable) + let key = this.payload.privateKeyUri; + return Utils.makeURI(this.uri.resolve(key) || key); }, get publicKeyUri() { @@ -91,6 +81,8 @@ PubKey.prototype = { } }; +Utils.deferGetSet(PubKey, "payload", ["keyData", "privateKeyUri"]); + function PrivKey(uri) { this._PrivKey_init(uri); } @@ -104,36 +96,18 @@ PrivKey.prototype = { type: "privkey", salt: null, iv: null, - key_data: null, - public_key: null + keyData: null, + publicKeyUri: null }; }, - get salt() this.payload.salt, - set salt(value) { - this.payload.salt = value; - }, - get iv() this.payload.iv, - set iv(value) { - this.payload.iv = value; - }, - get keyData() { return this.payload.key_data; }, - set keyData(value) { - this.payload.key_data = value; - }, - get publicKeyUri() { if (!this.data) return null; - // resolve - let uri = this.uri.resolve(this.payload.public_key); - if (uri) - return Utils.makeURI(uri); - // does not resolve, return raw (this uri type might not be able to resolve) - return Utils.makeURI(this.payload.public_key); - }, - set publicKeyUri(value) { - this.payload.public_key = value; + + // Use the uri if it resolves, otherwise return raw (uri type unresolvable) + let key = this.payload.publicKeyUri; + return Utils.makeURI(this.uri.resolve(key) || key); }, get privateKeyUri() { @@ -141,6 +115,8 @@ PrivKey.prototype = { } }; +Utils.deferGetSet(PrivKey, "payload", ["salt", "iv", "keyData", "publicKeyUri"]); + // XXX unused/unfinished function SymKey(keyData, wrapped) { this._init(keyData, wrapped); diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index bf76ea89c370..279b6f3ddd44 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -63,9 +63,6 @@ WBORecord.prototype = { this.uri = uri; }, - get id() { return this.data.id; }, - set id(value) { this.data.id = value; }, - // NOTE: baseUri must have a trailing slash, or baseUri.resolve() will omit // the collection name get uri() { @@ -79,42 +76,23 @@ WBORecord.prototype = { this.baseUri = Utils.makeURI(foo.join('/') + '/'); }, - get parentid() { return this.data.parentid; }, - set parentid(value) { - this.data.parentid = value; - }, - get modified() { if (typeof(this.data.modified) == "string") this.data.modified = parseInt(this.data.modified); return this.data.modified; }, - set modified(value) { - this.data.modified = value; - }, get depth() { if (this.data.depth) return this.data.depth; return 0; }, - set depth(value) { - this.data.depth = value; - }, get sortindex() { if (this.data.sortindex) return this.data.sortindex; return 0; }, - set sortindex(value) { - this.data.sortindex = value; - }, - - get payload() { return this.data.payload; }, - set payload(value) { - this.data.payload = value; - }, serialize: function WBORec_serialize() { // Convert the payload into a string because the server expects that @@ -148,6 +126,9 @@ WBORecord.prototype = { ].join("\n ") + " }", }; +Utils.deferGetSet(WBORecord, "data", ["id", "parentid", "modified", "depth", + "sortindex", "payload"]); + Utils.lazy(this, 'Records', RecordManager); function RecordManager() { diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e76718b32f42..c2f51c2da1e5 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -195,9 +195,9 @@ BookmarksStore.prototype = { case "bookmark": case "microsummary": { this._log.debug(" -> creating bookmark \"" + record.cleartext.title + "\""); - let uri = Utils.makeURI(record.cleartext.uri); + let uri = Utils.makeURI(record.bmkUri); this._log.debug(" -> -> ParentID is " + parentId); - this._log.debug(" -> -> uri is " + record.cleartext.uri); + this._log.debug(" -> -> uri is " + record.bmkUri); this._log.debug(" -> -> sortindex is " + record.sortindex); this._log.debug(" -> -> title is " + record.cleartext.title); newId = this._bms.insertBookmark(parentId, uri, record.sortindex, @@ -355,7 +355,7 @@ BookmarksStore.prototype = { case "title": this._bms.setItemTitle(itemId, val); break; - case "uri": + case "bmkUri": this._bms.changeBookmarkURI(itemId, Utils.makeURI(val)); break; case "tags": { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index f38a1ce17d1c..153e60b23664 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -411,12 +411,12 @@ HistoryStore.prototype = { }, update: function HistStore_update(record) { - this._log.trace(" -> processing history entry: " + record.cleartext.uri); + this._log.trace(" -> processing history entry: " + record.histUri); - let uri = Utils.makeURI(record.cleartext.uri); + let uri = Utils.makeURI(record.histUri); let curvisits = []; if (this.urlExists(uri)) - curvisits = this._getVisits(record.cleartext.uri); + curvisits = this._getVisits(record.histUri); let visit; while ((visit = record.cleartext.visits.pop())) { @@ -429,7 +429,7 @@ HistoryStore.prototype = { this._hsvc.setPageTitle(uri, record.cleartext.title); // Equalize IDs - let localId = this._getGUID(record.cleartext.uri); + let localId = this._getGUID(record.histUri); if (localId != record.id) this.changeItemID(localId, record.id); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 16398b892d2e..db203d4239e3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -703,16 +703,26 @@ WeaveSvc.prototype = { let needKeys = true; let pubkey = yield PubKeys.getDefaultKey(self.cb); - if (pubkey) { + if (!pubkey) + this._log.debug("Could not get public key"); + else if (pubkey.keyData == null) + this._log.debug("Public key has no key data"); + else { // make sure we have a matching privkey let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); - if (privkey) { + if (!privkey) + this._log.debug("Could not get private key"); + else if (privkey.keyData == null) + this._log.debug("Private key has no key data"); + else { needKeys = false; ret = true; } } + if (needKeys) { - if (PubKeys.lastResource.lastChannel.responseStatus != 404 && + if (PubKeys.lastResource != null && PrivKeys.lastResource != null && + PubKeys.lastResource.lastChannel.responseStatus != 404 && PrivKeys.lastResource.lastChannel.responseStatus != 404) { this._log.warn("Couldn't download keys from server, aborting sync"); this._log.debug("PubKey HTTP response status: " + diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 8744a4cf3fcb..193564034691 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -90,14 +90,10 @@ PlacesItem.prototype = { this.cleartext = { }; }, - - get type() this.cleartext.type, - set type(value) { - // XXX check type is valid? - this.cleartext.type = value; - } }; +Utils.deferGetSet(PlacesItem, "cleartext", "type"); + function Bookmark(uri) { this._Bookmark_init(uri); } @@ -110,35 +106,17 @@ Bookmark.prototype = { this.cleartext.type = "bookmark"; }, - get title() this.cleartext.title, - set title(value) { - this.cleartext.title = value; - }, - - get bmkUri() this.cleartext.uri, set bmkUri(value) { if (typeof(value) == "string") - this.cleartext.uri = value; + this.cleartext.bmkUri = value; else - this.cleartext.uri = value.spec; + this.cleartext.bmkUri = value.spec; }, - - get description() this.cleartext.description, - set description(value) { - this.cleartext.description = value; - }, - - get tags() this.cleartext.tags, - set tags(value) { - this.cleartext.tags = value; - }, - - get keyword() this.cleartext.keyword, - set keyword(value) { - this.cleartext.keyword = value; - } }; +Utils.deferGetSet(Bookmark, "cleartext", ["title", "bmkUri", "description", + "tags", "keyword"]); + function BookmarkMicsum(uri) { this._BookmarkMicsum_init(uri); } @@ -150,21 +128,10 @@ BookmarkMicsum.prototype = { this._Bookmark_init(uri); this.cleartext.type = "microsummary"; }, - - get generatorURI() this.cleartext.generatorURI, - set generatorURI(value) { - if (typeof(value) == "string") - this.cleartext.generatorURI = value; - else - this.cleartext.generatorURI = value? value.spec : ""; - }, - - get staticTitle() this.cleartext.staticTitle, - set staticTitle(value) { - this.cleartext.staticTitle = value; - } }; +Utils.deferGetSet(BookmarkMicsum, "cleartext", ["generatorURI", "staticTitle"]); + function BookmarkFolder(uri) { this._BookmarkFolder_init(uri); } @@ -176,13 +143,10 @@ BookmarkFolder.prototype = { this._PlacesItem_init(uri); this.cleartext.type = "folder"; }, - - get title() this.cleartext.title, - set title(value) { - this.cleartext.title = value; - } }; +Utils.deferGetSet(BookmarkFolder, "cleartext", "title"); + function Livemark(uri) { this._Livemark_init(uri); } @@ -195,7 +159,6 @@ Livemark.prototype = { this.cleartext.type = "livemark"; }, - get siteURI() this.cleartext.siteURI, set siteURI(value) { if (typeof(value) == "string") this.cleartext.siteURI = value; @@ -203,7 +166,6 @@ Livemark.prototype = { this.cleartext.siteURI = value? value.spec : ""; }, - get feedURI() this.cleartext.feedURI, set feedURI(value) { if (typeof(value) == "string") this.cleartext.feedURI = value; @@ -212,6 +174,8 @@ Livemark.prototype = { } }; +Utils.deferGetSet(Livemark, "cleartext", ["siteURI", "feedURI"]); + function BookmarkSeparator(uri) { this._BookmarkSeparator_init(uri); } diff --git a/services/sync/modules/type_records/forms.js b/services/sync/modules/type_records/forms.js index 9a9a85ab8d03..1d77e5782fb3 100644 --- a/services/sync/modules/type_records/forms.js +++ b/services/sync/modules/type_records/forms.js @@ -62,14 +62,6 @@ FormRec.prototype = { this.cleartext = { }; }, - - get name() this.cleartext.name, - set name(p) { - this.cleartext.name = p; - }, - - get value() this.cleartext.value, - set value(p) { - this.cleartext.value = p; - } }; + +Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]); diff --git a/services/sync/modules/type_records/history.js b/services/sync/modules/type_records/history.js index 28e83083fdb4..3b9e651a695e 100644 --- a/services/sync/modules/type_records/history.js +++ b/services/sync/modules/type_records/history.js @@ -63,21 +63,12 @@ HistoryRec.prototype = { }; }, - get histUri() this.cleartext.uri, set histUri(value) { if (typeof(value) == "string") - this.cleartext.uri = value; + this.cleartext.histUri = value; else - this.cleartext.uri = value.spec; + this.cleartext.histUri = value.spec; }, - - get title() this.cleartext.title, - set title(value) { - this.cleartext.title = value; - }, - - get visits() this.cleartext.visits, - set visits(value) { - this.cleartext.visits = value; - } }; + +Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]); diff --git a/services/sync/modules/type_records/passwords.js b/services/sync/modules/type_records/passwords.js index fe81b4ba01ff..b81f3752bdd1 100644 --- a/services/sync/modules/type_records/passwords.js +++ b/services/sync/modules/type_records/passwords.js @@ -62,39 +62,7 @@ LoginRec.prototype = { this.cleartext = { }; }, - - get hostname() this.cleartext.hostname, - set hostname(value) { - this.cleartext.hostname = value; - }, - - get formSubmitURL() this.cleartext.formSubmitURL, - set formSubmitURL(value) { - this.cleartext.formSubmitURL = value; - }, - - get httpRealm() this.cleartext.httpRealm, - set httpRealm(value) { - this.cleartext.httpRealm = value; - }, - - get username() this.cleartext.username, - set username(value) { - this.cleartext.username = value; - }, - - get password() this.cleartext.password, - set password(value) { - this.cleartext.password = value; - }, - - get usernameField() this.cleartext.usernameField, - set usernameField(value) { - this.cleartext.usernameField = value; - }, - - get passwordField() this.cleartext.passwordField, - set passwordField(value) { - this.cleartext.passwordField = value; - } }; + +Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL", + "httpRealm", "username", "password", "usernameField", "passwordField"]); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 5ca840c89c6c..b0d6270b30ef 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -122,6 +122,47 @@ let Utils = { lm.addLogin(login); }, + /** + * Add a simple getter/setter to an object that defers access of a property + * to an inner property. + * + * @param obj + * Object to add properties to defer in its prototype + * @param defer + * Hash property of obj to defer to (dot split each level) + * @param prop + * Property name to defer (or an array of property names) + */ + deferGetSet: function Utils_deferGetSet(obj, defer, prop) { + if (Utils.isArray(prop)) + return prop.map(function(prop) Utils.deferGetSet(obj, defer, prop)); + + // Split the defer into each dot part for each level to dereference + let parts = defer.split("."); + let deref = function(base) Utils.deref(base, parts); + + let prot = obj.prototype; + + // Create a getter if it doesn't exist yet + if (!prot.__lookupGetter__(prop)) + prot.__defineGetter__(prop, function() deref(this)[prop]); + + // Create a setter if it doesn't exist yet + if (!prot.__lookupSetter__(prop)) + prot.__defineSetter__(prop, function(val) deref(this)[prop] = val); + }, + + /** + * Dereference an array of properties starting from a base object + * + * @param base + * Base object to start dereferencing + * @param props + * Array of properties to dereference (one for each level) + */ + deref: function Utils_deref(base, props) props.reduce(function(curr, prop) + curr[prop], base), + /** * Determine if some value is an array * From faa72abde2326be84153c4b56c981e5d2ddb4666 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 13 Apr 2009 16:39:29 -0500 Subject: [PATCH 1076/1860] Bug 488170 - Consistently set record cleartext URIs as text spec uris Switch cleartext properties to Uri that always takes a text spec, so eventually URI refers to nsIURI. GeneratorURIs seems to have been broken either way.. makeURI of an already URI and storing of a URI into cleartext. This landed with changes to keys, so the server wipe also handles the local name changes. --- services/sync/modules/engines/bookmarks.js | 22 ++++++++-------- .../sync/modules/type_records/bookmark.js | 25 ++----------------- services/sync/modules/type_records/history.js | 7 ------ 3 files changed, 13 insertions(+), 41 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c2f51c2da1e5..dbc456b18459 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -215,7 +215,7 @@ BookmarksStore.prototype = { this._log.debug(" \-> is a microsummary"); this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", record.cleartext.staticTitle || "", 0, this._ans.EXPIRE_NEVER); - let genURI = Utils.makeURI(record.cleartext.generatorURI); + let genURI = Utils.makeURI(record.generatorUri); if (this._ms) { try { let micsum = this._ms.createMicrosummary(uri, genURI); @@ -251,8 +251,8 @@ BookmarksStore.prototype = { this._log.debug(" -> creating livemark \"" + record.cleartext.title + "\""); newId = this._ls.createLivemark(parentId, record.cleartext.title, - Utils.makeURI(record.cleartext.siteURI), - Utils.makeURI(record.cleartext.feedURI), + Utils.makeURI(record.siteUri), + Utils.makeURI(record.feedUri), record.sortindex); break; case "incoming-share": @@ -373,9 +373,9 @@ BookmarksStore.prototype = { val, 0, this._ans.EXPIRE_NEVER); break; - case "generatorURI": { + case "generatorUri": { try { - let micsumURI = Utils.makeURI(this._bms.getBookmarkURI(itemId)); + let micsumURI = this._bms.getBookmarkURI(itemId); let genURI = Utils.makeURI(val); if (this._ms == SERVICE_NOT_SUPPORTED) { this._log.warn("Can't create microsummary -- not supported."); @@ -387,10 +387,10 @@ BookmarksStore.prototype = { this._log.debug("Could not set microsummary generator URI: " + e); } } break; - case "siteURI": + case "siteUri": this._ls.setSiteURI(itemId, Utils.makeURI(val)); break; - case "feedURI": + case "feedUri": this._ls.setFeedURI(itemId, Utils.makeURI(val)); break; case "outgoingSharedAnno": @@ -493,7 +493,7 @@ BookmarksStore.prototype = { if (this._ms && this._ms.hasMicrosummary(placeId)) { record = new BookmarkMicsum(); let micsum = this._ms.getMicrosummary(placeId); - record.generatorURI = micsum.generator.uri; // breaks local generators + record.generatorUri = micsum.generator.uri.spec; // breaks local generators record.staticTitle = this._getStaticTitle(placeId); } else { @@ -501,7 +501,7 @@ BookmarksStore.prototype = { record.title = this._bms.getItemTitle(placeId); } - record.bmkUri = this._bms.getBookmarkURI(placeId); + record.bmkUri = this._bms.getBookmarkURI(placeId).spec; record.tags = this._getTags(record.bmkUri); record.keyword = this._bms.getKeywordForBookmark(placeId); record.description = this._getDescription(placeId); @@ -510,8 +510,8 @@ BookmarksStore.prototype = { case this._bms.TYPE_FOLDER: if (this._ls.isLivemark(placeId)) { record = new Livemark(); - record.siteURI = this._ls.getSiteURI(placeId); - record.feedURI = this._ls.getFeedURI(placeId); + record.siteUri = this._ls.getSiteURI(placeId).spec; + record.feedUri = this._ls.getFeedURI(placeId).spec; } else { record = new BookmarkFolder(); diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 193564034691..d7334c576afe 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -105,13 +105,6 @@ Bookmark.prototype = { this._PlacesItem_init(uri); this.cleartext.type = "bookmark"; }, - - set bmkUri(value) { - if (typeof(value) == "string") - this.cleartext.bmkUri = value; - else - this.cleartext.bmkUri = value.spec; - }, }; Utils.deferGetSet(Bookmark, "cleartext", ["title", "bmkUri", "description", @@ -130,7 +123,7 @@ BookmarkMicsum.prototype = { }, }; -Utils.deferGetSet(BookmarkMicsum, "cleartext", ["generatorURI", "staticTitle"]); +Utils.deferGetSet(BookmarkMicsum, "cleartext", ["generatorUri", "staticTitle"]); function BookmarkFolder(uri) { this._BookmarkFolder_init(uri); @@ -158,23 +151,9 @@ Livemark.prototype = { this._BookmarkFolder_init(uri); this.cleartext.type = "livemark"; }, - - set siteURI(value) { - if (typeof(value) == "string") - this.cleartext.siteURI = value; - else - this.cleartext.siteURI = value? value.spec : ""; - }, - - set feedURI(value) { - if (typeof(value) == "string") - this.cleartext.feedURI = value; - else - this.cleartext.feedURI = value? value.spec : ""; - } }; -Utils.deferGetSet(Livemark, "cleartext", ["siteURI", "feedURI"]); +Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]); function BookmarkSeparator(uri) { this._BookmarkSeparator_init(uri); diff --git a/services/sync/modules/type_records/history.js b/services/sync/modules/type_records/history.js index 3b9e651a695e..b11fd7dd9c4e 100644 --- a/services/sync/modules/type_records/history.js +++ b/services/sync/modules/type_records/history.js @@ -62,13 +62,6 @@ HistoryRec.prototype = { this.cleartext = { }; }, - - set histUri(value) { - if (typeof(value) == "string") - this.cleartext.histUri = value; - else - this.cleartext.histUri = value.spec; - }, }; Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]); From 709d0640ead0fe08bc6df9627eca26151ec43a1e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 13 Apr 2009 16:39:29 -0500 Subject: [PATCH 1077/1860] Bug 488190 - Use record properties instead of cleartext properties Switch pretty much all references to cleartext in modules/engines/ to just use the record. Also clean up some references to null cleartext to use deleted. The only reference to cleartext is to iterate over that hash in bookmarks. --- services/sync/modules/engines/bookmarks.js | 45 +++++++++---------- services/sync/modules/engines/forms.js | 6 +-- services/sync/modules/engines/history.js | 4 +- services/sync/modules/engines/passwords.js | 12 ++--- .../sync/modules/type_records/bookmark.js | 10 ++--- 5 files changed, 35 insertions(+), 42 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index dbc456b18459..21e6eac8df8b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -190,31 +190,30 @@ BookmarksStore.prototype = { parentId = this._bms.bookmarksMenuFolder; } - switch (record.cleartext.type) { - case "query": + switch (record.type) { case "bookmark": case "microsummary": { - this._log.debug(" -> creating bookmark \"" + record.cleartext.title + "\""); + this._log.debug(" -> creating bookmark \"" + record.title + "\""); let uri = Utils.makeURI(record.bmkUri); this._log.debug(" -> -> ParentID is " + parentId); this._log.debug(" -> -> uri is " + record.bmkUri); this._log.debug(" -> -> sortindex is " + record.sortindex); - this._log.debug(" -> -> title is " + record.cleartext.title); + this._log.debug(" -> -> title is " + record.title); newId = this._bms.insertBookmark(parentId, uri, record.sortindex, - record.cleartext.title); + record.title); this._ts.untagURI(uri, null); - this._ts.tagURI(uri, record.cleartext.tags); - this._bms.setKeywordForBookmark(newId, record.cleartext.keyword); - if (record.cleartext.description) { + this._ts.tagURI(uri, record.tags); + this._bms.setKeywordForBookmark(newId, record.keyword); + if (record.description) { this._ans.setItemAnnotation(newId, "bookmarkProperties/description", - record.cleartext.description, 0, + record.description, 0, this._ans.EXPIRE_NEVER); } - if (record.cleartext.type == "microsummary") { + if (record.type == "microsummary") { this._log.debug(" \-> is a microsummary"); this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", - record.cleartext.staticTitle || "", 0, this._ans.EXPIRE_NEVER); + record.staticTitle || "", 0, this._ans.EXPIRE_NEVER); let genURI = Utils.makeURI(record.generatorUri); if (this._ms) { try { @@ -228,29 +227,29 @@ BookmarksStore.prototype = { } } break; case "folder": - this._log.debug(" -> creating folder \"" + record.cleartext.title + "\""); + this._log.debug(" -> creating folder \"" + record.title + "\""); newId = this._bms.createFolder(parentId, - record.cleartext.title, + record.title, record.sortindex); // If folder is an outgoing share, put the annotations on it: - if ( record.cleartext.outgoingSharedAnno != undefined ) { + if ( record.outgoingSharedAnno != undefined ) { this._ans.setItemAnnotation(newId, OUTGOING_SHARED_ANNO, - record.cleartext.outgoingSharedAnno, + record.outgoingSharedAnno, 0, this._ans.EXPIRE_NEVER); this._ans.setItemAnnotation(newId, SERVER_PATH_ANNO, - record.cleartext.serverPathAnno, + record.serverPathAnno, 0, this._ans.EXPIRE_NEVER); } break; case "livemark": - this._log.debug(" -> creating livemark \"" + record.cleartext.title + "\""); + this._log.debug(" -> creating livemark \"" + record.title + "\""); newId = this._ls.createLivemark(parentId, - record.cleartext.title, + record.title, Utils.makeURI(record.siteUri), Utils.makeURI(record.feedUri), record.sortindex); @@ -259,18 +258,18 @@ BookmarksStore.prototype = { /* even though incoming shares are folders according to the * bookmarkService, _wrap() wraps them as type=incoming-share, so we * handle them separately, like so: */ - this._log.debug(" -> creating incoming-share \"" + record.cleartext.title + "\""); + this._log.debug(" -> creating incoming-share \"" + record.title + "\""); newId = this._bms.createFolder(parentId, - record.cleartext.title, + record.title, record.sortindex); this._ans.setItemAnnotation(newId, INCOMING_SHARED_ANNO, - record.cleartext.incomingSharedAnno, + record.incomingSharedAnno, 0, this._ans.EXPIRE_NEVER); this._ans.setItemAnnotation(newId, SERVER_PATH_ANNO, - record.cleartext.serverPathAnno, + record.serverPathAnno, 0, this._ans.EXPIRE_NEVER); break; @@ -279,7 +278,7 @@ BookmarksStore.prototype = { newId = this._bms.insertSeparator(parentId, record.sortindex); break; default: - this._log.error("_create: Unknown item type: " + record.cleartext.type); + this._log.error("_create: Unknown item type: " + record.type); break; } if (newId) { diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 8e9ee8b50851..3bc73c73d547 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -78,12 +78,10 @@ FormEngine.prototype = { }, _recordLike: function SyncEngine__recordLike(a, b) { - if (a.cleartext == null || b.cleartext == null) + if (a.deleted || b.deleted) return false; - if (a.cleartext.name == b.cleartext.name && - a.cleartext.value == b.cleartext.value) { + if (a.name == b.name && a.value == b.value) return true; - } return false; } }; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 153e60b23664..db3a212bc75d 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -419,14 +419,14 @@ HistoryStore.prototype = { curvisits = this._getVisits(record.histUri); let visit; - while ((visit = record.cleartext.visits.pop())) { + while ((visit = record.visits.pop())) { if (curvisits.filter(function(i) i.date == visit.date).length) continue; this._log.debug(" visit " + visit.date); this._hsvc.addVisit(uri, visit.date, null, visit.type, (visit.type == 5 || visit.type == 6), 0); } - this._hsvc.setPageTitle(uri, record.cleartext.title); + this._hsvc.setPageTitle(uri, record.title); // Equalize IDs let localId = this._getGUID(record.histUri); diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 4594da5dc8d0..0fdc31779cf1 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -78,17 +78,13 @@ PasswordEngine.prototype = { }, _recordLike: function SyncEngine__recordLike(a, b) { - if (a.cleartext == null || b.cleartext == null) + if (a.deleted || b.deleted) return false; - if (a.cleartext.hostname == b.cleartext.hostname) { - } - if (a.cleartext.hostname != b.cleartext.hostname || - a.cleartext.httpRealm != b.cleartext.httpRealm || - a.cleartext.username != b.cleartext.username) + if (["hostname", "httpRealm", "username"].some(function(k) a[k] != b[k])) return false; - if (!a.cleartext.formSubmitURL || !b.cleartext.formSubmitURL) + if (!a.formSubmitURL || !b.formSubmitURL) return true; - return a.cleartext.formSubmitURL == b.cleartext.formSubmitURL; + return a.formSubmitURL == b.formSubmitURL; } }; diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index d7334c576afe..7dce3c39ef9c 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -103,7 +103,7 @@ Bookmark.prototype = { _Bookmark_init: function BmkRec_init(uri) { this._PlacesItem_init(uri); - this.cleartext.type = "bookmark"; + this.type = "bookmark"; }, }; @@ -119,7 +119,7 @@ BookmarkMicsum.prototype = { _BookmarkMicsum_init: function BmkMicsumRec_init(uri) { this._Bookmark_init(uri); - this.cleartext.type = "microsummary"; + this.type = "microsummary"; }, }; @@ -134,7 +134,7 @@ BookmarkFolder.prototype = { _BookmarkFolder_init: function FolderRec_init(uri) { this._PlacesItem_init(uri); - this.cleartext.type = "folder"; + this.type = "folder"; }, }; @@ -149,7 +149,7 @@ Livemark.prototype = { _Livemark_init: function LvmkRec_init(uri) { this._BookmarkFolder_init(uri); - this.cleartext.type = "livemark"; + this.type = "livemark"; }, }; @@ -164,6 +164,6 @@ BookmarkSeparator.prototype = { _BookmarkSeparator_init: function SepRec_init(uri) { this._PlacesItem_init(uri); - this.cleartext.type = "separator"; + this.type = "separator"; } }; From 3f770b5d5d06740e3c59746f5aa855b2d3d3974b Mon Sep 17 00:00:00 2001 From: Igor Velkov Date: Tue, 14 Apr 2009 11:05:30 -0500 Subject: [PATCH 1078/1860] Bug 484982 - Weave should support SeaMonkey. r=thunder, r=Mardak Add seamonkey bits for manifest/install. Start with Form, History, Password engines.. No places bookmarks and tabs seem to have issues applying(?) --- services/sync/modules/service.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index db203d4239e3..38b9010f4d62 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -394,6 +394,10 @@ WeaveSvc.prototype = { engines = ["Bookmarks", "Form", "History", "Password", "Tab"]; break; + case "SeaMonkey": + engines = ["Form", "History", "Password"]; + break; + case "Thunderbird": engines = ["Cookie", "Password"]; break; From 68bff6f1726be2966e2a8dfc3b70edfff10b99ec Mon Sep 17 00:00:00 2001 From: Date: Mon, 27 Apr 2009 19:50:24 -0700 Subject: [PATCH 1079/1860] Added an openID munger. Turn on the pref extensions.weave.openId.enabled and then load any page with an OpenID login form; Weave alters the form and prefills a URI for a Weave-provided openID. --- services/sync/services-sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9fc36f49c4f4..9e3d292e55ff 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -39,4 +39,5 @@ pref("extensions.weave.log.logger.engine.clients", "Debug"); pref("extensions.weave.network.numRetries", 2); -pref("extensions.weave.tabs.sortMode", "recency"); \ No newline at end of file +pref("extensions.weave.tabs.sortMode", "recency"); +pref("extensions.weave.openId.enabled", false); \ No newline at end of file From 18957dd585c4ce9d1c8a88b9ecc45ec00d28a007 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 28 Apr 2009 20:46:02 +0200 Subject: [PATCH 1080/1860] Remove OAuth (we don't use it anymore) --- services/sync/modules/oauth.js | 156 ------------------------------- services/sync/modules/service.js | 2 - 2 files changed, 158 deletions(-) delete mode 100644 services/sync/modules/oauth.js diff --git a/services/sync/modules/oauth.js b/services/sync/modules/oauth.js deleted file mode 100644 index ecadefd46094..000000000000 --- a/services/sync/modules/oauth.js +++ /dev/null @@ -1,156 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008. - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Anant Narayanan - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['OAuth']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/engines.js"); - -Function.prototype.async = Async.sugar; - -Utils.lazy(this, 'OAuth', OAuthSvc); - -function OAuthSvc() { - this._init(); -} -OAuthSvc.prototype = { - _logName: "OAuth", - _keyring: null, - _consKey: null, - _bulkID: null, - _rsaKey: null, - _token: null, - _cback: null, - _uid: null, - _pwd: null, - _pas: null, - _cb1: null, - _cb2: null, - - _init: function OAuth__init() { - this._log = Log4Moz.repository.getLogger("Service." + this._logName); - this._log.level = "Debug"; - this._log.info("OAuth Module Initialized"); - }, - - setToken: function OAuth_setToken(token, cback) { - this._token = token; - this._cback = cback; - }, - - setUser: function OAuth_setUser(username, password, passphrase) { - this._uid = username; - this._pwd = password; - this._pas = passphrase; - }, - - validate: function OAuth_getName(obj, cb) { - if (!this._token || !this._uid || !this._pwd) - cb(obj, false); - - var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(Ci.nsIXMLHttpRequest); - var key = btoa(this._uid + ":" + this._pwd); - - req.onreadystatechange = function(e) { - if (req.readyState == 4) { - if (req.status == 200) { - var fields = req.responseText.split(','); - cb(obj, fields[0], fields[1], fields[2]); - } else { - cb(obj, req.responseText); - } - } - }; - req.open('GET', 'https://services.mozilla.com/api/oauth/info.php?token=' + this._token + '&key=' + key); - req.send(null); - }, - - finalize: function OAuth_finalize(cb1, cb2, bundle) { - this._cb1 = cb1; - this._cb2 = cb2; - this._bundle = bundle; - - var bmkEngine = Engines.get('bookmarks'); - var bmkRstore = bmkEngine._remote; - - this._keyring = bmkRstore.keys; - this._keyring.getKeyAndIV(Utils.bind2(this, this._gotBulkKey), ID.get('WeaveID')); - }, - - _gotBulkKey: function OAuth_gotBulkKey() { - let consID = new Identity(); - consID.pubkey = this._rsaKey; - consID.username = this._consKey; - this._cb1(this._bundle); - this._log.info("Updating keyring for 3rd party access"); - this._keyring.setKey(Utils.bind2(this, this._done), ID.get('WeaveID'), consID); - }, - - _done: function OAuth__done() { - var cb = this._cb2; - var bu = this._bundle; - - if (!this._token || !this._uid || !this._pwd) - cb(this._bundle, false); - - var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(Ci.nsIXMLHttpRequest); - var key = btoa(this._uid + ":" + this._pwd); - - req.onreadystatechange = function(e) { - if (req.readyState == 4) { - if (req.status == 200 && req.responseText == "1") { - cb(bu, true); - } else { - cb(bu, false); - } - } - }; - req.open('GET', 'https://services.mozilla.com/api/oauth/update.php?token=' + this._token + '&key=' + key); - req.send(null); - } -}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 38b9010f4d62..fd94f6442f43 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -76,7 +76,6 @@ Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/oauth.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines/clientData.js"); @@ -96,7 +95,6 @@ Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/engines.js", Weave); -Cu.import("resource://weave/oauth.js", Weave); Cu.import("resource://weave/engines/bookmarks.js", Weave); Cu.import("resource://weave/engines/clientData.js", Weave); From d350066e6614b988819bbc67c19e02ffe0ac7de6 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Sat, 2 May 2009 16:20:08 -0700 Subject: [PATCH 1081/1860] automatic sign-in with site-specific prefs --- services/sync/modules/ext/Preferences.js | 85 +++++++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/ext/Preferences.js b/services/sync/modules/ext/Preferences.js index b23c71e87b7f..39a01d3a2c26 100644 --- a/services/sync/modules/ext/Preferences.js +++ b/services/sync/modules/ext/Preferences.js @@ -51,9 +51,15 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); const MAX_INT = Math.pow(2, 31) - 1; const MIN_INT = -MAX_INT; -function Preferences(prefBranch) { - if (prefBranch) - this._prefBranch = prefBranch; +function Preferences(args) { + if (isObject(args)) { + if (args.branch) + this._prefBranch = args.branch; + if (args.site) + this._site = args.site; + } + else if (args) + this._prefBranch = args; } Preferences.prototype = { @@ -72,6 +78,13 @@ Preferences.prototype = { if (isArray(prefName)) return prefName.map(function(v) this.get(v, defaultValue), this); + if (this._site) + return this._siteGet(prefName, defaultValue); + else + return this._get(prefName, defaultValue); + }, + + _get: function(prefName, defaultValue) { switch (this._prefSvc.getPrefType(prefName)) { case Ci.nsIPrefBranch.PREF_STRING: return this._prefSvc.getComplexValue(prefName, Ci.nsISupportsString).data; @@ -93,6 +106,11 @@ Preferences.prototype = { } }, + _siteGet: function(prefName, defaultValue) { + let value = this._contentPrefSvc.getPref(this._site, this._prefBranch + prefName); + return typeof value != "undefined" ? value : defaultValue; + }, + /** * Set a preference to a value. * @@ -122,6 +140,13 @@ Preferences.prototype = { return; } + if (this._site) + this._siteSet(prefName, prefValue); + else + this._set(prefName, prefValue); + }, + + _set: function(prefName, prefValue) { let prefType; if (typeof prefValue != "undefined" && prefValue != null) prefType = prefValue.constructor.name; @@ -165,6 +190,10 @@ Preferences.prototype = { } }, + _siteSet: function(prefName, prefValue) { + this._contentPrefSvc.setPref(this._site, this._prefBranch + prefName, prefValue); + }, + /** * Whether or not the given pref has a value. This is different from isSet * because it returns true whether the value of the pref is a default value @@ -183,9 +212,20 @@ Preferences.prototype = { if (isArray(prefName)) return prefName.map(this.has, this); + if (this._site) + return this._siteHas(prefName); + else + return this._has(prefName); + }, + + _has: function(prefName) { return (this._prefSvc.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_INVALID); }, + _siteHas: function(prefName) { + return this._contentPrefSvc.hasPref(this._site, this._prefBranch + prefName); + }, + /** * Whether or not the given pref has a user-set value. This is different * from |has| because it returns true only if the value of the pref is a user- @@ -220,6 +260,13 @@ Preferences.prototype = { return; } + if (this._site) + this._siteReset(prefName); + else + this._reset(prefName); + }, + + _reset: function(prefName) { try { this._prefSvc.clearUserPref(prefName); } @@ -236,6 +283,10 @@ Preferences.prototype = { } }, + _siteReset: function(prefName) { + return this._contentPrefSvc.removePref(this._site, this._prefBranch + prefName); + }, + /** * Lock a pref so it can't be changed. * @@ -367,6 +418,12 @@ Preferences.prototype = { */ _prefBranch: "", + site: function(site) { + if (!(site instanceof Ci.nsIURI)) + site = this._ioSvc.newURI("http://" + site, null, null); + return new Preferences({ branch: this._prefBranch, site: site }); + }, + /** * Preferences Service * @private @@ -378,6 +435,28 @@ Preferences.prototype = { QueryInterface(Ci.nsIPrefBranch2); this.__defineGetter__("_prefSvc", function() prefSvc); return this._prefSvc; + }, + + /** + * IO Service + * @private + */ + get _ioSvc() { + let ioSvc = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + this.__defineGetter__("_ioSvc", function() ioSvc); + return this._ioSvc; + }, + + /** + * Site Preferences Service + * @private + */ + get _contentPrefSvc() { + let contentPrefSvc = Cc["@mozilla.org/content-pref/service;1"]. + getService(Ci.nsIContentPrefService); + this.__defineGetter__("_contentPrefSvc", function() contentPrefSvc); + return this._contentPrefSvc; } }; From 64515ded4e4d72c139810145475be1a7ab51f82f Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Sun, 3 May 2009 02:23:08 -0700 Subject: [PATCH 1082/1860] basic implementation of support for saved logins via login manager --- services/sync/modules/LoginManager.js | 1296 +++++++++++++++++++++++++ 1 file changed, 1296 insertions(+) create mode 100644 services/sync/modules/LoginManager.js diff --git a/services/sync/modules/LoginManager.js b/services/sync/modules/LoginManager.js new file mode 100644 index 000000000000..21be263dc045 --- /dev/null +++ b/services/sync/modules/LoginManager.js @@ -0,0 +1,1296 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Justin Dolske (original author) + * Ehsan Akhgari + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ["WeaveLoginManager"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +let WeaveLoginManager = { + + classDescription: "LoginManager", + contractID: "@mozilla.org/login-manager;1", + classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager, + Ci.nsISupportsWeakReference]), + + + /* ---------- private memebers ---------- */ + + + __logService : null, // Console logging service, used for debugging. + get _logService() { + if (!this.__logService) + this.__logService = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__logService; + }, + + + __ioService: null, // IO service for string -> nsIURI conversion + get _ioService() { + if (!this.__ioService) + this.__ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return this.__ioService; + }, + + + __formFillService : null, // FormFillController, for username autocompleting + get _formFillService() { + if (!this.__formFillService) + this.__formFillService = + Cc["@mozilla.org/satchel/form-fill-controller;1"]. + getService(Ci.nsIFormFillController); + return this.__formFillService; + }, + + + __observerService : null, // Observer Service, for notifications + get _observerService() { + if (!this.__observerService) + this.__observerService = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + return this.__observerService; + }, + + + __storage : null, // Storage component which contains the saved logins + get _storage() { + if (!this.__storage) { + + var contractID = "@mozilla.org/login-manager/storage/mozStorage;1"; + try { + var catMan = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + contractID = catMan.getCategoryEntry("login-manager-storage", + "nsILoginManagerStorage"); + this.log("Found alternate nsILoginManagerStorage with " + + "contract ID: " + contractID); + } catch (e) { + this.log("No alternate nsILoginManagerStorage registered"); + } + + this.__storage = Cc[contractID]. + createInstance(Ci.nsILoginManagerStorage); + try { + this.__storage.init(); + } catch (e) { + this.log("Initialization of storage component failed: " + e); + this.__storage = null; + } + } + + return this.__storage; + }, + + + // Private Browsing Service + // If the service is not available, null will be returned. + __privateBrowsingService : undefined, + get _privateBrowsingService() { + if (this.__privateBrowsingService == undefined) { + if ("@mozilla.org/privatebrowsing;1" in Cc) + this.__privateBrowsingService = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); + else + this.__privateBrowsingService = null; + } + return this.__privateBrowsingService; + }, + + + // Whether we are in private browsing mode + get _inPrivateBrowsing() { + var pbSvc = this._privateBrowsingService; + if (pbSvc) + return pbSvc.privateBrowsingEnabled; + else + return false; + }, + + _prefBranch : null, // Preferences service + _nsLoginInfo : null, // Constructor for nsILoginInfo implementation + + _remember : true, // mirrors signon.rememberSignons preference + _debug : false, // mirrors signon.debug + + + /* + * init + * + * Initialize the Login Manager. Automatically called when service + * is created. + * + * Note: Service created in /browser/base/content/browser.js, + * delayedStartup() + */ + init : function () { + + // Cache references to current |this| in utility objects + this._webProgressListener._domEventListener = this._domEventListener; + this._webProgressListener._pwmgr = this; + this._domEventListener._pwmgr = this; + this._observer._pwmgr = this; + + // Preferences. Add observer so we get notified of changes. + this._prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService).getBranch("signon."); + this._prefBranch.QueryInterface(Ci.nsIPrefBranch2); + this._prefBranch.addObserver("", this._observer, false); + + // Get current preference values. + this._debug = this._prefBranch.getBoolPref("debug"); + + this._remember = this._prefBranch.getBoolPref("rememberSignons"); + + + // Get constructor for nsILoginInfo + this._nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo); + + + // Form submit observer checks forms for new logins and pw changes. + this._observerService.addObserver(this._observer, "earlyformsubmit", false); + this._observerService.addObserver(this._observer, "xpcom-shutdown", false); + + // WebProgressListener for getting notification of new doc loads. + var progress = Cc["@mozilla.org/docloaderservice;1"]. + getService(Ci.nsIWebProgress); + progress.addProgressListener(this._webProgressListener, + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); + + + }, + + + /* + * log + * + * Internal function for logging debug messages to the Error Console window + */ + log : function (message) { + if (!this._debug) + return; + dump("Login Manager: " + message + "\n"); + this._logService.logStringMessage("Login Manager: " + message); + }, + + + /* ---------- Utility objects ---------- */ + + + /* + * _observer object + * + * Internal utility object, implements the nsIObserver interface. + * Used to receive notification for: form submission, preference changes. + */ + _observer : { + _pwmgr : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsIFormSubmitObserver, + Ci.nsISupportsWeakReference]), + + + // nsFormSubmitObserver + notify : function (formElement, aWindow, actionURI) { + this._pwmgr.log("observer notified for form submission."); + + // We're invoked before the content's |onsubmit| handlers, so we + // can grab form data before it might be modified (see bug 257781). + + try { + this._pwmgr._onFormSubmit(formElement); + } catch (e) { + this._pwmgr.log("Caught error in onFormSubmit: " + e); + } + + return true; // Always return true, or form submit will be canceled. + }, + + // nsObserver + observe : function (subject, topic, data) { + + if (topic == "nsPref:changed") { + var prefName = data; + this._pwmgr.log("got change to " + prefName + " preference"); + + if (prefName == "debug") { + this._pwmgr._debug = + this._pwmgr._prefBranch.getBoolPref("debug"); + } else if (prefName == "rememberSignons") { + this._pwmgr._remember = + this._pwmgr._prefBranch.getBoolPref("rememberSignons"); + } else { + this._pwmgr.log("Oops! Pref not handled, change ignored."); + } + } else if (topic == "xpcom-shutdown") { + for (let i in this._pwmgr) { + try { + this._pwmgr[i] = null; + } catch(ex) {} + } + this._pwmgr = null; + } else { + this._pwmgr.log("Oops! Unexpected notification: " + topic); + } + } + }, + + + /* + * _webProgressListener object + * + * Internal utility object, implements nsIWebProgressListener interface. + * This is attached to the document loader service, so we get + * notifications about all page loads. + */ + _webProgressListener : { + _pwmgr : null, + _domEventListener : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + + onStateChange : function (aWebProgress, aRequest, + aStateFlags, aStatus) { + + // STATE_START is too early, doc is still the old page. + if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING)) + return; + + if (!this._pwmgr._remember) + return; + + var domWin = aWebProgress.DOMWindow; + var domDoc = domWin.document; + + // Only process things which might have HTML forms. + if (!(domDoc instanceof Ci.nsIDOMHTMLDocument)) + return; + + this._pwmgr.log("onStateChange accepted: req = " + + (aRequest ? aRequest.name : "(null)") + + ", flags = 0x" + aStateFlags.toString(16)); + + // Fastback doesn't fire DOMContentLoaded, so process forms now. + if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) { + this._pwmgr.log("onStateChange: restoring document"); + return this._pwmgr._fillDocument(domDoc); + } + + // Add event listener to process page when DOM is complete. + domDoc.addEventListener("DOMContentLoaded", + this._domEventListener, false); + return; + }, + + // stubs for the nsIWebProgressListener interfaces which we don't use. + onProgressChange : function() { throw "Unexpected onProgressChange"; }, + onLocationChange : function() { throw "Unexpected onLocationChange"; }, + onStatusChange : function() { throw "Unexpected onStatusChange"; }, + onSecurityChange : function() { throw "Unexpected onSecurityChange"; } + }, + + + /* + * _domEventListener object + * + * Internal utility object, implements nsIDOMEventListener + * Used to catch certain DOM events needed to properly implement form fill. + */ + _domEventListener : { + _pwmgr : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener, + Ci.nsISupportsWeakReference]), + + + handleEvent : function (event) { + this._pwmgr.log("domEventListener: got event " + event.type); + + switch (event.type) { + case "DOMContentLoaded": + this._pwmgr._fillDocument(event.target); + return; + + case "DOMAutoComplete": + case "blur": + var acInputField = event.target; + var acForm = acInputField.form; + // Make sure the username field fillForm will use is the + // same field as the autocomplete was activated on. If + // not, the DOM has been altered and we'll just give up. + var [usernameField, passwordField, ignored] = + this._pwmgr._getFormFields(acForm, false); + if (usernameField == acInputField && passwordField) { + // Clobber any existing password. + passwordField.value = ""; + this._pwmgr._fillForm(acForm, true, true, null); + } else { + this._pwmgr.log("Oops, form changed before AC invoked"); + } + return; + + default: + this._pwmgr.log("Oops! This event unexpected."); + return; + } + } + }, + + + + + /* ---------- Primary Public interfaces ---------- */ + + + + + /* + * addLogin + * + * Add a new login to login storage. + */ + addLogin : function (login) { + // Sanity check the login + if (login.hostname == null || login.hostname.length == 0) + throw "Can't add a login with a null or empty hostname."; + + // For logins w/o a username, set to "", not null. + if (login.username == null) + throw "Can't add a login with a null username."; + + if (login.password == null || login.password.length == 0) + throw "Can't add a login with a null or empty password."; + + if (login.formSubmitURL || login.formSubmitURL == "") { + // We have a form submit URL. Can't have a HTTP realm. + if (login.httpRealm != null) + throw "Can't add a login with both a httpRealm and formSubmitURL."; + } else if (login.httpRealm) { + // We have a HTTP realm. Can't have a form submit URL. + if (login.formSubmitURL != null) + throw "Can't add a login with both a httpRealm and formSubmitURL."; + } else { + // Need one or the other! + throw "Can't add a login without a httpRealm or formSubmitURL."; + } + + + // Look for an existing entry. + var logins = this.findLogins({}, login.hostname, login.formSubmitURL, + login.httpRealm); + + if (logins.some(function(l) login.matches(l, true))) + throw "This login already exists."; + + this.log("Adding login: " + login); + return this._storage.addLogin(login); + }, + + + /* + * removeLogin + * + * Remove the specified login from the stored logins. + */ + removeLogin : function (login) { + this.log("Removing login: " + login); + return this._storage.removeLogin(login); + }, + + + /* + * modifyLogin + * + * Change the specified login to match the new login. + */ + modifyLogin : function (oldLogin, newLogin) { + this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin); + return this._storage.modifyLogin(oldLogin, newLogin); + }, + + + /* + * getAllLogins + * + * Get a dump of all stored logins. Used by the login manager UI. + * + * |count| is only needed for XPCOM. + * + * Returns an array of logins. If there are no logins, the array is empty. + */ + getAllLogins : function (count) { + this.log("Getting a list of all logins"); + return this._storage.getAllLogins(count); + }, + + + /* + * removeAllLogins + * + * Remove all stored logins. + */ + removeAllLogins : function () { + this.log("Removing all logins"); + this._storage.removeAllLogins(); + }, + + /* + * getAllDisabledHosts + * + * Get a list of all hosts for which logins are disabled. + * + * |count| is only needed for XPCOM. + * + * Returns an array of disabled logins. If there are no disabled logins, + * the array is empty. + */ + getAllDisabledHosts : function (count) { + this.log("Getting a list of all disabled hosts"); + return this._storage.getAllDisabledHosts(count); + }, + + + /* + * findLogins + * + * Search for the known logins for entries matching the specified criteria. + */ + findLogins : function (count, hostname, formSubmitURL, httpRealm) { + this.log("Searching for logins matching host: " + hostname + + ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); + + return this._storage.findLogins(count, hostname, formSubmitURL, + httpRealm); + }, + + + /* + * searchLogins + * + * Public wrapper around _searchLogins to convert the nsIPropertyBag to a + * JavaScript object and decrypt the results. + * + * Returns an array of decrypted nsILoginInfo. + */ + searchLogins : function(count, matchData) { + this.log("Searching for logins"); + + return this._storage.searchLogins(count, matchData); + }, + + + /* + * countLogins + * + * Search for the known logins for entries matching the specified criteria, + * returns only the count. + */ + countLogins : function (hostname, formSubmitURL, httpRealm) { + this.log("Counting logins matching host: " + hostname + + ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); + + return this._storage.countLogins(hostname, formSubmitURL, httpRealm); + }, + + + /* + * getLoginSavingEnabled + * + * Check to see if user has disabled saving logins for the host. + */ + getLoginSavingEnabled : function (host) { + this.log("Checking if logins to " + host + " can be saved."); + if (!this._remember) + return false; + + return this._storage.getLoginSavingEnabled(host); + }, + + + /* + * setLoginSavingEnabled + * + * Enable or disable storing logins for the specified host. + */ + setLoginSavingEnabled : function (hostname, enabled) { + // Nulls won't round-trip with getAllDisabledHosts(). + if (hostname.indexOf("\0") != -1) + throw "Invalid hostname"; + + this.log("Saving logins for " + hostname + " enabled? " + enabled); + return this._storage.setLoginSavingEnabled(hostname, enabled); + }, + + + /* + * autoCompleteSearch + * + * Yuck. This is called directly by satchel: + * nsFormFillController::StartSearch() + * [toolkit/components/satchel/src/nsFormFillController.cpp] + * + * We really ought to have a simple way for code to register an + * auto-complete provider, and not have satchel calling pwmgr directly. + */ + autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) { + // aPreviousResult & aResult are nsIAutoCompleteResult, + // aElement is nsIDOMHTMLInputElement + + if (!this._remember) + return false; + + this.log("AutoCompleteSearch invoked. Search is: " + aSearchString); + + var result = null; + + if (aPreviousResult) { + this.log("Using previous autocomplete result"); + result = aPreviousResult; + + // We have a list of results for a shorter search string, so just + // filter them further based on the new search string. + // Count backwards, because result.matchCount is decremented + // when we remove an entry. + for (var i = result.matchCount - 1; i >= 0; i--) { + var match = result.getValueAt(i); + + // Remove results that are too short, or have different prefix. + if (aSearchString.length > match.length || + aSearchString.toLowerCase() != + match.substr(0, aSearchString.length).toLowerCase()) + { + this.log("Removing autocomplete entry '" + match + "'"); + result.removeValueAt(i, false); + } + } + } else { + this.log("Creating new autocomplete search result."); + + var doc = aElement.ownerDocument; + var origin = this._getPasswordOrigin(doc.documentURI); + var actionOrigin = this._getActionOrigin(aElement.form); + + var logins = this.findLogins({}, origin, actionOrigin, null); + var matchingLogins = []; + + for (i = 0; i < logins.length; i++) { + var username = logins[i].username.toLowerCase(); + if (aSearchString.length <= username.length && + aSearchString.toLowerCase() == + username.substr(0, aSearchString.length)) + { + matchingLogins.push(logins[i]); + } + } + this.log(matchingLogins.length + " autocomplete logins avail."); + result = new UserAutoCompleteResult(aSearchString, matchingLogins); + } + + return result; + }, + + + + + /* ------- Internal methods / callbacks for document integration ------- */ + + + + + /* + * _getPasswordFields + * + * Returns an array of password field elements for the specified form. + * If no pw fields are found, or if more than 3 are found, then null + * is returned. + * + * skipEmptyFields can be set to ignore password fields with no value. + */ + _getPasswordFields : function (form, skipEmptyFields) { + // Locate the password fields in the form. + var pwFields = []; + for (var i = 0; i < form.elements.length; i++) { + if (form.elements[i].type != "password") + continue; + + if (skipEmptyFields && !form.elements[i].value) + continue; + + pwFields[pwFields.length] = { + index : i, + element : form.elements[i] + }; + } + + // If too few or too many fields, bail out. + if (pwFields.length == 0) { + this.log("(form ignored -- no password fields.)"); + return null; + } else if (pwFields.length > 3) { + this.log("(form ignored -- too many password fields. [got " + + pwFields.length + "])"); + return null; + } + + return pwFields; + }, + + + /* + * _getFormFields + * + * Returns the username and password fields found in the form. + * Can handle complex forms by trying to figure out what the + * relevant fields are. + * + * Returns: [usernameField, newPasswordField, oldPasswordField] + * + * usernameField may be null. + * newPasswordField will always be non-null. + * oldPasswordField may be null. If null, newPasswordField is just + * "theLoginField". If not null, the form is apparently a + * change-password field, with oldPasswordField containing the password + * that is being changed. + */ + _getFormFields : function (form, isSubmission) { + var usernameField = null; + + // Locate the password field(s) in the form. Up to 3 supported. + // If there's no password field, there's nothing for us to do. + var pwFields = this._getPasswordFields(form, isSubmission); + if (!pwFields) + return [null, null, null]; + + + // Locate the username field in the form by searching backwards + // from the first passwordfield, assume the first text field is the + // username. We might not find a username field if the user is + // already logged in to the site. + for (var i = pwFields[0].index - 1; i >= 0; i--) { + if (form.elements[i].type == "text") { + usernameField = form.elements[i]; + break; + } + } + + if (!usernameField) + this.log("(form -- no username field found)"); + + + // If we're not submitting a form (it's a page load), there are no + // password field values for us to use for identifying fields. So, + // just assume the first password field is the one to be filled in. + if (!isSubmission || pwFields.length == 1) + return [usernameField, pwFields[0].element, null]; + + + // Try to figure out WTF is in the form based on the password values. + var oldPasswordField, newPasswordField; + var pw1 = pwFields[0].element.value; + var pw2 = pwFields[1].element.value; + var pw3 = (pwFields[2] ? pwFields[2].element.value : null); + + if (pwFields.length == 3) { + // Look for two identical passwords, that's the new password + + if (pw1 == pw2 && pw2 == pw3) { + // All 3 passwords the same? Weird! Treat as if 1 pw field. + newPasswordField = pwFields[0].element; + oldPasswordField = null; + } else if (pw1 == pw2) { + newPasswordField = pwFields[0].element; + oldPasswordField = pwFields[2].element; + } else if (pw2 == pw3) { + oldPasswordField = pwFields[0].element; + newPasswordField = pwFields[2].element; + } else if (pw1 == pw3) { + // A bit odd, but could make sense with the right page layout. + newPasswordField = pwFields[0].element; + oldPasswordField = pwFields[1].element; + } else { + // We can't tell which of the 3 passwords should be saved. + this.log("(form ignored -- all 3 pw fields differ)"); + return [null, null, null]; + } + } else { // pwFields.length == 2 + if (pw1 == pw2) { + // Treat as if 1 pw field + newPasswordField = pwFields[0].element; + oldPasswordField = null; + } else { + // Just assume that the 2nd password is the new password + oldPasswordField = pwFields[0].element; + newPasswordField = pwFields[1].element; + } + } + + return [usernameField, newPasswordField, oldPasswordField]; + }, + + + /* + * _isAutoCompleteDisabled + * + * Returns true if the page requests autocomplete be disabled for the + * specified form input. + */ + _isAutocompleteDisabled : function (element) { + if (element && element.hasAttribute("autocomplete") && + element.getAttribute("autocomplete").toLowerCase() == "off") + return true; + + return false; + }, + + /* + * _onFormSubmit + * + * Called by the our observer when notified of a form submission. + * [Note that this happens before any DOM onsubmit handlers are invoked.] + * Looks for a password change in the submitted form, so we can update + * our stored password. + */ + _onFormSubmit : function (form) { + + // local helper function + function getPrompter(aWindow) { + var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"]. + createInstance(Ci.nsILoginManagerPrompter); + prompterSvc.init(aWindow); + return prompterSvc; + } + + if (this._inPrivateBrowsing) { + // We won't do anything in private browsing mode anyway, + // so there's no need to perform further checks. + this.log("(form submission ignored in private browsing mode)"); + return; + } + + var doc = form.ownerDocument; + var win = doc.defaultView; + + // If password saving is disabled (globally or for host), bail out now. + if (!this._remember) + return; + + var hostname = this._getPasswordOrigin(doc.documentURI); + var formSubmitURL = this._getActionOrigin(form) + if (!this.getLoginSavingEnabled(hostname)) { + this.log("(form submission ignored -- saving is " + + "disabled for: " + hostname + ")"); + return; + } + + + // Get the appropriate fields from the form. + var [usernameField, newPasswordField, oldPasswordField] = + this._getFormFields(form, true); + + // Need at least 1 valid password field to do anything. + if (newPasswordField == null) + return; + + // Check for autocomplete=off attribute. We don't use it to prevent + // autofilling (for existing logins), but won't save logins when it's + // present. + if (this._isAutocompleteDisabled(form) || + this._isAutocompleteDisabled(usernameField) || + this._isAutocompleteDisabled(newPasswordField) || + this._isAutocompleteDisabled(oldPasswordField)) { + this.log("(form submission ignored -- autocomplete=off found)"); + return; + } + + + var formLogin = new this._nsLoginInfo(); + formLogin.init(hostname, formSubmitURL, null, + (usernameField ? usernameField.value : ""), + newPasswordField.value, + (usernameField ? usernameField.name : ""), + newPasswordField.name); + + // If we didn't find a username field, but seem to be changing a + // password, allow the user to select from a list of applicable + // logins to update the password for. + if (!usernameField && oldPasswordField) { + + var logins = this.findLogins({}, hostname, formSubmitURL, null); + + if (logins.length == 0) { + // Could prompt to save this as a new password-only login. + // This seems uncommon, and might be wrong, so ignore. + this.log("(no logins for this host -- pwchange ignored)"); + return; + } + + var prompter = getPrompter(win); + + if (logins.length == 1) { + var oldLogin = logins[0]; + formLogin.username = oldLogin.username; + formLogin.usernameField = oldLogin.usernameField; + + prompter.promptToChangePassword(oldLogin, formLogin); + } else { + prompter.promptToChangePasswordWithUsernames( + logins, logins.length, formLogin); + } + + return; + } + + + // Look for an existing login that matches the form login. + var existingLogin = null; + var logins = this.findLogins({}, hostname, formSubmitURL, null); + + for (var i = 0; i < logins.length; i++) { + var same, login = logins[i]; + + // If one login has a username but the other doesn't, ignore + // the username when comparing and only match if they have the + // same password. Otherwise, compare the logins and match even + // if the passwords differ. + if (!login.username && formLogin.username) { + var restoreMe = formLogin.username; + formLogin.username = ""; + same = formLogin.matches(login, false); + formLogin.username = restoreMe; + } else if (!formLogin.username && login.username) { + formLogin.username = login.username; + same = formLogin.matches(login, false); + formLogin.username = ""; // we know it's always blank. + } else { + same = formLogin.matches(login, true); + } + + if (same) { + existingLogin = login; + break; + } + } + + if (existingLogin) { + this.log("Found an existing login matching this form submission"); + + // Change password if needed. + if (existingLogin.password != formLogin.password) { + this.log("...passwords differ, prompting to change."); + prompter = getPrompter(win); + prompter.promptToChangePassword(existingLogin, formLogin); + } + + return; + } + + + // Prompt user to save login (via dialog or notification bar) + prompter = getPrompter(win); + prompter.promptToSavePassword(formLogin); + }, + + + /* + * _getPasswordOrigin + * + * Get the parts of the URL we want for identification. + */ + _getPasswordOrigin : function (uriString, allowJS) { + var realm = ""; + try { + var uri = this._ioService.newURI(uriString, null, null); + + if (allowJS && uri.scheme == "javascript") + return "javascript:" + + realm = uri.scheme + "://" + uri.host; + + // If the URI explicitly specified a port, only include it when + // it's not the default. (We never want "http://foo.com:80") + var port = uri.port; + if (port != -1) { + var handler = this._ioService.getProtocolHandler(uri.scheme); + if (port != handler.defaultPort) + realm += ":" + port; + } + + } catch (e) { + // bug 159484 - disallow url types that don't support a hostPort. + // (although we handle "javascript:..." as a special case above.) + this.log("Couldn't parse origin for " + uriString); + realm = null; + } + + return realm; + }, + + _getActionOrigin : function (form) { + var uriString = form.action; + + // A blank or mission action submits to where it came from. + if (uriString == "") + uriString = form.baseURI; // ala bug 297761 + + return this._getPasswordOrigin(uriString, true); + }, + + + /* + * _fillDocument + * + * Called when a page has loaded. For each form in the document, + * we check to see if it can be filled with a stored login. + */ + _fillDocument : function (doc) { + var forms = doc.forms; + if (!forms || forms.length == 0) + return []; + + var formOrigin = this._getPasswordOrigin(doc.documentURI); + + // If there are no logins for this site, bail out now. + if (!this.countLogins(formOrigin, "", null)) + return []; + + this.log("fillDocument processing " + forms.length + + " forms on " + doc.documentURI); + + var autofillForm = !this._inPrivateBrowsing && + this._prefBranch.getBoolPref("autofillForms"); + var previousActionOrigin = null; + var foundLogins = null; + + let auths = []; + for (var i = 0; i < forms.length; i++) { + var form = forms[i]; + + // Only the actionOrigin might be changing, so if it's the same + // as the last form on the page we can reuse the same logins. + var actionOrigin = this._getActionOrigin(form); + if (actionOrigin != previousActionOrigin) { + foundLogins = null; + previousActionOrigin = actionOrigin; + } + this.log("_fillDocument processing form[" + i + "]"); + let auth = this._fillForm(form, autofillForm, false, foundLogins); + foundLogins = auth.foundLogins; + if (auth.canFillForm) + auths.push(auth); + } // foreach form + return auths; + }, + + + /* + * _fillform + * + * Fill the form with login information if we can find it. This will find + * an array of logins if not given any, otherwise it will use the logins + * passed in. The logins are returned so they can be reused for + * optimization. Success of action is also returned in format + * [success, foundLogins]. autofillForm denotes if we should fill the form + * in automatically, ignoreAutocomplete denotes if we should ignore + * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo + * for optimization + */ + _fillForm : function (form, autofillForm, ignoreAutocomplete, foundLogins) { + // Heuristically determine what the user/pass fields are + // We do this before checking to see if logins are stored, + // so that the user isn't prompted for a master password + // without need. + var [usernameField, passwordField, ignored] = + this._getFormFields(form, false); + + // Need a valid password field to do anything. + if (passwordField == null) + return { foundLogins: foundLogins }; + + // If the fields are disabled or read-only, there's nothing to do. + if (passwordField.disabled || passwordField.readOnly || + usernameField && (usernameField.disabled || + usernameField.readOnly)) { + this.log("not filling form, login fields disabled"); + return { foundLogins: foundLogins }; + } + + // Need to get a list of logins if we weren't given them + if (foundLogins == null) { + var formOrigin = + this._getPasswordOrigin(form.ownerDocument.documentURI); + var actionOrigin = this._getActionOrigin(form); + foundLogins = this.findLogins({}, formOrigin, actionOrigin, null); + this.log("found " + foundLogins.length + " matching logins."); + } else { + this.log("reusing logins from last form."); + } + + // Discard logins which have username/password values that don't + // fit into the fields (as specified by the maxlength attribute). + // The user couldn't enter these values anyway, and it helps + // with sites that have an extra PIN to be entered (bug 391514) + var maxUsernameLen = Number.MAX_VALUE; + var maxPasswordLen = Number.MAX_VALUE; + + // If attribute wasn't set, default is -1. + if (usernameField && usernameField.maxLength >= 0) + maxUsernameLen = usernameField.maxLength; + if (passwordField.maxLength >= 0) + maxPasswordLen = passwordField.maxLength; + + logins = foundLogins.filter(function (l) { + var fit = (l.username.length <= maxUsernameLen && + l.password.length <= maxPasswordLen); + if (!fit) + this.log("Ignored " + l.username + " login: won't fit"); + + return fit; + }, this); + + + // Nothing to do if we have no matching logins available. + if (logins.length == 0) + return { foundLogins: foundLogins }; + + + + // Attach autocomplete stuff to the username field, if we have + // one. This is normally used to select from multiple accounts, + // but even with one account we should refill if the user edits. + //if (usernameField) + // this._attachToInput(usernameField); + + // Don't clobber an existing password. + if (passwordField.value) + return { foundLogins: foundLogins }; + + // If the form has an autocomplete=off attribute in play, don't + // fill in the login automatically. We check this after attaching + // the autocomplete stuff to the username field, so the user can + // still manually select a login to be filled in. + var isFormDisabled = false; + if (!ignoreAutocomplete && + (this._isAutocompleteDisabled(form) || + this._isAutocompleteDisabled(usernameField) || + this._isAutocompleteDisabled(passwordField))) { + + isFormDisabled = true; + this.log("form not filled, has autocomplete=off"); + } + + // Variable such that we reduce code duplication and can be sure we + // should be firing notifications if and only if we can fill the form. + var selectedLogin = null; + + if (usernameField && usernameField.value) { + // If username was specified in the form, only fill in the + // password if we find a matching login. + var username = usernameField.value.toLowerCase(); + + let matchingLogins = logins.filter(function(l) + l.username.toLowerCase() == username); + if (matchingLogins.length) + selectedLogin = matchingLogins[0]; + else + this.log("Password not filled. None of the stored " + + "logins match the username already present."); + } else if (logins.length == 1) { + selectedLogin = logins[0]; + } else { + // We have multiple logins. Handle a special case here, for sites + // which have a normal user+pass login *and* a password-only login + // (eg, a PIN). Prefer the login that matches the type of the form + // (user+pass or pass-only) when there's exactly one that matches. + let matchingLogins; + if (usernameField) + matchingLogins = logins.filter(function(l) l.username); + else + matchingLogins = logins.filter(function(l) !l.username); + if (matchingLogins.length == 1) + selectedLogin = matchingLogins[0]; + else + this.log("Multiple logins for form, so not filling any."); + } + + //var didFillForm = false; + //if (selectedLogin && autofillForm && !isFormDisabled) { + // // Fill the form + // if (usernameField) + // usernameField.value = selectedLogin.username; + // passwordField.value = selectedLogin.password; + // didFillForm = true; + //} else if (selectedLogin && !autofillForm) { + // // For when autofillForm is false, but we still have the information + // // to fill a form, we notify observers. + // this._observerService.notifyObservers(form, "passwordmgr-found-form", "noAutofillForms"); + // this.log("autofillForms=false but form can be filled; notified observers"); + //} else if (selectedLogin && isFormDisabled) { + // // For when autocomplete is off, but we still have the information + // // to fill a form, we notify observers. + // this._observerService.notifyObservers(form, "passwordmgr-found-form", "autocompleteOff"); + // this.log("autocomplete=off but form can be filled; notified observers"); + //} + + return { canFillForm: true, + form: form, + usernameField: usernameField, + passwordField: passwordField, + foundLogins: foundLogins, + selectedLogin: selectedLogin }; + }, + + + /* + * fillForm + * + * Fill the form with login information if we can find it. + */ + fillForm : function (form) { + this.log("fillForm processing form[id=" + form.id + "]"); + return this._fillForm(form, true, true, null)[0]; + }, + + + /* + * _attachToInput + * + * Hooks up autocomplete support to a username field, to allow + * a user editing the field to select an existing login and have + * the password field filled in. + */ + _attachToInput : function (element) { + this.log("attaching autocomplete stuff"); + element.addEventListener("blur", + this._domEventListener, false); + element.addEventListener("DOMAutoComplete", + this._domEventListener, false); + this._formFillService.markAsLoginManagerField(element); + } +}; // end of LoginManager implementation + + + + +// nsIAutoCompleteResult implementation +function UserAutoCompleteResult (aSearchString, matchingLogins) { + function loginSort(a,b) { + var userA = a.username.toLowerCase(); + var userB = b.username.toLowerCase(); + + if (userA < userB) + return -1; + + if (userB > userA) + return 1; + + return 0; + }; + + this.searchString = aSearchString; + this.logins = matchingLogins.sort(loginSort); + this.matchCount = matchingLogins.length; + + if (this.matchCount > 0) { + this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; + this.defaultIndex = 0; + } +} + +UserAutoCompleteResult.prototype = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, + Ci.nsISupportsWeakReference]), + + // private + logins : null, + + // Interfaces from idl... + searchString : null, + searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH, + defaultIndex : -1, + errorDescription : "", + matchCount : 0, + + getValueAt : function (index) { + if (index < 0 || index >= this.logins.length) + throw "Index out of range."; + + return this.logins[index].username; + }, + + getCommentAt : function (index) { + return ""; + }, + + getStyleAt : function (index) { + return ""; + }, + + getImageAt : function (index) { + return ""; + }, + + removeValueAt : function (index, removeFromDB) { + if (index < 0 || index >= this.logins.length) + throw "Index out of range."; + + var [removedLogin] = this.logins.splice(index, 1); + + this.matchCount--; + if (this.defaultIndex > this.logins.length) + this.defaultIndex--; + + if (removeFromDB) { + var pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeLogin(removedLogin); + } + } +}; + +//var component = [LoginManager]; +//function NSGetModule (compMgr, fileSpec) { +// return XPCOMUtils.generateModule(component); +//} +WeaveLoginManager.init(); From 46a78dbae3cebae8bed5cec58a2278fafeec881e Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Sun, 3 May 2009 15:26:06 -0700 Subject: [PATCH 1083/1860] make it possible to auto-auth using login info from the login manager; clean up terminology --- services/sync/modules/LoginManager.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/LoginManager.js b/services/sync/modules/LoginManager.js index 21be263dc045..d687cb4881e5 100644 --- a/services/sync/modules/LoginManager.js +++ b/services/sync/modules/LoginManager.js @@ -1003,7 +1003,7 @@ let WeaveLoginManager = { var previousActionOrigin = null; var foundLogins = null; - let auths = []; + let formInfos = []; for (var i = 0; i < forms.length; i++) { var form = forms[i]; @@ -1015,12 +1015,12 @@ let WeaveLoginManager = { previousActionOrigin = actionOrigin; } this.log("_fillDocument processing form[" + i + "]"); - let auth = this._fillForm(form, autofillForm, false, foundLogins); - foundLogins = auth.foundLogins; - if (auth.canFillForm) - auths.push(auth); + let formInfo = this._fillForm(form, autofillForm, false, foundLogins); + foundLogins = formInfo.foundLogins; + if (formInfo.canFillForm) + formInfos.push(formInfo); } // foreach form - return auths; + return formInfos; }, @@ -1174,7 +1174,6 @@ let WeaveLoginManager = { //} return { canFillForm: true, - form: form, usernameField: usernameField, passwordField: passwordField, foundLogins: foundLogins, From f42500ccd8171d8a589dbe16f0fb839eca4fef5a Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Tue, 5 May 2009 14:16:49 -0700 Subject: [PATCH 1084/1860] disable the authenticator unless it is enabled via the authenticator.enabled pref --- services/sync/services-sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9e3d292e55ff..22af09f9ef76 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -40,4 +40,5 @@ pref("extensions.weave.log.logger.engine.clients", "Debug"); pref("extensions.weave.network.numRetries", 2); pref("extensions.weave.tabs.sortMode", "recency"); -pref("extensions.weave.openId.enabled", false); \ No newline at end of file +pref("extensions.weave.openId.enabled", false); +pref("extensions.weave.authenticator.enabled", false); From 6b14f7b172365257d0750d8450d6d0c514c9f8ca Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 6 May 2009 20:11:19 -0700 Subject: [PATCH 1085/1860] default openid/authenticator to on --- services/sync/services-sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 22af09f9ef76..d6759f80dccc 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -40,5 +40,5 @@ pref("extensions.weave.log.logger.engine.clients", "Debug"); pref("extensions.weave.network.numRetries", 2); pref("extensions.weave.tabs.sortMode", "recency"); -pref("extensions.weave.openId.enabled", false); -pref("extensions.weave.authenticator.enabled", false); +pref("extensions.weave.openId.enabled", true); +pref("extensions.weave.authenticator.enabled", true); From 70384430bcd8a5d1ba4ab03c01fbfda9c8962c70 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 11 May 2009 00:58:44 -0700 Subject: [PATCH 1086/1860] move authenticator code into authenticator module --- services/sync/modules/{LoginManager.js => authenticator.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename services/sync/modules/{LoginManager.js => authenticator.js} (100%) diff --git a/services/sync/modules/LoginManager.js b/services/sync/modules/authenticator.js similarity index 100% rename from services/sync/modules/LoginManager.js rename to services/sync/modules/authenticator.js From c0b7547ccbcad8e36115e92d25788edab260eca4 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 11 May 2009 01:30:46 -0700 Subject: [PATCH 1087/1860] remove a bunch of login manager code that isn't being used by new Authenticator module --- services/sync/modules/authenticator.js | 648 +------------------------ 1 file changed, 10 insertions(+), 638 deletions(-) diff --git a/services/sync/modules/authenticator.js b/services/sync/modules/authenticator.js index d687cb4881e5..3c6492423d42 100644 --- a/services/sync/modules/authenticator.js +++ b/services/sync/modules/authenticator.js @@ -35,25 +35,16 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ["WeaveLoginManager"]; +const EXPORTED_SYMBOLS = ["Authenticator"]; const Cc = Components.classes; const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -let WeaveLoginManager = { - - classDescription: "LoginManager", - contractID: "@mozilla.org/login-manager;1", - classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"), - QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager, - Ci.nsISupportsWeakReference]), - - - /* ---------- private memebers ---------- */ - +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +let Authenticator = { __logService : null, // Console logging service, used for debugging. get _logService() { if (!this.__logService) @@ -62,7 +53,6 @@ let WeaveLoginManager = { return this.__logService; }, - __ioService: null, // IO service for string -> nsIURI conversion get _ioService() { if (!this.__ioService) @@ -155,18 +145,14 @@ let WeaveLoginManager = { /* * init * - * Initialize the Login Manager. Automatically called when service + * Initialize the Authenticator. Automatically called when service * is created. * * Note: Service created in /browser/base/content/browser.js, * delayedStartup() */ init : function () { - // Cache references to current |this| in utility objects - this._webProgressListener._domEventListener = this._domEventListener; - this._webProgressListener._pwmgr = this; - this._domEventListener._pwmgr = this; this._observer._pwmgr = this; // Preferences. Add observer so we get notified of changes. @@ -177,29 +163,13 @@ let WeaveLoginManager = { // Get current preference values. this._debug = this._prefBranch.getBoolPref("debug"); - this._remember = this._prefBranch.getBoolPref("rememberSignons"); - // Get constructor for nsILoginInfo this._nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo); - - - // Form submit observer checks forms for new logins and pw changes. - this._observerService.addObserver(this._observer, "earlyformsubmit", false); - this._observerService.addObserver(this._observer, "xpcom-shutdown", false); - - // WebProgressListener for getting notification of new doc loads. - var progress = Cc["@mozilla.org/docloaderservice;1"]. - getService(Ci.nsIWebProgress); - progress.addProgressListener(this._webProgressListener, - Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); - - }, - /* * log * @@ -208,8 +178,8 @@ let WeaveLoginManager = { log : function (message) { if (!this._debug) return; - dump("Login Manager: " + message + "\n"); - this._logService.logStringMessage("Login Manager: " + message); + dump("Authenticator: " + message + "\n"); + this._logService.logStringMessage("Authenticator: " + message); }, @@ -226,26 +196,8 @@ let WeaveLoginManager = { _pwmgr : null, QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsIFormSubmitObserver, Ci.nsISupportsWeakReference]), - - // nsFormSubmitObserver - notify : function (formElement, aWindow, actionURI) { - this._pwmgr.log("observer notified for form submission."); - - // We're invoked before the content's |onsubmit| handlers, so we - // can grab form data before it might be modified (see bug 257781). - - try { - this._pwmgr._onFormSubmit(formElement); - } catch (e) { - this._pwmgr.log("Caught error in onFormSubmit: " + e); - } - - return true; // Always return true, or form submit will be canceled. - }, - // nsObserver observe : function (subject, topic, data) { @@ -262,220 +214,14 @@ let WeaveLoginManager = { } else { this._pwmgr.log("Oops! Pref not handled, change ignored."); } - } else if (topic == "xpcom-shutdown") { - for (let i in this._pwmgr) { - try { - this._pwmgr[i] = null; - } catch(ex) {} - } - this._pwmgr = null; } else { this._pwmgr.log("Oops! Unexpected notification: " + topic); } } }, - - /* - * _webProgressListener object - * - * Internal utility object, implements nsIWebProgressListener interface. - * This is attached to the document loader service, so we get - * notifications about all page loads. - */ - _webProgressListener : { - _pwmgr : null, - _domEventListener : null, - - QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference]), - - - onStateChange : function (aWebProgress, aRequest, - aStateFlags, aStatus) { - - // STATE_START is too early, doc is still the old page. - if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING)) - return; - - if (!this._pwmgr._remember) - return; - - var domWin = aWebProgress.DOMWindow; - var domDoc = domWin.document; - - // Only process things which might have HTML forms. - if (!(domDoc instanceof Ci.nsIDOMHTMLDocument)) - return; - - this._pwmgr.log("onStateChange accepted: req = " + - (aRequest ? aRequest.name : "(null)") + - ", flags = 0x" + aStateFlags.toString(16)); - - // Fastback doesn't fire DOMContentLoaded, so process forms now. - if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) { - this._pwmgr.log("onStateChange: restoring document"); - return this._pwmgr._fillDocument(domDoc); - } - - // Add event listener to process page when DOM is complete. - domDoc.addEventListener("DOMContentLoaded", - this._domEventListener, false); - return; - }, - - // stubs for the nsIWebProgressListener interfaces which we don't use. - onProgressChange : function() { throw "Unexpected onProgressChange"; }, - onLocationChange : function() { throw "Unexpected onLocationChange"; }, - onStatusChange : function() { throw "Unexpected onStatusChange"; }, - onSecurityChange : function() { throw "Unexpected onSecurityChange"; } - }, - - - /* - * _domEventListener object - * - * Internal utility object, implements nsIDOMEventListener - * Used to catch certain DOM events needed to properly implement form fill. - */ - _domEventListener : { - _pwmgr : null, - - QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener, - Ci.nsISupportsWeakReference]), - - - handleEvent : function (event) { - this._pwmgr.log("domEventListener: got event " + event.type); - - switch (event.type) { - case "DOMContentLoaded": - this._pwmgr._fillDocument(event.target); - return; - - case "DOMAutoComplete": - case "blur": - var acInputField = event.target; - var acForm = acInputField.form; - // Make sure the username field fillForm will use is the - // same field as the autocomplete was activated on. If - // not, the DOM has been altered and we'll just give up. - var [usernameField, passwordField, ignored] = - this._pwmgr._getFormFields(acForm, false); - if (usernameField == acInputField && passwordField) { - // Clobber any existing password. - passwordField.value = ""; - this._pwmgr._fillForm(acForm, true, true, null); - } else { - this._pwmgr.log("Oops, form changed before AC invoked"); - } - return; - - default: - this._pwmgr.log("Oops! This event unexpected."); - return; - } - } - }, - - - - /* ---------- Primary Public interfaces ---------- */ - - - - /* - * addLogin - * - * Add a new login to login storage. - */ - addLogin : function (login) { - // Sanity check the login - if (login.hostname == null || login.hostname.length == 0) - throw "Can't add a login with a null or empty hostname."; - - // For logins w/o a username, set to "", not null. - if (login.username == null) - throw "Can't add a login with a null username."; - - if (login.password == null || login.password.length == 0) - throw "Can't add a login with a null or empty password."; - - if (login.formSubmitURL || login.formSubmitURL == "") { - // We have a form submit URL. Can't have a HTTP realm. - if (login.httpRealm != null) - throw "Can't add a login with both a httpRealm and formSubmitURL."; - } else if (login.httpRealm) { - // We have a HTTP realm. Can't have a form submit URL. - if (login.formSubmitURL != null) - throw "Can't add a login with both a httpRealm and formSubmitURL."; - } else { - // Need one or the other! - throw "Can't add a login without a httpRealm or formSubmitURL."; - } - - - // Look for an existing entry. - var logins = this.findLogins({}, login.hostname, login.formSubmitURL, - login.httpRealm); - - if (logins.some(function(l) login.matches(l, true))) - throw "This login already exists."; - - this.log("Adding login: " + login); - return this._storage.addLogin(login); - }, - - - /* - * removeLogin - * - * Remove the specified login from the stored logins. - */ - removeLogin : function (login) { - this.log("Removing login: " + login); - return this._storage.removeLogin(login); - }, - - - /* - * modifyLogin - * - * Change the specified login to match the new login. - */ - modifyLogin : function (oldLogin, newLogin) { - this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin); - return this._storage.modifyLogin(oldLogin, newLogin); - }, - - - /* - * getAllLogins - * - * Get a dump of all stored logins. Used by the login manager UI. - * - * |count| is only needed for XPCOM. - * - * Returns an array of logins. If there are no logins, the array is empty. - */ - getAllLogins : function (count) { - this.log("Getting a list of all logins"); - return this._storage.getAllLogins(count); - }, - - - /* - * removeAllLogins - * - * Remove all stored logins. - */ - removeAllLogins : function () { - this.log("Removing all logins"); - this._storage.removeAllLogins(); - }, - /* * getAllDisabledHosts * @@ -505,22 +251,6 @@ let WeaveLoginManager = { httpRealm); }, - - /* - * searchLogins - * - * Public wrapper around _searchLogins to convert the nsIPropertyBag to a - * JavaScript object and decrypt the results. - * - * Returns an array of decrypted nsILoginInfo. - */ - searchLogins : function(count, matchData) { - this.log("Searching for logins"); - - return this._storage.searchLogins(count, matchData); - }, - - /* * countLogins * @@ -535,105 +265,6 @@ let WeaveLoginManager = { }, - /* - * getLoginSavingEnabled - * - * Check to see if user has disabled saving logins for the host. - */ - getLoginSavingEnabled : function (host) { - this.log("Checking if logins to " + host + " can be saved."); - if (!this._remember) - return false; - - return this._storage.getLoginSavingEnabled(host); - }, - - - /* - * setLoginSavingEnabled - * - * Enable or disable storing logins for the specified host. - */ - setLoginSavingEnabled : function (hostname, enabled) { - // Nulls won't round-trip with getAllDisabledHosts(). - if (hostname.indexOf("\0") != -1) - throw "Invalid hostname"; - - this.log("Saving logins for " + hostname + " enabled? " + enabled); - return this._storage.setLoginSavingEnabled(hostname, enabled); - }, - - - /* - * autoCompleteSearch - * - * Yuck. This is called directly by satchel: - * nsFormFillController::StartSearch() - * [toolkit/components/satchel/src/nsFormFillController.cpp] - * - * We really ought to have a simple way for code to register an - * auto-complete provider, and not have satchel calling pwmgr directly. - */ - autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) { - // aPreviousResult & aResult are nsIAutoCompleteResult, - // aElement is nsIDOMHTMLInputElement - - if (!this._remember) - return false; - - this.log("AutoCompleteSearch invoked. Search is: " + aSearchString); - - var result = null; - - if (aPreviousResult) { - this.log("Using previous autocomplete result"); - result = aPreviousResult; - - // We have a list of results for a shorter search string, so just - // filter them further based on the new search string. - // Count backwards, because result.matchCount is decremented - // when we remove an entry. - for (var i = result.matchCount - 1; i >= 0; i--) { - var match = result.getValueAt(i); - - // Remove results that are too short, or have different prefix. - if (aSearchString.length > match.length || - aSearchString.toLowerCase() != - match.substr(0, aSearchString.length).toLowerCase()) - { - this.log("Removing autocomplete entry '" + match + "'"); - result.removeValueAt(i, false); - } - } - } else { - this.log("Creating new autocomplete search result."); - - var doc = aElement.ownerDocument; - var origin = this._getPasswordOrigin(doc.documentURI); - var actionOrigin = this._getActionOrigin(aElement.form); - - var logins = this.findLogins({}, origin, actionOrigin, null); - var matchingLogins = []; - - for (i = 0; i < logins.length; i++) { - var username = logins[i].username.toLowerCase(); - if (aSearchString.length <= username.length && - aSearchString.toLowerCase() == - username.substr(0, aSearchString.length)) - { - matchingLogins.push(logins[i]); - } - } - this.log(matchingLogins.length + " autocomplete logins avail."); - result = new UserAutoCompleteResult(aSearchString, matchingLogins); - } - - return result; - }, - - - - /* ------- Internal methods / callbacks for document integration ------- */ @@ -784,155 +415,6 @@ let WeaveLoginManager = { return false; }, - /* - * _onFormSubmit - * - * Called by the our observer when notified of a form submission. - * [Note that this happens before any DOM onsubmit handlers are invoked.] - * Looks for a password change in the submitted form, so we can update - * our stored password. - */ - _onFormSubmit : function (form) { - - // local helper function - function getPrompter(aWindow) { - var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"]. - createInstance(Ci.nsILoginManagerPrompter); - prompterSvc.init(aWindow); - return prompterSvc; - } - - if (this._inPrivateBrowsing) { - // We won't do anything in private browsing mode anyway, - // so there's no need to perform further checks. - this.log("(form submission ignored in private browsing mode)"); - return; - } - - var doc = form.ownerDocument; - var win = doc.defaultView; - - // If password saving is disabled (globally or for host), bail out now. - if (!this._remember) - return; - - var hostname = this._getPasswordOrigin(doc.documentURI); - var formSubmitURL = this._getActionOrigin(form) - if (!this.getLoginSavingEnabled(hostname)) { - this.log("(form submission ignored -- saving is " + - "disabled for: " + hostname + ")"); - return; - } - - - // Get the appropriate fields from the form. - var [usernameField, newPasswordField, oldPasswordField] = - this._getFormFields(form, true); - - // Need at least 1 valid password field to do anything. - if (newPasswordField == null) - return; - - // Check for autocomplete=off attribute. We don't use it to prevent - // autofilling (for existing logins), but won't save logins when it's - // present. - if (this._isAutocompleteDisabled(form) || - this._isAutocompleteDisabled(usernameField) || - this._isAutocompleteDisabled(newPasswordField) || - this._isAutocompleteDisabled(oldPasswordField)) { - this.log("(form submission ignored -- autocomplete=off found)"); - return; - } - - - var formLogin = new this._nsLoginInfo(); - formLogin.init(hostname, formSubmitURL, null, - (usernameField ? usernameField.value : ""), - newPasswordField.value, - (usernameField ? usernameField.name : ""), - newPasswordField.name); - - // If we didn't find a username field, but seem to be changing a - // password, allow the user to select from a list of applicable - // logins to update the password for. - if (!usernameField && oldPasswordField) { - - var logins = this.findLogins({}, hostname, formSubmitURL, null); - - if (logins.length == 0) { - // Could prompt to save this as a new password-only login. - // This seems uncommon, and might be wrong, so ignore. - this.log("(no logins for this host -- pwchange ignored)"); - return; - } - - var prompter = getPrompter(win); - - if (logins.length == 1) { - var oldLogin = logins[0]; - formLogin.username = oldLogin.username; - formLogin.usernameField = oldLogin.usernameField; - - prompter.promptToChangePassword(oldLogin, formLogin); - } else { - prompter.promptToChangePasswordWithUsernames( - logins, logins.length, formLogin); - } - - return; - } - - - // Look for an existing login that matches the form login. - var existingLogin = null; - var logins = this.findLogins({}, hostname, formSubmitURL, null); - - for (var i = 0; i < logins.length; i++) { - var same, login = logins[i]; - - // If one login has a username but the other doesn't, ignore - // the username when comparing and only match if they have the - // same password. Otherwise, compare the logins and match even - // if the passwords differ. - if (!login.username && formLogin.username) { - var restoreMe = formLogin.username; - formLogin.username = ""; - same = formLogin.matches(login, false); - formLogin.username = restoreMe; - } else if (!formLogin.username && login.username) { - formLogin.username = login.username; - same = formLogin.matches(login, false); - formLogin.username = ""; // we know it's always blank. - } else { - same = formLogin.matches(login, true); - } - - if (same) { - existingLogin = login; - break; - } - } - - if (existingLogin) { - this.log("Found an existing login matching this form submission"); - - // Change password if needed. - if (existingLogin.password != formLogin.password) { - this.log("...passwords differ, prompting to change."); - prompter = getPrompter(win); - prompter.promptToChangePassword(existingLogin, formLogin); - } - - return; - } - - - // Prompt user to save login (via dialog or notification bar) - prompter = getPrompter(win); - prompter.promptToSavePassword(formLogin); - }, - - /* * _getPasswordOrigin * @@ -1178,118 +660,8 @@ let WeaveLoginManager = { passwordField: passwordField, foundLogins: foundLogins, selectedLogin: selectedLogin }; - }, - - - /* - * fillForm - * - * Fill the form with login information if we can find it. - */ - fillForm : function (form) { - this.log("fillForm processing form[id=" + form.id + "]"); - return this._fillForm(form, true, true, null)[0]; - }, - - - /* - * _attachToInput - * - * Hooks up autocomplete support to a username field, to allow - * a user editing the field to select an existing login and have - * the password field filled in. - */ - _attachToInput : function (element) { - this.log("attaching autocomplete stuff"); - element.addEventListener("blur", - this._domEventListener, false); - element.addEventListener("DOMAutoComplete", - this._domEventListener, false); - this._formFillService.markAsLoginManagerField(element); } + }; // end of LoginManager implementation - - - -// nsIAutoCompleteResult implementation -function UserAutoCompleteResult (aSearchString, matchingLogins) { - function loginSort(a,b) { - var userA = a.username.toLowerCase(); - var userB = b.username.toLowerCase(); - - if (userA < userB) - return -1; - - if (userB > userA) - return 1; - - return 0; - }; - - this.searchString = aSearchString; - this.logins = matchingLogins.sort(loginSort); - this.matchCount = matchingLogins.length; - - if (this.matchCount > 0) { - this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; - this.defaultIndex = 0; - } -} - -UserAutoCompleteResult.prototype = { - QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, - Ci.nsISupportsWeakReference]), - - // private - logins : null, - - // Interfaces from idl... - searchString : null, - searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH, - defaultIndex : -1, - errorDescription : "", - matchCount : 0, - - getValueAt : function (index) { - if (index < 0 || index >= this.logins.length) - throw "Index out of range."; - - return this.logins[index].username; - }, - - getCommentAt : function (index) { - return ""; - }, - - getStyleAt : function (index) { - return ""; - }, - - getImageAt : function (index) { - return ""; - }, - - removeValueAt : function (index, removeFromDB) { - if (index < 0 || index >= this.logins.length) - throw "Index out of range."; - - var [removedLogin] = this.logins.splice(index, 1); - - this.matchCount--; - if (this.defaultIndex > this.logins.length) - this.defaultIndex--; - - if (removeFromDB) { - var pwmgr = Cc["@mozilla.org/login-manager;1"]. - getService(Ci.nsILoginManager); - pwmgr.removeLogin(removedLogin); - } - } -}; - -//var component = [LoginManager]; -//function NSGetModule (compMgr, fileSpec) { -// return XPCOMUtils.generateModule(component); -//} -WeaveLoginManager.init(); +Authenticator.init(); From 6f693e3d5ef1963ddbee940e7c2edbdbca2e9980 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 11 May 2009 01:40:04 -0700 Subject: [PATCH 1088/1860] remove more unused code, make Authenticator use built-in nsILoginManager for stuff nsILoginManager makes public --- services/sync/modules/authenticator.js | 80 ++------------------------ 1 file changed, 6 insertions(+), 74 deletions(-) diff --git a/services/sync/modules/authenticator.js b/services/sync/modules/authenticator.js index 3c6492423d42..9e2edf897eb8 100644 --- a/services/sync/modules/authenticator.js +++ b/services/sync/modules/authenticator.js @@ -80,37 +80,12 @@ let Authenticator = { return this.__observerService; }, - - __storage : null, // Storage component which contains the saved logins - get _storage() { - if (!this.__storage) { - - var contractID = "@mozilla.org/login-manager/storage/mozStorage;1"; - try { - var catMan = Cc["@mozilla.org/categorymanager;1"]. - getService(Ci.nsICategoryManager); - contractID = catMan.getCategoryEntry("login-manager-storage", - "nsILoginManagerStorage"); - this.log("Found alternate nsILoginManagerStorage with " + - "contract ID: " + contractID); - } catch (e) { - this.log("No alternate nsILoginManagerStorage registered"); - } - - this.__storage = Cc[contractID]. - createInstance(Ci.nsILoginManagerStorage); - try { - this.__storage.init(); - } catch (e) { - this.log("Initialization of storage component failed: " + e); - this.__storage = null; - } - } - - return this.__storage; + get loginManager() { + delete this.loginManager; + return this.loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); }, - // Private Browsing Service // If the service is not available, null will be returned. __privateBrowsingService : undefined, @@ -222,49 +197,6 @@ let Authenticator = { /* ---------- Primary Public interfaces ---------- */ - /* - * getAllDisabledHosts - * - * Get a list of all hosts for which logins are disabled. - * - * |count| is only needed for XPCOM. - * - * Returns an array of disabled logins. If there are no disabled logins, - * the array is empty. - */ - getAllDisabledHosts : function (count) { - this.log("Getting a list of all disabled hosts"); - return this._storage.getAllDisabledHosts(count); - }, - - - /* - * findLogins - * - * Search for the known logins for entries matching the specified criteria. - */ - findLogins : function (count, hostname, formSubmitURL, httpRealm) { - this.log("Searching for logins matching host: " + hostname + - ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); - - return this._storage.findLogins(count, hostname, formSubmitURL, - httpRealm); - }, - - /* - * countLogins - * - * Search for the known logins for entries matching the specified criteria, - * returns only the count. - */ - countLogins : function (hostname, formSubmitURL, httpRealm) { - this.log("Counting logins matching host: " + hostname + - ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); - - return this._storage.countLogins(hostname, formSubmitURL, httpRealm); - }, - - /* ------- Internal methods / callbacks for document integration ------- */ @@ -474,7 +406,7 @@ let Authenticator = { var formOrigin = this._getPasswordOrigin(doc.documentURI); // If there are no logins for this site, bail out now. - if (!this.countLogins(formOrigin, "", null)) + if (!this.loginManager.countLogins(formOrigin, "", null)) return []; this.log("fillDocument processing " + forms.length + @@ -543,7 +475,7 @@ let Authenticator = { var formOrigin = this._getPasswordOrigin(form.ownerDocument.documentURI); var actionOrigin = this._getActionOrigin(form); - foundLogins = this.findLogins({}, formOrigin, actionOrigin, null); + foundLogins = this.loginManager.findLogins({}, formOrigin, actionOrigin, null); this.log("found " + foundLogins.length + " matching logins."); } else { this.log("reusing logins from last form."); From 059b8ab0141294dcdc2900d4f78c5f6aadf12a0f Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 11 May 2009 01:42:47 -0700 Subject: [PATCH 1089/1860] remove more unused code in the Authenticator module --- services/sync/modules/authenticator.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/services/sync/modules/authenticator.js b/services/sync/modules/authenticator.js index 9e2edf897eb8..3258a0d813d4 100644 --- a/services/sync/modules/authenticator.js +++ b/services/sync/modules/authenticator.js @@ -111,9 +111,7 @@ let Authenticator = { }, _prefBranch : null, // Preferences service - _nsLoginInfo : null, // Constructor for nsILoginInfo implementation - _remember : true, // mirrors signon.rememberSignons preference _debug : false, // mirrors signon.debug @@ -138,11 +136,6 @@ let Authenticator = { // Get current preference values. this._debug = this._prefBranch.getBoolPref("debug"); - this._remember = this._prefBranch.getBoolPref("rememberSignons"); - - // Get constructor for nsILoginInfo - this._nsLoginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo); }, /* @@ -183,9 +176,6 @@ let Authenticator = { if (prefName == "debug") { this._pwmgr._debug = this._pwmgr._prefBranch.getBoolPref("debug"); - } else if (prefName == "rememberSignons") { - this._pwmgr._remember = - this._pwmgr._prefBranch.getBoolPref("rememberSignons"); } else { this._pwmgr.log("Oops! Pref not handled, change ignored."); } @@ -594,6 +584,6 @@ let Authenticator = { selectedLogin: selectedLogin }; } -}; // end of LoginManager implementation +}; Authenticator.init(); From 6892e074c1e84fe88e2e9e0a7fbb83d7a8762c20 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 11 May 2009 16:11:04 -0700 Subject: [PATCH 1090/1860] bug 492435: work around crasher bug 492442 by not calling isVisited for a history entry without a URL; r=thunder --- services/sync/modules/engines/history.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index db3a212bc75d..964c58f203b8 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -414,6 +414,10 @@ HistoryStore.prototype = { this._log.trace(" -> processing history entry: " + record.histUri); let uri = Utils.makeURI(record.histUri); + if (!uri) { + this._log.warn("Attempted to process invalid URI, skipping"); + throw "invalid URI in record"; + } let curvisits = []; if (this.urlExists(uri)) curvisits = this._getVisits(record.histUri); @@ -444,7 +448,8 @@ HistoryStore.prototype = { urlExists: function HistStore_urlExists(url) { if (typeof(url) == "string") url = Utils.makeURI(url); - return this._hsvc.isVisited(url); + // Don't call isVisited on a null URL to work around crasher bug 492442. + return url ? this._hsvc.isVisited(url) : false; }, createRecord: function HistStore_createRecord(guid, cryptoMetaURL) { From 013b1ca6e90cf99c47f88fa0c6aa6d1d52538cb9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 13 May 2009 16:31:42 -0500 Subject: [PATCH 1091/1860] Remove old Firefox 3 places DB code. Min version is 3.1/5, so we grab DBConnection from places directly now. --- services/sync/modules/engines/history.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 964c58f203b8..8be855f66fa0 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -136,18 +136,6 @@ HistoryStore.prototype = { return anno; }, - get _histDB_30() { - let file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("places.sqlite"); - let stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - let db = stor.openDatabase(file); - this.__defineGetter__("_histDB_30", function() db); - return db; - }, - get _db() { return this._hsvc.DBConnection; }, From 30b165b1398982b09875ec4d76aba9c033a9f97e Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Thu, 14 May 2009 00:50:05 -0700 Subject: [PATCH 1092/1860] updated authenticator icons from Sean Martell --- services/sync/services-sync.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index d6759f80dccc..5d26414b51c7 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -30,6 +30,7 @@ pref("extensions.weave.log.appender.debugLog", "Trace"); pref("extensions.weave.log.rootLogger", "Trace"); pref("extensions.weave.log.logger.service.main", "Trace"); pref("extensions.weave.log.logger.async", "Debug"); +pref("extensions.weave.log.logger.authenticator", "Debug"); pref("extensions.weave.log.logger.network.resources", "Debug"); pref("extensions.weave.log.logger.engine.bookmarks", "Debug"); pref("extensions.weave.log.logger.engine.forms", "Debug"); From 9d8b55251b15efbef72715bed445c0ae59503136 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 14 May 2009 11:09:08 -0500 Subject: [PATCH 1093/1860] Fix form engine displayname to displayName --- services/sync/modules/engines/forms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 3bc73c73d547..5b8d79ddfbac 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -58,7 +58,7 @@ function FormEngine() { FormEngine.prototype = { __proto__: SyncEngine.prototype, name: "forms", - displayname: "Forms", + displayName: "Forms", logName: "Forms", _storeObj: FormStore, _trackerObj: FormTracker, From 3b3efaf691d92a6cbe6fe658d68774f15c66a467 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 14 May 2009 11:38:15 -0500 Subject: [PATCH 1094/1860] Bug 493001 - Dynamically generate weave prefs engine list based on registered engines Remove existing hardcoded engine list + prefs and build checkboxes based on registered engines. Engines that give null for enabled (instead of true/false) are considered disabled. Update base Engine enabled get/set to give null and allow setting. --- services/sync/modules/engines.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index aa4a9811012b..f5925a6ac98c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -156,7 +156,9 @@ Engine.prototype = { _storeObj: Store, _trackerObj: Tracker, - get enabled() Utils.prefs.getBoolPref("engine." + this.name), + get enabled() Svc.Prefs.get("engine." + this.name, null), + set enabled(val) Svc.Prefs.set("engine." + this.name, !!val), + get score() this._tracker.score, get _store() { From d1e58d45de36f24bbdc9465073ece49167f5d0cd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 14 May 2009 11:43:56 -0500 Subject: [PATCH 1095/1860] Bug 493002 - List partially implemented engines (cookies, input history) as disabled Import the outdated engines, but delete their pref so they show up as disabled. Clean up the old engines a little bit so they don't allocate unnecessarily and to not use syncCore. --- services/sync/modules/engines/cookies.js | 38 +++++------------------- services/sync/modules/engines/input.js | 35 +++++----------------- services/sync/modules/service.js | 5 +++- services/sync/services-sync.js | 8 ++--- 4 files changed, 23 insertions(+), 63 deletions(-) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 78c53ed3e6eb..657560c1fab6 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -43,7 +43,6 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); @@ -53,7 +52,7 @@ function CookieEngine(pbeId) { this._init(pbeId); } CookieEngine.prototype = { - __proto__: new SyncEngine(), + __proto__: SyncEngine.prototype, get name() { return "cookies"; }, get displayName() { return "Cookies"; }, @@ -67,13 +66,6 @@ CookieEngine.prototype = { return this.__store; }, - __core: null, - get _core() { - if (!this.__core) - this.__core = new CookieSyncCore(this._store); - return this.__core; - }, - __tracker: null, get _tracker() { if (!this.__tracker) @@ -82,27 +74,9 @@ CookieEngine.prototype = { } }; -function CookieSyncCore(store) { - this._store = store; - this._init(); -} -CookieSyncCore.prototype = { - _logName: "CookieSync", - _store: null, - - _commandLike: function CSC_commandLike(a, b) { - /* Method required to be overridden. - a and b each have a .data and a .GUID - If this function returns true, an editCommand will be - generated to try to resolve the thing. - but are a and b objects of the type in the Store or - are they "commands"?? */ - return false; - } -}; -CookieSyncCore.prototype.__proto__ = new SyncCore(); - function CookieStore( cookieManagerStub ) { + // XXX disabled for now.. + return; /* If no argument is passed in, this store will query/write to the real Mozilla cookie manager component. This is the normal way to use this class in production code. But for unit-testing purposes, you can pass @@ -111,6 +85,7 @@ function CookieStore( cookieManagerStub ) { this._cookieManagerStub = cookieManagerStub; } CookieStore.prototype = { + __proto__: Store.prototype, _logName: "CookieStore", _lookup: null, @@ -297,12 +272,14 @@ CookieStore.prototype = { nothing to do here. */ } }; -CookieStore.prototype.__proto__ = new Store(); function CookieTracker() { + // XXX disabled for now.. + return; this._init(); } CookieTracker.prototype = { + __proto__: Tracker.prototype, _logName: "CookieTracker", _init: function CT__init() { @@ -332,4 +309,3 @@ CookieTracker.prototype = { } } } -CookieTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/engines/input.js b/services/sync/modules/engines/input.js index 14d981445fe0..0d3282919445 100644 --- a/services/sync/modules/engines/input.js +++ b/services/sync/modules/engines/input.js @@ -44,7 +44,6 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/async.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/syncCores.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); @@ -54,10 +53,10 @@ function InputEngine(pbeId) { this._init(pbeId); } InputEngine.prototype = { - __proto__: new SyncEngine(), + __proto__: SyncEngine.prototype, get name() { return "input"; }, - get displayName() { return "Input History"; }, + get displayName() { return "Input History (Location Bar)"; }, get logName() { return "InputEngine"; }, get serverPrefix() { return "user-data/input/"; }, @@ -68,13 +67,6 @@ InputEngine.prototype = { return this.__store; }, - __core: null, - get _core() { - if (!this.__core) - this.__core = new InputSyncCore(this._store); - return this.__core; - }, - __tracker: null, get _tracker() { if (!this.__tracker) @@ -83,25 +75,13 @@ InputEngine.prototype = { } }; -function InputSyncCore(store) { - this._store = store; - this._init(); -} -InputSyncCore.prototype = { - _logName: "InputSync", - _store: null, - - _commandLike: function FSC_commandLike(a, b) { - /* Not required as GUIDs for similar data sets will be the same */ - return false; - } -}; -InputSyncCore.prototype.__proto__ = new SyncCore(); - function InputStore() { + // XXX disabled for now.. + return; this._init(); } InputStore.prototype = { + __proto__: Store.prototype, _logName: "InputStore", _lookup: null, @@ -231,12 +211,14 @@ InputStore.prototype = { // Not needed. } }; -InputStore.prototype.__proto__ = new Store(); function InputTracker() { + // XXX disabled for now.. + return; this._init(); } InputTracker.prototype = { + __proto__: Tracker.prototype, _logName: "InputTracker", __placeDB: null, @@ -292,4 +274,3 @@ InputTracker.prototype = { stmnt.reset(); } }; -InputTracker.prototype.__proto__ = new Tracker(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index fd94f6442f43..8e766fafc0bf 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -98,8 +98,10 @@ Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/engines/bookmarks.js", Weave); Cu.import("resource://weave/engines/clientData.js", Weave); +Cu.import("resource://weave/engines/cookies.js", Weave); Cu.import("resource://weave/engines/forms.js", Weave); Cu.import("resource://weave/engines/history.js", Weave); +Cu.import("resource://weave/engines/input.js", Weave); Cu.import("resource://weave/engines/passwords.js", Weave); Cu.import("resource://weave/engines/tabs.js", Weave); @@ -389,7 +391,8 @@ WeaveSvc.prototype = { break; case "Firefox": - engines = ["Bookmarks", "Form", "History", "Password", "Tab"]; + engines = ["Bookmarks", "Cookie", "Form", "History", "Input", + "Password", "Tab"]; break; case "SeaMonkey": diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 5d26414b51c7..72dbe1932e08 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -15,12 +15,12 @@ pref("extensions.weave.schedule", 1); pref("extensions.weave.syncOnQuit.enabled", true); pref("extensions.weave.engine.bookmarks", true); -pref("extensions.weave.engine.history", true); -pref("extensions.weave.engine.cookies", false); -pref("extensions.weave.engine.passwords", true); +//pref("extensions.weave.engine.cookies", false); pref("extensions.weave.engine.forms", false); +pref("extensions.weave.engine.history", true); +//pref("extensions.weave.engine.input", false); +pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.tabs", true); -pref("extensions.weave.engine.input", false); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 6106d364c721241e09c032ec3c97119e52926b57 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 14 May 2009 11:50:59 -0500 Subject: [PATCH 1096/1860] Bug 493004 - Create stubs of unimplemented engines to list them as disabled (extension, microformat, plugin, theme) Add the 4 engines with just the base code to set their name and register them for Firefox. --- services/sync/modules/engines/extensions.js | 49 +++++++++++++++++++ services/sync/modules/engines/microformats.js | 49 +++++++++++++++++++ services/sync/modules/engines/plugins.js | 49 +++++++++++++++++++ services/sync/modules/engines/themes.js | 49 +++++++++++++++++++ services/sync/modules/service.js | 8 ++- 5 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 services/sync/modules/engines/extensions.js create mode 100644 services/sync/modules/engines/microformats.js create mode 100644 services/sync/modules/engines/plugins.js create mode 100644 services/sync/modules/engines/themes.js diff --git a/services/sync/modules/engines/extensions.js b/services/sync/modules/engines/extensions.js new file mode 100644 index 000000000000..123bc0587ddc --- /dev/null +++ b/services/sync/modules/engines/extensions.js @@ -0,0 +1,49 @@ +/***************************** BEGIN LICENSE BLOCK ***************************** +* Version: MPL 1.1/GPL 2.0/LGPL 2.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with the +* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +* the specific language governing rights and limitations under the License. +* +* The Original Code is Weave Extension Engine. +* +* The Initial Developer of the Original Code is Mozilla Corporation. +* Portions created by the Initial Developer are Copyright (C) 2009 the Initial +* Developer. All Rights Reserved. +* +* Contributor(s): +* Edward Lee (original author) +* +* Alternatively, the contents of this file may be used under the terms of either +* the GNU General Public License Version 2 or later (the "GPL"), or the GNU +* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case +* the provisions of the GPL or the LGPL are applicable instead of those above. +* If you wish to allow use of your version of this file only under the terms of +* either the GPL or the LGPL, and not to allow others to use your version of +* this file under the terms of the MPL, indicate your decision by deleting the +* provisions above and replace them with the notice and other provisions +* required by the GPL or the LGPL. If you do not delete the provisions above, a +* recipient may use your version of this file under the terms of any one of the +* MPL, the GPL or the LGPL. +* +****************************** END LICENSE BLOCK ******************************/ + +const EXPORTED_SYMBOLS = ["ExtensionEngine"]; + +const Cu = Components.utils; +Cu.import("resource://weave/engines.js"); + +function ExtensionEngine() { + this._init(); +} +ExtensionEngine.prototype = { + __proto__: SyncEngine.prototype, + + displayName: "Extensions", + logName: "Extensions", + name: "extensions", +}; diff --git a/services/sync/modules/engines/microformats.js b/services/sync/modules/engines/microformats.js new file mode 100644 index 000000000000..7da3df431e27 --- /dev/null +++ b/services/sync/modules/engines/microformats.js @@ -0,0 +1,49 @@ +/***************************** BEGIN LICENSE BLOCK ***************************** +* Version: MPL 1.1/GPL 2.0/LGPL 2.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with the +* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +* the specific language governing rights and limitations under the License. +* +* The Original Code is Weave MicroFormat Engine. +* +* The Initial Developer of the Original Code is Mozilla Corporation. +* Portions created by the Initial Developer are Copyright (C) 2009 the Initial +* Developer. All Rights Reserved. +* +* Contributor(s): +* Edward Lee (original author) +* +* Alternatively, the contents of this file may be used under the terms of either +* the GNU General Public License Version 2 or later (the "GPL"), or the GNU +* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case +* the provisions of the GPL or the LGPL are applicable instead of those above. +* If you wish to allow use of your version of this file only under the terms of +* either the GPL or the LGPL, and not to allow others to use your version of +* this file under the terms of the MPL, indicate your decision by deleting the +* provisions above and replace them with the notice and other provisions +* required by the GPL or the LGPL. If you do not delete the provisions above, a +* recipient may use your version of this file under the terms of any one of the +* MPL, the GPL or the LGPL. +* +****************************** END LICENSE BLOCK ******************************/ + +const EXPORTED_SYMBOLS = ["MicroFormatEngine"]; + +const Cu = Components.utils; +Cu.import("resource://weave/engines.js"); + +function MicroFormatEngine() { + this._init(); +} +MicroFormatEngine.prototype = { + __proto__: SyncEngine.prototype, + + displayName: "MicroFormats", + logName: "MicroFormats", + name: "microformats", +}; diff --git a/services/sync/modules/engines/plugins.js b/services/sync/modules/engines/plugins.js new file mode 100644 index 000000000000..dee546bf34c7 --- /dev/null +++ b/services/sync/modules/engines/plugins.js @@ -0,0 +1,49 @@ +/***************************** BEGIN LICENSE BLOCK ***************************** +* Version: MPL 1.1/GPL 2.0/LGPL 2.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with the +* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +* the specific language governing rights and limitations under the License. +* +* The Original Code is Weave Plugin Engine. +* +* The Initial Developer of the Original Code is Mozilla Corporation. +* Portions created by the Initial Developer are Copyright (C) 2009 the Initial +* Developer. All Rights Reserved. +* +* Contributor(s): +* Edward Lee (original author) +* +* Alternatively, the contents of this file may be used under the terms of either +* the GNU General Public License Version 2 or later (the "GPL"), or the GNU +* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case +* the provisions of the GPL or the LGPL are applicable instead of those above. +* If you wish to allow use of your version of this file only under the terms of +* either the GPL or the LGPL, and not to allow others to use your version of +* this file under the terms of the MPL, indicate your decision by deleting the +* provisions above and replace them with the notice and other provisions +* required by the GPL or the LGPL. If you do not delete the provisions above, a +* recipient may use your version of this file under the terms of any one of the +* MPL, the GPL or the LGPL. +* +****************************** END LICENSE BLOCK ******************************/ + +const EXPORTED_SYMBOLS = ["PluginEngine"]; + +const Cu = Components.utils; +Cu.import("resource://weave/engines.js"); + +function PluginEngine() { + this._init(); +} +PluginEngine.prototype = { + __proto__: SyncEngine.prototype, + + displayName: "Plugins", + logName: "Plugins", + name: "plugins", +}; diff --git a/services/sync/modules/engines/themes.js b/services/sync/modules/engines/themes.js new file mode 100644 index 000000000000..f87ed070bc68 --- /dev/null +++ b/services/sync/modules/engines/themes.js @@ -0,0 +1,49 @@ +/***************************** BEGIN LICENSE BLOCK ***************************** +* Version: MPL 1.1/GPL 2.0/LGPL 2.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with the +* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +* the specific language governing rights and limitations under the License. +* +* The Original Code is Weave Theme Engine. +* +* The Initial Developer of the Original Code is Mozilla Corporation. +* Portions created by the Initial Developer are Copyright (C) 2009 the Initial +* Developer. All Rights Reserved. +* +* Contributor(s): +* Edward Lee (original author) +* +* Alternatively, the contents of this file may be used under the terms of either +* the GNU General Public License Version 2 or later (the "GPL"), or the GNU +* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case +* the provisions of the GPL or the LGPL are applicable instead of those above. +* If you wish to allow use of your version of this file only under the terms of +* either the GPL or the LGPL, and not to allow others to use your version of +* this file under the terms of the MPL, indicate your decision by deleting the +* provisions above and replace them with the notice and other provisions +* required by the GPL or the LGPL. If you do not delete the provisions above, a +* recipient may use your version of this file under the terms of any one of the +* MPL, the GPL or the LGPL. +* +****************************** END LICENSE BLOCK ******************************/ + +const EXPORTED_SYMBOLS = ["ThemeEngine"]; + +const Cu = Components.utils; +Cu.import("resource://weave/engines.js"); + +function ThemeEngine() { + this._init(); +} +ThemeEngine.prototype = { + __proto__: SyncEngine.prototype, + + displayName: "Themes", + logName: "Themes", + name: "themes", +}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8e766fafc0bf..335674909a18 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -99,11 +99,15 @@ Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/engines/bookmarks.js", Weave); Cu.import("resource://weave/engines/clientData.js", Weave); Cu.import("resource://weave/engines/cookies.js", Weave); +Cu.import("resource://weave/engines/extensions.js", Weave); Cu.import("resource://weave/engines/forms.js", Weave); Cu.import("resource://weave/engines/history.js", Weave); Cu.import("resource://weave/engines/input.js", Weave); +Cu.import("resource://weave/engines/microformats.js", Weave); Cu.import("resource://weave/engines/passwords.js", Weave); +Cu.import("resource://weave/engines/plugins.js", Weave); Cu.import("resource://weave/engines/tabs.js", Weave); +Cu.import("resource://weave/engines/themes.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); @@ -391,8 +395,8 @@ WeaveSvc.prototype = { break; case "Firefox": - engines = ["Bookmarks", "Cookie", "Form", "History", "Input", - "Password", "Tab"]; + engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", "Input", + "MicroFormat", "Password", "Plugin", "Tab", "Theme"]; break; case "SeaMonkey": From 30981b7672c41230f99dcd2b063ce53a518d776a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 15 May 2009 13:18:16 -0500 Subject: [PATCH 1097/1860] Bug 493256 - Reconcile doesn't handle removed items correctly. r=thunder Have engines check if the deleted flag is the same for _isEqual. --- services/sync/modules/engines.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f5925a6ac98c..9d39053c9e0a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -383,6 +383,7 @@ SyncEngine.prototype = { this._log.trace("Local record: \n" + local); if (item.parentid == local.parentid && item.sortindex == local.sortindex && + item.deleted == local.deleted && Utils.deepEquals(item.cleartext, local.cleartext)) { this._log.trace("Local record is the same"); return true; From 454c4df934d60ace338f49e68d5c57c353c8b3f7 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 18 May 2009 11:11:07 -0700 Subject: [PATCH 1098/1860] Bug 493442: use app IDs instead of names to determine what engines to load --- services/sync/modules/constants.js | 8 +++++++- services/sync/modules/service.js | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 82f994c7f941..6500a520f8f3 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -51,7 +51,8 @@ const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'MIN_SERVER_STORAGE_VERSION', 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', - 'kSyncNotScheduled']; + 'kSyncNotScheduled', + 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID']; const WEAVE_VERSION = "@weave_version@"; @@ -113,3 +114,8 @@ const kSyncNotScheduled = "Not scheduled to do sync"; // Ways that a sync can be aborted: const ABORT_SYNC_COMMAND = "aborting sync, process commands said so"; +// Application IDs +const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; +const THUNDERBIRD_ID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; +const FENNEC_ID = "{a23983c0-fd0e-11dc-95ff-0800200c9a66}"; +const SEAMONKEY_ID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}"; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 335674909a18..44109bd552ee 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -389,21 +389,21 @@ WeaveSvc.prototype = { */ _registerEngines: function WeaveSvc__registerEngines() { let engines = []; - switch (Svc.AppInfo.name) { - case "Fennec": + switch (Svc.AppInfo.ID) { + case FENNEC_ID: engines = ["Bookmarks", "History", "Password", "Tab"]; break; - case "Firefox": + case FIREFOX_ID: engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", "Input", "MicroFormat", "Password", "Plugin", "Tab", "Theme"]; break; - case "SeaMonkey": + case SEAMONKEY_ID: engines = ["Form", "History", "Password"]; break; - case "Thunderbird": + case THUNDERBIRD_ID: engines = ["Cookie", "Password"]; break; } From 05ec0a8109966bdf67ba16f68b6f0617646adb2f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 19 May 2009 10:53:30 -0500 Subject: [PATCH 1099/1860] Bug 493363 - Several issues in Utils.deepEquals() Use triple-equals to determine if two things are the same; otherwise, check if both are objects with the same keys and same values for the keys. --- services/sync/modules/util.js | 43 ++++++++--------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index b0d6270b30ef..199c6a0726b9 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -224,42 +224,19 @@ let Utils = { dest.__defineGetter__(prop, getter); }, - deepEquals: function Weave_deepEquals(a, b) { - if (!a && !b) + deepEquals: function eq(a, b) { + // If they're triple equals, then it must be equals! + if (a === b) return true; - if (!a || !b) + + // Grab the keys from both sides + let [A, B] = [[i for (i in a)], [i for (i in b)]]; + // Don't bother doing any more if they aren't objects with same # keys + if (typeof a != "object" || typeof b != "object" || A.length != B.length) return false; - // if neither is an object, just use == - if (typeof(a) != "object" && typeof(b) != "object") - return a == b; - - // check if only one of them is an object - if (typeof(a) != "object" || typeof(b) != "object") - return false; - - let isArray = [Utils.isArray(a), Utils.isArray(b)]; - if (isArray[0] && isArray[1]) { - if (a.length != b.length) - return false; - - for (let i = 0; i < a.length; i++) { - if (!Utils.deepEquals(a[i], b[i])) - return false; - } - - } else { - // check if only one of them is an array - if (isArray[0] || isArray[1]) - return false; - - for (let key in a) { - if (!Utils.deepEquals(a[key], b[key])) - return false; - } - } - - return true; + // Check if both sides have the same keys and same value for the key + return A.every(function(A) B.some(function(B) A == B && eq(a[A], b[B]))); }, deepCopy: function Weave_deepCopy(thing, noSort) { From e305309f812eeec6ece7eacbfe6ea0c600b7f4fe Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 19 May 2009 14:24:26 -0500 Subject: [PATCH 1100/1860] Add test for Utils.deepEquals --- .../sync/tests/unit/test_utils_deepEquals.js | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_deepEquals.js diff --git a/services/sync/tests/unit/test_utils_deepEquals.js b/services/sync/tests/unit/test_utils_deepEquals.js new file mode 100644 index 000000000000..e3c84986f390 --- /dev/null +++ b/services/sync/tests/unit/test_utils_deepEquals.js @@ -0,0 +1,42 @@ +/** + * Print some debug message to the console. All arguments will be printed, + * separated by spaces. + * + * @param [arg0, arg1, arg2, ...] + * Any number of arguments to print out + * @usage _("Hello World") -> prints "Hello World" + * @usage _(1, 2, 3) -> prints "1 2 3" + */ +let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" ")); + +_("Make sure Utils.deepEquals correctly finds items that are deeply equal"); +Components.utils.import("resource://weave/util.js"); + +function run_test() { + let data = '[NaN, undefined, null, true, false, Infinity, 0, 1, "a", "b", {a: 1}, {a: "a"}, [{a: 1}], [{a: true}], {a: 1, b: 2}, [1, 2], [1, 2, 3]]'; + _("Generating two copies of data:", data); + let d1 = eval(data); + let d2 = eval(data); + + d1.forEach(function(a) { + _("Testing", a, typeof a, JSON.stringify([a])); + let numMatch = 0; + + d2.forEach(function(b) { + if (Utils.deepEquals(a, b)) { + numMatch++; + _("Found a match", b, typeof b, JSON.stringify([b])); + } + }); + + let expect = 1; + if (isNaN(a) && typeof a == "number") { + expect = 0; + _("Checking NaN should result in no matches"); + } + + _("Making sure we found the correct # match:", expect); + _("Actual matches:", numMatch); + do_check_eq(numMatch, expect); + }); +} From ef37c49fbff14dfedf8da570b31a48460b6a2f6b Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Sun, 24 May 2009 00:03:53 +0200 Subject: [PATCH 1101/1860] Use searchLogins instead of caching passwords (bug #489268) --- services/sync/modules/engines/passwords.js | 71 ++++++++++------------ 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 0fdc31779cf1..94c263454076 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -64,19 +64,6 @@ PasswordEngine.prototype = { _trackerObj: PasswordTracker, _recordObj: LoginRec, - _syncStartup: function PasswordEngine__syncStartup() { - let self = yield; - this._store.cacheLogins(); - yield SyncEngine.prototype._syncStartup.async(this, self.cb); - }, - - /* Wipe cache when sync finishes */ - _syncFinish: function PasswordEngine__syncFinish() { - let self = yield; - this._store.clearLoginCache(); - yield SyncEngine.prototype._syncFinish.async(this, self.cb); - }, - _recordLike: function SyncEngine__recordLike(a, b) { if (a.deleted || b.deleted) return false; @@ -118,16 +105,20 @@ PasswordStore.prototype = { return info; }, - cacheLogins: function PasswordStore_cacheLogins() { - this._log.debug("Caching all logins"); - this._loginItems = this.getAllIDs(); + _getLoginFromGUID: function PasswordStore__getLoginFromGUID(id) { + let prop = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag2); + prop.setPropertyAsAUTF8String("guid", id); + + let logins = Svc.Login.searchLogins({}, prop); + if (logins.length == 1) { + return logins[0]; + } else { + this._log.warn(logins.length + " items matching " + id + ". Ignoring"); + } + return false; }, - - clearLoginCache: function PasswordStore_clearLoginCache() { - this._log.debug("Clearing login cache"); - this._loginItems = null; - }, - + getAllIDs: function PasswordStore__getAllIDs() { let items = {}; let logins = Svc.Login.getAllLogins({}); @@ -143,11 +134,12 @@ PasswordStore.prototype = { changeItemID: function PasswordStore__changeItemID(oldID, newID) { this._log.debug("Changing item ID: " + oldID + " to " + newID); - if (!(oldID in this._loginItems)) { + let oldLogin = this._getLoginFromGUID(oldID); + if (!oldLogin) { this._log.warn("Can't change item ID: item doesn't exist"); return; } - if (newID in this._loginItems) { + if (this._getLoginFromGUID(newID)) { this._log.warn("Can't change item ID: new ID already in use"); return; } @@ -156,18 +148,21 @@ PasswordStore.prototype = { createInstance(Ci.nsIWritablePropertyBag2); prop.setPropertyAsAUTF8String("guid", newID); - Svc.Login.modifyLogin(this._loginItems[oldID], prop); + Svc.Login.modifyLogin(oldLogin, prop); }, itemExists: function PasswordStore__itemExists(id) { - return (id in this._loginItems); + if (this._getLoginFromGUID(id)) + return true; + return false; }, createRecord: function PasswordStore__createRecord(guid, cryptoMetaURL) { let record = new LoginRec(); - record.id = guid; - if (guid in this._loginItems) { - let login = this._loginItems[guid]; + let login = this._getLoginFromGUID(guid); + + record.id = guid; + if (login) { record.encryption = cryptoMetaURL; record.hostname = login.hostname; record.formSubmitURL = login.formSubmitURL; @@ -189,26 +184,26 @@ PasswordStore.prototype = { remove: function PasswordStore__remove(record) { this._log.debug("Removing login " + record.id); - if (record.id in this._loginItems) { - Svc.Login.removeLogin(this._loginItems[record.id]); + + let loginItem = this._getLoginFromGUID(record.id); + if (!loginItem) { + this._log.debug("Asked to remove record that doesn't exist, ignoring"); return; } - this._log.debug("Asked to remove record that doesn't exist, ignoring!"); + Svc.Login.removeLogin(loginItem); }, update: function PasswordStore__update(record) { - this._log.debug("Updating login for " + record.hostname); - - if (!(record.id in this._loginItems)) { + let loginItem = this._getLoginFromGUID(record.id); + if (!loginItem) { this._log.debug("Skipping update for unknown item: " + record.id); return; } - let login = this._loginItems[record.id]; - this._log.trace("Updating " + record.id); + this._log.debug("Updating " + record.id); let newinfo = this._nsLoginInfoFromRecord(record); - Svc.Login.modifyLogin(login, newinfo); + Svc.Login.modifyLogin(loginItem, newinfo); }, wipe: function PasswordStore_wipe() { From bd98b2f157ce884f330102b169b4fa483575ae49 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Sun, 24 May 2009 00:25:30 +0200 Subject: [PATCH 1102/1860] Use first login found if there are multiple GUIDs --- services/sync/modules/engines/passwords.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 94c263454076..c0b1d6839939 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -111,10 +111,11 @@ PasswordStore.prototype = { prop.setPropertyAsAUTF8String("guid", id); let logins = Svc.Login.searchLogins({}, prop); - if (logins.length == 1) { + if (logins.length > 0) { + this._log.info(logins.length + " items matching " + id + " found."); return logins[0]; } else { - this._log.warn(logins.length + " items matching " + id + ". Ignoring"); + this._log.warn("No items matching " + id + " found. Ignoring"); } return false; }, From 71577e556b7b91fa870e1cb3682e32ad495a4027 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Sun, 24 May 2009 02:11:05 +0200 Subject: [PATCH 1103/1860] Preference sync (bug #428370) --- services/sync/modules/engines/prefs.js | 248 ++++++++++++++++++++ services/sync/modules/service.js | 6 +- services/sync/modules/type_records/prefs.js | 67 ++++++ services/sync/services-sync.js | 29 +++ 4 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 services/sync/modules/engines/prefs.js create mode 100644 services/sync/modules/type_records/prefs.js diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js new file mode 100644 index 000000000000..3c2165e71bad --- /dev/null +++ b/services/sync/modules/engines/prefs.js @@ -0,0 +1,248 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Bookmarks Sync. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Anant Narayanan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['PrefsEngine']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const WEAVE_SYNC_PREFS = "extensions.weave.prefs.sync"; +const WEAVE_PREFS_GUID = "preferences"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/type_records/prefs.js"); + +Function.prototype.async = Async.sugar; + +function PrefsEngine() { + this._init(); +} +PrefsEngine.prototype = { + __proto__: SyncEngine.prototype, + name: "prefs", + displayName: "Preferences", + logName: "Prefs", + _storeObj: PrefStore, + _trackerObj: PrefTracker, + _recordObj: PrefRec, + + _recordLike: function SyncEngine__recordLike(a, b) { + if (a.deleted || b.deleted) + return false; + if (a.name == b.name && a.value == b.value) + return true; + return false; + } +}; + + +function PrefStore() { + this._init(); +} +PrefStore.prototype = { + __proto__: Store.prototype, + _logName: "PrefStore", + + get _prefs() { + let prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + this.__defineGetter__("_prefs", function() prefs); + return prefs; + }, + + get _syncPrefs() { + let service = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}). + map(function(elem) { return elem.substr(1); }); + + this.__defineGetter__("_syncPrefs", function() syncPrefs); + return syncPrefs; + }, + + _getAllPrefs: function PrefStore__getAllPrefs() { + let values = []; + let toSync = this._syncPrefs; + + let pref; + for (let i = 0; i < toSync.length; i++) { + if (!this._prefs.getBoolPref(WEAVE_SYNC_PREFS + "." + toSync[i])) + continue; + + pref = {}; + pref["name"] = toSync[i]; + + switch (this._prefs.getPrefType(toSync[i])) { + case Ci.nsIPrefBranch.PREF_INT: + pref["type"] = "int"; + pref["value"] = this._prefs.getIntPref(toSync[i]); + break; + case Ci.nsIPrefBranch.PREF_STRING: + pref["type"] = "string"; + pref["value"] = this._prefs.getCharPref(toSync[i]); + break; + case Ci.nsIPrefBranch.PREF_BOOL: + pref["type"] = "boolean"; + pref["value"] = this._prefs.getBoolPref(toSync[i]); + break; + default: + this._log.warn("Unsupported pref type for " + toSync[i]); + } + if ("value" in pref) + values[values.length] = pref; + } + + return values; + }, + + _setAllPrefs: function PrefStore__setAllPrefs(values) { + for (let i = 0; i < values.length; i++) { + switch (values[i]["type"]) { + case "int": + this._prefs.setIntPref(values[i]["name"], values[i]["value"]); + break; + case "string": + this._prefs.setCharPref(values[i]["name"], values[i]["value"]); + break; + case "boolean": + this._prefs.setBoolPref(values[i]["name"], values[i]["value"]); + break; + default: + this._log.warn("Unexpected preference type: " + values[i]["type"]); + } + } + }, + + getAllIDs: function PrefStore_getAllIDs() { + /* We store all prefs in just one WBO, with just one GUID */ + let allprefs = {}; + allprefs[WEAVE_PREFS_GUID] = this._getAllPrefs(); + return allprefs; + }, + + changeItemID: function PrefStore_changeItemID(oldID, newID) { + this._log.warn("PrefStore GUID is constant!"); + }, + + itemExists: function FormStore_itemExists(id) { + return (id === WEAVE_PREFS_GUID); + }, + + createRecord: function FormStore_createRecord(guid, cryptoMetaURL) { + let record = new PrefRec(); + record.id = guid; + + if (guid == WEAVE_PREFS_GUID) { + record.encryption = cryptoMetaURL; + record.value = this._getAllPrefs(); + } else { + record.deleted = true; + } + + return record; + }, + + create: function PrefStore_create(record) { + this._log.warn("Ignoring create request"); + }, + + remove: function PrefStore_remove(record) { + this._log.warn("Ignoring remove request") + }, + + update: function PrefStore_update(record) { + this._log.debug("Received pref updates, applying..."); + this._setAllPrefs(record.value); + }, + + wipe: function PrefStore_wipe() { + this._log.warn("Ignoring wipe request"); + } +}; + +function PrefTracker() { + this._init(); +} +PrefTracker.prototype = { + __proto__: Tracker.prototype, + _logName: "PrefTracker", + file: "prefs", + + get _prefs() { + let prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch2); + + this.__defineGetter__("_prefs", function() prefs); + return prefs; + }, + + get _syncPrefs() { + let service = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}). + map(function(elem) { return elem.substr(1); }); + + this.__defineGetter__("_syncPrefs", function() syncPrefs); + return syncPrefs; + }, + + _init: function PrefTracker__init() { + this.__proto__.__proto__._init.call(this); + this._log.debug("PrefTracker initializing!"); + this._prefs.addObserver("", this, false); + }, + + /* 25 points per pref change */ + observe: function(aSubject, aTopic, aData) { + if (aTopic != "nsPref:changed") + return; + + if (this._syncPrefs.indexOf(aData) != -1) { + this._score += 25; + this.addChangedID(WEAVE_PREFS_GUID); + this._log.debug("Preference " + aData + " changed"); + } + } +}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 44109bd552ee..d5585e3bc0cb 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -103,6 +103,7 @@ Cu.import("resource://weave/engines/extensions.js", Weave); Cu.import("resource://weave/engines/forms.js", Weave); Cu.import("resource://weave/engines/history.js", Weave); Cu.import("resource://weave/engines/input.js", Weave); +Cu.import("resource://weave/engines/prefs.js", Weave); Cu.import("resource://weave/engines/microformats.js", Weave); Cu.import("resource://weave/engines/passwords.js", Weave); Cu.import("resource://weave/engines/plugins.js", Weave); @@ -395,8 +396,9 @@ WeaveSvc.prototype = { break; case FIREFOX_ID: - engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", "Input", - "MicroFormat", "Password", "Plugin", "Tab", "Theme"]; + engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", + "Input", "MicroFormat", "Password", "Plugin", "Prefs", "Tab", + "Theme"]; break; case SEAMONKEY_ID: diff --git a/services/sync/modules/type_records/prefs.js b/services/sync/modules/type_records/prefs.js new file mode 100644 index 000000000000..c67b15456f40 --- /dev/null +++ b/services/sync/modules/type_records/prefs.js @@ -0,0 +1,67 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Anant Narayanan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['PrefRec']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/async.js"); +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/keys.js"); + +Function.prototype.async = Async.sugar; + +function PrefRec(uri) { + this._PrefRec_init(uri); +} +PrefRec.prototype = { + __proto__: CryptoWrapper.prototype, + _logName: "Record.Pref", + + _PrefRec_init: function PrefRec_init(uri) { + this._CryptoWrap_init(uri); + this.cleartext = { + }; + }, +}; + +Utils.deferGetSet(PrefRec, "cleartext", ["type", "value"]); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 72dbe1932e08..abb3216b4f32 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -20,6 +20,7 @@ pref("extensions.weave.engine.forms", false); pref("extensions.weave.engine.history", true); //pref("extensions.weave.engine.input", false); pref("extensions.weave.engine.passwords", true); +pref("extensions.weave.engine.prefs", false); pref("extensions.weave.engine.tabs", true); pref("extensions.weave.log.appender.console", "Warn"); @@ -43,3 +44,31 @@ pref("extensions.weave.network.numRetries", 2); pref("extensions.weave.tabs.sortMode", "recency"); pref("extensions.weave.openId.enabled", true); pref("extensions.weave.authenticator.enabled", true); + +// Preferences to be synced by default + +pref("extensions.weave.prefs.sync.browser.download.manager.scanWhenDone", true); + +pref("extensions.weave.prefs.sync.browser.search.openintab", true); +pref("extensions.weave.prefs.sync.browser.search.selectedEngine", true); + +pref("extensions.weave.prefs.sync.browser.sessionstore.resume_from_crash", true); +pref("extensions.weave.prefs.sync.browser.sessionstore.resume_session_once", true); +pref("extensions.weave.prefs.sync.browser.sessionstore.interval", true); + +pref("extensions.weave.prefs.sync.browser.startup.homepage", true); +pref("extensions.weave.prefs.sync.startup.homepage_override_url", true); + +pref("extensions.weave.prefs.sync.browser.tabs.tabMinWidth", true); +pref("extensions.weave.prefs.sync.browser.tabs.warnOnClose", true); +pref("extensions.weave.prefs.sync.browser.tabs.closeButtons", true); + +pref("extensions.weave.prefs.sync.browser.urlbar.autoFill", true); +pref("extensions.weave.prefs.sync.browser.urlbar.maxRichResults", true); +pref("extensions.weave.prefs.sync.browser.urlbar.clickSelectsAll", true); +pref("extensions.weave.prefs.sync.browser.urlbar.doubleClickSelectsAll", true); + +pref("extensions.weave.prefs.sync.layout.spellcheckDefault", true); +pref("extensions.weave.prefs.sync.security.dialog_enable_delay", true); +pref("extensions.weave.prefs.sync.signon.rememberSignons", true); +pref("extensions.weave.prefs.sync.spellchecker.dictionary", true); From 702fd8d9f88c389abd90878a909eb87735f55a1b Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Tue, 26 May 2009 07:29:17 -0700 Subject: [PATCH 1104/1860] bug 492725: make chrome-based website authenticator use core Firefox form fill notifications --- services/sync/modules/authenticator.js | 589 ------------------------- services/sync/services-sync.js | 1 + 2 files changed, 1 insertion(+), 589 deletions(-) delete mode 100644 services/sync/modules/authenticator.js diff --git a/services/sync/modules/authenticator.js b/services/sync/modules/authenticator.js deleted file mode 100644 index 3258a0d813d4..000000000000 --- a/services/sync/modules/authenticator.js +++ /dev/null @@ -1,589 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is mozilla.org code. - * - * The Initial Developer of the Original Code is Mozilla Corporation. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Justin Dolske (original author) - * Ehsan Akhgari - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ["Authenticator"]; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -let Authenticator = { - __logService : null, // Console logging service, used for debugging. - get _logService() { - if (!this.__logService) - this.__logService = Cc["@mozilla.org/consoleservice;1"]. - getService(Ci.nsIConsoleService); - return this.__logService; - }, - - __ioService: null, // IO service for string -> nsIURI conversion - get _ioService() { - if (!this.__ioService) - this.__ioService = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - return this.__ioService; - }, - - - __formFillService : null, // FormFillController, for username autocompleting - get _formFillService() { - if (!this.__formFillService) - this.__formFillService = - Cc["@mozilla.org/satchel/form-fill-controller;1"]. - getService(Ci.nsIFormFillController); - return this.__formFillService; - }, - - - __observerService : null, // Observer Service, for notifications - get _observerService() { - if (!this.__observerService) - this.__observerService = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - return this.__observerService; - }, - - get loginManager() { - delete this.loginManager; - return this.loginManager = Cc["@mozilla.org/login-manager;1"]. - getService(Ci.nsILoginManager); - }, - - // Private Browsing Service - // If the service is not available, null will be returned. - __privateBrowsingService : undefined, - get _privateBrowsingService() { - if (this.__privateBrowsingService == undefined) { - if ("@mozilla.org/privatebrowsing;1" in Cc) - this.__privateBrowsingService = Cc["@mozilla.org/privatebrowsing;1"]. - getService(Ci.nsIPrivateBrowsingService); - else - this.__privateBrowsingService = null; - } - return this.__privateBrowsingService; - }, - - - // Whether we are in private browsing mode - get _inPrivateBrowsing() { - var pbSvc = this._privateBrowsingService; - if (pbSvc) - return pbSvc.privateBrowsingEnabled; - else - return false; - }, - - _prefBranch : null, // Preferences service - - _debug : false, // mirrors signon.debug - - - /* - * init - * - * Initialize the Authenticator. Automatically called when service - * is created. - * - * Note: Service created in /browser/base/content/browser.js, - * delayedStartup() - */ - init : function () { - // Cache references to current |this| in utility objects - this._observer._pwmgr = this; - - // Preferences. Add observer so we get notified of changes. - this._prefBranch = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefService).getBranch("signon."); - this._prefBranch.QueryInterface(Ci.nsIPrefBranch2); - this._prefBranch.addObserver("", this._observer, false); - - // Get current preference values. - this._debug = this._prefBranch.getBoolPref("debug"); - }, - - /* - * log - * - * Internal function for logging debug messages to the Error Console window - */ - log : function (message) { - if (!this._debug) - return; - dump("Authenticator: " + message + "\n"); - this._logService.logStringMessage("Authenticator: " + message); - }, - - - /* ---------- Utility objects ---------- */ - - - /* - * _observer object - * - * Internal utility object, implements the nsIObserver interface. - * Used to receive notification for: form submission, preference changes. - */ - _observer : { - _pwmgr : null, - - QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - - // nsObserver - observe : function (subject, topic, data) { - - if (topic == "nsPref:changed") { - var prefName = data; - this._pwmgr.log("got change to " + prefName + " preference"); - - if (prefName == "debug") { - this._pwmgr._debug = - this._pwmgr._prefBranch.getBoolPref("debug"); - } else { - this._pwmgr.log("Oops! Pref not handled, change ignored."); - } - } else { - this._pwmgr.log("Oops! Unexpected notification: " + topic); - } - } - }, - - /* ---------- Primary Public interfaces ---------- */ - - /* ------- Internal methods / callbacks for document integration ------- */ - - - - - /* - * _getPasswordFields - * - * Returns an array of password field elements for the specified form. - * If no pw fields are found, or if more than 3 are found, then null - * is returned. - * - * skipEmptyFields can be set to ignore password fields with no value. - */ - _getPasswordFields : function (form, skipEmptyFields) { - // Locate the password fields in the form. - var pwFields = []; - for (var i = 0; i < form.elements.length; i++) { - if (form.elements[i].type != "password") - continue; - - if (skipEmptyFields && !form.elements[i].value) - continue; - - pwFields[pwFields.length] = { - index : i, - element : form.elements[i] - }; - } - - // If too few or too many fields, bail out. - if (pwFields.length == 0) { - this.log("(form ignored -- no password fields.)"); - return null; - } else if (pwFields.length > 3) { - this.log("(form ignored -- too many password fields. [got " + - pwFields.length + "])"); - return null; - } - - return pwFields; - }, - - - /* - * _getFormFields - * - * Returns the username and password fields found in the form. - * Can handle complex forms by trying to figure out what the - * relevant fields are. - * - * Returns: [usernameField, newPasswordField, oldPasswordField] - * - * usernameField may be null. - * newPasswordField will always be non-null. - * oldPasswordField may be null. If null, newPasswordField is just - * "theLoginField". If not null, the form is apparently a - * change-password field, with oldPasswordField containing the password - * that is being changed. - */ - _getFormFields : function (form, isSubmission) { - var usernameField = null; - - // Locate the password field(s) in the form. Up to 3 supported. - // If there's no password field, there's nothing for us to do. - var pwFields = this._getPasswordFields(form, isSubmission); - if (!pwFields) - return [null, null, null]; - - - // Locate the username field in the form by searching backwards - // from the first passwordfield, assume the first text field is the - // username. We might not find a username field if the user is - // already logged in to the site. - for (var i = pwFields[0].index - 1; i >= 0; i--) { - if (form.elements[i].type == "text") { - usernameField = form.elements[i]; - break; - } - } - - if (!usernameField) - this.log("(form -- no username field found)"); - - - // If we're not submitting a form (it's a page load), there are no - // password field values for us to use for identifying fields. So, - // just assume the first password field is the one to be filled in. - if (!isSubmission || pwFields.length == 1) - return [usernameField, pwFields[0].element, null]; - - - // Try to figure out WTF is in the form based on the password values. - var oldPasswordField, newPasswordField; - var pw1 = pwFields[0].element.value; - var pw2 = pwFields[1].element.value; - var pw3 = (pwFields[2] ? pwFields[2].element.value : null); - - if (pwFields.length == 3) { - // Look for two identical passwords, that's the new password - - if (pw1 == pw2 && pw2 == pw3) { - // All 3 passwords the same? Weird! Treat as if 1 pw field. - newPasswordField = pwFields[0].element; - oldPasswordField = null; - } else if (pw1 == pw2) { - newPasswordField = pwFields[0].element; - oldPasswordField = pwFields[2].element; - } else if (pw2 == pw3) { - oldPasswordField = pwFields[0].element; - newPasswordField = pwFields[2].element; - } else if (pw1 == pw3) { - // A bit odd, but could make sense with the right page layout. - newPasswordField = pwFields[0].element; - oldPasswordField = pwFields[1].element; - } else { - // We can't tell which of the 3 passwords should be saved. - this.log("(form ignored -- all 3 pw fields differ)"); - return [null, null, null]; - } - } else { // pwFields.length == 2 - if (pw1 == pw2) { - // Treat as if 1 pw field - newPasswordField = pwFields[0].element; - oldPasswordField = null; - } else { - // Just assume that the 2nd password is the new password - oldPasswordField = pwFields[0].element; - newPasswordField = pwFields[1].element; - } - } - - return [usernameField, newPasswordField, oldPasswordField]; - }, - - - /* - * _isAutoCompleteDisabled - * - * Returns true if the page requests autocomplete be disabled for the - * specified form input. - */ - _isAutocompleteDisabled : function (element) { - if (element && element.hasAttribute("autocomplete") && - element.getAttribute("autocomplete").toLowerCase() == "off") - return true; - - return false; - }, - - /* - * _getPasswordOrigin - * - * Get the parts of the URL we want for identification. - */ - _getPasswordOrigin : function (uriString, allowJS) { - var realm = ""; - try { - var uri = this._ioService.newURI(uriString, null, null); - - if (allowJS && uri.scheme == "javascript") - return "javascript:" - - realm = uri.scheme + "://" + uri.host; - - // If the URI explicitly specified a port, only include it when - // it's not the default. (We never want "http://foo.com:80") - var port = uri.port; - if (port != -1) { - var handler = this._ioService.getProtocolHandler(uri.scheme); - if (port != handler.defaultPort) - realm += ":" + port; - } - - } catch (e) { - // bug 159484 - disallow url types that don't support a hostPort. - // (although we handle "javascript:..." as a special case above.) - this.log("Couldn't parse origin for " + uriString); - realm = null; - } - - return realm; - }, - - _getActionOrigin : function (form) { - var uriString = form.action; - - // A blank or mission action submits to where it came from. - if (uriString == "") - uriString = form.baseURI; // ala bug 297761 - - return this._getPasswordOrigin(uriString, true); - }, - - - /* - * _fillDocument - * - * Called when a page has loaded. For each form in the document, - * we check to see if it can be filled with a stored login. - */ - _fillDocument : function (doc) { - var forms = doc.forms; - if (!forms || forms.length == 0) - return []; - - var formOrigin = this._getPasswordOrigin(doc.documentURI); - - // If there are no logins for this site, bail out now. - if (!this.loginManager.countLogins(formOrigin, "", null)) - return []; - - this.log("fillDocument processing " + forms.length + - " forms on " + doc.documentURI); - - var autofillForm = !this._inPrivateBrowsing && - this._prefBranch.getBoolPref("autofillForms"); - var previousActionOrigin = null; - var foundLogins = null; - - let formInfos = []; - for (var i = 0; i < forms.length; i++) { - var form = forms[i]; - - // Only the actionOrigin might be changing, so if it's the same - // as the last form on the page we can reuse the same logins. - var actionOrigin = this._getActionOrigin(form); - if (actionOrigin != previousActionOrigin) { - foundLogins = null; - previousActionOrigin = actionOrigin; - } - this.log("_fillDocument processing form[" + i + "]"); - let formInfo = this._fillForm(form, autofillForm, false, foundLogins); - foundLogins = formInfo.foundLogins; - if (formInfo.canFillForm) - formInfos.push(formInfo); - } // foreach form - return formInfos; - }, - - - /* - * _fillform - * - * Fill the form with login information if we can find it. This will find - * an array of logins if not given any, otherwise it will use the logins - * passed in. The logins are returned so they can be reused for - * optimization. Success of action is also returned in format - * [success, foundLogins]. autofillForm denotes if we should fill the form - * in automatically, ignoreAutocomplete denotes if we should ignore - * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo - * for optimization - */ - _fillForm : function (form, autofillForm, ignoreAutocomplete, foundLogins) { - // Heuristically determine what the user/pass fields are - // We do this before checking to see if logins are stored, - // so that the user isn't prompted for a master password - // without need. - var [usernameField, passwordField, ignored] = - this._getFormFields(form, false); - - // Need a valid password field to do anything. - if (passwordField == null) - return { foundLogins: foundLogins }; - - // If the fields are disabled or read-only, there's nothing to do. - if (passwordField.disabled || passwordField.readOnly || - usernameField && (usernameField.disabled || - usernameField.readOnly)) { - this.log("not filling form, login fields disabled"); - return { foundLogins: foundLogins }; - } - - // Need to get a list of logins if we weren't given them - if (foundLogins == null) { - var formOrigin = - this._getPasswordOrigin(form.ownerDocument.documentURI); - var actionOrigin = this._getActionOrigin(form); - foundLogins = this.loginManager.findLogins({}, formOrigin, actionOrigin, null); - this.log("found " + foundLogins.length + " matching logins."); - } else { - this.log("reusing logins from last form."); - } - - // Discard logins which have username/password values that don't - // fit into the fields (as specified by the maxlength attribute). - // The user couldn't enter these values anyway, and it helps - // with sites that have an extra PIN to be entered (bug 391514) - var maxUsernameLen = Number.MAX_VALUE; - var maxPasswordLen = Number.MAX_VALUE; - - // If attribute wasn't set, default is -1. - if (usernameField && usernameField.maxLength >= 0) - maxUsernameLen = usernameField.maxLength; - if (passwordField.maxLength >= 0) - maxPasswordLen = passwordField.maxLength; - - logins = foundLogins.filter(function (l) { - var fit = (l.username.length <= maxUsernameLen && - l.password.length <= maxPasswordLen); - if (!fit) - this.log("Ignored " + l.username + " login: won't fit"); - - return fit; - }, this); - - - // Nothing to do if we have no matching logins available. - if (logins.length == 0) - return { foundLogins: foundLogins }; - - - - // Attach autocomplete stuff to the username field, if we have - // one. This is normally used to select from multiple accounts, - // but even with one account we should refill if the user edits. - //if (usernameField) - // this._attachToInput(usernameField); - - // Don't clobber an existing password. - if (passwordField.value) - return { foundLogins: foundLogins }; - - // If the form has an autocomplete=off attribute in play, don't - // fill in the login automatically. We check this after attaching - // the autocomplete stuff to the username field, so the user can - // still manually select a login to be filled in. - var isFormDisabled = false; - if (!ignoreAutocomplete && - (this._isAutocompleteDisabled(form) || - this._isAutocompleteDisabled(usernameField) || - this._isAutocompleteDisabled(passwordField))) { - - isFormDisabled = true; - this.log("form not filled, has autocomplete=off"); - } - - // Variable such that we reduce code duplication and can be sure we - // should be firing notifications if and only if we can fill the form. - var selectedLogin = null; - - if (usernameField && usernameField.value) { - // If username was specified in the form, only fill in the - // password if we find a matching login. - var username = usernameField.value.toLowerCase(); - - let matchingLogins = logins.filter(function(l) - l.username.toLowerCase() == username); - if (matchingLogins.length) - selectedLogin = matchingLogins[0]; - else - this.log("Password not filled. None of the stored " + - "logins match the username already present."); - } else if (logins.length == 1) { - selectedLogin = logins[0]; - } else { - // We have multiple logins. Handle a special case here, for sites - // which have a normal user+pass login *and* a password-only login - // (eg, a PIN). Prefer the login that matches the type of the form - // (user+pass or pass-only) when there's exactly one that matches. - let matchingLogins; - if (usernameField) - matchingLogins = logins.filter(function(l) l.username); - else - matchingLogins = logins.filter(function(l) !l.username); - if (matchingLogins.length == 1) - selectedLogin = matchingLogins[0]; - else - this.log("Multiple logins for form, so not filling any."); - } - - //var didFillForm = false; - //if (selectedLogin && autofillForm && !isFormDisabled) { - // // Fill the form - // if (usernameField) - // usernameField.value = selectedLogin.username; - // passwordField.value = selectedLogin.password; - // didFillForm = true; - //} else if (selectedLogin && !autofillForm) { - // // For when autofillForm is false, but we still have the information - // // to fill a form, we notify observers. - // this._observerService.notifyObservers(form, "passwordmgr-found-form", "noAutofillForms"); - // this.log("autofillForms=false but form can be filled; notified observers"); - //} else if (selectedLogin && isFormDisabled) { - // // For when autocomplete is off, but we still have the information - // // to fill a form, we notify observers. - // this._observerService.notifyObservers(form, "passwordmgr-found-form", "autocompleteOff"); - // this.log("autocomplete=off but form can be filled; notified observers"); - //} - - return { canFillForm: true, - usernameField: usernameField, - passwordField: passwordField, - foundLogins: foundLogins, - selectedLogin: selectedLogin }; - } - -}; - -Authenticator.init(); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index abb3216b4f32..37e30f392b05 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -38,6 +38,7 @@ pref("extensions.weave.log.logger.engine.forms", "Debug"); pref("extensions.weave.log.logger.engine.history", "Debug"); pref("extensions.weave.log.logger.engine.tabs", "Debug"); pref("extensions.weave.log.logger.engine.clients", "Debug"); +pref("extensions.weave.log.logger.authenticator", "Debug"); pref("extensions.weave.network.numRetries", 2); From 65d48437a958c204db22dafb03971e5599150d8f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 26 May 2009 11:51:29 -0700 Subject: [PATCH 1105/1860] Bug 486259: correctly convert log messages with non-ascii encodings. Patch by Igor Velkov --- services/sync/modules/log4moz.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index aa12029b44b8..a2a30050bdf7 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -452,7 +452,6 @@ function FileAppender(file, formatter) { } FileAppender.prototype = { __proto__: Appender.prototype, - __fos: null, get _fos() { if (!this.__fos) @@ -461,10 +460,19 @@ FileAppender.prototype = { }, openStream: function FApp_openStream() { - this.__fos = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; - this.__fos.init(this._file, flags, PERMS_FILE, 0); + try { + let __fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; + __fos.init(this._file, flags, PERMS_FILE, 0); + + this.__fos = Cc["@mozilla.org/intl/converter-output-stream;1"] + .createInstance(Ci.nsIConverterOutputStream); + this.__fos.init(__fos, "UTF-8", 4096, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + } catch(e) { + dump("Error opening stream:\n" + e); + } }, closeStream: function FApp_closeStream() { @@ -482,7 +490,7 @@ FileAppender.prototype = { if (message === null || message.length <= 0) return; try { - this._fos.write(message, message.length); + this._fos.writeString(message); } catch(e) { dump("Error writing file:\n" + e); } @@ -524,11 +532,12 @@ RotatingFileAppender.prototype = { return; try { this.rotateLogs(); - this._fos.write(message, message.length); + FileAppender.prototype.doAppend.call(this, message); } catch(e) { - dump("Error writing file:\n" + e); + dump("Error writing file:" + e + "\n"); } }, + rotateLogs: function RFApp_rotateLogs() { if(this._file.exists() && this._file.fileSize < this._maxSize) From 1def544c4304a9fedca2505589fcb9f561ea7be2 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 26 May 2009 14:18:45 -0700 Subject: [PATCH 1106/1860] initial about:weave impl (does nothing) --- services/sync/Weave.js | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index bc117581e38a..6aaecd704199 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -34,7 +34,11 @@ * * ***** END LICENSE BLOCK ***** */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function WeaveService() {} WeaveService.prototype = { @@ -43,28 +47,51 @@ WeaveService.prototype = { classID: Components.ID("{74b89fb0-f200-4ae8-a3ec-dd164117f6de}"), _xpcom_categories: [{ category: "app-startup", service: true }], - QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver, - Components.interfaces.nsISupportsWeakReference]), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), observe: function BSS__observe(subject, topic, data) { switch (topic) { case "app-startup": - let os = Components.classes["@mozilla.org/observer-service;1"]. - getService(Components.interfaces.nsIObserverService); + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); os.addObserver(this, "sessionstore-windows-restored", true); break; /* The following event doesn't exist on Fennec; for Fennec loading, see * fennec-weave-overlay.js. */ case "sessionstore-windows-restored": - Components.utils.import("resource://weave/service.js"); + Cu.import("resource://weave/service.js"); Weave.Service.onStartup(); break; } } }; +function AboutWeaveService() {} +AboutWeaveService.prototype = { + classDescription: "about:weave", + contractID: "@mozilla.org/network/protocol/about;1?what=weave", + classID: Components.ID("{ecb6987d-9d71-475d-a44d-a5ff2099b08c}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, + Ci.nsISupportsWeakReference]), + + getURIFlags: function(aURI) { + return (Ci.nsIAboutModule.ALLOW_SCRIPT | + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT); + }, + + newChannel: function(aURI) { + let ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let ch = ios.newChannel("chrome://weave/content/weave.html", null, null); + ch.originalURI = aURI; + return ch; + } +}; + function NSGetModule(compMgr, fileSpec) { - return XPCOMUtils.generateModule([WeaveService]); + return XPCOMUtils.generateModule([WeaveService, AboutWeaveService]); } From e67c026b1a45ee1c7b9aec101a610f1718995128 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 2 Jun 2009 09:29:58 -0700 Subject: [PATCH 1107/1860] about:weave changes, add jquery --- services/sync/modules/base_records/keys.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index f766b6819a48..14b71ab884cb 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -132,13 +132,9 @@ SymKey.prototype = { }, unwrap: function SymKey_unwrap(privkey, passphrase, meta_record) { - let svc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - createInstance(Ci.IWeaveCrypto); - this._data = svc.unwrapSymmetricKey(this._data, - privkey.keyData, - passphrase, - identity.passphraseSalt, - identity.privkeyWrapIV); + this._data = + Svc.Crypto.unwrapSymmetricKey(this._data, privkey.keyData, passphrase, + privkey.salt, privkey.iv); } }; From 8043d7e3217ba8c97a173502e11b1c9db9754cd9 Mon Sep 17 00:00:00 2001 From: Wladimir Palant Date: Tue, 2 Jun 2009 18:08:52 +0200 Subject: [PATCH 1108/1860] Bug 495964 - Building WeaveCrypto.dll fails if Mercurial revision doesn't start with a number --- services/crypto/WeaveCrypto.rc.in | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/crypto/WeaveCrypto.rc.in b/services/crypto/WeaveCrypto.rc.in index 85d02f93f8a0..3f67f00b9ec3 100644 --- a/services/crypto/WeaveCrypto.rc.in +++ b/services/crypto/WeaveCrypto.rc.in @@ -43,15 +43,15 @@ #include #define VER_BUILDID_STR "@buildid@" -#define VER_FILEVERSION 1,9,0,@buildid@ -#define VER_PRODUCTVERSION 1,9,0,@buildid@ +#define VER_FILEVERSION 1,9,0,@buildid_short@ +#define VER_PRODUCTVERSION 1,9,0,@buildid_short@ #define VER_FILEFLAGS 0 | VS_FF_PRIVATEBUILD | VS_FF_PRERELEASE #define VER_PRODUCTNAME_STR "Weave" #define VER_INTERNALNAME_STR "WeaveCrypto" -#define VER_FILEVERSION_STR "1.9pre" -#define VER_PRODUCTVERSION_STR "1.9pre" +#define VER_FILEVERSION_STR "1.9.0.@buildid_short@" +#define VER_PRODUCTVERSION_STR "1.9.0.@buildid_short@" #define VER_COMPANYNAME_STR "Mozilla Corporation" #define VER_LEGALTRADEMARKS_STR "Mozilla" @@ -62,7 +62,7 @@ #define VER_ORIGINALFILENAME_STR "" VS_VERSION_INFO VERSIONINFO -FILEVERSION VER_PRODUCTVERSION +FILEVERSION VER_FILEVERSION PRODUCTVERSION VER_PRODUCTVERSION FILEFLAGSMASK 0x3fL FILEFLAGS VER_FILEFLAGS From dd8b198dc91a37b3d4fc8d41210cc27bae5660fe Mon Sep 17 00:00:00 2001 From: Wladimir Palant Date: Tue, 2 Jun 2009 11:25:37 +0200 Subject: [PATCH 1109/1860] Bug 495924 - TabTracker wrongly assumes that "this" pointer will be set correctly for event handlers --- services/sync/modules/engines/tabs.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 3f37f97d5e8a..f5d5ddabd5fd 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -358,6 +358,11 @@ TabTracker.prototype = { _TabTracker_init: function TabTracker__init() { this._init(); + // Make sure "this" pointer is always set correctly for event listeners + this.onTabOpened = Utils.bind2(this, this.onTabOpened); + this.onTabClosed = Utils.bind2(this, this.onTabClosed); + this.onTabSelected = Utils.bind2(this, this.onTabSelected); + // TODO Figure out how this will work on Fennec. // Register as an observer so we can catch windows opening and closing: From 206022559996a86d5ea114a5052e7e3c4605dc51 Mon Sep 17 00:00:00 2001 From: Wladimir Palant Date: Tue, 2 Jun 2009 21:25:55 +0200 Subject: [PATCH 1110/1860] Bug 495996 - Bookmark observer doesn't implement onBeforeItemRemoved method. r=Mardak --- services/sync/modules/engines/bookmarks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 21e6eac8df8b..018986214577 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -768,5 +768,6 @@ dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in al onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, onEndUpdateBatch: function BMT_onEndUpdateBatch() {}, + onBeforeItemRemoved: function BMT_onBeforeItemRemoved(itemId) {}, onItemVisited: function BMT_onItemVisited(itemId, aVisitID, time) {} }; From cb4ce7cb72eee381bbad9e375c0e536981828aff Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 3 Jun 2009 15:35:43 -0700 Subject: [PATCH 1111/1860] Sync persona by default --- services/sync/services-sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 37e30f392b05..83d9b5becad8 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -47,7 +47,6 @@ pref("extensions.weave.openId.enabled", true); pref("extensions.weave.authenticator.enabled", true); // Preferences to be synced by default - pref("extensions.weave.prefs.sync.browser.download.manager.scanWhenDone", true); pref("extensions.weave.prefs.sync.browser.search.openintab", true); @@ -69,6 +68,8 @@ pref("extensions.weave.prefs.sync.browser.urlbar.maxRichResults", true); pref("extensions.weave.prefs.sync.browser.urlbar.clickSelectsAll", true); pref("extensions.weave.prefs.sync.browser.urlbar.doubleClickSelectsAll", true); +pref("extensions.weave.prefs.sync.extensions.personas.current", true); + pref("extensions.weave.prefs.sync.layout.spellcheckDefault", true); pref("extensions.weave.prefs.sync.security.dialog_enable_delay", true); pref("extensions.weave.prefs.sync.signon.rememberSignons", true); From 4a57037ba4ae47697c635dc6fc0069d9350b09be Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 10:13:25 -0700 Subject: [PATCH 1112/1860] Add ext/Sync.js to do sync-async --- services/sync/modules/ext/Sync.js | 193 ++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 services/sync/modules/ext/Sync.js diff --git a/services/sync/modules/ext/Sync.js b/services/sync/modules/ext/Sync.js new file mode 100644 index 000000000000..17400f356a01 --- /dev/null +++ b/services/sync/modules/ext/Sync.js @@ -0,0 +1,193 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Edward Lee + * Dan Mills + * Myk Melez + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let EXPORTED_SYMBOLS = ["Sync"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +// Define some constants to specify various sync. callback states +const CB_READY = {}; +const CB_COMPLETE = {}; +const CB_FAIL = {}; + +// Share a secret only for functions in this file to prevent outside access +const SECRET = {}; + +/** + * Create a callback that remembers state like whether it's been called + */ +function makeCallback() { + // Initialize private callback data to prepare to be called + let _ = { + state: CB_READY, + value: null + }; + + // The main callback remembers the value it's passed and that it got data + let onComplete = function makeCallback_onComplete(data) { + _.state = CB_COMPLETE; + _.value = data; + }; + + // Only allow access to the private data if the secret matches + onComplete._ = function onComplete__(secret) secret == SECRET ? _ : {}; + + // Allow an alternate callback to trigger an exception to be thrown + onComplete.throw = function onComplete_throw(data) { + _.state = CB_FAIL; + _.value = data; + + // Cause the caller to get an exception and stop execution + throw data; + }; + + return onComplete; +} + +/** + * Make a synchronous version of the function object that will be called with + * the provided thisArg. + * + * @param func {Function} + * The asynchronous function to make a synchronous function + * @param thisArg {Object} [optional] + * The object that the function accesses with "this" + * @param callback {Function} [optional] [internal] + * The callback that will trigger the end of the async. call + * @usage let ret = Sync(asyncFunc, obj)(arg1, arg2); + * @usage let ret = Sync(ignoreThisFunc)(arg1, arg2); + * @usage let sync = Sync(async); let ret = sync(arg1, arg2); + */ +function Sync(func, thisArg, callback) { + return function syncFunc(/* arg1, arg2, ... */) { + // Grab the current thread so we can make it give up priority + let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + + // Save the original arguments into an array + let args = Array.slice(arguments); + + let instanceCallback = callback; + // We need to create a callback and insert it if we weren't given one + if (instanceCallback == null) { + // Create a new callback for this invocation instance and pass it in + instanceCallback = makeCallback(); + args.unshift(instanceCallback); + } + + // Call the async function bound to thisArg with the passed args + func.apply(thisArg, args); + + // Keep waiting until our callback is triggered + let callbackData = instanceCallback._(SECRET); + while (callbackData.state == CB_READY) + thread.processNextEvent(true); + + // Reset the state of the callback to prepare for another call + let state = callbackData.state; + callbackData.state = CB_READY; + + // Throw the value the callback decided to fail with + if (state == CB_FAIL) + throw callbackData.value; + + // Return the value passed to the callback + return callbackData.value; + }; +} + +/** + * Make a synchronous version of an async. function and the callback to trigger + * the end of the async. call. + * + * @param func {Function} + * The asynchronous function to make a synchronous function + * @param thisArg {Object} [optional] + * The object that the function accesses with "this" + * @usage let [sync, cb] = Sync.withCb(async); let ret = sync(arg1, arg2, cb); + */ +Sync.withCb = function Sync_withCb(func, thisArg) { + let cb = makeCallback(); + return [Sync(func, thisArg, cb), cb]; +}; + +/** + * Set a timer, simulating the API for the window.setTimeout call. + * This only simulates the API for the version of the call that accepts + * a function as its first argument and no additional parameters, + * and it doesn't return the timeout ID. + * + * @param func {Function} + * the function to call after the delay + * @param delay {Number} + * the number of milliseconds to wait + */ +function setTimeout(func, delay) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let callback = { + _func: func, + notify: function(timer) { + // Call the function such that "this" inside the function is the global + // object, just as it would be with window.setTimeout. + (this._func)(); + } + } + timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT); +} + +function sleep(callback, milliseconds) { + setTimeout(callback, milliseconds); +} + +/** + * Sleep the specified number of milliseconds, pausing execution of the caller + * without halting the current thread. + * For example, the following code pauses 1000ms between dumps: + * + * dump("Wait for it...\n"); + * Sync.sleep(1000); + * dump("Wait for it...\n"); + * Sync.sleep(1000); + * dump("What are you waiting for?!\n"); + * + * @param milliseconds {Number} + * The number of milliseconds to sleep + */ +Sync.sleep = Sync(sleep); From cc4238124c1f1525b621f58a66425e8bfae67d8e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 10:14:54 -0700 Subject: [PATCH 1113/1860] Switch Resource._request to Sync. (ChannelListener, filterUpload/Download) --- services/sync/modules/resource.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index b83d2ef52e5b..89ec6dc8e953 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -44,6 +44,7 @@ const Cu = Components.utils; Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/ext/Preferences.js"); +Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -208,7 +209,7 @@ Resource.prototype = { this._data = data; if ("PUT" == action || "POST" == action) { - yield this.filterUpload(self.cb); + Sync(this.filterUpload, this)(); this._log.trace(action + " Body:\n" + this._data); let type = ('Content-Type' in this._headers)? @@ -222,9 +223,10 @@ Resource.prototype = { channel.setUploadStream(stream, type, this._data.length); } - let listener = new ChannelListener(self.cb, this._onProgress, this._log); + let [chanOpen, chanCb] = Sync.withCb(channel.asyncOpen, channel); + let listener = new ChannelListener(chanCb, this._onProgress, this._log); channel.requestMethod = action; - this._data = yield channel.asyncOpen(listener, null); + this._data = chanOpen(listener, null); if (!channel.requestSucceeded) { this._log.debug(action + " request failed (" + channel.responseStatus + ")"); @@ -245,7 +247,7 @@ Resource.prototype = { case "GET": case "POST": this._log.trace(action + " Body:\n" + this._data); - yield this.filterDownload(self.cb); + Sync(this.filterDownload, this)(); break; } } From 8d04a9bdbe0300aed5c6474fb10c69a2206f1557 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 10:16:07 -0700 Subject: [PATCH 1114/1860] Change Resource.get() to be sync (no callback) and fix up call sites used for login + sync now. --- services/sync/modules/base_records/wbo.js | 2 +- services/sync/modules/engines.js | 4 ++-- services/sync/modules/engines/history.js | 2 +- services/sync/modules/resource.js | 19 +++++++++---------- services/sync/modules/service.js | 4 ++-- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 279b6f3ddd44..1a56f14e71cf 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -153,7 +153,7 @@ RecordManager.prototype = { try { this.lastResource = new Resource(url); - yield this.lastResource.get(self.cb); + this.lastResource.get(); record = new this._recordType(); record.deserialize(this.lastResource.data); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9d39053c9e0a..f75083d3f5bd 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -343,7 +343,7 @@ SyncEngine.prototype = { newitems.newer = this.lastSync; newitems.full = true; newitems.sort = "depthindex"; - yield newitems.get(self.cb); + newitems.get(); let item; let count = {applied: 0, reconciled: 0}; @@ -505,7 +505,7 @@ SyncEngine.prototype = { this._log.info("Uploading " + outnum + " records + " + count + " index/depth records)"); // do the upload - yield up.post(self.cb); + up.post(); // save last modified date let mod = up.data.modified; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 8be855f66fa0..b126a015d32b 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -108,7 +108,7 @@ HistoryEngine.prototype = { let coll = new Collection(this.engineURL, this._recordObj); coll.older = this.lastSync - 604800; // 1 week coll.full = 0; - yield coll.delete(self.cb); + coll.delete(); } }; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 89ec6dc8e953..1cc219a2e7cb 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -201,7 +201,6 @@ Resource.prototype = { }, _request: function Res__request(action, data) { - let self = yield; let iter = 0; let channel = this._createRequest(); @@ -252,23 +251,23 @@ Resource.prototype = { } } - self.done(this._data); + return this._data; }, - get: function Res_get(onComplete) { - this._request.async(this, onComplete, "GET"); + get: function Res_get() { + return this._request("GET"); }, - put: function Res_put(onComplete, data) { - this._request.async(this, onComplete, "PUT", data); + put: function Res_put(data) { + return this._request("PUT", data); }, - post: function Res_post(onComplete, data) { - this._request.async(this, onComplete, "POST", data); + post: function Res_post(data) { + return this._request("POST", data); }, - delete: function Res_delete(onComplete) { - this._request.async(this, onComplete, "DELETE"); + delete: function Res_delete() { + return this._request("DELETE"); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d5585e3bc0cb..6aa3c084f6a2 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -461,7 +461,7 @@ WeaveSvc.prototype = { let res = new Resource(this.baseURL + "api/register/chknode/" + username); try { - yield res.get(self.cb); + res.get(); } catch (e) { /* we check status below */ } if (res.lastChannel.responseStatus == 404) { @@ -516,7 +516,7 @@ WeaveSvc.prototype = { return headers; } }; - yield res.get(self.cb); + res.get(); //JSON.parse(res.data); // throws if not json self.done(true); From b11468b880dec8c3b90ac3a833e85d68f47dca74 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 10:17:27 -0700 Subject: [PATCH 1115/1860] Fix Resource.* call sites used for wiping the server and initial sync (+ key gen upload) --- services/sync/modules/base_records/keys.js | 2 +- services/sync/modules/engines.js | 2 +- services/sync/modules/service.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 14b71ab884cb..34a1d94b8cbd 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -187,7 +187,7 @@ PubKeyManager.prototype = { let self = yield; for each (let key in keys) { let res = new Resource(key.uri); - yield res.put(self.cb, key.serialize()); + res.put(key.serialize()); } }; fn.async(this, onComplete, keys); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f75083d3f5bd..ecc52d736381 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -309,7 +309,7 @@ SyncEngine.prototype = { meta.generateIV(); yield meta.addUnwrappedKey(self.cb, pubkey, symkey); let res = new Resource(meta.uri); - yield res.put(self.cb, meta.serialize()); + res.put(meta.serialize()); } // first sync special case: upload all items diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6aa3c084f6a2..50771a08aa41 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -989,7 +989,7 @@ WeaveSvc.prototype = { meta.payload.storageVersion = WEAVE_VERSION; meta.payload.syncID = Clients.syncID; let res = new Resource(meta.uri); - yield res.put(self.cb, meta.serialize()); + res.put(meta.serialize()); }, /** @@ -1007,7 +1007,7 @@ WeaveSvc.prototype = { // Grab all the collections for the user let userURL = this.clusterURL + this.username + "/"; let res = new Resource(userURL); - yield res.get(self.cb); + res.get(); // Get the array of collections and delete each one let allCollections = JSON.parse(res.data); @@ -1017,7 +1017,7 @@ WeaveSvc.prototype = { if (engines && engines.indexOf(name) == -1) continue; - yield new Resource(userURL + name).delete(self.cb); + new Resource(userURL + name).delete(); } catch(ex) { this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); From 92c96a7f688105242889ee743d67ef3da607acfb Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 14:18:04 -0700 Subject: [PATCH 1116/1860] Change SyncEngine._reconcile to not be async/yield. --- services/sync/modules/engines.js | 26 ++++++++---------------- services/sync/modules/engines/history.js | 14 ++++--------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index ecc52d736381..00d217599bd6 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -353,7 +353,7 @@ SyncEngine.prototype = { this._lowMemCheck(); try { yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); - if (yield this._reconcile.async(this, self.cb, item)) { + if (this._reconcile(item)) { count.applied++; yield this._applyIncoming.async(this, self.cb, item); } else { @@ -406,33 +406,25 @@ SyncEngine.prototype = { // case when syncing for the first time two machines that already have the // same bookmarks. In this case we change the IDs to match. _reconcile: function SyncEngine__reconcile(item) { - let self = yield; - let ret = true; - // Step 1: Check for conflicts // If same as local record, do not upload this._log.trace("Reconcile step 1"); if (item.id in this._tracker.changedIDs) { if (this._isEqual(item)) this._tracker.removeChangedID(item.id); - self.done(false); - return; + return false; } // Step 2: Check for updates // If different from local record, apply server update this._log.trace("Reconcile step 2"); - if (this._store.itemExists(item.id)) { - self.done(!this._isEqual(item)); - return; - } + if (this._store.itemExists(item.id)) + return !this._isEqual(item); // If the incoming item has been deleted, skip step 3 this._log.trace("Reconcile step 2.5"); - if (item.deleted) { - self.done(true); - return; - } + if (item.deleted) + return true; // Step 3: Check for similar items this._log.trace("Reconcile step 3"); @@ -443,11 +435,11 @@ SyncEngine.prototype = { this._tracker.removeChangedID(id); this._tracker.removeChangedID(item.id); this._store.cache.clear(); // because parentid refs will be wrong - self.done(false); - return; + return false; } } - self.done(true); + + return true; }, // Apply incoming records diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index b126a015d32b..e869db09a222 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -77,26 +77,20 @@ HistoryEngine.prototype = { // Note that we don't attempt to equalize the IDs, the history store does that // as part of update() _reconcile: function HistEngine__reconcile(item) { - let self = yield; - let ret = true; - // Step 1: Check for conflicts // If same as local record, do not upload if (item.id in this._tracker.changedIDs) { if (this._isEqual(item)) this._tracker.removeChangedID(item.id); - self.done(false); - return; + return false; } // Step 2: Check if the item is deleted - we don't support that (yet?) - if (item.deleted) { - self.done(false); - return; - } + if (item.deleted) + return false; // Step 3: Apply update/new record - self.done(true); + return true; }, _syncFinish: function HistEngine__syncFinish(error) { From 300abe02a7a43a9e1726e9e9a75409f633088c81 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 4 Jun 2009 15:29:31 -0700 Subject: [PATCH 1117/1860] fix wbo record test, expand to use record manager as well --- services/sync/tests/unit/test_records_wbo.js | 34 ++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index 501789e00d70..d1dace815ff1 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -4,6 +4,7 @@ try { Cu.import("resource://weave/async.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/identity.js"); + Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); } catch (e) { do_throw(e); } @@ -11,11 +12,18 @@ Function.prototype.async = Async.sugar; function record_handler(metadata, response) { let obj = {id: "asdf-1234-asdf-1234", - modified: "2454725.98283", + modified: 2454725.98283, payload: JSON.stringify({cheese: "roquefort"})}; return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } +function record_handler2(metadata, response) { + let obj = {id: "record2", + modified: 2454725.98284, + payload: JSON.stringify({cheese: "gruyere"})}; + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); +} + function async_test() { let self = yield; let server; @@ -26,27 +34,41 @@ function async_test() { log.info("Setting up server and authenticator"); - server = httpd_setup({"/record": record_handler}); + server = httpd_setup({"/record": record_handler, + "/record2": record_handler2}); let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); Auth.defaultAuthenticator = auth; log.info("Getting a WBO record"); - let res = new WBORecord("http://localhost:8080/record"); - let rec = yield res.get(self.cb); + let res = new Resource("http://localhost:8080/record"); + yield res.get(self.cb); + + let rec = new WBORecord(res.uri.spec); + rec.deserialize(res.data); + do_check_eq(rec.id, "record"); // NOT "asdf-1234-asdf-1234"! do_check_eq(rec.modified, 2454725.98283); do_check_eq(typeof(rec.payload), "object"); do_check_eq(rec.payload.cheese, "roquefort"); - do_check_eq(res.lastRequest.status, 200); + do_check_eq(res.lastChannel.responseStatus, 200); + + log.info("Getting a WBO record using the record manager"); + + let rec2 = yield Records.get(self.cb, "http://localhost:8080/record2"); + do_check_eq(rec2.id, "record2"); + do_check_eq(rec2.modified, 2454725.98284); + do_check_eq(typeof(rec2.payload), "object"); + do_check_eq(rec2.payload.cheese, "gruyere"); + do_check_eq(Records.lastResource.lastChannel.responseStatus, 200); log.info("Done!"); do_test_finished(); } catch (e) { do_throw(e); } finally { server.stop(); } - + self.done(); } From 2ecd1d7f854dfe9f6e947a4366861ca86fa56f53 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 4 Jun 2009 15:30:36 -0700 Subject: [PATCH 1118/1860] make sure record id comes from the URI, only use the payload body in the collection GET case --- services/sync/modules/base_records/collection.js | 1 + services/sync/modules/base_records/wbo.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index f0d77088c556..332e4743561e 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -158,6 +158,7 @@ CollectionIterator.prototype = { let record = new this._coll._recordObj(); record.deserialize(JSON.stringify(item)); // FIXME: inefficient record.baseUri = this._coll.uri; + record.id = record.data.id; self.done(record); }; diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 279b6f3ddd44..904bc7baf1f8 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -52,6 +52,7 @@ function WBORecord(uri) { this._WBORec_init(uri); } WBORecord.prototype = { + id: null, deleted: false, _logName: "Record.WBO", @@ -72,7 +73,7 @@ WBORecord.prototype = { if (typeof(value) != "string") value = value.spec; let foo = value.split('/'); - this.data.id = foo.pop(); + this.id = foo.pop(); this.baseUri = Utils.makeURI(foo.join('/') + '/'); }, @@ -126,7 +127,7 @@ WBORecord.prototype = { ].join("\n ") + " }", }; -Utils.deferGetSet(WBORecord, "data", ["id", "parentid", "modified", "depth", +Utils.deferGetSet(WBORecord, "data", ["parentid", "modified", "depth", "sortindex", "payload"]); Utils.lazy(this, 'Records', RecordManager); From dc628e09529be10b93d1a50795d8cb7c43ad5e0d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 16:25:12 -0700 Subject: [PATCH 1119/1860] RecordMgr_import: async + async/yield -> sync. --- services/sync/modules/base_records/wbo.js | 39 ++++++++++------------- services/sync/modules/service.js | 3 +- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 823216ee537c..901b43462c99 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -145,29 +145,22 @@ RecordManager.prototype = { this._aliases = {}; }, - import: function RegordMgr_import(onComplete, url) { - let fn = function RegordMgr__import(url) { - let self = yield; - let record; + import: function RecordMgr_import(url) { + this._log.trace("Importing record: " + (url.spec ? url.spec : url)); + try { + this.lastResource = new Resource(url); + this.lastResource.get(); - this._log.trace("Importing record: " + (url.spec? url.spec : url)); + let record = new this._recordType(); + record.deserialize(this.lastResource.data); + record.uri = url; // NOTE: may override id in this.lastResource.data - try { - this.lastResource = new Resource(url); - this.lastResource.get(); - - record = new this._recordType(); - record.deserialize(this.lastResource.data); - record.uri = url; // NOTE: may override id in this.lastResource.data - - this.set(url, record); - } catch (e) { - this._log.debug("Failed to import record: " + e); - record = null; - } - self.done(record); - }; - fn.async(this, onComplete, url); + return this.set(url, record); + } + catch(ex) { + this._log.debug("Failed to import record: " + ex); + return null; + } }, get: function RegordMgr_get(onComplete, url) { @@ -187,7 +180,7 @@ RecordManager.prototype = { record = this._records[spec]; if (!record) - record = yield this.import(self.cb, url); + record = this.import(url); self.done(record); }; @@ -196,7 +189,7 @@ RecordManager.prototype = { set: function RegordMgr_set(url, record) { let spec = url.spec ? url.spec : url; - this._records[spec] = record; + return this._records[spec] = record; }, contains: function RegordMgr_contains(url) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 50771a08aa41..b893e912bbe9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -649,8 +649,7 @@ WeaveSvc.prototype = { let reset = false; this._log.debug("Fetching global metadata record"); - let meta = yield Records.import(self.cb, this.clusterURL + this.username + - "/meta/global"); + let meta = Records.import(this.clusterURL + this.username + "/meta/global"); let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; From 2c9336fcedb89efbe50cdb7807813975285b63dd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 16:50:57 -0700 Subject: [PATCH 1120/1860] RecordMgr_get: async + async/yield -> sync. --- services/sync/modules/base_records/crypto.js | 8 ++--- services/sync/modules/base_records/keys.js | 2 +- services/sync/modules/base_records/wbo.js | 33 +++++++------------- services/sync/modules/engines.js | 2 +- services/sync/modules/service.js | 2 +- 5 files changed, 19 insertions(+), 28 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 4ddb1d0b6401..7cf71d650471 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -81,9 +81,9 @@ CryptoWrapper.prototype = { } let pubkey = yield PubKeys.getDefaultKey(self.cb); - let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); + let privkey = PrivKeys.get(pubkey.privateKeyUri); - let meta = yield CryptoMetas.get(self.cb, this.encryption); + let meta = CryptoMetas.get(this.encryption); let symkey = yield meta.getKey(self.cb, privkey, passphrase); this.ciphertext = Svc.Crypto.encrypt(JSON.stringify([this.cleartext]), @@ -106,9 +106,9 @@ CryptoWrapper.prototype = { } let pubkey = yield PubKeys.getDefaultKey(self.cb); - let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); + let privkey = PrivKeys.get(pubkey.privateKeyUri); - let meta = yield CryptoMetas.get(self.cb, this.encryption); + let meta = CryptoMetas.get(this.encryption); let symkey = yield meta.getKey(self.cb, privkey, passphrase); // note: payload is wrapped in an array, see _encrypt diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 34a1d94b8cbd..1c3f9e89f596 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -152,7 +152,7 @@ PubKeyManager.prototype = { getDefaultKey: function KeyMgr_getDefaultKey(onComplete) { let fn = function KeyMgr__getDefaultKey() { let self = yield; - let ret = yield this.get(self.cb, this.defaultKeyUri); + let ret = this.get(this.defaultKeyUri); self.done(ret); }; fn.async(this, onComplete); diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 901b43462c99..273905c04faa 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -163,28 +163,19 @@ RecordManager.prototype = { } }, - get: function RegordMgr_get(onComplete, url) { - let fn = function RegordMgr__get(url) { - let self = yield; + get: function RecordMgr_get(url) { + // Note: using url object directly as key for this._records cache does not + // work because different url objects (even pointing to the same place) are + // different objects and therefore not equal. So always use the string, not + // the object, as a key. + // TODO: use the string as key for this._aliases as well? (Don't know) + let spec = url.spec ? url.spec : url; + if (spec in this._records) + return this._records[spec]; - let record = null; - let spec = url.spec? url.spec : url; - /* Note: using url object directly as key for this._records cache - * does not work because different url objects (even pointing to the - * same place) are different objects and therefore not equal. So - * always use the string, not the object, as a key. TODO: use the - * string as key for this._aliases as well? (Don't know) */ - if (url in this._aliases) - url = this._aliases[url]; - if (spec in this._records) - record = this._records[spec]; - - if (!record) - record = this.import(url); - - self.done(record); - }; - fn.async(this, onComplete, url); + if (url in this._aliases) + url = this._aliases[url]; + return this.import(url); }, set: function RegordMgr_set(url, record) { diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 00d217599bd6..48a3fc8faefa 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -301,7 +301,7 @@ SyncEngine.prototype = { this._log.debug("Ensuring server crypto records are there"); - let meta = yield CryptoMetas.get(self.cb, this.cryptoMetaURL); + let meta = CryptoMetas.get(this.cryptoMetaURL); if (!meta) { let symkey = Svc.Crypto.generateRandomKey(); let pubkey = yield PubKeys.getDefaultKey(self.cb); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b893e912bbe9..5e31ccfe9979 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -719,7 +719,7 @@ WeaveSvc.prototype = { this._log.debug("Public key has no key data"); else { // make sure we have a matching privkey - let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); + let privkey = PrivKeys.get(pubkey.privateKeyUri); if (!privkey) this._log.debug("Could not get private key"); else if (privkey.keyData == null) From 025c936ce7af2e37c89c4202db95fdb1f5546abc Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 16:52:28 -0700 Subject: [PATCH 1121/1860] Remove Async.sugar from wbo.js. --- services/sync/modules/base_records/wbo.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 273905c04faa..f700f6f5841d 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -44,9 +44,6 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; function WBORecord(uri) { this._WBORec_init(uri); From f6386cab93b604dc1b96d08bc5bb4d3891744eb1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 17:04:51 -0700 Subject: [PATCH 1122/1860] PubKeyManager_getDefaultKey: async + async/yield -> sync. --- services/sync/modules/base_records/crypto.js | 4 ++-- services/sync/modules/base_records/keys.js | 9 ++------- services/sync/modules/engines.js | 2 +- services/sync/modules/service.js | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 7cf71d650471..7f1dfc2faedc 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -80,7 +80,7 @@ CryptoWrapper.prototype = { return; } - let pubkey = yield PubKeys.getDefaultKey(self.cb); + let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); let meta = CryptoMetas.get(this.encryption); @@ -105,7 +105,7 @@ CryptoWrapper.prototype = { return; } - let pubkey = yield PubKeys.getDefaultKey(self.cb); + let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); let meta = CryptoMetas.get(this.encryption); diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 1c3f9e89f596..b18ba4b35dd2 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -149,13 +149,8 @@ PubKeyManager.prototype = { get defaultKeyUri() this._defaultKeyUri, set defaultKeyUri(value) { this._defaultKeyUri = value; }, - getDefaultKey: function KeyMgr_getDefaultKey(onComplete) { - let fn = function KeyMgr__getDefaultKey() { - let self = yield; - let ret = this.get(this.defaultKeyUri); - self.done(ret); - }; - fn.async(this, onComplete); + getDefaultKey: function PubKeyManager_getDefaultKey() { + return this.get(this.defaultKeyUri); }, createKeypair: function KeyMgr_createKeypair(passphrase, pubkeyUri, privkeyUri) { diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 48a3fc8faefa..bdcc1586eaf9 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -304,7 +304,7 @@ SyncEngine.prototype = { let meta = CryptoMetas.get(this.cryptoMetaURL); if (!meta) { let symkey = Svc.Crypto.generateRandomKey(); - let pubkey = yield PubKeys.getDefaultKey(self.cb); + let pubkey = PubKeys.getDefaultKey(); meta = new CryptoMeta(this.cryptoMetaURL); meta.generateIV(); yield meta.addUnwrappedKey(self.cb, pubkey, symkey); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5e31ccfe9979..3d8ded63eb94 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -712,7 +712,7 @@ WeaveSvc.prototype = { } let needKeys = true; - let pubkey = yield PubKeys.getDefaultKey(self.cb); + let pubkey = PubKeys.getDefaultKey(); if (!pubkey) this._log.debug("Could not get public key"); else if (pubkey.keyData == null) From 59124520af87a7b930e59559a26776bb3b6c5c37 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 17:09:32 -0700 Subject: [PATCH 1123/1860] PubKeyManager_uploadKeypair: async + async/yield -> sync. --- services/sync/modules/base_records/keys.js | 12 +++--------- services/sync/modules/service.js | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index b18ba4b35dd2..d4f3ad4b9bba 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -177,15 +177,9 @@ PubKeyManager.prototype = { return {pubkey: pubkey, privkey: privkey}; }, - uploadKeypair: function KeyMgr_uploadKeypair(onComplete, keys) { - let fn = function KeyMgr__uploadKeypair(keys) { - let self = yield; - for each (let key in keys) { - let res = new Resource(key.uri); - res.put(key.serialize()); - } - }; - fn.async(this, onComplete, keys); + uploadKeypair: function PubKeyManager_uploadKeypair(keys) { + for each (let key in keys) + new Resource(key.uri).put(key.serialize()); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3d8ded63eb94..e2ba14ef0fa9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -763,7 +763,7 @@ WeaveSvc.prototype = { let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, PrivKeys.defaultKeyUri); try { - yield PubKeys.uploadKeypair(self.cb, keys); + PubKeys.uploadKeypair(keys); ret = true; } catch (e) { this._setSyncFailure(KEYS_UPLOAD_FAIL); From f08f1f1746b0288f7d04d1853eef56624899c40f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 17:10:25 -0700 Subject: [PATCH 1124/1860] Remove Async.sugar from keys.js. --- services/sync/modules/base_records/keys.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index d4f3ad4b9bba..287781f45601 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -45,12 +45,9 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); -Function.prototype.async = Async.sugar; - function PubKey(uri) { this._PubKey_init(uri); } From 46b7ae0328c1d7d7f5d883916b2192d15d53b6ab Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 17:21:42 -0700 Subject: [PATCH 1125/1860] CryptoMeta_getKey: async + async/yield -> sync. --- services/sync/modules/base_records/crypto.js | 25 +++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 7f1dfc2faedc..71d034468be0 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -84,7 +84,7 @@ CryptoWrapper.prototype = { let privkey = PrivKeys.get(pubkey.privateKeyUri); let meta = CryptoMetas.get(this.encryption); - let symkey = yield meta.getKey(self.cb, privkey, passphrase); + let symkey = meta.getKey(privkey, passphrase); this.ciphertext = Svc.Crypto.encrypt(JSON.stringify([this.cleartext]), symkey, meta.bulkIV); @@ -109,7 +109,7 @@ CryptoWrapper.prototype = { let privkey = PrivKeys.get(pubkey.privateKeyUri); let meta = CryptoMetas.get(this.encryption); - let symkey = yield meta.getKey(self.cb, privkey, passphrase); + let symkey = meta.getKey(privkey, passphrase); // note: payload is wrapped in an array, see _encrypt this.cleartext = JSON.parse(Svc.Crypto.decrypt(this.ciphertext, @@ -152,20 +152,16 @@ CryptoMeta.prototype = { this.bulkIV = Svc.Crypto.generateRandomIV(); }, - _getKey: function CryptoMeta__getKey(privkey, passphrase) { - let self = yield; - let wrapped_key; - + getKey: function CryptoMeta_getKey(privkey, passphrase) { // We cache the unwrapped key, as it's expensive to generate - if (this._unwrappedKey) { - self.done(this._unwrappedKey); - return; - } + if (this._unwrappedKey) + return this._unwrappedKey; // get the uri to our public key let pubkeyUri = privkey.publicKeyUri.spec; // each hash key is a relative uri, resolve those and match against ours + let wrapped_key; for (let relUri in this.payload.keyring) { if (pubkeyUri == this.baseUri.resolve(relUri)) wrapped_key = this.payload.keyring[relUri]; @@ -173,13 +169,8 @@ CryptoMeta.prototype = { if (!wrapped_key) throw "keyring doesn't contain a key for " + pubkeyUri; - this._unwrappedKey = - Svc.Crypto.unwrapSymmetricKey(wrapped_key, privkey.keyData, passphrase, - privkey.salt, privkey.iv); - self.done(this._unwrappedKey); - }, - getKey: function CryptoMeta_getKey(onComplete, privkey, passphrase) { - this._getKey.async(this, onComplete, privkey, passphrase); + return this._unwrappedKey = Svc.Crypto.unwrapSymmetricKey(wrapped_key, + privkey.keyData, passphrase, privkey.salt, privkey.iv); }, _addKey: function CryptoMeta__addKey(new_pubkey, privkey, passphrase) { From 120304afb94b8f9e1fda13c274906f73ca17dd34 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 17:36:34 -0700 Subject: [PATCH 1126/1860] CryptoMeta_addUnwrappedKey: async + async/yield -> sync. --- services/sync/modules/base_records/crypto.js | 11 +++-------- services/sync/modules/engines.js | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 71d034468be0..0cbbfa0d8ee4 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -182,12 +182,10 @@ CryptoMeta.prototype = { this._addKey.async(this, onComplete, new_pubkey, privkey, passphrase); }, - _addUnwrappedKey: function CryptoMeta__addUnwrappedKey(new_pubkey, symkey) { - let self = yield; - + addUnwrappedKey: function CryptoMeta_addUnwrappedKey(new_pubkey, symkey) { // get the new public key - if (typeof new_pubkey == 'string') - new_pubkey = PubKeys.get(self.cb, new_pubkey); + if (typeof new_pubkey == "string") + new_pubkey = PubKeys.get(new_pubkey); // each hash key is a relative uri, resolve those and // if we find the one we're about to add, remove it @@ -198,9 +196,6 @@ CryptoMeta.prototype = { this.payload.keyring[new_pubkey.uri.spec] = Svc.Crypto.wrapSymmetricKey(symkey, new_pubkey.keyData); - }, - addUnwrappedKey: function CryptoMeta_addUnwrappedKey(onComplete, new_pubkey, symkey) { - this._addUnwrappedKey.async(this, onComplete, new_pubkey, symkey); } }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index bdcc1586eaf9..fbd0f8e01ec9 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -307,7 +307,7 @@ SyncEngine.prototype = { let pubkey = PubKeys.getDefaultKey(); meta = new CryptoMeta(this.cryptoMetaURL); meta.generateIV(); - yield meta.addUnwrappedKey(self.cb, pubkey, symkey); + meta.addUnwrappedKey(pubkey, symkey); let res = new Resource(meta.uri); res.put(meta.serialize()); } From d0662c8a6f23dd12be8eed50c371f675a147e158 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 17:42:22 -0700 Subject: [PATCH 1127/1860] Bug 496455 - CryptoMeta__addUnwrappedKey always gets undefined new_pubkey yield for PubKeys.get() when passed a string uri --- services/sync/modules/base_records/crypto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 4ddb1d0b6401..a530d3704a2c 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -196,7 +196,7 @@ CryptoMeta.prototype = { // get the new public key if (typeof new_pubkey == 'string') - new_pubkey = PubKeys.get(self.cb, new_pubkey); + new_pubkey = yield PubKeys.get(self.cb, new_pubkey); // each hash key is a relative uri, resolve those and // if we find the one we're about to add, remove it From 98970d317557e75f3605850339aa022de0ab82be Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 17:58:37 -0700 Subject: [PATCH 1128/1860] CryptoWrapper_encrypt: async + async/yield -> sync. --- services/sync/modules/base_records/crypto.js | 13 ++----------- services/sync/modules/engines.js | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 0cbbfa0d8ee4..15509ab85d05 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -71,14 +71,10 @@ CryptoWrapper.prototype = { // with the encrypted payload cleartext: null, - _encrypt: function CryptoWrap__encrypt(passphrase) { - let self = yield; - + encrypt: function CryptoWrapper_encrypt(passphrase) { // No need to encrypt deleted records - if (this.deleted) { - self.done(); + if (this.deleted) return; - } let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); @@ -89,11 +85,6 @@ CryptoWrapper.prototype = { this.ciphertext = Svc.Crypto.encrypt(JSON.stringify([this.cleartext]), symkey, meta.bulkIV); this.cleartext = null; - - self.done(); - }, - encrypt: function CryptoWrap_encrypt(onComplete, passphrase) { - this._encrypt.async(this, onComplete, passphrase); }, _decrypt: function CryptoWrap__decrypt(passphrase) { diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index fbd0f8e01ec9..db9651aef8be 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -479,7 +479,7 @@ SyncEngine.prototype = { // skip getting siblings of already processed and deleted records if (!out.deleted && !(out.id in meta)) this._store.createMetaRecords(out.id, meta); - yield out.encrypt(self.cb, ID.get('WeaveCryptoID').password); + out.encrypt(ID.get('WeaveCryptoID').password); up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient } From 906c66ed2e243ed8fa59380f16587737c43c0531 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 18:06:56 -0700 Subject: [PATCH 1129/1860] Fix ClientRecord_encrypt to not need to do async. --- services/sync/modules/type_records/clients.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index b2037b3d0592..bbba09368209 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -64,10 +64,7 @@ ClientRecord.prototype = { get cleartext() this.serialize(), // XXX engines.js calls encrypt/decrypt for all records, so define these: - encrypt: function ClientRec_encrypt(onComplete) { - let fn = function ClientRec__encrypt() {let self = yield;}; - fn.async(this, onComplete); - }, + encrypt: function ClientRecord_encrypt(passphrase) {}, decrypt: function ClientRec_decrypt(onComplete) { let fn = function ClientRec__decrypt() {let self = yield;}; fn.async(this, onComplete); From d19c9bdaa2b147d3d1fbce352bf86817fcb4b855 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 19:06:57 -0700 Subject: [PATCH 1130/1860] CryptoWrapper_decrypt: async + async/yield -> sync. Fix up bookmark/client decrypt. --- services/sync/modules/base_records/crypto.js | 13 +++---------- services/sync/modules/engines.js | 2 +- services/sync/modules/type_records/bookmark.js | 16 ++++++++-------- services/sync/modules/type_records/clients.js | 5 +---- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 15509ab85d05..2ee4319d4423 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -87,14 +87,10 @@ CryptoWrapper.prototype = { this.cleartext = null; }, - _decrypt: function CryptoWrap__decrypt(passphrase) { - let self = yield; - + decrypt: function CryptoWrapper_decrypt(passphrase) { // Deleted records aren't encrypted - if (this.deleted) { - self.done(); + if (this.deleted) return; - } let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); @@ -107,10 +103,7 @@ CryptoWrapper.prototype = { symkey, meta.bulkIV))[0]; this.ciphertext = null; - self.done(this.cleartext); - }, - decrypt: function CryptoWrap_decrypt(onComplete, passphrase) { - this._decrypt.async(this, onComplete, passphrase); + return this.cleartext; }, toString: function CryptoWrap_toString() "{ " + [ diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index db9651aef8be..4866f50ffd01 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -352,7 +352,7 @@ SyncEngine.prototype = { while ((item = yield newitems.iter.next(self.cb))) { this._lowMemCheck(); try { - yield item.decrypt(self.cb, ID.get('WeaveCryptoID').password); + item.decrypt(ID.get('WeaveCryptoID').password); if (this._reconcile(item)) { count.applied++; yield this._applyIncoming.async(this, self.cb, item); diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 7dce3c39ef9c..fef7e865ed16 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -55,15 +55,15 @@ function PlacesItem(uri) { this._PlacesItem_init(uri); } PlacesItem.prototype = { - decrypt: function PlacesItem_decrypt(onComplete, passphrase) { - CryptoWrapper.prototype.decrypt.call(this, Utils.bind2(this, function(ret) { - // Convert the abstract places item to the actual object type - if (!this.deleted) - this.__proto__ = this.getTypeObject(this.type).prototype; + decrypt: function PlacesItem_decrypt(passphrase) { + // Do the normal CryptoWrapper decrypt, but change types before returning + let clear = CryptoWrapper.prototype.decrypt.apply(this, arguments); - // Give the original callback the result - onComplete(ret); - }), passphrase); + // 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) { diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index bbba09368209..d945cd4a020c 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -65,8 +65,5 @@ ClientRecord.prototype = { // XXX engines.js calls encrypt/decrypt for all records, so define these: encrypt: function ClientRecord_encrypt(passphrase) {}, - decrypt: function ClientRec_decrypt(onComplete) { - let fn = function ClientRec__decrypt() {let self = yield;}; - fn.async(this, onComplete); - } + decrypt: function ClientRecord_decrypt(passphrase) {} }; From e425436220623c7aa5310005e144316328e3545c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 19:17:03 -0700 Subject: [PATCH 1131/1860] Remove Async.sugar from type/bookmark.js and type/clientData.js. --- services/sync/modules/type_records/bookmark.js | 3 --- services/sync/modules/type_records/clients.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index fef7e865ed16..53391d71fad5 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -44,13 +44,10 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); -Function.prototype.async = Async.sugar; - function PlacesItem(uri) { this._PlacesItem_init(uri); } diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index d945cd4a020c..eee34164032c 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -43,11 +43,8 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); -Function.prototype.async = Async.sugar; - function ClientRecord(uri) { this._ClientRec_init(uri); } From 3a5ba40728873e8528421619e26977a8a4bf44e4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 22:10:41 -0700 Subject: [PATCH 1132/1860] Remove Sync(filterUpload/Download) and make them plain sync function as well as beforePUT and afterGET. --- services/sync/modules/resource.js | 35 +++++++++++-------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 1cc219a2e7cb..247248d24344 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -179,25 +179,16 @@ Resource.prototype = { this._lastProgress = Date.now(); }, - filterUpload: function Res_filterUpload(onComplete) { - let fn = function() { - let self = yield; - for each (let filter in this._filters) { - this._data = yield filter.beforePUT.async(filter, self.cb, this._data, this); - } - }; - fn.async(this, onComplete); + filterUpload: function Resource_filterUpload() { + this._data = this._filters.reduce(function(data, filter) { + return filter.beforePUT(data); + }, this._data); }, - filterDownload: function Res_filterUpload(onComplete) { - let fn = function() { - let self = yield; - let filters = this._filters.slice(); // reverse() mutates, so we copy - for each (let filter in filters.reverse()) { - this._data = yield filter.afterGET.async(filter, self.cb, this._data, this); - } - }; - fn.async(this, onComplete); + filterDownload: function Resource_filterDownload() { + this._data = this._filters.reduceRight(function(data, filter) { + return filter.afterGET(data); + }, this._data); }, _request: function Res__request(action, data) { @@ -208,7 +199,7 @@ Resource.prototype = { this._data = data; if ("PUT" == action || "POST" == action) { - Sync(this.filterUpload, this)(); + this.filterUpload(); this._log.trace(action + " Body:\n" + this._data); let type = ('Content-Type' in this._headers)? @@ -246,7 +237,7 @@ Resource.prototype = { case "GET": case "POST": this._log.trace(action + " Body:\n" + this._data); - Sync(this.filterDownload, this)(); + this.filterDownload(); break; } } @@ -348,15 +339,13 @@ function JsonFilter() { } JsonFilter.prototype = { beforePUT: function JsonFilter_beforePUT(data) { - let self = yield; this._log.trace("Encoding data as JSON"); - self.done(JSON.stringify(data)); + return JSON.stringify(data); }, afterGET: function JsonFilter_afterGET(data) { - let self = yield; this._log.trace("Decoding JSON data"); - self.done(JSON.parse(data)); + return JSON.parse(data); } }; From 56da47581ae6ef3ad8be8d440ea0f835dc88d9b4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 23:04:56 -0700 Subject: [PATCH 1133/1860] Remove trailing newline bug 480480 work-around: trim(). --- services/sync/modules/service.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d5585e3bc0cb..46b323400702 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -468,8 +468,7 @@ WeaveSvc.prototype = { this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); ret = Svc.Prefs.get("serverURL"); } else if (res.lastChannel.responseStatus == 200) { - // XXX Bug 480480 Work around the server sending a trailing newline - ret = 'https://' + res.data.trim() + '/'; + ret = "https://" + res.data + "/"; } self.done(ret); From 24e486dc7759147bc62d68f5e7d635d48efc3aed Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 23:24:15 -0700 Subject: [PATCH 1134/1860] WeaveSvc_findCluster: async + async/yield -> sync. --- services/sync/modules/service.js | 34 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0f15030e5f4c..e81bbcae3e40 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -452,28 +452,24 @@ WeaveSvc.prototype = { // These are global (for all engines) // gets cluster from central LDAP server and returns it, or null on error - findCluster: function WeaveSvc_findCluster(onComplete, username) { - let fn = function WeaveSvc__findCluster() { - let self = yield; - let ret = null; + findCluster: function WeaveSvc_findCluster(username) { + this._log.debug("Finding cluster for user " + username); - this._log.debug("Finding cluster for user " + username); + let res = new Resource(this.baseURL + "api/register/chknode/" + username); + try { + res.get(); + } + catch(ex) { /* we check status below */ } - let res = new Resource(this.baseURL + "api/register/chknode/" + username); - try { - res.get(); - } catch (e) { /* we check status below */ } + if (res.lastChannel.responseStatus == 404) { + this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); + return Svc.Prefs.get("serverURL"); + } - if (res.lastChannel.responseStatus == 404) { - this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); - ret = Svc.Prefs.get("serverURL"); - } else if (res.lastChannel.responseStatus == 200) { - ret = "https://" + res.data + "/"; - } + if (res.lastChannel.responseStatus == 200) + return "https://" + res.data + "/"; - self.done(ret); - }; - fn.async(this, onComplete); + return null; }, // gets cluster from central LDAP server and sets this.clusterURL @@ -500,7 +496,7 @@ WeaveSvc.prototype = { let self = yield; this._log.debug("Verifying login for user " + username); - let url = yield this.findCluster(self.cb, username); + let url = this.findCluster(username); if (isLogin) this.clusterURL = url; From be703c8e9562b1ea76359e936d5d65201f5964a7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 4 Jun 2009 23:48:27 -0700 Subject: [PATCH 1135/1860] SyncEngine__syncStartup, SyncEngine__uploadOutgoing, SyncEngine__syncFinish all async/yield -> sync. --- services/sync/modules/engines.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 4866f50ffd01..a67963d7a9f2 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -297,8 +297,6 @@ SyncEngine.prototype = { // Any setup that needs to happen at the beginning of each sync. // Makes sure crypto records and keys are all set-up _syncStartup: function SyncEngine__syncStartup() { - let self = yield; - this._log.debug("Ensuring server crypto records are there"); let meta = CryptoMetas.get(this.cryptoMetaURL); @@ -461,8 +459,6 @@ SyncEngine.prototype = { // Upload outgoing records _uploadOutgoing: function SyncEngine__uploadOutgoing() { - let self = yield; - let outnum = [i for (i in this._tracker.changedIDs)].length; this._log.debug("Preparing " + outnum + " outgoing records"); if (outnum) { @@ -509,8 +505,7 @@ SyncEngine.prototype = { // Any cleanup necessary. // Save the current snapshot so as to calculate changes at next sync - _syncFinish: function SyncEngine__syncFinish(error) { - let self = yield; + _syncFinish: function SyncEngine__syncFinish() { this._log.debug("Finishing up sync"); this._tracker.resetScore(); }, @@ -519,12 +514,12 @@ SyncEngine.prototype = { let self = yield; try { - yield this._syncStartup.async(this, self.cb); + this._syncStartup(); Observers.notify("weave:engine:sync:status", "process-incoming"); yield this._processIncoming.async(this, self.cb); Observers.notify("weave:engine:sync:status", "upload-outgoing"); - yield this._uploadOutgoing.async(this, self.cb); - yield this._syncFinish.async(this, self.cb); + this._uploadOutgoing(); + this._syncFinish(); } catch (e) { this._log.warn("Sync failed"); From 3b6388147051669074ad306b3eb744eac467aabf Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 00:36:11 -0700 Subject: [PATCH 1136/1860] Store_applyIncoming, SyncEngine__applyIncoming: async + async/yield -> sync. --- services/sync/modules/engines.js | 5 ++--- services/sync/modules/stores.js | 18 +++++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index a67963d7a9f2..262254b8afc8 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -353,7 +353,7 @@ SyncEngine.prototype = { item.decrypt(ID.get('WeaveCryptoID').password); if (this._reconcile(item)) { count.applied++; - yield this._applyIncoming.async(this, self.cb, item); + this._applyIncoming(item); } else { count.reconciled++; this._log.trace("Skipping reconciled incoming item " + item.id); @@ -442,11 +442,10 @@ SyncEngine.prototype = { // Apply incoming records _applyIncoming: function SyncEngine__applyIncoming(item) { - let self = yield; this._log.trace("Incoming:\n" + item); try { this._tracker.ignoreAll = true; - yield this._store.applyIncoming(self.cb, item); + this._store.applyIncoming(item); if (this._lastSyncTmp < item.modified) this._lastSyncTmp = item.modified; } catch (e) { diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 7725e5c2aeb1..d11fbd727060 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -74,17 +74,13 @@ Store.prototype = { this._log = Log4Moz.repository.getLogger("Store." + this._logName); }, - applyIncoming: function Store_applyIncoming(onComplete, record) { - let fn = function(rec) { - let self = yield; - if (rec.deleted) - this.remove(rec); - else if (!this.itemExists(rec.id)) - this.create(rec); - else - this.update(rec); - }; - fn.async(this, onComplete, record); + applyIncoming: function Store_applyIncoming(record) { + if (record.deleted) + this.remove(record); + else if (!this.itemExists(record.id)) + this.create(record); + else + this.update(record); }, // override these in derived objects From 5f0c4afda0324e671c017912daab051d923d03f4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 00:38:23 -0700 Subject: [PATCH 1137/1860] CollectionIterator_next: async + async/yield -> sync. --- .../sync/modules/base_records/collection.js | 22 ++++++++----------- services/sync/modules/engines.js | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 332e4743561e..69fc5d920e2d 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -148,21 +148,17 @@ CollectionIterator.prototype = { get count() { return this._coll.data.length; }, - next: function CollIter_next(onComplete) { - let fn = function CollIter__next() { - let self = yield; + next: function CollectionIterator_next() { + if (this._idx >= this.count) + return null; - if (this._idx >= this.count) - return; - let item = this._coll.data[this._idx++]; - let record = new this._coll._recordObj(); - record.deserialize(JSON.stringify(item)); // FIXME: inefficient - record.baseUri = this._coll.uri; - record.id = record.data.id; + let item = this._coll.data[this._idx++]; + let record = new this._coll._recordObj(); + record.deserialize(JSON.stringify(item)); // FIXME: inefficient + record.baseUri = this._coll.uri; + record.id = record.data.id; - self.done(record); - }; - fn.async(this, onComplete); + return record; }, reset: function CollIter_reset() { diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 262254b8afc8..4effa8872efd 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -347,7 +347,7 @@ SyncEngine.prototype = { let count = {applied: 0, reconciled: 0}; this._lastSyncTmp = 0; - while ((item = yield newitems.iter.next(self.cb))) { + while ((item = newitems.iter.next())) { this._lowMemCheck(); try { item.decrypt(ID.get('WeaveCryptoID').password); From dfb3e84af90fe2f4f1abe40737875747cc34028c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 00:39:35 -0700 Subject: [PATCH 1138/1860] SyncEngine__processIncoming: async/yield -> sync. --- services/sync/modules/engines.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 4effa8872efd..450369857987 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -326,8 +326,6 @@ SyncEngine.prototype = { // Generate outgoing records _processIncoming: function SyncEngine__processIncoming() { - let self = yield; - this._log.debug("Downloading & applying server changes"); // enable cache, and keep only the first few items. Otherwise (when @@ -515,7 +513,7 @@ SyncEngine.prototype = { try { this._syncStartup(); Observers.notify("weave:engine:sync:status", "process-incoming"); - yield this._processIncoming.async(this, self.cb); + this._processIncoming(); Observers.notify("weave:engine:sync:status", "upload-outgoing"); this._uploadOutgoing(); this._syncFinish(); From 54df61d8d672218f7c5e0f075cbf598d1fc6585e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 00:51:09 -0700 Subject: [PATCH 1139/1860] Remove Async.sugar from auth.js, resource.js, stores.js, trackers.js. --- services/sync/modules/auth.js | 3 --- services/sync/modules/resource.js | 3 --- services/sync/modules/stores.js | 3 --- services/sync/modules/trackers.js | 3 --- 4 files changed, 12 deletions(-) diff --git a/services/sync/modules/auth.js b/services/sync/modules/auth.js index a66063c1cf8e..12890aaa7fb2 100644 --- a/services/sync/modules/auth.js +++ b/services/sync/modules/auth.js @@ -44,9 +44,6 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; Utils.lazy(this, 'Auth', AuthMgr); diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 247248d24344..530b3172b415 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -48,11 +48,8 @@ Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/auth.js"); -Function.prototype.async = Async.sugar; - function RequestException(resource, action, request) { this._resource = resource; this._action = action; diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index d11fbd727060..94f6a3114dfe 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -46,9 +46,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; /* * Data Stores diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index b53ad9c05a7d..867e9858f2ae 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -45,9 +45,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; /* * Trackers are associated with a single engine and deal with From 9c1e3b17410ffb623691006439495a4972e29903 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Jun 2009 10:39:51 -0700 Subject: [PATCH 1140/1860] partially revert d6cac1e80da9 - use id from the body; add additional tests to catch more errors --- services/sync/modules/base_records/wbo.js | 3 +-- services/sync/tests/unit/test_records_wbo.js | 28 ++++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 904bc7baf1f8..21b33e956d46 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -52,7 +52,6 @@ function WBORecord(uri) { this._WBORec_init(uri); } WBORecord.prototype = { - id: null, deleted: false, _logName: "Record.WBO", @@ -127,7 +126,7 @@ WBORecord.prototype = { ].join("\n ") + " }", }; -Utils.deferGetSet(WBORecord, "data", ["parentid", "modified", "depth", +Utils.deferGetSet(WBORecord, "data", ["id", "parentid", "modified", "depth", "sortindex", "payload"]); Utils.lazy(this, 'Records', RecordManager); diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index d1dace815ff1..32f721bce8ac 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -6,6 +6,7 @@ try { Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); + Cu.import("resource://weave/base_records/collection.js"); } catch (e) { do_throw(e); } Function.prototype.async = Async.sugar; @@ -24,6 +25,13 @@ function record_handler2(metadata, response) { return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } +function coll_handler(metadata, response) { + let obj = [{id: "record2", + modified: 2454725.98284, + payload: JSON.stringify({cheese: "gruyere"})}]; + return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); +} + function async_test() { let self = yield; let server; @@ -35,7 +43,8 @@ function async_test() { log.info("Setting up server and authenticator"); server = httpd_setup({"/record": record_handler, - "/record2": record_handler2}); + "/record2": record_handler2, + "/coll": coll_handler}); let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); Auth.defaultAuthenticator = auth; @@ -45,10 +54,13 @@ function async_test() { let res = new Resource("http://localhost:8080/record"); yield res.get(self.cb); - let rec = new WBORecord(res.uri.spec); + let rec = new WBORecord(); rec.deserialize(res.data); + do_check_eq(rec.id, "asdf-1234-asdf-1234"); // NOT "record"! + rec.uri = res.uri; do_check_eq(rec.id, "record"); // NOT "asdf-1234-asdf-1234"! + do_check_eq(rec.modified, 2454725.98283); do_check_eq(typeof(rec.payload), "object"); do_check_eq(rec.payload.cheese, "roquefort"); @@ -63,6 +75,18 @@ function async_test() { do_check_eq(rec2.payload.cheese, "gruyere"); do_check_eq(Records.lastResource.lastChannel.responseStatus, 200); + log.info("Using a collection to get a record"); + + let coll = new Collection("http://localhost:8080/coll", WBORecord); + yield coll.get(self.cb); + do_check_eq(coll.iter.count, 1); + + let rec3 = yield coll.iter.next(self.cb); + do_check_eq(rec3.id, "record2"); + do_check_eq(rec3.modified, 2454725.98284); + do_check_eq(typeof(rec3.payload), "object"); + do_check_eq(rec3.payload.cheese, "gruyere"); + log.info("Done!"); do_test_finished(); } From 7e7fab678595742ddf9b911b8df0ebd366e42c50 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Fri, 5 Jun 2009 11:23:31 -0700 Subject: [PATCH 1141/1860] fix keys test to work with latest key structure + nsIChannel --- services/sync/tests/unit/test_records_keys.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/sync/tests/unit/test_records_keys.js b/services/sync/tests/unit/test_records_keys.js index 91a6c56f05e7..33d31eebe845 100644 --- a/services/sync/tests/unit/test_records_keys.js +++ b/services/sync/tests/unit/test_records_keys.js @@ -13,8 +13,8 @@ function pubkey_handler(metadata, response) { let obj = {id: "asdf-1234-asdf-1234", modified: "2454725.98283", payload: JSON.stringify({type: "pubkey", - private_key: "http://localhost:8080/privkey", - key_data: "asdfasdfasf..."})}; + privateKeyUri: "http://localhost:8080/privkey", + keyData: "asdfasdfasf..."})}; return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } @@ -22,8 +22,8 @@ function privkey_handler(metadata, response) { let obj = {id: "asdf-1234-asdf-1234-2", modified: "2454725.98283", payload: JSON.stringify({type: "privkey", - public_key: "http://localhost:8080/pubkey", - key_data: "asdfasdfasf..."})}; + publicKeyUri: "http://localhost:8080/pubkey", + keyData: "asdfasdfasf..."})}; return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } @@ -47,13 +47,13 @@ function async_test() { let pubkey = yield PubKeys.get(self.cb, "http://localhost:8080/pubkey"); do_check_eq(pubkey.data.payload.type, "pubkey"); - do_check_eq(pubkey.lastRequest.status, 200); + do_check_eq(PubKeys.lastResource.lastChannel.responseStatus, 200); - log.info("Getting a private key"); + log.info("Getting matching private key"); let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); do_check_eq(privkey.data.payload.type, "privkey"); - do_check_eq(privkey.lastRequest.status, 200); + do_check_eq(PrivKeys.lastResource.lastChannel.responseStatus, 200); log.info("Done!"); do_test_finished(); From 5a0fa9f732b35724a91040b8e4b8bd345206ce47 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 15:34:23 -0700 Subject: [PATCH 1142/1860] Temporarily convert _notify to _notifyAsync and add a Utils.notify. --- services/sync/modules/engines.js | 11 ++++++----- services/sync/modules/util.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 450369857987..8e16482c9123 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -179,7 +179,8 @@ Engine.prototype = { try { level = Utils.prefs.getCharPref(levelPref); } catch (e) { /* ignore unset prefs */ } - this._notify = Wrap.notify("weave:engine:"); + this._notify = Utils.notify("weave:engine:"); + this._notifyAsync = Wrap.notify("weave:engine:"); this._log = Log4Moz.repository.getLogger("Engine." + this.logName); this._log.level = Log4Moz.Level[level]; @@ -190,13 +191,13 @@ Engine.prototype = { sync: function Engine_sync(onComplete) { if (!this._sync) throw "engine does not implement _sync method"; - this._notify("sync", this.name, this._sync).async(this, onComplete); + this._notifyAsync("sync", this.name, this._sync).async(this, onComplete); }, wipeServer: function Engimne_wipeServer(onComplete) { if (!this._wipeServer) throw "engine does not implement _wipeServer method"; - this._notify("wipe-server", this.name, this._wipeServer).async(this, onComplete); + this._notifyAsync("wipe-server", this.name, this._wipeServer).async(this, onComplete); }, /** @@ -206,7 +207,7 @@ Engine.prototype = { if (!this._resetClient) throw "engine does not implement _resetClient method"; - this._notify("reset-client", this.name, this._resetClient). + this._notifyAsync("reset-client", this.name, this._resetClient). async(this, onComplete); }, @@ -220,7 +221,7 @@ Engine.prototype = { }, wipeClient: function Engine_wipeClient(onComplete) { - this._notify("wipe-client", this.name, this._wipeClient).async(this, onComplete); + this._notifyAsync("wipe-client", this.name, this._wipeClient).async(this, onComplete); } }; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 199c6a0726b9..d7619307f569 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -52,6 +52,38 @@ Cu.import("resource://weave/log4moz.js"); */ let Utils = { + /** + * Wrap functions to notify when it starts and finishes executing or if it got + * an error. The message is a combination of a provided prefix and local name + * with the current state and the subject is the provided subject. + * + * @usage function MyObj() { this._notify = Utils.notify("prefix:"); } + * MyObj.foo = function() { this._notify(name, subject, func)(); } + */ + notify: function Utils_notify(prefix) { + return function NotifyMaker(name, subject, func) { + let thisArg = this; + let notify = function(state) { + let mesg = prefix + name + ":" + state; + thisArg._log.debug("Event: " + mesg); + Observers.notify(mesg, subject); + }; + + return function WrappedNotify() { + try { + notify("start"); + let ret = func.call(thisArg); + notify("finish"); + return ret; + } + catch(ex) { + notify("error"); + throw ex; + } + }; + }; + }, + // Generates a brand-new globally unique identifier (GUID). makeGUID: function makeGUID() { let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. From 147c0e64604c9372c303051f85f8d5723b270b6e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 15:34:32 -0700 Subject: [PATCH 1143/1860] Engine_sync: async -> sync. SyncEngine__sync: async/yield -> sync. --- services/sync/modules/engines.js | 6 ++---- services/sync/modules/service.js | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 8e16482c9123..0e29a9d65aa0 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -188,10 +188,10 @@ Engine.prototype = { this._log.debug("Engine initialized"); }, - sync: function Engine_sync(onComplete) { + sync: function Engine_sync() { if (!this._sync) throw "engine does not implement _sync method"; - this._notifyAsync("sync", this.name, this._sync).async(this, onComplete); + this._notify("sync", this.name, this._sync)(); }, wipeServer: function Engimne_wipeServer(onComplete) { @@ -509,8 +509,6 @@ SyncEngine.prototype = { }, _sync: function SyncEngine__sync() { - let self = yield; - try { this._syncStartup(); Observers.notify("weave:engine:sync:status", "process-incoming"); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e81bbcae3e40..dc76c5f6cd92 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -859,7 +859,7 @@ WeaveSvc.prototype = { } this._log.debug("Refreshing client list"); - yield Clients.sync(self.cb); + Clients.sync(); // Process the incoming commands if we have any if (Clients.getClients()[Clients.clientID].commands) { @@ -875,7 +875,7 @@ WeaveSvc.prototype = { } finally { // Always immediately push back the local client (now without commands) - yield Clients.sync(self.cb); + Clients.sync(); } } @@ -957,7 +957,7 @@ WeaveSvc.prototype = { _syncEngine: function WeaveSvc__syncEngine(engine) { let self = yield; try { - yield engine.sync(self.cb); + engine.sync(); if (!this.cancelRequested) self.done(true); } From 99de61e6bebab9372e37910bd0ef7650c8daedb5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 15:34:34 -0700 Subject: [PATCH 1144/1860] Engine_resetClient: async -> sync. SyncEngine__resetClient: async/yield -> sync. --- services/sync/modules/engines.js | 9 +++------ services/sync/modules/service.js | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 0e29a9d65aa0..91d92a8836be 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -203,19 +203,17 @@ Engine.prototype = { /** * Get rid of any local meta-data */ - resetClient: function Engine_resetClient(onComplete) { + resetClient: function Engine_resetClient() { if (!this._resetClient) throw "engine does not implement _resetClient method"; - this._notifyAsync("reset-client", this.name, this._resetClient). - async(this, onComplete); + this._notify("reset-client", this.name, this._resetClient)(); }, _wipeClient: function Engine__wipeClient() { let self = yield; - yield this.resetClient(self.cb); - + this.resetClient(); this._log.debug("Deleting all local data"); this._store.wipe(); }, @@ -532,7 +530,6 @@ SyncEngine.prototype = { }, _resetClient: function SyncEngine__resetClient() { - let self = yield; this.resetLastSync(); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index dc76c5f6cd92..d0dff46893b1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1127,7 +1127,7 @@ WeaveSvc.prototype = { // Have each engine drop any temporary meta data for each (let engine in engines) - yield engine.resetClient(self.cb); + engine.resetClient(); // XXX Bug 480448: Delete any snapshots from old code try { From dce5167785a746b44d00b867e8408b0f3216a46b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 15:34:35 -0700 Subject: [PATCH 1145/1860] Engine_wipeClient: async -> sync. Engine__wipeClient: async/yield -> sync. --- services/sync/modules/engines.js | 6 ++---- services/sync/modules/service.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 91d92a8836be..239dc3d1e011 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -211,15 +211,13 @@ Engine.prototype = { }, _wipeClient: function Engine__wipeClient() { - let self = yield; - this.resetClient(); this._log.debug("Deleting all local data"); this._store.wipe(); }, - wipeClient: function Engine_wipeClient(onComplete) { - this._notifyAsync("wipe-client", this.name, this._wipeClient).async(this, onComplete); + wipeClient: function Engine_wipeClient() { + this._notify("wipe-client", this.name, this._wipeClient)(); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d0dff46893b1..3b52a7f5524e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1046,7 +1046,7 @@ WeaveSvc.prototype = { // Fully wipe each engine for each (let engine in engines) - yield engine.wipeClient(self.cb); + engine.wipeClient(); }; this._catchAll(this._notify("wipe-client", "", fn)).async(this, onComplete); }, From 9ace4f3c8a81b7a0701d3b4baf04eb350c5984e7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 15:34:37 -0700 Subject: [PATCH 1146/1860] Engine_wipeServer: async -> sync. SyncEngine__wipeServer: async/yield -> sync. --- services/sync/modules/engines.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 239dc3d1e011..b50ddcdb4522 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -194,10 +194,10 @@ Engine.prototype = { this._notify("sync", this.name, this._sync)(); }, - wipeServer: function Engimne_wipeServer(onComplete) { + wipeServer: function Engine_wipeServer() { if (!this._wipeServer) throw "engine does not implement _wipeServer method"; - this._notifyAsync("wipe-server", this.name, this._wipeServer).async(this, onComplete); + this._notify("wipe-server", this.name, this._wipeServer)(); }, /** @@ -520,11 +520,8 @@ SyncEngine.prototype = { }, _wipeServer: function SyncEngine__wipeServer() { - let self = yield; - let all = new Resource(this.engineURL); - yield all.delete(self.cb); - let crypto = new Resource(this.cryptoMetaURL); - yield crypto.delete(self.cb); + new Resource(this.engineURL).delete(); + new Resource(this.cryptoMetaURL).delete(); }, _resetClient: function SyncEngine__resetClient() { From 14cdd263273b076c9d71d4f37d73558057479c76 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 15:34:39 -0700 Subject: [PATCH 1147/1860] Remove Async.sugar from engines.js. --- services/sync/modules/engines.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b50ddcdb4522..687920ee7788 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -52,15 +52,12 @@ Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/collection.js"); -Function.prototype.async = Async.sugar; - // Singleton service, holds registered engines Utils.lazy(this, 'Engines', EngineManagerSvc); @@ -180,7 +177,6 @@ Engine.prototype = { catch (e) { /* ignore unset prefs */ } this._notify = Utils.notify("weave:engine:"); - this._notifyAsync = Wrap.notify("weave:engine:"); this._log = Log4Moz.repository.getLogger("Engine." + this.logName); this._log.level = Log4Moz.Level[level]; From 4f944ef1b5d3858a10c800a3c948b3109cb1d6ae Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 15:34:40 -0700 Subject: [PATCH 1148/1860] Clean up various engines to remove Async.sugar and random self/yields. --- services/sync/modules/engines/bookmarks.js | 3 --- services/sync/modules/engines/clients.js | 4 ---- services/sync/modules/engines/cookies.js | 4 ---- services/sync/modules/engines/forms.js | 9 ++------- services/sync/modules/engines/history.js | 4 ---- services/sync/modules/engines/input.js | 4 ---- services/sync/modules/engines/passwords.js | 3 --- services/sync/modules/engines/prefs.js | 3 --- services/sync/modules/engines/tabs.js | 4 ---- 9 files changed, 2 insertions(+), 36 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 21e6eac8df8b..8e3fe9781063 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -58,7 +58,6 @@ const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); @@ -67,8 +66,6 @@ Cu.import("resource://weave/notifications.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/type_records/bookmark.js"); -Function.prototype.async = Async.sugar; - function BookmarksEngine() { this._init(); } diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 818a0e4ee579..28f4d2019564 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -48,11 +48,8 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/type_records/clientData.js"); -Function.prototype.async = Async.sugar; - Utils.lazy(this, 'Clients', ClientEngine); function ClientEngine() { @@ -149,7 +146,6 @@ ClientEngine.prototype = { }, _resetClient: function ClientEngine__resetClient() { - let self = yield; this.resetLastSync(); this._store.wipe(); } diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 657560c1fab6..10a1e5a1f1e0 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -41,13 +41,10 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Function.prototype.async = Async.sugar; - function CookieEngine(pbeId) { this._init(pbeId); } @@ -264,7 +261,6 @@ CookieStore.prototype = { }, _resetGUIDs: function CookieStore__resetGUIDs() { - let self = yield; /* called in the case where remote/local sync GUIDs do not match. We do need to override this, but since we're deriving GUIDs from the cookie data itself and not generating them, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 5b8d79ddfbac..1aa7b340552b 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -43,15 +43,12 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/forms.js"); -Function.prototype.async = Async.sugar; - function FormEngine() { this._init(); } @@ -65,16 +62,14 @@ FormEngine.prototype = { _recordObj: FormRec, _syncStartup: function FormEngine__syncStartup() { - let self = yield; this._store.cacheFormItems(); - yield SyncEngine.prototype._syncStartup.async(this, self.cb); + SyncEngine.prototype._syncStartup(); }, /* Wipe cache when sync finishes */ _syncFinish: function FormEngine__syncFinish() { - let self = yield; this._store.clearFormCache(); - yield SyncEngine.prototype._syncFinish.async(this, self.cb); + SyncEngine.prototype._syncFinish(); }, _recordLike: function SyncEngine__recordLike(a, b) { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index e869db09a222..dd43989dfcab 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -47,12 +47,9 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/collection.js"); Cu.import("resource://weave/type_records/history.js"); -Function.prototype.async = Async.sugar; - function HistoryEngine() { this._init(); } @@ -94,7 +91,6 @@ HistoryEngine.prototype = { }, _syncFinish: function HistEngine__syncFinish(error) { - let self = yield; this._log.debug("Finishing up sync"); this._tracker.resetScore(); diff --git a/services/sync/modules/engines/input.js b/services/sync/modules/engines/input.js index 0d3282919445..dbda8fcd8e17 100644 --- a/services/sync/modules/engines/input.js +++ b/services/sync/modules/engines/input.js @@ -41,14 +41,11 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Function.prototype.async = Async.sugar; - function InputEngine(pbeId) { this._init(pbeId); } @@ -207,7 +204,6 @@ InputStore.prototype = { }, _resetGUIDs: function InputStore__resetGUIDs() { - let self = yield; // Not needed. } }; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index c0b1d6839939..906862051ce6 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -46,12 +46,9 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/type_records/passwords.js"); -Function.prototype.async = Async.sugar; - function PasswordEngine() { this._init(); } diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 3c2165e71bad..22a900cdf574 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -46,15 +46,12 @@ const WEAVE_PREFS_GUID = "preferences"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/prefs.js"); -Function.prototype.async = Async.sugar; - function PrefsEngine() { this._init(); } diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index f5d5ddabd5fd..d7010eaf1a7a 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -45,7 +45,6 @@ const TAB_TIME_ATTR = "weave.tabEngine.lastUsed.timeStamp"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); @@ -53,8 +52,6 @@ Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/type_records/tabs.js"); Cu.import("resource://weave/engines/clientData.js"); -Function.prototype.async = Async.sugar; - function TabEngine() { this._init(); } @@ -77,7 +74,6 @@ TabEngine.prototype = { }, _resetClient: function TabEngine__resetClient() { - let self = yield; this.resetLastSync(); this._store.wipe(); }, From ee57c40047bacaa3d97ff93c9db6efa38e20e6c3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 18:36:31 -0700 Subject: [PATCH 1149/1860] Fix form engine to correctly pass in the engine. --- services/sync/modules/engines/forms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 1aa7b340552b..71ed1054ffcc 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -63,13 +63,13 @@ FormEngine.prototype = { _syncStartup: function FormEngine__syncStartup() { this._store.cacheFormItems(); - SyncEngine.prototype._syncStartup(); + SyncEngine.prototype._syncStartup.call(this); }, /* Wipe cache when sync finishes */ _syncFinish: function FormEngine__syncFinish() { this._store.clearFormCache(); - SyncEngine.prototype._syncFinish(); + SyncEngine.prototype._syncFinish.call(this); }, _recordLike: function SyncEngine__recordLike(a, b) { From 7a192fc662c48bc09afe6b7361669b04e7bf8484 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:14 -0700 Subject: [PATCH 1150/1860] Add a Utils.catch as a sync. version of Wrap.catchAll. --- services/sync/modules/service.js | 24 +++++++++++++----------- services/sync/modules/util.js | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3b52a7f5524e..6f678468b5e5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -147,11 +147,13 @@ StatusRecord.prototype = { */ function WeaveSvc() { - this._notify = Wrap.notify("weave:service:"); + this._notify = Utils.notify("weave:service:"); + this._notifyAsync = Wrap.notify("weave:service:"); } WeaveSvc.prototype = { _localLock: Wrap.localLock, + _catch: Utils.catch, _catchAll: Wrap.catchAll, _isQuitting: false, _loggedIn: false, @@ -516,7 +518,7 @@ WeaveSvc.prototype = { //JSON.parse(res.data); // throws if not json self.done(true); }; - this._catchAll(this._notify("verify-login", "", fn)).async(this, onComplete); + this._catchAll(this._notifyAsync("verify-login", "", fn)).async(this, onComplete); }, verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, @@ -539,7 +541,7 @@ WeaveSvc.prototype = { }; this._catchAll( this._localLock( - this._notify("verify-passphrase", "", fn))).async(this, onComplete); + this._notifyAsync("verify-passphrase", "", fn))).async(this, onComplete); }, login: function WeaveSvc_login(onComplete, username, password, passphrase) { @@ -581,7 +583,7 @@ WeaveSvc.prototype = { self.done(true); }; - this._catchAll(this._localLock(this._notify("login", "", fn))). + this._catchAll(this._localLock(this._notifyAsync("login", "", fn))). async(this, onComplete); }, @@ -948,7 +950,7 @@ WeaveSvc.prototype = { */ sync: function WeaveSvc_sync(onComplete, fullSync) { fullSync = true; // not doing thresholds yet - this._catchAll(this._notify("sync", "", this._localLock(this._sync))). + this._catchAll(this._notifyAsync("sync", "", this._localLock(this._sync))). async(this, onComplete, fullSync); }, @@ -1018,7 +1020,7 @@ WeaveSvc.prototype = { } } }; - this._catchAll(this._notify("wipe-server", "", fn)).async(this, onComplete); + this._catchAll(this._notifyAsync("wipe-server", "", fn)).async(this, onComplete); }, /** @@ -1048,7 +1050,7 @@ WeaveSvc.prototype = { for each (let engine in engines) engine.wipeClient(); }; - this._catchAll(this._notify("wipe-client", "", fn)).async(this, onComplete); + this._catchAll(this._notifyAsync("wipe-client", "", fn)).async(this, onComplete); }, /** @@ -1076,7 +1078,7 @@ WeaveSvc.prototype = { // Tell the remote machines to wipe themselves this.prepCommand("wipeAll", []); }; - this._catchAll(this._notify("wipe-remote", "", fn)).async(this, onComplete); + this._catchAll(this._notifyAsync("wipe-remote", "", fn)).async(this, onComplete); }, /** @@ -1099,7 +1101,7 @@ WeaveSvc.prototype = { for each (let cache in [PubKeys, PrivKeys, CryptoMetas, Records]) cache.clearCache(); }; - this._catchAll(this._notify("reset-service", "", fn)).async(this, onComplete); + this._catchAll(this._notifyAsync("reset-service", "", fn)).async(this, onComplete); }, /** @@ -1141,7 +1143,7 @@ WeaveSvc.prototype = { this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); } }; - this._catchAll(this._notify("reset-client", "", fn)).async(this, onComplete); + this._catchAll(this._notifyAsync("reset-client", "", fn)).async(this, onComplete); }, /** @@ -1206,7 +1208,7 @@ WeaveSvc.prototype = { self.done(true); }; - this._notify("process-commands", "", fn).async(this, onComplete); + this._notifyAsync("process-commands", "", fn).async(this, onComplete); }, /** diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index d7619307f569..6d2f9dd1253f 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -52,6 +52,25 @@ Cu.import("resource://weave/log4moz.js"); */ let Utils = { + /** + * Wrap a function to catch all exceptions and log them + * + * @usage MyObj._catch = Utils.catch; + * MyObj.foo = function() { this._catch(func)(); } + */ + catch: function Utils_catch(func) { + let thisArg = this; + return function WrappedCatch() { + try { + return func.call(thisArg); + } + catch(ex) { + thisArg._log.debug(["Caught exception:", Utils.exceptionStr(ex), + Utils.stackTrace(ex)].join(" ").replace(/\n/g, " ")); + } + }; + }, + /** * Wrap functions to notify when it starts and finishes executing or if it got * an error. The message is a combination of a provided prefix and local name From 117e220d076fe2c557befb6c3b84ae7b10b389cf Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:16 -0700 Subject: [PATCH 1151/1860] WeaveSvc_verifyLogin: async + async/yield -> sync. --- services/sync/modules/service.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6f678468b5e5..955dd192f2d3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -493,9 +493,8 @@ WeaveSvc.prototype = { fn.async(this, onComplete); }, - verifyLogin: function WeaveSvc_verifyLogin(onComplete, username, password, isLogin) { - let fn = function WeaveSvc__verifyLogin() { - let self = yield; + verifyLogin: function WeaveSvc_verifyLogin(username, password, isLogin) + this._catch(this._notify("verify-login", "", function() { this._log.debug("Verifying login for user " + username); let url = this.findCluster(username); @@ -516,10 +515,8 @@ WeaveSvc.prototype = { res.get(); //JSON.parse(res.data); // throws if not json - self.done(true); - }; - this._catchAll(this._notifyAsync("verify-login", "", fn)).async(this, onComplete); - }, + return true; + }))(), verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, password, passphrase) { @@ -572,7 +569,7 @@ WeaveSvc.prototype = { this._log.debug("Logging in user " + this.username); - if (!(yield this.verifyLogin(self.cb, this.username, this.password, true))) { + if (!(this.verifyLogin(this.username, this.password, true))) { this._setSyncFailure(LOGIN_FAILED_REJECTED); throw "Login failed"; } From ec5632e4234425ac6c5ec6be112879c0d5524d0b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:18 -0700 Subject: [PATCH 1152/1860] WeaveSvc_resetService: async + async/yield -> sync. --- services/sync/modules/service.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 955dd192f2d3..bf0b3d66e0e3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1035,7 +1035,7 @@ WeaveSvc.prototype = { // If we don't have any engines, reset the service and wipe all engines if (!engines) { // Clear out any service data - yield this.resetService(self.cb); + this.resetService(); engines = [Clients].concat(Engines.getAll()); } @@ -1080,14 +1080,9 @@ WeaveSvc.prototype = { /** * Reset local service information like logs, sync times, caches. - * - * @param onComplete - * Callback when this method completes */ - resetService: function WeaveSvc__resetService(onComplete) { - let fn = function WeaveSvc__resetService() { - let self = yield; - + resetService: function WeaveSvc_resetService() + this._catch(this._notify("reset-service", "", function() { // First drop old logs to track client resetting behavior this.clearLogs(); this._log.info("Logs reinitialized for service reset"); @@ -1097,9 +1092,7 @@ WeaveSvc.prototype = { Svc.Prefs.reset("lastSync"); for each (let cache in [PubKeys, PrivKeys, CryptoMetas, Records]) cache.clearCache(); - }; - this._catchAll(this._notifyAsync("reset-service", "", fn)).async(this, onComplete); - }, + }))(), /** * Reset the client by getting rid of any local server data and client data. @@ -1116,7 +1109,7 @@ WeaveSvc.prototype = { // If we don't have any engines, reset everything including the service if (!engines) { // Clear out any service data - yield this.resetService(self.cb); + this.resetService(); engines = [Clients].concat(Engines.getAll()); } From 26623815b66add4af5c93a55233a017bb0597e5e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:20 -0700 Subject: [PATCH 1153/1860] WeaveSvc_wipeClient: async + async/yield -> sync. --- services/sync/modules/service.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bf0b3d66e0e3..663f40ba6527 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1023,15 +1023,11 @@ WeaveSvc.prototype = { /** * Wipe all local user data. * - * @param onComplete - * Callback when this method completes * @param engines [optional] * Array of engine names to wipe. If not given, all engines are used. */ - wipeClient: function WeaveSvc_wipeClient(onComplete, engines) { - let fn = function WeaveSvc__wipeClient() { - let self = yield; - + wipeClient: function WeaveSvc_wipeClient(engines) + this._catch(this._notify("wipe-client", "", function() { // If we don't have any engines, reset the service and wipe all engines if (!engines) { // Clear out any service data @@ -1046,9 +1042,7 @@ WeaveSvc.prototype = { // Fully wipe each engine for each (let engine in engines) engine.wipeClient(); - }; - this._catchAll(this._notifyAsync("wipe-client", "", fn)).async(this, onComplete); - }, + }))(), /** * Wipe all remote user data by wiping the server then telling each remote @@ -1187,7 +1181,7 @@ WeaveSvc.prototype = { engines = null; // Fallthrough case "wipeEngine": - yield this.wipeClient(self.cb, engines); + this.wipeClient(engines); break; default: From 09205bade11cbef0d80d6ddc04885d3a21780317 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:22 -0700 Subject: [PATCH 1154/1860] WeaveSvc_resetClient: async + async/yield -> sync. --- services/sync/modules/service.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 663f40ba6527..74ddeeb61ca7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -699,7 +699,7 @@ WeaveSvc.prototype = { } else if (meta.payload.syncID != Clients.syncID) { this._log.warn("Meta.payload.syncID is " + meta.payload.syncID + ", Clients.syncID is " + Clients.syncID); - yield this.resetClient(self.cb); + this.resetClient(); this._log.info("Reset client because of syncID mismatch."); Clients.syncID = meta.payload.syncID; this._log.info("Reset the client after a server/client sync ID mismatch"); @@ -971,7 +971,7 @@ WeaveSvc.prototype = { _freshStart: function WeaveSvc__freshStart() { let self = yield; - yield this.resetClient(self.cb); + this.resetClient(); this._log.info("Reset client data from freshStart."); this._log.info("Client metadata wiped, deleting server data"); yield this.wipeServer(self.cb); @@ -1091,15 +1091,11 @@ WeaveSvc.prototype = { /** * Reset the client by getting rid of any local server data and client data. * - * @param onComplete - * Callback when this method completes * @param engines [optional] * Array of engine names to reset. If not given, all engines are used. */ - resetClient: function WeaveSvc_resetClient(onComplete, engines) { - let fn = function WeaveSvc__resetClient() { - let self = yield; - + resetClient: function WeaveSvc_resetClient(engines) + this._catch(this._notify("reset-client", "", function() { // If we don't have any engines, reset everything including the service if (!engines) { // Clear out any service data @@ -1126,9 +1122,7 @@ WeaveSvc.prototype = { } catch (e) { this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); } - }; - this._catchAll(this._notifyAsync("reset-client", "", fn)).async(this, onComplete); - }, + }))(), /** * A hash of valid commands that the client knows about. The key is a command @@ -1174,7 +1168,7 @@ WeaveSvc.prototype = { engines = null; // Fallthrough case "resetEngine": - yield this.resetClient(self.cb, engines); + this.resetClient(engines); break; case "wipeAll": From 1bb63265033508c602b4c581531e02cbd7454340 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:24 -0700 Subject: [PATCH 1155/1860] WeaveSvc_wipeRemote: async + async/yield -> sync. --- services/sync/modules/service.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 74ddeeb61ca7..9b4d7c7d6626 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1048,17 +1048,13 @@ WeaveSvc.prototype = { * Wipe all remote user data by wiping the server then telling each remote * client to wipe itself. * - * @param onComplete - * Callback when this method completes * @param engines [optional] * Array of engine names to wipe. If not given, all engines are used. */ - wipeRemote: function WeaveSvc_wipeRemote(onComplete, engines) { - let fn = function WeaveSvc__wipeRemote() { - let self = yield; - + wipeRemote: function WeaveSvc_wipeRemote(engines) + this._catch(this._notify("wipe-remote", "", function() { // Clear out any server data - //yield this.wipeServer(self.cb, engines); + //this.wipeServer(engines); // Only wipe the engines provided if (engines) { @@ -1068,9 +1064,7 @@ WeaveSvc.prototype = { // Tell the remote machines to wipe themselves this.prepCommand("wipeAll", []); - }; - this._catchAll(this._notifyAsync("wipe-remote", "", fn)).async(this, onComplete); - }, + }))(), /** * Reset local service information like logs, sync times, caches. From 684d8fa43a6e655d657d87063d6f446c878bca8c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:25 -0700 Subject: [PATCH 1156/1860] WeaveSvc_processCommands: async + async/yield -> sync. --- services/sync/modules/service.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9b4d7c7d6626..611b6c1e2e0b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -863,7 +863,7 @@ WeaveSvc.prototype = { // Process the incoming commands if we have any if (Clients.getClients()[Clients.clientID].commands) { try { - if (!(yield this.processCommands(self.cb))) { + if (!(this.processCommands())) { this._detailedStatus.setSyncStatus(ABORT_SYNC_COMMAND); throw "aborting sync, process commands said so"; } @@ -1138,13 +1138,10 @@ WeaveSvc.prototype = { /** * Check if the local client has any remote commands and perform them. * - * @param onComplete - * Callback when this method completes * @return False to abort sync */ - processCommands: function WeaveSvc_processCommands(onComplete) { - let fn = function WeaveSvc__processCommands() { - let self = yield; + processCommands: function WeaveSvc_processCommands() + this._notify("process-commands", "", function() { let info = Clients.getInfo(Clients.clientID); let commands = info.commands; @@ -1178,10 +1175,8 @@ WeaveSvc.prototype = { } } - self.done(true); - }; - this._notifyAsync("process-commands", "", fn).async(this, onComplete); - }, + return true; + })(), /** * Prepare to send a command to each remote client. Calling this doesn't From 30e95c1fcfc2249cc1783324e00c48ca69fcee6b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:27 -0700 Subject: [PATCH 1157/1860] WeaveSvc_wipeServer: async + async/yield -> sync. Fix up chrome uses of wipeServer, resetClient, wipeRemote, wipeClient. --- services/sync/modules/service.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 611b6c1e2e0b..df7bc3eee046 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -974,7 +974,7 @@ WeaveSvc.prototype = { this.resetClient(); this._log.info("Reset client data from freshStart."); this._log.info("Client metadata wiped, deleting server data"); - yield this.wipeServer(self.cb); + this.wipeServer(); this._log.debug("Uploading new metadata record"); meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); @@ -988,15 +988,11 @@ WeaveSvc.prototype = { /** * Wipe all user data from the server. * - * @param onComplete - * Callback when this method completes * @param engines [optional] * Array of engine names to wipe. If not given, all engines are used. */ - wipeServer: function WeaveSvc_wipeServer(onComplete, engines) { - let fn = function WeaveSvc__wipeServer() { - let self = yield; - + wipeServer: function WeaveSvc_wipeServer(engines) + this._catch(this._notify("wipe-server", "", function() { // Grab all the collections for the user let userURL = this.clusterURL + this.username + "/"; let res = new Resource(userURL); @@ -1016,9 +1012,7 @@ WeaveSvc.prototype = { this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); } } - }; - this._catchAll(this._notifyAsync("wipe-server", "", fn)).async(this, onComplete); - }, + }))(), /** * Wipe all local user data. From 38c835b6bbb79bf1af93e5eb46cf7ffee40e9fa6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:29 -0700 Subject: [PATCH 1158/1860] WeaveSvc__freshStart: async/yield -> sync. --- services/sync/modules/service.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index df7bc3eee046..8f93b31f0abc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -680,7 +680,7 @@ WeaveSvc.prototype = { } reset = true; this._log.info("Wiping server data"); - yield this._freshStart.async(this, self.cb); + this._freshStart(); if (status == 404) this._log.info("Metadata record not found, server wiped to ensure " + @@ -748,7 +748,7 @@ WeaveSvc.prototype = { if (!reset) { this._log.warn("Calling freshStart from !reset case."); - yield this._freshStart.async(this, self.cb); + this._freshStart(); this._log.info("Server data wiped to ensure consistency due to missing keys"); } @@ -970,7 +970,6 @@ WeaveSvc.prototype = { }, _freshStart: function WeaveSvc__freshStart() { - let self = yield; this.resetClient(); this._log.info("Reset client data from freshStart."); this._log.info("Client metadata wiped, deleting server data"); From caf413d05ea6f6cc8a79503e2c39a11a20c8d7e9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:21:34 -0700 Subject: [PATCH 1159/1860] WeaveSvc__syncEngine: async/yield -> sync. --- services/sync/modules/service.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8f93b31f0abc..b1662a4273a6 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -913,7 +913,7 @@ WeaveSvc.prototype = { } // If there's any problems with syncing the engine, report the failure - if (!(yield this._syncEngine.async(this, self.cb, engine))) { + if (!(this._syncEngine(engine))) { this._log.info("Aborting sync"); break; } @@ -954,18 +954,17 @@ WeaveSvc.prototype = { // returns true if sync should proceed // false / no return value means sync should be aborted _syncEngine: function WeaveSvc__syncEngine(engine) { - let self = yield; try { engine.sync(); if (!this.cancelRequested) - self.done(true); + return true; } catch(e) { this._syncError = true; this._weaveStatusCode = WEAVE_STATUS_PARTIAL; this._detailedStatus.setEngineStatus(engine.name, e); if (FaultTolerance.Service.onException(e)) - self.done(true); + return true; } }, From 053d45e464ea8e830245b6a9e350ec2716762ba0 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Jun 2009 22:33:08 -0700 Subject: [PATCH 1160/1860] Identity_getPassword: async + async/yield -> sync. Make onGetPassword take one cb to pass back the password. --- services/sync/modules/identity.js | 27 +++++++-------------------- services/sync/modules/service.js | 2 +- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index e5f15f2bf0a0..2da038bb5455 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -42,11 +42,9 @@ const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; Utils.lazy(this, 'ID', IDManager); @@ -136,26 +134,15 @@ Identity.prototype = { }, // we'll call this if set to call out to the ui to prompt the user - // note:the ui is expected to set the password/setTempPassword - // note2: callback must be an async.js style generator function + // This function takes a callback function to pass back the password onGetPassword: null, // Attempts to get the password from the user if not set - _getPassword: function Id__getPassword() { - let self = yield; - let pass; - + getPassword: function Identity_getPassword() { if (this.password) - pass = this.password; - else { - if (this.onGetPassword) { - yield Async.run(this, this.onGetPassword, self.cb, this); - pass = this.password; // retry after ui callback - } - } - self.done(pass); - }, - getPassword: function Id_getPassword(onComplete) { - this._getPassword.async(this, onComplete); + return this.password; + + if (typeof this.onGetPassword == "function") + return this.password = Sync(this.onGetPassword)(); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b1662a4273a6..896f040acbd6 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -752,7 +752,7 @@ WeaveSvc.prototype = { this._log.info("Server data wiped to ensure consistency due to missing keys"); } - let pass = yield ID.get('WeaveCryptoID').getPassword(self.cb); + let pass = ID.get("WeaveCryptoID").getPassword(); if (pass) { let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, PrivKeys.defaultKeyUri); From e78d7a7d5bbecdc13ef62362fd48caf51eeeba9f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 11:52:49 -0700 Subject: [PATCH 1161/1860] Add a Utils.lock as a sync. version of Wrap.localLock. --- services/sync/modules/service.js | 1 + services/sync/modules/util.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 896f040acbd6..047c0e799a4f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -152,6 +152,7 @@ function WeaveSvc() { } WeaveSvc.prototype = { + _lock: Utils.lock, _localLock: Wrap.localLock, _catch: Utils.catch, _catchAll: Wrap.catchAll, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 6d2f9dd1253f..f2ea9f45bade 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -71,6 +71,27 @@ let Utils = { }; }, + /** + * Wrap a function to call lock before calling the function then unlock. + * + * @usage MyObj._lock = Utils.lock; + * MyObj.foo = function() { this._lock(func)(); } + */ + lock: function Utils_lock(func) { + let thisArg = this; + return function WrappedLock() { + if (!thisArg.lock()) + throw "Could not acquire lock"; + + try { + return func.call(thisArg); + } + finally { + thisArg.unlock(); + } + }; + }, + /** * Wrap functions to notify when it starts and finishes executing or if it got * an error. The message is a combination of a provided prefix and local name From e28891094985a09e061e329dc26fa9cb8380fa33 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 11:52:54 -0700 Subject: [PATCH 1162/1860] WeaveSvc_login: async + async/yield -> sync. Fix up chrome code. --- services/sync/modules/service.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 047c0e799a4f..09bc86c599b5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -335,7 +335,7 @@ WeaveSvc.prototype = { if (Svc.Prefs.get("autoconnect") && this.username) { try { - if (yield this.login(self.cb)) + if (this.login()) yield this.sync(self.cb, true); } catch (e) {} } @@ -542,21 +542,17 @@ WeaveSvc.prototype = { this._notifyAsync("verify-passphrase", "", fn))).async(this, onComplete); }, - login: function WeaveSvc_login(onComplete, username, password, passphrase) { - let user = username, pass = password, passp = passphrase; - - let fn = function WeaveSvc__login() { - let self = yield; - + login: function WeaveSvc_login(username, password, passphrase) + this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; this._detailedStatus = new StatusRecord(); - if (typeof(user) != 'undefined') - this.username = user; - if (typeof(pass) != 'undefined') - ID.get('WeaveID').setTempPassword(pass); - if (typeof(passp) != 'undefined') - ID.get('WeaveCryptoID').setTempPassword(passp); + if (typeof(username) != "undefined") + this.username = username; + if (typeof(password) != "undefined") + ID.get("WeaveID").setTempPassword(password); + if (typeof(passphrase) != "undefined") + ID.get("WeaveCryptoID").setTempPassword(passphrase); if (!this.username) { this._setSyncFailure(LOGIN_FAILED_NO_USERNAME); @@ -579,11 +575,8 @@ WeaveSvc.prototype = { this._loggedIn = true; this._checkSync(); - self.done(true); - }; - this._catchAll(this._localLock(this._notifyAsync("login", "", fn))). - async(this, onComplete); - }, + return true; + })))(), logout: function WeaveSvc_logout() { this._log.info("Logging out"); From ddfca49b5e67cd2176f6f19009aa71aa49728488 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 11:53:23 -0700 Subject: [PATCH 1163/1860] WeaveSvc__remoteSetup: async/yield -> sync. --- services/sync/modules/service.js | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 09bc86c599b5..5b969ae0a58a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -632,8 +632,6 @@ WeaveSvc.prototype = { // stuff we need to to after login, before we can really do // anything (e.g. key setup) _remoteSetup: function WeaveSvc__remoteSetup() { - let self = yield; - let ret = false; // false to abort sync let reset = false; this._log.debug("Fetching global metadata record"); @@ -654,8 +652,7 @@ WeaveSvc.prototype = { this._setSyncFailure(METARECORD_DOWNLOAD_FAIL); this._log.warn("Unknown error while downloading metadata record. " + "Aborting sync."); - self.done(false); - return; + return false; } if (!meta) @@ -669,8 +666,7 @@ WeaveSvc.prototype = { this._log.info("...and key generation is disabled. Not wiping. " + "Aborting sync."); this._setSyncFailure(DESKTOP_VERSION_OUT_OF_DATE); - self.done(false); - return; + return false; } reset = true; this._log.info("Wiping server data"); @@ -687,8 +683,7 @@ WeaveSvc.prototype = { this._setSyncFailure(VERSION_OUT_OF_DATE); this._log.warn("Server data is of a newer Weave version, this client " + "needs to be upgraded. Aborting sync."); - self.done(false); - return; + return false; } else if (meta.payload.syncID != Clients.syncID) { this._log.warn("Meta.payload.syncID is " + meta.payload.syncID + @@ -712,10 +707,8 @@ WeaveSvc.prototype = { this._log.debug("Could not get private key"); else if (privkey.keyData == null) this._log.debug("Private key has no key data"); - else { - needKeys = false; - ret = true; - } + else + return true; } if (needKeys) { @@ -728,16 +721,14 @@ WeaveSvc.prototype = { this._log.debug("PrivKey HTTP response status: " + PrivKeys.lastResource.lastChannel.responseStatus); this._setSyncFailure(KEYS_DOWNLOAD_FAIL); - self.done(false); - return; + return false; } if (!this._keyGenEnabled) { this._log.warn("Couldn't download keys from server, and key generation" + "is disabled. Aborting sync"); this._setSyncFailure(NO_KEYS_NO_KEYGEN); - self.done(false); - return; + return false; } if (!reset) { @@ -752,7 +743,7 @@ WeaveSvc.prototype = { PrivKeys.defaultKeyUri); try { PubKeys.uploadKeypair(keys); - ret = true; + return true; } catch (e) { this._setSyncFailure(KEYS_UPLOAD_FAIL); this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); @@ -765,7 +756,7 @@ WeaveSvc.prototype = { } } - self.done(ret); + return false; }, /** @@ -847,9 +838,8 @@ WeaveSvc.prototype = { throw reason; } - if (!(yield this._remoteSetup.async(this, self.cb))) { + if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; - } this._log.debug("Refreshing client list"); Clients.sync(); @@ -863,7 +853,7 @@ WeaveSvc.prototype = { } // Repeat remoteSetup in-case the commands forced us to reset - if (!(yield this._remoteSetup.async(this, self.cb))) + if (!(this._remoteSetup())) throw "aborting sync, remote setup failed after processing commands"; } finally { From 36494b2409db7c0140dfd92b228aeac1d4c50eed Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 18:23:58 -0500 Subject: [PATCH 1164/1860] WeaveSvc_sync: async + async/yield -> sync. Fix chrome callers to setTimeout(sync) to async run sync. --- services/sync/modules/service.js | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5b969ae0a58a..507aabb905d8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -336,7 +336,7 @@ WeaveSvc.prototype = { if (Svc.Prefs.get("autoconnect") && this.username) { try { if (this.login()) - yield this.sync(self.cb, true); + this.sync(true); } catch (e) {} } self.done(); @@ -803,7 +803,7 @@ WeaveSvc.prototype = { if (this.locked) this._log.debug("Skipping scheduled sync: already locked for sync"); else - this.sync(null, false); + this.sync(false); })); this._syncTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK); @@ -818,10 +818,10 @@ WeaveSvc.prototype = { * * @param fullSync * True to unconditionally sync all engines - * @throw Reason for not syncing */ - _sync: function WeaveSvc__sync(fullSync) { - let self = yield; + sync: function WeaveSvc_sync(fullSync) + this._catch(this._lock(this._notify("sync", "", function() { + fullSync = true; // not doing thresholds yet // Use thresholds to determine what to sync only if it's not a full sync let useThresh = !fullSync; @@ -919,21 +919,7 @@ WeaveSvc.prototype = { this.cancelRequested = false; this._syncError = false; } - }, - - /** - * Do a synchronized sync (only one sync at a time). - * - * @param onComplete - * Callback when this method completes - * @param fullSync - * True to unconditionally sync all engines - */ - sync: function WeaveSvc_sync(onComplete, fullSync) { - fullSync = true; // not doing thresholds yet - this._catchAll(this._notifyAsync("sync", "", this._localLock(this._sync))). - async(this, onComplete, fullSync); - }, + })))(), // returns true if sync should proceed // false / no return value means sync should be aborted From 3196861bae5ee72e8660bd5d0a0712066f97e35f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 18:24:06 -0500 Subject: [PATCH 1165/1860] WeaveSvc_onStartup: async + async/yield -> sync. Convert chrome callers from using async callback to just run after. --- services/sync/modules/service.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 507aabb905d8..0f42f497504e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -292,8 +292,7 @@ WeaveSvc.prototype = { // one-time initialization like setting up observers and the like // xxx we might need to split some of this out into something we can call // again when username/server/etc changes - _onStartup: function WeaveSvc__onStartup() { - let self = yield; + onStartup: function WeaveSvc_onStartup() { this._initLogs(); this._log.info("Weave " + WEAVE_VERSION + " initializing"); this._registerEngines(); @@ -339,10 +338,6 @@ WeaveSvc.prototype = { this.sync(true); } catch (e) {} } - self.done(); - }, - onStartup: function WeaveSvc_onStartup(callback) { - this._onStartup.async(this, callback); }, _initLogs: function WeaveSvc__initLogs() { From d7d5ae42262faa720de62295e7c4ebcda340ae0b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 18:24:10 -0500 Subject: [PATCH 1166/1860] WeaveSvc_verifyPassphrase: async + async/yield -> sync. Fix chrome to setTimeout(verifyPassphrase). --- services/sync/modules/service.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0f42f497504e..ed04720a69ee 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -514,28 +514,21 @@ WeaveSvc.prototype = { return true; }))(), - verifyPassphrase: function WeaveSvc_verifyPassphrase(onComplete, username, - password, passphrase) { - let fn = function WeaveSvc__verifyPassphrase() { - let self = yield; - + verifyPassphrase: function WeaveSvc_verifyPassphrase(username, password, passphrase) + this._catch(this._lock(this._notify("verify-passphrase", "", function() { this._log.debug("Verifying passphrase"); this.username = username; - ID.get('WeaveID').setTempPassword(password); + ID.get("WeaveID").setTempPassword(password); - let id = new Identity('Passphrase Verification', username); + let id = new Identity("Passphrase Verification", username); id.setTempPassword(passphrase); - let pubkey = yield PubKeys.getDefaultKey(self.cb); - let privkey = yield PrivKeys.get(self.cb, pubkey.PrivKeyUri); + let pubkey = PubKeys.getDefaultKey(); + let privkey = PrivKeys.get(pubkey.PrivKeyUri); // fixme: decrypt something here - }; - this._catchAll( - this._localLock( - this._notifyAsync("verify-passphrase", "", fn))).async(this, onComplete); - }, + }))), login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { From 27a82242c4181ccd7d6742bef6a263f338f7a1ba Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 18:24:14 -0500 Subject: [PATCH 1167/1860] WeaveSvc_setCluster: async + async/yield -> sync. (Unused?) --- services/sync/modules/service.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ed04720a69ee..f1d998189c6e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -471,22 +471,16 @@ WeaveSvc.prototype = { }, // gets cluster from central LDAP server and sets this.clusterURL - setCluster: function WeaveSvc_setCluster(onComplete, username) { - let fn = function WeaveSvc__setCluster() { - let self = yield; - let ret = false; + setCluster: function WeaveSvc_setCluster(username) { + let cluster = this.findCluster(username); + if (cluster) { + this._log.debug("Saving cluster setting"); + this.clusterURL = cluster; + return true; + } - let cluster = yield this.findCluster(self.cb, username); - if (cluster) { - this._log.debug("Saving cluster setting"); - this.clusterURL = cluster; - ret = true; - } else - this._log.debug("Error setting cluster for user " + username); - - self.done(ret); - }; - fn.async(this, onComplete); + this._log.debug("Error setting cluster for user " + username); + return false; }, verifyLogin: function WeaveSvc_verifyLogin(username, password, isLogin) From d5899148af83b486d8b9518ded7945321ad0fdfc Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 18:24:17 -0500 Subject: [PATCH 1168/1860] WeaveSvc_createAccount: async + async/yield -> sync. Fix chrome to call its onComplete with the return of createAccount. --- services/sync/modules/service.js | 53 +++++++++++++++----------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f1d998189c6e..fed554f44b67 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -573,42 +573,37 @@ WeaveSvc.prototype = { Svc.Observer.notifyObservers(null, "weave:service:logout:finish", ""); }, - createAccount: function WeaveSvc_createAccount(onComplete, username, password, email, + createAccount: function WeaveSvc_createAccount(username, password, email, captchaChallenge, captchaResponse) { - let fn = function WeaveSvc__createAccount() { - let self = yield; + function enc(x) encodeURIComponent(x); + let message = "uid=" + enc(username) + "&password=" + enc(password) + + "&mail=" + enc(email) + "&recaptcha_challenge_field=" + + enc(captchaChallenge) + "&recaptcha_response_field=" + enc(captchaResponse); - function enc(x) encodeURIComponent(x); - let message = "uid=" + enc(username) + "&password=" + enc(password) + - "&mail=" + enc(email) + - "&recaptcha_challenge_field=" + enc(captchaChallenge) + - "&recaptcha_response_field=" + enc(captchaResponse); + let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/new'; + let res = new Weave.Resource(url); + res.authenticator = new Weave.NoOpAuthenticator(); + res.setHeader("Content-Type", "application/x-www-form-urlencoded", + "Content-Length", message.length); - let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/new'; - let res = new Weave.Resource(url); - res.authenticator = new Weave.NoOpAuthenticator(); - res.setHeader("Content-Type", "application/x-www-form-urlencoded", - "Content-Length", message.length); + // fixme: Resource throws on error - it really shouldn't :-/ + let resp; + try { + resp = res.post(message); + } + catch(ex) { + this._log.trace("Create account error: " + ex); + } - // fixme: Resource throws on error - it really shouldn't :-/ - let resp; - try { - resp = yield res.post(self.cb, message); - } catch (e) { - this._log.trace("Create account error: " + e); - } - - if (res.lastChannel.responseStatus != 200 && - res.lastChannel.responseStatus != 201) - this._log.info("Failed to create account. " + + if (res.lastChannel.responseStatus != 200 && + res.lastChannel.responseStatus != 201) + this._log.info("Failed to create account. " + "status: " + res.lastChannel.responseStatus + ", " + "response: " + resp); - else - this._log.info("Account created: " + resp); + else + this._log.info("Account created: " + resp); - self.done(res.lastChannel.responseStatus); - }; - fn.async(this, onComplete); + return res.lastChannel.responseStatus; }, // stuff we need to to after login, before we can really do From 00dd0e2443a8500186d1a4a9103c977b9b784c4f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 18:24:23 -0500 Subject: [PATCH 1169/1860] CryptoMeta_addKey: async + async/yield -> sync. (Unused?) --- services/sync/modules/base_records/crypto.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 2ee4319d4423..0f70d3b10f3f 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -157,13 +157,9 @@ CryptoMeta.prototype = { privkey.keyData, passphrase, privkey.salt, privkey.iv); }, - _addKey: function CryptoMeta__addKey(new_pubkey, privkey, passphrase) { - let self = yield; - let symkey = yield this.getKey(self.cb, privkey, passphrase); - yield this.addUnwrappedKey(self.cb, new_pubkey, symkey); - }, - addKey: function CryptoMeta_addKey(onComplete, new_pubkey, privkey, passphrase) { - this._addKey.async(this, onComplete, new_pubkey, privkey, passphrase); + addKey: function CryptoMeta_addKey(new_pubkey, privkey, passphrase) { + let symkey = this.getKey(privkey, passphrase); + this.addUnwrappedKey(new_pubkey, symkey); }, addUnwrappedKey: function CryptoMeta_addUnwrappedKey(new_pubkey, symkey) { From afe32be2dcc9e2256b9464dd8d29d426c697d4d6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 6 Jun 2009 18:24:30 -0500 Subject: [PATCH 1170/1860] Remove async.js and wrap.js and remove remaining references to Async.sugar, etc. --- services/sync/modules/async.js | 350 ------------------ .../sync/modules/base_records/collection.js | 3 - services/sync/modules/base_records/crypto.js | 3 - services/sync/modules/engines.js | 1 - services/sync/modules/service.js | 8 - services/sync/modules/type_records/forms.js | 3 - services/sync/modules/type_records/history.js | 3 - .../sync/modules/type_records/passwords.js | 3 - services/sync/modules/type_records/prefs.js | 3 - services/sync/modules/type_records/tabs.js | 3 - services/sync/modules/util.js | 9 +- services/sync/modules/wrap.js | 165 --------- 12 files changed, 1 insertion(+), 553 deletions(-) delete mode 100644 services/sync/modules/async.js delete mode 100644 services/sync/modules/wrap.js diff --git a/services/sync/modules/async.js b/services/sync/modules/async.js deleted file mode 100644 index f8a69c3a2d6f..000000000000 --- a/services/sync/modules/async.js +++ /dev/null @@ -1,350 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['Async']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); - -/* - * Asynchronous generator helpers - */ - -// Simple class to help us keep track of "namable" objects: that is, -// any object with a string property called "name". -function NamableTracker() { - this.__length = 0; - this.__dict = {}; -} - -NamableTracker.prototype = { - get length() { return this.__length; }, - add: function GD_add(item) { - this.__dict[item.name] = item; - this.__length++; - }, - remove: function GD_remove(item) { - delete this.__dict[item.name]; - this.__length--; - }, - __iterator__: function GD_iterator() { - for (name in this.__dict) - yield name; - } -} - -let gCurrentId = 0; -let gCurrentCbId = 0; -let gOutstandingGenerators = new NamableTracker(); - -// The AsyncException class represents an exception that's been thrown -// by an asynchronous coroutine-based function. For the most part, it -// behaves just like the "real" exception it wraps, only it adds some -// embellishments that provide information about what other -// asynchronous coroutine-based functions our current one was called -// from. -function AsyncException(asyncStack, exceptionToWrap) { - this._originalException = exceptionToWrap; - this._asyncStack = asyncStack; - - // Here we'll have our exception instance's prototype be dynamically - // generated; this ultimately allows us to dynamically "subclass" - // the exception we're wrapping at runtime. - this.__proto__ = { - __proto__: this._originalException, - - get asyncStack() this._asyncStack, - get originalException() this._originalException, - - addAsyncFrame: function AsyncException_addAsyncFrame(frame) { - this._asyncStack += ((this._asyncStack? "\n" : "") + - Utils.formatFrame(frame)); - }, - - toString: function AsyncException_toString() { - return this._originalException.toString(); - } - }; -} - -function Generator(thisArg, method, onComplete, args) { - this._outstandingCbs = 0; - this._log = Log4Moz.repository.getLogger("Async.Generator"); - this._log.level = - Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; - this._thisArg = thisArg; - this._method = method; - this._id = gCurrentId++; - this.onComplete = onComplete; - this._args = args; - this._stackAtLastCallbackGen = null; - - gOutstandingGenerators.add(this); - - this._initFrame = skipAsyncFrames(Components.stack.caller); -} -Generator.prototype = { - get name() { return this._method.name + "-" + this._id; }, - get generator() { return this._generator; }, - - get cb() { - let caller = Components.stack.caller; - let cbId = gCurrentCbId++; - this._outstandingCbs++; - this._stackAtLastCallbackGen = caller; - this._log.trace(this.name + ": cb-" + cbId + " generated at:\n" + - Utils.formatFrame(caller)); - let self = this; - let cb = function(data) { - self._log.trace(self.name + ": cb-" + cbId + " called."); - self._cont(data); - }; - cb.parentGenerator = this; - return cb; - }, - get listener() { return new Utils.EventListener(this.cb); }, - - get _thisArg() { return this.__thisArg; }, - set _thisArg(value) { - if (typeof value != "object") - throw "Generator: expected type 'object', got type '" + typeof(value) + "'"; - this.__thisArg = value; - }, - - get _method() { return this.__method; }, - set _method(value) { - if (typeof value != "function") - throw "Generator: expected type 'function', got type '" + typeof(value) + "'"; - this.__method = value; - }, - - get onComplete() { - if (this._onComplete) - return this._onComplete; - return function() { - //this._log.trace("Generator " + this.name + " has no onComplete"); - }; - }, - set onComplete(value) { - if (value && typeof value != "function") - throw "Generator: expected type 'function', got type '" + typeof(value) + "'"; - this._onComplete = value; - }, - - get asyncStack() { - let cbGenText = ""; - if (this._stackAtLastCallbackGen) - cbGenText = "Last callback created at " + - Utils.formatFrame(this._stackAtLastCallbackGen); - return Utils.stackTraceFromFrame(this._initFrame, Utils.formatFrame) + cbGenText; - }, - - _handleException: function AsyncGen__handleException(e) { - if (e instanceof StopIteration) { - this._log.trace(this.name + ": End of coroutine reached."); - // skip to calling done() - - } else if (this.onComplete && this.onComplete.parentGenerator && - this.onComplete.parentGenerator instanceof Generator) { - this._log.trace("[" + this.name + "] Saving exception and stack trace"); - this._log.trace("Exception: " + Utils.exceptionStr(e)); - - if (e instanceof AsyncException) { - // FIXME: attempt to skip repeated frames, which can happen if the - // child generator never yielded. Would break for valid repeats (recursion) - if (e.asyncStack.indexOf(Utils.formatFrame(this._initFrame)) == -1) - e.addAsyncFrame(this._initFrame); - } else { - e = new AsyncException(this.asyncStack, e); - } - - this._exception = e; - - } else if (e.message && e.message == 'Cannot acquire lock (internal lock)') { - this._log.warn("Exception: " + Utils.exceptionStr(e)); - } else { - this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.debug("Async stack trace:\n" + Utils.stackTrace(e, Utils.formatFrame)); - } - - // continue execution of caller. - // in the case of StopIteration we could return an error right - // away, but instead it's easiest/best to let the caller handle - // the error after a yield / in a callback. - if (!this._timer) { - this._log.trace("[" + this.name + "] running done() from _handleException()"); - this.done(); - } - }, - - _detectDeadlock: function AsyncGen__detectDeadlock() { - if (this._outstandingCbs == 0) - this._log.warn("Async method '" + this.name + - "' may have yielded without an outstanding callback."); - }, - - run: function AsyncGen_run() { - this._continued = false; - try { - this._generator = this._method.apply(this._thisArg, this._args); - this.generator.next(); // must initialize before sending - this.generator.send(this); - this._detectDeadlock(); - } catch (e) { - if (!(e instanceof StopIteration) || !this._timer) - this._handleException(e); - } - }, - - _cont: function AsyncGen__cont(data) { - this._outstandingCbs--; - this._log.trace(this.name + ": resuming coroutine."); - this._continued = true; - try { - this.generator.send(data); - this._detectDeadlock(); - } catch (e) { - if (!(e instanceof StopIteration) || !this._timer) - this._handleException(e); - } - }, - - _throw: function AsyncGen__throw(exception) { - this._outstandingCbs--; - try { this.generator.throw(exception); } - catch (e) { - if (!(e instanceof StopIteration) || !this._timer) - this._handleException(e); - } - }, - - // async generators can't simply call a callback with the return - // value, since that would cause the calling function to end up - // running (after the yield) from inside the generator. Instead, - // generators can call this method which sets up a timer to call the - // callback from a timer (and cleans up the timer to avoid leaks). - // It also closes generators after the timeout, to keep things - // clean. - done: function AsyncGen_done(retval) { - if (this._timer) // the generator/_handleException called self.done() twice - return; - let self = this; - let cb = function() { self._done(retval); }; - this._log.trace(this.name + ": done() called."); - if (!this._exception && this._outstandingCbs > 0) - this._log.warn("Async method '" + this.name + - "' may have outstanding callbacks."); - this._timer = Utils.makeTimerForCall(cb); - }, - - _done: function AsyncGen__done(retval) { - if (!this._generator) { - this._log.error("Async method '" + this.name + "' is missing a 'yield' call " + - "(or called done() after being finalized)"); - this._log.trace("Initial async stack trace:\n" + this.asyncStack); - } else { - this._generator.close(); - } - this._generator = null; - this._timer = null; - - if (this._exception) { - this._log.trace("[" + this.name + "] Propagating exception to parent generator"); - this.onComplete.parentGenerator._throw(this._exception); - } else { - try { - this._log.trace("[" + this.name + "] Running onComplete()"); - this.onComplete(retval); - } catch (e) { - this._log.error("Exception caught from onComplete handler of " + - this.name + " generator"); - this._log.error("Exception: " + Utils.exceptionStr(e)); - this._log.trace("Current stack trace:\n" + - Utils.stackTrace(e, Utils.formatFrame)); - this._log.trace("Initial async stack trace:\n" + this.asyncStack); - } - } - gOutstandingGenerators.remove(this); - } -}; - -function skipAsyncFrames(frame) { - while (frame.name && frame.name.match(/^Async(Gen|)_/)) - frame = frame.caller; - return frame; -} - -Async = { - get outstandingGenerators() { return gOutstandingGenerators; }, - - // Use: - // let gen = Async.run(this, this.fooGen, ...); - // let ret = yield; - // - // where fooGen is a generator function, and gen is a Generator instance - // ret is whatever the generator 'returns' via Generator.done(). - - run: function Async_run(thisArg, method, onComplete /* , arg1, arg2, ... */) { - let args = Array.prototype.slice.call(arguments, 3); - let gen = new Generator(thisArg, method, onComplete, args); - gen.run(); - return gen; - }, - - // Syntactic sugar for run(). Meant to be used like this in code - // that imports this file: - // - // Function.prototype.async = Async.sugar; - // - // So that you can do: - // - // let gen = fooGen.async(...); - // let ret = yield; - // - // Note that 'this' refers to the method being called, not the - // Async object. - - sugar: function Async_sugar(thisArg, onComplete /* , arg1, arg2, ... */) { - let args = Array.prototype.slice.call(arguments, 1); - args.unshift(thisArg, this); - Async.run.apply(Async, args); - } -}; diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 69fc5d920e2d..b8a18276b6ee 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -44,14 +44,11 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); -Function.prototype.async = Async.sugar; - function Collection(uri, recordObj) { this._Coll_init(uri); this._recordObj = recordObj; diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 0f70d3b10f3f..9b1d4c724efb 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -44,12 +44,9 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/keys.js"); -Function.prototype.async = Async.sugar; - function CryptoWrapper(uri) { this._CryptoWrap_init(uri); } diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 687920ee7788..7838a8fe457e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -47,7 +47,6 @@ Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/stores.js"); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index fed554f44b67..e40588619603 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -68,7 +68,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/wrap.js"); Cu.import("resource://weave/faultTolerance.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/resource.js"); @@ -77,16 +76,12 @@ Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/engines/clientData.js"); -Function.prototype.async = Async.sugar; - // for export let Weave = {}; Cu.import("resource://weave/constants.js", Weave); Cu.import("resource://weave/util.js", Weave); -Cu.import("resource://weave/async.js", Weave); Cu.import("resource://weave/faultTolerance.js", Weave); Cu.import("resource://weave/auth.js", Weave); Cu.import("resource://weave/resource.js", Weave); @@ -148,14 +143,11 @@ StatusRecord.prototype = { function WeaveSvc() { this._notify = Utils.notify("weave:service:"); - this._notifyAsync = Wrap.notify("weave:service:"); } WeaveSvc.prototype = { _lock: Utils.lock, - _localLock: Wrap.localLock, _catch: Utils.catch, - _catchAll: Wrap.catchAll, _isQuitting: false, _loggedIn: false, _syncInProgress: false, diff --git a/services/sync/modules/type_records/forms.js b/services/sync/modules/type_records/forms.js index 1d77e5782fb3..abe1d0d6d6c5 100644 --- a/services/sync/modules/type_records/forms.js +++ b/services/sync/modules/type_records/forms.js @@ -43,13 +43,10 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); -Function.prototype.async = Async.sugar; - function FormRec(uri) { this._FormRec_init(uri); } diff --git a/services/sync/modules/type_records/history.js b/services/sync/modules/type_records/history.js index b11fd7dd9c4e..5ca5040ebb58 100644 --- a/services/sync/modules/type_records/history.js +++ b/services/sync/modules/type_records/history.js @@ -43,13 +43,10 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); -Function.prototype.async = Async.sugar; - function HistoryRec(uri) { this._HistoryRec_init(uri); } diff --git a/services/sync/modules/type_records/passwords.js b/services/sync/modules/type_records/passwords.js index b81f3752bdd1..4b6f4de3e0e3 100644 --- a/services/sync/modules/type_records/passwords.js +++ b/services/sync/modules/type_records/passwords.js @@ -43,13 +43,10 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); -Function.prototype.async = Async.sugar; - function LoginRec(uri) { this._LoginRec_init(uri); } diff --git a/services/sync/modules/type_records/prefs.js b/services/sync/modules/type_records/prefs.js index c67b15456f40..2bfb3f652a2d 100644 --- a/services/sync/modules/type_records/prefs.js +++ b/services/sync/modules/type_records/prefs.js @@ -43,13 +43,10 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); -Function.prototype.async = Async.sugar; - function PrefRec(uri) { this._PrefRec_init(uri); } diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js index 187b5ef76f5a..9fcecd155e50 100644 --- a/services/sync/modules/type_records/tabs.js +++ b/services/sync/modules/type_records/tabs.js @@ -43,13 +43,10 @@ const Cu = Components.utils; Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); -Function.prototype.async = Async.sugar; - function TabSetRecord(uri) { this._TabSetRecord_init(uri); } diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index f2ea9f45bade..20bf20b7894f 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -343,10 +343,6 @@ let Utils = { else if (frame.fileName) tmp = frame.fileName.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); - // skip async.js frames - if (tmp == "module:async.js") - return null; - if (frame.lineNumber) tmp += ":" + frame.lineNumber; if (frame.name) @@ -386,10 +382,7 @@ let Utils = { }, stackTrace: function Weave_stackTrace(e, formatter) { - if (e.asyncStack) // AsyncException - return "Original exception: " + Utils.exceptionStr(e.originalException) + "\n" + - "Async stack trace:\n" + e.asyncStack; - else if (e.location) // Wrapped nsIException + if (e.location) // Wrapped nsIException return "Stack trace:\n" + this.stackTraceFromFrame(e.location, formatter); else if (e.stack) // Standard JS exception return "JS Stack trace:\n" + e.stack; diff --git a/services/sync/modules/wrap.js b/services/sync/modules/wrap.js deleted file mode 100644 index eb285b3fd214..000000000000 --- a/services/sync/modules/wrap.js +++ /dev/null @@ -1,165 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['Wrap']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/ext/Observers.js"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/faultTolerance.js"); - -Function.prototype.async = Async.sugar; - -/* - * Wrapper utility functions - * - * Not in util.js because that would cause a circular dependency - * between util.js and async.js (we include async.js so that our - * returned generator functions have the .async sugar defined) - */ - -let Wrap = { - - // NOTE: copy this function over to your objects, use like this: - // - // function MyObj() { - // this._notify = Wrap.notify("some:prefix:"); - // } - // MyObj.prototype = { - // ... - // method: function MyMethod() { - // let self = yield; - // ... - // // doFoo is assumed to be an asynchronous method - // this._notify("foo", "", this._doFoo, arg1, arg2).async(this, self.cb); - // let ret = yield; - // ... - // } - // }; - notify: function WeaveWrap_notify(prefix) { - return function NotifyWrapMaker(name, subject, method) { - let savedArgs = Array.prototype.slice.call(arguments, 4); - return function NotifyWrap() { - let self = yield; - let ret; - let args = Array.prototype.slice.call(arguments); - - try { - this._log.debug("Event: " + prefix + name + ":start"); - Observers.notify(prefix + name + ":start", subject); - - args = savedArgs.concat(args); - args.unshift(this, method, self.cb); - Async.run.apply(Async, args); - ret = yield; - - this._log.debug("Event: " + prefix + name + ":finish"); - let foo = Observers.notify(prefix + name + ":finish", subject); - - } catch (e) { - this._log.debug("Event: " + prefix + name + ":error"); - Observers.notify(prefix + name + ":error", subject); - throw e; - } - - self.done(ret); - }; - }; - }, - - // NOTE: see notify, this works the same way. they can be - // chained together as well. - localLock: function WeaveSync_localLock(method /* , arg1, arg2, ..., argN */) { - let savedMethod = method; - let savedArgs = Array.prototype.slice.call(arguments, 1); - - return function WeaveLocalLockWrapper(/* argN+1, argN+2, ... */) { - let self = yield; - let ret; - let args = Array.prototype.slice.call(arguments); - - ret = this.lock(); - if (!ret) - throw "Could not acquire lock"; - - try { - args = savedArgs.concat(args); - args.unshift(this, savedMethod, self.cb); - ret = yield Async.run.apply(Async, args); - - } catch (e) { - throw e; - - } finally { - this.unlock(); - } - - self.done(ret); - }; - }, - - // NOTE: see notify, this works the same way. they can be - // chained together as well. - // catchAll catches any exceptions and prints a stack trace for them - catchAll: function WeaveSync_catchAll(method /* , arg1, arg2, ..., argN */) { - let savedMethod = method; - let savedArgs = Array.prototype.slice.call(arguments, 1); - - return function WeaveCatchAllWrapper(/* argN+1, argN+2, ... */) { - let self = yield; - let ret; - let args = Array.prototype.slice.call(arguments); - - try { - args = savedArgs.concat(args); - args.unshift(this, savedMethod, self.cb); - ret = yield Async.run.apply(Async, args); - - } catch (e) { - this._log.debug("Caught exception: " + Utils.exceptionStr(e)); - this._log.debug("\n" + Utils.stackTrace(e)); - } - self.done(ret); - }; - } -} From ed448e26111c3d2a1db67ccda924ff6fe9388a54 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 9 Jun 2009 14:25:55 -0500 Subject: [PATCH 1171/1860] Convert test_resource to sync. --- services/sync/tests/unit/test_resource.js | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index b14b7dedb3cf..b035d7b16fc2 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -1,12 +1,9 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/identity.js"); -Function.prototype.async = Async.sugar; - let logger; let Httpd = {}; Cu.import("resource://harness/modules/httpd.js", Httpd); @@ -42,9 +39,7 @@ function server_404(metadata, response) { response.bodyOutputStream.write(body, body.length); } -function async_test() { - let self = yield; - +function run_test() { logger = Log4Moz.repository.getLogger('Test'); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); @@ -59,14 +54,14 @@ function async_test() { // 1. A regular non-password-protected resource let res = new Resource("http://localhost:8080/open"); - let content = yield res.get(self.cb); + let content = res.get(); do_check_eq(content, "This path exists"); do_check_eq(res.lastChannel.responseStatus, 200); // 2. A password protected resource (test that it'll fail w/o pass) let res2 = new Resource("http://localhost:8080/protected"); try { - content = yield res2.get(self.cb); + content = res2.get(); do_check_true(false); // unreachable, get() above must fail } catch (e) {} @@ -75,7 +70,7 @@ function async_test() { let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); let res3 = new Resource("http://localhost:8080/protected"); res3.authenticator = auth; - content = yield res3.get(self.cb); + content = res3.get(); do_check_eq(content, "This path exists and is protected"); do_check_eq(res3.lastChannel.responseStatus, 200); @@ -83,7 +78,7 @@ function async_test() { let res4 = new Resource("http://localhost:8080/404"); try { - let content = yield res4.get(self.cb); + let content = res4.get(); do_check_true(false); // unreachable, get() above must fail } catch (e) {} do_check_eq(res4.lastChannel.responseStatusText, "Not Found"); @@ -94,12 +89,5 @@ function async_test() { // * DELETE requests // * JsonFilter - do_test_finished(); server.stop(); - self.done(); -} - -function run_test() { - async_test.async(this); - do_test_pending(); } From a375762483750471edf0d0f5a196711880107eab Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 9 Jun 2009 14:28:37 -0500 Subject: [PATCH 1172/1860] Convert test_records_wbo to sync. --- services/sync/tests/unit/test_records_wbo.js | 22 +++++--------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index 32f721bce8ac..6dde12a51faa 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -1,7 +1,6 @@ try { Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); - Cu.import("resource://weave/async.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/resource.js"); @@ -9,8 +8,6 @@ try { Cu.import("resource://weave/base_records/collection.js"); } catch (e) { do_throw(e); } -Function.prototype.async = Async.sugar; - function record_handler(metadata, response) { let obj = {id: "asdf-1234-asdf-1234", modified: 2454725.98283, @@ -32,8 +29,7 @@ function coll_handler(metadata, response) { return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } -function async_test() { - let self = yield; +function run_test() { let server; try { @@ -52,7 +48,7 @@ function async_test() { log.info("Getting a WBO record"); let res = new Resource("http://localhost:8080/record"); - yield res.get(self.cb); + res.get(); let rec = new WBORecord(); rec.deserialize(res.data); @@ -68,7 +64,7 @@ function async_test() { log.info("Getting a WBO record using the record manager"); - let rec2 = yield Records.get(self.cb, "http://localhost:8080/record2"); + let rec2 = Records.get("http://localhost:8080/record2"); do_check_eq(rec2.id, "record2"); do_check_eq(rec2.modified, 2454725.98284); do_check_eq(typeof(rec2.payload), "object"); @@ -78,25 +74,17 @@ function async_test() { log.info("Using a collection to get a record"); let coll = new Collection("http://localhost:8080/coll", WBORecord); - yield coll.get(self.cb); + coll.get(); do_check_eq(coll.iter.count, 1); - let rec3 = yield coll.iter.next(self.cb); + let rec3 = coll.iter.next(); do_check_eq(rec3.id, "record2"); do_check_eq(rec3.modified, 2454725.98284); do_check_eq(typeof(rec3.payload), "object"); do_check_eq(rec3.payload.cheese, "gruyere"); log.info("Done!"); - do_test_finished(); } catch (e) { do_throw(e); } finally { server.stop(); } - - self.done(); -} - -function run_test() { - async_test.async(this); - do_test_pending(); } From 607b173ca52cfc153beb596041cd6bc93ea60a33 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 9 Jun 2009 14:31:26 -0500 Subject: [PATCH 1173/1860] Convert test_records_keys to sync. --- services/sync/tests/unit/test_records_keys.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/services/sync/tests/unit/test_records_keys.js b/services/sync/tests/unit/test_records_keys.js index 33d31eebe845..b336127267e0 100644 --- a/services/sync/tests/unit/test_records_keys.js +++ b/services/sync/tests/unit/test_records_keys.js @@ -1,14 +1,11 @@ try { Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); - Cu.import("resource://weave/async.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/base_records/keys.js"); } catch (e) { do_throw(e); } -Function.prototype.async = Async.sugar; - function pubkey_handler(metadata, response) { let obj = {id: "asdf-1234-asdf-1234", modified: "2454725.98283", @@ -27,8 +24,7 @@ function privkey_handler(metadata, response) { return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } -function async_test() { - let self = yield; +function run_test() { let server; try { @@ -45,26 +41,18 @@ function async_test() { log.info("Getting a public key"); - let pubkey = yield PubKeys.get(self.cb, "http://localhost:8080/pubkey"); + let pubkey = PubKeys.get("http://localhost:8080/pubkey"); do_check_eq(pubkey.data.payload.type, "pubkey"); do_check_eq(PubKeys.lastResource.lastChannel.responseStatus, 200); log.info("Getting matching private key"); - let privkey = yield PrivKeys.get(self.cb, pubkey.privateKeyUri); + let privkey = PrivKeys.get(pubkey.privateKeyUri); do_check_eq(privkey.data.payload.type, "privkey"); do_check_eq(PrivKeys.lastResource.lastChannel.responseStatus, 200); log.info("Done!"); - do_test_finished(); } catch (e) { do_throw(e); } finally { server.stop(); } - - self.done(); -} - -function run_test() { - async_test.async(this); - do_test_pending(); } From ef6a176071012ec68e77af6027342788cc93160c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 9 Jun 2009 14:33:45 -0500 Subject: [PATCH 1174/1860] Convert test_records_crypto to sync. --- .../sync/tests/unit/test_records_crypto.js | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index b0ce7cf080db..3ea357b29455 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -1,7 +1,6 @@ try { Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); - Cu.import("resource://weave/async.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/base_records/keys.js"); @@ -9,7 +8,6 @@ try { } catch (e) { do_throw(e); } -Function.prototype.async = Async.sugar; let keys, cryptoMeta, cryptoWrap; @@ -41,8 +39,7 @@ function crypto_meta_handler(metadata, response) { return httpd_basic_auth_handler(JSON.stringify(obj), metadata, response); } -function async_test() { - let self = yield; +function run_test() { let server; try { @@ -74,38 +71,30 @@ function async_test() { cryptoMeta = new CryptoMeta("http://localhost:8080/crypto-meta", auth); cryptoMeta.generateIV(); - yield cryptoMeta.addUnwrappedKey(self.cb, keys.pubkey, keys.symkey); + cryptoMeta.addUnwrappedKey(keys.pubkey, keys.symkey); log.info("Creating and encrypting a record"); cryptoWrap = new CryptoWrapper("http://localhost:8080/crypted-resource", auth); cryptoWrap.encryption = "http://localhost:8080/crypto-meta"; cryptoWrap.cleartext = "my payload here"; - yield cryptoWrap.encrypt(self.cb, "my passphrase"); + cryptoWrap.encrypt("my passphrase"); log.info("Decrypting the record"); - let payload = yield cryptoWrap.decrypt(self.cb, "my passphrase"); + let payload = cryptoWrap.decrypt("my passphrase"); do_check_eq(payload, "my payload here"); do_check_neq(payload, cryptoWrap.payload); // wrap.data.payload is the encrypted one log.info("Re-encrypting the record with alternate payload"); cryptoWrap.cleartext = "another payload"; - yield cryptoWrap.encrypt(self.cb, "my passphrase"); - payload = yield cryptoWrap.decrypt(self.cb, "my passphrase"); + cryptoWrap.encrypt("my passphrase"); + payload = cryptoWrap.decrypt("my passphrase"); do_check_eq(payload, "another payload"); log.info("Done!"); - do_test_finished(); } catch (e) { do_throw(e); } finally { server.stop(); } - - self.done(); -} - -function run_test() { - async_test.async(this); - do_test_pending(); } From 020c3d18b4aa6c754cc39f88a69c1a53040c6dfd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 9 Jun 2009 14:34:27 -0500 Subject: [PATCH 1175/1860] Remove test_async* tests. --- services/sync/tests/unit/test_async.js | 50 --------------- .../sync/tests/unit/test_async_exceptions.js | 62 ------------------- .../tests/unit/test_async_missing_yield.js | 33 ---------- 3 files changed, 145 deletions(-) delete mode 100644 services/sync/tests/unit/test_async.js delete mode 100644 services/sync/tests/unit/test_async_exceptions.js delete mode 100644 services/sync/tests/unit/test_async_missing_yield.js diff --git a/services/sync/tests/unit/test_async.js b/services/sync/tests/unit/test_async.js deleted file mode 100644 index 06f42c7e2854..000000000000 --- a/services/sync/tests/unit/test_async.js +++ /dev/null @@ -1,50 +0,0 @@ -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -function run_test() { - var fts = new FakeTimerService(); - - Function.prototype.async = Async.sugar; - - var onCompleteCalled = false; - - function onComplete() { - onCompleteCalled = true; - } - - let timesYielded = 0; - - function testAsyncFunc(x) { - let self = yield; - timesYielded++; - - // Ensure that argument was passed in properly. - do_check_eq(x, 5); - - // Ensure that 'this' is set properly. - do_check_eq(this.sampleProperty, true); - - fts.makeTimerForCall(self.cb); - yield; - - timesYielded++; - self.done(); - } - - var thisArg = {sampleProperty: true}; - testAsyncFunc.async(thisArg, onComplete, 5); - - do_check_eq(timesYielded, 1); - - do_check_true(fts.processCallback()); - - do_check_eq(timesYielded, 2); - - do_check_eq(Async.outstandingGenerators.length, 1); - - do_check_true(fts.processCallback()); - - do_check_false(fts.processCallback()); - - do_check_eq(Async.outstandingGenerators.length, 0); -} diff --git a/services/sync/tests/unit/test_async_exceptions.js b/services/sync/tests/unit/test_async_exceptions.js deleted file mode 100644 index 5cccc980504a..000000000000 --- a/services/sync/tests/unit/test_async_exceptions.js +++ /dev/null @@ -1,62 +0,0 @@ -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; - -function _ensureExceptionIsValid(e) { - do_check_eq(e.message, "intentional failure"); - do_check_eq(typeof(e.asyncStack), "string"); - do_check_true(e.asyncStack.indexOf("thirdGen") > -1); - do_check_true(e.asyncStack.indexOf("secondGen") > -1); - do_check_true(e.asyncStack.indexOf("runTestGenerator") > -1); - do_check_true(e.stack.indexOf("thirdGen") > -1); - do_check_eq(e.stack.indexOf("secondGen"), -1); - do_check_eq(e.stack.indexOf("runTestGenerator"), -1); -} - -function thirdGen() { - let self = yield; - - Utils.makeTimerForCall(self.cb); - yield; - - throw new Error("intentional failure"); -} - -function secondGen() { - let self = yield; - - thirdGen.async({}, self.cb); - try { - let result = yield; - } catch (e) { - ensureExceptionIsValid(e); - throw e; - } - - self.done(); -} - -function runTestGenerator() { - let self = yield; - - secondGen.async({}, self.cb); - let wasCaught = false; - try { - let result = yield; - } catch (e) { - ensureExceptionIsValid(e); - wasCaught = true; - } - - do_check_true(wasCaught); - self.done(); -} - -function run_test() { - var fts = new FakeTimerService(); - runTestGenerator.async({}); - for (var i = 0; fts.processCallback(); i++) {} - do_check_eq(i, 4); - do_check_eq(Async.outstandingGenerators.length, 0); -} diff --git a/services/sync/tests/unit/test_async_missing_yield.js b/services/sync/tests/unit/test_async_missing_yield.js deleted file mode 100644 index b3ae785627fe..000000000000 --- a/services/sync/tests/unit/test_async_missing_yield.js +++ /dev/null @@ -1,33 +0,0 @@ -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); - -Function.prototype.async = Async.sugar; - -function secondGen() { - let self = yield; - - Utils.makeTimerForCall(self.cb); - - self.done(); -} - -function runTestGenerator() { - let self = yield; - - secondGen.async({}, self.cb); - let result = yield; - - self.done(); -} - -function run_test() { - Cu.import("resource://weave/log4moz.js"); - Cu.import("resource://weave/async.js"); - - var fts = new FakeTimerService(); - var logStats = initTestLogging(); - - runTestGenerator.async({}); - while (fts.processCallback()) {}; - do_check_eq(logStats.errorsLogged, 3); -} From 3e9f2657fe0a113ac0867183b09d53f601f3d5b1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 9 Jun 2009 14:39:45 -0500 Subject: [PATCH 1176/1860] Convert test_auth_manager to sync. --- services/sync/tests/unit/test_auth_manager.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js index e7cb18f210c9..310d1eceb7ad 100644 --- a/services/sync/tests/unit/test_auth_manager.js +++ b/services/sync/tests/unit/test_auth_manager.js @@ -1,12 +1,9 @@ Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/resource.js"); -Function.prototype.async = Async.sugar; - let logger; let Httpd = {}; Cu.import("resource://harness/modules/httpd.js", Httpd); @@ -30,9 +27,7 @@ function server_handler(metadata, response) { response.bodyOutputStream.write(body, body.length); } -function async_test() { - let self = yield; - +function run_test() { logger = Log4Moz.repository.getLogger('Test'); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); @@ -44,16 +39,9 @@ function async_test() { Auth.defaultAuthenticator = auth; let res = new Resource("http://localhost:8080/foo"); - let content = yield res.get(self.cb); + let content = res.get(); do_check_eq(content, "This path exists and is protected"); do_check_eq(res.lastChannel.responseStatus, 200); - do_test_finished(); server.stop(); - self.done(); -} - -function run_test() { - async_test.async(this); - do_test_pending(); } From 15f42d6f9ef4d51a1a20c5cca784e4357cd7dba4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 9 Jun 2009 14:48:49 -0500 Subject: [PATCH 1177/1860] Remove some async related code in head.js. --- services/sync/tests/unit/head_first.js | 91 -------------------------- 1 file changed, 91 deletions(-) diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index b7c118a17140..3d17641253c6 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -105,27 +105,6 @@ function initTestLogging(level) { return logStats; } -function makeAsyncTestRunner(generator) { - Cu.import("resource://weave/async.js"); - - var logStats = initTestLogging(); - - function run_test() { - do_test_pending(); - - let onComplete = function() { - if (logStats.errorsLogged) - do_throw("Errors were logged."); - else - do_test_finished(); - }; - - Async.run({}, generator, onComplete); - } - - return run_test; -} - function FakePrefService(contents) { Cu.import("resource://weave/util.js"); this.fakeContents = contents; @@ -149,75 +128,6 @@ FakePrefService.prototype = { addObserver: function fake_addObserver() {} }; -function makeFakeAsyncFunc(retval) { - Cu.import("resource://weave/async.js"); - Function.prototype.async = Async.sugar; - - function fakeAsyncFunc() { - let self = yield; - - Utils.makeTimerForCall(self.cb); - yield; - - self.done(retval); - } - - return fakeAsyncFunc; -} - -function FakeDAVService(contents) { - Cu.import("resource://weave/dav.js"); - - this.fakeContents = contents; - DAV.__proto__ = this; - this.checkLogin = makeFakeAsyncFunc(200); -} - -FakeDAVService.prototype = { - PUT: function fake_PUT(path, data, onComplete) { - getTestLogger().info("HTTP PUT to " + path + " with data: " + data); - this.fakeContents[path] = data; - makeFakeAsyncFunc({status: 200}).async(this, onComplete); - }, - - GET: function fake_GET(path, onComplete) { - var result = {status: 404}; - if (path in this.fakeContents) - result = {status: 200, responseText: this.fakeContents[path]}; - getTestLogger().info("HTTP GET from " + path + ", returning status " + - result.status); - return makeFakeAsyncFunc(result).async(this, onComplete); - }, - - MKCOL: function fake_MKCOL(path, onComplete) { - getTestLogger().info("HTTP MKCOL on " + path); - makeFakeAsyncFunc(true).async(this, onComplete); - }, - - DELETE: function fake_DELETE(path, onComplete) { - var result = {status: 404}; - if (path in this.fakeContents) { - result = {status: 200}; - delete this.fakeContents[path]; - } - getTestLogger().info("HTTP DELETE on " + path + ", returning status " + - result.status); - return makeFakeAsyncFunc(result).async(this, onComplete); - }, - - listFiles: function fake_listFiles(path) { - let self = yield; - if (typeof(path) != "undefined") - throw new Error("Not yet implemented!"); - let filenames = []; - for (name in this.fakeContents) { - getTestLogger().info("file " + name); - filenames.push(name); - } - self.done(filenames); - } -}; - function FakePasswordService(contents) { Cu.import("resource://weave/util.js"); @@ -318,7 +228,6 @@ function SyncTestingInfrastructure(engineFactory) { this.fakePasswordService = new FakePasswordService(__fakePasswords); this.fakePrefService = new FakePrefService(__fakePrefs); - this.fakeDAVService = new FakeDAVService({}); this.fakeTimerService = new FakeTimerService(); this.logStats = initTestLogging(); this.fakeFilesystem = new FakeFilesystemService({}); From bd399d0d30cc9835bc091d0d1e578d92c8a43f65 Mon Sep 17 00:00:00 2001 From: "Mounir Lamouri (volkmar)" Date: Tue, 16 Jun 2009 12:16:03 -0700 Subject: [PATCH 1178/1860] Bug 486797 - ppc architecture is not defined if not MacOS. r=Mardak --- services/crypto/Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 1f00d7cdc02b..17277a60d116 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -89,6 +89,9 @@ else ifeq ($(machine), i686) arch = x86 else +ifeq ($(machine), ppc) + arch = ppc +else ifeq ($(machine), Power Macintosh) arch = ppc else @@ -102,6 +105,7 @@ endif endif endif endif +endif # Universal binary so no need for $(arch) for Darwin From d68b1b82fe4553c6d1b4637fac55c203a5779398 Mon Sep 17 00:00:00 2001 From: "Mikhail Stepura (mishail)" Date: Tue, 16 Jun 2009 16:16:39 -0700 Subject: [PATCH 1179/1860] Bug 486042 - "Load this bookmark in the sidebar" status not synced. r=Mardak --- services/sync/modules/engines/bookmarks.js | 25 ++++++++++++++++--- .../sync/modules/type_records/bookmark.js | 2 +- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 018986214577..e6dc9c214f38 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -210,6 +210,10 @@ BookmarksStore.prototype = { this._ans.EXPIRE_NEVER); } + if (record.loadInSidebar) + this._ans.setItemAnnotation(newId, "bookmarkProperties/loadInSidebar", + true, 0, this._ans.EXPIRE_NEVER); + if (record.type == "microsummary") { this._log.debug(" \-> is a microsummary"); this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", @@ -372,6 +376,13 @@ BookmarksStore.prototype = { val, 0, this._ans.EXPIRE_NEVER); break; + case "loadInSidebar": + if (val) + this._ans.setItemAnnotation(itemId, "bookmarkProperties/loadInSidebar", + true, 0, this._ans.EXPIRE_NEVER); + else + this._ans.removeItemAnnotation(itemId, "bookmarkProperties/loadInSidebar"); + break; case "generatorUri": { try { let micsumURI = this._bms.getBookmarkURI(itemId); @@ -464,6 +475,10 @@ BookmarksStore.prototype = { } }, + _isLoadInSidebar: function BStore__isLoadInSidebar(id) { + return this._ans.itemHasAnnotation(id, "bookmarkProperties/loadInSidebar"); + }, + _getStaticTitle: function BStore__getStaticTitle(id) { try { return this._ans.getItemAnnotation(id, "bookmarks/staticTitle"); @@ -504,6 +519,7 @@ BookmarksStore.prototype = { record.tags = this._getTags(record.bmkUri); record.keyword = this._bms.getKeywordForBookmark(placeId); record.description = this._getDescription(placeId); + record.loadInSidebar = this._isLoadInSidebar(placeId); break; case this._bms.TYPE_FOLDER: @@ -736,10 +752,11 @@ dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in al return; // ignore annotations except for the ones that we sync - if (isAnno && (property != "livemark/feedURI" || - property != "livemark/siteURI" || - property != "microsummary/generatorURI")) - return; + let annos = ["bookmarkProperties/description", + "bookmarkProperties/loadInSidebar", "bookmarks/staticTitle", + "livemark/feedURI", "livemark/siteURI", "microsummary/generatorURI"]; + if (isAnno && annos.indexOf(property) == -1) + return; this._log.trace("onItemChanged: " + itemId + (", " + property + (isAnno? " (anno)" : "")) + diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 7dce3c39ef9c..78ba9cb834b7 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -108,7 +108,7 @@ Bookmark.prototype = { }; Utils.deferGetSet(Bookmark, "cleartext", ["title", "bmkUri", "description", - "tags", "keyword"]); + "loadInSidebar", "tags", "keyword"]); function BookmarkMicsum(uri) { this._BookmarkMicsum_init(uri); From ca93d78d47ba875c6032e94fc2f19064b7db7a44 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 16 Jun 2009 17:07:01 -0700 Subject: [PATCH 1180/1860] Check if password and username are different on account creation (bug #442878) --- services/sync/locales/en-US/wizard.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties index 87a1e1664d19..bcae4db070ad 100644 --- a/services/sync/locales/en-US/wizard.properties +++ b/services/sync/locales/en-US/wizard.properties @@ -19,8 +19,9 @@ email-invalid.label = Please re-enter a valid email address. email-success.label = -passwordsUnmatched.label = Passwords do not match +passwordsUnmatched.label = Passwords do not match. passphrasesUnmatched.label = Passphrases do not match. +samePasswordAndUsername.label = Your password and username must be different. samePasswordAndPassphrase.label = Your password and passphrase must be different. From 170a97605fd42603946db2ffec40fa4b0c1ae028 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Jun 2009 17:22:59 -0700 Subject: [PATCH 1181/1860] Bug 493816 - Timestamps need to be saved as string prefs Internally store .lastSync as a string but keep exposing set/getters as float values. parseFloat takes both strings and numbers and gives a number. --- services/sync/modules/engines.js | 10 +++++----- services/sync/tests/unit/test_engine_lastSync.js | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 services/sync/tests/unit/test_engine_lastSync.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9d39053c9e0a..33a0167aec8f 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -249,18 +249,18 @@ SyncEngine.prototype = { }, get lastSync() { - return Svc.Prefs.get(this.name + ".lastSync", 0); + return parseFloat(Svc.Prefs.get(this.name + ".lastSync", "0")); }, set lastSync(value) { + // Reset the pref in-case it's a number instead of a string Svc.Prefs.reset(this.name + ".lastSync"); - if (typeof(value) == "string") - value = parseInt(value); - Svc.Prefs.set(this.name + ".lastSync", value); + // Store the value as a string to keep floating point precision + Svc.Prefs.set(this.name + ".lastSync", value.toString()); }, resetLastSync: function SyncEngine_resetLastSync() { this._log.debug("Resetting " + this.name + " last sync time"); Svc.Prefs.reset(this.name + ".lastSync"); - Svc.Prefs.set(this.name + ".lastSync", 0); + Svc.Prefs.set(this.name + ".lastSync", "0"); }, // Create a new record by querying the store, and add the engine metadata diff --git a/services/sync/tests/unit/test_engine_lastSync.js b/services/sync/tests/unit/test_engine_lastSync.js new file mode 100644 index 000000000000..f243022d46e4 --- /dev/null +++ b/services/sync/tests/unit/test_engine_lastSync.js @@ -0,0 +1,8 @@ +Cu.import("resource://weave/engines.js"); + +function run_test() { + // Make sure storing floats for lastSync stay as floats + let engine = new SyncEngine(); + engine.lastSync = 123.45; + do_check_eq(engine.lastSync, 123.45); +} From 8c92ef555ccc62169421eb06bb6c596160dfc346 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 17 Jun 2009 08:51:54 -0700 Subject: [PATCH 1182/1860] Bug 481327 - Script gets stuck during startup. r=Mardak --- services/sync/modules/engines/bookmarks.js | 58 ++++++---------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e6dc9c214f38..d5c14e360a9a 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -662,33 +662,19 @@ BookmarksTracker.prototype = { return ls; }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]), + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver, + Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS + ]), _init: function BMT__init() { this.__proto__.__proto__._init.call(this); - // NOTE: since the callbacks give us item IDs (not GUIDs), we use - // getItemGUID to get it within the callback. For removals, however, - // that doesn't work because the item is already gone! (and worse, Places - // has a bug where it will generate a new one instead of throwing). - // Our solution: cache item IDs -> GUIDs - -let before = new Date(); - // FIXME: very roundabout way of getting id -> guid mapping! - let store = new BookmarksStore(); - let all = store.getAllIDs(); - this._all = {}; - for (let guid in all) { - this._all[this._bms.getItemIdForGUID(guid)] = guid; - } -let after = new Date(); -dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in all)].length + " bookmark items\n"); - // Ignore changes to the special roots. We use special names for them, so // ignore their "real" places guid as well as ours, just in case + let store = new BookmarksStore(); for (let [weaveId, id] in Iterator(store.specialIds)) { this.ignoreID(weaveId); - this.ignoreID(this._all[id]); } this._bms.addObserver(this, false); @@ -729,21 +715,14 @@ dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in al return; this._log.trace("onItemAdded: " + itemId); - - this._all[itemId] = this._bms.getItemGUID(itemId); - if (this.addChangedID(this._all[itemId])) + if (this.addChangedID(this._bms.getItemGUID(itemId))) this._upScore(); }, - onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) { - if (this._ignore(folder)) - return; - - this._log.trace("onItemRemoved: " + itemId); - - if (this.addChangedID(this._all[itemId])) - this._upScore(); - delete this._all[itemId]; + onBeforeItemRemoved: function BMT_onBeforeItemRemoved(itemId) { + this._log.trace("onItemBeforeRemoved: " + itemId); + if (this.addChangedID(this._bms.getItemGUID(itemId))) + this._upScore(); }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { @@ -761,12 +740,10 @@ dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in al this._log.trace("onItemChanged: " + itemId + (", " + property + (isAnno? " (anno)" : "")) + (value? (" = \"" + value + "\"") : "")); - // 1) notifications for already-deleted items are ignored - // 2) note that engine/store are responsible for manually updating the - // tracker's placesId->weaveId cache - if ((itemId in this._all) && - (this._bms.getItemGUID(itemId) == this._all[itemId]) && - this.addChangedID(this._all[itemId])) + + // we should never really get onItemChanged for a deleted item + let guid = this._bms.getItemGUID(itemId); + if (guid && this.addChangedID(guid)) this._upScore(); }, @@ -776,15 +753,12 @@ dump((after - before) + "ms spent mapping id -> guid for " + [key for (key in al return; this._log.trace("onItemMoved: " + itemId); - - if (!this._all[itemId]) - this._all[itemId] = this._bms.itemGUID(itemId); - if (this.addChangedID(this._all[itemId])) + if (this.addChangedID(this._bms.itemGUID(itemId))) this._upScore(); }, onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, onEndUpdateBatch: function BMT_onEndUpdateBatch() {}, - onBeforeItemRemoved: function BMT_onBeforeItemRemoved(itemId) {}, + onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) {}, onItemVisited: function BMT_onItemVisited(itemId, aVisitID, time) {} }; From 82985e7f0a0e61539014a89832fc425e382e868d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 17 Jun 2009 08:51:54 -0700 Subject: [PATCH 1183/1860] Cleanup/refactor followup to bug 481327. Change _ignore to take itemId to find folderId and pull out addChanged/getItemGUID/upScore to addId. --- services/sync/modules/engines/bookmarks.js | 64 ++++++++++++---------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d5c14e360a9a..a2175c2d07aa 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -290,10 +290,8 @@ BookmarksStore.prototype = { let cur = this._bms.getItemGUID(newId); if (cur == record.id) this._log.warn("Item " + newId + " already has GUID " + record.id); - else { + else this._bms.setItemGUID(newId, record.id); - Engines.get("bookmarks")._tracker._all[newId] = record.id; // HACK - see tracker - } } }, @@ -441,7 +439,6 @@ BookmarksStore.prototype = { this._log.debug("Changing GUID " + oldID + " to " + newID); this._bms.setItemGUID(itemId, newID); - Engines.get("bookmarks")._tracker._all[itemId] = newID; // HACK - see tracker }, _getNode: function BStore__getNode(folder) { @@ -664,22 +661,31 @@ BookmarksTracker.prototype = { QueryInterface: XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver, - Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS + Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS ]), _init: function BMT__init() { this.__proto__.__proto__._init.call(this); - // Ignore changes to the special roots. We use special names for them, so - // ignore their "real" places guid as well as ours, just in case + // Ignore changes to the special roots let store = new BookmarksStore(); - for (let [weaveId, id] in Iterator(store.specialIds)) { - this.ignoreID(weaveId); - } + for (let [weaveId, id] in Iterator(store.specialIds)) + this.ignoreID(this._bms.getItemGUID(id)); this._bms.addObserver(this, false); }, + /** + * Add a bookmark (places) id to be uploaded and bump up the sync score + * + * @param itemId + * Places internal id of the bookmark to upload + */ + _addId: function BMT__addId(itemId) { + if (this.addChangedID(this._bms.getItemGUID(itemId))) + this._upScore(); + }, + /* Every add/remove/change is worth 10 points */ _upScore: function BMT__upScore() { this._score += 10; @@ -689,14 +695,20 @@ BookmarksTracker.prototype = { * Determine if a change should be ignored: we're ignoring everything or the * folder is for livemarks * - * @param folder + * @param itemId + * Item under consideration to ignore + * @param folder (optional) * Folder of the item being changed */ - _ignore: function BMT__ignore(folder) { + _ignore: function BMT__ignore(itemId, folder) { // 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) + folder = this._bms.getFolderIdForItem(itemId); + let tags = this._bms.tagsFolder; // Ignore changes to tags (folders under the tags folder) if (folder == tags) @@ -711,23 +723,23 @@ BookmarksTracker.prototype = { }, onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) { - if (this._ignore(folder)) + if (this._ignore(itemId, folder)) return; this._log.trace("onItemAdded: " + itemId); - if (this.addChangedID(this._bms.getItemGUID(itemId))) - this._upScore(); + this._addId(itemId); }, onBeforeItemRemoved: function BMT_onBeforeItemRemoved(itemId) { - this._log.trace("onItemBeforeRemoved: " + itemId); - if (this.addChangedID(this._bms.getItemGUID(itemId))) - this._upScore(); + if (this._ignore(itemId)) + return; + + this._log.trace("onBeforeItemRemoved: " + itemId); + this._addId(itemId); }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { - let folder = this._bms.getFolderIdForItem(itemId); - if (this._ignore(folder)) + if (this._ignore(itemId)) return; // ignore annotations except for the ones that we sync @@ -740,21 +752,15 @@ BookmarksTracker.prototype = { this._log.trace("onItemChanged: " + itemId + (", " + property + (isAnno? " (anno)" : "")) + (value? (" = \"" + value + "\"") : "")); - - // we should never really get onItemChanged for a deleted item - let guid = this._bms.getItemGUID(itemId); - if (guid && this.addChangedID(guid)) - this._upScore(); + this._addId(itemId); }, onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) { - let folder = this._bms.getFolderIdForItem(itemId); - if (this._ignore(folder)) + if (this._ignore(itemId)) return; this._log.trace("onItemMoved: " + itemId); - if (this.addChangedID(this._bms.itemGUID(itemId))) - this._upScore(); + this._addId(itemId); }, onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, From 0d418aff8c8af61b58a438dd11717336f5b53c2a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 17 Jun 2009 09:28:15 -0700 Subject: [PATCH 1184/1860] Bug 498924 - Refactor bookmark special ids out of BookmarksStore Lazily load places ids for a given weave id/place name pair into kSpecialIds and update all consumers. Expose a Svc.Bookmark in utils. --- services/sync/modules/engines/bookmarks.js | 39 +++++++++++----------- services/sync/modules/util.js | 1 + 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index a2175c2d07aa..912ba46fed74 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -69,6 +69,17 @@ Cu.import("resource://weave/type_records/bookmark.js"); Function.prototype.async = Async.sugar; +// Lazily initialize the special top level folders +let kSpecialIds = {}; +[["menu", "bookmarksMenuFolder"], + ["places", "placesRoot"], + ["tags", "tagsFolder"], + ["toolbar", "toolbarFolder"], + ["unfiled", "unfiledBookmarksFolder"], +].forEach(function([weaveId, placeName]) { + Utils.lazy2(kSpecialIds, weaveId, function() Svc.Bookmark[placeName]); +}); + function BookmarksEngine() { this._init(); } @@ -84,19 +95,10 @@ BookmarksEngine.prototype = { function BookmarksStore() { this._init(); - - // Initialize the special top level folders - [["menu", "bookmarksMenuFolder"], - ["places", "placesRoot"], - ["tags", "tagsFolder"], - ["toolbar", "toolbarFolder"], - ["unfiled", "unfiledBookmarksFolder"], - ].forEach(function(top) this.specialIds[top[0]] = this._bms[top[1]], this); } BookmarksStore.prototype = { __proto__: Store.prototype, _logName: "BStore", - specialIds: {}, __bms: null, get _bms() { @@ -153,14 +155,14 @@ BookmarksStore.prototype = { }, _getItemIdForGUID: function BStore__getItemIdForGUID(GUID) { - if (GUID in this.specialIds) - return this.specialIds[GUID]; + if (GUID in kSpecialIds) + return kSpecialIds[GUID]; return this._bms.getItemIdForGUID(GUID); }, _getWeaveIdForItem: function BStore__getWeaveIdForItem(placeId) { - for (let [weaveId, id] in Iterator(this.specialIds)) + for (let [weaveId, id] in Iterator(kSpecialIds)) if (placeId == id) return weaveId; @@ -168,7 +170,7 @@ BookmarksStore.prototype = { }, _isToplevel: function BStore__isToplevel(placeId) { - for (let [weaveId, id] in Iterator(this.specialIds)) + for (let [weaveId, id] in Iterator(kSpecialIds)) if (placeId == id) return true; @@ -296,7 +298,7 @@ BookmarksStore.prototype = { }, remove: function BStore_remove(record) { - if (record.id in this.specialIds) { + if (record.id in kSpecialIds) { this._log.warn("Attempted to remove root node (" + record.id + "). Skipping record removal."); return; @@ -332,7 +334,7 @@ BookmarksStore.prototype = { update: function BStore_update(record) { let itemId = this._getItemIdForGUID(record.id); - if (record.id in this.specialIds) { + if (record.id in kSpecialIds) { this._log.debug("Skipping update for root node."); return; } @@ -618,7 +620,7 @@ BookmarksStore.prototype = { getAllIDs: function BStore_getAllIDs() { let items = {}; - for (let [weaveId, id] in Iterator(this.specialIds)) + for (let [weaveId, id] in Iterator(kSpecialIds)) if (weaveId != "places" && weaveId != "tags") this._getChildren(weaveId, true, items); return items; @@ -631,7 +633,7 @@ BookmarksStore.prototype = { }, wipe: function BStore_wipe() { - for (let [weaveId, id] in Iterator(this.specialIds)) + for (let [weaveId, id] in Iterator(kSpecialIds)) if (weaveId != "places") this._bms.removeFolderChildren(id); } @@ -668,8 +670,7 @@ BookmarksTracker.prototype = { this.__proto__.__proto__._init.call(this); // Ignore changes to the special roots - let store = new BookmarksStore(); - for (let [weaveId, id] in Iterator(store.specialIds)) + for (let [weaveId, id] in Iterator(kSpecialIds)) this.ignoreID(this._bms.getItemGUID(id)); this._bms.addObserver(this, false); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 199c6a0726b9..dbfb17ea5a7f 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -651,6 +651,7 @@ Utils.EventListener.prototype = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); [["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], + ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], From c59f93970b792591e05d4b6b3451937e57d474a2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 22 Jun 2009 15:53:42 -0700 Subject: [PATCH 1185/1860] Bug 487282 - "browser is null" error in tabs.js Share browser detection code for register and unregister tab listeners and better check for tabbrowser windows. --- services/sync/modules/engines/tabs.js | 31 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index f5d5ddabd5fd..5f9093826c0b 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -380,14 +380,24 @@ TabTracker.prototype = { } }, - _registerListenersForWindow: function TabTracker__registerListen(window) { - if (! window.getBrowser) { - return; - } + _getBrowser: function TabTracker__getBrowser(window) { + // Make sure the window is browser-like + if (typeof window.getBrowser != "function") + return null; + + // Make sure it's a tabbrowser-like window let browser = window.getBrowser(); - if (! browser.tabContainer) { + if (browser == null || typeof browser.tabContainer != "object") + return null; + + return browser; + }, + + _registerListenersForWindow: function TabTracker__registerListen(window) { + let browser = this._getBrowser(window); + if (browser == null) return; - } + //this._log.trace("Registering tab listeners in new window.\n"); //dump("Tab listeners registered!\n"); let container = browser.tabContainer; @@ -397,13 +407,10 @@ TabTracker.prototype = { }, _unRegisterListenersForWindow: function TabTracker__unregister(window) { - if (! window.getBrowser) { + let browser = this._getBrowser(window); + if (browser == null) return; - } - let browser = window.getBrowser(); - if (! browser.tabContainer) { - return; - } + let container = browser.tabContainer; container.removeEventListener("TabOpen", this.onTabOpened, false); container.removeEventListener("TabClose", this.onTabClosed, false); From 9e3e6075427d88d47155a94fdf24d744e283f4e7 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Mon, 22 Jun 2009 18:19:42 -0700 Subject: [PATCH 1186/1860] Change priority of some log messages to trace --- services/sync/modules/engines/passwords.js | 6 +++--- services/sync/modules/engines/prefs.js | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index c0b1d6839939..b9ff74bb4e9c 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -115,7 +115,7 @@ PasswordStore.prototype = { this._log.info(logins.length + " items matching " + id + " found."); return logins[0]; } else { - this._log.warn("No items matching " + id + " found. Ignoring"); + this._log.trace("No items matching " + id + " found. Ignoring"); } return false; }, @@ -137,11 +137,11 @@ PasswordStore.prototype = { let oldLogin = this._getLoginFromGUID(oldID); if (!oldLogin) { - this._log.warn("Can't change item ID: item doesn't exist"); + this._log.trace("Can't change item ID: item doesn't exist"); return; } if (this._getLoginFromGUID(newID)) { - this._log.warn("Can't change item ID: new ID already in use"); + this._log.trace("Can't change item ID: new ID already in use"); return; } diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 3c2165e71bad..31cba2308358 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -128,7 +128,7 @@ PrefStore.prototype = { pref["value"] = this._prefs.getBoolPref(toSync[i]); break; default: - this._log.warn("Unsupported pref type for " + toSync[i]); + this._log.trace("Unsupported pref type for " + toSync[i]); } if ("value" in pref) values[values.length] = pref; @@ -150,7 +150,7 @@ PrefStore.prototype = { this._prefs.setBoolPref(values[i]["name"], values[i]["value"]); break; default: - this._log.warn("Unexpected preference type: " + values[i]["type"]); + this._log.trace("Unexpected preference type: " + values[i]["type"]); } } }, @@ -163,7 +163,7 @@ PrefStore.prototype = { }, changeItemID: function PrefStore_changeItemID(oldID, newID) { - this._log.warn("PrefStore GUID is constant!"); + this._log.trace("PrefStore GUID is constant!"); }, itemExists: function FormStore_itemExists(id) { @@ -185,11 +185,11 @@ PrefStore.prototype = { }, create: function PrefStore_create(record) { - this._log.warn("Ignoring create request"); + this._log.trace("Ignoring create request"); }, remove: function PrefStore_remove(record) { - this._log.warn("Ignoring remove request") + this._log.trace("Ignoring remove request") }, update: function PrefStore_update(record) { @@ -198,7 +198,7 @@ PrefStore.prototype = { }, wipe: function PrefStore_wipe() { - this._log.warn("Ignoring wipe request"); + this._log.trace("Ignoring wipe request"); } }; From 85124c0094a5c3001ed4d041fa17e2a05c21afb6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 25 Jun 2009 14:26:33 -0700 Subject: [PATCH 1187/1860] Bug 500518 - Update fennec login screen to use richpref styling Use a richlistbox with richpref styling for input/buttons like the rest of the weave prefs. Hide password/passphrase after entering, but keep them visible when entering for the first time. --- services/sync/locales/en-US/fennec.properties | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/sync/locales/en-US/fennec.properties b/services/sync/locales/en-US/fennec.properties index cbd1e9abac7e..34e69f9f4d71 100644 --- a/services/sync/locales/en-US/fennec.properties +++ b/services/sync/locales/en-US/fennec.properties @@ -13,10 +13,6 @@ fennec.sync.error.generic = Weave had an error when trying to sync. fennec.sync.status = Syncing %S... fennec.sync.status.detail = Syncing (%S %S)... -fennec.username.here = Your Username Here -fennec.password.here = Your Password Here -fennec.passphrase.here = Your Passphrase Here - fennec.turn.weave.off = Turn Weave Off fennec.turn.weave.on = Turn Weave On From b31d60959eede7559e5161bccbd06271940cbf69 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 25 Jun 2009 16:13:52 -0700 Subject: [PATCH 1188/1860] Add some docs for Resource module --- services/sync/modules/resource.js | 117 ++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index b83d2ef52e5b..44e835ed67ff 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -52,6 +52,10 @@ Cu.import("resource://weave/auth.js"); Function.prototype.async = Async.sugar; +// = RequestException = +// +// This function raises an exception through the call +// stack for a failed network request. function RequestException(resource, action, request) { this._resource = resource; this._action = action; @@ -69,12 +73,21 @@ RequestException.prototype = { } }; +// = Resource = +// +// Represents a remote network resource, identified by a URI. function Resource(uri) { this._init(uri); } Resource.prototype = { _logName: "Net.Resource", + // ** {{{ Resource.authenticator }}} ** + // + // Getter and setter for the authenticator module + // responsible for this particular resource. The authenticator + // module may modify the headers to perform authentication + // while performing a request for the resource, for example. get authenticator() { if (this._authenticator) return this._authenticator; @@ -85,6 +98,11 @@ Resource.prototype = { this._authenticator = value; }, + // ** {{{ Resource.headers }}} ** + // + // Getter for access to received headers after the request + // for the resource has been made, setter for headers to be included + // while making a request for the resource. get headers() { return this.authenticator.onRequest(this._headers); }, @@ -99,6 +117,9 @@ Resource.prototype = { } }, + // ** {{{ Resource.uri }}} ** + // + // URI representing this resource. get uri() { return this._uri; }, @@ -111,12 +132,18 @@ Resource.prototype = { this._uri = value; }, + // ** {{{ Resource.spec }}} ** + // + // Get the string representation of the URI. get spec() { if (this._uri) return this._uri.spec; return null; }, + // ** {{{ Resource.data }}} ** + // + // Get and set the data encapulated in the resource. _data: null, get data() this._data, set data(value) { @@ -131,6 +158,11 @@ Resource.prototype = { get downloaded() this._downloaded, get dirty() this._dirty, + // ** {{{ Resource.filters }}} ** + // + // Filters are used to perform pre and post processing on + // requests made for resources. Use these methods to add, + // remove and clear filters applied to the resource. _filters: null, pushFilter: function Res_pushFilter(filter) { this._filters.push(filter); @@ -151,19 +183,28 @@ Resource.prototype = { this._filters = []; }, + // ** {{{ Resource._createRequest }}} ** + // + // This method returns a new IO Channel for requests to be made + // through. It is never called directly, only {{{_request}}} uses it + // to obtain a request channel. + // _createRequest: function Res__createRequest() { this._lastChannel = Svc.IO.newChannel(this.spec, null, null). QueryInterface(Ci.nsIRequest); + // Always validate the cache: let loadFlags = this._lastChannel.loadFlags; loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; this._lastChannel.loadFlags = loadFlags; this._lastChannel = this._lastChannel.QueryInterface(Ci.nsIHttpChannel); - + + // Setup a callback to handle bad HTTPS certificates. this._lastChannel.notificationCallbacks = new badCertListener(); - - let headers = this.headers; // avoid calling the authorizer more than once + + // Avoid calling the authorizer more than once + let headers = this.headers; for (let key in headers) { if (key == 'Authorization') this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); @@ -178,6 +219,10 @@ Resource.prototype = { this._lastProgress = Date.now(); }, + // ** {{{ Resource.filterUpload }}} ** + // + // Apply pre-request filters. Currently, this is done before + // any PUT request. filterUpload: function Res_filterUpload(onComplete) { let fn = function() { let self = yield; @@ -188,6 +233,10 @@ Resource.prototype = { fn.async(this, onComplete); }, + // ** {{{ Resource.filterDownload }}} ** + // + // Apply post-request filters. Currently, this done after + // any GET request. filterDownload: function Res_filterUpload(onComplete) { let fn = function() { let self = yield; @@ -199,6 +248,11 @@ Resource.prototype = { fn.async(this, onComplete); }, + // ** {{{ Resource._request }}} ** + // + // Perform a particular HTTP request on the resource. This method + // is never called directly, but is used by the high-level + // {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods. _request: function Res__request(action, data) { let self = yield; let iter = 0; @@ -207,6 +261,8 @@ Resource.prototype = { if ("undefined" != typeof(data)) this._data = data; + // PUT and POST are trreated differently because + // they have payload data. if ("PUT" == action || "POST" == action) { yield this.filterUpload(self.cb); this._log.trace(action + " Body:\n" + this._data); @@ -222,6 +278,8 @@ Resource.prototype = { channel.setUploadStream(stream, type, this._data.length); } + // Setup a channel listener so that the actual network operation + // is performed asynchronously. let listener = new ChannelListener(self.cb, this._onProgress, this._log); channel.requestMethod = action; this._data = yield channel.asyncOpen(listener, null); @@ -253,23 +311,40 @@ Resource.prototype = { self.done(this._data); }, + // ** {{{ Resource.get }}} ** + // + // Perform an asynchronous HTTP GET for this resource. + // onComplete will be called on completion of the request. get: function Res_get(onComplete) { this._request.async(this, onComplete, "GET"); }, - + + // ** {{{ Resource.get }}} ** + // + // Perform a HTTP PUT for this resource. put: function Res_put(onComplete, data) { this._request.async(this, onComplete, "PUT", data); }, + // ** {{{ Resource.post }}} ** + // + // Perform a HTTP POST for this resource. post: function Res_post(onComplete, data) { this._request.async(this, onComplete, "POST", data); }, + // ** {{{ Resource.delete }}} ** + // + // Perform a HTTP DELETE for this resource. delete: function Res_delete(onComplete) { this._request.async(this, onComplete, "DELETE"); } }; +// = ChannelListener = +// +// This object implements the {{{nsIStreamListener}}} interface +// and is called as the network operation proceeds. function ChannelListener(onComplete, onProgress, logger) { this._onComplete = onComplete; this._onProgress = onProgress; @@ -302,11 +377,24 @@ ChannelListener.prototype = { } }; -/* Parses out single WBOs from a full dump */ +// = RecordParser = +// +// This object retrives single WBOs from a stream of incoming +// JSON. This should be useful for performance optimizations +// in cases where memory is low (on Fennec, for example). +// +// XXX: Note that this parser is currently not used because we +// are yet to figure out the best way to integrate it with the +// asynchronous nature of {{{ChannelListener}}}. Ed's work in the +// Sync module will make this easier in the future. function RecordParser(data) { this._data = data; } RecordParser.prototype = { + // ** {{{ RecordParser.getNextRecord }}} ** + // + // Returns a single WBO from the stream of JSON received + // so far. getNextRecord: function RecordParser_getNextRecord() { let start; let bCount = 0; @@ -333,11 +421,23 @@ RecordParser.prototype = { return false; }, + // ** {{{ RecordParser.append }}} ** + // + // Appends data to the current internal buffer + // of received data by the parser. The buffer + // is continously processed as {{{getNextRecord}}} + // is called, so the caller need not keep a copy + // of the data passed to this function. append: function RecordParser_append(data) { this._data += data; } }; +// = JsonFilter = +// +// Currently, the only filter used in conjunction with +// {{{Resource.filters}}}. It simply encodes outgoing records +// as JSON, and decodes incoming JSON into JS objects. function JsonFilter() { let level = "Debug"; try { level = Utils.prefs.getCharPref("log.logger.network.jsonFilter"); } @@ -359,6 +459,13 @@ JsonFilter.prototype = { } }; +// = badCertListener = +// +// We use this listener to ignore bad HTTPS +// certificates and continue a request on a network +// channel. Probably not a very smart thing to do, +// but greatly simplifies debugging and is just very +// convenient. function badCertListener() { } badCertListener.prototype = { From 53ff5ba7bf0d8b2bd22e2f94f98a28c244523760 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 25 Jun 2009 17:27:03 -0700 Subject: [PATCH 1189/1860] Bug 500551 - Failed to load XPCOM component WeaveCrypto.so Only try loading WeaveCrypto maemo binary on Linux (until we get Linux_arm-msvc). --- services/sync/WeaveCrypto.so | Bin 56986 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 services/sync/WeaveCrypto.so diff --git a/services/sync/WeaveCrypto.so b/services/sync/WeaveCrypto.so deleted file mode 100755 index 0259efa9c6626898ec69c1abcc980b723f688f0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56986 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 LT7qN0kP!bL_4K;I From e945f09f6271ddd0d6e2cd3b2055ee52e6ea0ca8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 25 Jun 2009 22:46:14 -0700 Subject: [PATCH 1190/1860] Bug 500598 - Undefined item in tabContainer.childNodes after closing tabs The childNodes NodeList keeps its indices even after removal, so it's safer to convert the array-like thing into an actual array. Condense the QueryInterface code to the instanceof magic. --- services/sync/modules/engines/tabs.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 5f9093826c0b..5d22706cf35c 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -200,16 +200,12 @@ TabStore.prototype = { while (enumerator.hasMoreElements()) { let window = enumerator.getNext(); let tabContainer = window.getBrowser().tabContainer; - for each (let tabChild in tabContainer.childNodes) { - if (!tabChild) { - this._log.warn("Undefined item in tabContainer.childNodes."); - continue; - } - if (!tabChild.QueryInterface) - continue; - let tab = tabChild.QueryInterface(Ci.nsIDOMNode); - if (!tab) + + // Grab each tab child from the array-like child NodeList + for each (let tab in Array.slice(tabContainer.childNodes)) { + if (!(tab instanceof Ci.nsIDOMNode)) continue; + let tabState = JSON.parse(this._sessionStore.getTabState(tab)); // Skip empty (i.e. just-opened, no history yet) tabs: if (tabState.entries.length == 0) From eb82f2642ea2ca5bc4db9d0a24d02835268ae6bd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 25 Jun 2009 22:52:28 -0700 Subject: [PATCH 1191/1860] Convert a log.info to log.debug for passwords so it doesn't clutter the brief log. --- services/sync/modules/engines/passwords.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index b9ff74bb4e9c..982fe05a4927 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -112,7 +112,7 @@ PasswordStore.prototype = { let logins = Svc.Login.searchLogins({}, prop); if (logins.length > 0) { - this._log.info(logins.length + " items matching " + id + " found."); + this._log.debug(logins.length + " items matching " + id + " found."); return logins[0]; } else { this._log.trace("No items matching " + id + " found. Ignoring"); From eb276c27cb0de0564541aed74c62388bc5413367 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 1 Jul 2009 11:51:52 -0700 Subject: [PATCH 1192/1860] switch from autogenerated load-module tests to a single test that loads all modules --- services/sync/tests/unit/test_load_modules.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 services/sync/tests/unit/test_load_modules.js diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js new file mode 100644 index 000000000000..13dec34b8e7b --- /dev/null +++ b/services/sync/tests/unit/test_load_modules.js @@ -0,0 +1,48 @@ +const modules = ["async.js", + "auth.js", + "base_records/collection.js", + "base_records/crypto.js", + "base_records/keys.js", + "base_records/wbo.js", + "constants.js", + "engines/bookmarks.js", + "engines/clientData.js", + "engines/cookies.js", + "engines/extensions.js", + "engines/forms.js", + "engines/history.js", + "engines/input.js", + "engines/microformats.js", + "engines/passwords.js", + "engines/plugins.js", + "engines/prefs.js", + "engines/tabs.js", + "engines/themes.js", + "engines.js", + "ext/Observers.js", + "ext/Preferences.js", + "faultTolerance.js", + "identity.js", + "log4moz.js", + "notifications.js", + "resource.js", + "service.js", + "stores.js", + "trackers.js", + "type_records/bookmark.js", + "type_records/clientData.js", + "type_records/forms.js", + "type_records/history.js", + "type_records/passwords.js", + "type_records/prefs.js", + "type_records/tabs.js", + "util.js", + "wrap.js"]; + +function run_test() { + for each (let m in modules) { + dump("Attempting to load resource://weave/" + m + "\n"); + Components.utils.import("resource://weave/" + m, {}); + } +} + From 03f07a7697dfe40b925c559652c6860db9e8f2ae Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 9 Jul 2009 13:58:19 -0700 Subject: [PATCH 1193/1860] Remove async and wrap from test_load_modules. --- services/sync/tests/unit/test_load_modules.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js index 13dec34b8e7b..ca62312cd5ed 100644 --- a/services/sync/tests/unit/test_load_modules.js +++ b/services/sync/tests/unit/test_load_modules.js @@ -1,4 +1,4 @@ -const modules = ["async.js", +const modules = [ "auth.js", "base_records/collection.js", "base_records/crypto.js", @@ -37,7 +37,7 @@ const modules = ["async.js", "type_records/prefs.js", "type_records/tabs.js", "util.js", - "wrap.js"]; +]; function run_test() { for each (let m in modules) { From 48c4a9585ace74c19990e9d0e3ab3c491d34646f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 9 Jul 2009 17:15:00 -0700 Subject: [PATCH 1194/1860] Bug 502482 - error while syncing bookmarks from server Set the item type for generic PlacesItems, but don't do anything when trying to create it. --- services/sync/modules/engines/bookmarks.js | 3 +++ services/sync/modules/type_records/bookmark.js | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e38445b8248b..345d86a688dd 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -280,6 +280,9 @@ BookmarksStore.prototype = { this._log.debug(" -> creating separator"); newId = this._bms.insertSeparator(parentId, record.sortindex); break; + case "item": + this._log.debug(" -> got a generic places item.. do nothing?"); + break; default: this._log.error("_create: Unknown item type: " + record.type); break; diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index caaa197f2721..9e0518758be1 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -75,8 +75,10 @@ PlacesItem.prototype = { return Livemark; case "separator": return BookmarkSeparator; + case "item": + return PlacesItem; } - throw "Unknown places item object type"; + throw "Unknown places item object type: " + type; }, __proto__: CryptoWrapper.prototype, @@ -84,8 +86,8 @@ PlacesItem.prototype = { _PlacesItem_init: function BmkItemRec_init(uri) { this._CryptoWrap_init(uri); - this.cleartext = { - }; + this.cleartext = {}; + this.type = "item"; }, }; From e8a4268525b617a759ede553c01f93caabddc647 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 9 Jul 2009 17:44:41 -0700 Subject: [PATCH 1195/1860] Strip newline from various debug output. --- services/sync/modules/faultTolerance.js | 2 +- services/sync/modules/resource.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index 124f43c36d3d..d0766dbbce46 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -64,7 +64,7 @@ FTService.prototype = { onException: function FTS_onException(exception) { this._lastException = exception; - this._log.debug("\n" + Utils.stackTrace(exception)); + this._log.debug(Utils.stackTrace(exception)); return true; // continue sync if thrown by a sync engine } }; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index c2db2151320d..6b1cc74556c2 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -253,7 +253,7 @@ Resource.prototype = { // they have payload data. if ("PUT" == action || "POST" == action) { this.filterUpload(); - this._log.trace(action + " Body:\n" + this._data); + this._log.trace(action + " Body: " + this._data); let type = ('Content-Type' in this._headers)? this._headers['Content-Type'] : 'text/plain'; @@ -276,7 +276,7 @@ Resource.prototype = { if (!channel.requestSucceeded) { this._log.debug(action + " request failed (" + channel.responseStatus + ")"); if (this._data) - this._log.debug("Error response: \n" + this._data); + this._log.debug("Error response: " + this._data); throw new RequestException(this, action, channel); } else { @@ -291,7 +291,7 @@ Resource.prototype = { break; case "GET": case "POST": - this._log.trace(action + " Body:\n" + this._data); + this._log.trace(action + " Body: " + this._data); this.filterDownload(); break; } From 404fe51c0466c82b41009900c2e6760d75c3bc87 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 9 Jul 2009 18:05:58 -0700 Subject: [PATCH 1196/1860] Print the exception with the stack on fault tolerance. --- services/sync/modules/faultTolerance.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index d0766dbbce46..620c38784f1b 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -62,9 +62,9 @@ FTService.prototype = { // our current state }, - onException: function FTS_onException(exception) { - this._lastException = exception; - this._log.debug(Utils.stackTrace(exception)); + onException: function FTS_onException(ex) { + this._lastException = ex; + this._log.debug(Utils.exceptionStr(ex) + " " + Utils.stackTrace(ex)); return true; // continue sync if thrown by a sync engine } }; From 695dce72b13e8da8c51e2bca593df3bf6b6547f4 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 10 Jul 2009 11:57:56 -0400 Subject: [PATCH 1197/1860] file moves --- .../components}/IWeaveCrypto.xpt | Bin .../Darwin/components/WeaveCrypto.dylib | Bin 0 -> 65776 bytes .../platform/Linux/components/WeaveCrypto.so | Bin 0 -> 56986 bytes .../Linux_x86-gcc3/components/WeaveCrypto.so | Bin 0 -> 53420 bytes .../Linux_x86_64-gcc3/components/WeaveCrypto.so | Bin 0 -> 67182 bytes .../WINNT_x86-msvc/components/WeaveCrypto.dll | Bin 0 -> 63488 bytes 6 files changed, 0 insertions(+), 0 deletions(-) rename services/{sync => crypto/components}/IWeaveCrypto.xpt (100%) create mode 100755 services/crypto/platform/Darwin/components/WeaveCrypto.dylib create mode 100755 services/crypto/platform/Linux/components/WeaveCrypto.so create mode 100755 services/crypto/platform/Linux_x86-gcc3/components/WeaveCrypto.so create mode 100644 services/crypto/platform/Linux_x86_64-gcc3/components/WeaveCrypto.so create mode 100644 services/crypto/platform/WINNT_x86-msvc/components/WeaveCrypto.dll diff --git a/services/sync/IWeaveCrypto.xpt b/services/crypto/components/IWeaveCrypto.xpt similarity index 100% rename from services/sync/IWeaveCrypto.xpt rename to services/crypto/components/IWeaveCrypto.xpt diff --git a/services/crypto/platform/Darwin/components/WeaveCrypto.dylib b/services/crypto/platform/Darwin/components/WeaveCrypto.dylib new file mode 100755 index 0000000000000000000000000000000000000000..a7ac2d1bb3410738e66d3ef638cde884cfe961d6 GIT binary patch literal 65776 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 N%T@hw?{ydA{{nY9n>qjh literal 0 HcmV?d00001 diff --git a/services/crypto/platform/Linux/components/WeaveCrypto.so b/services/crypto/platform/Linux/components/WeaveCrypto.so new file mode 100755 index 0000000000000000000000000000000000000000..0259efa9c6626898ec69c1abcc980b723f688f0b GIT binary patch literal 56986 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 LT7qN0kP!bL_4K;I literal 0 HcmV?d00001 diff --git a/services/crypto/platform/Linux_x86-gcc3/components/WeaveCrypto.so b/services/crypto/platform/Linux_x86-gcc3/components/WeaveCrypto.so new file mode 100755 index 0000000000000000000000000000000000000000..2c78e87717ce86eaf9f50d295b46c661a6eaebbc GIT binary patch literal 53420 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 Date: Fri, 10 Jul 2009 14:15:21 -0400 Subject: [PATCH 1198/1860] new makefile for crypto --- services/crypto/Makefile | 282 +-------------------------------------- 1 file changed, 5 insertions(+), 277 deletions(-) mode change 100755 => 100644 services/crypto/Makefile diff --git a/services/crypto/Makefile b/services/crypto/Makefile old mode 100755 new mode 100644 index 17277a60d116..b52d26debb31 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -21,7 +21,6 @@ # # Contributor(s): # Dan Mills (original author) -# Godwin Chan (Darwin Universal Binary) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -37,289 +36,18 @@ # # ***** END LICENSE BLOCK ***** -# OS detection - -sys := $(shell uname -s) - -ifeq ($(sys), Darwin) - os = Darwin - compiler = gcc3 - cxx = c++ - so = dylib - cppflags += -dynamiclib -DDEBUG -else -ifeq ($(sys), Linux) - os = Linux - compiler = gcc3 - cxx = c++ - so = so - cppflags += -shared -else -ifeq ($(sys), MINGW32_NT-6.0) - os = WINNT - compiler = msvc - cxx = cl - so = dll -else -ifeq ($(sys), MINGW32_NT-5.1) - os = WINNT - compiler = msvc - cxx = cl - so = dll -else - $(error Sorry, your os is unknown/unsupported: $(sys)) -endif -endif -endif -endif - -# Arch detection - -machine := $(shell uname -m) - -ifeq ($(machine), arm) - arch = arm -else -ifeq ($(machine), i386) - arch = x86 -else -ifeq ($(machine), i586) - arch = x86 -else -ifeq ($(machine), i686) - arch = x86 -else -ifeq ($(machine), ppc) - arch = ppc -else -ifeq ($(machine), Power Macintosh) - arch = ppc -else -ifeq ($(machine), x86_64) - arch = x86_64 -else - $(error: Sorry, your architecture is unknown/unsupported: $(machine)) -endif -endif -endif -endif -endif -endif -endif - -# Universal binary so no need for $(arch) for Darwin - -ifeq ($(sys), Darwin) - platform = $(os) -else - platform = $(os)_$(arch)-$(compiler) -endif - -################################################################### -# Target and objects - -ifeq ($(sys), Darwin) - target = WeaveCrypto - target_i386 = WeaveCrypto.i386 - target_ppc = WeaveCrypto.ppc - so_target = $(target:=.$(so)) - so_target_i386 = $(target_i386:=.$(so)) - so_target_ppc = $(target_ppc:=.$(so)) - cpp_objects_i386 = $(cpp_sources:.cpp=.oi386) - cpp_objects_ppc = $(cpp_sources:.cpp=.oppc) -else - target = WeaveCrypto - so_target = $(target:=.$(so)) - cpp_objects = $(cpp_sources:.cpp=.o) -endif - -# source and path configurations -idl = IWeaveCrypto.idl -cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp - sdkdir ?= ${MOZSDKDIR} -destdir = .. -platformdir = $(destdir)/platform/$(platform) - -xpidl = $(sdkdir)/bin/xpidl - -# FIXME: we don't actually require this for e.g. clean ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) $(warning It should point to the location of the Gecko SDK) $(warning For example: "make sdkdir=/foo/bar/baz") $(warning Or set the MOZSDKDIR environment variable to point to it) - $(error ) + $(error) endif -idl_headers = $(idl:.idl=.h) -idl_typelib = $(idl:.idl=.xpt) -cpp_objects = $(cpp_sources:.cpp=.o) -so_target = $(target:=.$(so)) +all: build -headers = -I$(sdkdir)/include \ - -I$(sdkdir)/include/system_wrappers \ - -I$(sdkdir)/include/nss \ - -I$(sdkdir)/include/xpcom \ - -I$(sdkdir)/include/string \ - -I$(sdkdir)/include/pipnss \ - -I$(sdkdir)/include/nspr \ - -I$(sdkdir)/sdk/include +.PHONY: build -# libraries -libdirs := $(sdkdir)/lib $(sdkdir)/bin -libs := xpcomglue_s xpcom nspr4 \ - crmf smime3 ssl3 nss3 nssutil3 \ - plds4 plc4 - -ifeq ($(os), linux) - libs := xpcom_core $(libs) -endif - -# compiler and Linker Flags - -ifeq ($(os), Darwin) - libdirs := $(patsubst %,-L%,$(libdirs)) - libs := $(patsubst %,-l%,$(libs)) - cppflags_i386 += -c -pipe -Os -arch i386 \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -fshort-wchar -fpascal-strings -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) \ - -isysroot /Developer/SDKs/MacOSX10.4u.sdk - ldflags_i386 += -pthread -pipe -bundle -arch i386 \ - -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - $(libdirs) $(libs) - cppflags_ppc += -c -pipe -Os -arch ppc \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -fshort-wchar -fpascal-strings -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) \ - -isysroot /Developer/SDKs/MacOSX10.4u.sdk \ - -force_cpusubtype_ALL - ldflags_ppc += -pthread -pipe -bundle -arch ppc \ - -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - -force_cpusubtype_ALL \ - $(libdirs) $(libs) -else -ifeq ($(os), Linux) - libdirs := $(patsubst %,-L%,$(libdirs)) - libs := $(patsubst %,-l%,$(libs)) - cppflags += -pipe -Os \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) -ifneq ($(arch), arm) - cppflags += -fshort-wchar -else -endif - ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,-z,defs -Wl,-h,WeaveCrypto.so \ - -Wl,-rpath-link,$(sdkdir)/bin \ - $(sdkdir)/lib/libxpcomglue_s.a \ - $(libdirs) $(libs) -else -ifeq ($(os), WINNT) - libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) - libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) - cppflags += -c -nologo -O1 -GR- -TP -MT -Zc:wchar_t- -W3 -Gy $(headers) \ - -DNDEBUG -DTRIMMED -D_CRT_SECURE_NO_DEPRECATE=1 \ - -D_CRT_NONSTDC_NO_DEPRECATE=1 -DWINVER=0x500 -D_WIN32_WINNT=0x500 \ - -D_WIN32_IE=0x0500 -DX_DISPLAY_MISSING=1 -DMOZILLA_VERSION=\"1.9pre\" \ - -DMOZILLA_VERSION_U=1.9pre -DHAVE_SNPRINTF=1 -D_WINDOWS=1 -D_WIN32=1 \ - -DWIN32=1 -DXP_WIN=1 -DXP_WIN32=1 -DHW_THREADS=1 -DSTDC_HEADERS=1 \ - -DWIN32_LEAN_AND_MEAN=1 -DNO_X11=1 -DHAVE_MMINTRIN_H=1 \ - -DHAVE_OLEACC_IDL=1 -DHAVE_ATLBASE_H=1 -DHAVE_WPCAPI_H=1 -D_X86_=1 \ - -DD_INO=d_ino - ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ - $(libdirs) $(libs) \ - kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib - rcflags := -r $(headers) -endif -endif -endif - -###################################################################### - -.PHONY: all build install clean - -all: build # default target - -build: $(so_target) $(idl_typelib) - -install: build - mkdir -p $(destdir)/components - mkdir -p $(platformdir)/components - cp $(idl_typelib) $(destdir)/components - cp $(so_target) $(platformdir)/components - -clean: - rm -f $(so_target) $(so_target_i386) $(so_target_ppc) \ - $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ - $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp - -# rules to build the c headers and .xpt from idl -$(idl_headers): $(idl) - $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) - -$(idl_typelib): $(idl) - $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) - -# build and link rules -ifeq ($(os), Darwin) - $(so_target): $(so_target_i386) $(so_target_ppc) - lipo -create -output $(so_target) -arch ppc $(so_target_ppc) \ - -arch i386 $(so_target_i386) - chmod +x $(so_target) - - #i386 - $(cpp_objects_i386): $(cpp_sources) - $(cxx) -o $@ $(cppflags_i386) $(@:.oi386=.cpp) - - $(so_target_i386): $(idl_headers) $(cpp_objects_i386) - $(cxx) -o $@ $(ldflags_i386) $(cpp_objects_i386) - chmod +x $(so_target_i386) - - #ppc - $(cpp_objects_ppc): $(cpp_sources) - $(cxx) -o $@ $(cppflags_ppc) $(@:.oppc=.cpp) - - $(so_target_ppc): $(idl_headers) $(cpp_objects_ppc) - $(cxx) -o $@ $(ldflags_ppc) $(cpp_objects_ppc) - chmod +x $(so_target_ppc) -else -ifeq ($(os), Linux) - $(so_target): $(idl_headers) - $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) - chmod +x $@ -else -ifeq ($(os), WINNT) - $(target:=.res): $(target:=.rc) - rc -Fo$@ $(rcflags) $(target:=.rc) - - $(cpp_objects): $(cpp_sources) - $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) - - $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) - link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) - chmod +x $@ -endif -endif -endif +build: + $(MAKE) -C src install From dccde84cd952d4ca71f32f2bd63a798dbc992063 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 10 Jul 2009 23:32:04 -0400 Subject: [PATCH 1199/1860] build system updates (still WIP) --- services/crypto/Makefile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index b52d26debb31..47cc60976434 100644 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -36,6 +36,8 @@ # # ***** END LICENSE BLOCK ***** +stage_dir=../dist/stage + sdkdir ?= ${MOZSDKDIR} ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) @@ -47,7 +49,13 @@ endif all: build -.PHONY: build +.PHONY: build crypto rebuild_all + +crypto: + $(MAKE) -C src install build: - $(MAKE) -C src install + cp -R -v platform $(stage_dir) + cp -R -v components $(stage_dir) + +rebuild_all: crypto build From 39e8f9f935a5bc48533590c30634b55cca7d6226 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Sat, 11 Jul 2009 01:20:45 -0400 Subject: [PATCH 1200/1860] arglebargle --- services/sync/tests/unit/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile index 720637ea7026..1b1516ebe135 100644 --- a/services/sync/tests/unit/Makefile +++ b/services/sync/tests/unit/Makefile @@ -38,9 +38,9 @@ endif # manager when it installs an extension via a file in the /extensions/ # directory that contains the path to the extension. ifeq ($(os), WINNT) -extensiondir = $(subst /,\,$(shell pwd -W))\$(native_topsrcdir) +extensiondir = $(subst /,\,$(shell pwd -W))\$(native_topsrcdir)\dist\stage else -extensiondir = `pwd`/$(topsrcdir) +extensiondir = `pwd`/$(topsrcdir)/dist/stage endif # A command that installs the extension into the profile when the harness From 9377172b793909a59d0e66a19668f006aedaa688 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Sat, 11 Jul 2009 02:30:37 -0400 Subject: [PATCH 1201/1860] fix tests --- services/sync/tests/unit/Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile index 1b1516ebe135..17c643402f07 100644 --- a/services/sync/tests/unit/Makefile +++ b/services/sync/tests/unit/Makefile @@ -22,6 +22,9 @@ endif endif endif +topsrcdir ?= ${TOPSRCDIR} +native_topsrcdir ?= ${NATIVE_TOPSRCDIR} + ifeq ($(topsrcdir),) topsrcdir = ../.. endif @@ -38,13 +41,14 @@ endif # manager when it installs an extension via a file in the /extensions/ # directory that contains the path to the extension. ifeq ($(os), WINNT) -extensiondir = $(subst /,\,$(shell pwd -W))\$(native_topsrcdir)\dist\stage +extensiondir = $(native_topsrcdir)\dist\stage else -extensiondir = `pwd`/$(topsrcdir)/dist/stage +extensiondir = $(topsrcdir)/dist/stage endif # A command that installs the extension into the profile when the harness # creates a new profile. + configure_profile = echo "$(extensiondir)" > $(profiledir)/extensions/\{340c2bbc-ce74-4362-90b5-7c26312808ef\}; include ../harness/Makefile From eb26fa1543b37f57cded3aa85de8536ec24d4876 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 13 Jul 2009 13:40:49 -0700 Subject: [PATCH 1202/1860] Bug 503938 - Tags with a single bookmark keep generating new tag ids Share a tagging code for create/update that will tag a dummy uri temporarily while it untags the bookmark to make sure every tag has at least one child during this untag/tag process. --- services/sync/modules/engines/bookmarks.js | 25 ++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 345d86a688dd..b63877345013 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -200,8 +200,7 @@ BookmarksStore.prototype = { this._log.debug(" -> -> title is " + record.title); newId = this._bms.insertBookmark(parentId, uri, record.sortindex, record.title); - this._ts.untagURI(uri, null); - this._ts.tagURI(uri, record.tags); + this._tagURI(uri, record.tags); this._bms.setKeywordForBookmark(newId, record.keyword); if (record.description) { this._ans.setItemAnnotation(newId, "bookmarkProperties/description", @@ -361,13 +360,9 @@ BookmarksStore.prototype = { case "bmkUri": this._bms.changeBookmarkURI(itemId, Utils.makeURI(val)); break; - case "tags": { - // filter out null/undefined/empty tags - let tags = val.filter(function(t) t); - let tagsURI = this._bms.getBookmarkURI(itemId); - this._ts.untagURI(tagsURI, null); - this._ts.tagURI(tagsURI, tags); - } break; + case "tags": + this._tagURI(this._bms.getBookmarkURI(itemId), val); + break; case "keyword": this._bms.setKeywordForBookmark(itemId, val); break; @@ -617,6 +612,18 @@ BookmarksStore.prototype = { return items; }, + + _tagURI: function BStore_tagURI(bmkURI, tags) { + // Filter out any null/undefined/empty tags + tags = tags.filter(function(t) t); + + // Temporarily tag a dummy uri to preserve tag ids when untagging + let dummyURI = Utils.makeURI("about:weave"); + this._ts.tagURI(dummyURI, tags); + this._ts.untagURI(bmkURI, null); + this._ts.tagURI(bmkURI, tags); + this._ts.untagURI(dummyURI, null); + }, getAllIDs: function BStore_getAllIDs() { let items = {}; From 9c11a93af2a6051e8bd943b97872c437a2fb43e2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 13 Jul 2009 17:43:15 -0700 Subject: [PATCH 1203/1860] Bug 479189 - weave does not sync tag smart folders properly Create a new record type, BookmarkQuery, to handle place: uri smart bookmarks. Store what tag name the query wants and lookup the id for that tag on other machines on create/update. --- services/sync/modules/engines/bookmarks.js | 65 +++++++++++++++++-- .../sync/modules/type_records/bookmark.js | 21 +++++- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index b63877345013..a29e05586621 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -180,7 +180,43 @@ BookmarksStore.prototype = { return this._getItemIdForGUID(id) > 0; }, + _preprocess: function BStore_preprocess(record) { + switch (record.type) { + case "query": { + // Convert the query uri if necessary + if (record.bmkUri == null || record.folderName == null) + break; + + // Tag something so that the tag exists + let tag = record.folderName; + let dummyURI = Utils.makeURI("about:weave#BStore_preprocess"); + this._ts.tagURI(dummyURI, [tag]); + + // Look for the id of the tag (that might have just been added) + let tags = this._getNode(this._bms.tagsFolder); + if (!(tags instanceof Ci.nsINavHistoryQueryResultNode)) + break; + + tags.containerOpen = true; + for (let i = 0; i < tags.childCount; i++) { + let child = tags.getChild(i); + // Found the tag, so fix up the query to use the right id + if (child.title == tag) { + this._log.debug("query folder: " + tag + " = " + child.itemId); + record.bmkUri = record.bmkUri.replace(/([:&]folder=)\d+/, "$1" + + child.itemId); + break; + } + } + break; + } + } + }, + create: function BStore_create(record) { + // Modify the record if necessary + this._preprocess(record); + let newId; let parentId = this._getItemIdForGUID(record.parentid); @@ -191,6 +227,7 @@ BookmarksStore.prototype = { switch (record.type) { case "bookmark": + case "query": case "microsummary": { this._log.debug(" -> creating bookmark \"" + record.title + "\""); let uri = Utils.makeURI(record.bmkUri); @@ -331,6 +368,9 @@ BookmarksStore.prototype = { }, update: function BStore_update(record) { + // Modify the record if necessary + this._preprocess(record); + let itemId = this._getItemIdForGUID(record.id); if (record.id in kSpecialIds) { @@ -498,18 +538,35 @@ BookmarksStore.prototype = { switch (this._bms.getItemType(placeId)) { case this._bms.TYPE_BOOKMARK: + let bmkUri = this._bms.getBookmarkURI(placeId).spec; if (this._ms && this._ms.hasMicrosummary(placeId)) { record = new BookmarkMicsum(); let micsum = this._ms.getMicrosummary(placeId); record.generatorUri = micsum.generator.uri.spec; // breaks local generators record.staticTitle = this._getStaticTitle(placeId); + } + else { + if (bmkUri.search(/^place:/) == 0) { + record = new BookmarkQuery(); - } else { - record = new Bookmark(); + // Get the actual tag name instead of the local itemId + let folder = bmkUri.match(/[:&]folder=(\d+)/); + try { + // There might not be the tag yet when creating on a new client + if (folder != null) { + folder = folder[1]; + record.folderName = this._bms.getItemTitle(folder); + this._log.debug("query id: " + folder + " = " + record.folderName); + } + } + catch(ex) {} + } + else + record = new Bookmark(); record.title = this._bms.getItemTitle(placeId); } - record.bmkUri = this._bms.getBookmarkURI(placeId).spec; + record.bmkUri = bmkUri; record.tags = this._getTags(record.bmkUri); record.keyword = this._bms.getKeywordForBookmark(placeId); record.description = this._getDescription(placeId); @@ -618,7 +675,7 @@ BookmarksStore.prototype = { tags = tags.filter(function(t) t); // Temporarily tag a dummy uri to preserve tag ids when untagging - let dummyURI = Utils.makeURI("about:weave"); + let dummyURI = Utils.makeURI("about:weave#BStore_tagURI"); this._ts.tagURI(dummyURI, tags); this._ts.untagURI(bmkURI, null); this._ts.tagURI(bmkURI, tags); diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 9e0518758be1..767713598053 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['PlacesItem', 'Bookmark', 'BookmarkFolder', 'BookmarkMicsum', - 'Livemark', 'BookmarkSeparator']; +const EXPORTED_SYMBOLS = ["PlacesItem", "Bookmark", "BookmarkFolder", + "BookmarkMicsum", "BookmarkQuery", "Livemark", "BookmarkSeparator"]; const Cc = Components.classes; const Ci = Components.interfaces; @@ -69,6 +69,8 @@ PlacesItem.prototype = { return Bookmark; case "microsummary": return BookmarkMicsum; + case "query": + return BookmarkQuery; case "folder": return BookmarkFolder; case "livemark": @@ -124,6 +126,21 @@ BookmarkMicsum.prototype = { Utils.deferGetSet(BookmarkMicsum, "cleartext", ["generatorUri", "staticTitle"]); +function BookmarkQuery(uri) { + this._BookmarkQuery_init(uri); +} +BookmarkQuery.prototype = { + __proto__: Bookmark.prototype, + _logName: "Record.BookmarkQuery", + + _BookmarkQuery_init: function BookmarkQuery_init(uri) { + this._Bookmark_init(uri); + this.type = "query"; + }, +}; + +Utils.deferGetSet(BookmarkQuery, "cleartext", ["folderName"]); + function BookmarkFolder(uri) { this._BookmarkFolder_init(uri); } From 02301638b0c8d15d1b245a3c6b398ec800b6aa35 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 14 Jul 2009 12:28:18 -0700 Subject: [PATCH 1204/1860] Limit form history records (bug 494952, r=thunder) --- services/sync/modules/engines/forms.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 71ed1054ffcc..5a62d386ee3d 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -47,6 +47,7 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/base_records/collection.js"); Cu.import("resource://weave/type_records/forms.js"); function FormEngine() { @@ -67,9 +68,15 @@ FormEngine.prototype = { }, /* Wipe cache when sync finishes */ - _syncFinish: function FormEngine__syncFinish() { + _syncFinish: function FormEngine__syncFinish(error) { this._store.clearFormCache(); - SyncEngine.prototype._syncFinish.call(this); + + // Only leave 1 month's worth of form history + this._tracker.resetScore(); + let coll = new Collection(this.engineURL, this._recordObj); + coll.older = this.lastSync - 2592000; // 60*60*24*30 + coll.full = 0; + coll.delete(); }, _recordLike: function SyncEngine__recordLike(a, b) { @@ -111,7 +118,18 @@ FormStore.prototype = { }, get _formStatement() { - let stmnt = this._formDB.createStatement("SELECT * FROM moz_formhistory"); + // This is essentially: + // SELECT * FROM moz_formhistory ORDER BY 1.0 * (lastUsed - minLast) / + // (maxLast - minLast) * timesUsed / minTimes DESC LIMIT 200 + let stmnt = this._formDB.createStatement( + "SELECT * FROM moz_formhistory ORDER BY 1.0 * (lastUsed - \ + (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) / \ + ((SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed DESC LIMIT 1) - \ + (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) * \ + timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) \ + DESC LIMIT 200" + ); + this.__defineGetter__("_formStatement", function() stmnt); return stmnt; }, From 0d842644e13bea5539556475be2c46092c764a82 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 13:59:46 -0700 Subject: [PATCH 1205/1860] Debug log the size of PUT/POSTs messages. --- services/sync/modules/resource.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 6b1cc74556c2..fb14cfc98f77 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -253,6 +253,7 @@ Resource.prototype = { // they have payload data. if ("PUT" == action || "POST" == action) { this.filterUpload(); + this._log.debug(action + " Length: " + this._data.length); this._log.trace(action + " Body: " + this._data); let type = ('Content-Type' in this._headers)? From 4f018c3581ab02074456f17f93da5e35686b9017 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 14:01:26 -0700 Subject: [PATCH 1206/1860] Bug 504177 - Don't block the UI when syncing up/down Sync.sleep(0) to let the main thread do UI, etc. after every record encrypt or decrypt. --- services/sync/modules/engines.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9fac56106876..e27913da7c23 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -44,6 +44,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Observers.js"); +Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -354,6 +355,7 @@ SyncEngine.prototype = { this._log.error("Could not process incoming record: " + Utils.exceptionStr(e)); } + Sync.sleep(0); } if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; @@ -466,6 +468,7 @@ SyncEngine.prototype = { this._store.createMetaRecords(out.id, meta); out.encrypt(ID.get('WeaveCryptoID').password); up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient + Sync.sleep(0); } this._store.cache.enabled = true; From 757da6e9530f8f4222fdad6ab2109fa681f84dad Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 16:08:15 -0700 Subject: [PATCH 1207/1860] Bug 504196 - Enable Forms and Prefs sync by default Toggle forms/prefs preferences to true by default. --- services/sync/services-sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 83d9b5becad8..e27b6b58c876 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -16,11 +16,11 @@ pref("extensions.weave.syncOnQuit.enabled", true); pref("extensions.weave.engine.bookmarks", true); //pref("extensions.weave.engine.cookies", false); -pref("extensions.weave.engine.forms", false); +pref("extensions.weave.engine.forms", true); pref("extensions.weave.engine.history", true); //pref("extensions.weave.engine.input", false); pref("extensions.weave.engine.passwords", true); -pref("extensions.weave.engine.prefs", false); +pref("extensions.weave.engine.prefs", true); pref("extensions.weave.engine.tabs", true); pref("extensions.weave.log.appender.console", "Warn"); From 57b36f278df196c859e62f03a903be49ac61de7e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 16:51:04 -0700 Subject: [PATCH 1208/1860] Bug 504212 - Have javascript stack traces show [object Object] for various CryptoWrapper functions Pass the ID object instead of reading out the password and passing it in to encrypt/decrypt. --- services/sync/modules/base_records/crypto.js | 2 +- services/sync/modules/engines.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 9b1d4c724efb..4ffd9297021c 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -151,7 +151,7 @@ CryptoMeta.prototype = { throw "keyring doesn't contain a key for " + pubkeyUri; return this._unwrappedKey = Svc.Crypto.unwrapSymmetricKey(wrapped_key, - privkey.keyData, passphrase, privkey.salt, privkey.iv); + privkey.keyData, passphrase.password, privkey.salt, privkey.iv); }, addKey: function CryptoMeta_addKey(new_pubkey, privkey, passphrase) { diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e27913da7c23..f7a5bc13321a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -341,7 +341,7 @@ SyncEngine.prototype = { while ((item = newitems.iter.next())) { this._lowMemCheck(); try { - item.decrypt(ID.get('WeaveCryptoID').password); + item.decrypt(ID.get("WeaveCryptoID")); if (this._reconcile(item)) { count.applied++; this._applyIncoming(item); @@ -466,7 +466,7 @@ SyncEngine.prototype = { // skip getting siblings of already processed and deleted records if (!out.deleted && !(out.id in meta)) this._store.createMetaRecords(out.id, meta); - out.encrypt(ID.get('WeaveCryptoID').password); + out.encrypt(ID.get("WeaveCryptoID")); up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient Sync.sleep(0); } From 4f34080e00a46f528ce718b55ac200a55d124c0d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 17:03:57 -0700 Subject: [PATCH 1209/1860] Bug 504216 - Force various engines to be disabled Have the disabled engines return null for enabled instead of the pref's true/false. --- services/sync/modules/engines/cookies.js | 1 + services/sync/modules/engines/extensions.js | 1 + services/sync/modules/engines/input.js | 1 + services/sync/modules/engines/microformats.js | 1 + services/sync/modules/engines/plugins.js | 1 + services/sync/modules/engines/themes.js | 1 + 6 files changed, 6 insertions(+) diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 10a1e5a1f1e0..5cc46e1485e4 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -49,6 +49,7 @@ function CookieEngine(pbeId) { this._init(pbeId); } CookieEngine.prototype = { + get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, get name() { return "cookies"; }, diff --git a/services/sync/modules/engines/extensions.js b/services/sync/modules/engines/extensions.js index 123bc0587ddc..c4d43c01d268 100644 --- a/services/sync/modules/engines/extensions.js +++ b/services/sync/modules/engines/extensions.js @@ -41,6 +41,7 @@ function ExtensionEngine() { this._init(); } ExtensionEngine.prototype = { + get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, displayName: "Extensions", diff --git a/services/sync/modules/engines/input.js b/services/sync/modules/engines/input.js index dbda8fcd8e17..9ef24abf8464 100644 --- a/services/sync/modules/engines/input.js +++ b/services/sync/modules/engines/input.js @@ -50,6 +50,7 @@ function InputEngine(pbeId) { this._init(pbeId); } InputEngine.prototype = { + get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, get name() { return "input"; }, diff --git a/services/sync/modules/engines/microformats.js b/services/sync/modules/engines/microformats.js index 7da3df431e27..54b370c7f6d4 100644 --- a/services/sync/modules/engines/microformats.js +++ b/services/sync/modules/engines/microformats.js @@ -41,6 +41,7 @@ function MicroFormatEngine() { this._init(); } MicroFormatEngine.prototype = { + get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, displayName: "MicroFormats", diff --git a/services/sync/modules/engines/plugins.js b/services/sync/modules/engines/plugins.js index dee546bf34c7..38231f94d292 100644 --- a/services/sync/modules/engines/plugins.js +++ b/services/sync/modules/engines/plugins.js @@ -41,6 +41,7 @@ function PluginEngine() { this._init(); } PluginEngine.prototype = { + get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, displayName: "Plugins", diff --git a/services/sync/modules/engines/themes.js b/services/sync/modules/engines/themes.js index f87ed070bc68..959a7356a11c 100644 --- a/services/sync/modules/engines/themes.js +++ b/services/sync/modules/engines/themes.js @@ -41,6 +41,7 @@ function ThemeEngine() { this._init(); } ThemeEngine.prototype = { + get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, displayName: "Themes", From cda39eca46fbb18941f5e173a0a322a9981af61d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 18:34:03 -0700 Subject: [PATCH 1210/1860] Bug 504230 - Use Engine's logging level for Store and Tracker Use the log.logger.engine.* prefs to determine what the Store and Tracker should log with. This requires putting the same engine name on each Store/Tracker object, so there's some duplicate string values right now. --- services/sync/modules/engines.js | 6 +----- services/sync/modules/engines/bookmarks.js | 2 ++ services/sync/modules/engines/clients.js | 2 ++ services/sync/modules/engines/forms.js | 2 ++ services/sync/modules/engines/history.js | 2 ++ services/sync/modules/engines/passwords.js | 2 ++ services/sync/modules/engines/prefs.js | 2 ++ services/sync/modules/engines/tabs.js | 2 ++ services/sync/modules/stores.js | 2 ++ services/sync/modules/trackers.js | 3 +++ services/sync/services-sync.js | 3 ++- 11 files changed, 22 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f7a5bc13321a..f732387ac650 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -171,13 +171,9 @@ Engine.prototype = { }, _init: function Engine__init() { - let levelPref = "log.logger.engine." + this.name; - let level = "Debug"; - try { level = Utils.prefs.getCharPref(levelPref); } - catch (e) { /* ignore unset prefs */ } - this._notify = Utils.notify("weave:engine:"); this._log = Log4Moz.repository.getLogger("Engine." + this.logName); + let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); this._log.level = Log4Moz.Level[level]; this._tracker; // initialize tracker to load previously changed IDs diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index a29e05586621..98ceca013a74 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -95,6 +95,7 @@ function BookmarksStore() { } BookmarksStore.prototype = { __proto__: Store.prototype, + name: "bookmarks", _logName: "BStore", __bms: null, @@ -708,6 +709,7 @@ function BookmarksTracker() { } BookmarksTracker.prototype = { __proto__: Tracker.prototype, + name: "bookmarks", _logName: "BmkTracker", file: "bookmarks", diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 28f4d2019564..2c8e65bd4f82 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -218,6 +218,7 @@ ClientStore.prototype = { ////////////////////////////////////////////////////////////////////////////// // Store.prototype Attributes + name: "clients", _logName: "Clients.Store", ////////////////////////////////////////////////////////////////////////////// @@ -271,6 +272,7 @@ function ClientTracker() { } ClientTracker.prototype = { __proto__: Tracker.prototype, + name: "clients", _logName: "ClientTracker", file: "clients", get score() 100 // always sync diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 5a62d386ee3d..2ca20f954135 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -94,6 +94,7 @@ function FormStore() { } FormStore.prototype = { __proto__: Store.prototype, + name: "forms", _logName: "FormStore", _formItems: null, @@ -215,6 +216,7 @@ function FormTracker() { } FormTracker.prototype = { __proto__: Tracker.prototype, + name: "forms", _logName: "FormTracker", file: "form", diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index dd43989dfcab..80a68fae8fa1 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -107,6 +107,7 @@ function HistoryStore() { } HistoryStore.prototype = { __proto__: Store.prototype, + name: "history", _logName: "HistStore", get _hsvc() { @@ -456,6 +457,7 @@ function HistoryTracker() { } HistoryTracker.prototype = { __proto__: Tracker.prototype, + name: "history", _logName: "HistoryTracker", file: "history", diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 85b38b7a3335..07673626a8d0 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -77,6 +77,7 @@ function PasswordStore() { } PasswordStore.prototype = { __proto__: Store.prototype, + name: "passwords", _logName: "PasswordStore", _nsLoginInfo: null, @@ -215,6 +216,7 @@ function PasswordTracker() { PasswordTracker.prototype = { __proto__: Tracker.prototype, _logName: "PasswordTracker", + name: "passwords", file: "password", _init: function PasswordTracker_init() { diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 627014a2a814..620b6f1f1056 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -79,6 +79,7 @@ function PrefStore() { } PrefStore.prototype = { __proto__: Store.prototype, + name: "prefs", _logName: "PrefStore", get _prefs() { @@ -204,6 +205,7 @@ function PrefTracker() { } PrefTracker.prototype = { __proto__: Tracker.prototype, + name: "prefs", _logName: "PrefTracker", file: "prefs", diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 85a4cc91b263..1e75c271da4e 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -127,6 +127,7 @@ function TabStore() { } TabStore.prototype = { __proto__: Store.prototype, + name: "tabs", _logName: "Tabs.Store", _filePath: "meta/tabSets", _remoteClients: {}, @@ -342,6 +343,7 @@ function TabTracker() { } TabTracker.prototype = { __proto__: Tracker.prototype, + name: "tabs", _logName: "TabTracker", file: "tab_tracker", diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 94f6a3114dfe..e476aac4a858 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -69,6 +69,8 @@ Store.prototype = { _init: function Store__init() { this._log = Log4Moz.repository.getLogger("Store." + this._logName); + let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); + this._log.level = Log4Moz.Level[level]; }, applyIncoming: function Store_applyIncoming(record) { diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 867e9858f2ae..16cf578d2761 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -66,6 +66,9 @@ Tracker.prototype = { _init: function T__init() { this._log = Log4Moz.repository.getLogger(this._logName); + let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); + this._log.level = Log4Moz.Level[level]; + this._score = 0; this._ignored = []; this.ignoreAll = false; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index e27b6b58c876..fc453b496ffc 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -34,10 +34,11 @@ pref("extensions.weave.log.logger.async", "Debug"); pref("extensions.weave.log.logger.authenticator", "Debug"); pref("extensions.weave.log.logger.network.resources", "Debug"); pref("extensions.weave.log.logger.engine.bookmarks", "Debug"); +pref("extensions.weave.log.logger.engine.clients", "Debug"); pref("extensions.weave.log.logger.engine.forms", "Debug"); pref("extensions.weave.log.logger.engine.history", "Debug"); +pref("extensions.weave.log.logger.engine.passwords", "Debug"); pref("extensions.weave.log.logger.engine.tabs", "Debug"); -pref("extensions.weave.log.logger.engine.clients", "Debug"); pref("extensions.weave.log.logger.authenticator", "Debug"); pref("extensions.weave.network.numRetries", 2); From 353e7a75870815f4623b69d92506c9b7a691f596 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 18:44:41 -0700 Subject: [PATCH 1211/1860] Log jsonLoad/Save as trace instead of debug. --- services/sync/modules/util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index cbaf3359a080..a84f35d6e969 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -493,7 +493,7 @@ let Utils = { jsonLoad: function Utils_jsonLoad(filePath, that, callback) { filePath = "weave/" + filePath + ".json"; if (that._log) - that._log.debug("Loading json from disk: " + filePath); + that._log.trace("Loading json from disk: " + filePath); let file = Utils.getProfileFile(filePath); if (!file.exists()) @@ -525,7 +525,7 @@ let Utils = { jsonSave: function Utils_jsonSave(filePath, that, callback) { filePath = "weave/" + filePath + ".json"; if (that._log) - that._log.debug("Saving json to disk: " + filePath); + that._log.trace("Saving json to disk: " + filePath); let file = Utils.getProfileFile({ autoCreate: true, path: filePath }); let json = typeof callback == "function" ? callback.call(that) : callback; From 46e2d42f7679c2b6bd78c057f15c6cfa98453457 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 18:47:29 -0700 Subject: [PATCH 1212/1860] Remove pref tracker init debug message. --- services/sync/modules/engines/prefs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 620b6f1f1056..00f533f79fda 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -229,7 +229,6 @@ PrefTracker.prototype = { _init: function PrefTracker__init() { this.__proto__.__proto__._init.call(this); - this._log.debug("PrefTracker initializing!"); this._prefs.addObserver("", this, false); }, From 708d98975f6e5a5680eea11e39fadf458b5881f5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 19:04:32 -0700 Subject: [PATCH 1213/1860] Bug 504212 - Have javascript stack traces show [object Object] for various CryptoWrapper functions Also update createKeypair for keys to pass around the passphrase object and update the tests to pass around an object. --- services/sync/modules/base_records/keys.js | 2 +- services/sync/modules/service.js | 6 +++--- services/sync/tests/unit/test_records_crypto.js | 11 ++++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 287781f45601..4d8d9a0a1db3 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -158,7 +158,7 @@ PubKeyManager.prototype = { privkey.iv = Svc.Crypto.generateRandomIV(); let pub = {}, priv = {}; - Svc.Crypto.generateKeypair(passphrase, privkey.salt, privkey.iv, pub, priv); + Svc.Crypto.generateKeypair(passphrase.password, privkey.salt, privkey.iv, pub, priv); [pubkey.keyData, privkey.keyData] = [pub.value, priv.value]; if (pubkeyUri) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e40588619603..1b6e03a3c1c0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -706,9 +706,9 @@ WeaveSvc.prototype = { this._log.info("Server data wiped to ensure consistency due to missing keys"); } - let pass = ID.get("WeaveCryptoID").getPassword(); - if (pass) { - let keys = PubKeys.createKeypair(pass, PubKeys.defaultKeyUri, + let passphrase = ID.get("WeaveCryptoID"); + if (passphrase.getPassword()) { + let keys = PubKeys.createKeypair(passphrase, PubKeys.defaultKeyUri, PrivKeys.defaultKeyUri); try { PubKeys.uploadKeypair(keys); diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 3ea357b29455..aab47922b1dd 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -59,7 +59,8 @@ function run_test() { log.info("Generating keypair + symmetric key"); PubKeys.defaultKeyUri = "http://localhost:8080/pubkey"; - keys = PubKeys.createKeypair("my passphrase", + let passphrase = { password: "my passphrase" }; + keys = PubKeys.createKeypair(passphrase, "http://localhost:8080/pubkey", "http://localhost:8080/privkey"); let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. @@ -78,19 +79,19 @@ function run_test() { cryptoWrap = new CryptoWrapper("http://localhost:8080/crypted-resource", auth); cryptoWrap.encryption = "http://localhost:8080/crypto-meta"; cryptoWrap.cleartext = "my payload here"; - cryptoWrap.encrypt("my passphrase"); + cryptoWrap.encrypt(passphrase); log.info("Decrypting the record"); - let payload = cryptoWrap.decrypt("my passphrase"); + let payload = cryptoWrap.decrypt(passphrase); do_check_eq(payload, "my payload here"); do_check_neq(payload, cryptoWrap.payload); // wrap.data.payload is the encrypted one log.info("Re-encrypting the record with alternate payload"); cryptoWrap.cleartext = "another payload"; - cryptoWrap.encrypt("my passphrase"); - payload = cryptoWrap.decrypt("my passphrase"); + cryptoWrap.encrypt(passphrase); + payload = cryptoWrap.decrypt(passphrase); do_check_eq(payload, "another payload"); log.info("Done!"); From f1204c22ed15e45cf14478e51e6bddebbda8b83b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 14 Jul 2009 19:19:49 -0700 Subject: [PATCH 1214/1860] Bug 504236 - Allow separate weave and storage versions Update the substitution names to separately replace the storage version with the new storage version Makefile variable. --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 6500a520f8f3..5afad7caddba 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -59,7 +59,7 @@ const WEAVE_VERSION = "@weave_version@"; // last client version's server storage this version supports // e.g. if set to the current version, this client will wipe the server // data stored by any older client -const MIN_SERVER_STORAGE_VERSION = "@weave_version@"; +const MIN_SERVER_STORAGE_VERSION = "@storage_version@"; const PREFS_BRANCH = "extensions.weave."; From d6085495063696762bebdb2fa4615f18490c1830 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 15 Jul 2009 10:31:35 -0700 Subject: [PATCH 1215/1860] Bug 504346 - Set the storage version with storage version instead of weave version Rename MIN_SERVER_STORAGE_VERSION -> STORAGE_VERSION and use it when setting meta/global's storageVersion. --- services/sync/modules/constants.js | 4 ++-- services/sync/modules/service.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 5afad7caddba..b1401fa4adfc 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['WEAVE_VERSION', 'MIN_SERVER_STORAGE_VERSION', +const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "STORAGE_VERSION", 'PREFS_BRANCH', 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', @@ -59,7 +59,7 @@ const WEAVE_VERSION = "@weave_version@"; // last client version's server storage this version supports // e.g. if set to the current version, this client will wipe the server // data stored by any older client -const MIN_SERVER_STORAGE_VERSION = "@storage_version@"; +const STORAGE_VERSION = "@storage_version@"; const PREFS_BRANCH = "extensions.weave."; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1b6e03a3c1c0..8281367c9281 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -609,11 +609,11 @@ WeaveSvc.prototype = { let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; - this._log.debug("Min supported storage version is " + MIN_SERVER_STORAGE_VERSION); + this._log.debug("Local storage version is " + STORAGE_VERSION); this._log.debug("Remote storage version is " + remoteVersion); if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || - Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, remoteVersion) > 0) { + Svc.Version.compare(STORAGE_VERSION, remoteVersion) > 0) { // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.lastResource.lastChannel.responseStatus; @@ -628,7 +628,7 @@ WeaveSvc.prototype = { this._log.info("No metadata record, server wipe needed"); if (meta && !meta.payload.syncID) this._log.warn("No sync id, server wipe needed"); - if (Svc.Version.compare(MIN_SERVER_STORAGE_VERSION, remoteVersion) > 0) + if (Svc.Version.compare(STORAGE_VERSION, remoteVersion) > 0) this._log.info("Server storage version no longer supported, server wipe needed"); if (!this._keyGenEnabled) { @@ -915,8 +915,8 @@ WeaveSvc.prototype = { this._log.debug("Uploading new metadata record"); meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); - this._log.debug("Setting meta payload storage version to " + WEAVE_VERSION); - meta.payload.storageVersion = WEAVE_VERSION; + this._log.debug("Setting meta payload storage version to " + STORAGE_VERSION); + meta.payload.storageVersion = STORAGE_VERSION; meta.payload.syncID = Clients.syncID; let res = new Resource(meta.uri); res.put(meta.serialize()); From 40acbb03f6226a009009166e1af468cdedbe0ce8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 15 Jul 2009 11:07:44 -0700 Subject: [PATCH 1216/1860] Bug 504346 - Set the storage version with storage version instead of weave version Make it so WEAVE_VERSION is only used to detect add-on version upgrades. --- services/sync/modules/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8281367c9281..3c988dec07a0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -646,9 +646,9 @@ WeaveSvc.prototype = { "consistency."); else // 200 this._log.info("Server data wiped to ensure consistency after client " + - "upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")"); + "upgrade (" + remoteVersion + " -> " + STORAGE_VERSION + ")"); - } else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) { + } else if (Svc.Version.compare(remoteVersion, STORAGE_VERSION) > 0) { this._setSyncFailure(VERSION_OUT_OF_DATE); this._log.warn("Server data is of a newer Weave version, this client " + "needs to be upgraded. Aborting sync."); From 58f96ea68f5da426c5693263d067027499b759ee Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 15 Jul 2009 11:26:22 -0700 Subject: [PATCH 1217/1860] Bug 504372 - Don't immediately start syncing right after wiping the server Wait a while after wiping so that the DELETEs replicate. --- services/sync/modules/service.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3c988dec07a0..ebc664206806 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -65,6 +65,7 @@ const INITIAL_THRESHOLD = 75; const THRESHOLD_DECREMENT_STEP = 25; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); @@ -949,6 +950,9 @@ WeaveSvc.prototype = { this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); } } + + // XXX Bug 504125 Wait a while after wiping so that the DELETEs replicate + Sync.sleep(5000); }))(), /** From 92d3055a3d7cca46872954146742f1ce93e4831c Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 15 Jul 2009 22:46:42 -0400 Subject: [PATCH 1218/1860] fix crypto build on Windows --- services/crypto/Makefile | 289 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 279 insertions(+), 10 deletions(-) mode change 100644 => 100755 services/crypto/Makefile diff --git a/services/crypto/Makefile b/services/crypto/Makefile old mode 100644 new mode 100755 index 47cc60976434..aac2825c64e5 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -21,6 +21,7 @@ # # Contributor(s): # Dan Mills (original author) +# Godwin Chan (Darwin Universal Binary) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -36,26 +37,294 @@ # # ***** END LICENSE BLOCK ***** -stage_dir=../dist/stage +# OS detection + +sys := $(shell uname -s) + +ifeq ($(sys), Darwin) + os = Darwin + compiler = gcc3 + cxx = c++ + so = dylib + cppflags += -dynamiclib -DDEBUG +else +ifeq ($(sys), Linux) + os = Linux + compiler = gcc3 + cxx = c++ + so = so + cppflags += -shared +else +ifeq ($(sys), MINGW32_NT-6.0) + os = WINNT + compiler = msvc + cxx = cl + so = dll +else +ifeq ($(sys), MINGW32_NT-5.1) + os = WINNT + compiler = msvc + cxx = cl + so = dll +else + $(error Sorry, your os is unknown/unsupported: $(sys)) +endif +endif +endif +endif + +# Arch detection + +machine := $(shell uname -m) + +ifeq ($(machine), arm) + arch = arm +else +ifeq ($(machine), i386) + arch = x86 +else +ifeq ($(machine), i586) + arch = x86 +else +ifeq ($(machine), i686) + arch = x86 +else +ifeq ($(machine), ppc) + arch = ppc +else +ifeq ($(machine), Power Macintosh) + arch = ppc +else +ifeq ($(machine), x86_64) + arch = x86_64 +else + $(error: Sorry, your architecture is unknown/unsupported: $(machine)) +endif +endif +endif +endif +endif +endif +endif + +# Universal binary so no need for $(arch) for Darwin + +ifeq ($(sys), Darwin) + platform = $(os) +else + platform = $(os)_$(arch)-$(compiler) +endif + +################################################################### +# Target and objects + +ifeq ($(sys), Darwin) + target = WeaveCrypto + target_i386 = WeaveCrypto.i386 + target_ppc = WeaveCrypto.ppc + so_target = $(target:=.$(so)) + so_target_i386 = $(target_i386:=.$(so)) + so_target_ppc = $(target_ppc:=.$(so)) + cpp_objects_i386 = $(cpp_sources:.cpp=.oi386) + cpp_objects_ppc = $(cpp_sources:.cpp=.oppc) +else + target = WeaveCrypto + so_target = $(target:=.$(so)) + cpp_objects = $(cpp_sources:.cpp=.o) +endif + +# source and path configurations +idl = IWeaveCrypto.idl +cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp sdkdir ?= ${MOZSDKDIR} +destdir = .. +platformdir = $(destdir)/platform/$(platform) + +xpidl = $(sdkdir)/bin/xpidl + +# FIXME: we don't actually require this for e.g. clean ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) $(warning It should point to the location of the Gecko SDK) $(warning For example: "make sdkdir=/foo/bar/baz") $(warning Or set the MOZSDKDIR environment variable to point to it) - $(error) + $(error ) endif -all: build +idl_headers = $(idl:.idl=.h) +idl_typelib = $(idl:.idl=.xpt) +cpp_objects = $(cpp_sources:.cpp=.o) +so_target = $(target:=.$(so)) -.PHONY: build crypto rebuild_all +headers = -I$(sdkdir)/include \ + -I$(sdkdir)/include/system_wrappers \ + -I$(sdkdir)/include/nss \ + -I$(sdkdir)/include/xpcom \ + -I$(sdkdir)/include/string \ + -I$(sdkdir)/include/pipnss \ + -I$(sdkdir)/include/nspr \ + -I$(sdkdir)/sdk/include -crypto: - $(MAKE) -C src install +# libraries +libdirs := $(sdkdir)/lib $(sdkdir)/bin +libs := xpcomglue_s xpcom nspr4 \ + crmf smime3 ssl3 nss3 nssutil3 \ + plds4 plc4 -build: - cp -R -v platform $(stage_dir) - cp -R -v components $(stage_dir) +ifeq ($(os), linux) + libs := xpcom_core $(libs) +endif -rebuild_all: crypto build +# compiler and Linker Flags + +ifeq ($(os), Darwin) + libdirs := $(patsubst %,-L%,$(libdirs)) + libs := $(patsubst %,-l%,$(libs)) + cppflags_i386 += -c -pipe -Os -arch i386 \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -fshort-wchar -fpascal-strings -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) \ + -isysroot /Developer/SDKs/MacOSX10.4u.sdk + ldflags_i386 += -pthread -pipe -bundle -arch i386 \ + -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + $(libdirs) $(libs) + cppflags_ppc += -c -pipe -Os -arch ppc \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -fshort-wchar -fpascal-strings -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) \ + -isysroot /Developer/SDKs/MacOSX10.4u.sdk \ + -force_cpusubtype_ALL + ldflags_ppc += -pthread -pipe -bundle -arch ppc \ + -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + -force_cpusubtype_ALL \ + $(libdirs) $(libs) +else +ifeq ($(os), Linux) + libdirs := $(patsubst %,-L%,$(libdirs)) + libs := $(patsubst %,-l%,$(libs)) + cppflags += -pipe -Os \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) +ifneq ($(arch), arm) + cppflags += -fshort-wchar +else +endif + ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,-z,defs -Wl,-h,WeaveCrypto.so \ + -Wl,-rpath-link,$(sdkdir)/bin \ + $(sdkdir)/lib/libxpcomglue_s.a \ + $(libdirs) $(libs) +else +ifeq ($(os), WINNT) + libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) + libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) + cppflags += -c -nologo -O1 -GR- -TP -MT -Zc:wchar_t- -W3 -Gy $(headers) \ + -DNDEBUG -DTRIMMED -D_CRT_SECURE_NO_DEPRECATE=1 \ + -D_CRT_NONSTDC_NO_DEPRECATE=1 -DWINVER=0x500 -D_WIN32_WINNT=0x500 \ + -D_WIN32_IE=0x0500 -DX_DISPLAY_MISSING=1 -DMOZILLA_VERSION=\"1.9pre\" \ + -DMOZILLA_VERSION_U=1.9pre -DHAVE_SNPRINTF=1 -D_WINDOWS=1 -D_WIN32=1 \ + -DWIN32=1 -DXP_WIN=1 -DXP_WIN32=1 -DHW_THREADS=1 -DSTDC_HEADERS=1 \ + -DWIN32_LEAN_AND_MEAN=1 -DNO_X11=1 -DHAVE_MMINTRIN_H=1 \ + -DHAVE_OLEACC_IDL=1 -DHAVE_ATLBASE_H=1 -DHAVE_WPCAPI_H=1 -D_X86_=1 \ + -DD_INO=d_ino + ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ + $(libdirs) $(libs) \ + kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib + rcflags := -r $(headers) +endif +endif +endif + +###################################################################### + +.PHONY: all build install clean subst + +all: build # default target + +build: subst $(so_target) $(idl_typelib) + +install: build + mkdir -p $(destdir)/components + mkdir -p $(platformdir)/components + cp $(idl_typelib) $(destdir)/components + cp $(so_target) $(platformdir)/components + +clean: + rm -f $(so_target) $(so_target_i386) $(so_target_ppc) \ + $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ + $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp WeaveCrypto.rc + +substitute := perl -p -e 's/@([^@]+)@/defined $$ENV{$$1} ? $$ENV{$$1} : $$&/ge' + +subst: + $(substitute) WeaveCrypto.rc.in > WeaveCrypto.rc + +# rules to build the c headers and .xpt from idl +$(idl_headers): $(idl) + $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) + +$(idl_typelib): $(idl) + $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) + +# build and link rules +ifeq ($(os), Darwin) + $(so_target): $(so_target_i386) $(so_target_ppc) + lipo -create -output $(so_target) -arch ppc $(so_target_ppc) \ + -arch i386 $(so_target_i386) + chmod +x $(so_target) + + #i386 + $(cpp_objects_i386): $(cpp_sources) + $(cxx) -o $@ $(cppflags_i386) $(@:.oi386=.cpp) + + $(so_target_i386): $(idl_headers) $(cpp_objects_i386) + $(cxx) -o $@ $(ldflags_i386) $(cpp_objects_i386) + chmod +x $(so_target_i386) + + #ppc + $(cpp_objects_ppc): $(cpp_sources) + $(cxx) -o $@ $(cppflags_ppc) $(@:.oppc=.cpp) + + $(so_target_ppc): $(idl_headers) $(cpp_objects_ppc) + $(cxx) -o $@ $(ldflags_ppc) $(cpp_objects_ppc) + chmod +x $(so_target_ppc) +else +ifeq ($(os), Linux) + $(so_target): $(idl_headers) + $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) + chmod +x $@ +else +ifeq ($(os), WINNT) + $(target:=.res): $(target:=.rc) + rc -Fo$@ $(rcflags) $(target:=.rc) + + $(cpp_objects): $(cpp_sources) + $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) + + $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) + link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) + chmod +x $@ +endif +endif +endif From 3aa573ba856b60be651ca9069ddf1adaf73d407c Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 16 Jul 2009 11:51:10 -0400 Subject: [PATCH 1219/1860] move password tests to need-work because they've been broken for a year --- services/sync/tests/unit/{ => need-work}/test_passwords.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename services/sync/tests/unit/{ => need-work}/test_passwords.js (100%) diff --git a/services/sync/tests/unit/test_passwords.js b/services/sync/tests/unit/need-work/test_passwords.js similarity index 100% rename from services/sync/tests/unit/test_passwords.js rename to services/sync/tests/unit/need-work/test_passwords.js From 617888ef82bce3f23a771b138d7f179a71b3de3c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 16 Jul 2009 12:11:31 -0700 Subject: [PATCH 1220/1860] Re/Move substitute declarations. --- services/crypto/Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index aac2825c64e5..9d440ae20142 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -275,8 +275,7 @@ clean: $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp WeaveCrypto.rc -substitute := perl -p -e 's/@([^@]+)@/defined $$ENV{$$1} ? $$ENV{$$1} : $$&/ge' - +substitute = perl -p -e 's/@([^@]+)@/defined $$ENV{$$1} ? $$ENV{$$1} : $$&/ge' subst: $(substitute) WeaveCrypto.rc.in > WeaveCrypto.rc From 46b3df1d1ba2c0561275c129a97c2775d09415bf Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 16 Jul 2009 14:07:07 -0700 Subject: [PATCH 1221/1860] $hare the $ub$titute command from root Makefile. --- services/crypto/Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 9d440ae20142..93b2ec9bff39 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -275,7 +275,6 @@ clean: $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp WeaveCrypto.rc -substitute = perl -p -e 's/@([^@]+)@/defined $$ENV{$$1} ? $$ENV{$$1} : $$&/ge' subst: $(substitute) WeaveCrypto.rc.in > WeaveCrypto.rc From d310c5143fddba7c5d398dddf60a5d78e75d37e4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 16 Jul 2009 15:17:28 -0700 Subject: [PATCH 1222/1860] Fix trailing spaces. --- services/crypto/Makefile | 287 ++------------------------------------- 1 file changed, 10 insertions(+), 277 deletions(-) mode change 100755 => 100644 services/crypto/Makefile diff --git a/services/crypto/Makefile b/services/crypto/Makefile old mode 100755 new mode 100644 index 93b2ec9bff39..6b6f2ec0f99e --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -21,7 +21,6 @@ # # Contributor(s): # Dan Mills (original author) -# Godwin Chan (Darwin Universal Binary) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -37,292 +36,26 @@ # # ***** END LICENSE BLOCK ***** -# OS detection - -sys := $(shell uname -s) - -ifeq ($(sys), Darwin) - os = Darwin - compiler = gcc3 - cxx = c++ - so = dylib - cppflags += -dynamiclib -DDEBUG -else -ifeq ($(sys), Linux) - os = Linux - compiler = gcc3 - cxx = c++ - so = so - cppflags += -shared -else -ifeq ($(sys), MINGW32_NT-6.0) - os = WINNT - compiler = msvc - cxx = cl - so = dll -else -ifeq ($(sys), MINGW32_NT-5.1) - os = WINNT - compiler = msvc - cxx = cl - so = dll -else - $(error Sorry, your os is unknown/unsupported: $(sys)) -endif -endif -endif -endif - -# Arch detection - -machine := $(shell uname -m) - -ifeq ($(machine), arm) - arch = arm -else -ifeq ($(machine), i386) - arch = x86 -else -ifeq ($(machine), i586) - arch = x86 -else -ifeq ($(machine), i686) - arch = x86 -else -ifeq ($(machine), ppc) - arch = ppc -else -ifeq ($(machine), Power Macintosh) - arch = ppc -else -ifeq ($(machine), x86_64) - arch = x86_64 -else - $(error: Sorry, your architecture is unknown/unsupported: $(machine)) -endif -endif -endif -endif -endif -endif -endif - -# Universal binary so no need for $(arch) for Darwin - -ifeq ($(sys), Darwin) - platform = $(os) -else - platform = $(os)_$(arch)-$(compiler) -endif - -################################################################### -# Target and objects - -ifeq ($(sys), Darwin) - target = WeaveCrypto - target_i386 = WeaveCrypto.i386 - target_ppc = WeaveCrypto.ppc - so_target = $(target:=.$(so)) - so_target_i386 = $(target_i386:=.$(so)) - so_target_ppc = $(target_ppc:=.$(so)) - cpp_objects_i386 = $(cpp_sources:.cpp=.oi386) - cpp_objects_ppc = $(cpp_sources:.cpp=.oppc) -else - target = WeaveCrypto - so_target = $(target:=.$(so)) - cpp_objects = $(cpp_sources:.cpp=.o) -endif - -# source and path configurations -idl = IWeaveCrypto.idl -cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp +stage_dir=../dist/stage sdkdir ?= ${MOZSDKDIR} -destdir = .. -platformdir = $(destdir)/platform/$(platform) - -xpidl = $(sdkdir)/bin/xpidl - -# FIXME: we don't actually require this for e.g. clean ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) $(warning It should point to the location of the Gecko SDK) $(warning For example: "make sdkdir=/foo/bar/baz") $(warning Or set the MOZSDKDIR environment variable to point to it) - $(error ) + $(error) endif -idl_headers = $(idl:.idl=.h) -idl_typelib = $(idl:.idl=.xpt) -cpp_objects = $(cpp_sources:.cpp=.o) -so_target = $(target:=.$(so)) +all: build -headers = -I$(sdkdir)/include \ - -I$(sdkdir)/include/system_wrappers \ - -I$(sdkdir)/include/nss \ - -I$(sdkdir)/include/xpcom \ - -I$(sdkdir)/include/string \ - -I$(sdkdir)/include/pipnss \ - -I$(sdkdir)/include/nspr \ - -I$(sdkdir)/sdk/include +.PHONY: build crypto rebuild_all -# libraries -libdirs := $(sdkdir)/lib $(sdkdir)/bin -libs := xpcomglue_s xpcom nspr4 \ - crmf smime3 ssl3 nss3 nssutil3 \ - plds4 plc4 +crypto: + $(MAKE) -C src install -ifeq ($(os), linux) - libs := xpcom_core $(libs) -endif +build: + cp -R -v platform $(stage_dir) + cp -R -v components $(stage_dir) -# compiler and Linker Flags - -ifeq ($(os), Darwin) - libdirs := $(patsubst %,-L%,$(libdirs)) - libs := $(patsubst %,-l%,$(libs)) - cppflags_i386 += -c -pipe -Os -arch i386 \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -fshort-wchar -fpascal-strings -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) \ - -isysroot /Developer/SDKs/MacOSX10.4u.sdk - ldflags_i386 += -pthread -pipe -bundle -arch i386 \ - -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - $(libdirs) $(libs) - cppflags_ppc += -c -pipe -Os -arch ppc \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -fshort-wchar -fpascal-strings -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) \ - -isysroot /Developer/SDKs/MacOSX10.4u.sdk \ - -force_cpusubtype_ALL - ldflags_ppc += -pthread -pipe -bundle -arch ppc \ - -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - -force_cpusubtype_ALL \ - $(libdirs) $(libs) -else -ifeq ($(os), Linux) - libdirs := $(patsubst %,-L%,$(libdirs)) - libs := $(patsubst %,-l%,$(libs)) - cppflags += -pipe -Os \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) -ifneq ($(arch), arm) - cppflags += -fshort-wchar -else -endif - ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,-z,defs -Wl,-h,WeaveCrypto.so \ - -Wl,-rpath-link,$(sdkdir)/bin \ - $(sdkdir)/lib/libxpcomglue_s.a \ - $(libdirs) $(libs) -else -ifeq ($(os), WINNT) - libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) - libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) - cppflags += -c -nologo -O1 -GR- -TP -MT -Zc:wchar_t- -W3 -Gy $(headers) \ - -DNDEBUG -DTRIMMED -D_CRT_SECURE_NO_DEPRECATE=1 \ - -D_CRT_NONSTDC_NO_DEPRECATE=1 -DWINVER=0x500 -D_WIN32_WINNT=0x500 \ - -D_WIN32_IE=0x0500 -DX_DISPLAY_MISSING=1 -DMOZILLA_VERSION=\"1.9pre\" \ - -DMOZILLA_VERSION_U=1.9pre -DHAVE_SNPRINTF=1 -D_WINDOWS=1 -D_WIN32=1 \ - -DWIN32=1 -DXP_WIN=1 -DXP_WIN32=1 -DHW_THREADS=1 -DSTDC_HEADERS=1 \ - -DWIN32_LEAN_AND_MEAN=1 -DNO_X11=1 -DHAVE_MMINTRIN_H=1 \ - -DHAVE_OLEACC_IDL=1 -DHAVE_ATLBASE_H=1 -DHAVE_WPCAPI_H=1 -D_X86_=1 \ - -DD_INO=d_ino - ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ - $(libdirs) $(libs) \ - kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib - rcflags := -r $(headers) -endif -endif -endif - -###################################################################### - -.PHONY: all build install clean subst - -all: build # default target - -build: subst $(so_target) $(idl_typelib) - -install: build - mkdir -p $(destdir)/components - mkdir -p $(platformdir)/components - cp $(idl_typelib) $(destdir)/components - cp $(so_target) $(platformdir)/components - -clean: - rm -f $(so_target) $(so_target_i386) $(so_target_ppc) \ - $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ - $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp WeaveCrypto.rc - -subst: - $(substitute) WeaveCrypto.rc.in > WeaveCrypto.rc - -# rules to build the c headers and .xpt from idl -$(idl_headers): $(idl) - $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) - -$(idl_typelib): $(idl) - $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) - -# build and link rules -ifeq ($(os), Darwin) - $(so_target): $(so_target_i386) $(so_target_ppc) - lipo -create -output $(so_target) -arch ppc $(so_target_ppc) \ - -arch i386 $(so_target_i386) - chmod +x $(so_target) - - #i386 - $(cpp_objects_i386): $(cpp_sources) - $(cxx) -o $@ $(cppflags_i386) $(@:.oi386=.cpp) - - $(so_target_i386): $(idl_headers) $(cpp_objects_i386) - $(cxx) -o $@ $(ldflags_i386) $(cpp_objects_i386) - chmod +x $(so_target_i386) - - #ppc - $(cpp_objects_ppc): $(cpp_sources) - $(cxx) -o $@ $(cppflags_ppc) $(@:.oppc=.cpp) - - $(so_target_ppc): $(idl_headers) $(cpp_objects_ppc) - $(cxx) -o $@ $(ldflags_ppc) $(cpp_objects_ppc) - chmod +x $(so_target_ppc) -else -ifeq ($(os), Linux) - $(so_target): $(idl_headers) - $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) - chmod +x $@ -else -ifeq ($(os), WINNT) - $(target:=.res): $(target:=.rc) - rc -Fo$@ $(rcflags) $(target:=.rc) - - $(cpp_objects): $(cpp_sources) - $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) - - $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) - link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) - chmod +x $@ -endif -endif -endif +rebuild_all: crypto build From 7b35e39ab7163f064f1ef36d8bd65d2fae19e5ef Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 16 Jul 2009 16:31:54 -0700 Subject: [PATCH 1223/1860] Bug 504389 - Don't automatically sync after clicking on wipe server Don't open the sync status after wiping, and move the after-wipe sleep to inside freshStart with shorter wait. --- services/sync/modules/service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ebc664206806..4982ab6b1f18 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -914,6 +914,9 @@ WeaveSvc.prototype = { this._log.info("Client metadata wiped, deleting server data"); this.wipeServer(); + // XXX Bug 504125 Wait a while after wiping so that the DELETEs replicate + Sync.sleep(2000); + this._log.debug("Uploading new metadata record"); meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); this._log.debug("Setting meta payload storage version to " + STORAGE_VERSION); @@ -950,9 +953,6 @@ WeaveSvc.prototype = { this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); } } - - // XXX Bug 504125 Wait a while after wiping so that the DELETEs replicate - Sync.sleep(5000); }))(), /** From 9e374cf331aee130cdc68eedbc9b036a0ef884f3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 16 Jul 2009 23:13:58 -0700 Subject: [PATCH 1224/1860] Bug 504387 - Cache keys after creating/PUTting them instead of after GETting. r=thunder Set the record manager for CryptoMetas, PubKeys, PrivKeys after uploading keys. --- services/sync/modules/engines.js | 3 +++ services/sync/modules/service.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f732387ac650..c598b6e628ec 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -297,6 +297,9 @@ SyncEngine.prototype = { meta.addUnwrappedKey(pubkey, symkey); let res = new Resource(meta.uri); res.put(meta.serialize()); + + // Cache the cryto meta that we just put on the server + CryptoMetas.set(meta.uri, meta); } // first sync special case: upload all items diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4982ab6b1f18..574dd4138334 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -712,7 +712,10 @@ WeaveSvc.prototype = { let keys = PubKeys.createKeypair(passphrase, PubKeys.defaultKeyUri, PrivKeys.defaultKeyUri); try { + // Upload and cache the keypair PubKeys.uploadKeypair(keys); + PubKeys.set(keys.pubkey.uri, keys.pubkey); + PrivKeys.set(keys.privkey.uri, keys.privkey); return true; } catch (e) { this._setSyncFailure(KEYS_UPLOAD_FAIL); From a2e703647134e63626a1c840a9e51525d53d92ac Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 17 Jul 2009 12:23:03 -0700 Subject: [PATCH 1225/1860] Bug 504389 - Don't automatically sync after clicking on wipe server Remove description reference to automatically sending local data. --- services/sync/locales/en-US/preferences.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 7cd4913d06e9..8884e1c90636 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -70,7 +70,7 @@ - + From 8083cd06acd82ffccd1dbc9b740024c66bd91cd6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 17 Jul 2009 16:55:15 -0700 Subject: [PATCH 1226/1860] Bug 504256 - sync should observe idle service and only sync when the user is idle Detect if the user has been recently active, and if so, skip the sync. Arbitrarily picking 30 seconds for now. --- services/sync/modules/service.js | 7 +++++++ services/sync/modules/util.js | 1 + 2 files changed, 8 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 574dd4138334..9cabcbed9063 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -794,6 +794,13 @@ WeaveSvc.prototype = { */ sync: function WeaveSvc_sync(fullSync) this._catch(this._lock(this._notify("sync", "", function() { + + // Skip this incremental sync if the user has been active recently + if (!fullSync && Svc.Idle.idleTime < 30000) { + this._log.debug("Skipped sync because the user was active."); + return; + } + fullSync = true; // not doing thresholds yet // Use thresholds to determine what to sync only if it's not a full sync diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index a84f35d6e969..b047cfda8ff6 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -719,6 +719,7 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], + ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], ["Login", "@mozilla.org/login-manager;1", "nsILoginManager"], ["Memory", "@mozilla.org/xpcom/memory-service;1", "nsIMemory"], From b3a6a82bf71924bb69d9920a501d8f83252c11fe Mon Sep 17 00:00:00 2001 From: Daniel Holbert Date: Tue, 21 Jul 2009 12:35:12 -0700 Subject: [PATCH 1227/1860] Bug 502293: Fix a string so that Weave will *actually* display error messages, instead of showing a percent sign. r=thunder --- services/sync/locales/en-US/sync.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 976961ce87f4..146c59dec72d 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -15,7 +15,7 @@ status.idle = Idle status.active = Working... error.login.title = Error While Signing In -error.login.description = Weave encountered an error while signing you in: %1. Please try again. +error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. error.login.reason.password = Your username/password failed error.login.reason.unknown = Unknown error error.logout.title = Error While Signing Out From aade7b6e052b6dcd48799ff187cc8f372aba999c Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 21 Jul 2009 11:18:29 -0700 Subject: [PATCH 1228/1860] Crypto changes --- services/crypto/IWeaveCrypto.idl | 41 ++++ services/crypto/WeaveCrypto.cpp | 191 ++++++++++++++++++ services/crypto/components/IWeaveCrypto.xpt | Bin 529 -> 601 bytes .../Darwin/components/WeaveCrypto.dylib | Bin 65776 -> 65944 bytes .../sync/tests/unit/test_crypto_rewrap.js | 35 ++++ .../sync/tests/unit/test_crypto_verify.js | 23 +++ 6 files changed, 290 insertions(+) create mode 100644 services/sync/tests/unit/test_crypto_rewrap.js create mode 100644 services/sync/tests/unit/test_crypto_verify.js diff --git a/services/crypto/IWeaveCrypto.idl b/services/crypto/IWeaveCrypto.idl index 611f4011d67c..36ed444bf2e9 100644 --- a/services/crypto/IWeaveCrypto.idl +++ b/services/crypto/IWeaveCrypto.idl @@ -179,5 +179,46 @@ interface IWeaveCrypto : nsISupports in ACString aPassphrase, in ACString aSalt, in ACString aIV); + + /** + * Rewrap a private key with a new user passphrase. + * + * @param aWrappedPrivateKey + * The base64 encoded string holding an encrypted private key. + * @param aPassphrase + * The passphrase to decrypt the private key. + * @param aSalt + * The salt for the passphrase. + * @param aIV + * The random IV used when unwrapping the private key. + * @param aNewPassphrase + * The new passphrase to wrap the private key with. + * @returns The (re)wrapped private key, base64 encoded + * + */ + ACString rewrapPrivateKey(in ACString aWrappedPrivateKey, + in ACString aPassphrase, + in ACString aSalt, + in ACString aIV, + in ACString aNewPassphrase); + + /** + * Verify a user's passphrase against a private key. + * + * @param aWrappedPrivateKey + * The base64 encoded string holding an encrypted private key. + * @param aPassphrase + * The passphrase to decrypt the private key. + * @param aSalt + * The salt for the passphrase. + * @param aIV + * The random IV used when unwrapping the private key. + * @returns Boolean true if the passphrase decrypted the key correctly. + * + */ + boolean verifyPassphrase(in ACString aWrappedPrivateKey, + in ACString aPassphrase, + in ACString aSalt, + in ACString aIV); }; diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index 84714c8b02c1..4aa39bea5484 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -1005,3 +1005,194 @@ unwrap_done: return rv; } + +/* + * RewrapPrivateKey + */ +NS_IMETHODIMP +WeaveCrypto::RewrapPrivateKey(const nsACString& aWrappedPrivateKey, + const nsACString& aPassphrase, + const nsACString& aSalt, + const nsACString& aIV, + const nsACString& aNewPassphrase, + nsACString& aPrivateKey) +{ + nsresult rv = NS_OK; + PK11SlotInfo *slot = nsnull; + PK11SymKey *pbeKey = nsnull; + SECKEYPrivateKey *privKey = nsnull; + SECItem *ivParam = nsnull; + SECItem *keyID = nsnull; + + CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; + PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); + + // Step 1. Get rid of the base64 encoding on the inputs. + char privateKeyBuffer[STACK_BUFFER_SIZE]; + PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); + rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; + + + // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. + rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); + NS_ENSURE_SUCCESS(rv, rv); + + char ivData[STACK_BUFFER_SIZE]; + PRUint32 ivDataSize = sizeof(ivData); + rv = DecodeBase64(aIV, ivData, &ivDataSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); + wrapMech = PK11_GetPadMechanism(wrapMech); + if (wrapMech == CKM_INVALID_MECHANISM) { + NS_WARNING("Unknown key mechanism"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + + ivParam = PK11_ParamFromIV(wrapMech, &ivItem); + if (!ivParam) { + NS_WARNING("Couldn't create IV param"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + + // Step 3. Unwrap the private key with the key from the passphrase. + slot = PK11_GetInternalSlot(); + if (!slot) { + NS_WARNING("Can't get internal PK11 slot"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + + keyID = &ivItem; + privKey = PK11_UnwrapPrivKey(slot, + pbeKey, wrapMech, ivParam, &wrappedPrivKey, + nsnull, + keyID, + PR_FALSE, + PR_TRUE, + CKK_RSA, + privKeyUsage, privKeyUsageLength, + nsnull); + if (!privKey) { + NS_WARNING("PK11_UnwrapPrivKey failed"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + + // Step 4. Rewrap the private key with the new passphrase. + rv = WrapPrivateKey(privKey, aNewPassphrase, aSalt, aIV, aPrivateKey); + if (NS_FAILED(rv)) { + NS_WARNING("RewrapPrivateKey failed"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + +rewrap_done: + if (privKey) + SECKEY_DestroyPrivateKey(privKey); + if (pbeKey) + PK11_FreeSymKey(pbeKey); + if (slot) + PK11_FreeSlot(slot); + if (ivParam) + SECITEM_FreeItem(ivParam, PR_TRUE); + + return rv; +} + +/* + * VerifyPassphrase + */ +NS_IMETHODIMP +WeaveCrypto::VerifyPassphrase(const nsACString& aWrappedPrivateKey, + const nsACString& aPassphrase, + const nsACString& aSalt, + const nsACString& aIV, + PRBool *result) +{ + *result = PR_FALSE; + nsresult rv = NS_OK; + PK11SlotInfo *slot = nsnull; + PK11SymKey *pbeKey = nsnull; + SECKEYPrivateKey *privKey = nsnull; + SECItem *ivParam = nsnull; + SECItem *keyID = nsnull; + + CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; + PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); + + // Step 1. Get rid of the base64 encoding on the input. + char privateKeyBuffer[STACK_BUFFER_SIZE]; + PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); + rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; + + // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. + rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); + NS_ENSURE_SUCCESS(rv, rv); + + char ivData[STACK_BUFFER_SIZE]; + PRUint32 ivDataSize = sizeof(ivData); + rv = DecodeBase64(aIV, ivData, &ivDataSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); + wrapMech = PK11_GetPadMechanism(wrapMech); + if (wrapMech == CKM_INVALID_MECHANISM) { + NS_WARNING("Unknown key mechanism"); + rv = NS_ERROR_FAILURE; + goto verify_done; + } + + ivParam = PK11_ParamFromIV(wrapMech, &ivItem); + if (!ivParam) { + NS_WARNING("Couldn't create IV param"); + rv = NS_ERROR_FAILURE; + goto verify_done; + } + + // Step 3. Unwrap the private key with the key from the passphrase. + slot = PK11_GetInternalSlot(); + if (!slot) { + NS_WARNING("Can't get internal PK11 slot"); + rv = NS_ERROR_FAILURE; + goto verify_done; + } + + keyID = &ivItem; + privKey = PK11_UnwrapPrivKey(slot, + pbeKey, wrapMech, ivParam, &wrappedPrivKey, + nsnull, + keyID, + PR_FALSE, + PR_TRUE, + CKK_RSA, + privKeyUsage, privKeyUsageLength, + nsnull); + if (!privKey) { + NS_WARNING("PK11_UnwrapPrivKey failed"); + } else { + *result = PR_TRUE; + } + +verify_done: + if (privKey) + SECKEY_DestroyPrivateKey(privKey); + if (pbeKey) + PK11_FreeSymKey(pbeKey); + if (slot) + PK11_FreeSlot(slot); + if (ivParam) + SECITEM_FreeItem(ivParam, PR_TRUE); + + return rv; +} diff --git a/services/crypto/components/IWeaveCrypto.xpt b/services/crypto/components/IWeaveCrypto.xpt index 0962806b3443265ac2ff92deac91a390b442f80a..2ed7438b519b70a0a5bc2ab3444d9d0e404b96bc 100644 GIT binary patch delta 309 zcmbQpa+76(IAi2QiP(C+1_lO3cE$uY1~vyEOOUYv$O4H;!^PCWVhlhrW0r>P4Lcfk ztk}W82I5<@fOtUub_m~@6~gb>(E(Kv%m`DP3TKruHL$@%>${kd4V=veF#uU?9Tu_u zP>ULNBya((W&yh80?0*kfQ-8!#ttCkC5W*L$oL9k>}g2MNzX6JEXl}aK(HqJGEP=3 pN-Zx+EC?vdEK4j&^-is1C`&EMOsfn?EG{m{C`v3&oqV6s2mmB@SsMTV delta 211 zcmcb~GLdD1IHTZ1iP(Cc1_lO(hl~kq3~UZS)@#NFAPXe+6(+{W2o?hhG4irBY;V}n zuw%sz1~w31lm)~C^0!0yvaAq($BquD3SCB+T6;JvfT@8EE}FuOY+xxH!~nP$3(%Gh ckX>_tjHw{T4j^M8h_MUESUWiqh&7lD0sjFrm;e9( diff --git a/services/crypto/platform/Darwin/components/WeaveCrypto.dylib b/services/crypto/platform/Darwin/components/WeaveCrypto.dylib index a7ac2d1bb3410738e66d3ef638cde884cfe961d6..c1cd643278a53a75787c97f19758c3b10df1a2fe 100755 GIT binary patch delta 10084 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 delta 8153 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 diff --git a/services/sync/tests/unit/test_crypto_rewrap.js b/services/sync/tests/unit/test_crypto_rewrap.js new file mode 100644 index 000000000000..a8ecde0e3586 --- /dev/null +++ b/services/sync/tests/unit/test_crypto_rewrap.js @@ -0,0 +1,35 @@ +function run_test() { + var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + + var salt = cryptoSvc.generateRandomBytes(16); + var iv = cryptoSvc.generateRandomIV(); + var symKey = cryptoSvc.generateRandomKey(); + + // Tests with a 2048 bit key (the default) + do_check_eq(cryptoSvc.keypairBits, 2048) + var pubOut = {}; + var privOut = {}; + cryptoSvc.generateKeypair("old passphrase", salt, iv, pubOut, privOut); + var pubKey = pubOut.value; + var privKey = privOut.value; + + // do some key wrapping + var wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey); + var unwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, privKey, + "old passphrase", salt, iv); + + // Is our unwrapped key the same thing we started with? + do_check_eq(unwrappedKey, symKey); + + // Rewrap key with a new passphrase + var newPrivKey = cryptoSvc.rewrapPrivateKey(privKey, "old passphrase", + salt, iv, "new passphrase"); + + // Unwrap symkey with new symkey + var newUnwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, newPrivKey, + "new passphrase", salt, iv); + + // The acid test... Is this unwrapped symkey the same as before? + do_check_eq(newUnwrappedKey, unwrappedKey); +} diff --git a/services/sync/tests/unit/test_crypto_verify.js b/services/sync/tests/unit/test_crypto_verify.js new file mode 100644 index 000000000000..f55f7c004d1c --- /dev/null +++ b/services/sync/tests/unit/test_crypto_verify.js @@ -0,0 +1,23 @@ +function run_test() { + var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + + var salt = cryptoSvc.generateRandomBytes(16); + var iv = cryptoSvc.generateRandomIV(); + + // Tests with a 2048 bit key (the default) + do_check_eq(cryptoSvc.keypairBits, 2048) + var privOut = {}; + cryptoSvc.generateKeypair("passphrase", salt, iv, {}, privOut); + var privKey = privOut.value; + + // Check with correct passphrase + var shouldBeTrue = cryptoSvc.verifyPassphrase(privKey, "passphrase", + salt, iv); + do_check_eq(shouldBeTrue, true); + + // Check with incorrect passphrase + var shouldBeFalse = cryptoSvc.verifyPassphrase(privKey, "NotPassphrase", + salt, iv); + do_check_eq(shouldBeFalse, false); +} From ab5dbc7b7e6ff98cea589736a558009a5d2b30f8 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 21 Jul 2009 14:31:16 -0700 Subject: [PATCH 1229/1860] Reset passphrase code + UI (bug #443696) --- services/sync/locales/en-US/login.dtd | 10 ++++- services/sync/locales/en-US/login.properties | 5 +++ services/sync/locales/en-US/preferences.dtd | 8 ++++ .../sync/locales/en-US/preferences.properties | 6 +++ services/sync/modules/service.js | 37 +++++++++++++++++-- 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/services/sync/locales/en-US/login.dtd b/services/sync/locales/en-US/login.dtd index 26112bed1c84..7580210cc704 100644 --- a/services/sync/locales/en-US/login.dtd +++ b/services/sync/locales/en-US/login.dtd @@ -1,9 +1,17 @@ - + + + + + + + + + diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties index 664bf09fedad..0742305409e1 100644 --- a/services/sync/locales/en-US/login.properties +++ b/services/sync/locales/en-US/login.properties @@ -1,4 +1,5 @@ loginFailed.alert = Login failed +noUsername.alert = You must enter a username noPassword.alert = You must enter a password noPassphrase.alert = You must enter a passphrase samePasswordAndPassphrase.alert = Your password and passphrase must be different @@ -6,3 +7,7 @@ loginStart.label = Signing in, please wait... loginError.label = Invalid username and/or password. loginSuccess.label = Signed In hide.label = Hide +pphStart.label = Resetting passphrase, please wait... +pphError.label = There was an error while resetting your passphrase! +pphSuccess.label = Your passphrase was successfully reset! +pphNoMatch.alert = Your passphrases do not match. Try again! diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 8884e1c90636..390958d71722 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -22,6 +22,7 @@ + @@ -111,3 +112,10 @@ + + + + + + + diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index cbe21be60565..9ed3b2db0815 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -21,3 +21,9 @@ change.password.status.passwordsDoNotMatch = The passwords you entered do not ma change.password.status.noOldPassword = You didn't enter your current password. change.password.status.noNewPassword = You didn't enter a new password. change.password.status.badOldPassword = Your current password is incorrect. + +change.passphrase.label = Changing passphrase, please wait... +change.passphrase.error = There was an error while changing your passphrase! +change.passphrase.success = Your passphrase was successfully changed! +change.passphrase.noMatchAlert = Your passphrases do not match. Try again! +change.passphrase.noPassAlert = You must enter the new passphrase diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9cabcbed9063..7633f877b059 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -517,6 +517,35 @@ WeaveSvc.prototype = { // fixme: decrypt something here }))), + changePassphrase: function WeaveSvc_changePassphrase(newphrase) + this._catch(this._notify("changepph", "", function() { + throw "Unimplemented!"; + }))(), + + resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) + this._catch(this._notify("resetpph", "", function() { + /* Make remote commands ready so we have a list of clients beforehand */ + this.prepCommand("logout", []); + let clientsBackup = Clients._store.clients; + + /* Wipe */ + this.wipeServer(); + + /* Set remote commands before syncing */ + Clients._store.clients = clientsBackup; + let username = this.username; + let password = this.password; + this.logout(); + + /* Set this so UI is updated on next run */ + this.passphrase = newphrase; + + /* Login in sync: this also generates new keys */ + this.login(username, password, newphrase); + this.sync(true); + return true; + }))(), + login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; @@ -1073,6 +1102,7 @@ WeaveSvc.prototype = { ["resetEngine", 1, "Clear temporary local data for engine"], ["wipeAll", 0, "Delete all client data for all engines"], ["wipeEngine", 1, "Delete all client data for engine"], + ["logout", 0, "Log out client"], ].reduce(function WeaveSvc__commands(commands, entry) { commands[entry[0]] = {}; for (let [i, attr] in Iterator(["args", "desc"])) @@ -1097,7 +1127,7 @@ WeaveSvc.prototype = { // Process each command in order for each ({command: command, args: args} in commands) { this._log.debug("Processing command: " + command + "(" + args + ")"); - + let engines = [args[0]]; switch (command) { case "resetAll": @@ -1106,14 +1136,15 @@ WeaveSvc.prototype = { case "resetEngine": this.resetClient(engines); break; - case "wipeAll": engines = null; // Fallthrough case "wipeEngine": this.wipeClient(engines); break; - + case "logout": + this.logout(); + return false; default: this._log.debug("Received an unknown command: " + command); break; From a1d152afbd617b4edcbb42ce8d55cf1090a868e5 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 21 Jul 2009 14:32:57 -0700 Subject: [PATCH 1230/1860] Backed out changeset 5768a19f21d3 (crypto review not done yet) --- services/crypto/IWeaveCrypto.idl | 41 ---- services/crypto/WeaveCrypto.cpp | 191 ------------------ services/crypto/components/IWeaveCrypto.xpt | Bin 601 -> 529 bytes .../Darwin/components/WeaveCrypto.dylib | Bin 65944 -> 65776 bytes .../sync/tests/unit/test_crypto_rewrap.js | 35 ---- .../sync/tests/unit/test_crypto_verify.js | 23 --- 6 files changed, 290 deletions(-) delete mode 100644 services/sync/tests/unit/test_crypto_rewrap.js delete mode 100644 services/sync/tests/unit/test_crypto_verify.js diff --git a/services/crypto/IWeaveCrypto.idl b/services/crypto/IWeaveCrypto.idl index 36ed444bf2e9..611f4011d67c 100644 --- a/services/crypto/IWeaveCrypto.idl +++ b/services/crypto/IWeaveCrypto.idl @@ -179,46 +179,5 @@ interface IWeaveCrypto : nsISupports in ACString aPassphrase, in ACString aSalt, in ACString aIV); - - /** - * Rewrap a private key with a new user passphrase. - * - * @param aWrappedPrivateKey - * The base64 encoded string holding an encrypted private key. - * @param aPassphrase - * The passphrase to decrypt the private key. - * @param aSalt - * The salt for the passphrase. - * @param aIV - * The random IV used when unwrapping the private key. - * @param aNewPassphrase - * The new passphrase to wrap the private key with. - * @returns The (re)wrapped private key, base64 encoded - * - */ - ACString rewrapPrivateKey(in ACString aWrappedPrivateKey, - in ACString aPassphrase, - in ACString aSalt, - in ACString aIV, - in ACString aNewPassphrase); - - /** - * Verify a user's passphrase against a private key. - * - * @param aWrappedPrivateKey - * The base64 encoded string holding an encrypted private key. - * @param aPassphrase - * The passphrase to decrypt the private key. - * @param aSalt - * The salt for the passphrase. - * @param aIV - * The random IV used when unwrapping the private key. - * @returns Boolean true if the passphrase decrypted the key correctly. - * - */ - boolean verifyPassphrase(in ACString aWrappedPrivateKey, - in ACString aPassphrase, - in ACString aSalt, - in ACString aIV); }; diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index 4aa39bea5484..84714c8b02c1 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -1005,194 +1005,3 @@ unwrap_done: return rv; } - -/* - * RewrapPrivateKey - */ -NS_IMETHODIMP -WeaveCrypto::RewrapPrivateKey(const nsACString& aWrappedPrivateKey, - const nsACString& aPassphrase, - const nsACString& aSalt, - const nsACString& aIV, - const nsACString& aNewPassphrase, - nsACString& aPrivateKey) -{ - nsresult rv = NS_OK; - PK11SlotInfo *slot = nsnull; - PK11SymKey *pbeKey = nsnull; - SECKEYPrivateKey *privKey = nsnull; - SECItem *ivParam = nsnull; - SECItem *keyID = nsnull; - - CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; - PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); - - // Step 1. Get rid of the base64 encoding on the inputs. - char privateKeyBuffer[STACK_BUFFER_SIZE]; - PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); - rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; - - - // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. - rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); - NS_ENSURE_SUCCESS(rv, rv); - - char ivData[STACK_BUFFER_SIZE]; - PRUint32 ivDataSize = sizeof(ivData); - rv = DecodeBase64(aIV, ivData, &ivDataSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; - - // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD - CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); - wrapMech = PK11_GetPadMechanism(wrapMech); - if (wrapMech == CKM_INVALID_MECHANISM) { - NS_WARNING("Unknown key mechanism"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - - ivParam = PK11_ParamFromIV(wrapMech, &ivItem); - if (!ivParam) { - NS_WARNING("Couldn't create IV param"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - - // Step 3. Unwrap the private key with the key from the passphrase. - slot = PK11_GetInternalSlot(); - if (!slot) { - NS_WARNING("Can't get internal PK11 slot"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - - keyID = &ivItem; - privKey = PK11_UnwrapPrivKey(slot, - pbeKey, wrapMech, ivParam, &wrappedPrivKey, - nsnull, - keyID, - PR_FALSE, - PR_TRUE, - CKK_RSA, - privKeyUsage, privKeyUsageLength, - nsnull); - if (!privKey) { - NS_WARNING("PK11_UnwrapPrivKey failed"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - - // Step 4. Rewrap the private key with the new passphrase. - rv = WrapPrivateKey(privKey, aNewPassphrase, aSalt, aIV, aPrivateKey); - if (NS_FAILED(rv)) { - NS_WARNING("RewrapPrivateKey failed"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - -rewrap_done: - if (privKey) - SECKEY_DestroyPrivateKey(privKey); - if (pbeKey) - PK11_FreeSymKey(pbeKey); - if (slot) - PK11_FreeSlot(slot); - if (ivParam) - SECITEM_FreeItem(ivParam, PR_TRUE); - - return rv; -} - -/* - * VerifyPassphrase - */ -NS_IMETHODIMP -WeaveCrypto::VerifyPassphrase(const nsACString& aWrappedPrivateKey, - const nsACString& aPassphrase, - const nsACString& aSalt, - const nsACString& aIV, - PRBool *result) -{ - *result = PR_FALSE; - nsresult rv = NS_OK; - PK11SlotInfo *slot = nsnull; - PK11SymKey *pbeKey = nsnull; - SECKEYPrivateKey *privKey = nsnull; - SECItem *ivParam = nsnull; - SECItem *keyID = nsnull; - - CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; - PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); - - // Step 1. Get rid of the base64 encoding on the input. - char privateKeyBuffer[STACK_BUFFER_SIZE]; - PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); - rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; - - // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. - rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); - NS_ENSURE_SUCCESS(rv, rv); - - char ivData[STACK_BUFFER_SIZE]; - PRUint32 ivDataSize = sizeof(ivData); - rv = DecodeBase64(aIV, ivData, &ivDataSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; - - // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD - CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); - wrapMech = PK11_GetPadMechanism(wrapMech); - if (wrapMech == CKM_INVALID_MECHANISM) { - NS_WARNING("Unknown key mechanism"); - rv = NS_ERROR_FAILURE; - goto verify_done; - } - - ivParam = PK11_ParamFromIV(wrapMech, &ivItem); - if (!ivParam) { - NS_WARNING("Couldn't create IV param"); - rv = NS_ERROR_FAILURE; - goto verify_done; - } - - // Step 3. Unwrap the private key with the key from the passphrase. - slot = PK11_GetInternalSlot(); - if (!slot) { - NS_WARNING("Can't get internal PK11 slot"); - rv = NS_ERROR_FAILURE; - goto verify_done; - } - - keyID = &ivItem; - privKey = PK11_UnwrapPrivKey(slot, - pbeKey, wrapMech, ivParam, &wrappedPrivKey, - nsnull, - keyID, - PR_FALSE, - PR_TRUE, - CKK_RSA, - privKeyUsage, privKeyUsageLength, - nsnull); - if (!privKey) { - NS_WARNING("PK11_UnwrapPrivKey failed"); - } else { - *result = PR_TRUE; - } - -verify_done: - if (privKey) - SECKEY_DestroyPrivateKey(privKey); - if (pbeKey) - PK11_FreeSymKey(pbeKey); - if (slot) - PK11_FreeSlot(slot); - if (ivParam) - SECITEM_FreeItem(ivParam, PR_TRUE); - - return rv; -} diff --git a/services/crypto/components/IWeaveCrypto.xpt b/services/crypto/components/IWeaveCrypto.xpt index 2ed7438b519b70a0a5bc2ab3444d9d0e404b96bc..0962806b3443265ac2ff92deac91a390b442f80a 100644 GIT binary patch delta 211 zcmcb~GLdD1IHTZ1iP(Cc1_lO(hl~kq3~UZS)@#NFAPXe+6(+{W2o?hhG4irBY;V}n zuw%sz1~w31lm)~C^0!0yvaAq($BquD3SCB+T6;JvfT@8EE}FuOY+xxH!~nP$3(%Gh ckX>_tjHw{T4j^M8h_MUESUWiqh&7lD0sjFrm;e9( delta 309 zcmbQpa+76(IAi2QiP(C+1_lO3cE$uY1~vyEOOUYv$O4H;!^PCWVhlhrW0r>P4Lcfk ztk}W82I5<@fOtUub_m~@6~gb>(E(Kv%m`DP3TKruHL$@%>${kd4V=veF#uU?9Tu_u zP>ULNBya((W&yh80?0*kfQ-8!#ttCkC5W*L$oL9k>}g2MNzX6JEXl}aK(HqJGEP=3 pN-Zx+EC?vdEK4j&^-is1C`&EMOsfn?EG{m{C`v3&oqV6s2mmB@SsMTV diff --git a/services/crypto/platform/Darwin/components/WeaveCrypto.dylib b/services/crypto/platform/Darwin/components/WeaveCrypto.dylib index c1cd643278a53a75787c97f19758c3b10df1a2fe..a7ac2d1bb3410738e66d3ef638cde884cfe961d6 100755 GIT binary patch delta 8153 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 delta 10084 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 diff --git a/services/sync/tests/unit/test_crypto_rewrap.js b/services/sync/tests/unit/test_crypto_rewrap.js deleted file mode 100644 index a8ecde0e3586..000000000000 --- a/services/sync/tests/unit/test_crypto_rewrap.js +++ /dev/null @@ -1,35 +0,0 @@ -function run_test() { - var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - getService(Ci.IWeaveCrypto); - - var salt = cryptoSvc.generateRandomBytes(16); - var iv = cryptoSvc.generateRandomIV(); - var symKey = cryptoSvc.generateRandomKey(); - - // Tests with a 2048 bit key (the default) - do_check_eq(cryptoSvc.keypairBits, 2048) - var pubOut = {}; - var privOut = {}; - cryptoSvc.generateKeypair("old passphrase", salt, iv, pubOut, privOut); - var pubKey = pubOut.value; - var privKey = privOut.value; - - // do some key wrapping - var wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey); - var unwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, privKey, - "old passphrase", salt, iv); - - // Is our unwrapped key the same thing we started with? - do_check_eq(unwrappedKey, symKey); - - // Rewrap key with a new passphrase - var newPrivKey = cryptoSvc.rewrapPrivateKey(privKey, "old passphrase", - salt, iv, "new passphrase"); - - // Unwrap symkey with new symkey - var newUnwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, newPrivKey, - "new passphrase", salt, iv); - - // The acid test... Is this unwrapped symkey the same as before? - do_check_eq(newUnwrappedKey, unwrappedKey); -} diff --git a/services/sync/tests/unit/test_crypto_verify.js b/services/sync/tests/unit/test_crypto_verify.js deleted file mode 100644 index f55f7c004d1c..000000000000 --- a/services/sync/tests/unit/test_crypto_verify.js +++ /dev/null @@ -1,23 +0,0 @@ -function run_test() { - var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. - getService(Ci.IWeaveCrypto); - - var salt = cryptoSvc.generateRandomBytes(16); - var iv = cryptoSvc.generateRandomIV(); - - // Tests with a 2048 bit key (the default) - do_check_eq(cryptoSvc.keypairBits, 2048) - var privOut = {}; - cryptoSvc.generateKeypair("passphrase", salt, iv, {}, privOut); - var privKey = privOut.value; - - // Check with correct passphrase - var shouldBeTrue = cryptoSvc.verifyPassphrase(privKey, "passphrase", - salt, iv); - do_check_eq(shouldBeTrue, true); - - // Check with incorrect passphrase - var shouldBeFalse = cryptoSvc.verifyPassphrase(privKey, "NotPassphrase", - salt, iv); - do_check_eq(shouldBeFalse, false); -} From 37b6e1a6153952d878181de9f85b321d75d31c4d Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 21 Jul 2009 15:02:30 -0700 Subject: [PATCH 1231/1860] Expose rewrapPrivateKey & verifyPassphrase (bug #505401) --- services/crypto/IWeaveCrypto.idl | 41 ++++ services/crypto/WeaveCrypto.cpp | 191 ++++++++++++++++++ .../sync/tests/unit/test_crypto_rewrap.js | 35 ++++ .../sync/tests/unit/test_crypto_verify.js | 23 +++ 4 files changed, 290 insertions(+) create mode 100644 services/sync/tests/unit/test_crypto_rewrap.js create mode 100644 services/sync/tests/unit/test_crypto_verify.js diff --git a/services/crypto/IWeaveCrypto.idl b/services/crypto/IWeaveCrypto.idl index 611f4011d67c..36ed444bf2e9 100644 --- a/services/crypto/IWeaveCrypto.idl +++ b/services/crypto/IWeaveCrypto.idl @@ -179,5 +179,46 @@ interface IWeaveCrypto : nsISupports in ACString aPassphrase, in ACString aSalt, in ACString aIV); + + /** + * Rewrap a private key with a new user passphrase. + * + * @param aWrappedPrivateKey + * The base64 encoded string holding an encrypted private key. + * @param aPassphrase + * The passphrase to decrypt the private key. + * @param aSalt + * The salt for the passphrase. + * @param aIV + * The random IV used when unwrapping the private key. + * @param aNewPassphrase + * The new passphrase to wrap the private key with. + * @returns The (re)wrapped private key, base64 encoded + * + */ + ACString rewrapPrivateKey(in ACString aWrappedPrivateKey, + in ACString aPassphrase, + in ACString aSalt, + in ACString aIV, + in ACString aNewPassphrase); + + /** + * Verify a user's passphrase against a private key. + * + * @param aWrappedPrivateKey + * The base64 encoded string holding an encrypted private key. + * @param aPassphrase + * The passphrase to decrypt the private key. + * @param aSalt + * The salt for the passphrase. + * @param aIV + * The random IV used when unwrapping the private key. + * @returns Boolean true if the passphrase decrypted the key correctly. + * + */ + boolean verifyPassphrase(in ACString aWrappedPrivateKey, + in ACString aPassphrase, + in ACString aSalt, + in ACString aIV); }; diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp index 84714c8b02c1..4aa39bea5484 100644 --- a/services/crypto/WeaveCrypto.cpp +++ b/services/crypto/WeaveCrypto.cpp @@ -1005,3 +1005,194 @@ unwrap_done: return rv; } + +/* + * RewrapPrivateKey + */ +NS_IMETHODIMP +WeaveCrypto::RewrapPrivateKey(const nsACString& aWrappedPrivateKey, + const nsACString& aPassphrase, + const nsACString& aSalt, + const nsACString& aIV, + const nsACString& aNewPassphrase, + nsACString& aPrivateKey) +{ + nsresult rv = NS_OK; + PK11SlotInfo *slot = nsnull; + PK11SymKey *pbeKey = nsnull; + SECKEYPrivateKey *privKey = nsnull; + SECItem *ivParam = nsnull; + SECItem *keyID = nsnull; + + CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; + PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); + + // Step 1. Get rid of the base64 encoding on the inputs. + char privateKeyBuffer[STACK_BUFFER_SIZE]; + PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); + rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; + + + // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. + rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); + NS_ENSURE_SUCCESS(rv, rv); + + char ivData[STACK_BUFFER_SIZE]; + PRUint32 ivDataSize = sizeof(ivData); + rv = DecodeBase64(aIV, ivData, &ivDataSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); + wrapMech = PK11_GetPadMechanism(wrapMech); + if (wrapMech == CKM_INVALID_MECHANISM) { + NS_WARNING("Unknown key mechanism"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + + ivParam = PK11_ParamFromIV(wrapMech, &ivItem); + if (!ivParam) { + NS_WARNING("Couldn't create IV param"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + + // Step 3. Unwrap the private key with the key from the passphrase. + slot = PK11_GetInternalSlot(); + if (!slot) { + NS_WARNING("Can't get internal PK11 slot"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + + keyID = &ivItem; + privKey = PK11_UnwrapPrivKey(slot, + pbeKey, wrapMech, ivParam, &wrappedPrivKey, + nsnull, + keyID, + PR_FALSE, + PR_TRUE, + CKK_RSA, + privKeyUsage, privKeyUsageLength, + nsnull); + if (!privKey) { + NS_WARNING("PK11_UnwrapPrivKey failed"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + + // Step 4. Rewrap the private key with the new passphrase. + rv = WrapPrivateKey(privKey, aNewPassphrase, aSalt, aIV, aPrivateKey); + if (NS_FAILED(rv)) { + NS_WARNING("RewrapPrivateKey failed"); + rv = NS_ERROR_FAILURE; + goto rewrap_done; + } + +rewrap_done: + if (privKey) + SECKEY_DestroyPrivateKey(privKey); + if (pbeKey) + PK11_FreeSymKey(pbeKey); + if (slot) + PK11_FreeSlot(slot); + if (ivParam) + SECITEM_FreeItem(ivParam, PR_TRUE); + + return rv; +} + +/* + * VerifyPassphrase + */ +NS_IMETHODIMP +WeaveCrypto::VerifyPassphrase(const nsACString& aWrappedPrivateKey, + const nsACString& aPassphrase, + const nsACString& aSalt, + const nsACString& aIV, + PRBool *result) +{ + *result = PR_FALSE; + nsresult rv = NS_OK; + PK11SlotInfo *slot = nsnull; + PK11SymKey *pbeKey = nsnull; + SECKEYPrivateKey *privKey = nsnull; + SECItem *ivParam = nsnull; + SECItem *keyID = nsnull; + + CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; + PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); + + // Step 1. Get rid of the base64 encoding on the input. + char privateKeyBuffer[STACK_BUFFER_SIZE]; + PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); + rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; + + // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. + rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); + NS_ENSURE_SUCCESS(rv, rv); + + char ivData[STACK_BUFFER_SIZE]; + PRUint32 ivDataSize = sizeof(ivData); + rv = DecodeBase64(aIV, ivData, &ivDataSize); + NS_ENSURE_SUCCESS(rv, rv); + SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); + wrapMech = PK11_GetPadMechanism(wrapMech); + if (wrapMech == CKM_INVALID_MECHANISM) { + NS_WARNING("Unknown key mechanism"); + rv = NS_ERROR_FAILURE; + goto verify_done; + } + + ivParam = PK11_ParamFromIV(wrapMech, &ivItem); + if (!ivParam) { + NS_WARNING("Couldn't create IV param"); + rv = NS_ERROR_FAILURE; + goto verify_done; + } + + // Step 3. Unwrap the private key with the key from the passphrase. + slot = PK11_GetInternalSlot(); + if (!slot) { + NS_WARNING("Can't get internal PK11 slot"); + rv = NS_ERROR_FAILURE; + goto verify_done; + } + + keyID = &ivItem; + privKey = PK11_UnwrapPrivKey(slot, + pbeKey, wrapMech, ivParam, &wrappedPrivKey, + nsnull, + keyID, + PR_FALSE, + PR_TRUE, + CKK_RSA, + privKeyUsage, privKeyUsageLength, + nsnull); + if (!privKey) { + NS_WARNING("PK11_UnwrapPrivKey failed"); + } else { + *result = PR_TRUE; + } + +verify_done: + if (privKey) + SECKEY_DestroyPrivateKey(privKey); + if (pbeKey) + PK11_FreeSymKey(pbeKey); + if (slot) + PK11_FreeSlot(slot); + if (ivParam) + SECITEM_FreeItem(ivParam, PR_TRUE); + + return rv; +} diff --git a/services/sync/tests/unit/test_crypto_rewrap.js b/services/sync/tests/unit/test_crypto_rewrap.js new file mode 100644 index 000000000000..a8ecde0e3586 --- /dev/null +++ b/services/sync/tests/unit/test_crypto_rewrap.js @@ -0,0 +1,35 @@ +function run_test() { + var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + + var salt = cryptoSvc.generateRandomBytes(16); + var iv = cryptoSvc.generateRandomIV(); + var symKey = cryptoSvc.generateRandomKey(); + + // Tests with a 2048 bit key (the default) + do_check_eq(cryptoSvc.keypairBits, 2048) + var pubOut = {}; + var privOut = {}; + cryptoSvc.generateKeypair("old passphrase", salt, iv, pubOut, privOut); + var pubKey = pubOut.value; + var privKey = privOut.value; + + // do some key wrapping + var wrappedKey = cryptoSvc.wrapSymmetricKey(symKey, pubKey); + var unwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, privKey, + "old passphrase", salt, iv); + + // Is our unwrapped key the same thing we started with? + do_check_eq(unwrappedKey, symKey); + + // Rewrap key with a new passphrase + var newPrivKey = cryptoSvc.rewrapPrivateKey(privKey, "old passphrase", + salt, iv, "new passphrase"); + + // Unwrap symkey with new symkey + var newUnwrappedKey = cryptoSvc.unwrapSymmetricKey(wrappedKey, newPrivKey, + "new passphrase", salt, iv); + + // The acid test... Is this unwrapped symkey the same as before? + do_check_eq(newUnwrappedKey, unwrappedKey); +} diff --git a/services/sync/tests/unit/test_crypto_verify.js b/services/sync/tests/unit/test_crypto_verify.js new file mode 100644 index 000000000000..f55f7c004d1c --- /dev/null +++ b/services/sync/tests/unit/test_crypto_verify.js @@ -0,0 +1,23 @@ +function run_test() { + var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + getService(Ci.IWeaveCrypto); + + var salt = cryptoSvc.generateRandomBytes(16); + var iv = cryptoSvc.generateRandomIV(); + + // Tests with a 2048 bit key (the default) + do_check_eq(cryptoSvc.keypairBits, 2048) + var privOut = {}; + cryptoSvc.generateKeypair("passphrase", salt, iv, {}, privOut); + var privKey = privOut.value; + + // Check with correct passphrase + var shouldBeTrue = cryptoSvc.verifyPassphrase(privKey, "passphrase", + salt, iv); + do_check_eq(shouldBeTrue, true); + + // Check with incorrect passphrase + var shouldBeFalse = cryptoSvc.verifyPassphrase(privKey, "NotPassphrase", + salt, iv); + do_check_eq(shouldBeFalse, false); +} From e5839657d0c37238f67143db80b7f4599f316163 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 21 Jul 2009 16:54:46 -0700 Subject: [PATCH 1232/1860] Add ability to change passphrase (and merge XUL files) bug #443696 --- services/crypto/components/IWeaveCrypto.xpt | Bin 529 -> 601 bytes .../Darwin/components/WeaveCrypto.dylib | Bin 65776 -> 65944 bytes services/sync/locales/en-US/login.dtd | 9 +------- services/sync/locales/en-US/login.properties | 4 ---- services/sync/locales/en-US/preferences.dtd | 7 ------ .../sync/locales/en-US/preferences.properties | 15 ------------- services/sync/modules/service.js | 20 +++++++++++++++--- services/sync/modules/util.js | 7 +++++- 8 files changed, 24 insertions(+), 38 deletions(-) diff --git a/services/crypto/components/IWeaveCrypto.xpt b/services/crypto/components/IWeaveCrypto.xpt index 0962806b3443265ac2ff92deac91a390b442f80a..2ed7438b519b70a0a5bc2ab3444d9d0e404b96bc 100644 GIT binary patch delta 309 zcmbQpa+76(IAi2QiP(C+1_lO3cE$uY1~vyEOOUYv$O4H;!^PCWVhlhrW0r>P4Lcfk ztk}W82I5<@fOtUub_m~@6~gb>(E(Kv%m`DP3TKruHL$@%>${kd4V=veF#uU?9Tu_u zP>ULNBya((W&yh80?0*kfQ-8!#ttCkC5W*L$oL9k>}g2MNzX6JEXl}aK(HqJGEP=3 pN-Zx+EC?vdEK4j&^-is1C`&EMOsfn?EG{m{C`v3&oqV6s2mmB@SsMTV delta 211 zcmcb~GLdD1IHTZ1iP(Cc1_lO(hl~kq3~UZS)@#NFAPXe+6(+{W2o?hhG4irBY;V}n zuw%sz1~w31lm)~C^0!0yvaAq($BquD3SCB+T6;JvfT@8EE}FuOY+xxH!~nP$3(%Gh ckX>_tjHw{T4j^M8h_MUESUWiqh&7lD0sjFrm;e9( diff --git a/services/crypto/platform/Darwin/components/WeaveCrypto.dylib b/services/crypto/platform/Darwin/components/WeaveCrypto.dylib index a7ac2d1bb3410738e66d3ef638cde884cfe961d6..c1cd643278a53a75787c97f19758c3b10df1a2fe 100755 GIT binary patch delta 10084 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 delta 8153 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 diff --git a/services/sync/locales/en-US/login.dtd b/services/sync/locales/en-US/login.dtd index 7580210cc704..8d3d5a48fb0e 100644 --- a/services/sync/locales/en-US/login.dtd +++ b/services/sync/locales/en-US/login.dtd @@ -7,11 +7,4 @@ - - - - - - - - + \ No newline at end of file diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties index 0742305409e1..78296a628591 100644 --- a/services/sync/locales/en-US/login.properties +++ b/services/sync/locales/en-US/login.properties @@ -7,7 +7,3 @@ loginStart.label = Signing in, please wait... loginError.label = Invalid username and/or password. loginSuccess.label = Signed In hide.label = Hide -pphStart.label = Resetting passphrase, please wait... -pphError.label = There was an error while resetting your passphrase! -pphSuccess.label = Your passphrase was successfully reset! -pphNoMatch.alert = Your passphrases do not match. Try again! diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 390958d71722..721f0dbccdde 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -112,10 +112,3 @@ - - - - - - - diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index 9ed3b2db0815..24711fa762f1 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -12,18 +12,3 @@ reset.login.warning.title = Reset Login reset.login.warning = This will permanently remove your username, password and passphrase from this instance of Firefox.\n\nAre you sure you want to do this? reset.lock.warning.title = Reset Server Lock reset.lock.warning = This will reset the write lock on the Weave server for your account. To avoid corruption, you should only do this if a lock has become stale.\n\nAre you sure you want to do this? - -change.password.status.active = Changing your password... -change.password.status.success = Your password has been changed. -change.password.status.error = There was an error changing your password. -change.password.status.passwordSameAsPassphrase = The password cannot be the same as the passphrase. -change.password.status.passwordsDoNotMatch = The passwords you entered do not match. -change.password.status.noOldPassword = You didn't enter your current password. -change.password.status.noNewPassword = You didn't enter a new password. -change.password.status.badOldPassword = Your current password is incorrect. - -change.passphrase.label = Changing passphrase, please wait... -change.passphrase.error = There was an error while changing your passphrase! -change.passphrase.success = Your passphrase was successfully changed! -change.passphrase.noMatchAlert = Your passphrases do not match. Try again! -change.passphrase.noPassAlert = You must enter the new passphrase diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7633f877b059..4d9e8539cdf4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -512,14 +512,28 @@ WeaveSvc.prototype = { id.setTempPassword(passphrase); let pubkey = PubKeys.getDefaultKey(); - let privkey = PrivKeys.get(pubkey.PrivKeyUri); + let privkey = PrivKeys.get(pubkey.privateKeyUri); - // fixme: decrypt something here + // FIXME: Use Svc.Crypto.verifyPassphrase. }))), changePassphrase: function WeaveSvc_changePassphrase(newphrase) this._catch(this._notify("changepph", "", function() { - throw "Unimplemented!"; + let pubkey = PubKeys.getDefaultKey(); + let privkey = PrivKeys.get(pubkey.privateKeyUri); + + /* Re-encrypt with new passphrase. + * FIXME: verifyPassphrase first! + */ + let newkey = Svc.Crypto.rewrapPrivateKey(privkey.payload.keyData, + this.passphrase, privkey.payload.salt, + privkey.payload.iv, newphrase); + privkey.payload.keyData = newkey; + + new Resource(privkey.uri).put(privkey.serialize()); + this.passphrase = newphrase; + + return true; }))(), resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index b047cfda8ff6..a0d8f3a4a3a7 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -626,6 +626,11 @@ let Utils = { options || "centerscreen,chrome,dialog,modal,resizable=no", args); }, + openGenericDialog: function Utils_openGenericDialog(type) { + this._genericDialogType = type; + this.openDialog("ChangeSomething", "generic-change.xul"); + }, + openLog: function Utils_openLog() { Utils.openWindow("Log", "log.xul"); }, @@ -654,7 +659,7 @@ let Utils = { openWizard: function Utils_openWizard() { Utils.openWindow("Wizard", "wizard.xul"); }, - + // assumes an nsIConverterInputStream readStream: function Weave_readStream(is) { let ret = "", str = {}; From 4793698ffcf4da6bd1a4bc2250a8039ed58615a7 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 21 Jul 2009 20:59:02 -0700 Subject: [PATCH 1233/1860] Make change password work (bug 505570) --- .../locales/en-US/generic-change.properties | 28 +++++++++++++++++ services/sync/modules/service.js | 31 +++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 services/sync/locales/en-US/generic-change.properties diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties new file mode 100644 index 000000000000..d20e80e3afe1 --- /dev/null +++ b/services/sync/locales/en-US/generic-change.properties @@ -0,0 +1,28 @@ +noPassword.alert = You must enter a password. +noPassphrase.alert = You must enter a passphrase. +passwordNoMatch.alert = Your passwords do not match. Try again! +passphraseNoMatch.alert = Your passphrases do not match. Try again! + +change.password.title = Change your Password +change.password.status.active = Changing your password... +change.password.status.success = Your password has been changed. +change.password.status.error = There was an error changing your password. +change.password.status.passwordSameAsPassphrase = The password cannot be the same as the passphrase. +change.password.status.passwordSameAsUsername = The password cannot be the same as the username. +change.password.status.passwordsDoNotMatch = The passwords you entered do not match. +change.password.status.badOldPassword = Your current password is incorrect. + +change.passphrase.title = Change your Passphrase +change.passphrase.label = Changing passphrase, please wait... +change.passphrase.error = There was an error while changing your passphrase! +change.passphrase.success = Your passphrase was successfully changed! + +reset.passphrase.title = Reset your Passphrase +reset.passphrase.label = Resetting passphrase, please wait... +reset.passphrase.error = There was an error while resetting your passphrase! +reset.passphrase.success = Your passphrase was successfully reset! + +new.passphrase.label = Enter your new passphrase +new.passphrase.confirm = Confirm your new passphrase +new.password.label = Enter your new password +new.password.confirm = Confirm your new password \ No newline at end of file diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4d9e8539cdf4..ece8b171ab09 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -183,10 +183,16 @@ WeaveSvc.prototype = { }, get password() { return ID.get('WeaveID').password; }, - set password(value) { ID.get('WeaveID').password = value; }, + set password(value) { + ID.get('WeaveID').setTempPassword(null); + ID.get('WeaveID').password = value; + }, get passphrase() { return ID.get('WeaveCryptoID').password; }, - set passphrase(value) { ID.get('WeaveCryptoID').password = value; }, + set passphrase(value) { + ID.get('WeaveCryptoID').setTempPassword(null); + ID.get('WeaveCryptoID').password = value; + }, // chrome-provided callbacks for when the service needs a password/passphrase set onGetPassword(value) { @@ -536,6 +542,27 @@ WeaveSvc.prototype = { return true; }))(), + changePassword: function WeaveSvc_changePassword(newpass) + this._catch(this._notify("changepwd", "", function() { + function enc(x) encodeURIComponent(x); + let message = "uid=" + enc(this.username) + "&password=" + + enc(this.password) + "&new=" + enc(newpass); + let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/chpwd'; + let res = new Weave.Resource(url); + res.authenticator = new Weave.NoOpAuthenticator(); + res.setHeader("Content-Type", "application/x-www-form-urlencoded", + "Content-Length", message.length); + + let resp = res.post(message); + if (res.lastChannel.responseStatus != 200) { + this._log.info("Password change failed: " + resp); + throw "Could not change password"; + } + + this.password = newpass; + return true; + }))(), + resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) this._catch(this._notify("resetpph", "", function() { /* Make remote commands ready so we have a list of clients beforehand */ From 0573d8c8beb69e0034eecece5b03df193f234596 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 21 Jul 2009 21:05:23 -0700 Subject: [PATCH 1234/1860] Verify passphrase on login (bug 505571) --- services/sync/locales/en-US/login.properties | 2 +- services/sync/modules/service.js | 40 ++++++++++---------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties index 78296a628591..c465c9c59564 100644 --- a/services/sync/locales/en-US/login.properties +++ b/services/sync/locales/en-US/login.properties @@ -4,6 +4,6 @@ noPassword.alert = You must enter a password noPassphrase.alert = You must enter a passphrase samePasswordAndPassphrase.alert = Your password and passphrase must be different loginStart.label = Signing in, please wait... -loginError.label = Invalid username and/or password. +loginError.label = Invalid username, password or passphrase. loginSuccess.label = Signed In hide.label = Hide diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ece8b171ab09..b6e98f8336fb 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -482,7 +482,7 @@ WeaveSvc.prototype = { return false; }, - verifyLogin: function WeaveSvc_verifyLogin(username, password, isLogin) + verifyLogin: function WeaveSvc_verifyLogin(username, password, passphrase, isLogin) this._catch(this._notify("verify-login", "", function() { this._log.debug("Verifying login for user " + username); @@ -503,25 +503,28 @@ WeaveSvc.prototype = { }; res.get(); - //JSON.parse(res.data); // throws if not json - return true; + if (passphrase) + return this.verifyPassphrase(username, password, passphrase); + else + return true; }))(), verifyPassphrase: function WeaveSvc_verifyPassphrase(username, password, passphrase) - this._catch(this._lock(this._notify("verify-passphrase", "", function() { - this._log.debug("Verifying passphrase"); + this._catch(this._notify("verify-passphrase", "", function() { + if ('verifyPassphrase' in Svc.Crypto) { + this._log.debug("Verifying passphrase"); + this.username = username; + ID.get("WeaveID").setTempPassword(password); + let pubkey = PubKeys.getDefaultKey(); + let privkey = PrivKeys.get(pubkey.privateKeyUri); - this.username = username; - ID.get("WeaveID").setTempPassword(password); - - let id = new Identity("Passphrase Verification", username); - id.setTempPassword(passphrase); - - let pubkey = PubKeys.getDefaultKey(); - let privkey = PrivKeys.get(pubkey.privateKeyUri); - - // FIXME: Use Svc.Crypto.verifyPassphrase. - }))), + return Svc.Crypto.verifyPassphrase( + privkey.payload.keyData, passphrase, + privkey.payload.salt, privkey.payload.iv + ); + } + return true; + }))(), changePassphrase: function WeaveSvc_changePassphrase(newphrase) this._catch(this._notify("changepph", "", function() { @@ -603,15 +606,14 @@ WeaveSvc.prototype = { this._setSyncFailure(LOGIN_FAILED_NO_USERNAME); throw "No username set, login failed"; } - if (!this.password) { this._setSyncFailure(LOGIN_FAILED_NO_PASSWORD); throw "No password given or found in password manager"; } - this._log.debug("Logging in user " + this.username); - if (!(this.verifyLogin(this.username, this.password, true))) { + if (!(this.verifyLogin(this.username, this.password, + passphrase, true))) { this._setSyncFailure(LOGIN_FAILED_REJECTED); throw "Login failed"; } From 6ecdc1dc910a93a24c7c9a12aae3bcdd8ff3217a Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 22 Jul 2009 15:30:16 -0700 Subject: [PATCH 1235/1860] Crypto binary for Linux_x86 --- .../Linux_x86-gcc3/components/WeaveCrypto.so | Bin 53420 -> 55737 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/services/crypto/platform/Linux_x86-gcc3/components/WeaveCrypto.so b/services/crypto/platform/Linux_x86-gcc3/components/WeaveCrypto.so index 2c78e87717ce86eaf9f50d295b46c661a6eaebbc..7a45ec7e7414ecd9b479882e959c4afdd753f843 100755 GIT binary patch literal 55737 zcmeFa3w%_?**|`AAw&pf!6>MxK~bRyWJ9yfzSPd#Wryaa)juS8gmP=eq^xCtQ}f!9og z>kuX)3`e*Wfmbp@BfCbk7WYmQKG`j0&#MSwh`7_VVJaRd8j6P=gi!+|o`+|3 zrspHn{TV7QFr{5>x=%9QpR4XG5vQA;k2Kw9;@*XDiF#IznB!)W!dD{BMaW0E2q6w3 z6=9(3`~e=0#e~uJPiS?irlzH>ZMhj|%tuE)JGzNi-*w;Ttw0}uT1$=fcwdYI?I zdv18ExayW+j=|d-KYHWS(=RCA7<0v{&bL<#JN&};|32yKUyOZj-lC56znpe(^{~r) zkG|S*v!h|*pY}h|wdjGki4Qz{!k3j-z3<0wJv8yainKrN|F3_1_|o^DTo~$@|3K|6 z!_N4!W^4J>;dB0a^U~4J|LyKun@_)H$sg`1xx?=K%y#fkZ(KRY@#pRz|Ea6_*juZI zm7h58^j&L{_HIiZx$fEEXQwYI+VD>2+?{WusZm!pm0Vra;k-cx(lp}Xmh`_G0|#kL zpJu_!j6tT-_fa9$#r%d&@lh(m1m37C74Zg>;xK=)gH(1i&wv^AeiL z5bK*~!H-LYM2hThnH8G`0?R*UssB0)Kj&D=PqySAWx;Wl_TFttf6_E~S^r2&dmb|7 z=Ug(`Lho%$e|K8a&$P(TZQ*BuCI4fV{6Ds|$8V|cD@*x@E%JZZ!oS;0{b7H1S<=6; zlrONf=Q9hw+`_+0Ea|zXiH7yPXQ{8&(*A2Kc&DYk%PjQXu%w@3DW7R+PoD+<$ddmF z3$|O@+h(cn=N9~9OZ)z0k(ZcYzz1IB$IF)X9I@2*nWa6;E%|+x`VLz#=O^R35H~i9 z{ykyQueFx)laYR6s;0e*vLm#i+8pRdEF8ND3fBWq0?t>s8S#+u8oW@M{~4t3$3yyA zV?cqv(l{D?TqCq3?N*HE^Jink`!ISTIry-g@je`gH5 zkDI`+1Wf41RQho|zaHh8&hnFxe={7Mnn6hI2&p>Z4@?WCxSnzYxWKH{n!XE=S*fs4wMSmsu_YRyp&UGyReUx9{EPYV& zqaFR54JYMu)&4=?$0RtzpQ`+vhr7o`__-hWDpPwnx6Wve&|3=!DeF!=#AVYy>)QzI zG-2||`yKe1s=f;lH=#dhvgm&s>i-DxSOU!UK8Aj5fUrMN`PW$J(|%p?RL0w~+Ui?eTIp4dr3J1P)gG_QJ*T$J z<8gVk(rXvl?Io_VM%Nto>IQH9wCswC0#~J@v0qAVx_G#xI4XbtZ1w@l+SBK{yxFxY z>fP1es+A6BO}}FH^g?rv0WxHO#C+H4hO%mRPPNxFAdw6ZG3SwFX4HA+6nfp&bt_6; zP3Sw=F~{yGm~T(3^JJ?OZBb#Vx}8&B=Pj$QbGh@$N%QlD)ogcExoHbrbt}A8=yN0^ z8k$??EgPVZxvuj13Rg~<$2Bd((g0^UxZ+$;DidW!6U}g=@*r4GRX^J4Q*vGI>P8oO zpXaV$>4bC}s@$l^QcYocsncnn;+$`{7p`83++d7GR657)D)YLWIgaA=u%z;HWg2>v zUne` z=4x3dXnV_6EU91UDz7R7BUXmn=PawRJam@1%T|&Z`Nc9V_(p`|1XnF}HdJSP#u8t5pxABvjFU4W175 z;%M)$8(?)oVOpu?EGS)AR$E(NZc;&vEuq#(BP;q*wWps#y#>{N@=tfvnFa(oR}eKQ z!{b5b>tE#zDO0d!r3)jx92_06Q|MQ?YgBfpZ+W<6Smurntqs!?nDQg7{D$!RKK9CuA-h6!C{dB`Gv1Ahi!PmCJWyd;k78*j4W62->`yf_ znrhB!ugl}Gs7Al0OqDtk?yAFSpCK6)egs<=-8aQZS@L9p=~q(*MG-L=s3=oa6gDEP zDQcc5%qYeG0w|$mfkA@e>{r3G0@teODlEi=Hk)+1ABCv~2SJ}R=758Ox>-0yHON%J zi2@~-JUQ}IvQ(f80{uW?582K%Bg~|kd$bo6OsZcm276-&0Z4;t9 zKA;uBT&}#8I!}I~uYqRN1DkyH!X>L4TpU~Z)sBpt^7C^Y{a8ImIyKQgkUe9u&*fe% ztaN2rIj7JJkj`~F`+G&x3tY7>PP0s^s1-TjBBke>d@ALKHTc}F(#C4H*9S9QdMzC( zQ-ej*qKciQ3yTWDzlHS`zFG>PZdDCUG#+6NYN)Ss)p-||)s?Mqxz*!Bm%FjL++{pm z=~`J1H^x~|n(eJ$SzVqFiMdvy6tiawiw~m`m4%Z2tt z|Ga2Po_*Sq`fqWw(pi0&L^RE?>qCDmUGvm6peAo6ODL*pW`YG<;nLW{l?@SkQN>M# zmeh-;MKVTF2{$KPTsdB*1n@D^hB>uhzA-;~JmqC|m72#};c~mR#>z;G$spItdiQEW zAfO;-Q^W->Ipv}UE9)CwT5a|6riSwRm6JU6ysz^#xHHtfXJz$D*AxSIYK_}EkH>WD z^Hzsb8fq&%M(Kvya^n_FET1p|geFa69jr^{F3rx#FSSpaqRm~9p95GrWm1N`FHN5mevbPzu(S}V zc4~yUNtGwq|BTw!RNe;a@@_9^q3NlvjG{$V7Kyr|6i;&8*5Om6R`}`-gtsL z0Eybe>X~=^-B^JR(w+e36@!*}O!++*RAU7?M0;L6vsG8)2qRAG4Cf!D4OM9@@2()d z)3n#svpc?90X7ZR-c&gE5T3HMa564c$74KGYmx}kQ zc)yAdsQ93YyH(ty;v*{VRq=5ZYj?>0$E!F|#Yrl*sW?T&sVdG=@jMkfRUD7K4hT$3 zRPksPr>gkea*UXjI4`P{aZ9a?vmlQcC(eQx1UckG&7Z*q^yq z#hX<8kcu~}_+b@4s^Z5~yhX)Nsd$@;Ur=$IieFaoP8IJ~ahHk@s`!wKbrr7%pV_{< zRJ>8e_o{f4iXT$(W)*Ky@lz_^rsAC{-mT&;74KE?eia{3@j(^$sQ8GAdsTc~#agRu z=QtH#rs6aeXQ+6Fif5}hOU1b=o~PmkDt4;4oBUX+X-61)u}8>y5ieEoy=!3=fbU|Q z2)Qzb<=MozTGJk4d;#_*8PC_WEr_}2`4r<=P20vePSajsjQz4U#_`xMW(S z#rQm&hciAOww!S)&Sn{p!Ja1L3$ZuK_##cqVmub(lkvqEpNucT_+)%3#wX)(7@v&C zLk^6wZ+SiA%P>9}W8b%eF%EyL7`rh(8DEd_$+!gLld%uulksGXPsV8&pNz3dx|VS| z_AnV^2EL1N2IR|lD#k5i?DuYBd;{dnxK`6PGsZsV!;EKW+M|rI@B0|znUFtY?E5~& zcsAtE82hL%Fvk9D8)Iy?zRWla@@EV?wwrM_ss0qmup(!9=)%x zZ`~g6ApOU9svjPNKN(9PgX?y+_x&y&&-#82)f89X&!LQ374HupM6te~L+P|CCLit+ z7|Nwp@!{b%fuT%V75^OGA~2Lkt4bx_EHH1Y#u0B67)qj5L1NnBW`UVh1&(Wn8w7?j zXjNIn%LHcDs(Hjtfgwz-%1N9hFpE|#B~B9+@fx$SfY9n!*z^uAz6Y&;-p{!cfX5!5PFC%`Gc%#4- z#9N4)1!mK#wh=c7TuaRUUSb<@o4}ihM-guk_#xs{;>`kYCLTw; zQQ(J((}1cL|(AY$I+Hcogv{;w=J?CQc>Z zEO09EIO2^0k0nkcZWeeP@eJYyfiEM@B3>qN8u2`0r@$G+PU0+qXAmzXP7`=G@iO96 zfwPFKh;0Jr5;qWQ0?#9EBJTN0jDO;0;)4P^iPsZ%3A}`OBXOI+ONloTZxQ%<;?2aH zfnkq}3-#YXCN1r^T(4=Z-L~fEA3_$*cSE3+0$A7PJw4F643GTrWA(f6BoNpP6l|?R z`-1mW;P3dZ?Y*(VhQ7?tJgLF0D69puWBjem)^~3!bI3B8pSeHmXeIfW)|YM9-FQPg z=K~${DXRbaI#wSaxThBRF&&&4xTl--@9_=|w(=S3@7vS*a{P5SvC;o=^jru!qC|b7v4s=vf%MlQ0ZAZ_ngLDvy~&viGJLJr_1`LGn` zyQTeNEOJ`9b8XNBu)=pJxMqK`em`omw>=C0RQuxB{*1BuyW`N(jIn;l0sT2hI(Xmi zPy6~h9X(v9`|oQ501JbT9%liX)aOgo$NqzDN?q6H??_(10dy3(;F{fE{5s(138+GQ zAwz#r7X<5c1|7SdRis#>Rb2udEGW>2p)yIP)0?B$Q}IC2J00DsLAIb{zq6!RUxnrd zic(r%9_5d%i7C;a0Y+=>NA-_U0&FCAW@tWxD~AME%Fx7qTxscEkP4T_Ft zwDWa2sRdvu`v}e_virC?9o=;7I~}@Y#s|o%ohEe4 z-WT%d?|_96NT+YGcor&?a)}8#x^diF6H}x0Jxv;r6Hb5oo@V*Jr&&>-uct)60L5Eh z&VmIS7bqHyvYoN$facVmC)J$$eb&x$t2ylyPSf9Wpx~4kwBCFB7Bb?5f z7+I(8kVz47%Q2v&?MtNU>T|)Ami82^62Nb$H26JQ={ST;o)`Ni)ANoROz%uy|D+I7 zw-XwJdZ8Stl5xfQ1?Fr4%4UxkQ~oZr9|P@+Ut>4~1j*mVVQisuPziq*rPdt~+=tXK zJo*|F^`C(!6xmG48;Yzmc`dyOIdV$$EwZ`@vmhEJ)$Pcpw;`R6$e!f2G-u$D7(;!& zu{H4}idTmPTRC*sZ4)`^@kyzHBV$W`Pt&5e!gMP=^{xy!Mg?X#k!q9nv;?;)=)cfS zP)c6^1JDQ*>Bu<#FMbxRO-!+Vqf84u zWZ1|IFuSFl8$hky;{v{Pa;=pRaY9LnA+L@T9<({Bj( zrDOF_R9B*3he|>to%W-$jW)doWm$X9z_nvFIXbY$rhf#9w6s4{h@FN$-x%t@PcPO# zM&WBp!IBhx59wg4c?DI5=8JJ+-xHi0i>${!+kPYxKCcoRgeBIxXVXOPG0*o z9-$)I{fPZ9p@G>8)6ZhyLr>q+%&m_Vij~FEj{$qXD@uxyP#kpZL;*h5KNi`<07zc@ ztgOJP|MFAS)8uu(kk5+sIHUK;Yrl_Yu)^qLUqd{3xJvfJ-x<0s(w9!r6J%9(g>y== ze!a|Uo@Vmpqt+J%7rfHSh)(>cXq~O~MRqPc;SlJ1Ws=3ppbFKoy^`p1{{$&errYpXPBBn6_8R*3p5i0)Y)A51y3KO>+3T$0nzAOo0A?NK^eT~&LSIKU z_j*BqTCZQ(V{~|38xyWWg0S9BJvW@x;hm4iH8%ZbkO>y;1XVFuoO+kY1G>H@VQstm ze7mV0OJ!N8$1+SiARJK&DQs4zzb_RGlMQnyHQ>EIPL^+dk*zmUL{I8unTQskWySi= zPeeCDKUZ?cL#&hZooH`M`+bWrQRXCG{u~Gp5^LK>CA|I!>AIjbuON{WcLewZ|jgn~t0fUpxXzCHj6@BU-H~cvF!5ZD*Dw>0N#?4i`nv)*9N)#2HCACQ-Iqat!u{NdW!C9#&U_~_Z_gV&-7Qryg-=}7%j0ldzyZK zSPo+1*T|0gy44Wv=S zJ{Sa34Tbt{FrcOV0ad|5Otjc~Obp&L4?9`94eb?EhRI)81N|3eTqqNm8;{N1bw~ZH z;(g~r5}kADP-!Llq7TqLtj&%d61o{JjbaRZ40*EitQDr&JTcv z=fB_EV&a>NK^B{~Hoa36xfew`DGsf5O`_(@MdI)28Nj2fM>YNqs+SLUbYG5o#|9FR zqqxr*kNwadS~q<=c94L_4z=~eXu*Kp8tu zwm_QymEgi7eP;zm!e=_xb&=x;YIYp)4<4`zq5G^O#oxt(y}A#BDsYx6+sm@O{)FhV zqS8o7m|1_3E=yPc%cYHapZ=Fin(O_qf1z}Vl6xxGOGBiF?DC%O3=9uCjyQ`K_l=-f zO5(xoSbr=y5mqa2#d)?xtm>SrQ8q zXE5b0?e$sx`E?kzh_=o6C)$Q15!tr+|BG!)5N&${ud!J5YoXD$-KUggSihdee!Z1r zuwp{Lrt3iyyNt1DXuMJ{Vdr7M6T#y&{rYf$LBc?F%3NQlak1!cL6s7bT8g-*x4oI$q5VycsD+!3#B6m=BC*(nSxOwuR3 zPZss!jisPF9&Qx}vHz;1U~4=^sXw;0EqC26obq~43uMDfgmFw$r^nLE=mz^*+Mi`{ zSt0=?{CV+k*^&F~K-O3m>gxgyWRXCEY?I-6NGgvADxUu~70$mnGR{g0I7e+;5);qa zKN${;F=}ymZQBK(&)(+$Kzcl?#6oP)*jO%W9NLDR!o-5beO)4VXAD`<3BL{>wio`Q zDx(Tqh62MDLkqF1qDqLo_BNRt&JP=rSk1xSR~1fU`Rp;X`j~20Mu+J2@E)3lyXwN{}stkme;}#$L>4gHQ=h-tBGlT3E(( zcCM%6oTFtsUsaQd!SBBe+vm4+W??!-hqHtVh~sxM$M1w+oYI-ay(!J_OyOCMb2Lu% z{LWOI?H|HkbIbN;XZ^2GD-!cEUoPNenkaj=Wl-c~`?hz<%lW7c4lBIenz4EY`Bjt7el!INn1z6_hNhcvta@@rmTF_unZ@5NStQzOa@C*h71_)&T`OjKY^ zcwb0=?j82wKV}O5IfSV?fO%D%2XVXZ;=dxd9K;QyFh&EnjKZwSxYog1%VNv%(j=XmW?V>z!LzCdm|MX2d30Ss9n}p6h!TZjoXGQCMAy$p-qOpCM0L{I}5n`x{2t+oEOC0VQo|zlYS|tw&aYx1#U*5KKiT znfy@XUC}S^L(I!_`AA+8gN3OQC8n|jjEnxWeFNml6?uk>Jc&;IHgg_HriOM<5a=8W zTU8{czF53_vg_=&i@8n{Hc+w$im8A87W?#jyc&yr*gy`fQVIs`oZ}cLe`(#t$@TNc zkG;NFZ%0)Sf`146&IEC~=RGU1FtMZrGgx0kN`e0QUo1y?_BJ(*e?pCxGsId^+hWuP z!ofUr;cKZK*v1*N?kIGN#?yNiQj7J6!6hot{y&E)i>0ohrslmVf}jS1|KhwC;Xn`c zyrum`yfmyhK&zG0j{!T-3JU|y1OuE-G9V582ZN%HGNb!1>KjctH;eifqCR6^tRbb- zagd|w@Ou1dy=|1{y#|uQK3H%Kj&k&1U;#!5R|9-`Q1cfZf&mj}vw?-(>vnlBa0X(G zT|rx4V@lt&U~xQVK<8+Bk&d3$H9cC>Rah3Bzz$0wZ#2vf-{z28qQ>Nk$&b5;j-IA7 z`y2;bJ7#cv6dj!9v?bqBP2-2wC<_HjPk~aX8y9R`e*h)e zrY3N!3-?-RGETavMib zHqYGF+ri|7qjNJ-y?;ZdPP`q2OLrh|K|zUA=evl~oOLu!WoF+!+?><7o3cr!;mqYB z*l{u2A7xuNV@sPxRBjHk>hIx6=#b#jOL4GEt=*}CHHocjMrp}w??Qce-9q|Nt+#_; zYyEt*D#gAlaJ$XF=1?deQpldQ@Q}9_C-e|$ATNH_VBbgB)tEIm-uFI^p=QlZ^u38u zJ8N!|Z%=Ry47jmZ_FYt6P=L43?u1=3dXfU8x|lW<8-yuNu~-%hpbxRrFAORd!!ObE z3iT{r4AWbppN+TBRPkhgDL5J0$Nt#+XkdjtZ^6KCX&M&4v7mr1km zMhOo$DQBU}(BH6}4}o`lci=oKr_kpVq0j6wl{l57j`J{;a;`oTZG{@lYO=ZS1-FfP z-LNnhiq?FJj|fmR@?c0G*sFTr8&3-(w%)wwGgVHL3B~$2yn8`QQ}=p!3=2!snmacB z_~Cou9Lc+v!gue7?-Gt0sY&6xuVA@ki7awoP`NgIcWe0WQ24F}It9K84dv8Q9L&fG zon9G;smzS^CAaQ~ZQUN5`MLWOdmE;_v)fvC#)OU{KZTH@PlD;e`=9uD1m;t<<#*2> zRQxgy88Cl~7Z#HAX7R{o-;>$lJ&oe-;{3_CAA0(HNBWA8A|#!B?~c$xv4SD_RQ)wr zzLxff1o<9CJ|j%NlV@FYg~!N8{*KHOjj{e-JnJUqA}$)C5!i#1{!Q!7zRY9p&ys(@ zw=6MZi%&F+(yK)+(?l)CzU8Zs2j*l_I1;kz1{*(4ludlLc)F5qs)9u#uc;0?M&{d5$tw4eEI6Zz&dpQ8MMsRLooiyjn+ zycftG$U&8;Z#`8Pg#SoRjTA)>;ZroGK0N=b=)L`9eR%&3cKitxQhnHf0_a0bn^_9% z#Ws<19doK)lrpOlLv%0h5_zvdUdX}dMVf4i5X9ghKHHjLk%W*VET&CgAj+RDx`K_< z6rqJl`s1&O&SdubxBGWM>Aniar)TbLjQ77feqUTsPt6c)EkO*^CA$m-G{#N9XVbrc zgj(9$@CLOpbdvO?ifWIb`T6*+xS}I9L-<;0qTbylYQ*R<*3S@rivE}&v)V)^QU9&z zUepNuvF!1`JOURA^5+Qh#t1A3lMj!;E9ImA?g;!l*woVg)I^~#YMgifX#nZ#FN&NS zqH^wF&Vk0@Mv?gjWR8r%`;gffgZ)KM-6Kv8P)( zE6tmf+2#2JMn&u#jqiHBH}s{j0gwwM4C5DAlLQ(^prwiWa?k*Q)>ry6I~t`Crv((STo=N6>FET3Xs~!)N^>BWUX%<+zk1sAv2R(b<|I zaLyouZM*u7A^j#nWd^9wM{?k0`60o3p8NtwfC$HMYHELxDN#%b`LCs020N9u(bug5V`TH1GgC(`Idy$nU5 zDp>R!hZOuf>}{VvZf`sGdVc0BUNZAK=!x7X5|gEnl>8__=)JJDOVxiWNY@F{##l`- zlg5ggr2Rd|UJtH@hN#JsFYgRIS)PbYmZ~0owy5MZR01cgbxjJk?QVM$K8ypW>A}|P zv8KW;loaohiOD}vuRh+tchCW};pV=~&pnaZS*q#hnMDybh3nQGSkn#dfay*u=_@)Y zI`2EP_0!l{YkH)bz7&j~Hr~+J)*Zt1YrPPDXJ>hW)l7CtI=5<$z^AP z* zc$VwbV@9wH4Hb3}lQzW8Oz@qN154pAO1c``T>H0iO%#gd_K7fk)Y<{;`96BdFWW-n zny=8jQYdYR=o;_kAyp#+8=s|cT>3SSlM z*HbarTG~Ipgk#$nfERZ{bhF3e6^!4a^Qcrz!D7ox`h8BmRL1dV@`25x`j6mZOZ%oM zqFvyazjG;gtzRdoVr2gl!#Udb6&^fXRkT&F6qQUtCE@wyNvurBqcrm9a)U?BvJ>FZ zcLEEKY)DAnI(+8ZGhTp=1Fy2lt0R~qXE}P@JI%aOvK2C2m>pu~lN#bWc;0Amjg~a|a2qVVJL-i07mtt}kIy|S2 z))xv==YSNwDBtaz@$jJ_wV=c_R~(+_TD}zGXkFPIt2Wj6nH9`n`vbo2S-$;l>{=#; zh9#;vD7kN)v^w6U0+F;QPzXE@5zZ3vW~+SBPEy@JD1bP zGeww})KWH8+K1!110ri_A8{cJg#Q?w2zV#hrooH5pkTZf;5#j3tK5`V>CZW_AptuF z=v{c{F3eNx*V*)=kU&d&`xvmbcZhcs94)UKTQr^cRfoaNk=77Y%Qn!>y&m(Fd2KM1P5!VMRK{TE~M%vR&J`CmEg5WjrzPYkU$>; z8$@t3Li;8Z>1fm;=j&e;l<2R$B*y}_y6{0veT#&~uu<*Pdob#-|9t`4)186!0LAP$ zh?&%>S0)P9wVoK_y{Pqs&AYr4-_Zfpz;@?B!T1+tr;RPo-w8z#8iD;>XMyqhN`g>u z&T;h1y7fF1gS_TR_gQJfVDvKdNbq+;8=hn9gvEyi(ie&*o`WVTWq4W;A-oma#sz53^s<5$u*7hC%Z044+Zc^Cz`FXlhIQZhY&Bq9(^*e`-+6lTw3? z*S88ntsn%>iP3Q&YFMDR`cTo;B)?dsi?s>-Q~gUk4HVI2K)h4YaV!+ip>zk$#0y(e zv|nyWSRb3yhavLrgd#ff;*+h3`m>UG;D69@z^Tt1P^#Rw8!4K1=sYpCsrCG=A~zfr z*&uq!Y>!LQ#E9CeVPY1Hgh^X(L-l<;3GK=3b^ndFS#EBJbvsr6Ek-xie^IrZHE=C` zvZ;|;z~FDI1wtFO!I7k2C8`;TYHGCgj*zh@@YsuFB=-b@@v))A4GTjv6TQRcVN2i$ zmIuOx9^Vx@7HKbOoq+bVwEuCm+L2iAz!5-?n9Oe=xrH<`+#%=;X2dj_oWx*!CaNg< z+f18xNLFT#+#fjT-!cA`(3_$LxhtN=I({hXSb;j&-nWbjj=#Y6a!ZtRMQi|PAO}NN zWuEX(_xFtNrRBun!~6exaWd8|?JFkBy;vORL*wz2Ah^dwnFlJ=ojz|SCL8ai!MvDR z_{2}A_q^nn9B(a_Z{?oWCo|!=W%Z8(z5}5LgjZz@f3yOxxdpTdSSVt?E6^RFFU)l4 z06oO8htvh#Xleh^xn?z*DjNVMts}pDCMIQeuKFB&8-F4+oZZ1Gld-EYbP;T*=Ob>a zbooD@P}CFJCEw-X3XHEx7V*8>>iPzex+zoQdEN;eqJ&Q+udTe zKS;w$&paR!)P~0i%Q|-wnV#vet-2V~A$&R<>4-*~wqjCLqp=b5GL9f(P$0CQ8zDWR zr%Y}7HB`H$z3Ch?lWekT^p{=>26+^lQ0r&G*$ETiC!c|q+M-xBBo7OT!{pSTB&RyQ zl~WA^aOzCas`t+}x9XPXgj#@hU`-GD?(ZEV+3<%AT^g5k@E1IYrw*xV~FjtO) zi@^TzJ@cUOZoCUXQ+c}gqMQvw*6#9siJAWy7{u|N^xniM(tjq1q=tU7DAZA4$E<1$ z!PSa-2YNum*5gs+620RMa&Xgb1V2HN1eKfSloSu=A|*>WX>HU1d9wX0jEI)@Riii; z;OzRTXYkTLz6=7l58#&eDe|6n2^PaV@(-*@3D|COBqg=(NDOAg-sG@h^B@6>x6oO5 zV=T$Z?@l<*X22YEz%xn0o}ljo5Hj9bg=10dtVs$C4rp->8{7u|oB?v%_kban;BgvS z0NpAs(brKu2H$j)F9RV*j1AsSTVgOJjdNrd35K;fiD$WQLU~)-e>oC1(%Qt?387?m z5^d67ke$ms;rrMyEP@W)2k^xvmeu$y7%K0KAImpJa0ny(>IdOL!MjnNiSTi`sCP2z z6@0AS%I?c0Ly~@@6rEzF)GR$sl=uc8NZ8Ig2Jb*S^y=8=Rz588GVjvC2{7$uf3-W1zf-IYu~5W;%qJpy8V9WOL7M% zgZ?hOPS_S($*;;db^~|1h0O300+;fk5y{UChub=Shri1=632z`Yv~-~&5950ZI!K= z8cwS~6SVX%x!bz^6?~z~hN8q9lDF=dac>OQ`N)BVy`ZYUF7iAQod^C^I1k^Xs>G{9 zx8IO_zbpg237uBi`qk~LMzEa$TjjbvzE5uu(@uNjJ+@HXt;4dK(TQGDlISDey4ewj zp`C!#rlhRQF85JW7RiMhf;qphZ0$W8m9~CVhT3qqHoo!TsU#C!`aIlvI^PI@-Cw%G)SDJpQ zR2?Duh+XD{ISTrBT>r5se-p}!cW1D}s;~W>5I^31==6@jd*wd7`qCgY$kq`*U884Z zS@n*BV(3c*r7@rsoUP%*_D+3Pm=NCn7M8A9zf=;c?2N}Z)kT~%F+S`^<;)t0g-N&) zS|nBNn0Ka9wPU_d{T+==%?2_tS5KJ=MP_(#i))6S9HA2wPhRGttsM3;Dp$Kw|Ui!P;tA=lIO+-eJ)` zoA|JH@)OSZ;4O(I`sCjVZt*+;^TnIsI#`fitdB%D0MKMOcs9H>((*p|emgeIwC2;g z_*yRpTAJKrBlCzs{VRM4n@c{e~et|w%>YLJk zom^A($k-aGljN~Uef=gl(o&KKW zC$@{fooGM`)+^4K&kOXcWV1tm5^w7~egaz|J%)Dh{Ti5deW7T;Ic&fhTOd(LF0fdM zHFF={nr$5J?>+Wm>sMS);Qg@1A(8Ey%gKaQpDJ-|_g;tZH}dPC50xqh`lB6;mZ5Rk8I+LE7`i3-G* z(@c6o)N!~1spJ{`A$T2u!EtG@*(q>)o&-PP>Ch!Y%~Jb%+^BN1 z(JA3!k>c0>Bwy``5s!v0~PY0^c6cp-jL+IH5W#QKA zzea}MKJVGV-|;s16pkmricjM4X@CSI_%JMmS3xVHGY#KM+JK_^L6E)%-$BY}C%eQ~ zwWk(&T<%or*rCA(TP8f9QzyBBlDix z8e03-$T@eXciru|x$uqQ4=mD}OZU`&Vl>5Qhj#m}S?})NGyJ)2PtU{m?;ocKd9{hZ zT3*-i+D%zs>>a+*r`aC<`{h5pxJkRXUc0zLySQ5Bh>4AhACxdS@w6dBlZKt1Y#Tn} zjFdCa8ae9hbIu)o-uV}#j=Aunu@_%*>A3OVnQ+;}Nt4s;=~FVMPMba>bLOnsmtS#Z zR@w6M3RmTds_L4XYFF0PH{9&@czunlnpWR(-Ss!zSi0=w>EFF|&26_gx7^XXcHN!- zvEIKSa98kq|GDw|ci(gGefR%h(*r+z@Sz|5_$Qlx`m>)u{EJ6^`RITB>er9`=J6-C z{PxM;J@xd~XSO~2-19HI_|os&w(n?v`41hPJ9oYE$K8K=^|h|o_q?(9&-?ze|F3Vp zb>QuH-aYu<`+qz1!H0j>4}TQu{`iwmdp`U8iz8niJ=Xh=uf9J1&51tkf7<`-oH@CU zyt(u8ubRJL;i9YY0k^^>Ma3mcuenzC|K#caXY^lc!N33hn*;ymz`r^0Zw~w)#sN5% zeQc5u_iG+LJoe}+`Fj_}+}|(H|3wcva-utP?VMx-3p8yR!lR4v z8v{=Kj`t$`mI3ZF@{Js)jx$hpEkfQxY%$Ko?*Jea%){5_BRqurE`+^E--q-^5nqPq zvk^|H>oN;O~qM=g|j|j z?*BI)a-6({qjknSoM-%nit&@g0`oV3`1~vtU#w!giuoHvOrNjfA{Cdbn7=uM6`OV+ zV*D4sX^S<9`0Zoz|0#qoPxRG~1sH_;R3u_YTH6SKZ$Qz$K_N7W-;t%oAw0a6zw`v% z!5{3#X~Pk)N3H2NljeOFPNPO39tC>XpW;{8h~HX9#IND6w;>&BR!>?zh|kAK(N~Dm znBjkTEoqGq*mt3(eTZ-rA>ksl0pSvaX$W};#R!!MjRh`G!d8S1gnbAfA{<3X z7>n`e1YsIN9zrofB|;;@ zoe1|MJc6(lp#xzb!iNY)5fZS{JPY9xglPzQ2*n7M2#pALBHWMg2*Or`4upLOA0iw@ zNVpW`5iUWPhLDF)j8KWth;S#u{RodBY(?lm*oW{T!cl~TaVU=vf2XEB2OIA^w&jh+ zU0r!ErQZMK)p=`QcoomS!j9`Hueo#P%uF4Z>snr2R+pMFY09K26K9N5EsOPgj-v)fb06}T!==aqTIbNj^fOuU#Y+wt$*|5KhsiTTu$j3|Ln z9}E{^UyRv-zrl_F!d}sy7?;fVIAFYZ^a3*RE(^LSpU9lQLKG`*#pm6`-Q?xI>e>qV zw_>7e1%F5nzj>EA(VjlBddiGx6LXf9O|5lLmMIgFBJ)o4;`eqZiySO6x!mKMT<-Q( zPb$>@8zfO$gQlfs7pCI3_cHMVe=*G~L~Ch3<757Szo`h5y(=3gmzNiMG8TAlndJH3 zsw!r_$#>8<(j7WimLEK+g|*3diA7oDab*w)QPxZF=Hl=x%}EOD^^UN96Jb2 zi^op|#*Tyd8TEK>U=A)0pPzy0z^I@75eh%}|_Yq;Rq(QA9E&4zc5m=V_=qoYMM=~y+yd zxA1eAk3J5==eRfO<#UD{1o}n1r*Fj2hI`h>yo`Cl4acuP$};q^80cde^q7yn*0rX5 z^vM|Lt1%z_IU`>=?%CD^1nMw@*#|S~z0s79dd#ql)BOk{M1Kte_Uq-z?l`j_YGUQ4|)|ARmH3gISFG|4{h^znTFM z$S>nE@_i2vNRRmvn^nNQaT)pU!viCqtr`78Fw&UUxc?#UjeM!d!;m2wfdpKs;;*Lt z5;0}LB>J%oGf>c=!Cx55qD(P?wSKC14|8{GBL^ zfbY@?U(CnrWdzpG_GKXtgCSEq3b%6??oHK*d>2wU>N3*y02}vA;xCUmRUSr$Y~I7Y zIo~$qJKOZYxc%6Y?{z=MIti)E$oDm{anGbLk#8V+{HYXkK3f2NW&w2>?cT4Ddm>!iJ zQ;g%p%2bXWiBlMfF(UDq0&^@#jQ5yuQRjKZi50KBm)JZ4nT}WcOjn$y7^|MeiCHO< zAuh@Svix~Uf-+CM_F12pVwxq6LH|k7=zk3QPu3d!k3s*Z0vr5}1;6J5v;EYc7_|Rd z#JRwfJaHA`%}PGZZ`L!`??rliGydRW{Vj+M`NTjz{|Ria|3ToMM*P7=dcT#o;xFlK z$6a@`v|TZf&zF{TjtR<-oMk$HF^FqSk|aI;S{Z*^cZNtm%VOI;>syI*<6?b&3;vM> z^T%;g?}*g*5pWhTS;G1+l0!(4B)uuX7nWiTjVEl+0;b=fX%2-eEcx9Qe7gnT1)SI- z_h@2bM`%v~a}RME>BSD!-T`ie5csQb#PJxy+@IsfA|4OCr%ux@QaBU1>q<@N3Dcdx zdy6#fDwTdGF!%mwbC~`d@ON@G&82WR@GZzsJz@GqP?S-o_FW15LIb{{NTnA5KUJe? zoIja=HSp{TP5X($KL9=f{w!1YMc|Df{+Po1fw|{dtne{ltx2{oCU%&1E=g@>-}rY%(X17I)sk1Qg6D5faxk)w+O&je02^?wEM zA|!H3B)!$ZcVhg}@)P@kp8`EYpMDPfGUR!kN^b*R4t*P^@VmfEuaDp%Nw~-O0Luiu zbYPwbbSeB@VD8JK*&_W#;PWvaFvJA@8}MvY_A`aKM!C?VX@6Gu!eKZM0Dp26UIOgK zc)m*E6~OnR|3>-)z~`X9$twN7fa9|w@^}}RXBtygdgAFA3-j?i#|mE!y!;xhy%eqm z<~f7W{+|K!>|%mSe*^fEI(!gA;ePq5dj`p920)z4S?`A05D(=0(Qy@Zp;FCeqJT`7Z%}$JC!Cz&sba zNTs`hN1?q?VbPv@fFEy+=0^zej*KnDp_?5s>Gy zi2Yvxye~`BE?4Cr0)8CrHTb_3IH?(lxME^$T01b$myGm}fgdV}=;xVdVD5(gfn}oo zdBEQRvvw=I4*0|Dh&*-xpI@VCH>q?TxcLsrPuh=Tz&w*Oj%I*3!A9Y_X6__Z;Ha-056698R;V+7@l20Y(m~MfD3A&Ln{B>z_()jp$nov&jP;& ze}xz2-35FLgtbuNkAc%}*0hTi{s#D}C7L!~;q%VIT5mqS8(-lPV4mk0>GuQk91kP} z{oe!kdZ5D!zXv>{CE`!SkAyD)VupPi4}2HoH%8^Z0yxh!KQ9C3*`LAB`+z$zzK!|i zY2b%2K8*gn2h4NE3`OsvQLu+-99h8r&jNn3FoKr@FT!Ah2u1o2fcM@Qq4zBCJFs83 zsPs31mt|p(P2tfnv`gL4S%sazyRVAOS9bz$hWyf1`UAkHmqhIAHejBU8sq6Nz`ydL z4=VpRz&t};r0{v?U>^y>tyVY}crTb?*js+$>D6mA{s5=w&rg86mTTIB3cm!r2>NJ@ zA5I*b(OzSGoDIC8CL-S}fbT;7YgPI01M}Qf7&tb6_mqR{38A{vPNX_ENh5^jjkK<1FB>zz;*eX9DYY$h|4rm-)am zvLgE92JXiAH~RNW;7U{ZKLXE$d{9KR=TqQiP@eG$pP7pJ1LM!G@I}BpqaUGgB{0wB z&s2Cl@BvJwKT`M?z^Scrys>@Tfd4WMiMZJR*HSg@!UjzPO9XxwxZ%pk{1Z1up5Gh% zn*q!{Ctmc|vVohgkBq15f%*P`VIS`TZUcYDiu@zAhk?5wf3QH*7h?Xbi2YrBp{8Ar z&eML5ztsn&jaG;3ua` z`^@qUmh|-&{GbK@#)4lr;Z#(05I6zG?pTa$@7ZL8x>6BOG-38H+k)o zwxris@EsPs5qR_Ea{grhegQlL{Dqne{yk^O|B3~_VZrZP@K?aKpvQeM(!Y!omAcrU zxxgD>ygpIsYk|+7t!aNm8uRlUdmPF$FY$g$`QyL`ps&d){j5tMU+{zaF-9AvT@Gwq zq)}kOl@@$E@KvVzeh&N*+Jh-o)c20^*_kkN=Ebohz z^1S3bMsC@kOyEhd7x{QheOm&2g=u_L1K)_rZH-EQ5V&cgrd^=u?FIe{^2kyA9}C?+ zfc*EW{6)aV{+|u|KV`ti#rED}!QTg-G)vPyMH<`lnkD@k3qJcYgPQEml@@%R1$!;{ zKY)LQ{&SdJi28mC>_wlr7tQ)VvE=`U1&;(TjEnvGjs;(7!DSZwT?-Cc@FT#^J7o>z z->)s{yMXti{TQaAy??f(zYW}kbk3LTU&16wi+|Z(ekWu%bo6|Ihia28>3J4hY{6a& z{!a^j9@s4Z9hUTc7TjaOiLgD!#rBQ^erKB8Kcjs3m7vDOsm*7?%zrnqvA;e7rv$WR zGchKN`Se*!{(TnA19f~LqOual=%uA|mRwy>x*)%BNogriZWOv|jwz|4zH~)x{qnNf zQgK3DTIOrgaNyfe>+-rPCQY55K1E|fX>~;t(Ns+wE04Pkj${QNA(rd3>WVf51+A|jWT<+ERbzYadvaHV;0PThstxq%16}D_h}m7kR5|Jt{3sE+2eU&*sYa9`edq z>1x$Xba-l=MmH^&H(USTeWP=Te-QYleE~;2GKYcm~x?N>nS3cxXR#)!IbJwp_{VA_C$(z~PR#`{s@-mOh zp5dtT_}s2JWesKJ)!x;P8n$~$Rb58uB6~mXOfPWNx=_l|$j+rLDlE zeJU71U2yR|iZU-=?pT0#D16Wx2fhpf4Vp2hthP3q6^1HC>1Uz66xz>PzO`+3Yuf~S ziqoO){lp^VkqM@tgWmd^paV{)U`$l+GV-eHDnPZcw8oiNjZB48N^2Z!OeR<$Ibcs? z+%TuA%9#8d(x&_q`Vp;6~jkKpZ>M9CcWfhRMRD^yFuxAw2 zxm_!&J)lwOs;zW5?QDhBC^(_u`FYi~uKt|J6n!M09A44KekyBdVZX{}6ju4X74@s? zRO3tvm|5uZE^yVY@K!m5E=D*nI#uGX_C`7tJ)Y*+CAlb(D@jo1)fSnjgLZguoMdc7 zsu!*K*L7BK2JJKtPSsAB7ikUh?YLC~Fg!FJ_Ka){Z|JKWbe{ZgtK8vm7N(Ut=PxN0 zZ8Z<;{=7qt()OjNV=&DX6R3Pp=yEq!W3Cc|w4dm}WT--ob3TWm)Y-7kP-mTaHBKs$ zOmaB$8VjeDQh`rq*X)_bV9s@U$}#e2TpV)r+B0ZXkgnRKEK09r!-ziWoJnrEr$ zg{#UM#LQ@DqN4x?RAl`Z=6>d%!f6XrDBchB7A?uMPxICzni1A{C|7}rl`Qfv4pP~y zewN5S-B7ap+(q@-wYBx-4ySO%SjT)jtXz~9<>&TSpLBFK+MO$@hLv)=>)qDOcKee0 z1@)_3?m3vS3(PVQTr-(FdwLFxaGhb+p!S?jM6UjwIK@a=#4hAVyJ-KchM_h!bJn`* zFs-rnh~2Xq9d_=VlzxhhH^(vW}ed1KyIF^7LHb4y*upm)HtKvZt~2e zxzijs`^suP!qpJ7PQ(hBa!FIcF)p2i=;_R!QCJ0M?WTx(IIvm>i+mDmj1t0}z$>b& z@HIfVC($i9f+scU29bP^ut{>VkDk)(GvFh`B`IBnAyJxL0q27rg?V8jIK*ZSU}vr4 zC!Ka;k!w}7K7-}`mHbq#US!tBOgWokE#?qD*~tWcidD$K{QtHBXtVSzPhsZZKEtha zyF8wfDtL7sEC^f$kufMLw%XcU!`+zzSwvZ{@EYZ$2I{1ppTfZtF0ZEHMc?MBOe?@L zs?lZ457}P1l(8;&GChu!4c^uBD%`@OjT+)6Z&JA2kypFa?lF_9Vp8C?NvbqZF zU_@KKe*SjE@>!O@V2W@tqbgPNVbt2?+x%V-vMgeyk&8A_=~I=7s>BMtbcLt1T&a0= zh2!L2q?#tsNnw+e35*O<$V2W;sePh>7SDE0A=2;5#fUPw9j589-;5Dno7$&A2bOTN zC>P(l;;LJm?`(9=x2G@87Y`lSkXUUjwS~y+b|==ZE@^b4Ia6NnEkoImxM{g!&oDFY z=4uDs#%M<hED16Sb$I4ij}bmU2QE(2;y& zgY0BWhHo{{_G~dd7w~g0|HL}7u?7pgC2|E}GQ`qT<1VH$g2vb`t)4bx+N2dOZ)rn$ zskh2kchjWuCbT}9v*y_tiOT1cZi&%VV;{=m7fbP5%_>&N6%~p%i|Q+|Co_P9ASm~f zA6Ax8+jbUb#TZX=r>g%t$v*HrX|Zsbg;mv+-ducE33Hd++!LFM(N&ult!%8xL9|#y zC;Yd5zi|V~a9&U;Iy!B$0o`Vw_D};|DPWkNVHwhqnAQof& zYPYA$)zd$HJWx1QIt*m}$!EBJtmlB}-#qaoT{KcO@7uNkErS~UKADc4%p32|fV6yk zIJvs4mb+U63qY{fB-ZF9BB5JJ3GqEMb{aQf@i?g+84^kBi?}VeI$x#&T ziZ%xJY0J6MSSnp+p5n>X*SD+(>{GBSFAkc8?rW*S+^2RvL=KPIS*2&H%1}0%$X_#zJKQj+jM{VR|1=!#H zT~7Dc?BS}zNHjj$=vv3aN!9igJJ`9`EXrP(KZjk541Mc<*8r!wr*!+!O^i&-mNKgN z{~Ecr<+NcS$`^HFLunp~!R@#Wi6^9#_Qm5G5=`RD7(&9|_w4FoNp?tQGI;%ZJ~41}6^ zwzN+3Cddza#7NkSU`>2OfjVnB>xk9I0r2tY1IqmPeq9xqBkZR2tr72ceqeS|p~&dw z7$5-klJJdN3o!XIMTf#pTjc44Jdy3`Gh(uR(Ie!f5=fNox!$HmJC>)ONN4A^RQ2pWly(az>m- z!P^bVpY?Nml$fkmz-dH+&rvU|U~BxAM0bOz`{ny>d+H#L`3pDV(BZeAPpcZeL5{pC zVP$QC^I`FI65xg~Mneak0?orUCIdH~v4zosyt9($&o(rXoYY1o&X5gZg@N<|cKft| zhcZxi#P-HZyjSvYieX^&wt<{Zx(qOjKaMBtGx~xZHK+Q}J476gyByV%ubv+(RO+|L zr%G9&H}q&QOb9A+lV?vQ-u6&Jk2(lgk)Y);-$~^%Aeb74s%G^GemY_9@sTl;Mh?HF zGgm(zc0}Mp5`LxGLMEXs752r6_a0O)JP0p808}2W;2W$TgRLku=S-Lq*Sq+{+mlv_ z1QwN3O$9bgTH>Hxq=JHp$lMND(0-q#<%58U>{CAVtQH;)1g#_-7gQ%rFX{&{dsib+ zw=~%^IHAH3?rbvZ3_?xeRW2`-rdI`p;xf`AL@-&Bc(Vo{wY=G5fIXDJTp+-K2UMMP z0urQF(}J43qGh{d!1fjIE)5VVISVQ~<>U$)u5g}o>urb9zAayHPQRhd(Nc8Bs2PR^ z$KY#4V>Vk$ua;zQX;P!(EIH=Q0R_s*WV%{i1ySO`%jk0@=9!_!|H3_Q`hY30Qmc-P zRXGjA>&h9VAJYTXpdJ&u{8`c=pZ5XcjPYL@RQbBIdEeC?J8fXmwdxVFK0Hgey;h7v z!jnQ($&Zm!&1grgZ1XhwUW%?QjFe4DrN+fV`f1IMK`XmVQ1AQ6_*@QN9 z7)OZ5`Pw)Lm%jEc(M$>JA#9--3Q9)+yBlonp8SE~@nN|l)*)IJu@1D!LVqmV(L|!l zq)Q){BPz+jsEHBIIat)Lha{j73@1zmgen(DD=azLHuVXEBy5HNi&1gap)vei=8zPd zYx@NYftR&9DTpLOC?n!V2i7C=S0@c&*n!+vk~mEJwxh!YEomBSZN7lOoQ+Tx&-i0z zrbe<+N(^A@Rs*Wr9XB48tMz5_DHVA>86}7NT(D6h1 zcwcp7?lDW;e@*`yE#1or6F)*?&SkW~66^C~(wECXUZI1P5H53N!J$h8bJ_-RiQ+kI U8QM9p|J!J$!Ua97HC=!358sx&lK=n! literal 53420 zcmeIb3w%`7)i!=|Aw&pdz^JHHgQ9|$kO={apyoD#0D&PPiJ~%uWI_^>Nt{azDoPBo zjAL4=&{u6=YpZQ-tx8)_>xD)_iMQHleJ! z|L^;LJ+QOSbM|fRwbxpE?Y+;;-0E0b6c-n#8CQaqpb@2CplR8NyT2+^vbA(=j5b4? zrky3yjr@#jyfTa-uxTPa`zDkR$*?j>)2>Cpe~})KXxl9aUz1@T8f0L4BEpLI{WXYU zE+f4Z>E`-hMIi>3CGAxR0ffsDc;zBwAuK@PH4$Mhf)C+B1YVaRoFndH{}if6jfhJT z)*>_@tU)M8XhEn$xDtU^4#L$4mmo|+04v3nioal$wiuxRVJSiy!g2)TYEn0g5EmoN zGZGN5LZ~reJMQxkPBlHd2>0_5PFK%XBd$caPT`9YUxRSH!qX9R+%8r46vSsD)FYgM zP>o=|rU7IkkjE~BYY~bNFziBJJcwr?EJ2usaJ-8$U|d(>W|;v_5OaKal_881cbYaq z#iK>z@vs?T$|#9n!L#+I=abd_$trf5(vopsV!|n=d*04A;nPg_yggsxTEue@G8Dc7 zaX!LAgmV!R5YiDwyUrTr;Z!^uYkIyC_cIaJA;hWYZWULknEGSB`~YX09-L&lH{Y%R zaHt38A!Z;y$QK4kWp3P#@7^`M;Q_5AJu~;VwsDS}bZz`l)sj3daZ$3tJ7bK=Tj~)n z(oOm(qm%<)lvT7#suk3Rj0%ew$F|O3tCh#)Mwwj>*h@puU`49yDM+AJ3qD+IR0z!{$KZPI`W4N6RMAW zW#UVlQ{H?!{j|U-PL-d9ZdCxMh5 zmiqo^>2I;6{Ye)7pJd_ZZ!PtmW657-;ZM-g-c6SF&a>3F&eER|Oa418>9<(Q-)^C2 zodv&V;t$8mZ2X8{wDh;oG{G?aAxryzv$X%V1+TG`|CS~F6ia;p3%zGq`k!gRf3VOy z-O|7NE$#o>G9E`Q>6cmBf679i*V6tsE%^hM`rfx-&Ns$&4sL#EDetw&&t^;cPe6|s z6X3fjJ6RjA?Sa0wE|7MGcnJ7=z{M(k6XJ0*GO5x3rkIitPYVn-;Gr`}|xtjKfN}r7K-62gY1SUUjNB_PD$MYkV zPXBZ8f(ZZi0WUVSPd}~ARQ@01HEqi`@DP_x`*>t5oSdyDd<6aT&jd}1e`g~e1V8M^ z&;I6tKda$*ay}=$hb;A7iRb%4PpV4yA^*>ize?fT(Z6Hp-!_G}Aid5sJ_X>%5S%9? zo#XQGH2lFeSvyI47Ul24_%0H7yfz8>y~w{&Va?@Q*VOEB`TSL0zsse$>OJ*-%~gvC zN!4vtysc`iztOF^7Pq=e-RtUoez&)vvC8Lj`!v_8Wp;a|yQ>{(^z9HV5&Miook8(OOB zz4`Ti->8*jqY`r-SJPMV*$Y#`lH<>oGRd)E2u(C0`-5L#H}uNtL~h3@L+8h3t`&pkKWLV&XxTyZXS z$wZmaRA-o|A`GmrE~f6R*@bRzeXAS2FY-1wIWf8|bzU@NX{Id8<#gI-JD1q)WgD81 z8;sG2Tm@ctmEY~mcT{ABN2<6`rlCj09?7|~#%8~Kh^#2T(CyL690kjZ3tfe7pWoZO zA*OVZ*Xu7UW!qt62=Z0My<3g)1^5}T_shGjm_02 z$$-cUX+s)Yi>cL~bpojqwZ;rfmcwI`Q;th%l&*xuROX9qa$dRT25(ghc-Q2H;#Gs? zqjn1Y3U`e%D2q8E$S5J1>I|$6R~CNgX!Coks;TM~RgD3+%qi`I;04&f#BsGDkgN}) z&C)Slom=^ZbP67YYb0X|9i?S~wGHlSKd5UI-7EIgHgnY3tfDTQ7Vd~@i-d&h6Do6} z+LX{lYK!T^rO-jQ7d|*I-hI|S{@;H zS$*|-PgPT_u3riUet`v{C}1@6VMNxK39rE70obFpKbi-l+6zpk%3bjIipv5mw5UGV z_T?ojH?+8^8O8OE?DfUPg^n0ODUe=8v=d^_UJ-D6H;5^xwyK(QST?FIbUI_*l1oe7 zjc(3!OsZ)VIpCvY6`PzIa*900l z0G=BfXpHd)b6HEX$L;Z#RC%h_xxMOfncLf1U+p#?Ho2Rs;YYw)=lPqP>Z^+}V(uoC zV)i^?+hG!-vT#}mXHiZHNeK9>);79@!9!WdD?FUo=jAJl>~mK(e}SKq#pc6RMAsSi zJ?Ic+X+BR2TJqPjgsN4|L9jpT+!}k>)Do#Ls<^4p%4R`YBx6)9VRFL7mFHth03RbX z6f}bQ#!Tq*RabdxHJ`u6?e%J{wGoQRAa_%J`RtSkag&*0+-9ps2A2E7@nVmc>lG|b zaG4h90`qy2NQ*aZZp7mW&t!P9Tmt^erFF76FNsGziTHOCp2lNWT-MFMe9DXEd6$HA zdC!#b2>ciKKmIRX`ySSQT%Tnk{Pywn=xDNbCf1XD=AU^NR(4~w^MQHAfx6$T{JFrs zv+J?49j|4n{Au;|6EyVh3WaR~Cuobp<;Q5(tMaVRTf_RtX{9RvZ8z0`U1POt6fQhJ z2xcZ}wF>78oTzyeE)Y0gYX#<26Owz}bt=xiQ(~uz*QmHf#hX<8u!?`8;vFh}Ld8$3 z_&F7Kt5{d@po)i7d`QK^Dn6=WtxM9AsN!T5r>NMb;xrXcQSnq2r>pp46=$kATgCHK zygRlG*Un^e3-#amUp6nhL9FwLpr8Wq>6`0Lmph;P6?OO1>- zVI1Ockg%C?6ZR|^FW0m?8DEDz3&z)CpMbGj)9zu6gN1K1F4MFJ7~^o^A;#6%LttEj zy%)x4YX@WOJ3PS{hZ0XSUW5Hb#@O$9RmFWOepAK!RJ>os{VG17VqL|9DjrhtAr%j+ z_^66Cj4#`>sW?r=Q&jvS_$TO5ai5CcRPjC)?^kiZiicEuNX5e{PX3y#FGa;R6{o3q zii)SII9jo z&bS48M(hvbdsO`9*P-peuQG<9^)ZHp*~hpRds2+g*0g@c#Sp};a?dc4F)T+iV_1w7 z#yB{#F@`x!V+_kSg)#P?rZUDJTsmX4GoA4`?D;VsuW1)EPJvBiJOO8=j3?rJm$4IP z(~MI!Etjzk`+AHgVLyv8HVv0D#^#!n@yXa{W1NOFGsZZyy_PXHMb|Js4R(w16!3#F z4ucyR<3P8CF%EeBjHhA`knvYE?PkW`)gYmp9B43d@k6-cslfp@p<4g z0OJ|ZFUHs}e3#!K;@LGb;uj=@A76l&MB z_vC_Qp}Y5ujEr=?;UA;lkEiYPn?o!2m4WM zBJ8R z`~dL`;;jNdM4U;yN#KWx`C8Ax7J+|4oJ+h$;2p#~#y#j1_z7Ysajw8m6JJG~De!Z| zYlzbY?k27ywh6q4xP@2~n5?a9BOW@+{=Z7RiMU^2vbt^yai73%5^p8$7MQHByN7s( z!25|GAbvpLe&UCTw+eiKcn9$&fjKmFPZPHYJV@M4yhdOSRo#okPJs^*_Yvm`%%Q8> zN1Q3}QR05$bb&S8*6GAHffI>`h&6$eiHC`Y{=xpI5MvF1{tIj)PA2XXIE~mw+%51F z;wi*C1fEKqPW*tt>BKXLw+cL+IFopjz%z*F5w{3@F>x;O8i6y37ZE!J&L(ye=L$TJ z_$uN|ffo?3Ax;-Km$;7DCU7Bf3$Z5fBH}jUq0fc>6K^8!7uZR>g}6`PmBd?#yMe)M zal!6Zlp~rasJOG+KQYvC6&?i>r|X;XBow*_sHbMbysl@fUJ6H{{e=Y7qmtSWpn{I>V4cTxr96?nnkVLgluuo&6s{A~vQ4JG*gH;r?zK`WSQ!*}H?=pTLvg^UvY0 zpr>p5o47^7?|uOViCqr>b|dS`cVZ|ausqh@=zXrSx)7m@twBOy^iXgf29o|gstSbY1d zd{UY0MCHuttYfPUTHX1O>e5pEJ@7(S-0NSW&!H%z4Sy~e)qreWo$QaZvO+%t+zxGL zM?3aR3C1_XRqB;^gnro%>s=@p5?mnTuhasP;i#mmlgxAKXZ{^|heG8;?%Ai1eeja{ z;E!P##30BqXdk>bos0+9)AScndwb9OHFyw62E*Pp81@A2LXIiH?K^l=p5EK3htqz7 zJJa}puXx}LZhsh2*LHHZbR;k}xcwpE3jGI1gruOyy{TJ{(nyG^I1Vf}vXjMvoezl2 z8_b!1gUt3f#1I7g*v;qPM5a$4!FV7Jb+Z1SCSb$`A`uv9{_W#fk>1dDRvYZ>x809~C2@V62?%Wqm>3Cjn#xGMuPgVUL z&$Dlue=;}{Cp)TVLNp<^5!nn5`_CLm6O}Vtg}zMY82F|>HP0%udOCR6-gEg{TEiKk zKuU%F8&E-gcMR!!c$uzJ?|TmI&>$ztOcY9$Alk{6c@clf2E7ga-FMO^$XR_Fv$v0H;#)HjzMfh{}Hd ziLC6WsLbAt(XiU0#!T8GPN`ixX*u+{g1$1)2Xpirsd~ZAhj53fDY*Rs-jq*)Il>KJ zbn5Lqz+AAm!Om}sT(CU%h+OHx&TY7fGF8m%1ZmJH)*0Ns6@T@3*&Xsdb;}*_hr*V> z56QG9EEnAQCjNp>NOx~%Kkj9-`}kB~)Vz}v%F6Xe(0e6;sas~7tKEZ4td^AocfQEK zov-3AY$^Ui!P$e6o$QdR>!Yv)fW*I|<6WKmnHA(9Lc8p$%-btcNSi*>Ou}cRf!zFvOUO865g)0$hg9A>MDN0{03{ldAh7u>KH)fRaUd zmI4~u&i2V=D8Y6$dJz8Ishc0ih|8It?KLmuJ3Uoa7wjFV#jK80WR9B4spI+vDgT(unZuaNney&Og3=D5aq_6(=g%Y}UIyfusn%+uin+k)5m$ldf1e>FJ98-7gHEJ#BZeFETn<1iXUHV9Qvv4^CCI zishI7OVr)ov;QiCToZ}y#{?~cenDFlQ9lF~a*m57DkM7;9F?fKCZe!1DHGjm@3|Tz zV8emF#GQ&C^p7!MXL8mK57;bg-Pav`i8}{aWwsA4g|J{8^n)O-y(bA(!rp~8!fhCQ zS89TE+(QmH3v_CuP2LlDqEa;(-*b$Da{hN13)$;q^~}S*Xz8`m4e3y&L2TJmeS(PxWo8 zwh=M)!`{fQbNS(?i6kX^dq8!G(C0RcW25i;Uc>h;#(jg7p zxk=IA09{>)Nn%AIRz5@e<6*LT{nvBN2*>_Fg+32$(VKq&CB$;6U+;un?J6G9A;`}JKlip_9mF1s1<(t!Tfx1;@Vy60x*P{6nh)bvy7ke0(LJCW_HQM% zS?qXmnL&LZHIo%Jz54KJTyoj4rbX-5h*r`ng1x$vO@Kke#?A#ckIocT&J$H)C7Ik6 zIMkb;NQvmpPv!&?>fqj9DAbR?z4<9TWEUHKU3a@M*1@4($06p{9oRMs4(Z&e1?}8& zqyQFuD!&hG@o2 z^W{d!zbKQ3GNFZu*ih{}9K0bha3)5gcOktGty2HweRL0Nzxxo!j79~Ns7!wcEN}1m zM=868iCX&$7krSC3jKYf5O^&)fwkfk{ZUciWfbUg3`71nKo>&D1cjj#xz}2z-!8H} zDC^6tB~jR3G+F4<(WC5oaRyAVW!<3qbsc4ciw<5UCMovg&w5VuEks^e|+5I)U>C zc8r~&_!SkUBQ|6V1=MU2{9Q%xHOx1^V5XqsP{@Xy6IO)cD=KBq;0vMhL)cG-hQ|k! zyWGRU7rCK{UUdv6i`*$8u8m721!4|5UP|X1;6<>vBKVd+TnOE&Ul9f#1e<4|zys7(mjXB%n3 zJ{BC-15m2asjBQS%MJ&VqRWa#BPC&GgXOv`UH>naHrn0!FPAj8`=fuMbfp^i^q}KF zXq=Rgm;4i*p-EkiL(Ym7Ba=BSm5JbXd@vrI2$dWVjHHN63dUJS0dk>6OO7u_0a-F3 zab4wyN>@NmQ6Hv}vqGnZ#Au8@6v{vi{9agq;BPSWq1?n!GGwte**UTzMb=^qRisGD zRJ*-nsYe64^ndhEpxo^}smr0d8;3B928Y$msXzKp7zp~s5BwRLTt3A5-oRFLS}0y> z6to*S zazdKecR^D-Y~^S)QMX(EC7ND_ zDElicpOrF>A($onR&i2h@as53Xz(jT(6(e0+76->LEC=JbEYvIm9{j4wk7|AwB3bi zw!P=^rLlY~F=$(J!m$j?*G}x~S~+IO*QI)wS-g}k8WOL>i}N-OcrtjLsb3o|Fh&@t zUYYAtr5A6%N4k&T6%(soZ08<5_2S{^@wVtiSO)+3_x~+)Z|4rXhbl+qr0&4 zC7i7KPYUJ1ON4RER42C5%V-nDx3jn`k%SV#qC~jt$bEV!cRCA=^Z|!*SwWJd$?!a6 zEm@+L<^SzkIR9c5o|_VKPI-DITn|{LR5&n(*5bg}MlTCTi0?~}N0lhyQ6$Ud&H!!L zyh|=!G14b;^Fy?7Sm4)bn)J|A8CBq76qv9AQV6S{N{GDnZkZd-4~fz%AYko1KD_pX zb{cFQ-H9M9MR)!Yx_sSHBR7ucdU+s)gNm7VG zT9kqrdj+43K_fWrws+HOVHwkTwVs8ejrJGbTO?1w;fsFb368X_Hy6_>I-Dy+Kpc6K zIYB2p@3h`rtRWy6&NS@B2c1(xG7t1QnI_Ag?HCg| zO&$jW*xqviT7$z1FSlX3zHdMHSdj*2BvfGwmJfz1lCh5&x+SISZuTp*EHT`*U>iCq zdIw&?j+#3)C_1I1Q|x(s$OdP0q%;wx%HAzkB$S<_UF8ShNpuWe{7A3kfQGj#9^nNR zW9fvoDV*I>oJw)b97Eh*bqwoyFj1k6;k^pI=w0@qdZF;k2QXEKFt6SZHbRdp^_PB+ z+){~Kra(tSH%`H^&D$L}CTsrFLiy=RLm_)7A%CfmJP;W^b1&2X@($}y5cP{iQ->Je z_)`5QX)MDljxzljk@t5+q$N7`4jk zjW!9rMS}OeMJW^|!=JyKc1BE**uvY13a~!GoWe!p^ZP`~z;|e`%Jd1M*$V}+Y4qB< zI#M7HSnccANNU9@_H6iD6%`@Q*3OYqOo!;$A(29hh(l6%^ms=BuaB6#_<@7vPmke( zfZItRLVp12+1~R!US{mrn41D_+Sz(#mN`Z!EQrZG8R1Sxp1&ps3)}b6~5=#nczyF&y{O>D}jXon~I8mM81Ch#J3z z8gV=u%7;}-gVGkjqCr7kqBMW<*pb&)=T2Otfe0i?yWnI{K+i2e;r)$1PJd|J4{d?1Ob}9E3gU3hjrr;c9>{ z=4rw512ACv->@GaDjDp2$$z#p6el))#6;PeHZr%XA`vs7b1J<^$56+{A+2pOmIcSK zTO2By3bQkn9FCNclOH$P97AoVj5zu`_Rgbzl=shf+EQ<;hhk#4%+LnZ^aN@e7{o|a z;wT%cb}@UF1-)wTS)PwRv?b#_;3E>dJCw@?Ii%9gQHfKx{FZ5`(t$Jo1F*DR<^8go zMkgKpILz0R*~!3A=i$^X_o4)8Y6G|WaIX!_3ONQTN0nG=jRf?O8`4HDEyZ$6I?tVl z+it=-Q=Hw5IQlwz=g|;5`sU}`d<#2n1(TBwFU(H&zlTh{4xKgX4s@%u6o;dHM@@(& zHq_=YbKq`n>Auxkskq%;DRx<6^cyOj;knt~HIQe07RVe+^Ft4hSyoN`PJJ7)%ds zOzzk?MN8d$C)&fCI?|8oxE1``9OR=L((EsVZnXtB9vDc(DCEsAIpA-^8vq#6P*LLi zv4Ib;t1*9JV&Km>hMB)GIq){LcK*VYz#Cl~VZe>OvYXI!X(1ra8r8SuBA5f~{I%P`Mc1MbB$sh?}aI$xc5OE}j~6K@JAqVt?!-G_XRR7eMi` z|DOxmThazU_PRbremv8KpDhWv)d`2cvwcN4xs;S}o^ZnC>QFNfmVpY{5G3+8@O%E>-2?bVIG4Tw)ebprLr(Eq$U5QdJvdN3nRAP{AW>a z;#> zx+;8kbNKE+_^tsm1-=Ri<M?bsdP@j`sgC*BY3-I(%D@9uap zZs0KTV+^L~&%!db_w2(P|Jd=PS4tsW_$C_OgSQ_s&va}Y)?P~8F8oNAKsNn$QRqPw zayE>xz^N#Jya|qBu@`3KTP^b4#C)pezx|d2FdUx0gk{6dT#BA0@-9bS$g329WPKO1 zQ~q!Y>rVB`6EeMs2=6`Z{}o zLr-{8IN3n^`hDV&&HhHtUjIo{JaFT>9=_i)G!i&8QjQd|tUGnv?ty->mSpwm`s)~@ z_MQ(Hu=*iY{jzZNy-*}k{e1Z-xHsomYkZI*G02+BxqOFiVmDNJnjJ5W-ZLAS!BrdpQ{?V8F*Qas z44#GpHvRXIA-w;Q~HIYGh0;e6xAD};|y0H9-YhOqyO&c{2Df@z2}3=gtjXg z_~5lsMdy@4Z&{rrQ zMmbD29eO-=UTf!P`crcHd>=yK#Qx6AzSoBbJ`?RjYcRqP=g`I!)Nu&Hnyjxy9jKt= z<&m7dtx`caGjT`1WT-3AJCIyEe;(HP-SIgudJiGnz#nGz1$V#x`M__bwxCw(no-|m zQJ+#lsUstDUJLe836A+-lf+JN$3~mhmOb-WPQZ4;)l1t%8up3N(mj4kEuB`|h4&Ma+6!}%+OX|Ll5KY37rQ(iIWd{0o2uG;9j`$`lVc!hEy^!3ls zl=hwxzHkt>rY;nLs9h%%&@TaRN%k3|frYDYguG;$>xfveQ9F%6q* zi+&AHgJ<4d9p}FyZXu+D2%H#__4=c+y@L#ZhU-UiKJi5?w-nP)a>^rO3P*Y{v~dv9 z0ZWxqIa1y)Iv+TtZb8;7Kr?&?aMJJT@;F&)0hF$6JvR9XSO6~oisb2mPQ_+@E* zUb(@$DWv7L)-Z7x9xQ|~Ko^zUK6xxm{#!S^1Jfegj~hwY2!*mxI-#Q%lnzQc{;5IX z!(zV=^@+(O9kn0TUj!TR{yW}1_~bD-+R_Y)k<#CJ32fcll{hbov1h=;!wN>cNZ@B3 zU5PDF>w%Qo)LjclYOzD=7>dVc(!%-WgWdsSf&_0Mh{^iZqPbQy7d}7GTYH6m2scP* znm$|Paw6B4(~dl$9s7iKJPl!hld#t@py98!v59+;l`6i-~8`d9H%h%{8#=S#UG{<5UN$@#Dp!Sa;l*o@luG*>PI@!aGPrjJrv z5o^!iqA$D0HZWt;6`Fre>Qm?(D!(vX`8hE5cO+dFA2_`$KfVZCWtd*(I}UmK?A<5< z)ngOkvrgg1fc<(Bl&!tz+-&N$p@8SUh@o3B9dD{!D*7v@V6ibJ-9_=TGWE~oADOlK zr{H3H&ug>ImA(d!1$)Ko7k`>ajzF`&ggQt2MZ&v-iwj!yrJ|8~G!mX)eg^3xk21-l z%MBhS$xdLVxG+?5=#He+CnsID`Q%2}IPfZuygGy_a=v58`=XgwYHY=rmLy|LQ_A#x zFvjgYpJidq{F{W0wt*?BPoA3kWZWIwM0NAFC5T!lKN}QYkX&6@_jA8a3 zk<(Ddi1h~5`d+Acd(V4zOYQx?`m(jZ)e}>@8fBYaFDk!XR4(246#aHpv-CHuv*i|} zHpAmIjFnBFDk{sdQu@skP?}IqCJ0wH)lm3RIJ_JB(%$oKriBL!zFf^aitxGnyBXr)I+~2a4YA)KzN?O#8(lX`I#0xG0cqvPohC=T!foEdWdtvAQU&~*bfJ9eqg`P(#lNLN+(_k zzLHL)YzHsBbG9y%Z5%ugDP2YJ3pt?(Ta0b)^D%DiJq5EcHUWIHZbpUf*iDL3D)2%c z%3gp$;+DfNAvlQf>eMN?!(gaA_^sub@zeq;U0R-_`s?5`)_)l=J;5V%PvG}pn}SOj4)gT_&W!pq1f+Fu}dGy<^j{>DWJTs$ueuoMhxk7C!WT z@B8xu{r>4Qj|D$}{a9Dw++#Bx`@2%c4veb}<#7*&p2F_<`Q`h)AHrD~*cx)|ujLk= za5m5#WPg3*{*eIwC+na7QjBPLr$g9;Z0rE)$)bxU(yw<(ARu| zqHt_RU^OfCYd_-f>hxaT@LwTBbG12HXH=x*Yp(z96BY{s#9wP;3x1bjFet%l1N7%YY=nGZch^~Jyv*`#WK)9(Pjyh=FDY3h-E<`z zW4Yq`LD$cPY&=8igmuP3Kz{{_(%z$A1g=XVxJKw9-!gfD%OhLrlkus$HYYUxX=G$e z{0L^*@P-o2=t zQliVP4wqo29M%0!y>FKoWXq^xM%K4O3)_2scA=Oo)V{%FMb2JH5Z4W)XS}QvYVxE8 z2S6|qhH{Gy&5!V0bSP)o`(9+E!&W6-pCwvZ8r8~A1Wj?O|}S_FIV#i=6MJkft{{vG2s zzZCck#^Nam^UPk(C$wbA`qAgf)AWJwEF0KcYKKPB{4n9SA^~!j5Wx2IP|lD~RhR6a zX;g=aWAzBiM@<|*mNgzML`trhHoD0}h~$(z94Yz-;D37$zmldw+u|57c2;UoO75(5 zCB_dV-%&DfS+ak^B5bT2!Xis7L}vC49Epqu>$+dm_3R9_ud~I0;|@vvDXgx9rjz?D zyZC;M$L`QkG-& zZ95wLot*cCd*lTN#j$*ZCjAizR(sEj7nli}AqWyXKo}{=08K4g*Ha~>#y(KkIT0H{ z8Ns2M!#NJy4d-FHglCE)pGlUzmo!B*wt)6Ptm05$U>~<&h6bK6(R7-iDLaa$ZOYj;D90g8;TLb5Zt^+gTI1k^tsl{t1w_ca}O<4v0meHCsck8_Iep&4rYw>R*LCIp zvbJORbTr!WK^0oV-R_h647x!BQq%os;_n3iDfl}MCoYIif-7>;Zg#fkXK~V^BJXt_ zdZtMHX1M2N`V|nD_MU6br3B(&Erk;UH$ggd=o?;byj6;h82X5g(wCV6`8%pp-P(K3 zM0xR^1@`&$qdSG@;+(t}pDe|sjMpUeC&jb9iI-}ecd^dGK4;jzOo3qNPl%dc$6Fs= z3pDt@3yK2LJF5o_No(On(b}#OL3qk`o&62GpPhht z5iAb&8J{K7e<1Sri2UF*PO`uMgu&Lmcw?M1dxicDS%-xWWqO?`-HFoHH%`yR7PxSB z_eQE~I~j9IX(S)|ZaDhhRdk z#&@eRzj2B0l1(r7xxMN9;%R=#b#7DhjrEO()@Q{ePhxyPw!lW zuLb6Jl8DPl@0-+kPwuC0PTCsKY!ChQ^83zf)6Q$w&a2VRtJl)UoissZKYkP!pO82v zX>9UIuX=%`i;Bp-nRXl|8dW~_kHWz|9Ss+9{BF}9(?Hg zKltIpKlp=aWx8{flRweeU^PzwCZtch8<*?d^T>rI%lM_1CZc zrtkGPe*5Nsz4g0&zkmA=`~UdPyZwLq^Lq#0|I1(XgC7hG{_Vq$hCcq}(?g#fJ~I6G z&;N1spT|bD|JDBIiWV*^Uc6*!$+G46utC|%@`}o ze$9d(%8gF1!;9?ZD-o~kf0^rMd;pH0#GC5GcZM(1v}cy$yUCa1`^6W2nbOFIvcm|^ z-~*5EIPksRMVhu3;d2BVLLtH;q%B6hQxQJ^yaU0Da8!-=LumUkgjW%~2sb0_MtBS1 zX@so^_aWSlU|jte`x9O*GS#Nybi|1#Eb#w+BK0J1ywsNx6<@95S{3s*KbY@k6^B&( zA1dZ=kTCtHDt=bQFR7TnIWiIHDJcku`CF1)%$?~Ck^o=1bm%`W;7KE=M z+>h`W!Y+i@5Z*!f5aAfY#B)&|Ap_wGgk=b;5j+UDAbbtseuT#mb|Jim@D9R<2*(g6 zVxRLYgbai$5SAgVM(`lqg77tj`w<>P*oE*K!aE2bA{;}Qh%Mi<5Hb+1Kv;&rJ!B8U zEeKyjxF6v$gk1=)A-sd|A;K|)iRYs{LI%PW2+I&wBX|&QLHHWN{RodC>_T`A;T?nz z5so2D#3=-S%j7=DkMqcm-?qNgSF|hrn?GA#wE4Nm@XUkP*H3sYEGW1veMb4(fX5$5 z&(4^gk#&hZAdo$4rhI^3i~s*4)52xtB*31Lm7bNEIoFFu6Fu- zi*Iyh`2NS+jGLs{lFE}3lFv*&FZtv#XfgK5<9HgcwJ|O39}_3wZycG35cjXJAc}`t zM#e1P249W4#W!mmf6%hNTGJF?Q89k@){kE@uFt5J!!{<(Zr!X| z@nb+pB7Qh5o|9d|pr+E|lknUrNQOZhg=BCPO+{GBzMvM0PQux7)22;}$M=xM;R%H( z;gZM`WHWqaQu8tbo4yFaxQujGwhw_-WSi2p4Bv(({E#1FzgNa}#@#wkAfv7dzxJC^ zv%!OGGR6<~W-z7I?e*0+d!m2~X9JzFhczk%WCjDbf^Kp<;&kO%&2j% z4XktF7kKe&fUHu$dj3GFk+gQL*WGHMM*P;Yaj(i4%=;2C+z{c%P?zc3FswiMeK_W$pJ(K&#yx3GLZA#YWa8eqjCNP!fsv1L%#cg#pf2WPs7EmJrNeh%7+$C{ zAtLX1Q3Z^A)ES1UB1Qf&+6}0Ds8%3#!iM`8Yb75B`? zDzZZeH%Wd0ipwZNSJKG00QnXiC*O8czCz?HL_VVqR%qPcXUew*`PLv`_9*@MA@Xr7 zm_&be+b#GlB@klN!7V^85i?0>_jl~(U(7jd{^;-x{Q3gfQ@|g z_m;M!&qgN1%*SgF0^28jTaa%H@(~-Ck?$ouFf}9cIXF1#GSc1vHtw0!*8$mAc^DaW z{|WczeCeAt?R3)vl=O$zb$q#9;(Zz&%oxG zHjr0>qMWu)V%ainn#8gN+AfJ@M%pNeWk%W}i7A<&^-K|={;}eQwuM)M;s$Mq#O#H% z6%y0(p!@|QLjLjMhPuxyL9AG%j!P_$VX4m&b7d>_R^n8Fsh<){>q0%0n4_X;)HhxU z!j+PGC9(7zsXr2Pg)7&*5}zV4^+95}vZdTh%$0EDN{G@xwx<}WvLtAVkzuSD8OCM{ z$nsxNBPcT@Y9Ei7rWnY|$0MYGc8ph?ma3hH*x*MTBxyFV!H+ns-gAKs{>Ov=_{LSC z$CRfytnA&0??nG73B*3c52*2Cerx+MC)zwy`*#8x;}wUU2--b!`#%8g_uvmM)@PPu z_UAd7B>ob=iaY)mB5hk7$In!N>8Ap7e2w%AfVn1ZMSj+Q4KVj8=ZW&CYU?fK+bmf8 z1QPP{V*L+W(w_%j(-mp&GvH0YGmxM3Wtfzk<*xu5`s!pA)? z=HF;3&mSt7hyJG^JuZH-_9*Z}xtcZ?I6i*7wjY@LcGN85IH56iG0sPJlF?g>(Jn0^=V{rQ@Z1L8fvKgQ(0N~Qk;c#(;o(;z6VFLQ_C=ajrP5ymwyn^# zEQLP+PAZMmpOl3ClsZkjTcu|JXI5gqRJaOwwJXBk{~$K;|1scv)K7U~|6T6tuekV+M}7u8V_k%wzX9$? z{x*?5S^Enx_q;CxW__n(ig*k1lC5wtrwqs&R7Rw41m<4#B?>L;HsQ-2}YbRR52F->KC!kRD8tnmZG0D$^z=@#Ou)l8uFNQE*uJT_v3ABM8 zgWrX~xgjLtii@{tD}Z_KW2D~%d>{I2$kT(sKLP!Q{d*I5Rc?g8lO}80&%m7ZsE_i0 zDR6g}5kJ&NlV`FTgyLxPgMcsn|r zUIolEBy?NY|E~gn6Z&9`*JHp1WCkvl{}u4j>WKdT8Mxac&n}q$ci_GzP54Q~C#B(o z6Du@rrYb)Zcsj-pA};Ey2Ije%k^Vhko~K=}(q93tyE8IBe+cY^@dXP+`I8|i7h`;; zDLeys4UEMag%<2{iK2zb>fI0aa`ZMixd7f$TdlhhIX(WFeFwa7b^lt-qW4u6; zpyvf(o(<1d_+#LHK3qH%Wno|csOnn?9FO{;!XkYG@B@(d>1+b}|8-yw_+iNF&w*!rP449|eGl+Y zAWt(?`2pa!b0hgvzk;y=|M5uFKO6WEyZK{XO|C$P?;cuJF0Q2QH7;|AoLj_cr)-Gq7>a zNPnjj_`X#U{dfSF=kA7l^a0ZuHu$9j7cSDm^YyH=kstD$quSpNd@Yz|1D=fa!qdRU z#Xi3V?8RWei+iSP>5#PrQvQiA2JQ}_4Wv&)jw^wUi{-CNH<09EGw@xP!lvOd%RgaB zf7OEDwcw8}_>^f{_+rv@;19Ai?W^&!{_8C1*PAfu<7@QBMf`POV}D>WRzce>>5p0P zUJK^Fdm8vtg}T_kgTUj!Uk(c~&xnnS?M*z#KupJ3w*{YX!B+q`qCV~uvAz~ddIn48Jl0N~4Vn5^)ZiPty3UFPsl!s|J$uFcKQ5WmD+Jc)b_;%pM zruKdeyn4Q-twdS2zaRJ~muT9r6rM;yRTt}@XTggtxXOatfZI&+cZVf?n*~2+!M_1M zHdn6SNMGW41~u}1gHuhI^v$r~d6P_ge5Xz-^}S__HPb zs0B|vKSDq0J=5@0b$B_?`|~`WeRCkpB!| zZ-(42Cx6!f4_}DA#dw+jUf{Q34|GK{Eh{GZo%oC7}Uk~7Fck(1=j(e1$)k6IS1|E2JA=JtorvGOa8xE za6EWpTx@T;1z%yoP7AIF-uew$1Nr5#q~8g=2=s9n$)9_G2Qhw}??~?pmi+(4bc~5% zzdo>}$6X}b;$PA`$%1ED@L~&Y1vZcGMoaoPEcg)%?y=zafJ>pzj|h6kYiSpgGI`PT zWtuSQEde(6k0)anybSm<$bc~)`YrjtVZr8uVw@i1Gc~ofI5T#+3RW&JbuBF}Tj_EE z6-J@O1*W8$X4ksL=CxIgE^!#@stUAeINNM#bo<>k8FS}m&C!_Ps;_Ayx>OSNs#VMEcGFXP_KJYpyP??Q zcYABAs@;y#CD|TdaiP=M>X?JW<3_h;$}K-{rD|Ggx38BEig^<2TD3GQ%apgE*@IK( z0BLdB?WnUX(9+Tj{zY0~8G9x<(d@xFuTS&%3YM2R{a(=mfRR#C<*8cd_LlqW8+|G* zTwO8vsGcp9hx|oVY;=Pn6CIx8@u?(9@0aMbVAR}jIaTGCDCqH(`Mvd?b$QNWMSy*_ z$L9hoJoQag{^~mC62Egvbp!ft&r%6d&(ZDDl?xjKZq=z0cT=P@D=V_*ktlybW0lXh zd~Jif+MmAxtoD1+>Ec4vx@<|-rKrQ}uJXH!F$7hfYIl*hxk+`Qy3sVm$iA}9lkHk& zkLl{ArS3*|mCx;HC8e3m%3Q_1@|G627m^Q_qEB+DTx*f3FiT`xHapYfD|ENGJvA84 zg0jF`bQ`>I;XMJj*HhJ4kcGzW$dp$gxkNUM(sA1@^=#o2!VLg2th zz(LTwf~v;G=uR4P6(wwCb{9mMt$ZPEb}Mac2n>hp#xPuPItNTa2mQ_KF`!PTU`$l+ zvWx0HHK?`B)!;0uN2apbt_BB*$pH%_2ke=QTMFu`ygmoAFm*{J`BLgYl^5NH*c8>H zJNPf_Fy)$4Dq2GE`g&FnBk@tA$<~XNCfG5GD>t-2Qfixv>mAwai;D}b6wc1``Rdns zmeykss~Sfk(w^z?)RelbYB1JP5Ml_hXP0}t?sfG()KTVctaUi;q{6DOmkK~iXFZviGvW67KG(NAaF5s_ezQLo2GYMc$ncKhA?OErqa|l_Ca9(t((p&Ej zcPeUZ70Wre^_IcSLM34>IF1rT+7ZqE>%u8mV9`Q*Z2w|=0sA3MN>&!cdZCz??fj%Q))Ifhylx_#Bq z0jjw}>XAL0`i*o&lhW;oHZRG-G!;oV+d!*@Gpo})>WP%0h;~Pt5mY7ZXsk`N+m|+1 zuUCfDAvDdTgjQ|$hqaxCzF1gnGRl5Dol{me&j&3gtI4~H6B={PwQ%Y3{Jf>EVDqhf}yMvgQ&|rq;Pp0p zt(ooimCZ|=Z*Y4HFx8cs87sJEvL^PdeAqaTVKpGjwALb5jI}zE_%E~54o15(Usi0; znwppz-5$&uY&|jvR(lt=2KKB)?y8m&yvx^Af1_Iox4EyQ7P*KikiJ)pQ6DT z?V^%rCdr-axIR$T=o2%!u=NquHszA~PsNpUaLkAc3wj*ui57+W!=Lcf1X?g|#}OO2 zP{%dFMkU2QVMF9J89hhX=fQ8pWbV2F((cNu!L-e}*zBK+&Xdeh?4ngQvSC5Z=u3#_|NtIJ+>4**N}MM-%|f(kDBC1%u-Whs^8t z`6}z+5BXZKW-5)Sm#D{TMhn%vyks`UBFZp@{r2P9p5u0YHuWN0UQL_PX5Zv73AZvr zCfvc6My~c68tA}`&Mt#&ibBLuD-}+hx5|55dt|aU*(<&EO|%2jEgjtokb-CrDtaQ6 zVr*rq(1ZVuJ7S+B)-pmv3hUR^`$hdrvV^)C{)TV@jyou^{`Fj?Nn)`SHC;sYxs<$i z)%coSbyc1ktgWN%OpH%yvNL84QPWJ6$NhyKWX#e_8klJFkTpk{hgz%{UF&?VY9(Uz zHICzZT*~^QH^Lq$^Ak}Ja9D1SsI7`om6MiP`bdS4X47nLnq^`Pad<^#pNmml$^D8# ze7B6-v!dA9>Re*aT2U+>Ic+ix~ds*=%J6 zG*0=$;LY(=c^*1m)L69+%fC^FB*#n`hortnT5mU<6--Je65R;R^kT#UUU)IFVmaEN z{43KX&4e+8;f;w{&&!zQd@UZ#a@A#-()wVKi;Yc|Tsz_UK1x()1)?!Q@`Af4DIsb24@u3PV_t*UQy!J({bL>c8s$q5-aYM!z< zdvffoX3Cxo%b+^8thom3_)(@tQFDyDfHhFmCXQvAHl$hZZj3V9jXFcf4W#Ib&5I|J zV)AJ$mc|Lcq%4agX11bvjV*Om(uzi=82fDLG)3L&WMO52YwU~=mrMitD{>nLPI?`}nn zgK1|h+BkpVQZ#z;+7LU$#$<#+uv8sILC(r%3O2T{A=Od@Ei;edl85J9Ot!QVPzkmD zA>0x3NLbB`vGo#MtcVpho1zw+4f$)~d)2^`6(q(O ze*0W@(>Mm?%!swY@phm`ufpnHo9so5X$IG#wrb%S#(I-$=}nqFLv`>IvtF=@_HiZf z1lyT~S1-#eDK22wB9_LwLo&*gctVE?-HhFzK@)cNDzcj+eG`m+gOkqvsC)jcCP&5L zEX$VOe6*;|o@-sfgcp_3bIq67ABoI4)^lt)arjEnma2Mhe!bs^(;W0g8bG8J;)BPf z{PeJpk*EW3FdrWc$6+bbYnq!-ha+~6H>$)5w~;79#inC$4X&Nt7r3%AGGhsxgFs>ga=CIK!pF8(iG#GUgSr`p4(H zU8`yuovn%|S6u}@Ws~p{oCq~T*Tm<|V>^hGoqBw*J)8i;Q4$buFmUiA&kLGc@KS|^ zUhF1G*VEhyu>s~>>dJ@jJvUp90_zb64ko^d8Y66sVtGtU_N+oThf`G=%@t=gwg7~{ zfckv&c6}Kp8xz+pWM2xRyc=5lblgqT4wxI}ucd^YhkkjD>zZ+}S%KMQ-{?0o@4)#yl*f%cs+IP{3c4vs;tJu^dePbn5Q zUU)oamO+zNDn5i~IfE5E{fkm6DuK^eUFE5jY@&*(Z&EfIo~=pC;KGrr-`l()Y$1#c z&|FKGc@AYZfcYD+t?g)twkL9qHk&CLt`-=Ch~XMV14hwFp^UO?@L-L1f@FuB_=blj zzN=DP5uq2IF2dKNXy}inK|@i*z5(7Y!Dy5B@9R{@L~W}B28 z$%Y+?HkWEzaaFffxmp5Vx2v_@>&J0XZf?O7{1Q>$W!LA0xCP!mbGpiyWP1({dwx#0_qurw7BYvH+}A=>a-#U*;J9VMSQ--L3OQ-}}+*5#Q9!!-EEzMT>=->uQRX=|6&khU+H|~D z5VK<$(Zg&*)}-nC0$H0a>6VfbV@o9IViVGt@2JQM&l|;sMn~-MDd^xpAk~ZyEi@9% zHjtLyG#x{$r6WRvD;*I?TGYtQRqy4*%2d|uLVTG|E9WX&3n$#mmO3wH=~$lOC~`Il zl%s;tm*i5}`N|`#MzqP0bCTM2F)VRdd!o!^&~5X1Vrj>4 z6t8rYh-snN?+&+Zh+@72Jd5%)1Wk}$f7QB`%`r5OqKsH}n&-sg3hSCcn0t7lVmBbd zSlW6i3v&Kd{ Date: Wed, 22 Jul 2009 16:21:33 -0700 Subject: [PATCH 1236/1860] Bug 481347: Split uploads into multiple chunks to get around server upload limit and curtail memory usage --- services/sync/modules/constants.js | 7 ++++-- services/sync/modules/engines.js | 37 ++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index b1401fa4adfc..f3cb556bc516 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -40,7 +40,7 @@ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "STORAGE_VERSION", 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', - 'CONNECTION_TIMEOUT', 'KEEP_DELTAS', + 'CONNECTION_TIMEOUT', 'MAX_UPLOAD_RECORDS', 'WEAVE_STATUS_OK', 'WEAVE_STATUS_FAILED', 'WEAVE_STATUS_PARTIAL', 'SERVER_LOW_QUOTA', 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', @@ -79,7 +79,10 @@ const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; const CONNECTION_TIMEOUT = 30000; -const KEEP_DELTAS = 25; +// How many records to upload in a single POST +// If there are more, multiple POST calls will be made. +// Record size limit is currently 10K, so 100 is a bit over 1MB +const MAX_UPLOAD_RECORDS = 100; // Top-level statuses: const WEAVE_STATUS_OK = "Sync succeeded."; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c598b6e628ec..ec54b2543ff7 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -459,14 +459,28 @@ SyncEngine.prototype = { // don't cache the outgoing items, we won't need them later this._store.cache.enabled = false; + let count = 0; for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); this._log.trace("Outgoing:\n" + out); + // skip getting siblings of already processed and deleted records if (!out.deleted && !(out.id in meta)) this._store.createMetaRecords(out.id, meta); + out.encrypt(ID.get("WeaveCryptoID")); up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient + + if ((++count % MAX_UPLOAD_RECORDS) == 0) { + // partial upload + this._log.info("Uploading " + (count - MAX_UPLOAD_RECORDS) + " - " + + count + " out of " + outnum + " records"); + up.post(); + if (up.data.modified > this.lastSync) + this.lastSync = up.data.modified; + up.clearRecords(); + } + Sync.sleep(0); } @@ -474,22 +488,21 @@ SyncEngine.prototype = { // now add short depth-and-index-only records, except the ones we're // sending as full records - let count = 0; + let metaCount = 0; for each (let obj in meta) { - if (!(obj.id in this._tracker.changedIDs)) { - up.pushData(obj); - count++; - } + if (!(obj.id in this._tracker.changedIDs)) { + up.pushData(obj); + metaCount++; + } } - this._log.info("Uploading " + outnum + " records + " + count + " index/depth records)"); - // do the upload + // final upload + this._log.info("Uploading " + + (count >= MAX_UPLOAD_RECORDS? "last batch of " : "") + + count + " records, and " + metaCount + " index/depth records"); up.post(); - - // save last modified date - let mod = up.data.modified; - if (mod > this.lastSync) - this.lastSync = mod; + if (up.data.modified > this.lastSync) + this.lastSync = up.data.modified; } this._tracker.clearChangedIDs(); }, From c1c348a273939ad68406bdeee3c278f2c7610398 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 22 Jul 2009 16:22:08 -0700 Subject: [PATCH 1237/1860] Crypto binary for Windows --- .../WINNT_x86-msvc/components/WeaveCrypto.dll | Bin 63488 -> 79872 bytes 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 services/crypto/platform/WINNT_x86-msvc/components/WeaveCrypto.dll diff --git a/services/crypto/platform/WINNT_x86-msvc/components/WeaveCrypto.dll b/services/crypto/platform/WINNT_x86-msvc/components/WeaveCrypto.dll old mode 100644 new mode 100755 index 1cec4a8139a95a7d97fcf4e106d4875b41b3556f..d68700e47da44e02a8cc28880ad9a812e7134b0e GIT binary patch literal 79872 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 R;Aaf{jDepq@FOws{{iUBpdA1J delta 26019 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 NOvS$4!)1q~|1V`0Wdr~K From a042352f6cd431f0b8857f6cf66f61f15ff03101 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 22 Jul 2009 16:33:50 -0700 Subject: [PATCH 1238/1860] Crypto binary for Linux_x86-64 --- .../components/WeaveCrypto.so | Bin 67182 -> 73368 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/services/crypto/platform/Linux_x86_64-gcc3/components/WeaveCrypto.so b/services/crypto/platform/Linux_x86_64-gcc3/components/WeaveCrypto.so index 4afd6dd1be09fe950d657f76d61cdbb27195ac72..2e79b67ca8e4d5152072066153acc66995960dc9 100644 GIT binary patch literal 73368 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 Date: Wed, 22 Jul 2009 16:38:34 -0700 Subject: [PATCH 1239/1860] Bug 485573 - Fennec runs out of memory and crashes if too many history items to sync. r=thunder Incrementally process records as the collection finds record boundaries and converts them to records for the engine to use. Get rid of the collection iterator and original RecordParser. Add tests for incremental record parsing and remove old iter tests. --- .../sync/modules/base_records/collection.js | 103 +++++++--- services/sync/modules/engines.js | 21 +- services/sync/modules/resource.js | 63 +----- .../tests/unit/test_collection_inc_get.js | 184 ++++++++++++++++++ services/sync/tests/unit/test_records_wbo.js | 12 -- 5 files changed, 263 insertions(+), 120 deletions(-) create mode 100644 services/sync/tests/unit/test_collection_inc_get.js diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index b8a18276b6ee..8892b1976a6d 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -117,48 +117,89 @@ Collection.prototype = { this._rebuildURL(); }, - get iter() { - if (!this._iter) - this._iter = new CollectionIterator(this); - return this._iter; - }, - pushData: function Coll_pushData(data) { this._data.push(data); }, clearRecords: function Coll_clearRecords() { this._data = []; - } -}; - -// FIXME: iterator should return WBOs / CryptoWrappers / Keys ? -// FIXME: need to reset iterator after a get/put/post -function CollectionIterator(coll) { - this._init(coll); -} -CollectionIterator.prototype = { - _init: function CollIter__init(coll) { - this._coll = coll; - this._idx = 0; }, - get count() { return this._coll.data.length; }, + set recordHandler(onRecord) { + // Save this because onProgress is called with this as the ChannelListener + let coll = this; - next: function CollectionIterator_next() { - if (this._idx >= this.count) - return null; + this._onProgress = function() { + // Save some work by quitting early when there's no records + if (this._data == "[]") + return; - let item = this._coll.data[this._idx++]; - let record = new this._coll._recordObj(); - record.deserialize(JSON.stringify(item)); // FIXME: inefficient - record.baseUri = this._coll.uri; - record.id = record.data.id; + do { + // Strip off the the starting "[" or separating "," or trailing "]" if + // it wasn't stripped off from a previous progress update + let start = this._data[0]; + if (start == "[" || start == "," || start == "]") + this._data = this._data.slice(1); - return record; - }, + // Track various states of # open braces and ignore for strings + let json = ""; + let braces = 1; + let ignore = false; + let escaped = false; + let length = this._data.length; - reset: function CollIter_reset() { - this._idx = 0; + // Skip the first character, the "{", and try to find a json record + for (let i = 1; i < length; i++) { + let char = this._data[i]; + + // Opening a string makes us ignore all characters except close " + if (char == '"') { + if (!ignore) + ignore = true; + // It's a real close " if it's not escaped + else if (!escaped) + ignore = false; + } + + // Track if an end quote might be escaped when processing strings + if (ignore) { + escaped = char == "\\" ? !escaped : false; + + // Don't bother checking other characters when ignoring + continue; + } + + // Increase the brace count on open { + if (char == "{") + braces++; + // Decrement brace count on close } + else if (char == "}" && --braces == 0) { + // Split the json record from the rest of the data + json = this._data.slice(0, i + 1); + this._data = this._data.slice(i + 1); + + // Stop processing for now that we found one record + break; + } + } + + // No valid record json found? + if (json.length == 0) + break; + + // Deserialize a record from json and give it to the callback + let record = new coll._recordObj(); + record.deserialize(json); + record.baseURI = coll.uri; + record.id = record.data.id; + onRecord(record); + + // Keep processing the data until we can't find a json record + } while (true); + + // Aggressively clean up the objects we created above so that the next set + // of records have enough memory to decrypt, reconcile, apply, etc. + Cu.forceGC(); + }; } }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index ec54b2543ff7..b39c56f5c0b1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -272,17 +272,6 @@ SyncEngine.prototype = { return Utils.deepEquals(a.cleartext, b.cleartext); }, - _lowMemCheck: function SyncEngine__lowMemCheck() { - if (Svc.Memory.isLowMemory()) { - this._log.warn("Low memory, forcing GC"); - Cu.forceGC(); - if (Svc.Memory.isLowMemory()) { - this._log.warn("Low memory, aborting sync!"); - throw "Low memory"; - } - } - }, - // Any setup that needs to happen at the beginning of each sync. // Makes sure crypto records and keys are all set-up _syncStartup: function SyncEngine__syncStartup() { @@ -331,14 +320,11 @@ SyncEngine.prototype = { newitems.newer = this.lastSync; newitems.full = true; newitems.sort = "depthindex"; - newitems.get(); - let item; let count = {applied: 0, reconciled: 0}; this._lastSyncTmp = 0; - while ((item = newitems.iter.next())) { - this._lowMemCheck(); + newitems.recordHandler = Utils.bind2(this, function(item) { try { item.decrypt(ID.get("WeaveCryptoID")); if (this._reconcile(item)) { @@ -355,7 +341,10 @@ SyncEngine.prototype = { Utils.exceptionStr(e)); } Sync.sleep(0); - } + }); + + newitems.get(); + if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index fb14cfc98f77..a67bdbad95ed 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -213,9 +213,7 @@ Resource.prototype = { return this._lastChannel; }, - _onProgress: function Res__onProgress(event) { - this._lastProgress = Date.now(); - }, + _onProgress: function Res__onProgress(channel) {}, // ** {{{ Resource.filterUpload }}} ** // @@ -357,69 +355,12 @@ ChannelListener.prototype = { }, onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) { - this._onProgress(); - let siStream = Cc["@mozilla.org/scriptableinputstream;1"]. createInstance(Ci.nsIScriptableInputStream); siStream.init(stream); this._data += siStream.read(count); - } -}; - -// = RecordParser = -// -// This object retrives single WBOs from a stream of incoming -// JSON. This should be useful for performance optimizations -// in cases where memory is low (on Fennec, for example). -// -// XXX: Note that this parser is currently not used because we -// are yet to figure out the best way to integrate it with the -// asynchronous nature of {{{ChannelListener}}}. Ed's work in the -// Sync module will make this easier in the future. -function RecordParser(data) { - this._data = data; -} -RecordParser.prototype = { - // ** {{{ RecordParser.getNextRecord }}} ** - // - // Returns a single WBO from the stream of JSON received - // so far. - getNextRecord: function RecordParser_getNextRecord() { - let start; - let bCount = 0; - let done = false; - - for (let i = 1; i < this._data.length; i++) { - if (this._data[i] == '{') { - if (bCount == 0) - start = i; - bCount++; - } else if (this._data[i] == '}') { - bCount--; - if (bCount == 0) - done = true; - } - - if (done) { - let ret = this._data.substring(start, i + 1); - this._data = this._data.substring(i + 1); - return ret; - } - } - - return false; - }, - - // ** {{{ RecordParser.append }}} ** - // - // Appends data to the current internal buffer - // of received data by the parser. The buffer - // is continously processed as {{{getNextRecord}}} - // is called, so the caller need not keep a copy - // of the data passed to this function. - append: function RecordParser_append(data) { - this._data += data; + this._onProgress(); } }; diff --git a/services/sync/tests/unit/test_collection_inc_get.js b/services/sync/tests/unit/test_collection_inc_get.js new file mode 100644 index 000000000000..cf271bf3459b --- /dev/null +++ b/services/sync/tests/unit/test_collection_inc_get.js @@ -0,0 +1,184 @@ +/** + * Print some debug message to the console. All arguments will be printed, + * separated by spaces. + * + * @param [arg0, arg1, arg2, ...] + * Any number of arguments to print out + * @usage _("Hello World") -> prints "Hello World" + * @usage _(1, 2, 3) -> prints "1 2 3" + */ +let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" ")); + +_("Make sure Collection can correctly incrementally parse GET requests"); +Cu.import("resource://weave/base_records/collection.js"); +Cu.import("resource://weave/base_records/wbo.js"); + +function run_test() { + let coll = new Collection("", WBORecord); + let stream = { _data: "" }; + let called, recCount, sum; + + _("Parse empty string payload as deleted"); + called = false; + stream._data = '[{"payload":""}]'; + coll.recordHandler = function(rec) { + called = true; + _("Got record:", JSON.stringify(rec)); + do_check_true(rec.deleted); + }; + coll._onProgress.call(stream); + do_check_eq(stream._data, ''); + do_check_true(called); + _("\n"); + + + _("Parse record with payload"); + called = false; + stream._data = '[{"payload":"{\\"value\\":123}"}]'; + coll.recordHandler = function(rec) { + called = true; + _("Got record:", JSON.stringify(rec)); + do_check_eq(rec.payload.value, 123); + }; + coll._onProgress.call(stream); + do_check_eq(stream._data, ''); + do_check_true(called); + _("\n"); + + + _("Parse multiple records in one go"); + called = false; + recCount = 0; + sum = 0; + stream._data = '[{"payload":"{\\"value\\":100}"},{"payload":"{\\"value\\":10}"},{"payload":"{\\"value\\":1}"}]'; + coll.recordHandler = function(rec) { + called = true; + _("Got record:", JSON.stringify(rec)); + recCount++; + sum += rec.payload.value; + _("Incremental status: count", recCount, "sum", sum); + switch (recCount) { + case 1: + do_check_eq(rec.payload.value, 100); + do_check_eq(sum, 100); + break; + case 2: + do_check_eq(rec.payload.value, 10); + do_check_eq(sum, 110); + break; + case 3: + do_check_eq(rec.payload.value, 1); + do_check_eq(sum, 111); + break; + default: + do_throw("unexpected number of record counts", recCount); + break; + } + }; + coll._onProgress.call(stream); + do_check_eq(recCount, 3); + do_check_eq(sum, 111); + do_check_eq(stream._data, ''); + do_check_true(called); + _("\n"); + + + _("Handle incremental data incoming"); + called = false; + recCount = 0; + sum = 0; + stream._data = '[{"payl'; + coll.recordHandler = function(rec) { + called = true; + do_throw("shouldn't have gotten a record.."); + }; + coll._onProgress.call(stream); + _("shouldn't have gotten anything yet"); + do_check_eq(recCount, 0); + do_check_eq(sum, 0); + _("leading array bracket should have been trimmed"); + do_check_eq(stream._data, '{"payl'); + do_check_false(called); + _(); + + _("adding more data enough for one record.."); + called = false; + stream._data += 'oad":"{\\"value\\":100}"},'; + coll.recordHandler = function(rec) { + called = true; + _("Got record:", JSON.stringify(rec)); + recCount++; + sum += rec.payload.value; + }; + coll._onProgress.call(stream); + _("should have 1 record with sum 100"); + do_check_eq(recCount, 1); + do_check_eq(sum, 100); + _("all data should have been consumed including trailing comma"); + do_check_eq(stream._data, ''); + do_check_true(called); + _(); + + _("adding more data.."); + called = false; + stream._data += '{"payload":"{\\"value\\":10}"'; + coll.recordHandler = function(rec) { + called = true; + do_throw("shouldn't have gotten a record.."); + }; + coll._onProgress.call(stream); + _("should still have 1 record with sum 100"); + do_check_eq(recCount, 1); + do_check_eq(sum, 100); + _("should almost have a record"); + do_check_eq(stream._data, '{"payload":"{\\"value\\":10}"'); + do_check_false(called); + _(); + + _("add data for two records.."); + called = false; + stream._data += '},{"payload":"{\\"value\\":1}"}'; + coll.recordHandler = function(rec) { + called = true; + _("Got record:", JSON.stringify(rec)); + recCount++; + sum += rec.payload.value; + switch (recCount) { + case 2: + do_check_eq(rec.payload.value, 10); + do_check_eq(sum, 110); + break; + case 3: + do_check_eq(rec.payload.value, 1); + do_check_eq(sum, 111); + break; + default: + do_throw("unexpected number of record counts", recCount); + break; + } + }; + coll._onProgress.call(stream); + _("should have gotten all 3 records with sum 111"); + do_check_eq(recCount, 3); + do_check_eq(sum, 111); + _("should have consumed all data"); + do_check_eq(stream._data, ''); + do_check_true(called); + _(); + + _("add ending array bracket"); + called = false; + stream._data += ']'; + coll.recordHandler = function(rec) { + called = true; + do_throw("shouldn't have gotten a record.."); + }; + coll._onProgress.call(stream); + _("should still have 3 records with sum 111"); + do_check_eq(recCount, 3); + do_check_eq(sum, 111); + _("should have consumed the last array bracket"); + do_check_eq(stream._data, ""); + do_check_false(called); + _("\n"); +} diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index 6dde12a51faa..65d7b846a397 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -71,18 +71,6 @@ function run_test() { do_check_eq(rec2.payload.cheese, "gruyere"); do_check_eq(Records.lastResource.lastChannel.responseStatus, 200); - log.info("Using a collection to get a record"); - - let coll = new Collection("http://localhost:8080/coll", WBORecord); - coll.get(); - do_check_eq(coll.iter.count, 1); - - let rec3 = coll.iter.next(); - do_check_eq(rec3.id, "record2"); - do_check_eq(rec3.modified, 2454725.98284); - do_check_eq(typeof(rec3.payload), "object"); - do_check_eq(rec3.payload.cheese, "gruyere"); - log.info("Done!"); } catch (e) { do_throw(e); } From f9a5269972541ebebcfefc6fb17e7eba6e62221c Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 22 Jul 2009 17:48:47 -0700 Subject: [PATCH 1240/1860] Bug 505906: Don't do a last POST if there is nothing left to send. Regression from bug 481347. r=mardak --- services/sync/modules/engines.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b39c56f5c0b1..24ed25a478bf 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -486,12 +486,14 @@ SyncEngine.prototype = { } // final upload - this._log.info("Uploading " + - (count >= MAX_UPLOAD_RECORDS? "last batch of " : "") + - count + " records, and " + metaCount + " index/depth records"); - up.post(); - if (up.data.modified > this.lastSync) - this.lastSync = up.data.modified; + if ((count % MAX_UPLOAD_RECORDS) + metaCount > 0) { + this._log.info("Uploading " + + (count >= MAX_UPLOAD_RECORDS? "last batch of " : "") + + count + " records, and " + metaCount + " index/depth records"); + up.post(); + if (up.data.modified > this.lastSync) + this.lastSync = up.data.modified; + } } this._tracker.clearChangedIDs(); }, From aebb1a673db5d477141a05f8e37e9c725cbc71e2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 22 Jul 2009 18:49:07 -0700 Subject: [PATCH 1241/1860] Bug 504788 - Handle different weave versions by wiping, upgrading, updating. r=thunder Store the most newest Weave version on the server and update it if necessary on each remoteSetup. Make a concept of a compatible version where this version of Weave can read but might change records with its full sync. --- services/sync/modules/constants.js | 4 ++-- services/sync/modules/service.js | 30 +++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index f3cb556bc516..162cb9ccfceb 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "STORAGE_VERSION", +const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", 'PREFS_BRANCH', 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', @@ -59,7 +59,7 @@ const WEAVE_VERSION = "@weave_version@"; // last client version's server storage this version supports // e.g. if set to the current version, this client will wipe the server // data stored by any older client -const STORAGE_VERSION = "@storage_version@"; +const COMPATIBLE_VERSION = "@compatible_version@"; const PREFS_BRANCH = "extensions.weave."; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b6e98f8336fb..9c8bf2b205df 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -682,11 +682,11 @@ WeaveSvc.prototype = { let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; - this._log.debug("Local storage version is " + STORAGE_VERSION); - this._log.debug("Remote storage version is " + remoteVersion); + this._log.debug(["Weave Version:", WEAVE_VERSION, "Compatible:", + COMPATIBLE_VERSION, "Remote:", remoteVersion].join(" ")); if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || - Svc.Version.compare(STORAGE_VERSION, remoteVersion) > 0) { + Svc.Version.compare(COMPATIBLE_VERSION, remoteVersion) > 0) { // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.lastResource.lastChannel.responseStatus; @@ -701,8 +701,8 @@ WeaveSvc.prototype = { this._log.info("No metadata record, server wipe needed"); if (meta && !meta.payload.syncID) this._log.warn("No sync id, server wipe needed"); - if (Svc.Version.compare(STORAGE_VERSION, remoteVersion) > 0) - this._log.info("Server storage version no longer supported, server wipe needed"); + if (Svc.Version.compare(COMPATIBLE_VERSION, remoteVersion) > 0) + this._log.info("Server data is older than what Weave supports, server wipe needed"); if (!this._keyGenEnabled) { this._log.info("...and key generation is disabled. Not wiping. " + @@ -719,9 +719,9 @@ WeaveSvc.prototype = { "consistency."); else // 200 this._log.info("Server data wiped to ensure consistency after client " + - "upgrade (" + remoteVersion + " -> " + STORAGE_VERSION + ")"); + "upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")"); - } else if (Svc.Version.compare(remoteVersion, STORAGE_VERSION) > 0) { + } else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) { this._setSyncFailure(VERSION_OUT_OF_DATE); this._log.warn("Server data is of a newer Weave version, this client " + "needs to be upgraded. Aborting sync."); @@ -734,7 +734,11 @@ WeaveSvc.prototype = { this._log.info("Reset client because of syncID mismatch."); Clients.syncID = meta.payload.syncID; this._log.info("Reset the client after a server/client sync ID mismatch"); + this._updateRemoteVersion(meta); } + // We didn't wipe the server and we're not out of date, so update remote + else + this._updateRemoteVersion(meta); let needKeys = true; let pubkey = PubKeys.getDefaultKey(); @@ -1001,9 +1005,17 @@ WeaveSvc.prototype = { this._log.debug("Uploading new metadata record"); meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); - this._log.debug("Setting meta payload storage version to " + STORAGE_VERSION); - meta.payload.storageVersion = STORAGE_VERSION; meta.payload.syncID = Clients.syncID; + this._updateRemoteVersion(meta); + }, + + _updateRemoteVersion: function WeaveSvc__updateRemoteVersion(meta) { + // Don't update if the remote version is already newer + if (Svc.Version.compare(meta.payload.storageVersion, WEAVE_VERSION) >= 0) + return; + + this._log.debug("Setting meta payload storage version to " + WEAVE_VERSION); + meta.payload.storageVersion = WEAVE_VERSION; let res = new Resource(meta.uri); res.put(meta.serialize()); }, From 3ac716b7fb736350d3093533379b80f1a32663a2 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 22 Jul 2009 23:48:41 -0400 Subject: [PATCH 1242/1860] bug 497938 - Client should back off server when it is busy, r=edilee --- services/sync/modules/service.js | 159 ++++++++++++++++++++++++------- 1 file changed, 126 insertions(+), 33 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9c8bf2b205df..0b2dc4bb7c53 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -55,6 +55,9 @@ const Cu = Components.utils; // How long we wait between sync checks. const SCHEDULED_SYNC_INTERVAL = 60 * 1000 * 5; // five minutes +// how long we should wait before actually syncing on idle +const IDLE_TIME = 5; // xxxmpc: in seconds, should be preffable + // INITIAL_THRESHOLD represents the value an engine's score has to exceed // in order for us to sync it the first time we start up (and the first time // we do a sync check after having synced the engine or reset the threshold). @@ -317,6 +320,8 @@ WeaveSvc.prototype = { Svc.Observer.addObserver(this, "network:offline-status-changed", true); Svc.Observer.addObserver(this, "private-browsing", true); Svc.Observer.addObserver(this, "quit-application", true); + Svc.Observer.addObserver(this, "weave:service:sync:finish", true); + Svc.Observer.addObserver(this, "weave:service:sync:error", true); FaultTolerance.Service; // initialize FT service if (!this.enabled) @@ -334,7 +339,7 @@ WeaveSvc.prototype = { if (Svc.Prefs.get("autoconnect") && this.username) { try { if (this.login()) - this.sync(true); + this.syncOnIdle(); } catch (e) {} } }, @@ -423,23 +428,35 @@ WeaveSvc.prototype = { case "enabled": case "schedule": // Potentially we'll want to reschedule syncs - this._checkSync(); + this._checkSyncStatus(); break; } break; case "network:offline-status-changed": // Whether online or offline, we'll reschedule syncs this._log.debug("Network offline status change: " + data); - this._checkSync(); + this._checkSyncStatus(); break; case "private-browsing": // Entering or exiting private browsing? Reschedule syncs this._log.debug("Private browsing change: " + data); - this._checkSync(); + this._checkSyncStatus(); break; case "quit-application": this._onQuitApplication(); break; + case "weave:service:sync:error": + this._handleSyncError(); + break; + case "weave:service:sync:finish": + this._scheduleNextSync(); + this._serverErrors = 0; + break; + case "idle": + this._log.debug("idle time hit, trying to sync"); + Svc.Idle.removeIdleObserver(this, IDLE_TIME); + this.sync(false); + break; } }, @@ -620,7 +637,7 @@ WeaveSvc.prototype = { // Try starting the sync timer now that we're logged in this._loggedIn = true; - this._checkSync(); + this._checkSyncStatus(); return true; })))(), @@ -633,7 +650,7 @@ WeaveSvc.prototype = { ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase // Cancel the sync timer now that we're logged out - this._checkSync(); + this._checkSyncStatus(); Svc.Observer.notifyObservers(null, "weave:service:logout:finish", ""); }, @@ -816,8 +833,7 @@ WeaveSvc.prototype = { _syncThresh: {}, /** - * Determine if a sync should run. If so, schedule a repeating sync; - * otherwise, cancel future syncs and return a reason. + * Determine if a sync should run. * * @return Reason for not syncing; not-truthy if sync should run */ @@ -835,31 +851,114 @@ WeaveSvc.prototype = { else if (Svc.Prefs.get("schedule", 0) != 1) reason = kSyncNotScheduled; - // A truthy reason means we shouldn't continue to sync - if (reason) { - // Cancel any future syncs + return reason; + }, + + /** + * Check if we should be syncing and schedule the next sync, if it's not scheduled + */ + _checkSyncStatus: function WeaveSvc__checkSyncStatus() { + // Should we be syncing now, if not, cancel any sync timers and return + if (this._checkSync()) { if (this._syncTimer) { this._syncTimer.cancel(); this._syncTimer = null; } - this._log.config("Weave scheduler disabled: " + reason); - } - // We're good to sync, so schedule a repeating sync if we haven't yet - else if (!this._syncTimer) { - this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let listener = new Utils.EventListener(Utils.bind2(this, - function WeaveSvc__checkSyncCallback(timer) { - if (this.locked) - this._log.debug("Skipping scheduled sync: already locked for sync"); - else - this.sync(false); - })); - this._syncTimer.initWithCallback(listener, SCHEDULED_SYNC_INTERVAL, - Ci.nsITimer.TYPE_REPEATING_SLACK); - this._log.config("Weave scheduler enabled"); + + try { + Svc.Idle.removeIdleObserver(this, IDLE_TIME); + } catch(e) {} // this throws if there isn't an observer, but that's fine + + return; } - return reason; + // otherwise, schedule the sync + this._scheduleNextSync(); + }, + + /** + * Call sync() on an idle timer + * + */ + syncOnIdle: function WeaveSvc_syncOnIdle() { + this._log.debug("Idle timer created for sync, will sync after " + + IDLE_TIME + " seconds of inactivity."); + Svc.Idle.addIdleObserver(this, IDLE_TIME); + }, + + /** + * Set a timer for the next sync + */ + _scheduleNextSync: function WeaveSvc__scheduleNextSync(interval) { + if (!interval) + interval = SCHEDULED_SYNC_INTERVAL; + + // if there's an existing timer, cancel it and restart + if (this._syncTimer) + this._syncTimer.cancel(); + else + this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + let listener = new Utils.EventListener(Utils.bind2(this, + function WeaveSvc__scheduleNextSyncCallback(timer) { + this._syncTimer = null; + this.syncOnIdle(); + })); + this._syncTimer.initWithCallback(listener, interval, + Ci.nsITimer.TYPE_ONE_SHOT); + this._log.debug("Next sync call in: " + this._syncTimer.delay / 1000 + " seconds.") + }, + + _serverErrors: 0, + /** + * Deal with sync errors appropriately + */ + _handleSyncError: function WeaveSvc__handleSyncError() { + let shouldBackoff = false; + + let err = Weave.Service.detailedStatus.sync; + // we'll assume the server is just borked a little for these + switch (err) { + case METARECORD_DOWNLOAD_FAIL: + case KEYS_DOWNLOAD_FAIL: + case KEYS_UPLOAD_FAIL: + shouldBackoff = true; + } + + // specifcally handle 500, 502, 503, 504 errors + // xxxmpc: what else should be in this list? + // this is sort of pseudocode, need a way to get at the + if (!shouldBackoff && + Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]])) { + shouldBackoff = true; + } + + // if this is a client error, do the next sync as normal and return + if (!shouldBackoff) { + this._scheduleNextSync(); + return; + } + + // ok, something failed connecting to the server, rev the counter + this._serverErrors++; + + // do nothing on the first failure, if we fail again we'll back off + if (this._serverErrors < 2) { + this._scheduleNextSync(); + return; + } + + // 30-60 minute backoff interval, increasing each time + const MINIMUM_BACKOFF_INTERVAL = 15 * 60 * 1000; // 15 minutes * >= 2 + const MAXIMUM_BACKOFF_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours + let backoffInterval = this._serverErrors * + (Math.floor(Math.random() * MINIMUM_BACKOFF_INTERVAL) + + MINIMUM_BACKOFF_INTERVAL); + backoffInterval = Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL); + this._scheduleNextSync(backoffInterval); + + let d = new Date(Date.now() + backoffInterval); + this._log.config("Starting backoff, next sync at:" + d.toString()); }, /** @@ -871,12 +970,6 @@ WeaveSvc.prototype = { sync: function WeaveSvc_sync(fullSync) this._catch(this._lock(this._notify("sync", "", function() { - // Skip this incremental sync if the user has been active recently - if (!fullSync && Svc.Idle.idleTime < 30000) { - this._log.debug("Skipped sync because the user was active."); - return; - } - fullSync = true; // not doing thresholds yet // Use thresholds to determine what to sync only if it's not a full sync From 2d9a8a51cbeba1114a7d8854a7a34d20b214fba9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 22 Jul 2009 21:40:18 -0700 Subject: [PATCH 1243/1860] Convert various debug messages to trace. --- services/sync/modules/engines.js | 2 +- services/sync/modules/engines/tabs.js | 4 ++-- services/sync/modules/service.js | 2 +- services/sync/modules/trackers.js | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 24ed25a478bf..b9eb84645a8c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -501,7 +501,7 @@ SyncEngine.prototype = { // Any cleanup necessary. // Save the current snapshot so as to calculate changes at next sync _syncFinish: function SyncEngine__syncFinish() { - this._log.debug("Finishing up sync"); + this._log.trace("Finishing up sync"); this._tracker.resetScore(); }, diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 1e75c271da4e..1cc8ab2cb336 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -230,7 +230,7 @@ TabStore.prototype = { // add tab to record this._log.debug("Wrapping a tab with title " + currentPage.title); - this._log.debug("And timestamp " + lastUsedTimestamp); + this._log.trace("And timestamp " + lastUsedTimestamp); record.addTab(currentPage.title, urlHistory, lastUsedTimestamp); } } @@ -255,7 +255,7 @@ TabStore.prototype = { let lastUsedTimestamp = "0"; this._log.debug("Wrapping a tab with title " + title); - this._log.debug("And timestamp " + lastUsedTimestamp); + this._log.trace("And timestamp " + lastUsedTimestamp); record.addTab(title, urlHistory, lastUsedTimestamp); // TODO add last-visited date for this tab... but how? } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0b2dc4bb7c53..8445e44b6b68 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -453,7 +453,7 @@ WeaveSvc.prototype = { this._serverErrors = 0; break; case "idle": - this._log.debug("idle time hit, trying to sync"); + this._log.trace("Idle time hit, trying to sync"); Svc.Idle.removeIdleObserver(this, IDLE_TIME); this.sync(false); break; diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 16cf578d2761..67ae9cbdfd27 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -146,7 +146,7 @@ Tracker.prototype = { if (this.ignoreAll || (id in this._ignored)) return false; if (!this.changedIDs[id]) { - this._log.debug("Adding changed ID " + id); + this._log.trace("Adding changed ID " + id); this.changedIDs[id] = true; this.saveChangedIDs(); } @@ -161,7 +161,7 @@ Tracker.prototype = { if (this.ignoreAll || (id in this._ignored)) return false; if (this.changedIDs[id]) { - this._log.debug("Removing changed ID " + id); + this._log.trace("Removing changed ID " + id); delete this.changedIDs[id]; this.saveChangedIDs(); } @@ -169,7 +169,7 @@ Tracker.prototype = { }, clearChangedIDs: function T_clearChangedIDs() { - this._log.debug("Clearing changed ID list"); + this._log.trace("Clearing changed ID list"); for (let id in this.changedIDs) { delete this.changedIDs[id]; } From 0bd4c8f2dac29ef2d7461c9acd7298be26cce4ec Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 22 Jul 2009 23:49:15 -0700 Subject: [PATCH 1244/1860] Bug 505940 - Unnecessarily uploading records on first sync Remove short-circuit logic of comparing number of keys for deepEquals and iterate through each key on both objects to make sure both have the same value. --- services/sync/modules/util.js | 23 ++++++++++++++----- .../sync/tests/unit/test_utils_deepEquals.js | 13 +++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index a0d8f3a4a3a7..4317559948f5 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -301,14 +301,25 @@ let Utils = { if (a === b) return true; - // Grab the keys from both sides - let [A, B] = [[i for (i in a)], [i for (i in b)]]; - // Don't bother doing any more if they aren't objects with same # keys - if (typeof a != "object" || typeof b != "object" || A.length != B.length) + // If they weren't equal, they must be objects to be different + if (typeof a != "object" || typeof b != "object") return false; - // Check if both sides have the same keys and same value for the key - return A.every(function(A) B.some(function(B) A == B && eq(a[A], b[B]))); + // But null objects won't have properties to compare + if (a === null || b === null) + return false; + + // Make sure all of a's keys have a matching value in b + for (let k in a) + if (!eq(a[k], b[k])) + return false; + + // Do the same for b's keys but skip those that we already checked + for (let k in b) + if (!(k in a) && !eq(a[k], b[k])) + return false; + + return true; }, deepCopy: function Weave_deepCopy(thing, noSort) { diff --git a/services/sync/tests/unit/test_utils_deepEquals.js b/services/sync/tests/unit/test_utils_deepEquals.js index e3c84986f390..65af9f530621 100644 --- a/services/sync/tests/unit/test_utils_deepEquals.js +++ b/services/sync/tests/unit/test_utils_deepEquals.js @@ -39,4 +39,17 @@ function run_test() { _("Actual matches:", numMatch); do_check_eq(numMatch, expect); }); + + _("Make sure adding undefined properties doesn't affect equalness"); + let a = {}; + let b = { a: undefined }; + do_check_true(Utils.deepEquals(a, b)); + a.b = 5; + do_check_false(Utils.deepEquals(a, b)); + b.b = 5; + do_check_true(Utils.deepEquals(a, b)); + a.c = undefined; + do_check_true(Utils.deepEquals(a, b)); + b.d = undefined; + do_check_true(Utils.deepEquals(a, b)); } From b81f8c18afd7e18ed70d6e250f1c2df613244bb0 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 23 Jul 2009 16:52:28 -0700 Subject: [PATCH 1245/1860] Fix verifyPassphrase returning false on first login --- services/sync/modules/service.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8445e44b6b68..273b34655b5a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -20,6 +20,7 @@ * Contributor(s): * Dan Mills * Myk Melez + * Anant Narayanan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -532,8 +533,14 @@ WeaveSvc.prototype = { this._log.debug("Verifying passphrase"); this.username = username; ID.get("WeaveID").setTempPassword(password); - let pubkey = PubKeys.getDefaultKey(); - let privkey = PrivKeys.get(pubkey.privateKeyUri); + + try { + let pubkey = PubKeys.getDefaultKey(); + let privkey = PrivKeys.get(pubkey.privateKeyUri); + } catch (e) { + // this means no keys are present (or there's a network error) + return true; + } return Svc.Crypto.verifyPassphrase( privkey.payload.keyData, passphrase, From 34eb22dfd61208263c44d891e53d5ce0e65f6e24 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 23 Jul 2009 17:01:58 -0700 Subject: [PATCH 1246/1860] Fix verifyPassphrase for cases when the key actually exits :-/ --- services/sync/modules/service.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 273b34655b5a..756459f0da79 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -537,15 +537,14 @@ WeaveSvc.prototype = { try { let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); + return Svc.Crypto.verifyPassphrase( + privkey.payload.keyData, passphrase, + privkey.payload.salt, privkey.payload.iv + ); } catch (e) { // this means no keys are present (or there's a network error) return true; } - - return Svc.Crypto.verifyPassphrase( - privkey.payload.keyData, passphrase, - privkey.payload.salt, privkey.payload.iv - ); } return true; }))(), From f40e1688a24ed9fdff9ad014632b2ca61c0be1b6 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 23 Jul 2009 17:12:32 -0700 Subject: [PATCH 1247/1860] Remove useless check for crypto methods --- services/sync/modules/service.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 756459f0da79..ebf7efe520ad 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -529,22 +529,20 @@ WeaveSvc.prototype = { verifyPassphrase: function WeaveSvc_verifyPassphrase(username, password, passphrase) this._catch(this._notify("verify-passphrase", "", function() { - if ('verifyPassphrase' in Svc.Crypto) { - this._log.debug("Verifying passphrase"); - this.username = username; - ID.get("WeaveID").setTempPassword(password); + this._log.debug("Verifying passphrase"); + this.username = username; + ID.get("WeaveID").setTempPassword(password); - try { - let pubkey = PubKeys.getDefaultKey(); - let privkey = PrivKeys.get(pubkey.privateKeyUri); - return Svc.Crypto.verifyPassphrase( - privkey.payload.keyData, passphrase, - privkey.payload.salt, privkey.payload.iv - ); - } catch (e) { - // this means no keys are present (or there's a network error) - return true; - } + try { + let pubkey = PubKeys.getDefaultKey(); + let privkey = PrivKeys.get(pubkey.privateKeyUri); + return Svc.Crypto.verifyPassphrase( + privkey.payload.keyData, passphrase, + privkey.payload.salt, privkey.payload.iv + ); + } catch (e) { + // this means no keys are present (or there's a network error) + return true; } return true; }))(), From 89e19d5b854a04f9c56503c9e5095db149f251ec Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 23 Jul 2009 23:05:24 -0700 Subject: [PATCH 1248/1860] Bug 506091 - New binary needed for Maemo devices Rebuild Linux_arm-gcc3 (maemo) WeaveCrypto.so binary. --- .../platform/Linux/components/WeaveCrypto.so | Bin 56986 -> 85574 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/services/crypto/platform/Linux/components/WeaveCrypto.so b/services/crypto/platform/Linux/components/WeaveCrypto.so index 0259efa9c6626898ec69c1abcc980b723f688f0b..7a161e9740fc8543b3bbf8437075c06d9b32f186 100755 GIT binary patch literal 85574 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 literal 56986 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 LT7qN0kP!bL_4K;I From 6bb81ca09504ef7a96db294badc6ed0b11841e1f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 24 Jul 2009 15:28:42 -0700 Subject: [PATCH 1249/1860] Bug 506302 - Shorten stack traces Trim the full path from JS stacks and just show the file and convert nsIException traces to look like js stacks: func()@file:line. Only difference is js stacks show arguments while nsIException always shows just (). Fix up some places where we print the exception instead of the fixed up string. --- services/sync/modules/base_records/wbo.js | 2 +- services/sync/modules/faultTolerance.js | 2 +- services/sync/modules/util.js | 60 +++++++++-------------- 3 files changed, 26 insertions(+), 38 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 254d265b750b..07cb3d90427d 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -154,7 +154,7 @@ RecordManager.prototype = { return this.set(url, record); } catch(ex) { - this._log.debug("Failed to import record: " + ex); + this._log.debug("Failed to import record: " + Utils.exceptionStr(ex)); return null; } }, diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js index 620c38784f1b..9d255565f88b 100644 --- a/services/sync/modules/faultTolerance.js +++ b/services/sync/modules/faultTolerance.js @@ -64,7 +64,7 @@ FTService.prototype = { onException: function FTS_onException(ex) { this._lastException = ex; - this._log.debug(Utils.exceptionStr(ex) + " " + Utils.stackTrace(ex)); + this._log.debug(Utils.exceptionStr(ex)); return true; // continue sync if thrown by a sync engine } }; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 4317559948f5..9774f6de74ec 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -65,8 +65,7 @@ let Utils = { return func.call(thisArg); } catch(ex) { - thisArg._log.debug(["Caught exception:", Utils.exceptionStr(ex), - Utils.stackTrace(ex)].join(" ").replace(/\n/g, " ")); + thisArg._log.debug("Exception: " + Utils.exceptionStr(ex)); } }; }, @@ -349,56 +348,45 @@ let Utils = { formatFrame: function Utils_formatFrame(frame) { let tmp = ""; - if (frame.filename) - tmp = frame.filename.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); - else if (frame.fileName) - tmp = frame.fileName.replace(/^file:\/\/.*\/([^\/]+.js)$/, "module:$1"); + let file = frame.filename || frame.fileName; + if (file) + tmp = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1"); if (frame.lineNumber) tmp += ":" + frame.lineNumber; if (frame.name) - tmp += " :: " + frame.name; + tmp = frame.name + "()@" + tmp; return tmp; }, exceptionStr: function Weave_exceptionStr(e) { let message = e.message ? e.message : e; - let location = ""; - - if (e.location) // Wrapped nsIException. - location = e.location; - else if (e.fileName && e.lineNumber) // Standard JS exception - location = Utils.formatFrame(e); - - if (location) - location = " (" + location + ")"; - return message + location; + return message + " " + Utils.stackTrace(e); }, - stackTraceFromFrame: function Weave_stackTraceFromFrame(frame, formatter) { - if (!formatter) - formatter = function defaultFormatter(frame) { return frame; }; - - let output = ""; - + stackTraceFromFrame: function Weave_stackTraceFromFrame(frame) { + let output = []; while (frame) { - let str = formatter(frame); + let str = Utils.formatFrame(frame); if (str) - output += str + "\n"; + output.push(str); frame = frame.caller; } - - return output; + return output.join(" < "); }, - stackTrace: function Weave_stackTrace(e, formatter) { - if (e.location) // Wrapped nsIException - return "Stack trace:\n" + this.stackTraceFromFrame(e.location, formatter); - else if (e.stack) // Standard JS exception - return "JS Stack trace:\n" + e.stack; - else - return "No traceback available"; + stackTrace: function Weave_stackTrace(e) { + // Wrapped nsIException + if (e.location) + return "Stack trace: " + Utils.stackTraceFromFrame(e.location); + + // Standard JS exception + if (e.stack) + return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < "). + replace(/@(?:chrome|file):.*?([^\/\.]+\.\w+:)/g, "@$1"); + + return "No traceback available"; }, checkStatus: function Weave_checkStatus(code, msg, ranges) { @@ -456,7 +444,7 @@ let Utils = { return Svc.IO.newURI(URIString, null, null); } catch (e) { let log = Log4Moz.repository.getLogger("Service.Util"); - log.debug("Could not create URI: " + e); + log.debug("Could not create URI: " + Utils.exceptionStr(e)); return null; } }, @@ -518,7 +506,7 @@ let Utils = { } catch (ex) { if (that._log) - that._log.debug("Failed to load json: " + ex); + that._log.debug("Failed to load json: " + Utils.exceptionStr(ex)); } }, From a064a7d3ed53546c2c5e0c18ff4f76178c541293 Mon Sep 17 00:00:00 2001 From: Gary Gendel Date: Fri, 24 Jul 2009 21:16:59 -0700 Subject: [PATCH 1250/1860] Bug 501630 - OpenSolaris x86 support Add SunOS for crypto Makefile to build a dynamic shared library. --- services/crypto/Makefile | 311 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 301 insertions(+), 10 deletions(-) mode change 100644 => 100755 services/crypto/Makefile diff --git a/services/crypto/Makefile b/services/crypto/Makefile old mode 100644 new mode 100755 index 6b6f2ec0f99e..d82a44fd743d --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -21,6 +21,7 @@ # # Contributor(s): # Dan Mills (original author) +# Godwin Chan (Darwin Universal Binary) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -36,26 +37,316 @@ # # ***** END LICENSE BLOCK ***** -stage_dir=../dist/stage +# OS detection + +sys := $(shell uname -s) + +ifeq ($(sys), Darwin) + os = Darwin + compiler = gcc3 + cxx = c++ + so = dylib + cppflags += -dynamiclib -DDEBUG +else +ifeq ($(sys), Linux) + os = Linux + compiler = gcc3 + cxx = c++ + so = so + cppflags += -shared +else +ifeq ($(sys), MINGW32_NT-6.0) + os = WINNT + compiler = msvc + cxx = cl + so = dll +else +ifeq ($(sys), MINGW32_NT-5.1) + os = WINNT + compiler = msvc + cxx = cl + so = dll +else +ifeq ($(sys), SunOS) + os = SunOS + compiler = cc + cxx = CC + so = so + cppflags += -G +else + $(error Sorry, your os is unknown/unsupported: $(sys)) +endif +endif +endif +endif +endif + +# Arch detection + +machine := $(shell uname -m) + +ifeq ($(machine), arm) + arch = arm +else +ifeq ($(machine), i386) + arch = x86 +else +ifeq ($(machine), i586) + arch = x86 +else +ifeq ($(machine), i686) + arch = x86 +else +ifeq ($(machine), ppc) + arch = ppc +else +ifeq ($(machine), Power Macintosh) + arch = ppc +else +ifeq ($(machine), x86_64) + arch = x86_64 +else + $(error: Sorry, your architecture is unknown/unsupported: $(machine)) +endif +endif +endif +endif +endif +endif +endif +endif + +# Universal binary so no need for $(arch) for Darwin + +ifeq ($(sys), Darwin) + platform = $(os) +else + platform = $(os)_$(arch)-$(compiler) +endif + +################################################################### +# Target and objects + +ifeq ($(sys), Darwin) + target = WeaveCrypto + target_i386 = WeaveCrypto.i386 + target_ppc = WeaveCrypto.ppc + so_target = $(target:=.$(so)) + so_target_i386 = $(target_i386:=.$(so)) + so_target_ppc = $(target_ppc:=.$(so)) + cpp_objects_i386 = $(cpp_sources:.cpp=.oi386) + cpp_objects_ppc = $(cpp_sources:.cpp=.oppc) +else + target = WeaveCrypto + so_target = $(target:=.$(so)) + cpp_objects = $(cpp_sources:.cpp=.o) +endif + +# source and path configurations +idl = IWeaveCrypto.idl +cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp sdkdir ?= ${MOZSDKDIR} +destdir = .. +platformdir = $(destdir)/platform/$(platform) + +xpidl = $(sdkdir)/bin/xpidl + +# FIXME: we don't actually require this for e.g. clean ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) $(warning It should point to the location of the Gecko SDK) $(warning For example: "make sdkdir=/foo/bar/baz") $(warning Or set the MOZSDKDIR environment variable to point to it) - $(error) + $(error ) endif -all: build +idl_headers = $(idl:.idl=.h) +idl_typelib = $(idl:.idl=.xpt) +cpp_objects = $(cpp_sources:.cpp=.o) +so_target = $(target:=.$(so)) -.PHONY: build crypto rebuild_all +headers = -I$(sdkdir)/include \ + -I$(sdkdir)/include/system_wrappers \ + -I$(sdkdir)/include/nss \ + -I$(sdkdir)/include/xpcom \ + -I$(sdkdir)/include/string \ + -I$(sdkdir)/include/pipnss \ + -I$(sdkdir)/include/nspr \ + -I$(sdkdir)/sdk/include -crypto: - $(MAKE) -C src install +# libraries +libdirs := $(sdkdir)/lib $(sdkdir)/bin +libs := xpcomglue_s xpcom nspr4 \ + crmf smime3 ssl3 nss3 nssutil3 \ + plds4 plc4 -build: - cp -R -v platform $(stage_dir) - cp -R -v components $(stage_dir) +ifeq ($(os), linux) + libs := xpcom_core $(libs) +endif -rebuild_all: crypto build +# compiler and Linker Flags + +ifeq ($(os), Darwin) + libdirs := $(patsubst %,-L%,$(libdirs)) + libs := $(patsubst %,-l%,$(libs)) + cppflags_i386 += -c -pipe -Os -arch i386 \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -fshort-wchar -fpascal-strings -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) \ + -isysroot /Developer/SDKs/MacOSX10.4u.sdk + ldflags_i386 += -pthread -pipe -bundle -arch i386 \ + -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + $(libdirs) $(libs) + cppflags_ppc += -c -pipe -Os -arch ppc \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -fshort-wchar -fpascal-strings -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) \ + -isysroot /Developer/SDKs/MacOSX10.4u.sdk \ + -force_cpusubtype_ALL + ldflags_ppc += -pthread -pipe -bundle -arch ppc \ + -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ + -Wl,-executable_path,$(sdkdir)/bin \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,_NSGetModule \ + -force_cpusubtype_ALL \ + $(libdirs) $(libs) +else +ifeq ($(os), Linux) + libdirs := $(patsubst %,-L%,$(libdirs)) + libs := $(patsubst %,-l%,$(libs)) + cppflags += -pipe -Os \ + -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ + -fno-common -pthread \ + -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ + -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ + -Wno-long-long \ + -include xpcom-config.h $(headers) +ifneq ($(arch), arm) + cppflags += -fshort-wchar +else +endif + ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ + -Wl,-dead_strip \ + -Wl,-exported_symbol \ + -Wl,-z,defs -Wl,-h,WeaveCrypto.so \ + -Wl,-rpath-link,$(sdkdir)/bin \ + $(sdkdir)/lib/libxpcomglue_s.a \ + $(libdirs) $(libs) +else +ifeq ($(os), SunOS) + libdirs := $(patsubst %,-L%,$(libdirs)) + libs := $(patsubst %,-l%,$(libs)) + cppflags += -xO5 -s -ztext $(headers) + ldflags += -DMOZILLA_STRICT_API \ + $(sdkdir)/lib/libxpcomglue_s.a \ + $(libdirs) $(libs) +else +ifeq ($(os), WINNT) + libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) + libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) + cppflags += -c -nologo -O1 -GR- -TP -MT -Zc:wchar_t- -W3 -Gy $(headers) \ + -DNDEBUG -DTRIMMED -D_CRT_SECURE_NO_DEPRECATE=1 \ + -D_CRT_NONSTDC_NO_DEPRECATE=1 -DWINVER=0x500 -D_WIN32_WINNT=0x500 \ + -D_WIN32_IE=0x0500 -DX_DISPLAY_MISSING=1 -DMOZILLA_VERSION=\"1.9pre\" \ + -DMOZILLA_VERSION_U=1.9pre -DHAVE_SNPRINTF=1 -D_WINDOWS=1 -D_WIN32=1 \ + -DWIN32=1 -DXP_WIN=1 -DXP_WIN32=1 -DHW_THREADS=1 -DSTDC_HEADERS=1 \ + -DWIN32_LEAN_AND_MEAN=1 -DNO_X11=1 -DHAVE_MMINTRIN_H=1 \ + -DHAVE_OLEACC_IDL=1 -DHAVE_ATLBASE_H=1 -DHAVE_WPCAPI_H=1 -D_X86_=1 \ + -DD_INO=d_ino + ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ + $(libdirs) $(libs) \ + kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib + rcflags := -r $(headers) +endif +endif +endif +endif + +###################################################################### + +.PHONY: all build install clean subst + +all: build # default target + +build: subst $(so_target) $(idl_typelib) + +install: build + mkdir -p $(destdir)/components + mkdir -p $(platformdir)/components + cp $(idl_typelib) $(destdir)/components + cp $(so_target) $(platformdir)/components + +clean: + rm -f $(so_target) $(so_target_i386) $(so_target_ppc) \ + $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ + $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp WeaveCrypto.rc + +subst: + $(substitute) WeaveCrypto.rc.in > WeaveCrypto.rc + +# rules to build the c headers and .xpt from idl +$(idl_headers): $(idl) + $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) + +$(idl_typelib): $(idl) + $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) + +# build and link rules +ifeq ($(os), Darwin) + $(so_target): $(so_target_i386) $(so_target_ppc) + lipo -create -output $(so_target) -arch ppc $(so_target_ppc) \ + -arch i386 $(so_target_i386) + chmod +x $(so_target) + + #i386 + $(cpp_objects_i386): $(cpp_sources) + $(cxx) -o $@ $(cppflags_i386) $(@:.oi386=.cpp) + + $(so_target_i386): $(idl_headers) $(cpp_objects_i386) + $(cxx) -o $@ $(ldflags_i386) $(cpp_objects_i386) + chmod +x $(so_target_i386) + + #ppc + $(cpp_objects_ppc): $(cpp_sources) + $(cxx) -o $@ $(cppflags_ppc) $(@:.oppc=.cpp) + + $(so_target_ppc): $(idl_headers) $(cpp_objects_ppc) + $(cxx) -o $@ $(ldflags_ppc) $(cpp_objects_ppc) + chmod +x $(so_target_ppc) +else +ifeq ($(os), Linux) + $(so_target): $(idl_headers) + $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) + chmod +x $@ +else +ifeq ($(os), SunOS) + $(so_target): $(idl_headers) + $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) + chmod +x $@ +else +ifeq ($(os), WINNT) + $(target:=.res): $(target:=.rc) + rc -Fo$@ $(rcflags) $(target:=.rc) + + $(cpp_objects): $(cpp_sources) + $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) + + $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) + link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) + chmod +x $@ +endif +endif +endif +endif From bdf2ceb3f5d90b542d97520fc51b2a10e91042ff Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sun, 26 Jul 2009 02:55:38 -0700 Subject: [PATCH 1251/1860] Add i86pc machine detection for crypto Makefile. Add SunOS for test Makefiles. --- services/crypto/Makefile | 3 +++ services/sync/tests/unit/Makefile | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/services/crypto/Makefile b/services/crypto/Makefile index d82a44fd743d..cdefc6e79657 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -97,6 +97,9 @@ else ifeq ($(machine), i686) arch = x86 else +ifeq ($(machine), i86pc) + arch = x86 +else ifeq ($(machine), ppc) arch = ppc else diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile index 17c643402f07..4c06ad5db5c7 100644 --- a/services/sync/tests/unit/Makefile +++ b/services/sync/tests/unit/Makefile @@ -15,12 +15,16 @@ ifeq ($(sys), MINGW32_NT-6.0) else ifeq ($(sys), MINGW32_NT-5.1) os = WINNT +else +ifeq ($(sys), SunOS) + os = SunOS else $(error Sorry, your os is unknown/unsupported: $(sys)) endif endif endif endif +endif topsrcdir ?= ${TOPSRCDIR} native_topsrcdir ?= ${NATIVE_TOPSRCDIR} From 33c2929786b37e736e709ee6ce1da092dc568c4c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sun, 26 Jul 2009 02:55:38 -0700 Subject: [PATCH 1252/1860] Generate SunOS x86 binaries and put them under platform/SunOS because the contrib Firefox builds are SunOS_x86-sunc while we build SunOS_x86-cc. --- .../platform/SunOS/components/WeaveCrypto.so | Bin 0 -> 80256 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 services/crypto/platform/SunOS/components/WeaveCrypto.so diff --git a/services/crypto/platform/SunOS/components/WeaveCrypto.so b/services/crypto/platform/SunOS/components/WeaveCrypto.so new file mode 100755 index 0000000000000000000000000000000000000000..2bceb136b5e42c9c603bc187b0f4c29c4221d6fc GIT binary patch literal 80256 zcmeFa3wTx4wKlwfs8OQ{8kJVmc*lYoE{clU1lT}?z~+)ftuX}HkU&Ucvcts#6%48D zF4N{zth7g4TD4LS+MEhCZ4sm9G-#=fme#aVjg}r)+^ME*)M%;s-*?Qpu6u`zU;p!b z&-4FVXXYAf-o_laIp$b%E$#}BJ!h({XZRh?J{)L*1GYzg8Wu}{Ux2>rt@N*2eaF8qXcOo zQYq46q$NlZq@_swm8r~Fj(jCjm4@#?UW*h(szX|hv=)iKJCXiPt_))x^7Tkx*6>|A z--!HfeO-@ylfG6`WEd%cl?>XlrYj&jSzn&w-ZC;g*MnVtP8Xb=q(YYG-i5fn2@qJ%k;f+ov%P%p|2}-Uaj*Qo!i+eeX+*AF-X24$^C{T_jqfF=YERu z*MWV*Aj5AO#va@sI`m}YWC&07PQ##0AUp|hPK#mu0}TlmQ@9>9j3;z?3~>02?nWdqemufLq@; z49W-L?*MN4hSE{OzaqTb?e7D?8Q(OFzv}wqPCz-7lKf`<+W|NKUD+_g6@WKvG7Q=+ z!e0R#?lcVACc;m!{(nM;HGZD}&iJmi zUnPwBtkQ7WNobGpY4ceBV#3e3@-YW+&MyoDQemh9X4L?%r z6U%=}_+7)G4Ium>U<3TN>3u;O==nSJugmiQhhwgM-of$@4P&b=Hvu<6{&FuGTA-pOyi9?s4%c2fXc9hEc4?YXZEm9sbKOHC{8|CJ_6Fy8O3*yC4tK zHT)OA*?)8O>A2w-AIdOCmtPFn06%T{m=C!6_l5xy$b6!JJ3RQj&GO$G#&2}_$)^Ed z=+A8$z6@|S=<}q8BY+nIxv5%TcLL7%o?(1Lm#3VL_L!fI@1=lypEZn^ba^4*F5q{e zhVNqim)-i?0Czx`rs(qL0dG5G7!PPT4mi8b)&I;))c=-YfP^x?S%9-%fFGdww*qhm z8vk6Ee;=^X2mE#U3xIop{{&tBCg6_0x%B)5a5Kttboq!gfbZ8_`JD>5_dZv?)&g$& zvtj&5)8~1>2I!Hi+rPv1+YDouhOa&o_&y5x(EP6foDF*YSeI`FT>W?DD^i}G0vz6K z7%*8f-+ut6a&FXc)>)Wuukx!{|2n|l^(G9s1IA~zuHOK7+h1J$`wrmd2i^6qgY_}k zUAq405f~rxf2oG&0`}UYD!^U)-Sz2PEdQrr{BelF_ZNT{V)8fX_7>}7K1(!wIvlSI z=y$D#Cjj=!UnSt?zbYS#^V;DMw1`q%C0?xpAHvM~8{(ZxET-QJK^XMP2 zEpJx>?s&kpH?skI<>wy2-u3ps0atHv$Nva$_;>F5b`BiK4WL(}9&a(==6{HbApDB~ z-Uj)%_4kK>7j9L)H0SpV*6(!f?|Xo|J~oW!G(FE8iT3*p<4d~#9KanO`?n2n7S@+< z>hgA$H@WudL%_y=7zWrN_?&wV@P)E|PRnl&;OY<6-iG+x3b^U(uKa%m@Ivsz#{WUU zz5i|)_+R>amE~``>tXu2n9on#`d0wXehdDGp3lvIH~h)P_b$L4_q+bne*y0Gz;6NW z!uq;ek2mBz$a}M4tkCcrz~1#~1z_)d?g6}QyQ^=H1NPdpLx4NLU%UTnve5A+;Hdjw z33%Z{YClB!JP0@=ZWveS`5YpQ`P%Yz-ud7M=)F?6UjVq{Vd#Ud{|&$y5Ek42`xW5U zR>OEl*Z%;p*S?>B0q6nwiR<=L0XL!lh_2rNxb;Vd@qvb40G#zLSN{Kp^>@1T#q#3( z=r2S4ogZP41HJ+V!*3{d;}emu@xZgBMEzm?+X8S6;O4va`i9-vlY#P|2jJHLH-X<1 zkdwYcF?84iU#U=$Rvd3i0KOS;^?$kT>j7tb;2#5S27ER92R_dO?)AX0vObKlUH&%U zY7hK3w)en8hoZd)J`Hg8Q*QeU09Sk9ae$jW@MO04!1LH1{ISPh1lay@zBPcmx>fvx z^!Wzh=I7k?vKesrdBgZM=ZErN18()y?**I#_{+NdbHh+>x$rc=IUcwMu>Irs8v<}6 z;EWGldN&8kp9#SK9f0k=?fml>g8wH1N&JQn8*XF+wtuJLA_wqoSUm2-^=aT=6!6|V zus(t>gkK2Me;sh-8=LIm*)oH(H@xNPXm1Y2IxCb1EDFA;d z051i+W-0vXVQM}P03Nf@FyOMw{QfIYza;=a8-QO8z#j$R;g=XjXSrd_OHuvL0&M@X zaIpmN_PbSlhxA?VDd+sR06wtLE#DC+-x+}S2H;l$@S6eny#V|n;NB&~RpEO!9U=YW zy{iFtMh#=WmcJE%*DNxOw>12{K>cRG>#AV?wfr!6G-egz7P|h`fbCxvEVj{4!kV zAIHB8u$MnMfHTp*O`m0f`fCGlV*vg^0RDLZehKiHO64yQ|CBG-*r+kj48WHM;7NdQ zt5DbKYyPzZez4XsuG9RJ0HB9n!$%v&z3X73 zwSMITwtu8q3E*uduqn7Ee(M2OK>ls`JAm6t4FgM*l)oBi{~q8hERr_AjvoVe<$NHA z-*Dq><7B|s!`|$X_G!j&!1j;hj|#w72H>dy_!htifNvOWiSNCE^7{c_19|wOrpGS= zg6^zil1i$;E@6Nx&WLHc$7!JYdz&z==x3%%=wyt!{FB}ToeD_0&elhdvBor z@mHXt{;~bJ0r-jloEv~|4ZzU={N(`L_&((+Cq8=v@E-#3djWXJ zm45lk0&M@--`D^=1MnJ0o<3*H@swwweuW2S`;CCdEOgh8Zw1O955PYLoQ?I7b3GsH z;~xU$?*`yQ0XQQYzWG|j{k4A025kR`k5LpUT@{&9v$i@~bwzEJvA8B$enqV@Ws1s6 zN+N4YmPBeI%gSq`k(!dyC~lNTjgpeZ)fIKMB}>aI%blCvt`=8Ud%MoBT2faLxq^2~ zO2#fO@>HEtRaqOYsaqVas=0bvb>*ze+PwL7)zwus(b|%l6_p{tQ%kC=SC!=0{czA~ZKa5>%~^M9KCVXiMHy)U94j)h6;DoMuo5OKTz+uVzJd zHAG->^>|8vmVsFds%BNKj?_#kt&Lngt;RPUyF1h=sIESI9gpJq$UXQ({gUbuAhe*S ze5FjiyxgAroGGOh6_~HKJ(T3dL7T>jhZj`Mj;w~%T%(AI8Lg;vyDljKjf6t(z3HJl z>PjnWUHZ)M(dWo=kSGN<*2&pG4os^a>tzBIQAuOTBCuf&M6a$UGNrV-ba8of?bZ1Z zE6puWkNJ~f(khE;%A*iV)N!W<^?|T1sjC)+FR7lPrVM*XiK!^9t(~)Id1P^P^4ckR zQ^zfpnfsRqlG3G+M}%lV0=Un`a0^NoRYa!Nl&^|Z&RsG!596+?UQwCPYbgprO4oW8 zp=?{SAk?T-7pYmBR|&JRv~+PKDMHoIz35U*3rlE?Fc2hDOPG+%Yiv9;nl^7*dF2vJ zFbU1Wjy|b1HD4_ek(&9Dilq?0%A^Zeb>$4G^R)7cND2D9+A|Noa?Tay)T?S<<_CpyrR$a;-`#B9)5y+8{tXl>saQUehb8 z7L`^g*ik4LBXHa?lR-1$P!dL@Jkg5y1$fa^CBd_+q6M{FvFf5#Vr(bZEnOO^p|EOs z_H%I#M6-NZWy#W-s+A>olwX~{qB4nqxN>CcnJcO@LzPP$m2e~E?F`!B7)-cv>coiBNnezT3DTJPuWG* zqgUEA`%RBq69QMcgi8WMd4@y0pajnes*+eHpWjEtE~2RiNg|cCupRJO^#VbgI@alY zCh*Uz6|;Z!w8b{^9sx~erX6?0A+F!Zdz#<0po%IFXOrsbS~xA*+IlT^t(vnyI-&@C zu|jA`3KHX{DW#PKl@(Q`O9t2Fa$E0?Bp`F=!M9iy2~|W^!qchEje2FoWz)Q=k)@?| z61&yi_z#BsHceRLz=3es^iDN0%zEw8Ga1$$nH4J}N7BNTp}nU6LJ%ab_f(`P{V zuv1s-Dv9oAI=FMJM8DO$zLDv=1I7DW0=##6PZR-<#0K0zxB|)K|E8pJ?UHr2DxXyj zC#qRpMC2@(Y+HfBc9^t-i5ga+6t~vBB!tx$sy~7gTw%@SGwBLbk6)c}}u%=C^ zKxBoKTLl+gyA_{B?rp~+!B}Gw9;bfL)y}a=H7mw@mXKg~<6!wPnY_WBRZLd7iV})~ z?^-==B9PNl^DvCgm}|l5F9-KtMb6e!ZNhB%6NlXv7{9`X^C}nDh=$LKR4$8_(V0v> zw6i8SK18CevYj}4#qf_cn_f~)bbdvZz2-aak*hk6J7RmrekU$r%%k+u+yQJsBUc=E zC!t%#w?Q`;nnaFR=EJ&G=OgI2viwd+r{f7Fh=V7%7j`u}x1zeN)T7ji&Ad@4@dyH9 z>Usozmu69JKrQp8I{G;1+BPeUXi3Dg<@ZQo$>N$yO#84KfD&8QJne!Wh%3LeaELPa zH+<0WY6*|}Z==PpG%!)TC)aZ}5el}9~{gTQ;t zr_U=Xi&P-yBeoJK>RRe%qG9wPk}I1yTa|-L2{u}lc4z_yQO73S=3XZ@LCELXLXqp4=y$dGT?X06N@F}d>DhG zX?{cxD{1K3ChMSX=-_lrx~UEBX%nV1cRu6S{t(#_jiq0bZ<=3LzBD?uG+N3Hxw|Rx zXvR$##q|`l@0x&%*Xnp0J8L`EUPiqR7qp#Uh8X1vPtevTsHeZ6IAK?dCwb1U)~{EK zrUrf4@+Co|bGX$q(dY!?`2J@XHBL|#LCS=!&S3(@3B;T=t7`EI{R|J=Xj>m#tr{%$ z<7X~C8k>emJqQrbacVsttx{|8EIpAcKCAN2Y8s-98p}aN`^c&!=E=ii&(_#8;bcNM zpTb`gsjc;HlWKfVE3`rfp3w}(H+VT}m7b71c3mE_l4+lj%vpx|y|RrOW2zy!p>w=WEoiAA%8C1`l_=9kq)msG9B;yzfFaoV(g z_I=M$Y$pemvctI!gEmS~yP38-qaFqZ+@d47z_XOwCK2!nl}D2Mk`fx+>J?jw#*HJoU_I~Qc;9IT>RYiCz25f7Q78}Pbq5lR}1q|rTcY|5RG zy#a%3B|Clp2Jc9u8f0lX>c_kVAkqwR#iv!(xB;y^P|6o`w5ROfohTg~MIn$=i4=pHJkUM!+sKf}*Dw#}o zX~NXVV(?L%#aW)yEcxobs*_-3Yn(OH+h$HtO=)!)k0B9yo*BV|Q#{0*ADS{Vbc=fv zn?x>*UWAOhwBzqlrKg%MoD^D`y$m2fe*E}qSz@JETcL3N0^KoiA$Uy(UNv=E~rJsO&|UU zQiU;hFEwXjxnEgTDJR4rnOqYWSLX$|IdPV#@v?|I9)>5Or7I&PXjYDgNiutn;JVud zU;%|{kU;Q_fyIJ^8tNCezAv=q!llDlFK2ItpB<-7;BU0~q6 z1q<@f=M*eQ9`o&3Q+c#rwbs#^L?L#)GH(ZSFc64VU0PlMgT#^YeHb5b8&yqk9k9J>J}x;N^TPsZ z?VZD1u8a`al@S~+^CGJQ z%X7f^VJXvA+np9DBODD%ZK7L}7` z5*$rvI|I``>~g7HAmw`vw&ppNFqf*eNtZ>esN1F>XSw0MMC8FrkvSV_yBSD4T`teEYXAo_*#)g+vIG~?&XUE;3{~}MbYqpRqvN7w$Ux~F89tGdmz zq&leVy9`Da+w~n%1uR!&iCS02E)ICp)GF;l(t!pw!=+1H#R^&oJACAfg9bu-$R-c- z2yOE7JZsi0L{Ap1t;VU9rBxHAl$V57x&dR+KNQtgWt@z!wW<&j;HlT#?P2)fJ279xm5ZFRoh2a$MK0EMFNJZv(Xz z_GM*lt>>~1hx~1@x?)MK^mJuab@WPfab?ZwiPzbHx|m-uy9h{BvG}?x>uPJRT)ene z0nQy<*e^x=|M~xCDZnqb732LFzH5`kbkWVJd<(iIBV7qd}$4AMlTTaft9{cz+1$WKSkZvyaD%{KubkNh;G5lA^m z*CO@d{y&h;#Wg;YXLRA3@3W6WN<~UTx)f<4(if24LHXs#`L5|%NdE-9k?N3gkeZP4kmey(BK;c@-!V@^x(;av(o&>vB7FmC1`<9?A>Y}{MSd63 zGNgNu9!28&gwG&7g!CPxGm#pQN|81p-HXI;0sI&Vudm6s@A%I4<4Da&$06ahS>rLJ z8l-7Rcnwy*2{;n@RHWrdk02Ex;T1pm23NS#P0BHfSleWZy<-$NRX^fjcfA{8U8MY;fKI?}iy_-+PrehcUfq->-=0=^LW zZAkCpdMxrs@cjYKf5S;CzgOo?I^U-AM|8ej=R0)Xtn(I~@78&%&f9dpSLge5-l6mT zI`7o^0iAd0{E*HKd{>irrszCX=V?06(0QiL$LKs;=Tmh)L+7(}9@hB+o!_kUB|6`r z^UXTnqVugfe^BRRPEq(}>pZOU1v6D;{W|Z|dB4t$ zb2VLbo~H8*osZD@NS$A#^HDm_*7*dTU$65Voln*I44u!?d06KQbbhnWZ`1igoiEXO zna(S8Uaj+}&e!OCgU&bUe6!B))p?W7x9EJU&L7nIHl07B^X)p{q4S+O->vgDogdVB zkIs8_-ly|^ogdPyQZ6Bj!-vsm#a1rZN9F*jVNe=n2fTU@Mu! zD9ytA!9%b}gqg$O%wP_~xD9%G{Hd@Bguej0$^2^QGIJR0h0I|@mN17gC}R#oQo(#O zY$tOV@hEc`p=#zZNb8uxC~siC3F{ei7^Th3PsUmbIUBM8^B@d^6otGW4@37L@Hqj- zZ98)q?;Xsmu--A>Xc#-0-+=ecnZx++W_}aCzrwr$>l1Sr$-T^BME5a=0qkH71Gk^~ zP}n}^m*YKr=0)%un8T2m`0&6kzo8kX3zYTn4jzx7R^M#-@ zb1YUC^W~s3b1XEy%+COwnP-5`%(0-4fP9>g33@UABIw0D3VJc03wklH27Q=gVZ4U< zHJ}gksh|(@Qp}fm9_GvZM$D6WF6PO66z0kNY|M}OC72)cdd!dcMexa(p9T44ei!`I zr@%a8;9;&I{0%$|AyHrT?d}@D?_ns5QO4r#4uL@=qYN`Mx?2U#BFswN%>rW(ql`DZ zw+RfSj57Y|ZW0(uW|Tq0jP4Br^Rf&xHM*+>hOilBIfNGq%%ZXxgu?<~PdH3CN8lX7 zHxtel7{oHl781@9m`%&d2xka9i*PkzLtu7QwuW%;r?~O(0>T>zcL~g{%Qh445E#N> zlr<4<6?h@xt%REeUP5>q;cWtQ=(6pEn*^>P+)Q|bz(l32g>bdN5DKHLmGDA=iCWoS z!eN2e5$+(IBk%^oorJRm-bA>IaF)PCz04wL%u zz*`A(E7IK|@PmXi2)7EnjWD+~-OU0&LO6@?Hi0?yGHz$On*`oLm~L1127#Li^V4VD z)dKG%oI`k_z%7Jl5Dp8xn{b$Lj=-&i`N^;DY=PSdFC?5LFiBTdMmR&@eT1tC8v>J* zWoroc9^(A>6W&0$OJI_=Y%}2wfe#RFBHSu)7vZghn*}~dcpKqu0+aM*+X*)b+(WpT z@CJeV2)7Wf7MQFkYbCr;;1pbz?Ij!*IF)b*;T(avLoDkgoGtK0gu4i53A})?ML0v? zn+f+4HUy^ETh>pw_aB`9Lc#`?nC>otmk>@R+#zro;S9p90&gZff^f6I_Y%$`yiH(| zs%#YDCV{sQ&L+G;;H`wOAzUr+BZPAZFBEt?;TeR(0`DLkCY&QMNm+I?;cS8T5nf0* zOJI_!tc-Amz$8^!HDN>G1BBNQ?)`-GC+W&I5bhF~q$}G@xI^F`!cBx*1@0rfm2k7b zhX`*YyiH&Omu1@tHvtB7onL|3W9FxtxhXlZ+>{qzXaYRHz9)qOV|;1j3kQEV#8Blk zP~Om5cSd_Ig~6!rNt65#w89^(mRySl6fF zZ@b)QM+0>k19fSQ_1xiy2kKI-cdtV4ZJD~i!lL?~kv2Ak=rI=Qj1?a+%?z|PL#eUU z@h`?^q_pRxQYYJU(zp|8tZ(oCbRZV$YOFgL%Rh+u3_OMIF;-Wh^%7PjAdLwWTJK$` zuG>bI+>QawNT*qR040r~gJJ6xRcNjMcwit%mwk{34-U@!LBS@eOT5R}cz*}AW08Ha z;{9e_M#G2EqQ==nTmJL$#~=UbtykWC?(Lzmf({Hi&$hnclp^b5ReOAEe09Qqe)CHPQw#<1D*T zecKp4=Y3}Jers$0zcbtXfL`%Z7DB25zVuGDqtE z%3rrb>aO$F{gidhQ0BGQ)KtZ;kw#lpBT~X7O(8D<6U!J!#JtL9_}ji%?re{B~Vhrb~~@F;M!FF3r)UH~C6;=+X;x=@`Jk zxE&fvQpxC>RUPtpmX=Gw!eCuN2o)R2<4RT2`kxQ%661!T ztG2NaY(iStdJ63*ioMXuY1Db@8|6>%SBkjiJWz3(IP4d(5EUkD?d)@=pT6mr ziWfrS*Hr=R2inP<-`jT-c=a+7VUL7Ktv{(+WmEwe9#E_?zhiEp(&;Sg$!H);?T1e7IhuN!#R& zJr!BlFH!EWxSjB@5|4%*Zw?G-B4w%WVhzo>NBwEo=@t&N3%5%lNO%W^g7G(d2X?W6 zRVr{stf2`I4aF;1bQ?ZQ-}EB#M(H#>u#1IS(z;XwQYC#eS9nt@#Tp(#ofd{z8yj12 z8Ea5~cjJ0)W5Zr#U}^>!AGRL!_Ay!pVzn@;y)V^3?o?})x7KkSslFGRyjWMj=A zXR1L$9cF!(6m_aDVxa?O1G~hM>Pp*H(qT&2y3Ovn9JL!3K8mp42b02L2{O9&6a5>wO3HDA~J6Va1EahE};ITVVrsv8weuX_(c}x~9Oq zpXIUp*`hJ+OtXO%M?Vh&x8Wi@H+Da7=yp%40hOGaQe&8KGbYXAX+j`a3epLPEFYp9 zHmHV9Nt&96wO-#pM>RZCW$BW+bgq9!C>|XS25Ay4Pz}eoUMob7!c@s(jU6+#)$ zpn==P89*OLSbKVfVCbgMdS0$kN{O|0suIV@ZET}9`SoiZ?%4YE*mq%~9^HVZv4_}! zh_hk@wU?gC9rj?bG$RYG8XJV(k%wS=q1C8v!il^@kSbR2_z%YqbbkdhVagDu3}JOZ z;ze!AD``Gk_t~FPO9!dYI!E2uL;62U4eQPc_VQJ`7g9xfNmY$J%lNoOVgq9h_o9vr zsm(~C^*Wnl`DkR3Z$1BKX|PeVJ;DzHjbvfpI6wqfDa4nk8Xm;&fh_x`|H^yB1(V#8 zFp#!mh1LzK8=@?xWiBid5w)hO0{8^yOCNpg4DS7h@e6!|RM><3Mpf8D7Xe0Y^CzHV zqbM3wqXoUeL}k(Te4;9Hp-tbspVZd2rv>#ebS$(xw!YQ2Ixt9VECz|HsMagJ#yu-t z!Rq{v-Dmx$19fK#QN|Cn3pYR&#Z+ZHAVV*z%8FUE7H0zq>jm5u%e#rTh1y{>hFZdA z=%QG|W(*u&o>Ew3Jt1vjX*S^&+rEPC70Cv6@!r5LRtC-OK?t&96WJ3QVP>Oi7;BjP z!bUM}pi8D|WmT!(VPG#t&d0k_J&IvQAXhXi6w5 zjwunjIrJAL?!VaBM)gnb^5mOncJ78VwG(VTFO*Pyx zIE|^+TX97iIz1=CTU1uRVPLJgFhd70B1w$>mg+(}?6gu96d!>qf{;`14MjB6gS}MO zNouvI5|1Zp({p3bESMDAMi2WoZS;1k+e163|6q+$UF)~+3hqJ&=VphxY1VhZK`_nz zbQ8qQh&Ap*bF;ov?kF?tv^u_6n?2SMTiJ{Y7*>0%AF6I+4QvRX3xC_?FU?=LaaSj9 z&;ut;pU^0X%XF$3lUC)e#OmOuuq`A5<^owS$)D6{?=Bbwsy*h9!2 z-9pItYEuYp3Zbn_c-N)&J*tutzIygkrYV{?9@8jtx)~)USfyAOF`*3W61ju8*X62FlDOA} zsszlH-TFl~2urE@12M7CPD9hlrcBC|Nm<|iGxb2^Np4UN7;6}+da&*clw0Mvv9XPW z(u`p2Gf2C~5N7t$PRS=FK_jdM%3R(iA&Tp>lL{J zqIRlANr?KgDp5pDvwm%ZAgbzbFrtP~GZ|5zP(4@|2gXbWeI-qwRaJa<3$35( zyGdG^2u|72BiGK}mhI}~oNo*_@l@x#BBkG{QJ%3VCATIYuVnLh4 zlF{E*m6fGLz=55J#m34j9rCYhk0m5F(c2$lr?8S62C>M}hpgmgeYMAuu1RW1ts6iK z<%RPwA&6$yr4Fo0wSN1y%%Hv>ONjWR?F~DC7x-s%9}+I60>}61N6eLw)AAI=6$Ydg zymi*4ulIAsa7-B!4_j55Wetz$@-3#esm7rc%)n^|WmdV22*0t5Q~Im#1P8=H7~fB^N+2r2Y# zpVwBQLeX*AHEhBwr|<;qRE0;L2EIN7VH%DOQh=|Uab^U;3cyOrgp@HE5j#fQ<{~Xi z`UUp%xUc)J#FQ2eB4otT7BW|A;Q-32FAjF9^=g+#Xy-_O;X#B<39WJ$i8=%yAw`LF zjZ~Bnecz0;B}VKS5olSV8XI{{Jps2;t%cqZ`H#h!^*J0dgwPjxEuk+FXglcjDiQZb3a#w0nxkwFJHjts8Y^aW_7X66<@|B` zj%!>0elK;Q=~-JBHi<550gRnH{vzPUvJCpG+i^uz5c_!68;TOLD+vvwAZ3f_Qjfq& z)KxYKyL`D;Mw=r+7HuE7^i5AM0}$QDh6hmtK~((ZsN6B3LnZC0*7yGCsn367%dF2D zL;=}@Qa})2{N#IahjLsYoj{K7vwN#YAXmj1$A8$~uz_P!j>ljC@tnn!7Fo5rzAUVK z#%$dID#WA#MfrbRQ6>d#Q5H)q(oEXmq7Xq{5@KK%hqBV%?=TsZGhV4 zFOgA1gYV2)SeId3%Arty7OKOZ;lA)mu3SR{E0JoIAMn&4iTY-J^B{(X{rLA6Hr*yj zC)9ttH`47S*}`?V14p5iXkL*uUDZKg3H@SC2^GmWayk zev@j~iobd@Dk^JlF{uDyYw;hP>85XfR-_&$y^-G8{^j?cQS)R}gQNPE9Jcd4ZE2TQ5VrSJ^O!uCqH||w;-{Te$0Ve`Uz`9cv zN}wZ4OAFpJkUg1a|8V9#*pNlKV#R#?OEYjWkjzu5t|xe z&Xl0vz$^7_Y33AeSAkF0K!-d_G^b=}AOr59JW&3Hi{b&GDCl=tzxkb@Z@rFOKmYc` z0!9H-Fdwd3WBmx8uCcy223(P&HKe`#Fu(eqj9786Z1`{8`20TxAPxn6Egv;Nu{wwS z0n;;cjQV~fI!axdGYqpJ)eL2X8TBwH8(}jym6#(Q+aEs(c(jN5hat9MhWcZnLni(& z?v-;fDB^S2^F7pZLc;i;*Yjl;crpMg zV73{YX{uS=uV*Cvjf6D#`?Fe}-el7+jVNV;SY{Z3MMbnU)lCRA>rumic)Td+Q7G#+ z{kl!RXXt|tF>QKg#frQ9;~#99sK;*%p)Huz2xeKQeoBE!!`Ws$dIVM{fK?O&p^;@H z(kF;mf=Hi6q)#K#=Ru?kh{*htjsJALn!i0h=WTuZ&;PaYCkDn>^FM_O@V_(vyOYfS zy|n+e@h1kxm-+uMj{o$=B=djye{ueLe5H3@DKsZ5bM-IO0$Mip8`K^=wn5ROJ;cMe zG$}ABN3HY>dMQ&1?Pq)YC*y@P@5TQlm7ZJS>4a9Ph_xoF378I zU8`2(+^n2q(MZY^)^)u4% zN!{hK%U|;$O(?(o-SEm`=r>h!eu=_7LH_VyU6w;xh8+nUc^C@vEGak<|Dr8_KUFuZ z$gdLVw`ZM?ez&J8`d!DWpOJo*y31pyAJ4KU(cgbU8RT{{{k7?Lk;42a>6djF`hE4o z0R7gg8`j1966yElfAi7r+!GZ2KKbQmr{5L2%VVcswlXw9`}Yi#VNm+n_V3{FN3Xw! zwSONd>Ur&-x@G0oqzCBTZ z?SlvL$yoSuv6JV!8?x}h^Y3@Ok(01Mw&%$DE>41MAJmQ}E!mF7_B=fPM>2daJu*JZ zUSJ{qSL2h3{bj7K$GcK%I__cbF@n_1S`2YU+AJrw9KVk@`Sf9g= z4|mYn_76J02L|bX^pX2Nif#Up>3h~7F8&s-!acGSv+H+9-npxq0W@| z#pt?$3r!*6PqOwO)hhgT`?mMo_V(U`xzpa>&?wF$JfdnHRrxh|i@|Rk_O-0m<8%L( z!}wGBrtRobfwH7zm<9V$G-%KNh6CAia^i5d z!Vikir8YkQ+CgQ!WcK^%YCV6(4#A7Cb$^@od$@9VOr(vv<BN2(DZ7&3FI?De|lVcub@QC9B}YV}UW1DQ`+thmol|NfSD zDjj}`K#riKo=<_KMtL@slf@my0(l4~2KsYo!H|><;ym_&XwpCJttPA#*uiAbc+l~e z1;!sm_MHQ>W2=JWHQFqL;Zw-jk@`{GrGfd%L=&6CjQ7z>|9r=Az8KHn3LG=;P;Y}Kec!cNCI z*6T%G8nBS}VzV)>Szx=+=!p+4tZAxk{AA#56VZ#e@)d<2$?$!x!cQN0MB>30BeW?M z^5FZtx^ry!^05PY`=)ApH25Zy-%EY?F3*644_kP1gZo3X10)awkw823JV(|!M}}=) zGI!|19vBN0lg`zn*DUBY^ZS5%EY~n6r{Mi@L|$XLsaQv2x!?w(d_FA@f;;-5m_l9l zGR31Q3JXttSt!8foC&eXgEsQuE0{va&Q(Ycy2fBi$F^?v%DTyQ8d54)aoy~7JqDqq zN4{teSd+-79R_)u-lJlXgLveFH$=DT>L!X z$3A<)R-M}VYiRV(WJqNX`{jYAtbp+`O=iUR@4Ue}d& zY5FGHAH23K5kE2wi)@REeaT+K(NF*U4z)a>+v_%)JZ%+74IqG%DEvz``gwQ-|rK=vFVIkJCMwA4{^k``HT0GrO*{-FCwW4egjgx_fI0tuv2#6RYdo&Ewu2#N@kI!j=Ikt~Z#W?QLn~x9X0zlKdjZUZHLI zcB!pA!uaK?wsq!@c@|VgEz+YVw!Pio_Kj2(pZ~*uLGco6XM1X6GQFyH{4uS2qFbiu zmMOYr@6VE*XL)C#ZN_MoD0yTdE^*LE45KnueJ}_ptQ!`;5cGj&SmY8cN&V}Z)?zU} z2ZU)?Y|9hhZp*x=@Fj}0pdT8eW6E}PxJQBIfg6q?@d%FZC-!(MJJ<2Ldn!S42uRDqtF1ubF%aCc{8Xs4+7b}<3Xj1RxK=)(RJ}0&ix1SB7)j6t__2QFUiJH8; zZ@J)hSMN~u?cmTv<87tV?@^Z22 z);UTBc~t`7?0^b-MF3_f0Z{6bzUfaQ-B@NiWtqWx-zm%dep$3FIgoM?i%lb!pS_JC zEIb|Up?ylKI?RH7^oZ?cW-pc*>^oM<{ISfSavP45P;bPhqHMTQe`OJ4U)_?wM)@9*D9p3kX_r2Xa6z`Sb#S6`d@4!#X4Y#>W22Q6a9IT)2pgj1ip60Lm z4R{MMN!Hg`mGZE|tlMs`f8`abZluloZ>cPYs6TWI$Kj9GiDkB51x1M7pw6L~n>U-e~Z z7-t0v{4=(CD9WYD(L4;}{dpAf&3O$EE~hg_un7&%0JAXe8ITfg6L4-U5M!DoJW zUO?LeaP}hW)+dApbRh8MhB|x<3$h@qpG4!p?_9X0)3E8DP7)(1aZtXu0`Yf)60Xl) zDmGD!1)_fHpepP|57b^it?7XZ+fQryVN~N+2%UYWWLt)7OsSI?vf+nonKz_bGtPKUsXf{btRy5#;;9u=OGI2p%uKP+^2o#HhOkDH1dxIuIHv2Q7hAe;9HB z)&W;FwjR~tm!HgsT8ixP*mc;tNFB-xvdfJ(2iu?JZ+}o7N;~@YuNA5JV)S|o9#E=j zj-q{sybQeMx+H_*|3G5Q`iY@7KTeQxQ2buQzy6}$58>?T%Yp(I z*gYOK1qSVZweDZdUQ_t_$BsmS!RPPkZ#nEcu#KwTKI8lmY0$^wFn5jL1rC1PiQM0s zzNa<)C#^P7$?!q(yGQq@B$i7g;-H$t$8sNYkSFG!*F7#%Ju0`f z8^(xn9&T6QgiH!qwM&7%#4bB*{#=BT^r!hyMQ)`(-8!iQae`COt^=Ivi2nv$(S@Ye zSyCUiqo7OCrAz3-0py}5zJ>L>Mu!gm6hSlNTOWNK7P+1wM8|`Cnd+WLgmZ#uXZH?N z42CFAq<;dBO*gnUTRD*p+vsol1D|iq@l~xHr>fqXr0P@ts(->iut3-J$|O}c`>VF- zswu1*2#{+2UYrTvpaeYH0Wm*_a! zT)IH6$)*1ErxBj$z!`~CN#`E$s3-ng;gMY%WoNPs9zP@&sw#W5`1g5_5+BVLDLmL7 z>LR+hDI!S`vxC(Tny~eShsmQ6;vw?EclxI1gXq_OEYc~_@xFSC8tqJ_Z&Ikg<`$U> zc1Zi}saz=6oJyab%5m{@!GAyIvOm6GUF+)hTy*F5T)yYbWt_}qAGYbEIS+(z@V8CW z1w*uj5vW1hhwp)YU(cok-x2nC&w730^DahzLaAa1K>k!3f+FNfLDC$2uM`~px7VM9 zy#+$y1M-};AF7A(#mgVrgf4^^*j(U17O0dK|DKMA%c2{6z$NlD24Cj(%Ts5|NAN~$ zrO*a=!uK=_WEYCEs!KJ$ z3~kW1_7tEglR8()6mPEL83xWzKOgF{ml9`FBKT*=iaY%H?{Aryh<_CNgdGXcrKU3q zfjN}eH^Bv(W6lAncgh@kMBF+>E@|`G#yba2P|rb3d2kNA8Jq)xIckg3LD3NWlRp2P znLz)Y-wn|JN6Jz;^pBpXSsjPytND5HrDAOftjS5`A=&$!@7$Dt&qX#qUV5h6H#I$d zIB`|<(=ZuM-WNQDKFR#KB8`vF#?6*kWf~?;;|6_loP#mAN2S67ZH-c5i=m@u=-cUg zI&|ep9Z|*vAae#D&o`mmwry%Pewg>{ZWG9v;9v;;$^454@)SLN_)ZHOxdTO~ zdhjnBD@zISzg*wm9D3KhuL7Eqz9;l9T(0z=qzv60!e?^|;zv0z^RV-B91xjL()~Yn zKgu8UtT}t+RRd}q^xoEIDtY&Ppf9yhSqAHVyCOmay`y=U)tc0?3U*GJgfh?^7PGH0 zB}B%I?!fVRF`&w4KZr#H3}1a6pEO{lLZa^?N9N_BE7*K+bg18U6QF_dGdSOa!rO!K zXL(MIc==)vFuh7Ylbt7eZMvGTT5qEzV!M@m?N#eH_#Yc~6uHKa%aACi?J8-$a6YaJ(o9j(8MI9H=InL!UVE z^ob)+|Im1E#!_!fTfbBKF{JGukiUq(}bL0^TDFIQZECpO^C26$XuvI*|GqGpGE~%yf(n^(4q& zhtQ=XAb&U|4XUZ1@OM;_^U+12B>JYl^=z+jTDPv1P$F(px9FOu+a%zbC=8=Sc-<+Y zdYl)g4(B_laLRY0+oH6N+c33^)BW=39%jgBAjUzx(L8Ufhx3y{U5+2py_wW;X#=r<2x!^_ zg~6qbk_eF6Qjg@|0gyJfT+UyI^rp?|zCtIJV<0D%M_V-44YaxmK zjn5|EU0(LUYI4cUMot$r{pnB|Tz|-IPcq)w8?YCBc9ZM2$ZN9o`mxDL@cYhRL^~l! z%A?1HXY|tn21ZZEEl6AKue1n@z4k7;7=L3AAH2sNl`|s`@ytkUR|Wq*Q-;4jIl7;x zOIp%3YVmQPN&OnJCH?VST(8sDSK)etzP=3CoAmWLNZmRD;DcWA& zpAmgIjt`dE!Lk1E@=S?x&;2GRjEzp0et7#4Zp))pxV8`CpM$|Zn2kE{eee54Fy*1^ zZaIC?(inO~gbK$l_R4d(w)Rjfx4vqmR>+oT8}fm4Fan*1xzauow*D@6@M%wU)68L- z!&dq?^f1lxXc!1=ZVxqqEHKUYqH9^Dx1#6GVQaiN3bxXWb*yLqO%bkehs-1_>;qffBasCiUnTfS$d1`a- zrD`Xsc%f5~6tSMVKy~|FbgRj8aEp*4{ycRfaiq7uXyYZstT|6QKoBBH-Jdvhg~-29 zb-N>=ThhM2;?x)9zIm=fZfNp3-|W;Ds%}=jTl>Eb3W>m*BJidNyk{}NyhJ}2#3C)R z;+?U&=GgijUfRbBcDHan~Oe3&AN!80eRbf6R$Em6NqRv?`Zwr$ zGcmEhsW!ekTx)N{1M5ff?=!tK$yN)_K2uTPZRqE*QD9K|-lO~X#ryA8{U3$`gU{d7 zpN{uWu(AD2^S@5b--qAZqQBg)g8hTX`wtth{+jMs?2R3-zQxp9sN>Zdwq1KsrnwU4 z#mh1H&l|74R-O}#;O-7PFgsp-sp?U5eSbHc=RmyrcE~OkUo1Ix_%MsU&>kmV4R>g0 z98*=X>enEDZr!>}shh)C_2$Wt`!4Krx^NF!Py8XN%Q&@?nyucgxwjKzw?EaE0Ef%@uEade2eBr)53q2Opg1^ds2k z&{}1S#2qmM+V+KQ+Er*>#inN@M5gD5V_C=Wx_9=^6J@$}o#SOj_(mviQbFSKIb315&Fk{%O(+^h8B4 z5r~lX$>PUP>-Lm#hD@yX*gJ;>Qyc@t&~s;l zCY_g_-?eXQdirqc1un$v@Hi#JkCVkK&(iq#6!}y6G*6QKa`#=(d2+o6qa$AQTe~7- zDx(wPl?$3=>k8)*8jB~x8FHjkVwBKW+f((&DB+_b#L(}?CZXsD<1fM%c=x)_i!r#* z-=O+CalB1$LHHVxUA*b2wu%|yd5%)U@WYjcX>gQUb?x?_EZ*?iIMu(8etSN!IjYRO z#6P*0jN_FO&;$`U^-M?Z&69iB;)6V-lRCl1K<-#n6+*-CBX@&ISbrIiAdxcBpITpE zLO)&_CGbC4{Oqh`_C0*+SqCmSKJjmf7>`g3a%W2j}OFkqyrOv3~xGTKy~epPh{Vb{N0+Gw>hufzBfg z)n9Ye{0Dt{66lk_|77xeRx z>)Z4kuEl)z>T5Y;Uf4a@0+T2Ab6^w{0pvgh-a+0TZ<)VvJdeLwF++zy`u-1sRob^_ox4);2jAr z;=v4_#NhkPPUs_z_KJ{-eL&$3b#DSyAqe2rLEWRIxN1SjA5ZnP5`S3 zk&fXXN0d1z>=})4Ca^)KK0=@O_RrybXJnbf+>_Nd?xBAR%z<{n{vCNfFMq+Nj5{Cn zC}EN0^e^Zi#wQOR`okFk@uvzIlH(q6Mvhqx1TXzO=Y0 zaOtlmP%nBoB7Qmb$xr)7FSDs6`0rBhMOo)3#Xnm>)l#Ip zAQN?b>k%lUh@JO5FGNr|miP+?4(>&7t;7)YVGwh;%!mO2_-*!st>3Ad>Gd0A(dm)1 zOu7&znUBbq#zMctW3u1zM9DO_lkrK$xYRB7j?t2Z&e@GTEX31FOCASX?A0vjA;qPwV@$>R<^hd1PjZsmG&fbwtvF17JU8Ru*m2eqn=IyicsE&@-7o zbi$=JqaoD;KCj+-mtBmIZMqz+!I!f78IRc^#m1dVJr^-?Owt^{Sm8_n%l_;-WV2_kG^ht)_vWpdceE7{yj^E6|@zd1$Kf^Cs zyr=ylg&)kD>~~>{c&IVG{wtzEsY+hnbSU(ub8f^wp|m;lhEw{6Q~EB?FoD1CO8j+{ z3#~_}P@RHQ-{|Rw9Kb&m|5wgIg4Xc4d|qLF3amEyA2+N-elEnseL9HOCC?#CR<-W@BhK{_Wh4 zMMH6IzcIfB4{;Si;4bXIi@Y`l&3GU=EY!4|5xtDCwL6364-tFQX%o^`)y1{)kyymrc@JHFtX`zVH5$qH*d8$pwl=2RFT~G(q%oyq z*xp$Fy?9#80Z-wE0;c30JehOeX?_RqG{3Vs^h>T&nBVeR&JT+m=C=tB?k_zKZZ6Sm z;DdZq{Z*v)Jfj|s6%hKSu=@v|mt(?O7P_~HK!DnPh_hZmz1#=6Ap<00Oa;do^%I-A zr+jL^b4+yksT@4_rO0>%q-D?fN;EXyr^;aX_latNU1IGfe{DWB<%*fc+7MEaMN?$a z6j`*_!^407NOuYWgR!Bo^n|U;&?UOYHDplvCkK6qALpPV_0Q0#Y`g=g6nj28B=+gB zzCofTW}V*|em{N?j(5fT$o1q=jpaI>HLl3pL*^8K}o zA&{ioi!gY2-Nmf4%=K7zo#EZ@m3n)L72w~0(H&8RxyZ@j=PG4^+QX6bl^hhiF+$## zbbHs|LrsYlW68&R@P<4MPE*%>AY6RNiwqhHJ#u1^efR`wtgbD#J`jlLXsJBcktf@p z^y1@Ajy=K4G-6Nsah%Dyi4$salVL@uaCC`SQMg!{>Lu#%@-(YX85QLd)Bl58gR?k2 zV!GwC6;PaxN=<$huXAA>3x?I~M~`xm8Na_NUO>O!=c%?lagNJPbf?}2!uNFiweHci zR3L+A3Y5NDLohr(n`+jznd=eH(DWJ^JB<9?=ot}9%(J%SCzqd7{IzfRr;?x8%EQTz z@K>=?_;=X-%9A7i1^gQ${KFYeFv$L*NC9vZ&9H=Nnj!IDH7Wb%-^X06v7i`R_*NDI zxYU#HVY&BgH#B|uJPb0cRsj3K<7Lna^*C63U$`Ui<;A;D@5g9|m+OM^j=8w~6jiX@ z5|=}c?Fzk;o%g@zY@1BpZxB+oy7?*(`hoN-t-M(&xevX%(^x~kw5^x6_+3IXHIZJd zfA&PC>ad}{g^66EHxYf}tIB)&(wpb|ZsTNL>fz&4a;>W&0*O@~Q+)tU5L)6-23C_! z`hJ0*zSv7q`|$ijAGMEn3k^qyQthpbkAxWA{Rur)fAZ1{dzuK|dETSpX>sT&k0?a3 z0*Zqu-}>e2G4v2+*OeB3Mw$sS2}c>6BiAxsvhyqLpK}!*YM1HdkzyM*fn3wB_4@D8=Ny+6#4&Lv#??A%%I%X8R>VAy^zGpR2qfpR?<3NIf z5-xlCIuPyf&p-eEmeqRv^gZ}if_>OLRc{OMUg@O0NtrZgf0z3D)7zkC%crS}(sh~Y;?BG5Z;!tNe_BaINc??;FC<> zUxSOR&RYu+?>h+g*$U7&ISVnN71WqC@es)z^^^V%`|nkK+36UTsz;xFgj23Q^nq{} z92Pk=+zD?1Yl1vy;Ijp5!xpWh`^AO9St@yY@T=web_WhD$)*&6f9`ND21?xXML!P> z#-zDvf${jgI+WRc4~m=gz56jcQUASiRM5SX z_bjCE_3Dmw8BW3m|Lv-~Gi{3_Ro}+&T%te4E?m?i;oq*J&JD_+iB4}YkYQ^EcJz+@ z;+!*i8#0fCImgNsvt)JfT1e?XnSFfibRV{5(gBP+t)W9$M~ASl=ei?3`yRnb|I}~4 z{X5jM+UcK2T2sF!_$T+bwS5p_P-DP^c;qcv|7Yp`@j-o&T=~9g;A1y7eY5d?Xsj*} ziSBIKf0`lz5Y)ZkJl6Bk+X&|%-jVX0bskB5A-;G+S;REXIZ>Eu7V^bn+aA2{+5@eB z2Ozj;SvMSw&=QW1b(UfvA~$9o1H<|!^t)}4k8`!OjP;->C|!(o`7Uf9CUvZ43;8U{ZCw?2Bu%x@m{QY^pO>@e{# z4sGy-q@0vVF98lUpWPwv*RYT7E))V!KYh|0_{a_8rT>V}pAaAkTaQB>L^|8lH@lqo z#=0L8=Cs+4Rel?A--xd@ViLJ2B4;N*&o4{ow~0lq?;2uuJool_EMz5B2!0f_Vf$rI zc!rBt4pFy>qWuWG{zJD2)`#EU(!7*9x`O!IJ@ST{pVJq+oF0FIhtsyLecfwoUx%%I zUEXBaODW;|+a8iU0JRsA^uoeW7Wi(hP*vVqvhjlXYn4wawX!XVX-=jigl8VRgs5rw z=7Q>2LQ(h<9_F@NYFWRL8c2iFQlX&S(oKjyFgd-*r<3R^hvrGeqvb!KXFJ1T(^eV zGGp=a2)a5hK1%DK{z_J={Oi4a$uy{Vw^`?X@~jNZ;P(F%o0)jFoLP55cQa<7bR9ZX zkj6)xImYPcm4`)66l9LZJ=h8%`LI{yfV`rlF*=%#j$KGeV!MnmuXM=CM4S~5o4kIe zcMmD}jY;yog6R%^j9-C>KZc~kfKV;)YLjzi%=Tr^_`!rcJuGuE|pNQ#_=c(KjR?*DFp zQP>}O=)tNZ^Mr%{Ns3QZ6Mrn6&DoS_*d1ut`9t+*WAg;Euc83|^^3-v{g>-G^P6JN zoB8JT9P%dLo@8DqPP=#4==QBJ5x$DAtn{NHKou2jc{#JcthVihx-R%V^=UgfVf;Q_ z8u)am>%#o{LhZ_8aKSb~6`fjEe1dz=jaapKyG_ZFRwjI+Zm9)44Z%Z5%(h z94n`#q`rkZ%)V*uU(k;2uc&BeHJ8CU@`nK{@NLd)I=)h`12W6L3}!nDdYPS~S39eH zBppavqaRi^tjf6d*t`hGmi4C#)1F$gW7V7Zoe#sf$msFNqE4d|Z%i2Rde8d?bFLT* zD>ipL8~=8jZ}z`-e$u}8Gk{7n+<9J~W3tOx=!)|c=lk4YhH(N{Q}lldX&`y`qE5)$0V=I<4E*m6^?Ng8?m|WlB}E@XSmxlb6~zV zcUUC+t~R|5V=&T;BmD}_e9&oQ<>cy{RF^tnc(%g($E*UiOoqvMfz z`n}Kkz1pX3zwq5-;~hhWj-<`XN{%m!hd7CP`li9S#KXzDdSr?eEkDcCLeY{3BQLf>5 zH7sq7(Ezk*;nN@Nnp@KIlU?a+Kgs%HNIZ~foEv7TvF_;wIj%;1q|=zSipD z03}1&Pzh8Ht%9ndZO|U*FjNbjgMy|RMh_?qih&ZKWGEXdfy$v(P&Kp-+5;ViYN2yb z&>bibg+Vb;0+bA8LnTl-v4&c>g(6ioABcTN@6j7QE&WJYRa$FdR@CUT;{9v4Sce2BTKO zX9a}x9Q=b)&=x2Rih&ZKe8fuJiLyHkqc<)?`$C~me<&IG*-!~o4y}T&0UrQW5-F-RsLJ+?{=a8M7{7_;mT0<;N^HLzY_}NrwZndE*?i95qziRi z$a9Sak$wcmZ8242hlF7>`RxE0woO3GXqye(MT`ZE5%~ zRPZ`PSd`xj^8_rg-c&HQ`9*FRv1DQEG&V`%m1%66uq=(G3md1g84?fwwd*&h_+e$R z-?GIIAI1IV62Bk(#4u)oaSYzo*xlmyhQ{1robS&EFqS2^tPp;jvz(`vmD;d!8}{_d4MIt zetR5@7w_l&1YcGxWlw30ew?QVHMSazWA(bm)_`$)aBf+)Ss2I0vVAt}Q(}6wAvl_?C7N*CWEliKM6f7{_ZQ`z;vb zMQiOFS==~Yf-pVaG+}zYSzv+jt}AIAFGiRiZ;UWKo(n9nzrUK@I9`}AJ>D>3db~+s zf$`>+t9Wx|47lqIU!>SQ!g!eR2y3UY^}@E`!iSiCODa{_f5<+_Ypk9RDz;l=eT7-) zQ2Ip+o2dCs6~-U%qR}r!*kFw<0At_2rxT2RtHrO107h0V%<3;$hsCOWR^P~S9#ZT8 zUeh?o?-sVrB#NyPW{nm7)`77t=8aFJWS=fo@xH}t8po`pqt$o4E( zWfz-7v3G<$rm+(Aqz;|MYwVEt@i9A%ey4Ta>L6Pn4F5rA8rgbb zyA(3ZZWXpxV@HK;*Vt{FRlWLl8z<}=@#FTe*hNw=nN{{{&2OTVZLRt36{c^?-NN*3 zxn!qm%NmKtxwuH!QX95TV=KgOk;a}Bwn}4FyHp+V!g%TMx-f1B8d?9>l;0GSD3&U$ zqsD54>G4k6_)XcZ;!V`bE)>RdCXMx;5N4f2x$mD8mO&>NS*d$0heKa;tn6+;)>!>h2-rD^tUN5ce6NePrAdLN8D{Q64-V>&e>E~daht}sjmbL5> zbl#H1m~K~ZM#M#o_%0>#^i5le=Y47XtBoQ7|EI*%e0a~VfhD#w zM&TaO_MYmVkd-$FEb+{S1BGwZjCjSdY}5D=^Iu8^+c3}auupU3U{+I!Yuh$zM@oAT z_nz9S*C+9MxEvTOagudhta$Jed~W3gf$`oN@3HY-TNMvT4;x15qxhWsdoaA_y*S>J zd3LTP_QOsrkl{tVu)$NMH?UD(_G>StVQ zM?JUstN9;xk6WxF)$b2AAvr5zPah1aEHHwfJa)ZxSoZHPgK8Fg?@l@pvT~eJn);6W z8>3b0Hf=Au_>y+*gD&mRu~X;ZkS<*>>vnnfUtH1S%Byh0*$_m@|P_WM=;Yr_6* zz^}sxMnn#Z8hq`L>!OFo3>$v^4L8O*(=(>KX3WgY%DyWnH_x4ax5w)%D4bPPTr%yB zJ5y5An$Dj+=bpL0DZO`I+5G$d{kQioD6d%f!0#4SKDcb>8^!HVd zJ^sX#Yu5hZsXtafz3!Pmt>3V5)3eV#zj@2n7q-3l(#x;Bx_!saU9ata{f#%@+Ov1x zpWpsV&Hi@|9DMh^_YZw=_`@R~{q^IcpM3h+=U@Eo*zqrGzdG^tH~(?+)VHU<`~L4| z&i>=v59fcpQ1?IX|4lc?#osbw^xvF` zc$|PwIpW;8`B^zw3xXH9?mS22AmZ^! zsaa9cLmY`tUq&X_kikQve;pa=7!(l^Juot2V8oD?`QI}Cf6nvY=7`?1OZNA3t!s7&i^5}qGfQJxjJeYoMr~c=5X}SL@i_ge>e93%TB)8Dz z@n*U6w0Cqh3o3uF+v5}9apkx(jBq63`8j|-R}ubZ z8%~Jm%*{dwS5c}r)9Gf9ID5+S!hD?T(zAS33v5bcgh5tNk&=2~@LZ>7i*K1N-ugN?b<5Cl#_7FMO zV^Yz`(8q<(tL_xPn~`$;CdGA~^y4!}#-g7SVmtXv_p)TAd=9)A{WvZ&Aj^+yD5>fy zKr#Bzav;l(>sqN-%pCQH@n{~%^5eQ!%4O!LKb9ZY1<;TE&BUSu>L@q5nDuO~6z?rR zK8G=7IU%*;EvWYku7n?@oAqw7_;I|rZk2w)Y=ju&QOhBY9sRgAyz0_c#{CFl)yI@= ztb!Pue*Lljv_JkGI2x}lv;6VM!|#^{zclz+&uZyw`Msd|aXo7e{Hi(unosxZ{q|~p z!FXmBjPJF|)V8lRe)~1QBCLfi>SK)rG9-`b`|_IoXwL6wy3%&rm-KlVPCh*>eUYbtkWSGd!kwy);a?s%kMt{=*z+A literal 0 HcmV?d00001 From 8adb628d3d03430eb5223c02984d5afa90fd75f8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 28 Jul 2009 10:06:02 -0700 Subject: [PATCH 1253/1860] Bug 506795 - Handle adding duplicate logins. r=thunder Refactor reconcile to call findLikeId that by default will look for recordLike in the outgoing queue as it does now. Override findLikeId for password engine to search local logins. --- services/sync/modules/engines.js | 32 ++++++++++++++++------ services/sync/modules/engines/passwords.js | 17 ++++++------ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b9eb84645a8c..f420c50a679e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -356,6 +356,18 @@ SyncEngine.prototype = { Cu.forceGC(); }, + /** + * Find a GUID that is like the incoming item + * + * @return GUID of the similar record; falsy otherwise + */ + _findLikeId: function SyncEngine__findLikeId(item) { + // By default, only look in the outgoing queue for similar records + for (let id in this._tracker.changedIDs) + if (this._recordLike(item, this._createRecord(id))) + return id; + }, + _isEqual: function SyncEngine__isEqual(item) { let local = this._createRecord(item.id); this._log.trace("Local record: \n" + local); @@ -406,15 +418,17 @@ SyncEngine.prototype = { // Step 3: Check for similar items this._log.trace("Reconcile step 3"); - for (let id in this._tracker.changedIDs) { - let out = this._createRecord(id); - if (this._recordLike(item, out)) { - this._store.changeItemID(id, item.id); - this._tracker.removeChangedID(id); - this._tracker.removeChangedID(item.id); - this._store.cache.clear(); // because parentid refs will be wrong - return false; - } + let likeId = this._findLikeId(item); + if (likeId) { + // Change the local item GUID to the incoming one + this._store.changeItemID(likeId, item.id); + + // Remove outgoing changes of the original id any any that were just made + this._tracker.removeChangedID(likeId); + this._tracker.removeChangedID(item.id); + + this._store.cache.clear(); // because parentid refs will be wrong + return false; } return true; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 07673626a8d0..5bb23cd53bc2 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -61,14 +61,15 @@ PasswordEngine.prototype = { _trackerObj: PasswordTracker, _recordObj: LoginRec, - _recordLike: function SyncEngine__recordLike(a, b) { - if (a.deleted || b.deleted) - return false; - if (["hostname", "httpRealm", "username"].some(function(k) a[k] != b[k])) - return false; - if (!a.formSubmitURL || !b.formSubmitURL) - return true; - return a.formSubmitURL == b.formSubmitURL; + _findLikeId: function PasswordEngine__findLikeId(item) { + let login = this._store._nsLoginInfoFromRecord(item); + let logins = Svc.Login.findLogins({}, login.hostname, login.formSubmitURL, + login.httpRealm); + + // Look for existing logins that match the hostname but ignore the password + for each (let local in logins) + if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) + return local.guid; } }; From 4bceea2dabd22d41710b97b516e57092e43ddb43 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 28 Jul 2009 10:07:07 -0700 Subject: [PATCH 1254/1860] Change client engine to override findLikeId instead of recordLike to save on some work (iterating over changed ids). --- services/sync/modules/engines/clients.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 2c8e65bd4f82..770358358f1b 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -71,9 +71,7 @@ ClientEngine.prototype = { // We never want to change one client id to another, even if // they look exactly the same - _recordLike: function SyncEngine__recordLike(a, b) { - return false; - }, + _findLikeId: function ClientEngine__findLikeId() {}, // get and set info for clients From 4c44e3c4d255c444d05e1e6b54d9750a4920341c Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 28 Jul 2009 14:30:32 -0700 Subject: [PATCH 1255/1860] Re-structure window utils --- services/sync/modules/util.js | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 9774f6de74ec..8cf7833c5444 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -619,7 +619,16 @@ let Utils = { win = win.activeWindow; win["open" + type].apply(win, Array.slice(arguments, 2)); }, - + + _openChromeWindow: function Utils_openCWindow(name, uri, options, args) { + Utils.openWindow(name, "chrome://weave/content/" + uri, options, args); + }, + + openWindow: function Utils_openWindow(name, uri, options, args) { + Utils._openWin(name, "Window", null, uri, "", + options || "centerscreen,chrome,dialog,resizable=yes", args); + }, + openDialog: function Utils_openDialog(name, uri, options, args) { Utils._openWin(name, "Dialog", "chrome://weave/content/" + uri, "", options || "centerscreen,chrome,dialog,modal,resizable=no", args); @@ -629,10 +638,6 @@ let Utils = { this._genericDialogType = type; this.openDialog("ChangeSomething", "generic-change.xul"); }, - - openLog: function Utils_openLog() { - Utils.openWindow("Log", "log.xul"); - }, openLogin: function Utils_openLogin() { Utils.openDialog("Login", "login.xul"); @@ -641,22 +646,20 @@ let Utils = { openShare: function Utils_openShare() { Utils.openDialog("Share", "share.xul"); }, - + + openLog: function Utils_openLog() { + Utils._openChromeWindow("Log", "log.xul"); + }, openStatus: function Utils_openStatus() { - Utils.openWindow("Status", "status.xul"); + Utils._openChromeWindow("Status", "status.xul"); }, openSync: function Utils_openSync() { - Utils.openWindow("Sync", "pick-sync.xul"); + Utils._openChromeWindow("Sync", "pick-sync.xul"); }, - - openWindow: function Utils_openWindow(name, uri, options, args) { - Utils._openWin(name, "Window", null, "chrome://weave/content/" + uri, - "", options || "centerscreen,chrome,dialog,resizable=yes", args); - }, - + openWizard: function Utils_openWizard() { - Utils.openWindow("Wizard", "wizard.xul"); + Utils._openChromeWindow("Wizard", "wizard.xul"); }, // assumes an nsIConverterInputStream From 9531cb10c6382b87264f809443f7c01163ca97f0 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 28 Jul 2009 16:54:24 -0700 Subject: [PATCH 1256/1860] WinCE crypto binary --- services/crypto/Makefile | 39 +++++++++++++++--- .../platform/WINCE/components/WeaveCrypto.dll | Bin 0 -> 33280 bytes services/sync/tests/unit/Makefile | 2 +- 3 files changed, 34 insertions(+), 7 deletions(-) create mode 100755 services/crypto/platform/WINCE/components/WeaveCrypto.dll diff --git a/services/crypto/Makefile b/services/crypto/Makefile index cdefc6e79657..a9268fe75dbe 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -39,8 +39,19 @@ # OS detection -sys := $(shell uname -s) +xpidl = $(sdkdir)/bin/xpidl +link = link +sys := $(shell uname -s) +wince = $(WINCE) + +ifeq ($(wince), 1) + os = WINNT + cxx = $(sdkdir)/sdk/bin/arm-wince-gcc + xpidl = $(sdkdir)/host/bin/host_xpidl + link = $(sdkdir)/sdk/bin/arm-wince-link + so = dll +else ifeq ($(sys), Darwin) os = Darwin compiler = gcc3 @@ -55,7 +66,7 @@ ifeq ($(sys), Linux) so = so cppflags += -shared else -ifeq ($(sys), MINGW32_NT-6.0) +ifeq ($(sys), MINGW32_NT-6.1) os = WINNT compiler = msvc cxx = cl @@ -80,6 +91,7 @@ endif endif endif endif +endif # Arch detection @@ -153,8 +165,6 @@ sdkdir ?= ${MOZSDKDIR} destdir = .. platformdir = $(destdir)/platform/$(platform) -xpidl = $(sdkdir)/bin/xpidl - # FIXME: we don't actually require this for e.g. clean ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) @@ -180,11 +190,17 @@ headers = -I$(sdkdir)/include \ # libraries libdirs := $(sdkdir)/lib $(sdkdir)/bin +ifeq ($(wince),1) libs := xpcomglue_s xpcom nspr4 \ crmf smime3 ssl3 nss3 nssutil3 \ plds4 plc4 +else +libs := xpcomglue xpcomglue_s nspr4 \ + crmf smime3 ssl3 nss3 nssutil3 \ + plds4 plc4 +endif -ifeq ($(os), linux) +ifeq ($(os), Linux) libs := xpcom_core $(libs) endif @@ -256,6 +272,16 @@ ifeq ($(os), SunOS) $(sdkdir)/lib/libxpcomglue_s.a \ $(libdirs) $(libs) else +ifneq ($(wince),) + libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) + libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) + cppflags += -c -nologo -O1 -GR- -TP -Zc:wchar_t- -W3 -Gy $(headers) \ + -DMOZILLA_STRICT_API \ + -D"_WIN32_WCE=0x502" -D"UNDER_CE" -D"WIN32_PLATFORM_PSPC" \ + -D"WINCE" -D"ARM" -D"_ARM_" -D"POCKETPC2003_UI_MODEL" -DXP_WIN + ldflags += -DLL $(libdirs) $(libs) + rcflags := -r $(headers) +else ifeq ($(os), WINNT) libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) @@ -276,6 +302,7 @@ endif endif endif endif +endif ###################################################################### @@ -347,7 +374,7 @@ ifeq ($(os), WINNT) $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) - link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) + $(link) -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) chmod +x $@ endif endif diff --git a/services/crypto/platform/WINCE/components/WeaveCrypto.dll b/services/crypto/platform/WINCE/components/WeaveCrypto.dll new file mode 100755 index 0000000000000000000000000000000000000000..b2cddef1bf6dcf58b70b4f5a690dc750f9073270 GIT binary patch literal 33280 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 literal 0 HcmV?d00001 diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile index 4c06ad5db5c7..346940183b32 100644 --- a/services/sync/tests/unit/Makefile +++ b/services/sync/tests/unit/Makefile @@ -10,7 +10,7 @@ else ifeq ($(sys), Linux) os = Linux else -ifeq ($(sys), MINGW32_NT-6.0) +ifeq ($(sys), MINGW32_NT-6.1) os = WINNT else ifeq ($(sys), MINGW32_NT-5.1) From b06c307a0ca519d4960649a88732f2773fe568eb Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 28 Jul 2009 17:13:15 -0700 Subject: [PATCH 1257/1860] Backed out changeset 9e8936813ae1 --- services/crypto/Makefile | 37 +++--------------- .../platform/WINCE/components/WeaveCrypto.dll | Bin 33280 -> 0 bytes services/sync/tests/unit/Makefile | 2 +- 3 files changed, 6 insertions(+), 33 deletions(-) delete mode 100755 services/crypto/platform/WINCE/components/WeaveCrypto.dll diff --git a/services/crypto/Makefile b/services/crypto/Makefile index a9268fe75dbe..cdefc6e79657 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -39,19 +39,8 @@ # OS detection -xpidl = $(sdkdir)/bin/xpidl -link = link - sys := $(shell uname -s) -wince = $(WINCE) -ifeq ($(wince), 1) - os = WINNT - cxx = $(sdkdir)/sdk/bin/arm-wince-gcc - xpidl = $(sdkdir)/host/bin/host_xpidl - link = $(sdkdir)/sdk/bin/arm-wince-link - so = dll -else ifeq ($(sys), Darwin) os = Darwin compiler = gcc3 @@ -66,7 +55,7 @@ ifeq ($(sys), Linux) so = so cppflags += -shared else -ifeq ($(sys), MINGW32_NT-6.1) +ifeq ($(sys), MINGW32_NT-6.0) os = WINNT compiler = msvc cxx = cl @@ -91,7 +80,6 @@ endif endif endif endif -endif # Arch detection @@ -165,6 +153,8 @@ sdkdir ?= ${MOZSDKDIR} destdir = .. platformdir = $(destdir)/platform/$(platform) +xpidl = $(sdkdir)/bin/xpidl + # FIXME: we don't actually require this for e.g. clean ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) @@ -190,17 +180,11 @@ headers = -I$(sdkdir)/include \ # libraries libdirs := $(sdkdir)/lib $(sdkdir)/bin -ifeq ($(wince),1) libs := xpcomglue_s xpcom nspr4 \ crmf smime3 ssl3 nss3 nssutil3 \ plds4 plc4 -else -libs := xpcomglue xpcomglue_s nspr4 \ - crmf smime3 ssl3 nss3 nssutil3 \ - plds4 plc4 -endif -ifeq ($(os), Linux) +ifeq ($(os), linux) libs := xpcom_core $(libs) endif @@ -272,16 +256,6 @@ ifeq ($(os), SunOS) $(sdkdir)/lib/libxpcomglue_s.a \ $(libdirs) $(libs) else -ifneq ($(wince),) - libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) - libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) - cppflags += -c -nologo -O1 -GR- -TP -Zc:wchar_t- -W3 -Gy $(headers) \ - -DMOZILLA_STRICT_API \ - -D"_WIN32_WCE=0x502" -D"UNDER_CE" -D"WIN32_PLATFORM_PSPC" \ - -D"WINCE" -D"ARM" -D"_ARM_" -D"POCKETPC2003_UI_MODEL" -DXP_WIN - ldflags += -DLL $(libdirs) $(libs) - rcflags := -r $(headers) -else ifeq ($(os), WINNT) libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) @@ -302,7 +276,6 @@ endif endif endif endif -endif ###################################################################### @@ -374,7 +347,7 @@ ifeq ($(os), WINNT) $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) - $(link) -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) + link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) chmod +x $@ endif endif diff --git a/services/crypto/platform/WINCE/components/WeaveCrypto.dll b/services/crypto/platform/WINCE/components/WeaveCrypto.dll deleted file mode 100755 index b2cddef1bf6dcf58b70b4f5a690dc750f9073270..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33280 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 diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile index 346940183b32..4c06ad5db5c7 100644 --- a/services/sync/tests/unit/Makefile +++ b/services/sync/tests/unit/Makefile @@ -10,7 +10,7 @@ else ifeq ($(sys), Linux) os = Linux else -ifeq ($(sys), MINGW32_NT-6.1) +ifeq ($(sys), MINGW32_NT-6.0) os = WINNT else ifeq ($(sys), MINGW32_NT-5.1) From fcbd11bff1ea8a2525395a42a62c05401aaa4472 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 28 Jul 2009 17:14:52 -0700 Subject: [PATCH 1258/1860] WinCE crypto binary (bug #487171) --- services/crypto/Makefile | 39 +++++++++++++++--- .../platform/WINCE/components/WeaveCrypto.dll | Bin 0 -> 33280 bytes services/sync/tests/unit/Makefile | 2 +- 3 files changed, 34 insertions(+), 7 deletions(-) create mode 100755 services/crypto/platform/WINCE/components/WeaveCrypto.dll diff --git a/services/crypto/Makefile b/services/crypto/Makefile index cdefc6e79657..a9268fe75dbe 100755 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -39,8 +39,19 @@ # OS detection -sys := $(shell uname -s) +xpidl = $(sdkdir)/bin/xpidl +link = link +sys := $(shell uname -s) +wince = $(WINCE) + +ifeq ($(wince), 1) + os = WINNT + cxx = $(sdkdir)/sdk/bin/arm-wince-gcc + xpidl = $(sdkdir)/host/bin/host_xpidl + link = $(sdkdir)/sdk/bin/arm-wince-link + so = dll +else ifeq ($(sys), Darwin) os = Darwin compiler = gcc3 @@ -55,7 +66,7 @@ ifeq ($(sys), Linux) so = so cppflags += -shared else -ifeq ($(sys), MINGW32_NT-6.0) +ifeq ($(sys), MINGW32_NT-6.1) os = WINNT compiler = msvc cxx = cl @@ -80,6 +91,7 @@ endif endif endif endif +endif # Arch detection @@ -153,8 +165,6 @@ sdkdir ?= ${MOZSDKDIR} destdir = .. platformdir = $(destdir)/platform/$(platform) -xpidl = $(sdkdir)/bin/xpidl - # FIXME: we don't actually require this for e.g. clean ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) @@ -180,11 +190,17 @@ headers = -I$(sdkdir)/include \ # libraries libdirs := $(sdkdir)/lib $(sdkdir)/bin +ifeq ($(wince),1) libs := xpcomglue_s xpcom nspr4 \ crmf smime3 ssl3 nss3 nssutil3 \ plds4 plc4 +else +libs := xpcomglue xpcomglue_s nspr4 \ + crmf smime3 ssl3 nss3 nssutil3 \ + plds4 plc4 +endif -ifeq ($(os), linux) +ifeq ($(os), Linux) libs := xpcom_core $(libs) endif @@ -256,6 +272,16 @@ ifeq ($(os), SunOS) $(sdkdir)/lib/libxpcomglue_s.a \ $(libdirs) $(libs) else +ifneq ($(wince),) + libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) + libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) + cppflags += -c -nologo -O1 -GR- -TP -Zc:wchar_t- -W3 -Gy $(headers) \ + -DMOZILLA_STRICT_API \ + -D"_WIN32_WCE=0x502" -D"UNDER_CE" -D"WIN32_PLATFORM_PSPC" \ + -D"WINCE" -D"ARM" -D"_ARM_" -D"POCKETPC2003_UI_MODEL" -DXP_WIN + ldflags += -DLL $(libdirs) $(libs) + rcflags := -r $(headers) +else ifeq ($(os), WINNT) libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) @@ -276,6 +302,7 @@ endif endif endif endif +endif ###################################################################### @@ -347,7 +374,7 @@ ifeq ($(os), WINNT) $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) - link -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) + $(link) -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) chmod +x $@ endif endif diff --git a/services/crypto/platform/WINCE/components/WeaveCrypto.dll b/services/crypto/platform/WINCE/components/WeaveCrypto.dll new file mode 100755 index 0000000000000000000000000000000000000000..b2cddef1bf6dcf58b70b4f5a690dc750f9073270 GIT binary patch literal 33280 zcmeHw4}4VBo$r}skRe9AsZmp!+KUY~(g1fR0VWXO0s#XC7)j9ROG}1iNK%K%IGJFu zw2pw9Rnm=0E$xa-w?5ZrSvRk>Ep@Svw(cvnw9B@At!>@cN#b-dxNTl*mF?QH-|xBi zOm40Lwfg${KFjbqoO}NL&hP*4cg~$8>uwql`9cUU%9%4l97fHLDgQtBe`6S){=TQC zizlajcjjSF{dZ?Jc64^@@kCd9BD_s+4aZ_#NqtL1PxQp}&X``ip+Vo))fSn1(M1Ji zHt3tpPi>nw^WT=L^6=b8md-)@NB&2beh{_4`y)%sKyoo5b)-8}T3UBds>xc#hA?q=A4+~>g z)@%3Y8R;Pp#)Nh-`_&7?s1_F**ZGWrER-5B?~v<#fXOz;3SDD`jS+p7Axp92{EYPv=kd?anVe(VsVV$J0Kqo*tT_rH7^hc1MvoWdMe6 zCg?}s_$*>9HK;%T_~VbCM(suWHPmC~oEJ4kBk1#l z(nGz=2}{wpFWWYBFiRZRC(MVlq|rN;9;)+zMjdKsup%G!G^M!fWlx0E` zl={c#j>CQcYeFONUb8Rrr$;<%c8^qnPh;er4c9q0Wu!7}zyG@Q(2u8xQ^z>ZwDGd% zTl@!H%>5^$LE^kbG>mvctMZ=38Y!1^toc6HaK11e%yP{X(J=JmGiMexyKoYryCfgP z7oO^hpY7dVPB>{TWI!Y|KiM>^b6G zX8?~f>{X7vDvbVYX{bM|>;2g_O`O_{!o58rIRcsHL#FxbD1WVgm^3$VJU%$2#^4kS z*0K-wrTP}3FK1`EnZ9fx;hGkop~D4Ahx4JsLf@a&j04$xV^>BPeXz&tM?6~k62K_>-NoWP zLfi=0>7w7r=S?G^tLP>9e1n_^doOwcZQ`HzEaY%>?+A5a>@S}_dHm;R&d@g{zi#VC z9CKPM+ZCzwP!sI8P$*s7m#ww!mwitA74Wm{D}xO4p}QqTu%{4gm$qcym-Rtr9507_ zmCa+#5M@r?qu%*|pP_>{8hjaKGWv&c-7BX)iTzn$=s?y}0ho}bk?9{UF`>+L#d;lkc}>V5)t}8=Z0SWYc>5DR!#@_`nQH`aU|oj2E#H(LI?g%3k2>Pf zIOp!{H_=BPeo~Ho3UYvL!yb_$%uw8@X%d$-mcy1`3!-vvFo%VU+SC9d0)KoIa zKn5oEO@XG6MHlQ%+T3&b3hgHMBpoHrlyG~KpE z&;z~?m+Rn>2Kxi-KG3wAbhXglw$qPVG)>z*auswC^Q6vh=N#DFt7py>P#%S_aWMd! zh72n*pBR~*>Cbo`2fqf>BgK%-1>m6%I2RA}!%iTVbbr=2Xv-zupZz%DnO009pBJHB zC=O&z_KU`me?D_&2JN>I_FD@4ORyhln{CsD*qcwCu2B4iPX7=(Z6Fqrhvc6J^E)KG zkmX0F?ant+Py2j2;*5p@90~JM2TY8gj01+}r|X9E2!pU%P!}|gAg=C4T;1*YY5%Tx z>ghhpgE71Tdyv-`fOk^>)2pN3=Gz~BZ^?o9V%;#-<=}e&mw3`oUIiGdh%5Fx*Iq%# zUgWXbi*$@kvKMXKi`eW*VXrv$GCq|(4E-&D4LpS!I2Rm2?esl3B8=*0nth`ssF7xk zYN)3XKls3xp#!uV+7Eds^z_Jz_oe|WJ*0Dv!w_;qvUz@@9bi19?H^l8J z*hk662e`~vo&Zhc3DzKELk=sjWOCS%vkdGV;Sp)_nL10IHeR|@qa>0`3= zGxRajwe-;UZJ3O09|Ar;_*m>A{fZ8nJRh9hQpZ&T6M!kX| z0K;A*GT`3~w6#0ni(wmUrR`P^z`Vk9;_7E2ARK-CZYf7M_-($Z+E;t5Bxje&u$P~wp0uA_c zJ$%maR2}gQun%+G%CB;k1>t<4X{D8{Epr!ziTG3lUuzF~y%s1DD> za|Pg1hWW7JoR7Tw>60h^(ejCX&b-Bh99t~?_0&M`Lg|sIncX9vrz~DHLaww$#+VB5 z6>^tz=%304`?J?OWOX-WRc6b(G1EU>OL^zu+$-fxx~Y3Lkc|m=gt5v21IITBBY$@P zaIb#9)SVFEG-mFVey|+;V?IC_b%0S181Hw)w5?d5j~rvC?Yk+@yxHlIMmwkCI++_e z_PmjMN2vikK`NunV{aEWj=*GD_ab{UQC&p;71xU6FHRA#+E>Km@D%r z59bY7KK)&MPFc3i7(rX9!MqZ{^Z`EnS(d)tlYyQ==Fqnx(n!0|2Q9lOM$On$h}w&? zgfO978gz?tn*w~ft`2Z%#{zQ<&VgQ1|E8d?kZp&b@Y-!B4e(i``G^fE#0KRr$g|JA zX88)GgO@sV@bQbNk2k^ZrHlSXJ~xd3_oB3Xz7aN>zJ$5dezcWdHacR%c#$i!I1KP4F&`{uFH8#*x0K&7q5Rwk}e4E(DLJfVZ?0 zrGpPRbm4le%X_?kIGX;9%%Qj+Wl^VpLh3KqQGfP3_F9wEpHEBusWbXsr#p9EBu=GJ zr0%3%uRC)cx|85M_!;H{yZLO#g|q5T$kH9=)YJhV@TCqgS7Xjrj9TgrYA?zYgbBSU zgkCToPr_F6U~h5Q+l7Ro^=B22M7lqF$iW}vWjSA^e9-+4nSVe&TYgHH&*S-Wi7kKT z7IhB!A7;LcTd`nIrzz3-h&tKgWg)|19%j5_!FfI%9mW~*az`tf-k8v?tPsuZQAi z7mmZ+`WiYMicNuj=LAym%J;g+nqlLHgg) z$Ba#7sCizSqFa8DvYO-I#U1Tizqlr!!w0z7To=AHjT-<7;VOm?L!P19(xW<6Z&!OdJfiz3aBdD3d5C*n z(lECC>62FgehRwAvwX98Fxv=xu>Ytq_DMKcR|Fi2yyl+aDWY-cH11X_Ft6-Q%ri_2 zehJ_gqv$B5D2fL{TsI<8>7kPbcozR=_I;q?IlvXX%LJahJ9?%CZQSj8&6UVw$MK?^ z$7zD>%-UhxWemM?=FDu|Ny`{QUn7jWp!auXdx3NOP2gOIdFGp#C&UdS@QqWDTX|OC zz5%#+z$cI5>FHO+m;qWVP|8utP?9LUC_7OY7a8{`AKX`rd=+<1-pqj)C=WRX-hkc< zJs8XNA05{%;nFQH+7}&#UWqv`iqM=FeTRDFy0&NcLy=9)mVb`+0Z*e4n7N;oANzWHmaS?+dEw zujs$W_r@Km-1|?lFLCwCcTg5Rw6k2FP8x{suCwBI@dVx?CTqBpMjZaA%qzf4tk3gY zGD4qc`6tsJpo^g)q$@PKe2oGL;2#=Gx|Y!|4;rXybnp6*(KXP%zNha8M&qq z&&V(2vu5K*esP^fJV7Jvh3_w%eTHM~Vtb79^6h8xh`e^v^Sbrmvqht|pPSZbuh90R zxP3u|%l`3R4zeM>mQKi;w&~zo(B)H@gY(|F2>dDmzY6WOvbNXqo{+xeNf!=j%$NQE z{AEqO=(G9Dwv%tTJScGXl{xqZpE&85@@vj@PJX-B$uVj3maY?LH}5p~QRcC{!=n7x zQvTqZ|{Scn_pqMC12S`Ub=%}*kC;=VF>9f+$Sai%# zbi4%oUH}fy+jN|8;h7t_)@Ja!n`n%7jp$d5Jam|yuYuUyOkH+_v7H`L1J#&iB zl0NnU4rQrw8O~+?h<8h;bnHc6R)#ssR`^UGP;B>cA3hgjO}Wjhli-owgIvevRWY8u zC`WBxDIcNqd_7_^W$h7`Pbc5+!d<{*`mW*^dCi=?+{PU`lH+X$c+mphG}*kZckx!f z-$INe54pZfN5Nbj``&__R_Pwcow~q$nYwTSHb))U3z~MK1W}x^hB*Q0QMOj%u&W8N z9s5l-PuZCC9i-3Zb)l5;MBmZq@us-9lc$uA@>j}FD88wjh5LGOPoQutci_kz*@}6V z&6a>a7EH!$3pVbsMy?zUy*I+C!hsX{cDDW0yvg=sjy)OvZaftKR4(SE zr`Ul9&w9l!-pO(CK=ymcJ(SEm@ME}7vT_RWPx=7(WLevB3-M@%Jf8-wd~G^#ofNkA zfbByb$l^qGih17IXmaKb&K#Aw5zoiYTt(rkbVbGS7htzeotlg%D)usua^uWA^^mzls!=&owllQCJf%0FbA`8=E-f_IiG&hslQd!U&LV2t@2WZ z_hfsz{g*nEIDM5n=D%*t5%c93@(_!kyC(R&KSloLbz`;hW%-I=2~+jo|mU z;Gcu!E#mD$`cC8}KgIo(iv8{wYuyzb$OaJOR6N&hA5UEQ%($!K58=|zOz1(4#(w%^ z-j7(d@_S{7*JX0dKBHhA^Iy)#-JImRiu;7gT+IV}ka-T`UOCn?9#%MT;WPb>(@*jI zreeNf=b^l#aq^V08Sit44`*Mev&a;)JgS+H>o@Jyh;w;1QVziZQuw8<-6(_9mQrCJN zZAqiOz6@=hG}~>-8|a3z?eZPtIV$77qTypUuI~Aiqlz6GXq5RO&l7<89o~f_2Xex6 z=I`XeO@Nz|6ZDMv`){THdUN?ZVVo;}F9)nYCV#KkKAyjq;@OLG)Xv>VuQPuq4&|Uj z*)Z*eIi{lH&E)Td@lNLNgeQPg6?F>wN?mdL7TWVWk-w+G88MSLvSepORP5U6sGr!XB8vdv6um%b;n{MVC5@Qs&Ij)Omrt zz7oE~2Y=1k7yD2%m)Y*wXLHbe5SN=@xsr)VXq+gRT zb@~y@&bd~~9#3LT5*lE z^ZW8_6IaF@=3AtL_iGV+2b-}&t*3l=pUFO%S7DxxbL$Q0;~nKO>|H1Mjqi`5Jr&;{ ztw*~a^F!Dp1fI+S{*CD4{X-+#24Zg$+D({4o^ajo0Y}k?^O(GgDaRQ1xF2Jra{zU@ z!~@^u0iA9<>908dA26>O@SCw8WwH|X+JbhAy&u1W&v}23d2Q$??1f%&ssqm*m=ndm zZK$cE+=D!l`3L!gyGGLC)$SOM0(LZmzRbgTxA%z&->HoQRvc@cGFNml$Kd|WHeENN zuE1RC0`eD$+a~mJ?;!T33trpYJlbJ_eYKC4>q67$u#wQ|j+mpl7_d;u!U&&ejoiZ$)`B#J;915Wz~EZmo3sCyz)SI>4tu9CP94~ZnzHESeAL`q`OHG_ zmOhi;HK)(4BTq8ugU>8=`OJFoxgKlD6Ad-xi94kN!c%dU?=)09gn6|2CY(w6oa3YI zXW-uooPCY<{4&hvGxrDqKiWp2=V10a&__C7f$id4JId$Z*gn$fzbbM#SUlJX7`=dz z1a17888=-`po_82Nf&vl=&)@%*KW4)OhS+N?4*Hn-E_6s^G(d>GwEsuPA$NxTpY~4 z2XujNquh^rN`2Y}J>|SAJkze)?09h)IpQIdM^HLYT2Pu$s!)_aqW)Gve`za9Z=1kB z5B6bSPVWGldQ07)-qJ3qw`_ABb%f_vm3!dlA!ZBoJM${WO!n1dUY&LzTMJx826J4x z!~CfhiEm7=UYFOvNhdEWZa!A1Md}y3agLgYFA_x32>7_zLWcG*K4PUa&vD8+_`^kO}!k zKU0D|t8AH6;7snd&*duLm9dwg}h%}TV z*Kfx=6BQ$r&T=2VcjQ@2**4|Kw$uUIoZXi1T+yxq{e}82`Tn&^v+SQdU|ycZxkbsL z9P3o<<}-6V>Zr62n?7|upkGBi8>L@i3{W`L_?a%yR%)Vb%F?2D**>S zN%~~QYl#QO0DJgD7(q}NA++r4V3v15e1FIH!6g~+ z-SIw{a988Jquahip8IdZ_oGk_+xKG1?wxv*B3~pOXV(|b=Uwg_ogJL;Ets?o@Bp;Q zHqH@@J(TGb(5LjI#ic_^?wrH?kNXDCGKaP+=fggw4PvfSAC=#uEh@QBz!SJlW{>C6 z>uShsGM@1c)xC$DXZsk%lga!Q^Car;A;?gjEtJove&V~9_VFrb$*u@-@mr) zNaf_ryM43)%+c|jcIiwR`pazExgI}@K|T(D1#ndkK;2|+z<&J9)`W9OJ=QWmV7|dK zije^yk@}@7G@Vz+rrKc!Qz_sF* zm%3y6+x2DepOgEt+sA#G)1Jru zQ2+4VxTA+&pVJTZg11haR`}946l<2R`Z?whj*Jb^L(=G6qj(|x3C5na=f6?jLY-qi zO}%!;1h*a0=6Sy4cMXUW_x~*TGMO#$I|I(|U>|X{)2{X-uJ&WD_90h$(A9p@)jsTM z?{~G2y4ugU+Q(e&=Q(hm1Lrw#o&)DO@c$VHj#~JxE|1%18dckLtTa}?{9*rZmVaf(t{25G z&ipQ@;pcHsO0N`J2z3ie6lEvMGw9RG@J@3Ue!sxG@pDZj__T!?UeB;1A$edkU+ z+?WV=M$rKDHL=Lnt&!GbC#Y&nbZ+B@v${~)Pr)es3MT01DDtz8NIwn(CDPBap0Pj=|tQkZkW$3Mj$ z|EcAFO(YgcbhfSzw!D+A@*Hb%Ctj3uwumoCxqK0v@XMB2kqJrUX3 z8pAJ4h;>g~j45wjUK2sc{SAHQ65ZaMFxz_K*fSalcSk1O^PSo^ye8J2498j{p(IUM zT-Osx>;NkwiLK#Q{MPiyEj^uyNVgu=V_mU1>o?Wc>ur(lWM?d#?CgrozwXdbYRjVy(bpl5{>A|E{NKKhVF4_`jVyktlmjrQSOt$Q#k1zU0pXzA2$J9 zB9iP$#1I}Lj2SuT_(BlqVf;8ZMU(G2PD4))!sJv4acBd+^MvvwN=-m$Cs3LykvCK! z9?uurNtBWWn1kBu$M{_QJp4T54|p!Y+-8i`FU0R4x&G{h?}f=9AKr_xEhO2j z%Qhe7=k{+e{`v9m7d>_OOLdXj@M|g;IBJcqK2a3iZ~o?oMFaR*jz|7w$!DIa5!%2a z;JX-bRsy$Lw68&)vj%wCd%5=roVdUHmWCe`eR{n}ZT^1OFK_JX;U7i#KoVvQpN0b4 zhjmNKj&-lxunvwuzqJzqsVg2ygyA6PEG@s9VM%P;7`~OG@Kkh-u>0m7<}1x%<}1yu z$XA+O`AW0QSDKlxG%M(HJFxZ--?_Z(ZzGS^J^AHRdwzEL%KKe@Cl-mcMUb9|Tj-BA z!vpn1lXKf6$?~i9C3KV$6C%NnCokVS<$|fcX$2P+UNrq;t>|5s6kmGTE&aZSUy($j#Ah zv99*-1c9ZKlI_vEoUG9=+2LQ{PxrxyLRun^RB!5{^Fi{_x69{F9+_s z|ABp(4#%UmyLO$Nt;npFi}sPyF5BfB(W4pZwC7zjFAi zPyPLor@!_;j(+_c-+bmD{^z%jef!z(9RKe3{_(^=eg6kP{9ix%@pC`<>Cc}3=l}il z%+T=23)x@%%gGltM|M~TAUjFSXub%$5-~Ib*zyHITw@v@@6)S63t-hvi z&Dv}0*R9_Wx^81buy}0NQ8H6-oj`}A2G50mi@K0T*hUU z+b&NgBirJLd+qd<;VAw9L^v7IBfYJW?rtR3(H(j;oM?wB5tBb6COP(;xu$!0PqItW zR1W_gMni(tyF0piqHTHy&nH_ule(cJMc78KPcwKsbauwtBE5Re61`q#quf@k=xXm_ zIDw5b*y9Ov-tIn0s;MMeJjqi zV2lh;R!q>1-dRn5j83kLY*V?vaR&g{A9I%wWdlMKI{HHB6X<)%(O17)hz$B_9DPrq z&p=AB z+=UWFX+gOjg`am}-`DOGVi@HJ$`dFLqEw(ij`}8)0Lm380_Ef$;LS0VK9q-1zJPK9 zWe>)0!uXF+jsyPlD8EGUqFsVgj;kno*9U@Z%B7mQ|WJE^i7gZ!&LU&$49!w9#C{Hgb;E$mZ^jo>+2j zTQn+GZrHf0w!U6IUqEeIXvD#>#==2G_%iG#;>J;LA zcCTvBvfFBvw2b8pJwm*jV<4y@lGxta8d--FtUZDgoXoc*<#&qE#?7k}k%;(LKCP26 z3u=sKtXtWWK;RT+;&S#bPj+qVY+Vy;O~{ZU#AV~XwUJzJtUI2lk~=KUYgn~% zP2;L{a+@^>kT|z`Lf88JvZEo|l@#rc_Kt15N;9pVTAU&iT|3A>{#B?tySK9){orLd ziZg-Lx3V)1f4(6u{B|2CCnNU46+0R%u;NK;bPdiyiKN~6F{=;s$|EhH0t1QHFRYPJ z7=I;fb)swA8hknMzTE6kxNTjewId9Xf~GR7e|fY$8E$Xva*W(X4s2Lc3yDi=Fr_Pj z+>1dZ4*0HZ*tA`R=5}9 z_FNwpODox6^{nc}y<=+4Miw*`LHgI-md&~8R)L3!;e#8cE0Zm;FW>l;9h;|3)M z_W#L4+4-3Fx|sJyf_NTx387a9f3W@ACsuz=fA9+%R_{4-7=6uGx$YR=2+*IyxCWf_ zg;{9VC9Ly=sk6R7n0(KJpl#N(_6U>jdIY|R-ohH|qIR3_ec0|pomY+iBGg-j>Agf~ z`F+AHyA*X;n2%kCx>1;?FUR|rB4P5qP+o~J`Hm>>J;LPsBChAVqWq*V`MxOsQDO3( zQGNnlMI?#aCKg;{~SGOpvD8R7BX%v&lpvF3V zDQfT|%KFRT({j`o0WNFIKg=5YKeobZpG2Jx{_+mZ3;FUM%{z$waF^z7f&A8>2E1n0 z%dr1i%zrQBfZAIOxnGMKbTqOCoqJeA{wG<3e{J=svHyP7^Du86YR4mI@R7;EfXyAd_^6Ah@rzjD@q zzn3-OKF1n(6*i&<+#1#x|8sBw<2ZO=B=S#wrpEjD&^kp_L;yJ^whCNjh(mIY0o<*BRbmV2kblskMIUV9kz^q44y9fgoXT-%0xkEcf@fr%FouXB)>Bj$ygf7;J5PEgt z7jseOiEHe30Br;9`UyRJIbA1;v5&QX96QCZ$9mMng`aiU_clpQ6ur8fnE)nTz$6ST zI`M?GR+OW7a&*^XbvJM$KjMyAI3tQn9P_TnyaZ+{C_-XhEwP;|0+LtN_sz*W*}$@W%fK>lU5Lkw zL&l)-q;c3dVjMO4{CoWa{(b)a{*J0-b#T$%MKz1f#kGs;7S}HhEpA-gQ4_6+*CcCt zYj)P8YU-BMF9|JaT+*}zfK1$Zph_<6UAS{$YT=%ReGB(4^i>yDA6rziIJ5YL#Vs|p zOY9X(o0cA4dSodP;63Y@aoB&vf7Jhs-wcceUY7jWQ`uMfP}QNTBUQ(%p09eT>b0uE z`K9wK=GTD!=K0b2JLm76|IqwH^N-9wKL7doFU@~#e&K@B1r-Zw7Su0jUJzZdbHQG4 z^U#7L3yv>%e!)u%URzMOuykR?!W#S{1Sq#iq0)>JBWTnZrcrCu8TCfUXf&FPW~0Su zGdhf@5jT=Xuko^R+K2|@!DO&ExHFgv?g{n<_XY=o`-1y}4+Yb~M}m(94+RHB7S?DT%oxh%@p2octDq;8x z;-nkph5^h>U=;#JEx;xYOj1UlF<|UB(#B(u*ZE$Njzjl)n$s-S1EPAM+3T4};^!{3ra+`7{2L{xSb)7>6&Q1xf;X zpgdp%f&nv77YGHK0xf}#Ks?YJNCo-=1A+a4bl|bTAawI+;8@^9;JH91a1wGm4ITDX uYLz9GdS!W~Q5mc>E9)vll}(i`l^vDw%HB$nc>Z&q1Lrw#o&*0GIq)Bms#yO3 literal 0 HcmV?d00001 diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile index 4c06ad5db5c7..346940183b32 100644 --- a/services/sync/tests/unit/Makefile +++ b/services/sync/tests/unit/Makefile @@ -10,7 +10,7 @@ else ifeq ($(sys), Linux) os = Linux else -ifeq ($(sys), MINGW32_NT-6.0) +ifeq ($(sys), MINGW32_NT-6.1) os = WINNT else ifeq ($(sys), MINGW32_NT-5.1) From f384d18164ddc9c34c57773b210108565e6d0489 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 30 Jul 2009 11:52:26 -0700 Subject: [PATCH 1259/1860] Bug 507296 - Sync bookmarks and history in batched mode. r=thunder Wrap the original SyncEngine._sync function with a call to runInBatchMode, so SQL disk writes happen at the end of batch mode instead of on every change. --- services/sync/modules/engines/bookmarks.js | 8 +++++++- services/sync/modules/engines/history.js | 6 ++++++ services/sync/modules/util.js | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 98ceca013a74..98aa0e025190 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -87,7 +87,13 @@ BookmarksEngine.prototype = { logName: "Bookmarks", _recordObj: PlacesItem, _storeObj: BookmarksStore, - _trackerObj: BookmarksTracker + _trackerObj: BookmarksTracker, + + _sync: function BookmarksEngine__sync() { + Svc.Bookmark.runInBatchMode({ + runBatched: Utils.bind2(this, SyncEngine.prototype._sync) + }, null); + } }; function BookmarksStore() { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 80a68fae8fa1..f6c2d85e56f9 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -62,6 +62,12 @@ HistoryEngine.prototype = { _storeObj: HistoryStore, _trackerObj: HistoryTracker, + _sync: function HistoryEngine__sync() { + Svc.History.runInBatchMode({ + runBatched: Utils.bind2(this, SyncEngine.prototype._sync) + }, null); + }, + // History reconciliation is simpler than the default one from SyncEngine, // because we have the advantage that we can use the URI as a definitive // check for local existence of incoming items. The steps are as follows: diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 8cf7833c5444..146d70dfe13d 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -726,6 +726,7 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], + ["History", "@mozilla.org/browser/nav-history-service;1", "nsINavHistoryService"], ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], ["Login", "@mozilla.org/login-manager;1", "nsILoginManager"], From 4d4f687fde55f898c6a21344959001e80a9e314a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 6 Aug 2009 11:28:33 -0700 Subject: [PATCH 1260/1860] Bug 507691 - Server responds with json decode failure for fat unicode characters Make sure we generate ASCII data for upload by escaping on serialize and unescaping on deserialize. Test to make sure serialized data is ASCII and the original records aren't modified by serialize. --- services/sync/modules/type_records/clients.js | 23 ++++++++++ .../sync/tests/unit/test_clients_escape.js | 46 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 services/sync/tests/unit/test_clients_escape.js diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index eee34164032c..4cabb0e69a50 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -56,6 +56,29 @@ ClientRecord.prototype = { this._WBORec_init(uri); }, + _escape: function ClientRecord__escape(toAscii) { + // Escape-to-ascii or unescape-from-ascii each value + if (this.payload != null) + for (let [key, val] in Iterator(this.payload)) + this.payload[key] = (toAscii ? escape : unescape)(val); + }, + + serialize: function ClientRecord_serialize() { + // Convert potential non-ascii to ascii before serializing + this._escape(true); + let ret = WBORecord.prototype.serialize.apply(this, arguments); + + // Restore the original data for normal use + this._escape(false); + return ret; + }, + + deserialize: function ClientRecord_deserialize(json) { + // Deserialize then convert potential escaped non-ascii + WBORecord.prototype.deserialize.apply(this, arguments); + this._escape(false); + }, + // engines.js uses cleartext to determine if records _isEqual // XXX Bug 482669 Implement .equals() for SyncEngine to compare records get cleartext() this.serialize(), diff --git a/services/sync/tests/unit/test_clients_escape.js b/services/sync/tests/unit/test_clients_escape.js new file mode 100644 index 000000000000..d318e0c01575 --- /dev/null +++ b/services/sync/tests/unit/test_clients_escape.js @@ -0,0 +1,46 @@ +/** + * Print some debug message to the console. All arguments will be printed, + * separated by spaces. + * + * @param [arg0, arg1, arg2, ...] + * Any number of arguments to print out + * @usage _("Hello World") -> prints "Hello World" + * @usage _(1, 2, 3) -> prints "1 2 3" + */ +let _ = function(some, debug, text, to) print(Array.join(arguments, " ")); + +Components.utils.import("resource://weave/engines/clientData.js"); + +function run_test() { + _("Test that serializing client records results in uploadable ascii"); + Clients.setInfo("ascii", { + name: "wéävê" + }); + + _("Make sure we have the expected record"); + let record = Clients._store.createRecord("ascii"); + do_check_eq(record.id, "ascii"); + do_check_eq(record.payload.name, "wéävê"); + + let serialized = record.serialize(); + let checkCount = 0; + _("Checking for all ASCII:", serialized); + Array.forEach(serialized, function(ch) { + let code = ch.charCodeAt(0); + _("Checking asciiness of '", ch, "'=", code); + do_check_true(code < 128); + checkCount++; + }); + + _("Processed", checkCount, "characters out of", serialized.length); + do_check_eq(checkCount, serialized.length); + + _("Making sure the record still looks like it did before"); + do_check_eq(record.id, "ascii"); + do_check_eq(record.payload.name, "wéävê"); + + _("Sanity check that creating the record also gives the same"); + record = Clients._store.createRecord("ascii"); + do_check_eq(record.id, "ascii"); + do_check_eq(record.payload.name, "wéävê"); +} From 325979686dd8f1969068fee04e032d4e67efb536 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 11 Aug 2009 11:22:13 -0700 Subject: [PATCH 1261/1860] Ask for password/passphrase before changing it (bug #507434) --- services/sync/locales/en-US/generic-change.properties | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties index d20e80e3afe1..d477881fa189 100644 --- a/services/sync/locales/en-US/generic-change.properties +++ b/services/sync/locales/en-US/generic-change.properties @@ -3,6 +3,9 @@ noPassphrase.alert = You must enter a passphrase. passwordNoMatch.alert = Your passwords do not match. Try again! passphraseNoMatch.alert = Your passphrases do not match. Try again! +incorrectPassword.alert = Your current password is incorrect! +incorrectPassphrase.alert = Your current passphrase is incorrect! + change.password.title = Change your Password change.password.status.active = Changing your password... change.password.status.success = Your password has been changed. @@ -22,7 +25,9 @@ reset.passphrase.label = Resetting passphrase, please wait... reset.passphrase.error = There was an error while resetting your passphrase! reset.passphrase.success = Your passphrase was successfully reset! +new.passphrase.old = Enter your current passphrase new.passphrase.label = Enter your new passphrase new.passphrase.confirm = Confirm your new passphrase +new.password.old = Enter your current password new.password.label = Enter your new password -new.password.confirm = Confirm your new password \ No newline at end of file +new.password.confirm = Confirm your new password From 651badb65c60aff0baa27494c388f68693a39fe8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 12 Aug 2009 20:28:46 -0700 Subject: [PATCH 1262/1860] Bug 510152 - Check for failure onStopRequest and throw the error for extra debugging Wrap the error code with a JS Error to track the stack, and re-wrap it to get the full stack trace. Also, remove args for __request for the stack because it's sometimes the whole record.. --- services/sync/modules/resource.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index a67bdbad95ed..c4afb9176959 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -270,7 +270,24 @@ Resource.prototype = { let [chanOpen, chanCb] = Sync.withCb(channel.asyncOpen, channel); let listener = new ChannelListener(chanCb, this._onProgress, this._log); channel.requestMethod = action; - this._data = chanOpen(listener, null); + + // The channel listener might get a failure code + try { + this._data = chanOpen(listener, null); + } + catch(ex) { + // Combine the channel stack with this request stack + let error = Error(ex.message); + let chanStack = ex.stack.trim().split(/\n/).slice(1); + let requestStack = error.stack.split(/\n/).slice(1); + + // Strip out the args for the last 2 frames because they're usually HUGE! + for (let i = 0; i <= 1; i++) + requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@"); + + error.stack = chanStack.concat(requestStack).join("\n"); + throw error; + } if (!channel.requestSucceeded) { this._log.debug(action + " request failed (" + channel.responseStatus + ")"); @@ -347,10 +364,14 @@ ChannelListener.prototype = { this._data = ''; }, - onStopRequest: function Channel_onStopRequest(channel, ctx, time) { + onStopRequest: function Channel_onStopRequest(channel, context, status) { if (this._data == '') this._data = null; + // Throw the failure code name (and stop execution) + if (!Components.isSuccessCode(status)) + this._onComplete.throw(Error(Components.Exception("", status).name)); + this._onComplete(this._data); }, From 3dd36a07ae2b941ae5a4c618b4a56a459f4df4a5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 13 Aug 2009 17:43:20 -0700 Subject: [PATCH 1263/1860] Remove unused bookmark annotations/constants. --- services/sync/modules/engines/bookmarks.js | 59 ---------------------- 1 file changed, 59 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 98aa0e025190..9baa386777e0 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -42,17 +42,6 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -// Annotation to use for shared bookmark folders, incoming and outgoing: -const INCOMING_SHARED_ANNO = "weave/shared-incoming"; -const OUTGOING_SHARED_ANNO = "weave/shared-outgoing"; -const SERVER_PATH_ANNO = "weave/shared-server-path"; -// Standard names for shared files on the server -const KEYRING_FILE_NAME = "keyring"; -const SHARED_BOOKMARK_FILE_NAME = "shared_bookmarks"; -// Information for the folder that contains all incoming shares -const INCOMING_SHARE_ROOT_ANNO = "weave/mounted-shares-folder"; -const INCOMING_SHARE_ROOT_NAME = "Shared Folders"; - const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -277,20 +266,6 @@ BookmarksStore.prototype = { newId = this._bms.createFolder(parentId, record.title, record.sortindex); - // If folder is an outgoing share, put the annotations on it: - if ( record.outgoingSharedAnno != undefined ) { - this._ans.setItemAnnotation(newId, - OUTGOING_SHARED_ANNO, - record.outgoingSharedAnno, - 0, - this._ans.EXPIRE_NEVER); - this._ans.setItemAnnotation(newId, - SERVER_PATH_ANNO, - record.serverPathAnno, - 0, - this._ans.EXPIRE_NEVER); - - } break; case "livemark": this._log.debug(" -> creating livemark \"" + record.title + "\""); @@ -300,25 +275,6 @@ BookmarksStore.prototype = { Utils.makeURI(record.feedUri), record.sortindex); break; - case "incoming-share": - /* even though incoming shares are folders according to the - * bookmarkService, _wrap() wraps them as type=incoming-share, so we - * handle them separately, like so: */ - this._log.debug(" -> creating incoming-share \"" + record.title + "\""); - newId = this._bms.createFolder(parentId, - record.title, - record.sortindex); - this._ans.setItemAnnotation(newId, - INCOMING_SHARED_ANNO, - record.incomingSharedAnno, - 0, - this._ans.EXPIRE_NEVER); - this._ans.setItemAnnotation(newId, - SERVER_PATH_ANNO, - record.serverPathAnno, - 0, - this._ans.EXPIRE_NEVER); - break; case "separator": this._log.debug(" -> creating separator"); newId = this._bms.insertSeparator(parentId, record.sortindex); @@ -445,21 +401,6 @@ BookmarksStore.prototype = { case "feedUri": this._ls.setFeedURI(itemId, Utils.makeURI(val)); break; - case "outgoingSharedAnno": - this._ans.setItemAnnotation(itemId, OUTGOING_SHARED_ANNO, - val, 0, - this._ans.EXPIRE_NEVER); - break; - case "incomingSharedAnno": - this._ans.setItemAnnotation(itemId, INCOMING_SHARED_ANNO, - val, 0, - this._ans.EXPIRE_NEVER); - break; - case "serverPathAnno": - this._ans.setItemAnnotation(itemId, SERVER_PATH_ANNO, - val, 0, - this._ans.EXPIRE_NEVER); - break; } } }, From 8903cb90e67a593a8fbb91f3c198527f6d69200d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 13 Aug 2009 17:59:26 -0700 Subject: [PATCH 1264/1860] Put Annotations in Svc and expose a get/set Utils.anno call to use from bookmarks. --- services/sync/modules/engines/bookmarks.js | 36 ++++++---------------- services/sync/modules/util.js | 17 +++++++++- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 9baa386777e0..b48f323c4328 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -139,14 +139,6 @@ BookmarksStore.prototype = { return this.__ts; }, - __ans: null, - get _ans() { - if (!this.__ans) - this.__ans = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - return this.__ans; - }, - _getItemIdForGUID: function BStore__getItemIdForGUID(GUID) { if (GUID in kSpecialIds) return kSpecialIds[GUID]; @@ -235,20 +227,15 @@ BookmarksStore.prototype = { record.title); this._tagURI(uri, record.tags); this._bms.setKeywordForBookmark(newId, record.keyword); - if (record.description) { - this._ans.setItemAnnotation(newId, "bookmarkProperties/description", - record.description, 0, - this._ans.EXPIRE_NEVER); - } + if (record.description) + Utils.anno(newId, "bookmarkProperties/description", record.description); if (record.loadInSidebar) - this._ans.setItemAnnotation(newId, "bookmarkProperties/loadInSidebar", - true, 0, this._ans.EXPIRE_NEVER); + Utils.anno(newId, "bookmarkProperties/loadInSidebar", true); if (record.type == "microsummary") { this._log.debug(" \-> is a microsummary"); - this._ans.setItemAnnotation(newId, "bookmarks/staticTitle", - record.staticTitle || "", 0, this._ans.EXPIRE_NEVER); + Utils.anno(newId, "bookmarks/staticTitle", record.staticTitle || ""); let genURI = Utils.makeURI(record.generatorUri); if (this._ms) { try { @@ -370,16 +357,13 @@ BookmarksStore.prototype = { this._bms.setKeywordForBookmark(itemId, val); break; case "description": - this._ans.setItemAnnotation(itemId, "bookmarkProperties/description", - val, 0, - this._ans.EXPIRE_NEVER); + Utils.anno(itemId, "bookmarkProperties/description", val); break; case "loadInSidebar": if (val) - this._ans.setItemAnnotation(itemId, "bookmarkProperties/loadInSidebar", - true, 0, this._ans.EXPIRE_NEVER); + Utils.anno(itemId, "bookmarkProperties/loadInSidebar", true); else - this._ans.removeItemAnnotation(itemId, "bookmarkProperties/loadInSidebar"); + Svc.Annos.removeItemAnnotation(itemId, "bookmarkProperties/loadInSidebar"); break; case "generatorUri": { try { @@ -451,19 +435,19 @@ BookmarksStore.prototype = { _getDescription: function BStore__getDescription(id) { try { - return this._ans.getItemAnnotation(id, "bookmarkProperties/description"); + return Utils.anno(id, "bookmarkProperties/description"); } catch (e) { return undefined; } }, _isLoadInSidebar: function BStore__isLoadInSidebar(id) { - return this._ans.itemHasAnnotation(id, "bookmarkProperties/loadInSidebar"); + return Svc.Annos.itemHasAnnotation(id, "bookmarkProperties/loadInSidebar"); }, _getStaticTitle: function BStore__getStaticTitle(id) { try { - return this._ans.getItemAnnotation(id, "bookmarks/staticTitle"); + return Utils.anno(id, "bookmarks/staticTitle"); } catch (e) { return ""; } diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 146d70dfe13d..7790632e3a9a 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -130,6 +130,20 @@ let Utils = { return uuidgen.generateUUID().toString().replace(/[{}]/g, ''); }, + anno: function anno(id, anno, val, expire) { + switch (arguments.length) { + case 2: + // Get the annotation with 2 args + return Svc.Annos.getItemAnnotation(id, anno); + case 3: + expire = Svc.Annos.EXPIRE_NEVER; + // Fallthrough! + case 4: + // Set the annotation with 3 or 4 args + return Svc.Annos.setItemAnnotation(id, anno, val, 0, expire); + } + }, + // Returns a nsILocalFile representing a file relative to the // current user's profile directory. If the argument is a string, // it should be a string with unix-style slashes for directory names @@ -722,7 +736,8 @@ Utils.EventListener.prototype = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); -[["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], +[["Annos", "@mozilla.org/browser/annotation-service;1", "nsIAnnotationService"], + ["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], From 5350f6957a3b54aee6c784a67c1bf4c60af00141 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 13 Aug 2009 18:50:54 -0700 Subject: [PATCH 1265/1860] Remove meta/mini records. Bye! --- services/sync/modules/engines.js | 52 +++++++--------------- services/sync/modules/engines/bookmarks.js | 46 +++---------------- services/sync/modules/stores.js | 7 --- 3 files changed, 23 insertions(+), 82 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f420c50a679e..4d6c560583d2 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -457,57 +457,39 @@ SyncEngine.prototype = { if (outnum) { // collection we'll upload let up = new Collection(this.engineURL); - let meta = {}; + let count = 0; + + // Upload what we've got so far in the collection + let doUpload = Utils.bind2(this, function(desc) { + this._log.info("Uploading " + desc + " of " + outnum + " records"); + up.post(); + if (up.data.modified > this.lastSync) + this.lastSync = up.data.modified; + up.clearRecords(); + }); // don't cache the outgoing items, we won't need them later this._store.cache.enabled = false; - let count = 0; for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); this._log.trace("Outgoing:\n" + out); - // skip getting siblings of already processed and deleted records - if (!out.deleted && !(out.id in meta)) - this._store.createMetaRecords(out.id, meta); - out.encrypt(ID.get("WeaveCryptoID")); up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient - if ((++count % MAX_UPLOAD_RECORDS) == 0) { - // partial upload - this._log.info("Uploading " + (count - MAX_UPLOAD_RECORDS) + " - " + - count + " out of " + outnum + " records"); - up.post(); - if (up.data.modified > this.lastSync) - this.lastSync = up.data.modified; - up.clearRecords(); - } + // Partial upload + if ((++count % MAX_UPLOAD_RECORDS) == 0) + doUpload((count - MAX_UPLOAD_RECORDS) + " - " + count + " out"); Sync.sleep(0); } + // Final upload + if (count % MAX_UPLOAD_RECORDS > 0) + doUpload(count >= MAX_UPLOAD_RECORDS ? "last batch" : "all"); + this._store.cache.enabled = true; - - // now add short depth-and-index-only records, except the ones we're - // sending as full records - let metaCount = 0; - for each (let obj in meta) { - if (!(obj.id in this._tracker.changedIDs)) { - up.pushData(obj); - metaCount++; - } - } - - // final upload - if ((count % MAX_UPLOAD_RECORDS) + metaCount > 0) { - this._log.info("Uploading " + - (count >= MAX_UPLOAD_RECORDS? "last batch of " : "") + - count + " records, and " + metaCount + " index/depth records"); - up.post(); - if (up.data.modified > this.lastSync) - this.lastSync = up.data.modified; - } } this._tracker.clearChangedIDs(); }, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index b48f323c4328..4dd97e3d886b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -543,15 +543,6 @@ BookmarksStore.prototype = { return record; }, - _createMiniRecord: function BStore__createMiniRecord(placesId, depthIndex) { - let foo = {id: this._bms.getItemGUID(placesId)}; - if (depthIndex) { - foo.depth = this._itemDepth(placesId); - foo.sortindex = this._bms.getItemIndex(placesId); - } - return foo; - }, - _getWeaveParentIdForItem: function BStore__getWeaveParentIdForItem(itemId) { let parentid = this._bms.getFolderIdForItem(itemId); if (parentid == -1) { @@ -562,9 +553,7 @@ BookmarksStore.prototype = { return this._getWeaveIdForItem(parentid); }, - _getChildren: function BStore_getChildren(guid, depthIndex, items) { - if (typeof(items) == "undefined") - items = {}; + _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 node = this._getNode(this._getItemIdForGUID(guid)); @@ -573,35 +562,18 @@ BookmarksStore.prototype = { !this._ls.isLivemark(node.itemId)) { node.QueryInterface(Ci.nsINavHistoryQueryResultNode); node.containerOpen = true; + + // Remember all the children GUIDs and recursively get more for (var i = 0; i < node.childCount; i++) { let child = node.getChild(i); - let foo = this._createMiniRecord(child.itemId, true); - items[foo.id] = foo; - this._getChildren(child, depthIndex, items); + items[this._bms.getItemGUID(child.itemId)] = true; + this._getChildren(child, items); } } return items; }, - _getSiblings: function BStore__getSiblings(guid, depthIndex, items) { - if (typeof(items) == "undefined") - items = {}; - - let parentid = this._bms.getFolderIdForItem(this._getItemIdForGUID(guid)); - let parent = this._getNode(parentid); - parent.QueryInterface(Ci.nsINavHistoryQueryResultNode); - parent.containerOpen = true; - - for (var i = 0; i < parent.childCount; i++) { - let child = parent.getChild(i); - let foo = this._createMiniRecord(child.itemId, true); - items[foo.id] = foo; - } - - return items; - }, - _tagURI: function BStore_tagURI(bmkURI, tags) { // Filter out any null/undefined/empty tags tags = tags.filter(function(t) t); @@ -618,13 +590,7 @@ BookmarksStore.prototype = { let items = {}; for (let [weaveId, id] in Iterator(kSpecialIds)) if (weaveId != "places" && weaveId != "tags") - this._getChildren(weaveId, true, items); - return items; - }, - - createMetaRecords: function BStore_createMetaRecords(guid, items) { - this._getChildren(guid, true, items); - this._getSiblings(guid, true, items); + this._getChildren(weaveId, items); return items; }, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index e476aac4a858..9b98dc556631 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -104,13 +104,6 @@ Store.prototype = { throw "override createRecord in a subclass"; }, - // override if depth and/or depthindex are used - // should return all objects potentially affected by change of guid, in - // short form (no full content, only id, depth, sortindex) - createMetaRecords: function Store_createMetaRecords(guid, items) { - return {}; - }, - changeItemID: function Store_changeItemID(oldID, newID) { throw "override changeItemID in a subclass"; }, From 30a156bda6d54cbefce2902e64e1df97622ff44c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 15 Aug 2009 00:56:27 -0700 Subject: [PATCH 1266/1860] Get rid of depth and sort on index instead. --- .../sync/modules/base_records/collection.js | 1 - services/sync/modules/base_records/crypto.js | 2 +- services/sync/modules/base_records/wbo.js | 12 +++--------- services/sync/modules/engines.js | 4 +--- services/sync/modules/engines/bookmarks.js | 18 ------------------ 5 files changed, 5 insertions(+), 32 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 8892b1976a6d..1e0aea57d5fc 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -110,7 +110,6 @@ Collection.prototype = { // oldest (oldest first) // newest (newest first) // index - // depthindex get sort() { return this._sort; }, set sort(value) { this._sort = value; diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 4ffd9297021c..804772556970 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -106,7 +106,7 @@ CryptoWrapper.prototype = { toString: function CryptoWrap_toString() "{ " + [ "id: " + this.id, "parent: " + this.parentid, - "depth: " + this.depth + ", index: " + this.sortindex, + "index: " + this.sortindex, "modified: " + this.modified, "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.cleartext)) ].join("\n ") + " }", diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 07cb3d90427d..14d1193a4ee8 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -79,12 +79,6 @@ WBORecord.prototype = { return this.data.modified; }, - get depth() { - if (this.data.depth) - return this.data.depth; - return 0; - }, - get sortindex() { if (this.data.sortindex) return this.data.sortindex; @@ -117,14 +111,14 @@ WBORecord.prototype = { toString: function WBORec_toString() "{ " + [ "id: " + this.id, "parent: " + this.parentid, - "depth: " + this.depth + ", index: " + this.sortindex, + "index: " + this.sortindex, "modified: " + this.modified, "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.payload)) ].join("\n ") + " }", }; -Utils.deferGetSet(WBORecord, "data", ["id", "parentid", "modified", "depth", - "sortindex", "payload"]); +Utils.deferGetSet(WBORecord, "data", ["id", "parentid", "modified", "sortindex", + "payload"]); Utils.lazy(this, 'Records', RecordManager); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 4d6c560583d2..7d5ba77dccec 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -264,8 +264,6 @@ SyncEngine.prototype = { _recordLike: function SyncEngine__recordLike(a, b) { if (a.parentid != b.parentid) return false; - if (a.depth != b.depth) - return false; // note: sortindex ignored if (a.deleted || b.deleted) return false; @@ -319,7 +317,7 @@ SyncEngine.prototype = { let newitems = new Collection(this.engineURL, this._recordObj); newitems.newer = this.lastSync; newitems.full = true; - newitems.sort = "depthindex"; + newitems.sort = "index"; let count = {applied: 0, reconciled: 0}; this._lastSyncTmp = 0; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 4dd97e3d886b..c7e4868d15f0 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -154,16 +154,6 @@ BookmarksStore.prototype = { return this._bms.getItemGUID(placeId); }, - _isToplevel: function BStore__isToplevel(placeId) { - for (let [weaveId, id] in Iterator(kSpecialIds)) - if (placeId == id) - return true; - - if (this._bms.getFolderIdForItem(placeId) <= 0) - return true; - return false; - }, - itemExists: function BStore_itemExists(id) { return this._getItemIdForGUID(id) > 0; }, @@ -416,13 +406,6 @@ BookmarksStore.prototype = { return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root; }, - // XXX a little inefficient - isToplevel calls getFolderIdForItem too - _itemDepth: function BStore__itemDepth(id) { - if (this._isToplevel(id)) - return 0; - return this._itemDepth(this._bms.getFolderIdForItem(id)) + 1; - }, - _getTags: function BStore__getTags(uri) { try { if (typeof(uri) == "string") @@ -535,7 +518,6 @@ BookmarksStore.prototype = { record.id = guid; record.parentid = this._getWeaveParentIdForItem(placeId); - record.depth = this._itemDepth(placeId); record.sortindex = this._bms.getItemIndex(placeId); record.encryption = cryptoMetaURL; From 7b23b20e566ce853841a558aaed4b9dcfc10de8e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 15 Aug 2009 00:57:09 -0700 Subject: [PATCH 1267/1860] Temporarily append everything and don't use sortindex for ordering within a folder. --- services/sync/modules/engines/bookmarks.js | 29 ++++++++-------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c7e4868d15f0..6ef573f7a40f 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -211,10 +211,8 @@ BookmarksStore.prototype = { let uri = Utils.makeURI(record.bmkUri); this._log.debug(" -> -> ParentID is " + parentId); this._log.debug(" -> -> uri is " + record.bmkUri); - this._log.debug(" -> -> sortindex is " + record.sortindex); this._log.debug(" -> -> title is " + record.title); - newId = this._bms.insertBookmark(parentId, uri, record.sortindex, - record.title); + newId = this._bms.insertBookmark(parentId, uri, -1, record.title); this._tagURI(uri, record.tags); this._bms.setKeywordForBookmark(newId, record.keyword); if (record.description) @@ -240,21 +238,16 @@ BookmarksStore.prototype = { } break; case "folder": this._log.debug(" -> creating folder \"" + record.title + "\""); - newId = this._bms.createFolder(parentId, - record.title, - record.sortindex); + newId = this._bms.createFolder(parentId, record.title, -1); break; case "livemark": this._log.debug(" -> creating livemark \"" + record.title + "\""); - newId = this._ls.createLivemark(parentId, - record.title, - Utils.makeURI(record.siteUri), - Utils.makeURI(record.feedUri), - record.sortindex); + newId = this._ls.createLivemark(parentId, record.title, + Utils.makeURI(record.siteUri), Utils.makeURI(record.feedUri), -1); break; case "separator": this._log.debug(" -> creating separator"); - newId = this._bms.insertSeparator(parentId, record.sortindex); + newId = this._bms.insertSeparator(parentId, -1); break; case "item": this._log.debug(" -> got a generic places item.. do nothing?"); @@ -324,12 +317,11 @@ BookmarksStore.prototype = { this._log.trace("Updating " + record.id + " (" + itemId + ")"); - if ((this._bms.getItemIndex(itemId) != record.sortindex) || - (this._bms.getFolderIdForItem(itemId) != - this._getItemIdForGUID(record.parentid))) { - this._log.trace("Moving item (changing folder/index)"); - let parentid = this._getItemIdForGUID(record.parentid); - this._bms.moveItem(itemId, parentid, record.sortindex); + // FIXME check for predecessor changes + let parentid = this._getItemIdForGUID(record.parentid); + if (this._bms.getFolderIdForItem(itemId) != parentid) { + this._log.trace("Moving item (changing folder/position)"); + this._bms.moveItem(itemId, parentid, -1); } for (let [key, val] in Iterator(record.cleartext)) { @@ -518,7 +510,6 @@ BookmarksStore.prototype = { record.id = guid; record.parentid = this._getWeaveParentIdForItem(placeId); - record.sortindex = this._bms.getItemIndex(placeId); record.encryption = cryptoMetaURL; this.cache.put(guid, record); From 77282066138ae47e5acb59a5b9a17eb3689f8d6d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 15 Aug 2009 00:59:49 -0700 Subject: [PATCH 1268/1860] Rename weaveId to just GUID and share a id<->guid function across store and tracker. --- services/sync/modules/engines/bookmarks.js | 77 +++++++++++----------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 6ef573f7a40f..c985dd9fdfc1 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -62,10 +62,23 @@ let kSpecialIds = {}; ["tags", "tagsFolder"], ["toolbar", "toolbarFolder"], ["unfiled", "unfiledBookmarksFolder"], -].forEach(function([weaveId, placeName]) { - Utils.lazy2(kSpecialIds, weaveId, function() Svc.Bookmark[placeName]); +].forEach(function([guid, placeName]) { + Utils.lazy2(kSpecialIds, guid, function() Svc.Bookmark[placeName]); }); +// Create some helper functions to convert GUID/ids +function idForGUID(guid) { + if (guid in kSpecialIds) + return kSpecialIds[guid]; + return Svc.Bookmark.getItemIdForGUID(guid); +} +function GUIDForId(placeId) { + for (let [guid, id] in Iterator(kSpecialIds)) + if (placeId == id) + return guid; + return Svc.Bookmark.getItemGUID(placeId); +} + function BookmarksEngine() { this._init(); } @@ -139,23 +152,9 @@ BookmarksStore.prototype = { return this.__ts; }, - _getItemIdForGUID: function BStore__getItemIdForGUID(GUID) { - if (GUID in kSpecialIds) - return kSpecialIds[GUID]; - - return this._bms.getItemIdForGUID(GUID); - }, - - _getWeaveIdForItem: function BStore__getWeaveIdForItem(placeId) { - for (let [weaveId, id] in Iterator(kSpecialIds)) - if (placeId == id) - return weaveId; - - return this._bms.getItemGUID(placeId); - }, itemExists: function BStore_itemExists(id) { - return this._getItemIdForGUID(id) > 0; + return idForGUID(id) > 0; }, _preprocess: function BStore_preprocess(record) { @@ -196,7 +195,7 @@ BookmarksStore.prototype = { this._preprocess(record); let newId; - let parentId = this._getItemIdForGUID(record.parentid); + let parentId = idForGUID(record.parentid); if (parentId <= 0) { this._log.warn("Creating node with unknown parent -> reparenting to root"); @@ -258,7 +257,7 @@ BookmarksStore.prototype = { } if (newId) { this._log.trace("Setting GUID of new item " + newId + " to " + record.id); - let cur = this._bms.getItemGUID(newId); + let cur = GUIDForId(newId); if (cur == record.id) this._log.warn("Item " + newId + " already has GUID " + record.id); else @@ -273,7 +272,7 @@ BookmarksStore.prototype = { return; } - var itemId = this._bms.getItemIdForGUID(record.id); + let itemId = idForGUID(record.id); if (itemId <= 0) { this._log.debug("Item " + record.id + " already removed"); return; @@ -304,7 +303,7 @@ BookmarksStore.prototype = { // Modify the record if necessary this._preprocess(record); - let itemId = this._getItemIdForGUID(record.id); + let itemId = idForGUID(record.id); if (record.id in kSpecialIds) { this._log.debug("Skipping update for root node."); @@ -318,7 +317,7 @@ BookmarksStore.prototype = { this._log.trace("Updating " + record.id + " (" + itemId + ")"); // FIXME check for predecessor changes - let parentid = this._getItemIdForGUID(record.parentid); + let parentid = idForGUID(record.parentid); if (this._bms.getFolderIdForItem(itemId) != parentid) { this._log.trace("Moving item (changing folder/position)"); this._bms.moveItem(itemId, parentid, -1); @@ -372,7 +371,7 @@ BookmarksStore.prototype = { }, changeItemID: function BStore_changeItemID(oldID, newID) { - var itemId = this._getItemIdForGUID(oldID); + let itemId = idForGUID(oldID); if (itemId == null) // toplevel folder return; if (itemId <= 0) { @@ -381,7 +380,7 @@ BookmarksStore.prototype = { return; } - var collision = this._getItemIdForGUID(newID); + let collision = idForGUID(newID); if (collision > 0) { this._log.warn("Can't change GUID " + oldID + " to " + newID + ": new ID already in use"); @@ -434,7 +433,7 @@ BookmarksStore.prototype = { if (record) return record; - let placeId = this._bms.getItemIdForGUID(guid); + let placeId = idForGUID(guid); if (placeId <= 0) { // deleted item record = new PlacesItem(); record.id = guid; @@ -509,27 +508,27 @@ BookmarksStore.prototype = { } record.id = guid; - record.parentid = this._getWeaveParentIdForItem(placeId); + record.parentid = this._getParentGUIDForItemId(placeId); record.encryption = cryptoMetaURL; this.cache.put(guid, record); return record; }, - _getWeaveParentIdForItem: function BStore__getWeaveParentIdForItem(itemId) { + _getParentGUIDForItemId: function BStore__getParentGUIDForItemId(itemId) { let parentid = this._bms.getFolderIdForItem(itemId); if (parentid == -1) { this._log.debug("Found orphan bookmark, reparenting to unfiled"); parentid = this._bms.unfiledBookmarksFolder; this._bms.moveItem(itemId, parentid, -1); } - return this._getWeaveIdForItem(parentid); + return GUIDForId(parentid); }, _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 - node = this._getNode(this._getItemIdForGUID(guid)); + node = this._getNode(idForGUID(guid)); if (node.type == node.RESULT_TYPE_FOLDER && !this._ls.isLivemark(node.itemId)) { @@ -539,7 +538,7 @@ BookmarksStore.prototype = { // Remember all the children GUIDs and recursively get more for (var i = 0; i < node.childCount; i++) { let child = node.getChild(i); - items[this._bms.getItemGUID(child.itemId)] = true; + items[GUIDForId(child.itemId)] = true; this._getChildren(child, items); } } @@ -561,15 +560,15 @@ BookmarksStore.prototype = { getAllIDs: function BStore_getAllIDs() { let items = {}; - for (let [weaveId, id] in Iterator(kSpecialIds)) - if (weaveId != "places" && weaveId != "tags") - this._getChildren(weaveId, items); + for (let [guid, id] in Iterator(kSpecialIds)) + if (guid != "places" && guid != "tags") + this._getChildren(guid, items); return items; }, wipe: function BStore_wipe() { - for (let [weaveId, id] in Iterator(kSpecialIds)) - if (weaveId != "places") + for (let [guid, id] in Iterator(kSpecialIds)) + if (guid != "places") this._bms.removeFolderChildren(id); } }; @@ -606,8 +605,8 @@ BookmarksTracker.prototype = { this.__proto__.__proto__._init.call(this); // Ignore changes to the special roots - for (let [weaveId, id] in Iterator(kSpecialIds)) - this.ignoreID(this._bms.getItemGUID(id)); + for (let guid in kSpecialIds) + this.ignoreID(guid); this._bms.addObserver(this, false); }, @@ -619,7 +618,7 @@ BookmarksTracker.prototype = { * Places internal id of the bookmark to upload */ _addId: function BMT__addId(itemId) { - if (this.addChangedID(this._bms.getItemGUID(itemId))) + if (this.addChangedID(GUIDForId(itemId))) this._upScore(); }, @@ -646,7 +645,7 @@ BookmarksTracker.prototype = { if (folder == null) folder = this._bms.getFolderIdForItem(itemId); - let tags = this._bms.tagsFolder; + let tags = kSpecialIds.tags; // Ignore changes to tags (folders under the tags folder) if (folder == tags) return true; From b7452791df59550d94e685f8c0575a4712e10a48 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 15 Aug 2009 01:00:32 -0700 Subject: [PATCH 1269/1860] Move _preprocess to before applyIncoming to share code for create/update/remove. --- services/sync/modules/engines/bookmarks.js | 28 +++++++++------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c985dd9fdfc1..be30831ac3f2 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -157,7 +157,14 @@ BookmarksStore.prototype = { return idForGUID(id) > 0; }, - _preprocess: function BStore_preprocess(record) { + applyIncoming: function BStore_applyIncoming(record) { + // Ignore (accidental?) root changes + if (record.id in kSpecialIds) { + this._log.debug("Skipping change to root node: " + record.id); + return; + } + + // Preprocess the record before doing the normal apply switch (record.type) { case "query": { // Convert the query uri if necessary @@ -188,12 +195,12 @@ BookmarksStore.prototype = { break; } } + + // Do the normal processing of incoming records + Store.prototype.applyIncoming.apply(this, arguments); }, create: function BStore_create(record) { - // Modify the record if necessary - this._preprocess(record); - let newId; let parentId = idForGUID(record.parentid); @@ -266,12 +273,6 @@ BookmarksStore.prototype = { }, remove: function BStore_remove(record) { - if (record.id in kSpecialIds) { - this._log.warn("Attempted to remove root node (" + record.id + - "). Skipping record removal."); - return; - } - let itemId = idForGUID(record.id); if (itemId <= 0) { this._log.debug("Item " + record.id + " already removed"); @@ -300,15 +301,8 @@ BookmarksStore.prototype = { }, update: function BStore_update(record) { - // Modify the record if necessary - this._preprocess(record); - let itemId = idForGUID(record.id); - if (record.id in kSpecialIds) { - this._log.debug("Skipping update for root node."); - return; - } if (itemId <= 0) { this._log.debug("Skipping update for unknown item: " + record.id); return; From bd7b2c60ff0f7731009d16c6b0c2266b3c4a1956 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 15 Aug 2009 01:00:46 -0700 Subject: [PATCH 1270/1860] Store the parent GUID as an annotation if the item is missing a parent and share some parent lookup code. --- services/sync/modules/engines/bookmarks.js | 49 +++++++++++++++------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index be30831ac3f2..56246399bb1a 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -42,6 +42,7 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +const PARENT_ANNO = "weave/parent"; const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -196,29 +197,48 @@ BookmarksStore.prototype = { } } + // Figure out the local id of the parent GUID if available + let parentGUID = record.parentid; + record._orphan = false; + if (parentGUID != null) { + let parentId = idForGUID(parentGUID); + + // Default to unfiled if we don't have the parent yet + if (parentId <= 0) { + this._log.trace("Reparenting to unfiled until parent is synced"); + record._orphan = true; + parentId = kSpecialIds.unfiled; + } + + // Save the parent id for modifying the bookmark later + record._parent = parentId; + } + // Do the normal processing of incoming records Store.prototype.applyIncoming.apply(this, arguments); + + // Do some post-processing if we have an item + let itemId = idForGUID(record.id); + if (itemId > 0) { + // Create an annotation to remember that it needs a parent + // XXX Work around Bug 510628 by prepending parenT + if (record._orphan) + Utils.anno(itemId, PARENT_ANNO, "T" + parentGUID); + } }, create: function BStore_create(record) { let newId; - let parentId = idForGUID(record.parentid); - - if (parentId <= 0) { - this._log.warn("Creating node with unknown parent -> reparenting to root"); - parentId = this._bms.bookmarksMenuFolder; - } - switch (record.type) { case "bookmark": case "query": case "microsummary": { this._log.debug(" -> creating bookmark \"" + record.title + "\""); let uri = Utils.makeURI(record.bmkUri); - this._log.debug(" -> -> ParentID is " + parentId); + this._log.debug(" -> -> ParentID is " + record._parent); this._log.debug(" -> -> uri is " + record.bmkUri); this._log.debug(" -> -> title is " + record.title); - newId = this._bms.insertBookmark(parentId, uri, -1, record.title); + newId = this._bms.insertBookmark(record._parent, uri, -1, record.title); this._tagURI(uri, record.tags); this._bms.setKeywordForBookmark(newId, record.keyword); if (record.description) @@ -244,16 +264,16 @@ BookmarksStore.prototype = { } break; case "folder": this._log.debug(" -> creating folder \"" + record.title + "\""); - newId = this._bms.createFolder(parentId, record.title, -1); + newId = this._bms.createFolder(record._parent, record.title, -1); break; case "livemark": this._log.debug(" -> creating livemark \"" + record.title + "\""); - newId = this._ls.createLivemark(parentId, record.title, + newId = this._ls.createLivemark(record._parent, record.title, Utils.makeURI(record.siteUri), Utils.makeURI(record.feedUri), -1); break; case "separator": this._log.debug(" -> creating separator"); - newId = this._bms.insertSeparator(parentId, -1); + newId = this._bms.insertSeparator(record._parent, -1); break; case "item": this._log.debug(" -> got a generic places item.. do nothing?"); @@ -311,10 +331,9 @@ BookmarksStore.prototype = { this._log.trace("Updating " + record.id + " (" + itemId + ")"); // FIXME check for predecessor changes - let parentid = idForGUID(record.parentid); - if (this._bms.getFolderIdForItem(itemId) != parentid) { + if (this._bms.getFolderIdForItem(itemId) != record._parent) { this._log.trace("Moving item (changing folder/position)"); - this._bms.moveItem(itemId, parentid, -1); + this._bms.moveItem(itemId, record._parent, -1); } for (let [key, val] in Iterator(record.cleartext)) { From 8a00864b3bf6093d3a9213df4df822ac6b767e4f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 15 Aug 2009 01:04:06 -0700 Subject: [PATCH 1271/1860] Use a shared setGUID for new items and changing guids so that the item always ends up with the GUID (because conflicts shouldn't have been Weave generated). --- services/sync/modules/engines/bookmarks.js | 33 ++++++++++------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 56246399bb1a..de74e00123b0 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -277,19 +277,14 @@ BookmarksStore.prototype = { break; case "item": this._log.debug(" -> got a generic places item.. do nothing?"); - break; + return; default: this._log.error("_create: Unknown item type: " + record.type); - break; - } - if (newId) { - this._log.trace("Setting GUID of new item " + newId + " to " + record.id); - let cur = GUIDForId(newId); - if (cur == record.id) - this._log.warn("Item " + newId + " already has GUID " + record.id); - else - this._bms.setItemGUID(newId, record.id); + return; } + + this._log.trace("Setting GUID of new item " + newId + " to " + record.id); + this._setGUID(newId, record.id); }, remove: function BStore_remove(record) { @@ -393,15 +388,17 @@ BookmarksStore.prototype = { return; } - let collision = idForGUID(newID); - if (collision > 0) { - this._log.warn("Can't change GUID " + oldID + " to " + - newID + ": new ID already in use"); - return; - } - this._log.debug("Changing GUID " + oldID + " to " + newID); - this._bms.setItemGUID(itemId, newID); + this._setGUID(itemId, newID); + }, + + _setGUID: function BStore__setGUID(itemId, guid) { + let collision = idForGUID(guid); + if (collision != -1) { + this._log.warn("Freeing up GUID " + guid + " used by " + collision); + Svc.Annos.removeItemAnnotation(collision, "placesInternal/GUID"); + } + Svc.Bookmark.setItemGUID(itemId, guid); }, _getNode: function BStore__getNode(folder) { From e413f017c0a38d6a26fccff178dc9b105fc61165 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 15 Aug 2009 01:07:40 -0700 Subject: [PATCH 1272/1860] Reparent orphans when creating the parent folder. --- services/sync/modules/engines/bookmarks.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index de74e00123b0..dc03360a17d7 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -227,6 +227,18 @@ BookmarksStore.prototype = { } }, + /** + * Find all ids of items that have a given value for an annotation + */ + _findAnnoItems: function BStore__findAnnoItems(anno, val) { + // XXX Work around Bug 510628 by prepending parenT + if (anno == PARENT_ANNO) + val = "T" + val; + + return Svc.Annos.getItemsWithAnnotation(anno, {}).filter(function(id) + Utils.anno(id, anno) == val); + }, + create: function BStore_create(record) { let newId; switch (record.type) { @@ -285,6 +297,15 @@ BookmarksStore.prototype = { this._log.trace("Setting GUID of new item " + newId + " to " + record.id); this._setGUID(newId, record.id); + + // Find orphans and reunite with this new folder parent + if (record.type == "folder") { + let orphans = this._findAnnoItems(PARENT_ANNO, record.id); + this._log.debug("Reparenting orphans " + orphans + " to " + record.title); + orphans.forEach(function(orphan) { + Svc.Bookmark.moveItem(orphan, newId, -1); + }); + } }, remove: function BStore_remove(record) { From 781df5d65b362779bcfc1543f1d23b8d92ece697 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sat, 15 Aug 2009 14:43:10 -0700 Subject: [PATCH 1273/1860] Remove the missing parent annotation after reparenting an orphan. --- services/sync/modules/engines/bookmarks.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index dc03360a17d7..493ba0c230e6 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -303,7 +303,9 @@ BookmarksStore.prototype = { let orphans = this._findAnnoItems(PARENT_ANNO, record.id); this._log.debug("Reparenting orphans " + orphans + " to " + record.title); orphans.forEach(function(orphan) { + // Move the orphan to the parent and drop the missing parent annotation Svc.Bookmark.moveItem(orphan, newId, -1); + Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO); }); } }, From f7ecaa48f84d6f211d7a9f795978914931062345 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sun, 16 Aug 2009 12:39:00 -0700 Subject: [PATCH 1274/1860] Set the predecessorid when creating records for upload. --- services/sync/modules/engines/bookmarks.js | 16 ++++++++++++++-- services/sync/modules/type_records/bookmark.js | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 493ba0c230e6..5a28cfed7f7b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -541,14 +541,15 @@ BookmarksStore.prototype = { } record.id = guid; - record.parentid = this._getParentGUIDForItemId(placeId); + record.parentid = this._getParentGUIDForId(placeId); + record.predecessorid = this._getPredecessorGUIDForId(placeId); record.encryption = cryptoMetaURL; this.cache.put(guid, record); return record; }, - _getParentGUIDForItemId: function BStore__getParentGUIDForItemId(itemId) { + _getParentGUIDForId: function BStore__getParentGUIDForId(itemId) { let parentid = this._bms.getFolderIdForItem(itemId); if (parentid == -1) { this._log.debug("Found orphan bookmark, reparenting to unfiled"); @@ -558,6 +559,17 @@ BookmarksStore.prototype = { return GUIDForId(parentid); }, + _getPredecessorGUIDForId: function BStore__getPredecessorGUIDForId(itemId) { + // Figure out the predecessor, unless it's the first item + let itemPos = Svc.Bookmark.getItemIndex(itemId); + if (itemPos == 0) + return; + + let parentId = Svc.Bookmark.getFolderIdForItem(itemId); + let predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos - 1); + return GUIDForId(predecessorId); + }, + _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 diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 767713598053..f16909398f7b 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -93,7 +93,7 @@ PlacesItem.prototype = { }, }; -Utils.deferGetSet(PlacesItem, "cleartext", "type"); +Utils.deferGetSet(PlacesItem, "cleartext", ["predecessorid", "type"]); function Bookmark(uri) { this._Bookmark_init(uri); From f1c0b8f9995bd758e82a9deac5bb7a12bca72e2d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sun, 16 Aug 2009 12:39:15 -0700 Subject: [PATCH 1275/1860] Use the predecessorid to figure out where to put the item or save the predecessor as an annotation. --- services/sync/modules/engines/bookmarks.js | 43 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5a28cfed7f7b..959956154757 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -43,6 +43,7 @@ const Ci = Components.interfaces; const Cu = Components.utils; const PARENT_ANNO = "weave/parent"; +const PREDECESSOR_ANNO = "weave/predecessor"; const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -214,6 +215,23 @@ BookmarksStore.prototype = { record._parent = parentId; } + // Default to append unless we're not an orphan with the predecessor + let predGUID = record.predecessorid; + record._insertPos = Svc.Bookmark.DEFAULT_INDEX; + if (!record._orphan) { + // No predecessor means it's the first item + if (predGUID == null) + record._insertPos = 0; + else { + // The insert position is one after the predecessor if we have it + let predId = idForGUID(predGUID); + if (predId != -1) + record._insertPos = Svc.Bookmark.getItemIndex(predId) + 1; + else + this._log.trace("Appending to end until predecessor is synced"); + } + } + // Do the normal processing of incoming records Store.prototype.applyIncoming.apply(this, arguments); @@ -224,6 +242,11 @@ BookmarksStore.prototype = { // XXX Work around Bug 510628 by prepending parenT if (record._orphan) Utils.anno(itemId, PARENT_ANNO, "T" + parentGUID); + + // Create an annotation if we have a predecessor but no position + // XXX Work around Bug 510628 by prepending predecessoR + if (predGUID != null && record._insertPos == Svc.Bookmark.DEFAULT_INDEX) + Utils.anno(itemId, PREDECESSOR_ANNO, "R" + predGUID); } }, @@ -250,7 +273,8 @@ BookmarksStore.prototype = { this._log.debug(" -> -> ParentID is " + record._parent); this._log.debug(" -> -> uri is " + record.bmkUri); this._log.debug(" -> -> title is " + record.title); - newId = this._bms.insertBookmark(record._parent, uri, -1, record.title); + newId = this._bms.insertBookmark(record._parent, uri, record._insertPos, + record.title); this._tagURI(uri, record.tags); this._bms.setKeywordForBookmark(newId, record.keyword); if (record.description) @@ -276,16 +300,18 @@ BookmarksStore.prototype = { } break; case "folder": this._log.debug(" -> creating folder \"" + record.title + "\""); - newId = this._bms.createFolder(record._parent, record.title, -1); + newId = this._bms.createFolder(record._parent, record.title, + record._insertPos); break; case "livemark": this._log.debug(" -> creating livemark \"" + record.title + "\""); newId = this._ls.createLivemark(record._parent, record.title, - Utils.makeURI(record.siteUri), Utils.makeURI(record.feedUri), -1); + Utils.makeURI(record.siteUri), Utils.makeURI(record.feedUri), + record._insertPos); break; case "separator": this._log.debug(" -> creating separator"); - newId = this._bms.insertSeparator(record._parent, -1); + newId = this._bms.insertSeparator(record._parent, record._insertPos); break; case "item": this._log.debug(" -> got a generic places item.. do nothing?"); @@ -304,7 +330,7 @@ BookmarksStore.prototype = { this._log.debug("Reparenting orphans " + orphans + " to " + record.title); orphans.forEach(function(orphan) { // Move the orphan to the parent and drop the missing parent annotation - Svc.Bookmark.moveItem(orphan, newId, -1); + Svc.Bookmark.moveItem(orphan, newId, Svc.Bookmark.DEFAULT_INDEX); Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO); }); } @@ -348,10 +374,11 @@ BookmarksStore.prototype = { this._log.trace("Updating " + record.id + " (" + itemId + ")"); - // FIXME check for predecessor changes - if (this._bms.getFolderIdForItem(itemId) != record._parent) { + // Need to move if the parent or position isn't correct + if (Svc.Bookmark.getFolderIdForItem(itemId) != record._parent || + Svc.Bookmark.getItemIndex(itemId) != record._insertPos) { this._log.trace("Moving item (changing folder/position)"); - this._bms.moveItem(itemId, record._parent, -1); + this._bms.moveItem(itemId, record._parent, record._insertPos); } for (let [key, val] in Iterator(record.cleartext)) { From 0044d4ed361da3c82d274b545dbceb970ceb28e8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sun, 16 Aug 2009 12:39:23 -0700 Subject: [PATCH 1276/1860] Print out the stack when failing to apply an incoming record. --- services/sync/modules/engines.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 7d5ba77dccec..c0634a6569ca 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -441,8 +441,7 @@ SyncEngine.prototype = { if (this._lastSyncTmp < item.modified) this._lastSyncTmp = item.modified; } catch (e) { - this._log.warn("Error while applying incoming record: " + - (e.message? e.message : e)); + this._log.warn("Error while applying record: " + Utils.stackTrace(e)); } finally { this._tracker.ignoreAll = false; } From 385bf81537f1cc9d13c6b355f487a84c6096fad7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sun, 16 Aug 2009 12:39:30 -0700 Subject: [PATCH 1277/1860] Simplify the debug output when creating bookmarks and include the parent and position. --- services/sync/modules/engines/bookmarks.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 959956154757..2db42620c688 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -268,13 +268,12 @@ BookmarksStore.prototype = { case "bookmark": case "query": case "microsummary": { - this._log.debug(" -> creating bookmark \"" + record.title + "\""); let uri = Utils.makeURI(record.bmkUri); - this._log.debug(" -> -> ParentID is " + record._parent); - this._log.debug(" -> -> uri is " + record.bmkUri); - this._log.debug(" -> -> title is " + record.title); newId = this._bms.insertBookmark(record._parent, uri, record._insertPos, record.title); + this._log.debug(["created bookmark", newId, "under", record._parent, "at", + record._insertPos, "as", record.title, record.bmkUri].join(" ")); + this._tagURI(uri, record.tags); this._bms.setKeywordForBookmark(newId, record.keyword); if (record.description) @@ -299,19 +298,23 @@ BookmarksStore.prototype = { } } break; case "folder": - this._log.debug(" -> creating folder \"" + record.title + "\""); newId = this._bms.createFolder(record._parent, record.title, record._insertPos); + this._log.debug(["created folder", newId, "under", record._parent, "at", + record._insertPos, "as", record.title].join(" ")); break; case "livemark": - this._log.debug(" -> creating livemark \"" + record.title + "\""); newId = this._ls.createLivemark(record._parent, record.title, Utils.makeURI(record.siteUri), Utils.makeURI(record.feedUri), record._insertPos); + this._log.debug(["created livemark", newId, "under", record._parent, "at", + record._insertPos, "as", record.title, record.siteUri, record.feedUri]. + join(" ")); break; case "separator": - this._log.debug(" -> creating separator"); newId = this._bms.insertSeparator(record._parent, record._insertPos); + this._log.debug(["created separator", newId, "under", record._parent, + "at", record._insertPos].join(" ")); break; case "item": this._log.debug(" -> got a generic places item.. do nothing?"); From 8e76c3d11961b3a2a69102879fd37433b26a6806 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 17 Aug 2009 02:34:03 -0700 Subject: [PATCH 1278/1860] For items that have the correct parent, see if there's an item following it and reposition the chain of items to after the correct parented item. A chain of followers starts at the item with the predecessor annotation and goes until the end of the folder, an item that is looking for a predecessor, or ends up at where we started (the predecessor). --- services/sync/modules/engines/bookmarks.js | 62 +++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 2db42620c688..42046b860c41 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -257,6 +257,9 @@ BookmarksStore.prototype = { // XXX Work around Bug 510628 by prepending parenT if (anno == PARENT_ANNO) val = "T" + val; + // XXX Work around Bug 510628 by prepending predecessoR + else if (anno == PREDECESSOR_ANNO) + val = "R" + val; return Svc.Annos.getItemsWithAnnotation(anno, {}).filter(function(id) Utils.anno(id, anno) == val); @@ -327,16 +330,73 @@ BookmarksStore.prototype = { this._log.trace("Setting GUID of new item " + newId + " to " + record.id); this._setGUID(newId, record.id); + // Remember which guids now have the correct parent + let parented = []; + if (!record._orphan) + parented.push(newId); + // Find orphans and reunite with this new folder parent if (record.type == "folder") { let orphans = this._findAnnoItems(PARENT_ANNO, record.id); this._log.debug("Reparenting orphans " + orphans + " to " + record.title); orphans.forEach(function(orphan) { + // Append the orphan under the parent unless it's supposed to be first + let insertPos = Svc.Bookmark.DEFAULT_INDEX; + if (!Svc.Annos.itemHasAnnotation(orphan, PREDECESSOR_ANNO)) + insertPos = 0; + // Move the orphan to the parent and drop the missing parent annotation - Svc.Bookmark.moveItem(orphan, newId, Svc.Bookmark.DEFAULT_INDEX); + Svc.Bookmark.moveItem(orphan, newId, insertPos); Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO); + parented.push(orphan); }); } + + // Reposition all chains of siblings for the now correctly parented items + parented.forEach(function(predId) { + let predGUID = GUIDForId(predId); + let followers = this._findAnnoItems(PREDECESSOR_ANNO, predGUID); + if (followers.length > 1) + this._log.warn(predId + " has more than one followers: " + followers); + + // Start at the first follower and move the chain of followers + let parent = Svc.Bookmark.getFolderIdForItem(predId); + followers.forEach(function(follow) { + this._log.debug("Repositioning " + follow + " behind " + predId); + if (Svc.Bookmark.getFolderIdForItem(follow) != parent) { + this._log.warn("Follower doesn't have the same parent: " + parent); + return; + } + + // Remove the annotation now that we're putting it in the right spot + Svc.Annos.removeItemAnnotation(follow, PREDECESSOR_ANNO); + + // Process each item in the chain until we loop back to the start + let insertAfter = predId; + do { + // Figure out what's next in the chain + let followPos = Svc.Bookmark.getItemIndex(follow); + let nextFollow = Svc.Bookmark.getIdForItemAt(parent, followPos + 1); + + let insertPos = Svc.Bookmark.getItemIndex(insertAfter) + 1; + Svc.Bookmark.moveItem(follow, parent, insertPos); + this._log.trace(["Moved", follow, "to", insertPos, "after", + insertAfter, "with", nextFollow, "next"].join(" ")); + + // Move to the next item and check some stop conditions + insertAfter = follow; + follow = nextFollow; + + // Stop if we ran off the end of the folder + if (follow == -1) + break; + + // We're at the end of a chain if the item is looking for its pred. + if (Svc.Annos.itemHasAnnotation(follow, PREDECESSOR_ANNO)) + break; + } while (follow != predId); + }, this); + }, this); }, remove: function BStore_remove(record) { From 285b08828d38c98bcaeeb2802b733f62e60f47ca Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 18 Aug 2009 18:36:31 -0700 Subject: [PATCH 1279/1860] When updating the position of an item, move all of its followers with it. Share chain moving logic used for fixing followers on create and updating positions. --- services/sync/modules/engines/bookmarks.js | 78 +++++++++++++--------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 42046b860c41..26420e60d134 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -223,10 +223,12 @@ BookmarksStore.prototype = { if (predGUID == null) record._insertPos = 0; else { - // The insert position is one after the predecessor if we have it + // The insert position is one after the predecessor of the same parent let predId = idForGUID(predGUID); - if (predId != -1) + if (predId != -1 && this._getParentGUIDForId(predId) == parentGUID) { record._insertPos = Svc.Bookmark.getItemIndex(predId) + 1; + record._predId = predId; + } else this._log.trace("Appending to end until predecessor is synced"); } @@ -265,6 +267,32 @@ BookmarksStore.prototype = { Utils.anno(id, anno) == val); }, + /** + * Move an item and all of its followers to a new position until reaching an + * item that shouldn't be moved + */ + _moveItemChain: function BStore__moveItemChain(itemId, insertPos, stopId) { + let parentId = Svc.Bookmark.getFolderIdForItem(itemId); + + // Keep processing the item chain until it loops to the stop item + do { + // Figure out what's next in the chain + let itemPos = Svc.Bookmark.getItemIndex(itemId); + let nextId = Svc.Bookmark.getIdForItemAt(parentId, itemPos + 1); + + Svc.Bookmark.moveItem(itemId, parentId, insertPos); + this._log.trace("Moved " + itemId + " to " + insertPos); + + // Prepare for the next item in the chain + insertPos = Svc.Bookmark.getItemIndex(itemId) + 1; + itemId = nextId; + + // Stop if we ran off the end or the item is looking for its pred. + if (itemId == -1 || Svc.Annos.itemHasAnnotation(itemId, PREDECESSOR_ANNO)) + break; + } while (itemId != stopId); + }, + create: function BStore_create(record) { let newId; switch (record.type) { @@ -368,33 +396,12 @@ BookmarksStore.prototype = { return; } + // Move the chain of followers to after the predecessor + let insertPos = Svc.Bookmark.getItemIndex(predId) + 1; + this._moveItemChain(follow, insertPos, predId); + // Remove the annotation now that we're putting it in the right spot Svc.Annos.removeItemAnnotation(follow, PREDECESSOR_ANNO); - - // Process each item in the chain until we loop back to the start - let insertAfter = predId; - do { - // Figure out what's next in the chain - let followPos = Svc.Bookmark.getItemIndex(follow); - let nextFollow = Svc.Bookmark.getIdForItemAt(parent, followPos + 1); - - let insertPos = Svc.Bookmark.getItemIndex(insertAfter) + 1; - Svc.Bookmark.moveItem(follow, parent, insertPos); - this._log.trace(["Moved", follow, "to", insertPos, "after", - insertAfter, "with", nextFollow, "next"].join(" ")); - - // Move to the next item and check some stop conditions - insertAfter = follow; - follow = nextFollow; - - // Stop if we ran off the end of the folder - if (follow == -1) - break; - - // We're at the end of a chain if the item is looking for its pred. - if (Svc.Annos.itemHasAnnotation(follow, PREDECESSOR_ANNO)) - break; - } while (follow != predId); }, this); }, this); }, @@ -437,11 +444,18 @@ BookmarksStore.prototype = { this._log.trace("Updating " + record.id + " (" + itemId + ")"); - // Need to move if the parent or position isn't correct - if (Svc.Bookmark.getFolderIdForItem(itemId) != record._parent || - Svc.Bookmark.getItemIndex(itemId) != record._insertPos) { - this._log.trace("Moving item (changing folder/position)"); - this._bms.moveItem(itemId, record._parent, record._insertPos); + // Move the bookmark to a new parent if necessary + if (Svc.Bookmark.getFolderIdForItem(itemId) != record._parent) { + this._log.trace("Moving item to a new parent"); + Svc.Bookmark.moveItem(itemId, record._parent, record._insertPos); + } + // Move the chain of bookmarks to a new position + else if (Svc.Bookmark.getItemIndex(itemId) != record._insertPos && + !record._orphan) { + this._log.trace("Moving item and followers to a new position"); + + // Stop moving at the predecessor unless we don't have one + this._moveItemChain(itemId, record._insertPos, record._predId || itemId); } for (let [key, val] in Iterator(record.cleartext)) { From 00e41c08daa8c47d3dc3c226ad3881225e5380cd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 18 Aug 2009 18:39:10 -0700 Subject: [PATCH 1280/1860] Add the successor ids when adding/removing items as well as both old and new successors when moving. --- services/sync/modules/engines/bookmarks.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 26420e60d134..b0e9e67fdb35 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -771,6 +771,17 @@ BookmarksTracker.prototype = { this._upScore(); }, + /** + * Add the successor id for the item that follows the given item + */ + _addSuccessor: function BMT__addSuccessor(itemId) { + let parentId = Svc.Bookmark.getFolderIdForItem(itemId); + let itemPos = Svc.Bookmark.getItemIndex(itemId); + let succId = Svc.Bookmark.getIdForItemAt(parentId, itemPos + 1); + if (succId != -1) + this._addId(succId); + }, + /* Every add/remove/change is worth 10 points */ _upScore: function BMT__upScore() { this._score += 10; @@ -813,6 +824,7 @@ BookmarksTracker.prototype = { this._log.trace("onItemAdded: " + itemId); this._addId(itemId); + this._addSuccessor(itemId); }, onBeforeItemRemoved: function BMT_onBeforeItemRemoved(itemId) { @@ -821,6 +833,7 @@ BookmarksTracker.prototype = { this._log.trace("onBeforeItemRemoved: " + itemId); this._addId(itemId); + this._addSuccessor(itemId); }, onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { @@ -846,6 +859,12 @@ BookmarksTracker.prototype = { this._log.trace("onItemMoved: " + itemId); this._addId(itemId); + this._addSuccessor(itemId); + + // Get the thing that's now at the old place + let oldSucc = Svc.Bookmark.getIdForItemAt(oldParent, oldIndex); + if (oldSucc != -1) + this._addId(oldSucc); }, onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, From 0cd0a73876051d705fadb57686b39a7e6e7ac289 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 18 Aug 2009 18:42:15 -0700 Subject: [PATCH 1281/1860] Attach followers for both created items and updated items (call from applyIncoming) when it's under the right parent. Share logic for attaching followers when creating a folder then fixing the reparented children and when creating/updating an item in the right parent. --- services/sync/modules/engines/bookmarks.js | 68 +++++++++++----------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index b0e9e67fdb35..a0d84bf2ebee 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -244,6 +244,9 @@ BookmarksStore.prototype = { // XXX Work around Bug 510628 by prepending parenT if (record._orphan) Utils.anno(itemId, PARENT_ANNO, "T" + parentGUID); + // It's now in the right folder, so move annotated items behind this + else + this._attachFollowers(itemId); // Create an annotation if we have a predecessor but no position // XXX Work around Bug 510628 by prepending predecessoR @@ -293,6 +296,33 @@ BookmarksStore.prototype = { } while (itemId != stopId); }, + /** + * For the provided predecessor item, attach its followers to it + */ + _attachFollowers: function BStore__attachFollowers(predId) { + let predGUID = GUIDForId(predId); + let followers = this._findAnnoItems(PREDECESSOR_ANNO, predGUID); + if (followers.length > 1) + this._log.warn(predId + " has more than one followers: " + followers); + + // Start at the first follower and move the chain of followers + let parent = Svc.Bookmark.getFolderIdForItem(predId); + followers.forEach(function(follow) { + this._log.trace("Repositioning " + follow + " behind " + predId); + if (Svc.Bookmark.getFolderIdForItem(follow) != parent) { + this._log.warn("Follower doesn't have the same parent: " + parent); + return; + } + + // Move the chain of followers to after the predecessor + let insertPos = Svc.Bookmark.getItemIndex(predId) + 1; + this._moveItemChain(follow, insertPos, predId); + + // Remove the annotation now that we're putting it in the right spot + Svc.Annos.removeItemAnnotation(follow, PREDECESSOR_ANNO); + }, this); + }, + create: function BStore_create(record) { let newId; switch (record.type) { @@ -358,16 +388,11 @@ BookmarksStore.prototype = { this._log.trace("Setting GUID of new item " + newId + " to " + record.id); this._setGUID(newId, record.id); - // Remember which guids now have the correct parent - let parented = []; - if (!record._orphan) - parented.push(newId); - // Find orphans and reunite with this new folder parent if (record.type == "folder") { let orphans = this._findAnnoItems(PARENT_ANNO, record.id); this._log.debug("Reparenting orphans " + orphans + " to " + record.title); - orphans.forEach(function(orphan) { + orphans.map(function(orphan) { // Append the orphan under the parent unless it's supposed to be first let insertPos = Svc.Bookmark.DEFAULT_INDEX; if (!Svc.Annos.itemHasAnnotation(orphan, PREDECESSOR_ANNO)) @@ -376,34 +401,11 @@ BookmarksStore.prototype = { // Move the orphan to the parent and drop the missing parent annotation Svc.Bookmark.moveItem(orphan, newId, insertPos); Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO); - parented.push(orphan); - }); + + // Return the now-parented orphan so it can attach its followers + return orphan; + }).forEach(this._attachFollowers, this); } - - // Reposition all chains of siblings for the now correctly parented items - parented.forEach(function(predId) { - let predGUID = GUIDForId(predId); - let followers = this._findAnnoItems(PREDECESSOR_ANNO, predGUID); - if (followers.length > 1) - this._log.warn(predId + " has more than one followers: " + followers); - - // Start at the first follower and move the chain of followers - let parent = Svc.Bookmark.getFolderIdForItem(predId); - followers.forEach(function(follow) { - this._log.debug("Repositioning " + follow + " behind " + predId); - if (Svc.Bookmark.getFolderIdForItem(follow) != parent) { - this._log.warn("Follower doesn't have the same parent: " + parent); - return; - } - - // Move the chain of followers to after the predecessor - let insertPos = Svc.Bookmark.getItemIndex(predId) + 1; - this._moveItemChain(follow, insertPos, predId); - - // Remove the annotation now that we're putting it in the right spot - Svc.Annos.removeItemAnnotation(follow, PREDECESSOR_ANNO); - }, this); - }, this); }, remove: function BStore_remove(record) { From 5df64143ee411ccf472ea7f4bde4a82060fbf674 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 18 Aug 2009 20:03:11 -0700 Subject: [PATCH 1282/1860] initial commit of new about:weave --- services/sync/Weave.js | 2 +- services/sync/locales/en-US/about.properties | 29 +++ services/sync/modules/ext/StringBundle.js | 238 +++++++++++++++++++ services/sync/modules/service.js | 133 ++++++++--- services/sync/modules/util.js | 26 +- 5 files changed, 381 insertions(+), 47 deletions(-) create mode 100644 services/sync/locales/en-US/about.properties create mode 100644 services/sync/modules/ext/StringBundle.js diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 6aaecd704199..9c626e07aa54 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -85,7 +85,7 @@ AboutWeaveService.prototype = { newChannel: function(aURI) { let ios = Cc["@mozilla.org/network/io-service;1"] .getService(Ci.nsIIOService); - let ch = ios.newChannel("chrome://weave/content/weave.html", null, null); + let ch = ios.newChannel("chrome://weave/content/about/index.html", null, null); ch.originalURI = aURI; return ch; } diff --git a/services/sync/locales/en-US/about.properties b/services/sync/locales/en-US/about.properties new file mode 100644 index 000000000000..32c7cd5296bc --- /dev/null +++ b/services/sync/locales/en-US/about.properties @@ -0,0 +1,29 @@ +welcome = Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam id neque lectus. Donec cursus pulvinar nibh at pretium. Vivamus ante tellus, accumsan vel placerat vestibulum, tristique ac augue. 'Cause that's how I roll. + +#prev = « prev +#next = next » +prev = prev +next = next + +signedin-title = Signed In +signedin-text = You are currently logged in as %S +signedin-signout = sign out + +signin-title = Sign Into Weave +signin-newacct = new account +signin-username = username +signin-password = password +signin-passphrase = passphrase +signin-help = help +signin-help-url = https://services.mozilla.com/help/login/ + +newacct-title = New Weave Account +newacct-username = username +newacct-password = password +newacct-passphrase = passphrase +newacct-email = email address +newacct-tos-label = I agree to the +newacct-tos-label2 = +newacct-tos-link = Terms of Service +newacct-tos-url = http://labs.mozilla.com/projects/weave/tos/ +recaptcha_response_field = Type in the words above diff --git a/services/sync/modules/ext/StringBundle.js b/services/sync/modules/ext/StringBundle.js new file mode 100644 index 000000000000..74f463eefcf8 --- /dev/null +++ b/services/sync/modules/ext/StringBundle.js @@ -0,0 +1,238 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is String Bundle. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let EXPORTED_SYMBOLS = ["StringBundle"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +/** + * A string bundle. + * + * This object presents two APIs: a deprecated one that is equivalent to the API + * for the stringbundle XBL binding, to make it easy to switch from that binding + * to this module, and a new one that is simpler and easier to use. + * + * The benefit of this module over the XBL binding is that it can also be used + * in JavaScript modules and components, not only in chrome JS. + * + * To use this module, import it, create a new instance of StringBundle, + * and then use the instance's |get| and |getAll| methods to retrieve strings + * (you can get both plain and formatted strings with |get|): + * + * let strings = + * new StringBundle("chrome://example/locale/strings.properties"); + * let foo = strings.get("foo"); + * let barFormatted = strings.get("bar", [arg1, arg2]); + * for each (let string in strings.getAll()) + * dump (string.key + " = " + string.value + "\n"); + * + * @param url {String} + * the URL of the string bundle + */ +function StringBundle(url) { + this.url = url; +} + +StringBundle.prototype = { + /** + * the locale associated with the application + * @type nsILocale + * @private + */ + get _appLocale() { + try { + return Cc["@mozilla.org/intl/nslocaleservice;1"]. + getService(Ci.nsILocaleService). + getApplicationLocale(); + } + catch(ex) { + return null; + } + }, + + /** + * the wrapped nsIStringBundle + * @type nsIStringBundle + * @private + */ + get _stringBundle() { + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(this.url, this._appLocale); + this.__defineGetter__("_stringBundle", function() stringBundle); + return this._stringBundle; + }, + + + // the new API + + /** + * the URL of the string bundle + * @type String + */ + _url: null, + get url() { + return this._url; + }, + set url(newVal) { + this._url = newVal; + delete this._stringBundle; + }, + + /** + * Get a string from the bundle. + * + * @param key {String} + * the identifier of the string to get + * @param args {array} [optional] + * an array of arguments that replace occurrences of %S in the string + * + * @returns {String} the value of the string + */ + get: function(key, args) { + if (args) + return this.stringBundle.formatStringFromName(key, args, args.length); + else + return this.stringBundle.GetStringFromName(key); + }, + + /** + * Get all the strings in the bundle. + * + * @returns {Array} + * an array of objects with key and value properties + */ + getAll: function() { + let strings = []; + + // FIXME: for performance, return an enumerable array that wraps the string + // bundle's nsISimpleEnumerator (does JavaScript already support this?). + + let enumerator = this.stringBundle.getSimpleEnumeration(); + + while (enumerator.hasMoreElements()) { + // We could simply return the nsIPropertyElement objects, but I think + // it's better to return standard JS objects that behave as consumers + // expect JS objects to behave (f.e. you can modify them dynamically). + let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + strings.push({ key: string.key, value: string.value }); + } + + return strings; + }, + + + // the deprecated XBL binding-compatible API + + /** + * the URL of the string bundle + * @deprecated because its name doesn't make sense outside of an XBL binding + * @type String + */ + get src() { + return this.url; + }, + set src(newVal) { + this.url = newVal; + }, + + /** + * the locale associated with the application + * @deprecated because it has never been used outside the XBL binding itself, + * and consumers should obtain it directly from the locale service anyway. + * @type nsILocale + */ + get appLocale() { + return this._appLocale; + }, + + /** + * the wrapped nsIStringBundle + * @deprecated because this module should provide all necessary functionality + * @type nsIStringBundle + * + * If you do ever need to use this, let the authors of this module know why + * so they can surface functionality for your use case in the module itself + * and you don't have to access this underlying XPCOM component. + */ + get stringBundle() { + return this._stringBundle; + }, + + /** + * Get a string from the bundle. + * @deprecated use |get| instead + * + * @param key {String} + * the identifier of the string to get + * + * @returns {String} + * the value of the string + */ + getString: function(key) { + return this.get(key); + }, + + /** + * Get a formatted string from the bundle. + * @deprecated use |get| instead + * + * @param key {string} + * the identifier of the string to get + * @param args {array} + * an array of arguments that replace occurrences of %S in the string + * + * @returns {String} + * the formatted value of the string + */ + getFormattedString: function(key, args) { + return this.get(key, args); + }, + + /** + * Get an enumeration of the strings in the bundle. + * @deprecated use |getAll| instead + * + * @returns {nsISimpleEnumerator} + * a enumeration of the strings in the bundle + */ + get strings() { + return this.stringBundle.getSimpleEnumeration(); + } +} diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ebf7efe520ad..ad9a9304dc8f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -399,8 +399,8 @@ WeaveSvc.prototype = { break; case FIREFOX_ID: - engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", - "Input", "MicroFormat", "Password", "Plugin", "Prefs", "Tab", + engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", + "Input", "MicroFormat", "Password", "Plugin", "Prefs", "Tab", "Theme"]; break; @@ -532,7 +532,7 @@ WeaveSvc.prototype = { this._log.debug("Verifying passphrase"); this.username = username; ID.get("WeaveID").setTempPassword(password); - + try { let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); @@ -551,7 +551,7 @@ WeaveSvc.prototype = { this._catch(this._notify("changepph", "", function() { let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); - + /* Re-encrypt with new passphrase. * FIXME: verifyPassphrase first! */ @@ -559,17 +559,17 @@ WeaveSvc.prototype = { this.passphrase, privkey.payload.salt, privkey.payload.iv, newphrase); privkey.payload.keyData = newkey; - + new Resource(privkey.uri).put(privkey.serialize()); this.passphrase = newphrase; - + return true; }))(), - + changePassword: function WeaveSvc_changePassword(newpass) this._catch(this._notify("changepwd", "", function() { function enc(x) encodeURIComponent(x); - let message = "uid=" + enc(this.username) + "&password=" + + let message = "uid=" + enc(this.username) + "&password=" + enc(this.password) + "&new=" + enc(newpass); let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/chpwd'; let res = new Weave.Resource(url); @@ -582,35 +582,35 @@ WeaveSvc.prototype = { this._log.info("Password change failed: " + resp); throw "Could not change password"; } - + this.password = newpass; return true; }))(), - + resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) this._catch(this._notify("resetpph", "", function() { /* Make remote commands ready so we have a list of clients beforehand */ this.prepCommand("logout", []); let clientsBackup = Clients._store.clients; - + /* Wipe */ this.wipeServer(); - + /* Set remote commands before syncing */ Clients._store.clients = clientsBackup; let username = this.username; let password = this.password; this.logout(); - + /* Set this so UI is updated on next run */ this.passphrase = newphrase; - + /* Login in sync: this also generates new keys */ this.login(username, password, newphrase); this.sync(true); return true; }))(), - + login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; @@ -659,37 +659,94 @@ WeaveSvc.prototype = { Svc.Observer.notifyObservers(null, "weave:service:logout:finish", ""); }, + _errorStr: function WeaveSvc__errorStr(code) { + switch (code) { + case "0": + return "uid-in-use"; + case "-1": + return "invalid-http-method"; + case "-2": + return "uid-missing"; + case "-3": + return "uid-invalid"; + case "-4": + return "mail-invalid"; + case "-5": + return "mail-in-use"; + case "-6": + return "captcha-challenge-missing"; + case "-7": + return "captcha-response-missing"; + case "-8": + return "password-missing"; + case "-9": + return "internal-server-error"; + case "-10": + return "server-quota-exceeded"; + case "-11": + return "missing-new-field"; + case "-12": + return "password-incorrect"; + default: + return "generic-server-error"; + } + }, + + checkUsername: function WeaveSvc_checkUsername(username) { + let url = Svc.Prefs.get('tmpServerURL') + + "0.3/api/register/checkuser/" + username; + + let res = new Resource(url); + res.authenticator = new NoOpAuthenticator(); + let data = res.get(); + + if (res.lastChannel.responseStatus == 200 && data == "0") + return "available"; + + return this._errorStr(data); + }, + createAccount: function WeaveSvc_createAccount(username, password, email, captchaChallenge, captchaResponse) { + let ret = null; + function enc(x) encodeURIComponent(x); let message = "uid=" + enc(username) + "&password=" + enc(password) + "&mail=" + enc(email) + "&recaptcha_challenge_field=" + enc(captchaChallenge) + "&recaptcha_response_field=" + enc(captchaResponse); let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/new'; - let res = new Weave.Resource(url); + let res = new Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); res.setHeader("Content-Type", "application/x-www-form-urlencoded", "Content-Length", message.length); - // fixme: Resource throws on error - it really shouldn't :-/ let resp; try { resp = res.post(message); - } - catch(ex) { - this._log.trace("Create account error: " + ex); - } + ret = { + status: res.lastChannel.responseStatus, + response: resp + }; + + if (res.lastChannel.responseStatus != 200 && + res.lastChannel.responseStatus != 201) + throw "Server returned error code " + res.lastChannel.responseStatus; - if (res.lastChannel.responseStatus != 200 && - res.lastChannel.responseStatus != 201) - this._log.info("Failed to create account. " + - "status: " + res.lastChannel.responseStatus + ", " + - "response: " + resp); - else this._log.info("Account created: " + resp); + ret.error = false; - return res.lastChannel.responseStatus; + } catch(ex) { + this._log.warn("Failed to create account: " + Utils.exceptionStr(ex)); + + ret.error = "generic-server-error"; + if (ret.status == 400) + ret.error = this._errorStr(ret.status); + else if (ret.status == 417) + ret.error = "captcha-incorrect"; + } + + return ret; }, // stuff we need to to after login, before we can really do @@ -703,7 +760,7 @@ WeaveSvc.prototype = { let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; - this._log.debug(["Weave Version:", WEAVE_VERSION, "Compatible:", + this._log.debug(["Weave Version:", WEAVE_VERSION, "Compatible:", COMPATIBLE_VERSION, "Remote:", remoteVersion].join(" ")); if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || @@ -882,14 +939,14 @@ WeaveSvc.prototype = { /** * Call sync() on an idle timer - * + * */ syncOnIdle: function WeaveSvc_syncOnIdle() { this._log.debug("Idle timer created for sync, will sync after " + IDLE_TIME + " seconds of inactivity."); Svc.Idle.addIdleObserver(this, IDLE_TIME); }, - + /** * Set a timer for the next sync */ @@ -902,7 +959,7 @@ WeaveSvc.prototype = { this._syncTimer.cancel(); else this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - + let listener = new Utils.EventListener(Utils.bind2(this, function WeaveSvc__scheduleNextSyncCallback(timer) { this._syncTimer = null; @@ -919,7 +976,7 @@ WeaveSvc.prototype = { */ _handleSyncError: function WeaveSvc__handleSyncError() { let shouldBackoff = false; - + let err = Weave.Service.detailedStatus.sync; // we'll assume the server is just borked a little for these switch (err) { @@ -931,16 +988,16 @@ WeaveSvc.prototype = { // specifcally handle 500, 502, 503, 504 errors // xxxmpc: what else should be in this list? - // this is sort of pseudocode, need a way to get at the - if (!shouldBackoff && + // this is sort of pseudocode, need a way to get at the + if (!shouldBackoff && Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]])) { shouldBackoff = true; } - + // if this is a client error, do the next sync as normal and return if (!shouldBackoff) { this._scheduleNextSync(); - return; + return; } // ok, something failed connecting to the server, rev the counter @@ -1279,7 +1336,7 @@ WeaveSvc.prototype = { // Process each command in order for each ({command: command, args: args} in commands) { this._log.debug("Processing command: " + command + "(" + args + ")"); - + let engines = [args[0]]; switch (command) { case "resetAll": diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 146d70dfe13d..eb3130df5e4c 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Utils', 'Svc']; +const EXPORTED_SYMBOLS = ['Utils', 'Svc', 'Str']; const Cc = Components.classes; const Ci = Components.interfaces; @@ -44,6 +44,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Preferences.js"); Cu.import("resource://weave/ext/Observers.js"); +Cu.import("resource://weave/ext/StringBundle.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/log4moz.js"); @@ -295,6 +296,11 @@ let Utils = { dest.__defineGetter__(prop, getter); }, + lazyStrings: function Weave_lazyStrings(name) { + let bundle = "chrome://weave/locale/" + name + ".properties"; + return function() new StringBundle(bundle); + }, + deepEquals: function eq(a, b) { // If they're triple equals, then it must be equals! if (a === b) @@ -619,16 +625,16 @@ let Utils = { win = win.activeWindow; win["open" + type].apply(win, Array.slice(arguments, 2)); }, - + _openChromeWindow: function Utils_openCWindow(name, uri, options, args) { Utils.openWindow(name, "chrome://weave/content/" + uri, options, args); }, - + openWindow: function Utils_openWindow(name, uri, options, args) { Utils._openWin(name, "Window", null, uri, "", - options || "centerscreen,chrome,dialog,resizable=yes", args); + options || "centerscreen,chrome,dialog,resizable=yes", args); }, - + openDialog: function Utils_openDialog(name, uri, options, args) { Utils._openWin(name, "Dialog", "chrome://weave/content/" + uri, "", options || "centerscreen,chrome,dialog,modal,resizable=no", args); @@ -646,7 +652,7 @@ let Utils = { openShare: function Utils_openShare() { Utils.openDialog("Share", "share.xul"); }, - + openLog: function Utils_openLog() { Utils._openChromeWindow("Log", "log.xul"); }, @@ -657,11 +663,11 @@ let Utils = { openSync: function Utils_openSync() { Utils._openChromeWindow("Sync", "pick-sync.xul"); }, - + openWizard: function Utils_openWizard() { Utils._openChromeWindow("Wizard", "wizard.xul"); }, - + // assumes an nsIConverterInputStream readStream: function Weave_readStream(is) { let ret = "", str = {}; @@ -738,3 +744,7 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ["WinMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"], ["WinWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"], ].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], Ci[lazy[2]])); + +let Str = {}; +["service", "about"] + .forEach(function(lazy) Utils.lazy2(Str, lazy, Utils.lazyStrings(lazy))); From fa45a8314b6a5ec8bde9918e0d302c77bf53e339 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 19 Aug 2009 17:10:17 -0700 Subject: [PATCH 1283/1860] clean up weave events handling; add timed bubble after account creation; add status text below arrows --- services/sync/locales/en-US/about.properties | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/services/sync/locales/en-US/about.properties b/services/sync/locales/en-US/about.properties index 32c7cd5296bc..d0d2cf4af0bf 100644 --- a/services/sync/locales/en-US/about.properties +++ b/services/sync/locales/en-US/about.properties @@ -1,3 +1,12 @@ +status-offline = signed out +status-offline-2 = (offline) +status-signing-in = +status-signing-in-2 = (signing in...) +status-idle = signed in as %S +status-idle-2 = (idle) +status-sync = signed in as %S +status-sync-2 = (syncing) + welcome = Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam id neque lectus. Donec cursus pulvinar nibh at pretium. Vivamus ante tellus, accumsan vel placerat vestibulum, tristique ac augue. 'Cause that's how I roll. #prev = « prev @@ -27,3 +36,7 @@ newacct-tos-label2 = newacct-tos-link = Terms of Service newacct-tos-url = http://labs.mozilla.com/projects/weave/tos/ recaptcha_response_field = Type in the words above + +willsync-title = Account Created! +willsync-text = Sync will begin in %S seconds... +willsync-config = choose what to sync \ No newline at end of file From 6fb9f64023a6965a589482e6f4f7e0190162d128 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 19 Aug 2009 18:01:06 -0700 Subject: [PATCH 1284/1860] Share the logging _ code for tests. --- services/sync/tests/unit/bookmark_setup.js | 7 ------- services/sync/tests/unit/head_first.js | 11 +++++++++++ services/sync/tests/unit/test_clients_escape.js | 13 +------------ services/sync/tests/unit/test_collection_inc_get.js | 11 ----------- services/sync/tests/unit/test_load_modules.js | 4 ++-- services/sync/tests/unit/test_utils_deepEquals.js | 13 +------------ 6 files changed, 15 insertions(+), 44 deletions(-) diff --git a/services/sync/tests/unit/bookmark_setup.js b/services/sync/tests/unit/bookmark_setup.js index f7eaf969a194..5d777a5ef781 100644 --- a/services/sync/tests/unit/bookmark_setup.js +++ b/services/sync/tests/unit/bookmark_setup.js @@ -41,13 +41,6 @@ const NS_APP_USER_PROFILE_50_DIR = "ProfD"; -function LOG(aMsg) { - aMsg = ("*** PLACES TESTS: " + aMsg); - Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService). - logStringMessage(aMsg); - print(aMsg); -} - // If there's no location registered for the profile direcotry, register one now. var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); var profileDir = null; diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 3d17641253c6..d18688ce661a 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -319,3 +319,14 @@ function SyncTestingInfrastructure(engineFactory) { engine._store.wipe(); }; } + +/** + * Print some debug message to the console. All arguments will be printed, + * separated by spaces. + * + * @param [arg0, arg1, arg2, ...] + * Any number of arguments to print out + * @usage _("Hello World") -> prints "Hello World" + * @usage _(1, 2, 3) -> prints "1 2 3" + */ +let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" ")); diff --git a/services/sync/tests/unit/test_clients_escape.js b/services/sync/tests/unit/test_clients_escape.js index d318e0c01575..70d4efc29527 100644 --- a/services/sync/tests/unit/test_clients_escape.js +++ b/services/sync/tests/unit/test_clients_escape.js @@ -1,15 +1,4 @@ -/** - * Print some debug message to the console. All arguments will be printed, - * separated by spaces. - * - * @param [arg0, arg1, arg2, ...] - * Any number of arguments to print out - * @usage _("Hello World") -> prints "Hello World" - * @usage _(1, 2, 3) -> prints "1 2 3" - */ -let _ = function(some, debug, text, to) print(Array.join(arguments, " ")); - -Components.utils.import("resource://weave/engines/clientData.js"); +Cu.import("resource://weave/engines/clientData.js"); function run_test() { _("Test that serializing client records results in uploadable ascii"); diff --git a/services/sync/tests/unit/test_collection_inc_get.js b/services/sync/tests/unit/test_collection_inc_get.js index cf271bf3459b..6815c27f8460 100644 --- a/services/sync/tests/unit/test_collection_inc_get.js +++ b/services/sync/tests/unit/test_collection_inc_get.js @@ -1,14 +1,3 @@ -/** - * Print some debug message to the console. All arguments will be printed, - * separated by spaces. - * - * @param [arg0, arg1, arg2, ...] - * Any number of arguments to print out - * @usage _("Hello World") -> prints "Hello World" - * @usage _(1, 2, 3) -> prints "1 2 3" - */ -let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" ")); - _("Make sure Collection can correctly incrementally parse GET requests"); Cu.import("resource://weave/base_records/collection.js"); Cu.import("resource://weave/base_records/wbo.js"); diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js index ca62312cd5ed..4b72f0a698f8 100644 --- a/services/sync/tests/unit/test_load_modules.js +++ b/services/sync/tests/unit/test_load_modules.js @@ -41,8 +41,8 @@ const modules = [ function run_test() { for each (let m in modules) { - dump("Attempting to load resource://weave/" + m + "\n"); - Components.utils.import("resource://weave/" + m, {}); + _("Attempting to load resource://weave/" + m); + Cu.import("resource://weave/" + m, {}); } } diff --git a/services/sync/tests/unit/test_utils_deepEquals.js b/services/sync/tests/unit/test_utils_deepEquals.js index 65af9f530621..ec8bad523de0 100644 --- a/services/sync/tests/unit/test_utils_deepEquals.js +++ b/services/sync/tests/unit/test_utils_deepEquals.js @@ -1,16 +1,5 @@ -/** - * Print some debug message to the console. All arguments will be printed, - * separated by spaces. - * - * @param [arg0, arg1, arg2, ...] - * Any number of arguments to print out - * @usage _("Hello World") -> prints "Hello World" - * @usage _(1, 2, 3) -> prints "1 2 3" - */ -let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" ")); - _("Make sure Utils.deepEquals correctly finds items that are deeply equal"); -Components.utils.import("resource://weave/util.js"); +Cu.import("resource://weave/util.js"); function run_test() { let data = '[NaN, undefined, null, true, false, Infinity, 0, 1, "a", "b", {a: 1}, {a: "a"}, [{a: 1}], [{a: true}], {a: 1, b: 2}, [1, 2], [1, 2, 3]]'; From a4a950eb133c91599154becaa258e80f9618a51a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 19 Aug 2009 18:15:12 -0700 Subject: [PATCH 1285/1860] Add tests to make sure bookmarks are in the right position after creating/updating. --- .../sync/tests/unit/test_bookmark_order.js | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 services/sync/tests/unit/test_bookmark_order.js diff --git a/services/sync/tests/unit/test_bookmark_order.js b/services/sync/tests/unit/test_bookmark_order.js new file mode 100644 index 000000000000..486bb58a57b8 --- /dev/null +++ b/services/sync/tests/unit/test_bookmark_order.js @@ -0,0 +1,171 @@ +_("Making sure after processing incoming bookmarks, they show up in the right order"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines/bookmarks.js"); +Cu.import("resource://weave/type_records/bookmark.js"); + +function getBookmarks(folderId) { + let bookmarks = []; + + let pos = 0; + while (true) { + let itemId = Svc.Bookmark.getIdForItemAt(folderId, pos); + _("Got itemId", itemId, "under", folderId, "at", pos); + if (itemId == -1) + break; + + switch (Svc.Bookmark.getItemType(itemId)) { + case Svc.Bookmark.TYPE_BOOKMARK: + bookmarks.push(Svc.Bookmark.getItemTitle(itemId)); + break; + case Svc.Bookmark.TYPE_FOLDER: + bookmarks.push(getBookmarks(itemId)); + break; + default: + _("Unsupported item type.."); + } + + pos++; + } + + return bookmarks; +} + +function check(expected) { + let bookmarks = getBookmarks(Svc.Bookmark.unfiledBookmarksFolder); + + _("Checking if the bookmark structure is", JSON.stringify(expected)); + _("Got bookmarks:", JSON.stringify(bookmarks)); + do_check_true(Utils.deepEquals(bookmarks, expected)); +} + +function run_test() { + _("Starting with a clean slate of no bookmarks"); + let store = new (new BookmarksEngine())._storeObj(); + store.wipe(); + check([]); + + function $B(name, parent, pred) { + let bookmark = new Bookmark(); + bookmark.id = name; + bookmark.title = name; + bookmark.bmkUri = "http://uri/"; + bookmark.parentid = parent || "unfiled"; + bookmark.predecessorid = pred; + bookmark.tags = []; + store.applyIncoming(bookmark); + } + + function $F(name, parent, pred) { + let folder = new BookmarkFolder(); + folder.id = name; + folder.title = name; + folder.parentid = parent || "unfiled"; + folder.predecessorid = pred; + store.applyIncoming(folder); + } + + _("basic add first bookmark"); + $B("10", ""); + check(["10"]); + + _("basic append behind 10"); + $B("20", "", "10"); + check(["10", "20"]); + + _("basic create in folder"); + $F("f30", "", "20"); + $B("31", "f30"); + check(["10", "20", ["31"]]); + + _("insert missing predecessor -> append"); + $B("50", "", "f40"); + check(["10", "20", ["31"], "50"]); + + _("insert missing parent -> append"); + $B("41", "f40"); + check(["10", "20", ["31"], "50", "41"]); + + _("insert another missing parent -> append"); + $B("42", "f40", "41"); + check(["10", "20", ["31"], "50", "41", "42"]); + + _("insert folder -> move children and followers"); + $F("f40", "", "f30"); + check(["10", "20", ["31"], ["41", "42"], "50"]); + + _("Moving 10 behind 50 -> update 10, 20"); + $B("10", "", "50"); + $B("20", ""); + check(["20", ["31"], ["41", "42"], "50", "10"]); + + _("Moving 10 back to front -> update 10, 20"); + $B("10", ""); + $B("20", "", "10"); + check(["10", "20", ["31"], ["41", "42"], "50"]); + + _("Moving 10 behind 50 in different order -> update 20, 10"); + $B("20", ""); + $B("10", "", "50"); + check(["20", ["31"], ["41", "42"], "50", "10"]); + + _("Moving 10 back to front in different order -> update 20, 10"); + $B("20", "", "10"); + $B("10", ""); + check(["10", "20", ["31"], ["41", "42"], "50"]); + + _("Moving 50 behind 42 in f40 -> update 50"); + $B("50", "f40", "42"); + check(["10", "20", ["31"], ["41", "42", "50"]]); + + _("Moving 10 in front of 31 in f30 -> update 10, 20, 31"); + $B("10", "f30"); + $B("20", ""); + $B("31", "f30", "10"); + check(["20", ["10", "31"], ["41", "42", "50"]]); + + _("Moving 20 between 10 and 31 -> update 20, f30, 31"); + $B("20", "f30", "10"); + $F("f30", ""); + $B("31", "f30", "20"); + check([["10", "20", "31"], ["41", "42", "50"]]); + + _("Move 20 back to front -> update 20, f30, 31"); + $B("20", ""); + $F("f30", "", "20"); + $B("31", "f30", "10"); + check(["20", ["10", "31"], ["41", "42", "50"]]); + + _("Moving 20 between 10 and 31 different order -> update f30, 20, 31"); + $F("f30", ""); + $B("20", "f30", "10"); + $B("31", "f30", "20"); + check([["10", "20", "31"], ["41", "42", "50"]]); + + _("Move 20 back to front different order -> update f30, 31, 20"); + $F("f30", "", "20"); + $B("31", "f30", "10"); + $B("20", ""); + check(["20", ["10", "31"], ["41", "42", "50"]]); + + _("Moving 20 between 10 and 31 different order 2 -> update 31, f30, 20"); + $B("31", "f30", "20"); + $F("f30", ""); + $B("20", "f30", "10"); + check([["10", "20", "31"], ["41", "42", "50"]]); + + _("Move 20 back to front different order 2 -> update 31, f30, 20"); + $B("31", "f30", "10"); + $F("f30", "", "20"); + $B("20", ""); + check(["20", ["10", "31"], ["41", "42", "50"]]); + + _("Move 10, 31 to f40 but update in reverse -> update 41, 31, 10"); + $B("41", "f40", "31"); + $B("31", "f40", "10"); + $B("10", "f40"); + check(["20", [], ["10", "31", "41", "42", "50"]]); + + _("Reparent f40 into f30"); + $F("f40", "f30"); + check(["20", [["10", "31", "41", "42", "50"]]]); +} From 9210e18f4be274a53c96346d64526e574b6eb441 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 19 Aug 2009 18:22:22 -0700 Subject: [PATCH 1286/1860] Handle 401s from the server correctly. (bug #509552) --- services/sync/modules/service.js | 34 ++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ebf7efe520ad..ff4f8d9d782b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -68,6 +68,9 @@ const INITIAL_THRESHOLD = 75; // threshold each time we do a sync check and don't sync that engine. const THRESHOLD_DECREMENT_STEP = 25; +// How long before refreshing the cluster +const CLUSTER_BACKOFF = SCHEDULED_SYNC_INTERVAL; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/log4moz.js"); @@ -499,7 +502,19 @@ WeaveSvc.prototype = { this._log.debug("Error setting cluster for user " + username); return false; }, - + + // update cluster if required. returns false if the update was not required + updateCluster: function WeaveSvc_updateCluster(username) { + let cTime = Date.now(); + let lastUp = parseFloat(Svc.Prefs.get("lastClusterUpdate")); + if (!lastUp || ((cTime - lastUp) >= CLUSTER_BACKOFF)) { + this.setCluster(username); + Svc.Prefs.set("lastClusterUpdate", cTime.toString()); + return true; + } + return false; + }, + verifyLogin: function WeaveSvc_verifyLogin(username, password, passphrase, isLogin) this._catch(this._notify("verify-login", "", function() { this._log.debug("Verifying login for user " + username); @@ -519,7 +534,17 @@ WeaveSvc.prototype = { return headers; } }; - res.get(); + + // login may fail because of cluster change + try { + res.get(); + } catch (e) { + if (res.lastChannel.responseStatus == 401) { + if (this.updateCluster(username)) + return this.verifyLogin(username, password, passphrase, isLogin); + } + throw e; + } if (passphrase) return this.verifyPassphrase(username, password, passphrase); @@ -1083,6 +1108,11 @@ WeaveSvc.prototype = { return true; } catch(e) { + // maybe a 401, cluster update needed? + if (e.constructor.name == "RequestException" && e.status == 401) { + if (this.updateCluster(this.username)) + return this._syncEngine(engine); + } this._syncError = true; this._weaveStatusCode = WEAVE_STATUS_PARTIAL; this._detailedStatus.setEngineStatus(engine.name, e); From 73ac78ba70b487cae78eb02e464702cbf9dac024 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 19 Aug 2009 19:32:05 -0700 Subject: [PATCH 1287/1860] Handle holes in bookmark folders and fix up bookmarks to be at an earlier position if a hole was detected. --- services/sync/modules/engines/bookmarks.js | 23 +++++++++++++ .../tests/unit/test_bookmark_predecessor.js | 34 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 services/sync/tests/unit/test_bookmark_predecessor.js diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index a0d84bf2ebee..1f6a0d709ca6 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -673,6 +673,29 @@ BookmarksStore.prototype = { let parentId = Svc.Bookmark.getFolderIdForItem(itemId); let predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos - 1); + + if (predecessorId == -1) { + this._log.debug("No predecessor directly before " + itemId + " under " + + parentId + " at " + itemPos); + + // Find the predecessor before the item + do { + // No more items to check, it must be the first one + if (--itemPos < 0) + break; + predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos); + } while (predecessorId == -1); + + // Fix up the item to be at the right position for next time + itemPos++; + this._log.debug("Fixing " + itemId + " to be at position " + itemPos); + Svc.Bookmark.moveItem(itemId, parentId, itemPos); + + // There must be no predecessor for this item! + if (itemPos == 0) + return; + } + return GUIDForId(predecessorId); }, diff --git a/services/sync/tests/unit/test_bookmark_predecessor.js b/services/sync/tests/unit/test_bookmark_predecessor.js new file mode 100644 index 000000000000..38c9ca565714 --- /dev/null +++ b/services/sync/tests/unit/test_bookmark_predecessor.js @@ -0,0 +1,34 @@ +_("Make sure bad bookmarks can still get their predecessors"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines/bookmarks.js"); + +function run_test() { + _("Starting with a clean slate of no bookmarks"); + let store = new (new BookmarksEngine())._storeObj(); + store.wipe(); + + let unfiled = Svc.Bookmark.unfiledBookmarksFolder; + let uri = Utils.makeURI("http://uri/"); + function insert(pos) { + let name = "pos" + pos; + let bmk = Svc.Bookmark.insertBookmark(unfiled, uri, pos, name); + Svc.Bookmark.setItemGUID(bmk, name); + return bmk; + } + + _("Creating a couple bookmarks that create holes"); + let first = insert(5); + let second = insert(10); + + _("Making sure the record created for the first has no predecessor"); + let pos5 = store.createRecord("pos5"); + do_check_eq(pos5.predecessorid, undefined); + + _("Making sure the second record has the first as its predecessor"); + let pos10 = store.createRecord("pos10"); + do_check_eq(pos10.predecessorid, "pos5"); + + _("Make sure the index of item gets fixed"); + do_check_eq(Svc.Bookmark.getItemIndex(first), 0); + do_check_eq(Svc.Bookmark.getItemIndex(second), 1); +} From 46d1bc8dd3e4a5419bc7563286d71f3da5989093 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 19 Aug 2009 23:27:22 -0400 Subject: [PATCH 1288/1860] bug 481733 - provide better error messages, handle errors better, make autoconnect more robust, r=edilee --- services/sync/locales/en-US/login.properties | 1 + services/sync/locales/en-US/sync.properties | 4 +- services/sync/modules/constants.js | 7 +- services/sync/modules/service.js | 133 ++++++++++++++----- 4 files changed, 109 insertions(+), 36 deletions(-) diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties index c465c9c59564..777d01fd9ffe 100644 --- a/services/sync/locales/en-US/login.properties +++ b/services/sync/locales/en-US/login.properties @@ -7,3 +7,4 @@ loginStart.label = Signing in, please wait... loginError.label = Invalid username, password or passphrase. loginSuccess.label = Signed In hide.label = Hide +error.login.description = Error: %1$S diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 146c59dec72d..203acb17132e 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -6,7 +6,7 @@ weaveButtonOnline.label = Weave shareBookmark.menuItem = Share This Folder... unShareBookmark.menuItem = Stop Sharing This Folder -status.offline = Sign in +status.offline = Not Signed In # The next two are not normally used, as we now display the username # when the user is logged in. But if for some reason we can't get the username, @@ -18,6 +18,8 @@ error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. error.login.reason.password = Your username/password failed error.login.reason.unknown = Unknown error +error.login.reason.passphrase = Invalid passphrase +error.login.reason.network = Network error error.logout.title = Error While Signing Out error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it (then why are we bugging you with this info?). error.sync.title = Error While Syncing diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 162cb9ccfceb..7055b0630dbc 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -45,7 +45,8 @@ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", 'WEAVE_STATUS_PARTIAL', 'SERVER_LOW_QUOTA', 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', - 'LOGIN_FAILED_REJECTED', 'METARECORD_DOWNLOAD_FAIL', + 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', + 'LOGIN_FAILED_LOGIN_REJECTED', 'METARECORD_DOWNLOAD_FAIL', 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', @@ -97,7 +98,9 @@ const SERVER_UNREACHABLE = "Weave server is unreachable."; // Ways that a sync can fail during setup or login: const LOGIN_FAILED_NO_USERNAME = "No username set, login failed."; const LOGIN_FAILED_NO_PASSWORD = "No password set, login failed."; -const LOGIN_FAILED_REJECTED = "Incorrect username or password."; +const LOGIN_FAILED_NETWORK_ERROR = "Weave failed to connect to the server."; +const LOGIN_FAILED_INVALID_PASSPHRASE = "Incorrect passphrase given."; +const LOGIN_FAILED_LOGIN_REJECTED = "Incorrect username or password."; const METARECORD_DOWNLOAD_FAIL = "Can't download metadata record, HTTP error."; const VERSION_OUT_OF_DATE = "This copy of Weave needs to be updated."; const DESKTOP_VERSION_OUT_OF_DATE = "Weave needs updating on your desktop browser."; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ff4f8d9d782b..f158e2b8a9f3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -340,12 +340,8 @@ WeaveSvc.prototype = { this._genKeyURLs(); - if (Svc.Prefs.get("autoconnect") && this.username) { - try { - if (this.login()) - this.syncOnIdle(); - } catch (e) {} - } + if (Svc.Prefs.get("autoconnect")) + this._autoConnect(); }, _initLogs: function WeaveSvc__initLogs() { @@ -476,24 +472,30 @@ WeaveSvc.prototype = { let res = new Resource(this.baseURL + "api/register/chknode/" + username); try { res.get(); - } - catch(ex) { /* we check status below */ } - if (res.lastChannel.responseStatus == 404) { - this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); - return Svc.Prefs.get("serverURL"); - } + switch (res.lastChannel.responseStatus) { + case 404: + this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); + return Svc.Prefs.get("serverURL"); + case 200: + return "https://" + res.data + "/"; + default: + this._log.debug("Unexpected response code trying to find cluster: " + res.lastChannel.responseStatus); + break; + } + } catch(ex) { /* if the channel failed to start we'll get here, just return false */} - if (res.lastChannel.responseStatus == 200) - return "https://" + res.data + "/"; - - return null; + return false; }, // gets cluster from central LDAP server and sets this.clusterURL setCluster: function WeaveSvc_setCluster(username) { let cluster = this.findCluster(username); + if (cluster) { + if (cluster == this.clusterURL) + return false; + this._log.debug("Saving cluster setting"); this.clusterURL = cluster; return true; @@ -508,9 +510,10 @@ WeaveSvc.prototype = { let cTime = Date.now(); let lastUp = parseFloat(Svc.Prefs.get("lastClusterUpdate")); if (!lastUp || ((cTime - lastUp) >= CLUSTER_BACKOFF)) { - this.setCluster(username); - Svc.Prefs.set("lastClusterUpdate", cTime.toString()); - return true; + if (this.setCluster(username)) { + Svc.Prefs.set("lastClusterUpdate", cTime.toString()); + return true; + } } return false; }, @@ -538,18 +541,32 @@ WeaveSvc.prototype = { // login may fail because of cluster change try { res.get(); - } catch (e) { - if (res.lastChannel.responseStatus == 401) { - if (this.updateCluster(username)) - return this.verifyLogin(username, password, passphrase, isLogin); + } catch (e) {} + + try { + switch (res.lastChannel.responseStatus) { + case 200: + if (passphrase && !this.verifyPassphrase(username, password, passphrase)) { + this._setSyncFailure(LOGIN_FAILED_INVALID_PASSPHRASE); + return false; + } + return true; + case 401: + if (this.updateCluster(username)) + return this.verifyLogin(username, password, passphrase, isLogin); + + this._setSyncFailure(LOGIN_FAILED_LOGIN_REJECTED); + this._log.debug("verifyLogin failed: login failed") + return false; + default: + throw "unexpected HTTP response: " + res.lastChannel.responseStatus; } + } catch (e) { + // if we get here, we have either a busted channel or a network error + this._log.debug("verifyLogin failed: " + e) + this._setSyncFailure(LOGIN_FAILED_NETWORK_ERROR); throw e; } - - if (passphrase) - return this.verifyPassphrase(username, password, passphrase); - else - return true; }))(), verifyPassphrase: function WeaveSvc_verifyPassphrase(username, password, passphrase) @@ -636,10 +653,51 @@ WeaveSvc.prototype = { return true; }))(), + _autoConnectAttempts: 0, + _autoConnect: function WeaveSvc__attemptAutoConnect() { + try { + if (!this.username || !this.password || !this.passphrase) + return; + + let failureReason; + if (Svc.IO.offline) + failureReason = "Application is offline"; + else if (this.login()) { + this.syncOnIdle(); + return; + } + + failureReason = this.detailedStatus.sync; + } + catch (ex) { + failureReason = ex; + } + + this._log.debug("Autoconnect failed: " + failureReason); + + let listener = new Utils.EventListener(Utils.bind2(this, + function WeaveSvc__autoConnectCallback(timer) { + this._autoConnectTimer = null; + this._autoConnect(); + })); + this._autoConnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + // back off slowly, with some random fuzz to avoid repeated collisions + var interval = Math.floor(Math.random() * SCHEDULED_SYNC_INTERVAL + + SCHEDULED_SYNC_INTERVAL * this._autoConnectAttempts); + this._autoConnectAttempts++; + this._autoConnectTimer.initWithCallback(listener, interval, + Ci.nsITimer.TYPE_ONE_SHOT); + this._log.debug("Scheduling next autoconnect attempt in " + + interval / 1000 + " seconds."); + }, + login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; this._detailedStatus = new StatusRecord(); + if (Svc.IO.offline) + throw "Application is offline, login should not be called"; if (typeof(username) != "undefined") this.username = username; @@ -660,13 +718,17 @@ WeaveSvc.prototype = { if (!(this.verifyLogin(this.username, this.password, passphrase, true))) { - this._setSyncFailure(LOGIN_FAILED_REJECTED); - throw "Login failed"; + // verifyLogin sets the failure states here + throw "Login failed: " + this.detailedStatus.sync; } // Try starting the sync timer now that we're logged in this._loggedIn = true; this._checkSyncStatus(); + if (this._autoConnectTimer) { + this._autoConnectTimer.cancel(); + this._autoConnectTimer = null; + } return true; })))(), @@ -957,9 +1019,14 @@ WeaveSvc.prototype = { // specifcally handle 500, 502, 503, 504 errors // xxxmpc: what else should be in this list? // this is sort of pseudocode, need a way to get at the - if (!shouldBackoff && - Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]])) { - shouldBackoff = true; + if (!shouldBackoff) { + try { + shouldBackoff = Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]]); + } + catch (e) { + // if responseStatus throws, we have a network issue in play + shouldBackoff = true; + } } // if this is a client error, do the next sync as normal and return From 4a8e3208dff25560fdc42a87204087cd8b88b0dc Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 19 Aug 2009 23:27:22 -0400 Subject: [PATCH 1289/1860] bug 481733 - provide better error messages, handle errors better, make autoconnect more robust, r=edilee --- services/sync/locales/en-US/login.properties | 1 + services/sync/locales/en-US/sync.properties | 4 +- services/sync/modules/constants.js | 7 +- services/sync/modules/service.js | 140 ++++++++++++++----- 4 files changed, 112 insertions(+), 40 deletions(-) diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties index c465c9c59564..777d01fd9ffe 100644 --- a/services/sync/locales/en-US/login.properties +++ b/services/sync/locales/en-US/login.properties @@ -7,3 +7,4 @@ loginStart.label = Signing in, please wait... loginError.label = Invalid username, password or passphrase. loginSuccess.label = Signed In hide.label = Hide +error.login.description = Error: %1$S diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 146c59dec72d..203acb17132e 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -6,7 +6,7 @@ weaveButtonOnline.label = Weave shareBookmark.menuItem = Share This Folder... unShareBookmark.menuItem = Stop Sharing This Folder -status.offline = Sign in +status.offline = Not Signed In # The next two are not normally used, as we now display the username # when the user is logged in. But if for some reason we can't get the username, @@ -18,6 +18,8 @@ error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. error.login.reason.password = Your username/password failed error.login.reason.unknown = Unknown error +error.login.reason.passphrase = Invalid passphrase +error.login.reason.network = Network error error.logout.title = Error While Signing Out error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it (then why are we bugging you with this info?). error.sync.title = Error While Syncing diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 162cb9ccfceb..7055b0630dbc 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -45,7 +45,8 @@ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", 'WEAVE_STATUS_PARTIAL', 'SERVER_LOW_QUOTA', 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', - 'LOGIN_FAILED_REJECTED', 'METARECORD_DOWNLOAD_FAIL', + 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', + 'LOGIN_FAILED_LOGIN_REJECTED', 'METARECORD_DOWNLOAD_FAIL', 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', @@ -97,7 +98,9 @@ const SERVER_UNREACHABLE = "Weave server is unreachable."; // Ways that a sync can fail during setup or login: const LOGIN_FAILED_NO_USERNAME = "No username set, login failed."; const LOGIN_FAILED_NO_PASSWORD = "No password set, login failed."; -const LOGIN_FAILED_REJECTED = "Incorrect username or password."; +const LOGIN_FAILED_NETWORK_ERROR = "Weave failed to connect to the server."; +const LOGIN_FAILED_INVALID_PASSPHRASE = "Incorrect passphrase given."; +const LOGIN_FAILED_LOGIN_REJECTED = "Incorrect username or password."; const METARECORD_DOWNLOAD_FAIL = "Can't download metadata record, HTTP error."; const VERSION_OUT_OF_DATE = "This copy of Weave needs to be updated."; const DESKTOP_VERSION_OUT_OF_DATE = "Weave needs updating on your desktop browser."; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f75be400b53d..cafcc40741d7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -340,12 +340,8 @@ WeaveSvc.prototype = { this._genKeyURLs(); - if (Svc.Prefs.get("autoconnect") && this.username) { - try { - if (this.login()) - this.syncOnIdle(); - } catch (e) {} - } + if (Svc.Prefs.get("autoconnect")) + this._autoConnect(); }, _initLogs: function WeaveSvc__initLogs() { @@ -476,24 +472,30 @@ WeaveSvc.prototype = { let res = new Resource(this.baseURL + "api/register/chknode/" + username); try { res.get(); - } - catch(ex) { /* we check status below */ } - if (res.lastChannel.responseStatus == 404) { - this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); - return Svc.Prefs.get("serverURL"); - } + switch (res.lastChannel.responseStatus) { + case 404: + this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); + return Svc.Prefs.get("serverURL"); + case 200: + return "https://" + res.data + "/"; + default: + this._log.debug("Unexpected response code trying to find cluster: " + res.lastChannel.responseStatus); + break; + } + } catch(ex) { /* if the channel failed to start we'll get here, just return false */} - if (res.lastChannel.responseStatus == 200) - return "https://" + res.data + "/"; - - return null; + return false; }, // gets cluster from central LDAP server and sets this.clusterURL setCluster: function WeaveSvc_setCluster(username) { let cluster = this.findCluster(username); + if (cluster) { + if (cluster == this.clusterURL) + return false; + this._log.debug("Saving cluster setting"); this.clusterURL = cluster; return true; @@ -502,19 +504,20 @@ WeaveSvc.prototype = { this._log.debug("Error setting cluster for user " + username); return false; }, - - // update cluster if required. returns false if the update was not required + + // update cluster if required. returns false if the update was not required updateCluster: function WeaveSvc_updateCluster(username) { let cTime = Date.now(); let lastUp = parseFloat(Svc.Prefs.get("lastClusterUpdate")); if (!lastUp || ((cTime - lastUp) >= CLUSTER_BACKOFF)) { - this.setCluster(username); - Svc.Prefs.set("lastClusterUpdate", cTime.toString()); - return true; + if (this.setCluster(username)) { + Svc.Prefs.set("lastClusterUpdate", cTime.toString()); + return true; + } } return false; }, - + verifyLogin: function WeaveSvc_verifyLogin(username, password, passphrase, isLogin) this._catch(this._notify("verify-login", "", function() { this._log.debug("Verifying login for user " + username); @@ -534,22 +537,36 @@ WeaveSvc.prototype = { return headers; } }; - + // login may fail because of cluster change try { res.get(); - } catch (e) { - if (res.lastChannel.responseStatus == 401) { - if (this.updateCluster(username)) - return this.verifyLogin(username, password, passphrase, isLogin); + } catch (e) {} + + try { + switch (res.lastChannel.responseStatus) { + case 200: + if (passphrase && !this.verifyPassphrase(username, password, passphrase)) { + this._setSyncFailure(LOGIN_FAILED_INVALID_PASSPHRASE); + return false; + } + return true; + case 401: + if (this.updateCluster(username)) + return this.verifyLogin(username, password, passphrase, isLogin); + + this._setSyncFailure(LOGIN_FAILED_LOGIN_REJECTED); + this._log.debug("verifyLogin failed: login failed") + return false; + default: + throw "unexpected HTTP response: " + res.lastChannel.responseStatus; } + } catch (e) { + // if we get here, we have either a busted channel or a network error + this._log.debug("verifyLogin failed: " + e) + this._setSyncFailure(LOGIN_FAILED_NETWORK_ERROR); throw e; } - - if (passphrase) - return this.verifyPassphrase(username, password, passphrase); - else - return true; }))(), verifyPassphrase: function WeaveSvc_verifyPassphrase(username, password, passphrase) @@ -636,10 +653,50 @@ WeaveSvc.prototype = { return true; }))(), + _autoConnectAttempts: 0, + _autoConnect: function WeaveSvc__attemptAutoConnect() { + try { + if (!this.username || !this.password || !this.passphrase) + return; + let failureReason; + if (Svc.IO.offline) + failureReason = "Application is offline"; + else if (this.login()) { + this.syncOnIdle(); + return; + } + + failureReason = this.detailedStatus.sync; + } + catch (ex) { + failureReason = ex; + } + + this._log.debug("Autoconnect failed: " + failureReason); + + let listener = new Utils.EventListener(Utils.bind2(this, + function WeaveSvc__autoConnectCallback(timer) { + this._autoConnectTimer = null; + this._autoConnect(); + })); + this._autoConnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + // back off slowly, with some random fuzz to avoid repeated collisions + var interval = Math.floor(Math.random() * SCHEDULED_SYNC_INTERVAL + + SCHEDULED_SYNC_INTERVAL * this._autoConnectAttempts); + this._autoConnectAttempts++; + this._autoConnectTimer.initWithCallback(listener, interval, + Ci.nsITimer.TYPE_ONE_SHOT); + this._log.debug("Scheduling next autoconnect attempt in " + + interval / 1000 + " seconds."); + }, + login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; this._detailedStatus = new StatusRecord(); + if (Svc.IO.offline) + throw "Application is offline, login should not be called"; if (typeof(username) != "undefined") this.username = username; @@ -660,13 +717,17 @@ WeaveSvc.prototype = { if (!(this.verifyLogin(this.username, this.password, passphrase, true))) { - this._setSyncFailure(LOGIN_FAILED_REJECTED); - throw "Login failed"; + // verifyLogin sets the failure states here + throw "Login failed: " + this.detailedStatus.sync; } // Try starting the sync timer now that we're logged in this._loggedIn = true; this._checkSyncStatus(); + if (this._autoConnectTimer) { + this._autoConnectTimer.cancel(); + this._autoConnectTimer = null; + } return true; })))(), @@ -1014,9 +1075,14 @@ WeaveSvc.prototype = { // specifcally handle 500, 502, 503, 504 errors // xxxmpc: what else should be in this list? // this is sort of pseudocode, need a way to get at the - if (!shouldBackoff && - Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]])) { - shouldBackoff = true; + if (!shouldBackoff) { + try { + shouldBackoff = Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]]); + } + catch (e) { + // if responseStatus throws, we have a network issue in play + shouldBackoff = true; + } } // if this is a client error, do the next sync as normal and return From b9ea1746171571cf9288ee2c0d40b901664fffa3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 19 Aug 2009 22:53:04 -0700 Subject: [PATCH 1290/1860] Resolve commit conflicts from merging in weave. --- services/sync/modules/service.js | 33 +------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2d34c3c99221..299d36e53fd7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -542,11 +542,7 @@ WeaveSvc.prototype = { try { res.get(); } catch (e) {} -<<<<<<< local -======= - ->>>>>>> other try { switch (res.lastChannel.responseStatus) { case 200: @@ -656,20 +652,13 @@ WeaveSvc.prototype = { this.sync(true); return true; }))(), -<<<<<<< local -======= - ->>>>>>> other _autoConnectAttempts: 0, _autoConnect: function WeaveSvc__attemptAutoConnect() { try { if (!this.username || !this.password || !this.passphrase) return; -<<<<<<< local -======= ->>>>>>> other let failureReason; if (Svc.IO.offline) failureReason = "Application is offline"; @@ -692,33 +681,17 @@ WeaveSvc.prototype = { this._autoConnect(); })); this._autoConnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); -<<<<<<< local -======= - ->>>>>>> other // back off slowly, with some random fuzz to avoid repeated collisions -<<<<<<< local - var interval = Math.floor(Math.random() * SCHEDULED_SYNC_INTERVAL + -======= - var interval = Math.floor(Math.random() * SCHEDULED_SYNC_INTERVAL + ->>>>>>> other + let interval = Math.floor(Math.random() * SCHEDULED_SYNC_INTERVAL + SCHEDULED_SYNC_INTERVAL * this._autoConnectAttempts); this._autoConnectAttempts++; this._autoConnectTimer.initWithCallback(listener, interval, Ci.nsITimer.TYPE_ONE_SHOT); -<<<<<<< local this._log.debug("Scheduling next autoconnect attempt in " + -======= - this._log.debug("Scheduling next autoconnect attempt in " + ->>>>>>> other interval / 1000 + " seconds."); }, -<<<<<<< local -======= - ->>>>>>> other login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; @@ -1102,11 +1075,7 @@ WeaveSvc.prototype = { // specifcally handle 500, 502, 503, 504 errors // xxxmpc: what else should be in this list? -<<<<<<< local // this is sort of pseudocode, need a way to get at the -======= - // this is sort of pseudocode, need a way to get at the ->>>>>>> other if (!shouldBackoff) { try { shouldBackoff = Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]]); From b23453510b3294ca7fbd8e451ffbeefbbdac3d7d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 Aug 2009 00:49:09 -0700 Subject: [PATCH 1291/1860] Remove trailing whitespace in service.js and util.js. --- services/sync/modules/service.js | 66 ++++++++++++++++---------------- services/sync/modules/util.js | 14 +++---- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f158e2b8a9f3..52180a492fc3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -398,8 +398,8 @@ WeaveSvc.prototype = { break; case FIREFOX_ID: - engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", - "Input", "MicroFormat", "Password", "Plugin", "Prefs", "Tab", + engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", + "Input", "MicroFormat", "Password", "Plugin", "Prefs", "Tab", "Theme"]; break; @@ -504,8 +504,8 @@ WeaveSvc.prototype = { this._log.debug("Error setting cluster for user " + username); return false; }, - - // update cluster if required. returns false if the update was not required + + // update cluster if required. returns false if the update was not required updateCluster: function WeaveSvc_updateCluster(username) { let cTime = Date.now(); let lastUp = parseFloat(Svc.Prefs.get("lastClusterUpdate")); @@ -517,7 +517,7 @@ WeaveSvc.prototype = { } return false; }, - + verifyLogin: function WeaveSvc_verifyLogin(username, password, passphrase, isLogin) this._catch(this._notify("verify-login", "", function() { this._log.debug("Verifying login for user " + username); @@ -537,12 +537,12 @@ WeaveSvc.prototype = { return headers; } }; - + // login may fail because of cluster change try { res.get(); } catch (e) {} - + try { switch (res.lastChannel.responseStatus) { case 200: @@ -574,7 +574,7 @@ WeaveSvc.prototype = { this._log.debug("Verifying passphrase"); this.username = username; ID.get("WeaveID").setTempPassword(password); - + try { let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); @@ -593,7 +593,7 @@ WeaveSvc.prototype = { this._catch(this._notify("changepph", "", function() { let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); - + /* Re-encrypt with new passphrase. * FIXME: verifyPassphrase first! */ @@ -601,17 +601,17 @@ WeaveSvc.prototype = { this.passphrase, privkey.payload.salt, privkey.payload.iv, newphrase); privkey.payload.keyData = newkey; - + new Resource(privkey.uri).put(privkey.serialize()); this.passphrase = newphrase; - + return true; }))(), - + changePassword: function WeaveSvc_changePassword(newpass) this._catch(this._notify("changepwd", "", function() { function enc(x) encodeURIComponent(x); - let message = "uid=" + enc(this.username) + "&password=" + + let message = "uid=" + enc(this.username) + "&password=" + enc(this.password) + "&new=" + enc(newpass); let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/chpwd'; let res = new Weave.Resource(url); @@ -624,35 +624,35 @@ WeaveSvc.prototype = { this._log.info("Password change failed: " + resp); throw "Could not change password"; } - + this.password = newpass; return true; }))(), - + resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) this._catch(this._notify("resetpph", "", function() { /* Make remote commands ready so we have a list of clients beforehand */ this.prepCommand("logout", []); let clientsBackup = Clients._store.clients; - + /* Wipe */ this.wipeServer(); - + /* Set remote commands before syncing */ Clients._store.clients = clientsBackup; let username = this.username; let password = this.password; this.logout(); - + /* Set this so UI is updated on next run */ this.passphrase = newphrase; - + /* Login in sync: this also generates new keys */ this.login(username, password, newphrase); this.sync(true); return true; }))(), - + _autoConnectAttempts: 0, _autoConnect: function WeaveSvc__attemptAutoConnect() { try { @@ -681,17 +681,17 @@ WeaveSvc.prototype = { this._autoConnect(); })); this._autoConnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - + // back off slowly, with some random fuzz to avoid repeated collisions - var interval = Math.floor(Math.random() * SCHEDULED_SYNC_INTERVAL + + let interval = Math.floor(Math.random() * SCHEDULED_SYNC_INTERVAL + SCHEDULED_SYNC_INTERVAL * this._autoConnectAttempts); this._autoConnectAttempts++; this._autoConnectTimer.initWithCallback(listener, interval, Ci.nsITimer.TYPE_ONE_SHOT); - this._log.debug("Scheduling next autoconnect attempt in " + + this._log.debug("Scheduling next autoconnect attempt in " + interval / 1000 + " seconds."); }, - + login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; @@ -790,7 +790,7 @@ WeaveSvc.prototype = { let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; - this._log.debug(["Weave Version:", WEAVE_VERSION, "Compatible:", + this._log.debug(["Weave Version:", WEAVE_VERSION, "Compatible:", COMPATIBLE_VERSION, "Remote:", remoteVersion].join(" ")); if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || @@ -969,14 +969,14 @@ WeaveSvc.prototype = { /** * Call sync() on an idle timer - * + * */ syncOnIdle: function WeaveSvc_syncOnIdle() { this._log.debug("Idle timer created for sync, will sync after " + IDLE_TIME + " seconds of inactivity."); Svc.Idle.addIdleObserver(this, IDLE_TIME); }, - + /** * Set a timer for the next sync */ @@ -989,7 +989,7 @@ WeaveSvc.prototype = { this._syncTimer.cancel(); else this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - + let listener = new Utils.EventListener(Utils.bind2(this, function WeaveSvc__scheduleNextSyncCallback(timer) { this._syncTimer = null; @@ -1006,7 +1006,7 @@ WeaveSvc.prototype = { */ _handleSyncError: function WeaveSvc__handleSyncError() { let shouldBackoff = false; - + let err = Weave.Service.detailedStatus.sync; // we'll assume the server is just borked a little for these switch (err) { @@ -1018,7 +1018,7 @@ WeaveSvc.prototype = { // specifcally handle 500, 502, 503, 504 errors // xxxmpc: what else should be in this list? - // this is sort of pseudocode, need a way to get at the + // this is sort of pseudocode, need a way to get at the if (!shouldBackoff) { try { shouldBackoff = Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]]); @@ -1028,11 +1028,11 @@ WeaveSvc.prototype = { shouldBackoff = true; } } - + // if this is a client error, do the next sync as normal and return if (!shouldBackoff) { this._scheduleNextSync(); - return; + return; } // ok, something failed connecting to the server, rev the counter @@ -1376,7 +1376,7 @@ WeaveSvc.prototype = { // Process each command in order for each ({command: command, args: args} in commands) { this._log.debug("Processing command: " + command + "(" + args + ")"); - + let engines = [args[0]]; switch (command) { case "resetAll": diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 7790632e3a9a..ab27cac33b11 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -633,16 +633,16 @@ let Utils = { win = win.activeWindow; win["open" + type].apply(win, Array.slice(arguments, 2)); }, - + _openChromeWindow: function Utils_openCWindow(name, uri, options, args) { Utils.openWindow(name, "chrome://weave/content/" + uri, options, args); }, - + openWindow: function Utils_openWindow(name, uri, options, args) { Utils._openWin(name, "Window", null, uri, "", - options || "centerscreen,chrome,dialog,resizable=yes", args); + options || "centerscreen,chrome,dialog,resizable=yes", args); }, - + openDialog: function Utils_openDialog(name, uri, options, args) { Utils._openWin(name, "Dialog", "chrome://weave/content/" + uri, "", options || "centerscreen,chrome,dialog,modal,resizable=no", args); @@ -660,7 +660,7 @@ let Utils = { openShare: function Utils_openShare() { Utils.openDialog("Share", "share.xul"); }, - + openLog: function Utils_openLog() { Utils._openChromeWindow("Log", "log.xul"); }, @@ -671,11 +671,11 @@ let Utils = { openSync: function Utils_openSync() { Utils._openChromeWindow("Sync", "pick-sync.xul"); }, - + openWizard: function Utils_openWizard() { Utils._openChromeWindow("Wizard", "wizard.xul"); }, - + // assumes an nsIConverterInputStream readStream: function Weave_readStream(is) { let ret = "", str = {}; From 83cfc1aef25409e95430ce2b8b5d3aaea452d825 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 Aug 2009 01:09:23 -0700 Subject: [PATCH 1292/1860] Switch to the new about page.. currently empty. --- services/sync/Weave.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 6aaecd704199..9c626e07aa54 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -85,7 +85,7 @@ AboutWeaveService.prototype = { newChannel: function(aURI) { let ios = Cc["@mozilla.org/network/io-service;1"] .getService(Ci.nsIIOService); - let ch = ios.newChannel("chrome://weave/content/weave.html", null, null); + let ch = ios.newChannel("chrome://weave/content/about/index.html", null, null); ch.originalURI = aURI; return ch; } From d048f2a8461fb2f741cc496ac8fa062d8766158b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 Aug 2009 01:09:28 -0700 Subject: [PATCH 1293/1860] Add Myk's StringBundle to modules/ext --- services/sync/modules/ext/StringBundle.js | 238 ++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 services/sync/modules/ext/StringBundle.js diff --git a/services/sync/modules/ext/StringBundle.js b/services/sync/modules/ext/StringBundle.js new file mode 100644 index 000000000000..74f463eefcf8 --- /dev/null +++ b/services/sync/modules/ext/StringBundle.js @@ -0,0 +1,238 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is String Bundle. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let EXPORTED_SYMBOLS = ["StringBundle"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +/** + * A string bundle. + * + * This object presents two APIs: a deprecated one that is equivalent to the API + * for the stringbundle XBL binding, to make it easy to switch from that binding + * to this module, and a new one that is simpler and easier to use. + * + * The benefit of this module over the XBL binding is that it can also be used + * in JavaScript modules and components, not only in chrome JS. + * + * To use this module, import it, create a new instance of StringBundle, + * and then use the instance's |get| and |getAll| methods to retrieve strings + * (you can get both plain and formatted strings with |get|): + * + * let strings = + * new StringBundle("chrome://example/locale/strings.properties"); + * let foo = strings.get("foo"); + * let barFormatted = strings.get("bar", [arg1, arg2]); + * for each (let string in strings.getAll()) + * dump (string.key + " = " + string.value + "\n"); + * + * @param url {String} + * the URL of the string bundle + */ +function StringBundle(url) { + this.url = url; +} + +StringBundle.prototype = { + /** + * the locale associated with the application + * @type nsILocale + * @private + */ + get _appLocale() { + try { + return Cc["@mozilla.org/intl/nslocaleservice;1"]. + getService(Ci.nsILocaleService). + getApplicationLocale(); + } + catch(ex) { + return null; + } + }, + + /** + * the wrapped nsIStringBundle + * @type nsIStringBundle + * @private + */ + get _stringBundle() { + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(this.url, this._appLocale); + this.__defineGetter__("_stringBundle", function() stringBundle); + return this._stringBundle; + }, + + + // the new API + + /** + * the URL of the string bundle + * @type String + */ + _url: null, + get url() { + return this._url; + }, + set url(newVal) { + this._url = newVal; + delete this._stringBundle; + }, + + /** + * Get a string from the bundle. + * + * @param key {String} + * the identifier of the string to get + * @param args {array} [optional] + * an array of arguments that replace occurrences of %S in the string + * + * @returns {String} the value of the string + */ + get: function(key, args) { + if (args) + return this.stringBundle.formatStringFromName(key, args, args.length); + else + return this.stringBundle.GetStringFromName(key); + }, + + /** + * Get all the strings in the bundle. + * + * @returns {Array} + * an array of objects with key and value properties + */ + getAll: function() { + let strings = []; + + // FIXME: for performance, return an enumerable array that wraps the string + // bundle's nsISimpleEnumerator (does JavaScript already support this?). + + let enumerator = this.stringBundle.getSimpleEnumeration(); + + while (enumerator.hasMoreElements()) { + // We could simply return the nsIPropertyElement objects, but I think + // it's better to return standard JS objects that behave as consumers + // expect JS objects to behave (f.e. you can modify them dynamically). + let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + strings.push({ key: string.key, value: string.value }); + } + + return strings; + }, + + + // the deprecated XBL binding-compatible API + + /** + * the URL of the string bundle + * @deprecated because its name doesn't make sense outside of an XBL binding + * @type String + */ + get src() { + return this.url; + }, + set src(newVal) { + this.url = newVal; + }, + + /** + * the locale associated with the application + * @deprecated because it has never been used outside the XBL binding itself, + * and consumers should obtain it directly from the locale service anyway. + * @type nsILocale + */ + get appLocale() { + return this._appLocale; + }, + + /** + * the wrapped nsIStringBundle + * @deprecated because this module should provide all necessary functionality + * @type nsIStringBundle + * + * If you do ever need to use this, let the authors of this module know why + * so they can surface functionality for your use case in the module itself + * and you don't have to access this underlying XPCOM component. + */ + get stringBundle() { + return this._stringBundle; + }, + + /** + * Get a string from the bundle. + * @deprecated use |get| instead + * + * @param key {String} + * the identifier of the string to get + * + * @returns {String} + * the value of the string + */ + getString: function(key) { + return this.get(key); + }, + + /** + * Get a formatted string from the bundle. + * @deprecated use |get| instead + * + * @param key {string} + * the identifier of the string to get + * @param args {array} + * an array of arguments that replace occurrences of %S in the string + * + * @returns {String} + * the formatted value of the string + */ + getFormattedString: function(key, args) { + return this.get(key, args); + }, + + /** + * Get an enumeration of the strings in the bundle. + * @deprecated use |getAll| instead + * + * @returns {nsISimpleEnumerator} + * a enumeration of the strings in the bundle + */ + get strings() { + return this.stringBundle.getSimpleEnumeration(); + } +} From 7515f7d420fb525511b2fa7089e3f7ceacfc499c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 Aug 2009 10:14:19 -0700 Subject: [PATCH 1294/1860] Handle createAccount resource exceptions correctly (null ret, throw fail) and cleanup. Check for non-failure in the UI instead of only success 200. --- services/sync/modules/service.js | 47 +++++++++++++++----------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 299d36e53fd7..35d282ffc31f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -795,8 +795,6 @@ WeaveSvc.prototype = { createAccount: function WeaveSvc_createAccount(username, password, email, captchaChallenge, captchaResponse) { - let ret = null; - function enc(x) encodeURIComponent(x); let message = "uid=" + enc(username) + "&password=" + enc(password) + "&mail=" + enc(email) + "&recaptcha_challenge_field=" + @@ -808,32 +806,31 @@ WeaveSvc.prototype = { res.setHeader("Content-Type", "application/x-www-form-urlencoded", "Content-Length", message.length); - let resp; + let ret = {}; try { - resp = res.post(message); - ret = { - status: res.lastChannel.responseStatus, - response: resp - }; + ret.response = res.post(message); + ret.status = res.lastChannel.responseStatus; - if (res.lastChannel.responseStatus != 200 && - res.lastChannel.responseStatus != 201) - throw "Server returned error code " + res.lastChannel.responseStatus; - - this._log.info("Account created: " + resp); - ret.error = false; - - } catch(ex) { - this._log.warn("Failed to create account: " + Utils.exceptionStr(ex)); - - ret.error = "generic-server-error"; - if (ret.status == 400) - ret.error = this._errorStr(ret.status); - else if (ret.status == 417) - ret.error = "captcha-incorrect"; + // No exceptions must have meant it was successful + this._log.info("Account created: " + ret.response); + return ret; + } + catch(ex) { + this._log.warn("Failed to create account: " + ex); + let status = ex.request.responseStatus; + switch (status) { + case 400: + ret.error = this._errorStr(status); + break; + case 417: + ret.error = "captcha-incorrect"; + break; + default: + ret.error = "generic-server-error"; + break; + } + return ret; } - - return ret; }, // stuff we need to to after login, before we can really do From 95c9b5eab81d4f24b4e84670d48fc7ba45d9022c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 Aug 2009 10:20:01 -0700 Subject: [PATCH 1295/1860] Try/catch in checkUsername if resource throws and default to generic-server-error. --- services/sync/modules/service.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 35d282ffc31f..bad1849ee86d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -785,11 +785,16 @@ WeaveSvc.prototype = { let res = new Resource(url); res.authenticator = new NoOpAuthenticator(); - let data = res.get(); - if (res.lastChannel.responseStatus == 200 && data == "0") + let data = ""; + try { + data = res.get(); + if (res.lastChannel.responseStatus == 200 && data == "0") return "available"; + } + catch(ex) {} + // Convert to the error string, or default to generic on exception return this._errorStr(data); }, From 558606aa98982a14e272449d900bab2bbc57399d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Thu, 20 Aug 2009 11:49:50 -0700 Subject: [PATCH 1296/1860] Always load about:weave, never the wizard; fix captcha to load in an iframe; fix password fields to be type text when the page is loaded (with the explanation text in them) --- services/sync/locales/en-US/about.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/about.properties b/services/sync/locales/en-US/about.properties index d0d2cf4af0bf..9281926311cc 100644 --- a/services/sync/locales/en-US/about.properties +++ b/services/sync/locales/en-US/about.properties @@ -35,7 +35,7 @@ newacct-tos-label = I agree to the newacct-tos-label2 = newacct-tos-link = Terms of Service newacct-tos-url = http://labs.mozilla.com/projects/weave/tos/ -recaptcha_response_field = Type in the words above +captcha-response = Type in the words above willsync-title = Account Created! willsync-text = Sync will begin in %S seconds... From 7f9a2eb93665f7df3124a07ac841c75780741434 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Thu, 20 Aug 2009 12:14:34 -0700 Subject: [PATCH 1297/1860] Correct network handling in findCluster --- services/sync/modules/service.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bad1849ee86d..6e4ac16afd92 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -472,7 +472,9 @@ WeaveSvc.prototype = { let res = new Resource(this.baseURL + "api/register/chknode/" + username); try { res.get(); - + } catch(ex) {} + + try { switch (res.lastChannel.responseStatus) { case 404: this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); @@ -483,9 +485,11 @@ WeaveSvc.prototype = { this._log.debug("Unexpected response code trying to find cluster: " + res.lastChannel.responseStatus); break; } - } catch(ex) { /* if the channel failed to start we'll get here, just return false */} - - return false; + } catch (e) { + this._log.debug("Network error on findCluster"); + this._setSyncFailure(LOGIN_FAILED_NETWORK_ERROR); + throw e; + } }, // gets cluster from central LDAP server and sets this.clusterURL From 9e3c18c15762103901b143f2a4b7a7a95c525cc7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 Aug 2009 16:26:22 -0700 Subject: [PATCH 1298/1860] Remove unused cancelRequested code that causes JS strict warnings. --- services/sync/locales/en-US/status.properties | 2 -- services/sync/modules/service.js | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index 6a9ee2b82a7d..2019a0699c36 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -4,8 +4,6 @@ status.wait = Waiting for Current Sync to Finish status.active = Syncing with Weave status.success = Sync Complete status.error = Sync Failed -status.cancel = Cancelling Sync, Please Wait -status.cancelled = Sync Cancelled status.closing = Closing... status.locked = Server Busy diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6e4ac16afd92..9e67de0b0235 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -243,9 +243,6 @@ WeaveSvc.prototype = { get isQuitting() { return this._isQuitting; }, set isQuitting(value) { this._isQuitting = value; }, - get cancelRequested() { return Engines.cancelRequested; }, - set cancelRequested(value) { Engines.cancelRequested = value; }, - get keyGenEnabled() { return this._keyGenEnabled; }, set keyGenEnabled(value) { this._keyGenEnabled = value; }, @@ -1224,7 +1221,6 @@ WeaveSvc.prototype = { this._log.info("Sync completed successfully"); } } finally { - this.cancelRequested = false; this._syncError = false; } })))(), @@ -1234,8 +1230,7 @@ WeaveSvc.prototype = { _syncEngine: function WeaveSvc__syncEngine(engine) { try { engine.sync(); - if (!this.cancelRequested) - return true; + return true; } catch(e) { // maybe a 401, cluster update needed? From 62c439f1e8a213d7b2abd2a6ebea4c998560068d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 Aug 2009 17:00:15 -0700 Subject: [PATCH 1299/1860] Conditionally log trace records to avoid always doing record.toString(). --- services/sync/modules/engines.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c0634a6569ca..eea82b7e28aa 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -368,7 +368,8 @@ SyncEngine.prototype = { _isEqual: function SyncEngine__isEqual(item) { let local = this._createRecord(item.id); - this._log.trace("Local record: \n" + local); + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace("Local record: " + local); if (item.parentid == local.parentid && item.sortindex == local.sortindex && item.deleted == local.deleted && @@ -434,7 +435,8 @@ SyncEngine.prototype = { // Apply incoming records _applyIncoming: function SyncEngine__applyIncoming(item) { - this._log.trace("Incoming:\n" + item); + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace("Incoming: " + item); try { this._tracker.ignoreAll = true; this._store.applyIncoming(item); @@ -470,7 +472,8 @@ SyncEngine.prototype = { for (let id in this._tracker.changedIDs) { let out = this._createRecord(id); - this._log.trace("Outgoing:\n" + out); + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace("Outgoing: " + out); out.encrypt(ID.get("WeaveCryptoID")); up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient From 77d3b215aae232c92e48df5dd81c8fb49f9d3d72 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 Aug 2009 17:10:14 -0700 Subject: [PATCH 1300/1860] Use the plain get/set for wbo.modified because it's not needed anymore and was causing JS strict warnings: reference undefined property. --- services/sync/modules/base_records/wbo.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 14d1193a4ee8..468fc63405e7 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -73,12 +73,6 @@ WBORecord.prototype = { this.baseUri = Utils.makeURI(foo.join('/') + '/'); }, - get modified() { - if (typeof(this.data.modified) == "string") - this.data.modified = parseInt(this.data.modified); - return this.data.modified; - }, - get sortindex() { if (this.data.sortindex) return this.data.sortindex; From 7b7fedca2db065a9065e31d619367b904a7318c1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 21 Aug 2009 14:29:30 -0700 Subject: [PATCH 1301/1860] Bug 511794 - Always use _view instead of _temp + table or just table Also use subqueries instead of multiple queries and drop the query count down to 2. --- services/sync/modules/engines/history.js | 163 +++-------------------- 1 file changed, 20 insertions(+), 143 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index f6c2d85e56f9..49920b1d56d8 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -137,154 +137,44 @@ HistoryStore.prototype = { return this._hsvc.DBConnection; }, - _fetchRow: function HistStore__fetchRow(stm, params, retparams) { - try { - for (let key in params) { - stm.params[key] = params[key]; - } - if (!stm.step()) - return null; - if (retparams.length == 1) - return stm.row[retparams[0]]; - let ret = {}; - for each (let key in retparams) { - ret[key] = stm.row[key]; - } - return ret; - } finally { - stm.reset(); - } - }, - - // Check for table existance manually because - // mozIStorageConnection.tableExists() gives us false negatives - tableExists: function HistStore__tableExists(tableName) { - let statement = this._db.createStatement( - "SELECT count(*) as count FROM sqlite_temp_master WHERE type='table' " + - "AND name='" + tableName + "'"); - statement.step(); - let num = statement.row["count"]; - return num != 0; - }, - get _visitStm() { this._log.trace("Creating SQL statement: _visitStm"); - let stm; - if (this.tableExists("moz_historyvisits_temp")) { - stm = this._db.createStatement( - "SELECT * FROM ( " + - "SELECT visit_type AS type, visit_date AS date " + - "FROM moz_historyvisits_temp " + - "WHERE place_id = :placeid " + - "ORDER BY visit_date DESC " + - "LIMIT 10 " + - ") " + - "UNION ALL " + - "SELECT * FROM ( " + - "SELECT visit_type AS type, visit_date AS date " + - "FROM moz_historyvisits " + - "WHERE place_id = :placeid " + - "AND id NOT IN (SELECT id FROM moz_historyvisits_temp) " + - "ORDER BY visit_date DESC " + - "LIMIT 10 " + - ") " + - "ORDER BY 2 DESC LIMIT 10"); /* 2 is visit_date */ - } else { - stm = this._db.createStatement( - "SELECT visit_type AS type, visit_date AS date " + - "FROM moz_historyvisits " + - "WHERE place_id = :placeid " + - "ORDER BY visit_date DESC LIMIT 10" - ); - } + let stm = this._db.createStatement( + "SELECT visit_type type, visit_date date " + + "FROM moz_historyvisits_view " + + "WHERE place_id = (" + + "SELECT id " + + "FROM moz_places_view " + + "WHERE url = :url) " + + "ORDER BY date DESC LIMIT 10"); this.__defineGetter__("_visitStm", function() stm); return stm; }, - get _pidStm() { - this._log.trace("Creating SQL statement: _pidStm"); - let stm; - // See comment in get _urlStm() - if (this.tableExists("moz_places_temp")) { - stm = this._db.createStatement( - "SELECT * FROM " + - "(SELECT id FROM moz_places_temp WHERE url = :url LIMIT 1) " + - "UNION ALL " + - "SELECT * FROM ( " + - "SELECT id FROM moz_places WHERE url = :url " + - "AND id NOT IN (SELECT id from moz_places_temp) " + - "LIMIT 1 " + - ") " + - "LIMIT 1"); - } else { - stm = this._db.createStatement( - "SELECT id FROM moz_places WHERE url = :url LIMIT 1" - ); - } - this.__defineGetter__("_pidStm", function() stm); - return stm; - }, - get _urlStm() { this._log.trace("Creating SQL statement: _urlStm"); - /* On Fennec, there is no table called moz_places_temp. - * (We think there should be; we're not sure why there's not.) - * As a workaround until we figure out why, we'll check if the table - * exists first, and if it doesn't we'll use a simpler query without - * that table. - */ - let stm; - if (this.tableExists("moz_places_temp")) { - stm = this._db.createStatement( - "SELECT * FROM " + - "(SELECT url,title FROM moz_places_temp WHERE id = :id LIMIT 1) " + - "UNION ALL " + - "SELECT * FROM ( " + - "SELECT url,title FROM moz_places WHERE id = :id " + - "AND id NOT IN (SELECT id from moz_places_temp) " + - "LIMIT 1 " + - ") " + - "LIMIT 1"); - } else { - stm = this._db.createStatement( - "SELECT url,title FROM moz_places WHERE id = :id LIMIT 1" - ); - } + let stm = this._db.createStatement( + "SELECT url, title " + + "FROM moz_places_view " + + "WHERE id = (" + + "SELECT place_id " + + "FROM moz_annos " + + "WHERE content = :guid AND anno_attribute_id = (" + + "SELECT id " + + "FROM moz_anno_attributes " + + "WHERE name = 'weave/guid'))"); this.__defineGetter__("_urlStm", function() stm); return stm; }, - get _annoAttrIdStm() { - this._log.trace("Creating SQL statement: _annoAttrIdStm"); - let stm = this._db.createStatement( - "SELECT id from moz_anno_attributes WHERE name = :name"); - this.__defineGetter__("_annoAttrIdStm", function() stm); - return stm; - }, - - get _findPidByAnnoStm() { - this._log.trace("Creating SQL statement: _findPidByAnnoStm"); - let stm = this._db.createStatement( - "SELECT place_id AS id FROM moz_annos " + - "WHERE anno_attribute_id = :attr AND content = :content"); - this.__defineGetter__("_findPidByAnnoStm", function() stm); - return stm; - }, - // See bug 320831 for why we use SQL here _getVisits: function HistStore__getVisits(uri) { let visits = []; if (typeof(uri) != "string") uri = uri.spec; - let placeid = this._fetchRow(this._pidStm, {url: uri}, ['id']); - if (!placeid) { - this._log.debug("Could not find place ID for history URL: " + placeid); - return visits; - } - try { - this._visitStm.params.placeid = placeid; + this._visitStm.params.url = uri; while (this._visitStm.step()) { visits.push({date: this._visitStm.row.date, type: this._visitStm.row.type}); @@ -314,26 +204,13 @@ HistoryStore.prototype = { // See bug 468732 for why we use SQL here _findURLByGUID: function HistStore__findURLByGUID(guid) { try { - this._annoAttrIdStm.params.name = "weave/guid"; - if (!this._annoAttrIdStm.step()) - return null; - let annoId = this._annoAttrIdStm.row.id; - - this._findPidByAnnoStm.params.attr = annoId; - this._findPidByAnnoStm.params.content = guid; - if (!this._findPidByAnnoStm.step()) - return null; - let placeId = this._findPidByAnnoStm.row.id; - - this._urlStm.params.id = placeId; + this._urlStm.params.guid = guid; if (!this._urlStm.step()) return null; return {url: this._urlStm.row.url, title: this._urlStm.row.title}; } finally { - this._annoAttrIdStm.reset(); - this._findPidByAnnoStm.reset(); this._urlStm.reset(); } }, From c022700373b1e43af4ce7a7fa65255046539621b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 21 Aug 2009 14:29:35 -0700 Subject: [PATCH 1302/1860] Remove old dumping code for debugging bug 476903. --- services/sync/modules/engines/history.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 49920b1d56d8..ed98b24da28a 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -231,22 +231,6 @@ HistoryStore.prototype = { query.minVisits = 1; options.maxResults = 100; - /* - dump("SORT_BY_DATE_DESCENDING is " + options.SORT_BY_DATE_DESCENDING + "\n"); - dump("SORT_BY_ANNOTATIOn_DESCENDING is " + options.SORT_BY_ANNOTATION_DESCENDING + "\n"); - dump("BEFORE SETTING, options.sortingMode is " + options.sortingMode + "\n"); - - for ( let z = -1; z < 20; z++) { - try { - options.sortingMode = z; - dump(" -> Can set to " + z + " OK...\n"); - } catch (e) { - dump(" -> Setting to " + z + " raises exception.\n"); - } - } - dump("AFTER SETTING, options.sortingMode is " + options.sortingMode + "\n"); - */ - // TODO the following line throws exception on Fennec; see above. options.sortingMode = options.SORT_BY_DATE_DESCENDING; options.queryType = options.QUERY_TYPE_HISTORY; From 7497cbc05c21ec9ec68d5e05bcefc4d2ff2cb552 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 21 Aug 2009 14:29:37 -0700 Subject: [PATCH 1303/1860] Extend Utils.anno to handle pages and clean up GUID code for history engine. Also fix a bug where changeItemID would set annotations to expire on session. --- services/sync/modules/engines/history.js | 65 +++++++++--------------- services/sync/modules/util.js | 19 +++++-- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index ed98b24da28a..8f0a26e1898a 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -40,8 +40,9 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +const GUID_ANNO = "weave/guid"; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); @@ -50,6 +51,24 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/base_records/collection.js"); Cu.import("resource://weave/type_records/history.js"); +// Create some helper functions to handle GUIDs +function setGUID(uri, guid) { + if (arguments.length == 1) + guid = Utils.makeGUID(); + Utils.anno(uri, GUID_ANNO, guid, "WITH_HISTORY"); + return guid; +} +function GUIDForUri(uri) { + try { + // Use the existing GUID if it exists + return Utils.anno(uri, GUID_ANNO); + } + catch (ex) { + // Give the uri a GUID if it doesn't have one + return setGUID(uri); + } +} + function HistoryEngine() { this._init(); } @@ -126,13 +145,6 @@ HistoryStore.prototype = { return hsvc; }, - get _anno() { - let anno = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); - this.__defineGetter__("_ans", function() anno); - return anno; - }, - get _db() { return this._hsvc.DBConnection; }, @@ -162,7 +174,7 @@ HistoryStore.prototype = { "WHERE content = :guid AND anno_attribute_id = (" + "SELECT id " + "FROM moz_anno_attributes " + - "WHERE name = 'weave/guid'))"); + "WHERE name = '" + GUID_ANNO + "'))"); this.__defineGetter__("_urlStm", function() stm); return stm; }, @@ -186,21 +198,6 @@ HistoryStore.prototype = { return visits; }, - _getGUID: function HistStore__getGUID(uri) { - if (typeof(uri) == "string") - uri = Utils.makeURI(uri); - try { - return this._anno.getPageAnnotation(uri, "weave/guid"); - } catch (e) { - // FIXME - // if (e != NS_ERROR_NOT_AVAILABLE) - // throw e; - } - let guid = Utils.makeGUID(); - this._anno.setPageAnnotation(uri, "weave/guid", guid, 0, this._anno.EXPIRE_WITH_HISTORY); - return guid; - }, - // See bug 468732 for why we use SQL here _findURLByGUID: function HistStore__findURLByGUID(guid) { try { @@ -216,12 +213,7 @@ HistoryStore.prototype = { }, changeItemID: function HStore_changeItemID(oldID, newID) { - try { - let uri = Utils.makeURI(this._findURLByGUID(oldID).url); - this._anno.setPageAnnotation(uri, "weave/guid", newID, 0, 0); - } catch (e) { - this._log.debug("Could not change item ID: " + e); - } + setGUID(this._findURLByGUID(oldID).url, newID); }, @@ -241,7 +233,7 @@ HistoryStore.prototype = { let items = {}; for (let i = 0; i < root.childCount; i++) { let item = root.getChild(i); - let guid = this._getGUID(item.uri); + let guid = GUIDForUri(item.uri); items[guid] = item.uri; } return items; @@ -279,7 +271,7 @@ HistoryStore.prototype = { this._hsvc.setPageTitle(uri, record.title); // Equalize IDs - let localId = this._getGUID(record.histUri); + let localId = GUIDForUri(record.histUri); if (localId != record.id) this.changeItemID(localId, record.id); @@ -337,13 +329,6 @@ HistoryTracker.prototype = { return hsvc; }, - // FIXME: hack! - get _store() { - let store = new HistoryStore(); - this.__defineGetter__("_store", function() store); - return store; - }, - _init: function HT__init() { Tracker.prototype._init.call(this); this._hsvc.addObserver(this, false); @@ -365,7 +350,7 @@ HistoryTracker.prototype = { if (this.ignoreAll) return; this._log.trace("onVisit: " + uri.spec); - if (this.addChangedID(this._store._getGUID(uri))) + if (this.addChangedID(GUIDForUri(uri))) this._upScore(); }, onPageExpired: function HT_onPageExpired(uri, time, entry) { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 1b1b3f5daf9e..2bfb3f523b5b 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -132,16 +132,29 @@ let Utils = { }, anno: function anno(id, anno, val, expire) { + // Figure out if we have a bookmark or page + let annoFunc = (typeof id == "number" ? "Item" : "Page") + "Annotation"; + + // Convert to a nsIURI if necessary + if (typeof id == "string") + id = Utils.makeURI(id); + + if (id == null) + throw "Null id for anno! (invalid uri)"; + switch (arguments.length) { case 2: // Get the annotation with 2 args - return Svc.Annos.getItemAnnotation(id, anno); + return Svc.Annos["get" + annoFunc](id, anno); case 3: - expire = Svc.Annos.EXPIRE_NEVER; + expire = "NEVER"; // Fallthrough! case 4: + // Convert to actual EXPIRE value + expire = Svc.Annos["EXPIRE_" + expire]; + // Set the annotation with 3 or 4 args - return Svc.Annos.setItemAnnotation(id, anno, val, 0, expire); + return Svc.Annos["set" + annoFunc](id, anno, val, 0, expire); } }, From 1f09e0d28e1eeefe4bcdc165675b418fd24971bf Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 21 Aug 2009 17:32:41 -0700 Subject: [PATCH 1304/1860] Bug 512001 - Kill the Wizard! (and Account Pane + Login) Remove the Wizard Dialog, Account Pane, Login Dialog, Add-ons Pane, Advanced Dialog and their related xul/js/css/strings/images. Clean up some unused functions, xul:preferences, css, strings. Orphaned functionality: autoconnect preference, reset/forgot password/passphrase. --- services/sync/locales/en-US/login.dtd | 10 -- services/sync/locales/en-US/login.properties | 10 -- services/sync/locales/en-US/preferences.dtd | 62 ------------- .../sync/locales/en-US/preferences.properties | 8 -- services/sync/locales/en-US/wizard.dtd | 91 ------------------- services/sync/locales/en-US/wizard.properties | 81 ----------------- services/sync/modules/identity.js | 13 --- services/sync/modules/service.js | 10 +- services/sync/modules/util.js | 8 -- 9 files changed, 1 insertion(+), 292 deletions(-) delete mode 100644 services/sync/locales/en-US/login.dtd delete mode 100644 services/sync/locales/en-US/login.properties delete mode 100644 services/sync/locales/en-US/wizard.dtd delete mode 100644 services/sync/locales/en-US/wizard.properties diff --git a/services/sync/locales/en-US/login.dtd b/services/sync/locales/en-US/login.dtd deleted file mode 100644 index 8d3d5a48fb0e..000000000000 --- a/services/sync/locales/en-US/login.dtd +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/services/sync/locales/en-US/login.properties b/services/sync/locales/en-US/login.properties deleted file mode 100644 index 777d01fd9ffe..000000000000 --- a/services/sync/locales/en-US/login.properties +++ /dev/null @@ -1,10 +0,0 @@ -loginFailed.alert = Login failed -noUsername.alert = You must enter a username -noPassword.alert = You must enter a password -noPassphrase.alert = You must enter a passphrase -samePasswordAndPassphrase.alert = Your password and passphrase must be different -loginStart.label = Signing in, please wait... -loginError.label = Invalid username, password or passphrase. -loginSuccess.label = Signed In -hide.label = Hide -error.login.description = Error: %1$S diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 721f0dbccdde..357ef295b658 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -1,29 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - @@ -40,14 +19,10 @@ - - - - @@ -57,44 +32,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -107,8 +50,3 @@ - - - - - diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index 24711fa762f1..be845ef3328e 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -1,14 +1,6 @@ -# %S is the username of the signed in user -signedIn.description = Signed in as %S - erase.server.warning.title = Erase Server Data erase.server.warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? - reset.server.warning.title = Reset Server Data reset.server.warning = This will delete all data on the Weave server.\n\nYou must restart this and any other instances of Firefox you may have running on any computer once you do this.\n\nAre you sure you want to do this? reset.client.warning.title = Reset Client Data reset.client.warning = This will permanently delete all of your bookmarks and browsing history.\n\nAre you sure you want to do this? -reset.login.warning.title = Reset Login -reset.login.warning = This will permanently remove your username, password and passphrase from this instance of Firefox.\n\nAre you sure you want to do this? -reset.lock.warning.title = Reset Server Lock -reset.lock.warning = This will reset the write lock on the Weave server for your account. To avoid corruption, you should only do this if a lock has become stale.\n\nAre you sure you want to do this? diff --git a/services/sync/locales/en-US/wizard.dtd b/services/sync/locales/en-US/wizard.dtd deleted file mode 100644 index b04ad0406644..000000000000 --- a/services/sync/locales/en-US/wizard.dtd +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/services/sync/locales/en-US/wizard.properties b/services/sync/locales/en-US/wizard.properties deleted file mode 100644 index bcae4db070ad..000000000000 --- a/services/sync/locales/en-US/wizard.properties +++ /dev/null @@ -1,81 +0,0 @@ -verify-progress.label = Verifying username and password -verify-error.label = Invalid username and password -verify-success.label = Username and password verified -passphrase-progress.label = Verifying passphrase -passphrase-success.label = Passphrase verified -passphrase-error.label = Invalid passphrase - -serverError.label = Server Error -serverTimeoutError.label = Server Timeout - - -createUsername-progress.label = Checking username -createUsername-error.label = %S is already taken. Please choose another. -createUsername-success.label = %S is available. - -email-progress.label = Checking email -email-unavailable.label = %S is already taken. Please choose another. -email-invalid.label = Please re-enter a valid email address. -email-success.label = - - -passwordsUnmatched.label = Passwords do not match. -passphrasesUnmatched.label = Passphrases do not match. -samePasswordAndUsername.label = Your password and username must be different. -samePasswordAndPassphrase.label = Your password and passphrase must be different. - - - -missingCaptchaResponse.label = Please enter the words in the Captcha box. -incorrectCaptcha.label = Incorrect Captcha response. Try again. - - - -create-progress.label = Creating your account -create-uid-inuse.label = Username in use -create-uid-missing.label = Username missing -create-uid-invalid.label = Username invalid -create-mail-invalid.label = Email invalid -create-mail-inuse.label = Emali in use -create-captcha-missing.label = Captcha response missing -create-password-missing.label = Password missing -create-password-incorrect.label = Password incorrect - -create-success.label = Account for %S created. - -final-pref-value.label = %S -final-account-value.label = Username: %S -final-sync-value.label = Backup and synchronization of browser settings and metadata. -final-success.label = Weave was successfully installed. - -default-name.label = %S's Firefox -default-name-nouser.label = Firefox - -bookmarks.label = Bookmarks -history.label = Browsing History -tabs.label = Tabs -passwords.label = Saved Passwords -forms.label = Saved Form Data -cookies.label = Cookies - -initialLogin-progress.label = Signing you in -initialLogin-error.label = Problem signing in. - -initialPrefs-progress.label = Setting your preferences - -initialSync-progress.label = Synchronizing your data -initialSync-error.label = Problem syncing data. - -installation-complete.label = Installation complete. - - -tryAgain.text = Try again now. -clickHere.text = Click here - -data-verify.title = Data (Step 2 of 3) -data-create.title = Data (Step 4 of 5) -final-verify.title = Finish (Step 3 of 3) -final-create.title = Finish (Step 5 of 5) - -registration-closed.title = Registration Closed -registration-closed.label = Sorry, but we've reached our account limit for this round of alpha testing. Please try again in a few days. diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 2da038bb5455..508fda2b6e31 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -131,18 +131,5 @@ Identity.prototype = { setTempPassword: function Id_setTempPassword(value) { this._password = value; - }, - - // we'll call this if set to call out to the ui to prompt the user - // This function takes a callback function to pass back the password - onGetPassword: null, - - // Attempts to get the password from the user if not set - getPassword: function Identity_getPassword() { - if (this.password) - return this.password; - - if (typeof this.onGetPassword == "function") - return this.password = Sync(this.onGetPassword)(); } }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9e67de0b0235..094cdefcb707 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -201,14 +201,6 @@ WeaveSvc.prototype = { ID.get('WeaveCryptoID').password = value; }, - // chrome-provided callbacks for when the service needs a password/passphrase - set onGetPassword(value) { - ID.get('WeaveID').onGetPassword = value; - }, - set onGetPassphrase(value) { - ID.get('WeaveCryptoID').onGetPassword = value; - }, - get baseURL() { let url = Svc.Prefs.get("serverURL"); if (!url) @@ -952,7 +944,7 @@ WeaveSvc.prototype = { } let passphrase = ID.get("WeaveCryptoID"); - if (passphrase.getPassword()) { + if (passphrase.password) { let keys = PubKeys.createKeypair(passphrase, PubKeys.defaultKeyUri, PrivKeys.defaultKeyUri); try { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 2bfb3f523b5b..42f366b1ccbf 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -672,10 +672,6 @@ let Utils = { this.openDialog("ChangeSomething", "generic-change.xul"); }, - openLogin: function Utils_openLogin() { - Utils.openDialog("Login", "login.xul"); - }, - openShare: function Utils_openShare() { Utils.openDialog("Share", "share.xul"); }, @@ -691,10 +687,6 @@ let Utils = { Utils._openChromeWindow("Sync", "pick-sync.xul"); }, - openWizard: function Utils_openWizard() { - Utils._openChromeWindow("Wizard", "wizard.xul"); - }, - // assumes an nsIConverterInputStream readStream: function Weave_readStream(is) { let ret = "", str = {}; From ad6bc4f43de74f51d0d8345332f77d5ff8e7dfe6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 24 Aug 2009 16:17:59 -0700 Subject: [PATCH 1305/1860] Create records with the correct parent/pred if it's still waiting for them to sync. --- services/sync/modules/engines/bookmarks.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 1f6a0d709ca6..d78063c4bead 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -656,6 +656,13 @@ BookmarksStore.prototype = { }, _getParentGUIDForId: function BStore__getParentGUIDForId(itemId) { + // Give the parent annotation if it exists + try { + // XXX Work around Bug 510628 by removing prepended parenT + return Utils.anno(itemId, PARENT_ANNO).slice(1); + } + catch(ex) {} + let parentid = this._bms.getFolderIdForItem(itemId); if (parentid == -1) { this._log.debug("Found orphan bookmark, reparenting to unfiled"); @@ -666,6 +673,13 @@ BookmarksStore.prototype = { }, _getPredecessorGUIDForId: function BStore__getPredecessorGUIDForId(itemId) { + // Give the predecessor annotation if it exists + try { + // XXX Work around Bug 510628 by removing prepended predecessoR + return Utils.anno(itemId, PREDECESSOR_ANNO).slice(1); + } + catch(ex) {} + // Figure out the predecessor, unless it's the first item let itemPos = Svc.Bookmark.getItemIndex(itemId); if (itemPos == 0) From 82b4490e2205c1f8973f4424bfb9150d06e118ec Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 25 Aug 2009 00:47:35 -0400 Subject: [PATCH 1306/1860] bug 512393 - l10n cleanup for 0.6, includes fix for bug 511548 as well --- services/sync/locales/en-US/fennec.properties | 1 + services/sync/locales/en-US/log.dtd | 2 - .../sync/locales/en-US/pick-sync.properties | 10 ----- services/sync/locales/en-US/preferences.dtd | 39 ++----------------- .../sync/locales/en-US/preferences.properties | 6 +-- services/sync/locales/en-US/status.properties | 2 +- services/sync/locales/en-US/sync.dtd | 4 +- services/sync/locales/en-US/sync.properties | 21 +++++----- services/sync/modules/util.js | 26 +++++++++++++ 9 files changed, 44 insertions(+), 67 deletions(-) diff --git a/services/sync/locales/en-US/fennec.properties b/services/sync/locales/en-US/fennec.properties index 34e69f9f4d71..50da8e7d19eb 100644 --- a/services/sync/locales/en-US/fennec.properties +++ b/services/sync/locales/en-US/fennec.properties @@ -7,6 +7,7 @@ fennec.login.error.detail = Login error: %S fennec.default.client.type = Mobile +fennec.sync.start = Syncing Now... fennec.sync.complete.time = Sync completed at %S, %S fennec.sync.error.detail = Error: %S fennec.sync.error.generic = Weave had an error when trying to sync. diff --git a/services/sync/locales/en-US/log.dtd b/services/sync/locales/en-US/log.dtd index f5d44b37727b..9b980e94d523 100644 --- a/services/sync/locales/en-US/log.dtd +++ b/services/sync/locales/en-US/log.dtd @@ -1,5 +1,3 @@ - - diff --git a/services/sync/locales/en-US/pick-sync.properties b/services/sync/locales/en-US/pick-sync.properties index b443d9d7eb9a..b9c42b228349 100644 --- a/services/sync/locales/en-US/pick-sync.properties +++ b/services/sync/locales/en-US/pick-sync.properties @@ -1,15 +1,5 @@ -dialog.title = Sync with Weave -dialog.accept = Sync - -dir.caption = Direction to sync - -dir.resetClient = Do a fresh, full sync with the server -dir.wipeClient = Erase local data and restart with server data -dir.wipeRemote = Erase remote data and restart with local data - dir.wipeClient.warning.title = Confirm Erase Local Data dir.wipeClient.warning = This will erase your local data.\n\nAre you sure you want to do this? dir.wipeRemote.warning.title = Confirm Erase Remote Data dir.wipeRemote.warning = This will erase your remote data.\n\nAre you sure you want to do this? -data.caption = Data to sync diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 357ef295b658..5d3be795e85e 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -1,43 +1,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties index be845ef3328e..774792ff62ed 100644 --- a/services/sync/locales/en-US/preferences.properties +++ b/services/sync/locales/en-US/preferences.properties @@ -1,6 +1,2 @@ erase.server.warning.title = Erase Server Data -erase.server.warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? -reset.server.warning.title = Reset Server Data -reset.server.warning = This will delete all data on the Weave server.\n\nYou must restart this and any other instances of Firefox you may have running on any computer once you do this.\n\nAre you sure you want to do this? -reset.client.warning.title = Reset Client Data -reset.client.warning = This will permanently delete all of your bookmarks and browsing history.\n\nAre you sure you want to do this? +erase.server.warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? \ No newline at end of file diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index 2019a0699c36..d34a23fa28b7 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -3,7 +3,7 @@ dialog.accept = Hide status.wait = Waiting for Current Sync to Finish status.active = Syncing with Weave status.success = Sync Complete -status.error = Sync Failed +status.error = Sync Failed (%1$S) status.closing = Closing... status.locked = Server Busy diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index dd20b4411d78..55d1453379f4 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -1,9 +1,9 @@ - + - + diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 203acb17132e..c1e76cea75dc 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -1,28 +1,25 @@ # %S is the date and time at which the last sync successfully completed lastSync.label = Last Update: %S -weaveButtonOffline.label = Sign In -weaveButtonOnline.label = Weave -shareBookmark.menuItem = Share This Folder... -unShareBookmark.menuItem = Stop Sharing This Folder +#weaveButtonOffline.label = Sign In +#weaveButtonOnline.label = Weave +#shareBookmark.menuItem = Share This Folder... +#unShareBookmark.menuItem = Stop Sharing This Folder status.offline = Not Signed In # The next two are not normally used, as we now display the username # when the user is logged in. But if for some reason we can't get the username, # we display these. -status.idle = Idle -status.active = Working... +#status.idle = Idle +#status.active = Working... error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. -error.login.reason.password = Your username/password failed -error.login.reason.unknown = Unknown error -error.login.reason.passphrase = Invalid passphrase -error.login.reason.network = Network error +# should decide if we're going to show this error.logout.title = Error While Signing Out -error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it (then why are we bugging you with this info?). +error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it. error.sync.title = Error While Syncing error.sync.description = Weave encountered an error while syncing. You might want to try syncing manually to make sure you are up-to-date. -error.sync.tryAgainButton.label = Sync Now... +error.sync.tryAgainButton.label = Sync Now error.sync.tryAgainButton.accesskey = S diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 42f366b1ccbf..8c4b63d7ab53 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -687,6 +687,32 @@ let Utils = { Utils._openChromeWindow("Sync", "pick-sync.xul"); }, + __errorBundle: null, + get _errorBundle() { + if (!this.__errorBundle) { + this.__errorBundle = new StringBundle("chrome://weave/locales/errors.properties"); + } + return this.__errorBundle; + }, + + getErrorString: function Utils_getErrorString(error, args) { + switch (error) { + case Weave.LOGIN_FAILED_NETWORK_ERROR: + errorString = "error.login.reason.network"; + break; + case Weave.LOGIN_FAILED_INVALID_PASSPHRASE: + errorString = "error.login.reason.passphrase"; + break; + case Weave.LOGIN_FAILED_LOGIN_REJECTED: + errorString = "error.login.reason.password"; + break; + default: + errorString = "error.login.reason.unknown"; + break; + } + return this._errorBundle.get(errorString, args || null); + }, + // assumes an nsIConverterInputStream readStream: function Weave_readStream(is) { let ret = "", str = {}; From b9c8602c47baedd7e3b601ed8701ed5701527dd7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 25 Aug 2009 08:52:52 -0700 Subject: [PATCH 1307/1860] Remove unused alias for wbo/identity. --- services/sync/modules/base_records/wbo.js | 24 ++--------------------- services/sync/modules/identity.js | 12 ------------ 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 468fc63405e7..002786a1f127 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -126,7 +126,6 @@ RecordManager.prototype = { _init: function RegordMgr__init() { this._log = Log4Moz.repository.getLogger(this._logName); this._records = {}; - this._aliases = {}; }, import: function RecordMgr_import(url) { @@ -148,17 +147,10 @@ RecordManager.prototype = { }, get: function RecordMgr_get(url) { - // Note: using url object directly as key for this._records cache does not - // work because different url objects (even pointing to the same place) are - // different objects and therefore not equal. So always use the string, not - // the object, as a key. - // TODO: use the string as key for this._aliases as well? (Don't know) + // Use a url string as the key to the hash let spec = url.spec ? url.spec : url; if (spec in this._records) return this._records[spec]; - - if (url in this._aliases) - url = this._aliases[url]; return this.import(url); }, @@ -168,10 +160,7 @@ RecordManager.prototype = { }, contains: function RegordMgr_contains(url) { - let record = null; - if (url in this._aliases) - url = this._aliases[url]; - if (url in this._records) + if ((url.spec || url) in this._records) return true; return false; }, @@ -182,14 +171,5 @@ RecordManager.prototype = { del: function RegordMgr_del(url) { delete this._records[url]; - }, - getAlias: function RegordMgr_getAlias(alias) { - return this._aliases[alias]; - }, - setAlias: function RegordMgr_setAlias(url, alias) { - this._aliases[alias] = url; - }, - delAlias: function RegordMgr_delAlias(alias) { - delete this._aliases[alias]; } }; diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 508fda2b6e31..0c138e353a71 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -51,12 +51,9 @@ Utils.lazy(this, 'ID', IDManager); // For storing identities we'll use throughout Weave function IDManager() { this._ids = {}; - this._aliases = {}; } IDManager.prototype = { get: function IDMgr_get(name) { - if (name in this._aliases) - return this._ids[this._aliases[name]]; if (name in this._ids) return this._ids[name]; return null; @@ -67,15 +64,6 @@ IDManager.prototype = { }, del: function IDMgr_del(name) { delete this._ids[name]; - }, - getAlias: function IDMgr_getAlias(alias) { - return this._aliases[alias]; - }, - setAlias: function IDMgr_setAlias(name, alias) { - this._aliases[alias] = name; - }, - delAlias: function IDMgr_delAlias(alias) { - delete this._aliases[alias]; } }; From 955437633b311b8bee83da37e6d754ef46d96095 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 25 Aug 2009 16:15:05 -0700 Subject: [PATCH 1308/1860] Don't include changes to special places Library "exclude from backup" items. --- services/sync/modules/engines.js | 5 +++-- services/sync/modules/engines/bookmarks.js | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index eea82b7e28aa..758e430e413a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -395,6 +395,9 @@ SyncEngine.prototype = { // case when syncing for the first time two machines that already have the // same bookmarks. In this case we change the IDs to match. _reconcile: function SyncEngine__reconcile(item) { + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace("Incoming: " + item); + // Step 1: Check for conflicts // If same as local record, do not upload this._log.trace("Reconcile step 1"); @@ -435,8 +438,6 @@ SyncEngine.prototype = { // Apply incoming records _applyIncoming: function SyncEngine__applyIncoming(item) { - if (this._log.level <= Log4Moz.Level.Trace) - this._log.trace("Incoming: " + item); try { this._tracker.ignoreAll = true; this._store.applyIncoming(item); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 1f6a0d709ca6..2e966de7413b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -865,6 +865,12 @@ BookmarksTracker.prototype = { if (this._ignore(itemId)) return; + // Make sure to remove items that now have the exclude annotation + if (property == "places/excludeFromBackup") { + this.removeChangedID(GUIDForId(itemId)); + return; + } + // ignore annotations except for the ones that we sync let annos = ["bookmarkProperties/description", "bookmarkProperties/loadInSidebar", "bookmarks/staticTitle", From d73d26bceafbf9b04c04b3b25c77c2050452173d Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Tue, 25 Aug 2009 17:06:13 -0700 Subject: [PATCH 1309/1860] Bug 507433 - Update client to use the weave 0.5 server. r-Mardak Initial 0.5 server API switch not quite working. --- services/sync/modules/engines.js | 8 +++-- services/sync/modules/service.js | 58 ++++++++++++++------------------ 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 758e430e413a..1a56011e0342 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -225,16 +225,18 @@ SyncEngine.prototype = { return null; if (url[url.length-1] != '/') url += '/'; - url += "0.3/user/"; + url += "0.5/"; return url; }, get engineURL() { - return this.baseURL + ID.get('WeaveID').username + '/' + this.name + '/'; + return this.baseURL + ID.get('WeaveID').username + + '/storage/' + this.name + '/'; }, get cryptoMetaURL() { - return this.baseURL + ID.get('WeaveID').username + '/crypto/' + this.name; + return this.baseURL + ID.get('WeaveID').username + + '/storage/crypto/' + this.name; }, get lastSync() { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 094cdefcb707..79ad5d943283 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -207,7 +207,6 @@ WeaveSvc.prototype = { throw "No server URL set"; if (url[url.length-1] != '/') url += '/'; - url += "0.3/"; return url; }, set baseURL(value) { @@ -220,7 +219,7 @@ WeaveSvc.prototype = { return null; if (url[url.length-1] != '/') url += '/'; - url += "0.3/user/"; + url += "0.5/"; return url; }, set clusterURL(value) { @@ -262,8 +261,8 @@ WeaveSvc.prototype = { _genKeyURLs: function WeaveSvc__genKeyURLs() { let url = this.clusterURL + this.username; - PubKeys.defaultKeyUri = url + "/keys/pubkey"; - PrivKeys.defaultKeyUri = url + "/keys/privkey"; + PubKeys.defaultKeyUri = url + "/storage/keys/pubkey"; + PrivKeys.defaultKeyUri = url + "/storage/keys/privkey"; }, _checkCrypto: function WeaveSvc__checkCrypto() { @@ -458,7 +457,8 @@ WeaveSvc.prototype = { findCluster: function WeaveSvc_findCluster(username) { this._log.debug("Finding cluster for user " + username); - let res = new Resource(this.baseURL + "api/register/chknode/" + username); + let res = new Resource(this.baseURL + "user/1/" + + username + '/node/weave'); try { res.get(); } catch(ex) {} @@ -519,11 +519,8 @@ WeaveSvc.prototype = { if (isLogin) this.clusterURL = url; - if (url[url.length-1] != '/') - url += '/'; - url += "0.3/user/"; - - let res = new Resource(url + username); + let res = new Resource(this.clusterURL + username + + '/info/collections'); res.authenticator = { onRequest: function(headers) { headers['Authorization'] = 'Basic ' + btoa(username + ':' + password); @@ -603,16 +600,12 @@ WeaveSvc.prototype = { changePassword: function WeaveSvc_changePassword(newpass) this._catch(this._notify("changepwd", "", function() { - function enc(x) encodeURIComponent(x); - let message = "uid=" + enc(this.username) + "&password=" + - enc(this.password) + "&new=" + enc(newpass); - let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/chpwd'; + let url = Svc.Prefs.get('tmpServerURL') + 'user/1/' + + username + "/password"; let res = new Weave.Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); - res.setHeader("Content-Type", "application/x-www-form-urlencoded", - "Content-Length", message.length); - let resp = res.post(message); + let resp = res.post(newpass); if (res.lastChannel.responseStatus != 200) { this._log.info("Password change failed: " + resp); throw "Could not change password"; @@ -773,9 +766,7 @@ WeaveSvc.prototype = { }, checkUsername: function WeaveSvc_checkUsername(username) { - let url = Svc.Prefs.get('tmpServerURL') + - "0.3/api/register/checkuser/" + username; - + let url = Svc.Prefs.get('tmpServerURL') + "/user/1/" + username; let res = new Resource(url); res.authenticator = new NoOpAuthenticator(); @@ -792,28 +783,27 @@ WeaveSvc.prototype = { }, createAccount: function WeaveSvc_createAccount(username, password, email, - captchaChallenge, captchaResponse) { - function enc(x) encodeURIComponent(x); - let message = "uid=" + enc(username) + "&password=" + enc(password) + - "&mail=" + enc(email) + "&recaptcha_challenge_field=" + - enc(captchaChallenge) + "&recaptcha_response_field=" + enc(captchaResponse); + captchaChallenge, captchaResponse) + { + let payload = JSON.stringify({ + "password": password, "email": email, + "captcha_challenge": captchaChallenge, + "captcha_response": captchaResponse + }); - let url = Svc.Prefs.get('tmpServerURL') + '0.3/api/register/new'; + let url = Svc.Prefs.get('tmpServerURL') + 'user/1/' + username; let res = new Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); - res.setHeader("Content-Type", "application/x-www-form-urlencoded", - "Content-Length", message.length); let ret = {}; try { - ret.response = res.post(message); + ret.response = res.put(payload); ret.status = res.lastChannel.responseStatus; // No exceptions must have meant it was successful this._log.info("Account created: " + ret.response); return ret; - } - catch(ex) { + } catch(ex) { this._log.warn("Failed to create account: " + ex); let status = ex.request.responseStatus; switch (status) { @@ -837,7 +827,8 @@ WeaveSvc.prototype = { let reset = false; this._log.debug("Fetching global metadata record"); - let meta = Records.import(this.clusterURL + this.username + "/meta/global"); + let meta = Records.import(this.clusterURL + this.username + + "/storage/meta/global"); let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; @@ -1248,7 +1239,8 @@ WeaveSvc.prototype = { Sync.sleep(2000); this._log.debug("Uploading new metadata record"); - meta = new WBORecord(this.clusterURL + this.username + "/meta/global"); + meta = new WBORecord(this.clusterURL + + this.username + "/storage/meta/global"); meta.payload.syncID = Clients.syncID; this._updateRemoteVersion(meta); }, From f28bd82f5c4dfdf9784ff5b4ef4e0615b40cd44e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 25 Aug 2009 17:15:36 -0700 Subject: [PATCH 1310/1860] Make an alias to the clusterURL + username as userURL. Fix whitespace problems. --- services/sync/modules/service.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 79ad5d943283..8bf595aae204 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -227,6 +227,10 @@ WeaveSvc.prototype = { this._genKeyURLs(); }, + get userURL() { + return this.clusterURL + this.username; + }, + get userPath() { return ID.get('WeaveID').username; }, get isLoggedIn() { return this._loggedIn; }, @@ -260,7 +264,7 @@ WeaveSvc.prototype = { }, _genKeyURLs: function WeaveSvc__genKeyURLs() { - let url = this.clusterURL + this.username; + let url = this.userURL; PubKeys.defaultKeyUri = url + "/storage/keys/pubkey"; PrivKeys.defaultKeyUri = url + "/storage/keys/privkey"; }, @@ -457,12 +461,11 @@ WeaveSvc.prototype = { findCluster: function WeaveSvc_findCluster(username) { this._log.debug("Finding cluster for user " + username); - let res = new Resource(this.baseURL + "user/1/" + - username + '/node/weave'); + let res = new Resource(this.baseURL + "user/1/" + username + "/node/weave"); try { res.get(); } catch(ex) {} - + try { switch (res.lastChannel.responseStatus) { case 404: @@ -519,8 +522,7 @@ WeaveSvc.prototype = { if (isLogin) this.clusterURL = url; - let res = new Resource(this.clusterURL + username + - '/info/collections'); + let res = new Resource(this.userURL + "/info/collections"); res.authenticator = { onRequest: function(headers) { headers['Authorization'] = 'Basic ' + btoa(username + ':' + password); @@ -783,7 +785,7 @@ WeaveSvc.prototype = { }, createAccount: function WeaveSvc_createAccount(username, password, email, - captchaChallenge, captchaResponse) + captchaChallenge, captchaResponse) { let payload = JSON.stringify({ "password": password, "email": email, @@ -827,8 +829,7 @@ WeaveSvc.prototype = { let reset = false; this._log.debug("Fetching global metadata record"); - let meta = Records.import(this.clusterURL + this.username + - "/storage/meta/global"); + let meta = Records.import(this.userURL + "/storage/meta/global"); let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; @@ -1239,8 +1240,7 @@ WeaveSvc.prototype = { Sync.sleep(2000); this._log.debug("Uploading new metadata record"); - meta = new WBORecord(this.clusterURL + - this.username + "/storage/meta/global"); + meta = new WBORecord(this.userURL + "/storage/meta/global"); meta.payload.syncID = Clients.syncID; this._updateRemoteVersion(meta); }, @@ -1265,7 +1265,7 @@ WeaveSvc.prototype = { wipeServer: function WeaveSvc_wipeServer(engines) this._catch(this._notify("wipe-server", "", function() { // Grab all the collections for the user - let userURL = this.clusterURL + this.username + "/"; + let userURL = this.userURL + "/"; let res = new Resource(userURL); res.get(); From 5486c9a707dd274555bc9a29aa33bd25ca514033 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 25 Aug 2009 17:42:25 -0700 Subject: [PATCH 1311/1860] Just strip out " from cluster response because Spidermonkey JSON.parse does not allow literals. --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8bf595aae204..7edf9c97b550 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -472,6 +472,8 @@ WeaveSvc.prototype = { this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); return Svc.Prefs.get("serverURL"); case 200: + // The server gives JSON of a string literal, but JS doesn't allow it + res.data = res.data.replace(/"/g, ""); return "https://" + res.data + "/"; default: this._log.debug("Unexpected response code trying to find cluster: " + res.lastChannel.responseStatus); From edca62d1da84934618d288f2d5c05ad45fa22925 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 25 Aug 2009 17:43:40 -0700 Subject: [PATCH 1312/1860] Fix wipeServer to use the new 0.5 API: get list at info/collections, a hash; delete storage/. --- services/sync/modules/service.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7edf9c97b550..53537eda30e1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1267,19 +1267,18 @@ WeaveSvc.prototype = { wipeServer: function WeaveSvc_wipeServer(engines) this._catch(this._notify("wipe-server", "", function() { // Grab all the collections for the user - let userURL = this.userURL + "/"; - let res = new Resource(userURL); + let res = new Resource(this.userURL + "/info/collections"); res.get(); // Get the array of collections and delete each one let allCollections = JSON.parse(res.data); - for each (let name in allCollections) { + for (let name in allCollections) { try { // If we have a list of engines, make sure it's one we want if (engines && engines.indexOf(name) == -1) continue; - new Resource(userURL + name).delete(); + new Resource(this.userURL + "/storage/" + name).delete(); } catch(ex) { this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); From 0ef4384a7adabc92348f8e44e6757fd04d5f061f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 25 Aug 2009 18:04:46 -0700 Subject: [PATCH 1313/1860] Read out the timestamp of the POST response header because the modified time is not part of the response data. --- services/sync/modules/engines.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 1a56011e0342..894a75ef02ec 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -465,8 +465,12 @@ SyncEngine.prototype = { let doUpload = Utils.bind2(this, function(desc) { this._log.info("Uploading " + desc + " of " + outnum + " records"); up.post(); - if (up.data.modified > this.lastSync) - this.lastSync = up.data.modified; + + // Record the modified time of the upload + let modified = up._lastChannel.getResponseHeader("X-Weave-Timestamp"); + if (modified > this.lastSync) + this.lastSync = modified; + up.clearRecords(); }); From 4e0ddac9910740f5b809db4a2fe5199937e9e284 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Wed, 26 Aug 2009 00:05:57 -0700 Subject: [PATCH 1314/1860] Change Resource.get() semantics and support X-Weave-Alert (bug #478330) --- services/sync/modules/engines.js | 16 +++++++++--- services/sync/modules/resource.js | 21 +++++++++++++-- services/sync/modules/service.js | 43 ++++++++++++++++++++++++++----- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 758e430e413a..803e090b3e47 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -218,7 +218,10 @@ SyncEngine.prototype = { __proto__: Engine.prototype, _recordObj: CryptoWrapper, - + + _alerts: "", + get alerts() { return this._alerts; }, + get baseURL() { let url = Svc.Prefs.get("clusterURL"); if (!url) @@ -341,8 +344,15 @@ SyncEngine.prototype = { Sync.sleep(0); }); - newitems.get(); - + let resp = newitems.get(); + try { + // we only need to store the latest alert + this._alerts = resp.getHeader("X-Weave-Alert"); + } catch (e) { + // no alert headers were present + this._alerts = ""; + } + if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index c4afb9176959..c435832340cb 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -50,6 +50,23 @@ Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/auth.js"); +// = RequestResponse = +// +// This object is returned on a successful +// HTTP response (codes 200-300) and encapsulates +// returned headers and the actual response text +function RequestResponse(channel, response) { + this._channel = channel; + this._response = response; +} +RequestResponse.prototype = { + get response() { return this._response }, + get status() { return this._channel.responseStatus }, + getHeader: function ReqRe_getHeader(which) { + return this._channel.getResponseHeader(which); + } +}; + // = RequestException = // // This function raises an exception through the call @@ -293,7 +310,7 @@ Resource.prototype = { this._log.debug(action + " request failed (" + channel.responseStatus + ")"); if (this._data) this._log.debug("Error response: " + this._data); - throw new RequestException(this, action, channel); + return new RequestResponse(channel, this._data); } else { this._log.debug(action + " request successful (" + channel.responseStatus + ")"); @@ -313,7 +330,7 @@ Resource.prototype = { } } - return this._data; + return new RequestResponse(channel, this._data); }, // ** {{{ Resource.get }}} ** diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 094cdefcb707..a91a6c675b69 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -172,6 +172,10 @@ WeaveSvc.prototype = { // Timer object for automagically syncing _syncTimer: null, + // Current alerts + _alerts: "[]", + get alerts() { return JSON.parse(this._alerts); }, + get username() { return Svc.Prefs.get("username", ""); }, @@ -532,17 +536,38 @@ WeaveSvc.prototype = { }; // login may fail because of cluster change + let response; try { - res.get(); + response = res.get(); } catch (e) {} - try { - switch (res.lastChannel.responseStatus) { + if (response) { + switch (response.status) { case 200: - if (passphrase && !this.verifyPassphrase(username, password, passphrase)) { + if (passphrase && + !this.verifyPassphrase(username, password, passphrase)) { this._setSyncFailure(LOGIN_FAILED_INVALID_PASSPHRASE); return false; } + + // check for alerts + try { + let alerts = response.getHeader("X-Weave-Alert"); + if (this._alerts != alerts) { + this._alerts = alerts; + Svc.Observer.notifyObservers( + null, "weave:service:alerts:changed", "" + ); + } + } catch (e) { + // no alert headers were present + if (this.alerts.length != 0) { + this._alerts = ""; + Svc.Observer.notifyObservers( + null, "weave:service:alerts:changed", "" + ); + } + } return true; case 401: if (this.updateCluster(username)) @@ -554,9 +579,9 @@ WeaveSvc.prototype = { default: throw "unexpected HTTP response: " + res.lastChannel.responseStatus; } - } catch (e) { + } else { // if we get here, we have either a busted channel or a network error - this._log.debug("verifyLogin failed: " + e) + this._log.debug("verifyLogin failed: network error"); this._setSyncFailure(LOGIN_FAILED_NETWORK_ERROR); throw e; } @@ -1222,6 +1247,12 @@ WeaveSvc.prototype = { _syncEngine: function WeaveSvc__syncEngine(engine) { try { engine.sync(); + if (this._alerts != engine.alerts) { + this._alerts = engine.alerts; + Svc.Observer.notifyObservers( + null, "weave:service:alerts:changed", "" + ); + } return true; } catch(e) { From ed89623557624ad5db80fe96d8cb19087d59d322 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 01:50:36 -0700 Subject: [PATCH 1315/1860] Backed out changeset 129ca9a54aed due to burning test_auth_manager: FAIL test_resource: FAIL --- services/sync/modules/engines.js | 16 +++--------- services/sync/modules/resource.js | 21 ++------------- services/sync/modules/service.js | 43 +++++-------------------------- 3 files changed, 11 insertions(+), 69 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 803e090b3e47..758e430e413a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -218,10 +218,7 @@ SyncEngine.prototype = { __proto__: Engine.prototype, _recordObj: CryptoWrapper, - - _alerts: "", - get alerts() { return this._alerts; }, - + get baseURL() { let url = Svc.Prefs.get("clusterURL"); if (!url) @@ -344,15 +341,8 @@ SyncEngine.prototype = { Sync.sleep(0); }); - let resp = newitems.get(); - try { - // we only need to store the latest alert - this._alerts = resp.getHeader("X-Weave-Alert"); - } catch (e) { - // no alert headers were present - this._alerts = ""; - } - + newitems.get(); + if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index c435832340cb..c4afb9176959 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -50,23 +50,6 @@ Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/auth.js"); -// = RequestResponse = -// -// This object is returned on a successful -// HTTP response (codes 200-300) and encapsulates -// returned headers and the actual response text -function RequestResponse(channel, response) { - this._channel = channel; - this._response = response; -} -RequestResponse.prototype = { - get response() { return this._response }, - get status() { return this._channel.responseStatus }, - getHeader: function ReqRe_getHeader(which) { - return this._channel.getResponseHeader(which); - } -}; - // = RequestException = // // This function raises an exception through the call @@ -310,7 +293,7 @@ Resource.prototype = { this._log.debug(action + " request failed (" + channel.responseStatus + ")"); if (this._data) this._log.debug("Error response: " + this._data); - return new RequestResponse(channel, this._data); + throw new RequestException(this, action, channel); } else { this._log.debug(action + " request successful (" + channel.responseStatus + ")"); @@ -330,7 +313,7 @@ Resource.prototype = { } } - return new RequestResponse(channel, this._data); + return this._data; }, // ** {{{ Resource.get }}} ** diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a91a6c675b69..094cdefcb707 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -172,10 +172,6 @@ WeaveSvc.prototype = { // Timer object for automagically syncing _syncTimer: null, - // Current alerts - _alerts: "[]", - get alerts() { return JSON.parse(this._alerts); }, - get username() { return Svc.Prefs.get("username", ""); }, @@ -536,38 +532,17 @@ WeaveSvc.prototype = { }; // login may fail because of cluster change - let response; try { - response = res.get(); + res.get(); } catch (e) {} - if (response) { - switch (response.status) { + try { + switch (res.lastChannel.responseStatus) { case 200: - if (passphrase && - !this.verifyPassphrase(username, password, passphrase)) { + if (passphrase && !this.verifyPassphrase(username, password, passphrase)) { this._setSyncFailure(LOGIN_FAILED_INVALID_PASSPHRASE); return false; } - - // check for alerts - try { - let alerts = response.getHeader("X-Weave-Alert"); - if (this._alerts != alerts) { - this._alerts = alerts; - Svc.Observer.notifyObservers( - null, "weave:service:alerts:changed", "" - ); - } - } catch (e) { - // no alert headers were present - if (this.alerts.length != 0) { - this._alerts = ""; - Svc.Observer.notifyObservers( - null, "weave:service:alerts:changed", "" - ); - } - } return true; case 401: if (this.updateCluster(username)) @@ -579,9 +554,9 @@ WeaveSvc.prototype = { default: throw "unexpected HTTP response: " + res.lastChannel.responseStatus; } - } else { + } catch (e) { // if we get here, we have either a busted channel or a network error - this._log.debug("verifyLogin failed: network error"); + this._log.debug("verifyLogin failed: " + e) this._setSyncFailure(LOGIN_FAILED_NETWORK_ERROR); throw e; } @@ -1247,12 +1222,6 @@ WeaveSvc.prototype = { _syncEngine: function WeaveSvc__syncEngine(engine) { try { engine.sync(); - if (this._alerts != engine.alerts) { - this._alerts = engine.alerts; - Svc.Observer.notifyObservers( - null, "weave:service:alerts:changed", "" - ); - } return true; } catch(e) { From 12839c1f7b065982a134b7abde627381554ce778 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 26 Aug 2009 13:03:33 -0400 Subject: [PATCH 1316/1860] add missing pick-sync.dtd --- services/sync/locales/en-US/fennec.properties | 4 ++-- services/sync/locales/en-US/pick-sync.dtd | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 services/sync/locales/en-US/pick-sync.dtd diff --git a/services/sync/locales/en-US/fennec.properties b/services/sync/locales/en-US/fennec.properties index 50da8e7d19eb..b10ea7e31d3d 100644 --- a/services/sync/locales/en-US/fennec.properties +++ b/services/sync/locales/en-US/fennec.properties @@ -8,11 +8,11 @@ fennec.login.error.detail = Login error: %S fennec.default.client.type = Mobile fennec.sync.start = Syncing Now... -fennec.sync.complete.time = Sync completed at %S, %S +fennec.sync.complete.time = Sync completed at %1$S, %2$S fennec.sync.error.detail = Error: %S fennec.sync.error.generic = Weave had an error when trying to sync. fennec.sync.status = Syncing %S... -fennec.sync.status.detail = Syncing (%S %S)... +fennec.sync.status.detail = Syncing (%1$S %1$S)... fennec.turn.weave.off = Turn Weave Off fennec.turn.weave.on = Turn Weave On diff --git a/services/sync/locales/en-US/pick-sync.dtd b/services/sync/locales/en-US/pick-sync.dtd new file mode 100644 index 000000000000..2977210c2604 --- /dev/null +++ b/services/sync/locales/en-US/pick-sync.dtd @@ -0,0 +1,9 @@ + + + + + + + + + From c84db924c0808e5c99a5832a0b0cc42127eaefcb Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 26 Aug 2009 13:07:23 -0400 Subject: [PATCH 1317/1860] remove files that were supposed to be culled already, stupid Hg --- services/sync/locales/en-US/oauth.dtd | 18 ------------------ services/sync/locales/en-US/oauth.properties | 10 ---------- services/sync/locales/en-US/share.dtd | 6 ------ services/sync/locales/en-US/share.properties | 4 ---- 4 files changed, 38 deletions(-) delete mode 100644 services/sync/locales/en-US/oauth.dtd delete mode 100644 services/sync/locales/en-US/oauth.properties delete mode 100644 services/sync/locales/en-US/share.dtd delete mode 100644 services/sync/locales/en-US/share.properties diff --git a/services/sync/locales/en-US/oauth.dtd b/services/sync/locales/en-US/oauth.dtd deleted file mode 100644 index 87b9843cc37a..000000000000 --- a/services/sync/locales/en-US/oauth.dtd +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/services/sync/locales/en-US/oauth.properties b/services/sync/locales/en-US/oauth.properties deleted file mode 100644 index bb2f02fb718c..000000000000 --- a/services/sync/locales/en-US/oauth.properties +++ /dev/null @@ -1,10 +0,0 @@ -intro.uidmsg = You are logged in as %S -conf.conmsg = A third party identifying itself as %S is requesting access to your Weave Data. -conf.error = Sorry, but the third party's request was invalid: %S -conf.error1 = a request token was not issued. -conf.error2 = the third party is unregistered. -conf.error3 = the request token has expired. -conf.error4 = your account details could not be verified. -final.step1 = Unwrapping symmetric key... -final.step2 = Adding third party to your keyring... -final.step3 = Done! \ No newline at end of file diff --git a/services/sync/locales/en-US/share.dtd b/services/sync/locales/en-US/share.dtd deleted file mode 100644 index c14d4873cf00..000000000000 --- a/services/sync/locales/en-US/share.dtd +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/services/sync/locales/en-US/share.properties b/services/sync/locales/en-US/share.properties deleted file mode 100644 index 9f5914754368..000000000000 --- a/services/sync/locales/en-US/share.properties +++ /dev/null @@ -1,4 +0,0 @@ -status.ok = Weave sharing complete! -status.error = Error. Please check the Weave ID and try again. -status.working = Working... -folder.message = You have chosen to share the bookmark folder "%S". \ No newline at end of file From c1047f6c5a626beaed9153d5371dd92d44d81c14 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 14:22:11 -0700 Subject: [PATCH 1318/1860] Bug 506297 - Livemarks with null site/feed uris cause sync to fail It's possible for livemarks to not have a siteURI, so don't assume it to be there. --- services/sync/modules/engines/bookmarks.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 2e966de7413b..d93ef982379e 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -365,9 +365,12 @@ BookmarksStore.prototype = { record._insertPos, "as", record.title].join(" ")); break; case "livemark": - newId = this._ls.createLivemark(record._parent, record.title, - Utils.makeURI(record.siteUri), Utils.makeURI(record.feedUri), - record._insertPos); + let siteURI = null; + if (record.siteUri != null) + siteURI = Utils.makeURI(record.siteUri); + + newId = this._ls.createLivemark(record._parent, record.title, siteURI, + Utils.makeURI(record.feedUri), record._insertPos); this._log.debug(["created livemark", newId, "under", record._parent, "at", record._insertPos, "as", record.title, record.siteUri, record.feedUri]. join(" ")); @@ -621,7 +624,10 @@ BookmarksStore.prototype = { case this._bms.TYPE_FOLDER: if (this._ls.isLivemark(placeId)) { record = new Livemark(); - record.siteUri = this._ls.getSiteURI(placeId).spec; + + let siteURI = this._ls.getSiteURI(placeId); + if (siteURI != null) + record.siteUri = siteURI.spec; record.feedUri = this._ls.getFeedURI(placeId).spec; } else { From 99576c503536a38a43452149ab552d6ce498169b Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Aug 2009 15:01:28 -0700 Subject: [PATCH 1319/1860] Adds another pref for the "misc" api, makes base/misc url prefs default to auth.smc/{user,misc}/, fixes about:weave captcha path. r=Mardak --- services/sync/modules/service.js | 37 +++++++++++++------------------- services/sync/modules/util.js | 19 ++++++++++++++++ services/sync/services-sync.js | 4 ++-- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 53537eda30e1..71e6055748e1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -202,25 +202,21 @@ WeaveSvc.prototype = { }, get baseURL() { - let url = Svc.Prefs.get("serverURL"); - if (!url) - throw "No server URL set"; - if (url[url.length-1] != '/') - url += '/'; - return url; + return Utils.getURLPref("serverURL"); }, set baseURL(value) { Svc.Prefs.set("serverURL", value); }, + get miscURL() { + return Utils.getURLPref("miscURL"); + }, + set miscURL(value) { + Svc.Prefs.set("miscURL", value); + }, + get clusterURL() { - let url = Svc.Prefs.get("clusterURL"); - if (!url) - return null; - if (url[url.length-1] != '/') - url += '/'; - url += "0.5/"; - return url; + return Utils.getURLPref("clusterURL", null, "0.5/"); }, set clusterURL(value) { Svc.Prefs.set("clusterURL", value); @@ -461,7 +457,7 @@ WeaveSvc.prototype = { findCluster: function WeaveSvc_findCluster(username) { this._log.debug("Finding cluster for user " + username); - let res = new Resource(this.baseURL + "user/1/" + username + "/node/weave"); + let res = new Resource(this.baseURL + "1/" + username + "/node/weave"); try { res.get(); } catch(ex) {} @@ -470,11 +466,9 @@ WeaveSvc.prototype = { switch (res.lastChannel.responseStatus) { case 404: this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); - return Svc.Prefs.get("serverURL"); + return this.baseURL; case 200: - // The server gives JSON of a string literal, but JS doesn't allow it - res.data = res.data.replace(/"/g, ""); - return "https://" + res.data + "/"; + return res.data; default: this._log.debug("Unexpected response code trying to find cluster: " + res.lastChannel.responseStatus); break; @@ -604,8 +598,7 @@ WeaveSvc.prototype = { changePassword: function WeaveSvc_changePassword(newpass) this._catch(this._notify("changepwd", "", function() { - let url = Svc.Prefs.get('tmpServerURL') + 'user/1/' + - username + "/password"; + let url = this.baseURL + '1/' + username + "/password"; let res = new Weave.Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); @@ -770,7 +763,7 @@ WeaveSvc.prototype = { }, checkUsername: function WeaveSvc_checkUsername(username) { - let url = Svc.Prefs.get('tmpServerURL') + "/user/1/" + username; + let url = this.baseURL + "1/" + username; let res = new Resource(url); res.authenticator = new NoOpAuthenticator(); @@ -795,7 +788,7 @@ WeaveSvc.prototype = { "captcha_response": captchaResponse }); - let url = Svc.Prefs.get('tmpServerURL') + 'user/1/' + username; + let url = this.baseURL + '1/' + username; let res = new Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 8c4b63d7ab53..6a4aaa7c404c 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -488,6 +488,25 @@ let Utils = { return url; }, + // ensures url ends with a slash, optionally adds an extra string at the end + slashify: function Weave_slashify(url, extra) { + if (url[url.length-1] != '/') + url += '/'; + if (extra) + url += extra; + return url; + }, + + // + getURLPref: function Weave_getURLPref(pref, def, extra) { + let url = Svc.Prefs.get(pref); + if (!url && typeof(def) == "undefined") + throw pref + " not set"; + else if (!url) + return def; + return Utils.slashify(url, extra); + }, + xpath: function Weave_xpath(xmlDoc, xpathString) { let root = xmlDoc.ownerDocument == null ? xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index fc453b496ffc..ae040ee5b48b 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,5 +1,5 @@ -pref("extensions.weave.serverURL", "https://auth.services.mozilla.com/"); -pref("extensions.weave.tmpServerURL", "https://services.mozilla.com/"); +pref("extensions.weave.serverURL", "https://auth.services.mozilla.com/user/"); +pref("extensions.weave.miscURL", "https://auth.services.mozilla.com/misc/"); pref("extensions.weave.encryption", "aes-256-cbc"); From 02d4d48115054ac4a1d3c8c3e411e6c83ce1109d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 15:32:46 -0700 Subject: [PATCH 1320/1860] Bug 511746 - Resource.foo shouldn't throw except in exceptional cases. r=thunder Get rid of lastChannel and return a String object from _request with additional properties of status, succeeded, headers -- even if the response was handled by cache. Update engines to check for non-success and throw the failure. Update tests to use these additional properties instead of lastChannel, etc. --- services/sync/modules/base_records/wbo.js | 9 +- services/sync/modules/engines.js | 13 ++- services/sync/modules/resource.js | 103 +++++++----------- services/sync/modules/service.js | 97 ++++++++--------- services/sync/tests/unit/test_auth_manager.js | 2 +- services/sync/tests/unit/test_records_keys.js | 4 +- services/sync/tests/unit/test_records_wbo.js | 6 +- services/sync/tests/unit/test_resource.js | 33 +++--- 8 files changed, 125 insertions(+), 142 deletions(-) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 002786a1f127..50989447ae6d 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -131,12 +131,13 @@ RecordManager.prototype = { import: function RecordMgr_import(url) { this._log.trace("Importing record: " + (url.spec ? url.spec : url)); try { - this.lastResource = new Resource(url); - this.lastResource.get(); + // Clear out the last response with empty object if GET fails + this.response = {}; + this.response = new Resource(url).get(); let record = new this._recordType(); - record.deserialize(this.lastResource.data); - record.uri = url; // NOTE: may override id in this.lastResource.data + record.deserialize(this.response); + record.uri = url; return this.set(url, record); } diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 758e430e413a..709f12aa2090 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -283,7 +283,9 @@ SyncEngine.prototype = { meta.generateIV(); meta.addUnwrappedKey(pubkey, symkey); let res = new Resource(meta.uri); - res.put(meta.serialize()); + let resp = res.put(meta.serialize()); + if (!resp.success) + throw resp; // Cache the cryto meta that we just put on the server CryptoMetas.set(meta.uri, meta); @@ -341,7 +343,9 @@ SyncEngine.prototype = { Sync.sleep(0); }); - newitems.get(); + let resp = newitems.get(); + if (!resp.success) + throw resp; if (this.lastSync < this._lastSyncTmp) this.lastSync = this._lastSyncTmp; @@ -462,7 +466,10 @@ SyncEngine.prototype = { // Upload what we've got so far in the collection let doUpload = Utils.bind2(this, function(desc) { this._log.info("Uploading " + desc + " of " + outnum + " records"); - up.post(); + let resp = up.post(); + if (!resp.success) + throw resp; + if (up.data.modified > this.lastSync) this.lastSync = up.data.modified; up.clearRecords(); diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index c4afb9176959..6e522371d9ce 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -50,27 +50,6 @@ Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/auth.js"); -// = RequestException = -// -// This function raises an exception through the call -// stack for a failed network request. -function RequestException(resource, action, request) { - this._resource = resource; - this._action = action; - this._request = request; - this.location = Components.stack.caller; -} -RequestException.prototype = { - get resource() { return this._resource; }, - get action() { return this._action; }, - get request() { return this._request; }, - get status() { return this._request.status; }, - toString: function ReqEx_toString() { - return "Could not " + this._action + " resource " + this._resource.spec + - " (" + this._request.responseStatus + ")"; - } -}; - // = Resource = // // Represents a remote network resource, identified by a URI. @@ -122,8 +101,6 @@ Resource.prototype = { return this._uri; }, set uri(value) { - this._dirty = true; - this._downloaded = false; if (typeof value == 'string') this._uri = Utils.makeURI(value); else @@ -145,17 +122,9 @@ Resource.prototype = { _data: null, get data() this._data, set data(value) { - this._dirty = true; this._data = value; }, - _lastChannel: null, - _downloaded: false, - _dirty: false, - get lastChannel() this._lastChannel, - get downloaded() this._downloaded, - get dirty() this._dirty, - // ** {{{ Resource.filters }}} ** // // Filters are used to perform pre and post processing on @@ -188,18 +157,15 @@ Resource.prototype = { // to obtain a request channel. // _createRequest: function Res__createRequest() { - this._lastChannel = Svc.IO.newChannel(this.spec, null, null). - QueryInterface(Ci.nsIRequest); + let channel = Svc.IO.newChannel(this.spec, null, null). + QueryInterface(Ci.nsIRequest).QueryInterface(Ci.nsIHttpChannel); // Always validate the cache: - let loadFlags = this._lastChannel.loadFlags; - loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; - loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; - this._lastChannel.loadFlags = loadFlags; - this._lastChannel = this._lastChannel.QueryInterface(Ci.nsIHttpChannel); + channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; // Setup a callback to handle bad HTTPS certificates. - this._lastChannel.notificationCallbacks = new badCertListener(); + channel.notificationCallbacks = new badCertListener(); // Avoid calling the authorizer more than once let headers = this.headers; @@ -208,9 +174,9 @@ Resource.prototype = { this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); else this._log.trace("HTTP Header " + key + ": " + headers[key]); - this._lastChannel.setRequestHeader(key, headers[key], false); + channel.setRequestHeader(key, headers[key], false); } - return this._lastChannel; + return channel; }, _onProgress: function Res__onProgress(channel) {}, @@ -289,31 +255,44 @@ Resource.prototype = { throw error; } - if (!channel.requestSucceeded) { - this._log.debug(action + " request failed (" + channel.responseStatus + ")"); - if (this._data) - this._log.debug("Error response: " + this._data); - throw new RequestException(this, action, channel); - - } else { - this._log.debug(action + " request successful (" + channel.responseStatus + ")"); - - switch (action) { - case "DELETE": - if (Utils.checkStatus(channel.responseStatus, null, [[200,300],404])){ - this._dirty = false; - this._data = null; + // Set some default values in-case there's no response header + let headers = {}; + let status = 0; + let success = true; + try { + // Read out the response headers if available + channel.visitResponseHeaders({ + visitHeader: function visitHeader(header, value) { + headers[header] = value; + } + }); + status = channel.responseStatus; + success = channel.requestSucceeded; + + if (success) { + this._log.debug(action + " success: " + status); + switch (action) { + case "GET": + case "POST": + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace(action + " Body: " + this._data); + this.filterDownload(); + break; } - break; - case "GET": - case "POST": - this._log.trace(action + " Body: " + this._data); - this.filterDownload(); - break; } + else + this._log.debug(action + " fail: " + status + " " + this._data); + } + // Got a response but no header; must be cached (use default values) + catch(ex) { + this._log.debug(action + " cached: " + status); } - return this._data; + let ret = new String(this._data); + ret.headers = headers; + ret.status = status; + ret.success = success; + return ret; }, // ** {{{ Resource.get }}} ** diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 094cdefcb707..eb31147522f0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -460,18 +460,16 @@ WeaveSvc.prototype = { let res = new Resource(this.baseURL + "api/register/chknode/" + username); try { - res.get(); - } catch(ex) {} - - try { - switch (res.lastChannel.responseStatus) { + let node = res.get(); + switch (node.status) { case 404: this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); return Svc.Prefs.get("serverURL"); + case 0: case 200: - return "https://" + res.data + "/"; + return "https://" + node + "/"; default: - this._log.debug("Unexpected response code trying to find cluster: " + res.lastChannel.responseStatus); + this._log.debug("Unexpected response code: " + node.status); break; } } catch (e) { @@ -533,11 +531,8 @@ WeaveSvc.prototype = { // login may fail because of cluster change try { - res.get(); - } catch (e) {} - - try { - switch (res.lastChannel.responseStatus) { + let test = res.get(); + switch (test.status) { case 200: if (passphrase && !this.verifyPassphrase(username, password, passphrase)) { this._setSyncFailure(LOGIN_FAILED_INVALID_PASSPHRASE); @@ -552,7 +547,7 @@ WeaveSvc.prototype = { this._log.debug("verifyLogin failed: login failed") return false; default: - throw "unexpected HTTP response: " + res.lastChannel.responseStatus; + throw "unexpected HTTP response: " + test.status; } } catch (e) { // if we get here, we have either a busted channel or a network error @@ -595,7 +590,10 @@ WeaveSvc.prototype = { privkey.payload.iv, newphrase); privkey.payload.keyData = newkey; - new Resource(privkey.uri).put(privkey.serialize()); + let resp = new Resource(privkey.uri).put(privkey.serialize()); + if (!resp.success) + throw resp; + this.passphrase = newphrase; return true; @@ -613,7 +611,7 @@ WeaveSvc.prototype = { "Content-Length", message.length); let resp = res.post(message); - if (res.lastChannel.responseStatus != 200) { + if (resp.status != 200) { this._log.info("Password change failed: " + resp); throw "Could not change password"; } @@ -740,7 +738,7 @@ WeaveSvc.prototype = { }, _errorStr: function WeaveSvc__errorStr(code) { - switch (code) { + switch (code.toString()) { case "0": return "uid-in-use"; case "-1": @@ -782,7 +780,7 @@ WeaveSvc.prototype = { let data = ""; try { data = res.get(); - if (res.lastChannel.responseStatus == 200 && data == "0") + if (data.status == 200 && data == "0") return "available"; } catch(ex) {} @@ -804,31 +802,29 @@ WeaveSvc.prototype = { res.setHeader("Content-Type", "application/x-www-form-urlencoded", "Content-Length", message.length); - let ret = {}; + let error = "generic-server-error"; try { - ret.response = res.post(message); - ret.status = res.lastChannel.responseStatus; + let register = res.post(message); + if (register.success) { + this._log.info("Account created: " + register); + return; + } - // No exceptions must have meant it was successful - this._log.info("Account created: " + ret.response); - return ret; + // Must have failed, so figure out the reason + switch (register.status) { + case 400: + error = this._errorStr(register); + break; + case 417: + error = "captcha-incorrect"; + break; + } } catch(ex) { this._log.warn("Failed to create account: " + ex); - let status = ex.request.responseStatus; - switch (status) { - case 400: - ret.error = this._errorStr(status); - break; - case 417: - ret.error = "captcha-incorrect"; - break; - default: - ret.error = "generic-server-error"; - break; - } - return ret; } + + return error; }, // stuff we need to to after login, before we can really do @@ -849,7 +845,7 @@ WeaveSvc.prototype = { Svc.Version.compare(COMPATIBLE_VERSION, remoteVersion) > 0) { // abort the server wipe if the GET status was anything other than 404 or 200 - let status = Records.lastResource.lastChannel.responseStatus; + let status = Records.response.status; if (status != 200 && status != 404) { this._setSyncFailure(METARECORD_DOWNLOAD_FAIL); this._log.warn("Unknown error while downloading metadata record. " + @@ -918,14 +914,10 @@ WeaveSvc.prototype = { } if (needKeys) { - if (PubKeys.lastResource != null && PrivKeys.lastResource != null && - PubKeys.lastResource.lastChannel.responseStatus != 404 && - PrivKeys.lastResource.lastChannel.responseStatus != 404) { + if (PubKeys.response.status != 404 && PrivKeys.response.status != 404) { this._log.warn("Couldn't download keys from server, aborting sync"); - this._log.debug("PubKey HTTP response status: " + - PubKeys.lastResource.lastChannel.responseStatus); - this._log.debug("PrivKey HTTP response status: " + - PrivKeys.lastResource.lastChannel.responseStatus); + this._log.debug("PubKey HTTP status: " + PubKeys.response.status); + this._log.debug("PrivKey HTTP status: " + PrivKeys.response.status); this._setSyncFailure(KEYS_DOWNLOAD_FAIL); return false; } @@ -956,8 +948,6 @@ WeaveSvc.prototype = { } catch (e) { this._setSyncFailure(KEYS_UPLOAD_FAIL); this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); - // FIXME no lastRequest anymore - //this._log.error(keys.pubkey.lastRequest.responseText); } } else { this._setSyncFailure(SETUP_FAILED_NO_PASSPHRASE); @@ -1073,7 +1063,8 @@ WeaveSvc.prototype = { // this is sort of pseudocode, need a way to get at the if (!shouldBackoff) { try { - shouldBackoff = Utils.checkStatus(Records.lastResource.lastChannel.responseStatus, null, [500,[502,504]]); + shouldBackoff = Utils.checkStatus(Records.response.status, null, + [500, [502, 504]]); } catch (e) { // if responseStatus throws, we have a network issue in play @@ -1226,10 +1217,9 @@ WeaveSvc.prototype = { } catch(e) { // maybe a 401, cluster update needed? - if (e.constructor.name == "RequestException" && e.status == 401) { - if (this.updateCluster(this.username)) - return this._syncEngine(engine); - } + if (e.status == 401 && this.updateCluster(this.username)) + return this._syncEngine(engine); + this._syncError = true; this._weaveStatusCode = WEAVE_STATUS_PARTIAL; this._detailedStatus.setEngineStatus(engine.name, e); @@ -1260,8 +1250,9 @@ WeaveSvc.prototype = { this._log.debug("Setting meta payload storage version to " + WEAVE_VERSION); meta.payload.storageVersion = WEAVE_VERSION; - let res = new Resource(meta.uri); - res.put(meta.serialize()); + let resp = new Resource(meta.uri).put(meta.serialize()); + if (!resp.success) + throw resp; }, /** diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js index 310d1eceb7ad..170cc6260523 100644 --- a/services/sync/tests/unit/test_auth_manager.js +++ b/services/sync/tests/unit/test_auth_manager.js @@ -41,7 +41,7 @@ function run_test() { let res = new Resource("http://localhost:8080/foo"); let content = res.get(); do_check_eq(content, "This path exists and is protected"); - do_check_eq(res.lastChannel.responseStatus, 200); + do_check_eq(content.status, 200); server.stop(); } diff --git a/services/sync/tests/unit/test_records_keys.js b/services/sync/tests/unit/test_records_keys.js index b336127267e0..90b56ca35c9f 100644 --- a/services/sync/tests/unit/test_records_keys.js +++ b/services/sync/tests/unit/test_records_keys.js @@ -43,13 +43,13 @@ function run_test() { let pubkey = PubKeys.get("http://localhost:8080/pubkey"); do_check_eq(pubkey.data.payload.type, "pubkey"); - do_check_eq(PubKeys.lastResource.lastChannel.responseStatus, 200); + do_check_eq(PubKeys.response.status, 200); log.info("Getting matching private key"); let privkey = PrivKeys.get(pubkey.privateKeyUri); do_check_eq(privkey.data.payload.type, "privkey"); - do_check_eq(PrivKeys.lastResource.lastChannel.responseStatus, 200); + do_check_eq(PrivKeys.response.status, 200); log.info("Done!"); } diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index 65d7b846a397..994a60ffe4c9 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -48,7 +48,7 @@ function run_test() { log.info("Getting a WBO record"); let res = new Resource("http://localhost:8080/record"); - res.get(); + let resp = res.get(); let rec = new WBORecord(); rec.deserialize(res.data); @@ -60,7 +60,7 @@ function run_test() { do_check_eq(rec.modified, 2454725.98283); do_check_eq(typeof(rec.payload), "object"); do_check_eq(rec.payload.cheese, "roquefort"); - do_check_eq(res.lastChannel.responseStatus, 200); + do_check_eq(resp.status, 200); log.info("Getting a WBO record using the record manager"); @@ -69,7 +69,7 @@ function run_test() { do_check_eq(rec2.modified, 2454725.98284); do_check_eq(typeof(rec2.payload), "object"); do_check_eq(rec2.payload.cheese, "gruyere"); - do_check_eq(Records.lastResource.lastChannel.responseStatus, 200); + do_check_eq(Records.response.status, 200); log.info("Done!"); } diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index b035d7b16fc2..8218070e69a6 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -56,14 +56,15 @@ function run_test() { let res = new Resource("http://localhost:8080/open"); let content = res.get(); do_check_eq(content, "This path exists"); - do_check_eq(res.lastChannel.responseStatus, 200); + do_check_eq(content.status, 200); + do_check_true(content.success); - // 2. A password protected resource (test that it'll fail w/o pass) + // 2. A password protected resource (test that it'll fail w/o pass, no throw) let res2 = new Resource("http://localhost:8080/protected"); - try { - content = res2.get(); - do_check_true(false); // unreachable, get() above must fail - } catch (e) {} + content = res2.get(); + do_check_eq(content, "This path exists and is protected - failed") + do_check_eq(content.status, 401); + do_check_false(content.success); // 3. A password protected resource @@ -72,17 +73,21 @@ function run_test() { res3.authenticator = auth; content = res3.get(); do_check_eq(content, "This path exists and is protected"); - do_check_eq(res3.lastChannel.responseStatus, 200); + do_check_eq(content.status, 200); + do_check_true(content.success); - // 4. A non-existent resource (test that it'll fail) + // 4. A non-existent resource (test that it'll fail, but not throw) let res4 = new Resource("http://localhost:8080/404"); - try { - let content = res4.get(); - do_check_true(false); // unreachable, get() above must fail - } catch (e) {} - do_check_eq(res4.lastChannel.responseStatusText, "Not Found"); - do_check_eq(res4.lastChannel.responseStatus, 404); + content = res4.get(); + do_check_eq(content, "File not found"); + do_check_eq(content.status, 404); + do_check_false(content.success); + + _("5. Check some headers of the 404 response"); + do_check_eq(content.headers.Connection, "close"); + do_check_eq(content.headers.Server, "httpd.js"); + do_check_eq(content.headers["Content-Length"], 14); // FIXME: additional coverage needed: // * PUT requests From 98742bf13ec416aceb5208515cea50f7fb417ebf Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 16:09:48 -0700 Subject: [PATCH 1321/1860] Resolve lastChannel removal and 0.5 api changes. r=thunder --- services/sync/modules/engines.js | 11 +--------- services/sync/modules/service.js | 37 ++------------------------------ 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 01b21350e2d2..f4a7a6cf337e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -468,24 +468,15 @@ SyncEngine.prototype = { // Upload what we've got so far in the collection let doUpload = Utils.bind2(this, function(desc) { this._log.info("Uploading " + desc + " of " + outnum + " records"); -<<<<<<< local - up.post(); -======= let resp = up.post(); if (!resp.success) throw resp; ->>>>>>> other -<<<<<<< local // Record the modified time of the upload - let modified = up._lastChannel.getResponseHeader("X-Weave-Timestamp"); + let modified = resp.headers["X-Weave-Timestamp"]; if (modified > this.lastSync) this.lastSync = modified; -======= - if (up.data.modified > this.lastSync) - this.lastSync = up.data.modified; ->>>>>>> other up.clearRecords(); }); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5078e5c34a36..d91517a788d0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -459,30 +459,14 @@ WeaveSvc.prototype = { let res = new Resource(this.baseURL + "1/" + username + "/node/weave"); try { -<<<<<<< local - res.get(); - } catch(ex) {} - - try { - switch (res.lastChannel.responseStatus) { -======= let node = res.get(); switch (node.status) { ->>>>>>> other case 404: this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); -<<<<<<< local return this.baseURL; -======= - return Svc.Prefs.get("serverURL"); case 0: ->>>>>>> other case 200: -<<<<<<< local - return res.data; -======= - return "https://" + node + "/"; ->>>>>>> other + return node; default: this._log.debug("Unexpected response code: " + node.status); break; @@ -616,13 +600,8 @@ WeaveSvc.prototype = { let res = new Weave.Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); -<<<<<<< local let resp = res.post(newpass); - if (res.lastChannel.responseStatus != 200) { -======= - let resp = res.post(message); if (resp.status != 200) { ->>>>>>> other this._log.info("Password change failed: " + resp); throw "Could not change password"; } @@ -813,23 +792,12 @@ WeaveSvc.prototype = { let error = "generic-server-error"; try { -<<<<<<< local - ret.response = res.put(payload); - ret.status = res.lastChannel.responseStatus; -======= - let register = res.post(message); + let register = res.put(payload); if (register.success) { this._log.info("Account created: " + register); return; } ->>>>>>> other -<<<<<<< local - // No exceptions must have meant it was successful - this._log.info("Account created: " + ret.response); - return ret; - } catch(ex) { -======= // Must have failed, so figure out the reason switch (register.status) { case 400: @@ -841,7 +809,6 @@ WeaveSvc.prototype = { } } catch(ex) { ->>>>>>> other this._log.warn("Failed to create account: " + ex); } From c41a09eb119fc43f50177823518674c929f74a99 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Aug 2009 16:44:30 -0700 Subject: [PATCH 1322/1860] Parse new 0.5 error codes. --- services/sync/modules/service.js | 44 +++++++++++++------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d91517a788d0..1954b6b5c07b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -729,32 +729,24 @@ WeaveSvc.prototype = { _errorStr: function WeaveSvc__errorStr(code) { switch (code.toString()) { - case "0": - return "uid-in-use"; - case "-1": - return "invalid-http-method"; - case "-2": - return "uid-missing"; - case "-3": - return "uid-invalid"; - case "-4": - return "mail-invalid"; - case "-5": - return "mail-in-use"; - case "-6": - return "captcha-challenge-missing"; - case "-7": - return "captcha-response-missing"; - case "-8": - return "password-missing"; - case "-9": - return "internal-server-error"; - case "-10": - return "server-quota-exceeded"; - case "-11": - return "missing-new-field"; - case "-12": - return "password-incorrect"; + case "1": + return "illegal-method"; + case "2": + return "invalid-captcha"; + case "3": + return "invalid-username"; + case "4": + return "cannot-overwrite-resource"; + case "5": + return "userid-mismatch"; + case "6": + return "json-parse-failure"; + case "7": + return "invalid-password"; + case "8": + return "invalid-record"; + case "9": + return "weak-password"; default: return "generic-server-error"; } From c94b90b15cd83a7739af71dc7f4cca28956f84d5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 16:45:24 -0700 Subject: [PATCH 1323/1860] Server expects catcha-challenge not captcha_challenge for new users. --- services/sync/modules/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1954b6b5c07b..68a32c1722b7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -774,8 +774,8 @@ WeaveSvc.prototype = { { let payload = JSON.stringify({ "password": password, "email": email, - "captcha_challenge": captchaChallenge, - "captcha_response": captchaResponse + "captcha-challenge": captchaChallenge, + "captcha-response": captchaResponse }); let url = this.baseURL + '1/' + username; From e6b73e3fcff0fac82a8446bdd0fe0dfa28cac246 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 17:49:23 -0700 Subject: [PATCH 1324/1860] Save an exception/catch when failing to import a record. --- services/sync/modules/base_records/wbo.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 50989447ae6d..47ac697969db 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -135,6 +135,10 @@ RecordManager.prototype = { this.response = {}; this.response = new Resource(url).get(); + // Don't parse and save the record on failure + if (!this.response.success) + return null; + let record = new this._recordType(); record.deserialize(this.response); record.uri = url; From 4b1ca62c0ccb385ace8f42081a6b2b1e5b77e182 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 17:52:46 -0700 Subject: [PATCH 1325/1860] Remove old modified arg for collections. --- services/sync/modules/base_records/collection.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 1e0aea57d5fc..ff672eafdd41 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -75,7 +75,6 @@ Collection.prototype = { args.push('older=' + this.older); else if (this.newer) { args.push('newer=' + this.newer); - args.push('modified=' + this.newer); // tmp hack for older servers } if (this.full) args.push('full=1'); From 21c206489933a567fd8da2a8e2465a9d9b344a09 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 17:59:50 -0700 Subject: [PATCH 1326/1860] New User API returns server code 2 instead of http 417. --- services/sync/modules/service.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 68a32c1722b7..aeebd4fd3ddc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -791,14 +791,8 @@ WeaveSvc.prototype = { } // Must have failed, so figure out the reason - switch (register.status) { - case 400: - error = this._errorStr(register); - break; - case 417: - error = "captcha-incorrect"; - break; - } + if (register.status == 400) + error = this._errorStr(register); } catch(ex) { this._log.warn("Failed to create account: " + ex); From c65eb3dccfc36763ea92748c89f8911b22e79762 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 18:09:41 -0700 Subject: [PATCH 1327/1860] Bug 512637 - Use newlines instead of JSON collection for incremental parsing. r=thunder Switch to newline mode when using a collection record handler, and look for newlines! Easy! Update test to provide newline-separated strings instead of JSON. --- .../sync/modules/base_records/collection.js | 68 +++---------------- .../tests/unit/test_collection_inc_get.js | 18 ++--- 2 files changed, 18 insertions(+), 68 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index ff672eafdd41..57d3a286794f 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -127,63 +127,15 @@ Collection.prototype = { // Save this because onProgress is called with this as the ChannelListener let coll = this; + // Switch to newline separated records for incremental parsing + coll.setHeader("Accept", "application/newlines"); + this._onProgress = function() { - // Save some work by quitting early when there's no records - if (this._data == "[]") - return; - - do { - // Strip off the the starting "[" or separating "," or trailing "]" if - // it wasn't stripped off from a previous progress update - let start = this._data[0]; - if (start == "[" || start == "," || start == "]") - this._data = this._data.slice(1); - - // Track various states of # open braces and ignore for strings - let json = ""; - let braces = 1; - let ignore = false; - let escaped = false; - let length = this._data.length; - - // Skip the first character, the "{", and try to find a json record - for (let i = 1; i < length; i++) { - let char = this._data[i]; - - // Opening a string makes us ignore all characters except close " - if (char == '"') { - if (!ignore) - ignore = true; - // It's a real close " if it's not escaped - else if (!escaped) - ignore = false; - } - - // Track if an end quote might be escaped when processing strings - if (ignore) { - escaped = char == "\\" ? !escaped : false; - - // Don't bother checking other characters when ignoring - continue; - } - - // Increase the brace count on open { - if (char == "{") - braces++; - // Decrement brace count on close } - else if (char == "}" && --braces == 0) { - // Split the json record from the rest of the data - json = this._data.slice(0, i + 1); - this._data = this._data.slice(i + 1); - - // Stop processing for now that we found one record - break; - } - } - - // No valid record json found? - if (json.length == 0) - break; + let newline; + while ((newline = this._data.indexOf("\n")) > 0) { + // Split the json record from the rest of the data + let json = this._data.slice(0, newline); + this._data = this._data.slice(newline + 1); // Deserialize a record from json and give it to the callback let record = new coll._recordObj(); @@ -191,9 +143,7 @@ Collection.prototype = { record.baseURI = coll.uri; record.id = record.data.id; onRecord(record); - - // Keep processing the data until we can't find a json record - } while (true); + } // Aggressively clean up the objects we created above so that the next set // of records have enough memory to decrypt, reconcile, apply, etc. diff --git a/services/sync/tests/unit/test_collection_inc_get.js b/services/sync/tests/unit/test_collection_inc_get.js index 6815c27f8460..0c89f1a6c8be 100644 --- a/services/sync/tests/unit/test_collection_inc_get.js +++ b/services/sync/tests/unit/test_collection_inc_get.js @@ -9,7 +9,7 @@ function run_test() { _("Parse empty string payload as deleted"); called = false; - stream._data = '[{"payload":""}]'; + stream._data = '{"payload":""}\n'; coll.recordHandler = function(rec) { called = true; _("Got record:", JSON.stringify(rec)); @@ -23,7 +23,7 @@ function run_test() { _("Parse record with payload"); called = false; - stream._data = '[{"payload":"{\\"value\\":123}"}]'; + stream._data = '{"payload":"{\\"value\\":123}"}\n'; coll.recordHandler = function(rec) { called = true; _("Got record:", JSON.stringify(rec)); @@ -39,7 +39,7 @@ function run_test() { called = false; recCount = 0; sum = 0; - stream._data = '[{"payload":"{\\"value\\":100}"},{"payload":"{\\"value\\":10}"},{"payload":"{\\"value\\":1}"}]'; + stream._data = '{"payload":"{\\"value\\":100}"}\n{"payload":"{\\"value\\":10}"}\n{"payload":"{\\"value\\":1}"}\n'; coll.recordHandler = function(rec) { called = true; _("Got record:", JSON.stringify(rec)); @@ -76,7 +76,7 @@ function run_test() { called = false; recCount = 0; sum = 0; - stream._data = '[{"payl'; + stream._data = '{"payl'; coll.recordHandler = function(rec) { called = true; do_throw("shouldn't have gotten a record.."); @@ -92,7 +92,7 @@ function run_test() { _("adding more data enough for one record.."); called = false; - stream._data += 'oad":"{\\"value\\":100}"},'; + stream._data += 'oad":"{\\"value\\":100}"}\n'; coll.recordHandler = function(rec) { called = true; _("Got record:", JSON.stringify(rec)); @@ -126,7 +126,7 @@ function run_test() { _("add data for two records.."); called = false; - stream._data += '},{"payload":"{\\"value\\":1}"}'; + stream._data += '}\n{"payload":"{\\"value\\":1}"}\n'; coll.recordHandler = function(rec) { called = true; _("Got record:", JSON.stringify(rec)); @@ -155,9 +155,9 @@ function run_test() { do_check_true(called); _(); - _("add ending array bracket"); + _("add no extra data"); called = false; - stream._data += ']'; + stream._data += ''; coll.recordHandler = function(rec) { called = true; do_throw("shouldn't have gotten a record.."); @@ -166,7 +166,7 @@ function run_test() { _("should still have 3 records with sum 111"); do_check_eq(recCount, 3); do_check_eq(sum, 111); - _("should have consumed the last array bracket"); + _("should have consumed nothing but still have nothing"); do_check_eq(stream._data, ""); do_check_false(called); _("\n"); From 85a08fd3a503df7244a755b3dce26db522f5df35 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Aug 2009 18:48:22 -0700 Subject: [PATCH 1328/1860] about:weave polish: add localized help drawer & other small fixes --- services/sync/locales/en-US/about.properties | 64 +++++++++++++++----- services/sync/modules/util.js | 1 + services/sync/services-sync.js | 1 + 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/services/sync/locales/en-US/about.properties b/services/sync/locales/en-US/about.properties index 9281926311cc..d5917e833330 100644 --- a/services/sync/locales/en-US/about.properties +++ b/services/sync/locales/en-US/about.properties @@ -1,3 +1,5 @@ +# Status messages (below the arrow in the center) + status-offline = signed out status-offline-2 = (offline) status-signing-in = @@ -7,36 +9,70 @@ status-idle-2 = (idle) status-sync = signed in as %S status-sync-2 = (syncing) -welcome = Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam id neque lectus. Donec cursus pulvinar nibh at pretium. Vivamus ante tellus, accumsan vel placerat vestibulum, tristique ac augue. 'Cause that's how I roll. +# Bubbles -#prev = « prev -#next = next » prev = prev next = next -signedin-title = Signed In -signedin-text = You are currently logged in as %S signedin-signout = sign out +signedin-change-password = change password +signedin-change-passphrase = change passphrase signin-title = Sign Into Weave -signin-newacct = new account +signin-newacct = new user signin-username = username signin-password = password signin-passphrase = passphrase -signin-help = help -signin-help-url = https://services.mozilla.com/help/login/ +signin-next = sign in newacct-title = New Weave Account newacct-username = username newacct-password = password newacct-passphrase = passphrase newacct-email = email address -newacct-tos-label = I agree to the -newacct-tos-label2 = -newacct-tos-link = Terms of Service -newacct-tos-url = http://labs.mozilla.com/projects/weave/tos/ +newacct-tos-label = I agree to the %S +newacct-tos = Terms of Service captcha-response = Type in the words above +user-taken-password = My username won't work willsync-title = Account Created! -willsync-text = Sync will begin in %S seconds... -willsync-config = choose what to sync \ No newline at end of file +willsync-1 = Sync will begin in %S seconds... +willsync-config = choose what to sync + +setup-title = Sync Settings +setup-1 = Check the things you'd like to sync: +setup-sync = sync now + +clientinfo-type-desktop = desktop +clientinfo-type-laptop = laptop +clientinfo-type-mobile = mobile +clientinfo-prefs = choose what to sync + +cloudinfo-title = What's In The Cloud? + +# Help items + +help-forgot-password = I forgot my password +forgot-password-1 = Type in your username and we'll send you an email so you can reset it: +forgot-password-box = username +forgot-password-ok = send email + +help-forgot-passphrase = I forgot my passphrase +forgot-passphrase-1 = You can pick a new passphrase, but all your server data will need to be deleted (it cannot be recovered). +forgot-passphrase-2 = To go ahead, click the button below: +forgot-passphrase-ok = reset passphrase + +help-helpme = I'm stuck! What do I do? +help-helpme-1 = If you're stuck, you might want to try the %S or the %S for help. +help-helpme-faq = FAQ +help-helpme-forum = Weave discussion forum + +help-user-taken = My username won't work +help-user-taken-1 = Your username might be taken, try adding numbers or additional words to it. +help-user-taken-2 = Additionally, you can't use special symbols or spaces inside usernames. + +help-newacct-pass = Weave won't accept my password or passphrase +help-newacct-pass-1 = The password and passphrase must be different from each other. + +help-no-captcha = I can't see the verification image +help-no-captcha-1 = Some add-ons can interfere with the verification image. Try disabling NoScript or similar add-ons. diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 6a4aaa7c404c..1efd3416e69c 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -792,6 +792,7 @@ Utils.EventListener.prototype = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); +Svc.GPrefs = new Preferences(); [["Annos", "@mozilla.org/browser/annotation-service;1", "nsIAnnotationService"], ["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index ae040ee5b48b..d20b86ddc1bb 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,5 +1,6 @@ pref("extensions.weave.serverURL", "https://auth.services.mozilla.com/user/"); pref("extensions.weave.miscURL", "https://auth.services.mozilla.com/misc/"); +pref("extensions.weave.termsURL", "https://labs.mozilla.com/projects/weave/tos/"); pref("extensions.weave.encryption", "aes-256-cbc"); From 733fb23ce024020b5c4edad955ba8f54dc5cd2ba Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 19:50:39 -0700 Subject: [PATCH 1329/1860] Remove Firefox Weave pref pane and point Preferences... to about:weave. --- services/sync/locales/en-US/preferences.dtd | 10 ---------- services/sync/modules/util.js | 4 ---- 2 files changed, 14 deletions(-) diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd index 5d3be795e85e..4cc4e2ab46e6 100644 --- a/services/sync/locales/en-US/preferences.dtd +++ b/services/sync/locales/en-US/preferences.dtd @@ -1,13 +1,3 @@ - - - - - - - - - - diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 1efd3416e69c..170ef4abeb1c 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -691,10 +691,6 @@ let Utils = { this.openDialog("ChangeSomething", "generic-change.xul"); }, - openShare: function Utils_openShare() { - Utils.openDialog("Share", "share.xul"); - }, - openLog: function Utils_openLog() { Utils._openChromeWindow("Log", "log.xul"); }, From 4705132321befec147342866d3bd5df6e819a26f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 Aug 2009 20:22:59 -0700 Subject: [PATCH 1330/1860] Add erase server data button to the cloud bubble. --- services/sync/locales/en-US/about.properties | 3 +++ services/sync/locales/en-US/preferences.properties | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 services/sync/locales/en-US/preferences.properties diff --git a/services/sync/locales/en-US/about.properties b/services/sync/locales/en-US/about.properties index d5917e833330..12023c3ed392 100644 --- a/services/sync/locales/en-US/about.properties +++ b/services/sync/locales/en-US/about.properties @@ -49,6 +49,9 @@ clientinfo-type-mobile = mobile clientinfo-prefs = choose what to sync cloudinfo-title = What's In The Cloud? +cloudinfo-erase = erase +erase-title = Erase Server Data +erase-warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? # Help items diff --git a/services/sync/locales/en-US/preferences.properties b/services/sync/locales/en-US/preferences.properties deleted file mode 100644 index 774792ff62ed..000000000000 --- a/services/sync/locales/en-US/preferences.properties +++ /dev/null @@ -1,2 +0,0 @@ -erase.server.warning.title = Erase Server Data -erase.server.warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? \ No newline at end of file From 78537497ebe8497181001a7c139e56448eb17562 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 26 Aug 2009 21:20:08 -0700 Subject: [PATCH 1331/1860] remove 'GPrefs' from Svc, Preferences will do fine; don't make the username in the center status a link, since the whole area is a click target; open 'generic dialogs' for change password & passphrase --- services/sync/modules/service.js | 4 +--- services/sync/modules/util.js | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index aeebd4fd3ddc..e02d48a9beae 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -596,10 +596,8 @@ WeaveSvc.prototype = { changePassword: function WeaveSvc_changePassword(newpass) this._catch(this._notify("changepwd", "", function() { - let url = this.baseURL + '1/' + username + "/password"; + let url = this.baseURL + '1/' + this.username + "/password"; let res = new Weave.Resource(url); - res.authenticator = new Weave.NoOpAuthenticator(); - let resp = res.post(newpass); if (resp.status != 200) { this._log.info("Password change failed: " + resp); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 170ef4abeb1c..40cbd0f50065 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -788,7 +788,6 @@ Utils.EventListener.prototype = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); -Svc.GPrefs = new Preferences(); [["Annos", "@mozilla.org/browser/annotation-service;1", "nsIAnnotationService"], ["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], From eca60cf71226bddf67155eaa73748a37388df706 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 27 Aug 2009 01:40:43 -0400 Subject: [PATCH 1332/1860] bug 503703 - add make target for building a single-OS XPI, r=edilee. actually allows specifying multiple package targets --- services/crypto/Makefile | 341 ++------------------------------------- 1 file changed, 13 insertions(+), 328 deletions(-) mode change 100755 => 100644 services/crypto/Makefile diff --git a/services/crypto/Makefile b/services/crypto/Makefile old mode 100755 new mode 100644 index a9268fe75dbe..84a971726bc3 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -21,7 +21,6 @@ # # Contributor(s): # Dan Mills (original author) -# Godwin Chan (Darwin Universal Binary) # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -37,346 +36,32 @@ # # ***** END LICENSE BLOCK ***** -# OS detection - -xpidl = $(sdkdir)/bin/xpidl -link = link - -sys := $(shell uname -s) -wince = $(WINCE) - -ifeq ($(wince), 1) - os = WINNT - cxx = $(sdkdir)/sdk/bin/arm-wince-gcc - xpidl = $(sdkdir)/host/bin/host_xpidl - link = $(sdkdir)/sdk/bin/arm-wince-link - so = dll -else -ifeq ($(sys), Darwin) - os = Darwin - compiler = gcc3 - cxx = c++ - so = dylib - cppflags += -dynamiclib -DDEBUG -else -ifeq ($(sys), Linux) - os = Linux - compiler = gcc3 - cxx = c++ - so = so - cppflags += -shared -else -ifeq ($(sys), MINGW32_NT-6.1) - os = WINNT - compiler = msvc - cxx = cl - so = dll -else -ifeq ($(sys), MINGW32_NT-5.1) - os = WINNT - compiler = msvc - cxx = cl - so = dll -else -ifeq ($(sys), SunOS) - os = SunOS - compiler = cc - cxx = CC - so = so - cppflags += -G -else - $(error Sorry, your os is unknown/unsupported: $(sys)) -endif -endif -endif -endif -endif -endif - -# Arch detection - -machine := $(shell uname -m) - -ifeq ($(machine), arm) - arch = arm -else -ifeq ($(machine), i386) - arch = x86 -else -ifeq ($(machine), i586) - arch = x86 -else -ifeq ($(machine), i686) - arch = x86 -else -ifeq ($(machine), i86pc) - arch = x86 -else -ifeq ($(machine), ppc) - arch = ppc -else -ifeq ($(machine), Power Macintosh) - arch = ppc -else -ifeq ($(machine), x86_64) - arch = x86_64 -else - $(error: Sorry, your architecture is unknown/unsupported: $(machine)) -endif -endif -endif -endif -endif -endif -endif -endif - -# Universal binary so no need for $(arch) for Darwin - -ifeq ($(sys), Darwin) - platform = $(os) -else - platform = $(os)_$(arch)-$(compiler) -endif - -################################################################### -# Target and objects - -ifeq ($(sys), Darwin) - target = WeaveCrypto - target_i386 = WeaveCrypto.i386 - target_ppc = WeaveCrypto.ppc - so_target = $(target:=.$(so)) - so_target_i386 = $(target_i386:=.$(so)) - so_target_ppc = $(target_ppc:=.$(so)) - cpp_objects_i386 = $(cpp_sources:.cpp=.oi386) - cpp_objects_ppc = $(cpp_sources:.cpp=.oppc) -else - target = WeaveCrypto - so_target = $(target:=.$(so)) - cpp_objects = $(cpp_sources:.cpp=.o) -endif - -# source and path configurations -idl = IWeaveCrypto.idl -cpp_sources = WeaveCrypto.cpp WeaveCryptoModule.cpp +stage_dir=../dist/stage sdkdir ?= ${MOZSDKDIR} -destdir = .. -platformdir = $(destdir)/platform/$(platform) - -# FIXME: we don't actually require this for e.g. clean ifeq ($(sdkdir),) $(warning No 'sdkdir' variable given) $(warning It should point to the location of the Gecko SDK) $(warning For example: "make sdkdir=/foo/bar/baz") $(warning Or set the MOZSDKDIR environment variable to point to it) - $(error ) + $(error) endif -idl_headers = $(idl:.idl=.h) -idl_typelib = $(idl:.idl=.xpt) -cpp_objects = $(cpp_sources:.cpp=.o) -so_target = $(target:=.$(so)) - -headers = -I$(sdkdir)/include \ - -I$(sdkdir)/include/system_wrappers \ - -I$(sdkdir)/include/nss \ - -I$(sdkdir)/include/xpcom \ - -I$(sdkdir)/include/string \ - -I$(sdkdir)/include/pipnss \ - -I$(sdkdir)/include/nspr \ - -I$(sdkdir)/sdk/include - -# libraries -libdirs := $(sdkdir)/lib $(sdkdir)/bin -ifeq ($(wince),1) -libs := xpcomglue_s xpcom nspr4 \ - crmf smime3 ssl3 nss3 nssutil3 \ - plds4 plc4 +ifdef platform_target + platform=$(platform_target) else -libs := xpcomglue xpcomglue_s nspr4 \ - crmf smime3 ssl3 nss3 nssutil3 \ - plds4 plc4 + platform=* endif -ifeq ($(os), Linux) - libs := xpcom_core $(libs) -endif +all: build -# compiler and Linker Flags +.PHONY: build crypto rebuild_all -ifeq ($(os), Darwin) - libdirs := $(patsubst %,-L%,$(libdirs)) - libs := $(patsubst %,-l%,$(libs)) - cppflags_i386 += -c -pipe -Os -arch i386 \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -fshort-wchar -fpascal-strings -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) \ - -isysroot /Developer/SDKs/MacOSX10.4u.sdk - ldflags_i386 += -pthread -pipe -bundle -arch i386 \ - -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - $(libdirs) $(libs) - cppflags_ppc += -c -pipe -Os -arch ppc \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -fshort-wchar -fpascal-strings -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) \ - -isysroot /Developer/SDKs/MacOSX10.4u.sdk \ - -force_cpusubtype_ALL - ldflags_ppc += -pthread -pipe -bundle -arch ppc \ - -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk \ - -Wl,-executable_path,$(sdkdir)/bin \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,_NSGetModule \ - -force_cpusubtype_ALL \ - $(libdirs) $(libs) -else -ifeq ($(os), Linux) - libdirs := $(patsubst %,-L%,$(libdirs)) - libs := $(patsubst %,-l%,$(libs)) - cppflags += -pipe -Os \ - -fPIC -fno-rtti -fno-exceptions -fno-strict-aliasing \ - -fno-common -pthread \ - -Wall -Wconversion -Wpointer-arith -Woverloaded-virtual -Wsynth \ - -Wno-ctor-dtor-privacy -Wno-non-virtual-dtor -Wcast-align \ - -Wno-long-long \ - -include xpcom-config.h $(headers) -ifneq ($(arch), arm) - cppflags += -fshort-wchar -else -endif - ldflags += -pthread -pipe -DMOZILLA_STRICT_API \ - -Wl,-dead_strip \ - -Wl,-exported_symbol \ - -Wl,-z,defs -Wl,-h,WeaveCrypto.so \ - -Wl,-rpath-link,$(sdkdir)/bin \ - $(sdkdir)/lib/libxpcomglue_s.a \ - $(libdirs) $(libs) -else -ifeq ($(os), SunOS) - libdirs := $(patsubst %,-L%,$(libdirs)) - libs := $(patsubst %,-l%,$(libs)) - cppflags += -xO5 -s -ztext $(headers) - ldflags += -DMOZILLA_STRICT_API \ - $(sdkdir)/lib/libxpcomglue_s.a \ - $(libdirs) $(libs) -else -ifneq ($(wince),) - libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) - libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) - cppflags += -c -nologo -O1 -GR- -TP -Zc:wchar_t- -W3 -Gy $(headers) \ - -DMOZILLA_STRICT_API \ - -D"_WIN32_WCE=0x502" -D"UNDER_CE" -D"WIN32_PLATFORM_PSPC" \ - -D"WINCE" -D"ARM" -D"_ARM_" -D"POCKETPC2003_UI_MODEL" -DXP_WIN - ldflags += -DLL $(libdirs) $(libs) - rcflags := -r $(headers) -else -ifeq ($(os), WINNT) - libdirs := $(patsubst %,-LIBPATH:%,$(libdirs)) - libs := $(patsubst %,$(sdkdir)/lib/%.lib,$(libs)) - cppflags += -c -nologo -O1 -GR- -TP -MT -Zc:wchar_t- -W3 -Gy $(headers) \ - -DNDEBUG -DTRIMMED -D_CRT_SECURE_NO_DEPRECATE=1 \ - -D_CRT_NONSTDC_NO_DEPRECATE=1 -DWINVER=0x500 -D_WIN32_WINNT=0x500 \ - -D_WIN32_IE=0x0500 -DX_DISPLAY_MISSING=1 -DMOZILLA_VERSION=\"1.9pre\" \ - -DMOZILLA_VERSION_U=1.9pre -DHAVE_SNPRINTF=1 -D_WINDOWS=1 -D_WIN32=1 \ - -DWIN32=1 -DXP_WIN=1 -DXP_WIN32=1 -DHW_THREADS=1 -DSTDC_HEADERS=1 \ - -DWIN32_LEAN_AND_MEAN=1 -DNO_X11=1 -DHAVE_MMINTRIN_H=1 \ - -DHAVE_OLEACC_IDL=1 -DHAVE_ATLBASE_H=1 -DHAVE_WPCAPI_H=1 -D_X86_=1 \ - -DD_INO=d_ino - ldflags += -DLL -NOLOGO -SUBSYSTEM:WINDOWS -NXCOMPAT -SAFESEH -IMPLIB:fake.lib \ - $(libdirs) $(libs) \ - kernel32.lib user32.lib gdi32.lib winmm.lib wsock32.lib advapi32.lib - rcflags := -r $(headers) -endif -endif -endif -endif -endif +crypto: + $(MAKE) -C src install -###################################################################### +build: + cp -R -v components $(stage_dir) + cd platform;mkdir -p ../$(stage_dir)/platform;cp -R -v $(platform) ../$(stage_dir)/platform -.PHONY: all build install clean subst - -all: build # default target - -build: subst $(so_target) $(idl_typelib) - -install: build - mkdir -p $(destdir)/components - mkdir -p $(platformdir)/components - cp $(idl_typelib) $(destdir)/components - cp $(so_target) $(platformdir)/components - -clean: - rm -f $(so_target) $(so_target_i386) $(so_target_ppc) \ - $(cpp_objects) $(cpp_objects_i386) $(cpp_objects_ppc) \ - $(idl_typelib) $(idl_headers) $(target:=.res) fake.lib fake.exp WeaveCrypto.rc - -subst: - $(substitute) WeaveCrypto.rc.in > WeaveCrypto.rc - -# rules to build the c headers and .xpt from idl -$(idl_headers): $(idl) - $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) - -$(idl_typelib): $(idl) - $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) - -# build and link rules -ifeq ($(os), Darwin) - $(so_target): $(so_target_i386) $(so_target_ppc) - lipo -create -output $(so_target) -arch ppc $(so_target_ppc) \ - -arch i386 $(so_target_i386) - chmod +x $(so_target) - - #i386 - $(cpp_objects_i386): $(cpp_sources) - $(cxx) -o $@ $(cppflags_i386) $(@:.oi386=.cpp) - - $(so_target_i386): $(idl_headers) $(cpp_objects_i386) - $(cxx) -o $@ $(ldflags_i386) $(cpp_objects_i386) - chmod +x $(so_target_i386) - - #ppc - $(cpp_objects_ppc): $(cpp_sources) - $(cxx) -o $@ $(cppflags_ppc) $(@:.oppc=.cpp) - - $(so_target_ppc): $(idl_headers) $(cpp_objects_ppc) - $(cxx) -o $@ $(ldflags_ppc) $(cpp_objects_ppc) - chmod +x $(so_target_ppc) -else -ifeq ($(os), Linux) - $(so_target): $(idl_headers) - $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) - chmod +x $@ -else -ifeq ($(os), SunOS) - $(so_target): $(idl_headers) - $(cxx) $(cppflags) -o $@ $(cpp_sources) $(ldflags) - chmod +x $@ -else -ifeq ($(os), WINNT) - $(target:=.res): $(target:=.rc) - rc -Fo$@ $(rcflags) $(target:=.rc) - - $(cpp_objects): $(cpp_sources) - $(cxx) -Fo$@ -Fd$(@:.o=.pdb) $(cppflags) $(@:.o=.cpp) - - $(so_target): $(idl_headers) $(cpp_objects) $(target:=.res) - $(link) -OUT:$@ -PDB:$(@:.dll=.pdb) $(cpp_objects) $(target:=.res) $(ldflags) - chmod +x $@ -endif -endif -endif -endif +rebuild_all: crypto build From 6939b9786de63c145d9938bc49b784be00318644 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 28 Aug 2009 12:21:26 -0700 Subject: [PATCH 1333/1860] Remove unused fennec preference overlays, scripts, strings, styles. --- services/sync/locales/en-US/fennec.properties | 29 ------------------- services/sync/locales/en-US/preferences.dtd | 11 ------- 2 files changed, 40 deletions(-) delete mode 100644 services/sync/locales/en-US/fennec.properties delete mode 100644 services/sync/locales/en-US/preferences.dtd diff --git a/services/sync/locales/en-US/fennec.properties b/services/sync/locales/en-US/fennec.properties deleted file mode 100644 index b10ea7e31d3d..000000000000 --- a/services/sync/locales/en-US/fennec.properties +++ /dev/null @@ -1,29 +0,0 @@ - -fennec.logging-in = Weave is trying to log in... -fennec.logged-in = Weave is logged in. -fennec.turned-off = Weave is turned off. -fennec.login.error = Weave had an error when trying to log in. -fennec.login.error.detail = Login error: %S - -fennec.default.client.type = Mobile - -fennec.sync.start = Syncing Now... -fennec.sync.complete.time = Sync completed at %1$S, %2$S -fennec.sync.error.detail = Error: %S -fennec.sync.error.generic = Weave had an error when trying to sync. -fennec.sync.status = Syncing %S... -fennec.sync.status.detail = Syncing (%1$S %1$S)... - -fennec.turn.weave.off = Turn Weave Off -fennec.turn.weave.on = Turn Weave On - -fennec.username.is = You are user: %S -fenec.no.username = No username set. - -fennec.need.credentials = Weave needs more info from you to get started. -fennec.need.username = You must enter a Weave username. -fennec.need.password = You must enter a Weave password. -fennec.need.passphrase = You must enter a Weave passphrase. - -fennec.quitting = Cannot sync, Weave is quitting. -fennec.no.sync = Cannot sync, Weave is not logged in. \ No newline at end of file diff --git a/services/sync/locales/en-US/preferences.dtd b/services/sync/locales/en-US/preferences.dtd deleted file mode 100644 index 4cc4e2ab46e6..000000000000 --- a/services/sync/locales/en-US/preferences.dtd +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - From 519937b8c345b02cde16eb801f4084835f1687f9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 16:28:00 -0700 Subject: [PATCH 1334/1860] Bug 512457 - Report errors before they get eaten up by runInBatchMode Save the exception from inside runBatched and not have runInBatchMode return failure, so we can then expose the exception. --- services/sync/modules/engines/bookmarks.js | 6 +---- services/sync/modules/engines/history.js | 6 +---- services/sync/modules/util.js | 23 +++++++++++++++++++ .../tests/unit/test_bookmark_batch_fail.js | 19 +++++++++++++++ 4 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 services/sync/tests/unit/test_bookmark_batch_fail.js diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d93ef982379e..c1bacc464228 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -93,11 +93,7 @@ BookmarksEngine.prototype = { _storeObj: BookmarksStore, _trackerObj: BookmarksTracker, - _sync: function BookmarksEngine__sync() { - Svc.Bookmark.runInBatchMode({ - runBatched: Utils.bind2(this, SyncEngine.prototype._sync) - }, null); - } + _sync: Utils.batchSync("Bookmark", SyncEngine) }; function BookmarksStore() { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 8f0a26e1898a..d4142abd1c9c 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -81,11 +81,7 @@ HistoryEngine.prototype = { _storeObj: HistoryStore, _trackerObj: HistoryTracker, - _sync: function HistoryEngine__sync() { - Svc.History.runInBatchMode({ - runBatched: Utils.bind2(this, SyncEngine.prototype._sync) - }, null); - }, + _sync: Utils.batchSync("History", SyncEngine), // History reconciliation is simpler than the default one from SyncEngine, // because we have the advantage that we can use the URI as a definitive diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 40cbd0f50065..7e01782c749d 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -124,6 +124,29 @@ let Utils = { }; }, + batchSync: function batchSync(service, engineType) { + return function batchedSync() { + let engine = this; + let batchEx = null; + + // Try running sync in batch mode + Svc[service].runInBatchMode({ + runBatched: function wrappedSync() { + try { + engineType.prototype._sync.call(engine); + } + catch(ex) { + batchEx = ex; + } + } + }, null); + + // Expose the exception if something inside the batch failed + if (batchEx!= null) + throw batchEx; + }; + }, + // Generates a brand-new globally unique identifier (GUID). makeGUID: function makeGUID() { let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. diff --git a/services/sync/tests/unit/test_bookmark_batch_fail.js b/services/sync/tests/unit/test_bookmark_batch_fail.js new file mode 100644 index 000000000000..8833c1ee5cb0 --- /dev/null +++ b/services/sync/tests/unit/test_bookmark_batch_fail.js @@ -0,0 +1,19 @@ +_("Making sure a failing sync reports a useful error"); +Cu.import("resource://weave/engines/bookmarks.js"); + +function run_test() { + let engine = new BookmarksEngine(); + engine._syncStartup = function() { + throw "FAIL!"; + }; + + try { + _("Try calling the sync that should throw right away"); + engine._sync(); + do_throw("Should have failed sync!"); + } + catch(ex) { + _("Making sure what we threw ended up as the exception:", ex); + do_check_eq(ex, "FAIL!"); + } +} From 00efc0b7c18399470cf63ef08349fa2584740615 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 17:17:20 -0700 Subject: [PATCH 1335/1860] Remove mostly unused FaultTolerance service and fix up remaining references. --- services/sync/locales/en-US/status.properties | 3 - services/sync/modules/faultTolerance.js | 88 ------------------- services/sync/modules/service.js | 7 +- .../sync/tests/unit/test_fault_tolerance.js | 13 --- services/sync/tests/unit/test_load_modules.js | 1 - 5 files changed, 2 insertions(+), 110 deletions(-) delete mode 100644 services/sync/modules/faultTolerance.js delete mode 100644 services/sync/tests/unit/test_fault_tolerance.js diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties index d34a23fa28b7..05f87cfce094 100644 --- a/services/sync/locales/en-US/status.properties +++ b/services/sync/locales/en-US/status.properties @@ -6,9 +6,6 @@ status.success = Sync Complete status.error = Sync Failed (%1$S) status.closing = Closing... -status.locked = Server Busy -status.tryagain = Please try again in a few minutes. - status.engine.start = Starting Sync status.engine.process-incoming = Processing Incoming Items status.engine.upload-outgoing = Uploading Outgoing Items diff --git a/services/sync/modules/faultTolerance.js b/services/sync/modules/faultTolerance.js deleted file mode 100644 index 9d255565f88b..000000000000 --- a/services/sync/modules/faultTolerance.js +++ /dev/null @@ -1,88 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Atul Varma - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); - -const EXPORTED_SYMBOLS = ["FaultTolerance"]; - -FaultTolerance = { - get Service() { - if (!this._Service) - this._Service = new FTService(); - return this._Service; - } -}; - -function FTService() { - this._log = Log4Moz.repository.getLogger("FaultTolerance"); - this._log.level = Log4Moz.Level["Debug"]; - this._appender = new FTAppender(this); - Log4Moz.repository.rootLogger.addAppender(this._appender); -} -FTService.prototype = { - get lastException() this._lastException, - onMessage: function FTS_onMessage(message) { - // FIXME: we get all log messages here, and could use them to keep track of - // our current state - }, - - onException: function FTS_onException(ex) { - this._lastException = ex; - this._log.debug(Utils.exceptionStr(ex)); - return true; // continue sync if thrown by a sync engine - } -}; - -function FTFormatter() {} -FTFormatter.prototype = { - __proto__: new Log4Moz.Formatter(), - format: function FTF_format(message) message -}; - -function FTAppender(ftService) { - this._ftService = ftService; - this._name = "FTAppender"; - this._formatter = new FTFormatter(); -} -FTAppender.prototype = { - __proto__: new Log4Moz.Appender(), - doAppend: function FTA_append(message) { - this._ftService.onMessage(message); - } -}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e02d48a9beae..9aae38e91fc4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -76,7 +76,6 @@ Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/faultTolerance.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); @@ -90,7 +89,6 @@ Cu.import("resource://weave/engines/clientData.js"); let Weave = {}; Cu.import("resource://weave/constants.js", Weave); Cu.import("resource://weave/util.js", Weave); -Cu.import("resource://weave/faultTolerance.js", Weave); Cu.import("resource://weave/auth.js", Weave); Cu.import("resource://weave/resource.js", Weave); Cu.import("resource://weave/base_records/keys.js", Weave); @@ -314,7 +312,6 @@ WeaveSvc.prototype = { Svc.Observer.addObserver(this, "quit-application", true); Svc.Observer.addObserver(this, "weave:service:sync:finish", true); Svc.Observer.addObserver(this, "weave:service:sync:error", true); - FaultTolerance.Service; // initialize FT service if (!this.enabled) this._log.info("Weave Sync disabled"); @@ -1195,8 +1192,8 @@ WeaveSvc.prototype = { this._syncError = true; this._weaveStatusCode = WEAVE_STATUS_PARTIAL; this._detailedStatus.setEngineStatus(engine.name, e); - if (FaultTolerance.Service.onException(e)) - return true; + this._log.debug(Utils.exceptionStr(e)); + return true; } }, diff --git a/services/sync/tests/unit/test_fault_tolerance.js b/services/sync/tests/unit/test_fault_tolerance.js deleted file mode 100644 index 8cdb62b444ea..000000000000 --- a/services/sync/tests/unit/test_fault_tolerance.js +++ /dev/null @@ -1,13 +0,0 @@ -function run_test() { - Components.utils.import("resource://weave/log4moz.js"); - Components.utils.import("resource://weave/faultTolerance.js"); - - // Just make sure the getter works and the service is a singleton. - FaultTolerance.Service._testProperty = "hi"; - do_check_eq(FaultTolerance.Service._testProperty, "hi"); - - var log = Log4Moz.repository.rootLogger; - log.level = Log4Moz.Level.All; - log.info("Testing."); - do_check_eq(Log4Moz.repository.rootLogger.appenders.length, 1); -} diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js index 4b72f0a698f8..7ae176357962 100644 --- a/services/sync/tests/unit/test_load_modules.js +++ b/services/sync/tests/unit/test_load_modules.js @@ -21,7 +21,6 @@ const modules = [ "engines.js", "ext/Observers.js", "ext/Preferences.js", - "faultTolerance.js", "identity.js", "log4moz.js", "notifications.js", From d0832b1854e2b83c7d35270026985a4e0fe1fd4e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 17:27:30 -0700 Subject: [PATCH 1336/1860] Convert uses of recordLike/findLikeId to findDupe except Bookmarks is unimplemented. --- services/sync/modules/engines.js | 35 ++++++-------------- services/sync/modules/engines/bookmarks.js | 5 +++ services/sync/modules/engines/clients.js | 4 --- services/sync/modules/engines/forms.js | 13 ++++---- services/sync/modules/engines/history.js | 38 +++------------------- services/sync/modules/engines/passwords.js | 2 +- services/sync/modules/engines/prefs.js | 8 ----- 7 files changed, 26 insertions(+), 79 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index eea82b7e28aa..51f3b79a1cef 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -257,19 +257,6 @@ SyncEngine.prototype = { return this._store.createRecord(id, this.cryptoMetaURL); }, - // Check if a record is "like" another one, even though the IDs are different, - // in that case, we'll change the ID of the local item to match - // Probably needs to be overridden in a subclass, to change which criteria - // make two records "the same one" - _recordLike: function SyncEngine__recordLike(a, b) { - if (a.parentid != b.parentid) - return false; - // note: sortindex ignored - if (a.deleted || b.deleted) - return false; - return Utils.deepEquals(a.cleartext, b.cleartext); - }, - // Any setup that needs to happen at the beginning of each sync. // Makes sure crypto records and keys are all set-up _syncStartup: function SyncEngine__syncStartup() { @@ -355,15 +342,13 @@ SyncEngine.prototype = { }, /** - * Find a GUID that is like the incoming item + * Find a GUID of an item that is a duplicate of the incoming item but happens + * to have a different GUID * - * @return GUID of the similar record; falsy otherwise + * @return GUID of the similar item; falsy otherwise */ - _findLikeId: function SyncEngine__findLikeId(item) { - // By default, only look in the outgoing queue for similar records - for (let id in this._tracker.changedIDs) - if (this._recordLike(item, this._createRecord(id))) - return id; + _findDupe: function _findDupe(item) { + // By default, assume there's no dupe items for the engine }, _isEqual: function SyncEngine__isEqual(item) { @@ -417,19 +402,19 @@ SyncEngine.prototype = { // Step 3: Check for similar items this._log.trace("Reconcile step 3"); - let likeId = this._findLikeId(item); - if (likeId) { + let dupeId = this._findDupe(item); + if (dupeId) { // Change the local item GUID to the incoming one - this._store.changeItemID(likeId, item.id); + this._store.changeItemID(dupeId, item.id); // Remove outgoing changes of the original id any any that were just made - this._tracker.removeChangedID(likeId); + this._tracker.removeChangedID(dupeId); this._tracker.removeChangedID(item.id); this._store.cache.clear(); // because parentid refs will be wrong - return false; } + // Apply the incoming item (now that the dupe is the right id) return true; }, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d78063c4bead..b89d21614465 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -97,6 +97,11 @@ BookmarksEngine.prototype = { Svc.Bookmark.runInBatchMode({ runBatched: Utils.bind2(this, SyncEngine.prototype._sync) }, null); + }, + + _findDupe: function _findDupe(item) { + // TODO for bookmarks, check if it exists and find guid + // for everything else (folders, separators) look for parent/pred? } }; diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 770358358f1b..3336428ba241 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -69,10 +69,6 @@ ClientEngine.prototype = { Utils.prefs.addObserver("", this, false); }, - // We never want to change one client id to another, even if - // they look exactly the same - _findLikeId: function ClientEngine__findLikeId() {}, - // get and set info for clients // FIXME: callers must use the setInfo interface or changes won't get synced, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 2ca20f954135..05e2eedd5269 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -78,13 +78,12 @@ FormEngine.prototype = { coll.full = 0; coll.delete(); }, - - _recordLike: function SyncEngine__recordLike(a, b) { - if (a.deleted || b.deleted) - return false; - if (a.name == b.name && a.value == b.value) - return true; - return false; + + _findDupe: function _findDupe(item) { + // Search through the items to find a matching name/value + for (let [guid, {name, value}] in Iterator(this._store._formItems)) + if (name == item.name && value == item.value) + return guid; } }; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 8f0a26e1898a..fe71d3154c94 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -87,34 +87,6 @@ HistoryEngine.prototype = { }, null); }, - // History reconciliation is simpler than the default one from SyncEngine, - // because we have the advantage that we can use the URI as a definitive - // check for local existence of incoming items. The steps are as follows: - // - // 1) Check for the same item in the locally modified list. In that case, - // local trumps remote. This step is unchanged from our superclass. - // 2) Check if the incoming item was deleted. Skip if so. - // 3) Apply new record/update. - // - // Note that we don't attempt to equalize the IDs, the history store does that - // as part of update() - _reconcile: function HistEngine__reconcile(item) { - // Step 1: Check for conflicts - // If same as local record, do not upload - if (item.id in this._tracker.changedIDs) { - if (this._isEqual(item)) - this._tracker.removeChangedID(item.id); - return false; - } - - // Step 2: Check if the item is deleted - we don't support that (yet?) - if (item.deleted) - return false; - - // Step 3: Apply update/new record - return true; - }, - _syncFinish: function HistEngine__syncFinish(error) { this._log.debug("Finishing up sync"); this._tracker.resetScore(); @@ -124,6 +96,10 @@ HistoryEngine.prototype = { coll.older = this.lastSync - 604800; // 1 week coll.full = 0; coll.delete(); + }, + + _findDupe: function _findDupe(item) { + return GUIDForUri(item.histUri); } }; @@ -269,12 +245,6 @@ HistoryStore.prototype = { (visit.type == 5 || visit.type == 6), 0); } this._hsvc.setPageTitle(uri, record.title); - - // Equalize IDs - let localId = GUIDForUri(record.histUri); - if (localId != record.id) - this.changeItemID(localId, record.id); - }, itemExists: function HistStore_itemExists(id) { diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 5bb23cd53bc2..069f9d2113ee 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -61,7 +61,7 @@ PasswordEngine.prototype = { _trackerObj: PasswordTracker, _recordObj: LoginRec, - _findLikeId: function PasswordEngine__findLikeId(item) { + _findDupe: function _findDupe(item) { let login = this._store._nsLoginInfoFromRecord(item); let logins = Svc.Login.findLogins({}, login.hostname, login.formSubmitURL, login.httpRealm); diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 00f533f79fda..a260df618cd5 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -63,14 +63,6 @@ PrefsEngine.prototype = { _storeObj: PrefStore, _trackerObj: PrefTracker, _recordObj: PrefRec, - - _recordLike: function SyncEngine__recordLike(a, b) { - if (a.deleted || b.deleted) - return false; - if (a.name == b.name && a.value == b.value) - return true; - return false; - } }; From 366d942fbc7eb0ec2bc682a81b58a5d3bbc5c4c7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 17:29:47 -0700 Subject: [PATCH 1337/1860] Use places utils to find an existing bookmark dupe for an incoming record. --- services/sync/modules/engines/bookmarks.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index b89d21614465..c374c1a6cae4 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -46,6 +46,7 @@ const PARENT_ANNO = "weave/parent"; const PREDECESSOR_ANNO = "weave/predecessor"; const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; +Cu.import("resource://gre/modules/utils.js"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); @@ -100,6 +101,17 @@ BookmarksEngine.prototype = { }, _findDupe: function _findDupe(item) { + switch (item.type) { + case "bookmark": + case "query": + case "microsummary": + // Find a bookmark that has the same uri + let uri = Utils.makeURI(item.bmkUri); + let localId = PlacesUtils.getMostRecentBookmarkForURI(uri); + if (localId == -1) + return; + return GUIDForId(localId); + } // TODO for bookmarks, check if it exists and find guid // for everything else (folders, separators) look for parent/pred? } From 71c516629a3078e9099b9eacee5b7dc87c1aee74 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 17:30:34 -0700 Subject: [PATCH 1338/1860] Switch end-of-sync deletes to something SyncEngine is aware of and runs at _syncFinish. --- services/sync/modules/engines.js | 14 ++++++++++++++ services/sync/modules/engines/forms.js | 7 ++----- services/sync/modules/engines/history.js | 9 ++------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 51f3b79a1cef..84cd547c36f0 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -288,6 +288,9 @@ SyncEngine.prototype = { let outnum = [i for (i in this._tracker.changedIDs)].length; this._log.info(outnum + " outgoing items pre-reconciliation"); + + // Keep track of what to delete at the end of sync + this._delete = {}; }, // Generate outgoing records @@ -484,6 +487,17 @@ SyncEngine.prototype = { _syncFinish: function SyncEngine__syncFinish() { this._log.trace("Finishing up sync"); this._tracker.resetScore(); + + for (let [key, val] in Iterator(this._delete)) { + // Remove the key for future uses + delete this._delete[key]; + + // Send a delete for the property + this._log.info("Sending delete for " + key + ": " + val); + let coll = new Collection(this.engineURL, this._recordObj); + coll[key] = val; + coll.delete(); + } }, _sync: function SyncEngine__sync() { diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 05e2eedd5269..5e0da875772c 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -72,11 +72,8 @@ FormEngine.prototype = { this._store.clearFormCache(); // Only leave 1 month's worth of form history - this._tracker.resetScore(); - let coll = new Collection(this.engineURL, this._recordObj); - coll.older = this.lastSync - 2592000; // 60*60*24*30 - coll.full = 0; - coll.delete(); + this._delete.older = this.lastSync - 2592000; + SyncEngine.prototype._syncFinish.call(this); }, _findDupe: function _findDupe(item) { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index fe71d3154c94..4f791fb26d5c 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -88,14 +88,9 @@ HistoryEngine.prototype = { }, _syncFinish: function HistEngine__syncFinish(error) { - this._log.debug("Finishing up sync"); - this._tracker.resetScore(); - // Only leave 1 week's worth of history on the server - let coll = new Collection(this.engineURL, this._recordObj); - coll.older = this.lastSync - 604800; // 1 week - coll.full = 0; - coll.delete(); + this._delete.older = this.lastSync - 604800; + SyncEngine.prototype._syncFinish.call(this); }, _findDupe: function _findDupe(item) { From 2ac4388b65f0dda2a63228a66078591000ea0d3a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 17:50:23 -0700 Subject: [PATCH 1339/1860] Implement multiple ids deletion for use with 0.3 APIs. --- services/sync/modules/engines.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 84cd547c36f0..5bb029595bc4 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -492,6 +492,13 @@ SyncEngine.prototype = { // Remove the key for future uses delete this._delete[key]; + // Specially handle ids deletion until we have collection?ids=,,, + if (key == "ids") { + for each (let id in val) + new Resource(this.engineURL + id).delete(); + continue; + } + // Send a delete for the property this._log.info("Sending delete for " + key + ": " + val); let coll = new Collection(this.engineURL, this._recordObj); From c24c139e80878e25bf371bc34e1318c18bf7154e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 17:51:26 -0700 Subject: [PATCH 1340/1860] Pick a canonical guid to keep in a way that all machines can agree without having all data -- "smaller" guid. --- services/sync/modules/engines.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 5bb029595bc4..e31b51e0bebf 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -370,6 +370,16 @@ SyncEngine.prototype = { } }, + _deleteId: function _deleteId(id) { + this._tracker.removeChangedID(id); + + // Remember this id to delete at the end of sync + if (this._delete.ids == null) + this._delete.ids = [id]; + else + this._delete.ids.push(id); + }, + // Reconciliation has three steps: // 1) Check for the same item (same ID) on both the incoming and outgoing // queues. This means the same item was modified on this profile and @@ -407,12 +417,16 @@ SyncEngine.prototype = { this._log.trace("Reconcile step 3"); let dupeId = this._findDupe(item); if (dupeId) { - // Change the local item GUID to the incoming one - this._store.changeItemID(dupeId, item.id); - - // Remove outgoing changes of the original id any any that were just made - this._tracker.removeChangedID(dupeId); - this._tracker.removeChangedID(item.id); + // Stick with the canonical lower id, so convert the dupe to incoming + if (item.id < dupeId) { + this._store.changeItemID(dupeId, item.id); + this._deleteId(dupeId); + } + // The local dupe is the lower id, so pretend the incoming is for it + else { + this._deleteId(item.id); + item.id = dupeId; + } this._store.cache.clear(); // because parentid refs will be wrong } From 91eb99398950c744c238330430813d08ae1722cc Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 17:54:21 -0700 Subject: [PATCH 1341/1860] Detect dupe folders by title for now. Might need to update the hash during sync if titles are added. --- services/sync/modules/engines/bookmarks.js | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c374c1a6cae4..beb5abd53b79 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -100,6 +100,30 @@ BookmarksEngine.prototype = { }, null); }, + _syncStartup: function _syncStart() { + SyncEngine.prototype._syncStartup.call(this); + + // Lazily create a mapping of folder titles to ids if necessary + this.__defineGetter__("_folderTitles", function() { + delete this._folderTitles; + + let folderTitles = {}; + for (let guid in this._store.getAllIDs()) { + let id = idForGUID(guid); + if (Svc.Bookmark.getItemType(id) != Svc.Bookmark.TYPE_FOLDER) + continue; + + folderTitles[Svc.Bookmark.getItemTitle(id)] = guid; + } + return this._folderTitles = folderTitles; + }); + }, + + _syncFinish: function _syncFinish() { + SyncEngine.prototype._syncFinish.call(this); + delete this._folderTitles; + }, + _findDupe: function _findDupe(item) { switch (item.type) { case "bookmark": @@ -111,6 +135,9 @@ BookmarksEngine.prototype = { if (localId == -1) return; return GUIDForId(localId); + case "folder": + case "livemark": + return this._folderTitles[item.title]; } // TODO for bookmarks, check if it exists and find guid // for everything else (folders, separators) look for parent/pred? From 3f876f5458442a4952afc26240e70d058f8a04f9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 18:04:40 -0700 Subject: [PATCH 1342/1860] Resolve bookmarks.js merge conflicts switching to batchSync (with trailing comma). --- services/sync/modules/engines/bookmarks.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 2761f95894dd..f81c1c1e1333 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -94,12 +94,7 @@ BookmarksEngine.prototype = { _storeObj: BookmarksStore, _trackerObj: BookmarksTracker, -<<<<<<< local - _sync: function BookmarksEngine__sync() { - Svc.Bookmark.runInBatchMode({ - runBatched: Utils.bind2(this, SyncEngine.prototype._sync) - }, null); - }, + _sync: Utils.batchSync("Bookmark", SyncEngine), _syncStartup: function _syncStart() { SyncEngine.prototype._syncStartup.call(this); @@ -143,9 +138,6 @@ BookmarksEngine.prototype = { // TODO for bookmarks, check if it exists and find guid // for everything else (folders, separators) look for parent/pred? } -======= - _sync: Utils.batchSync("Bookmark", SyncEngine) ->>>>>>> other }; function BookmarksStore() { From 17d34561e2667036807bb59475e6d74c5bad3376 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 31 Aug 2009 18:30:44 -0700 Subject: [PATCH 1343/1860] Switch to 0.5 server API for deleting multiple ids from a collection. --- services/sync/modules/base_records/collection.js | 10 ++++++++++ services/sync/modules/engines.js | 9 +-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 57d3a286794f..9a5bcf4652a6 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -61,6 +61,7 @@ Collection.prototype = { this._init(uri); this.pushFilter(new JsonFilter()); this._full = true; + this._ids = null; this._older = 0; this._newer = 0; this._data = []; @@ -80,6 +81,8 @@ Collection.prototype = { args.push('full=1'); if (this.sort) args.push('sort=' + this.sort); + if (this.ids != null) + args.push("ids=" + this.ids); this.uri.query = (args.length > 0)? '?' + args.join('&') : ''; }, @@ -91,6 +94,13 @@ Collection.prototype = { this._rebuildURL(); }, + // Apply the action to a certain set of ids + get ids() this._ids, + set ids(value) { + this._ids = value; + this._rebuildURL(); + }, + // get only items modified before some date get older() { return this._older; }, set older(value) { diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 34be75f27f8d..eaf53fb9e8dc 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -519,15 +519,8 @@ SyncEngine.prototype = { // Remove the key for future uses delete this._delete[key]; - // Specially handle ids deletion until we have collection?ids=,,, - if (key == "ids") { - for each (let id in val) - new Resource(this.engineURL + id).delete(); - continue; - } - // Send a delete for the property - this._log.info("Sending delete for " + key + ": " + val); + this._log.debug("Sending delete for " + key + ": " + val); let coll = new Collection(this.engineURL, this._recordObj); coll[key] = val; coll.delete(); From 6d289e9be1216364cb7cb5ecf1c272715a741586 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 3 Sep 2009 01:03:05 -0700 Subject: [PATCH 1344/1860] Bug 512600 - Split rel/dev-track users on to separate servers. r=thunder Insert the server_url for the default preference at package time. For now, have both rel and dev use auth.smc. --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index d20b86ddc1bb..81a84f6c872d 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,4 @@ -pref("extensions.weave.serverURL", "https://auth.services.mozilla.com/user/"); +pref("extensions.weave.serverURL", "@server_url@"); pref("extensions.weave.miscURL", "https://auth.services.mozilla.com/misc/"); pref("extensions.weave.termsURL", "https://labs.mozilla.com/projects/weave/tos/"); From d5ea20f5f8fab356fe29d9945961f3b96bb51993 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 3 Sep 2009 19:57:02 -0700 Subject: [PATCH 1345/1860] Remove unimplemented engines so they do not initialize. --- services/sync/modules/service.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9aae38e91fc4..3b8ac2db1ef0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -383,9 +383,7 @@ WeaveSvc.prototype = { break; case FIREFOX_ID: - engines = ["Bookmarks", "Cookie", "Extension", "Form", "History", - "Input", "MicroFormat", "Password", "Plugin", "Prefs", "Tab", - "Theme"]; + engines = ["Bookmarks", "Form", "History", "Password", "Prefs", "Tab"]; break; case SEAMONKEY_ID: From 532ba25cf835e1d8ebc201e9e002149c970fb63b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 3 Sep 2009 20:11:36 -0700 Subject: [PATCH 1346/1860] Bug 514600 - Track how long various parts of sync take Instrument all functions that are part of the sync engine (except some constructors, etc.) and generate statistics (min/max/sum/num/avg) for processing. For now with the default appender, implement toString to report just the total time. --- services/sync/modules/engines.js | 73 +++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f4a7a6cf337e..12e3c6ef0655 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -183,7 +183,78 @@ Engine.prototype = { sync: function Engine_sync() { if (!this._sync) throw "engine does not implement _sync method"; - this._notify("sync", this.name, this._sync)(); + + let times = {}; + let wrapped = {}; + // Find functions in any point of the prototype chain + for (let _name in this) { + let name = _name; + + // Ignore certain constructors/functions + if (name.search(/^_(.+Obj|notify)$/) == 0) + continue; + + // Only track functions but skip the constructors + if (typeof this[name] == "function") { + times[name] = []; + wrapped[name] = this[name]; + + // Wrap the original function with a start/stop timer + this[name] = function() { + let start = Date.now(); + try { + return wrapped[name].apply(this, arguments); + } + finally { + times[name].push(Date.now() - start); + } + }; + } + } + + try { + this._notify("sync", this.name, this._sync)(); + } + finally { + // Restore original unwrapped functionality + for (let [name, func] in Iterator(wrapped)) + this[name] = func; + + let stats = {}; + for (let [name, time] in Iterator(times)) { + // Figure out stats on the times unless there's nothing + let num = time.length; + if (num == 0) + continue; + + // Track the min/max/sum of the values + let stat = { + num: num, + sum: 0 + }; + time.forEach(function(val) { + if (val < stat.min || stat.min == null) + stat.min = val; + if (val > stat.max || stat.max == null) + stat.max = val; + stat.sum += val; + }); + + stat.avg = Number((stat.sum / num).toFixed(2)); + stats[name] = stat; + } + + stats.toString = function() { + let sums = []; + for (let [name, stat] in Iterator(this)) + if (stat.sum != null) + sums.push(name.replace(/^_/, "") + " " + stat.sum); + + return "Total (ms): " + sums.sort().join(", "); + }; + + this._log.info(stats); + } }, wipeServer: function Engine_wipeServer() { From 9cf978f2d9c749884b00b60a5a2e425b1bd68f86 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 3 Sep 2009 21:11:32 -0700 Subject: [PATCH 1347/1860] Bug 513191 - cloud -> client sync wipes auth cache, including weave credentials. r=thunder Bug 506790 - Utils.setPassword unnecessarily removes logins and add them when the realm/user/pass are the same Bug 506792 - Utils.setPassword gets called multiple times for both password and passphrase on login Bug 514499 - Passphrase not verified with auto-login Don't aggressively persist the password with smart (dumb?) setters that clear out the temp password; and provide a separate function to persist the login. This effectively makes setting password/passphrase always temporary until persisted, which will check if the value is different from the one already stored on disk. A number of verify/cluster functions are privitized to not need to take user/pass/passph as arguments so that the default authenticator will work, and verifyPassphrase will use the stored passphrase to correctly handle auto-login. --- services/sync/modules/constants.js | 5 +- services/sync/modules/identity.js | 74 +++++++++++++------------ services/sync/modules/service.js | 88 ++++++++++++------------------ services/sync/modules/util.js | 35 ------------ 4 files changed, 78 insertions(+), 124 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 7055b0630dbc..f6e7da3934a6 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -35,7 +35,7 @@ * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", - 'PREFS_BRANCH', + "PREFS_BRANCH", "PWDMGR_HOST", 'MODE_RDONLY', 'MODE_WRONLY', 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', @@ -64,6 +64,9 @@ const COMPATIBLE_VERSION = "@compatible_version@"; const PREFS_BRANCH = "extensions.weave."; +// Host "key" to access Weave Identity in the password manager +const PWDMGR_HOST = "chrome://weave"; + const MODE_RDONLY = 0x01; const MODE_WRONLY = 0x02; const MODE_CREATE = 0x08; diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 0c138e353a71..828b34bd23e4 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -44,6 +44,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Utils.lazy(this, 'ID', IDManager); @@ -76,48 +77,49 @@ IDManager.prototype = { */ function Identity(realm, username, password) { - this._realm = realm; - this._username = username; + this.realm = realm; + this.username = username; this._password = password; } Identity.prototype = { - get realm() this._realm, - set realm(value) { - let current = Utils.findPassword(this.realm, this.username); - if (current) - this.password = null; - - this._realm = value; - - if (current) - this.password = current; - }, - - get username() this._username, - set username(value) { - let current = Utils.findPassword(this.realm, this.username); - if (current) - this.password = null; - - this._username = value; - - if (current) - this.password = current; - }, - - get userHash() { return Utils.sha1(this.username); }, - - get password() { + get password password() { // Look up the password then cache it - if (!this._password) - return this._password = Utils.findPassword(this.realm, this.username); + if (this._password == null) + for each (let login in this._logins) + if (login.username == this.username) + this._password = login.password; return this._password; }, - set password(value) { - Utils.setPassword(this.realm, this.username, value); + + set password password(value) { + this._password = value; }, - setTempPassword: function Id_setTempPassword(value) { - this._password = value; - } + persist: function persist() { + // Clean up any existing passwords unless it's what we're persisting + let exists = false; + for each (let login in this._logins) { + if (login.username == this.username && login.password == this._password) + exists = true; + else + Svc.Login.removeLogin(login); + } + + // No need to create the login after clearing out the other ones + let log = Log4Moz.repository.getLogger("Identity"); + if (exists) { + log.trace("Skipping persist: " + this.realm + " for " + this.username); + return; + } + + // Add the new username/password + log.trace("Persisting " + this.realm + " for " + this.username); + let nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo(PWDMGR_HOST, null, this.realm, + this.username, this.password, "", ""); + Svc.Login.addLogin(login); + }, + + get _logins _logins() Svc.Login.findLogins({}, PWDMGR_HOST, null, this.realm) }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3b8ac2db1ef0..c3374b7b116f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -187,17 +187,11 @@ WeaveSvc.prototype = { this._genKeyURLs(); }, - get password() { return ID.get('WeaveID').password; }, - set password(value) { - ID.get('WeaveID').setTempPassword(null); - ID.get('WeaveID').password = value; - }, + get password password() ID.get("WeaveID").password, + set password password(value) ID.get("WeaveID").password = value, - get passphrase() { return ID.get('WeaveCryptoID').password; }, - set passphrase(value) { - ID.get('WeaveCryptoID').setTempPassword(null); - ID.get('WeaveCryptoID').password = value; - }, + get passphrase passphrase() ID.get("WeaveCryptoID").password, + set passphrase passphrase(value) ID.get("WeaveCryptoID").password = value, get baseURL() { return Utils.getURLPref("serverURL"); @@ -449,10 +443,10 @@ WeaveSvc.prototype = { // These are global (for all engines) // gets cluster from central LDAP server and returns it, or null on error - findCluster: function WeaveSvc_findCluster(username) { - this._log.debug("Finding cluster for user " + username); + _findCluster: function _findCluster() { + this._log.debug("Finding cluster for user " + this.username); - let res = new Resource(this.baseURL + "1/" + username + "/node/weave"); + let res = new Resource(this.baseURL + "1/" + this.username + "/node/weave"); try { let node = res.get(); switch (node.status) { @@ -474,9 +468,8 @@ WeaveSvc.prototype = { }, // gets cluster from central LDAP server and sets this.clusterURL - setCluster: function WeaveSvc_setCluster(username) { - let cluster = this.findCluster(username); - + _setCluster: function _setCluster() { + let cluster = this._findCluster(); if (cluster) { if (cluster == this.clusterURL) return false; @@ -486,16 +479,16 @@ WeaveSvc.prototype = { return true; } - this._log.debug("Error setting cluster for user " + username); + this._log.debug("Error setting cluster for user " + this.username); return false; }, // update cluster if required. returns false if the update was not required - updateCluster: function WeaveSvc_updateCluster(username) { + _updateCluster: function _updateCluster() { let cTime = Date.now(); let lastUp = parseFloat(Svc.Prefs.get("lastClusterUpdate")); if (!lastUp || ((cTime - lastUp) >= CLUSTER_BACKOFF)) { - if (this.setCluster(username)) { + if (this._setCluster()) { Svc.Prefs.set("lastClusterUpdate", cTime.toString()); return true; } @@ -503,35 +496,24 @@ WeaveSvc.prototype = { return false; }, - verifyLogin: function WeaveSvc_verifyLogin(username, password, passphrase, isLogin) + _verifyLogin: function _verifyLogin() this._catch(this._notify("verify-login", "", function() { - this._log.debug("Verifying login for user " + username); - - let url = this.findCluster(username); - if (isLogin) - this.clusterURL = url; + this._log.debug("Verifying login for user " + this.username); + this._setCluster(); let res = new Resource(this.userURL + "/info/collections"); - res.authenticator = { - onRequest: function(headers) { - headers['Authorization'] = 'Basic ' + btoa(username + ':' + password); - return headers; - } - }; - - // login may fail because of cluster change try { let test = res.get(); switch (test.status) { case 200: - if (passphrase && !this.verifyPassphrase(username, password, passphrase)) { + if (!this._verifyPassphrase()) { this._setSyncFailure(LOGIN_FAILED_INVALID_PASSPHRASE); return false; } return true; case 401: - if (this.updateCluster(username)) - return this.verifyLogin(username, password, passphrase, isLogin); + if (this._updateCluster()) + return this._verifyLogin(); this._setSyncFailure(LOGIN_FAILED_LOGIN_REJECTED); this._log.debug("verifyLogin failed: login failed") @@ -547,24 +529,20 @@ WeaveSvc.prototype = { } }))(), - verifyPassphrase: function WeaveSvc_verifyPassphrase(username, password, passphrase) + _verifyPassphrase: function _verifyPassphrase() this._catch(this._notify("verify-passphrase", "", function() { this._log.debug("Verifying passphrase"); - this.username = username; - ID.get("WeaveID").setTempPassword(password); - try { let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); return Svc.Crypto.verifyPassphrase( - privkey.payload.keyData, passphrase, + privkey.payload.keyData, this.passphrase, privkey.payload.salt, privkey.payload.iv ); } catch (e) { // this means no keys are present (or there's a network error) return true; } - return true; }))(), changePassphrase: function WeaveSvc_changePassphrase(newphrase) @@ -666,6 +644,15 @@ WeaveSvc.prototype = { interval / 1000 + " seconds."); }, + persistLogin: function persistLogin() { + // Canceled master password prompt can prevent these from succeeding + try { + ID.get("WeaveID").persist(); + ID.get("WeaveCryptoID").persist(); + } + catch(ex) {} + }, + login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; @@ -673,12 +660,12 @@ WeaveSvc.prototype = { if (Svc.IO.offline) throw "Application is offline, login should not be called"; - if (typeof(username) != "undefined") + if (username != null) this.username = username; - if (typeof(password) != "undefined") - ID.get("WeaveID").setTempPassword(password); - if (typeof(passphrase) != "undefined") - ID.get("WeaveCryptoID").setTempPassword(passphrase); + if (password != null) + this.password = password; + if (passphrase != null) + this.passphrase = passphrase; if (!this.username) { this._setSyncFailure(LOGIN_FAILED_NO_USERNAME); @@ -690,8 +677,7 @@ WeaveSvc.prototype = { } this._log.debug("Logging in user " + this.username); - if (!(this.verifyLogin(this.username, this.password, - passphrase, true))) { + if (!this._verifyLogin()) { // verifyLogin sets the failure states here throw "Login failed: " + this.detailedStatus.sync; } @@ -711,8 +697,6 @@ WeaveSvc.prototype = { this._log.info("Logging out"); this._loggedIn = false; this._keyPair = {}; - ID.get('WeaveID').setTempPassword(null); // clear cached password - ID.get('WeaveCryptoID').setTempPassword(null); // and passphrase // Cancel the sync timer now that we're logged out this._checkSyncStatus(); @@ -1184,7 +1168,7 @@ WeaveSvc.prototype = { } catch(e) { // maybe a 401, cluster update needed? - if (e.status == 401 && this.updateCluster(this.username)) + if (e.status == 401 && this._updateCluster()) return this._syncEngine(engine); this._syncError = true; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 7e01782c749d..e2295856e3d8 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -209,41 +209,6 @@ let Utils = { return file; }, - findPassword: function findPassword(realm, username) { - // fixme: make a request and get the realm ? - let password; - let logins = Svc.Login.findLogins({}, 'chrome://weave', null, realm); - - for (let i = 0; i < logins.length; i++) { - if (logins[i].username == username) { - password = logins[i].password; - break; - } - } - return password; - }, - - setPassword: function setPassword(realm, username, password) { - // cleanup any existing passwords - let lm = Svc.Login; - let logins = lm.findLogins({}, 'chrome://weave', null, realm); - for (let i = 0; i < logins.length; i++) - lm.removeLogin(logins[i]); - - if (!password) - return; - - let log = Log4Moz.repository.getLogger("Service.Util"); - log.trace("Setting '" + realm + "' password for user " + username); - - // save the new one - let nsLoginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new nsLoginInfo('chrome://weave', null, realm, - username, password, "", ""); - lm.addLogin(login); - }, - /** * Add a simple getter/setter to an object that defers access of a property * to an inner property. From c567d7439654e7acd0068dd3e55c7a017262642b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 3 Sep 2009 21:30:40 -0700 Subject: [PATCH 1348/1860] Bug 514323 - Use info/collections to check if there's new data. r=thunder Fetch info/collections before syncing engines and set the lastModified property on the engine so that they can check if they need to process incoming. --- services/sync/modules/engines.js | 6 ++++++ services/sync/modules/service.js | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 12e3c6ef0655..0064fef9c939 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -380,6 +380,12 @@ SyncEngine.prototype = { // Generate outgoing records _processIncoming: function SyncEngine__processIncoming() { + // Only bother getting data from the server if there's new things + if (this.lastModified <= this.lastSync) { + this._log.debug("Nothing new from the server to process"); + return; + } + this._log.debug("Downloading & applying server changes"); // enable cache, and keep only the first few items. Otherwise (when diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c3374b7b116f..0ce91a2cb13c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -215,9 +215,8 @@ WeaveSvc.prototype = { this._genKeyURLs(); }, - get userURL() { - return this.clusterURL + this.username; - }, + get userURL() this.clusterURL + this.username, + get infoURL() this.userURL + "/info/collections", get userPath() { return ID.get('WeaveID').username; }, @@ -501,7 +500,7 @@ WeaveSvc.prototype = { this._log.debug("Verifying login for user " + this.username); this._setCluster(); - let res = new Resource(this.userURL + "/info/collections"); + let res = new Resource(this.infoURL); try { let test = res.get(); switch (test.status) { @@ -1080,6 +1079,16 @@ WeaveSvc.prototype = { if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; + // Figure out what the last modified time is for each collection + let info = new Resource(this.infoURL).get(); + if (!info.success) + throw "aborting sync, failed to get collections"; + + // Convert the response to an object and read out the modified times + info = JSON.parse(info); + for each (let engine in [Clients].concat(Engines.getEnabled())) + engine.lastModified = info[engine.name] || 0; + this._log.debug("Refreshing client list"); Clients.sync(); @@ -1215,7 +1224,7 @@ WeaveSvc.prototype = { wipeServer: function WeaveSvc_wipeServer(engines) this._catch(this._notify("wipe-server", "", function() { // Grab all the collections for the user - let res = new Resource(this.userURL + "/info/collections"); + let res = new Resource(this.infoURL); res.get(); // Get the array of collections and delete each one From 344363dcc4024f4ad5f5e71f333eae23aaa7655f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 7 Sep 2009 18:24:21 -0700 Subject: [PATCH 1349/1860] Remove the brief log now that we only show verbose. --- services/sync/modules/service.js | 17 ++++------------- services/sync/services-sync.js | 1 - 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0ce91a2cb13c..4ff8e797ce5f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -339,29 +339,20 @@ WeaveSvc.prototype = { dapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.dump")]; root.addAppender(dapp); - let brief = Svc.Directory.get("ProfD", Ci.nsIFile); - brief.QueryInterface(Ci.nsILocalFile); - brief.append("weave"); - brief.append("logs"); - brief.append("brief-log.txt"); - if (!brief.exists()) - brief.create(brief.NORMAL_FILE_TYPE, PERMS_FILE); - - let verbose = brief.parent.clone(); + let verbose = Svc.Directory.get("ProfD", Ci.nsIFile); + verbose.QueryInterface(Ci.nsILocalFile); + verbose.append("weave"); + verbose.append("logs"); verbose.append("verbose-log.txt"); if (!verbose.exists()) verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); - this._briefApp = new Log4Moz.RotatingFileAppender(brief, formatter); - this._briefApp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.briefLog")]; - root.addAppender(this._briefApp); this._debugApp = new Log4Moz.RotatingFileAppender(verbose, formatter); this._debugApp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.debugLog")]; root.addAppender(this._debugApp); }, clearLogs: function WeaveSvc_clearLogs() { - this._briefApp.clear(); this._debugApp.clear(); }, diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 81a84f6c872d..5cebf5661972 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -26,7 +26,6 @@ pref("extensions.weave.engine.tabs", true); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); -pref("extensions.weave.log.appender.briefLog", "Info"); pref("extensions.weave.log.appender.debugLog", "Trace"); pref("extensions.weave.log.rootLogger", "Trace"); From 5de4a8ab3e415110507f4c5a9372b517707dc5ff Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 8 Sep 2009 23:33:15 -0700 Subject: [PATCH 1350/1860] Expose a _handleDupe on engines and provide a custom one for bookmarks that tracks GUID changes so that it can keep an alias mapping to fix incoming item properties (id, parent, predecessor). Move out _reparentOrphans so that it is triggered on update and not just create because folders can change ids to the right parent. --- services/sync/modules/engines.js | 32 ++++---- services/sync/modules/engines/bookmarks.js | 88 ++++++++++++++++------ 2 files changed, 81 insertions(+), 39 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index eaf53fb9e8dc..eb837b5141d1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -386,6 +386,22 @@ SyncEngine.prototype = { this._delete.ids.push(id); }, + _handleDupe: function _handleDupe(item, dupeId) { + // The local dupe is the lower id, so pretend the incoming is for it + if (dupeId < item.id) { + this._deleteId(item.id); + item.id = dupeId; + this._tracker.changedIDs[dupeId] = true; + } + // The incoming item has the lower id, so change the dupe to it + else { + this._store.changeItemID(dupeId, item.id); + this._deleteId(dupeId); + } + + this._store.cache.clear(); // because parentid refs will be wrong + }, + // Reconciliation has three steps: // 1) Check for the same item (same ID) on both the incoming and outgoing // queues. This means the same item was modified on this profile and @@ -425,20 +441,8 @@ SyncEngine.prototype = { // Step 3: Check for similar items this._log.trace("Reconcile step 3"); let dupeId = this._findDupe(item); - if (dupeId) { - // Stick with the canonical lower id, so convert the dupe to incoming - if (item.id < dupeId) { - this._store.changeItemID(dupeId, item.id); - this._deleteId(dupeId); - } - // The local dupe is the lower id, so pretend the incoming is for it - else { - this._deleteId(item.id); - item.id = dupeId; - } - - this._store.cache.clear(); // because parentid refs will be wrong - } + if (dupeId) + this._handleDupe(item, dupeId); // Apply the incoming item (now that the dupe is the right id) return true; diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f81c1c1e1333..1d096f5ebc72 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -137,6 +137,17 @@ BookmarksEngine.prototype = { } // TODO for bookmarks, check if it exists and find guid // for everything else (folders, separators) look for parent/pred? + }, + + _handleDupe: function _handleDupe(item, dupeId) { + // The local dupe has the lower id, so make it the winning id + if (dupeId < item.id) + [item.id, dupeId] = [dupeId, item.id]; + + // Trigger id change from dupe to winning and update the server + this._store.changeItemID(dupeId, item.id); + this._deleteId(dupeId); + this._tracker.changedIDs[item.id] = true; } }; @@ -199,6 +210,9 @@ BookmarksStore.prototype = { return idForGUID(id) > 0; }, + // Hash of old GUIDs to the new renamed GUIDs + aliases: {}, + applyIncoming: function BStore_applyIncoming(record) { // Ignore (accidental?) root changes if (record.id in kSpecialIds) { @@ -206,6 +220,13 @@ BookmarksStore.prototype = { return; } + // Convert GUID fields to the aliased GUID if necessary + ["id", "parentid", "predecessorid"].forEach(function(field) { + let alias = this.aliases[record[field]]; + if (alias != null) + record[field] = alias; + }, this); + // Preprocess the record before doing the normal apply switch (record.type) { case "query": { @@ -280,6 +301,10 @@ BookmarksStore.prototype = { // Do some post-processing if we have an item let itemId = idForGUID(record.id); if (itemId > 0) { + // Move any children that is looking for this folder as a parent + if (record.type == "folder") + this._reparentOrphans(itemId); + // Create an annotation to remember that it needs a parent // XXX Work around Bug 510628 by prepending parenT if (record._orphan) @@ -310,6 +335,30 @@ BookmarksStore.prototype = { Utils.anno(id, anno) == val); }, + /** + * For the provided parent item, attach its children to it + */ + _reparentOrphans: function _reparentOrphans(parentId) { + // Find orphans and reunite with this folder parent + let parentGUID = GUIDForId(parentId); + let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID); + + this._log.debug("Reparenting orphans " + orphans + " to " + parentId); + orphans.forEach(function(orphan) { + // Append the orphan under the parent unless it's supposed to be first + let insertPos = Svc.Bookmark.DEFAULT_INDEX; + if (!Svc.Annos.itemHasAnnotation(orphan, PREDECESSOR_ANNO)) + insertPos = 0; + + // Move the orphan to the parent and drop the missing parent annotation + Svc.Bookmark.moveItem(orphan, parentId, insertPos); + Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO); + }); + + // Fix up the ordering of the now-parented items + orphans.forEach(this._attachFollowers, this); + }, + /** * Move an item and all of its followers to a new position until reaching an * item that shouldn't be moved @@ -430,25 +479,6 @@ BookmarksStore.prototype = { this._log.trace("Setting GUID of new item " + newId + " to " + record.id); this._setGUID(newId, record.id); - - // Find orphans and reunite with this new folder parent - if (record.type == "folder") { - let orphans = this._findAnnoItems(PARENT_ANNO, record.id); - this._log.debug("Reparenting orphans " + orphans + " to " + record.title); - orphans.map(function(orphan) { - // Append the orphan under the parent unless it's supposed to be first - let insertPos = Svc.Bookmark.DEFAULT_INDEX; - if (!Svc.Annos.itemHasAnnotation(orphan, PREDECESSOR_ANNO)) - insertPos = 0; - - // Move the orphan to the parent and drop the missing parent annotation - Svc.Bookmark.moveItem(orphan, newId, insertPos); - Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO); - - // Return the now-parented orphan so it can attach its followers - return orphan; - }).forEach(this._attachFollowers, this); - } }, remove: function BStore_remove(record) { @@ -551,14 +581,22 @@ BookmarksStore.prototype = { }, changeItemID: function BStore_changeItemID(oldID, newID) { + // Remember the GUID change for incoming records and avoid invalid refs + this.aliases[oldID] = newID; + this.cache.clear(); + + // Update any existing annotation references + this._findAnnoItems(PARENT_ANNO, oldID).forEach(function(itemId) { + Utils.anno(itemId, PARENT_ANNO, "T" + newID); + }, this); + this._findAnnoItems(PREDECESSOR_ANNO, oldID).forEach(function(itemId) { + Utils.anno(itemId, PREDECESSOR_ANNO, "R" + newID); + }, this); + + // Make sure there's an item to change GUIDs let itemId = idForGUID(oldID); - if (itemId == null) // toplevel folder + if (itemId <= 0) return; - if (itemId <= 0) { - this._log.warn("Can't change GUID " + oldID + " to " + - newID + ": Item does not exist"); - return; - } this._log.debug("Changing GUID " + oldID + " to " + newID); this._setGUID(itemId, newID); From 862a9fed7a8e5fff87619ceb5320e8516570f26a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 8 Sep 2009 23:33:58 -0700 Subject: [PATCH 1351/1860] Detect dupe separators by identifying them by their position and generate the lookup lazily with the folder/title mapping for now. --- services/sync/modules/engines/bookmarks.js | 41 +++++++++++++++---- .../sync/modules/type_records/bookmark.js | 2 + 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 1d096f5ebc72..ede0c04c5c92 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -99,20 +99,40 @@ BookmarksEngine.prototype = { _syncStartup: function _syncStart() { SyncEngine.prototype._syncStartup.call(this); - // Lazily create a mapping of folder titles to ids if necessary - this.__defineGetter__("_folderTitles", function() { + // Lazily create a mapping of folder titles and separator positions to GUID + let lazyMap = function() { delete this._folderTitles; + delete this._separatorPos; let folderTitles = {}; + let separatorPos = {}; for (let guid in this._store.getAllIDs()) { let id = idForGUID(guid); - if (Svc.Bookmark.getItemType(id) != Svc.Bookmark.TYPE_FOLDER) - continue; - - folderTitles[Svc.Bookmark.getItemTitle(id)] = guid; + switch (Svc.Bookmark.getItemType(id)) { + case Svc.Bookmark.TYPE_FOLDER: + // Map the folder name to GUID + folderTitles[Svc.Bookmark.getItemTitle(id)] = guid; + break; + case Svc.Bookmark.TYPE_SEPARATOR: + // Map the separator position and parent position to GUID + let parent = Svc.Bookmark.getFolderIdForItem(id); + let pos = [id, parent].map(Svc.Bookmark.getItemIndex); + separatorPos[pos] = guid; + break; + } } - return this._folderTitles = folderTitles; - }); + + this._folderTitles = folderTitles; + this._separatorPos = separatorPos; + }; + + // Make the getters that lazily build the mapping + ["_folderTitles", "_separatorPos"].forEach(function(lazy) { + this.__defineGetter__(lazy, function() { + lazyMap.call(this); + return this[lazy]; + }); + }, this); }, _syncFinish: function _syncFinish() { @@ -134,6 +154,8 @@ BookmarksEngine.prototype = { case "folder": case "livemark": return this._folderTitles[item.title]; + case "separator": + return this._separatorPos[item.pos]; } // TODO for bookmarks, check if it exists and find guid // for everything else (folders, separators) look for parent/pred? @@ -717,6 +739,9 @@ BookmarksStore.prototype = { case this._bms.TYPE_SEPARATOR: record = new BookmarkSeparator(); + // Create a positioning identifier for the separator + let parent = Svc.Bookmark.getFolderIdForItem(placeId); + record.pos = [placeId, parent].map(Svc.Bookmark.getItemIndex); break; case this._bms.TYPE_DYNAMIC_CONTAINER: diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index f16909398f7b..132a41146bba 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -183,3 +183,5 @@ BookmarkSeparator.prototype = { this.type = "separator"; } }; + +Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos"); From c3472cb15f63ebe37d599f5d0cf56ab6cd29fe91 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Sep 2009 11:04:36 -0700 Subject: [PATCH 1352/1860] Bug 515678 - Provide a smart getter to get the object representation of a JSON Resource response Always allow getting an object representation of potentially JSON response for any Resource instead of explicitly on filterDownload. Update existing Resource GETs that manually parse the response. This prevents the String response from accidentally toString()ing a filteredDownload that converted to a different type. --- services/sync/modules/resource.js | 14 ++++++-------- services/sync/modules/service.js | 13 ++++--------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 6e522371d9ce..bfde08fc2388 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -271,14 +271,8 @@ Resource.prototype = { if (success) { this._log.debug(action + " success: " + status); - switch (action) { - case "GET": - case "POST": - if (this._log.level <= Log4Moz.Level.Trace) - this._log.trace(action + " Body: " + this._data); - this.filterDownload(); - break; - } + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace(action + " Body: " + this._data); } else this._log.debug(action + " fail: " + status + " " + this._data); @@ -292,6 +286,10 @@ Resource.prototype = { ret.headers = headers; ret.status = status; ret.success = success; + + // Make a lazy getter to convert the json response into an object + Utils.lazy2(ret, "obj", function() JSON.parse(ret)); + return ret; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4ff8e797ce5f..70c265cd755d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1076,9 +1076,8 @@ WeaveSvc.prototype = { throw "aborting sync, failed to get collections"; // Convert the response to an object and read out the modified times - info = JSON.parse(info); for each (let engine in [Clients].concat(Engines.getEnabled())) - engine.lastModified = info[engine.name] || 0; + engine.lastModified = info.obj[engine.name] || 0; this._log.debug("Refreshing client list"); Clients.sync(); @@ -1214,13 +1213,9 @@ WeaveSvc.prototype = { */ wipeServer: function WeaveSvc_wipeServer(engines) this._catch(this._notify("wipe-server", "", function() { - // Grab all the collections for the user - let res = new Resource(this.infoURL); - res.get(); - - // Get the array of collections and delete each one - let allCollections = JSON.parse(res.data); - for (let name in allCollections) { + // Grab all the collections for the user and delete each one + let info = new Resource(this.infoURL).get(); + for (let name in info.obj) { try { // If we have a list of engines, make sure it's one we want if (engines && engines.indexOf(name) == -1) From bae93fdb618e5289b8aaa2dee8418cf9739d908c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Sep 2009 11:05:13 -0700 Subject: [PATCH 1353/1860] Bug 515676 - Allow giving objects to PUT/POST without Filters to avoid stringify([parse(stringify(stringify(obj)))]) Get rid of Filters and automatically JSON.stringify PUT/POST data that aren't strings, so plain Records can be passed in to PUT and POST. This leverages toJSON of Records to provide an object that can be serialized. Fix up client record serialize/deserialize to still escape/unescape non-ASCII. --- .../sync/modules/base_records/collection.js | 1 - services/sync/modules/base_records/keys.js | 2 +- services/sync/modules/base_records/wbo.js | 27 ++++---- services/sync/modules/engines.js | 4 +- services/sync/modules/resource.js | 68 ++----------------- services/sync/modules/service.js | 4 +- services/sync/modules/type_records/clients.js | 31 +++------ .../sync/tests/unit/test_clients_escape.js | 2 +- 8 files changed, 33 insertions(+), 106 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 57d3a286794f..83ee9d51bff7 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -59,7 +59,6 @@ Collection.prototype = { _Coll_init: function Coll_init(uri) { this._init(uri); - this.pushFilter(new JsonFilter()); this._full = true; this._older = 0; this._newer = 0; diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 4d8d9a0a1db3..5140101affc5 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -176,7 +176,7 @@ PubKeyManager.prototype = { uploadKeypair: function PubKeyManager_uploadKeypair(keys) { for each (let key in keys) - new Resource(key.uri).put(key.serialize()); + new Resource(key.uri).put(key); } }; diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 47ac697969db..d653fd5b6da2 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -79,21 +79,8 @@ WBORecord.prototype = { return 0; }, - serialize: function WBORec_serialize() { - // Convert the payload into a string because the server expects that - let payload = this.payload; - this.payload = this.deleted ? "" : JSON.stringify(payload); - - let ret = JSON.stringify(this.data); - - // Restore the original payload - this.payload = payload; - - return ret; - }, - - deserialize: function WBORec_deserialize(json) { - this.data = JSON.parse(json); + deserialize: function deserialize(json) { + this.data = json.constructor.toString() == String ? JSON.parse(json) : json; // Empty string payloads are deleted records if (this.payload === "") @@ -102,6 +89,16 @@ WBORecord.prototype = { this.payload = JSON.parse(this.payload); }, + toJSON: function toJSON() { + // Copy fields from data to except payload which needs to be a string + let obj = {}; + for (let [key, val] in Iterator(this.data)) + if (key != "payload") + obj[key] = val; + obj.payload = this.deleted ? "" : JSON.stringify(this.payload); + return obj; + }, + toString: function WBORec_toString() "{ " + [ "id: " + this.id, "parent: " + this.parentid, diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 0064fef9c939..baf40034d399 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -356,7 +356,7 @@ SyncEngine.prototype = { meta.generateIV(); meta.addUnwrappedKey(pubkey, symkey); let res = new Resource(meta.uri); - let resp = res.put(meta.serialize()); + let resp = res.put(meta); if (!resp.success) throw resp; @@ -566,7 +566,7 @@ SyncEngine.prototype = { this._log.trace("Outgoing: " + out); out.encrypt(ID.get("WeaveCryptoID")); - up.pushData(JSON.parse(out.serialize())); // FIXME: inefficient + up.pushData(out); // Partial upload if ((++count % MAX_UPLOAD_RECORDS) == 0) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index bfde08fc2388..65af52359b5c 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -35,7 +35,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Resource', 'JsonFilter']; +const EXPORTED_SYMBOLS = ["Resource"]; const Cc = Components.classes; const Ci = Components.interfaces; @@ -125,29 +125,12 @@ Resource.prototype = { this._data = value; }, - // ** {{{ Resource.filters }}} ** - // - // Filters are used to perform pre and post processing on - // requests made for resources. Use these methods to add, - // remove and clear filters applied to the resource. - _filters: null, - pushFilter: function Res_pushFilter(filter) { - this._filters.push(filter); - }, - popFilter: function Res_popFilter() { - return this._filters.pop(); - }, - clearFilters: function Res_clearFilters() { - this._filters = []; - }, - _init: function Res__init(uri) { this._log = Log4Moz.repository.getLogger(this._logName); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; this.uri = uri; this._headers = {'Content-type': 'text/plain'}; - this._filters = []; }, // ** {{{ Resource._createRequest }}} ** @@ -181,26 +164,6 @@ Resource.prototype = { _onProgress: function Res__onProgress(channel) {}, - // ** {{{ Resource.filterUpload }}} ** - // - // Apply pre-request filters. Currently, this is done before - // any PUT request. - filterUpload: function Resource_filterUpload() { - this._data = this._filters.reduce(function(data, filter) { - return filter.beforePUT(data); - }, this._data); - }, - - // ** {{{ Resource.filterDownload }}} ** - // - // Apply post-request filters. Currently, this done after - // any GET request. - filterDownload: function Resource_filterDownload() { - this._data = this._filters.reduceRight(function(data, filter) { - return filter.afterGET(data); - }, this._data); - }, - // ** {{{ Resource._request }}} ** // // Perform a particular HTTP request on the resource. This method @@ -216,7 +179,10 @@ Resource.prototype = { // PUT and POST are trreated differently because // they have payload data. if ("PUT" == action || "POST" == action) { - this.filterUpload(); + // Convert non-string bodies into JSON + if (this._data.constructor.toString() != String) + this._data = JSON.stringify(this._data); + this._log.debug(action + " Length: " + this._data.length); this._log.trace(action + " Body: " + this._data); @@ -362,30 +328,6 @@ ChannelListener.prototype = { } }; -// = JsonFilter = -// -// Currently, the only filter used in conjunction with -// {{{Resource.filters}}}. It simply encodes outgoing records -// as JSON, and decodes incoming JSON into JS objects. -function JsonFilter() { - let level = "Debug"; - try { level = Utils.prefs.getCharPref("log.logger.network.jsonFilter"); } - catch (e) { /* ignore unset prefs */ } - this._log = Log4Moz.repository.getLogger("Net.JsonFilter"); - this._log.level = Log4Moz.Level[level]; -} -JsonFilter.prototype = { - beforePUT: function JsonFilter_beforePUT(data) { - this._log.trace("Encoding data as JSON"); - return JSON.stringify(data); - }, - - afterGET: function JsonFilter_afterGET(data) { - this._log.trace("Decoding JSON data"); - return JSON.parse(data); - } -}; - // = badCertListener = // // We use this listener to ignore bad HTTPS diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 70c265cd755d..c450b6021110 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -548,7 +548,7 @@ WeaveSvc.prototype = { privkey.payload.iv, newphrase); privkey.payload.keyData = newkey; - let resp = new Resource(privkey.uri).put(privkey.serialize()); + let resp = new Resource(privkey.uri).put(privkey); if (!resp.success) throw resp; @@ -1200,7 +1200,7 @@ WeaveSvc.prototype = { this._log.debug("Setting meta payload storage version to " + WEAVE_VERSION); meta.payload.storageVersion = WEAVE_VERSION; - let resp = new Resource(meta.uri).put(meta.serialize()); + let resp = new Resource(meta.uri).put(meta); if (!resp.success) throw resp; }, diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index 4cabb0e69a50..3bcf478010af 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -56,32 +56,21 @@ ClientRecord.prototype = { this._WBORec_init(uri); }, - _escape: function ClientRecord__escape(toAscii) { - // Escape-to-ascii or unescape-from-ascii each value - if (this.payload != null) - for (let [key, val] in Iterator(this.payload)) - this.payload[key] = (toAscii ? escape : unescape)(val); - }, - - serialize: function ClientRecord_serialize() { - // Convert potential non-ascii to ascii before serializing - this._escape(true); - let ret = WBORecord.prototype.serialize.apply(this, arguments); - - // Restore the original data for normal use - this._escape(false); - return ret; - }, - deserialize: function ClientRecord_deserialize(json) { - // Deserialize then convert potential escaped non-ascii - WBORecord.prototype.deserialize.apply(this, arguments); - this._escape(false); + let data = JSON.parse(json, function(key, val) key == "payload" ? + unescape(val) : val); + WBORecord.prototype.deserialize.call(this, data); + }, + + toJSON: function toJSON() { + let obj = WBORecord.prototype.toJSON.call(this); + obj.payload = escape(obj.payload); + return obj; }, // engines.js uses cleartext to determine if records _isEqual // XXX Bug 482669 Implement .equals() for SyncEngine to compare records - get cleartext() this.serialize(), + get cleartext() JSON.stringify(this), // XXX engines.js calls encrypt/decrypt for all records, so define these: encrypt: function ClientRecord_encrypt(passphrase) {}, diff --git a/services/sync/tests/unit/test_clients_escape.js b/services/sync/tests/unit/test_clients_escape.js index 70d4efc29527..936cf41806c8 100644 --- a/services/sync/tests/unit/test_clients_escape.js +++ b/services/sync/tests/unit/test_clients_escape.js @@ -11,7 +11,7 @@ function run_test() { do_check_eq(record.id, "ascii"); do_check_eq(record.payload.name, "wéävê"); - let serialized = record.serialize(); + let serialized = JSON.stringify(record); let checkCount = 0; _("Checking for all ASCII:", serialized); Array.forEach(serialized, function(ch) { From 5981175501cf2868b8e7caa420a92b80608e468e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Sep 2009 12:41:38 -0700 Subject: [PATCH 1354/1860] Update lastSync to the lastModified time from info/collections instead of updating times on each record. This is to prepare for fetching a list of GUIDs and processing a list of GUIDs. --- services/sync/modules/engines.js | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c1e9e2b2c5e5..bc9d4a65baeb 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -391,24 +391,23 @@ SyncEngine.prototype = { newitems.sort = "index"; let count = {applied: 0, reconciled: 0}; - this._lastSyncTmp = 0; newitems.recordHandler = Utils.bind2(this, function(item) { try { item.decrypt(ID.get("WeaveCryptoID")); if (this._reconcile(item)) { count.applied++; - this._applyIncoming(item); + this._tracker.ignoreAll = true; + this._store.applyIncoming(item); } else { count.reconciled++; this._log.trace("Skipping reconciled incoming item " + item.id); - if (this._lastSyncTmp < item.modified) - this._lastSyncTmp = item.modified; } - } catch (e) { - this._log.error("Could not process incoming record: " + - Utils.exceptionStr(e)); } + catch(ex) { + this._log.warn("Error processing record: " + Utils.exceptionStr(e)); + } + this._tracker.ignoreAll = false; Sync.sleep(0); }); @@ -416,8 +415,8 @@ SyncEngine.prototype = { if (!resp.success) throw resp; - if (this.lastSync < this._lastSyncTmp) - this.lastSync = this._lastSyncTmp; + if (this.lastSync < this.lastModified) + this.lastSync = this.lastModified; this._log.info("Applied " + count.applied + " records, reconciled " + count.reconciled + " records"); @@ -525,20 +524,6 @@ SyncEngine.prototype = { return true; }, - // Apply incoming records - _applyIncoming: function SyncEngine__applyIncoming(item) { - try { - this._tracker.ignoreAll = true; - this._store.applyIncoming(item); - if (this._lastSyncTmp < item.modified) - this._lastSyncTmp = item.modified; - } catch (e) { - this._log.warn("Error while applying record: " + Utils.stackTrace(e)); - } finally { - this._tracker.ignoreAll = false; - } - }, - // Upload outgoing records _uploadOutgoing: function SyncEngine__uploadOutgoing() { let outnum = [i for (i in this._tracker.changedIDs)].length; From 7ecb46e12b640c695746357b48891772abab3e17 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Sep 2009 14:18:31 -0700 Subject: [PATCH 1355/1860] Add limit as a param to Collection. Make requesting guids the default. --- services/sync/modules/base_records/collection.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index ff90aee32305..2f48cf9c9118 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -59,8 +59,9 @@ Collection.prototype = { _Coll_init: function Coll_init(uri) { this._init(uri); - this._full = true; + this._full = false; this._ids = null; + this._limit = 0; this._older = 0; this._newer = 0; this._data = []; @@ -82,6 +83,8 @@ Collection.prototype = { args.push('sort=' + this.sort); if (this.ids != null) args.push("ids=" + this.ids); + if (this.limit > 0) + args.push("limit=" + this.limit); this.uri.query = (args.length > 0)? '?' + args.join('&') : ''; }, @@ -100,6 +103,13 @@ Collection.prototype = { this._rebuildURL(); }, + // Limit how many records to get + get limit() this._limit, + set limit(value) { + this._limit = value; + this._rebuildURL(); + }, + // get only items modified before some date get older() { return this._older; }, set older(value) { From cccee2f2856178e358931d256c0f7b234ee80d06 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Sep 2009 20:04:34 -0700 Subject: [PATCH 1356/1860] Don't create a GUID when looking for dupes and set the GUID when adding a url. --- services/sync/modules/engines/history.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 0f6c5f45591a..71af8317376b 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -58,14 +58,15 @@ function setGUID(uri, guid) { Utils.anno(uri, GUID_ANNO, guid, "WITH_HISTORY"); return guid; } -function GUIDForUri(uri) { +function GUIDForUri(uri, create) { try { // Use the existing GUID if it exists return Utils.anno(uri, GUID_ANNO); } catch (ex) { // Give the uri a GUID if it doesn't have one - return setGUID(uri); + if (create) + return setGUID(uri); } } @@ -200,14 +201,16 @@ HistoryStore.prototype = { let items = {}; for (let i = 0; i < root.childCount; i++) { let item = root.getChild(i); - let guid = GUIDForUri(item.uri); + let guid = GUIDForUri(item.uri, true); items[guid] = item.uri; } return items; }, create: function HistStore_create(record) { + // Add the url and set the GUID this.update(record); + setGUID(record.histUri, record.id); }, remove: function HistStore_remove(record) { @@ -311,7 +314,7 @@ HistoryTracker.prototype = { if (this.ignoreAll) return; this._log.trace("onVisit: " + uri.spec); - if (this.addChangedID(GUIDForUri(uri))) + if (this.addChangedID(GUIDForUri(uri, true))) this._upScore(); }, onPageExpired: function HT_onPageExpired(uri, time, entry) { From a04fb6dbf9ad3290a87afc28e51004b94821fa00 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Sep 2009 21:27:47 -0700 Subject: [PATCH 1357/1860] Use a short-circuiting check to determine if a history visit already exists. --- services/sync/modules/engines/history.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index d4142abd1c9c..b4fc1e842ca2 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -256,14 +256,11 @@ HistoryStore.prototype = { if (this.urlExists(uri)) curvisits = this._getVisits(record.histUri); - let visit; - while ((visit = record.visits.pop())) { - if (curvisits.filter(function(i) i.date == visit.date).length) - continue; - this._log.debug(" visit " + visit.date); - this._hsvc.addVisit(uri, visit.date, null, visit.type, - (visit.type == 5 || visit.type == 6), 0); - } + // Add visits if there's no local visit with the same date + for each (let {date, type} in record.visits) + if (curvisits.every(function(cur) cur.date != date)) + Svc.History.addVisit(uri, date, null, type, type == 5 || type == 6, 0); + this._hsvc.setPageTitle(uri, record.title); // Equalize IDs From 976d5514bbd955373ffb054e4ec1082c89b228df Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Sep 2009 22:57:36 -0700 Subject: [PATCH 1358/1860] Remove some unnecessary logs that follow right after the notify/event now that we only show verbose. Only log non-success response body for Trace. --- services/sync/modules/engines.js | 3 ++- services/sync/modules/resource.js | 14 ++++++++++++-- services/sync/modules/service.js | 7 ++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index baf40034d399..49211715de52 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -536,8 +536,9 @@ SyncEngine.prototype = { // Upload outgoing records _uploadOutgoing: function SyncEngine__uploadOutgoing() { let outnum = [i for (i in this._tracker.changedIDs)].length; - this._log.debug("Preparing " + outnum + " outgoing records"); if (outnum) { + this._log.debug("Preparing " + outnum + " outgoing records"); + // collection we'll upload let up = new Collection(this.engineURL); let count = 0; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 65af52359b5c..1a2911e89c6e 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -240,8 +240,18 @@ Resource.prototype = { if (this._log.level <= Log4Moz.Level.Trace) this._log.trace(action + " Body: " + this._data); } - else - this._log.debug(action + " fail: " + status + " " + this._data); + else { + let log = "debug"; + let mesg = action + " fail: " + status; + + // Only log the full response body (may be HTML) when Trace logging + if (this._log.level <= Log4Moz.Level.Trace) { + log = "trace"; + mesg += " " + this._data; + } + + this._log[log](mesg); + } } // Got a response but no header; must be cached (use default values) catch(ex) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c450b6021110..785f5f139e55 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -488,8 +488,6 @@ WeaveSvc.prototype = { _verifyLogin: function _verifyLogin() this._catch(this._notify("verify-login", "", function() { - this._log.debug("Verifying login for user " + this.username); - this._setCluster(); let res = new Resource(this.infoURL); try { @@ -521,7 +519,6 @@ WeaveSvc.prototype = { _verifyPassphrase: function _verifyPassphrase() this._catch(this._notify("verify-passphrase", "", function() { - this._log.debug("Verifying passphrase"); try { let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); @@ -665,7 +662,7 @@ WeaveSvc.prototype = { this._setSyncFailure(LOGIN_FAILED_NO_PASSWORD); throw "No password given or found in password manager"; } - this._log.debug("Logging in user " + this.username); + this._log.info("Logging in user " + this.username); if (!this._verifyLogin()) { // verifyLogin sets the failure states here @@ -1079,7 +1076,7 @@ WeaveSvc.prototype = { for each (let engine in [Clients].concat(Engines.getEnabled())) engine.lastModified = info.obj[engine.name] || 0; - this._log.debug("Refreshing client list"); + this._log.trace("Refreshing client list"); Clients.sync(); // Process the incoming commands if we have any From e306f347a226b04b351f2de43ef387d8d9fecece Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Sep 2009 23:11:33 -0700 Subject: [PATCH 1359/1860] Bug 507429 - Partial sync download support Only fetch a limited number of items on first/update syncs and if we get the same number, ask the server for the ids to fetch later. Also on every download, process some of the backlog and save the list of GUIDs to disk as json for cross-session support. --- services/sync/modules/engines.js | 76 +++++++++++++++++++++++++++----- services/sync/modules/util.js | 7 +++ 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 4d1e8d399fe1..565550a84726 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -290,6 +290,11 @@ SyncEngine.prototype = { _recordObj: CryptoWrapper, + _init: function _init() { + Engine.prototype._init.call(this); + this.loadToFetch(); + }, + get baseURL() { let url = Svc.Prefs.get("clusterURL"); if (!url) @@ -325,6 +330,19 @@ SyncEngine.prototype = { Svc.Prefs.set(this.name + ".lastSync", "0"); }, + get toFetch() this._toFetch, + set toFetch(val) { + this._toFetch = val; + Utils.jsonSave("toFetch/" + this.name, this, val); + }, + + loadToFetch: function loadToFetch() { + // Initialize to empty if there's no file + this._toFetch = []; + Utils.jsonLoad("toFetch/" + this.name, this, Utils.bind2(this, function(o) + this._toFetch = o)); + }, + // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { return this._store.createRecord(id, this.cryptoMetaURL); @@ -370,12 +388,6 @@ SyncEngine.prototype = { // Generate outgoing records _processIncoming: function SyncEngine__processIncoming() { - // Only bother getting data from the server if there's new things - if (this.lastModified <= this.lastSync) { - this._log.debug("Nothing new from the server to process"); - return; - } - this._log.debug("Downloading & applying server changes"); // enable cache, and keep only the first few items. Otherwise (when @@ -389,10 +401,14 @@ SyncEngine.prototype = { newitems.newer = this.lastSync; newitems.full = true; newitems.sort = "index"; + newitems.limit = 300; let count = {applied: 0, reconciled: 0}; - + let handled = []; newitems.recordHandler = Utils.bind2(this, function(item) { + // Remember which records were processed + handled.push(item.id); + try { item.decrypt(ID.get("WeaveCryptoID")); if (this._reconcile(item)) { @@ -411,15 +427,50 @@ SyncEngine.prototype = { Sync.sleep(0); }); - let resp = newitems.get(); - if (!resp.success) - throw resp; + // Only bother getting data from the server if there's new things + if (this.lastModified > this.lastSync) { + let resp = newitems.get(); + if (!resp.success) + throw resp; + } + + // Check if we got the maximum that we requested; get the rest if so + if (handled.length == newitems.limit) { + let guidColl = new Collection(this.engineURL); + guidColl.newer = this.lastSync; + guidColl.sort = "index"; + + let guids = guidColl.get(); + if (!guids.success) + throw guids; + + // Figure out which guids weren't just fetched then remove any guids that + // were already waiting and prepend the new ones + let extra = Utils.arraySub(guids.obj, handled); + if (extra.length > 0) + this.toFetch = extra.concat(Utils.arraySub(this.toFetch, extra)); + } + + if (this.toFetch.length > 0) { + // Reuse the original query, but get rid of the restricting params + newitems.limit = 0; + newitems.newer = 0; + + // Get the first bunch of records and save the rest for later, but don't + // get too many records as there's a maximum server URI length (HTTP 414) + newitems.ids = this.toFetch.slice(0, 150); + this.toFetch = this.toFetch.slice(150); + + let resp = newitems.get(); + if (!resp.success) + throw resp; + } if (this.lastSync < this.lastModified) this.lastSync = this.lastModified; - this._log.info("Applied " + count.applied + " records, reconciled " + - count.reconciled + " records"); + this._log.info(["Records:", count.applied, "applied,", count.reconciled, + "reconciled,", this.toFetch.length, "left to fetch"].join(" ")); // try to free some memory this._store.cache.clear(); @@ -615,5 +666,6 @@ SyncEngine.prototype = { _resetClient: function SyncEngine__resetClient() { this.resetLastSync(); + this.toFetch = []; } }; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index e2295856e3d8..07492facd8d6 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -725,6 +725,13 @@ let Utils = { return ret; }, + /** + * Create an array like the first but without elements of the second + */ + arraySub: function arraySub(minuend, subtrahend) { + return minuend.filter(function(i) subtrahend.indexOf(i) == -1); + }, + bind2: function Async_bind2(object, method) { return function innerBind() { return method.apply(object, arguments); }; }, From 46c04623ec48ef11fab35d24eb3dd56cecb7723b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 11 Sep 2009 08:24:42 -0700 Subject: [PATCH 1360/1860] Add some comments to partial download code and remove other comments. --- services/sync/modules/engines.js | 3 ++- services/sync/modules/engines/bookmarks.js | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 68b692c0c13d..59fe1cd9d428 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -451,6 +451,7 @@ SyncEngine.prototype = { this.toFetch = extra.concat(Utils.arraySub(this.toFetch, extra)); } + // Process any backlog of GUIDs if necessary if (this.toFetch.length > 0) { // Reuse the original query, but get rid of the restricting params newitems.limit = 0; @@ -461,6 +462,7 @@ SyncEngine.prototype = { newitems.ids = this.toFetch.slice(0, 150); this.toFetch = this.toFetch.slice(150); + // Reuse the existing record handler set earlier let resp = newitems.get(); if (!resp.success) throw resp; @@ -638,7 +640,6 @@ SyncEngine.prototype = { delete this._delete[key]; // Send a delete for the property - this._log.debug("Sending delete for " + key + ": " + val); let coll = new Collection(this.engineURL, this._recordObj); coll[key] = val; coll.delete(); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index ede0c04c5c92..de80f8e875db 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -157,8 +157,6 @@ BookmarksEngine.prototype = { case "separator": return this._separatorPos[item.pos]; } - // TODO for bookmarks, check if it exists and find guid - // for everything else (folders, separators) look for parent/pred? }, _handleDupe: function _handleDupe(item, dupeId) { From 9cfe81dde6261318b875805a543e46bc30ebf0ec Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 11 Sep 2009 13:10:19 -0700 Subject: [PATCH 1361/1860] Fix exception name. --- services/sync/modules/engines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 59fe1cd9d428..3339cf5fc446 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -421,7 +421,7 @@ SyncEngine.prototype = { } } catch(ex) { - this._log.warn("Error processing record: " + Utils.exceptionStr(e)); + this._log.warn("Error processing record: " + Utils.exceptionStr(ex)); } this._tracker.ignoreAll = false; Sync.sleep(0); From 3e3ce3fb689957751748d2119a9025d9113eecab Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 11 Sep 2009 14:39:48 -0700 Subject: [PATCH 1362/1860] Be less chatty when showing GUIDs and ignore cases. --- services/sync/modules/engines/forms.js | 4 ++-- services/sync/modules/engines/passwords.js | 4 ++-- services/sync/modules/engines/tabs.js | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 2ca20f954135..593669e2037c 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -191,7 +191,7 @@ FormStore.prototype = { }, remove: function FormStore_remove(record) { - this._log.debug("Removing form record: " + record.id); + this._log.trace("Removing form record: " + record.id); if (record.id in this._formItems) { let item = this._formItems[record.id]; @@ -199,7 +199,7 @@ FormStore.prototype = { return; } - this._log.warn("Invalid GUID found, ignoring remove request."); + this._log.trace("Invalid GUID found, ignoring remove request."); }, update: function FormStore_update(record) { diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 5bb23cd53bc2..427b8eb02d65 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -183,11 +183,11 @@ PasswordStore.prototype = { }, remove: function PasswordStore__remove(record) { - this._log.debug("Removing login " + record.id); + this._log.trace("Removing login " + record.id); let loginItem = this._getLoginFromGUID(record.id); if (!loginItem) { - this._log.debug("Asked to remove record that doesn't exist, ignoring"); + this._log.trace("Asked to remove record that doesn't exist, ignoring"); return; } diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 1cc8ab2cb336..6c9fd7cb4ea4 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -309,8 +309,7 @@ TabStore.prototype = { create: function TabStore_create(record) { if (record.id == this._localClientGUID) return; // can't happen? - this._log.debug("Create called. Adding remote client record for "); - this._log.debug(record.getClientName()); + this._log.debug("Adding remote tabs from " + record.getClientName()); this._remoteClients[record.id] = record; this._writeToFile(); // TODO writing to file after every change is inefficient. How do we From a370c3ee409fa6454434332ef0034114b418e006 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 11 Sep 2009 14:52:27 -0700 Subject: [PATCH 1363/1860] Chop off the end of long URIs for debug output onStartRequest. --- services/sync/modules/resource.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 1a2911e89c6e..0a9564eadc95 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -312,8 +312,17 @@ ChannelListener.prototype = { onStartRequest: function Channel_onStartRequest(channel) { // XXX Bug 482179 Some reason xpconnect makes |channel| only nsIRequest channel.QueryInterface(Ci.nsIHttpChannel); - this._log.debug(channel.requestMethod + " request for " + - channel.URI.spec); + + let log = "trace"; + let mesg = channel.requestMethod + " request for " + channel.URI.spec; + // Only log a part of the uri for logs higher than trace + if (this._log.level > Log4Moz.Level.Trace) { + log = "debug"; + if (mesg.length > 200) + mesg = mesg.substr(0, 200) + "..."; + } + this._log[log](mesg); + this._data = ''; }, From 2bd045e2d80be3961ed0c51d2d4404fcd9551197 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 11 Sep 2009 16:44:26 -0700 Subject: [PATCH 1364/1860] Address review comments nits. r=thunder --- services/sync/modules/engines/bookmarks.js | 2 +- services/sync/modules/engines/forms.js | 2 +- services/sync/modules/engines/history.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index de80f8e875db..a6d6345e32c0 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -321,7 +321,7 @@ BookmarksStore.prototype = { // Do some post-processing if we have an item let itemId = idForGUID(record.id); if (itemId > 0) { - // Move any children that is looking for this folder as a parent + // Move any children that are looking for this folder as a parent if (record.type == "folder") this._reparentOrphans(itemId); diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 5e0da875772c..3fa713ce7dd2 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -72,7 +72,7 @@ FormEngine.prototype = { this._store.clearFormCache(); // Only leave 1 month's worth of form history - this._delete.older = this.lastSync - 2592000; + this._delete.older = this.lastSync - 2592000; // 60*60*24*30 SyncEngine.prototype._syncFinish.call(this); }, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index f7b60dde4c0f..ab83bff12d3a 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -86,7 +86,7 @@ HistoryEngine.prototype = { _syncFinish: function HistEngine__syncFinish(error) { // Only leave 1 week's worth of history on the server - this._delete.older = this.lastSync - 604800; + this._delete.older = this.lastSync - 604800; // 60*60*24*7 SyncEngine.prototype._syncFinish.call(this); }, From b7566906875ba36c0f2edcec0214c356dd1a943e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 11 Sep 2009 17:14:45 -0700 Subject: [PATCH 1365/1860] Bug 516096 - Nothing loads; about:weave has empty boxes/buttons; can't log in or sync Work around bug 514803 by not calling toLocaleString() and generate a date string manually. --- services/sync/modules/log4moz.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index a2a30050bdf7..57cdd5c5c46e 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -361,15 +361,19 @@ BasicFormatter.prototype = { }, format: function BF_format(message) { - let date = new Date(message.time); - // Pad a string to a certain length (20) with a character (space) let pad = function BF__pad(str, len, chr) str + new Array(Math.max((len || 20) - str.length + 1, 0)).join(chr || " "); - return date.toLocaleFormat(this.dateFormat) + "\t" + - pad(message.loggerName) + " " + message.levelDesc + "\t" + - message.message + "\n"; + // Generate a date string because toLocaleString doesn't work XXX 514803 + let z = function(n) n < 10 ? "0" + n : n; + let d = new Date(message.time); + let dateStr = [d.getFullYear(), "-", z(d.getMonth() + 1), "-", + z(d.getDate()), " ", z(d.getHours()), ":", z(d.getMinutes()), ":", + z(d.getSeconds())].join(""); + + return dateStr + "\t" + pad(message.loggerName) + " " + message.levelDesc + + "\t" + message.message + "\n"; } }; From a5142c6bc5489d3954897b484e3d4319daccd57c Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 15 Sep 2009 17:18:00 -0400 Subject: [PATCH 1366/1860] missing file fail from bug 512393 --- services/sync/locales/en-US/errors.properties | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 services/sync/locales/en-US/errors.properties diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties new file mode 100644 index 000000000000..d6624cfc8781 --- /dev/null +++ b/services/sync/locales/en-US/errors.properties @@ -0,0 +1,4 @@ +error.login.reason.password = Your username/password failed +error.login.reason.unknown = Unknown error +error.login.reason.passphrase = Invalid passphrase +error.login.reason.network = Network error From c06e17ff752228c9bef6027d17a21bfc31e26d44 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 15 Sep 2009 21:38:52 -0400 Subject: [PATCH 1367/1860] bug 511549 - make detailedStatus much smarter about errors and backoff, r=edilee --- services/sync/locales/en-US/sync.properties | 2 +- services/sync/modules/constants.js | 83 +++++--- services/sync/modules/engines.js | 21 +- services/sync/modules/resource.js | 4 + services/sync/modules/service.js | 211 +++++++++++--------- services/sync/modules/util.js | 21 +- 6 files changed, 196 insertions(+), 146 deletions(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index c1e76cea75dc..ba8ec37e9031 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -20,6 +20,6 @@ error.login.description = Weave encountered an error while signing you in: %1$S. error.logout.title = Error While Signing Out error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it. error.sync.title = Error While Syncing -error.sync.description = Weave encountered an error while syncing. You might want to try syncing manually to make sure you are up-to-date. +error.sync.description = Weave encountered an error while syncing: %1$S. Weave will automatically retry this action. error.sync.tryAgainButton.label = Sync Now error.sync.tryAgainButton.accesskey = S diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index f6e7da3934a6..83e7220f1e43 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -40,20 +40,23 @@ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', - 'CONNECTION_TIMEOUT', 'MAX_UPLOAD_RECORDS', - 'WEAVE_STATUS_OK', 'WEAVE_STATUS_FAILED', - 'WEAVE_STATUS_PARTIAL', 'SERVER_LOW_QUOTA', - 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', - 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', - 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', - 'LOGIN_FAILED_LOGIN_REJECTED', 'METARECORD_DOWNLOAD_FAIL', - 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', - 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', - 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', - 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', - 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', - 'kSyncNotScheduled', - 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID']; + 'CONNECTION_TIMEOUT', 'MAX_UPLOAD_RECORDS', + 'SYNC_SUCCEEDED', 'LOGIN_SUCCEEDED', 'ENGINE_SUCCEEDED', + 'STATUS_OK', 'LOGIN_FAILED', 'SYNC_FAILED', + 'SYNC_FAILED_PARTIAL', 'STATUS_DISABLED', 'SERVER_LOW_QUOTA', + 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', + 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', + 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', + 'LOGIN_FAILED_LOGIN_REJECTED', 'METARECORD_DOWNLOAD_FAIL', + 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', + 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', + 'ENGINE_UPLOAD_FAIL', 'ENGINE_DOWNLOAD_FAIL', 'ENGINE_UNKNOWN_FAIL', + 'ENGINE_METARECORD_UPLOAD_FAIL', + 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', + 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', + 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', + 'kSyncNotScheduled', + 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID']; const WEAVE_VERSION = "@weave_version@"; @@ -89,36 +92,52 @@ const CONNECTION_TIMEOUT = 30000; const MAX_UPLOAD_RECORDS = 100; // Top-level statuses: -const WEAVE_STATUS_OK = "Sync succeeded."; -const WEAVE_STATUS_FAILED = "Sync failed."; -const WEAVE_STATUS_PARTIAL = "Sync partially succeeded, some data failed to sync."; +const STATUS_OK = "success.status_ok"; +const SYNC_FAILED = "error.sync.failed"; +const LOGIN_FAILED = "error.login.failed"; +const SYNC_FAILED_PARTIAL = "error.sync.failed_partial"; +const STATUS_DISABLED = "service.disabled"; + +// success states +const LOGIN_SUCCEEDED = "success.login"; +const SYNC_SUCCEEDED = "success.sync"; +const ENGINE_SUCCEEDED = "success.engine"; + +// login failure status codes: +const LOGIN_FAILED_NO_USERNAME = "error.login.reason.no_username"; +const LOGIN_FAILED_NO_PASSWORD = "error.login.reason.no_password"; +const LOGIN_FAILED_NETWORK_ERROR = "error.login.reason.network"; +const LOGIN_FAILED_INVALID_PASSPHRASE = "error.login.reason.passphrase."; +const LOGIN_FAILED_LOGIN_REJECTED = "error.login.reason.password"; + +// sync failure status codes +const METARECORD_DOWNLOAD_FAIL = "error.sync.reason.metarecord_download_fail"; +const VERSION_OUT_OF_DATE = "error.sync.reason.version_out_of_date"; +const DESKTOP_VERSION_OUT_OF_DATE = "error.sync.reason.desktop_version_out_of_date"; +const KEYS_DOWNLOAD_FAIL = "error.sync.reason.keys_download_fail"; +const NO_KEYS_NO_KEYGEN = "error.sync.reason.no_keys_no_keygen"; +const KEYS_UPLOAD_FAIL = "error.sync.reason.keys_upload_fail"; +const SETUP_FAILED_NO_PASSPHRASE = "error.sync.reason.setup_failed_no_passphrase"; + +// engine failure status codes +const ENGINE_UPLOAD_FAIL = "error.engine.reason.record_upload_fail"; +const ENGINE_DOWNLOAD_FAIL = "error.engine.reason.record_download_fail"; +const ENGINE_UNKNOWN_FAIL = "error.engine.reason.unknown_fail"; +const ENGINE_METARECORD_UPLOAD_FAIL = "error.engine.reason.metarecord_upload_fail"; // Server statuses (Not mutually exclusive): const SERVER_LOW_QUOTA = "Getting close to your Weave server storage quota."; const SERVER_DOWNTIME = "Weave server is overloaded, try agian in 30 sec."; const SERVER_UNREACHABLE = "Weave server is unreachable."; -// Ways that a sync can fail during setup or login: -const LOGIN_FAILED_NO_USERNAME = "No username set, login failed."; -const LOGIN_FAILED_NO_PASSWORD = "No password set, login failed."; -const LOGIN_FAILED_NETWORK_ERROR = "Weave failed to connect to the server."; -const LOGIN_FAILED_INVALID_PASSPHRASE = "Incorrect passphrase given."; -const LOGIN_FAILED_LOGIN_REJECTED = "Incorrect username or password."; -const METARECORD_DOWNLOAD_FAIL = "Can't download metadata record, HTTP error."; -const VERSION_OUT_OF_DATE = "This copy of Weave needs to be updated."; -const DESKTOP_VERSION_OUT_OF_DATE = "Weave needs updating on your desktop browser."; -const KEYS_DOWNLOAD_FAIL = "Can't download keys from server, HTTP error."; -const NO_KEYS_NO_KEYGEN = "Key generation disabled. Sync from the desktop first."; -const KEYS_UPLOAD_FAIL = "Could not upload keys."; -const SETUP_FAILED_NO_PASSPHRASE = "Could not get encryption passphrase."; - // Ways that a sync can be disabled const kSyncWeaveDisabled = "Weave is disabled"; const kSyncNotLoggedIn = "User is not logged in"; const kSyncNetworkOffline = "Network is offline"; const kSyncInPrivateBrowsing = "Private browsing is enabled"; const kSyncNotScheduled = "Not scheduled to do sync"; -// If one of these happens, leave the top-level status the same! +const kSyncBackoffNotMet = "Trying to sync before the server said it's okay"; + // Ways that a sync can be aborted: const ABORT_SYNC_COMMAND = "aborting sync, process commands said so"; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3339cf5fc446..c25fac35c287 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -361,9 +361,12 @@ SyncEngine.prototype = { meta.generateIV(); meta.addUnwrappedKey(pubkey, symkey); let res = new Resource(meta.uri); - let resp = res.put(meta); - if (!resp.success) + let resp = res.put(meta.serialize()); + if (!resp.success) { + this._log.debug("Metarecord upload fail:" + resp); + resp.failureCode = ENGINE_METARECORD_UPLOAD_FAIL; throw resp; + } // Cache the cryto meta that we just put on the server CryptoMetas.set(meta.uri, meta); @@ -430,8 +433,10 @@ SyncEngine.prototype = { // Only bother getting data from the server if there's new things if (this.lastModified > this.lastSync) { let resp = newitems.get(); - if (!resp.success) + if (!resp.success) { + resp.failureCode = ENGINE_DOWNLOAD_FAIL; throw resp; + } } // Check if we got the maximum that we requested; get the rest if so @@ -464,8 +469,11 @@ SyncEngine.prototype = { // Reuse the existing record handler set earlier let resp = newitems.get(); - if (!resp.success) + if (!resp.success) { + resp.failureCode = ENGINE_DOWNLOAD_FAIL; throw resp; + } + } if (this.lastSync < this.lastModified) @@ -591,8 +599,11 @@ SyncEngine.prototype = { let doUpload = Utils.bind2(this, function(desc) { this._log.info("Uploading " + desc + " of " + outnum + " records"); let resp = up.post(); - if (!resp.success) + if (!resp.success) { + this._log.debug("Uploading records failed: " + resp); + resp.failureCode = ENGINE_UPLOAD_FAIL; throw resp; + } // Record the modified time of the upload let modified = resp.headers["X-Weave-Timestamp"]; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 0a9564eadc95..9355ba5a45da 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -252,6 +252,10 @@ Resource.prototype = { this._log[log](mesg); } + + // this is a server-side safety valve to allow slowing down clients without hurting performance + if (headers["X-Weave-Backoff"]) + Observers.notify("weave:service:backoff:interval", parseInt(headers["X-Weave-Backoff"], 10)) } // Got a response but no header; must be cached (use default values) catch(ex) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 785f5f139e55..35a1bceb1738 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -122,21 +122,44 @@ function StatusRecord() { } StatusRecord.prototype = { _init: function() { - this.server = []; + this.service = null; this.sync = null; + this.login = null; this.engines = {}; - }, - - addServerStatus: function(statusCode) { - this.server.push(statusCode); + this._resetBackoff(); }, setSyncStatus: function(statusCode) { + if (statusCode == SYNC_SUCCEEDED) { + this.service == STATUS_OK; + this._resetBackoff(); + } + else + this.service = SYNC_FAILED; + this.sync = statusCode; }, + setLoginStatus: function(statusCode) { + this.service = statusCode == LOGIN_SUCCEEDED ? STATUS_OK : LOGIN_FAILED; + this.login = statusCode; + }, + setEngineStatus: function(engineName, statusCode) { + if (statusCode != ENGINE_SUCCEEDED) + this.service = this.sync = SYNC_FAILED_PARTIAL; + this.engines[engineName] = statusCode; + }, + + resetEngineStatus: function() { + this.engines = {}; + }, + + _resetBackoff: function () { + this.enforceBackoff = false; + this.backoffInterval = 0; + this.minimumNextSync = 0; } }; @@ -159,10 +182,8 @@ WeaveSvc.prototype = { _syncInProgress: false, _keyGenEnabled: true, - // WEAVE_STATUS_OK, WEAVE_STATUS_FAILED, or WEAVE_STATUS_PARTIAL - _weaveStatusCode: null, - // More detailed status info: - _detailedStatus: null, + // the status object + _status: null, // object for caching public and private keys _keyPair: {}, @@ -231,8 +252,7 @@ WeaveSvc.prototype = { get enabled() { return Svc.Prefs.get("enabled"); }, set enabled(value) { Svc.Prefs.set("enabled", value); }, - get statusCode() { return this._weaveStatusCode; }, - get detailedStatus() { return this._detailedStatus; }, + get status() { return this._status; }, get locked() { return this._locked; }, lock: function Svc_lock() { @@ -245,11 +265,6 @@ WeaveSvc.prototype = { this._locked = false; }, - _setSyncFailure: function WeavSvc__setSyncFailure(code) { - this._weaveStatusCode = WEAVE_STATUS_FAILED; - this._detailedStatus.setSyncStatus(code); - }, - _genKeyURLs: function WeaveSvc__genKeyURLs() { let url = this.userURL; PubKeys.defaultKeyUri = url + "/storage/keys/pubkey"; @@ -281,7 +296,7 @@ WeaveSvc.prototype = { this._initLogs(); this._log.info("Weave " + WEAVE_VERSION + " initializing"); this._registerEngines(); - this._detailedStatus = new StatusRecord(); + this._status = new StatusRecord(); // Reset our sync id if we're upgrading, so sync knows to reset local data if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) { @@ -305,6 +320,7 @@ WeaveSvc.prototype = { Svc.Observer.addObserver(this, "quit-application", true); Svc.Observer.addObserver(this, "weave:service:sync:finish", true); Svc.Observer.addObserver(this, "weave:service:sync:error", true); + Svc.Observer.addObserver(this, "weave:service:backoff:interval", true); if (!this.enabled) this._log.info("Weave Sync disabled"); @@ -417,7 +433,12 @@ WeaveSvc.prototype = { break; case "weave:service:sync:finish": this._scheduleNextSync(); - this._serverErrors = 0; + this._syncErrors = 0; + break; + case "weave:service:backoff:interval": + let interval = data + Math.random() * data * 0.25; // required backoff + up to 25% + this.status.backoffInterval = interval; + this.status.minimumNextSync = Date.now() + data; break; case "idle": this._log.trace("Idle time hit, trying to sync"); @@ -452,7 +473,7 @@ WeaveSvc.prototype = { } } catch (e) { this._log.debug("Network error on findCluster"); - this._setSyncFailure(LOGIN_FAILED_NETWORK_ERROR); + this.status.setLoginStatus(LOGIN_FAILED_NETWORK_ERROR); throw e; } }, @@ -495,24 +516,26 @@ WeaveSvc.prototype = { switch (test.status) { case 200: if (!this._verifyPassphrase()) { - this._setSyncFailure(LOGIN_FAILED_INVALID_PASSPHRASE); + this.status.setLoginStatus(LOGIN_FAILED_INVALID_PASSPHRASE); return false; } + this.status.setLoginStatus(LOGIN_SUCCEEDED); return true; case 401: if (this._updateCluster()) return this._verifyLogin(); - this._setSyncFailure(LOGIN_FAILED_LOGIN_REJECTED); + this.status.setLoginStatus(LOGIN_FAILED_LOGIN_REJECTED); this._log.debug("verifyLogin failed: login failed") return false; default: + this._checkServerError(test.status); throw "unexpected HTTP response: " + test.status; } } catch (e) { // if we get here, we have either a busted channel or a network error this._log.debug("verifyLogin failed: " + e) - this._setSyncFailure(LOGIN_FAILED_NETWORK_ERROR); + this.status.setLoginStatus(LOGIN_FAILED_NETWORK_ERROR); throw e; } }))(), @@ -606,7 +629,7 @@ WeaveSvc.prototype = { return; } - failureReason = this.detailedStatus.sync; + failureReason = this.status.sync; } catch (ex) { failureReason = ex; @@ -621,10 +644,9 @@ WeaveSvc.prototype = { })); this._autoConnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - // back off slowly, with some random fuzz to avoid repeated collisions - let interval = Math.floor(Math.random() * SCHEDULED_SYNC_INTERVAL + - SCHEDULED_SYNC_INTERVAL * this._autoConnectAttempts); this._autoConnectAttempts++; + let interval = this._calculateBackoff(this._autoConnectAttempts, + SCHEDULED_SYNC_INTERVAL); this._autoConnectTimer.initWithCallback(listener, interval, Ci.nsITimer.TYPE_ONE_SHOT); this._log.debug("Scheduling next autoconnect attempt in " + @@ -643,7 +665,6 @@ WeaveSvc.prototype = { login: function WeaveSvc_login(username, password, passphrase) this._catch(this._lock(this._notify("login", "", function() { this._loggedIn = false; - this._detailedStatus = new StatusRecord(); if (Svc.IO.offline) throw "Application is offline, login should not be called"; @@ -655,22 +676,22 @@ WeaveSvc.prototype = { this.passphrase = passphrase; if (!this.username) { - this._setSyncFailure(LOGIN_FAILED_NO_USERNAME); + this.status.setLoginStatus(LOGIN_FAILED_NO_USERNAME); throw "No username set, login failed"; } if (!this.password) { - this._setSyncFailure(LOGIN_FAILED_NO_PASSWORD); + this.status.setLoginStatus(LOGIN_FAILED_NO_PASSWORD); throw "No password given or found in password manager"; } this._log.info("Logging in user " + this.username); if (!this._verifyLogin()) { // verifyLogin sets the failure states here - throw "Login failed: " + this.detailedStatus.sync; + throw "Login failed: " + this.status.login; } - // Try starting the sync timer now that we're logged in this._loggedIn = true; + // Try starting the sync timer now that we're logged in this._checkSyncStatus(); if (this._autoConnectTimer) { this._autoConnectTimer.cancel(); @@ -785,7 +806,8 @@ WeaveSvc.prototype = { // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.response.status; if (status != 200 && status != 404) { - this._setSyncFailure(METARECORD_DOWNLOAD_FAIL); + this._checkServerError(Records.response); + this.status.setSyncStatus(METARECORD_DOWNLOAD_FAIL); this._log.warn("Unknown error while downloading metadata record. " + "Aborting sync."); return false; @@ -801,7 +823,7 @@ WeaveSvc.prototype = { if (!this._keyGenEnabled) { this._log.info("...and key generation is disabled. Not wiping. " + "Aborting sync."); - this._setSyncFailure(DESKTOP_VERSION_OUT_OF_DATE); + this.status.setSyncStatus(DESKTOP_VERSION_OUT_OF_DATE); return false; } reset = true; @@ -816,7 +838,7 @@ WeaveSvc.prototype = { "upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")"); } else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) { - this._setSyncFailure(VERSION_OUT_OF_DATE); + this.status.setSyncStatus(VERSION_OUT_OF_DATE); this._log.warn("Server data is of a newer Weave version, this client " + "needs to be upgraded. Aborting sync."); return false; @@ -856,14 +878,16 @@ WeaveSvc.prototype = { this._log.warn("Couldn't download keys from server, aborting sync"); this._log.debug("PubKey HTTP status: " + PubKeys.response.status); this._log.debug("PrivKey HTTP status: " + PrivKeys.response.status); - this._setSyncFailure(KEYS_DOWNLOAD_FAIL); + this._checkServerError(PubKeys.response); + this._checkServerError(PrivKeys.response); + this.status.setSyncStatus(KEYS_DOWNLOAD_FAIL); return false; } if (!this._keyGenEnabled) { this._log.warn("Couldn't download keys from server, and key generation" + "is disabled. Aborting sync"); - this._setSyncFailure(NO_KEYS_NO_KEYGEN); + this.status.setSyncStatus(NO_KEYS_NO_KEYGEN); return false; } @@ -884,11 +908,11 @@ WeaveSvc.prototype = { PrivKeys.set(keys.privkey.uri, keys.privkey); return true; } catch (e) { - this._setSyncFailure(KEYS_UPLOAD_FAIL); + this.status.setSyncStatus(KEYS_UPLOAD_FAIL); this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); } } else { - this._setSyncFailure(SETUP_FAILED_NO_PASSPHRASE); + this.status.setSyncStatus(SETUP_FAILED_NO_PASSPHRASE); this._log.warn("Could not get encryption passphrase"); } } @@ -921,6 +945,8 @@ WeaveSvc.prototype = { reason = kSyncInPrivateBrowsing; else if (Svc.Prefs.get("schedule", 0) != 1) reason = kSyncNotScheduled; + else if (this.status.minimumNextSync > Date.now()) + reason = kSyncBackoffNotMet; return reason; }, @@ -930,7 +956,9 @@ WeaveSvc.prototype = { */ _checkSyncStatus: function WeaveSvc__checkSyncStatus() { // Should we be syncing now, if not, cancel any sync timers and return - if (this._checkSync()) { + // if we're in backoff, we'll schedule the next sync + let reason = this._checkSync(); + if (reason && reason != kSyncBackoffNotMet) { if (this._syncTimer) { this._syncTimer.cancel(); this._syncTimer = null; @@ -940,6 +968,7 @@ WeaveSvc.prototype = { Svc.Idle.removeIdleObserver(this, IDLE_TIME); } catch(e) {} // this throws if there isn't an observer, but that's fine + this.status.service = STATUS_DISABLED; return; } @@ -962,7 +991,7 @@ WeaveSvc.prototype = { */ _scheduleNextSync: function WeaveSvc__scheduleNextSync(interval) { if (!interval) - interval = SCHEDULED_SYNC_INTERVAL; + interval = this.status.backoffInterval || SCHEDULED_SYNC_INTERVAL; // if there's an existing timer, cancel it and restart if (this._syncTimer) @@ -980,61 +1009,28 @@ WeaveSvc.prototype = { this._log.debug("Next sync call in: " + this._syncTimer.delay / 1000 + " seconds.") }, - _serverErrors: 0, + _syncErrors: 0, /** * Deal with sync errors appropriately */ _handleSyncError: function WeaveSvc__handleSyncError() { - let shouldBackoff = false; + this._syncErrors++; - let err = Weave.Service.detailedStatus.sync; - // we'll assume the server is just borked a little for these - switch (err) { - case METARECORD_DOWNLOAD_FAIL: - case KEYS_DOWNLOAD_FAIL: - case KEYS_UPLOAD_FAIL: - shouldBackoff = true; - } - - // specifcally handle 500, 502, 503, 504 errors - // xxxmpc: what else should be in this list? - // this is sort of pseudocode, need a way to get at the - if (!shouldBackoff) { - try { - shouldBackoff = Utils.checkStatus(Records.response.status, null, - [500, [502, 504]]); - } - catch (e) { - // if responseStatus throws, we have a network issue in play - shouldBackoff = true; + // do nothing on the first couple of failures, if we're not in backoff due to 5xx errors + if (!this.status.enforceBackoff) { + if (this._syncErrors < 3) { + this._scheduleNextSync(); + return; } + this.status.enforceBackoff = true; } - // if this is a client error, do the next sync as normal and return - if (!shouldBackoff) { - this._scheduleNextSync(); - return; - } + const MINIMUM_BACKOFF_INTERVAL = 15 * 60 * 1000; // 15 minutes + let interval = this._calculateBackoff(this._syncErrors, MINIMUM_BACKOFF_INTERVAL); - // ok, something failed connecting to the server, rev the counter - this._serverErrors++; + this._scheduleNextSync(interval); - // do nothing on the first failure, if we fail again we'll back off - if (this._serverErrors < 2) { - this._scheduleNextSync(); - return; - } - - // 30-60 minute backoff interval, increasing each time - const MINIMUM_BACKOFF_INTERVAL = 15 * 60 * 1000; // 15 minutes * >= 2 - const MAXIMUM_BACKOFF_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours - let backoffInterval = this._serverErrors * - (Math.floor(Math.random() * MINIMUM_BACKOFF_INTERVAL) + - MINIMUM_BACKOFF_INTERVAL); - backoffInterval = Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL); - this._scheduleNextSync(backoffInterval); - - let d = new Date(Date.now() + backoffInterval); + let d = new Date(Date.now() + interval); this._log.config("Starting backoff, next sync at:" + d.toString()); }, @@ -1046,7 +1042,7 @@ WeaveSvc.prototype = { */ sync: function WeaveSvc_sync(fullSync) this._catch(this._lock(this._notify("sync", "", function() { - + this.status.resetEngineStatus(); fullSync = true; // not doing thresholds yet // Use thresholds to determine what to sync only if it's not a full sync @@ -1058,12 +1054,16 @@ WeaveSvc.prototype = { let reason = this._checkSync(); if (reason && (useThresh || reason != kSyncNotScheduled)) { // this is a purposeful abort rather than a failure, so don't set - // WEAVE_STATUS_FAILED; instead, leave it as it was. - this._detailedStatus.setSyncStatus(reason); + // any status bits reason = "Can't sync: " + reason; throw reason; } + if (this._autoConnectTimer) { + this._autoConnectTimer.cancel(); + this._autoConnectTimer = null; + } + if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; @@ -1083,7 +1083,7 @@ WeaveSvc.prototype = { if (Clients.getClients()[Clients.clientID].commands) { try { if (!(this.processCommands())) { - this._detailedStatus.setSyncStatus(ABORT_SYNC_COMMAND); + this.status.setSyncStatus(ABORT_SYNC_COMMAND); throw "aborting sync, process commands said so"; } @@ -1132,7 +1132,7 @@ WeaveSvc.prototype = { } // If there's any problems with syncing the engine, report the failure - if (!(this._syncEngine(engine))) { + if (!(this._syncEngine(engine)) || this.status.enforceBackoff) { this._log.info("Aborting sync"); break; } @@ -1147,7 +1147,7 @@ WeaveSvc.prototype = { this._log.warn("Some engines did not sync correctly"); else { Svc.Prefs.set("lastSync", new Date().toString()); - this._weaveStatusCode = WEAVE_STATUS_OK; + this.status.setSyncStatus(SYNC_SUCCEEDED); this._log.info("Sync completed successfully"); } } finally { @@ -1167,9 +1167,10 @@ WeaveSvc.prototype = { if (e.status == 401 && this._updateCluster()) return this._syncEngine(engine); + this._checkServerError(e); + + this.status.setEngineStatus(engine.name, e.failureCode || ENGINE_UNKNOWN_FAIL); this._syncError = true; - this._weaveStatusCode = WEAVE_STATUS_PARTIAL; - this._detailedStatus.setEngineStatus(engine.name, e); this._log.debug(Utils.exceptionStr(e)); return true; } @@ -1201,6 +1202,30 @@ WeaveSvc.prototype = { if (!resp.success) throw resp; }, + + + /** + * Check to see if this is a failure + * + */ + _checkServerError: function WeaveSvc__checkServerError(resp) { + if (Utils.checkStatus(resp.status, null, [500, [502, 504]])) { + this.status.enforceBackoff = true; + if (resp.status == 503 && resp.headers["Retry-After"]) + Observers.notify("weave:service:backoff:interval", parseInt(resp.headers["Retry-After"], 10)); + } + }, + /** + * Return a value for a backoff interval. Maximum is eight hours, unless this.status.backoffInterval is higher. + * + */ + _calculateBackoff: function WeaveSvc__calculateBackoff(attempts, base_interval) { + const MAXIMUM_BACKOFF_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours + let backoffInterval = attempts * + (Math.floor(Math.random() * base_interval) + + base_interval); + return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL), this.status.backoffInterval); + }, /** * Wipe all user data from the server. diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 07492facd8d6..2ab02476ff23 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -699,21 +699,12 @@ let Utils = { }, getErrorString: function Utils_getErrorString(error, args) { - switch (error) { - case Weave.LOGIN_FAILED_NETWORK_ERROR: - errorString = "error.login.reason.network"; - break; - case Weave.LOGIN_FAILED_INVALID_PASSPHRASE: - errorString = "error.login.reason.passphrase"; - break; - case Weave.LOGIN_FAILED_LOGIN_REJECTED: - errorString = "error.login.reason.password"; - break; - default: - errorString = "error.login.reason.unknown"; - break; - } - return this._errorBundle.get(errorString, args || null); + try { + return this._errorBundle.get(error, args || null); + } catch (e) {} + + // basically returns "Unknown Error" + return this._errorBundle.get("error.reason.unknown"); }, // assumes an nsIConverterInputStream From 96419ff15845e688ac7c3f65b2cc107c00cff55d Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Tue, 15 Sep 2009 21:54:05 -0400 Subject: [PATCH 1368/1860] initial patch --- services/sync/locales/en-US/about.properties | 128 ++++++++++-------- services/sync/modules/constants.js | 5 + services/sync/modules/engines.js | 1 + services/sync/modules/engines/bookmarks.js | 1 + services/sync/modules/engines/clients.js | 1 + services/sync/modules/engines/extensions.js | 1 + services/sync/modules/engines/forms.js | 1 + services/sync/modules/engines/history.js | 1 + services/sync/modules/engines/microformats.js | 1 + services/sync/modules/engines/passwords.js | 1 + services/sync/modules/engines/plugins.js | 1 + services/sync/modules/engines/prefs.js | 1 + services/sync/modules/engines/tabs.js | 1 + services/sync/modules/engines/themes.js | 1 + services/sync/modules/identity.js | 4 +- services/sync/modules/service.js | 6 +- 16 files changed, 92 insertions(+), 63 deletions(-) diff --git a/services/sync/locales/en-US/about.properties b/services/sync/locales/en-US/about.properties index 12023c3ed392..9505c4be0db8 100644 --- a/services/sync/locales/en-US/about.properties +++ b/services/sync/locales/en-US/about.properties @@ -1,81 +1,93 @@ +# Top menus + +user-menu-offline = Signed out +user-menu-signing-in = Signing in... +user-menu-online = Signed in as %S +sign-out-item = Sign out +my-account-item = My Account + +advanced-menu-title = Advanced +server-settings-item = Server settings +activity-log-item = Activity Log + # Status messages (below the arrow in the center) -status-offline = signed out -status-offline-2 = (offline) -status-signing-in = -status-signing-in-2 = (signing in...) -status-idle = signed in as %S -status-idle-2 = (idle) -status-sync = signed in as %S -status-sync-2 = (syncing) +status-offline = (offline) +status-signing-in = (signing in...) +status-idle = (idle) +status-sync = (syncing) # Bubbles -prev = prev -next = next +prev = prev +next = next -signedin-signout = sign out -signedin-change-password = change password -signedin-change-passphrase = change passphrase +welcome-title = Welcome To Sync +welcome-1 = Do you already have a Weave account? +welcome-yes = yes +welcome-no = no -signin-title = Sign Into Weave -signin-newacct = new user -signin-username = username -signin-password = password -signin-passphrase = passphrase -signin-next = sign in +my-account-change-password = change password +my-account-change-passphrase = change passphrase -newacct-title = New Weave Account -newacct-username = username -newacct-password = password -newacct-passphrase = passphrase -newacct-email = email address -newacct-tos-label = I agree to the %S -newacct-tos = Terms of Service -captcha-response = Type in the words above -user-taken-password = My username won't work +signin-title = Sign Into Weave +signin-newacct = new user +signin-username = +signin-password = +signin-passphrase = +signin-next = sign in -willsync-title = Account Created! -willsync-1 = Sync will begin in %S seconds... -willsync-config = choose what to sync +newacct-title = New Account +newacct-username = +newacct-password = +newacct-passphrase = +newacct-email = +newacct-tos-label = I agree to the %S +newacct-tos = Terms of Service +captcha-response = +user-taken-password = My username won't work -setup-title = Sync Settings -setup-1 = Check the things you'd like to sync: -setup-sync = sync now +willsync-title = Account Created! +willsync-1 = Sync will begin in %S seconds... +willsync-config = choose what to sync + +setup-title = Sync Settings +setup-1 = Check the things you'd like to sync: +setup-sync = sync now clientinfo-type-desktop = desktop clientinfo-type-laptop = laptop clientinfo-type-mobile = mobile -clientinfo-prefs = choose what to sync +clientinfo-prefs = choose what to sync -cloudinfo-title = What's In The Cloud? -cloudinfo-erase = erase -erase-title = Erase Server Data -erase-warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? +cloudinfo-title = What's In The Cloud? +cloudinfo-erase = erase +erase-title = Erase Server Data +erase-warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? # Help items -help-forgot-password = I forgot my password -forgot-password-1 = Type in your username and we'll send you an email so you can reset it: -forgot-password-box = username -forgot-password-ok = send email +help-forgot-password = I forgot my password +forgot-password-1 = Type in your username and we'll send you an email so you can reset it: +forgot-password-box = username +forgot-password-ok = send email -help-forgot-passphrase = I forgot my passphrase -forgot-passphrase-1 = You can pick a new passphrase, but all your server data will need to be deleted (it cannot be recovered). -forgot-passphrase-2 = To go ahead, click the button below: -forgot-passphrase-ok = reset passphrase +help-forgot-passphrase = I forgot my passphrase +forgot-passphrase-1 = You can pick a new passphrase, but all your server data will need to be deleted (it cannot be recovered). +forgot-passphrase-2 = To go ahead, click the button below: +forgot-passphrase-ok = reset passphrase -help-helpme = I'm stuck! What do I do? -help-helpme-1 = If you're stuck, you might want to try the %S or the %S for help. -help-helpme-faq = FAQ -help-helpme-forum = Weave discussion forum +help-helpme = I'm stuck! What do I do? +help-helpme-1 = If you're stuck, you might want to try the %S or the %S for help. +help-helpme-faq = FAQ +help-helpme-forum = Weave discussion forum -help-user-taken = My username won't work -help-user-taken-1 = Your username might be taken, try adding numbers or additional words to it. -help-user-taken-2 = Additionally, you can't use special symbols or spaces inside usernames. +help-user-taken = My username won't work +help-user-taken-1 = Your username might be taken, try adding numbers or additional words to it. +help-user-taken-2 = Additionally, you can't use special symbols or spaces inside usernames. -help-newacct-pass = Weave won't accept my password or passphrase -help-newacct-pass-1 = The password and passphrase must be different from each other. +help-newacct-pass = Weave won't accept my password or passphrase +help-newacct-pass-1 = The password and passphrase must be different from each other. -help-no-captcha = I can't see the verification image -help-no-captcha-1 = Some add-ons can interfere with the verification image. Try disabling NoScript or similar add-ons. +help-no-captcha = I can't see the verification image +help-no-captcha-1 = Some add-ons can interfere with the verification image. Try disabling NoScript or similar add-ons. diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 83e7220f1e43..8cc93dd774c2 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -147,3 +147,8 @@ const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; const THUNDERBIRD_ID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; const FENNEC_ID = "{a23983c0-fd0e-11dc-95ff-0800200c9a66}"; const SEAMONKEY_ID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}"; + +//UI constants + +// How many data types (bookmarks, history, etc) to display per row +const UI_DATA_TYPES_PER_ROW = 3; \ No newline at end of file diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c25fac35c287..843e675c1119 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -146,6 +146,7 @@ function Engine() { this._init(); } Engine.prototype = { name: "engine", displayName: "Boring Engine", + description: "An engine example - it doesn't actually sync anything", logName: "Engine", // _storeObj, and _trackerObj should to be overridden in subclasses diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index a6d6345e32c0..89756ccce814 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -89,6 +89,7 @@ BookmarksEngine.prototype = { __proto__: SyncEngine.prototype, name: "bookmarks", displayName: "Bookmarks", + description: "Keep your favorite links always at hand", logName: "Bookmarks", _recordObj: PlacesItem, _storeObj: BookmarksStore, diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 3336428ba241..399e681761d0 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -59,6 +59,7 @@ ClientEngine.prototype = { __proto__: SyncEngine.prototype, name: "clients", displayName: "Clients", + description: "Sync information about other clients connected to Weave Sync", logName: "Clients", _storeObj: ClientStore, _trackerObj: ClientTracker, diff --git a/services/sync/modules/engines/extensions.js b/services/sync/modules/engines/extensions.js index c4d43c01d268..2a09deea19fe 100644 --- a/services/sync/modules/engines/extensions.js +++ b/services/sync/modules/engines/extensions.js @@ -45,6 +45,7 @@ ExtensionEngine.prototype = { __proto__: SyncEngine.prototype, displayName: "Extensions", + description: "", logName: "Extensions", name: "extensions", }; diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 85603b9f2fb0..74f606fb1248 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -57,6 +57,7 @@ FormEngine.prototype = { __proto__: SyncEngine.prototype, name: "forms", displayName: "Forms", + description: "Take advantage of form-fill convenience on all your devices", logName: "Forms", _storeObj: FormStore, _trackerObj: FormTracker, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index ab83bff12d3a..b38c08138565 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -77,6 +77,7 @@ HistoryEngine.prototype = { __proto__: SyncEngine.prototype, name: "history", displayName: "History", + description: "All the sites you've been to. Take your awesomebar with you!", logName: "History", _recordObj: HistoryRec, _storeObj: HistoryStore, diff --git a/services/sync/modules/engines/microformats.js b/services/sync/modules/engines/microformats.js index 54b370c7f6d4..3cd47656520d 100644 --- a/services/sync/modules/engines/microformats.js +++ b/services/sync/modules/engines/microformats.js @@ -45,6 +45,7 @@ MicroFormatEngine.prototype = { __proto__: SyncEngine.prototype, displayName: "MicroFormats", + description: "", logName: "MicroFormats", name: "microformats", }; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index abb0548d3889..efd48065edb0 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -56,6 +56,7 @@ PasswordEngine.prototype = { __proto__: SyncEngine.prototype, name: "passwords", displayName: "Passwords", + description: "Forget all your passwords, Weave will remember them for you", logName: "Passwords", _storeObj: PasswordStore, _trackerObj: PasswordTracker, diff --git a/services/sync/modules/engines/plugins.js b/services/sync/modules/engines/plugins.js index 38231f94d292..94c61aeec424 100644 --- a/services/sync/modules/engines/plugins.js +++ b/services/sync/modules/engines/plugins.js @@ -45,6 +45,7 @@ PluginEngine.prototype = { __proto__: SyncEngine.prototype, displayName: "Plugins", + description: "", logName: "Plugins", name: "plugins", }; diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index a260df618cd5..6f64c4b76f02 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -59,6 +59,7 @@ PrefsEngine.prototype = { __proto__: SyncEngine.prototype, name: "prefs", displayName: "Preferences", + description: "Synchronize your home page, selected persona, and more", logName: "Prefs", _storeObj: PrefStore, _trackerObj: PrefTracker, diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 6c9fd7cb4ea4..cba9fd90b613 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -59,6 +59,7 @@ TabEngine.prototype = { __proto__: SyncEngine.prototype, name: "tabs", displayName: "Tabs", + description: "Access tabs from other devices via the History menu", logName: "Tabs", _storeObj: TabStore, _trackerObj: TabTracker, diff --git a/services/sync/modules/engines/themes.js b/services/sync/modules/engines/themes.js index 959a7356a11c..98025bd1f806 100644 --- a/services/sync/modules/engines/themes.js +++ b/services/sync/modules/engines/themes.js @@ -45,6 +45,7 @@ ThemeEngine.prototype = { __proto__: SyncEngine.prototype, displayName: "Themes", + description: "", logName: "Themes", name: "themes", }; diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 828b34bd23e4..368ff4452e85 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -116,9 +116,9 @@ Identity.prototype = { log.trace("Persisting " + this.realm + " for " + this.username); let nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new nsLoginInfo(PWDMGR_HOST, null, this.realm, + let newLogin = new nsLoginInfo(PWDMGR_HOST, null, this.realm, this.username, this.password, "", ""); - Svc.Login.addLogin(login); + Svc.Login.addLogin(newLogin); }, get _logins _logins() Svc.Login.findLogins({}, PWDMGR_HOST, null, this.realm) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 35a1bceb1738..05773d2e9a47 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -668,11 +668,11 @@ WeaveSvc.prototype = { if (Svc.IO.offline) throw "Application is offline, login should not be called"; - if (username != null) + if (username) this.username = username; - if (password != null) + if (password) this.password = password; - if (passphrase != null) + if (passphrase) this.passphrase = passphrase; if (!this.username) { From b3c87a780cda48d3d1b89936feea16614715d80c Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 15 Sep 2009 21:57:06 -0400 Subject: [PATCH 1369/1860] fix minor merge kvetch --- services/sync/modules/constants.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 8cc93dd774c2..432df0bd8e79 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -56,7 +56,8 @@ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', 'kSyncNotScheduled', - 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID']; + 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID', + 'UI_DATA_TYPES_PER_ROW']; const WEAVE_VERSION = "@weave_version@"; From 9e5db72ea9825393ca859fe82031bbd664134a1f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 15 Sep 2009 19:07:23 -0700 Subject: [PATCH 1370/1860] Get rid of the ugly EXPORTED_SYMBOLS array of strings for constants and generate it from a constants hash that sets the value and provides the exported name. Remove some unused constants and line-up the values at 40th column. --- services/sync/modules/constants.js | 149 +++++++++++------------------ 1 file changed, 58 insertions(+), 91 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 83e7220f1e43..713260c55a92 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -34,116 +34,83 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", - "PREFS_BRANCH", "PWDMGR_HOST", - 'MODE_RDONLY', 'MODE_WRONLY', - 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', - 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', - 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', - 'CONNECTION_TIMEOUT', 'MAX_UPLOAD_RECORDS', - 'SYNC_SUCCEEDED', 'LOGIN_SUCCEEDED', 'ENGINE_SUCCEEDED', - 'STATUS_OK', 'LOGIN_FAILED', 'SYNC_FAILED', - 'SYNC_FAILED_PARTIAL', 'STATUS_DISABLED', 'SERVER_LOW_QUOTA', - 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', - 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', - 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', - 'LOGIN_FAILED_LOGIN_REJECTED', 'METARECORD_DOWNLOAD_FAIL', - 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', - 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', - 'ENGINE_UPLOAD_FAIL', 'ENGINE_DOWNLOAD_FAIL', 'ENGINE_UNKNOWN_FAIL', - 'ENGINE_METARECORD_UPLOAD_FAIL', - 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', - 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', - 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', - 'kSyncNotScheduled', - 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID']; +// Process each item in the "constants hash" to add to "global" and give a name +let EXPORTED_SYMBOLS = [((this[key] = val), key) for ([key, val] in Iterator({ -const WEAVE_VERSION = "@weave_version@"; +WEAVE_VERSION: "@weave_version@", -// last client version's server storage this version supports -// e.g. if set to the current version, this client will wipe the server -// data stored by any older client -const COMPATIBLE_VERSION = "@compatible_version@"; +// Last client version this client can read. If the server contains an older +// version, this client will wipe the data on the server first. +COMPATIBLE_VERSION: "@compatible_version@", -const PREFS_BRANCH = "extensions.weave."; +PREFS_BRANCH: "extensions.weave.", // Host "key" to access Weave Identity in the password manager -const PWDMGR_HOST = "chrome://weave"; +PWDMGR_HOST: "chrome://weave", -const MODE_RDONLY = 0x01; -const MODE_WRONLY = 0x02; -const MODE_CREATE = 0x08; -const MODE_APPEND = 0x10; -const MODE_TRUNCATE = 0x20; +// File IO Flags +MODE_RDONLY: 0x01, +MODE_WRONLY: 0x02, +MODE_CREATE: 0x08, +MODE_APPEND: 0x10, +MODE_TRUNCATE: 0x20, -const PERMS_FILE = 0644; -const PERMS_PASSFILE = 0600; -const PERMS_DIRECTORY = 0755; +// File Permission flags +PERMS_FILE: 0644, +PERMS_PASSFILE: 0600, +PERMS_DIRECTORY: 0755, -const ONE_BYTE = 1; -const ONE_KILOBYTE = 1024 * ONE_BYTE; -const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; - -const CONNECTION_TIMEOUT = 30000; - -// How many records to upload in a single POST -// If there are more, multiple POST calls will be made. +// Number of records to upload in a single POST (multiple POSTS if exceeded) // Record size limit is currently 10K, so 100 is a bit over 1MB -const MAX_UPLOAD_RECORDS = 100; +MAX_UPLOAD_RECORDS: 100, // Top-level statuses: -const STATUS_OK = "success.status_ok"; -const SYNC_FAILED = "error.sync.failed"; -const LOGIN_FAILED = "error.login.failed"; -const SYNC_FAILED_PARTIAL = "error.sync.failed_partial"; -const STATUS_DISABLED = "service.disabled"; +STATUS_OK: "success.status_ok", +SYNC_FAILED: "error.sync.failed", +LOGIN_FAILED: "error.login.failed", +SYNC_FAILED_PARTIAL: "error.sync.failed_partial", +STATUS_DISABLED: "service.disabled", // success states -const LOGIN_SUCCEEDED = "success.login"; -const SYNC_SUCCEEDED = "success.sync"; -const ENGINE_SUCCEEDED = "success.engine"; +LOGIN_SUCCEEDED: "success.login", +SYNC_SUCCEEDED: "success.sync", +ENGINE_SUCCEEDED: "success.engine", // login failure status codes: -const LOGIN_FAILED_NO_USERNAME = "error.login.reason.no_username"; -const LOGIN_FAILED_NO_PASSWORD = "error.login.reason.no_password"; -const LOGIN_FAILED_NETWORK_ERROR = "error.login.reason.network"; -const LOGIN_FAILED_INVALID_PASSPHRASE = "error.login.reason.passphrase."; -const LOGIN_FAILED_LOGIN_REJECTED = "error.login.reason.password"; +LOGIN_FAILED_NO_USERNAME: "error.login.reason.no_username", +LOGIN_FAILED_NO_PASSWORD: "error.login.reason.no_password", +LOGIN_FAILED_NETWORK_ERROR: "error.login.reason.network", +LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.passphrase.", +LOGIN_FAILED_LOGIN_REJECTED: "error.login.reason.password", // sync failure status codes -const METARECORD_DOWNLOAD_FAIL = "error.sync.reason.metarecord_download_fail"; -const VERSION_OUT_OF_DATE = "error.sync.reason.version_out_of_date"; -const DESKTOP_VERSION_OUT_OF_DATE = "error.sync.reason.desktop_version_out_of_date"; -const KEYS_DOWNLOAD_FAIL = "error.sync.reason.keys_download_fail"; -const NO_KEYS_NO_KEYGEN = "error.sync.reason.no_keys_no_keygen"; -const KEYS_UPLOAD_FAIL = "error.sync.reason.keys_upload_fail"; -const SETUP_FAILED_NO_PASSPHRASE = "error.sync.reason.setup_failed_no_passphrase"; +METARECORD_DOWNLOAD_FAIL: "error.sync.reason.metarecord_download_fail", +VERSION_OUT_OF_DATE: "error.sync.reason.version_out_of_date", +DESKTOP_VERSION_OUT_OF_DATE: "error.sync.reason.desktop_version_out_of_date", +KEYS_DOWNLOAD_FAIL: "error.sync.reason.keys_download_fail", +NO_KEYS_NO_KEYGEN: "error.sync.reason.no_keys_no_keygen", +KEYS_UPLOAD_FAIL: "error.sync.reason.keys_upload_fail", +SETUP_FAILED_NO_PASSPHRASE: "error.sync.reason.setup_failed_no_passphrase", +ABORT_SYNC_COMMAND: "aborting sync, process commands said so", // engine failure status codes -const ENGINE_UPLOAD_FAIL = "error.engine.reason.record_upload_fail"; -const ENGINE_DOWNLOAD_FAIL = "error.engine.reason.record_download_fail"; -const ENGINE_UNKNOWN_FAIL = "error.engine.reason.unknown_fail"; -const ENGINE_METARECORD_UPLOAD_FAIL = "error.engine.reason.metarecord_upload_fail"; +ENGINE_UPLOAD_FAIL: "error.engine.reason.record_upload_fail", +ENGINE_DOWNLOAD_FAIL: "error.engine.reason.record_download_fail", +ENGINE_UNKNOWN_FAIL: "error.engine.reason.unknown_fail", +ENGINE_METARECORD_UPLOAD_FAIL: "error.engine.reason.metarecord_upload_fail", -// Server statuses (Not mutually exclusive): -const SERVER_LOW_QUOTA = "Getting close to your Weave server storage quota."; -const SERVER_DOWNTIME = "Weave server is overloaded, try agian in 30 sec."; -const SERVER_UNREACHABLE = "Weave server is unreachable."; - -// Ways that a sync can be disabled -const kSyncWeaveDisabled = "Weave is disabled"; -const kSyncNotLoggedIn = "User is not logged in"; -const kSyncNetworkOffline = "Network is offline"; -const kSyncInPrivateBrowsing = "Private browsing is enabled"; -const kSyncNotScheduled = "Not scheduled to do sync"; -const kSyncBackoffNotMet = "Trying to sync before the server said it's okay"; - - -// Ways that a sync can be aborted: -const ABORT_SYNC_COMMAND = "aborting sync, process commands said so"; +// Ways that a sync can be disabled (messages only to be printed in debug log) +kSyncWeaveDisabled: "Weave is disabled", +kSyncNotLoggedIn: "User is not logged in", +kSyncNetworkOffline: "Network is offline", +kSyncInPrivateBrowsing: "Private browsing is enabled", +kSyncNotScheduled: "Not scheduled to do sync", +kSyncBackoffNotMet: "Trying to sync before the server said it's okay", // Application IDs -const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; -const THUNDERBIRD_ID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; -const FENNEC_ID = "{a23983c0-fd0e-11dc-95ff-0800200c9a66}"; -const SEAMONKEY_ID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}"; +FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", +THUNDERBIRD_ID: "{3550f703-e582-4d05-9a08-453d09bdfdc6}", +FENNEC_ID: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}", +SEAMONKEY_ID: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", + +}))]; From 8f70af1d499764e00af4b058c4f284572ee4491f Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Wed, 16 Sep 2009 13:36:11 -0700 Subject: [PATCH 1371/1860] Bug 516350 - about:weave round 3 initial landing --- services/sync/locales/en-US/about.properties | 128 ++++++++++-------- services/sync/modules/constants.js | 10 +- services/sync/modules/engines.js | 1 + services/sync/modules/engines/bookmarks.js | 1 + services/sync/modules/engines/clients.js | 1 + services/sync/modules/engines/extensions.js | 1 + services/sync/modules/engines/forms.js | 1 + services/sync/modules/engines/history.js | 1 + services/sync/modules/engines/microformats.js | 1 + services/sync/modules/engines/passwords.js | 1 + services/sync/modules/engines/plugins.js | 1 + services/sync/modules/engines/prefs.js | 1 + services/sync/modules/engines/tabs.js | 1 + services/sync/modules/engines/themes.js | 1 + services/sync/modules/identity.js | 4 +- services/sync/modules/service.js | 6 +- 16 files changed, 95 insertions(+), 65 deletions(-) diff --git a/services/sync/locales/en-US/about.properties b/services/sync/locales/en-US/about.properties index 12023c3ed392..9505c4be0db8 100644 --- a/services/sync/locales/en-US/about.properties +++ b/services/sync/locales/en-US/about.properties @@ -1,81 +1,93 @@ +# Top menus + +user-menu-offline = Signed out +user-menu-signing-in = Signing in... +user-menu-online = Signed in as %S +sign-out-item = Sign out +my-account-item = My Account + +advanced-menu-title = Advanced +server-settings-item = Server settings +activity-log-item = Activity Log + # Status messages (below the arrow in the center) -status-offline = signed out -status-offline-2 = (offline) -status-signing-in = -status-signing-in-2 = (signing in...) -status-idle = signed in as %S -status-idle-2 = (idle) -status-sync = signed in as %S -status-sync-2 = (syncing) +status-offline = (offline) +status-signing-in = (signing in...) +status-idle = (idle) +status-sync = (syncing) # Bubbles -prev = prev -next = next +prev = prev +next = next -signedin-signout = sign out -signedin-change-password = change password -signedin-change-passphrase = change passphrase +welcome-title = Welcome To Sync +welcome-1 = Do you already have a Weave account? +welcome-yes = yes +welcome-no = no -signin-title = Sign Into Weave -signin-newacct = new user -signin-username = username -signin-password = password -signin-passphrase = passphrase -signin-next = sign in +my-account-change-password = change password +my-account-change-passphrase = change passphrase -newacct-title = New Weave Account -newacct-username = username -newacct-password = password -newacct-passphrase = passphrase -newacct-email = email address -newacct-tos-label = I agree to the %S -newacct-tos = Terms of Service -captcha-response = Type in the words above -user-taken-password = My username won't work +signin-title = Sign Into Weave +signin-newacct = new user +signin-username = +signin-password = +signin-passphrase = +signin-next = sign in -willsync-title = Account Created! -willsync-1 = Sync will begin in %S seconds... -willsync-config = choose what to sync +newacct-title = New Account +newacct-username = +newacct-password = +newacct-passphrase = +newacct-email = +newacct-tos-label = I agree to the %S +newacct-tos = Terms of Service +captcha-response = +user-taken-password = My username won't work -setup-title = Sync Settings -setup-1 = Check the things you'd like to sync: -setup-sync = sync now +willsync-title = Account Created! +willsync-1 = Sync will begin in %S seconds... +willsync-config = choose what to sync + +setup-title = Sync Settings +setup-1 = Check the things you'd like to sync: +setup-sync = sync now clientinfo-type-desktop = desktop clientinfo-type-laptop = laptop clientinfo-type-mobile = mobile -clientinfo-prefs = choose what to sync +clientinfo-prefs = choose what to sync -cloudinfo-title = What's In The Cloud? -cloudinfo-erase = erase -erase-title = Erase Server Data -erase-warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? +cloudinfo-title = What's In The Cloud? +cloudinfo-erase = erase +erase-title = Erase Server Data +erase-warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? # Help items -help-forgot-password = I forgot my password -forgot-password-1 = Type in your username and we'll send you an email so you can reset it: -forgot-password-box = username -forgot-password-ok = send email +help-forgot-password = I forgot my password +forgot-password-1 = Type in your username and we'll send you an email so you can reset it: +forgot-password-box = username +forgot-password-ok = send email -help-forgot-passphrase = I forgot my passphrase -forgot-passphrase-1 = You can pick a new passphrase, but all your server data will need to be deleted (it cannot be recovered). -forgot-passphrase-2 = To go ahead, click the button below: -forgot-passphrase-ok = reset passphrase +help-forgot-passphrase = I forgot my passphrase +forgot-passphrase-1 = You can pick a new passphrase, but all your server data will need to be deleted (it cannot be recovered). +forgot-passphrase-2 = To go ahead, click the button below: +forgot-passphrase-ok = reset passphrase -help-helpme = I'm stuck! What do I do? -help-helpme-1 = If you're stuck, you might want to try the %S or the %S for help. -help-helpme-faq = FAQ -help-helpme-forum = Weave discussion forum +help-helpme = I'm stuck! What do I do? +help-helpme-1 = If you're stuck, you might want to try the %S or the %S for help. +help-helpme-faq = FAQ +help-helpme-forum = Weave discussion forum -help-user-taken = My username won't work -help-user-taken-1 = Your username might be taken, try adding numbers or additional words to it. -help-user-taken-2 = Additionally, you can't use special symbols or spaces inside usernames. +help-user-taken = My username won't work +help-user-taken-1 = Your username might be taken, try adding numbers or additional words to it. +help-user-taken-2 = Additionally, you can't use special symbols or spaces inside usernames. -help-newacct-pass = Weave won't accept my password or passphrase -help-newacct-pass-1 = The password and passphrase must be different from each other. +help-newacct-pass = Weave won't accept my password or passphrase +help-newacct-pass-1 = The password and passphrase must be different from each other. -help-no-captcha = I can't see the verification image -help-no-captcha-1 = Some add-ons can interfere with the verification image. Try disabling NoScript or similar add-ons. +help-no-captcha = I can't see the verification image +help-no-captcha-1 = Some add-ons can interfere with the verification image. Try disabling NoScript or similar add-ons. diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index f6e7da3934a6..161691e88ffa 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -45,7 +45,7 @@ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", 'WEAVE_STATUS_PARTIAL', 'SERVER_LOW_QUOTA', 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', - 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', + 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', 'LOGIN_FAILED_LOGIN_REJECTED', 'METARECORD_DOWNLOAD_FAIL', 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', @@ -53,7 +53,8 @@ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', 'kSyncNotScheduled', - 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID']; + 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID', + 'UI_DATA_TYPES_PER_ROW']; const WEAVE_VERSION = "@weave_version@"; @@ -128,3 +129,8 @@ const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; const THUNDERBIRD_ID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; const FENNEC_ID = "{a23983c0-fd0e-11dc-95ff-0800200c9a66}"; const SEAMONKEY_ID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}"; + +//UI constants + +// How many data types (bookmarks, history, etc) to display per row +const UI_DATA_TYPES_PER_ROW = 3; \ No newline at end of file diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3339cf5fc446..6db0fd295604 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -146,6 +146,7 @@ function Engine() { this._init(); } Engine.prototype = { name: "engine", displayName: "Boring Engine", + description: "An engine example - it doesn't actually sync anything", logName: "Engine", // _storeObj, and _trackerObj should to be overridden in subclasses diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index a6d6345e32c0..89756ccce814 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -89,6 +89,7 @@ BookmarksEngine.prototype = { __proto__: SyncEngine.prototype, name: "bookmarks", displayName: "Bookmarks", + description: "Keep your favorite links always at hand", logName: "Bookmarks", _recordObj: PlacesItem, _storeObj: BookmarksStore, diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 3336428ba241..399e681761d0 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -59,6 +59,7 @@ ClientEngine.prototype = { __proto__: SyncEngine.prototype, name: "clients", displayName: "Clients", + description: "Sync information about other clients connected to Weave Sync", logName: "Clients", _storeObj: ClientStore, _trackerObj: ClientTracker, diff --git a/services/sync/modules/engines/extensions.js b/services/sync/modules/engines/extensions.js index c4d43c01d268..2a09deea19fe 100644 --- a/services/sync/modules/engines/extensions.js +++ b/services/sync/modules/engines/extensions.js @@ -45,6 +45,7 @@ ExtensionEngine.prototype = { __proto__: SyncEngine.prototype, displayName: "Extensions", + description: "", logName: "Extensions", name: "extensions", }; diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 85603b9f2fb0..74f606fb1248 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -57,6 +57,7 @@ FormEngine.prototype = { __proto__: SyncEngine.prototype, name: "forms", displayName: "Forms", + description: "Take advantage of form-fill convenience on all your devices", logName: "Forms", _storeObj: FormStore, _trackerObj: FormTracker, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index ab83bff12d3a..b38c08138565 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -77,6 +77,7 @@ HistoryEngine.prototype = { __proto__: SyncEngine.prototype, name: "history", displayName: "History", + description: "All the sites you've been to. Take your awesomebar with you!", logName: "History", _recordObj: HistoryRec, _storeObj: HistoryStore, diff --git a/services/sync/modules/engines/microformats.js b/services/sync/modules/engines/microformats.js index 54b370c7f6d4..3cd47656520d 100644 --- a/services/sync/modules/engines/microformats.js +++ b/services/sync/modules/engines/microformats.js @@ -45,6 +45,7 @@ MicroFormatEngine.prototype = { __proto__: SyncEngine.prototype, displayName: "MicroFormats", + description: "", logName: "MicroFormats", name: "microformats", }; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index abb0548d3889..efd48065edb0 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -56,6 +56,7 @@ PasswordEngine.prototype = { __proto__: SyncEngine.prototype, name: "passwords", displayName: "Passwords", + description: "Forget all your passwords, Weave will remember them for you", logName: "Passwords", _storeObj: PasswordStore, _trackerObj: PasswordTracker, diff --git a/services/sync/modules/engines/plugins.js b/services/sync/modules/engines/plugins.js index 38231f94d292..94c61aeec424 100644 --- a/services/sync/modules/engines/plugins.js +++ b/services/sync/modules/engines/plugins.js @@ -45,6 +45,7 @@ PluginEngine.prototype = { __proto__: SyncEngine.prototype, displayName: "Plugins", + description: "", logName: "Plugins", name: "plugins", }; diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index a260df618cd5..6f64c4b76f02 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -59,6 +59,7 @@ PrefsEngine.prototype = { __proto__: SyncEngine.prototype, name: "prefs", displayName: "Preferences", + description: "Synchronize your home page, selected persona, and more", logName: "Prefs", _storeObj: PrefStore, _trackerObj: PrefTracker, diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 6c9fd7cb4ea4..cba9fd90b613 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -59,6 +59,7 @@ TabEngine.prototype = { __proto__: SyncEngine.prototype, name: "tabs", displayName: "Tabs", + description: "Access tabs from other devices via the History menu", logName: "Tabs", _storeObj: TabStore, _trackerObj: TabTracker, diff --git a/services/sync/modules/engines/themes.js b/services/sync/modules/engines/themes.js index 959a7356a11c..98025bd1f806 100644 --- a/services/sync/modules/engines/themes.js +++ b/services/sync/modules/engines/themes.js @@ -45,6 +45,7 @@ ThemeEngine.prototype = { __proto__: SyncEngine.prototype, displayName: "Themes", + description: "", logName: "Themes", name: "themes", }; diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 828b34bd23e4..368ff4452e85 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -116,9 +116,9 @@ Identity.prototype = { log.trace("Persisting " + this.realm + " for " + this.username); let nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new nsLoginInfo(PWDMGR_HOST, null, this.realm, + let newLogin = new nsLoginInfo(PWDMGR_HOST, null, this.realm, this.username, this.password, "", ""); - Svc.Login.addLogin(login); + Svc.Login.addLogin(newLogin); }, get _logins _logins() Svc.Login.findLogins({}, PWDMGR_HOST, null, this.realm) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 785f5f139e55..af334c5757fc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -647,11 +647,11 @@ WeaveSvc.prototype = { if (Svc.IO.offline) throw "Application is offline, login should not be called"; - if (username != null) + if (username) this.username = username; - if (password != null) + if (password) this.password = password; - if (passphrase != null) + if (passphrase) this.passphrase = passphrase; if (!this.username) { From 26f80356f5606da5a1420572a6c59e523ca3f7d2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Sep 2009 13:45:08 -0700 Subject: [PATCH 1372/1860] Resolve constants.js.in conflicts by switching to the new format. --- services/sync/modules/constants.js | 38 +++--------------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index de4aec679e0b..984ead7037f7 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -34,32 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -<<<<<<< local -const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", - "PREFS_BRANCH", "PWDMGR_HOST", - 'MODE_RDONLY', 'MODE_WRONLY', - 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', - 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', - 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', - 'CONNECTION_TIMEOUT', 'MAX_UPLOAD_RECORDS', - 'WEAVE_STATUS_OK', 'WEAVE_STATUS_FAILED', - 'WEAVE_STATUS_PARTIAL', 'SERVER_LOW_QUOTA', - 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', - 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', - 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', - 'LOGIN_FAILED_LOGIN_REJECTED', 'METARECORD_DOWNLOAD_FAIL', - 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', - 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', - 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', - 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', - 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', - 'kSyncNotScheduled', - 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID', - 'UI_DATA_TYPES_PER_ROW']; -======= // Process each item in the "constants hash" to add to "global" and give a name let EXPORTED_SYMBOLS = [((this[key] = val), key) for ([key, val] in Iterator({ ->>>>>>> other WEAVE_VERSION: "@weave_version@", @@ -132,22 +108,14 @@ kSyncNotScheduled: "Not scheduled to do sync", kSyncBackoffNotMet: "Trying to sync before the server said it's okay", // Application IDs -<<<<<<< local -const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; -const THUNDERBIRD_ID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; -const FENNEC_ID = "{a23983c0-fd0e-11dc-95ff-0800200c9a66}"; -const SEAMONKEY_ID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}"; -======= FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", THUNDERBIRD_ID: "{3550f703-e582-4d05-9a08-453d09bdfdc6}", FENNEC_ID: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}", SEAMONKEY_ID: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", ->>>>>>> other -<<<<<<< local -//UI constants +// UI constants // How many data types (bookmarks, history, etc) to display per row -const UI_DATA_TYPES_PER_ROW = 3;======= +UI_DATA_TYPES_PER_ROW: 3, + }))]; ->>>>>>> other From 6d637de4d0e9f18bb229a32e5526ce2f7e06e248 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 16 Sep 2009 16:55:27 -0400 Subject: [PATCH 1373/1860] move email address below password --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 432df0bd8e79..d638f7ca0894 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -55,7 +55,7 @@ const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', - 'kSyncNotScheduled', + 'kSyncNotScheduled', 'kSyncBackoffNotMet', 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID', 'UI_DATA_TYPES_PER_ROW']; From 6202fa5413408748d0367b5e2c802e67122efbe2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Sep 2009 13:56:37 -0700 Subject: [PATCH 1374/1860] Resolve conflicts by taking the already-fixed about.js and locally-fixed constants.js.in. --- services/sync/modules/constants.js | 41 ------------------------------ 1 file changed, 41 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 9c85a94a84b6..984ead7037f7 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -34,35 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -<<<<<<< local // Process each item in the "constants hash" to add to "global" and give a name let EXPORTED_SYMBOLS = [((this[key] = val), key) for ([key, val] in Iterator({ -======= -const EXPORTED_SYMBOLS = ["WEAVE_VERSION", "COMPATIBLE_VERSION", - "PREFS_BRANCH", "PWDMGR_HOST", - 'MODE_RDONLY', 'MODE_WRONLY', - 'MODE_CREATE', 'MODE_APPEND', 'MODE_TRUNCATE', - 'PERMS_FILE', 'PERMS_PASSFILE', 'PERMS_DIRECTORY', - 'ONE_BYTE', 'ONE_KILOBYTE', 'ONE_MEGABYTE', - 'CONNECTION_TIMEOUT', 'MAX_UPLOAD_RECORDS', - 'SYNC_SUCCEEDED', 'LOGIN_SUCCEEDED', 'ENGINE_SUCCEEDED', - 'STATUS_OK', 'LOGIN_FAILED', 'SYNC_FAILED', - 'SYNC_FAILED_PARTIAL', 'STATUS_DISABLED', 'SERVER_LOW_QUOTA', - 'SERVER_DOWNTIME', 'SERVER_UNREACHABLE', - 'LOGIN_FAILED_NO_USERNAME', 'LOGIN_FAILED_NO_PASSWORD', - 'LOGIN_FAILED_NETWORK_ERROR','LOGIN_FAILED_INVALID_PASSPHRASE', - 'LOGIN_FAILED_LOGIN_REJECTED', 'METARECORD_DOWNLOAD_FAIL', - 'VERSION_OUT_OF_DATE', 'DESKTOP_VERSION_OUT_OF_DATE', - 'KEYS_DOWNLOAD_FAIL', 'NO_KEYS_NO_KEYGEN', 'KEYS_UPLOAD_FAIL', - 'ENGINE_UPLOAD_FAIL', 'ENGINE_DOWNLOAD_FAIL', 'ENGINE_UNKNOWN_FAIL', - 'ENGINE_METARECORD_UPLOAD_FAIL', - 'SETUP_FAILED_NO_PASSPHRASE', 'ABORT_SYNC_COMMAND', - 'kSyncWeaveDisabled', 'kSyncNotLoggedIn', - 'kSyncNetworkOffline', 'kSyncInPrivateBrowsing', - 'kSyncNotScheduled', - 'FIREFOX_ID', 'THUNDERBIRD_ID', 'FENNEC_ID', 'SEAMONKEY_ID', - 'UI_DATA_TYPES_PER_ROW']; ->>>>>>> other WEAVE_VERSION: "@weave_version@", @@ -135,28 +108,14 @@ kSyncNotScheduled: "Not scheduled to do sync", kSyncBackoffNotMet: "Trying to sync before the server said it's okay", // Application IDs -<<<<<<< local FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", THUNDERBIRD_ID: "{3550f703-e582-4d05-9a08-453d09bdfdc6}", FENNEC_ID: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}", SEAMONKEY_ID: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", -======= -const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; -const THUNDERBIRD_ID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; -const FENNEC_ID = "{a23983c0-fd0e-11dc-95ff-0800200c9a66}"; -const SEAMONKEY_ID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}"; ->>>>>>> other -<<<<<<< local // UI constants -======= -//UI constants ->>>>>>> other // How many data types (bookmarks, history, etc) to display per row -<<<<<<< local UI_DATA_TYPES_PER_ROW: 3, }))]; -======= -const UI_DATA_TYPES_PER_ROW = 3;>>>>>>> other From fcd74808106e8691cb0beba396ed58cce58b0b28 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Sep 2009 16:52:43 -0700 Subject: [PATCH 1375/1860] Use the lazyStrings object for error strings to avoid using the wrong file (locales vs locale). --- services/sync/modules/util.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 2ab02476ff23..1b1270f44921 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -690,21 +690,13 @@ let Utils = { Utils._openChromeWindow("Sync", "pick-sync.xul"); }, - __errorBundle: null, - get _errorBundle() { - if (!this.__errorBundle) { - this.__errorBundle = new StringBundle("chrome://weave/locales/errors.properties"); - } - return this.__errorBundle; - }, - getErrorString: function Utils_getErrorString(error, args) { try { - return this._errorBundle.get(error, args || null); + return Str.errors.get(error, args || null); } catch (e) {} // basically returns "Unknown Error" - return this._errorBundle.get("error.reason.unknown"); + return Str.errors.get("error.reason.unknown"); }, // assumes an nsIConverterInputStream @@ -793,5 +785,5 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], Ci[lazy[2]])); let Str = {}; -["service", "about"] +["about", "errors"] .forEach(function(lazy) Utils.lazy2(Str, lazy, Utils.lazyStrings(lazy))); From f30f3cca5f0ac11905194f73d6e0f731974dd068 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Sep 2009 19:15:46 -0700 Subject: [PATCH 1376/1860] Remove trailing period for passphrase error code. --- services/sync/modules/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 713260c55a92..555eb0d0f2c8 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -80,7 +80,7 @@ ENGINE_SUCCEEDED: "success.engine", LOGIN_FAILED_NO_USERNAME: "error.login.reason.no_username", LOGIN_FAILED_NO_PASSWORD: "error.login.reason.no_password", LOGIN_FAILED_NETWORK_ERROR: "error.login.reason.network", -LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.passphrase.", +LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.passphrase", LOGIN_FAILED_LOGIN_REJECTED: "error.login.reason.password", // sync failure status codes From 223679ee8fe9011500c4c6d9f7dcfd9e927f0ba3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Sep 2009 19:15:55 -0700 Subject: [PATCH 1377/1860] Remove meta serialization now that resource takes objects. --- services/sync/modules/engines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c25fac35c287..2c9b51bbbad8 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -361,7 +361,7 @@ SyncEngine.prototype = { meta.generateIV(); meta.addUnwrappedKey(pubkey, symkey); let res = new Resource(meta.uri); - let resp = res.put(meta.serialize()); + let resp = res.put(meta); if (!resp.success) { this._log.debug("Metarecord upload fail:" + resp); resp.failureCode = ENGINE_METARECORD_UPLOAD_FAIL; From 6279977d96a1e88174f565afa5180f86367a73c4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 17 Sep 2009 14:41:24 -0700 Subject: [PATCH 1378/1860] Use trace logging for reporting GUIDs. --- services/sync/modules/engines/forms.js | 4 ++-- services/sync/modules/engines/passwords.js | 12 ++++++------ services/sync/modules/engines/prefs.js | 4 ++-- services/sync/modules/engines/tabs.js | 6 ++---- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 85603b9f2fb0..cabd9d6163b7 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -132,12 +132,12 @@ FormStore.prototype = { }, cacheFormItems: function FormStore_cacheFormItems() { - this._log.debug("Caching all form items"); + this._log.trace("Caching all form items"); this._formItems = this.getAllIDs(); }, clearFormCache: function FormStore_clearFormCache() { - this._log.debug("Clearing form cache"); + this._log.trace("Clearing form cache"); this._formItems = null; }, diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index abb0548d3889..f7c2729561b8 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -111,7 +111,7 @@ PasswordStore.prototype = { let logins = Svc.Login.searchLogins({}, prop); if (logins.length > 0) { - this._log.debug(logins.length + " items matching " + id + " found."); + this._log.trace(logins.length + " items matching " + id + " found."); return logins[0]; } else { this._log.trace("No items matching " + id + " found. Ignoring"); @@ -132,7 +132,7 @@ PasswordStore.prototype = { }, changeItemID: function PasswordStore__changeItemID(oldID, newID) { - this._log.debug("Changing item ID: " + oldID + " to " + newID); + this._log.trace("Changing item ID: " + oldID + " to " + newID); let oldLogin = this._getLoginFromGUID(oldID); if (!oldLogin) { @@ -197,11 +197,11 @@ PasswordStore.prototype = { update: function PasswordStore__update(record) { let loginItem = this._getLoginFromGUID(record.id); if (!loginItem) { - this._log.debug("Skipping update for unknown item: " + record.id); + this._log.debug("Skipping update for unknown item: " + record.hostname); return; } - this._log.debug("Updating " + record.id); + this._log.debug("Updating " + record.hostname); let newinfo = this._nsLoginInfoFromRecord(record); Svc.Login.modifyLogin(loginItem, newinfo); }, @@ -238,11 +238,11 @@ PasswordTracker.prototype = { case 'removeLogin': aSubject.QueryInterface(Ci.nsILoginMetaInfo); this._score += 15; - this._log.debug(aData + ": " + aSubject.guid); + this._log.trace(aData + ": " + aSubject.guid); this.addChangedID(aSubject.guid); break; case 'removeAllLogins': - this._log.debug(aData); + this._log.trace(aData); this._score += 50; break; } diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index a260df618cd5..ad9a733447b6 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -183,7 +183,7 @@ PrefStore.prototype = { }, update: function PrefStore_update(record) { - this._log.debug("Received pref updates, applying..."); + this._log.trace("Received pref updates, applying..."); this._setAllPrefs(record.value); }, @@ -232,7 +232,7 @@ PrefTracker.prototype = { if (this._syncPrefs.indexOf(aData) != -1) { this._score += 25; this.addChangedID(WEAVE_PREFS_GUID); - this._log.debug("Preference " + aData + " changed"); + this._log.trace("Preference " + aData + " changed"); } } }; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 6c9fd7cb4ea4..e3fcb9d7435f 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -302,7 +302,6 @@ TabStore.prototype = { }, wipe: function TabStore_wipe() { - this._log.debug("Wipe called. Clearing cache of remote client tabs."); this._remoteClients = {}; }, @@ -320,8 +319,7 @@ TabStore.prototype = { update: function TabStore_update(record) { if (record.id == this._localClientGUID) return; // can't happen? - this._log.debug("Update called. Updating remote client record for"); - this._log.debug(record.getClientName()); + this._log.debug("Updating remote client: " + record.getClientName()); this._remoteClients[record.id] = record; this._writeToFile(); }, @@ -329,7 +327,7 @@ TabStore.prototype = { remove: function TabStore_remove(record) { if (record.id == this._localClientGUID) return; // can't happen? - this._log.debug("Remove called. Deleting record with id " + record.id); + this._log.trace("Remove called. Deleting record with id " + record.id); delete this._remoteClients[record.id]; this._writeToFile(); } From be6a13a74ca387baa74fec36d8cd52777ffede76 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 18 Sep 2009 00:15:37 -0700 Subject: [PATCH 1379/1860] Keep track of previously opened windows of pages that should only have one instance open and close the old one if it's still open. --- services/sync/modules/util.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 1b1270f44921..42a8c603a131 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -181,6 +181,20 @@ let Utils = { } }, + ensureOneOpen: let (windows = {}) function ensureOneOpen(window) { + // Close the other window if it exists + let url = window.location.href; + let other = windows[url]; + if (other != null) + other.close(); + + // Save the new window for future closure + windows[url] = window; + + // Actively clean up when the window is closed + window.addEventListener("unload", function() windows[url] = null, false); + }, + // Returns a nsILocalFile representing a file relative to the // current user's profile directory. If the argument is a string, // it should be a string with unix-style slashes for directory names From 1506023bf79d14fc4889030736c28b0e6bf98cf0 Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Sun, 20 Sep 2009 19:26:01 -0700 Subject: [PATCH 1380/1860] implement forgot password/passphrase dialogs --- services/sync/modules/service.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 05773d2e9a47..77656ae9aae7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -155,7 +155,7 @@ StatusRecord.prototype = { resetEngineStatus: function() { this.engines = {}; }, - + _resetBackoff: function () { this.enforceBackoff = false; this.backoffInterval = 0; @@ -599,6 +599,8 @@ WeaveSvc.prototype = { /* Wipe */ this.wipeServer(); + PubKeys.clearCache(); + PrivKeys.clearCache(); /* Set remote commands before syncing */ Clients._store.clients = clientsBackup; @@ -645,7 +647,7 @@ WeaveSvc.prototype = { this._autoConnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this._autoConnectAttempts++; - let interval = this._calculateBackoff(this._autoConnectAttempts, + let interval = this._calculateBackoff(this._autoConnectAttempts, SCHEDULED_SYNC_INTERVAL); this._autoConnectTimer.initWithCallback(listener, interval, Ci.nsITimer.TYPE_ONE_SHOT); @@ -1202,8 +1204,8 @@ WeaveSvc.prototype = { if (!resp.success) throw resp; }, - - + + /** * Check to see if this is a failure * From fa8aa6d36a237d5b099eae113966c7590dee9dc8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 21 Sep 2009 17:13:41 -0700 Subject: [PATCH 1381/1860] Bug 518018 - Default to serverURL instead of userAPI on 404 cluster check Rework server/user/misc prefs to allow relative paths and full urls for generating API paths. Cache string properties of generated URLs under the storageAPI instead of using dynamic getters. --- services/sync/modules/engines.js | 21 ++------ services/sync/modules/service.js | 90 +++++++++++++++++--------------- services/sync/modules/util.js | 19 ------- services/sync/services-sync.js | 3 +- 4 files changed, 53 insertions(+), 80 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 0a3151347334..36f3209d30aa 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -296,25 +296,12 @@ SyncEngine.prototype = { this.loadToFetch(); }, - get baseURL() { - let url = Svc.Prefs.get("clusterURL"); - if (!url) - return null; - if (url[url.length-1] != '/') - url += '/'; - url += "0.5/"; - return url; - }, + get storageURL() Svc.Prefs.get("clusterURL") + "0.5/" + + ID.get("WeaveID").username + "/storage/", - get engineURL() { - return this.baseURL + ID.get('WeaveID').username + - '/storage/' + this.name + '/'; - }, + get engineURL() this.storageURL + this.name, - get cryptoMetaURL() { - return this.baseURL + ID.get('WeaveID').username + - '/storage/crypto/' + this.name; - }, + get cryptoMetaURL() this.storageURL + "crypto/" + this.name, get lastSync() { return parseFloat(Svc.Prefs.get(this.name + ".lastSync", "0")); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 77656ae9aae7..4fcf2bc8d1e5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -205,7 +205,7 @@ WeaveSvc.prototype = { ID.get('WeaveCryptoID').username = value; // FIXME: need to also call this whenever the username pref changes - this._genKeyURLs(); + this._updateCachedURLs(); }, get password password() ID.get("WeaveID").password, @@ -214,32 +214,30 @@ WeaveSvc.prototype = { get passphrase passphrase() ID.get("WeaveCryptoID").password, set passphrase passphrase(value) ID.get("WeaveCryptoID").password = value, - get baseURL() { - return Utils.getURLPref("serverURL"); - }, - set baseURL(value) { - Svc.Prefs.set("serverURL", value); - }, + get serverURL() Svc.Prefs.get("serverURL"), + set serverURL(value) Svc.Prefs.set("serverURL", value), - get miscURL() { - return Utils.getURLPref("miscURL"); - }, - set miscURL(value) { - Svc.Prefs.set("miscURL", value); - }, - - get clusterURL() { - return Utils.getURLPref("clusterURL", null, "0.5/"); - }, + get clusterURL() Svc.Prefs.get("clusterURL"), set clusterURL(value) { Svc.Prefs.set("clusterURL", value); - this._genKeyURLs(); + this._updateCachedURLs(); }, - get userURL() this.clusterURL + this.username, - get infoURL() this.userURL + "/info/collections", + get miscAPI() { + // Append to the serverURL if it's a relative fragment + let misc = Svc.Prefs.get("miscURL"); + if (misc.indexOf(":") == -1) + misc = this.serverURL + misc; + return misc + "1/"; + }, - get userPath() { return ID.get('WeaveID').username; }, + get userAPI() { + // Append to the serverURL if it's a relative fragment + let user = Svc.Prefs.get("userURL"); + if (user.indexOf(":") == -1) + user = this.serverURL + user; + return user + "1/"; + }, get isLoggedIn() { return this._loggedIn; }, @@ -265,10 +263,17 @@ WeaveSvc.prototype = { this._locked = false; }, - _genKeyURLs: function WeaveSvc__genKeyURLs() { - let url = this.userURL; - PubKeys.defaultKeyUri = url + "/storage/keys/pubkey"; - PrivKeys.defaultKeyUri = url + "/storage/keys/privkey"; + _updateCachedURLs: function _updateCachedURLs() { + let storageAPI = this.clusterURL + "0.5/"; + let userBase = storageAPI + this.username + "/"; + this._log.debug("Caching URLs under storage user base: " + userBase); + + // Generate and cache various URLs under the storage API for this user + this.infoURL = userBase + "info/collections"; + this.storageURL = userBase + "storage/"; + this.metaURL = this.storageURL + "meta/global"; + PubKeys.defaultKeyUri = this.storageURL + "keys/pubkey"; + PrivKeys.defaultKeyUri = this.storageURL + "keys/privkey"; }, _checkCrypto: function WeaveSvc__checkCrypto() { @@ -332,7 +337,7 @@ WeaveSvc.prototype = { ID.set('WeaveCryptoID', new Identity('Mozilla Services Encryption Passphrase', this.username)); - this._genKeyURLs(); + this._updateCachedURLs(); if (Svc.Prefs.get("autoconnect")) this._autoConnect(); @@ -457,13 +462,13 @@ WeaveSvc.prototype = { _findCluster: function _findCluster() { this._log.debug("Finding cluster for user " + this.username); - let res = new Resource(this.baseURL + "1/" + this.username + "/node/weave"); + let res = new Resource(this.userAPI + this.username + "/node/weave"); try { let node = res.get(); switch (node.status) { case 404: this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); - return this.baseURL; + return this.serverURL; case 0: case 200: return node; @@ -480,18 +485,17 @@ WeaveSvc.prototype = { // gets cluster from central LDAP server and sets this.clusterURL _setCluster: function _setCluster() { + // Make sure we didn't get some unexpected response for the cluster let cluster = this._findCluster(); - if (cluster) { - if (cluster == this.clusterURL) - return false; + if (cluster == null) + return false; - this._log.debug("Saving cluster setting"); - this.clusterURL = cluster; - return true; - } + // Don't update stuff if we already have the right cluster + if (cluster == this.clusterURL) + return false; - this._log.debug("Error setting cluster for user " + this.username); - return false; + this.clusterURL = cluster; + return true; }, // update cluster if required. returns false if the update was not required @@ -579,7 +583,7 @@ WeaveSvc.prototype = { changePassword: function WeaveSvc_changePassword(newpass) this._catch(this._notify("changepwd", "", function() { - let url = this.baseURL + '1/' + this.username + "/password"; + let url = this.userAPI + this.username + "/password"; let res = new Weave.Resource(url); let resp = res.post(newpass); if (resp.status != 200) { @@ -740,7 +744,7 @@ WeaveSvc.prototype = { }, checkUsername: function WeaveSvc_checkUsername(username) { - let url = this.baseURL + "1/" + username; + let url = this.userAPI + username; let res = new Resource(url); res.authenticator = new NoOpAuthenticator(); @@ -765,7 +769,7 @@ WeaveSvc.prototype = { "captcha-response": captchaResponse }); - let url = this.baseURL + '1/' + username; + let url = this.userAPI + username; let res = new Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); @@ -794,7 +798,7 @@ WeaveSvc.prototype = { let reset = false; this._log.debug("Fetching global metadata record"); - let meta = Records.import(this.userURL + "/storage/meta/global"); + let meta = Records.import(this.metaURL); let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; @@ -1188,7 +1192,7 @@ WeaveSvc.prototype = { Sync.sleep(2000); this._log.debug("Uploading new metadata record"); - meta = new WBORecord(this.userURL + "/storage/meta/global"); + meta = new WBORecord(this.metaURL); meta.payload.syncID = Clients.syncID; this._updateRemoteVersion(meta); }, @@ -1245,7 +1249,7 @@ WeaveSvc.prototype = { if (engines && engines.indexOf(name) == -1) continue; - new Resource(this.userURL + "/storage/" + name).delete(); + new Resource(this.storageURL + name).delete(); } catch(ex) { this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 42a8c603a131..1a5513b89041 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -490,25 +490,6 @@ let Utils = { return url; }, - // ensures url ends with a slash, optionally adds an extra string at the end - slashify: function Weave_slashify(url, extra) { - if (url[url.length-1] != '/') - url += '/'; - if (extra) - url += extra; - return url; - }, - - // - getURLPref: function Weave_getURLPref(pref, def, extra) { - let url = Svc.Prefs.get(pref); - if (!url && typeof(def) == "undefined") - throw pref + " not set"; - else if (!url) - return def; - return Utils.slashify(url, extra); - }, - xpath: function Weave_xpath(xmlDoc, xpathString) { let root = xmlDoc.ownerDocument == null ? xmlDoc.documentElement : xmlDoc.ownerDocument.documentElement; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 5cebf5661972..9e9689edd63a 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,5 +1,6 @@ pref("extensions.weave.serverURL", "@server_url@"); -pref("extensions.weave.miscURL", "https://auth.services.mozilla.com/misc/"); +pref("extensions.weave.userURL", "user/"); +pref("extensions.weave.miscURL", "misc/"); pref("extensions.weave.termsURL", "https://labs.mozilla.com/projects/weave/tos/"); pref("extensions.weave.encryption", "aes-256-cbc"); From 8d29c3ad391b07a5ee833fe22af970c98b666f81 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 21 Sep 2009 17:34:19 -0700 Subject: [PATCH 1382/1860] Bug 518022 - Logging in with invalid username results in "Couldn't sign in: Network error" Report 404 info/collections responses as "login rejected" if we're on the right cluster. --- services/sync/modules/service.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4fcf2bc8d1e5..c2cebb00da14 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -526,9 +526,12 @@ WeaveSvc.prototype = { this.status.setLoginStatus(LOGIN_SUCCEEDED); return true; case 401: + case 404: + // Check that we're verifying with the correct cluster if (this._updateCluster()) return this._verifyLogin(); + // We must have the right cluster, but the server doesn't expect us this.status.setLoginStatus(LOGIN_FAILED_LOGIN_REJECTED); this._log.debug("verifyLogin failed: login failed") return false; From 2fce9732170c2d754234b00a41440fd15218d548 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 21 Sep 2009 18:03:56 -0700 Subject: [PATCH 1383/1860] Don't update the cluster on each login unless verify login failed or there was no cluster set. --- services/sync/modules/service.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c2cebb00da14..faad0806aebb 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -217,7 +217,7 @@ WeaveSvc.prototype = { get serverURL() Svc.Prefs.get("serverURL"), set serverURL(value) Svc.Prefs.set("serverURL", value), - get clusterURL() Svc.Prefs.get("clusterURL"), + get clusterURL() Svc.Prefs.get("clusterURL", ""), set clusterURL(value) { Svc.Prefs.set("clusterURL", value); this._updateCachedURLs(); @@ -264,6 +264,10 @@ WeaveSvc.prototype = { }, _updateCachedURLs: function _updateCachedURLs() { + // Nothing to cache yet if we don't have the building blocks + if (this.clusterURL == "" || this.username == "") + return; + let storageAPI = this.clusterURL + "0.5/"; let userBase = storageAPI + this.username + "/"; this._log.debug("Caching URLs under storage user base: " + userBase); @@ -513,7 +517,10 @@ WeaveSvc.prototype = { _verifyLogin: function _verifyLogin() this._catch(this._notify("verify-login", "", function() { - this._setCluster(); + // Make sure we have a cluster to verify against + if (this.clusterURL == "") + this._setCluster(); + let res = new Resource(this.infoURL); try { let test = res.get(); @@ -528,7 +535,7 @@ WeaveSvc.prototype = { case 401: case 404: // Check that we're verifying with the correct cluster - if (this._updateCluster()) + if (this._setCluster()) return this._verifyLogin(); // We must have the right cluster, but the server doesn't expect us From 361104833b36ec9090baf7fff6dfbbf15ed356cd Mon Sep 17 00:00:00 2001 From: Dan Mills Date: Mon, 21 Sep 2009 23:52:00 -0700 Subject: [PATCH 1384/1860] Bug 518069: forgot password UI / bug 518067: directional sync dialog during setup --- services/sync/modules/service.js | 10 ++++++++++ services/sync/services-sync.js | 1 + 2 files changed, 11 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index faad0806aebb..dcddec710924 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -631,6 +631,16 @@ WeaveSvc.prototype = { return true; }))(), + requestPasswordReset: function WeaveSvc_requestPasswordReset(username) { + let res = new Resource(Utils.getURLPref("pwChangeURL")); + res.authenticator = new NoOpAuthenticator(); + res.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + let ret = res.post('uid=' + username); + if (ret.indexOf("Further instructions have been sent") >= 0) + return true; + return false; + }, + _autoConnectAttempts: 0, _autoConnect: function WeaveSvc__attemptAutoConnect() { try { diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9e9689edd63a..34f8492fcb06 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,6 +1,7 @@ pref("extensions.weave.serverURL", "@server_url@"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); +pref("extensions.weave.pwChangeURL", "https://services.mozilla.com/pw/forgot.php"); pref("extensions.weave.termsURL", "https://labs.mozilla.com/projects/weave/tos/"); pref("extensions.weave.encryption", "aes-256-cbc"); From 01786dd8b6e830c6c46e818e7b0628ef36f7690e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Sep 2009 01:00:43 -0700 Subject: [PATCH 1385/1860] Bug 518077 - Add custom settings for "sign in" to set things like server url Provide an initial implementation to read and set preferences: autoconnect, serverURL. For now show them in a "Custom settings" expando tab like the one for create account, but the styling isn't great. --- services/sync/modules/service.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index dcddec710924..82579749361c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -215,7 +215,15 @@ WeaveSvc.prototype = { set passphrase passphrase(value) ID.get("WeaveCryptoID").password = value, get serverURL() Svc.Prefs.get("serverURL"), - set serverURL(value) Svc.Prefs.set("serverURL", value), + set serverURL(value) { + // Only do work if it's actually changing + if (value == this.serverURL) + return; + + // A new server most likely uses a different cluster, so clear that + Svc.Prefs.set("serverURL", value); + Svc.Prefs.reset("clusterURL"); + }, get clusterURL() Svc.Prefs.get("clusterURL", ""), set clusterURL(value) { From 41e836f694fc00ea1f0d60f025fabc27cb508788 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 24 Sep 2009 14:31:05 -0700 Subject: [PATCH 1386/1860] Bug 506268 - Session restore fails halfway through, leaves tabs unloaded Load Weave off of the event that triggered sessionstore so that it and its callers can finish loading before autoconnect does network activity. --- services/sync/Weave.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 9c626e07aa54..4a513d3d68cb 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -62,7 +62,7 @@ WeaveService.prototype = { */ case "sessionstore-windows-restored": Cu.import("resource://weave/service.js"); - Weave.Service.onStartup(); + Weave.Utils.makeTimerForCall(function() Weave.Service.onStartup()); break; } } From b3f6f01c45c77eac35ef158506d837c4a3b90030 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 24 Sep 2009 15:54:47 -0700 Subject: [PATCH 1387/1860] Bug 518146 - Generate shorter GUIDs than nsIUUIDGenerator Randomly generate 10 characters from a pool of 70 possible characters for an equivalent 61.29-bit GUID which should have around 1% chance of collision (within a collection) after ~100 million records. --- services/sync/modules/util.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 1a5513b89041..f5b1618bd290 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -149,9 +149,28 @@ let Utils = { // Generates a brand-new globally unique identifier (GUID). makeGUID: function makeGUID() { - let uuidgen = Cc["@mozilla.org/uuid-generator;1"]. - getService(Ci.nsIUUIDGenerator); - return uuidgen.generateUUID().toString().replace(/[{}]/g, ''); + // 70 characters that are not-escaped URL-friendly + const code = + "!()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"; + + let guid = ""; + let num = 0; + let val; + + // Generate ten 70-value characters for a 70^10 (~61.29-bit) GUID + for (let i = 0; i < 10; i++) { + // Refresh the number source after using it a few times + if (i == 0 || i == 5) + num = Math.random(); + + // Figure out which code to use for the next GUID character + num *= 70; + val = Math.floor(num); + guid += code[val]; + num -= val; + } + + return guid; }, anno: function anno(id, anno, val, expire) { From a0d81914dc1a2e45824051d5ec551ecaa18f9deb Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 24 Sep 2009 19:04:06 -0700 Subject: [PATCH 1388/1860] Correctly cancel any sync triggers after starting a sync with a shared function that removes timers and idle observers. --- services/sync/modules/service.js | 33 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 82579749361c..52e7ca9decc7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -985,6 +985,23 @@ WeaveSvc.prototype = { return reason; }, + /** + * Remove any timers/observers that might trigger a sync + */ + _clearSyncTriggers: function _clearSyncTriggers() { + // Clear out any scheduled syncs + if (this._syncTimer) { + this._syncTimer.cancel(); + this._syncTimer = null; + } + + // Clear out a sync that's just waiting for idle if we happen to have one + try { + Svc.Idle.removeIdleObserver(this, IDLE_TIME); + } + catch(ex) {} + }, + /** * Check if we should be syncing and schedule the next sync, if it's not scheduled */ @@ -993,15 +1010,7 @@ WeaveSvc.prototype = { // if we're in backoff, we'll schedule the next sync let reason = this._checkSync(); if (reason && reason != kSyncBackoffNotMet) { - if (this._syncTimer) { - this._syncTimer.cancel(); - this._syncTimer = null; - } - - try { - Svc.Idle.removeIdleObserver(this, IDLE_TIME); - } catch(e) {} // this throws if there isn't an observer, but that's fine - + this._clearSyncTriggers(); this.status.service = STATUS_DISABLED; return; } @@ -1093,10 +1102,8 @@ WeaveSvc.prototype = { throw reason; } - if (this._autoConnectTimer) { - this._autoConnectTimer.cancel(); - this._autoConnectTimer = null; - } + // Clear out any potentially pending syncs now that we're syncing + this._clearSyncTriggers(); if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; From f245b8664f648059072b44e16178342005a20d2f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 24 Sep 2009 22:51:38 -0700 Subject: [PATCH 1389/1860] Add a simpler-to-use nsITimer wrapper, Utils.delay, that can use a named property of an object to additionally delay or clear the timer. Get rid of Utils.EventListener and simplify/fix up users. --- services/sync/Weave.js | 2 +- services/sync/modules/service.js | 49 +++++---------------- services/sync/modules/util.js | 74 ++++++++++++++++---------------- 3 files changed, 50 insertions(+), 75 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 4a513d3d68cb..a2b7caef75d7 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -62,7 +62,7 @@ WeaveService.prototype = { */ case "sessionstore-windows-restored": Cu.import("resource://weave/service.js"); - Weave.Utils.makeTimerForCall(function() Weave.Service.onStartup()); + Weave.Utils.delay(function() Weave.Service.onStartup()); break; } } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 52e7ca9decc7..ddb7a7199b0d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -188,9 +188,6 @@ WeaveSvc.prototype = { // object for caching public and private keys _keyPair: {}, - // Timer object for automagically syncing - _syncTimer: null, - get username() { return Svc.Prefs.get("username", ""); }, @@ -669,22 +666,12 @@ WeaveSvc.prototype = { failureReason = ex; } - this._log.debug("Autoconnect failed: " + failureReason); - - let listener = new Utils.EventListener(Utils.bind2(this, - function WeaveSvc__autoConnectCallback(timer) { - this._autoConnectTimer = null; - this._autoConnect(); - })); - this._autoConnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._autoConnectAttempts++; let interval = this._calculateBackoff(this._autoConnectAttempts, SCHEDULED_SYNC_INTERVAL); - this._autoConnectTimer.initWithCallback(listener, interval, - Ci.nsITimer.TYPE_ONE_SHOT); - this._log.debug("Scheduling next autoconnect attempt in " + - interval / 1000 + " seconds."); + this._log.debug("Autoconnect failed: " + failureReason + "; retrying in " + + Math.ceil(interval / 1000) + " sec."); + Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer"); }, persistLogin: function persistLogin() { @@ -724,13 +711,13 @@ WeaveSvc.prototype = { throw "Login failed: " + this.status.login; } + // No need to try automatically connecting after a successful login + if (this._autoTimer) + this._autoTimer.clear(); + this._loggedIn = true; // Try starting the sync timer now that we're logged in this._checkSyncStatus(); - if (this._autoConnectTimer) { - this._autoConnectTimer.cancel(); - this._autoConnectTimer = null; - } return true; })))(), @@ -990,10 +977,8 @@ WeaveSvc.prototype = { */ _clearSyncTriggers: function _clearSyncTriggers() { // Clear out any scheduled syncs - if (this._syncTimer) { - this._syncTimer.cancel(); - this._syncTimer = null; - } + if (this._syncTimer) + this._syncTimer.clear(); // Clear out a sync that's just waiting for idle if we happen to have one try { @@ -1036,20 +1021,8 @@ WeaveSvc.prototype = { if (!interval) interval = this.status.backoffInterval || SCHEDULED_SYNC_INTERVAL; - // if there's an existing timer, cancel it and restart - if (this._syncTimer) - this._syncTimer.cancel(); - else - this._syncTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - - let listener = new Utils.EventListener(Utils.bind2(this, - function WeaveSvc__scheduleNextSyncCallback(timer) { - this._syncTimer = null; - this.syncOnIdle(); - })); - this._syncTimer.initWithCallback(listener, interval, - Ci.nsITimer.TYPE_ONE_SHOT); - this._log.debug("Next sync call in: " + this._syncTimer.delay / 1000 + " seconds.") + this._log.debug("Next sync in " + Math.ceil(interval / 1000) + " sec."); + Utils.delay(function() this.syncOnIdle(), interval, this, "_syncTimer"); }, _syncErrors: 0, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index f5b1618bd290..8023dc648b38 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -588,13 +588,44 @@ let Utils = { fos.close(); }, - // Returns a timer that is scheduled to call the given callback as - // soon as possible. - makeTimerForCall: function makeTimerForCall(cb) { - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(new Utils.EventListener(cb), - 0, timer.TYPE_ONE_SHOT); - return timer; + /** + * Return a timer that is scheduled to call the callback after waiting the + * provided time or as soon as possible. The timer will be set as a property + * of the provided object with the given timer name. + */ + delay: function delay(callback, wait, thisObj, name) { + // Default to running right away + wait = wait || 0; + + // Use a dummy object if one wasn't provided + thisObj = thisObj || {}; + + // Delay an existing timer if it exists + if (thisObj[name] instanceof Ci.nsITimer) { + thisObj[name].delay = wait; + return; + } + + // Create a special timer that we can add extra properties + let timer = {}; + timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + // Provide an easy way to clear out the timer + timer.clear = function() { + thisObj[name] = null; + timer.cancel(); + }; + + // Initialize the timer with a smart callback + timer.initWithCallback({ + notify: function notify() { + // Clear out the timer once it's been triggered + timer.clear(); + callback.call(thisObj, timer); + } + }, wait, timer.TYPE_ONE_SHOT); + + return thisObj[name] = timer; }, open: function open(pathOrFile, mode, perms) { @@ -742,35 +773,6 @@ let Utils = { this.__prefs.QueryInterface(Ci.nsIPrefBranch2); } return this.__prefs; - }, - - /* - * Event listener object - * Used to handle XMLHttpRequest and nsITimer callbacks - */ - - EventListener: function Weave_EventListener(handler, eventName) { - this._handler = handler; - this._eventName = eventName; - this._log = Log4Moz.repository.getLogger("Async.EventHandler"); - this._log.level = - Log4Moz.Level[Utils.prefs.getCharPref("log.logger.async")]; - } -}; - -Utils.EventListener.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsISupports]), - - // DOM event listener - handleEvent: function EL_handleEvent(event) { - this._log.trace("Handling event " + this._eventName); - this._handler(event); - }, - - // nsITimerCallback - notify: function EL_notify(timer) { - //this._log.trace("Timer fired"); - this._handler(timer); } }; From b0ad85f8965d9d7b4d101742174d758d305f500a Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 24 Sep 2009 23:16:27 -0700 Subject: [PATCH 1390/1860] Land some initial statusbar UI bits of bug 513944 and remove unused/debug code. --- services/sync/modules/constants.js | 1 + services/sync/services-sync.js | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 3620b3bd11d0..00a5edbad93b 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -70,6 +70,7 @@ SYNC_FAILED: "error.sync.failed", LOGIN_FAILED: "error.login.failed", SYNC_FAILED_PARTIAL: "error.sync.failed_partial", STATUS_DISABLED: "service.disabled", +STATUS_DELAYED: "service.startup.delayed", // success states LOGIN_SUCCEEDED: "success.login", diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 34f8492fcb06..fbe2b7197707 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -8,8 +8,6 @@ pref("extensions.weave.encryption", "aes-256-cbc"); pref("extensions.weave.lastversion", "firstrun"); -pref("extensions.weave.ui.syncnow", true); -pref("extensions.weave.ui.sharebookmarks", false); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); From 9fa890a8da465b35b4419cfb1965f367f1126975 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 08:13:12 -0700 Subject: [PATCH 1391/1860] Bug 513944 - Weave should not load / do anything until it absolutely needs to Weave already triggers on a late notification and puts itself on the event loop, so just additionally delay startup based on the number of open tabs (which will all be busy at startup). --- services/sync/Weave.js | 2 +- services/sync/modules/service.js | 30 ++++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index a2b7caef75d7..9c626e07aa54 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -62,7 +62,7 @@ WeaveService.prototype = { */ case "sessionstore-windows-restored": Cu.import("resource://weave/service.js"); - Weave.Utils.delay(function() Weave.Service.onStartup()); + Weave.Service.onStartup(); break; } } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ddb7a7199b0d..8ca995d0a2d7 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -303,14 +303,36 @@ WeaveSvc.prototype = { onWindowOpened: function WeaveSvc__onWindowOpened() { }, + /** + * Prepare to initialize the rest of Weave after waiting a little bit + */ + onStartup: function onStartup() { + this._status = new StatusRecord(); + this.status.service = STATUS_DELAYED; + + // Figure out how many seconds to delay loading Weave based on the app + let wait = 0; + switch (Svc.AppInfo.ID) { + case FIREFOX_ID: + // Add one second delay for each tab in every window + let enum = Svc.WinMediator.getEnumerator("navigator:browser"); + while (enum.hasMoreElements()) + wait += enum.getNext().gBrowser.mTabs.length; + } + + // Make sure we wait a little but but not too long in the worst case + wait = Math.ceil(Math.max(5, Math.min(20, wait))); + + this._initLogs(); + this._log.info("Loading Weave " + WEAVE_VERSION + " in " + wait + " sec."); + Utils.delay(this._onStartup, wait * 1000, this, "_startupTimer"); + }, + // one-time initialization like setting up observers and the like // xxx we might need to split some of this out into something we can call // again when username/server/etc changes - onStartup: function WeaveSvc_onStartup() { - this._initLogs(); - this._log.info("Weave " + WEAVE_VERSION + " initializing"); + _onStartup: function _onStartup() { this._registerEngines(); - this._status = new StatusRecord(); // Reset our sync id if we're upgrading, so sync knows to reset local data if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) { From 539832c9961d22725712347d9fc27dfab019938b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 09:09:05 -0700 Subject: [PATCH 1392/1860] Bug 512809 - signup form uses colors for input validation feedback. r=Mardak Add initial error reporting and "available" tagging. Also add "start over" functionality and change some custom settings display. --- services/sync/locales/en-US/errors.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties index d6624cfc8781..6eb92829cfc1 100644 --- a/services/sync/locales/en-US/errors.properties +++ b/services/sync/locales/en-US/errors.properties @@ -1,4 +1,4 @@ -error.login.reason.password = Your username/password failed +error.login.reason.password = Incorrect username or password error.login.reason.unknown = Unknown error error.login.reason.passphrase = Invalid passphrase error.login.reason.network = Network error From d8f85b5d774ea41c2dd767c09d1401c4c52b12d6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 09:11:19 -0700 Subject: [PATCH 1393/1860] Wire up "custom" inputs to set/read and show/hide server url box. --- services/sync/modules/constants.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 00a5edbad93b..83ad16c2b3fd 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -43,6 +43,8 @@ WEAVE_VERSION: "@weave_version@", // version, this client will wipe the data on the server first. COMPATIBLE_VERSION: "@compatible_version@", +DEFAULT_SERVER: "@server_url@", + PREFS_BRANCH: "extensions.weave.", // Host "key" to access Weave Identity in the password manager From 3ee192971c47881fd2857d74a27a198e117c08c9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 11:46:29 -0700 Subject: [PATCH 1394/1860] Bug 518864 - Persist "next sync time" across events that disable sync Save a nextSync value in a pref and use it to trigger a sync-on-idle if the "next sync time" already passed when logging in. Make sure to default to backoff time first, then next sync, then use the default. --- services/sync/modules/service.js | 60 +++++++++++++++++++------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8ca995d0a2d7..7f51624377d1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -255,6 +255,10 @@ WeaveSvc.prototype = { get enabled() { return Svc.Prefs.get("enabled"); }, set enabled(value) { Svc.Prefs.set("enabled", value); }, + // nextSync is in milliseconds, but prefs can't hold that much + get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, + set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), + get status() { return this._status; }, get locked() { return this._locked; }, @@ -668,30 +672,18 @@ WeaveSvc.prototype = { return false; }, - _autoConnectAttempts: 0, - _autoConnect: function WeaveSvc__attemptAutoConnect() { - try { - if (!this.username || !this.password || !this.passphrase) - return; + _autoConnect: let (attempts = 0) function _autoConnect() { + // Can't autoconnect if we're missing these values + if (!this.username || !this.password || !this.passphrase) + return; - let failureReason; - if (Svc.IO.offline) - failureReason = "Application is offline"; - else if (this.login()) { - this.syncOnIdle(); - return; - } + // Nothing more to do on a successful login + if (this.login()) + return; - failureReason = this.status.sync; - } - catch (ex) { - failureReason = ex; - } - - this._autoConnectAttempts++; - let interval = this._calculateBackoff(this._autoConnectAttempts, - SCHEDULED_SYNC_INTERVAL); - this._log.debug("Autoconnect failed: " + failureReason + "; retrying in " + + // Something failed, so try again some time later + let interval = this._calculateBackoff(++attempts, 60 * 1000); + this._log.debug("Autoconnect failed: " + this.status.login + "; retry in " + Math.ceil(interval / 1000) + " sec."); Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer"); }, @@ -1040,11 +1032,30 @@ WeaveSvc.prototype = { * Set a timer for the next sync */ _scheduleNextSync: function WeaveSvc__scheduleNextSync(interval) { - if (!interval) - interval = this.status.backoffInterval || SCHEDULED_SYNC_INTERVAL; + // Figure out when to sync next if not given a interval to wait + if (interval == null) { + // Make sure we backoff we we need to + if (this.status.backoffInterval != 0) + interval = this.status.backoffInterval; + // Check if we had a pending sync from last time + else if (this.nextSync != 0) + interval = this.nextSync - Date.now(); + // Use the default sync interval + else + interval = SCHEDULED_SYNC_INTERVAL; + } + + // Start the sync right away if we're already late + if (interval <= 0) { + this.syncOnIdle(); + return; + } this._log.debug("Next sync in " + Math.ceil(interval / 1000) + " sec."); Utils.delay(function() this.syncOnIdle(), interval, this, "_syncTimer"); + + // Save the next sync time in-case sync is disabled (logout/offline/etc.) + this.nextSync = Date.now() + interval; }, _syncErrors: 0, @@ -1099,6 +1110,7 @@ WeaveSvc.prototype = { // Clear out any potentially pending syncs now that we're syncing this._clearSyncTriggers(); + this.nextSync = 0; if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; From f78f682675955766159a4d5690f130942dbdba65 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 15:05:09 -0700 Subject: [PATCH 1395/1860] Bug 517597 - waste fewer resources while a user only has one client active Adjust how often Weave syncs based on the number of clients and only sync tabs if there are multiple clients. --- services/sync/modules/constants.js | 5 +++ services/sync/modules/service.js | 61 ++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 83ad16c2b3fd..9c4acc325b2d 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -50,6 +50,11 @@ PREFS_BRANCH: "extensions.weave.", // Host "key" to access Weave Identity in the password manager PWDMGR_HOST: "chrome://weave", +// Sync intervals for various clients configurations +SINGLE_USER_SYNC: 24 * 60 * 60 * 1000, // 1 day +MULTI_DESKTOP_SYNC: 60 * 60 * 1000, // 1 hour +MULTI_MOBILE_SYNC: 5 * 60 * 1000, // 5 minutes + // File IO Flags MODE_RDONLY: 0x01, MODE_WRONLY: 0x02, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7f51624377d1..b4babc6dcf43 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -43,19 +43,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -// The following constants determine when Weave will automatically sync data. - -// An interval of one minute, initial threshold of 100, and step of 5 means -// that we'll try to sync each engine 21 times, once per minute, at -// consecutively lower thresholds (from 100 down to 5 in steps of 5 and then -// one more time with the threshold set to the minimum 1) before resetting -// the engine's threshold to the initial value and repeating the cycle -// until at some point the engine's score exceeds the threshold, at which point -// we'll sync it, reset its threshold to the initial value, rinse, and repeat. - -// How long we wait between sync checks. -const SCHEDULED_SYNC_INTERVAL = 60 * 1000 * 5; // five minutes - // how long we should wait before actually syncing on idle const IDLE_TIME = 5; // xxxmpc: in seconds, should be preffable @@ -69,7 +56,7 @@ const INITIAL_THRESHOLD = 75; const THRESHOLD_DECREMENT_STEP = 25; // How long before refreshing the cluster -const CLUSTER_BACKOFF = SCHEDULED_SYNC_INTERVAL; +const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Sync.js"); @@ -181,6 +168,7 @@ WeaveSvc.prototype = { _loggedIn: false, _syncInProgress: false, _keyGenEnabled: true, + _syncInterval: SINGLE_USER_SYNC, // the status object _status: null, @@ -259,6 +247,9 @@ WeaveSvc.prototype = { get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), + get numClients() Svc.Prefs.get("numClients", 0), + set numClients(value) Svc.Prefs.set("numClients", value), + get status() { return this._status; }, get locked() { return this._locked; }, @@ -1042,7 +1033,7 @@ WeaveSvc.prototype = { interval = this.nextSync - Date.now(); // Use the default sync interval else - interval = SCHEDULED_SYNC_INTERVAL; + interval = this._syncInterval; } // Start the sync right away if we're already late @@ -1145,6 +1136,9 @@ WeaveSvc.prototype = { } } + // Update the client mode now because it might change what we sync + this._updateClientMode(); + try { for each (let engine in Engines.getAll()) { let name = engine.name; @@ -1203,6 +1197,43 @@ WeaveSvc.prototype = { } })))(), + /** + * Process the locally stored clients list to figure out what mode to be in + */ + _updateClientMode: function _updateClientMode() { + let numClients = 0; + let hasMobile = false; + + // Check how many and what type of clients we have + for each (let {type} in Clients.getClients()) { + numClients++; + hasMobile = hasMobile || type == "mobile"; + } + + // Nothing to do if it's the same amount + if (this.numClients == numClients) + return; + + this._log.debug("Client count: " + this.numClients + " -> " + numClients); + this.numClients = numClients; + + let tabEngine = Engines.get("tabs"); + if (numClients == 1) { + this._syncInterval = SINGLE_USER_SYNC; + + // Disable tabs sync for single client, but store the original value + Svc.Prefs.set("engine.tabs.backup", tabEngine.enabled); + tabEngine.enabled = false; + } + else { + this._syncInterval = hasMobile ? MULTI_MOBILE_SYNC : MULTI_DESKTOP_SYNC; + + // Restore the original tab enabled value + tabEngine.enabled = Svc.Prefs.get("engine.tabs.backup", true); + Svc.Prefs.reset("engine.tabs.backup"); + } + }, + // returns true if sync should proceed // false / no return value means sync should be aborted _syncEngine: function WeaveSvc__syncEngine(engine) { From b09e036d1a3118d6a493f24264263f8b8674a9dc Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 15:30:26 -0700 Subject: [PATCH 1396/1860] Bug 514601 - Don't send DELETE for old history/form data on every sync Don't send any deletes as the server will do the delete with bug 518945. --- services/sync/modules/engines/forms.js | 3 --- services/sync/modules/engines/history.js | 6 ------ 2 files changed, 9 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index ba3c9de6f0d2..ab4faa22c944 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -71,9 +71,6 @@ FormEngine.prototype = { /* Wipe cache when sync finishes */ _syncFinish: function FormEngine__syncFinish(error) { this._store.clearFormCache(); - - // Only leave 1 month's worth of form history - this._delete.older = this.lastSync - 2592000; // 60*60*24*30 SyncEngine.prototype._syncFinish.call(this); }, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index b38c08138565..237aa14d3be1 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -85,12 +85,6 @@ HistoryEngine.prototype = { _sync: Utils.batchSync("History", SyncEngine), - _syncFinish: function HistEngine__syncFinish(error) { - // Only leave 1 week's worth of history on the server - this._delete.older = this.lastSync - 604800; // 60*60*24*7 - SyncEngine.prototype._syncFinish.call(this); - }, - _findDupe: function _findDupe(item) { return GUIDForUri(item.histUri); } From 21a7720f80c502af302266a040ebee141b355aef Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 15:41:27 -0700 Subject: [PATCH 1397/1860] Persist sync intervals because we only update it when changing the number of clients. --- services/sync/modules/service.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b4babc6dcf43..d1d5de478d36 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -168,7 +168,6 @@ WeaveSvc.prototype = { _loggedIn: false, _syncInProgress: false, _keyGenEnabled: true, - _syncInterval: SINGLE_USER_SYNC, // the status object _status: null, @@ -247,6 +246,9 @@ WeaveSvc.prototype = { get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), + get syncInterval() Svc.Prefs.get("syncInterval", SINGLE_USER_SYNC), + set syncInterval(value) Svc.Prefs.set("syncInterval", value), + get numClients() Svc.Prefs.get("numClients", 0), set numClients(value) Svc.Prefs.set("numClients", value), @@ -1033,7 +1035,7 @@ WeaveSvc.prototype = { interval = this.nextSync - Date.now(); // Use the default sync interval else - interval = this._syncInterval; + interval = this.syncInterval; } // Start the sync right away if we're already late @@ -1219,14 +1221,14 @@ WeaveSvc.prototype = { let tabEngine = Engines.get("tabs"); if (numClients == 1) { - this._syncInterval = SINGLE_USER_SYNC; + this.syncInterval = SINGLE_USER_SYNC; // Disable tabs sync for single client, but store the original value Svc.Prefs.set("engine.tabs.backup", tabEngine.enabled); tabEngine.enabled = false; } else { - this._syncInterval = hasMobile ? MULTI_MOBILE_SYNC : MULTI_DESKTOP_SYNC; + this.syncInterval = hasMobile ? MULTI_MOBILE_SYNC : MULTI_DESKTOP_SYNC; // Restore the original tab enabled value tabEngine.enabled = Svc.Prefs.get("engine.tabs.backup", true); From 910d0db8c98d2ae8a16a00424c0aafab51b69220 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 16:14:26 -0700 Subject: [PATCH 1398/1860] Bug 518958 - Only sync tabs if there has been tab activity Check the score, which gets bumped up on tab open/close/select, to decide if tabs should be marked as changed. --- services/sync/modules/engines/tabs.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index eee01bac49b8..0c3fc836f29a 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -445,20 +445,10 @@ TabTracker.prototype = { // TODO: Also listen for tabs loading new content? get changedIDs() { - // The record for my own client is always the only changed record. + // Only mark the current client as changed if we tracked changes let obj = {}; - obj[Clients.clientID] = true; + if (this._score > 0) + obj[Clients.clientID] = true; return obj; - }, - - /* Score is pegged to 100, which means tabs are always synced. - * Is this the right thing to do? Or should we be using the score - * calculated from tab open/close/select events (see above)? Note that - * we should definitely listen for tabs loading new content if we want to - * go that way. But tabs loading new content happens so often that it - * might be easier to just always sync. - */ - get score() { - return 100; } } From ca58b06596624d8ba2a4d02925018d8cac6006e3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 16:52:12 -0700 Subject: [PATCH 1399/1860] Bug 518972 - Only upload history records that have more than one visit Allow each engine to provide a custom Collection object and have History provide a collection that filters out certain data. This is inefficient because we have to first create then encrypt the record before we can filter it out. --- services/sync/modules/engines.js | 6 +++++- services/sync/modules/engines/history.js | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 36f3209d30aa..8fd29ae0cdf7 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -331,6 +331,10 @@ SyncEngine.prototype = { this._toFetch = o)); }, + _makeUploadColl: function _makeUploadColl() { + return new Collection(this.engineURL); + }, + // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { return this._store.createRecord(id, this.cryptoMetaURL); @@ -580,7 +584,7 @@ SyncEngine.prototype = { this._log.debug("Preparing " + outnum + " outgoing records"); // collection we'll upload - let up = new Collection(this.engineURL); + let up = this._makeUploadColl(); let count = 0; // Upload what we've got so far in the collection diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 237aa14d3be1..ef7ddc740a8e 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -87,6 +87,19 @@ HistoryEngine.prototype = { _findDupe: function _findDupe(item) { return GUIDForUri(item.histUri); + }, + + _makeUploadColl: function _makeUploadColl() { + let coll = SyncEngine.prototype._makeUploadColl.call(this); + let origPush = coll.pushData; + + // Only push the data for upload if it has more than 1 visit + coll.pushData = function(data) { + if (data._visitCount > 1) + origPush.call(coll, data); + }; + + return coll; } }; @@ -255,6 +268,10 @@ HistoryStore.prototype = { record.title = foo.title; record.visits = this._getVisits(record.histUri); record.encryption = cryptoMetaURL; + + // XXX Add a value to the object so that pushData can read out, but we end + // up encrypting the data before calling pushData :( + record._visitCount = record.visits.length; } else record.deleted = true; From bde8f17d13c69881c63586824b69cd9972fe7042 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 17:28:48 -0700 Subject: [PATCH 1400/1860] Run sync on idle on a separate event so that login can return and release the lock before trying to sync. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d1d5de478d36..72b1f762b711 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -476,7 +476,7 @@ WeaveSvc.prototype = { case "idle": this._log.trace("Idle time hit, trying to sync"); Svc.Idle.removeIdleObserver(this, IDLE_TIME); - this.sync(false); + Utils.delay(function() this.sync(false), 0, this); break; } }, From eb56c9983a409a2749b387f780a222cf0797916b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 17:32:35 -0700 Subject: [PATCH 1401/1860] Use the default or backoff intervals only after checking a previously scheduled sync and pick the bigger of the two. --- services/sync/modules/service.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 72b1f762b711..ef294e5eeaac 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1027,15 +1027,12 @@ WeaveSvc.prototype = { _scheduleNextSync: function WeaveSvc__scheduleNextSync(interval) { // Figure out when to sync next if not given a interval to wait if (interval == null) { - // Make sure we backoff we we need to - if (this.status.backoffInterval != 0) - interval = this.status.backoffInterval; // Check if we had a pending sync from last time - else if (this.nextSync != 0) + if (this.nextSync != 0) interval = this.nextSync - Date.now(); - // Use the default sync interval + // Use the bigger of default sync interval and backoff else - interval = this.syncInterval; + interval = Math.max(this.syncInterval, this.status.backoffInterval); } // Start the sync right away if we're already late From b5417e8f4e499207d873682d325e614c952e2f74 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 18:19:30 -0700 Subject: [PATCH 1402/1860] Add some account creation error handling, but it's still pretty clunky with username detection and alerts for other stuff. --- services/sync/locales/en-US/errors.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties index 6eb92829cfc1..270087a51cc7 100644 --- a/services/sync/locales/en-US/errors.properties +++ b/services/sync/locales/en-US/errors.properties @@ -2,3 +2,6 @@ error.login.reason.password = Incorrect username or password error.login.reason.unknown = Unknown error error.login.reason.passphrase = Invalid passphrase error.login.reason.network = Network error + +invalid-captcha = Incorrect words, try again +weak-password = Use a stronger password From 093ebd3fdeeaac414c577b993601aee5a5cfd696 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 18:38:47 -0700 Subject: [PATCH 1403/1860] Sort the sync timing report to show certain functions first. --- services/sync/modules/engines.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 8fd29ae0cdf7..bf5ff045ce2c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -251,7 +251,16 @@ Engine.prototype = { if (stat.sum != null) sums.push(name.replace(/^_/, "") + " " + stat.sum); - return "Total (ms): " + sums.sort().join(", "); + // Order certain functions first before any other random ones + let nameOrder = ["sync", "processIncoming", "uploadOutgoing", + "syncStartup", "syncFinish"]; + let getPos = function(str) { + let pos = nameOrder.indexOf(str.split(" ")[0]); + return pos != -1 ? pos : Infinity; + }; + let order = function(a, b) getPos(a) > getPos(b); + + return "Total (ms): " + sums.sort(order).join(", "); }; this._log.info(stats); From 03467e8a46cb94f49175e5f40e67eda27a1f4fd1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 18:39:24 -0700 Subject: [PATCH 1404/1860] Don't unconditionally forceGC on processIncoming especially when there's nothing to process. --- services/sync/modules/engines.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index bf5ff045ce2c..8ea7371fd3ac 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -485,7 +485,6 @@ SyncEngine.prototype = { // try to free some memory this._store.cache.clear(); - Cu.forceGC(); }, /** From 9ddfe2045a1bd1e3b725494b644072f6b3dcbbcc Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 20:47:38 -0700 Subject: [PATCH 1405/1860] Default to mobile sync interval (5 min) for account creation and upgrade. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ef294e5eeaac..83c96233aa0e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -246,7 +246,7 @@ WeaveSvc.prototype = { get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), - get syncInterval() Svc.Prefs.get("syncInterval", SINGLE_USER_SYNC), + get syncInterval() Svc.Prefs.get("syncInterval", MULTI_MOBILE_SYNC), set syncInterval(value) Svc.Prefs.set("syncInterval", value), get numClients() Svc.Prefs.get("numClients", 0), From 434912a149e9098b3158203766c62d4283e56d90 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 25 Sep 2009 20:53:46 -0700 Subject: [PATCH 1406/1860] Call eval() in Sync-async loop to work around crashes on 1.9.2 Fennec. --- services/sync/modules/ext/Sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/ext/Sync.js b/services/sync/modules/ext/Sync.js index 17400f356a01..1db9ff1e8004 100644 --- a/services/sync/modules/ext/Sync.js +++ b/services/sync/modules/ext/Sync.js @@ -118,7 +118,8 @@ function Sync(func, thisArg, callback) { // Keep waiting until our callback is triggered let callbackData = instanceCallback._(SECRET); while (callbackData.state == CB_READY) - thread.processNextEvent(true); + // XXX Work around bug 518220 until jit.chrome doesn't crash Fennec + eval(), thread.processNextEvent(true); // Reset the state of the callback to prepare for another call let state = callbackData.state; From ff7ecc713a391ef9e0cb7d35390710d7c5bbdcf7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 28 Sep 2009 13:34:56 -0700 Subject: [PATCH 1407/1860] Bug 519262 - Forgot password doesn't work -> throbber keeps spinning Just get the pref without slashifying with getURLPref. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 83c96233aa0e..d245b9dc2f01 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -656,7 +656,7 @@ WeaveSvc.prototype = { }))(), requestPasswordReset: function WeaveSvc_requestPasswordReset(username) { - let res = new Resource(Utils.getURLPref("pwChangeURL")); + let res = new Resource(Svc.Prefs.get("pwChangeURL")); res.authenticator = new NoOpAuthenticator(); res.headers['Content-Type'] = 'application/x-www-form-urlencoded'; let ret = res.post('uid=' + username); From 9e068c5ebcd73832a1a4887f9d7f792c6915e53c Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 28 Sep 2009 15:54:45 -0700 Subject: [PATCH 1408/1860] bug 515593 - remove a bunch of stuff we shouldn't sync, will revisit potential additions for 0.8 --- services/sync/services-sync.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index fbe2b7197707..4566b03c90e3 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -50,28 +50,20 @@ pref("extensions.weave.authenticator.enabled", true); // Preferences to be synced by default pref("extensions.weave.prefs.sync.browser.download.manager.scanWhenDone", true); -pref("extensions.weave.prefs.sync.browser.search.openintab", true); pref("extensions.weave.prefs.sync.browser.search.selectedEngine", true); -pref("extensions.weave.prefs.sync.browser.sessionstore.resume_from_crash", true); -pref("extensions.weave.prefs.sync.browser.sessionstore.resume_session_once", true); -pref("extensions.weave.prefs.sync.browser.sessionstore.interval", true); - pref("extensions.weave.prefs.sync.browser.startup.homepage", true); -pref("extensions.weave.prefs.sync.startup.homepage_override_url", true); pref("extensions.weave.prefs.sync.browser.tabs.tabMinWidth", true); +pref("extensions.weave.prefs.sync.browser.tabs.tabMaxWidth", true); pref("extensions.weave.prefs.sync.browser.tabs.warnOnClose", true); pref("extensions.weave.prefs.sync.browser.tabs.closeButtons", true); pref("extensions.weave.prefs.sync.browser.urlbar.autoFill", true); pref("extensions.weave.prefs.sync.browser.urlbar.maxRichResults", true); -pref("extensions.weave.prefs.sync.browser.urlbar.clickSelectsAll", true); -pref("extensions.weave.prefs.sync.browser.urlbar.doubleClickSelectsAll", true); pref("extensions.weave.prefs.sync.extensions.personas.current", true); pref("extensions.weave.prefs.sync.layout.spellcheckDefault", true); -pref("extensions.weave.prefs.sync.security.dialog_enable_delay", true); pref("extensions.weave.prefs.sync.signon.rememberSignons", true); pref("extensions.weave.prefs.sync.spellchecker.dictionary", true); From c1409c69f81ed6d40c9c2ca570bf458e598fc586 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 28 Sep 2009 16:13:42 -0700 Subject: [PATCH 1409/1860] Don't unnecessarily throw from verifyLogin and remove its _catch wrapper. --- services/sync/locales/en-US/errors.properties | 6 ++-- services/sync/modules/constants.js | 1 + services/sync/modules/service.js | 30 +++++++++++-------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties index 270087a51cc7..97695d7073a8 100644 --- a/services/sync/locales/en-US/errors.properties +++ b/services/sync/locales/en-US/errors.properties @@ -1,7 +1,7 @@ +error.login.reason.network = Failed to connect to the server +error.login.reason.passphrase = Wrong secret phrase error.login.reason.password = Incorrect username or password -error.login.reason.unknown = Unknown error -error.login.reason.passphrase = Invalid passphrase -error.login.reason.network = Network error +error.login.reason.server = Server incorrectly configured invalid-captcha = Incorrect words, try again weak-password = Use a stronger password diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 9c4acc325b2d..99ae8ecf3ab0 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -88,6 +88,7 @@ ENGINE_SUCCEEDED: "success.engine", LOGIN_FAILED_NO_USERNAME: "error.login.reason.no_username", LOGIN_FAILED_NO_PASSWORD: "error.login.reason.no_password", LOGIN_FAILED_NETWORK_ERROR: "error.login.reason.network", +LOGIN_FAILED_SERVER_ERROR: "error.login.reason.server", LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.passphrase", LOGIN_FAILED_LOGIN_REJECTED: "error.login.reason.password", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d245b9dc2f01..98ba83b1166d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -540,22 +540,25 @@ WeaveSvc.prototype = { }, _verifyLogin: function _verifyLogin() - this._catch(this._notify("verify-login", "", function() { + this._notify("verify-login", "", function() { // Make sure we have a cluster to verify against if (this.clusterURL == "") this._setCluster(); - let res = new Resource(this.infoURL); try { - let test = res.get(); + let test = new Resource(this.infoURL).get(); switch (test.status) { case 200: + // The user is authenticated, so check the passphrase now if (!this._verifyPassphrase()) { this.status.setLoginStatus(LOGIN_FAILED_INVALID_PASSPHRASE); return false; } + + // Username/password and passphrase all verified this.status.setLoginStatus(LOGIN_SUCCEEDED); return true; + case 401: case 404: // Check that we're verifying with the correct cluster @@ -564,19 +567,22 @@ WeaveSvc.prototype = { // We must have the right cluster, but the server doesn't expect us this.status.setLoginStatus(LOGIN_FAILED_LOGIN_REJECTED); - this._log.debug("verifyLogin failed: login failed") return false; + default: - this._checkServerError(test.status); - throw "unexpected HTTP response: " + test.status; + // Server didn't respond with something that we expected + this._checkServerError(test); + this.status.setLoginStatus(LOGIN_FAILED_SERVER_ERROR); + return false; } - } catch (e) { - // if we get here, we have either a busted channel or a network error - this._log.debug("verifyLogin failed: " + e) - this.status.setLoginStatus(LOGIN_FAILED_NETWORK_ERROR); - throw e; } - }))(), + catch (ex) { + // Must have failed on some network issue + this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex)); + this.status.setLoginStatus(LOGIN_FAILED_NETWORK_ERROR); + return false; + } + })(), _verifyPassphrase: function _verifyPassphrase() this._catch(this._notify("verify-passphrase", "", function() { From 644ae9403347c65cb45ccaec5c41dbedfd1f7bfe Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 28 Sep 2009 16:28:38 -0700 Subject: [PATCH 1410/1860] Don't wrap changePassword with _catch and persist the password on success. --- services/sync/modules/service.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 98ba83b1166d..245500cffa65 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -622,18 +622,26 @@ WeaveSvc.prototype = { }))(), changePassword: function WeaveSvc_changePassword(newpass) - this._catch(this._notify("changepwd", "", function() { + this._notify("changepwd", "", function() { let url = this.userAPI + this.username + "/password"; - let res = new Weave.Resource(url); - let resp = res.post(newpass); - if (resp.status != 200) { - this._log.info("Password change failed: " + resp); - throw "Could not change password"; + try { + let resp = new Resource(url).post(newpass); + if (resp.status != 200) { + this._log.debug("Password change failed: " + resp); + return false; + } + } + catch(ex) { + // Must have failed on some network issue + this._log.debug("changePassword failed: " + Utils.exceptionStr(ex)); + return false; } + // Save the new password for requests and login manager this.password = newpass; + this.persistLogin(); return true; - }))(), + })(), resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) this._catch(this._notify("resetpph", "", function() { From 117e394c5787e7db2ba2ae39d80dead593d417c6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 29 Sep 2009 13:02:08 -0700 Subject: [PATCH 1411/1860] Remove unused threshold code that will be replaced soon with bug 518075. --- services/sync/modules/constants.js | 1 - services/sync/modules/service.js | 65 ++---------------------------- services/sync/services-sync.js | 1 - 3 files changed, 3 insertions(+), 64 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 99ae8ecf3ab0..0bdc298a7df9 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -113,7 +113,6 @@ kSyncWeaveDisabled: "Weave is disabled", kSyncNotLoggedIn: "User is not logged in", kSyncNetworkOffline: "Network is offline", kSyncInPrivateBrowsing: "Private browsing is enabled", -kSyncNotScheduled: "Not scheduled to do sync", kSyncBackoffNotMet: "Trying to sync before the server said it's okay", // Application IDs diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 245500cffa65..ae39edb7f712 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -46,15 +46,6 @@ const Cu = Components.utils; // how long we should wait before actually syncing on idle const IDLE_TIME = 5; // xxxmpc: in seconds, should be preffable -// INITIAL_THRESHOLD represents the value an engine's score has to exceed -// in order for us to sync it the first time we start up (and the first time -// we do a sync check after having synced the engine or reset the threshold). -const INITIAL_THRESHOLD = 75; - -// THRESHOLD_DECREMENT_STEP is the amount by which we decrement an engine's -// threshold each time we do a sync check and don't sync that engine. -const THRESHOLD_DECREMENT_STEP = 25; - // How long before refreshing the cluster const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes @@ -442,7 +433,6 @@ WeaveSvc.prototype = { case "nsPref:changed": switch (data) { case "enabled": - case "schedule": // Potentially we'll want to reschedule syncs this._checkSyncStatus(); break; @@ -962,13 +952,6 @@ WeaveSvc.prototype = { return false; }, - /** - * Engine scores must meet or exceed this value before we sync them when - * using thresholds. These are engine-specific, as different kinds of data - * change at different rates, so we store them in a hash by engine name. - */ - _syncThresh: {}, - /** * Determine if a sync should run. * @@ -985,8 +968,6 @@ WeaveSvc.prototype = { else if (Svc.Private && Svc.Private.privateBrowsingEnabled) // Svc.Private doesn't exist on Fennec -- don't assume it's there. reason = kSyncInPrivateBrowsing; - else if (Svc.Prefs.get("schedule", 0) != 1) - reason = kSyncNotScheduled; else if (this.status.minimumNextSync > Date.now()) reason = kSyncBackoffNotMet; @@ -1089,23 +1070,14 @@ WeaveSvc.prototype = { /** * Sync up engines with the server. - * - * @param fullSync - * True to unconditionally sync all engines */ - sync: function WeaveSvc_sync(fullSync) + sync: function sync() this._catch(this._lock(this._notify("sync", "", function() { this.status.resetEngineStatus(); - fullSync = true; // not doing thresholds yet - // Use thresholds to determine what to sync only if it's not a full sync - let useThresh = !fullSync; - - // Make sure we should sync or record why we shouldn't. We always obey the - // reason if we're using thresholds (not a full sync); otherwise, allow - // "not scheduled" as future syncs have already been canceled by checkSync. + // Make sure we should sync or record why we shouldn't let reason = this._checkSync(); - if (reason && (useThresh || reason != kSyncNotScheduled)) { + if (reason) { // this is a purposeful abort rather than a failure, so don't set // any status bits reason = "Can't sync: " + reason; @@ -1160,42 +1132,11 @@ WeaveSvc.prototype = { if (!engine.enabled) continue; - // Conditionally reset the threshold for the current engine - let resetThresh = Utils.bind2(this, function WeaveSvc__resetThresh(cond) - cond ? this._syncThresh[name] = INITIAL_THRESHOLD : undefined); - - // Initialize the threshold if it doesn't exist yet - resetThresh(!(name in this._syncThresh)); - - // Determine if we should sync if using thresholds - if (useThresh) { - let score = engine.score; - let thresh = this._syncThresh[name]; - if (score >= thresh) - this._log.debug("Syncing " + name + "; " + - "score " + score + " >= thresh " + thresh); - else { - this._log.debug("Not syncing " + name + "; " + - "score " + score + " < thresh " + thresh); - - // Decrement the threshold by a standard amount with a lower bound of 1 - this._syncThresh[name] = Math.max(thresh - THRESHOLD_DECREMENT_STEP, 1); - - // No need to sync this engine for now - continue; - } - } - // If there's any problems with syncing the engine, report the failure if (!(this._syncEngine(engine)) || this.status.enforceBackoff) { this._log.info("Aborting sync"); break; } - - // We've successfully synced, so reset the threshold. We do this after - // a successful sync so failures can try again on next sync, but this - // could trigger too many syncs if the server is having problems. - resetThresh(useThresh); } if (this._syncError) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 4566b03c90e3..63be10d065d2 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -11,7 +11,6 @@ pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); pref("extensions.weave.enabled", true); -pref("extensions.weave.schedule", 1); pref("extensions.weave.syncOnQuit.enabled", true); From 383a46860ee57dbba3f499aa541cf016b46b8217 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 29 Sep 2009 18:33:41 -0700 Subject: [PATCH 1412/1860] bug 518273 - need to handle not having an active node assigned, r=edilee --- services/sync/locales/en-US/sync.properties | 2 ++ services/sync/modules/constants.js | 1 + services/sync/modules/service.js | 20 ++++++++++++++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index ba8ec37e9031..50b99b159af9 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -21,5 +21,7 @@ error.logout.title = Error While Signing Out error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it. error.sync.title = Error While Syncing error.sync.description = Weave encountered an error while syncing: %1$S. Weave will automatically retry this action. +error.sync.no_node_found = The Weave server is a little busy right now, but you don't need to do anything about it. We'll start syncing your data as soon as we can! +error.sync.no_node_found.title = Sync Delay error.sync.tryAgainButton.label = Sync Now error.sync.tryAgainButton.accesskey = S diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 0bdc298a7df9..0e7628802186 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -101,6 +101,7 @@ NO_KEYS_NO_KEYGEN: "error.sync.reason.no_keys_no_keygen", KEYS_UPLOAD_FAIL: "error.sync.reason.keys_upload_fail", SETUP_FAILED_NO_PASSPHRASE: "error.sync.reason.setup_failed_no_passphrase", ABORT_SYNC_COMMAND: "aborting sync, process commands said so", +NO_SYNC_NODE_FOUND: "error.sync.reason.no_node_found", // engine failure status codes ENGINE_UPLOAD_FAIL: "error.engine.reason.record_upload_fail", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ae39edb7f712..31b46a527f33 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -489,6 +489,8 @@ WeaveSvc.prototype = { return this.serverURL; case 0: case 200: + if (node == "null") + node = null; return node; default: this._log.debug("Unexpected response code: " + node.status); @@ -505,6 +507,7 @@ WeaveSvc.prototype = { _setCluster: function _setCluster() { // Make sure we didn't get some unexpected response for the cluster let cluster = this._findCluster(); + this._log.debug("cluster value = " + cluster); if (cluster == null) return false; @@ -532,8 +535,13 @@ WeaveSvc.prototype = { _verifyLogin: function _verifyLogin() this._notify("verify-login", "", function() { // Make sure we have a cluster to verify against - if (this.clusterURL == "") - this._setCluster(); + // this is a little weird, if we don't get a node we pretend + // to succeed, since that probably means we just don't have storage + if (this.clusterURL == "" && !this._setCluster()) { + this.status.setSyncStatus(NO_SYNC_NODE_FOUND); + Svc.Observer.notifyObservers(null, "weave:service:sync:delayed", ""); + return true; + } try { let test = new Resource(this.infoURL).get(); @@ -1074,6 +1082,14 @@ WeaveSvc.prototype = { sync: function sync() this._catch(this._lock(this._notify("sync", "", function() { this.status.resetEngineStatus(); + + // if we don't have a node, get one. if that fails, retry in 10 minutes + if (this.clusterURL == "" && !this._setCluster()) { + this._scheduleNextSync(10 * 60 * 1000); + return; + } + + fullSync = true; // not doing thresholds yet // Make sure we should sync or record why we shouldn't let reason = this._checkSync(); From 1af7a0737661ca0bf72adfd95f82da556297302a Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 29 Sep 2009 18:40:18 -0700 Subject: [PATCH 1413/1860] fix merge fail --- services/sync/modules/service.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 31b46a527f33..e9bd04cbc9ab 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1089,8 +1089,6 @@ WeaveSvc.prototype = { return; } - fullSync = true; // not doing thresholds yet - // Make sure we should sync or record why we shouldn't let reason = this._checkSync(); if (reason) { From f281e89032080997b50d1c644944ac89c8839021 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 30 Sep 2009 14:46:59 -0700 Subject: [PATCH 1414/1860] Bug 518226 - Service should bubble up partial sync information Set a flag on the status object to indicate partial sync after syncing an engine. For now, just show some text under the sync arrows to let the user know nothing needs to be done. --- services/sync/modules/service.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e9bd04cbc9ab..d853c95716f8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -132,6 +132,7 @@ StatusRecord.prototype = { resetEngineStatus: function() { this.engines = {}; + this.partial = false; }, _resetBackoff: function () { @@ -1221,6 +1222,11 @@ WeaveSvc.prototype = { this._log.debug(Utils.exceptionStr(e)); return true; } + finally { + // If this engine has more to fetch, remember that globally + if (engine.toFetch != null && engine.toFetch.length > 0) + this.status.partial = true; + } }, _freshStart: function WeaveSvc__freshStart() { From bdad7e4fe7dd618c15e47126f72421d5097afb08 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 30 Sep 2009 15:16:56 -0700 Subject: [PATCH 1415/1860] Don't try logging out when already logged out -- about:weave calls logout which tries to _log, but Fennec weave hasn't loaded Weave yet. --- services/sync/modules/service.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d853c95716f8..107a566ca1b6 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -743,6 +743,10 @@ WeaveSvc.prototype = { })))(), logout: function WeaveSvc_logout() { + // No need to do anything if we're already logged out + if (!this._loggedIn) + return; + this._log.info("Logging out"); this._loggedIn = false; this._keyPair = {}; From dea6e9037e2af166aeb3a0cd35b91b7a1fa896c6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 2 Oct 2009 13:46:38 -0700 Subject: [PATCH 1416/1860] Remove eval() workaround now that jit.chrome doesn't crash on latest 1.9.2 fennec (bug 517247). --- services/sync/modules/ext/Sync.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/ext/Sync.js b/services/sync/modules/ext/Sync.js index 1db9ff1e8004..17400f356a01 100644 --- a/services/sync/modules/ext/Sync.js +++ b/services/sync/modules/ext/Sync.js @@ -118,8 +118,7 @@ function Sync(func, thisArg, callback) { // Keep waiting until our callback is triggered let callbackData = instanceCallback._(SECRET); while (callbackData.state == CB_READY) - // XXX Work around bug 518220 until jit.chrome doesn't crash Fennec - eval(), thread.processNextEvent(true); + thread.processNextEvent(true); // Reset the state of the callback to prepare for another call let state = callbackData.state; From 372c8adac7ebaee804f067e1f04602430ead8cc2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 7 Oct 2009 10:47:43 -0700 Subject: [PATCH 1417/1860] Move the status record into a separate Status object exported with the Weave object. --- services/sync/modules/service.js | 132 +++++++++---------------------- services/sync/modules/status.js | 88 +++++++++++++++++++++ 2 files changed, 126 insertions(+), 94 deletions(-) create mode 100644 services/sync/modules/status.js diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 107a566ca1b6..5915c62fba85 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -61,6 +61,7 @@ Cu.import("resource://weave/base_records/crypto.js"); Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/status.js"); Cu.import("resource://weave/engines/clientData.js"); // for export @@ -72,6 +73,7 @@ Cu.import("resource://weave/resource.js", Weave); Cu.import("resource://weave/base_records/keys.js", Weave); Cu.import("resource://weave/notifications.js", Weave); Cu.import("resource://weave/identity.js", Weave); +Cu.import("resource://weave/status.js", Weave); Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/engines.js", Weave); @@ -91,59 +93,6 @@ Cu.import("resource://weave/engines/themes.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); -/* - * Service status query system. See constants defined in constants.js. - */ - -function StatusRecord() { - this._init(); -} -StatusRecord.prototype = { - _init: function() { - this.service = null; - this.sync = null; - this.login = null; - this.engines = {}; - this._resetBackoff(); - }, - - setSyncStatus: function(statusCode) { - if (statusCode == SYNC_SUCCEEDED) { - this.service == STATUS_OK; - this._resetBackoff(); - } - else - this.service = SYNC_FAILED; - - this.sync = statusCode; - }, - - setLoginStatus: function(statusCode) { - this.service = statusCode == LOGIN_SUCCEEDED ? STATUS_OK : LOGIN_FAILED; - this.login = statusCode; - }, - - setEngineStatus: function(engineName, statusCode) { - if (statusCode != ENGINE_SUCCEEDED) - this.service = this.sync = SYNC_FAILED_PARTIAL; - - this.engines[engineName] = statusCode; - }, - - resetEngineStatus: function() { - this.engines = {}; - this.partial = false; - }, - - _resetBackoff: function () { - this.enforceBackoff = false; - this.backoffInterval = 0; - this.minimumNextSync = 0; - } -}; - - - /* * Service singleton * Main entry point into Weave's sync framework @@ -161,9 +110,6 @@ WeaveSvc.prototype = { _syncInProgress: false, _keyGenEnabled: true, - // the status object - _status: null, - // object for caching public and private keys _keyPair: {}, @@ -244,8 +190,6 @@ WeaveSvc.prototype = { get numClients() Svc.Prefs.get("numClients", 0), set numClients(value) Svc.Prefs.set("numClients", value), - get status() { return this._status; }, - get locked() { return this._locked; }, lock: function Svc_lock() { if (this._locked) @@ -296,8 +240,7 @@ WeaveSvc.prototype = { * Prepare to initialize the rest of Weave after waiting a little bit */ onStartup: function onStartup() { - this._status = new StatusRecord(); - this.status.service = STATUS_DELAYED; + Status.service = STATUS_DELAYED; // Figure out how many seconds to delay loading Weave based on the app let wait = 0; @@ -461,8 +404,8 @@ WeaveSvc.prototype = { break; case "weave:service:backoff:interval": let interval = data + Math.random() * data * 0.25; // required backoff + up to 25% - this.status.backoffInterval = interval; - this.status.minimumNextSync = Date.now() + data; + Status.backoffInterval = interval; + Status.minimumNextSync = Date.now() + data; break; case "idle": this._log.trace("Idle time hit, trying to sync"); @@ -499,7 +442,7 @@ WeaveSvc.prototype = { } } catch (e) { this._log.debug("Network error on findCluster"); - this.status.setLoginStatus(LOGIN_FAILED_NETWORK_ERROR); + Status.login = LOGIN_FAILED_NETWORK_ERROR; throw e; } }, @@ -539,7 +482,7 @@ WeaveSvc.prototype = { // this is a little weird, if we don't get a node we pretend // to succeed, since that probably means we just don't have storage if (this.clusterURL == "" && !this._setCluster()) { - this.status.setSyncStatus(NO_SYNC_NODE_FOUND); + Status.sync = NO_SYNC_NODE_FOUND; Svc.Observer.notifyObservers(null, "weave:service:sync:delayed", ""); return true; } @@ -550,12 +493,12 @@ WeaveSvc.prototype = { case 200: // The user is authenticated, so check the passphrase now if (!this._verifyPassphrase()) { - this.status.setLoginStatus(LOGIN_FAILED_INVALID_PASSPHRASE); + Status.login = LOGIN_FAILED_INVALID_PASSPHRASE; return false; } // Username/password and passphrase all verified - this.status.setLoginStatus(LOGIN_SUCCEEDED); + Status.login = LOGIN_SUCCEEDED; return true; case 401: @@ -565,20 +508,20 @@ WeaveSvc.prototype = { return this._verifyLogin(); // We must have the right cluster, but the server doesn't expect us - this.status.setLoginStatus(LOGIN_FAILED_LOGIN_REJECTED); + Status.login = LOGIN_FAILED_LOGIN_REJECTED; return false; default: // Server didn't respond with something that we expected this._checkServerError(test); - this.status.setLoginStatus(LOGIN_FAILED_SERVER_ERROR); + Status.login = LOGIN_FAILED_SERVER_ERROR; return false; } } catch (ex) { // Must have failed on some network issue this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex)); - this.status.setLoginStatus(LOGIN_FAILED_NETWORK_ERROR); + Status.login = LOGIN_FAILED_NETWORK_ERROR; return false; } })(), @@ -689,7 +632,7 @@ WeaveSvc.prototype = { // Something failed, so try again some time later let interval = this._calculateBackoff(++attempts, 60 * 1000); - this._log.debug("Autoconnect failed: " + this.status.login + "; retry in " + + this._log.debug("Autoconnect failed: " + Status.login + "; retry in " + Math.ceil(interval / 1000) + " sec."); Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer"); }, @@ -717,18 +660,18 @@ WeaveSvc.prototype = { this.passphrase = passphrase; if (!this.username) { - this.status.setLoginStatus(LOGIN_FAILED_NO_USERNAME); + Status.login = LOGIN_FAILED_NO_USERNAME; throw "No username set, login failed"; } if (!this.password) { - this.status.setLoginStatus(LOGIN_FAILED_NO_PASSWORD); + Status.login = LOGIN_FAILED_NO_PASSWORD; throw "No password given or found in password manager"; } this._log.info("Logging in user " + this.username); if (!this._verifyLogin()) { // verifyLogin sets the failure states here - throw "Login failed: " + this.status.login; + throw "Login failed: " + Status.login; } // No need to try automatically connecting after a successful login @@ -852,7 +795,7 @@ WeaveSvc.prototype = { let status = Records.response.status; if (status != 200 && status != 404) { this._checkServerError(Records.response); - this.status.setSyncStatus(METARECORD_DOWNLOAD_FAIL); + Status.sync = METARECORD_DOWNLOAD_FAIL; this._log.warn("Unknown error while downloading metadata record. " + "Aborting sync."); return false; @@ -868,7 +811,7 @@ WeaveSvc.prototype = { if (!this._keyGenEnabled) { this._log.info("...and key generation is disabled. Not wiping. " + "Aborting sync."); - this.status.setSyncStatus(DESKTOP_VERSION_OUT_OF_DATE); + Status.sync = DESKTOP_VERSION_OUT_OF_DATE; return false; } reset = true; @@ -883,7 +826,7 @@ WeaveSvc.prototype = { "upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")"); } else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) { - this.status.setSyncStatus(VERSION_OUT_OF_DATE); + Status.sync = VERSION_OUT_OF_DATE; this._log.warn("Server data is of a newer Weave version, this client " + "needs to be upgraded. Aborting sync."); return false; @@ -925,14 +868,14 @@ WeaveSvc.prototype = { this._log.debug("PrivKey HTTP status: " + PrivKeys.response.status); this._checkServerError(PubKeys.response); this._checkServerError(PrivKeys.response); - this.status.setSyncStatus(KEYS_DOWNLOAD_FAIL); + Status.sync = KEYS_DOWNLOAD_FAIL; return false; } if (!this._keyGenEnabled) { this._log.warn("Couldn't download keys from server, and key generation" + "is disabled. Aborting sync"); - this.status.setSyncStatus(NO_KEYS_NO_KEYGEN); + Status.sync = NO_KEYS_NO_KEYGEN; return false; } @@ -953,11 +896,11 @@ WeaveSvc.prototype = { PrivKeys.set(keys.privkey.uri, keys.privkey); return true; } catch (e) { - this.status.setSyncStatus(KEYS_UPLOAD_FAIL); + Status.sync = KEYS_UPLOAD_FAIL; this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); } } else { - this.status.setSyncStatus(SETUP_FAILED_NO_PASSPHRASE); + Status.sync = SETUP_FAILED_NO_PASSPHRASE; this._log.warn("Could not get encryption passphrase"); } } @@ -981,7 +924,7 @@ WeaveSvc.prototype = { else if (Svc.Private && Svc.Private.privateBrowsingEnabled) // Svc.Private doesn't exist on Fennec -- don't assume it's there. reason = kSyncInPrivateBrowsing; - else if (this.status.minimumNextSync > Date.now()) + else if (Status.minimumNextSync > Date.now()) reason = kSyncBackoffNotMet; return reason; @@ -1011,7 +954,7 @@ WeaveSvc.prototype = { let reason = this._checkSync(); if (reason && reason != kSyncBackoffNotMet) { this._clearSyncTriggers(); - this.status.service = STATUS_DISABLED; + Status.service = STATUS_DISABLED; return; } @@ -1040,7 +983,7 @@ WeaveSvc.prototype = { interval = this.nextSync - Date.now(); // Use the bigger of default sync interval and backoff else - interval = Math.max(this.syncInterval, this.status.backoffInterval); + interval = Math.max(this.syncInterval, Status.backoffInterval); } // Start the sync right away if we're already late @@ -1064,12 +1007,12 @@ WeaveSvc.prototype = { this._syncErrors++; // do nothing on the first couple of failures, if we're not in backoff due to 5xx errors - if (!this.status.enforceBackoff) { + if (!Status.enforceBackoff) { if (this._syncErrors < 3) { this._scheduleNextSync(); return; } - this.status.enforceBackoff = true; + Status.enforceBackoff = true; } const MINIMUM_BACKOFF_INTERVAL = 15 * 60 * 1000; // 15 minutes @@ -1086,7 +1029,7 @@ WeaveSvc.prototype = { */ sync: function sync() this._catch(this._lock(this._notify("sync", "", function() { - this.status.resetEngineStatus(); + Status.resetSync(); // if we don't have a node, get one. if that fails, retry in 10 minutes if (this.clusterURL == "" && !this._setCluster()) { @@ -1126,7 +1069,7 @@ WeaveSvc.prototype = { if (Clients.getClients()[Clients.clientID].commands) { try { if (!(this.processCommands())) { - this.status.setSyncStatus(ABORT_SYNC_COMMAND); + Status.sync = ABORT_SYNC_COMMAND; throw "aborting sync, process commands said so"; } @@ -1152,7 +1095,7 @@ WeaveSvc.prototype = { continue; // If there's any problems with syncing the engine, report the failure - if (!(this._syncEngine(engine)) || this.status.enforceBackoff) { + if (!(this._syncEngine(engine)) || Status.enforceBackoff) { this._log.info("Aborting sync"); break; } @@ -1162,7 +1105,7 @@ WeaveSvc.prototype = { this._log.warn("Some engines did not sync correctly"); else { Svc.Prefs.set("lastSync", new Date().toString()); - this.status.setSyncStatus(SYNC_SUCCEEDED); + Status.sync = SYNC_SUCCEEDED; this._log.info("Sync completed successfully"); } } finally { @@ -1221,7 +1164,7 @@ WeaveSvc.prototype = { this._checkServerError(e); - this.status.setEngineStatus(engine.name, e.failureCode || ENGINE_UNKNOWN_FAIL); + Status.engine = [engine.name, e.failureCode || ENGINE_UNKNOWN_FAIL]; this._syncError = true; this._log.debug(Utils.exceptionStr(e)); return true; @@ -1229,7 +1172,7 @@ WeaveSvc.prototype = { finally { // If this engine has more to fetch, remember that globally if (engine.toFetch != null && engine.toFetch.length > 0) - this.status.partial = true; + Status.partial = true; } }, @@ -1267,13 +1210,14 @@ WeaveSvc.prototype = { */ _checkServerError: function WeaveSvc__checkServerError(resp) { if (Utils.checkStatus(resp.status, null, [500, [502, 504]])) { - this.status.enforceBackoff = true; + Status.enforceBackoff = true; if (resp.status == 503 && resp.headers["Retry-After"]) Observers.notify("weave:service:backoff:interval", parseInt(resp.headers["Retry-After"], 10)); } }, /** - * Return a value for a backoff interval. Maximum is eight hours, unless this.status.backoffInterval is higher. + * Return a value for a backoff interval. Maximum is eight hours, unless + * Status.backoffInterval is higher. * */ _calculateBackoff: function WeaveSvc__calculateBackoff(attempts, base_interval) { @@ -1281,7 +1225,7 @@ WeaveSvc.prototype = { let backoffInterval = attempts * (Math.floor(Math.random() * base_interval) + base_interval); - return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL), this.status.backoffInterval); + return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL), Status.backoffInterval); }, /** diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js new file mode 100644 index 000000000000..f6162ce4dcb2 --- /dev/null +++ b/services/sync/modules/status.js @@ -0,0 +1,88 @@ +/***************************** BEGIN LICENSE BLOCK ***************************** +* Version: MPL 1.1/GPL 2.0/LGPL 2.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with the +* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +* the specific language governing rights and limitations under the License. +* +* The Original Code is Weave Status. +* +* The Initial Developer of the Original Code is Mozilla Corporation. +* Portions created by the Initial Developer are Copyright (C) 2009 the Initial +* Developer. All Rights Reserved. +* +* Contributor(s): +* Edward Lee (original author) +* +* Alternatively, the contents of this file may be used under the terms of either +* the GNU General Public License Version 2 or later (the "GPL"), or the GNU +* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case +* the provisions of the GPL or the LGPL are applicable instead of those above. +* If you wish to allow use of your version of this file only under the terms of +* either the GPL or the LGPL, and not to allow others to use your version of +* this file under the terms of the MPL, indicate your decision by deleting the +* provisions above and replace them with the notice and other provisions +* required by the GPL or the LGPL. If you do not delete the provisions above, a +* recipient may use your version of this file under the terms of any one of the +* MPL, the GPL or the LGPL. +* +****************************** END LICENSE BLOCK ******************************/ + +const EXPORTED_SYMBOLS = ["Status"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://weave/constants.js"); + +let Status = { + get login() this._login, + set login(code) { + this._login = code; + + if (code != LOGIN_SUCCEEDED) + this.service = LOGIN_FAILED; + else + this.service = STATUS_OK; + }, + + get sync() this._sync, + set sync(code) { + this._sync = code; + + if (code != SYNC_SUCCEEDED) + this.service = SYNC_FAILED; + else { + this.service = STATUS_OK; + this.resetBackoff(); + } + }, + + get engines() this._engines, + set engines([name, code]) { + this._engines[name] = code; + + if (code != ENGINE_SUCCEEDED) + this.service = SYNC_FAILED_PARTIAL; + }, + + resetBackoff: function resetBackoff() { + this.enforceBackoff = false; + this.backoffInterval = 0; + this.minimumNextSync = 0; + }, + resetSync: function resetSync() { + this._engines = {}; + this.partial = false; + }, +}; + +// Initialize various status values +Status.resetBackoff(); +Status.resetSync(); From fdf312d46f48d9125e358bd49dc8b11f725036c2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 7 Oct 2009 10:47:55 -0700 Subject: [PATCH 1418/1860] Bug 519147 - "Sign In" is greyed out in Weave menu when Firefox starts, in Weave 0.7pre4 Switch back to STATUS_OK (from STATUS_DELAYED) when the delayed startup is actually called. --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5915c62fba85..e6ae48634bb4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -264,6 +264,8 @@ WeaveSvc.prototype = { // xxx we might need to split some of this out into something we can call // again when username/server/etc changes _onStartup: function _onStartup() { + Status.service = STATUS_OK; + this._registerEngines(); // Reset our sync id if we're upgrading, so sync knows to reset local data From 77a14656ca536cf2ef6d6cf568722412fe43c636 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 7 Oct 2009 18:15:38 -0700 Subject: [PATCH 1419/1860] Bug 517492 - Sync will not complete, causing firefox not to quit successfully Lazily listen for quit-application on the first sync-async call and bail out if the notification is sent while waiting for the callback. --- services/sync/modules/ext/Sync.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/ext/Sync.js b/services/sync/modules/ext/Sync.js index 17400f356a01..e44f5fd774b9 100644 --- a/services/sync/modules/ext/Sync.js +++ b/services/sync/modules/ext/Sync.js @@ -51,6 +51,24 @@ const CB_FAIL = {}; // Share a secret only for functions in this file to prevent outside access const SECRET = {}; +function checkAppReady() { + // Watch for app-quit notification to stop any sync. calls + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + os.addObserver({ + observe: function observe() { + // Now that the app is quitting, make checkAppReady throw + checkAppReady = function() { + throw Components.Exception("App. Quitting", Cr.NS_ERROR_ABORT); + }; + os.removeObserver(this, "quit-application"); + } + }, "quit-application", false); + + // In the common case, checkAppReady just returns true + return (checkAppReady = function() true)(); +}; + /** * Create a callback that remembers state like whether it's been called */ @@ -98,6 +116,9 @@ function makeCallback() { */ function Sync(func, thisArg, callback) { return function syncFunc(/* arg1, arg2, ... */) { + // Initialize the observer on first usage + checkAppReady(); + // Grab the current thread so we can make it give up priority let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; @@ -117,7 +138,7 @@ function Sync(func, thisArg, callback) { // Keep waiting until our callback is triggered let callbackData = instanceCallback._(SECRET); - while (callbackData.state == CB_READY) + while (callbackData.state == CB_READY && checkAppReady()) thread.processNextEvent(true); // Reset the state of the callback to prepare for another call From 5d81079ce39355391a33f6349f81ea05614aa040 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 8 Oct 2009 11:45:51 -0700 Subject: [PATCH 1420/1860] Land external changes to Sync.js: setTimeout timer reference, checkAppReady comments/calls. --- services/sync/modules/ext/Sync.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/ext/Sync.js b/services/sync/modules/ext/Sync.js index e44f5fd774b9..225033e481fe 100644 --- a/services/sync/modules/ext/Sync.js +++ b/services/sync/modules/ext/Sync.js @@ -51,6 +51,9 @@ const CB_FAIL = {}; // Share a secret only for functions in this file to prevent outside access const SECRET = {}; +/** + * Check if the app is ready (not quitting) + */ function checkAppReady() { // Watch for app-quit notification to stop any sync. calls let os = Cc["@mozilla.org/observer-service;1"]. @@ -116,9 +119,6 @@ function makeCallback() { */ function Sync(func, thisArg, callback) { return function syncFunc(/* arg1, arg2, ... */) { - // Initialize the observer on first usage - checkAppReady(); - // Grab the current thread so we can make it give up priority let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; @@ -136,9 +136,9 @@ function Sync(func, thisArg, callback) { // Call the async function bound to thisArg with the passed args func.apply(thisArg, args); - // Keep waiting until our callback is triggered + // Keep waiting until our callback is triggered unless the app is quitting let callbackData = instanceCallback._(SECRET); - while (callbackData.state == CB_READY && checkAppReady()) + while (checkAppReady() && callbackData.state == CB_READY) thread.processNextEvent(true); // Reset the state of the callback to prepare for another call @@ -183,11 +183,12 @@ Sync.withCb = function Sync_withCb(func, thisArg) { function setTimeout(func, delay) { let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let callback = { - _func: func, - notify: function(timer) { - // Call the function such that "this" inside the function is the global - // object, just as it would be with window.setTimeout. - (this._func)(); + notify: function notify() { + // This line actually just keeps a reference to timer (prevent GC) + timer = null; + + // Call the function so that "this" is global + func(); } } timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT); From ef5af0bd39ae59e495a705b6f69d5098d8dc5ea9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 8 Oct 2009 13:51:22 -0700 Subject: [PATCH 1421/1860] Simplify logic for Engines.getAll/Enabled and use Enabled for main sync loop. --- services/sync/modules/engines.js | 13 ++----------- services/sync/modules/service.js | 8 +------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 8ea7371fd3ac..e0fe6dabd8c2 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -87,19 +87,10 @@ EngineManagerSvc.prototype = { return engine; }, getAll: function EngMgr_getAll() { - let ret = []; - for (key in this._engines) { - ret.push(this._engines[key]); - } - return ret; + return [engine for ([name, engine] in Iterator(Engines._engines))]; }, getEnabled: function EngMgr_getEnabled() { - let ret = []; - for (key in this._engines) { - if(this._engines[key].enabled) - ret.push(this._engines[key]); - } - return ret; + return this.getAll().filter(function(engine) engine.enabled); }, /** diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e6ae48634bb4..ce18c34e24d6 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1089,13 +1089,7 @@ WeaveSvc.prototype = { this._updateClientMode(); try { - for each (let engine in Engines.getAll()) { - let name = engine.name; - - // Nothing to do for disabled engines - if (!engine.enabled) - continue; - + for each (let engine in Engines.getEnabled()) { // If there's any problems with syncing the engine, report the failure if (!(this._syncEngine(engine)) || Status.enforceBackoff) { this._log.info("Aborting sync"); From abe62404f3758c7fc6fdc0e88aaf495a6d31e56a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 12 Oct 2009 16:22:54 -0700 Subject: [PATCH 1422/1860] Bug 520215 - Only wipe if data is able to be restored from the server For each engine, try decrypting a record from the server before wiping local data. --- services/sync/modules/engines.js | 26 ++++++++++++++++++++++++++ services/sync/modules/service.js | 5 +++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e0fe6dabd8c2..2e8c01d59fcf 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -668,6 +668,32 @@ SyncEngine.prototype = { new Resource(this.cryptoMetaURL).delete(); }, + _testDecrypt: function _testDecrypt() { + // Report failure even if there's nothing to decrypt + let canDecrypt = false; + + // Fetch the most recently uploaded record and try to decrypt it + let test = new Collection(this.engineURL, this._recordObj); + test.limit = 1; + test.sort = "newest"; + test.full = true; + test.recordHandler = function(record) { + record.decrypt(ID.get("WeaveCryptoID")); + canDecrypt = true; + }; + + // Any failure fetching/decrypting will just result in false + try { + this._log.trace("Trying to decrypt a record from the server.."); + test.get(); + } + catch(ex) { + this._log.debug("Failed test decrypt: " + Utils.exceptionStr(ex)); + } + + return canDecrypt; + }, + _resetClient: function SyncEngine__resetClient() { this.resetLastSync(); this.toFetch = []; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ce18c34e24d6..74a1a8d533a5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1267,9 +1267,10 @@ WeaveSvc.prototype = { else engines = Engines.get(engines); - // Fully wipe each engine + // Fully wipe each engine if it's able to decrypt data for each (let engine in engines) - engine.wipeClient(); + if (engine._testDecrypt()) + engine.wipeClient(); }))(), /** From b37ad60a1a7afc0f49142377f15eab096dfb8178 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 12 Oct 2009 16:45:40 -0700 Subject: [PATCH 1423/1860] Bug 482906 - Gracefully handle wiping weave passwords if syncing doesn't restore them After potentially wiping out all local passwords, save the Weave passwords to disk. --- services/sync/modules/service.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 74a1a8d533a5..25fb2193364a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1271,6 +1271,9 @@ WeaveSvc.prototype = { for each (let engine in engines) if (engine._testDecrypt()) engine.wipeClient(); + + // Save the password/passphrase just in-case they aren't restored by sync + this.persistLogin(); }))(), /** From a65dded31d4a7c88b2a82ef9bcabd199e18410d4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 12 Oct 2009 18:11:31 -0700 Subject: [PATCH 1424/1860] Split multiple id deletes into 100-id chunks instead of hitting max apache URI length. --- services/sync/modules/engines.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 2e8c01d59fcf..0e29d2986070 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -637,14 +637,26 @@ SyncEngine.prototype = { this._log.trace("Finishing up sync"); this._tracker.resetScore(); + let doDelete = Utils.bind2(this, function(key, val) { + let coll = new Collection(this.engineURL, this._recordObj); + coll[key] = val; + coll.delete(); + }); + for (let [key, val] in Iterator(this._delete)) { // Remove the key for future uses delete this._delete[key]; - // Send a delete for the property - let coll = new Collection(this.engineURL, this._recordObj); - coll[key] = val; - coll.delete(); + // Send a simple delete for the property + if (key != "ids" || val.length <= 100) + doDelete(key, val); + else { + // For many ids, split into chunks of at most 100 + while (val.length > 0) { + doDelete(key, val.slice(0, 100)); + val = val.slice(100); + } + } } }, From 868b4d94d3498215ccbac6d26cf3a7dc50d4c848 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 13 Oct 2009 11:20:28 -0700 Subject: [PATCH 1425/1860] Bug 522077 - Download a certain total number of items each sync instead of fixed "catch up" Limit the initial the first fetch of new items by a total number of fetch and subtract the number of items processed. Use the difference to keep fetching more items from the backlog in chunks. --- services/sync/modules/engines.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 0e29d2986070..53c440ea3f74 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -385,6 +385,11 @@ SyncEngine.prototype = { _processIncoming: function SyncEngine__processIncoming() { this._log.debug("Downloading & applying server changes"); + // Figure out how many total items to fetch this sync; do less on mobile + let fetchNum = 1500; + if (Svc.Prefs.get("client.type") == "mobile") + fetchNum /= 10; + // enable cache, and keep only the first few items. Otherwise (when // we have more outgoing items than can fit in the cache), we will // keep rotating items in and out, perpetually getting cache misses @@ -396,7 +401,7 @@ SyncEngine.prototype = { newitems.newer = this.lastSync; newitems.full = true; newitems.sort = "index"; - newitems.limit = 300; + newitems.limit = fetchNum; let count = {applied: 0, reconciled: 0}; let handled = []; @@ -429,6 +434,9 @@ SyncEngine.prototype = { resp.failureCode = ENGINE_DOWNLOAD_FAIL; throw resp; } + + // Subtract out the number of items we just got + fetchNum -= handled.length; } // Check if we got the maximum that we requested; get the rest if so @@ -448,16 +456,17 @@ SyncEngine.prototype = { this.toFetch = extra.concat(Utils.arraySub(this.toFetch, extra)); } - // Process any backlog of GUIDs if necessary - if (this.toFetch.length > 0) { + // Process any backlog of GUIDs if we haven't fetched too many this sync + while (this.toFetch.length > 0 && fetchNum > 0) { // Reuse the original query, but get rid of the restricting params newitems.limit = 0; newitems.newer = 0; - // Get the first bunch of records and save the rest for later, but don't - // get too many records as there's a maximum server URI length (HTTP 414) - newitems.ids = this.toFetch.slice(0, 150); - this.toFetch = this.toFetch.slice(150); + // Get the first bunch of records and save the rest for later + let minFetch = Math.min(150, this.toFetch.length, fetchNum); + newitems.ids = this.toFetch.slice(0, minFetch); + this.toFetch = this.toFetch.slice(minFetch); + fetchNum -= minFetch; // Reuse the existing record handler set earlier let resp = newitems.get(); @@ -465,7 +474,6 @@ SyncEngine.prototype = { resp.failureCode = ENGINE_DOWNLOAD_FAIL; throw resp; } - } if (this.lastSync < this.lastModified) From 14ca23790c66c5c6fa574d5db0138d96f42a6d71 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 13 Oct 2009 11:56:46 -0700 Subject: [PATCH 1426/1860] Bug 522084 - Automatically sync much sooner if there's more data to fetch from the server Have the syncInterval getter return with a lower interval for non-mobile clients while still enforcing backoff if it's bigger than the sync interval. --- services/sync/modules/constants.js | 1 + services/sync/modules/service.js | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 0e7628802186..ac43742dd063 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -54,6 +54,7 @@ PWDMGR_HOST: "chrome://weave", SINGLE_USER_SYNC: 24 * 60 * 60 * 1000, // 1 day MULTI_DESKTOP_SYNC: 60 * 60 * 1000, // 1 hour MULTI_MOBILE_SYNC: 5 * 60 * 1000, // 5 minutes +PARTIAL_DATA_SYNC: 60 * 1000, // 1 minute // File IO Flags MODE_RDONLY: 0x01, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 25fb2193364a..4606d8a16309 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -184,7 +184,12 @@ WeaveSvc.prototype = { get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), - get syncInterval() Svc.Prefs.get("syncInterval", MULTI_MOBILE_SYNC), + get syncInterval() { + // If we have a partial download, sync sooner if we're not mobile + if (Status.partial && Clients.clientType != "mobile") + return PARTIAL_DATA_SYNC; + return Svc.Prefs.get("syncInterval", MULTI_MOBILE_SYNC); + }, set syncInterval(value) Svc.Prefs.set("syncInterval", value), get numClients() Svc.Prefs.get("numClients", 0), From 900845ab232367c3e4a98198e4c146be21576a44 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 13 Oct 2009 16:27:10 -0700 Subject: [PATCH 1427/1860] Keep the unsorted bookmark folder unsorted and not worry about the ordering. --- services/sync/modules/engines/bookmarks.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 89756ccce814..5d9d7217c028 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -793,9 +793,12 @@ BookmarksStore.prototype = { if (itemPos == 0) return; + // For items directly under unfiled/unsorted, give no predecessor let parentId = Svc.Bookmark.getFolderIdForItem(itemId); - let predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos - 1); + if (parentId == Svc.Bookmark.unfiledBookmarksFolder) + return; + let predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos - 1); if (predecessorId == -1) { this._log.debug("No predecessor directly before " + itemId + " under " + parentId + " at " + itemPos); From 7e8a4afd8f8d40c0ee7ffd8498042a02383c8df1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 13 Oct 2009 21:07:32 -0700 Subject: [PATCH 1428/1860] Update predecessor test to additionally test unsorted bookmark predecessors. --- .../sync/tests/unit/test_bookmark_predecessor.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/services/sync/tests/unit/test_bookmark_predecessor.js b/services/sync/tests/unit/test_bookmark_predecessor.js index 38c9ca565714..a80bcee47c7d 100644 --- a/services/sync/tests/unit/test_bookmark_predecessor.js +++ b/services/sync/tests/unit/test_bookmark_predecessor.js @@ -7,11 +7,11 @@ function run_test() { let store = new (new BookmarksEngine())._storeObj(); store.wipe(); - let unfiled = Svc.Bookmark.unfiledBookmarksFolder; let uri = Utils.makeURI("http://uri/"); - function insert(pos) { + function insert(pos, folder) { + folder = folder || Svc.Bookmark.toolbarFolder; let name = "pos" + pos; - let bmk = Svc.Bookmark.insertBookmark(unfiled, uri, pos, name); + let bmk = Svc.Bookmark.insertBookmark(folder, uri, pos, name); Svc.Bookmark.setItemGUID(bmk, name); return bmk; } @@ -31,4 +31,10 @@ function run_test() { _("Make sure the index of item gets fixed"); do_check_eq(Svc.Bookmark.getItemIndex(first), 0); do_check_eq(Svc.Bookmark.getItemIndex(second), 1); + + _("Make sure things that are in unsorted don't set the predecessor"); + insert(0, Svc.Bookmark.unfiledBookmarksFolder); + insert(1, Svc.Bookmark.unfiledBookmarksFolder); + do_check_eq(store.createRecord("pos0").predecessorid, undefined); + do_check_eq(store.createRecord("pos1").predecessorid, undefined); } From 65feac96d45dabcf827017d169445f0bc6535a3d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 16 Oct 2009 16:18:38 -0700 Subject: [PATCH 1429/1860] Ignore sortindex when comparing if records are different because it's only used for ordering of records. --- services/sync/modules/engines.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 53c440ea3f74..54c8f3571a60 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -501,7 +501,6 @@ SyncEngine.prototype = { if (this._log.level <= Log4Moz.Level.Trace) this._log.trace("Local record: " + local); if (item.parentid == local.parentid && - item.sortindex == local.sortindex && item.deleted == local.deleted && Utils.deepEquals(item.cleartext, local.cleartext)) { this._log.trace("Local record is the same"); From 7e82d988201bea339480e5578f80e20c646b0a29 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 16 Oct 2009 16:18:53 -0700 Subject: [PATCH 1430/1860] Bug 517598 - Put useful interestingness/index values for history Additionally get the frecency for a history GUID/page when getting the url and title. --- services/sync/modules/engines/history.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index ef7ddc740a8e..51d2664e26e0 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -142,7 +142,7 @@ HistoryStore.prototype = { get _urlStm() { this._log.trace("Creating SQL statement: _urlStm"); let stm = this._db.createStatement( - "SELECT url, title " + + "SELECT url, title, frecency " + "FROM moz_places_view " + "WHERE id = (" + "SELECT place_id " + @@ -181,9 +181,13 @@ HistoryStore.prototype = { if (!this._urlStm.step()) return null; - return {url: this._urlStm.row.url, - title: this._urlStm.row.title}; - } finally { + return { + url: this._urlStm.row.url, + title: this._urlStm.row.title, + frecency: this._urlStm.row.frecency + }; + } + finally { this._urlStm.reset(); } }, @@ -266,6 +270,7 @@ HistoryStore.prototype = { if (foo) { record.histUri = foo.url; record.title = foo.title; + record.sortindex = foo.frecency; record.visits = this._getVisits(record.histUri); record.encryption = cryptoMetaURL; From 1e3ed4b0a2ea743220ea9f0fc056eef5adf74525 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 16 Oct 2009 16:19:28 -0700 Subject: [PATCH 1431/1860] Bug 517598 - Put useful interestingness/index values for bookmark Use places to figure out the frecency of a bookmark and give items (not just bookmarks) a bonus if it's sitting in the toolbar. --- services/sync/modules/engines/bookmarks.js | 34 ++++++++++++++++++++++ services/sync/modules/util.js | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5d9d7217c028..96c4e4322971 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -758,11 +758,45 @@ BookmarksStore.prototype = { record.parentid = this._getParentGUIDForId(placeId); record.predecessorid = this._getPredecessorGUIDForId(placeId); record.encryption = cryptoMetaURL; + record.sortindex = this._calculateIndex(record); this.cache.put(guid, record); return record; }, + get _frecencyStm() { + this._log.trace("Creating SQL statement: _frecencyStm"); + let stm = Svc.History.DBConnection.createStatement( + "SELECT frecency " + + "FROM moz_places_view " + + "WHERE url = :url"); + this.__defineGetter__("_frecencyStm", function() stm); + return stm; + }, + + _calculateIndex: function _calculateIndex(record) { + // 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) { + try { + this._frecencyStm.params.url = record.bmkUri; + if (this._frecencyStm.step()) + index += this._frecencyStm.row.frecency; + } + finally { + this._frecencyStm.reset(); + } + } + + this._log.info("calculating index for: " + record.title + " = " + index); + return index; + }, + _getParentGUIDForId: function BStore__getParentGUIDForId(itemId) { // Give the parent annotation if it exists try { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 8023dc648b38..ccf859e44835 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -787,7 +787,7 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], - ["History", "@mozilla.org/browser/nav-history-service;1", "nsINavHistoryService"], + ["History", "@mozilla.org/browser/nav-history-service;1", "nsPIPlacesDatabase"], ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], ["Login", "@mozilla.org/login-manager;1", "nsILoginManager"], From 7198f3d76ce000710fafd22dac74007e5f9dbe36 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 19 Oct 2009 16:24:32 -0700 Subject: [PATCH 1432/1860] Remove unused pick-sync dialog (<==, <==>, ==>). --- services/sync/locales/en-US/pick-sync.dtd | 9 --------- services/sync/locales/en-US/pick-sync.properties | 5 ----- services/sync/modules/util.js | 4 ---- 3 files changed, 18 deletions(-) delete mode 100644 services/sync/locales/en-US/pick-sync.dtd delete mode 100644 services/sync/locales/en-US/pick-sync.properties diff --git a/services/sync/locales/en-US/pick-sync.dtd b/services/sync/locales/en-US/pick-sync.dtd deleted file mode 100644 index 2977210c2604..000000000000 --- a/services/sync/locales/en-US/pick-sync.dtd +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/services/sync/locales/en-US/pick-sync.properties b/services/sync/locales/en-US/pick-sync.properties deleted file mode 100644 index b9c42b228349..000000000000 --- a/services/sync/locales/en-US/pick-sync.properties +++ /dev/null @@ -1,5 +0,0 @@ -dir.wipeClient.warning.title = Confirm Erase Local Data -dir.wipeClient.warning = This will erase your local data.\n\nAre you sure you want to do this? -dir.wipeRemote.warning.title = Confirm Erase Remote Data -dir.wipeRemote.warning = This will erase your remote data.\n\nAre you sure you want to do this? - diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index ccf859e44835..5c8395829579 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -731,10 +731,6 @@ let Utils = { Utils._openChromeWindow("Status", "status.xul"); }, - openSync: function Utils_openSync() { - Utils._openChromeWindow("Sync", "pick-sync.xul"); - }, - getErrorString: function Utils_getErrorString(error, args) { try { return Str.errors.get(error, args || null); From bb04b22ca9dad4ef5561cb3e14fbaab3ce287888 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 19 Oct 2009 16:37:25 -0700 Subject: [PATCH 1433/1860] Remove unused change password/passphrase/generic dialog. --- .../locales/en-US/generic-change.properties | 33 ------------------- services/sync/modules/util.js | 5 --- 2 files changed, 38 deletions(-) delete mode 100644 services/sync/locales/en-US/generic-change.properties diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties deleted file mode 100644 index d477881fa189..000000000000 --- a/services/sync/locales/en-US/generic-change.properties +++ /dev/null @@ -1,33 +0,0 @@ -noPassword.alert = You must enter a password. -noPassphrase.alert = You must enter a passphrase. -passwordNoMatch.alert = Your passwords do not match. Try again! -passphraseNoMatch.alert = Your passphrases do not match. Try again! - -incorrectPassword.alert = Your current password is incorrect! -incorrectPassphrase.alert = Your current passphrase is incorrect! - -change.password.title = Change your Password -change.password.status.active = Changing your password... -change.password.status.success = Your password has been changed. -change.password.status.error = There was an error changing your password. -change.password.status.passwordSameAsPassphrase = The password cannot be the same as the passphrase. -change.password.status.passwordSameAsUsername = The password cannot be the same as the username. -change.password.status.passwordsDoNotMatch = The passwords you entered do not match. -change.password.status.badOldPassword = Your current password is incorrect. - -change.passphrase.title = Change your Passphrase -change.passphrase.label = Changing passphrase, please wait... -change.passphrase.error = There was an error while changing your passphrase! -change.passphrase.success = Your passphrase was successfully changed! - -reset.passphrase.title = Reset your Passphrase -reset.passphrase.label = Resetting passphrase, please wait... -reset.passphrase.error = There was an error while resetting your passphrase! -reset.passphrase.success = Your passphrase was successfully reset! - -new.passphrase.old = Enter your current passphrase -new.passphrase.label = Enter your new passphrase -new.passphrase.confirm = Confirm your new passphrase -new.password.old = Enter your current password -new.password.label = Enter your new password -new.password.confirm = Confirm your new password diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 5c8395829579..06de065769b3 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -719,11 +719,6 @@ let Utils = { options || "centerscreen,chrome,dialog,modal,resizable=no", args); }, - openGenericDialog: function Utils_openGenericDialog(type) { - this._genericDialogType = type; - this.openDialog("ChangeSomething", "generic-change.xul"); - }, - openLog: function Utils_openLog() { Utils._openChromeWindow("Log", "log.xul"); }, From 49f3d40e32916a20ac907533490ccef5f5be7727 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 19 Oct 2009 17:06:50 -0700 Subject: [PATCH 1434/1860] Remove unused TabsNotification for remote virtual tabs notification. --- services/sync/locales/en-US/notification.dtd | 3 --- services/sync/modules/notifications.js | 15 +-------------- 2 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 services/sync/locales/en-US/notification.dtd diff --git a/services/sync/locales/en-US/notification.dtd b/services/sync/locales/en-US/notification.dtd deleted file mode 100644 index 5bbdf7793ee2..000000000000 --- a/services/sync/locales/en-US/notification.dtd +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index 536b8a6bc18d..dcb8d25c3a49 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -34,8 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ["Notifications", "Notification", "NotificationButton", - "TabsNotification"]; +const EXPORTED_SYMBOLS = ["Notifications", "Notification", "NotificationButton"]; const Cc = Components.classes; const Ci = Components.interfaces; @@ -157,15 +156,3 @@ function NotificationButton(label, accessKey, callback) { this.accessKey = accessKey; this.callback = callbackWrapper; } - -function TabsNotification() { - // Call the base class's constructor to initialize the new instance. - // XXX Can we simply pass null, null for the title, description? - Notification.call(this, "", "", null, Notifications.PRIORITY_INFO, null); -} - -// We set each prototype property individually instead of redefining -// the entire prototype to avoid blowing away existing properties -// of the prototype like the the "constructor" property, which we use -// to bind notification objects to their XBL representations. -TabsNotification.prototype.__proto__ = Notification.prototype; From 2487322def955891a336fb3f4d4db50712da5e2e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 20 Oct 2009 17:17:30 -0700 Subject: [PATCH 1435/1860] Bug 520231 - strip identity from production track Remove bits related to authenticator/auto-login and openid (munge and redirect). --- services/sync/services-sync.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 63be10d065d2..ca23cc9a9acd 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -43,8 +43,6 @@ pref("extensions.weave.log.logger.authenticator", "Debug"); pref("extensions.weave.network.numRetries", 2); pref("extensions.weave.tabs.sortMode", "recency"); -pref("extensions.weave.openId.enabled", true); -pref("extensions.weave.authenticator.enabled", true); // Preferences to be synced by default pref("extensions.weave.prefs.sync.browser.download.manager.scanWhenDone", true); From b527c7617bcf22a19aa2848386b46140b8d90c96 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 22 Oct 2009 15:31:45 -0400 Subject: [PATCH 1436/1860] land new Firefox prefs/wizard --- services/sync/Weave.js | 4 +- services/sync/locales/en-US/fx-prefs.dtd | 57 +++++++++++++++++++ .../sync/locales/en-US/fx-prefs.properties | 14 +++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 services/sync/locales/en-US/fx-prefs.dtd create mode 100644 services/sync/locales/en-US/fx-prefs.properties diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 9c626e07aa54..2bd05a01f58c 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -55,12 +55,12 @@ WeaveService.prototype = { case "app-startup": let os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); - os.addObserver(this, "sessionstore-windows-restored", true); + os.addObserver(this, "final-ui-startup", true); break; /* The following event doesn't exist on Fennec; for Fennec loading, see * fennec-weave-overlay.js. */ - case "sessionstore-windows-restored": + case "final-ui-startup": Cu.import("resource://weave/service.js"); Weave.Service.onStartup(); break; diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd new file mode 100644 index 000000000000..cf69cc878267 --- /dev/null +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties new file mode 100644 index 000000000000..28031470f104 --- /dev/null +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -0,0 +1,14 @@ +connect.label = Connect +disconnect.label = Disconnect +startSyncing.label = Start Syncing + +usernameAvailable.label = Available! +usernameNotAvailable.label = Already in use + +passwordOK.label = Success +passwordMismatch.label = Passwords do not match +passwordTooWeak.label = Password is too weak + +serverInvalid.label = Please enter a valid server URL + +wizardPageTitle.label = Create your Weave Account - Step %1$S of %2$S \ No newline at end of file From 11f01af3414e6e009329754527ea134a64bd9e1c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 22 Oct 2009 13:51:50 -0700 Subject: [PATCH 1437/1860] Bug 523384 - Update Fennec UI for 0.8 release Provide an optionsURL to show settings on Fennec and update the options on various events like sync, login, options loaded. --- services/sync/locales/en-US/sync.dtd | 8 ++++++++ services/sync/locales/en-US/sync.properties | 3 +++ services/sync/modules/util.js | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 55d1453379f4..df8557ba0884 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -6,5 +6,13 @@ + + + + + + + + diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 50b99b159af9..229f768288a7 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -1,6 +1,9 @@ # %S is the date and time at which the last sync successfully completed lastSync.label = Last Update: %S +# %S is the username logged in +connected.label = Connected: %S + #weaveButtonOffline.label = Sign In #weaveButtonOnline.label = Weave #shareBookmark.menuItem = Share This Folder... diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 06de065769b3..77135466a40e 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -792,5 +792,5 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], Ci[lazy[2]])); let Str = {}; -["about", "errors"] +["about", "errors", "sync"] .forEach(function(lazy) Utils.lazy2(Str, lazy, Utils.lazyStrings(lazy))); From 9e111051296677e9784fc7cc2a38c00269ded95d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 22 Oct 2009 13:52:55 -0700 Subject: [PATCH 1438/1860] Load a local firstrun page with instructions on what to do instead of about:weave. --- services/sync/locales/en-US/fennec-firstrun.dtd | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 services/sync/locales/en-US/fennec-firstrun.dtd diff --git a/services/sync/locales/en-US/fennec-firstrun.dtd b/services/sync/locales/en-US/fennec-firstrun.dtd new file mode 100644 index 000000000000..dafd599f5474 --- /dev/null +++ b/services/sync/locales/en-US/fennec-firstrun.dtd @@ -0,0 +1,6 @@ + + + + + + From 8ba26ddf4047fc4d6dcbca23ecebfad7e1e4b7e3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 22 Oct 2009 16:15:34 -0700 Subject: [PATCH 1439/1860] Bug 483089 - Weave Preferences button in Add-ons manager should be enabled Show the new pref pane (bug 521357) by having the options.xul file for Fennec (bug 523384) close iteself and open the real prefs window. --- services/sync/modules/util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 77135466a40e..ce9a1018510e 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -786,6 +786,7 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ["Observer", "@mozilla.org/observer-service;1", "nsIObserverService"], ["Private", "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService"], ["Prompt", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"], + ["Script", "@mozilla.org/moz/jssubscript-loader;1", "mozIJSSubScriptLoader"], ["Version", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator"], ["WinMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"], ["WinWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"], From d18fbb16eb13d9e019c56ddc744a03a96a8115af Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 22 Oct 2009 17:03:51 -0700 Subject: [PATCH 1440/1860] Remove onStartup call from fennec overlay now that we listen to final-ui-startup. --- services/sync/Weave.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 2bd05a01f58c..317e89710a53 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -57,9 +57,7 @@ WeaveService.prototype = { getService(Ci.nsIObserverService); os.addObserver(this, "final-ui-startup", true); break; - /* The following event doesn't exist on Fennec; for Fennec loading, see - * fennec-weave-overlay.js. - */ + case "final-ui-startup": Cu.import("resource://weave/service.js"); Weave.Service.onStartup(); From a191da1aed1984f15ab02f1efa5ca07160653372 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 22 Oct 2009 23:23:03 -0400 Subject: [PATCH 1441/1860] bug 521357 - handle login errors in the main prefpane better, still needs work after pre2 --- services/sync/locales/en-US/fx-prefs.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties index 28031470f104..9701db140b93 100644 --- a/services/sync/locales/en-US/fx-prefs.properties +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -11,4 +11,4 @@ passwordTooWeak.label = Password is too weak serverInvalid.label = Please enter a valid server URL -wizardPageTitle.label = Create your Weave Account - Step %1$S of %2$S \ No newline at end of file +loginFailed = Login failed: %1$S From 036492c193bc511a8f1a16a71d918850df830574 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 23 Oct 2009 01:10:36 -0400 Subject: [PATCH 1442/1860] Backed out changeset 4e518b3ae5d2 overzealous pruning ftl ;) --- .../locales/en-US/generic-change.properties | 33 +++++++++++++++++++ services/sync/modules/util.js | 5 +++ 2 files changed, 38 insertions(+) create mode 100644 services/sync/locales/en-US/generic-change.properties diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties new file mode 100644 index 000000000000..d477881fa189 --- /dev/null +++ b/services/sync/locales/en-US/generic-change.properties @@ -0,0 +1,33 @@ +noPassword.alert = You must enter a password. +noPassphrase.alert = You must enter a passphrase. +passwordNoMatch.alert = Your passwords do not match. Try again! +passphraseNoMatch.alert = Your passphrases do not match. Try again! + +incorrectPassword.alert = Your current password is incorrect! +incorrectPassphrase.alert = Your current passphrase is incorrect! + +change.password.title = Change your Password +change.password.status.active = Changing your password... +change.password.status.success = Your password has been changed. +change.password.status.error = There was an error changing your password. +change.password.status.passwordSameAsPassphrase = The password cannot be the same as the passphrase. +change.password.status.passwordSameAsUsername = The password cannot be the same as the username. +change.password.status.passwordsDoNotMatch = The passwords you entered do not match. +change.password.status.badOldPassword = Your current password is incorrect. + +change.passphrase.title = Change your Passphrase +change.passphrase.label = Changing passphrase, please wait... +change.passphrase.error = There was an error while changing your passphrase! +change.passphrase.success = Your passphrase was successfully changed! + +reset.passphrase.title = Reset your Passphrase +reset.passphrase.label = Resetting passphrase, please wait... +reset.passphrase.error = There was an error while resetting your passphrase! +reset.passphrase.success = Your passphrase was successfully reset! + +new.passphrase.old = Enter your current passphrase +new.passphrase.label = Enter your new passphrase +new.passphrase.confirm = Confirm your new passphrase +new.password.old = Enter your current password +new.password.label = Enter your new password +new.password.confirm = Confirm your new password diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 06de065769b3..5c8395829579 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -719,6 +719,11 @@ let Utils = { options || "centerscreen,chrome,dialog,modal,resizable=no", args); }, + openGenericDialog: function Utils_openGenericDialog(type) { + this._genericDialogType = type; + this.openDialog("ChangeSomething", "generic-change.xul"); + }, + openLog: function Utils_openLog() { Utils._openChromeWindow("Log", "log.xul"); }, From 53c1af3392610d27aefa7d20ff75f6c7fab81e20 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 22 Oct 2009 23:46:50 -0700 Subject: [PATCH 1443/1860] Bug 524052 - Remove about:weave Remove files under content/about and content/ext (jQuery) and about:weave service. --- services/sync/Weave.js | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 317e89710a53..6db75d883eaa 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -66,30 +66,7 @@ WeaveService.prototype = { } }; -function AboutWeaveService() {} -AboutWeaveService.prototype = { - classDescription: "about:weave", - contractID: "@mozilla.org/network/protocol/about;1?what=weave", - classID: Components.ID("{ecb6987d-9d71-475d-a44d-a5ff2099b08c}"), - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, - Ci.nsISupportsWeakReference]), - - getURIFlags: function(aURI) { - return (Ci.nsIAboutModule.ALLOW_SCRIPT | - Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT); - }, - - newChannel: function(aURI) { - let ios = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let ch = ios.newChannel("chrome://weave/content/about/index.html", null, null); - ch.originalURI = aURI; - return ch; - } -}; - function NSGetModule(compMgr, fileSpec) { - return XPCOMUtils.generateModule([WeaveService, AboutWeaveService]); + return XPCOMUtils.generateModule([WeaveService]); } From d2b79369bda4e64766147dd5668eb35612cdebfb Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 23 Oct 2009 03:05:41 -0400 Subject: [PATCH 1444/1860] moar error checking, turn field validation back on in wizard, do better rewind/forward enabling --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4606d8a16309..7a861004fbd3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -767,7 +767,7 @@ WeaveSvc.prototype = { let register = res.put(payload); if (register.success) { this._log.info("Account created: " + register); - return; + return null; } // Must have failed, so figure out the reason From 7ecb1c91b3ec8ed22990d80b748d7e2ec88de817 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 27 Oct 2009 19:15:20 -0400 Subject: [PATCH 1445/1860] bug 520065 - partial sync fail should be an error, add better error string, and fix status object misuse so it actually works --- services/sync/locales/en-US/errors.properties | 5 +++++ services/sync/modules/service.js | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties index 97695d7073a8..ff4edabd4d62 100644 --- a/services/sync/locales/en-US/errors.properties +++ b/services/sync/locales/en-US/errors.properties @@ -3,5 +3,10 @@ error.login.reason.passphrase = Wrong secret phrase error.login.reason.password = Incorrect username or password error.login.reason.server = Server incorrectly configured +error.sync.failed_partial = One or more data types could not be synced + invalid-captcha = Incorrect words, try again weak-password = Use a stronger password + +# this is the fallback, if we hit an error we didn't bother to localize +error.reason.unknown = Unknown error diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7a861004fbd3..bc7ddc6657d9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1103,7 +1103,7 @@ WeaveSvc.prototype = { } if (this._syncError) - this._log.warn("Some engines did not sync correctly"); + throw "Some engines did not sync correctly"; else { Svc.Prefs.set("lastSync", new Date().toString()); Status.sync = SYNC_SUCCEEDED; @@ -1165,7 +1165,8 @@ WeaveSvc.prototype = { this._checkServerError(e); - Status.engine = [engine.name, e.failureCode || ENGINE_UNKNOWN_FAIL]; + Status.engines = [engine.name, e.failureCode || ENGINE_UNKNOWN_FAIL]; + this._syncError = true; this._log.debug(Utils.exceptionStr(e)); return true; From 52b7fab478e5e667796c6a314b87140f4acd5ea9 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 27 Oct 2009 20:36:43 -0400 Subject: [PATCH 1446/1860] bug 524562 - add login feedback to prefpane --- services/sync/locales/en-US/fx-prefs.dtd | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index cf69cc878267..9655eed55cb8 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -29,6 +29,7 @@ + From 94114c9ef8f512065eb4265ab8422da33ef93e71 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 27 Oct 2009 21:51:14 -0400 Subject: [PATCH 1447/1860] bug 522805 - better UI for private browsing mode, also fix 518035 by replacing Sign In/Out with Connect/Disconnect --- services/sync/locales/en-US/sync.dtd | 4 ++-- services/sync/locales/en-US/sync.properties | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index df8557ba0884..17d97e1d49b6 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -1,6 +1,6 @@ - - + + diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 229f768288a7..28d39ef53c12 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -10,6 +10,7 @@ connected.label = Connected: %S #unShareBookmark.menuItem = Stop Sharing This Folder status.offline = Not Signed In +status.privateBrowsing = Disabled (Private Browsing) # The next two are not normally used, as we now display the username # when the user is logged in. But if for some reason we can't get the username, From 029614bb64f9664812deb64570d546249295550a Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 28 Oct 2009 03:42:35 -0400 Subject: [PATCH 1448/1860] bug 485108 - drop status dialog, show status including engines on statusbar, remove dead status bits --- services/sync/locales/en-US/about.properties | 93 ------------------- .../sync/locales/en-US/engines.properties | 8 ++ services/sync/locales/en-US/status.dtd | 2 - services/sync/locales/en-US/status.properties | 39 -------- services/sync/locales/en-US/sync.properties | 3 + services/sync/modules/engines.js | 10 +- services/sync/modules/engines/bookmarks.js | 2 +- services/sync/modules/engines/clients.js | 2 +- services/sync/modules/engines/cookies.js | 2 +- services/sync/modules/engines/extensions.js | 2 +- services/sync/modules/engines/forms.js | 2 +- services/sync/modules/engines/history.js | 2 +- services/sync/modules/engines/input.js | 2 +- services/sync/modules/engines/microformats.js | 2 +- services/sync/modules/engines/passwords.js | 2 +- services/sync/modules/engines/plugins.js | 2 +- services/sync/modules/engines/prefs.js | 2 +- services/sync/modules/engines/tabs.js | 2 +- services/sync/modules/engines/themes.js | 2 +- services/sync/modules/util.js | 2 +- 20 files changed, 34 insertions(+), 149 deletions(-) delete mode 100644 services/sync/locales/en-US/about.properties create mode 100644 services/sync/locales/en-US/engines.properties delete mode 100644 services/sync/locales/en-US/status.dtd delete mode 100644 services/sync/locales/en-US/status.properties diff --git a/services/sync/locales/en-US/about.properties b/services/sync/locales/en-US/about.properties deleted file mode 100644 index 9505c4be0db8..000000000000 --- a/services/sync/locales/en-US/about.properties +++ /dev/null @@ -1,93 +0,0 @@ -# Top menus - -user-menu-offline = Signed out -user-menu-signing-in = Signing in... -user-menu-online = Signed in as %S -sign-out-item = Sign out -my-account-item = My Account - -advanced-menu-title = Advanced -server-settings-item = Server settings -activity-log-item = Activity Log - -# Status messages (below the arrow in the center) - -status-offline = (offline) -status-signing-in = (signing in...) -status-idle = (idle) -status-sync = (syncing) - -# Bubbles - -prev = prev -next = next - -welcome-title = Welcome To Sync -welcome-1 = Do you already have a Weave account? -welcome-yes = yes -welcome-no = no - -my-account-change-password = change password -my-account-change-passphrase = change passphrase - -signin-title = Sign Into Weave -signin-newacct = new user -signin-username = -signin-password = -signin-passphrase = -signin-next = sign in - -newacct-title = New Account -newacct-username = -newacct-password = -newacct-passphrase = -newacct-email = -newacct-tos-label = I agree to the %S -newacct-tos = Terms of Service -captcha-response = -user-taken-password = My username won't work - -willsync-title = Account Created! -willsync-1 = Sync will begin in %S seconds... -willsync-config = choose what to sync - -setup-title = Sync Settings -setup-1 = Check the things you'd like to sync: -setup-sync = sync now - -clientinfo-type-desktop = desktop -clientinfo-type-laptop = laptop -clientinfo-type-mobile = mobile -clientinfo-prefs = choose what to sync - -cloudinfo-title = What's In The Cloud? -cloudinfo-erase = erase -erase-title = Erase Server Data -erase-warning = This will delete all data on the Weave server.\n\nAre you sure you want to do this? - -# Help items - -help-forgot-password = I forgot my password -forgot-password-1 = Type in your username and we'll send you an email so you can reset it: -forgot-password-box = username -forgot-password-ok = send email - -help-forgot-passphrase = I forgot my passphrase -forgot-passphrase-1 = You can pick a new passphrase, but all your server data will need to be deleted (it cannot be recovered). -forgot-passphrase-2 = To go ahead, click the button below: -forgot-passphrase-ok = reset passphrase - -help-helpme = I'm stuck! What do I do? -help-helpme-1 = If you're stuck, you might want to try the %S or the %S for help. -help-helpme-faq = FAQ -help-helpme-forum = Weave discussion forum - -help-user-taken = My username won't work -help-user-taken-1 = Your username might be taken, try adding numbers or additional words to it. -help-user-taken-2 = Additionally, you can't use special symbols or spaces inside usernames. - -help-newacct-pass = Weave won't accept my password or passphrase -help-newacct-pass-1 = The password and passphrase must be different from each other. - -help-no-captcha = I can't see the verification image -help-no-captcha-1 = Some add-ons can interfere with the verification image. Try disabling NoScript or similar add-ons. diff --git a/services/sync/locales/en-US/engines.properties b/services/sync/locales/en-US/engines.properties new file mode 100644 index 000000000000..ff2fb7fe55a5 --- /dev/null +++ b/services/sync/locales/en-US/engines.properties @@ -0,0 +1,8 @@ +#bookmarks +clients = Client Data +bookmarks = Bookmarks +tabs = Tabs +forms = Form History +history = Browsing History +prefs = Preferences +passwords = Passwords diff --git a/services/sync/locales/en-US/status.dtd b/services/sync/locales/en-US/status.dtd deleted file mode 100644 index 42744bfe7e44..000000000000 --- a/services/sync/locales/en-US/status.dtd +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/services/sync/locales/en-US/status.properties b/services/sync/locales/en-US/status.properties deleted file mode 100644 index 05f87cfce094..000000000000 --- a/services/sync/locales/en-US/status.properties +++ /dev/null @@ -1,39 +0,0 @@ -dialog.accept = Hide - -status.wait = Waiting for Current Sync to Finish -status.active = Syncing with Weave -status.success = Sync Complete -status.error = Sync Failed (%1$S) -status.closing = Closing... - -status.engine.start = Starting Sync -status.engine.process-incoming = Processing Incoming Items -status.engine.upload-outgoing = Uploading Outgoing Items - -status.downloading-status = Downloading Status from Server -status.uploading-status = Updating Status on Server - -status.downloading-snapshot = Downloading Snapshot from Server -status.uploading-snapshot = Uploading Initial Snapshot to Server - -status.generating-random-key = Generating Random Key -status.encrypting-key = Encrypting Key -status.uploading-key = Uploading Key to Server -status.downloading-keyring = Downloading Key Ring from Server -status.decrypting-key = Decrypting Key - -status.downloading-deltas = Downloading Deltas from Server -status.uploading-deltas = Updating Deltas on Server - -status.calculating-differences = Calculating Differences -status.reconciling-updates = Reconciling Differences -status.applying-changes = Applying Required Changes -status.no-changes-required = No Changes Needed - -status.encoding-json = Encoding Data -status.decoding-json = Decoding Data - -status.encrypting = Encrypting Data -status.decrypting = Decrypting Data - - diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 28d39ef53c12..147d996fa087 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -4,6 +4,9 @@ lastSync.label = Last Update: %S # %S is the username logged in connected.label = Connected: %S +# %S is the engine being synced +syncing.label = Weave is syncing: %S + #weaveButtonOffline.label = Sign In #weaveButtonOnline.label = Weave #shareBookmark.menuItem = Share This Folder... diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 54c8f3571a60..98e2e6ea444d 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -136,7 +136,7 @@ EngineManagerSvc.prototype = { function Engine() { this._init(); } Engine.prototype = { name: "engine", - displayName: "Boring Engine", + _displayName: "Boring Engine", description: "An engine example - it doesn't actually sync anything", logName: "Engine", @@ -161,6 +161,14 @@ Engine.prototype = { this.__tracker = new this._trackerObj(); return this.__tracker; }, + + get displayName() { + try { + return Str.engines.get(this.name); + } catch (e) {} + + return this._displayName; + }, _init: function Engine__init() { this._notify = Utils.notify("weave:engine:"); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 96c4e4322971..6fe3fb1f4744 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -88,7 +88,7 @@ function BookmarksEngine() { BookmarksEngine.prototype = { __proto__: SyncEngine.prototype, name: "bookmarks", - displayName: "Bookmarks", + _displayName: "Bookmarks", description: "Keep your favorite links always at hand", logName: "Bookmarks", _recordObj: PlacesItem, diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 399e681761d0..0b87602218bf 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -58,7 +58,7 @@ function ClientEngine() { ClientEngine.prototype = { __proto__: SyncEngine.prototype, name: "clients", - displayName: "Clients", + _displayName: "Clients", description: "Sync information about other clients connected to Weave Sync", logName: "Clients", _storeObj: ClientStore, diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 5cc46e1485e4..7c31eeb47279 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -53,7 +53,7 @@ CookieEngine.prototype = { __proto__: SyncEngine.prototype, get name() { return "cookies"; }, - get displayName() { return "Cookies"; }, + get _displayName() { return "Cookies"; }, get logName() { return "CookieEngine"; }, get serverPrefix() { return "user-data/cookies/"; }, diff --git a/services/sync/modules/engines/extensions.js b/services/sync/modules/engines/extensions.js index 2a09deea19fe..cf482d79b9bb 100644 --- a/services/sync/modules/engines/extensions.js +++ b/services/sync/modules/engines/extensions.js @@ -44,7 +44,7 @@ ExtensionEngine.prototype = { get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, - displayName: "Extensions", + _displayName: "Extensions", description: "", logName: "Extensions", name: "extensions", diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index ab4faa22c944..d24063d546de 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -56,7 +56,7 @@ function FormEngine() { FormEngine.prototype = { __proto__: SyncEngine.prototype, name: "forms", - displayName: "Forms", + _displayName: "Forms", description: "Take advantage of form-fill convenience on all your devices", logName: "Forms", _storeObj: FormStore, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 51d2664e26e0..e92483da9c74 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -76,7 +76,7 @@ function HistoryEngine() { HistoryEngine.prototype = { __proto__: SyncEngine.prototype, name: "history", - displayName: "History", + _displayName: "History", description: "All the sites you've been to. Take your awesomebar with you!", logName: "History", _recordObj: HistoryRec, diff --git a/services/sync/modules/engines/input.js b/services/sync/modules/engines/input.js index 9ef24abf8464..c2dd16cb27e2 100644 --- a/services/sync/modules/engines/input.js +++ b/services/sync/modules/engines/input.js @@ -54,7 +54,7 @@ InputEngine.prototype = { __proto__: SyncEngine.prototype, get name() { return "input"; }, - get displayName() { return "Input History (Location Bar)"; }, + get _displayName() { return "Input History (Location Bar)"; }, get logName() { return "InputEngine"; }, get serverPrefix() { return "user-data/input/"; }, diff --git a/services/sync/modules/engines/microformats.js b/services/sync/modules/engines/microformats.js index 3cd47656520d..9c9f3f8d6608 100644 --- a/services/sync/modules/engines/microformats.js +++ b/services/sync/modules/engines/microformats.js @@ -44,7 +44,7 @@ MicroFormatEngine.prototype = { get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, - displayName: "MicroFormats", + _displayName: "MicroFormats", description: "", logName: "MicroFormats", name: "microformats", diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index fb4a42e69fe1..d221c5f54270 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -55,7 +55,7 @@ function PasswordEngine() { PasswordEngine.prototype = { __proto__: SyncEngine.prototype, name: "passwords", - displayName: "Passwords", + _displayName: "Passwords", description: "Forget all your passwords, Weave will remember them for you", logName: "Passwords", _storeObj: PasswordStore, diff --git a/services/sync/modules/engines/plugins.js b/services/sync/modules/engines/plugins.js index 94c61aeec424..0ac1790ea150 100644 --- a/services/sync/modules/engines/plugins.js +++ b/services/sync/modules/engines/plugins.js @@ -44,7 +44,7 @@ PluginEngine.prototype = { get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, - displayName: "Plugins", + _displayName: "Plugins", description: "", logName: "Plugins", name: "plugins", diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 7c880092f607..614a83e0c108 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -58,7 +58,7 @@ function PrefsEngine() { PrefsEngine.prototype = { __proto__: SyncEngine.prototype, name: "prefs", - displayName: "Preferences", + _displayName: "Preferences", description: "Synchronize your home page, selected persona, and more", logName: "Prefs", _storeObj: PrefStore, diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0c3fc836f29a..a336bc05e834 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -58,7 +58,7 @@ function TabEngine() { TabEngine.prototype = { __proto__: SyncEngine.prototype, name: "tabs", - displayName: "Tabs", + _displayName: "Tabs", description: "Access tabs from other devices via the History menu", logName: "Tabs", _storeObj: TabStore, diff --git a/services/sync/modules/engines/themes.js b/services/sync/modules/engines/themes.js index 98025bd1f806..b9e2674ab300 100644 --- a/services/sync/modules/engines/themes.js +++ b/services/sync/modules/engines/themes.js @@ -44,7 +44,7 @@ ThemeEngine.prototype = { get enabled() null, // XXX force disabled in-case the pref was somehow set __proto__: SyncEngine.prototype, - displayName: "Themes", + _displayName: "Themes", description: "", logName: "Themes", name: "themes", diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index e56ccb8e07ef..ef294c474c74 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -798,5 +798,5 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], Ci[lazy[2]])); let Str = {}; -["about", "errors", "sync"] +["engines", "errors", "sync"] .forEach(function(lazy) Utils.lazy2(Str, lazy, Utils.lazyStrings(lazy))); From 36f5a0a3df166a807d6b97676223354a6d2aeadd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 2 Nov 2009 10:59:35 -0800 Subject: [PATCH 1449/1860] =?UTF-8?q?Bug=20524617=20-=20give=20status=20up?= =?UTF-8?q?dates=20during=20connection=20and=20sync=20Show=20"Connecting?= =?UTF-8?q?=E2=80=A6"=20after=20tapping=20connect=20and=20"in=20progress?= =?UTF-8?q?=E2=80=A6"=20after=20tapping=20sync=20now.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/sync/locales/en-US/sync.dtd | 1 - services/sync/locales/en-US/sync.properties | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 17d97e1d49b6..9a6c795fdf02 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -9,7 +9,6 @@ - diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 147d996fa087..6bf06eb1c2b7 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -1,8 +1,11 @@ # %S is the date and time at which the last sync successfully completed lastSync.label = Last Update: %S +lastSyncInProgress.label = Last Update: in progress… # %S is the username logged in connected.label = Connected: %S +disconnected.label = Disconnected +connecting.label = Connecting… # %S is the engine being synced syncing.label = Weave is syncing: %S From 3933263a411e01deb21b93c4e5d0f426e150b978 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 2 Nov 2009 21:36:04 -0500 Subject: [PATCH 1450/1860] bug 524916 - remove unused strings --- services/sync/locales/en-US/fx-prefs.properties | 2 -- services/sync/locales/en-US/sync.properties | 11 ----------- 2 files changed, 13 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties index 9701db140b93..84ef406d0378 100644 --- a/services/sync/locales/en-US/fx-prefs.properties +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -2,10 +2,8 @@ connect.label = Connect disconnect.label = Disconnect startSyncing.label = Start Syncing -usernameAvailable.label = Available! usernameNotAvailable.label = Already in use -passwordOK.label = Success passwordMismatch.label = Passwords do not match passwordTooWeak.label = Password is too weak diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 6bf06eb1c2b7..1b0ea4c1b456 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -10,20 +10,9 @@ connecting.label = Connecting… # %S is the engine being synced syncing.label = Weave is syncing: %S -#weaveButtonOffline.label = Sign In -#weaveButtonOnline.label = Weave -#shareBookmark.menuItem = Share This Folder... -#unShareBookmark.menuItem = Stop Sharing This Folder - status.offline = Not Signed In status.privateBrowsing = Disabled (Private Browsing) -# The next two are not normally used, as we now display the username -# when the user is logged in. But if for some reason we can't get the username, -# we display these. -#status.idle = Idle -#status.active = Working... - error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. # should decide if we're going to show this From c9a2199b6e79aaff660b3feaf29a4bfc2e294b89 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 2 Nov 2009 22:38:50 -0500 Subject: [PATCH 1451/1860] bug 524916 - remove /xmpp from Sync, since we're not using it --- .../sync/modules/xmpp/authenticationLayer.js | 353 ------------- services/sync/modules/xmpp/readme.txt | 125 ----- services/sync/modules/xmpp/transportLayer.js | 415 --------------- services/sync/modules/xmpp/xmppClient.js | 485 ------------------ 4 files changed, 1378 deletions(-) delete mode 100644 services/sync/modules/xmpp/authenticationLayer.js delete mode 100644 services/sync/modules/xmpp/readme.txt delete mode 100644 services/sync/modules/xmpp/transportLayer.js delete mode 100644 services/sync/modules/xmpp/xmppClient.js diff --git a/services/sync/modules/xmpp/authenticationLayer.js b/services/sync/modules/xmpp/authenticationLayer.js deleted file mode 100644 index 50dd9eb0d0ad..000000000000 --- a/services/sync/modules/xmpp/authenticationLayer.js +++ /dev/null @@ -1,353 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Jono DiCarlo - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = [ "PlainAuthenticator", "Md5DigestAuthenticator" ]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cu = Components.utils; - -Cu.import("resource://weave/log4moz.js"); - -/* Two implementations of SASL authentication: - one using MD5-DIGEST, the other using PLAIN. - - -Here's the interface that each implementation must obey: - -{ - initialize( clientName, clientRealm, clientPassword ); - - generateResponse( rootElem ); - - // returns text of error message - getError(); -} - -*/ - -function BaseAuthenticator() { -} -BaseAuthenticator.prototype = { - COMPLETION_CODE: "success!", - - initialize: function( userName, realm, password ) { - this._name = userName; - this._realm = realm; - this._password = password; - this._stepNumber = 0; - this._errorMsg = ""; - }, - - getError: function () { - /* Returns text of most recent error message. - Client code should call this if generateResponse() returns false - to see what the problem was. */ - return this._errorMsg; - }, - - generateResponse: function( rootElem ) { - /* Subclasses must override this. rootElem is a DOM node which is - the root element of the XML the server has sent to us as part - of the authentication protocol. return value: the string that - should be sent back to the server in response. 'false' if - there's a failure, or COMPLETION_CODE if nothing else needs to - be sent because authentication is a success. */ - - this._errorMsg = "generateResponse() should be overridden by subclass."; - return false; - }, - - verifyProtocolSupport: function( rootElem, protocolName ) { - /* Parses the incoming stream from the server to check whether the - server supports the type of authentication we want to do - (specified in the protocolName argument). - Returns false if there is any problem. - */ - if ( rootElem.nodeName != "stream:stream" ) { - this._errorMsg = "Expected stream:stream but got " + rootElem.nodeName; - return false; - } - - dump( "Got response from server...\n" ); - dump( "ID is " + rootElem.getAttribute( "id" ) + "\n" ); - // TODO: Do I need to do anything with this ID value??? - dump( "From: " + rootElem.getAttribute( "from" ) + "\n" ); - if (rootElem.childNodes.length == 0) { - // No child nodes is unexpected, but allowed by the protocol. - // this shouldn't be an error. - this._errorMsg = "Expected child nodes but got none."; - return false; - } - - var child = rootElem.childNodes[0]; - if (child.nodeName == "stream:error" ) { - this._errorMsg = this.parseError( child ); - return false; - } - - if ( child.nodeName != "stream:features" ) { - this._errorMsg = "Expected stream:features but got " + child.nodeName; - return false; - } - - var protocolSupported = false; - var mechanisms = child.getElementsByTagName( "mechanism" ); - for ( var x = 0; x < mechanisms.length; x++ ) { - if ( mechanisms[x].firstChild.nodeValue == protocolName ) { - protocolSupported = true; - } - } - - if ( !protocolSupported ) { - this._errorMsg = protocolName + " not supported by server!"; - return false; - } - return true; - } - -}; - -function Md5DigestAuthenticator( ) { - /* SASL using DIGEST-MD5 authentication. - Uses complicated hash of password - with nonce and cnonce to obscure password while preventing replay - attacks. - - See http://www.faqs.org/rfcs/rfc2831.html - "Using Digest Authentication as a SASL mechanism" - - TODO: currently, this is getting rejected by my server. - What's wrong? - */ -} -Md5DigestAuthenticator.prototype = { - - _makeCNonce: function( ) { - return "\"" + Math.floor( 10000000000 * Math.random() ) + "\""; - }, - - generateResponse: function Md5__generateResponse( rootElem ) { - if ( this._stepNumber == 0 ) { - - if ( this.verifyProtocolSupport( rootElem, "DIGEST-MD5" ) == false ) { - return false; - } - // SASL step 1: request that we use DIGEST-MD5 authentication. - this._stepNumber = 1; - return ""; - - } else if ( this._stepNumber == 1 ) { - - // proceed to SASL step 2: are you asking for a CHALLENGE?!? - var challenge = this._unpackChallenge( rootElem.firstChild.nodeValue ); - dump( "Nonce is " + challenge.nonce + "\n" ); - // eg: - // nonce="3816627940",qop="auth",charset=utf-8,algorithm=md5-sess - - // Now i have the nonce: make a digest-response out of - /* username: required - realm: only needed if realm is in challenge - nonce: required, just as recieved - cnonce: required, opaque quoted string, 64 bits entropy - nonce-count: optional - qop: (quality of protection) optional - serv-type: optional? - host: optional? - serv-name: optional? - digest-uri: "service/host/serv-name" (replaces those three?) - response: required (32 lowercase hex), - maxbuf: optional, - charset, - LHEX (32 hex digits = ??), - cipher: required if auth-conf is negotiatedd?? - authzid: optional - */ - - - // TODO: Are these acceptable values for realm, nonceCount, and - // digestUri?? - var nonceCount = "00000001"; - var digestUri = "xmpp/" + this.realm; - var cNonce = this._makeCNonce(); - // Section 2.1.2.1 of RFC2831 - var A1 = str_md5( this.name + ":" + this.realm + ":" + this.password ) + ":" + challenge.nonce + ":" + cNonce; - var A2 = "AUTHENTICATE:" + digestUri; - var myResponse = hex_md5( hex_md5( A1 ) + ":" + challenge.nonce + ":" + nonceCount + ":" + cNonce + ":auth" + hex_md5( A2 ) ); - - var responseDict = { - username: "\"" + this.name + "\"", - nonce: challenge.nonce, - nc: nonceCount, - cnonce: cNonce, - qop: "\"auth\"", - algorithm: "md5-sess", - charset: "utf-8", - response: myResponse - }; - responseDict[ "digest-uri" ] = "\"" + digestUri + "\""; - var responseStr = this._packChallengeResponse( responseDict ); - this._stepNumber = 2; - return "" + responseStr + ""; - - } else if ( this._stepNumber = 2 ) { - dump( "Got to step 3!" ); - // At this point the server might reject us with a - // - if ( rootElem.nodeName == "failure" ) { - this._errorMsg = rootElem.firstChild.nodeName; - return false; - } - //this._connectionStatus = this.REQUESTED_SASL_3; - } - this._errorMsg = "Can't happen."; - return false; - }, - - _unpackChallenge: function( challengeString ) { - var challenge = atob( challengeString ); - dump( "After b64 decoding: " + challenge + "\n" ); - var challengeItemStrings = challenge.split( "," ); - var challengeItems = {}; - for ( var x in challengeItemStrings ) { - var stuff = challengeItemStrings[x].split( "=" ); - challengeItems[ stuff[0] ] = stuff[1]; - } - return challengeItems; - }, - - _packChallengeResponse: function( responseDict ) { - var responseArray = [] - for( var x in responseDict ) { - responseArray.push( x + "=" + responseDict[x] ); - } - var responseString = responseArray.join( "," ); - dump( "Here's my response string: \n" ); - dump( responseString + "\n" ); - return btoa( responseString ); - } -}; -Md5DigestAuthenticator.prototype.__proto__ = new BaseAuthenticator(); - - -function PlainAuthenticator( ) { - /* SASL using PLAIN authentication, which sends password in the clear. */ -} -PlainAuthenticator.prototype = { - - generateResponse: function( rootElem ) { - if ( this._stepNumber == 0 ) { - if ( this.verifyProtocolSupport( rootElem, "PLAIN" ) == false ) { - return false; - } - var authString = btoa( this._realm + '\0' + this._name + '\0' + this._password ); - this._stepNumber = 1; - - // XXX why does this not match the stanzas in XEP-025? - return "" + authString + ""; - - } else if ( this._stepNumber == 1 ) { - if ( rootElem.nodeName == "failure" ) { - // Authentication rejected: username or password may be wrong. - this._errorMsg = rootElem.firstChild.nodeName; - return false; - } else if ( rootElem.nodeName == "success" ) { - // Authentication accepted: now we start a new stream for - // resource binding. - /* RFC3920 part 7 says: upon receiving a success indication within the - SASL negotiation, the client MUST send a new stream header to the - server, to which the serer MUST respond with a stream header - as well as a list of available stream features. */ - // TODO: resource binding happens in any authentication mechanism - // so should be moved to base class. - this._stepNumber = 2; - return ""; - } - } else if ( this._stepNumber == 2 ) { - // See if the server is asking us to bind a resource, and if it's - // asking us to start a session: - var bindNodes = rootElem.getElementsByTagName( "bind" ); - if ( bindNodes.length > 0 ) { - this._needBinding = true; - } - - var sessionNodes = rootElem.getElementsByTagName( "session" ); - if ( sessionNodes.length > 0 ) { - this._needSession = true; - } - - if ( !this._needBinding && !this._needSession ) { - // Server hasn't requested either: we're done. - return this.COMPLETION_CODE; - } - - if ( this._needBinding ) { - // Do resource binding: - // Tell the server to generate the resource ID for us. - this._stepNumber = 3; - return ""; - } - - this._errorMsg = "Server requested session not binding: can't happen?"; - return false; - } else if ( this._stepNumber == 3 ) { - // Pull the JID out of the stuff the server sends us. - var jidNodes = rootElem.getElementsByTagName( "jid" ); - if ( jidNodes.length == 0 ) { - this._errorMsg = "Expected JID node from server, got none."; - return false; - } - this._jid = jidNodes[0].firstChild.nodeValue; - // TODO: Does the client need to do anything special with its new - // "client@host.com/resourceID" full JID? - - // If we still need to do session, then we're not done yet: - if ( this._needSession ) { - this._stepNumber = 4; - return ""; - } else { - return this.COMPLETION_CODE; - } - } else if ( this._stepNumber == 4 ) { - // OK, now we're done. - return this.COMPLETION_CODE; - } - } - -}; -PlainAuthenticator.prototype.__proto__ = new BaseAuthenticator(); diff --git a/services/sync/modules/xmpp/readme.txt b/services/sync/modules/xmpp/readme.txt deleted file mode 100644 index 29397a9dd13a..000000000000 --- a/services/sync/modules/xmpp/readme.txt +++ /dev/null @@ -1,125 +0,0 @@ -About the XMPP module - -Here is sample code demonstrating how client code can use the XMPP module. -It assumes that a Jabber server capable of HTTP-Polling is running on localhost -on port 5280. - - Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); - - var serverUrl = "http://127.0.0.1:5280/http-poll"; - var jabberDomain = "mylaptop.local"; - - var transport = new HTTPPollingTransport( serverUrl, - false, - 4000 ); - // "false" tells the transport not to use session keys. 4000 is the number of - // milliseconds to wait between attempts to poll the server. - var auth = new PlainAuthenticator(); - var alice = new XmppClient( "alice", jabberDomain, "iamalice", - transport, auth ); - // This sets up an XMPP client for the jabber ID - // "alice@jonathan-dicarlos-macbook-pro.local", who has password - // "iamalice". - - // Set up callback for incoming messages: - var aliceMessageHandler = { - handle: function( msgText, from ) { - // Your code goes here. It will be called whenever another XMPP client - // sends a message to Alice. - // msgText is the text of the incoming message, and "from" is the - // jabber ID of the sender. - } - }; - alice.registerMessageHandler( aliceMessageHandler ); - - // Connect - alice.connect( jabberDomain ); - alice.waitForConnection(); - // this will block until our connection attempt has either succeeded or failed. - // Check if connection succeeded: - if ( alice._connectionStatus == alice.FAILED ) { - // handle error - } - - // Send a message - alice.sendMessage( "bob@mylaptop.local", "Hello, Bob." ); - - // Disconnect - alice.disconnect(); - - -Installing an XMPP server to test against: - -I'm using ejabberd, which works well and is open source but is implemented in Erlang. (That's bad, because I don't know how to hack Erlang. I'm thinking of moving to jabberd, implemented in C.) - -ejabberd: http://www.ejabberd.im/ -jabberd: http://jabberd.jabberstudio.org/ - -Installation of ejabberd was simple. After it's installed, configure it (create an admin account, and enable http-polling) by editing the configuration file: - - ejabberd/conf/ejabberd.cfg - - Its web admin interface can be used to create accounts (so that the "alice" and "bob" accounts assumed by the testing code will actually exist). This admin interface is at: - - http://localhost:5280/admin/ - -The ejabberd process is started simply by running: - - ejabberd/bin/ejabberdctl start - - -Outstanding Issues -- bugs and things to do. - -* The test above is failing with a timeout. How to debug this? Let's start - by making it two processes again and seeing if that makes a difference... - - Nope. It doesn't. Bob just gets an HTTP status 0 - -* Occasionally (by which I mean, randomly) the server will respond to - a POST with an HTTP status 0. Generally things will go back to - normal with the next POST and then keep working, so it's not - actually preventing anything from working, but I don't understand - what status 0 means and that makes me uncomfortable as it could be - hiding other problems. - - What if I modify the sending code to repeat the sending of any messages - that result in HTTP status 0? - -- seems to work, but doesn't help. (It's not HTTP status 0 that's preventing - the test from passing...) - -* Occasionally (randomly) a message is received that has a raw integer - for a messageElem, which causes XML parsing of the incoming message - to fail. I don't see any pattern for the values of the raw - integers, nor do I understand how we can get an integer in place of - an XML element when requesting the root node of the incoming XML - document. - -* Duplicate messages. Occasionally, even though one instance of the - client sends only a single copy of a stanza, the intended - target will receive it multiple times. Sometimes if the recipient - signs off and then signs back on, it will recieve another copy of - the message. This makes me think it's probably a feature of the - server to resend messages that it thinks the recipient might have - missed while offline. Duplicate messages are a problem especially - when using xmpp to synchronize two processes, for instance. Do I - need to tweak the server settings, or change server implementations? - Do I need to have the client receiving messages acknowledge them to the - server somehow? - Do I need to move to using stanzas for synchronization? Or - start putting GUIDs into s and discarding duplicates - myself? - -* Whenever I send an request, I get a error back - from the server. Documentation for the standard error messages - indicates that bad-request is supposed to happen when the type of - the iq element is invalid, i.e. something other than 'set', 'get', - 'result', or 'error'. But I'm using 'get' or 'set' and still - getting . I don't understand what's happening here. - -* The authentication layer currently doesn't work with MD5-digest auth, only plain - auth. - -* The HTTPPolling transport layer gets a "key sequence error" if useKeys is turned on. - (Everything seems to be working OK with useKeys turned off, but that's less - secure.) - diff --git a/services/sync/modules/xmpp/transportLayer.js b/services/sync/modules/xmpp/transportLayer.js deleted file mode 100644 index 43356dc5c6ce..000000000000 --- a/services/sync/modules/xmpp/transportLayer.js +++ /dev/null @@ -1,415 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Jono DiCarlo - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['HTTPPollingTransport']; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cu = Components.utils; - -Cu.import("resource://weave/log4moz.js"); - -/* - The interface that should be implemented by any Transport object: - - send( messageXml ); - setCallbackObject(object with .onIncomingData(aStringData) and .onTransportError(aErrorText) ); - connect(); - disconnect(); -*/ - -function InputStreamBuffer() { -} -InputStreamBuffer.prototype = { - _data: "", - append: function( stuff ) { - this._data = this._data + stuff; - }, - clear: function() { - this._data = ""; - }, - getData: function() { - return this._data; - } -} - -/** - * A transport layer that uses raw sockets. - * Not recommended for use; currently fails when trying to negotiate - * TLS. - * Use HTTPPollingTransport instead. - */ -function SocketClient( host, port ) { - this._init( host, port ); -} -SocketClient.prototype = { - __threadManager: null, - get _threadManager() { - if (!this.__threadManager) - this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); - return this.__threadManager; - }, - - __transport: null, - get _transport() { - if (!this.__transport) { - var transportService = Cc["@mozilla.org/network/socket-transport-service;1"].getService(Ci.nsISocketTransportService); - this.__transport = transportService.createTransport(['starttls'], - 1, // ssl only - this._host, - this._port, - null); // proxy - } - return this.__transport; - }, - - _init: function( host, port ) { - this._host = host; - this._port = port; - this._contentRead = ""; - this._buffer = null; - this.connect(); - }, - - connect: function() { - var outstream = this._transport.openOutputStream( 0, // flags - 0, // buffer size - 0 ); // number of buffers - this._outstream = outstream; - - var buffer = new InputStreamBuffer; - this._buffer = buffer; - - // Wrap input stream is C only, nonscriptable, so wrap it in scriptable - // stream: - var rawInputStream = this._transport.openInputStream( 0, 0, 0 ); - var scriptablestream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); - scriptablestream.init(rawInputStream); - - // input stream pump for asynchronous reading - var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(Ci.nsIInputStreamPump); - pump.init(rawInputStream, -1, -1, 0, 0, - false); //automatically close once all data read? - - // create dataListener class for callback: - var dataListener = { - data : "", - onStartRequest: function(request, context){ - }, - onStopRequest: function(request, context, status){ - rawInputStream.close(); - outstream.close(); - }, - onDataAvailable: function(request, context, inputStream, offset, count){ - // use scriptable stream wrapper, not "real" stream. - // count is number of bytes available, offset is position in stream. - // Do stuff with data here! - buffer.append( scriptablestream.read( count )); - } - }; - // register it: - pump.asyncRead(dataListener, null); // second argument is a context - //which gets passed in as the context argument to methods of dataListener - - //Should be done connecting now. TODO: Catch and report errors. - }, - - send: function( messageText ) { - this._outstream.write( messageText, messageText.length ); - }, - - getBinaryOutStream: function() { - var binaryOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); - binaryOutStream.setOutputStream( this._outstream ); // is this right? - return binaryOutStream; - }, - - disconnect: function() { - var thread = this._threadManager.currentThread; - while ( thread.hasPendingEvents() ) { - thread.processNextEvent( true ); - } - }, - - checkResponse: function() { - return this._getData(); - }, - - waitForResponse: function() { - var thread = this._threadManager.currentThread; - while( this._buffer.getData().length == 0 ) { - thread.processNextEvent( true ); - } - var output = this._buffer.getData(); - this._buffer.clear(); - return output; - }, - - startTLS: function() { - this._transport.securityInfo.QueryInterface(Ci.nsISSLSocketControl); - this._transport.securityInfo.StartTLS(); - } -}; - - -/** - * Send HTTP requests periodically to the server using a timer. - * HTTP POST requests with content-type application/x-www-form-urlencoded. - * responses from the server have content-type text/xml - * request and response are UTF-8 encoded (ignore what HTTP header says) - * identify session by always using set-cookie header with cookie named ID - * first request sets this to 0 to indicate new session. - */ -function HTTPPollingTransport( serverUrl, useKeys, interval ) { - this._init( serverUrl, useKeys, interval ); -} -HTTPPollingTransport.prototype = { - _init: function( serverUrl, useKeys, interval ) { - this._log = Log4Moz.repository.getLogger("Service.XmppTransportLayer"); - this._log.info("Initializing transport: serverUrl=" + serverUrl + ", useKeys=" + useKeys + ", interval=" + interval); - this._serverUrl = serverUrl - this._n = 0; - this._key = this._makeSeed(); - this._latestCookie = ""; - this._connectionId = 0; - this._callbackObject = null; - this._useKeys = useKeys; - this._interval = interval; - this._outgoingRetryBuffer = ""; - this._retryCount = 0; - this._retryCap = 0; - }, - - get _request() { - let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance( Ci.nsIXMLHttpRequest ); - request.mozBackgroundRequest = true; - this.__defineGetter__("_request", function() request); - return this._request; - }, - - __hasher: null, - get _hasher() { - if (!this.__hasher) - this.__hasher = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash ); - return this.__hasher; - }, - - __timer: null, - get _timer() { - if (!this.__timer) - this.__timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); - return this.__timer; - }, - - _makeSeed: function() { - return "foo";//"MyKeyOfHorrors"; - }, - - _advanceKey: function() { - var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Ci.nsIScriptableUnicodeConverter); - - // we use UTF-8 here, you can choose other encodings. - // TODO make configurable - converter.charset = "UTF-8"; - // result is an out parameter, - // result.value will contain the array length - var result = {}; - // data is an array of bytes - var data = converter.convertToByteArray(this._key, result); - - this._n += 1; - this._hasher.initWithString( "SHA1" ); - this._hasher.update( data, data.length ); - this._key = this._hasher.finish( true ); // true means B64encode - }, - - _setIdFromCookie: function( self, cookie ) { - // parse connection ID out of the cookie: - var cookieSegments = cookie.split( ";" ); - cookieSegments = cookieSegments[0].split( "=" ); - var newConnectionId = cookieSegments[1]; - switch( newConnectionId) { - case "0:0": - self._onError( "Unknown error!\n" ); - break; - case "-1:0": - self._onError( "Server error!\n" ); - break; - case "-2:0": - self._onError( "Bad request!\n" ); - break; - case "-3:0": - self._onError( "Key sequence error!\n" ); - break; - default : - self._connectionId = cookieSegments[1]; - this._log.debug("Connection ID set to " + self._connectionId); - break; - } - }, - - _onError: function( errorText ) { - this._log.error( errorText ); - if ( this._callbackObject != null ) { - this._callbackObject.onTransportError( errorText ); - } - this.disconnect(); - }, - - _doPost: function( requestXml ) { - var request = this._request; - var callbackObj = this._callbackObject; - var self = this; - var contents = ""; - - if ( this._useKey ) { - this._advanceKey(); - contents = this._connectionId + ";" + this._key + "," + requestXml; - } else { - contents = this._connectionId + "," + requestXml; - /* TODO: - Currently I get a "-3:0" error (key sequence error) from the 2nd - exchange if using the keys is enabled. */ - } - - var _processReqChange = function() { - // Callback for XMLHTTPRequest object state change messages - if ( request.readyState == 4 ) { - if ( request.status == 200) { - // 200 means success. - - self._log.debug("Server says: " + request.responseText); - // Look for a set-cookie header: - var latestCookie = request.getResponseHeader( "Set-Cookie" ); - if ( latestCookie.length > 0 ) { - self._setIdFromCookie( self, latestCookie ); - } - - // Respond to any text we get back from the server in response - if ( callbackObj != null && request.responseText.length > 0 ) { - callbackObj.onIncomingData( request.responseText ); - } - } else { - self._log.error( "Got HTTP status code " + request.status ); - if ( request.status == 0 ) { - /* Sometimes the server gives us HTTP status code 0 in response - to an attempt to POST. I'm not sure why this happens, but - if we re-send the POST it seems to usually work the second - time. So put the message into a buffer and try again later: - */ - if (self._retryCount >= self._retryCap) { - self._onError("Maximum number of retries reached. Unable to communicate with the server."); - } - else { - self._outgoingRetryBuffer = requestXml; - self._retryCount++; - } - } - else if (request.status == 404) { - self._onError("Provided URL is not valid."); - } - else { - self._onError("Unable to communicate with the server."); - } - } - } - }; - - try { - request.open( "POST", this._serverUrl, true ); //async = true - request.setRequestHeader( "Content-type", "application/x-www-form-urlencoded;charset=UTF-8" ); - request.setRequestHeader( "Content-length", contents.length ); - request.setRequestHeader( "Connection", "close" ); - request.onreadystatechange = _processReqChange; - this._log.debug("Sending: " + contents); - request.send( contents ); - } catch(ex) { - this._onError("Unable to send message to server: " + this._serverUrl); - this._log.error("Connection failure: " + ex); - } - }, - - send: function( messageXml ) { - this._doPost( messageXml ); - }, - - setCallbackObject: function( callbackObject ) { - this._callbackObject = callbackObject; - }, - - notify: function( timer ) { - /* having a notify method makes this object satisfy the nsITimerCallback - interface, so the object can be passed to timer.initWithCallback. */ - - /* With HTTPPolling, we need to be contacting the server on a regular - heartbeat, even if we don't have anything to send, just to see if - the server has any data it's waiting to give us. - If we have a message waiting in the outgoingRetryBuffer, send that; - otherwise send nothing. */ - var outgoingMsg = this._outgoingRetryBuffer - this._outgoingRetryBuffer = ""; - this._doPost( outgoingMsg ); - }, - - connect: function() { - // In case this is a reconnect, make sure to re-initialize. - this._init(this._serverUrl, this._useKeys, this._interval); - - /* Set up a timer to poll the server periodically. */ - - // TODO doPost isn't reentrant; don't try to doPost if there's - //already a post in progress... or can that never happen? - - this._timer.initWithCallback( this, - this._interval, - this._timer.TYPE_REPEATING_SLACK ); - }, - - disconnect: function () { - this._request.abort(); - this._timer.cancel(); - }, - - testKeys: function () { - this._key = "foo"; - this._log.debug(this._key); - for ( var x = 1; x < 7; x++ ) { - this._advanceKey(); - this._log.debug(this._key); - } - } -}; diff --git a/services/sync/modules/xmpp/xmppClient.js b/services/sync/modules/xmpp/xmppClient.js deleted file mode 100644 index 7b398293739a..000000000000 --- a/services/sync/modules/xmpp/xmppClient.js +++ /dev/null @@ -1,485 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Jono DiCarlo - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['XmppClient', 'HTTPPollingTransport', 'PlainAuthenticator', 'Md5DigestAuthenticator']; - -// See www.xulplanet.com/tutorials/mozsdk/sockets.php -// http://www.xmpp.org/specs/rfc3920.html -// http://www.process-one.net/docs/ejabberd/guide_en.html -// http://www.xulplanet.com/tutorials/mozsdk/xmlparse.php -// http://developer.mozilla.org/en/docs/xpcshell -// http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests - -// IM level protocol stuff: presence announcements, conversations, etc. -// ftp://ftp.isi.edu/in-notes/rfc3921.txt - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cu = Components.utils; - -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/xmpp/transportLayer.js"); -Cu.import("resource://weave/xmpp/authenticationLayer.js"); - -function XmppClient( clientName, realm, clientPassword, transport, authenticator ) { - this._init( clientName, realm, clientPassword, transport, authenticator ); -} -XmppClient.prototype = { - //connection status codes: - NOT_CONNECTED: 0, - CALLED_SERVER: 1, - AUTHENTICATING: 2, - CONNECTED: 3, - FAILED: -1, - - // IQ stanza status codes: - IQ_WAIT: 0, - IQ_OK: 1, - IQ_ERROR: -1, - - _init: function( clientName, realm, clientPassword, transport, authenticator ) { - this._log = Log4Moz.repository.getLogger("Service.XmppClient"); - this._myName = clientName; - this._realm = realm; - this._fullName = clientName + "@" + realm; - this._myPassword = clientPassword; - this._connectionStatus = this.NOT_CONNECTED; - this._error = null; - this._streamOpen = false; - this._transportLayer = transport; - this._authenticationLayer = authenticator; - this._log.debug("initialized auth with clientName=" + clientName + ", realm=" + realm + ", pw=" + clientPassword); - this._authenticationLayer.initialize( clientName, realm, clientPassword ); - this._messageHandlers = []; - this._iqResponders = []; - this._nextIqId = 0; - this._pendingIqs = {}; - this._callbackOnConnect = null; - }, - - __parser: null, - get _parser() { - if (!this.__parser) - this.__parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance( Ci.nsIDOMParser ); - return this.__parser; - }, - - __threadManager: null, - get _threadManager() { - if (!this.__threadManager) - this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); - return this.__threadManager; - }, - - parseError: function( streamErrorNode ) { - var error = streamErrorNode.childNodes[0]; - this._log.error( "Name: " + error.nodeName + " Value: " + error.nodeValue ); - this._error = error.nodeName; - this.disconnect(); - /* Note there can be an optional bla bla node inside - stream: error giving additional info; there can also optionally - be an app-specific condition element qualified by an app-defined - namespace */ - }, - - _finishConnectionAttempt: function() { - if ( this._callbackOnConnect ) { - this._callbackOnConnect.call(); - } - }, - - setError: function( errorText ) { - this._log.error( errorText ); - this._error = errorText; - this._connectionStatus = this.FAILED; - this._finishConnectionAttempt(); - }, - - onIncomingData: function( messageText ) { - this._log.debug("onIncomingData(): rcvd: " + messageText); - var responseDOM = this._parser.parseFromString( messageText, "text/xml" ); - - // Handle server disconnection - if (messageText.match("^$")) { - this._handleServerDisconnection(); - return; - } - - // Detect parse errors, and attempt to handle them in the valid cases. - - if (responseDOM.documentElement.nodeName == "parsererror" ) { - /* - Before giving up, remember that XMPP doesn't close the top-level - element until the communication is done; this means - that what we get from the server is often technically only an - xml fragment. Try manually appending the closing tag to simulate - a complete xml document and then parsing that. */ - - var response = messageText + this._makeClosingXml(); - responseDOM = this._parser.parseFromString( response, "text/xml" ); - } - - if ( responseDOM.documentElement.nodeName == "parsererror" ) { - /* If that still doesn't work, it might be that we're getting a fragment - with multiple top-level tags, which is a no-no. Try wrapping it - all inside one proper top-level stream element and parsing. */ - response = this._makeHeaderXml( this._fullName ) + messageText + this._makeClosingXml(); - responseDOM = this._parser.parseFromString( response, "text/xml" ); - } - - if ( responseDOM.documentElement.nodeName == "parsererror" ) { - /* Still can't parse it, give up. */ - this.setError( "Can't parse incoming XML." ); - return; - } - - // Message is parseable, now look for message-level errors. - var rootElem = responseDOM.documentElement; - var errors = rootElem.getElementsByTagName( "stream:error" ); - if ( errors.length > 0 ) { - this.setError( errors[0].firstChild.nodeName ); - return; - } - errors = rootElem.getElementsByTagName( "error" ); - if ( errors.length > 0 ) { - this.setError( errors[0].firstChild.nodeName ); - return; - } - - // Stream is valid. - // Detect and handle mid-authentication steps. - if ( this._connectionStatus == this.CALLED_SERVER ) { - // skip TLS, go straight to SALS. (encryption should be negotiated - // at the HTTP layer, i.e. use HTTPS) - - //dispatch whatever the next stage of the connection protocol is. - response = this._authenticationLayer.generateResponse( rootElem ); - if ( response == false ) { - this.setError( this._authenticationLayer.getError() ); - } else if ( response == this._authenticationLayer.COMPLETION_CODE ){ - this._connectionStatus = this.CONNECTED; - this._finishConnectionAttempt(); - } else { - this._transportLayer.send( response ); - } - return; - } - - // Detect and handle regular communication. - if ( this._connectionStatus == this.CONNECTED ) { - var presences = rootElem.getElementsByTagName( "presence" ); - if (presences.length > 0 ) { - var from = presences[0].getAttribute( "from" ); - if ( from != undefined ) { - this._log.debug( "I see that " + from + " is online." ); - } - } - - if ( rootElem.nodeName == "message" ) { - this.processIncomingMessage( rootElem ); - } else { - var messages = rootElem.getElementsByTagName( "message" ); - if (messages.length > 0 ) { - for ( var message in messages ) { - this.processIncomingMessage( messages[ message ] ); - } - } - } - - if ( rootElem.nodeName == "iq" ) { - this.processIncomingIq( rootElem ); - } else { - var iqs = rootElem.getElementsByTagName( "iq" ); - if ( iqs.length > 0 ) { - for ( var iq in iqs ) { - this.processIncomingIq( iqs[ iq ] ); - } - } - } - } - }, - - processIncomingMessage: function( messageElem ) { - this._log.debug("processIncomingMsg: messageElem is a " + messageElem); - var from = messageElem.getAttribute( "from" ); - var contentElem = messageElem.firstChild; - // Go down till we find the element with nodeType = 3 (TEXT_NODE) - while ( contentElem.nodeType != 3 ) { - contentElem = contentElem.firstChild; - } - this._log.debug("Incoming msg from " + from + ":" + contentElem.nodeValue); - for ( var x in this._messageHandlers ) { - // TODO do messages have standard place for metadata? - // will want to have handlers that trigger only on certain metadata. - this._messageHandlers[x].handle( contentElem.nodeValue, from ); - } - }, - - processIncomingIq: function( iqElem ) { - /* This processes both kinds of incoming IQ stanzas -- - ones that are new (initated by another jabber client) and those that - are responses to ones we sent out previously. We can tell the - difference by the type attribute. */ - var buddy = iqElem.getAttribute( "from " ); - var id = iqElem.getAttribute( id ); - - switch( iqElem.getAttribute( "type" ) ) { - case "get": - /* Someone is asking us for the value of a variable. - Delegate this to the registered iqResponder; package the answer - up in an IQ stanza of the same ID and send it back to the asker. */ - var variable = iqElem.firstChild.firstChild.getAttribute( "var" ); - // TODO what to do here if there's more than one registered - // iqResponder? - var value = this._iqResponders[0].get( variable ); - var query = ""; - var xml = _makeIqXml( this._fullName, buddy, "result", id, query ); - this._transportLayer.send( xml ); - break; - case "set": - /* Someone is telling us to set the value of a variable. - Delegate this to the registered iqResponder; we can reply - either with an empty iq type="result" stanza, or else an - iq type="error" stanza */ - var variable = iqElem.firstChild.firstChild.getAttribute( "var" ); - var newValue = iqElem.firstChild.firstChildgetAttribute( "value" ); - // TODO what happens when there's more than one reigistered - // responder? - // TODO give the responder a chance to say "no" and give an error. - this._iqResponders[0].set( variable, value ); - var xml = _makeIqXml( this._fullName, buddy, "result", id, "" ); - this._transportLayer.send( xml ); - break; - case "result": - /* If all is right with the universe, then the id of this iq stanza - corresponds to a set or get stanza that we sent out, so it should - be in our pending dictionary. - */ - if ( this._pendingIqs[ id ] == undefined ) { - this.setError( "Unexpected IQ reply id" + id ); - return; - } - /* The result stanza may have a query with a value in it, in - which case this is the value of the variable we requested. - If there's no value, it was probably a set query, and should - just be considred a success. */ - var newValue = iqElem.firstChild.firstChild.getAttribute( "value" ); - if ( newValue != undefined ) { - this._pendingIqs[ id ].value = newValue; - } else { - this._pendingIqs[ id ].value = true; - } - this._pendingIqs[ id ].status = this.IQ_OK; - break; - case "error": - /* Dig down through the element tree till we find the one with - the error text... */ - var elems = iqElem.getElementsByTagName( "error" ); - var errorNode = elems[0].firstChild; - if ( errorNode.nodeValue != null ) { - this.setError( errorNode.nodeValue ); - } else { - this.setError( errorNode.nodeName ); - } - if ( this._pendingIqs[ id ] != undefined ) { - this._pendingIqs[ id ].status = this.IQ_ERROR; - } - break; - } - }, - - registerMessageHandler: function( handlerObject ) { - /* messageHandler object must have - handle( messageText, from ) method. - */ - this._messageHandlers.push( handlerObject ); - }, - - registerIQResponder: function( handlerObject ) { - /* IQResponder object must have - .get( variable ) and - .set( variable, newvalue ) methods. */ - this._iqResponders.push( handlerObject ); - }, - - onTransportError: function( errorText ) { - this.setError( errorText ); - }, - - connect: function( host, callback ) { - /* Do the handshake to connect with the server and authenticate. - callback is optional: if provided, it will be called (with no arguments) - when the connection has either succeeded or failed. */ - if ( callback ) { - this._callbackOnConnect = callback; - } - this._transportLayer.connect(); - this._transportLayer.setCallbackObject( this ); - this._transportLayer.send( this._makeHeaderXml( host ) ); - this._connectionStatus = this.CALLED_SERVER; - // Now we wait... the rest of the protocol will be driven by - // onIncomingData. - }, - - _makeHeaderXml: function( recipient ) { - return ""; - }, - - _makeMessageXml: function( messageText, fullName, recipient ) { - /* a "message stanza". Note the message element must have the - full namespace info or it will be rejected. */ - var msgXml = "" + - messageText + ""; - this._log.debug( "Outgoing Message xml: " + msgXml ); - return msgXml; - }, - - _makePresenceXml: function( fullName ) { - // a "presence stanza", sent to announce my presence to the server; - // the server is supposed to multiplex this to anyone subscribed to - // presence notifications. - return ""; - }, - - _makeIqXml: function( fullName, recipient, type, id, query ) { - /* an "iq (info/query) stanza". This can be used for structured data - exchange: I send an containing a query, - and get back an containing the answer to my - query. I can also send an to set a value - remotely. The recipient answers with either or - , with an id matching the id of my set or get. */ - - //Useful!! - return "" + query + ""; - }, - - _makeClosingXml: function () { - return ""; - }, - - _generateIqId: function() { - // Each time this is called, it returns an ID that has not - // previously been used this session. - var id = "client_" + this._nextIqId; - this._nextIqId = this._nextIqId + 1; - return id; - }, - - _sendIq: function( recipient, query, type ) { - var id = this._generateIqId(); - this._pendingIqs[ id ] = { status: this.IQ_WAIT }; - this._transportLayer.send( this._makeIqXml( this._fullName, - recipient, - type, - id, - query ) ); - /* And then wait for a response with the same ID to come back... - When we get a reply, the pendingIq dictionary entry will have - its status set to IQ_OK or IQ_ERROR and, if it's IQ_OK and - this was a query that's supposed to return a value, the value - will be in the value field of the entry. */ - var thread = this._threadManager.currentThread; - while( this._pendingIqs[ id ].status == this.IQ_WAIT ) { - thread.processNextEvent( true ); - } - if ( this._pendingIqs[ id ].status == this.IQ_OK ) { - return this._pendingIqs[ id ].value; - } else if ( this._pendingIqs[ id ].status == this.IQ_ERROR ) { - return false; - } - // Can't happen? - }, - - iqGet: function( recipient, variable ) { - var query = ""; - return this._sendIq( recipient, query, "get" ); - }, - - iqSet: function( recipient, variable, value ) { - var query = ""; - return this._sendIq( recipient, query, "set" ); - }, - - sendMessage: function( recipient, messageText ) { - // OK so now I'm doing that part, but what am I supposed to do with the - // new JID that I'm bound to?? - var body = this._makeMessageXml( messageText, this._fullName, recipient ); - this._transportLayer.send( body ); - }, - - announcePresence: function() { - this._transportLayer.send( this._makePresenceXml(this._myName) ); - }, - - subscribeForPresence: function( buddyId ) { - // OK, there are 'subscriptions' and also 'rosters'...? - //this._transportLayer.send( "" ); - // TODO - // other side must then approve this by sending back a presence to - // me with type ='subscribed'. - }, - - disconnect: function() { - // todo: only send closing xml if the stream has not already been - // closed (if there was an error, the server will have closed the stream.) - this._transportLayer.send( this._makeClosingXml() ); - - this.waitForDisconnect(); - }, - - _handleServerDisconnection: function() { - this._transportLayer.disconnect(); - this._connectionStatus = this.NOT_CONNECTED; - }, - - waitForConnection: function( ) { - var thread = this._threadManager.currentThread; - while ( this._connectionStatus != this.CONNECTED && - this._connectionStatus != this.FAILED ) { - thread.processNextEvent( true ); - } - }, - - waitForDisconnect: function() { - var thread = this._threadManager.currentThread; - while ( this._connectionStatus == this.CONNECTED ) { - thread.processNextEvent( true ); - } - } -}; From 2caa01f107a26d388370e7b79bc9827cc2ccb433 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 3 Nov 2009 00:45:11 -0500 Subject: [PATCH 1452/1860] bug 526096 - add ToS link to wizard, fix some custom server and email validation stuff that popped up testing --- services/sync/locales/en-US/fx-prefs.dtd | 4 ++++ services/sync/services-sync.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 9655eed55cb8..bca1321e83b4 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -11,6 +11,10 @@ + + + + diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index ca23cc9a9acd..248273a2820a 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -2,7 +2,7 @@ pref("extensions.weave.serverURL", "@server_url@"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); pref("extensions.weave.pwChangeURL", "https://services.mozilla.com/pw/forgot.php"); -pref("extensions.weave.termsURL", "https://labs.mozilla.com/projects/weave/tos/"); +pref("extensions.weave.termsURL", "http://mozillalabs.com/weave/tos/"); pref("extensions.weave.encryption", "aes-256-cbc"); From d5bb73d899715e6565f45c85fb0693ac041d9109 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 3 Nov 2009 14:39:35 -0800 Subject: [PATCH 1453/1860] Remove unused openStatus util call for status.xul. --- services/sync/modules/util.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index ef294c474c74..643930010a21 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -727,9 +727,6 @@ let Utils = { openLog: function Utils_openLog() { Utils._openChromeWindow("Log", "log.xul"); }, - openStatus: function Utils_openStatus() { - Utils._openChromeWindow("Status", "status.xul"); - }, getErrorString: function Utils_getErrorString(error, args) { try { From cffaed137be906fa62faa3ae77cb76070b2880b2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 4 Nov 2009 15:12:29 -0800 Subject: [PATCH 1454/1860] Avoid undefined property warnings by checking for null first instead of comparing. --- services/sync/modules/engines.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 98e2e6ea444d..2338573ccb2a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -233,9 +233,9 @@ Engine.prototype = { sum: 0 }; time.forEach(function(val) { - if (val < stat.min || stat.min == null) + if (stat.min == null || val < stat.min) stat.min = val; - if (val > stat.max || stat.max == null) + if (stat.max == null || val > stat.max) stat.max = val; stat.sum += val; }); From 21273de915b95521e2e1099a95b819048ba885f9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 4 Nov 2009 15:27:08 -0800 Subject: [PATCH 1455/1860] Bug 513438 - No easy way to enable / re-enable Weave Assume Weave is always enabled (don't store it in a pref) and only disable if checks fail on startup. --- services/sync/modules/service.js | 13 +------------ services/sync/services-sync.js | 1 - 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bc7ddc6657d9..7ded3a9fac8d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -177,9 +177,6 @@ WeaveSvc.prototype = { get keyGenEnabled() { return this._keyGenEnabled; }, set keyGenEnabled(value) { this._keyGenEnabled = value; }, - get enabled() { return Svc.Prefs.get("enabled"); }, - set enabled(value) { Svc.Prefs.set("enabled", value); }, - // nextSync is in milliseconds, but prefs can't hold that much get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), @@ -270,6 +267,7 @@ WeaveSvc.prototype = { // again when username/server/etc changes _onStartup: function _onStartup() { Status.service = STATUS_OK; + this.enabled = true; this._registerEngines(); @@ -289,7 +287,6 @@ WeaveSvc.prototype = { "Weave, since it will not work correctly."); } - Utils.prefs.addObserver("", this, false); Svc.Observer.addObserver(this, "network:offline-status-changed", true); Svc.Observer.addObserver(this, "private-browsing", true); Svc.Observer.addObserver(this, "quit-application", true); @@ -381,14 +378,6 @@ WeaveSvc.prototype = { observe: function WeaveSvc__observe(subject, topic, data) { switch (topic) { - case "nsPref:changed": - switch (data) { - case "enabled": - // Potentially we'll want to reschedule syncs - this._checkSyncStatus(); - break; - } - break; case "network:offline-status-changed": // Whether online or offline, we'll reschedule syncs this._log.debug("Network offline status change: " + data); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 248273a2820a..28591c51b727 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -10,7 +10,6 @@ pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); -pref("extensions.weave.enabled", true); pref("extensions.weave.syncOnQuit.enabled", true); From 6cdb1eafce3235badd66004515449938bf024f59 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 4 Nov 2009 16:07:05 -0800 Subject: [PATCH 1456/1860] Bug 519139 - Weave freezes the browser for 5-10 secs when starting sync Correctly remove the separator getter just like for folders to prevent triggering the lazy loader on every sync. --- services/sync/modules/engines/bookmarks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 6fe3fb1f4744..79176403190c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -139,6 +139,7 @@ BookmarksEngine.prototype = { _syncFinish: function _syncFinish() { SyncEngine.prototype._syncFinish.call(this); delete this._folderTitles; + delete this._separatorPos; }, _findDupe: function _findDupe(item) { From 62f27517c71dbacffef04ee73a81d427d50d9d78 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 4 Nov 2009 16:27:35 -0800 Subject: [PATCH 1457/1860] Missing let for declaring meta. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7ded3a9fac8d..d20d36d7af59 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1177,7 +1177,7 @@ WeaveSvc.prototype = { Sync.sleep(2000); this._log.debug("Uploading new metadata record"); - meta = new WBORecord(this.metaURL); + let meta = new WBORecord(this.metaURL); meta.payload.syncID = Clients.syncID; this._updateRemoteVersion(meta); }, From e7eedb9c5699b472846c377cffdfce6d533fbad3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 4 Nov 2009 17:01:49 -0800 Subject: [PATCH 1458/1860] Bug 526661 - Use Firefox content to display the activity log Just open the file as a tab to show the activity log. Remove related xul/js/strings. Saving can be done through normal Save Page As... No manual cleaning of the log, but the size is much smaller. --- services/sync/locales/en-US/log.dtd | 3 --- services/sync/locales/en-US/log.properties | 2 -- services/sync/modules/service.js | 3 ++- services/sync/modules/util.js | 4 ---- 4 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 services/sync/locales/en-US/log.dtd delete mode 100644 services/sync/locales/en-US/log.properties diff --git a/services/sync/locales/en-US/log.dtd b/services/sync/locales/en-US/log.dtd deleted file mode 100644 index 9b980e94d523..000000000000 --- a/services/sync/locales/en-US/log.dtd +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/services/sync/locales/en-US/log.properties b/services/sync/locales/en-US/log.properties deleted file mode 100644 index de6b5673aa5a..000000000000 --- a/services/sync/locales/en-US/log.properties +++ /dev/null @@ -1,2 +0,0 @@ -noLogAvailable.alert = No log available -filePicker.title = Choose Destination File diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d20d36d7af59..8f9d302fe6d1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -335,7 +335,8 @@ WeaveSvc.prototype = { if (!verbose.exists()) verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); - this._debugApp = new Log4Moz.RotatingFileAppender(verbose, formatter); + let maxSize = 65536; // 64 * 1024 (64KB) + this._debugApp = new Log4Moz.RotatingFileAppender(verbose, formatter, maxSize); this._debugApp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.debugLog")]; root.addAppender(this._debugApp); }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 643930010a21..10d1c1e2fa1b 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -724,10 +724,6 @@ let Utils = { this.openDialog("ChangeSomething", "generic-change.xul"); }, - openLog: function Utils_openLog() { - Utils._openChromeWindow("Log", "log.xul"); - }, - getErrorString: function Utils_getErrorString(error, args) { try { return Str.errors.get(error, args || null); From 89e1231eadd945c68de5ff5876ac25f26d700bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20L=C3=B3pez?= Date: Wed, 4 Nov 2009 17:45:22 -0800 Subject: [PATCH 1459/1860] =?UTF-8?q?Bug=20515809=20-=20Use=20of=20three?= =?UTF-8?q?=20dots=20(...)=20instead=20of=20ellipsis=20(=E2=80=A6)=20on=20?= =?UTF-8?q?Weave=20filess.=20r=3DMardak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/sync/locales/en-US/fx-prefs.dtd | 2 +- services/sync/locales/en-US/generic-change.properties | 6 +++--- services/sync/locales/en-US/sync.dtd | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index bca1321e83b4..99c8690dd4da 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -33,7 +33,7 @@ - + diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties index d477881fa189..e97277727a77 100644 --- a/services/sync/locales/en-US/generic-change.properties +++ b/services/sync/locales/en-US/generic-change.properties @@ -7,7 +7,7 @@ incorrectPassword.alert = Your current password is incorrect! incorrectPassphrase.alert = Your current passphrase is incorrect! change.password.title = Change your Password -change.password.status.active = Changing your password... +change.password.status.active = Changing your password… change.password.status.success = Your password has been changed. change.password.status.error = There was an error changing your password. change.password.status.passwordSameAsPassphrase = The password cannot be the same as the passphrase. @@ -16,12 +16,12 @@ change.password.status.passwordsDoNotMatch = The passwords you entered do not ma change.password.status.badOldPassword = Your current password is incorrect. change.passphrase.title = Change your Passphrase -change.passphrase.label = Changing passphrase, please wait... +change.passphrase.label = Changing passphrase, please wait… change.passphrase.error = There was an error while changing your passphrase! change.passphrase.success = Your passphrase was successfully changed! reset.passphrase.title = Reset your Passphrase -reset.passphrase.label = Resetting passphrase, please wait... +reset.passphrase.label = Resetting passphrase, please wait… reset.passphrase.error = There was an error while resetting your passphrase! reset.passphrase.success = Your passphrase was successfully reset! diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 9a6c795fdf02..94c915e7e001 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -2,7 +2,7 @@ - + From 461d4c8c6ad47db6736458da5544588e7c708aed Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Sat, 7 Nov 2009 18:27:31 -0500 Subject: [PATCH 1460/1860] bug 526765 - fix places we aren't localized properly --- services/sync/locales/en-US/fx-prefs.dtd | 2 ++ services/sync/locales/en-US/fx-prefs.properties | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 99c8690dd4da..3f22c36941ab 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -1,6 +1,8 @@ + + diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties index 84ef406d0378..d10d065c0bda 100644 --- a/services/sync/locales/en-US/fx-prefs.properties +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -7,6 +7,11 @@ usernameNotAvailable.label = Already in use passwordMismatch.label = Passwords do not match passwordTooWeak.label = Password is too weak +errorCreatingAccount.title = Error Creating Account + serverInvalid.label = Please enter a valid server URL loginFailed = Login failed: %1$S + +recoverPasswordSuccess.title = Recover Password Success! +recoverPasswordSuccess.label = We've sent you an email to your address on file. Please check it and follow the instructions to reset your password. From 6282f4dacc5523cb1a646b1198b569d8456d8b52 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 9 Nov 2009 12:57:58 -0500 Subject: [PATCH 1461/1860] bug 518075 - tweak original patch and item values --- services/sync/modules/constants.js | 5 +++ services/sync/modules/engines/bookmarks.js | 2 +- services/sync/modules/engines/cookies.js | 8 ++-- services/sync/modules/engines/forms.js | 2 +- services/sync/modules/engines/history.js | 4 +- services/sync/modules/engines/passwords.js | 4 +- services/sync/modules/engines/prefs.js | 2 +- services/sync/modules/engines/tabs.js | 8 ++-- services/sync/modules/service.js | 43 +++++++++++++++++++++- services/sync/modules/trackers.js | 11 ++++-- 10 files changed, 69 insertions(+), 20 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index ac43742dd063..67c91b200884 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -56,6 +56,11 @@ MULTI_DESKTOP_SYNC: 60 * 60 * 1000, // 1 hour MULTI_MOBILE_SYNC: 5 * 60 * 1000, // 5 minutes PARTIAL_DATA_SYNC: 60 * 1000, // 1 minute +// score thresholds for early syncs +SINGLE_USER_THRESHOLD: 100, +MULTI_DESKTOP_THRESHOLD: 500, +MULTI_MOBILE_THRESHOLD: 1000, + // File IO Flags MODE_RDONLY: 0x01, MODE_WRONLY: 0x02, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 79176403190c..c61c9acd5627 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -969,7 +969,7 @@ BookmarksTracker.prototype = { /* Every add/remove/change is worth 10 points */ _upScore: function BMT__upScore() { - this._score += 10; + this.score += 10; }, /** diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js index 7c31eeb47279..c94c4a41bc3b 100644 --- a/services/sync/modules/engines/cookies.js +++ b/services/sync/modules/engines/cookies.js @@ -281,7 +281,7 @@ CookieTracker.prototype = { _init: function CT__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; + this.score = 0; /* cookieService can't register observers, but what we CAN do is register a general observer with the global observerService to watch for the 'cookie-changed' message. */ @@ -299,9 +299,9 @@ CookieTracker.prototype = { var newCookie = aSubject.QueryInterface( Ci.nsICookie2 ); if ( newCookie ) { if ( !newCookie.isSession ) { - /* Any modification to a persistent cookie is worth - 10 points out of 100. Ignore session cookies. */ - this._score += 10; + /* Any modification to a persistent cookie is worth + 10 points out of 100. Ignore session cookies. */ + this.score += 10; } } } diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index d24063d546de..217dd021a1a4 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -295,7 +295,7 @@ FormTracker.prototype = { this._log.trace("Logging form element: " + name + " :: " + el.value); this.addChangedID(Utils.sha1(name + el.value)); - this._score += 10; + this.score += 10; } } }; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index e92483da9c74..2e84f442a700 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -321,7 +321,7 @@ HistoryTracker.prototype = { * Clearing the whole history is worth 50 points (see below) */ _upScore: function BMT__upScore() { - this._score += 1; + this.score += 1; }, onVisit: function HT_onVisit(uri, vid, time, session, referrer, trans) { @@ -337,6 +337,6 @@ HistoryTracker.prototype = { }, onClearHistory: function HT_onClearHistory() { this._log.trace("onClearHistory"); - this._score += 50; + this.score += 500; } }; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index d221c5f54270..108a4169a955 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -238,13 +238,13 @@ PasswordTracker.prototype = { case 'addLogin': case 'removeLogin': aSubject.QueryInterface(Ci.nsILoginMetaInfo); - this._score += 15; + this.score += 15; this._log.trace(aData + ": " + aSubject.guid); this.addChangedID(aSubject.guid); break; case 'removeAllLogins': this._log.trace(aData); - this._score += 50; + this.score += 500; break; } } diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 614a83e0c108..70b5acfe12d2 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -231,7 +231,7 @@ PrefTracker.prototype = { return; if (this._syncPrefs.indexOf(aData) != -1) { - this._score += 25; + this.score += 1; this.addChangedID(WEAVE_PREFS_GUID); this._log.trace("Preference " + aData + " changed"); } diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index a336bc05e834..8014b9945995 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -426,12 +426,12 @@ TabTracker.prototype = { this._log.trace("Tab opened."); event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); - this._score += 50; + this.score += 1; }, onTabClosed: function TabTracker_onTabSelected(event) { //this._log.trace("Tab closed.\n"); - this._score += 10; + this.score += 1; }, onTabSelected: function TabTracker_onTabSelected(event) { @@ -440,14 +440,14 @@ TabTracker.prototype = { //this._log.trace("Tab selected.\n"); event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); - this._score += 10; + this.score += 1; }, // TODO: Also listen for tabs loading new content? get changedIDs() { // Only mark the current client as changed if we tracked changes let obj = {}; - if (this._score > 0) + if (this.score > 0) obj[Clients.clientID] = true; return obj; } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8f9d302fe6d1..e36092995c0c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -189,6 +189,12 @@ WeaveSvc.prototype = { }, set syncInterval(value) Svc.Prefs.set("syncInterval", value), + get syncThreshold() Svc.Prefs.get("syncThreshold", SINGLE_USER_THRESHOLD), + set syncThreshold(value) Svc.Prefs.set("nextSync", value), + + get globalScore() Svc.Prefs.get("globalScore", 0), + set globalScore(value) Svc.Prefs.set("globalScore", value), + get numClients() Svc.Prefs.get("numClients", 0), set numClients(value) Svc.Prefs.set("numClients", value), @@ -293,6 +299,7 @@ WeaveSvc.prototype = { Svc.Observer.addObserver(this, "weave:service:sync:finish", true); Svc.Observer.addObserver(this, "weave:service:sync:error", true); Svc.Observer.addObserver(this, "weave:service:backoff:interval", true); + Svc.Observer.addObserver(this, "weave:engine:score:updated", true); if (!this.enabled) this._log.info("Weave Sync disabled"); @@ -404,6 +411,9 @@ WeaveSvc.prototype = { Status.backoffInterval = interval; Status.minimumNextSync = Date.now() + data; break; + case "weave:engine:score:updated": + this._handleScoreUpdate(); + break; case "idle": this._log.trace("Idle time hit, trying to sync"); Svc.Idle.removeIdleObserver(this, IDLE_TIME); @@ -412,7 +422,35 @@ WeaveSvc.prototype = { } }, - _onQuitApplication: function WeaveSvc__onQuitApplication() { + _doGS: function () { + this._scoreTimer = null; + this._calculateScore(); + }, + + _handleScoreUpdate: function WeaveSvc__handleScoreUpdate() { + const SCORE_UPDATE_DELAY = 3000; + if (this._scoreTimer) { + this._scoreTimer.delay = SCORE_UPDATE_DELAY; + return; + } + else { + Utils.delay(function() this._doGS(), SCORE_UPDATE_DELAY, this, "_scoreTimer"); + } + }, + + _calculateScore: function WeaveSvc_calculateScoreAndDoStuff() { + var engines = Engines.getEnabled(); + for (let i = 0;i < engines.length;i++) { + this.globalScore += engines[i].score; + this._log.debug(engines[i].name + ": score: " + engines[i].score); + } + + if (this.globalScore > this.syncThreshold) { + this._log.debug("Global Score threshold hit, triggering sync."); + this.syncOnIdle(); + } + else if (!this._syncTimer) // start the clock if it isn't already + this._scheduleNextSync(); }, // These are global (for all engines) @@ -1045,6 +1083,7 @@ WeaveSvc.prototype = { // Clear out any potentially pending syncs now that we're syncing this._clearSyncTriggers(); + this.globalScore = 0; this.nextSync = 0; if (!(this._remoteSetup())) @@ -1127,6 +1166,7 @@ WeaveSvc.prototype = { let tabEngine = Engines.get("tabs"); if (numClients == 1) { this.syncInterval = SINGLE_USER_SYNC; + this.syncThreshold = SINGLE_USER_THRESHOLD; // Disable tabs sync for single client, but store the original value Svc.Prefs.set("engine.tabs.backup", tabEngine.enabled); @@ -1134,6 +1174,7 @@ WeaveSvc.prototype = { } else { this.syncInterval = hasMobile ? MULTI_MOBILE_SYNC : MULTI_DESKTOP_SYNC; + this.syncThreshold = hasMobile ? MULTI_MOBILE_THRESHOLD : MULTI_DESKTOP_THRESHOLD; // Restore the original tab enabled value tabEngine.enabled = Svc.Prefs.get("engine.tabs.backup", true); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 67ae9cbdfd27..a0848701949a 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -45,6 +45,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/ext/Observers.js"); /* * Trackers are associated with a single engine and deal with @@ -86,10 +87,12 @@ Tracker.prototype = { * Setting it to other values should (but doesn't currently) throw an exception */ get score() { - if (this._score >= 100) - return 100; - else - return this._score; + return this._score; + }, + + set score(value) { + this._score = value; + Observers.notify("weave:engine:score:updated", this.name); }, // Should be called by service everytime a sync has been done for an engine From 28e8cfc374ea5b1d9df80300297175e016e4c125 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 9 Nov 2009 14:33:53 -0500 Subject: [PATCH 1462/1860] bug 518075 - fix timer usage, reset engine score once added to globalScore --- services/sync/modules/service.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e36092995c0c..d9875528546b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -429,22 +429,19 @@ WeaveSvc.prototype = { _handleScoreUpdate: function WeaveSvc__handleScoreUpdate() { const SCORE_UPDATE_DELAY = 3000; - if (this._scoreTimer) { - this._scoreTimer.delay = SCORE_UPDATE_DELAY; - return; - } - else { - Utils.delay(function() this._doGS(), SCORE_UPDATE_DELAY, this, "_scoreTimer"); - } + Utils.delay(function() this._doGS(), SCORE_UPDATE_DELAY, this, "_scoreTimer"); }, _calculateScore: function WeaveSvc_calculateScoreAndDoStuff() { var engines = Engines.getEnabled(); for (let i = 0;i < engines.length;i++) { + this._log.trace(engines[i].name + ": score: " + engines[i].score); this.globalScore += engines[i].score; - this._log.debug(engines[i].name + ": score: " + engines[i].score); + engines[i].resetScore(); } + this._log.trace("Global score updated: " + this.globalScore); + if (this.globalScore > this.syncThreshold) { this._log.debug("Global Score threshold hit, triggering sync."); this.syncOnIdle(); @@ -1083,9 +1080,10 @@ WeaveSvc.prototype = { // Clear out any potentially pending syncs now that we're syncing this._clearSyncTriggers(); - this.globalScore = 0; this.nextSync = 0; + this.globalScore = 0; + if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; From 483e3482e4508ef9f2d7028cdda8049761693482 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 9 Nov 2009 12:56:14 -0800 Subject: [PATCH 1463/1860] Bug 527379 - Passphrase gets synced back to previous value Persist the new passphrase in the login manager to have it sync the modify (delete+add). --- services/sync/modules/service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d9875528546b..6c14fd4eafd9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -590,8 +590,9 @@ WeaveSvc.prototype = { if (!resp.success) throw resp; + // Save the new passphrase to the login manager for it to sync this.passphrase = newphrase; - + this.persistLogin(); return true; }))(), From 6c56d8de0520eb109ce90952be4c43e95dc3cff2 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 9 Nov 2009 15:57:48 -0500 Subject: [PATCH 1464/1860] bug 526940 - go back to SSL, now that it's not broken --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 28591c51b727..cc041a0682e2 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -2,7 +2,7 @@ pref("extensions.weave.serverURL", "@server_url@"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); pref("extensions.weave.pwChangeURL", "https://services.mozilla.com/pw/forgot.php"); -pref("extensions.weave.termsURL", "http://mozillalabs.com/weave/tos/"); +pref("extensions.weave.termsURL", "https://mozillalabs.com/weave/tos/"); pref("extensions.weave.encryption", "aes-256-cbc"); From fbc4ce730a73641cb5283dd3dcfcd83e0e68cb7a Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 9 Nov 2009 16:30:37 -0500 Subject: [PATCH 1465/1860] bug 518075 - fix copy/paste fail --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6c14fd4eafd9..4459ebd5a341 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -190,7 +190,7 @@ WeaveSvc.prototype = { set syncInterval(value) Svc.Prefs.set("syncInterval", value), get syncThreshold() Svc.Prefs.get("syncThreshold", SINGLE_USER_THRESHOLD), - set syncThreshold(value) Svc.Prefs.set("nextSync", value), + set syncThreshold(value) Svc.Prefs.set("syncThreshold", value), get globalScore() Svc.Prefs.get("globalScore", 0), set globalScore(value) Svc.Prefs.set("globalScore", value), From 076e0894a8723c510d62cc9bdd8f8a9a19d198e1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Nov 2009 15:24:31 -0800 Subject: [PATCH 1466/1860] Bug 527775 - Allow customizable api version on the client Add a new pref storageAPI (currently 0.5) that gets used for storage urls. --- services/sync/modules/engines.js | 4 ++-- services/sync/modules/service.js | 2 +- services/sync/services-sync.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 2338573ccb2a..f937f001d96c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -304,8 +304,8 @@ SyncEngine.prototype = { this.loadToFetch(); }, - get storageURL() Svc.Prefs.get("clusterURL") + "0.5/" + - ID.get("WeaveID").username + "/storage/", + get storageURL() Svc.Prefs.get("clusterURL") + Svc.Prefs.get("storageAPI") + + "/" + ID.get("WeaveID").username + "/storage/", get engineURL() this.storageURL + this.name, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4459ebd5a341..4e6f8c77252d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -214,7 +214,7 @@ WeaveSvc.prototype = { if (this.clusterURL == "" || this.username == "") return; - let storageAPI = this.clusterURL + "0.5/"; + let storageAPI = this.clusterURL + Svc.Prefs.get("storageAPI") + "/"; let userBase = storageAPI + this.username + "/"; this._log.debug("Caching URLs under storage user base: " + userBase); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index cc041a0682e2..1659197557aa 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,5 @@ pref("extensions.weave.serverURL", "@server_url@"); +pref("extensions.weave.storageAPI", "0.5"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); pref("extensions.weave.pwChangeURL", "https://services.mozilla.com/pw/forgot.php"); From 857595974408efe6e97234a5100d4702dd7907f0 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 10 Nov 2009 15:52:40 -0800 Subject: [PATCH 1467/1860] Bug 527766 - Sync history pages with a single visit Backout bug 518972 (6954c93b8903) so now we push all data instead of pages with more than 1 visit. --- services/sync/modules/engines.js | 6 +----- services/sync/modules/engines/history.js | 17 ----------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f937f001d96c..80f2c743dd4c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -339,10 +339,6 @@ SyncEngine.prototype = { this._toFetch = o)); }, - _makeUploadColl: function _makeUploadColl() { - return new Collection(this.engineURL); - }, - // Create a new record by querying the store, and add the engine metadata _createRecord: function SyncEngine__createRecord(id) { return this._store.createRecord(id, this.cryptoMetaURL); @@ -598,7 +594,7 @@ SyncEngine.prototype = { this._log.debug("Preparing " + outnum + " outgoing records"); // collection we'll upload - let up = this._makeUploadColl(); + let up = new Collection(this.engineURL); let count = 0; // Upload what we've got so far in the collection diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 2e84f442a700..c694a44249a4 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -87,19 +87,6 @@ HistoryEngine.prototype = { _findDupe: function _findDupe(item) { return GUIDForUri(item.histUri); - }, - - _makeUploadColl: function _makeUploadColl() { - let coll = SyncEngine.prototype._makeUploadColl.call(this); - let origPush = coll.pushData; - - // Only push the data for upload if it has more than 1 visit - coll.pushData = function(data) { - if (data._visitCount > 1) - origPush.call(coll, data); - }; - - return coll; } }; @@ -273,10 +260,6 @@ HistoryStore.prototype = { record.sortindex = foo.frecency; record.visits = this._getVisits(record.histUri); record.encryption = cryptoMetaURL; - - // XXX Add a value to the object so that pushData can read out, but we end - // up encrypting the data before calling pushData :( - record._visitCount = record.visits.length; } else record.deleted = true; From 2f707f03785a59b800a66abfa0d3f1f976d1ac96 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 11 Nov 2009 15:19:00 -0800 Subject: [PATCH 1468/1860] Reset the score when calculating the global score. --- services/sync/modules/service.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4e6f8c77252d..eb5e6d4426f4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -422,14 +422,9 @@ WeaveSvc.prototype = { } }, - _doGS: function () { - this._scoreTimer = null; - this._calculateScore(); - }, - _handleScoreUpdate: function WeaveSvc__handleScoreUpdate() { const SCORE_UPDATE_DELAY = 3000; - Utils.delay(function() this._doGS(), SCORE_UPDATE_DELAY, this, "_scoreTimer"); + Utils.delay(this._calculateScore, SCORE_UPDATE_DELAY, this, "_scoreTimer"); }, _calculateScore: function WeaveSvc_calculateScoreAndDoStuff() { @@ -437,7 +432,7 @@ WeaveSvc.prototype = { for (let i = 0;i < engines.length;i++) { this._log.trace(engines[i].name + ": score: " + engines[i].score); this.globalScore += engines[i].score; - engines[i].resetScore(); + engines[i]._tracker.resetScore(); } this._log.trace("Global score updated: " + this.globalScore); From 328cd9d50c0a8fa21005fc547d76427eeb98e905 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 11 Nov 2009 15:20:19 -0800 Subject: [PATCH 1469/1860] Remove unwanted info logging for bookmark indices. --- services/sync/modules/engines/bookmarks.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c61c9acd5627..77674b852752 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -794,7 +794,6 @@ BookmarksStore.prototype = { } } - this._log.info("calculating index for: " + record.title + " = " + index); return index; }, From 9358b283bca0a2882c7a85aac7bcc320516e279d Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 12 Nov 2009 13:11:54 -0500 Subject: [PATCH 1470/1860] bug 526569 - add explicit choice to setup flow for additional computers --- services/sync/locales/en-US/fx-prefs.dtd | 20 +++++++++++++++++++- services/sync/modules/service.js | 16 +++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 3f22c36941ab..003ee97cfafd 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -29,7 +29,6 @@ - @@ -37,6 +36,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eb5e6d4426f4..f824d0077a54 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -405,6 +405,7 @@ WeaveSvc.prototype = { case "weave:service:sync:finish": this._scheduleNextSync(); this._syncErrors = 0; + Svc.Prefs.reset("firstSync"); break; case "weave:service:backoff:interval": let interval = data + Math.random() * data * 0.25; // required backoff + up to 25% @@ -1058,7 +1059,20 @@ WeaveSvc.prototype = { sync: function sync() this._catch(this._lock(this._notify("sync", "", function() { Status.resetSync(); - + if (Svc.Prefs.isSet("firstSync")) { + switch(Svc.Prefs.get("firstSync")) { + case "wipeClient": + this.wipeClient(); + break; + case "wipeRemote": + this.wipeRemote(); + break; + default: + this._scheduleNextSync(); + return; + } + } + // if we don't have a node, get one. if that fails, retry in 10 minutes if (this.clusterURL == "" && !this._setCluster()) { this._scheduleNextSync(10 * 60 * 1000); From a17e5af5145624f6cb6c1e18c7488312a15c3454 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 12 Nov 2009 13:42:56 -0500 Subject: [PATCH 1471/1860] Bug 528239 - hook up reset passphrase dialog --- services/sync/locales/en-US/fx-prefs.dtd | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 003ee97cfafd..6fc199535bda 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -64,6 +64,7 @@ + From 2a0d12dc9427dd99621fef9f6d32e7fa64cfbb12 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Nov 2009 11:54:21 -0800 Subject: [PATCH 1472/1860] Bug 528278 - Remove remote commands and wait for user on wipeRemote/changePassphrase Store the reason for starting fresh (new syncId) in meta/global and fail remoteSetup on certain reasons to let the UI show a notification to the user for a response (pick merge or change passphrase). Code paths (sync, prep, etc.) related to remote commands are removed. --- services/sync/locales/en-US/sync.properties | 7 +- services/sync/modules/constants.js | 5 +- services/sync/modules/service.js | 206 ++------------------ 3 files changed, 28 insertions(+), 190 deletions(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 1b0ea4c1b456..428d8a45ef21 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -15,12 +15,13 @@ status.privateBrowsing = Disabled (Private Browsing) error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. -# should decide if we're going to show this -error.logout.title = Error While Signing Out -error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it. error.sync.title = Error While Syncing error.sync.description = Weave encountered an error while syncing: %1$S. Weave will automatically retry this action. error.sync.no_node_found = The Weave server is a little busy right now, but you don't need to do anything about it. We'll start syncing your data as soon as we can! +error.sync.wipe_remote = You requested to replace the data here with your other client. Please choose how you want to merge your data for this client. +error.sync.wipe_remote.label = Choose Merge +error.sync.wipe_remote.accesskey = C +error.sync.reset_passphrase = You changed the passphrase on another client, so please re-connect with the new passphrase. error.sync.no_node_found.title = Sync Delay error.sync.tryAgainButton.label = Sync Now error.sync.tryAgainButton.accesskey = S diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 67c91b200884..2a37504cd30f 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -106,7 +106,6 @@ KEYS_DOWNLOAD_FAIL: "error.sync.reason.keys_download_fail", NO_KEYS_NO_KEYGEN: "error.sync.reason.no_keys_no_keygen", KEYS_UPLOAD_FAIL: "error.sync.reason.keys_upload_fail", SETUP_FAILED_NO_PASSPHRASE: "error.sync.reason.setup_failed_no_passphrase", -ABORT_SYNC_COMMAND: "aborting sync, process commands said so", NO_SYNC_NODE_FOUND: "error.sync.reason.no_node_found", // engine failure status codes @@ -115,6 +114,10 @@ ENGINE_DOWNLOAD_FAIL: "error.engine.reason.record_download_fail ENGINE_UNKNOWN_FAIL: "error.engine.reason.unknown_fail", ENGINE_METARECORD_UPLOAD_FAIL: "error.engine.reason.metarecord_upload_fail", +// reasons for changing the sync ID +SYNCID_WIPE_REMOTE: "error.sync.wipe_remote", +SYNCID_RESET_PASSPHRASE: "error.sync.reset_passphrase", + // Ways that a sync can be disabled (messages only to be printed in debug log) kSyncWeaveDisabled: "Weave is disabled", kSyncNotLoggedIn: "User is not logged in", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f824d0077a54..2363001f85ec 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -443,7 +443,7 @@ WeaveSvc.prototype = { this.syncOnIdle(); } else if (!this._syncTimer) // start the clock if it isn't already - this._scheduleNextSync(); + this._checkSyncStatus(); }, // These are global (for all engines) @@ -616,26 +616,13 @@ WeaveSvc.prototype = { resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) this._catch(this._notify("resetpph", "", function() { - /* Make remote commands ready so we have a list of clients beforehand */ - this.prepCommand("logout", []); - let clientsBackup = Clients._store.clients; - - /* Wipe */ - this.wipeServer(); - PubKeys.clearCache(); - PrivKeys.clearCache(); - - /* Set remote commands before syncing */ - Clients._store.clients = clientsBackup; - let username = this.username; - let password = this.password; - this.logout(); - - /* Set this so UI is updated on next run */ + // Save the new passphrase this.passphrase = newphrase; + this.persistLogin(); - /* Login in sync: this also generates new keys */ - this.login(username, password, newphrase); + // Start over by wiping the server and reuploading + this.login(); + this._freshStart(SYNCID_RESET_PASSPHRASE); this.sync(true); return true; }))(), @@ -806,8 +793,6 @@ WeaveSvc.prototype = { // stuff we need to to after login, before we can really do // anything (e.g. key setup) _remoteSetup: function WeaveSvc__remoteSetup() { - let reset = false; - this._log.debug("Fetching global metadata record"); let meta = Records.import(this.metaURL); @@ -843,7 +828,6 @@ WeaveSvc.prototype = { Status.sync = DESKTOP_VERSION_OUT_OF_DATE; return false; } - reset = true; this._log.info("Wiping server data"); this._freshStart(); @@ -861,13 +845,19 @@ WeaveSvc.prototype = { return false; } else if (meta.payload.syncID != Clients.syncID) { - this._log.warn("Meta.payload.syncID is " + meta.payload.syncID + - ", Clients.syncID is " + Clients.syncID); + let reason = meta.payload.reason; this.resetClient(); - this._log.info("Reset client because of syncID mismatch."); + this._log.debug("Reset client on syncID mismatch: " + reason); Clients.syncID = meta.payload.syncID; - this._log.info("Reset the client after a server/client sync ID mismatch"); this._updateRemoteVersion(meta); + + // For some syncID changes, we might want to wait for the user + switch (reason) { + case SYNCID_WIPE_REMOTE: + case SYNCID_RESET_PASSPHRASE: + Status.sync = reason; + return false; + } } // We didn't wipe the server and we're not out of date, so update remote else @@ -908,12 +898,6 @@ WeaveSvc.prototype = { return false; } - if (!reset) { - this._log.warn("Calling freshStart from !reset case."); - this._freshStart(); - this._log.info("Server data wiped to ensure consistency due to missing keys"); - } - let passphrase = ID.get("WeaveCryptoID"); if (passphrase.password) { let keys = PubKeys.createKeypair(passphrase, PubKeys.defaultKeyUri, @@ -1109,24 +1093,6 @@ WeaveSvc.prototype = { this._log.trace("Refreshing client list"); Clients.sync(); - // Process the incoming commands if we have any - if (Clients.getClients()[Clients.clientID].commands) { - try { - if (!(this.processCommands())) { - Status.sync = ABORT_SYNC_COMMAND; - throw "aborting sync, process commands said so"; - } - - // Repeat remoteSetup in-case the commands forced us to reset - if (!(this._remoteSetup())) - throw "aborting sync, remote setup failed after processing commands"; - } - finally { - // Always immediately push back the local client (now without commands) - Clients.sync(); - } - } - // Update the client mode now because it might change what we sync this._updateClientMode(); @@ -1217,18 +1183,17 @@ WeaveSvc.prototype = { } }, - _freshStart: function WeaveSvc__freshStart() { + _freshStart: function WeaveSvc__freshStart(reason) { this.resetClient(); - this._log.info("Reset client data from freshStart."); - this._log.info("Client metadata wiped, deleting server data"); + this._log.info("Resetting client and wiping server: " + reason); this.wipeServer(); // XXX Bug 504125 Wait a while after wiping so that the DELETEs replicate Sync.sleep(2000); - this._log.debug("Uploading new metadata record"); let meta = new WBORecord(this.metaURL); meta.payload.syncID = Clients.syncID; + meta.payload.reason = reason; this._updateRemoteVersion(meta); }, @@ -1331,16 +1296,7 @@ WeaveSvc.prototype = { wipeRemote: function WeaveSvc_wipeRemote(engines) this._catch(this._notify("wipe-remote", "", function() { // Clear out any server data - //this.wipeServer(engines); - - // Only wipe the engines provided - if (engines) { - engines.forEach(function(e) this.prepCommand("wipeEngine", [e]), this); - return; - } - - // Tell the remote machines to wipe themselves - this.prepCommand("wipeAll", []); + this._freshStart(SYNCID_WIPE_REMOTE); }))(), /** @@ -1394,126 +1350,4 @@ WeaveSvc.prototype = { this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); } }))(), - - /** - * 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", 0, "Clear temporary local data for all engines"], - ["resetEngine", 1, "Clear temporary local data for engine"], - ["wipeAll", 0, "Delete all client data for all engines"], - ["wipeEngine", 1, "Delete all client data for engine"], - ["logout", 0, "Log out client"], - ].reduce(function WeaveSvc__commands(commands, entry) { - commands[entry[0]] = {}; - for (let [i, attr] in Iterator(["args", "desc"])) - commands[entry[0]][attr] = entry[i + 1]; - return commands; - }, {}), - - /** - * Check if the local client has any remote commands and perform them. - * - * @return False to abort sync - */ - processCommands: function WeaveSvc_processCommands() - this._notify("process-commands", "", function() { - let info = Clients.getInfo(Clients.clientID); - let commands = info.commands; - - // Immediately clear out the commands as we've got them locally - delete info.commands; - Clients.setInfo(Clients.clientID, info); - - // Process each command in order - for each ({command: command, args: args} in commands) { - this._log.debug("Processing command: " + command + "(" + args + ")"); - - let engines = [args[0]]; - switch (command) { - case "resetAll": - engines = null; - // Fallthrough - case "resetEngine": - this.resetClient(engines); - break; - case "wipeAll": - engines = null; - // Fallthrough - case "wipeEngine": - this.wipeClient(engines); - break; - case "logout": - this.logout(); - return false; - default: - this._log.debug("Received an unknown command: " + command); - break; - } - } - - return true; - })(), - - /** - * Prepare to send a command to each remote client. Calling this doesn't - * actually sync the command data to the server. If the client already has - * the command/args pair, it won't get a duplicate action. - * - * @param command - * Command to invoke on remote clients - * @param args - * Array of arguments to give to the command - */ - prepCommand: function WeaveSvc_prepCommand(command, args) { - let commandData = this._commands[command]; - // Don't send commands that we don't know about - if (commandData == null) { - this._log.error("Unknown command to send: " + command); - return; - } - // Don't send a command with the wrong number of arguments - else if (args == null || args.length != commandData.args) { - this._log.error("Expected " + commandData.args + " args for '" + - command + "', but got " + args); - return; - } - - // Package the command/args pair into an object - let action = { - command: command, - args: args, - }; - let actionStr = command + "(" + args + ")"; - - // Convert args into a string to simplify array comparisons - let jsonArgs = JSON.stringify(args); - let notDupe = function(action) action.command != command || - JSON.stringify(action.args) != jsonArgs; - - this._log.info("Sending clients: " + actionStr + "; " + commandData.desc); - - // Add the action to each remote client - for (let guid in Clients.getClients()) { - // Don't send commands to the local client - if (guid == Clients.clientID) - continue; - - let info = Clients.getInfo(guid); - // Set the action to be a new commands array if none exists - if (info.commands == null) - info.commands = [action]; - // Add the new action if there are no duplicates - else if (info.commands.every(notDupe)) - info.commands.push(action); - // Must have been a dupe.. skip! - else - continue; - - Clients.setInfo(guid, info); - this._log.trace("Client " + guid + " got a new action: " + actionStr); - } - }, }; From e3ec4799cc7ac4efc805467d76a13bd9bf8d6ceb Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 12 Nov 2009 15:44:33 -0500 Subject: [PATCH 1473/1860] Backed out changeset 23d90c46b89c --- services/sync/locales/en-US/sync.properties | 7 +- services/sync/modules/constants.js | 5 +- services/sync/modules/service.js | 208 ++++++++++++++++++-- 3 files changed, 191 insertions(+), 29 deletions(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 428d8a45ef21..1b0ea4c1b456 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -15,13 +15,12 @@ status.privateBrowsing = Disabled (Private Browsing) error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. +# should decide if we're going to show this +error.logout.title = Error While Signing Out +error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it. error.sync.title = Error While Syncing error.sync.description = Weave encountered an error while syncing: %1$S. Weave will automatically retry this action. error.sync.no_node_found = The Weave server is a little busy right now, but you don't need to do anything about it. We'll start syncing your data as soon as we can! -error.sync.wipe_remote = You requested to replace the data here with your other client. Please choose how you want to merge your data for this client. -error.sync.wipe_remote.label = Choose Merge -error.sync.wipe_remote.accesskey = C -error.sync.reset_passphrase = You changed the passphrase on another client, so please re-connect with the new passphrase. error.sync.no_node_found.title = Sync Delay error.sync.tryAgainButton.label = Sync Now error.sync.tryAgainButton.accesskey = S diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 2a37504cd30f..67c91b200884 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -106,6 +106,7 @@ KEYS_DOWNLOAD_FAIL: "error.sync.reason.keys_download_fail", NO_KEYS_NO_KEYGEN: "error.sync.reason.no_keys_no_keygen", KEYS_UPLOAD_FAIL: "error.sync.reason.keys_upload_fail", SETUP_FAILED_NO_PASSPHRASE: "error.sync.reason.setup_failed_no_passphrase", +ABORT_SYNC_COMMAND: "aborting sync, process commands said so", NO_SYNC_NODE_FOUND: "error.sync.reason.no_node_found", // engine failure status codes @@ -114,10 +115,6 @@ ENGINE_DOWNLOAD_FAIL: "error.engine.reason.record_download_fail ENGINE_UNKNOWN_FAIL: "error.engine.reason.unknown_fail", ENGINE_METARECORD_UPLOAD_FAIL: "error.engine.reason.metarecord_upload_fail", -// reasons for changing the sync ID -SYNCID_WIPE_REMOTE: "error.sync.wipe_remote", -SYNCID_RESET_PASSPHRASE: "error.sync.reset_passphrase", - // Ways that a sync can be disabled (messages only to be printed in debug log) kSyncWeaveDisabled: "Weave is disabled", kSyncNotLoggedIn: "User is not logged in", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2363001f85ec..f824d0077a54 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -443,7 +443,7 @@ WeaveSvc.prototype = { this.syncOnIdle(); } else if (!this._syncTimer) // start the clock if it isn't already - this._checkSyncStatus(); + this._scheduleNextSync(); }, // These are global (for all engines) @@ -616,13 +616,26 @@ WeaveSvc.prototype = { resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) this._catch(this._notify("resetpph", "", function() { - // Save the new passphrase - this.passphrase = newphrase; - this.persistLogin(); + /* Make remote commands ready so we have a list of clients beforehand */ + this.prepCommand("logout", []); + let clientsBackup = Clients._store.clients; - // Start over by wiping the server and reuploading - this.login(); - this._freshStart(SYNCID_RESET_PASSPHRASE); + /* Wipe */ + this.wipeServer(); + PubKeys.clearCache(); + PrivKeys.clearCache(); + + /* Set remote commands before syncing */ + Clients._store.clients = clientsBackup; + let username = this.username; + let password = this.password; + this.logout(); + + /* Set this so UI is updated on next run */ + this.passphrase = newphrase; + + /* Login in sync: this also generates new keys */ + this.login(username, password, newphrase); this.sync(true); return true; }))(), @@ -793,6 +806,8 @@ WeaveSvc.prototype = { // stuff we need to to after login, before we can really do // anything (e.g. key setup) _remoteSetup: function WeaveSvc__remoteSetup() { + let reset = false; + this._log.debug("Fetching global metadata record"); let meta = Records.import(this.metaURL); @@ -828,6 +843,7 @@ WeaveSvc.prototype = { Status.sync = DESKTOP_VERSION_OUT_OF_DATE; return false; } + reset = true; this._log.info("Wiping server data"); this._freshStart(); @@ -845,19 +861,13 @@ WeaveSvc.prototype = { return false; } else if (meta.payload.syncID != Clients.syncID) { - let reason = meta.payload.reason; + this._log.warn("Meta.payload.syncID is " + meta.payload.syncID + + ", Clients.syncID is " + Clients.syncID); this.resetClient(); - this._log.debug("Reset client on syncID mismatch: " + reason); + this._log.info("Reset client because of syncID mismatch."); Clients.syncID = meta.payload.syncID; + this._log.info("Reset the client after a server/client sync ID mismatch"); this._updateRemoteVersion(meta); - - // For some syncID changes, we might want to wait for the user - switch (reason) { - case SYNCID_WIPE_REMOTE: - case SYNCID_RESET_PASSPHRASE: - Status.sync = reason; - return false; - } } // We didn't wipe the server and we're not out of date, so update remote else @@ -898,6 +908,12 @@ WeaveSvc.prototype = { return false; } + if (!reset) { + this._log.warn("Calling freshStart from !reset case."); + this._freshStart(); + this._log.info("Server data wiped to ensure consistency due to missing keys"); + } + let passphrase = ID.get("WeaveCryptoID"); if (passphrase.password) { let keys = PubKeys.createKeypair(passphrase, PubKeys.defaultKeyUri, @@ -1093,6 +1109,24 @@ WeaveSvc.prototype = { this._log.trace("Refreshing client list"); Clients.sync(); + // Process the incoming commands if we have any + if (Clients.getClients()[Clients.clientID].commands) { + try { + if (!(this.processCommands())) { + Status.sync = ABORT_SYNC_COMMAND; + throw "aborting sync, process commands said so"; + } + + // Repeat remoteSetup in-case the commands forced us to reset + if (!(this._remoteSetup())) + throw "aborting sync, remote setup failed after processing commands"; + } + finally { + // Always immediately push back the local client (now without commands) + Clients.sync(); + } + } + // Update the client mode now because it might change what we sync this._updateClientMode(); @@ -1183,17 +1217,18 @@ WeaveSvc.prototype = { } }, - _freshStart: function WeaveSvc__freshStart(reason) { + _freshStart: function WeaveSvc__freshStart() { this.resetClient(); - this._log.info("Resetting client and wiping server: " + reason); + this._log.info("Reset client data from freshStart."); + this._log.info("Client metadata wiped, deleting server data"); this.wipeServer(); // XXX Bug 504125 Wait a while after wiping so that the DELETEs replicate Sync.sleep(2000); + this._log.debug("Uploading new metadata record"); let meta = new WBORecord(this.metaURL); meta.payload.syncID = Clients.syncID; - meta.payload.reason = reason; this._updateRemoteVersion(meta); }, @@ -1296,7 +1331,16 @@ WeaveSvc.prototype = { wipeRemote: function WeaveSvc_wipeRemote(engines) this._catch(this._notify("wipe-remote", "", function() { // Clear out any server data - this._freshStart(SYNCID_WIPE_REMOTE); + //this.wipeServer(engines); + + // Only wipe the engines provided + if (engines) { + engines.forEach(function(e) this.prepCommand("wipeEngine", [e]), this); + return; + } + + // Tell the remote machines to wipe themselves + this.prepCommand("wipeAll", []); }))(), /** @@ -1350,4 +1394,126 @@ WeaveSvc.prototype = { this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); } }))(), + + /** + * 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", 0, "Clear temporary local data for all engines"], + ["resetEngine", 1, "Clear temporary local data for engine"], + ["wipeAll", 0, "Delete all client data for all engines"], + ["wipeEngine", 1, "Delete all client data for engine"], + ["logout", 0, "Log out client"], + ].reduce(function WeaveSvc__commands(commands, entry) { + commands[entry[0]] = {}; + for (let [i, attr] in Iterator(["args", "desc"])) + commands[entry[0]][attr] = entry[i + 1]; + return commands; + }, {}), + + /** + * Check if the local client has any remote commands and perform them. + * + * @return False to abort sync + */ + processCommands: function WeaveSvc_processCommands() + this._notify("process-commands", "", function() { + let info = Clients.getInfo(Clients.clientID); + let commands = info.commands; + + // Immediately clear out the commands as we've got them locally + delete info.commands; + Clients.setInfo(Clients.clientID, info); + + // Process each command in order + for each ({command: command, args: args} in commands) { + this._log.debug("Processing command: " + command + "(" + args + ")"); + + let engines = [args[0]]; + switch (command) { + case "resetAll": + engines = null; + // Fallthrough + case "resetEngine": + this.resetClient(engines); + break; + case "wipeAll": + engines = null; + // Fallthrough + case "wipeEngine": + this.wipeClient(engines); + break; + case "logout": + this.logout(); + return false; + default: + this._log.debug("Received an unknown command: " + command); + break; + } + } + + return true; + })(), + + /** + * Prepare to send a command to each remote client. Calling this doesn't + * actually sync the command data to the server. If the client already has + * the command/args pair, it won't get a duplicate action. + * + * @param command + * Command to invoke on remote clients + * @param args + * Array of arguments to give to the command + */ + prepCommand: function WeaveSvc_prepCommand(command, args) { + let commandData = this._commands[command]; + // Don't send commands that we don't know about + if (commandData == null) { + this._log.error("Unknown command to send: " + command); + return; + } + // Don't send a command with the wrong number of arguments + else if (args == null || args.length != commandData.args) { + this._log.error("Expected " + commandData.args + " args for '" + + command + "', but got " + args); + return; + } + + // Package the command/args pair into an object + let action = { + command: command, + args: args, + }; + let actionStr = command + "(" + args + ")"; + + // Convert args into a string to simplify array comparisons + let jsonArgs = JSON.stringify(args); + let notDupe = function(action) action.command != command || + JSON.stringify(action.args) != jsonArgs; + + this._log.info("Sending clients: " + actionStr + "; " + commandData.desc); + + // Add the action to each remote client + for (let guid in Clients.getClients()) { + // Don't send commands to the local client + if (guid == Clients.clientID) + continue; + + let info = Clients.getInfo(guid); + // Set the action to be a new commands array if none exists + if (info.commands == null) + info.commands = [action]; + // Add the new action if there are no duplicates + else if (info.commands.every(notDupe)) + info.commands.push(action); + // Must have been a dupe.. skip! + else + continue; + + Clients.setInfo(guid, info); + this._log.trace("Client " + guid + " got a new action: " + actionStr); + } + }, }; From a4a58c76d4ed3cde124334c044e60f12802339eb Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 12 Nov 2009 16:16:19 -0500 Subject: [PATCH 1474/1860] bug 527773 - warn noscript users when trying to use setup wizard --- services/sync/locales/en-US/fx-prefs.dtd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 6fc199535bda..965a24596a35 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -29,6 +29,8 @@ + + From ec749dab1d19cb7d678ab07928855df8394900c8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Nov 2009 13:49:41 -0800 Subject: [PATCH 1475/1860] Bug 528090 - When logging out, stay disconnected until told to connect Toggle the autoconnect pref on login/logout. --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f824d0077a54..b4c09bd195c9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -710,6 +710,7 @@ WeaveSvc.prototype = { this._loggedIn = true; // Try starting the sync timer now that we're logged in this._checkSyncStatus(); + Svc.Prefs.set("autoconnect", true); return true; })))(), @@ -725,6 +726,7 @@ WeaveSvc.prototype = { // Cancel the sync timer now that we're logged out this._checkSyncStatus(); + Svc.Prefs.set("autoconnect", false); Svc.Observer.notifyObservers(null, "weave:service:logout:finish", ""); }, From 48701ea2860112160a971e5f728d2ec8069a93e3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Nov 2009 13:51:28 -0800 Subject: [PATCH 1476/1860] Bug 527767 - Sync more history items on first sync Syncing 1000 items should be good enough for anybody...... --- services/sync/modules/engines/history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index c694a44249a4..033510416745 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -189,7 +189,7 @@ HistoryStore.prototype = { options = this._hsvc.getNewQueryOptions(); query.minVisits = 1; - options.maxResults = 100; + options.maxResults = 1000; options.sortingMode = options.SORT_BY_DATE_DESCENDING; options.queryType = options.QUERY_TYPE_HISTORY; From db987c87c5b6aad2d91d9af21b248b8537cc6b23 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Nov 2009 14:48:54 -0800 Subject: [PATCH 1477/1860] Bug 528343 - Tabs don't sync Set the current client on any tab change for tracker.changedIDs and reset that when sync finishes. --- services/sync/modules/engines/tabs.js | 33 +++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 8014b9945995..2b3e0fb5a864 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -79,6 +79,11 @@ TabEngine.prototype = { this._store.wipe(); }, + _syncFinish: function _syncFinish() { + SyncEngine.prototype._syncFinish.call(this); + this._tracker.resetChanged(); + }, + /* The intent is not to show tabs in the menu if they're already * open locally. There are a couple ways to interpret this: for * instance, we could do it by removing a tab from the list when @@ -349,6 +354,7 @@ TabTracker.prototype = { _TabTracker_init: function TabTracker__init() { this._init(); + this.resetChanged(); // Make sure "this" pointer is always set correctly for event listeners this.onTabOpened = Utils.bind2(this, this.onTabOpened); @@ -421,34 +427,33 @@ TabTracker.prototype = { } }, + _upScore: function _upScore(amount) { + this.score += amount; + this._changedIDs[Clients.clientID] = true; + }, + onTabOpened: function TabTracker_onTabOpened(event) { // Store a timestamp in the tab to track when it was last used this._log.trace("Tab opened."); event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); - //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); - this.score += 1; + this._upScore(1); }, onTabClosed: function TabTracker_onTabSelected(event) { - //this._log.trace("Tab closed.\n"); - this.score += 1; + this._log.trace("Tab closed."); + this._upScore(1); }, onTabSelected: function TabTracker_onTabSelected(event) { // Update the tab's timestamp this._log.trace("Tab selected."); - //this._log.trace("Tab selected.\n"); event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); - //this._log.debug("Tab timestamp set to " + event.target.getAttribute(TAB_TIME_ATTR) + "\n"); - this.score += 1; + this._upScore(1); }, // TODO: Also listen for tabs loading new content? - get changedIDs() { - // Only mark the current client as changed if we tracked changes - let obj = {}; - if (this.score > 0) - obj[Clients.clientID] = true; - return obj; - } + get changedIDs() this._changedIDs, + + // Provide a way to empty out the changed ids + resetChanged: function resetChanged() this._changedIDs = {} } From a3beccfdd97bb32f539d44c357adbde75e7e1c40 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Nov 2009 15:18:43 -0800 Subject: [PATCH 1478/1860] Bug 526942 - Try to sync tabs when viewing remote tabs Try fetching tabs when loading the remote tabs view and if the sync got new tabs, reload the page. --- services/sync/modules/engines.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 80f2c743dd4c..db6468dd7db1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -410,6 +410,10 @@ SyncEngine.prototype = { let count = {applied: 0, reconciled: 0}; let handled = []; newitems.recordHandler = Utils.bind2(this, function(item) { + // Grab a later last modified if possible + if (this.lastModified == null || item.modified > this.lastModified) + this.lastModified = item.modified; + // Remember which records were processed handled.push(item.id); @@ -432,7 +436,7 @@ SyncEngine.prototype = { }); // Only bother getting data from the server if there's new things - if (this.lastModified > this.lastSync) { + if (this.lastModified == null || this.lastModified > this.lastSync) { let resp = newitems.get(); if (!resp.success) { resp.failureCode = ENGINE_DOWNLOAD_FAIL; From d01aba28d91b1a6290fd159904dc229f63c0a2b3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Nov 2009 16:35:56 -0800 Subject: [PATCH 1479/1860] Bug 528356 - Bump storageAPI version to 1.0 --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 1659197557aa..2d3c03d769ac 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,5 +1,5 @@ pref("extensions.weave.serverURL", "@server_url@"); -pref("extensions.weave.storageAPI", "0.5"); +pref("extensions.weave.storageAPI", "1.0"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); pref("extensions.weave.pwChangeURL", "https://services.mozilla.com/pw/forgot.php"); From 81dd1c1ee3d0eaccc49d2b7c579ebc2dc7c494ee Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 12 Nov 2009 19:25:43 -0800 Subject: [PATCH 1480/1860] Comment out some trace logging in service.main for now (they always appear). --- services/sync/modules/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b4c09bd195c9..7fc72b23fa43 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -431,12 +431,12 @@ WeaveSvc.prototype = { _calculateScore: function WeaveSvc_calculateScoreAndDoStuff() { var engines = Engines.getEnabled(); for (let i = 0;i < engines.length;i++) { - this._log.trace(engines[i].name + ": score: " + engines[i].score); + //this._log.trace(engines[i].name + ": score: " + engines[i].score); this.globalScore += engines[i].score; engines[i]._tracker.resetScore(); } - this._log.trace("Global score updated: " + this.globalScore); + //this._log.trace("Global score updated: " + this.globalScore); if (this.globalScore > this.syncThreshold) { this._log.debug("Global Score threshold hit, triggering sync."); From 6b0650c77b16c4c1722fe9f67ff2bbb99b3423e1 Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Fri, 13 Nov 2009 18:00:42 +0100 Subject: [PATCH 1481/1860] Sync Personas by defaul for Firefox 3.6 (bug #527729) --- services/sync/services-sync.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 2d3c03d769ac..31d82b23a0e7 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -64,3 +64,9 @@ pref("extensions.weave.prefs.sync.extensions.personas.current", true); pref("extensions.weave.prefs.sync.layout.spellcheckDefault", true); pref("extensions.weave.prefs.sync.signon.rememberSignons", true); pref("extensions.weave.prefs.sync.spellchecker.dictionary", true); + +// Persona preferences for FF 3.6 +pref("extensions.weave.prefs.sync.lightweightThemes.isThemeSelected", true); +pref("extensions.weave.prefs.sync.lightweightThemes.persisted.footerURL", true); +pref("extensions.weave.prefs.sync.lightweightThemes.persisted.headerURL", true); +pref("extensions.weave.prefs.sync.lightweightThemes.usedThemes", true); From a10ce41e8aa19f17746c1b384809b510dac41b68 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Sat, 14 Nov 2009 13:40:39 -0500 Subject: [PATCH 1482/1860] bug 528541 - enforce length and uniqueness, tweak description to be clearer/less wordy --- services/sync/locales/en-US/fx-prefs.dtd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 965a24596a35..cc361bc77c82 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -35,7 +35,8 @@ - + + From 82751490dc5c018f45a79bddac6ab4afb120cd8f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 16 Nov 2009 17:11:10 -0800 Subject: [PATCH 1483/1860] Bug 525786 - Weave's log has a record of private browsing start/stop times Switch some messages like private browsing, network change to trace and make the default service.main level Debug instead of Trace. --- services/sync/modules/service.js | 12 ++++++------ services/sync/services-sync.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7fc72b23fa43..b46ed6c7988b 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -388,12 +388,12 @@ WeaveSvc.prototype = { switch (topic) { case "network:offline-status-changed": // Whether online or offline, we'll reschedule syncs - this._log.debug("Network offline status change: " + data); + this._log.trace("Network offline status change: " + data); this._checkSyncStatus(); break; case "private-browsing": // Entering or exiting private browsing? Reschedule syncs - this._log.debug("Private browsing change: " + data); + this._log.trace("Private browsing change: " + data); this._checkSyncStatus(); break; case "quit-application": @@ -431,12 +431,12 @@ WeaveSvc.prototype = { _calculateScore: function WeaveSvc_calculateScoreAndDoStuff() { var engines = Engines.getEnabled(); for (let i = 0;i < engines.length;i++) { - //this._log.trace(engines[i].name + ": score: " + engines[i].score); + this._log.trace(engines[i].name + ": score: " + engines[i].score); this.globalScore += engines[i].score; engines[i]._tracker.resetScore(); } - //this._log.trace("Global score updated: " + this.globalScore); + this._log.trace("Global score updated: " + this.globalScore); if (this.globalScore > this.syncThreshold) { this._log.debug("Global Score threshold hit, triggering sync."); @@ -810,7 +810,7 @@ WeaveSvc.prototype = { _remoteSetup: function WeaveSvc__remoteSetup() { let reset = false; - this._log.debug("Fetching global metadata record"); + this._log.trace("Fetching global metadata record"); let meta = Records.import(this.metaURL); let remoteVersion = (meta && meta.payload.storageVersion)? @@ -1228,7 +1228,7 @@ WeaveSvc.prototype = { // XXX Bug 504125 Wait a while after wiping so that the DELETEs replicate Sync.sleep(2000); - this._log.debug("Uploading new metadata record"); + this._log.trace("Uploading new metadata record"); let meta = new WBORecord(this.metaURL); meta.payload.syncID = Clients.syncID; this._updateRemoteVersion(meta); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 31d82b23a0e7..4cf36853c9ba 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -28,7 +28,7 @@ pref("extensions.weave.log.appender.dump", "Error"); pref("extensions.weave.log.appender.debugLog", "Trace"); pref("extensions.weave.log.rootLogger", "Trace"); -pref("extensions.weave.log.logger.service.main", "Trace"); +pref("extensions.weave.log.logger.service.main", "Debug"); pref("extensions.weave.log.logger.async", "Debug"); pref("extensions.weave.log.logger.authenticator", "Debug"); pref("extensions.weave.log.logger.network.resources", "Debug"); From 2989d4646b4f75dee57402a0dab1e1305afc1f31 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 18 Nov 2009 10:21:20 -0800 Subject: [PATCH 1484/1860] Add missing fennec-tabs.dtd for bug 529104. --- services/sync/locales/en-US/fennec-tabs.dtd | 1 + 1 file changed, 1 insertion(+) create mode 100644 services/sync/locales/en-US/fennec-tabs.dtd diff --git a/services/sync/locales/en-US/fennec-tabs.dtd b/services/sync/locales/en-US/fennec-tabs.dtd new file mode 100644 index 000000000000..329c827bcbbd --- /dev/null +++ b/services/sync/locales/en-US/fennec-tabs.dtd @@ -0,0 +1 @@ + From 0869f9caa5466facdbbbaa210b1ce5d184224d56 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 18 Nov 2009 10:24:09 -0800 Subject: [PATCH 1485/1860] Bug 529103 - Removing folders must use removeItem not removeFolder Switch to Svc.Bookmark.removeItem from this._bms.removeFolder. --- services/sync/modules/engines/bookmarks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 77674b852752..0b524f003854 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -519,7 +519,7 @@ BookmarksStore.prototype = { break; case this._bms.TYPE_FOLDER: this._log.debug(" -> removing folder " + record.id); - this._bms.removeFolder(itemId); + Svc.Bookmark.removeItem(itemId); break; case this._bms.TYPE_SEPARATOR: this._log.debug(" -> removing separator " + record.id); From 59ebbb1f234006a8a19d28a205de1d79d85f5642 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 18 Nov 2009 11:47:25 -0800 Subject: [PATCH 1486/1860] Bug 507666 - Handle deleting places history Use onBeforeDeleteURI to get the page's GUID and upload the delete record and process it by removing the page. --- services/sync/modules/engines/history.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 033510416745..78347585bb53 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -213,8 +213,15 @@ HistoryStore.prototype = { }, remove: function HistStore_remove(record) { - //this._log.trace(" -> NOT removing history entry: " + record.id); - // FIXME: implement! + let page = this._findURLByGUID(record.id) + if (page == null) { + this._log.debug("Page already removed: " + record.id); + return; + } + + let uri = Utils.makeURI(page.url); + Svc.History.removePage(uri); + this._log.trace("Removed page: " + [record.id, page.url, page.title]); }, update: function HistStore_update(record) { @@ -281,7 +288,10 @@ HistoryTracker.prototype = { _logName: "HistoryTracker", file: "history", - QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]), + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavHistoryObserver, + Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS + ]), get _hsvc() { let hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. @@ -316,6 +326,13 @@ HistoryTracker.prototype = { }, onPageExpired: function HT_onPageExpired(uri, time, entry) { }, + onBeforeDeleteURI: function onBeforeDeleteURI(uri) { + if (this.ignoreAll) + return; + this._log.trace("onBeforeDeleteURI: " + uri.spec); + if (this.addChangedID(GUIDForUri(uri, true))) + this._upScore(); + }, onDeleteURI: function HT_onDeleteURI(uri) { }, onClearHistory: function HT_onClearHistory() { From a60d6ac076b37472cd06fd81fde7cced1bd7534e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 18 Nov 2009 14:42:13 -0800 Subject: [PATCH 1487/1860] Bug 528539 - Start over should warn about incorrect username/password before prompting for secret phrase Show Connecting... when signin in on page 0 and Verifying... when checking the passphrase on page 1. --- services/sync/locales/en-US/fx-prefs.dtd | 2 ++ services/sync/modules/service.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index cc361bc77c82..ea44c476104b 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -37,7 +37,9 @@ + + diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b46ed6c7988b..020bb5277352 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -556,6 +556,10 @@ WeaveSvc.prototype = { _verifyPassphrase: function _verifyPassphrase() this._catch(this._notify("verify-passphrase", "", function() { + // Don't allow empty/missing passphrase + if (!this.passphrase) + return false; + try { let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); From f34463c1d613a8171daa98e86cd50860205999a6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 18 Nov 2009 16:59:34 -0800 Subject: [PATCH 1488/1860] Bug 516098 - Weave sync fails without any obvious reason, when I've upgraded one machine but not another Detect if sync failed because it's out of date and show a warning notification with a button to open the add-on manager's extension view. --- services/sync/locales/en-US/sync.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 1b0ea4c1b456..00c529e59dc0 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -22,5 +22,8 @@ error.sync.title = Error While Syncing error.sync.description = Weave encountered an error while syncing: %1$S. Weave will automatically retry this action. error.sync.no_node_found = The Weave server is a little busy right now, but you don't need to do anything about it. We'll start syncing your data as soon as we can! error.sync.no_node_found.title = Sync Delay +error.sync.needUpdate.description = You need to update Weave to continue syncing your data. +error.sync.needUpdate.label = Update Weave +error.sync.needUpdate.accesskey = U error.sync.tryAgainButton.label = Sync Now error.sync.tryAgainButton.accesskey = S From 5d3ffa3b5e4d2d55dbc75a80d0f796d00f8c32eb Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 19 Nov 2009 13:59:11 -0800 Subject: [PATCH 1489/1860] Bug 527786 - Help users recover/remember the secret phrase Show some help text if the passphrase is incorrect as well as showing the reset secret link on failure. --- services/sync/locales/en-US/fx-prefs.dtd | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index ea44c476104b..3deee112d217 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -38,6 +38,7 @@ + From 4a5c67e99956a6327e8aefbdbeebeedcf2fc5e77 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 19 Nov 2009 15:34:29 -0800 Subject: [PATCH 1490/1860] Bug 528543 - Set default extensions.weave.client.name so it shows up in about:config Just default to "Firefox" as both desktop and mobile are branded "Firefox". --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 4cf36853c9ba..c68a662410d5 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -6,7 +6,7 @@ pref("extensions.weave.pwChangeURL", "https://services.mozilla.com/pw/forgot.php pref("extensions.weave.termsURL", "https://mozillalabs.com/weave/tos/"); pref("extensions.weave.encryption", "aes-256-cbc"); - +pref("extensions.weave.client.name", "Firefox"); pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.rememberpassword", true); From 958e7f4904d5849bbe6b8829c87fff47e6dac672 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 19 Nov 2009 21:34:17 -0800 Subject: [PATCH 1491/1860] Bug 514545 - weave mangles bookmarks For both folders and separators, use the parent name (not guid) in addition to the item's title/position to determine if it's a dupe. This modifies the bookmark format for folders and separators, so a storage version bump is needed. --- services/sync/modules/engines/bookmarks.js | 71 ++++++++++++------- .../sync/modules/type_records/bookmark.js | 2 +- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 0b524f003854..1f6b0c0576dc 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -101,45 +101,61 @@ BookmarksEngine.prototype = { SyncEngine.prototype._syncStartup.call(this); // Lazily create a mapping of folder titles and separator positions to GUID - let lazyMap = function() { - delete this._folderTitles; - delete this._separatorPos; + this.__defineGetter__("_lazyMap", function() { + delete this._lazyMap; - let folderTitles = {}; - let separatorPos = {}; + let lazyMap = {}; for (let guid in this._store.getAllIDs()) { + // Figure out what key to store the mapping + let key; let id = idForGUID(guid); switch (Svc.Bookmark.getItemType(id)) { case Svc.Bookmark.TYPE_FOLDER: - // Map the folder name to GUID - folderTitles[Svc.Bookmark.getItemTitle(id)] = guid; + key = "f" + Svc.Bookmark.getItemTitle(id); break; case Svc.Bookmark.TYPE_SEPARATOR: - // Map the separator position and parent position to GUID - let parent = Svc.Bookmark.getFolderIdForItem(id); - let pos = [id, parent].map(Svc.Bookmark.getItemIndex); - separatorPos[pos] = guid; + key = "s" + Svc.Bookmark.getItemIndex(id); break; + default: + continue; } + + // The mapping is on a per parent-folder-name basis + let parent = Svc.Bookmark.getFolderIdForItem(id); + let parentName = Svc.Bookmark.getItemTitle(parent); + if (lazyMap[parentName] == null) + lazyMap[parentName] = {}; + + // Remember this item's guid for its parent-name/key pair + lazyMap[parentName][key] = guid; } - this._folderTitles = folderTitles; - this._separatorPos = separatorPos; - }; + // Expose a helper function to get a dupe guid for an item + return this._lazyMap = function(item) { + // Figure out if we have something to key with + let key; + switch (item.type) { + case "folder": + case "livemark": + key = "f" + item.title; + break; + case "separator": + key = "s" + item.pos; + break; + default: + return; + } - // Make the getters that lazily build the mapping - ["_folderTitles", "_separatorPos"].forEach(function(lazy) { - this.__defineGetter__(lazy, function() { - lazyMap.call(this); - return this[lazy]; - }); - }, this); + // Give the guid if we have the matching pair + let parent = lazyMap[item.parentName]; + return parent && parent[key]; + }; + }); }, _syncFinish: function _syncFinish() { SyncEngine.prototype._syncFinish.call(this); - delete this._folderTitles; - delete this._separatorPos; + delete this._lazyMap; }, _findDupe: function _findDupe(item) { @@ -155,9 +171,8 @@ BookmarksEngine.prototype = { return GUIDForId(localId); case "folder": case "livemark": - return this._folderTitles[item.title]; case "separator": - return this._separatorPos[item.pos]; + return this._lazyMap(item); } }, @@ -684,6 +699,7 @@ BookmarksStore.prototype = { return record; } + let parent = Svc.Bookmark.getFolderIdForItem(placeId); switch (this._bms.getItemType(placeId)) { case this._bms.TYPE_BOOKMARK: let bmkUri = this._bms.getBookmarkURI(placeId).spec; @@ -734,14 +750,15 @@ BookmarksStore.prototype = { record = new BookmarkFolder(); } + record.parentName = Svc.Bookmark.getItemTitle(parent); record.title = this._bms.getItemTitle(placeId); break; case this._bms.TYPE_SEPARATOR: record = new BookmarkSeparator(); // Create a positioning identifier for the separator - let parent = Svc.Bookmark.getFolderIdForItem(placeId); - record.pos = [placeId, parent].map(Svc.Bookmark.getItemIndex); + record.parentName = Svc.Bookmark.getItemTitle(parent); + record.pos = Svc.Bookmark.getItemIndex(placeId); break; case this._bms.TYPE_DYNAMIC_CONTAINER: diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 132a41146bba..404064440d51 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -93,7 +93,7 @@ PlacesItem.prototype = { }, }; -Utils.deferGetSet(PlacesItem, "cleartext", ["predecessorid", "type"]); +Utils.deferGetSet(PlacesItem, "cleartext", ["parentName", "predecessorid", "type"]); function Bookmark(uri) { this._Bookmark_init(uri); From 949e69926b83d82ad62b78b2f364d82c32042af7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 19 Nov 2009 23:31:04 -0800 Subject: [PATCH 1492/1860] Bug 527790 - Allow client names to be different from the default "Firefox" Generate a client name based on the logged in username, appname, hostname, and profile name. --- services/sync/locales/en-US/sync.properties | 3 +++ services/sync/modules/engines/clients.js | 25 ++++++++++++++++++++- services/sync/modules/util.js | 3 +++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 00c529e59dc0..689eadead636 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -1,3 +1,6 @@ +# %1: the user name (Ed), %2: the app name (Firefox), %3: the hostname (Comp) +client.name = %1$S's %2$S on %3$S + # %S is the date and time at which the last sync successfully completed lastSync.label = Last Update: %S lastSyncInProgress.label = Last Update: in progress… diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 0b87602218bf..a95a025b5f09 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -108,7 +108,30 @@ ClientEngine.prototype = { Svc.Prefs.reset("client.syncID"); }, - get clientName() { return Svc.Prefs.get("client.name", Svc.AppInfo.name); }, + get clientName() { + if (Svc.Prefs.isSet("client.name")) + return Svc.Prefs.get("client.name"); + + // Generate a client name if we don't have a useful one yet + let user = Svc.Env.get("USER") || Svc.Env.get("USERNAME"); + let app = Svc.AppInfo.name; + let host = Svc.SysInfo.get("host"); + + // Try figuring out the name of the current profile + let prof = Svc.Directory.get("ProfD", Components.interfaces.nsIFile).path; + let profiles = Svc.Profiles.profiles; + while (profiles.hasMoreElements()) { + let profile = profiles.getNext().QueryInterface(Ci.nsIToolkitProfile); + if (prof == profile.rootDir.path) { + // Only bother adding the profile name if it's not "default" + if (profile.name != "default") + host = profile.name + "-" + host; + break; + } + } + + return this.clientName = Str.sync.get("client.name", [user, app, host]); + }, set clientName(value) { Svc.Prefs.set("client.name", value); }, get clientType() { return Svc.Prefs.get("client.type", "desktop"); }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 10d1c1e2fa1b..e06d6e30d56c 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -776,6 +776,7 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], + ["Env", "@mozilla.org/process/environment;1", "nsIEnvironment"], ["History", "@mozilla.org/browser/nav-history-service;1", "nsPIPlacesDatabase"], ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], @@ -783,8 +784,10 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ["Memory", "@mozilla.org/xpcom/memory-service;1", "nsIMemory"], ["Observer", "@mozilla.org/observer-service;1", "nsIObserverService"], ["Private", "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService"], + ["Profiles", "@mozilla.org/toolkit/profile-service;1", "nsIToolkitProfileService"], ["Prompt", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"], ["Script", "@mozilla.org/moz/jssubscript-loader;1", "mozIJSSubScriptLoader"], + ["SysInfo", "@mozilla.org/system-info;1", "nsIPropertyBag2"], ["Version", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator"], ["WinMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"], ["WinWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"], From 0835511bf5aff470abbeeef84fed0411fde1b69a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 20 Nov 2009 14:34:20 -0800 Subject: [PATCH 1493/1860] Remove trailing whitespace from the codebase. --- services/sync/modules/engines.js | 4 +-- services/sync/modules/engines/bookmarks.js | 2 +- services/sync/modules/engines/forms.js | 38 ++++++++++---------- services/sync/modules/engines/input.js | 28 +++++++-------- services/sync/modules/engines/passwords.js | 8 ++--- services/sync/modules/engines/prefs.js | 40 +++++++++++----------- services/sync/modules/ext/Preferences.js | 2 +- services/sync/modules/resource.js | 12 +++---- services/sync/modules/service.js | 8 ++--- services/sync/modules/trackers.js | 4 +-- services/sync/modules/util.js | 2 +- 11 files changed, 74 insertions(+), 74 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index db6468dd7db1..c88b5ea737d4 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -161,12 +161,12 @@ Engine.prototype = { this.__tracker = new this._trackerObj(); return this.__tracker; }, - + get displayName() { try { return Str.engines.get(this.name); } catch (e) {} - + return this._displayName; }, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 1f6b0c0576dc..cfbaa42bdadf 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -391,7 +391,7 @@ BookmarksStore.prototype = { Svc.Bookmark.moveItem(orphan, parentId, insertPos); Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO); }); - + // Fix up the ordering of the now-parented items orphans.forEach(this._attachFollowers, this); }, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 217dd021a1a4..a3eef67dd512 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -100,7 +100,7 @@ FormStore.prototype = { let stor = Cc["@mozilla.org/storage/service;1"]. getService(Ci.mozIStorageService); let formDB = stor.openDatabase(file); - + this.__defineGetter__("_formDB", function() formDB); return formDB; }, @@ -111,7 +111,7 @@ FormStore.prototype = { this.__defineGetter__("_formHistory", function() formHistory); return formHistory; }, - + get _formStatement() { // This is essentially: // SELECT * FROM moz_formhistory ORDER BY 1.0 * (lastUsed - minLast) / @@ -124,21 +124,21 @@ FormStore.prototype = { timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) \ DESC LIMIT 200" ); - + this.__defineGetter__("_formStatement", function() stmnt); return stmnt; }, - + cacheFormItems: function FormStore_cacheFormItems() { this._log.trace("Caching all form items"); this._formItems = this.getAllIDs(); }, - + clearFormCache: function FormStore_clearFormCache() { this._log.trace("Clearing form cache"); this._formItems = null; }, - + getAllIDs: function FormStore_getAllIDs() { let items = {}; let stmnt = this._formStatement; @@ -154,19 +154,19 @@ FormStore.prototype = { return items; }, - + changeItemID: function FormStore_changeItemID(oldID, newID) { this._log.warn("FormStore IDs are data-dependent, cannot change!"); }, - + itemExists: function FormStore_itemExists(id) { return (id in this._formItems); }, - + createRecord: function FormStore_createRecord(guid, cryptoMetaURL) { let record = new FormRec(); record.id = guid; - + if (guid in this._formItems) { let item = this._formItems[guid]; record.encryption = cryptoMetaURL; @@ -175,10 +175,10 @@ FormStore.prototype = { } else { record.deleted = true; } - + return record; }, - + create: function FormStore_create(record) { this._log.debug("Adding form record for " + record.name); this._formHistory.addEntry(record.name, record.value); @@ -186,20 +186,20 @@ FormStore.prototype = { remove: function FormStore_remove(record) { this._log.trace("Removing form record: " + record.id); - + if (record.id in this._formItems) { let item = this._formItems[record.id]; this._formHistory.removeEntry(item.name, item.value); return; } - + this._log.trace("Invalid GUID found, ignoring remove request."); }, update: function FormStore_update(record) { this._log.warn("Ignoring form record update request!"); }, - + wipe: function FormStore_wipe() { this._formHistory.removeAllEntries(); } @@ -213,9 +213,9 @@ FormTracker.prototype = { name: "forms", _logName: "FormTracker", file: "form", - + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), - + __observerService: null, get _observerService() { if (!this.__observerService) @@ -223,13 +223,13 @@ FormTracker.prototype = { getService(Ci.nsIObserverService); return this.__observerService; }, - + _init: function FormTracker__init() { this.__proto__.__proto__._init.call(this); this._log.trace("FormTracker initializing!"); this._observerService.addObserver(this, "earlyformsubmit", false); }, - + /* 10 points per form element */ notify: function FormTracker_notify(formElement, aWindow, actionURI) { if (this.ignoreAll) diff --git a/services/sync/modules/engines/input.js b/services/sync/modules/engines/input.js index c2dd16cb27e2..54380ff3afdd 100644 --- a/services/sync/modules/engines/input.js +++ b/services/sync/modules/engines/input.js @@ -96,7 +96,7 @@ InputStore.prototype = { } return this.__histDB; }, - + _getIDfromURI: function InputStore__getIDfromURI(uri) { let pidStmnt = this._placeDB.createStatement("SELECT id FROM moz_places WHERE url = ?1"); pidStmnt.bindUTF8StringParameter(0, uri); @@ -105,31 +105,31 @@ InputStore.prototype = { else return null; }, - + _getInputHistory: function InputStore__getInputHistory(id) { let ipStmnt = this._placeDB.createStatement("SELECT input, use_count FROM moz_inputhistory WHERE place_id = ?1"); ipStmnt.bindInt32Parameter(0, id); - + let input = []; while (ipStmnt.executeStep()) { let ip = ipStmnt.getUTF8String(0); let cnt = ipStmnt.getInt32(1); input[input.length] = {'input': ip, 'count': cnt}; } - + return input; }, _createCommand: function InputStore__createCommand(command) { this._log.info("InputStore got createCommand: " + command); - + let placeID = this._getIDfromURI(command.GUID); if (placeID) { let createStmnt = this._placeDB.createStatement("INSERT INTO moz_inputhistory (?1, ?2, ?3)"); createStmnt.bindInt32Parameter(0, placeID); createStmnt.bindUTF8StringParameter(1, command.data.input); createStmnt.bindInt32Parameter(2, command.data.count); - + createStmnt.execute(); } }, @@ -144,17 +144,17 @@ InputStore.prototype = { let placeID = this._getIDfromURI(command.GUID); let remStmnt = this._placeDB.createStatement("DELETE FROM moz_inputhistory WHERE place_id = ?1 AND input = ?2"); - + remStmnt.bindInt32Parameter(0, placeID); remStmnt.bindUTF8StringParameter(1, command.data.input); remStmnt.execute(); - + delete this._lookup[command.GUID]; }, _editCommand: function InputStore__editCommand(command) { this._log.info("InputStore got editCommand: " + command); - + if (!(command.GUID in this._lookup)) { this._log.warn("Invalid GUID found, ignoring remove request."); return; @@ -162,19 +162,19 @@ InputStore.prototype = { let placeID = this._getIDfromURI(command.GUID); let editStmnt = this._placeDB.createStatement("UPDATE moz_inputhistory SET input = ?1, use_count = ?2 WHERE place_id = ?3"); - + if ('input' in command.data) { editStmnt.bindUTF8StringParameter(0, command.data.input); } else { editStmnt.bindUTF8StringParameter(0, this._lookup[command.GUID].input); } - + if ('count' in command.data) { editStmnt.bindInt32Parameter(1, command.data.count); } else { editStmnt.bindInt32Parameter(1, this._lookup[command.GUID].count); } - + editStmnt.bindInt32Parameter(2, placeID); editStmnt.execute(); }, @@ -182,7 +182,7 @@ InputStore.prototype = { wrap: function InputStore_wrap() { this._lookup = {}; let stmnt = this._placeDB.createStatement("SELECT * FROM moz_inputhistory"); - + while (stmnt.executeStep()) { let pid = stmnt.getInt32(0); let inp = stmnt.getUTF8String(1); @@ -232,7 +232,7 @@ InputTracker.prototype = { return this.__histDB; }, - /* + /* * To calculate scores, we just count the changes in * the database since the last time we were asked. * diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 108a4169a955..2386ac7b8236 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -109,7 +109,7 @@ PasswordStore.prototype = { let prop = Cc["@mozilla.org/hash-property-bag;1"]. createInstance(Ci.nsIWritablePropertyBag2); prop.setPropertyAsAUTF8String("guid", id); - + let logins = Svc.Login.searchLogins({}, prop); if (logins.length > 0) { this._log.trace(logins.length + " items matching " + id + " found."); @@ -119,7 +119,7 @@ PasswordStore.prototype = { } return false; }, - + getAllIDs: function PasswordStore__getAllIDs() { let items = {}; let logins = Svc.Login.getAllLogins({}); @@ -162,7 +162,7 @@ PasswordStore.prototype = { let record = new LoginRec(); let login = this._getLoginFromGUID(guid); - record.id = guid; + record.id = guid; if (login) { record.encryption = cryptoMetaURL; record.hostname = login.hostname; @@ -185,7 +185,7 @@ PasswordStore.prototype = { remove: function PasswordStore__remove(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"); diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 70b5acfe12d2..7f489afb187f 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -88,20 +88,20 @@ PrefStore.prototype = { getService(Ci.nsIPrefService); let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}). map(function(elem) { return elem.substr(1); }); - + this.__defineGetter__("_syncPrefs", function() syncPrefs); return syncPrefs; }, - + _getAllPrefs: function PrefStore__getAllPrefs() { let values = []; let toSync = this._syncPrefs; - + let pref; for (let i = 0; i < toSync.length; i++) { if (!this._prefs.getBoolPref(WEAVE_SYNC_PREFS + "." + toSync[i])) continue; - + pref = {}; pref["name"] = toSync[i]; @@ -124,10 +124,10 @@ PrefStore.prototype = { if ("value" in pref) values[values.length] = pref; } - + return values; }, - + _setAllPrefs: function PrefStore__setAllPrefs(values) { for (let i = 0; i < values.length; i++) { switch (values[i]["type"]) { @@ -145,36 +145,36 @@ PrefStore.prototype = { } } }, - + getAllIDs: function PrefStore_getAllIDs() { /* We store all prefs in just one WBO, with just one GUID */ let allprefs = {}; allprefs[WEAVE_PREFS_GUID] = this._getAllPrefs(); return allprefs; }, - + changeItemID: function PrefStore_changeItemID(oldID, newID) { this._log.trace("PrefStore GUID is constant!"); }, - + itemExists: function FormStore_itemExists(id) { return (id === WEAVE_PREFS_GUID); }, - + createRecord: function FormStore_createRecord(guid, cryptoMetaURL) { let record = new PrefRec(); record.id = guid; - + if (guid == WEAVE_PREFS_GUID) { record.encryption = cryptoMetaURL; record.value = this._getAllPrefs(); } else { record.deleted = true; } - + return record; }, - + create: function PrefStore_create(record) { this._log.trace("Ignoring create request"); }, @@ -187,7 +187,7 @@ PrefStore.prototype = { this._log.trace("Received pref updates, applying..."); this._setAllPrefs(record.value); }, - + wipe: function PrefStore_wipe() { this._log.trace("Ignoring wipe request"); } @@ -201,7 +201,7 @@ PrefTracker.prototype = { name: "prefs", _logName: "PrefTracker", file: "prefs", - + get _prefs() { let prefs = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefBranch2); @@ -215,21 +215,21 @@ PrefTracker.prototype = { getService(Ci.nsIPrefService); let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}). map(function(elem) { return elem.substr(1); }); - + this.__defineGetter__("_syncPrefs", function() syncPrefs); return syncPrefs; }, - + _init: function PrefTracker__init() { this.__proto__.__proto__._init.call(this); - this._prefs.addObserver("", this, false); + this._prefs.addObserver("", this, false); }, - + /* 25 points per pref change */ observe: function(aSubject, aTopic, aData) { if (aTopic != "nsPref:changed") return; - + if (this._syncPrefs.indexOf(aData) != -1) { this.score += 1; this.addChangedID(WEAVE_PREFS_GUID); diff --git a/services/sync/modules/ext/Preferences.js b/services/sync/modules/ext/Preferences.js index 39a01d3a2c26..c712bb4f0429 100644 --- a/services/sync/modules/ext/Preferences.js +++ b/services/sync/modules/ext/Preferences.js @@ -265,7 +265,7 @@ Preferences.prototype = { else this._reset(prefName); }, - + _reset: function(prefName) { try { this._prefSvc.clearUserPref(prefName); diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 9355ba5a45da..8346583e9270 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -146,12 +146,12 @@ Resource.prototype = { // Always validate the cache: channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; - + // Setup a callback to handle bad HTTPS certificates. channel.notificationCallbacks = new badCertListener(); - + // Avoid calling the authorizer more than once - let headers = this.headers; + let headers = this.headers; for (let key in headers) { if (key == 'Authorization') this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); @@ -167,7 +167,7 @@ Resource.prototype = { // ** {{{ Resource._request }}} ** // // Perform a particular HTTP request on the resource. This method - // is never called directly, but is used by the high-level + // is never called directly, but is used by the high-level // {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods. _request: function Res__request(action, data) { let iter = 0; @@ -176,7 +176,7 @@ Resource.prototype = { if ("undefined" != typeof(data)) this._data = data; - // PUT and POST are trreated differently because + // PUT and POST are trreated differently because // they have payload data. if ("PUT" == action || "POST" == action) { // Convert non-string bodies into JSON @@ -304,7 +304,7 @@ Resource.prototype = { }; // = ChannelListener = -// +// // This object implements the {{{nsIStreamListener}}} interface // and is called as the network operation proceeds. function ChannelListener(onComplete, onProgress, logger) { diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 020bb5277352..69ed95a16459 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -141,7 +141,7 @@ WeaveSvc.prototype = { // Only do work if it's actually changing if (value == this.serverURL) return; - + // A new server most likely uses a different cluster, so clear that Svc.Prefs.set("serverURL", value); Svc.Prefs.reset("clusterURL"); @@ -507,7 +507,7 @@ WeaveSvc.prototype = { _verifyLogin: function _verifyLogin() this._notify("verify-login", "", function() { // Make sure we have a cluster to verify against - // this is a little weird, if we don't get a node we pretend + // this is a little weird, if we don't get a node we pretend // to succeed, since that probably means we just don't have storage if (this.clusterURL == "" && !this._setCluster()) { Status.sync = NO_SYNC_NODE_FOUND; @@ -1017,7 +1017,7 @@ WeaveSvc.prototype = { if (this.nextSync != 0) interval = this.nextSync - Date.now(); // Use the bigger of default sync interval and backoff - else + else interval = Math.max(this.syncInterval, Status.backoffInterval); } @@ -1075,7 +1075,7 @@ WeaveSvc.prototype = { break; default: this._scheduleNextSync(); - return; + return; } } diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index a0848701949a..747dad1aebdf 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -50,7 +50,7 @@ Cu.import("resource://weave/ext/Observers.js"); /* * Trackers are associated with a single engine and deal with * listening for changes to their particular data type. - * + * * There are two things they keep track of: * 1) A score, indicating how urgently the engine wants to sync * 2) A list of IDs for all the changed items that need to be synced @@ -83,7 +83,7 @@ Tracker.prototype = { * -1: Do not sync unless the user specifically requests it (almost disabled) * 0: Nothing has changed * 100: Please sync me ASAP! - * + * * Setting it to other values should (but doesn't currently) throw an exception */ get score() { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index e06d6e30d56c..71c8a3e3f0ac 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -728,7 +728,7 @@ let Utils = { try { return Str.errors.get(error, args || null); } catch (e) {} - + // basically returns "Unknown Error" return Str.errors.get("error.reason.unknown"); }, From d17c549b3ca78186c0c76a6729837265e2509af5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 24 Nov 2009 14:53:59 -0800 Subject: [PATCH 1494/1860] Bug 515593 - audit set of prefs we're syncing Add in prefs that are linked to the Firefox Preferences UI. --- services/sync/services-sync.js | 78 ++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index c68a662410d5..1eee7aaeec35 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -45,28 +45,80 @@ pref("extensions.weave.network.numRetries", 2); pref("extensions.weave.tabs.sortMode", "recency"); // Preferences to be synced by default +pref("extensions.weave.prefs.sync.accessibility.blockautorefresh", true); +pref("extensions.weave.prefs.sync.accessibility.browsewithcaret", true); +pref("extensions.weave.prefs.sync.accessibility.typeaheadfind", true); +pref("extensions.weave.prefs.sync.accessibility.typeaheadfind.linksonly", true); +pref("extensions.weave.prefs.sync.app.update.mode", true); +pref("extensions.weave.prefs.sync.browser.download.manager.closeWhenDone", true); +pref("extensions.weave.prefs.sync.browser.download.manager.retention", true); pref("extensions.weave.prefs.sync.browser.download.manager.scanWhenDone", true); - +pref("extensions.weave.prefs.sync.browser.download.manager.showWhenStarting", true); +pref("extensions.weave.prefs.sync.browser.formfill.enable", true); +pref("extensions.weave.prefs.sync.browser.history_expire_days", true); +pref("extensions.weave.prefs.sync.browser.link.open_newwindow", true); +pref("extensions.weave.prefs.sync.browser.offline-apps.notify", true); +pref("extensions.weave.prefs.sync.browser.safebrowsing.enabled", true); +pref("extensions.weave.prefs.sync.browser.safebrowsing.malware.enabled", true); pref("extensions.weave.prefs.sync.browser.search.selectedEngine", true); - +pref("extensions.weave.prefs.sync.browser.search.update", true); pref("extensions.weave.prefs.sync.browser.startup.homepage", true); - -pref("extensions.weave.prefs.sync.browser.tabs.tabMinWidth", true); -pref("extensions.weave.prefs.sync.browser.tabs.tabMaxWidth", true); -pref("extensions.weave.prefs.sync.browser.tabs.warnOnClose", true); +pref("extensions.weave.prefs.sync.browser.startup.page", true); +pref("extensions.weave.prefs.sync.browser.tabs.autoHide", true); pref("extensions.weave.prefs.sync.browser.tabs.closeButtons", true); - +pref("extensions.weave.prefs.sync.browser.tabs.loadInBackground", true); +pref("extensions.weave.prefs.sync.browser.tabs.tabMaxWidth", true); +pref("extensions.weave.prefs.sync.browser.tabs.tabMinWidth", true); +pref("extensions.weave.prefs.sync.browser.tabs.warnOnClose", true); +pref("extensions.weave.prefs.sync.browser.tabs.warnOnOpen", true); +pref("extensions.weave.prefs.sync.browser.urlbar.autocomplete.enabled", true); pref("extensions.weave.prefs.sync.browser.urlbar.autoFill", true); +pref("extensions.weave.prefs.sync.browser.urlbar.default.behavior", true); pref("extensions.weave.prefs.sync.browser.urlbar.maxRichResults", true); - +pref("extensions.weave.prefs.sync.dom.disable_open_during_load", true); +pref("extensions.weave.prefs.sync.dom.disable_window_flip", true); +pref("extensions.weave.prefs.sync.dom.disable_window_move_resize", true); +pref("extensions.weave.prefs.sync.dom.disable_window_open_feature.status", true); +pref("extensions.weave.prefs.sync.dom.disable_window_status_change", true); +pref("extensions.weave.prefs.sync.dom.event.contextmenu.enabled", true); pref("extensions.weave.prefs.sync.extensions.personas.current", true); - +pref("extensions.weave.prefs.sync.extensions.update.enabled", true); +pref("extensions.weave.prefs.sync.general.autoScroll", true); +pref("extensions.weave.prefs.sync.general.smoothScroll", true); +pref("extensions.weave.prefs.sync.javascript.enabled", true); pref("extensions.weave.prefs.sync.layout.spellcheckDefault", true); -pref("extensions.weave.prefs.sync.signon.rememberSignons", true); -pref("extensions.weave.prefs.sync.spellchecker.dictionary", true); - -// Persona preferences for FF 3.6 pref("extensions.weave.prefs.sync.lightweightThemes.isThemeSelected", true); pref("extensions.weave.prefs.sync.lightweightThemes.persisted.footerURL", true); pref("extensions.weave.prefs.sync.lightweightThemes.persisted.headerURL", true); pref("extensions.weave.prefs.sync.lightweightThemes.usedThemes", true); +pref("extensions.weave.prefs.sync.network.cookie.cookieBehavior", true); +pref("extensions.weave.prefs.sync.network.cookie.lifetimePolicy", true); +pref("extensions.weave.prefs.sync.permissions.default.image", true); +pref("extensions.weave.prefs.sync.pref.advanced.images.disable_button.view_image", true); +pref("extensions.weave.prefs.sync.pref.advanced.javascript.disable_button.advanced", true); +pref("extensions.weave.prefs.sync.pref.downloads.disable_button.edit_actions", true); +pref("extensions.weave.prefs.sync.pref.privacy.disable_button.cookie_exceptions", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.cache", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.cookies", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.downloads", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.formdata", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.history", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.offlineApps", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.passwords", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.sessions", true); +pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.siteSettings", true); +pref("extensions.weave.prefs.sync.privacy.sanitize.sanitizeOnShutdown", true); +pref("extensions.weave.prefs.sync.security.OCSP.disable_button.managecrl", true); +pref("extensions.weave.prefs.sync.security.OCSP.enabled", true); +pref("extensions.weave.prefs.sync.security.OCSP.require", true); +pref("extensions.weave.prefs.sync.security.default_personal_cert", true); +pref("extensions.weave.prefs.sync.security.enable_java", true); +pref("extensions.weave.prefs.sync.security.enable_ssl3", true); +pref("extensions.weave.prefs.sync.security.enable_tls", true); +pref("extensions.weave.prefs.sync.security.warn_entering_secure", true); +pref("extensions.weave.prefs.sync.security.warn_entering_weak", true); +pref("extensions.weave.prefs.sync.security.warn_leaving_secure", true); +pref("extensions.weave.prefs.sync.security.warn_submit_insecure", true); +pref("extensions.weave.prefs.sync.security.warn_viewing_mixed", true); +pref("extensions.weave.prefs.sync.signon.rememberSignons", true); +pref("extensions.weave.prefs.sync.spellchecker.dictionary", true); From 8fb10b3e292aff904d36cf3d605bc1800566ef64 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 24 Nov 2009 16:02:55 -0800 Subject: [PATCH 1495/1860] Bug 524916 - remove any/all code/images/strings that we aren't using anymore Remove unused sync engines (cookies, extensions, input, microformats, plugins, themes). --- services/sync/modules/engines/cookies.js | 308 ------------------ services/sync/modules/engines/extensions.js | 51 --- services/sync/modules/engines/input.js | 273 ---------------- services/sync/modules/engines/microformats.js | 51 --- services/sync/modules/engines/plugins.js | 51 --- services/sync/modules/engines/themes.js | 51 --- services/sync/modules/service.js | 8 +- services/sync/services-sync.js | 2 - 8 files changed, 1 insertion(+), 794 deletions(-) delete mode 100644 services/sync/modules/engines/cookies.js delete mode 100644 services/sync/modules/engines/extensions.js delete mode 100644 services/sync/modules/engines/input.js delete mode 100644 services/sync/modules/engines/microformats.js delete mode 100644 services/sync/modules/engines/plugins.js delete mode 100644 services/sync/modules/engines/themes.js diff --git a/services/sync/modules/engines/cookies.js b/services/sync/modules/engines/cookies.js deleted file mode 100644 index c94c4a41bc3b..000000000000 --- a/services/sync/modules/engines/cookies.js +++ /dev/null @@ -1,308 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Jono DiCarlo - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['CookieEngine', 'CookieTracker', 'CookieStore']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); - -function CookieEngine(pbeId) { - this._init(pbeId); -} -CookieEngine.prototype = { - get enabled() null, // XXX force disabled in-case the pref was somehow set - __proto__: SyncEngine.prototype, - - get name() { return "cookies"; }, - get _displayName() { return "Cookies"; }, - get logName() { return "CookieEngine"; }, - get serverPrefix() { return "user-data/cookies/"; }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new CookieStore(); - return this.__store; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new CookieTracker(); - return this.__tracker; - } -}; - -function CookieStore( cookieManagerStub ) { - // XXX disabled for now.. - return; - /* If no argument is passed in, this store will query/write to the real - Mozilla cookie manager component. This is the normal way to use this - class in production code. But for unit-testing purposes, you can pass - in a stub object that will be used in place of the cookieManager. */ - this._init(); - this._cookieManagerStub = cookieManagerStub; -} -CookieStore.prototype = { - __proto__: Store.prototype, - _logName: "CookieStore", - _lookup: null, - - // Documentation of the nsICookie interface says: - // name ACString The name of the cookie. Read only. - // value ACString The cookie value. Read only. - // isDomain boolean True if the cookie is a domain cookie, false otherwise. Read only. - // host AUTF8String The host (possibly fully qualified) of the cookie. Read only. - // path AUTF8String The path pertaining to the cookie. Read only. - // isSecure boolean True if the cookie was transmitted over ssl, false otherwise. Read only. - // expires PRUint64 Expiration time (local timezone) expressed as number of seconds since Jan 1, 1970. Read only. - // status nsCookieStatus Holds the P3P status of cookie. Read only. - // policy nsCookiePolicy Holds the site's compact policy value. Read only. - // nsICookie2 deprecates expires, status, and policy, and adds: - //rawHost AUTF8String The host (possibly fully qualified) of the cookie without a leading dot to represent if it is a domain cookie. Read only. - //isSession boolean True if the cookie is a session cookie. Read only. - //expiry PRInt64 the actual expiry time of the cookie (where 0 does not represent a session cookie). Read only. - //isHttpOnly boolean True if the cookie is an http only cookie. Read only. - - __cookieManager: null, - get _cookieManager() { - if ( this._cookieManagerStub != undefined ) { - return this._cookieManagerStub; - } - // otherwise, use the real one - if (!this.__cookieManager) - this.__cookieManager = Cc["@mozilla.org/cookiemanager;1"]. - getService(Ci.nsICookieManager2); - // need the 2nd revision of the ICookieManager interface - // because it supports add() and the 1st one doesn't. - return this.__cookieManager; - }, - - _createCommand: function CookieStore__createCommand(command) { - /* we got a command to create a cookie in the local browser - in order to sync with the server. */ - - this._log.debug("CookieStore got create command for: " + command.GUID); - - // this assumes command.data fits the nsICookie2 interface - if ( !command.data.isSession ) { - // Add only persistent cookies ( not session cookies ) - this._cookieManager.add( command.data.host, - command.data.path, - command.data.name, - command.data.value, - command.data.isSecure, - command.data.isHttpOnly, - command.data.isSession, - command.data.expiry ); - } - }, - - _removeCommand: function CookieStore__removeCommand(command) { - /* we got a command to remove a cookie from the local browser - in order to sync with the server. - command.data appears to be equivalent to what wrap() puts in - the JSON dictionary. */ - - if (!(command.GUID in this._lookup)) { - this._log.warn("Warning! Remove command for unknown item: " + command.GUID); - return; - } - - this._log.debug("CookieStore got remove command for: " + command.GUID); - - /* I think it goes like this, according to - http://developer.mozilla.org/en/docs/nsICookieManager - the last argument is "always block cookies from this domain?" - and the answer is "no". */ - this._cookieManager.remove(this._lookup[command.GUID].host, - this._lookup[command.GUID].name, - this._lookup[command.GUID].path, - false); - }, - - _editCommand: function CookieStore__editCommand(command) { - /* we got a command to change a cookie in the local browser - in order to sync with the server. */ - - if (!(command.GUID in this._lookup)) { - this._log.warn("Warning! Edit command for unknown item: " + command.GUID); - return; - } - - this._log.debug("CookieStore got edit command for: " + command.GUID); - - /* Look up the cookie that matches the one in the command: */ - var iter = this._cookieManager.enumerator; - var matchingCookie = null; - while (iter.hasMoreElements()){ - let cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie2 ) ){ - // see if host:path:name of cookie matches GUID given in command - let key = cookie.host + ":" + cookie.path + ":" + cookie.name; - if (key == command.GUID) { - matchingCookie = cookie; - break; - } - } - } - // Update values in the cookie: - for (var key in command.data) { - // Whatever values command.data has, use them - matchingCookie[ key ] = command.data[ key ]; - } - // Remove the old incorrect cookie from the manager: - this._cookieManager.remove( matchingCookie.host, - matchingCookie.name, - matchingCookie.path, - false ); - - // Re-add the new updated cookie: - if ( !command.data.isSession ) { - /* ignore single-session cookies, add only persistent cookies. */ - this._cookieManager.add( matchingCookie.host, - matchingCookie.path, - matchingCookie.name, - matchingCookie.value, - matchingCookie.isSecure, - matchingCookie.isHttpOnly, - matchingCookie.isSession, - matchingCookie.expiry ); - } - - // Also, there's an exception raised because - // this._data[comand.GUID] is undefined - }, - - wrap: function CookieStore_wrap() { - /* Return contents of this store, as JSON. - A dictionary of cookies where the keys are GUIDs and the - values are sub-dictionaries containing all cookie fields. */ - let items = {}; - var iter = this._cookieManager.enumerator; - while (iter.hasMoreElements()) { - var cookie = iter.getNext(); - if (cookie.QueryInterface( Ci.nsICookie2 )) { - // String used to identify cookies is - // host:path:name - if ( cookie.isSession ) { - /* Skip session-only cookies, sync only persistent cookies. */ - continue; - } - - let key = cookie.host + ":" + cookie.path + ":" + cookie.name; - items[ key ] = { parentid: '', - name: cookie.name, - value: cookie.value, - isDomain: cookie.isDomain, - host: cookie.host, - path: cookie.path, - isSecure: cookie.isSecure, - // nsICookie2 values: - rawHost: cookie.rawHost, - isSession: cookie.isSession, - expiry: cookie.expiry, - isHttpOnly: cookie.isHttpOnly }; - - /* See http://developer.mozilla.org/en/docs/nsICookie - Note: not syncing "expires", "status", or "policy" - since they're deprecated. */ - - } - } - this._lookup = items; - return items; - }, - - wipe: function CookieStore_wipe() { - /* Remove everything from the store. Return nothing. - TODO are the semantics of this just wiping out an internal - buffer, or am I supposed to wipe out all cookies from - the browser itself for reals? */ - this._cookieManager.removeAll(); - }, - - _resetGUIDs: function CookieStore__resetGUIDs() { - /* called in the case where remote/local sync GUIDs do not - match. We do need to override this, but since we're deriving - GUIDs from the cookie data itself and not generating them, - there's basically no way they can get "out of sync" so there's - nothing to do here. */ - } -}; - -function CookieTracker() { - // XXX disabled for now.. - return; - this._init(); -} -CookieTracker.prototype = { - __proto__: Tracker.prototype, - _logName: "CookieTracker", - - _init: function CT__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this.score = 0; - /* cookieService can't register observers, but what we CAN do is - register a general observer with the global observerService - to watch for the 'cookie-changed' message. */ - let observerService = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - observerService.addObserver( this, 'cookie-changed', false ); - }, - - // implement observe method to satisfy nsIObserver interface - observe: function ( aSubject, aTopic, aData ) { - /* This gets called when any cookie is added, changed, or removed. - aData will contain a string "added", "changed", etc. to tell us which, - but for now we can treat them all the same. aSubject is the new - cookie object itself. */ - var newCookie = aSubject.QueryInterface( Ci.nsICookie2 ); - if ( newCookie ) { - if ( !newCookie.isSession ) { - /* Any modification to a persistent cookie is worth - 10 points out of 100. Ignore session cookies. */ - this.score += 10; - } - } - } -} diff --git a/services/sync/modules/engines/extensions.js b/services/sync/modules/engines/extensions.js deleted file mode 100644 index cf482d79b9bb..000000000000 --- a/services/sync/modules/engines/extensions.js +++ /dev/null @@ -1,51 +0,0 @@ -/***************************** BEGIN LICENSE BLOCK ***************************** -* Version: MPL 1.1/GPL 2.0/LGPL 2.1 -* -* The contents of this file are subject to the Mozilla Public License Version -* 1.1 (the "License"); you may not use this file except in compliance with the -* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ -* -* Software distributed under the License is distributed on an "AS IS" basis, -* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -* the specific language governing rights and limitations under the License. -* -* The Original Code is Weave Extension Engine. -* -* The Initial Developer of the Original Code is Mozilla Corporation. -* Portions created by the Initial Developer are Copyright (C) 2009 the Initial -* Developer. All Rights Reserved. -* -* Contributor(s): -* Edward Lee (original author) -* -* Alternatively, the contents of this file may be used under the terms of either -* the GNU General Public License Version 2 or later (the "GPL"), or the GNU -* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case -* the provisions of the GPL or the LGPL are applicable instead of those above. -* If you wish to allow use of your version of this file only under the terms of -* either the GPL or the LGPL, and not to allow others to use your version of -* this file under the terms of the MPL, indicate your decision by deleting the -* provisions above and replace them with the notice and other provisions -* required by the GPL or the LGPL. If you do not delete the provisions above, a -* recipient may use your version of this file under the terms of any one of the -* MPL, the GPL or the LGPL. -* -****************************** END LICENSE BLOCK ******************************/ - -const EXPORTED_SYMBOLS = ["ExtensionEngine"]; - -const Cu = Components.utils; -Cu.import("resource://weave/engines.js"); - -function ExtensionEngine() { - this._init(); -} -ExtensionEngine.prototype = { - get enabled() null, // XXX force disabled in-case the pref was somehow set - __proto__: SyncEngine.prototype, - - _displayName: "Extensions", - description: "", - logName: "Extensions", - name: "extensions", -}; diff --git a/services/sync/modules/engines/input.js b/services/sync/modules/engines/input.js deleted file mode 100644 index 54380ff3afdd..000000000000 --- a/services/sync/modules/engines/input.js +++ /dev/null @@ -1,273 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Bookmarks Sync. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Anant Narayanan - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const EXPORTED_SYMBOLS = ['InputEngine']; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); - -function InputEngine(pbeId) { - this._init(pbeId); -} -InputEngine.prototype = { - get enabled() null, // XXX force disabled in-case the pref was somehow set - __proto__: SyncEngine.prototype, - - get name() { return "input"; }, - get _displayName() { return "Input History (Location Bar)"; }, - get logName() { return "InputEngine"; }, - get serverPrefix() { return "user-data/input/"; }, - - __store: null, - get _store() { - if (!this.__store) - this.__store = new InputStore(); - return this.__store; - }, - - __tracker: null, - get _tracker() { - if (!this.__tracker) - this.__tracker = new InputTracker(); - return this.__tracker; - } -}; - -function InputStore() { - // XXX disabled for now.. - return; - this._init(); -} -InputStore.prototype = { - __proto__: Store.prototype, - _logName: "InputStore", - _lookup: null, - - __placeDB: null, - get _placeDB() { - if (!this.__placeDB) { - let file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("places.sqlite"); - let stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__histDB = stor.openDatabase(file); - } - return this.__histDB; - }, - - _getIDfromURI: function InputStore__getIDfromURI(uri) { - let pidStmnt = this._placeDB.createStatement("SELECT id FROM moz_places WHERE url = ?1"); - pidStmnt.bindUTF8StringParameter(0, uri); - if (pidStmnt.executeStep()) - return pidStmnt.getInt32(0); - else - return null; - }, - - _getInputHistory: function InputStore__getInputHistory(id) { - let ipStmnt = this._placeDB.createStatement("SELECT input, use_count FROM moz_inputhistory WHERE place_id = ?1"); - ipStmnt.bindInt32Parameter(0, id); - - let input = []; - while (ipStmnt.executeStep()) { - let ip = ipStmnt.getUTF8String(0); - let cnt = ipStmnt.getInt32(1); - input[input.length] = {'input': ip, 'count': cnt}; - } - - return input; - }, - - _createCommand: function InputStore__createCommand(command) { - this._log.info("InputStore got createCommand: " + command); - - let placeID = this._getIDfromURI(command.GUID); - if (placeID) { - let createStmnt = this._placeDB.createStatement("INSERT INTO moz_inputhistory (?1, ?2, ?3)"); - createStmnt.bindInt32Parameter(0, placeID); - createStmnt.bindUTF8StringParameter(1, command.data.input); - createStmnt.bindInt32Parameter(2, command.data.count); - - createStmnt.execute(); - } - }, - - _removeCommand: function InputStore__removeCommand(command) { - this._log.info("InputStore got removeCommand: " + command); - - if (!(command.GUID in this._lookup)) { - this._log.warn("Invalid GUID found, ignoring remove request."); - return; - } - - let placeID = this._getIDfromURI(command.GUID); - let remStmnt = this._placeDB.createStatement("DELETE FROM moz_inputhistory WHERE place_id = ?1 AND input = ?2"); - - remStmnt.bindInt32Parameter(0, placeID); - remStmnt.bindUTF8StringParameter(1, command.data.input); - remStmnt.execute(); - - delete this._lookup[command.GUID]; - }, - - _editCommand: function InputStore__editCommand(command) { - this._log.info("InputStore got editCommand: " + command); - - if (!(command.GUID in this._lookup)) { - this._log.warn("Invalid GUID found, ignoring remove request."); - return; - } - - let placeID = this._getIDfromURI(command.GUID); - let editStmnt = this._placeDB.createStatement("UPDATE moz_inputhistory SET input = ?1, use_count = ?2 WHERE place_id = ?3"); - - if ('input' in command.data) { - editStmnt.bindUTF8StringParameter(0, command.data.input); - } else { - editStmnt.bindUTF8StringParameter(0, this._lookup[command.GUID].input); - } - - if ('count' in command.data) { - editStmnt.bindInt32Parameter(1, command.data.count); - } else { - editStmnt.bindInt32Parameter(1, this._lookup[command.GUID].count); - } - - editStmnt.bindInt32Parameter(2, placeID); - editStmnt.execute(); - }, - - wrap: function InputStore_wrap() { - this._lookup = {}; - let stmnt = this._placeDB.createStatement("SELECT * FROM moz_inputhistory"); - - while (stmnt.executeStep()) { - let pid = stmnt.getInt32(0); - let inp = stmnt.getUTF8String(1); - let cnt = stmnt.getInt32(2); - - let idStmnt = this._placeDB.createStatement("SELECT url FROM moz_places WHERE id = ?1"); - idStmnt.bindInt32Parameter(0, pid); - if (idStmnt.executeStep()) { - let key = idStmnt.getUTF8String(0); - this._lookup[key] = { 'input': inp, 'count': cnt }; - } - } - - return this._lookup; - }, - - wipe: function InputStore_wipe() { - var stmnt = this._placeDB.createStatement("DELETE FROM moz_inputhistory"); - stmnt.execute(); - }, - - _resetGUIDs: function InputStore__resetGUIDs() { - // Not needed. - } -}; - -function InputTracker() { - // XXX disabled for now.. - return; - this._init(); -} -InputTracker.prototype = { - __proto__: Tracker.prototype, - _logName: "InputTracker", - - __placeDB: null, - get _placeDB() { - if (!this.__placeDB) { - let file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("places.sqlite"); - let stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - this.__histDB = stor.openDatabase(file); - } - return this.__histDB; - }, - - /* - * To calculate scores, we just count the changes in - * the database since the last time we were asked. - * - * Each change is worth 5 points. - */ - _rowCount: 0, - get score() { - var stmnt = this._placeDB.createStatement("SELECT COUNT(place_id) FROM moz_inputhistory"); - stmnt.executeStep(); - var count = stmnt.getInt32(0); - stmnt.reset(); - - this._score = Math.abs(this._rowCount - count) * 5; - - if (this._score >= 100) - return 100; - else - return this._score; - }, - - resetScore: function InputTracker_resetScore() { - var stmnt = this._placeDB.createStatement("SELECT COUNT(place_id) FROM moz_inputhistory"); - stmnt.executeStep(); - this._rowCount = stmnt.getInt32(0); - stmnt.reset(); - this._score = 0; - }, - - _init: function InputTracker__init() { - this._log = Log4Moz.Service.getLogger("Service." + this._logName); - this._score = 0; - - var stmnt = this._placeDB.createStatement("SELECT COUNT(place_id) FROM moz_inputhistory"); - stmnt.executeStep(); - this._rowCount = stmnt.getInt32(0); - stmnt.reset(); - } -}; diff --git a/services/sync/modules/engines/microformats.js b/services/sync/modules/engines/microformats.js deleted file mode 100644 index 9c9f3f8d6608..000000000000 --- a/services/sync/modules/engines/microformats.js +++ /dev/null @@ -1,51 +0,0 @@ -/***************************** BEGIN LICENSE BLOCK ***************************** -* Version: MPL 1.1/GPL 2.0/LGPL 2.1 -* -* The contents of this file are subject to the Mozilla Public License Version -* 1.1 (the "License"); you may not use this file except in compliance with the -* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ -* -* Software distributed under the License is distributed on an "AS IS" basis, -* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -* the specific language governing rights and limitations under the License. -* -* The Original Code is Weave MicroFormat Engine. -* -* The Initial Developer of the Original Code is Mozilla Corporation. -* Portions created by the Initial Developer are Copyright (C) 2009 the Initial -* Developer. All Rights Reserved. -* -* Contributor(s): -* Edward Lee (original author) -* -* Alternatively, the contents of this file may be used under the terms of either -* the GNU General Public License Version 2 or later (the "GPL"), or the GNU -* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case -* the provisions of the GPL or the LGPL are applicable instead of those above. -* If you wish to allow use of your version of this file only under the terms of -* either the GPL or the LGPL, and not to allow others to use your version of -* this file under the terms of the MPL, indicate your decision by deleting the -* provisions above and replace them with the notice and other provisions -* required by the GPL or the LGPL. If you do not delete the provisions above, a -* recipient may use your version of this file under the terms of any one of the -* MPL, the GPL or the LGPL. -* -****************************** END LICENSE BLOCK ******************************/ - -const EXPORTED_SYMBOLS = ["MicroFormatEngine"]; - -const Cu = Components.utils; -Cu.import("resource://weave/engines.js"); - -function MicroFormatEngine() { - this._init(); -} -MicroFormatEngine.prototype = { - get enabled() null, // XXX force disabled in-case the pref was somehow set - __proto__: SyncEngine.prototype, - - _displayName: "MicroFormats", - description: "", - logName: "MicroFormats", - name: "microformats", -}; diff --git a/services/sync/modules/engines/plugins.js b/services/sync/modules/engines/plugins.js deleted file mode 100644 index 0ac1790ea150..000000000000 --- a/services/sync/modules/engines/plugins.js +++ /dev/null @@ -1,51 +0,0 @@ -/***************************** BEGIN LICENSE BLOCK ***************************** -* Version: MPL 1.1/GPL 2.0/LGPL 2.1 -* -* The contents of this file are subject to the Mozilla Public License Version -* 1.1 (the "License"); you may not use this file except in compliance with the -* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ -* -* Software distributed under the License is distributed on an "AS IS" basis, -* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -* the specific language governing rights and limitations under the License. -* -* The Original Code is Weave Plugin Engine. -* -* The Initial Developer of the Original Code is Mozilla Corporation. -* Portions created by the Initial Developer are Copyright (C) 2009 the Initial -* Developer. All Rights Reserved. -* -* Contributor(s): -* Edward Lee (original author) -* -* Alternatively, the contents of this file may be used under the terms of either -* the GNU General Public License Version 2 or later (the "GPL"), or the GNU -* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case -* the provisions of the GPL or the LGPL are applicable instead of those above. -* If you wish to allow use of your version of this file only under the terms of -* either the GPL or the LGPL, and not to allow others to use your version of -* this file under the terms of the MPL, indicate your decision by deleting the -* provisions above and replace them with the notice and other provisions -* required by the GPL or the LGPL. If you do not delete the provisions above, a -* recipient may use your version of this file under the terms of any one of the -* MPL, the GPL or the LGPL. -* -****************************** END LICENSE BLOCK ******************************/ - -const EXPORTED_SYMBOLS = ["PluginEngine"]; - -const Cu = Components.utils; -Cu.import("resource://weave/engines.js"); - -function PluginEngine() { - this._init(); -} -PluginEngine.prototype = { - get enabled() null, // XXX force disabled in-case the pref was somehow set - __proto__: SyncEngine.prototype, - - _displayName: "Plugins", - description: "", - logName: "Plugins", - name: "plugins", -}; diff --git a/services/sync/modules/engines/themes.js b/services/sync/modules/engines/themes.js deleted file mode 100644 index b9e2674ab300..000000000000 --- a/services/sync/modules/engines/themes.js +++ /dev/null @@ -1,51 +0,0 @@ -/***************************** BEGIN LICENSE BLOCK ***************************** -* Version: MPL 1.1/GPL 2.0/LGPL 2.1 -* -* The contents of this file are subject to the Mozilla Public License Version -* 1.1 (the "License"); you may not use this file except in compliance with the -* License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ -* -* Software distributed under the License is distributed on an "AS IS" basis, -* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -* the specific language governing rights and limitations under the License. -* -* The Original Code is Weave Theme Engine. -* -* The Initial Developer of the Original Code is Mozilla Corporation. -* Portions created by the Initial Developer are Copyright (C) 2009 the Initial -* Developer. All Rights Reserved. -* -* Contributor(s): -* Edward Lee (original author) -* -* Alternatively, the contents of this file may be used under the terms of either -* the GNU General Public License Version 2 or later (the "GPL"), or the GNU -* Lesser General Public License Version 2.1 or later (the "LGPL"), in which case -* the provisions of the GPL or the LGPL are applicable instead of those above. -* If you wish to allow use of your version of this file only under the terms of -* either the GPL or the LGPL, and not to allow others to use your version of -* this file under the terms of the MPL, indicate your decision by deleting the -* provisions above and replace them with the notice and other provisions -* required by the GPL or the LGPL. If you do not delete the provisions above, a -* recipient may use your version of this file under the terms of any one of the -* MPL, the GPL or the LGPL. -* -****************************** END LICENSE BLOCK ******************************/ - -const EXPORTED_SYMBOLS = ["ThemeEngine"]; - -const Cu = Components.utils; -Cu.import("resource://weave/engines.js"); - -function ThemeEngine() { - this._init(); -} -ThemeEngine.prototype = { - get enabled() null, // XXX force disabled in-case the pref was somehow set - __proto__: SyncEngine.prototype, - - _displayName: "Themes", - description: "", - logName: "Themes", - name: "themes", -}; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 69ed95a16459..25dc741d3326 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -79,17 +79,11 @@ Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/engines/bookmarks.js", Weave); Cu.import("resource://weave/engines/clientData.js", Weave); -Cu.import("resource://weave/engines/cookies.js", Weave); -Cu.import("resource://weave/engines/extensions.js", Weave); Cu.import("resource://weave/engines/forms.js", Weave); Cu.import("resource://weave/engines/history.js", Weave); -Cu.import("resource://weave/engines/input.js", Weave); Cu.import("resource://weave/engines/prefs.js", Weave); -Cu.import("resource://weave/engines/microformats.js", Weave); Cu.import("resource://weave/engines/passwords.js", Weave); -Cu.import("resource://weave/engines/plugins.js", Weave); Cu.import("resource://weave/engines/tabs.js", Weave); -Cu.import("resource://weave/engines/themes.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); @@ -371,7 +365,7 @@ WeaveSvc.prototype = { break; case THUNDERBIRD_ID: - engines = ["Cookie", "Password"]; + engines = ["Password"]; break; } diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 1eee7aaeec35..685e144f1b19 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -15,10 +15,8 @@ pref("extensions.weave.autoconnect", true); pref("extensions.weave.syncOnQuit.enabled", true); pref("extensions.weave.engine.bookmarks", true); -//pref("extensions.weave.engine.cookies", false); pref("extensions.weave.engine.forms", true); pref("extensions.weave.engine.history", true); -//pref("extensions.weave.engine.input", false); pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.prefs", true); pref("extensions.weave.engine.tabs", true); From 49b31dbe604cc06b199b125f87ad766d6f26debc Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 24 Nov 2009 15:50:51 -0800 Subject: [PATCH 1496/1860] bug 527504 - overlay macBrowserOverlay so menu works on all windows that include --- services/sync/modules/service.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 25dc741d3326..a3d8ff60fa89 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -235,9 +235,6 @@ WeaveSvc.prototype = { return ok; }, - onWindowOpened: function WeaveSvc__onWindowOpened() { - }, - /** * Prepare to initialize the rest of Weave after waiting a little bit */ From 8b210ffa9c80a198d1b08ebed3a55f7837f247ae Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 24 Nov 2009 16:02:56 -0800 Subject: [PATCH 1497/1860] bug 527517 - replace Connected As with Current User to remove ambiguity --- services/sync/locales/en-US/fx-prefs.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 3deee112d217..e83eda2b3ced 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -63,7 +63,7 @@ - + From f7c0d91143d11d01292b7dabc07c06f9899913f1 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 24 Nov 2009 17:54:06 -0800 Subject: [PATCH 1498/1860] bug 528483 - fix prefpane to use the backup pref when numClients = 1 --- services/sync/services-sync.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 685e144f1b19..d7d97704fa2b 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -20,6 +20,7 @@ pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.prefs", true); pref("extensions.weave.engine.tabs", true); +pref("extensions.weave.engine.tabs.backup", true); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 506a06831e1a817b31ad7f4427497507e50bacc3 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 24 Nov 2009 18:55:59 -0800 Subject: [PATCH 1499/1860] bug 530822 - make label more explicit, simplify branding --- services/sync/locales/en-US/fx-prefs.dtd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index e83eda2b3ced..50c115fee73a 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -1,4 +1,4 @@ - + @@ -75,12 +75,12 @@ - + - + From fb138cfe6c5d9f0707b6be635b53bb83c6e32b6e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 25 Nov 2009 15:14:56 -0800 Subject: [PATCH 1500/1860] Bug 531170 - Tabs don't sync from fennec Add listeners for Fennec tab events the same way we do it for Firefox and share listener logic for various event types. --- services/sync/modules/engines/tabs.js | 101 ++++++-------------------- 1 file changed, 23 insertions(+), 78 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 2b3e0fb5a864..7f1104dca961 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -41,8 +41,6 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -const TAB_TIME_ATTR = "weave.tabEngine.lastUsed.timeStamp"; - Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); @@ -215,7 +213,7 @@ TabStore.prototype = { continue; // Get the time the tab was last used - let lastUsedTimestamp = tab.getAttribute(TAB_TIME_ATTR); + let lastUsedTimestamp = tab.lastUsed; // Get title of current page let currentPage = tabState.entries[tabState.entries.length - 1]; @@ -357,101 +355,48 @@ TabTracker.prototype = { this.resetChanged(); // Make sure "this" pointer is always set correctly for event listeners - this.onTabOpened = Utils.bind2(this, this.onTabOpened); - this.onTabClosed = Utils.bind2(this, this.onTabClosed); - this.onTabSelected = Utils.bind2(this, this.onTabSelected); - - // TODO Figure out how this will work on Fennec. + this.onTab = Utils.bind2(this, this.onTab); // Register as an observer so we can catch windows opening and closing: - var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] - .getService(Ci.nsIWindowWatcher); - ww.registerNotification(this); + Svc.WinWatcher.registerNotification(this); - /* Also directly register the listeners for any browser window alread - * open: */ - let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - let enumerator = wm.getEnumerator("navigator:browser"); - while (enumerator.hasMoreElements()) { - this._registerListenersForWindow(enumerator.getNext()); - } - }, - - _getBrowser: function TabTracker__getBrowser(window) { - // Make sure the window is browser-like - if (typeof window.getBrowser != "function") - return null; - - // Make sure it's a tabbrowser-like window - let browser = window.getBrowser(); - if (browser == null || typeof browser.tabContainer != "object") - return null; - - return browser; + // Also register listeners on already open windows + let wins = Svc.WinMediator.getEnumerator("navigator:browser"); + while (wins.hasMoreElements()) + this._registerListenersForWindow(wins.getNext()); }, _registerListenersForWindow: function TabTracker__registerListen(window) { - let browser = this._getBrowser(window); - if (browser == null) - return; + this._log.trace("Registering tab listeners in new window"); - //this._log.trace("Registering tab listeners in new window.\n"); - //dump("Tab listeners registered!\n"); - let container = browser.tabContainer; - container.addEventListener("TabOpen", this.onTabOpened, false); - container.addEventListener("TabClose", this.onTabClosed, false); - container.addEventListener("TabSelect", this.onTabSelected, false); - }, + // For each topic, add or remove onTab as the listener + let topics = ["TabOpen", "TabClose", "TabSelect"]; + let onTab = this.onTab; + let addRem = function(add) topics.forEach(function(topic) { + window[(add ? "add" : "remove") + "EventListener"](topic, onTab, false); + }); - _unRegisterListenersForWindow: function TabTracker__unregister(window) { - let browser = this._getBrowser(window); - if (browser == null) - return; - - let container = browser.tabContainer; - container.removeEventListener("TabOpen", this.onTabOpened, false); - container.removeEventListener("TabClose", this.onTabClosed, false); - container.removeEventListener("TabSelect", this.onTabSelected, false); + // Add the listeners now and remove them on unload + addRem(true); + window.addEventListener("unload", function() addRem(false), false); }, observe: function TabTracker_observe(aSubject, aTopic, aData) { - /* Called when a window opens or closes. Make sure that every - * window has the appropriate listeners registered. */ + // Add tab listeners now that a window has opened let window = aSubject.QueryInterface(Ci.nsIDOMWindow); - // TODO figure out how this will work in Fennec. - if (aTopic == "domwindowopened") { + if (aTopic == "domwindowopened") this._registerListenersForWindow(window); - } else if (aTopic == "domwindowclosed") { - this._unRegisterListenersForWindow(window); - } }, - _upScore: function _upScore(amount) { - this.score += amount; + onTab: function onTab(event) { + this._log.trace(event.type); + this.score += 1; this._changedIDs[Clients.clientID] = true; - }, - onTabOpened: function TabTracker_onTabOpened(event) { // Store a timestamp in the tab to track when it was last used - this._log.trace("Tab opened."); - event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); - this._upScore(1); + event.originalTarget.lastUsed = Math.floor(Date.now() / 1000); }, - onTabClosed: function TabTracker_onTabSelected(event) { - this._log.trace("Tab closed."); - this._upScore(1); - }, - - onTabSelected: function TabTracker_onTabSelected(event) { - // Update the tab's timestamp - this._log.trace("Tab selected."); - event.target.setAttribute(TAB_TIME_ATTR, event.timeStamp); - this._upScore(1); - }, - // TODO: Also listen for tabs loading new content? - get changedIDs() this._changedIDs, // Provide a way to empty out the changed ids From 760ef74b15ea77f27dcdd83b9b28ee525b8cb06f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 25 Nov 2009 15:17:39 -0800 Subject: [PATCH 1501/1860] Bug 531171 - Tabs don't always update Only expose that the tab engine knows about the current client's tabs, so always create/apply incoming and only create records tabs for local. --- services/sync/modules/engines/tabs.js | 56 +++------------------------ 1 file changed, 5 insertions(+), 51 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 7f1104dca961..43e425967528 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -141,10 +141,6 @@ TabStore.prototype = { this._readFromFile(); }, - get _localClientGUID() { - return Clients.clientID; - }, - get _localClientName() { return Clients.clientName; }, @@ -266,43 +262,20 @@ TabStore.prototype = { }, itemExists: function TabStore_itemExists(id) { - if (id == this._localClientGUID) { - return true; - } else if (this._remoteClients[id]) { - return true; - } else { - return false; - } + return id == Clients.clientID; }, createRecord: function TabStore_createRecord(id, cryptoMetaURL) { - let record; - if (id == this._localClientGUID) { - record = this._createLocalClientTabSetRecord(); - } else { - record = this._remoteClients[id]; - } + let record = this._createLocalClientTabSetRecord(); record.id = id; record.encryption = cryptoMetaURL; return record; }, - changeItemID: function TabStore_changeItemId(oldId, newId) { - if (this._remoteClients[oldId]) { - let record = this._remoteClients[oldId]; - record.id = newId; - delete this._remoteClients[oldId]; - this._remoteClients[newId] = record; - } - }, - getAllIDs: function TabStore_getAllIds() { - let items = {}; - items[ this._localClientGUID ] = true; - for (let id in this._remoteClients) { - items[id] = true; - } - return items; + let ids = {}; + ids[Clients.clientID] = true; + return ids; }, wipe: function TabStore_wipe() { @@ -310,32 +283,13 @@ TabStore.prototype = { }, create: function TabStore_create(record) { - if (record.id == this._localClientGUID) - return; // can't happen? this._log.debug("Adding remote tabs from " + record.getClientName()); this._remoteClients[record.id] = record; this._writeToFile(); // TODO writing to file after every change is inefficient. How do we // make sure to do it (or at least flush it) only after sync is done? // override syncFinished - }, - - update: function TabStore_update(record) { - if (record.id == this._localClientGUID) - return; // can't happen? - this._log.debug("Updating remote client: " + record.getClientName()); - this._remoteClients[record.id] = record; - this._writeToFile(); - }, - - remove: function TabStore_remove(record) { - if (record.id == this._localClientGUID) - return; // can't happen? - this._log.trace("Remove called. Deleting record with id " + record.id); - delete this._remoteClients[record.id]; - this._writeToFile(); } - }; From fdcd3d4d2e69b72a5163e80f89ab0ae23fcdcfcf Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 25 Nov 2009 15:22:45 -0800 Subject: [PATCH 1502/1860] Bug 531177 - Tabs don't sync from firefox (when there's lots of tabs) Simplify tab record to just use deferGetSet like the other records and sort tabs based on their last usage to pick out a subset. --- services/sync/modules/engines/tabs.js | 145 ++++++--------------- services/sync/modules/type_records/tabs.js | 39 +----- 2 files changed, 39 insertions(+), 145 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 43e425967528..527ccc54c191 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -141,25 +141,13 @@ TabStore.prototype = { this._readFromFile(); }, - get _localClientName() { - return Clients.clientName; - }, - _writeToFile: function TabStore_writeToFile() { - let json = {}; - for (let [id, val] in Iterator(this._remoteClients)) - json[id] = val.toJson(); - - Utils.jsonSave(this._filePath, this, json); + Utils.jsonSave(this._filePath, this, this._remoteClients); }, _readFromFile: function TabStore_readFromFile() { Utils.jsonLoad(this._filePath, this, function(json) { - for (let [id, val] in Iterator(json)) { - this._remoteClients[id] = new TabSetRecord(); - this._remoteClients[id].fromJson(val); - this._remoteClients[id].id = id; - } + this._remoteClients = json; }); }, @@ -170,103 +158,44 @@ TabStore.prototype = { return this._sessionStore; }, - get _windowMediator() { - let wm = Cc["@mozilla.org/appshell/window-mediator;1"] - .getService(Ci.nsIWindowMediator); - this.__defineGetter__("_windowMediator", function() { return wm;}); - return this._windowMediator; - }, - - _createLocalClientTabSetRecord: function TabStore__createLocalTabSet() { - // Test for existence of sessionStore. If it doesn't exist, then - // use get _fennecTabs instead. - let record = new TabSetRecord(); - record.setClientName( this._localClientName ); - - if (Cc["@mozilla.org/browser/sessionstore;1"]) { - this._addFirefoxTabsToRecord(record); - } else { - this._addFennecTabsToRecord(record); - } - return record; - }, - - _addFirefoxTabsToRecord: function TabStore__addFirefoxTabs(record) { - // Iterate through each tab of each window - let enumerator = this._windowMediator.getEnumerator("navigator:browser"); - while (enumerator.hasMoreElements()) { - let window = enumerator.getNext(); - let tabContainer = window.getBrowser().tabContainer; - - // Grab each tab child from the array-like child NodeList - for each (let tab in Array.slice(tabContainer.childNodes)) { - if (!(tab instanceof Ci.nsIDOMNode)) - continue; - - let tabState = JSON.parse(this._sessionStore.getTabState(tab)); - // Skip empty (i.e. just-opened, no history yet) tabs: - if (tabState.entries.length == 0) - continue; - - // Get the time the tab was last used - let lastUsedTimestamp = tab.lastUsed; - - // Get title of current page - let currentPage = tabState.entries[tabState.entries.length - 1]; - /* TODO not always accurate -- if you've hit Back in this tab, - * then the current page might not be the last entry. Deal - * with this later. */ - - // Get url history - let urlHistory = []; - // Include URLs in reverse order; max out at 10, and skip nulls. - for (let i = tabState.entries.length -1; i >= 0; i--) { - let entry = tabState.entries[i]; - if (entry && entry.url) - urlHistory.push(entry.url); - if (urlHistory.length >= 10) - break; - } - - // add tab to record - this._log.debug("Wrapping a tab with title " + currentPage.title); - this._log.trace("And timestamp " + lastUsedTimestamp); - record.addTab(currentPage.title, urlHistory, lastUsedTimestamp); - } - } - }, - - _addFennecTabsToRecord: function TabStore__addFennecTabs(record) { - let wm = Cc["@mozilla.org/appshell/window-mediator;1"] - .getService(Ci.nsIWindowMediator); - let browserWindow = wm.getMostRecentWindow("navigator:browser"); - for each (let tab in browserWindow.Browser._tabs ) { - let title = tab.browser.contentDocument.title; - let url = tab.browser.contentWindow.location.toString(); - let urlHistory = [url]; - - // TODO how to get older entries in urlHistory? without session store? - // can we use BrowserUI._getHistory somehow? - - // Get the time the tab was last used - /* let lastUsedTimestamp = tab.getAttribute(TAB_TIME_ATTR); - if (!lastUsedTimestamp) */ - // TODO that doesn't work: tab.getAttribute is not a function on Fennec. - let lastUsedTimestamp = "0"; - - this._log.debug("Wrapping a tab with title " + title); - this._log.trace("And timestamp " + lastUsedTimestamp); - record.addTab(title, urlHistory, lastUsedTimestamp); - // TODO add last-visited date for this tab... but how? - } - }, - itemExists: function TabStore_itemExists(id) { return id == Clients.clientID; }, createRecord: function TabStore_createRecord(id, cryptoMetaURL) { - let record = this._createLocalClientTabSetRecord(); + let record = new TabSetRecord(); + record.clientName = Clients.clientName; + + // Iterate through each tab of each window + let allTabs = []; + let wins = Svc.WinMediator.getEnumerator("navigator:browser"); + while (wins.hasMoreElements()) { + // Get the tabs for both Firefox and Fennec + let window = wins.getNext(); + let tabs = window.gBrowser && window.gBrowser.tabContainer.childNodes; + tabs = tabs || window.Browser._tabs; + Array.forEach(tabs, function(tab) { + allTabs.push(tab); + }); + } + + // Extract various pieces of tab data and sort them in descending used + let tabData = allTabs.map(function(tab) { + let browser = tab.linkedBrowser || tab.browser; + return { + title: browser.contentTitle || "", + urlHistory: [browser.currentURI.spec], + icon: browser.mIconURL || "", + lastUsed: tab.lastUsed || 0 + }; + }).sort(function(a, b) b.lastUsed - a.lastUsed); + + // Only grab the most recently used tabs to sync + record.tabs = tabData.slice(0, 25); + record.tabs.forEach(function(tab) { + this._log.debug("Wrapping tab: " + JSON.stringify(tab)); + }, this); + record.id = id; record.encryption = cryptoMetaURL; return record; @@ -283,8 +212,8 @@ TabStore.prototype = { }, create: function TabStore_create(record) { - this._log.debug("Adding remote tabs from " + record.getClientName()); - this._remoteClients[record.id] = record; + this._log.debug("Adding remote tabs from " + record.clientName); + this._remoteClients[record.id] = record.cleartext; this._writeToFile(); // TODO writing to file after every change is inefficient. How do we // make sure to do it (or at least flush it) only after sync is done? diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js index 9fcecd155e50..bf1d081390d4 100644 --- a/services/sync/modules/type_records/tabs.js +++ b/services/sync/modules/type_records/tabs.js @@ -59,41 +59,6 @@ TabSetRecord.prototype = { this.cleartext = { }; }, - - addTab: function TabSetRecord_addTab(title, urlHistory, lastUsed) { - if (!this.cleartext.tabs) - this.cleartext.tabs = []; - if (!title) { - title = ""; - } - if (!lastUsed) { - lastUsed = 0; - } - if (!urlHistory || urlHistory.length == 0) { - return; - } - this.cleartext.tabs.push( {title: title, - urlHistory: urlHistory, - lastUsed: lastUsed}); - }, - - getAllTabs: function TabSetRecord_getAllTabs() { - return this.cleartext.tabs; - }, - - setClientName: function TabSetRecord_setClientName(value) { - this.cleartext.clientName = value; - }, - - getClientName: function TabSetRecord_getClientName() { - return this.cleartext.clientName; - }, - - toJson: function TabSetRecord_toJson() { - return this.cleartext; - }, - - fromJson: function TabSetRecord_fromJson(json) { - this.cleartext = json; - } }; + +Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]); From 012934ac19ef2ac328f97e1dd40e7ede328d4923 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 25 Nov 2009 15:25:55 -0800 Subject: [PATCH 1503/1860] Bug 530926 - remove "Weave /" from tab list page title --- services/sync/locales/en-US/fennec-tabs.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fennec-tabs.dtd b/services/sync/locales/en-US/fennec-tabs.dtd index 329c827bcbbd..41387be9c39c 100644 --- a/services/sync/locales/en-US/fennec-tabs.dtd +++ b/services/sync/locales/en-US/fennec-tabs.dtd @@ -1 +1 @@ - + From 7f8033aa1dbeb90230a6e4d6280fb11d5015aba3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 25 Nov 2009 16:59:02 -0800 Subject: [PATCH 1504/1860] Bug 530904 - tabs list style doesn't match fennec lists Make headers larger, adjust text margins, add nokia sans font family. --- services/sync/modules/util.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 71c8a3e3f0ac..3961956ad485 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -724,6 +724,17 @@ let Utils = { this.openDialog("ChangeSomething", "generic-change.xul"); }, + getIcon: function(iconUri, defaultIcon) { + try { + let iconURI = Utils.makeURI(iconUri); + return Svc.Favicon.getFaviconLinkForIcon(iconURI).spec; + } + catch(ex) {} + + // Just give the provided default icon or the system's default + return defaultIcon || Svc.Favicon.defaultFavicon.spec; + }, + getErrorString: function Utils_getErrorString(error, args) { try { return Str.errors.get(error, args || null); @@ -777,6 +788,7 @@ Svc.Prefs = new Preferences(PREFS_BRANCH); ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], ["Env", "@mozilla.org/process/environment;1", "nsIEnvironment"], + ["Favicon", "@mozilla.org/browser/favicon-service;1", "nsIFaviconService"], ["History", "@mozilla.org/browser/nav-history-service;1", "nsPIPlacesDatabase"], ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], From d426b367c606e58e08bf259012c9eed2f025ecd6 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 25 Nov 2009 17:08:51 -0800 Subject: [PATCH 1505/1860] bug 530832 - make Merge more prominent, and clearly the recommended choice, with crisper wording --- services/sync/locales/en-US/fx-prefs.dtd | 13 +++++++------ services/sync/modules/service.js | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 50c115fee73a..d645f19af0c4 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -42,13 +42,14 @@ - - + + - - - - + + + + + diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a3d8ff60fa89..5fe1f6ba8748 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -396,7 +396,6 @@ WeaveSvc.prototype = { case "weave:service:sync:finish": this._scheduleNextSync(); this._syncErrors = 0; - Svc.Prefs.reset("firstSync"); break; case "weave:service:backoff:interval": let interval = data + Math.random() * data * 0.25; // required backoff + up to 25% @@ -1145,6 +1144,7 @@ WeaveSvc.prototype = { } } finally { this._syncError = false; + Svc.Prefs.reset("firstSync"); } })))(), From 247d07de79163e1221a566504ca76053576f7191 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 25 Nov 2009 17:09:32 -0800 Subject: [PATCH 1506/1860] archive local bookmarks before wiping --- services/sync/modules/engines/bookmarks.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index cfbaa42bdadf..9f93300bb5ef 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -917,6 +917,11 @@ BookmarksStore.prototype = { }, wipe: function BStore_wipe() { + // some nightly builds of 3.7 don't have this function + try { + PlacesUtils.archiveBookmarksFile(null, true); + } catch(e) {} + for (let [guid, id] in Iterator(kSpecialIds)) if (guid != "places") this._bms.removeFolderChildren(id); From 47fee9e098e5d383b49176c2244661f346d0544b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 25 Nov 2009 17:49:15 -0800 Subject: [PATCH 1507/1860] Remove unused prefs and imports. (Bug 524916) --- services/sync/modules/auth.js | 2 -- services/sync/modules/base_records/collection.js | 5 ----- services/sync/modules/base_records/crypto.js | 2 -- services/sync/modules/engines.js | 1 - services/sync/modules/engines/bookmarks.js | 4 ---- services/sync/modules/engines/clients.js | 4 ---- services/sync/modules/engines/forms.js | 3 --- services/sync/modules/engines/history.js | 2 -- services/sync/modules/engines/passwords.js | 1 - services/sync/modules/engines/prefs.js | 3 --- services/sync/modules/engines/tabs.js | 1 - services/sync/modules/identity.js | 1 - services/sync/modules/stores.js | 1 - services/sync/modules/trackers.js | 1 - services/sync/modules/type_records/bookmark.js | 3 --- services/sync/modules/type_records/clients.js | 1 - services/sync/modules/type_records/forms.js | 3 --- services/sync/modules/type_records/history.js | 3 --- services/sync/modules/type_records/passwords.js | 3 --- services/sync/modules/type_records/prefs.js | 3 --- services/sync/modules/type_records/tabs.js | 3 --- services/sync/modules/util.js | 1 - services/sync/services-sync.js | 12 ------------ 23 files changed, 63 deletions(-) diff --git a/services/sync/modules/auth.js b/services/sync/modules/auth.js index 12890aaa7fb2..8d676f812335 100644 --- a/services/sync/modules/auth.js +++ b/services/sync/modules/auth.js @@ -41,8 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Utils.lazy(this, 'Auth', AuthMgr); diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 2f48cf9c9118..793052fff5d4 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -41,13 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/base_records/wbo.js"); -Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); function Collection(uri, recordObj) { this._Coll_init(uri); diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 804772556970..01cd8ebc6606 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -41,8 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/keys.js"); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index c88b5ea737d4..2431062d310e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -42,7 +42,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/log4moz.js"); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 9f93300bb5ef..c73067ba718f 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -48,14 +48,10 @@ const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; Cu.import("resource://gre/modules/utils.js"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/notifications.js"); -Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/type_records/bookmark.js"); // Lazily initialize the special top level folders diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index a95a025b5f09..98860d04f3e2 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -40,10 +40,6 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index a3eef67dd512..004c75dbf1a9 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -41,13 +41,10 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/base_records/collection.js"); Cu.import("resource://weave/type_records/forms.js"); function FormEngine() { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 78347585bb53..b15aa6e958b8 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -43,12 +43,10 @@ const Cu = Components.utils; const GUID_ANNO = "weave/guid"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/base_records/collection.js"); Cu.import("resource://weave/type_records/history.js"); // Create some helper functions to handle GUIDs diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 2386ac7b8236..5e5d5769681c 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -41,7 +41,6 @@ const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 7f489afb187f..5df1f3c757b1 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -43,9 +43,6 @@ const Cu = Components.utils; const WEAVE_SYNC_PREFS = "extensions.weave.prefs.sync"; const WEAVE_PREFS_GUID = "preferences"; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 527ccc54c191..0d3625eb6942 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -46,7 +46,6 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/type_records/tabs.js"); Cu.import("resource://weave/engines/clientData.js"); diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 368ff4452e85..7963b02ee3c0 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/log4moz.js"); diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 9b98dc556631..80b98c6fee3d 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -42,7 +42,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 747dad1aebdf..3133cd5e45c4 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 404064440d51..a57833aa7aeb 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -42,11 +42,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); function PlacesItem(uri) { this._PlacesItem_init(uri); diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index 3bcf478010af..51dfeb59aa9b 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/wbo.js"); diff --git a/services/sync/modules/type_records/forms.js b/services/sync/modules/type_records/forms.js index abe1d0d6d6c5..06bea1d234b1 100644 --- a/services/sync/modules/type_records/forms.js +++ b/services/sync/modules/type_records/forms.js @@ -41,11 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); function FormRec(uri) { this._FormRec_init(uri); diff --git a/services/sync/modules/type_records/history.js b/services/sync/modules/type_records/history.js index 5ca5040ebb58..bb9e78df800c 100644 --- a/services/sync/modules/type_records/history.js +++ b/services/sync/modules/type_records/history.js @@ -41,11 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); function HistoryRec(uri) { this._HistoryRec_init(uri); diff --git a/services/sync/modules/type_records/passwords.js b/services/sync/modules/type_records/passwords.js index 4b6f4de3e0e3..5708b6591b81 100644 --- a/services/sync/modules/type_records/passwords.js +++ b/services/sync/modules/type_records/passwords.js @@ -41,11 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); function LoginRec(uri) { this._LoginRec_init(uri); diff --git a/services/sync/modules/type_records/prefs.js b/services/sync/modules/type_records/prefs.js index 2bfb3f652a2d..d130fa6f520c 100644 --- a/services/sync/modules/type_records/prefs.js +++ b/services/sync/modules/type_records/prefs.js @@ -41,11 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); function PrefRec(uri) { this._PrefRec_init(uri); diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js index bf1d081390d4..4d0b17668563 100644 --- a/services/sync/modules/type_records/tabs.js +++ b/services/sync/modules/type_records/tabs.js @@ -41,11 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); function TabSetRecord(uri) { this._TabSetRecord_init(uri); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 3961956ad485..bb7560fe1dcc 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -41,7 +41,6 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Preferences.js"); Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/ext/StringBundle.js"); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index d7d97704fa2b..e229ff809d9d 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -5,15 +5,10 @@ pref("extensions.weave.miscURL", "misc/"); pref("extensions.weave.pwChangeURL", "https://services.mozilla.com/pw/forgot.php"); pref("extensions.weave.termsURL", "https://mozillalabs.com/weave/tos/"); -pref("extensions.weave.encryption", "aes-256-cbc"); pref("extensions.weave.client.name", "Firefox"); pref("extensions.weave.lastversion", "firstrun"); - -pref("extensions.weave.rememberpassword", true); pref("extensions.weave.autoconnect", true); -pref("extensions.weave.syncOnQuit.enabled", true); - pref("extensions.weave.engine.bookmarks", true); pref("extensions.weave.engine.forms", true); pref("extensions.weave.engine.history", true); @@ -25,10 +20,8 @@ pref("extensions.weave.engine.tabs.backup", true); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); pref("extensions.weave.log.appender.debugLog", "Trace"); - pref("extensions.weave.log.rootLogger", "Trace"); pref("extensions.weave.log.logger.service.main", "Debug"); -pref("extensions.weave.log.logger.async", "Debug"); pref("extensions.weave.log.logger.authenticator", "Debug"); pref("extensions.weave.log.logger.network.resources", "Debug"); pref("extensions.weave.log.logger.engine.bookmarks", "Debug"); @@ -37,11 +30,6 @@ pref("extensions.weave.log.logger.engine.forms", "Debug"); pref("extensions.weave.log.logger.engine.history", "Debug"); pref("extensions.weave.log.logger.engine.passwords", "Debug"); pref("extensions.weave.log.logger.engine.tabs", "Debug"); -pref("extensions.weave.log.logger.authenticator", "Debug"); - -pref("extensions.weave.network.numRetries", 2); - -pref("extensions.weave.tabs.sortMode", "recency"); // Preferences to be synced by default pref("extensions.weave.prefs.sync.accessibility.blockautorefresh", true); From 1467984476b9abe1097ca72c47993ac135e9821c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 25 Nov 2009 17:59:26 -0800 Subject: [PATCH 1508/1860] Remove unused SnapshotStore. (Bug 524916) --- services/sync/modules/stores.js | 145 +------------------------------- 1 file changed, 1 insertion(+), 144 deletions(-) diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 80b98c6fee3d..de7dc1875c6c 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -34,8 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Store', - 'SnapshotStore']; +const EXPORTED_SYMBOLS = ["Store"]; const Cc = Components.classes; const Ci = Components.interfaces; @@ -188,145 +187,3 @@ Cache.prototype = { this._items = {}; } }; - -function SnapshotStore(name) { - this._init(name); -} -SnapshotStore.prototype = { - _logName: "SnapStore", - - _filename: null, - get filename() { - if (this._filename === null) - throw "filename is null"; - return this._filename; - }, - set filename(value) { - this._filename = value + ".json"; - }, - - // Last synced tree, version, and GUID (to detect if the store has - // been completely replaced and invalidate the snapshot) - - _data: {}, - get data() { return this._data; }, - set data(value) { this._data = value; }, - - _version: 0, - get version() { return this._version; }, - set version(value) { this._version = value; }, - - _GUID: null, - get GUID() { - if (!this._GUID) - this._GUID = Utils.makeGUID(); - return this._GUID; - }, - set GUID(GUID) { - this._GUID = GUID; - }, - - _init: function SStore__init(name) { - this.filename = name; - this._log = Log4Moz.repository.getLogger("Service." + this._logName); - }, - - _createCommand: function SStore__createCommand(command) { - this._data[command.GUID] = Utils.deepCopy(command.data); - }, - - _removeCommand: function SStore__removeCommand(command) { - delete this._data[command.GUID]; - }, - - _editCommand: function SStore__editCommand(command) { - if ("GUID" in command.data) { - // special-case guid changes - let newGUID = command.data.GUID, - oldGUID = command.GUID; - - this._data[newGUID] = this._data[oldGUID]; - delete this._data[oldGUID]; - - for (let GUID in this._data) { - if (("parentid" in this._data[GUID]) && - (this._data[GUID].parentid == oldGUID)) - this._data[GUID].parentid = newGUID; - } - } - for (let prop in command.data) { - if (prop == "GUID") - continue; - if (command.GUID in this._data) - this._data[command.GUID][prop] = command.data[prop]; - else - this._log.warn("Warning! Edit command for unknown item: " + command.GUID); - } - }, - - save: function SStore_save() { - this._log.info("Saving snapshot to disk"); - - let file = Utils.getProfileFile( - {path: "weave/snapshots/" + this.filename, - autoCreate: true} - ); - - let out = {version: this.version, - GUID: this.GUID, - snapshot: this.data}; - out = JSON.stringify(out); - - let [fos] = Utils.open(file, ">"); - fos.writeString(out); - fos.close(); - }, - - load: function SStore_load() { - let file = Utils.getProfileFile("weave/snapshots/" + this.filename); - if (!file.exists()) - return; - - try { - let [is] = Utils.open(file, "<"); - let json = Utils.readStream(is); - is.close(); - json = JSON.parse(json); - - if (json && 'snapshot' in json && 'version' in json && 'GUID' in json) { - this._log.debug("Read saved snapshot from disk"); - this.data = json.snapshot; - this.version = json.version; - this.GUID = json.GUID; - } - } catch (e) { - this._log.warn("Could not parse saved snapshot; reverting to initial-sync state"); - this._log.debug("Exception: " + e); - } - }, - - serialize: function SStore_serialize() { - let json = JSON.stringify(this.data); - json = json.replace(/:{type/g, ":\n\t{type"); - json = json.replace(/}, /g, "},\n "); - json = json.replace(/, parentid/g, ",\n\t parentid"); - json = json.replace(/, index/g, ",\n\t index"); - json = json.replace(/, title/g, ",\n\t title"); - json = json.replace(/, URI/g, ",\n\t URI"); - json = json.replace(/, tags/g, ",\n\t tags"); - json = json.replace(/, keyword/g, ",\n\t keyword"); - return json; - }, - - wrap: function SStore_wrap() { - return this.data; - }, - - wipe: function SStore_wipe() { - this.data = {}; - this.version = -1; - this.GUID = null; - this.save(); - } -}; -SnapshotStore.prototype.__proto__ = new Store(); From 22956047ea34aca40416ec64fd3acd296d9b9a24 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 30 Nov 2009 12:15:18 -0800 Subject: [PATCH 1509/1860] Bug 530823 - Engines need to be able to specify a prefName for sharing prefs (different history types) Use prefName for checking if an engine is enabled and have forms share a prefName with history. --- services/sync/modules/engines.js | 5 +++-- services/sync/modules/engines/forms.js | 1 + services/sync/services-sync.js | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 2431062d310e..0e24e9fc1dc5 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -144,8 +144,9 @@ Engine.prototype = { _storeObj: Store, _trackerObj: Tracker, - get enabled() Svc.Prefs.get("engine." + this.name, null), - set enabled(val) Svc.Prefs.set("engine." + this.name, !!val), + get prefName() this.name, + get enabled() Svc.Prefs.get("engine." + this.prefName, null), + set enabled(val) Svc.Prefs.set("engine." + this.prefName, !!val), get score() this._tracker.score, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 004c75dbf1a9..8fa36d7a221f 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -59,6 +59,7 @@ FormEngine.prototype = { _storeObj: FormStore, _trackerObj: FormTracker, _recordObj: FormRec, + get prefName() "history", _syncStartup: function FormEngine__syncStartup() { this._store.cacheFormItems(); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index e229ff809d9d..7aed038775a3 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -10,7 +10,6 @@ pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.autoconnect", true); pref("extensions.weave.engine.bookmarks", true); -pref("extensions.weave.engine.forms", true); pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.prefs", true); From e5f64a07a0b3450a146e35d5feacce04b29b5106 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 30 Nov 2009 13:35:20 -0800 Subject: [PATCH 1510/1860] Bug 530863 - Global threshold update causes multiple syncs to fire Make sure we only add one idle observer by keeping a flag. --- services/sync/modules/service.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5fe1f6ba8748..e76f10071e7f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -408,6 +408,7 @@ WeaveSvc.prototype = { case "idle": this._log.trace("Idle time hit, trying to sync"); Svc.Idle.removeIdleObserver(this, IDLE_TIME); + this._hasIdleObserver = false; Utils.delay(function() this.sync(false), 0, this); break; } @@ -966,6 +967,7 @@ WeaveSvc.prototype = { // Clear out a sync that's just waiting for idle if we happen to have one try { Svc.Idle.removeIdleObserver(this, IDLE_TIME); + this._hasIdleObserver = false; } catch(ex) {} }, @@ -992,6 +994,11 @@ WeaveSvc.prototype = { * */ syncOnIdle: function WeaveSvc_syncOnIdle() { + // No need to add a duplicate idle observer + if (this._hasIdleObserver) + return; + + this._hasIdleObserver = true; this._log.debug("Idle timer created for sync, will sync after " + IDLE_TIME + " seconds of inactivity."); Svc.Idle.addIdleObserver(this, IDLE_TIME); From 87473546f7d47ea69f5c7cbf253bf320f16732e5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 30 Nov 2009 14:03:59 -0800 Subject: [PATCH 1511/1860] Bug 531943 - Sync scheduled by global threshold during private browsing Use checkSyncStatus instead of directly calling syncOnIdle or scheduleNextSync so that we only schedule if we're okay to sync. --- services/sync/modules/service.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e76f10071e7f..dc227a13800a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -428,17 +428,9 @@ WeaveSvc.prototype = { } this._log.trace("Global score updated: " + this.globalScore); - - if (this.globalScore > this.syncThreshold) { - this._log.debug("Global Score threshold hit, triggering sync."); - this.syncOnIdle(); - } - else if (!this._syncTimer) // start the clock if it isn't already - this._scheduleNextSync(); + this._checkSyncStatus(); }, - // These are global (for all engines) - // gets cluster from central LDAP server and returns it, or null on error _findCluster: function _findCluster() { this._log.debug("Finding cluster for user " + this.username); @@ -985,8 +977,13 @@ WeaveSvc.prototype = { return; } - // otherwise, schedule the sync - this._scheduleNextSync(); + // Only set the wait time to 0 if we need to sync right away + let wait; + if (this.globalScore > this.syncThreshold) { + this._log.debug("Global Score threshold hit, triggering sync."); + wait = 0; + } + this._scheduleNextSync(wait); }, /** From 5f22d2c722a9481296497f3b31799d6fec6d0865 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 30 Nov 2009 14:36:03 -0800 Subject: [PATCH 1512/1860] Provide a fake service for platforms that don't have the service like Private Browsing (for Fennec/Seamonkey). --- services/sync/modules/util.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index bb7560fe1dcc..2c45580d4770 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -321,15 +321,29 @@ let Utils = { lazySvc: function Weave_lazySvc(dest, prop, cid, iface) { let getter = function() { delete dest[prop]; + let svc = null; + + // Try creating a fake service if we can handle that if (!Cc[cid]) { + switch (cid) { + case "@mozilla.org/privatebrowsing;1": + svc = { + autoStarted: false, + privateBrowsingEnabled: false + }; + break; + } + let log = Log4Moz.repository.getLogger("Service.Util"); - log.warn("Component " + cid + " requested, but doesn't exist on " - + "this platform."); - return null; - } else{ - dest[prop] = Cc[cid].getService(iface); - return dest[prop]; + if (svc == null) + log.warn("Component " + cid + " doesn't exist on this platform."); + else + log.debug("Using a fake svc object for " + cid); } + else + svc = Cc[cid].getService(iface); + + return dest[prop] = svc; }; dest.__defineGetter__(prop, getter); }, From b03daefa284168cfee2361a76ff1b598be5259b0 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 1 Dec 2009 11:36:56 -0800 Subject: [PATCH 1513/1860] Share tab-gathering code for both createRecord and locallyOpenTabMatches. --- services/sync/modules/engines/tabs.js | 70 +++++++++------------------ 1 file changed, 23 insertions(+), 47 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0d3625eb6942..c9998706c749 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -91,36 +91,9 @@ TabEngine.prototype = { * reappear in the menu. */ locallyOpenTabMatchesURL: function TabEngine_localTabMatches(url) { - // url should be string, not object - /* Some code duplication from _addFirefoxTabsToRecord and - * _addFennecTabsToRecord. Unify? */ - if (Cc["@mozilla.org/browser/sessionstore;1"]) { - let state = this._store._sessionStore.getBrowserState(); - let session = JSON.parse(state); - for (let i = 0; i < session.windows.length; i++) { - let window = session.windows[i]; - for (let j = 0; j < window.tabs.length; j++) { - let tab = window.tabs[j]; - if (tab.entries.length > 0) { - let tabUrl = tab.entries[tab.entries.length-1].url; - if (tabUrl == url) { - return true; - } - } - } - } - } else { - let wm = Cc["@mozilla.org/appshell/window-mediator;1"] - .getService(Ci.nsIWindowMediator); - let browserWindow = wm.getMostRecentWindow("navigator:browser"); - for each (let tab in browserWindow.Browser._tabs ) { - let tabUrl = tab.browser.contentWindow.location.toString(); - if (tabUrl == url) { - return true; - } - } - } - return false; + return this._store.getAllTabs().some(function(tab) { + return tab.urlHistory[0] == url; + }); } }; @@ -161,10 +134,7 @@ TabStore.prototype = { return id == Clients.clientID; }, - createRecord: function TabStore_createRecord(id, cryptoMetaURL) { - let record = new TabSetRecord(); - record.clientName = Clients.clientName; - + getAllTabs: function getAllTabs() { // Iterate through each tab of each window let allTabs = []; let wins = Svc.WinMediator.getEnumerator("navigator:browser"); @@ -173,24 +143,30 @@ TabStore.prototype = { let window = wins.getNext(); let tabs = window.gBrowser && window.gBrowser.tabContainer.childNodes; tabs = tabs || window.Browser._tabs; + + // Extract various pieces of tab data Array.forEach(tabs, function(tab) { - allTabs.push(tab); + let browser = tab.linkedBrowser || tab.browser; + allTabs.push({ + title: browser.contentTitle || "", + urlHistory: [browser.currentURI.spec], + icon: browser.mIconURL || "", + lastUsed: tab.lastUsed || 0 + }); }); } + + return allTabs; + }, - // Extract various pieces of tab data and sort them in descending used - let tabData = allTabs.map(function(tab) { - let browser = tab.linkedBrowser || tab.browser; - return { - title: browser.contentTitle || "", - urlHistory: [browser.currentURI.spec], - icon: browser.mIconURL || "", - lastUsed: tab.lastUsed || 0 - }; - }).sort(function(a, b) b.lastUsed - a.lastUsed); + createRecord: function TabStore_createRecord(id, cryptoMetaURL) { + let record = new TabSetRecord(); + record.clientName = Clients.clientName; - // Only grab the most recently used tabs to sync - record.tabs = tabData.slice(0, 25); + // Sort tabs in descending-used order to grab the most recently used + record.tabs = this.getAllTabs().sort(function(a, b) { + return b.lastUsed - a.lastUsed; + }).slice(0, 25); record.tabs.forEach(function(tab) { this._log.debug("Wrapping tab: " + JSON.stringify(tab)); }, this); From ff7003fe256ecd43bf63b039f55396da81b14475 Mon Sep 17 00:00:00 2001 From: Igor Velkov Date: Tue, 1 Dec 2009 11:49:23 -0800 Subject: [PATCH 1514/1860] Bug 526521 - Can't open preferences in SeaMonkey 2.0.1pre. r=Mardak Reorganize fx-prefs to overlay itself so seamonkey can reference the same id and add Tabs sync. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index dc227a13800a..54f824d55713 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -358,7 +358,7 @@ WeaveSvc.prototype = { break; case SEAMONKEY_ID: - engines = ["Form", "History", "Password"]; + engines = ["Form", "History", "Password", "Tab"]; break; case THUNDERBIRD_ID: From 04bd1a76268cd2c3a31042581022381c1ed31f33 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 1 Dec 2009 15:39:43 -0500 Subject: [PATCH 1515/1860] bug 531205 - show bookmarks/history/passwords for local and remote devices for remote when wiping --- services/sync/locales/en-US/fx-prefs.dtd | 10 +++++----- services/sync/locales/en-US/fx-prefs.properties | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index d645f19af0c4..2bd0eb9758dc 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -54,12 +54,12 @@ - - + + - - - + + + diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties index d10d065c0bda..95292f03c155 100644 --- a/services/sync/locales/en-US/fx-prefs.properties +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -15,3 +15,9 @@ loginFailed = Login failed: %1$S recoverPasswordSuccess.title = Recover Password Success! recoverPasswordSuccess.label = We've sent you an email to your address on file. Please check it and follow the instructions to reset your password. + +additionalClients.label = and %S additional devices + +bookmarkCount.label = %S bookmarks +historyCount.label = %S days of history +passwordCount.label = %S passwords \ No newline at end of file From f2081ace7a3ee8a2cd57d011e774b967f20cfd63 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 2 Dec 2009 11:30:16 -0500 Subject: [PATCH 1516/1860] bug 530820 - add confirmation, error feedback, and attempt to provide some clarity that the user can't recover pp --- services/sync/locales/en-US/fx-prefs.dtd | 7 ++++--- services/sync/locales/en-US/fx-prefs.properties | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 2bd0eb9758dc..fe6bcfead8a0 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -35,8 +35,10 @@ - - + + + + @@ -61,7 +63,6 @@ - diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties index 95292f03c155..db589ba61c9d 100644 --- a/services/sync/locales/en-US/fx-prefs.properties +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -7,6 +7,9 @@ usernameNotAvailable.label = Already in use passwordMismatch.label = Passwords do not match passwordTooWeak.label = Password is too weak +entriesMustMatch.label = Phrases do not match +cannotMatchPassword.label = Secret phrase cannot match password + errorCreatingAccount.title = Error Creating Account serverInvalid.label = Please enter a valid server URL From 10c59f345cf8e689551c8bc3a55539d531deb708 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Dec 2009 14:44:17 -0800 Subject: [PATCH 1517/1860] Bug 531239 - clear out duplicate/old machines from Clients record Get rid of persistent storage for clients and tabs to always fetch fresh records. --- services/sync/modules/engines/clients.js | 71 +++++++----------------- services/sync/modules/engines/tabs.js | 24 +++----- 2 files changed, 27 insertions(+), 68 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 98860d04f3e2..0558cc50cd3a 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -49,7 +49,7 @@ Cu.import("resource://weave/type_records/clientData.js"); Utils.lazy(this, 'Clients', ClientEngine); function ClientEngine() { - this._ClientEngine_init(); + this._init(); } ClientEngine.prototype = { __proto__: SyncEngine.prototype, @@ -61,8 +61,10 @@ ClientEngine.prototype = { _trackerObj: ClientTracker, _recordObj: ClientRecord, - _ClientEngine_init: function ClientEngine__init() { - this._init(); + _init: function _init() { + // Reset the client on every startup so that we fetch recent clients + SyncEngine.prototype._init.call(this); + this._resetClient(); Utils.prefs.addObserver("", this, false); }, @@ -160,8 +162,11 @@ ClientEngine.prototype = { }, _resetClient: function ClientEngine__resetClient() { - this.resetLastSync(); + SyncEngine.prototype._resetClient.call(this); this._store.wipe(); + + // Make sure the local client exists after wiping + this.setInfo(this.clientID, this.updateLocalInfo({})); } }; @@ -176,8 +181,6 @@ ClientStore.prototype = { __proto__: Store.prototype, - _snapshot: "meta/clients", - ////////////////////////////////////////////////////////////////////////////// // ClientStore Methods @@ -191,42 +194,14 @@ ClientStore.prototype = { */ init: function ClientStore_init() { this._init.call(this); - this.loadSnapshot(); - - // Get fresh local client info in case prefs were changed when closed - let id = Clients.clientID; - this.setInfo(id, Clients.updateLocalInfo(this.clients[id] || {})); - }, - - /** - * Load client data from json disk - */ - loadSnapshot: function ClientStore_loadSnapshot() { - Utils.jsonLoad(this._snapshot, this, function(json) this.clients = json); - }, - - /** - * Log that we're about to change the client store then save to disk - */ - modify: function ClientStore_modify(message, action) { - this._log.debug(message); - action.call(this); - this.saveSnapshot(); - }, - - /** - * Save client data to json disk - */ - saveSnapshot: function ClientStore_saveSnapshot() { - Utils.jsonSave(this._snapshot, this, this.clients); }, /** * Set the client data for a guid. Use Engine.setInfo to update tracker. */ setInfo: function ClientStore_setInfo(id, info) { - this.modify("Setting client " + id + ": " + JSON.stringify(info), - function() this.clients[id] = info); + this._log.debug("Setting client " + id + ": " + JSON.stringify(info)); + this.clients[id] = info; }, ////////////////////////////////////////////////////////////////////////////// @@ -239,10 +214,9 @@ ClientStore.prototype = { // Store.prototype Methods changeItemID: function ClientStore_changeItemID(oldID, newID) { - this.modify("Changing id from " + oldId + " to " + newID, function() { - this.clients[newID] = this.clients[oldID]; - delete this.clients[oldID]; - }); + this._log.debug("Changing id from " + oldId + " to " + newID); + this.clients[newID] = this.clients[oldID]; + delete this.clients[oldID]; }, create: function ClientStore_create(record) { @@ -253,7 +227,6 @@ ClientStore.prototype = { let record = new ClientRecord(); record.id = id; record.payload = this.clients[id]; - return record; }, @@ -262,22 +235,18 @@ ClientStore.prototype = { itemExists: function ClientStore_itemExists(id) id in this.clients, remove: function ClientStore_remove(record) { - this.modify("Removing client " + record.id, function() - delete this.clients[record.id]); + this._log.debug("Removing client " + record.id); + delete this.clients[record.id]; }, update: function ClientStore_update(record) { - this.modify("Updating client " + record.id, function() - this.clients[record.id] = record.payload); + this._log.debug("Updating client " + record.id); + this.clients[record.id] = record.payload; }, wipe: function ClientStore_wipe() { - this.modify("Wiping local clients store", function() { - this.clients = {}; - - // Make sure the local client is still here - this.clients[Clients.clientID] = Clients.updateLocalInfo({}); - }); + this._log.debug("Wiping local clients store") + this.clients = {}; }, }; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index c9998706c749..c97e9c92d03d 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -62,6 +62,12 @@ TabEngine.prototype = { _trackerObj: TabTracker, _recordObj: TabSetRecord, + _init: function _init() { + // Reset the client on every startup so that we fetch recent tabs + SyncEngine.prototype._init.call(this); + this._resetClient(); + }, + // API for use by Weave UI code to give user choices of tabs to open: getAllClients: function TabEngine_getAllClients() { return this._store._remoteClients; @@ -72,7 +78,7 @@ TabEngine.prototype = { }, _resetClient: function TabEngine__resetClient() { - this.resetLastSync(); + SyncEngine.prototype._resetClient.call(this); this._store.wipe(); }, @@ -105,22 +111,10 @@ TabStore.prototype = { __proto__: Store.prototype, name: "tabs", _logName: "Tabs.Store", - _filePath: "meta/tabSets", _remoteClients: {}, _TabStore_init: function TabStore__init() { this._init(); - this._readFromFile(); - }, - - _writeToFile: function TabStore_writeToFile() { - Utils.jsonSave(this._filePath, this, this._remoteClients); - }, - - _readFromFile: function TabStore_readFromFile() { - Utils.jsonLoad(this._filePath, this, function(json) { - this._remoteClients = json; - }); }, get _sessionStore() { @@ -189,10 +183,6 @@ TabStore.prototype = { create: function TabStore_create(record) { this._log.debug("Adding remote tabs from " + record.clientName); this._remoteClients[record.id] = record.cleartext; - this._writeToFile(); - // TODO writing to file after every change is inefficient. How do we - // make sure to do it (or at least flush it) only after sync is done? - // override syncFinished } }; From 3bb01a3aacb778621c133860ee47a2718273a45d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Dec 2009 14:44:52 -0800 Subject: [PATCH 1518/1860] Bug 530717 - sync after wipe local should always replace local values with remote Always take the incoming item after a wipe and otherwise do the normal reconcile. --- services/sync/modules/engines/prefs.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 5df1f3c757b1..9556935508a6 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -61,6 +61,20 @@ PrefsEngine.prototype = { _storeObj: PrefStore, _trackerObj: PrefTracker, _recordObj: PrefRec, + + _wipeClient: function _wipeClient() { + SyncEngine.prototype._wipeClient.call(this); + this.justWiped = true; + }, + + _reconcile: function _reconcile(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); + } }; From c4cc50eb66c5c8fdefd9bc8dbba07cc70d820cbe Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Dec 2009 14:46:02 -0800 Subject: [PATCH 1519/1860] Bug 532173 - Don't sync tabs of some pages (weave firstrun, about:blank) Ignore certain filtered urls when creating a list of tabs for remote machines. --- services/sync/modules/engines/tabs.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index c97e9c92d03d..0ea6484dd8c1 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -49,6 +49,8 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/tabs.js"); Cu.import("resource://weave/engines/clientData.js"); +const filteredUrls = /^(about:blank|chrome:\/\/weave\/.*)$/i; + function TabEngine() { this._init(); } @@ -128,7 +130,8 @@ TabStore.prototype = { return id == Clients.clientID; }, - getAllTabs: function getAllTabs() { + + getAllTabs: function getAllTabs(filter) { // Iterate through each tab of each window let allTabs = []; let wins = Svc.WinMediator.getEnumerator("navigator:browser"); @@ -141,9 +144,15 @@ TabStore.prototype = { // Extract various pieces of tab data Array.forEach(tabs, function(tab) { let browser = tab.linkedBrowser || tab.browser; + let url = browser.currentURI.spec; + + // Filter out some urls if necessary + if (filter && filteredUrls.test(url)) + return; + allTabs.push({ title: browser.contentTitle || "", - urlHistory: [browser.currentURI.spec], + urlHistory: [url], icon: browser.mIconURL || "", lastUsed: tab.lastUsed || 0 }); @@ -158,7 +167,7 @@ TabStore.prototype = { record.clientName = Clients.clientName; // Sort tabs in descending-used order to grab the most recently used - record.tabs = this.getAllTabs().sort(function(a, b) { + record.tabs = this.getAllTabs(true).sort(function(a, b) { return b.lastUsed - a.lastUsed; }).slice(0, 25); record.tabs.forEach(function(tab) { From 3191262181036f6a63474fdbb8c74a1201efc2e8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Dec 2009 17:25:14 -0800 Subject: [PATCH 1520/1860] Bug 531005 - Sync is not working after update IWeaveCrypto.unwrapSymmetricKey NS_ERROR_FAILURE Check that the cryptometa is unwrappable when syncing; if not purge the key and data and make a new crypto record. --- services/sync/modules/engines.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 0e24e9fc1dc5..d48e975e44a8 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -349,7 +349,26 @@ SyncEngine.prototype = { _syncStartup: function SyncEngine__syncStartup() { this._log.debug("Ensuring server crypto records are there"); + // Try getting/unwrapping the crypto record let meta = CryptoMetas.get(this.cryptoMetaURL); + if (meta) { + try { + let pubkey = PubKeys.getDefaultKey(); + let privkey = PrivKeys.get(pubkey.privateKeyUri); + meta.getKey(privkey, ID.get("WeaveCryptoID")); + } + catch(ex) { + // Remove traces of this bad cryptometa + this._log.debug("Purging bad data after failed unwrap crypto: " + ex); + CryptoMetas.del(this.cryptoMetaURL); + meta = null; + + // Remove any potentially tained data + new Resource(this.engineURL).delete(); + } + } + + // Generate a new crypto record if (!meta) { let symkey = Svc.Crypto.generateRandomKey(); let pubkey = PubKeys.getDefaultKey(); From 95434ee2bce4a1f5b903697b9f28f6a17c507372 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Dec 2009 17:57:13 -0800 Subject: [PATCH 1521/1860] Bug 531005 - Sync is not working after update IWeaveCrypto.unwrapSymmetricKey NS_ERROR_FAILURE Avoid concurrent key generation by updating meta/global immediately after resetting the client and have wipeServer not delete the meta collection. Also on detecting a syncid mismatch, sleep to allow remote keypair to be uploaded, and don't force a mismatch on upgrade. --- services/sync/modules/service.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 54f824d55713..d6a2046f5764 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -268,12 +268,6 @@ WeaveSvc.prototype = { this._registerEngines(); - // Reset our sync id if we're upgrading, so sync knows to reset local data - if (WEAVE_VERSION != Svc.Prefs.get("lastversion")) { - this._log.info("Resetting client syncID from _onStartup."); - Clients.resetSyncID(); - } - let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. getService(Ci.nsIHttpProtocolHandler).userAgent; this._log.info(ua); @@ -857,6 +851,10 @@ WeaveSvc.prototype = { Clients.syncID = meta.payload.syncID; this._log.info("Reset the client after a server/client sync ID mismatch"); this._updateRemoteVersion(meta); + + // XXX Bug 531005 Wait long enough to allow potentially another concurrent + // sync to finish generating the keypair and uploading them + Sync.sleep(15000); } // We didn't wipe the server and we're not out of date, so update remote else @@ -1220,17 +1218,17 @@ WeaveSvc.prototype = { _freshStart: function WeaveSvc__freshStart() { this.resetClient(); - this._log.info("Reset client data from freshStart."); - this._log.info("Client metadata wiped, deleting server data"); - this.wipeServer(); - // XXX Bug 504125 Wait a while after wiping so that the DELETEs replicate - Sync.sleep(2000); - - this._log.trace("Uploading new metadata record"); + this._log.debug("Uploading new metadata record from freshStart"); let meta = new WBORecord(this.metaURL); meta.payload.syncID = Clients.syncID; this._updateRemoteVersion(meta); + + // Wipe everything we know about except meta because we just uploaded it + let collections = [Clients].concat(Engines.getAll()).map(function(engine) { + return engine.name; + }); + this.wipeServer(["crypto", "keys"].concat(collections)); }, _updateRemoteVersion: function WeaveSvc__updateRemoteVersion(meta) { From 21e157f960fb398467504aac4da3c40bdddc8b91 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Dec 2009 18:25:18 -0800 Subject: [PATCH 1522/1860] Bug 532570 - "keyring doesn't contain a key" when signing-in with a differently-cased username Make sure username is always lowercase so that pubkey uri, storage uri, etc. are all the same no matter how the user logged in. Server needs to be wiped to make sure existing keys with other casing are removed. --- services/sync/modules/identity.js | 2 +- services/sync/modules/service.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 7963b02ee3c0..82e690046e9f 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -85,7 +85,7 @@ Identity.prototype = { // Look up the password then cache it if (this._password == null) for each (let login in this._logins) - if (login.username == this.username) + if (login.username.toLowerCase() == this.username) this._password = login.password; return this._password; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d6a2046f5764..1c8ec68c61aa 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -108,11 +108,14 @@ WeaveSvc.prototype = { _keyPair: {}, get username() { - return Svc.Prefs.get("username", ""); + return Svc.Prefs.get("username", "").toLowerCase(); }, set username(value) { - if (value) + if (value) { + // Make sure all uses of this new username is lowercase + value = value.toLowerCase(); Svc.Prefs.set("username", value); + } else Svc.Prefs.reset("username"); From 72b4f0a2f2c040d5f660d2f400ba44d844627e46 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Dec 2009 19:03:27 -0800 Subject: [PATCH 1523/1860] Bug 532449 - Tabs don't sync until 4 syncs have happened Always update the lastModified time to avoid tabs only sometimes getting updated. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1c8ec68c61aa..1670962bf7a1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1104,7 +1104,7 @@ WeaveSvc.prototype = { throw "aborting sync, failed to get collections"; // Convert the response to an object and read out the modified times - for each (let engine in [Clients].concat(Engines.getEnabled())) + for each (let engine in [Clients].concat(Engines.getAll())) engine.lastModified = info.obj[engine.name] || 0; this._log.trace("Refreshing client list"); From 1cc9742f2682eb188080dc5a46ee3081060749f4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Dec 2009 19:20:44 -0800 Subject: [PATCH 1524/1860] Bug 488922 - Treat bookmarks restore specially Add observers for bookmarks-restore-* and ignore changes during import and trigger a fresh start on success. --- services/sync/modules/engines/bookmarks.js | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c73067ba718f..440928de577a 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -48,6 +48,7 @@ const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; Cu.import("resource://gre/modules/utils.js"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); @@ -91,6 +92,28 @@ BookmarksEngine.prototype = { _storeObj: BookmarksStore, _trackerObj: BookmarksTracker, + _init: function _init() { + SyncEngine.prototype._init.call(this); + this._handleImport(); + }, + + _handleImport: function _handleImport() { + Observers.add("bookmarks-restore-begin", function() { + this._log.debug("Ignoring changes from importing bookmarks"); + this._tracker.ignoreAll = true; + }, this); + + Observers.add("bookmarks-restore-success", function() { + this._log.debug("Triggering fresh start on successful import"); + this.resetLastSync(); + this._tracker.ignoreAll = false; + }, this); + + Observers.add("bookmarks-restore-failed", function() { + this._tracker.ignoreAll = false; + }, this); + }, + _sync: Utils.batchSync("Bookmark", SyncEngine), _syncStartup: function _syncStart() { From abd820531151580aa3570dfbf85e55017c3c6367 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 3 Dec 2009 01:52:17 -0500 Subject: [PATCH 1525/1860] bug 530813 - better explanation of Sync and some iterations on bug 530824, just close the prefwindow on wizard finish --- services/sync/locales/en-US/fx-prefs.dtd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index fe6bcfead8a0..649c9ee53c9e 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -19,6 +19,8 @@ + + From 9c18198c2b8f5143e5410520ada35aa7460461d6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 3 Dec 2009 12:14:56 -0800 Subject: [PATCH 1526/1860] Bug 532722 - Use AddonOptionsLoad event to update Weave options Make the weave add-on guid available as a constant and watch for AddonOptionsLoad with the matching addonID. --- services/sync/modules/constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 67c91b200884..8f9fc4e13e99 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -38,6 +38,7 @@ let EXPORTED_SYMBOLS = [((this[key] = val), key) for ([key, val] in Iterator({ WEAVE_VERSION: "@weave_version@", +WEAVE_ID: "@weave_id@", // Last client version this client can read. If the server contains an older // version, this client will wipe the data on the server first. From 79f7d791eab7799e3c68363ccd16eabb6f19d701 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 3 Dec 2009 15:03:39 -0500 Subject: [PATCH 1527/1860] bug 530824 - close window and start syncing much sooner when setting up second machine --- services/sync/modules/service.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1670962bf7a1..f479e41558b9 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -404,8 +404,8 @@ WeaveSvc.prototype = { break; case "idle": this._log.trace("Idle time hit, trying to sync"); - Svc.Idle.removeIdleObserver(this, IDLE_TIME); - this._hasIdleObserver = false; + Svc.Idle.removeIdleObserver(this, this._idleTime); + this._idleTime = 0; Utils.delay(function() this.sync(false), 0, this); break; } @@ -959,8 +959,8 @@ WeaveSvc.prototype = { // Clear out a sync that's just waiting for idle if we happen to have one try { - Svc.Idle.removeIdleObserver(this, IDLE_TIME); - this._hasIdleObserver = false; + Svc.Idle.removeIdleObserver(this, this._idleTime); + this._idleTime = 0; } catch(ex) {} }, @@ -990,16 +990,17 @@ WeaveSvc.prototype = { /** * Call sync() on an idle timer * + * delay is optional */ - syncOnIdle: function WeaveSvc_syncOnIdle() { + syncOnIdle: function WeaveSvc_syncOnIdle(delay) { // No need to add a duplicate idle observer - if (this._hasIdleObserver) + if (this._idleTime) return; - this._hasIdleObserver = true; + this._idleTime = delay || IDLE_TIME; this._log.debug("Idle timer created for sync, will sync after " + - IDLE_TIME + " seconds of inactivity."); - Svc.Idle.addIdleObserver(this, IDLE_TIME); + this._idleTime + " seconds of inactivity."); + Svc.Idle.addIdleObserver(this, this._idleTime); }, /** From f078310fd0d2fd012e5eba9e904d8d4ae0fc0470 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 3 Dec 2009 17:24:41 -0500 Subject: [PATCH 1528/1860] bug 526078 - fix up some wording --- services/sync/locales/en-US/fx-prefs.dtd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 649c9ee53c9e..0b5a7089d489 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -50,15 +50,15 @@ - + - + - + From 7becea03b99634b2c9c03128d2446f36758ee6c6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 3 Dec 2009 14:54:23 -0800 Subject: [PATCH 1529/1860] Bug 532770 - Allow tab sync's filtered urls to be set by pref Expose engine.tabs.filteredUrls as a string to be used as regex and filter case insensitively. --- services/sync/modules/engines/tabs.js | 4 ++-- services/sync/services-sync.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0ea6484dd8c1..d0dd189b6a01 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -49,8 +49,6 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/tabs.js"); Cu.import("resource://weave/engines/clientData.js"); -const filteredUrls = /^(about:blank|chrome:\/\/weave\/.*)$/i; - function TabEngine() { this._init(); } @@ -132,6 +130,8 @@ TabStore.prototype = { getAllTabs: function getAllTabs(filter) { + let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i"); + // Iterate through each tab of each window let allTabs = []; let wins = Svc.WinMediator.getEnumerator("navigator:browser"); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 7aed038775a3..05dad250dd09 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -15,6 +15,7 @@ pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.prefs", true); pref("extensions.weave.engine.tabs", true); pref("extensions.weave.engine.tabs.backup", true); +pref("extensions.weave.engine.tabs.filteredUrls", "^(about:blank|chrome://weave/.*)$"); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 6a0d9ce4b1202f3f0a3635ed1b0bf3375fd52b99 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 8 Dec 2009 16:09:26 -0800 Subject: [PATCH 1530/1860] Bug 533580 - strip busted tbird support --- services/sync/modules/constants.js | 1 - services/sync/modules/service.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 8f9fc4e13e99..fc0b7b7bf975 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -125,7 +125,6 @@ kSyncBackoffNotMet: "Trying to sync before the server said it // Application IDs FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", -THUNDERBIRD_ID: "{3550f703-e582-4d05-9a08-453d09bdfdc6}", FENNEC_ID: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}", SEAMONKEY_ID: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f479e41558b9..b0918110f508 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -357,10 +357,6 @@ WeaveSvc.prototype = { case SEAMONKEY_ID: engines = ["Form", "History", "Password", "Tab"]; break; - - case THUNDERBIRD_ID: - engines = ["Password"]; - break; } // Grab the actual engine and register them From becc3b07a85bda15645082d99056dfb953b9c9d7 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 8 Dec 2009 17:51:00 -0800 Subject: [PATCH 1531/1860] bug 526078 - add accesskeys throughout the prefpane and wizard --- services/sync/locales/en-US/fx-prefs.dtd | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 0b5a7089d489..27200ae5e52b 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -7,18 +7,26 @@ + + + + + + + + @@ -27,18 +35,23 @@ + - + + + + + @@ -70,6 +83,7 @@ + @@ -80,6 +94,7 @@ + @@ -87,7 +102,12 @@ + + + + + From 9c76acdaf8ca89e19b83b334e38eaf73072acfd9 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 9 Dec 2009 15:23:48 -0800 Subject: [PATCH 1532/1860] bug 533573 - use aria attributes to make the radiobuttons accessible --- services/sync/locales/en-US/fx-prefs.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 27200ae5e52b..1db6bec1b086 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -55,7 +55,7 @@ - + From 0f19549d921cd370dc2ef6c1318831e596b208a1 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 10 Dec 2009 12:20:16 -0800 Subject: [PATCH 1533/1860] bug 533759 - delay real startup for 10 seconds on Fennec to get away from startup --- services/sync/modules/service.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b0918110f508..9953459e214d 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -252,6 +252,9 @@ WeaveSvc.prototype = { let enum = Svc.WinMediator.getEnumerator("navigator:browser"); while (enum.hasMoreElements()) wait += enum.getNext().gBrowser.mTabs.length; + break; + case FENNEC_ID: + wait = 10; } // Make sure we wait a little but but not too long in the worst case From 8ab8ce33feecf2f541fad310596be1fc86a6bdfe Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Dec 2009 16:12:14 -0800 Subject: [PATCH 1534/1860] Bug 532936 - Add root level node for bookmarks synced from Fennec Create a mobile root in Firefox and link it to the Library UI as a query that fixes its own title if changed. Mark the mobile root on Firefox/Fennec as special so Weave uses "mobile" for the guid in the record, and items will appear in the appropriate folder. --- services/sync/locales/en-US/sync.properties | 2 + services/sync/modules/engines/bookmarks.js | 56 ++++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 689eadead636..11364b94984b 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -16,6 +16,8 @@ syncing.label = Weave is syncing: %S status.offline = Not Signed In status.privateBrowsing = Disabled (Private Browsing) +mobile.label = Mobile Bookmarks + error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. # should decide if we're going to show this diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 440928de577a..4d150718763b 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -65,6 +65,18 @@ let kSpecialIds = {}; ].forEach(function([guid, placeName]) { Utils.lazy2(kSpecialIds, guid, function() Svc.Bookmark[placeName]); }); +Utils.lazy2(kSpecialIds, "mobile", function() { + // Use the (one) mobile root if it already exists + let anno = "mobile/bookmarksRoot"; + let root = Svc.Annos.getItemsWithAnnotation(anno, {}); + if (root.length != 0) + return root[0]; + + // Create the special mobile folder to store mobile bookmarks + let mobile = Svc.Bookmark.createFolder(Svc.Bookmark.placesRoot, "mobile", -1); + Utils.anno(mobile, anno, 1); + return mobile; +}); // Create some helper functions to convert GUID/ids function idForGUID(guid) { @@ -1026,6 +1038,15 @@ BookmarksTracker.prototype = { if (this.ignoreAll) return true; + // Ensure that the mobile bookmarks query is correct in the UI + this._ensureMobileQuery(); + + // Make sure to remove items that have the exclude annotation + if (Svc.Annos.itemHasAnnotation(itemId, "places/excludeFromBackup")) { + this.removeChangedID(GUIDForId(itemId)); + return true; + } + // Get the folder id if we weren't given one if (folder == null) folder = this._bms.getFolderIdForItem(itemId); @@ -1061,16 +1082,39 @@ BookmarksTracker.prototype = { this._addSuccessor(itemId); }, + _ensureMobileQuery: function _ensureMobileQuery() { + let anno = "PlacesOrganizer/OrganizerQuery"; + let find = function(val) Svc.Annos.getItemsWithAnnotation(anno, {}).filter( + function(id) Utils.anno(id, anno) == val); + + // Don't continue if the Library isn't ready + let all = find("AllBookmarks"); + if (all.length == 0) + return; + + // Disable handling of notifications while changing the mobile query + this.ignoreAll = true; + + // Make sure we have a mobile bookmarks query + let mobile = find("MobileBookmarks"); + let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile); + let title = Str.sync.get("mobile.label"); + if (mobile.length == 0) { + let query = Svc.Bookmark.insertBookmark(all[0], queryURI, -1, title); + Utils.anno(query, anno, "MobileBookmarks"); + Utils.anno(query, "places/excludeFromBackup", 1); + } + // Make sure the existing title is correct + else if (Svc.Bookmark.getItemTitle(mobile[0]) != title) + Svc.Bookmark.setItemTitle(mobile[0], title); + + this.ignoreAll = false; + }, + onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) { if (this._ignore(itemId)) return; - // Make sure to remove items that now have the exclude annotation - if (property == "places/excludeFromBackup") { - this.removeChangedID(GUIDForId(itemId)); - return; - } - // ignore annotations except for the ones that we sync let annos = ["bookmarkProperties/description", "bookmarkProperties/loadInSidebar", "bookmarks/staticTitle", From 16b7cee53c7f95624e8440da372e835410281915 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Dec 2009 18:37:30 -0800 Subject: [PATCH 1535/1860] Bug 533475 - Improve language for Fennec tab sync page Detect if tabs sync is pending or if tabs are all open or there's no tab data to show the appropriate message. --- services/sync/locales/en-US/sync.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 11364b94984b..3d5e9d5e56b6 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -18,6 +18,10 @@ status.privateBrowsing = Disabled (Private Browsing) mobile.label = Mobile Bookmarks +remote.pending.label = Remote tabs are being synced… +remote.missing.label = Sync your other computers again to access their tabs +remote.opened.label = All remote tabs are already open + error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. # should decide if we're going to show this From e49261a8afe4688314dc82645bebe74b9b01a542 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Dec 2009 18:39:51 -0800 Subject: [PATCH 1536/1860] Have wipeRemote wipe just the engines' data and make sure the clients process the command. --- services/sync/modules/engines/clients.js | 5 +++++ services/sync/modules/service.js | 18 +++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 0558cc50cd3a..c5ab1771205c 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -161,6 +161,11 @@ ClientEngine.prototype = { } }, + // Always process incoming items because they might have commands + _reconcile: function _reconcile() { + return true; + }, + _resetClient: function ClientEngine__resetClient() { SyncEngine.prototype._resetClient.call(this); this._store.wipe(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 9953459e214d..5dd827c6de7f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1066,7 +1066,7 @@ WeaveSvc.prototype = { this.wipeClient(); break; case "wipeRemote": - this.wipeRemote(); + this.wipeRemote(Engines.getAll().map(function(e) e.name)); break; default: this._scheduleNextSync(); @@ -1332,17 +1332,21 @@ WeaveSvc.prototype = { */ wipeRemote: function WeaveSvc_wipeRemote(engines) this._catch(this._notify("wipe-remote", "", function() { + // Make sure stuff gets uploaded + Clients.resetSyncID(); + // Clear out any server data - //this.wipeServer(engines); + this.wipeServer(engines); // Only wipe the engines provided - if (engines) { + if (engines) engines.forEach(function(e) this.prepCommand("wipeEngine", [e]), this); - return; - } - // Tell the remote machines to wipe themselves - this.prepCommand("wipeAll", []); + else + this.prepCommand("wipeAll", []); + + // Make sure the changed clients get updated + Clients.sync(); }))(), /** From 788eb9c14686aec0c78e1419ce405b0dafd413dd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 11 Dec 2009 11:32:23 -0800 Subject: [PATCH 1537/1860] Remove engines from failing load tests now that engines are gone. --- services/sync/tests/unit/test_load_modules.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js index 7ae176357962..5efff1fd96cb 100644 --- a/services/sync/tests/unit/test_load_modules.js +++ b/services/sync/tests/unit/test_load_modules.js @@ -7,17 +7,11 @@ const modules = [ "constants.js", "engines/bookmarks.js", "engines/clientData.js", - "engines/cookies.js", - "engines/extensions.js", "engines/forms.js", "engines/history.js", - "engines/input.js", - "engines/microformats.js", "engines/passwords.js", - "engines/plugins.js", "engines/prefs.js", "engines/tabs.js", - "engines/themes.js", "engines.js", "ext/Observers.js", "ext/Preferences.js", From 447a1522200199d1937cb76d631615c878445aa9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 14 Dec 2009 16:05:07 -0800 Subject: [PATCH 1538/1860] Bug 534687 - Weave's pref syncing can sync lightweight themes in a broken fashion Trigger lightweight theme manager's currentTheme setter when changing the usedThemes pref. --- services/sync/modules/engines/prefs.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 9556935508a6..f853bcd95c4c 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -147,6 +147,22 @@ PrefStore.prototype = { break; case "string": this._prefs.setCharPref(values[i]["name"], values[i]["value"]); + + // Notify the lightweight theme manager of the new value + if (values[i].name == "lightweightThemes.usedThemes") { + try { + let ltm = {}; + Cu.import("resource://gre/modules/LightweightThemeManager.jsm", ltm); + ltm = ltm.LightweightThemeManager; + if (ltm.currentTheme) { + ltm.currentTheme = null; + ltm.currentTheme = ltm.usedThemes[0]; + } + } + // LightweightThemeManager only exists in Firefox 3.6+ + catch (ex) {} + } + break; case "boolean": this._prefs.setBoolPref(values[i]["name"], values[i]["value"]); From ee7d9b04646bd43e36bd32fc5e877e97daba8f31 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 15 Dec 2009 14:21:12 -0800 Subject: [PATCH 1539/1860] Bug 534944 - Avoid loading/importing weave files until necessary to speed up fennec startup Have importing service.js trigger startup instead of only starting up from Weave.js, which now is used as a backup kickstarter. Only call import(service.js) when necessary from various Fennec files. --- services/sync/Weave.js | 9 +++++++-- services/sync/modules/service.js | 5 +++-- services/sync/modules/util.js | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 6db75d883eaa..cfa151d25e7a 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -59,8 +59,13 @@ WeaveService.prototype = { break; case "final-ui-startup": - Cu.import("resource://weave/service.js"); - Weave.Service.onStartup(); + // Force Weave service to load if it hasn't triggered from overlays + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.initWithCallback({ + notify: function() { + Cu.import("resource://weave/service.js"); + } + }, 10000, Ci.nsITimer.TYPE_ONE_SHOT); break; } } diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5dd827c6de7f..47cf262f1ec5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -253,8 +253,6 @@ WeaveSvc.prototype = { while (enum.hasMoreElements()) wait += enum.getNext().gBrowser.mTabs.length; break; - case FENNEC_ID: - wait = 10; } // Make sure we wait a little but but not too long in the worst case @@ -1523,3 +1521,6 @@ WeaveSvc.prototype = { } }, }; + +// Load Weave on the first time this file is loaded +Weave.Service.onStartup(); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 2c45580d4770..6c94b6f1584a 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -795,6 +795,7 @@ let Utils = { let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); +Svc.Obs = Observers; [["Annos", "@mozilla.org/browser/annotation-service;1", "nsIAnnotationService"], ["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], From 2dad15504d34f831623e655a13e769732c5529c0 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 15 Dec 2009 14:21:13 -0800 Subject: [PATCH 1540/1860] Bug 534923 - Only show Mobile Bookmarks if there are mobile bookmarks Fix up existing users as well as only creating the place query under AllBookmarks if there are mobile bookmarks. --- services/sync/modules/engines/bookmarks.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 4d150718763b..5c63793cce13 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -187,6 +187,7 @@ BookmarksEngine.prototype = { _syncFinish: function _syncFinish() { SyncEngine.prototype._syncFinish.call(this); delete this._lazyMap; + this._tracker._ensureMobileQuery(); }, _findDupe: function _findDupe(item) { @@ -1095,11 +1096,17 @@ BookmarksTracker.prototype = { // Disable handling of notifications while changing the mobile query this.ignoreAll = true; - // Make sure we have a mobile bookmarks query let mobile = find("MobileBookmarks"); let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile); let title = Str.sync.get("mobile.label"); - if (mobile.length == 0) { + + // Don't add OR do remove the mobile bookmarks if there's nothing + if (Svc.Bookmark.getIdForItemAt(kSpecialIds.mobile, 0) == -1) { + if (mobile.length != 0) + Svc.Bookmark.removeItem(mobile[0]); + } + // Add the mobile bookmarks query if it doesn't exist + else if (mobile.length == 0) { let query = Svc.Bookmark.insertBookmark(all[0], queryURI, -1, title); Utils.anno(query, anno, "MobileBookmarks"); Utils.anno(query, "places/excludeFromBackup", 1); From 587d848c00ed425940e5778917e6f78b42e1d7df Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 15 Dec 2009 14:21:13 -0800 Subject: [PATCH 1541/1860] Bug 532175 - Send event when Weave is ready to register new engines Notify with "weave:service:ready" so observers can lazily load and wait to add engines, Weave UI. --- services/sync/modules/service.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 47cf262f1ec5..bae26e33d046 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -302,6 +302,9 @@ WeaveSvc.prototype = { this._updateCachedURLs(); + // Send an event now that Weave service is ready + Svc.Obs.notify("weave:service:ready"); + if (Svc.Prefs.get("autoconnect")) this._autoConnect(); }, From 77e2e5531fd11a1d4b7423e621f0acb4071c3b6a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 15 Dec 2009 14:51:16 -0800 Subject: [PATCH 1542/1860] Add pref for prefs logger and remove lightweight persisted prefs as those will be generated. --- services/sync/services-sync.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 05dad250dd09..ddef301e1cdb 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -29,6 +29,7 @@ pref("extensions.weave.log.logger.engine.clients", "Debug"); pref("extensions.weave.log.logger.engine.forms", "Debug"); pref("extensions.weave.log.logger.engine.history", "Debug"); pref("extensions.weave.log.logger.engine.passwords", "Debug"); +pref("extensions.weave.log.logger.engine.prefs", "Debug"); pref("extensions.weave.log.logger.engine.tabs", "Debug"); // Preferences to be synced by default @@ -75,8 +76,6 @@ pref("extensions.weave.prefs.sync.general.smoothScroll", true); pref("extensions.weave.prefs.sync.javascript.enabled", true); pref("extensions.weave.prefs.sync.layout.spellcheckDefault", true); pref("extensions.weave.prefs.sync.lightweightThemes.isThemeSelected", true); -pref("extensions.weave.prefs.sync.lightweightThemes.persisted.footerURL", true); -pref("extensions.weave.prefs.sync.lightweightThemes.persisted.headerURL", true); pref("extensions.weave.prefs.sync.lightweightThemes.usedThemes", true); pref("extensions.weave.prefs.sync.network.cookie.cookieBehavior", true); pref("extensions.weave.prefs.sync.network.cookie.lifetimePolicy", true); From 725d2c04fd1c4288e7e68e654fb78844d0c7a4ec Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Dec 2009 19:08:36 -0800 Subject: [PATCH 1543/1860] Bug 535476 - Ping the server once a day to help count active daily clients Fetch info/collections? with an extra "?" once a day on a normal sync so that it gets the same data but is easily differentiated in access logs. --- services/sync/modules/service.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bae26e33d046..1414cefed399 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1099,8 +1099,17 @@ WeaveSvc.prototype = { if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; + // Ping the server with a special info request once a day + let infoURL = this.infoURL; + let now = Math.floor(Date.now() / 1000); + let lastPing = Svc.Prefs.get("lastPing", 0); + if (now - lastPing > 86400) { // 60 * 60 * 24 + infoURL += "?"; + Svc.Prefs.set("lastPing", now); + } + // Figure out what the last modified time is for each collection - let info = new Resource(this.infoURL).get(); + let info = new Resource(infoURL).get(); if (!info.success) throw "aborting sync, failed to get collections"; From fb061d3455927343080fd9df405c4d7c63195487 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Dec 2009 19:15:18 -0800 Subject: [PATCH 1544/1860] Bug 535477 - Always sync tabs but inform Fennec users that recent tabs need syncing Remove special casing of tabs prefs and remove tabs.backup to simplify the UI logic. Also remove dynamically enabling/disabling based on number of clients. For Fennec, show a notification about recent tabs need syncing. --- services/sync/locales/en-US/sync.properties | 1 + services/sync/modules/service.js | 8 -------- services/sync/services-sync.js | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 3d5e9d5e56b6..f5bd51f37074 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -21,6 +21,7 @@ mobile.label = Mobile Bookmarks remote.pending.label = Remote tabs are being synced… remote.missing.label = Sync your other computers again to access their tabs remote.opened.label = All remote tabs are already open +remote.notification.label = Recent desktop tabs will be available once they sync error.login.title = Error While Signing In error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 1414cefed399..67514d6c1f29 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1187,18 +1187,10 @@ WeaveSvc.prototype = { if (numClients == 1) { this.syncInterval = SINGLE_USER_SYNC; this.syncThreshold = SINGLE_USER_THRESHOLD; - - // Disable tabs sync for single client, but store the original value - Svc.Prefs.set("engine.tabs.backup", tabEngine.enabled); - tabEngine.enabled = false; } else { this.syncInterval = hasMobile ? MULTI_MOBILE_SYNC : MULTI_DESKTOP_SYNC; this.syncThreshold = hasMobile ? MULTI_MOBILE_THRESHOLD : MULTI_DESKTOP_THRESHOLD; - - // Restore the original tab enabled value - tabEngine.enabled = Svc.Prefs.get("engine.tabs.backup", true); - Svc.Prefs.reset("engine.tabs.backup"); } }, diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index ddef301e1cdb..d808d2da2d6c 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -14,7 +14,6 @@ pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.prefs", true); pref("extensions.weave.engine.tabs", true); -pref("extensions.weave.engine.tabs.backup", true); pref("extensions.weave.engine.tabs.filteredUrls", "^(about:blank|chrome://weave/.*)$"); pref("extensions.weave.log.appender.console", "Warn"); From 864886435f5abf838b52228e4052e4f5ade97efe Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 17 Dec 2009 10:34:36 -0500 Subject: [PATCH 1545/1860] bug 535562 - fix typo failure --- services/sync/locales/en-US/fx-prefs.dtd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 1db6bec1b086..0d84f453d433 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -54,7 +54,7 @@ - + From 2974e42b7534488bc40511c02fa77299ee30c38d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 17 Dec 2009 12:20:07 -0800 Subject: [PATCH 1546/1860] Bug 535479 - Be smarter in getting rid of the "remote tabs" notification Add a tri-state pref that is unset, 0, or a number (modified time in seconds) and don't show the notification if it's in the "0" state. Unset pref means no modified time has been saved, and a non-0 state is the modified time of the last synced tab. So when tabs arrive with a different modified time, switch to the 0 state. Additionally, still remember if the user ever dismissed the notification and never show it again. --- services/sync/modules/engines/tabs.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index d0dd189b6a01..770ab978a694 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -192,6 +192,19 @@ TabStore.prototype = { create: function TabStore_create(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); + // Don't change notifyState if it's already 0 (don't notify) + else if (notifyState == 0) + return; + // We must have gotten a new tab that isn't the same as last time + else if (notifyState != roundModify) + Svc.Prefs.set("notifyTabState", 0); } }; From 9d7f3f628167fd2b8b3c1146bca8927aa3659388 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 17 Dec 2009 18:40:29 -0800 Subject: [PATCH 1547/1860] Bug 534459 - Can't reset password Update the preference to point to the new password reset page on auth.smc, and for now, just open it per mconnor's suggestion. --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index d808d2da2d6c..70bc93034d28 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -2,7 +2,7 @@ pref("extensions.weave.serverURL", "@server_url@"); pref("extensions.weave.storageAPI", "1.0"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); -pref("extensions.weave.pwChangeURL", "https://services.mozilla.com/pw/forgot.php"); +pref("extensions.weave.pwChangeURL", "https://auth.services.mozilla.com/weave-password-reset"); pref("extensions.weave.termsURL", "https://mozillalabs.com/weave/tos/"); pref("extensions.weave.client.name", "Firefox"); From f4fd59f8ce3d27cb55e56869ce695161b2d83336 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 17 Dec 2009 18:51:55 -0800 Subject: [PATCH 1548/1860] Bug 535722 - Reduce the amount of logging for default log levels Include the URI on success/fail requests and only trace log the onStartRequest. Switch various debug messages to trace and remove importing Log4Moz in fx-weave-overlay and generic-change. Drop the rootLogger to Debug to not log trace messages from unpreffed loggers. --- services/sync/modules/engines.js | 10 ++++---- services/sync/modules/resource.js | 39 ++++++++----------------------- services/sync/modules/service.js | 2 +- services/sync/modules/util.js | 2 +- services/sync/services-sync.js | 2 +- 5 files changed, 18 insertions(+), 37 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index d48e975e44a8..8e79136f02f5 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -262,7 +262,7 @@ Engine.prototype = { return "Total (ms): " + sums.sort(order).join(", "); }; - this._log.info(stats); + this._log.debug(stats); } }, @@ -347,7 +347,7 @@ SyncEngine.prototype = { // Any setup that needs to happen at the beginning of each sync. // Makes sure crypto records and keys are all set-up _syncStartup: function SyncEngine__syncStartup() { - this._log.debug("Ensuring server crypto records are there"); + this._log.trace("Ensuring server crypto records are there"); // Try getting/unwrapping the crypto record let meta = CryptoMetas.get(this.cryptoMetaURL); @@ -391,7 +391,7 @@ SyncEngine.prototype = { // NOTE: we use a backdoor (of sorts) to the tracker so it // won't save to disk this list over and over if (!this.lastSync) { - this._log.info("First sync, uploading all items"); + this._log.debug("First sync, uploading all items"); this._tracker.clearChangedIDs(); [i for (i in this._store.getAllIDs())] .forEach(function(id) this._tracker.changedIDs[id] = true, this); @@ -406,7 +406,7 @@ SyncEngine.prototype = { // Generate outgoing records _processIncoming: function SyncEngine__processIncoming() { - this._log.debug("Downloading & applying server changes"); + this._log.trace("Downloading & applying server changes"); // Figure out how many total items to fetch this sync; do less on mobile let fetchNum = 1500; @@ -614,7 +614,7 @@ SyncEngine.prototype = { _uploadOutgoing: function SyncEngine__uploadOutgoing() { let outnum = [i for (i in this._tracker.changedIDs)].length; if (outnum) { - this._log.debug("Preparing " + outnum + " outgoing records"); + this._log.trace("Preparing " + outnum + " outgoing records"); // collection we'll upload let up = new Collection(this.engineURL); diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 8346583e9270..f84804e2dc72 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -235,23 +235,15 @@ Resource.prototype = { status = channel.responseStatus; success = channel.requestSucceeded; - if (success) { - this._log.debug(action + " success: " + status); - if (this._log.level <= Log4Moz.Level.Trace) - this._log.trace(action + " Body: " + this._data); - } - else { - let log = "debug"; - let mesg = action + " fail: " + status; - - // Only log the full response body (may be HTML) when Trace logging - if (this._log.level <= Log4Moz.Level.Trace) { - log = "trace"; - mesg += " " + this._data; - } - - this._log[log](mesg); - } + // Log the status of the request + let mesg = [action, success ? "success" : "fail", status, + channel.URI.spec].join(" "); + if (mesg.length > 200) + mesg = mesg.substr(0, 200) + "…"; + this._log.debug(mesg); + // Additionally give the full response body when Trace logging + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace(action + " body: " + this._data); // this is a server-side safety valve to allow slowing down clients without hurting performance if (headers["X-Weave-Backoff"]) @@ -314,19 +306,8 @@ function ChannelListener(onComplete, onProgress, logger) { } ChannelListener.prototype = { onStartRequest: function Channel_onStartRequest(channel) { - // XXX Bug 482179 Some reason xpconnect makes |channel| only nsIRequest channel.QueryInterface(Ci.nsIHttpChannel); - - let log = "trace"; - let mesg = channel.requestMethod + " request for " + channel.URI.spec; - // Only log a part of the uri for logs higher than trace - if (this._log.level > Log4Moz.Level.Trace) { - log = "debug"; - if (mesg.length > 200) - mesg = mesg.substr(0, 200) + "..."; - } - this._log[log](mesg); - + this._log.trace(channel.requestMethod + " " + channel.URI.spec); this._data = ''; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 67514d6c1f29..3dc6bb826b25 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1023,7 +1023,7 @@ WeaveSvc.prototype = { return; } - this._log.debug("Next sync in " + Math.ceil(interval / 1000) + " sec."); + this._log.trace("Next sync in " + Math.ceil(interval / 1000) + " sec."); Utils.delay(function() this.syncOnIdle(), interval, this, "_syncTimer"); // Save the next sync time in-case sync is disabled (logout/offline/etc.) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 6c94b6f1584a..a9dcceeb3b54 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -104,7 +104,7 @@ let Utils = { let thisArg = this; let notify = function(state) { let mesg = prefix + name + ":" + state; - thisArg._log.debug("Event: " + mesg); + thisArg._log.trace("Event: " + mesg); Observers.notify(mesg, subject); }; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 70bc93034d28..9ce1764384fd 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -19,7 +19,7 @@ pref("extensions.weave.engine.tabs.filteredUrls", "^(about:blank|chrome://weave/ pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); pref("extensions.weave.log.appender.debugLog", "Trace"); -pref("extensions.weave.log.rootLogger", "Trace"); +pref("extensions.weave.log.rootLogger", "Debug"); pref("extensions.weave.log.logger.service.main", "Debug"); pref("extensions.weave.log.logger.authenticator", "Debug"); pref("extensions.weave.log.logger.network.resources", "Debug"); From 742f0993053ac1cb43d4d72c79cdce4693770b8c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 18 Dec 2009 16:22:59 -0800 Subject: [PATCH 1549/1860] Bug 535670 - Poor UX on password change while connected Detect if there was a login failure when opening the prefs and re-use the first-sign-in flow and feedback instead of using the change pwd/pph dialog. --- services/sync/locales/en-US/errors.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties index ff4edabd4d62..0ccb8df1a29d 100644 --- a/services/sync/locales/en-US/errors.properties +++ b/services/sync/locales/en-US/errors.properties @@ -1,6 +1,7 @@ error.login.reason.network = Failed to connect to the server error.login.reason.passphrase = Wrong secret phrase error.login.reason.password = Incorrect username or password +error.login.reason.no_password= No saved password to use error.login.reason.server = Server incorrectly configured error.sync.failed_partial = One or more data types could not be synced From b8e3cb7c488244786ac673d1704fc0bfddc76e5b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Dec 2009 13:46:34 -0800 Subject: [PATCH 1550/1860] Bug 536457 - Include the version when pinging Append a v= to include the version and make it easier to add things in the future. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3dc6bb826b25..a6536b93b57c 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1104,7 +1104,7 @@ WeaveSvc.prototype = { let now = Math.floor(Date.now() / 1000); let lastPing = Svc.Prefs.get("lastPing", 0); if (now - lastPing > 86400) { // 60 * 60 * 24 - infoURL += "?"; + infoURL += "?v=" + WEAVE_VERSION; Svc.Prefs.set("lastPing", now); } From b82b86ca58ba17dd4eb5e695cd26962490f339d3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 6 Jan 2010 09:54:15 -0800 Subject: [PATCH 1551/1860] Bug 537954 - Some tabs don't get synced Listen for load before adding the unload listener (and the Tab listeners). --- services/sync/modules/engines/tabs.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 770ab978a694..8aa3f11141f0 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -254,8 +254,13 @@ TabTracker.prototype = { observe: function TabTracker_observe(aSubject, aTopic, aData) { // Add tab listeners now that a window has opened let window = aSubject.QueryInterface(Ci.nsIDOMWindow); - if (aTopic == "domwindowopened") - this._registerListenersForWindow(window); + if (aTopic == "domwindowopened") { + let self = this; + window.addEventListener("load", function() { + // Only register after the window is done loading to avoid unloads + self._registerListenersForWindow(window); + }, false); + } }, onTab: function onTab(event) { From 6dd63e81dfaef4f5e7b602e5fdf5e62760e7a557 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 6 Jan 2010 09:57:05 -0800 Subject: [PATCH 1552/1860] Bug 536596 - Don't force garbage collections on every record processed Remove the forceGC at the end of each onProgress from Collections and let normal actions trigger GC. --- services/sync/modules/base_records/collection.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 793052fff5d4..9d2b47dc7df5 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -158,10 +158,6 @@ Collection.prototype = { record.id = record.data.id; onRecord(record); } - - // Aggressively clean up the objects we created above so that the next set - // of records have enough memory to decrypt, reconcile, apply, etc. - Cu.forceGC(); }; } }; From ec25704430e718f2a6f221c5f8a4b5b7d4b9cdf7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 6 Jan 2010 09:59:05 -0800 Subject: [PATCH 1553/1860] Bug 536594 - Warn on record creation failure but continue the sync Wrap createRecord and encrypt incase an engine fails to create one or more of the records similar to how process incoming records are wrapped. --- services/sync/modules/engines.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 8e79136f02f5..cf909b167f37 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -642,12 +642,17 @@ SyncEngine.prototype = { this._store.cache.enabled = false; for (let id in this._tracker.changedIDs) { - let out = this._createRecord(id); - if (this._log.level <= Log4Moz.Level.Trace) - this._log.trace("Outgoing: " + out); + try { + let out = this._createRecord(id); + if (this._log.level <= Log4Moz.Level.Trace) + this._log.trace("Outgoing: " + out); - out.encrypt(ID.get("WeaveCryptoID")); - up.pushData(out); + out.encrypt(ID.get("WeaveCryptoID")); + up.pushData(out); + } + catch(ex) { + this._log.warn("Error creating record: " + Utils.exceptionStr(ex)); + } // Partial upload if ((++count % MAX_UPLOAD_RECORDS) == 0) From 07dea3b2f121a40f4ee603c0bbb6c42eb4f3f16d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 6 Jan 2010 10:00:05 -0800 Subject: [PATCH 1554/1860] Bug 536595 - Sync fewer items per data type on mobile Just sync a flat 50 instead of .1 of 1500 to avoid increasing slowdown when processing many items. --- services/sync/modules/engines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cf909b167f37..d9cfc3482821 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -411,7 +411,7 @@ SyncEngine.prototype = { // Figure out how many total items to fetch this sync; do less on mobile let fetchNum = 1500; if (Svc.Prefs.get("client.type") == "mobile") - fetchNum /= 10; + fetchNum = 50; // enable cache, and keep only the first few items. Otherwise (when // we have more outgoing items than can fit in the cache), we will From 14fc4d04a48b19b6781694dd7299db11486f3719 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 7 Jan 2010 16:13:44 -0800 Subject: [PATCH 1555/1860] Bug 524221 - Weave's current auto-login behavior is super-annoying if you have a master password. r=Mardak Only autoconnect if the master password has already been entered and autoretry later if not. --- services/sync/modules/service.js | 39 +++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a6536b93b57c..417700b1d438 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -635,21 +635,44 @@ WeaveSvc.prototype = { }, _autoConnect: let (attempts = 0) function _autoConnect() { - // Can't autoconnect if we're missing these values - if (!this.username || !this.password || !this.passphrase) - return; + let reason = ""; + if (this._mpLocked()) + reason = "master password still locked"; - // Nothing more to do on a successful login - if (this.login()) - return; + // Can't autoconnect if we're missing these values + if (!reason) { + if (!this.username || !this.password || !this.passphrase) + return; + + // Nothing more to do on a successful login + if (this.login()) + return; + } // Something failed, so try again some time later let interval = this._calculateBackoff(++attempts, 60 * 1000); - this._log.debug("Autoconnect failed: " + Status.login + "; retry in " + - Math.ceil(interval / 1000) + " sec."); + this._log.debug("Autoconnect failed: " + (reason || Status.login) + + "; retry in " + Math.ceil(interval / 1000) + " sec."); Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer"); }, + _mpLocked: function _mpLocked() { + let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]. + getService(Ci.nsIPKCS11ModuleDB); + let sdrSlot = modules.findSlotByName(""); + let status = sdrSlot.status; + let slots = Ci.nsIPKCS11Slot; + + if (status == slots.SLOT_READY || status == slots.SLOT_LOGGED_IN) + return false; + + if (status == slots.SLOT_NOT_LOGGED_IN) + return true; + + this._log.debug("something wacky happened, pretending MP is locked"); + return true; + }, + persistLogin: function persistLogin() { // Canceled master password prompt can prevent these from succeeding try { From b893b2d85562d7423afe37b79b3ddc915693708c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 19 Jan 2010 11:24:00 -0800 Subject: [PATCH 1556/1860] Bug 526937 - Bookmarks are re-uploaded when they get visited Don't mark item as changed on a favicon notification to avoid churn that can result in merge messing up bookmark ordering. --- services/sync/modules/engines/bookmarks.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 5c63793cce13..92f9e58725d8 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -1129,6 +1129,10 @@ BookmarksTracker.prototype = { if (isAnno && annos.indexOf(property) == -1) return; + // Ignore favicon changes to avoid unnecessary churn + if (property == "favicon") + return; + this._log.trace("onItemChanged: " + itemId + (", " + property + (isAnno? " (anno)" : "")) + (value? (" = \"" + value + "\"") : "")); From 4e33bc03519875e537f9e3fca0e1d70ea0e034cf Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 21 Jan 2010 09:59:27 -0800 Subject: [PATCH 1557/1860] Lower tab logging to trace when wrapping tabs. --- services/sync/modules/engines/tabs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 8aa3f11141f0..8c1d088ffca4 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -171,7 +171,7 @@ TabStore.prototype = { return b.lastUsed - a.lastUsed; }).slice(0, 25); record.tabs.forEach(function(tab) { - this._log.debug("Wrapping tab: " + JSON.stringify(tab)); + this._log.trace("Wrapping tab: " + JSON.stringify(tab)); }, this); record.id = id; From 3d47539caef6bb5521a8924f2519cdeb92812768 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 25 Jan 2010 09:33:49 -0800 Subject: [PATCH 1558/1860] Bug 531489 - weave lost bookmarks with same url Do dupe detection for bookmarks like how we do folders/separators but check against the url/title. Add parentName to bookmark records, so bump version. --- services/sync/modules/engines/bookmarks.js | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 92f9e58725d8..03a7c6abb35c 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -141,6 +141,10 @@ BookmarksEngine.prototype = { let key; let id = idForGUID(guid); switch (Svc.Bookmark.getItemType(id)) { + case Svc.Bookmark.TYPE_BOOKMARK: + key = "b" + Svc.Bookmark.getBookmarkURI(id).spec + ":" + + Svc.Bookmark.getItemTitle(id); + break; case Svc.Bookmark.TYPE_FOLDER: key = "f" + Svc.Bookmark.getItemTitle(id); break; @@ -166,6 +170,11 @@ BookmarksEngine.prototype = { // Figure out if we have something to key with let key; switch (item.type) { + case "bookmark": + case "query": + case "microsummary": + key = "b" + item.bmkUri + ":" + item.title; + break; case "folder": case "livemark": key = "f" + item.title; @@ -178,8 +187,11 @@ BookmarksEngine.prototype = { } // Give the guid if we have the matching pair + this._log.trace("Finding mapping: " + item.parentName + ", " + key); let parent = lazyMap[item.parentName]; - return parent && parent[key]; + let dupe = parent && parent[key]; + this._log.trace("Mapped dupe: " + dupe); + return dupe; }; }); }, @@ -191,21 +203,7 @@ BookmarksEngine.prototype = { }, _findDupe: function _findDupe(item) { - switch (item.type) { - case "bookmark": - case "query": - case "microsummary": - // Find a bookmark that has the same uri - let uri = Utils.makeURI(item.bmkUri); - let localId = PlacesUtils.getMostRecentBookmarkForURI(uri); - if (localId == -1) - return; - return GUIDForId(localId); - case "folder": - case "livemark": - case "separator": - return this._lazyMap(item); - } + return this._lazyMap(item); }, _handleDupe: function _handleDupe(item, dupeId) { @@ -762,6 +760,7 @@ BookmarksStore.prototype = { record.title = this._bms.getItemTitle(placeId); } + record.parentName = Svc.Bookmark.getItemTitle(parent); record.bmkUri = bmkUri; record.tags = this._getTags(record.bmkUri); record.keyword = this._bms.getKeywordForBookmark(placeId); From 276975a53b7a231711a0038437bdfa3bdf633908 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 25 Jan 2010 10:30:31 -0800 Subject: [PATCH 1559/1860] Bug 541766 - Logging in with email address kinda works but not Detect finding cluster failure due to invalid username and treat like failed login. --- services/sync/modules/service.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 417700b1d438..f9b22e4611b4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -432,10 +432,15 @@ WeaveSvc.prototype = { _findCluster: function _findCluster() { this._log.debug("Finding cluster for user " + this.username); + let fail; let res = new Resource(this.userAPI + this.username + "/node/weave"); try { let node = res.get(); switch (node.status) { + case 400: + Status.login = LOGIN_FAILED_LOGIN_REJECTED; + fail = "Find cluster denied: " + this._errorStr(node); + break; case 404: this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); return this.serverURL; @@ -451,8 +456,9 @@ WeaveSvc.prototype = { } catch (e) { this._log.debug("Network error on findCluster"); Status.login = LOGIN_FAILED_NETWORK_ERROR; - throw e; + fail = e; } + throw fail; }, // gets cluster from central LDAP server and sets this.clusterURL From 8063bb992b0892ebd46eada87065ef261c4f0100 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 26 Jan 2010 12:08:41 -0800 Subject: [PATCH 1560/1860] Reverse thresholds to make more sense for single vs multi. --- services/sync/modules/constants.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index fc0b7b7bf975..df7f61879999 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -58,9 +58,9 @@ MULTI_MOBILE_SYNC: 5 * 60 * 1000, // 5 minutes PARTIAL_DATA_SYNC: 60 * 1000, // 1 minute // score thresholds for early syncs -SINGLE_USER_THRESHOLD: 100, +SINGLE_USER_THRESHOLD: 1000, MULTI_DESKTOP_THRESHOLD: 500, -MULTI_MOBILE_THRESHOLD: 1000, +MULTI_MOBILE_THRESHOLD: 100, // File IO Flags MODE_RDONLY: 0x01, From 004ef445824da88576681cde05cfb92d7bc7c9f4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 3 Feb 2010 15:23:58 -0800 Subject: [PATCH 1561/1860] Bug 541722 - updating out of date machine can cause data to be reverted Don't have each engine reset their last sync but still have the service clear out its cached data (keys, records). --- services/sync/modules/service.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f9b22e4611b4..4c6a3dac2f92 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -876,12 +876,9 @@ WeaveSvc.prototype = { return false; } else if (meta.payload.syncID != Clients.syncID) { - this._log.warn("Meta.payload.syncID is " + meta.payload.syncID + - ", Clients.syncID is " + Clients.syncID); - this.resetClient(); - this._log.info("Reset client because of syncID mismatch."); + this.resetService(); Clients.syncID = meta.payload.syncID; - this._log.info("Reset the client after a server/client sync ID mismatch"); + this._log.debug("Clear cached values and take syncId: " + Clients.syncID); this._updateRemoteVersion(meta); // XXX Bug 531005 Wait long enough to allow potentially another concurrent From ad759fbb763f37412d79a82a22d55b5bfcdfb41e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 3 Feb 2010 15:34:42 -0800 Subject: [PATCH 1562/1860] Bug 544068 - Assume moved bookmarks should be where they're moved Don't keep the annotation around in-case the predecessor/parent finally syncs because the user positioned it otherwise. --- services/sync/modules/engines/bookmarks.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 03a7c6abb35c..135e99af9b63 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -1150,6 +1150,10 @@ BookmarksTracker.prototype = { let oldSucc = Svc.Bookmark.getIdForItemAt(oldParent, oldIndex); if (oldSucc != -1) this._addId(oldSucc); + + // Remove any position annotations now that the user moved the item + Svc.Annos.removeItemAnnotation(itemId, PARENT_ANNO); + Svc.Annos.removeItemAnnotation(itemId, PREDECESSOR_ANNO); }, onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {}, From 04f2b696e359c136c469c701d9b63502eb7015c2 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 11 Feb 2010 14:28:35 -0800 Subject: [PATCH 1563/1860] Bug 533207 - wyciwyg:// URLs should not be synced as-is Filter out tabs that start with wyciwyg:. --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 9ce1764384fd..e9c525ab1d49 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -14,7 +14,7 @@ pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.prefs", true); pref("extensions.weave.engine.tabs", true); -pref("extensions.weave.engine.tabs.filteredUrls", "^(about:blank|chrome://weave/.*)$"); +pref("extensions.weave.engine.tabs.filteredUrls", "^(about:blank|chrome://weave/.*|wyciwyg:.*)$"); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 4bf89122ddbc05d5e8e41eca3d2cfe5b451a9925 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 11 Feb 2010 15:05:22 -0800 Subject: [PATCH 1564/1860] Bug 545701 - explicitly exclude Weave password/passphrase from pw sync Don't include weave passwords when getting all ids and ignore their changes. --- services/sync/modules/engines/passwords.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 5e5d5769681c..ad41982301bc 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -41,6 +41,7 @@ const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; +Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); @@ -124,7 +125,11 @@ PasswordStore.prototype = { let logins = Svc.Login.getAllLogins({}); for (let i = 0; i < logins.length; i++) { + // Skip over Weave password/passphrase entries let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); + if (metaInfo.hostname == PWDMGR_HOST) + continue; + items[metaInfo.guid] = metaInfo; } @@ -236,7 +241,12 @@ PasswordTracker.prototype = { queryElementAt(1, Ci.nsILoginMetaInfo); case 'addLogin': case 'removeLogin': - aSubject.QueryInterface(Ci.nsILoginMetaInfo); + // Skip over Weave password/passphrase changes + aSubject.QueryInterface(Ci.nsILoginMetaInfo). + QueryInterface(Ci.nsILoginInfo); + if (aSubject.hostname == PWDMGR_HOST) + break; + this.score += 15; this._log.trace(aData + ": " + aSubject.guid); this.addChangedID(aSubject.guid); From cf948b9e9c105d625605983e3de5f6255b76f1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Thu, 11 Feb 2010 15:08:28 -0800 Subject: [PATCH 1565/1860] Bug 545487 - Cleanup TabTracker_observe [r=Mardak] Remove the "load" event listener and don't QI since we don't need to. --- services/sync/modules/engines/tabs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 8c1d088ffca4..24030ae72126 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -253,12 +253,12 @@ TabTracker.prototype = { observe: function TabTracker_observe(aSubject, aTopic, aData) { // Add tab listeners now that a window has opened - let window = aSubject.QueryInterface(Ci.nsIDOMWindow); if (aTopic == "domwindowopened") { let self = this; - window.addEventListener("load", function() { + aSubject.addEventListener("load", function onLoad(event) { + aSubject.removeEventListener("load", onLoad, false); // Only register after the window is done loading to avoid unloads - self._registerListenersForWindow(window); + self._registerListenersForWindow(aSubject); }, false); } }, From 395b71471072626a6225497d9cae82cef93d7fca Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 11 Feb 2010 15:25:31 -0800 Subject: [PATCH 1566/1860] Bug 545764 - Convert constructor -> _init pattern to just use constructor Inline various _init calls and do super's init with .call(this, args..). Add various get/set sugar to those missing e.g., meta.keyring. Also simplify crypto record creation by setting cleartext in the parent. --- services/sync/modules/auth.js | 8 +-- .../sync/modules/base_records/collection.js | 19 +++---- services/sync/modules/base_records/crypto.js | 47 ++++++--------- services/sync/modules/base_records/keys.js | 52 +++++++---------- services/sync/modules/base_records/wbo.js | 21 ++----- services/sync/modules/engines.js | 30 +++++----- services/sync/modules/engines/bookmarks.js | 28 ++++----- services/sync/modules/engines/clients.js | 24 +++----- services/sync/modules/engines/forms.js | 21 ++----- services/sync/modules/engines/history.js | 19 ++----- services/sync/modules/engines/passwords.js | 24 ++------ services/sync/modules/engines/prefs.js | 12 ++-- services/sync/modules/engines/tabs.js | 48 +++++++--------- services/sync/modules/log4moz.js | 14 ++--- services/sync/modules/resource.js | 14 ++--- services/sync/modules/stores.js | 10 +--- services/sync/modules/trackers.js | 20 +++---- .../sync/modules/type_records/bookmark.js | 57 ++++--------------- services/sync/modules/type_records/clients.js | 6 +- services/sync/modules/type_records/forms.js | 8 +-- services/sync/modules/type_records/history.js | 8 +-- .../sync/modules/type_records/passwords.js | 8 +-- services/sync/modules/type_records/prefs.js | 8 +-- services/sync/modules/type_records/tabs.js | 8 +-- 24 files changed, 157 insertions(+), 357 deletions(-) diff --git a/services/sync/modules/auth.js b/services/sync/modules/auth.js index 8d676f812335..0a5f47545d4a 100644 --- a/services/sync/modules/auth.js +++ b/services/sync/modules/auth.js @@ -67,16 +67,12 @@ BasicAuthenticator.prototype = { }; function AuthMgr() { - this._init(); + this._authenticators = {}; + this.defaultAuthenticator = new NoOpAuthenticator(); } AuthMgr.prototype = { defaultAuthenticator: null, - _init: function AuthMgr__init() { - this._authenticators = {}; - this.defaultAuthenticator = new NoOpAuthenticator(); - }, - registerAuthenticator: function AuthMgr_register(match, authenticator) { this._authenticators[match] = authenticator; }, diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 9d2b47dc7df5..e59e67e73a69 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -45,23 +45,20 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/resource.js"); function Collection(uri, recordObj) { - this._Coll_init(uri); + Resource.call(this, uri); this._recordObj = recordObj; + + this._full = false; + this._ids = null; + this._limit = 0; + this._older = 0; + this._newer = 0; + this._data = []; } Collection.prototype = { __proto__: Resource.prototype, _logName: "Collection", - _Coll_init: function Coll_init(uri) { - this._init(uri); - this._full = false; - this._ids = null; - this._limit = 0; - this._older = 0; - this._newer = 0; - this._data = []; - }, - _rebuildURL: function Coll__rebuildURL() { // XXX should consider what happens if it's not a URL... this.uri.QueryInterface(Ci.nsIURL); diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 01cd8ebc6606..689090aa4bd5 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -46,26 +46,15 @@ Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/keys.js"); function CryptoWrapper(uri) { - this._CryptoWrap_init(uri); + WBORecord.call(this, uri); + this.encryption = ""; + this.ciphertext = null; + this.cleartext = {}; } CryptoWrapper.prototype = { __proto__: WBORecord.prototype, _logName: "Record.CryptoWrapper", - _CryptoWrap_init: function CryptoWrap_init(uri) { - // FIXME: this will add a json filter, meaning our payloads will be json - // encoded, even though they are already a string - this._WBORec_init(uri); - this.data.payload = { - encryption: "", - ciphertext: null - }; - }, - - // FIXME: we make no attempt to ensure cleartext is in sync - // with the encrypted payload - cleartext: null, - encrypt: function CryptoWrapper_encrypt(passphrase) { // No need to encrypt deleted records if (this.deleted) @@ -113,20 +102,14 @@ CryptoWrapper.prototype = { Utils.deferGetSet(CryptoWrapper, "payload", ["encryption", "ciphertext"]); function CryptoMeta(uri) { - this._CryptoMeta_init(uri); + WBORecord.call(this, uri); + this.bulkIV = null; + this.keyring = {}; } CryptoMeta.prototype = { __proto__: WBORecord.prototype, _logName: "Record.CryptoMeta", - _CryptoMeta_init: function CryptoMeta_init(uri) { - this._WBORec_init(uri); - this.data.payload = { - bulkIV: null, - keyring: {} - }; - }, - generateIV: function CryptoMeta_generateIV() { this.bulkIV = Svc.Crypto.generateRandomIV(); }, @@ -141,9 +124,9 @@ CryptoMeta.prototype = { // each hash key is a relative uri, resolve those and match against ours let wrapped_key; - for (let relUri in this.payload.keyring) { + for (let relUri in this.keyring) { if (pubkeyUri == this.baseUri.resolve(relUri)) - wrapped_key = this.payload.keyring[relUri]; + wrapped_key = this.keyring[relUri]; } if (!wrapped_key) throw "keyring doesn't contain a key for " + pubkeyUri; @@ -164,21 +147,23 @@ CryptoMeta.prototype = { // each hash key is a relative uri, resolve those and // if we find the one we're about to add, remove it - for (let relUri in this.payload.keyring) { + for (let relUri in this.keyring) { if (pubkeyUri == this.uri.resolve(relUri)) - delete this.payload.keyring[relUri]; + delete this.keyring[relUri]; } - this.payload.keyring[new_pubkey.uri.spec] = + this.keyring[new_pubkey.uri.spec] = Svc.Crypto.wrapSymmetricKey(symkey, new_pubkey.keyData); } }; -Utils.deferGetSet(CryptoMeta, "data.payload", "bulkIV"); +Utils.deferGetSet(CryptoMeta, "payload", ["bulkIV", "keyring"]); Utils.lazy(this, 'CryptoMetas', CryptoRecordManager); -function CryptoRecordManager() { this._init(); } +function CryptoRecordManager() { + RecordManager.call(this); +} CryptoRecordManager.prototype = { __proto__: RecordManager.prototype, _recordType: CryptoMeta diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 5140101affc5..7bc7eafa2b23 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -49,21 +49,15 @@ Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/base_records/wbo.js"); function PubKey(uri) { - this._PubKey_init(uri); + WBORecord.call(this, uri); + this.type = "pubkey"; + this.keyData = null; + this.privateKeyUri = null; } PubKey.prototype = { __proto__: WBORecord.prototype, _logName: "Record.PubKey", - _PubKey_init: function PubKey_init(uri) { - this._WBORec_init(uri); - this.payload = { - type: "pubkey", - keyData: null, - privateKeyUri: null - }; - }, - get privateKeyUri() { if (!this.data) return null; @@ -78,26 +72,20 @@ PubKey.prototype = { } }; -Utils.deferGetSet(PubKey, "payload", ["keyData", "privateKeyUri"]); +Utils.deferGetSet(PubKey, "payload", ["keyData", "privateKeyUri", "type"]); function PrivKey(uri) { - this._PrivKey_init(uri); + WBORecord.call(this, uri); + this.type = "privkey"; + this.salt = null; + this.iv = null; + this.keyData = null; + this.publicKeyUri = null; } PrivKey.prototype = { __proto__: WBORecord.prototype, _logName: "Record.PrivKey", - _PrivKey_init: function PrivKey_init(uri) { - this._WBORec_init(uri); - this.payload = { - type: "privkey", - salt: null, - iv: null, - keyData: null, - publicKeyUri: null - }; - }, - get publicKeyUri() { if (!this.data) return null; @@ -112,22 +100,18 @@ PrivKey.prototype = { } }; -Utils.deferGetSet(PrivKey, "payload", ["salt", "iv", "keyData", "publicKeyUri"]); +Utils.deferGetSet(PrivKey, "payload", ["salt", "iv", "keyData", "publicKeyUri", "type"]); // XXX unused/unfinished function SymKey(keyData, wrapped) { - this._init(keyData, wrapped); + this._data = keyData; + this._wrapped = wrapped; } SymKey.prototype = { get wrapped() { return this._wrapped; }, - _init: function SymKey__init(keyData, wrapped) { - this._data = keyData; - this._wrapped = wrapped; - }, - unwrap: function SymKey_unwrap(privkey, passphrase, meta_record) { this._data = Svc.Crypto.unwrapSymmetricKey(this._data, privkey.keyData, passphrase, @@ -137,7 +121,9 @@ SymKey.prototype = { Utils.lazy(this, 'PubKeys', PubKeyManager); -function PubKeyManager() { this._init(); } +function PubKeyManager() { + RecordManager.call(this); +} PubKeyManager.prototype = { __proto__: RecordManager.prototype, _recordType: PubKey, @@ -182,7 +168,9 @@ PubKeyManager.prototype = { Utils.lazy(this, 'PrivKeys', PrivKeyManager); -function PrivKeyManager() { this._init(); } +function PrivKeyManager() { + PubKeyManager.call(this); +} PrivKeyManager.prototype = { __proto__: PubKeyManager.prototype, _recordType: PrivKey, diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index d653fd5b6da2..60f29954199e 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -46,20 +46,15 @@ Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/util.js"); function WBORecord(uri) { - this._WBORec_init(uri); + this.data = {}; + this.payload = {}; + if (uri) + this.uri = uri; } WBORecord.prototype = { deleted: false, _logName: "Record.WBO", - _WBORec_init: function WBORec_init(uri) { - this.data = { - payload: {} - }; - if (uri) - this.uri = uri; - }, - // NOTE: baseUri must have a trailing slash, or baseUri.resolve() will omit // the collection name get uri() { @@ -114,17 +109,13 @@ Utils.deferGetSet(WBORecord, "data", ["id", "parentid", "modified", "sortindex", Utils.lazy(this, 'Records', RecordManager); function RecordManager() { - this._init(); + this._log = Log4Moz.repository.getLogger(this._logName); + this._records = {}; } RecordManager.prototype = { _recordType: WBORecord, _logName: "RecordMgr", - _init: function RegordMgr__init() { - this._log = Log4Moz.repository.getLogger(this._logName); - this._records = {}; - }, - import: function RecordMgr_import(url) { this._log.trace("Importing record: " + (url.spec ? url.spec : url)); try { diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index d9cfc3482821..1e1bd7398fe4 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -132,7 +132,15 @@ EngineManagerSvc.prototype = { } }; -function Engine() { this._init(); } +function Engine() { + this._notify = Utils.notify("weave:engine:"); + this._log = Log4Moz.repository.getLogger("Engine." + this.logName); + let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); + this._log.level = Log4Moz.Level[level]; + + this._tracker; // initialize tracker to load previously changed IDs + this._log.debug("Engine initialized"); +} Engine.prototype = { name: "engine", _displayName: "Boring Engine", @@ -170,16 +178,6 @@ Engine.prototype = { return this._displayName; }, - _init: function Engine__init() { - this._notify = Utils.notify("weave:engine:"); - this._log = Log4Moz.repository.getLogger("Engine." + this.logName); - let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); - this._log.level = Log4Moz.Level[level]; - - this._tracker; // initialize tracker to load previously changed IDs - this._log.debug("Engine initialized"); - }, - sync: function Engine_sync() { if (!this._sync) throw "engine does not implement _sync method"; @@ -293,17 +291,15 @@ Engine.prototype = { } }; -function SyncEngine() { this._init(); } +function SyncEngine() { + Engine.call(this); + this.loadToFetch(); +} SyncEngine.prototype = { __proto__: Engine.prototype, _recordObj: CryptoWrapper, - _init: function _init() { - Engine.prototype._init.call(this); - this.loadToFetch(); - }, - get storageURL() Svc.Prefs.get("clusterURL") + Svc.Prefs.get("storageAPI") + "/" + ID.get("WeaveID").username + "/storage/", diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 135e99af9b63..11e037a88210 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -92,7 +92,8 @@ function GUIDForId(placeId) { } function BookmarksEngine() { - this._init(); + SyncEngine.call(this); + this._handleImport(); } BookmarksEngine.prototype = { __proto__: SyncEngine.prototype, @@ -104,11 +105,6 @@ BookmarksEngine.prototype = { _storeObj: BookmarksStore, _trackerObj: BookmarksTracker, - _init: function _init() { - SyncEngine.prototype._init.call(this); - this._handleImport(); - }, - _handleImport: function _handleImport() { Observers.add("bookmarks-restore-begin", function() { this._log.debug("Ignoring changes from importing bookmarks"); @@ -219,7 +215,7 @@ BookmarksEngine.prototype = { }; function BookmarksStore() { - this._init(); + Store.call(this); } BookmarksStore.prototype = { __proto__: Store.prototype, @@ -960,7 +956,13 @@ BookmarksStore.prototype = { }; function BookmarksTracker() { - this._init(); + Tracker.call(this); + + // Ignore changes to the special roots + for (let guid in kSpecialIds) + this.ignoreID(guid); + + Svc.Bookmark.addObserver(this, false); } BookmarksTracker.prototype = { __proto__: Tracker.prototype, @@ -987,16 +989,6 @@ BookmarksTracker.prototype = { Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS ]), - _init: function BMT__init() { - this.__proto__.__proto__._init.call(this); - - // Ignore changes to the special roots - for (let guid in kSpecialIds) - this.ignoreID(guid); - - this._bms.addObserver(this, false); - }, - /** * Add a bookmark (places) id to be uploaded and bump up the sync score * diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index c5ab1771205c..b4f9a96d270e 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -49,7 +49,11 @@ Cu.import("resource://weave/type_records/clientData.js"); Utils.lazy(this, 'Clients', ClientEngine); function ClientEngine() { - this._init(); + SyncEngine.call(this); + + // Reset the client on every startup so that we fetch recent clients + this._resetClient(); + Utils.prefs.addObserver("", this, false); } ClientEngine.prototype = { __proto__: SyncEngine.prototype, @@ -61,13 +65,6 @@ ClientEngine.prototype = { _trackerObj: ClientTracker, _recordObj: ClientRecord, - _init: function _init() { - // Reset the client on every startup so that we fetch recent clients - SyncEngine.prototype._init.call(this); - this._resetClient(); - Utils.prefs.addObserver("", this, false); - }, - // get and set info for clients // FIXME: callers must use the setInfo interface or changes won't get synced, @@ -176,7 +173,7 @@ ClientEngine.prototype = { }; function ClientStore() { - this.init(); + Store.call(this); } ClientStore.prototype = { ////////////////////////////////////////////////////////////////////////////// @@ -194,13 +191,6 @@ ClientStore.prototype = { */ getInfo: function ClientStore_getInfo(id) this.clients[id], - /** - * Initialize parent class then load client data from disk - */ - init: function ClientStore_init() { - this._init.call(this); - }, - /** * Set the client data for a guid. Use Engine.setInfo to update tracker. */ @@ -256,7 +246,7 @@ ClientStore.prototype = { }; function ClientTracker() { - this._init(); + Tracker.call(this); } ClientTracker.prototype = { __proto__: Tracker.prototype, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 8fa36d7a221f..79e7342f687f 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -48,7 +48,7 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/forms.js"); function FormEngine() { - this._init(); + SyncEngine.call(this); } FormEngine.prototype = { __proto__: SyncEngine.prototype, @@ -82,7 +82,7 @@ FormEngine.prototype = { function FormStore() { - this._init(); + Store.call(this); } FormStore.prototype = { __proto__: Store.prototype, @@ -204,7 +204,8 @@ FormStore.prototype = { }; function FormTracker() { - this._init(); + Tracker.call(this); + Svc.Observer.addObserver(this, "earlyformsubmit", false); } FormTracker.prototype = { __proto__: Tracker.prototype, @@ -214,20 +215,6 @@ FormTracker.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), - __observerService: null, - get _observerService() { - if (!this.__observerService) - this.__observerService = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - return this.__observerService; - }, - - _init: function FormTracker__init() { - this.__proto__.__proto__._init.call(this); - this._log.trace("FormTracker initializing!"); - this._observerService.addObserver(this, "earlyformsubmit", false); - }, - /* 10 points per form element */ notify: function FormTracker_notify(formElement, aWindow, actionURI) { if (this.ignoreAll) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index b15aa6e958b8..99cebe039c1f 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -69,7 +69,7 @@ function GUIDForUri(uri, create) { } function HistoryEngine() { - this._init(); + SyncEngine.call(this); } HistoryEngine.prototype = { __proto__: SyncEngine.prototype, @@ -89,7 +89,7 @@ HistoryEngine.prototype = { }; function HistoryStore() { - this._init(); + Store.call(this); } HistoryStore.prototype = { __proto__: Store.prototype, @@ -278,7 +278,8 @@ HistoryStore.prototype = { }; function HistoryTracker() { - this._init(); + Tracker.call(this); + Svc.History.addObserver(this, false); } HistoryTracker.prototype = { __proto__: Tracker.prototype, @@ -291,18 +292,6 @@ HistoryTracker.prototype = { Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS ]), - get _hsvc() { - let hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - this.__defineGetter__("_hsvc", function() hsvc); - return hsvc; - }, - - _init: function HT__init() { - Tracker.prototype._init.call(this); - this._hsvc.addObserver(this, false); - }, - onBeginUpdateBatch: function HT_onBeginUpdateBatch() {}, onEndUpdateBatch: function HT_onEndUpdateBatch() {}, onPageChanged: function HT_onPageChanged() {}, diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index ad41982301bc..ba23c5cc7032 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -50,7 +50,7 @@ Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/type_records/passwords.js"); function PasswordEngine() { - this._init(); + SyncEngine.call(this); } PasswordEngine.prototype = { __proto__: SyncEngine.prototype, @@ -75,23 +75,15 @@ PasswordEngine.prototype = { }; function PasswordStore() { - this._init(); + Store.call(this); + this._nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); } PasswordStore.prototype = { __proto__: Store.prototype, name: "passwords", _logName: "PasswordStore", - _nsLoginInfo: null, - _init: function PasswordStore_init() { - Store.prototype._init.call(this); - this._nsLoginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", - Ci.nsILoginInfo, - "init" - ); - }, - _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) { let info = new this._nsLoginInfo(record.hostname, record.formSubmitURL, @@ -217,7 +209,8 @@ PasswordStore.prototype = { }; function PasswordTracker() { - this._init(); + Tracker.call(this); + Observers.add("passwordmgr-storage-changed", this); } PasswordTracker.prototype = { __proto__: Tracker.prototype, @@ -225,11 +218,6 @@ PasswordTracker.prototype = { name: "passwords", file: "password", - _init: function PasswordTracker_init() { - Tracker.prototype._init.call(this); - Observers.add("passwordmgr-storage-changed", this); - }, - /* A single add, remove or change is 15 points, all items removed is 50 */ observe: function PasswordTracker_observe(aSubject, aTopic, aData) { if (this.ignoreAll) diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index f853bcd95c4c..9b8d3a76811b 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -50,7 +50,7 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/prefs.js"); function PrefsEngine() { - this._init(); + SyncEngine.call(this); } PrefsEngine.prototype = { __proto__: SyncEngine.prototype, @@ -79,7 +79,7 @@ PrefsEngine.prototype = { function PrefStore() { - this._init(); + Store.call(this); } PrefStore.prototype = { __proto__: Store.prototype, @@ -221,7 +221,8 @@ PrefStore.prototype = { }; function PrefTracker() { - this._init(); + Tracker.call(this); + this._prefs.addObserver("", this, false); } PrefTracker.prototype = { __proto__: Tracker.prototype, @@ -247,11 +248,6 @@ PrefTracker.prototype = { return syncPrefs; }, - _init: function PrefTracker__init() { - this.__proto__.__proto__._init.call(this); - this._prefs.addObserver("", this, false); - }, - /* 25 points per pref change */ observe: function(aSubject, aTopic, aData) { if (aTopic != "nsPref:changed") diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 24030ae72126..0032669de803 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -50,7 +50,10 @@ Cu.import("resource://weave/type_records/tabs.js"); Cu.import("resource://weave/engines/clientData.js"); function TabEngine() { - this._init(); + SyncEngine.call(this); + + // Reset the client on every startup so that we fetch recent tabs + this._resetClient(); } TabEngine.prototype = { __proto__: SyncEngine.prototype, @@ -62,12 +65,6 @@ TabEngine.prototype = { _trackerObj: TabTracker, _recordObj: TabSetRecord, - _init: function _init() { - // Reset the client on every startup so that we fetch recent tabs - SyncEngine.prototype._init.call(this); - this._resetClient(); - }, - // API for use by Weave UI code to give user choices of tabs to open: getAllClients: function TabEngine_getAllClients() { return this._store._remoteClients; @@ -105,7 +102,7 @@ TabEngine.prototype = { function TabStore() { - this._TabStore_init(); + Store.call(this); } TabStore.prototype = { __proto__: Store.prototype, @@ -113,10 +110,6 @@ TabStore.prototype = { _logName: "Tabs.Store", _remoteClients: {}, - _TabStore_init: function TabStore__init() { - this._init(); - }, - get _sessionStore() { let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. getService(Ci.nsISessionStore); @@ -210,7 +203,20 @@ TabStore.prototype = { function TabTracker() { - this._TabTracker_init(); + Tracker.call(this); + + this.resetChanged(); + + // Make sure "this" pointer is always set correctly for event listeners + this.onTab = Utils.bind2(this, this.onTab); + + // Register as an observer so we can catch windows opening and closing: + Svc.WinWatcher.registerNotification(this); + + // Also register listeners on already open windows + let wins = Svc.WinMediator.getEnumerator("navigator:browser"); + while (wins.hasMoreElements()) + this._registerListenersForWindow(wins.getNext()); } TabTracker.prototype = { __proto__: Tracker.prototype, @@ -220,22 +226,6 @@ TabTracker.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), - _TabTracker_init: function TabTracker__init() { - this._init(); - this.resetChanged(); - - // Make sure "this" pointer is always set correctly for event listeners - this.onTab = Utils.bind2(this, this.onTab); - - // Register as an observer so we can catch windows opening and closing: - Svc.WinWatcher.registerNotification(this); - - // Also register listeners on already open windows - let wins = Svc.WinMediator.getEnumerator("navigator:browser"); - while (wins.hasMoreElements()) - this._registerListenersForWindow(wins.getNext()); - }, - _registerListenersForWindow: function TabTracker__registerListen(window) { this._log.trace("Registering tab listeners in new window"); diff --git a/services/sync/modules/log4moz.js b/services/sync/modules/log4moz.js index 57cdd5c5c46e..9d2e4a483e4c 100644 --- a/services/sync/modules/log4moz.js +++ b/services/sync/modules/log4moz.js @@ -175,17 +175,13 @@ LogMessage.prototype = { */ function Logger(name, repository) { - this._init(name, repository); + if (!repository) + repository = Log4Moz.repository; + this._name = name; + this._appenders = []; + this._repository = repository; } Logger.prototype = { - _init: function Logger__init(name, repository) { - if (!repository) - repository = Log4Moz.repository; - this._name = name; - this._appenders = []; - this._repository = repository; - }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), parent: null, diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index f84804e2dc72..ccbc0863f905 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -54,7 +54,11 @@ Cu.import("resource://weave/auth.js"); // // Represents a remote network resource, identified by a URI. function Resource(uri) { - this._init(uri); + this._log = Log4Moz.repository.getLogger(this._logName); + this._log.level = + Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; + this.uri = uri; + this._headers = {'Content-type': 'text/plain'}; } Resource.prototype = { _logName: "Net.Resource", @@ -125,14 +129,6 @@ Resource.prototype = { this._data = value; }, - _init: function Res__init(uri) { - this._log = Log4Moz.repository.getLogger(this._logName); - this._log.level = - Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; - this.uri = uri; - this._headers = {'Content-type': 'text/plain'}; - }, - // ** {{{ Resource._createRequest }}} ** // // This method returns a new IO Channel for requests to be made diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index de7dc1875c6c..5cb78ba6d7f5 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -51,7 +51,9 @@ Cu.import("resource://weave/util.js"); */ function Store() { - this._init(); + this._log = Log4Moz.repository.getLogger("Store." + this._logName); + let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); + this._log.level = Log4Moz.Level[level]; } Store.prototype = { _logName: "BaseClass", @@ -65,12 +67,6 @@ Store.prototype = { return cache; }, - _init: function Store__init() { - this._log = Log4Moz.repository.getLogger("Store." + this._logName); - let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); - this._log.level = Log4Moz.Level[level]; - }, - applyIncoming: function Store_applyIncoming(record) { if (record.deleted) this.remove(record); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 3133cd5e45c4..a89c766f2c6c 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -58,23 +58,19 @@ Cu.import("resource://weave/ext/Observers.js"); * */ function Tracker() { - this._init(); + this._log = Log4Moz.repository.getLogger(this._logName); + let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); + this._log.level = Log4Moz.Level[level]; + + this._score = 0; + this._ignored = []; + this.ignoreAll = false; + this.loadChangedIDs(); } Tracker.prototype = { _logName: "Tracker", file: "none", - _init: function T__init() { - this._log = Log4Moz.repository.getLogger(this._logName); - let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); - this._log.level = Log4Moz.Level[level]; - - this._score = 0; - this._ignored = []; - this.ignoreAll = false; - this.loadChangedIDs(); - }, - /* * Score can be called as often as desired to decide which engines to sync * diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index a57833aa7aeb..4e173f970900 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -45,8 +45,9 @@ const Cu = Components.utils; Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/crypto.js"); -function PlacesItem(uri) { - this._PlacesItem_init(uri); +function PlacesItem(uri, type) { + CryptoWrapper.call(this, uri); + this.type = type || "item"; } PlacesItem.prototype = { decrypt: function PlacesItem_decrypt(passphrase) { @@ -82,103 +83,67 @@ PlacesItem.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Record.PlacesItem", - - _PlacesItem_init: function BmkItemRec_init(uri) { - this._CryptoWrap_init(uri); - this.cleartext = {}; - this.type = "item"; - }, }; Utils.deferGetSet(PlacesItem, "cleartext", ["parentName", "predecessorid", "type"]); -function Bookmark(uri) { - this._Bookmark_init(uri); +function Bookmark(uri, type) { + PlacesItem.call(this, uri, type || "bookmark"); } Bookmark.prototype = { __proto__: PlacesItem.prototype, _logName: "Record.Bookmark", - - _Bookmark_init: function BmkRec_init(uri) { - this._PlacesItem_init(uri); - this.type = "bookmark"; - }, }; Utils.deferGetSet(Bookmark, "cleartext", ["title", "bmkUri", "description", "loadInSidebar", "tags", "keyword"]); function BookmarkMicsum(uri) { - this._BookmarkMicsum_init(uri); + Bookmark.call(this, uri, "microsummary"); } BookmarkMicsum.prototype = { __proto__: Bookmark.prototype, _logName: "Record.BookmarkMicsum", - - _BookmarkMicsum_init: function BmkMicsumRec_init(uri) { - this._Bookmark_init(uri); - this.type = "microsummary"; - }, }; Utils.deferGetSet(BookmarkMicsum, "cleartext", ["generatorUri", "staticTitle"]); function BookmarkQuery(uri) { - this._BookmarkQuery_init(uri); + Bookmark.call(this, uri, "query"); } BookmarkQuery.prototype = { __proto__: Bookmark.prototype, _logName: "Record.BookmarkQuery", - - _BookmarkQuery_init: function BookmarkQuery_init(uri) { - this._Bookmark_init(uri); - this.type = "query"; - }, }; Utils.deferGetSet(BookmarkQuery, "cleartext", ["folderName"]); -function BookmarkFolder(uri) { - this._BookmarkFolder_init(uri); +function BookmarkFolder(uri, type) { + PlacesItem.call(this, uri, type || "folder"); } BookmarkFolder.prototype = { __proto__: PlacesItem.prototype, _logName: "Record.Folder", - - _BookmarkFolder_init: function FolderRec_init(uri) { - this._PlacesItem_init(uri); - this.type = "folder"; - }, }; Utils.deferGetSet(BookmarkFolder, "cleartext", "title"); function Livemark(uri) { - this._Livemark_init(uri); + BookmarkFolder.call(this, uri, "livemark"); } Livemark.prototype = { __proto__: BookmarkFolder.prototype, _logName: "Record.Livemark", - - _Livemark_init: function LvmkRec_init(uri) { - this._BookmarkFolder_init(uri); - this.type = "livemark"; - }, }; Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]); function BookmarkSeparator(uri) { - this._BookmarkSeparator_init(uri); + PlacesItem.call(this, uri, "separator"); } BookmarkSeparator.prototype = { __proto__: PlacesItem.prototype, _logName: "Record.Separator", - - _BookmarkSeparator_init: function SepRec_init(uri) { - this._PlacesItem_init(uri); - this.type = "separator"; - } }; Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos"); diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index 51dfeb59aa9b..a097722599c7 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -45,16 +45,12 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/wbo.js"); function ClientRecord(uri) { - this._ClientRec_init(uri); + WBORecord.call(this, uri); } ClientRecord.prototype = { __proto__: WBORecord.prototype, _logName: "Record.Client", - _ClientRec_init: function ClientRec_init(uri) { - this._WBORec_init(uri); - }, - deserialize: function ClientRecord_deserialize(json) { let data = JSON.parse(json, function(key, val) key == "payload" ? unescape(val) : val); diff --git a/services/sync/modules/type_records/forms.js b/services/sync/modules/type_records/forms.js index 06bea1d234b1..818ec06d8bb6 100644 --- a/services/sync/modules/type_records/forms.js +++ b/services/sync/modules/type_records/forms.js @@ -45,17 +45,11 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/crypto.js"); function FormRec(uri) { - this._FormRec_init(uri); + CryptoWrapper.call(this, uri); } FormRec.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Record.Form", - - _FormRec_init: function FormRec_init(uri) { - this._CryptoWrap_init(uri); - this.cleartext = { - }; - }, }; Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]); diff --git a/services/sync/modules/type_records/history.js b/services/sync/modules/type_records/history.js index bb9e78df800c..3b102af44a79 100644 --- a/services/sync/modules/type_records/history.js +++ b/services/sync/modules/type_records/history.js @@ -45,17 +45,11 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/crypto.js"); function HistoryRec(uri) { - this._HistoryRec_init(uri); + CryptoWrapper.call(this, uri); } HistoryRec.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Record.History", - - _HistoryRec_init: function HistItem_init(uri) { - this._CryptoWrap_init(uri); - this.cleartext = { - }; - }, }; Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]); diff --git a/services/sync/modules/type_records/passwords.js b/services/sync/modules/type_records/passwords.js index 5708b6591b81..c4229993fc94 100644 --- a/services/sync/modules/type_records/passwords.js +++ b/services/sync/modules/type_records/passwords.js @@ -45,17 +45,11 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/crypto.js"); function LoginRec(uri) { - this._LoginRec_init(uri); + CryptoWrapper.call(this, uri); } LoginRec.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Record.Login", - - _LoginRec_init: function LoginItem_init(uri) { - this._CryptoWrap_init(uri); - this.cleartext = { - }; - }, }; Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL", diff --git a/services/sync/modules/type_records/prefs.js b/services/sync/modules/type_records/prefs.js index d130fa6f520c..043a200c1d6a 100644 --- a/services/sync/modules/type_records/prefs.js +++ b/services/sync/modules/type_records/prefs.js @@ -45,17 +45,11 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/crypto.js"); function PrefRec(uri) { - this._PrefRec_init(uri); + CryptoWrapper.call(this, uri); } PrefRec.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Record.Pref", - - _PrefRec_init: function PrefRec_init(uri) { - this._CryptoWrap_init(uri); - this.cleartext = { - }; - }, }; Utils.deferGetSet(PrefRec, "cleartext", ["type", "value"]); diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js index 4d0b17668563..7a85cdd4e42f 100644 --- a/services/sync/modules/type_records/tabs.js +++ b/services/sync/modules/type_records/tabs.js @@ -45,17 +45,11 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/crypto.js"); function TabSetRecord(uri) { - this._TabSetRecord_init(uri); + CryptoWrapper.call(this, uri); } TabSetRecord.prototype = { __proto__: CryptoWrapper.prototype, _logName: "Record.Tabs", - - _TabSetRecord_init: function TabSetRecord__init(uri) { - this._CryptoWrap_init(uri); - this.cleartext = { - }; - }, }; Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]); From 8f397e996f6a057c69dcfe6e45864b33ee02f2d8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 11 Feb 2010 15:29:15 -0800 Subject: [PATCH 1567/1860] Bug 545767 - Clean up various names/strings needed for each set of engines/stores/trackers Pass the engine name when constructing a subclass engine and construct the store/tracker with the same name. --- services/sync/modules/engines.js | 36 ++++++++++------------ services/sync/modules/engines/bookmarks.js | 19 +++--------- services/sync/modules/engines/clients.js | 23 +++----------- services/sync/modules/engines/forms.js | 19 +++--------- services/sync/modules/engines/history.js | 19 +++--------- services/sync/modules/engines/passwords.js | 19 +++--------- services/sync/modules/engines/prefs.js | 19 +++--------- services/sync/modules/engines/tabs.js | 19 +++--------- services/sync/modules/stores.js | 12 +++----- services/sync/modules/trackers.js | 10 +++--- 10 files changed, 62 insertions(+), 133 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 1e1bd7398fe4..3fa33e299507 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -105,11 +105,12 @@ EngineManagerSvc.prototype = { return engineObject.map(this.register, this); try { - let name = engineObject.prototype.name; + let engine = new engineObject(); + let name = engine.name; if (name in this._engines) this._log.error("Engine '" + name + "' is already registered!"); else - this._engines[name] = new engineObject(); + this._engines[name] = engine; } catch(ex) { let mesg = ex.message ? ex.message : ex; @@ -132,9 +133,12 @@ EngineManagerSvc.prototype = { } }; -function Engine() { +function Engine(name) { + this.Name = name || "Unnamed"; + this.name = name.toLowerCase(); + this._notify = Utils.notify("weave:engine:"); - this._log = Log4Moz.repository.getLogger("Engine." + this.logName); + this._log = Log4Moz.repository.getLogger("Engine." + this.Name); let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); this._log.level = Log4Moz.Level[level]; @@ -142,13 +146,7 @@ function Engine() { this._log.debug("Engine initialized"); } Engine.prototype = { - name: "engine", - _displayName: "Boring Engine", - description: "An engine example - it doesn't actually sync anything", - logName: "Engine", - // _storeObj, and _trackerObj should to be overridden in subclasses - _storeObj: Store, _trackerObj: Tracker, @@ -159,15 +157,15 @@ Engine.prototype = { get score() this._tracker.score, get _store() { - if (!this.__store) - this.__store = new this._storeObj(); - return this.__store; + let store = new this._storeObj(this.Name); + this.__defineGetter__("_store", function() store); + return store; }, get _tracker() { - if (!this.__tracker) - this.__tracker = new this._trackerObj(); - return this.__tracker; + let tracker = new this._trackerObj(this.Name); + this.__defineGetter__("_tracker", function() tracker); + return tracker; }, get displayName() { @@ -175,7 +173,7 @@ Engine.prototype = { return Str.engines.get(this.name); } catch (e) {} - return this._displayName; + return this.Name; }, sync: function Engine_sync() { @@ -291,8 +289,8 @@ Engine.prototype = { } }; -function SyncEngine() { - Engine.call(this); +function SyncEngine(name) { + Engine.call(this, name || "SyncEngine"); this.loadToFetch(); } SyncEngine.prototype = { diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 11e037a88210..20d9f676f3a8 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -92,15 +92,11 @@ function GUIDForId(placeId) { } function BookmarksEngine() { - SyncEngine.call(this); + SyncEngine.call(this, "Bookmarks"); this._handleImport(); } BookmarksEngine.prototype = { __proto__: SyncEngine.prototype, - name: "bookmarks", - _displayName: "Bookmarks", - description: "Keep your favorite links always at hand", - logName: "Bookmarks", _recordObj: PlacesItem, _storeObj: BookmarksStore, _trackerObj: BookmarksTracker, @@ -214,13 +210,11 @@ BookmarksEngine.prototype = { } }; -function BookmarksStore() { - Store.call(this); +function BookmarksStore(name) { + Store.call(this, name); } BookmarksStore.prototype = { __proto__: Store.prototype, - name: "bookmarks", - _logName: "BStore", __bms: null, get _bms() { @@ -955,8 +949,8 @@ BookmarksStore.prototype = { } }; -function BookmarksTracker() { - Tracker.call(this); +function BookmarksTracker(name) { + Tracker.call(this, name); // Ignore changes to the special roots for (let guid in kSpecialIds) @@ -966,9 +960,6 @@ function BookmarksTracker() { } BookmarksTracker.prototype = { __proto__: Tracker.prototype, - name: "bookmarks", - _logName: "BmkTracker", - file: "bookmarks", get _bms() { let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index b4f9a96d270e..377a2ead9d6e 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -49,7 +49,7 @@ Cu.import("resource://weave/type_records/clientData.js"); Utils.lazy(this, 'Clients', ClientEngine); function ClientEngine() { - SyncEngine.call(this); + SyncEngine.call(this, "Clients"); // Reset the client on every startup so that we fetch recent clients this._resetClient(); @@ -57,10 +57,6 @@ function ClientEngine() { } ClientEngine.prototype = { __proto__: SyncEngine.prototype, - name: "clients", - _displayName: "Clients", - description: "Sync information about other clients connected to Weave Sync", - logName: "Clients", _storeObj: ClientStore, _trackerObj: ClientTracker, _recordObj: ClientRecord, @@ -172,8 +168,8 @@ ClientEngine.prototype = { } }; -function ClientStore() { - Store.call(this); +function ClientStore(name) { + Store.call(this, name); } ClientStore.prototype = { ////////////////////////////////////////////////////////////////////////////// @@ -199,12 +195,6 @@ ClientStore.prototype = { this.clients[id] = info; }, - ////////////////////////////////////////////////////////////////////////////// - // Store.prototype Attributes - - name: "clients", - _logName: "Clients.Store", - ////////////////////////////////////////////////////////////////////////////// // Store.prototype Methods @@ -245,13 +235,10 @@ ClientStore.prototype = { }, }; -function ClientTracker() { - Tracker.call(this); +function ClientTracker(name) { + Tracker.call(this, name); } ClientTracker.prototype = { __proto__: Tracker.prototype, - name: "clients", - _logName: "ClientTracker", - file: "clients", get score() 100 // always sync }; diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 79e7342f687f..ea5913acf41a 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -48,14 +48,10 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/forms.js"); function FormEngine() { - SyncEngine.call(this); + SyncEngine.call(this, "Forms"); } FormEngine.prototype = { __proto__: SyncEngine.prototype, - name: "forms", - _displayName: "Forms", - description: "Take advantage of form-fill convenience on all your devices", - logName: "Forms", _storeObj: FormStore, _trackerObj: FormTracker, _recordObj: FormRec, @@ -81,13 +77,11 @@ FormEngine.prototype = { }; -function FormStore() { - Store.call(this); +function FormStore(name) { + Store.call(this, name); } FormStore.prototype = { __proto__: Store.prototype, - name: "forms", - _logName: "FormStore", _formItems: null, get _formDB() { @@ -203,15 +197,12 @@ FormStore.prototype = { } }; -function FormTracker() { - Tracker.call(this); +function FormTracker(name) { + Tracker.call(this, name); Svc.Observer.addObserver(this, "earlyformsubmit", false); } FormTracker.prototype = { __proto__: Tracker.prototype, - name: "forms", - _logName: "FormTracker", - file: "form", QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 99cebe039c1f..4481aab704d2 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -69,14 +69,10 @@ function GUIDForUri(uri, create) { } function HistoryEngine() { - SyncEngine.call(this); + SyncEngine.call(this, "History"); } HistoryEngine.prototype = { __proto__: SyncEngine.prototype, - name: "history", - _displayName: "History", - description: "All the sites you've been to. Take your awesomebar with you!", - logName: "History", _recordObj: HistoryRec, _storeObj: HistoryStore, _trackerObj: HistoryTracker, @@ -88,13 +84,11 @@ HistoryEngine.prototype = { } }; -function HistoryStore() { - Store.call(this); +function HistoryStore(name) { + Store.call(this, name); } HistoryStore.prototype = { __proto__: Store.prototype, - name: "history", - _logName: "HistStore", get _hsvc() { let hsvc = Cc["@mozilla.org/browser/nav-history-service;1"]. @@ -277,15 +271,12 @@ HistoryStore.prototype = { } }; -function HistoryTracker() { - Tracker.call(this); +function HistoryTracker(name) { + Tracker.call(this, name); Svc.History.addObserver(this, false); } HistoryTracker.prototype = { __proto__: Tracker.prototype, - name: "history", - _logName: "HistoryTracker", - file: "history", QueryInterface: XPCOMUtils.generateQI([ Ci.nsINavHistoryObserver, diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index ba23c5cc7032..d36674b59b08 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -50,14 +50,10 @@ Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/type_records/passwords.js"); function PasswordEngine() { - SyncEngine.call(this); + SyncEngine.call(this, "Passwords"); } PasswordEngine.prototype = { __proto__: SyncEngine.prototype, - name: "passwords", - _displayName: "Passwords", - description: "Forget all your passwords, Weave will remember them for you", - logName: "Passwords", _storeObj: PasswordStore, _trackerObj: PasswordTracker, _recordObj: LoginRec, @@ -74,15 +70,13 @@ PasswordEngine.prototype = { } }; -function PasswordStore() { - Store.call(this); +function PasswordStore(name) { + Store.call(this, name); this._nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); } PasswordStore.prototype = { __proto__: Store.prototype, - name: "passwords", - _logName: "PasswordStore", _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) { let info = new this._nsLoginInfo(record.hostname, @@ -208,15 +202,12 @@ PasswordStore.prototype = { } }; -function PasswordTracker() { - Tracker.call(this); +function PasswordTracker(name) { + Tracker.call(this, name); Observers.add("passwordmgr-storage-changed", this); } PasswordTracker.prototype = { __proto__: Tracker.prototype, - _logName: "PasswordTracker", - name: "passwords", - file: "password", /* A single add, remove or change is 15 points, all items removed is 50 */ observe: function PasswordTracker_observe(aSubject, aTopic, aData) { diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 9b8d3a76811b..ca4b7249e2c9 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -50,14 +50,10 @@ Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/prefs.js"); function PrefsEngine() { - SyncEngine.call(this); + SyncEngine.call(this, "Prefs"); } PrefsEngine.prototype = { __proto__: SyncEngine.prototype, - name: "prefs", - _displayName: "Preferences", - description: "Synchronize your home page, selected persona, and more", - logName: "Prefs", _storeObj: PrefStore, _trackerObj: PrefTracker, _recordObj: PrefRec, @@ -78,13 +74,11 @@ PrefsEngine.prototype = { }; -function PrefStore() { - Store.call(this); +function PrefStore(name) { + Store.call(this, name); } PrefStore.prototype = { __proto__: Store.prototype, - name: "prefs", - _logName: "PrefStore", get _prefs() { let prefs = Cc["@mozilla.org/preferences-service;1"]. @@ -220,15 +214,12 @@ PrefStore.prototype = { } }; -function PrefTracker() { - Tracker.call(this); +function PrefTracker(name) { + Tracker.call(this, name); this._prefs.addObserver("", this, false); } PrefTracker.prototype = { __proto__: Tracker.prototype, - name: "prefs", - _logName: "PrefTracker", - file: "prefs", get _prefs() { let prefs = Cc["@mozilla.org/preferences-service;1"]. diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0032669de803..4b4422ea15b3 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -50,17 +50,13 @@ Cu.import("resource://weave/type_records/tabs.js"); Cu.import("resource://weave/engines/clientData.js"); function TabEngine() { - SyncEngine.call(this); + SyncEngine.call(this, "Tabs"); // Reset the client on every startup so that we fetch recent tabs this._resetClient(); } TabEngine.prototype = { __proto__: SyncEngine.prototype, - name: "tabs", - _displayName: "Tabs", - description: "Access tabs from other devices via the History menu", - logName: "Tabs", _storeObj: TabStore, _trackerObj: TabTracker, _recordObj: TabSetRecord, @@ -101,13 +97,11 @@ TabEngine.prototype = { }; -function TabStore() { - Store.call(this); +function TabStore(name) { + Store.call(this, name); } TabStore.prototype = { __proto__: Store.prototype, - name: "tabs", - _logName: "Tabs.Store", _remoteClients: {}, get _sessionStore() { @@ -202,8 +196,8 @@ TabStore.prototype = { }; -function TabTracker() { - Tracker.call(this); +function TabTracker(name) { + Tracker.call(this, name); this.resetChanged(); @@ -220,9 +214,6 @@ function TabTracker() { } TabTracker.prototype = { __proto__: Tracker.prototype, - name: "tabs", - _logName: "TabTracker", - file: "tab_tracker", QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 5cb78ba6d7f5..148ef4728001 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -50,17 +50,15 @@ Cu.import("resource://weave/util.js"); * These can wrap, serialize items and apply commands */ -function Store() { - this._log = Log4Moz.repository.getLogger("Store." + this._logName); +function Store(name) { + name = name || "Unnamed"; + this.name = name.toLowerCase(); + + this._log = Log4Moz.repository.getLogger("Store." + name); let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); this._log.level = Log4Moz.Level[level]; } Store.prototype = { - _logName: "BaseClass", - - // set this property in child object's wrap()! - _lookup: null, - get cache() { let cache = new Cache(); this.__defineGetter__("cache", function() cache); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index a89c766f2c6c..301cc7b995bf 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -57,8 +57,11 @@ Cu.import("resource://weave/ext/Observers.js"); * want to sync. * */ -function Tracker() { - this._log = Log4Moz.repository.getLogger(this._logName); +function Tracker(name) { + name = name || "Unnamed"; + this.name = this.file = name.toLowerCase(); + + this._log = Log4Moz.repository.getLogger("Tracker." + name); let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); this._log.level = Log4Moz.Level[level]; @@ -68,9 +71,6 @@ function Tracker() { this.loadChangedIDs(); } Tracker.prototype = { - _logName: "Tracker", - file: "none", - /* * Score can be called as often as desired to decide which engines to sync * From bba12e5d56f525601407d49106a1278443eb9781 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 11 Feb 2010 17:14:57 -0800 Subject: [PATCH 1568/1860] Bug 545785 - Actively remove passwords that we don't want to sync Check if we haven't deleted yet, and if not, grab the guids for weave credentials and delete them from the server. --- services/sync/modules/engines/passwords.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index d36674b59b08..00c506adfca0 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -46,6 +46,7 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/base_records/collection.js"); Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/type_records/passwords.js"); @@ -58,6 +59,27 @@ PasswordEngine.prototype = { _trackerObj: PasswordTracker, _recordObj: LoginRec, + _syncFinish: function _syncFinish() { + SyncEngine.prototype._syncFinish.call(this); + + // Delete the weave credentials from the server once + if (!Svc.Prefs.get("deletePwd", false)) { + try { + let ids = Svc.Login.findLogins({}, PWDMGR_HOST, "", "").map(function(info) + info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid); + let coll = new Collection(this.engineURL); + coll.ids = ids; + let ret = coll.delete(); + this._log.debug("Delete result: " + ret); + + Svc.Prefs.set("deletePwd", true); + } + catch(ex) { + this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex)); + } + } + }, + _findDupe: function _findDupe(item) { let login = this._store._nsLoginInfoFromRecord(item); let logins = Svc.Login.findLogins({}, login.hostname, login.formSubmitURL, From ff9d3d674d1424073cc58a345e2c165f9429d748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Fri, 12 Feb 2010 12:04:06 -0800 Subject: [PATCH 1569/1860] Bug 545756 - Get rid of quit-application observer [r=Mardak] Stop observing 'quit-application', remove handling from observe, and remove WeaveSvc.isQuitting --- services/sync/modules/service.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4c6a3dac2f92..22fecf458e3f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -99,7 +99,6 @@ WeaveSvc.prototype = { _lock: Utils.lock, _catch: Utils.catch, - _isQuitting: false, _loggedIn: false, _syncInProgress: false, _keyGenEnabled: true, @@ -168,9 +167,6 @@ WeaveSvc.prototype = { get isLoggedIn() { return this._loggedIn; }, - get isQuitting() { return this._isQuitting; }, - set isQuitting(value) { this._isQuitting = value; }, - get keyGenEnabled() { return this._keyGenEnabled; }, set keyGenEnabled(value) { this._keyGenEnabled = value; }, @@ -284,7 +280,6 @@ WeaveSvc.prototype = { Svc.Observer.addObserver(this, "network:offline-status-changed", true); Svc.Observer.addObserver(this, "private-browsing", true); - Svc.Observer.addObserver(this, "quit-application", true); Svc.Observer.addObserver(this, "weave:service:sync:finish", true); Svc.Observer.addObserver(this, "weave:service:sync:error", true); Svc.Observer.addObserver(this, "weave:service:backoff:interval", true); @@ -384,9 +379,6 @@ WeaveSvc.prototype = { this._log.trace("Private browsing change: " + data); this._checkSyncStatus(); break; - case "quit-application": - this._onQuitApplication(); - break; case "weave:service:sync:error": this._handleSyncError(); break; From 50ceeedc741c489fa4997fa1d94b780aa952d5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Fri, 19 Feb 2010 11:32:20 -0800 Subject: [PATCH 1570/1860] Bug 546807 - Tabs from other computers: "Restore Session" [r=Mardak] Filter out all about:* urls vefore syncing, including the specific case here: about:sessionrestore. --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index e9c525ab1d49..306356fc2f0b 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -14,7 +14,7 @@ pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.prefs", true); pref("extensions.weave.engine.tabs", true); -pref("extensions.weave.engine.tabs.filteredUrls", "^(about:blank|chrome://weave/.*|wyciwyg:.*)$"); +pref("extensions.weave.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*)$"); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From d33abb2e76533940fc69ab613d960182956a203b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Fri, 19 Feb 2010 13:36:42 -0800 Subject: [PATCH 1571/1860] Bug 546397 - Exception: Clients.getClients()[Clients.clientID] is undefined [r=Mardak] Overrode _wipeClient in ClientEngine; treat resetting the same as wiping. --- services/sync/modules/engines/clients.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 377a2ead9d6e..43f795b627fa 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -159,7 +159,10 @@ ClientEngine.prototype = { return true; }, - _resetClient: function ClientEngine__resetClient() { + // Treat reset the same as wiping for locally cached clients + _resetClient: function _resetClient() this._wipeClient(), + + _wipeClient: function _wipeClient() { SyncEngine.prototype._resetClient.call(this); this._store.wipe(); From 8d3c1c62922fe9710dd03cf7f78aa370059dd2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Tue, 23 Feb 2010 13:10:32 -0800 Subject: [PATCH 1572/1860] Bug 547827 - JavaScript strict warning: modules/util.js, line 617: reference to undefined property thisObj[name] [r=Mardak] Check that name is a property of thisObj before trying to access it. --- services/sync/modules/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index a9dcceeb3b54..8f4603fff2c1 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -614,7 +614,7 @@ let Utils = { thisObj = thisObj || {}; // Delay an existing timer if it exists - if (thisObj[name] instanceof Ci.nsITimer) { + if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) { thisObj[name].delay = wait; return; } From 8ea5d64207fc316649e03988bbb61819d0ff19a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Fri, 26 Feb 2010 13:52:37 -0800 Subject: [PATCH 1573/1860] Bug 548910 - Don't sync file:// urls [r=mconnor] Added file.* to the filter. --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 306356fc2f0b..55f7b3ee698d 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -14,7 +14,7 @@ pref("extensions.weave.engine.history", true); pref("extensions.weave.engine.passwords", true); pref("extensions.weave.engine.prefs", true); pref("extensions.weave.engine.tabs", true); -pref("extensions.weave.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*)$"); +pref("extensions.weave.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$"); pref("extensions.weave.log.appender.console", "Warn"); pref("extensions.weave.log.appender.dump", "Error"); From 42e2ae2c41195cc583cd04cd4ac5203591294310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Tue, 2 Mar 2010 11:07:30 -0800 Subject: [PATCH 1574/1860] Refactor fake services so it's easier to add more [r=Mardak] Created a FakeSvc object to put fake services into. --- services/sync/modules/util.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 8f4603fff2c1..6b9ad4324311 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -325,24 +325,17 @@ let Utils = { // Try creating a fake service if we can handle that if (!Cc[cid]) { - switch (cid) { - case "@mozilla.org/privatebrowsing;1": - svc = { - autoStarted: false, - privateBrowsingEnabled: false - }; - break; - } + svc = FakeSvc[cid]; let log = Log4Moz.repository.getLogger("Service.Util"); - if (svc == null) - log.warn("Component " + cid + " doesn't exist on this platform."); - else + if (svc) log.debug("Using a fake svc object for " + cid); + else + log.warn("Component " + cid + " doesn't exist on this platform."); } else svc = Cc[cid].getService(iface); - + return dest[prop] = svc; }; dest.__defineGetter__(prop, getter); @@ -789,6 +782,14 @@ let Utils = { } }; +let FakeSvc = { + // Private Browsing + "@mozilla.org/privatebrowsing;1": { + autoStarted: false, + privateBrowsingEnabled: false + } +}; + /* * Commonly-used services */ From bec4f27c015e20cb102a4e988b4a7904465b884e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 2 Mar 2010 11:22:20 -0800 Subject: [PATCH 1575/1860] Revert svc/svc == null change from 87f9860d8e5a. --- services/sync/modules/util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 6b9ad4324311..50da4eba0ac4 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -328,10 +328,10 @@ let Utils = { svc = FakeSvc[cid]; let log = Log4Moz.repository.getLogger("Service.Util"); - if (svc) - log.debug("Using a fake svc object for " + cid); - else + if (svc == null) log.warn("Component " + cid + " doesn't exist on this platform."); + else + log.debug("Using a fake svc object for " + cid); } else svc = Cc[cid].getService(iface); From 6a46ed1a4fd2b2df2959c2ba5fe95020c83914dc Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Mar 2010 14:43:11 -0800 Subject: [PATCH 1576/1860] Bug 549632 - Remove storage cache, which is mostly un/incorrectly used [r=mconnor] Remove incorrectly used cache from some engines and clean up references from SyncEngine. --- services/sync/modules/engines.js | 17 ----- services/sync/modules/engines/bookmarks.js | 9 +-- services/sync/modules/engines/history.js | 2 +- services/sync/modules/stores.js | 79 ---------------------- 4 files changed, 2 insertions(+), 105 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 3fa33e299507..48abea913d17 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -407,13 +407,6 @@ SyncEngine.prototype = { if (Svc.Prefs.get("client.type") == "mobile") fetchNum = 50; - // enable cache, and keep only the first few items. Otherwise (when - // we have more outgoing items than can fit in the cache), we will - // keep rotating items in and out, perpetually getting cache misses - this._store.cache.enabled = true; - this._store.cache.fifo = false; // filo - this._store.cache.clear(); - let newitems = new Collection(this.engineURL, this._recordObj); newitems.newer = this.lastSync; newitems.full = true; @@ -502,9 +495,6 @@ SyncEngine.prototype = { this._log.info(["Records:", count.applied, "applied,", count.reconciled, "reconciled,", this.toFetch.length, "left to fetch"].join(" ")); - - // try to free some memory - this._store.cache.clear(); }, /** @@ -554,8 +544,6 @@ SyncEngine.prototype = { this._store.changeItemID(dupeId, item.id); this._deleteId(dupeId); } - - this._store.cache.clear(); // because parentid refs will be wrong }, // Reconciliation has three steps: @@ -632,9 +620,6 @@ SyncEngine.prototype = { up.clearRecords(); }); - // don't cache the outgoing items, we won't need them later - this._store.cache.enabled = false; - for (let id in this._tracker.changedIDs) { try { let out = this._createRecord(id); @@ -658,8 +643,6 @@ SyncEngine.prototype = { // Final upload if (count % MAX_UPLOAD_RECORDS > 0) doUpload(count >= MAX_UPLOAD_RECORDS ? "last batch" : "all"); - - this._store.cache.enabled = true; } this._tracker.clearChangedIDs(); }, diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 20d9f676f3a8..bfb138ddba74 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -638,9 +638,8 @@ BookmarksStore.prototype = { }, changeItemID: function BStore_changeItemID(oldID, newID) { - // Remember the GUID change for incoming records and avoid invalid refs + // Remember the GUID change for incoming records this.aliases[oldID] = newID; - this.cache.clear(); // Update any existing annotation references this._findAnnoItems(PARENT_ANNO, oldID).forEach(function(itemId) { @@ -706,16 +705,11 @@ BookmarksStore.prototype = { // Create a record starting from the weave id (places guid) createRecord: function BStore_createRecord(guid, cryptoMetaURL) { - let record = this.cache.get(guid); - if (record) - return record; - let placeId = idForGUID(guid); if (placeId <= 0) { // deleted item record = new PlacesItem(); record.id = guid; record.deleted = true; - this.cache.put(guid, record); return record; } @@ -799,7 +793,6 @@ BookmarksStore.prototype = { record.encryption = cryptoMetaURL; record.sortindex = this._calculateIndex(record); - this.cache.put(guid, record); return record; }, diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 4481aab704d2..c1ef22a2073a 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -262,7 +262,7 @@ HistoryStore.prototype = { } else record.deleted = true; - this.cache.put(guid, record); + return record; }, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index 148ef4728001..da15572f034f 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -59,12 +59,6 @@ function Store(name) { this._log.level = Log4Moz.Level[level]; } Store.prototype = { - get cache() { - let cache = new Cache(); - this.__defineGetter__("cache", function() cache); - return cache; - }, - applyIncoming: function Store_applyIncoming(record) { if (record.deleted) this.remove(record); @@ -108,76 +102,3 @@ Store.prototype = { throw "override wipe in a subclass"; } }; - -function Cache() { - this.count = 0; - this.maxItems = 100; - this.fifo = true; - this.enabled = true; - this._head = this._tail = null; - this._items = {}; -} -Cache.prototype = { - remove: function Cache_remove(item) { - if (this.count <= 0 || this.count == 1) { - this.clear(); - return; - } - - item.next.prev = item.prev; - item.prev.next = item.next; - - if (item == this._head) - this._head = item.next; - if (item == this._tail) - this._tail = item.prev; - - item.next = null; - item.prev = null; - - delete this._items[item.id]; - this.count--; - }, - pop: function Cache_pop() { - if (this.fifo) - this.remove(this._tail); - else - this.remove(this._head); - }, - put: function Cache_put(id, item) { - if (!this.enabled) - return; - - let wrapper = {id: id, item: item}; - - if (this._head === null) { - wrapper.next = wrapper; - wrapper.prev = wrapper; - this._head = wrapper; - this._tail = wrapper; - } else { - wrapper.next = this._tail; - wrapper.prev = this._head; - this._head.prev = wrapper; - this._tail.next = wrapper; - this._tail = wrapper; - } - - this._items[wrapper.id] = wrapper; - this.count++; - - if (this.count >= this.maxItems) - this.pop(); - }, - get: function Cache_get(id) { - if (id in this._items) - return this._items[id].item; - return undefined; - }, - clear: function Cache_clear() { - this.count = 0; - this._head = null; - this._tail = null; - this._items = {}; - } -}; From 133ab81acdfd3ad4e8bebdca4b87a0fe3766e86d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 5 Mar 2010 14:46:48 -0800 Subject: [PATCH 1577/1860] Bug 549633 - Standardize record creation so all SyncEngines encrypt data [r=mconnor] Always add the id and encryption value so client and delete records can be encrypted. --- services/sync/modules/engines.js | 7 +++++-- services/sync/modules/engines/bookmarks.js | 5 +---- services/sync/modules/engines/clients.js | 5 ++--- services/sync/modules/engines/forms.js | 4 +--- services/sync/modules/engines/history.js | 4 +--- services/sync/modules/engines/passwords.js | 4 +--- services/sync/modules/engines/prefs.js | 4 +--- services/sync/modules/engines/tabs.js | 4 +--- 8 files changed, 13 insertions(+), 24 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 48abea913d17..e65c6ca40aec 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -333,9 +333,12 @@ SyncEngine.prototype = { this._toFetch = o)); }, - // Create a new record by querying the store, and add the engine metadata + // Create a new record using the store and add in crypto fields _createRecord: function SyncEngine__createRecord(id) { - return this._store.createRecord(id, this.cryptoMetaURL); + let record = this._store.createRecord(id); + record.id = id; + record.encryption = this.cryptoMetaURL; + return record; }, // Any setup that needs to happen at the beginning of each sync. diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index bfb138ddba74..f4edb03cb60f 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -704,11 +704,10 @@ BookmarksStore.prototype = { }, // Create a record starting from the weave id (places guid) - createRecord: function BStore_createRecord(guid, cryptoMetaURL) { + createRecord: function createRecord(guid) { let placeId = idForGUID(guid); if (placeId <= 0) { // deleted item record = new PlacesItem(); - record.id = guid; record.deleted = true; return record; } @@ -787,10 +786,8 @@ BookmarksStore.prototype = { this._bms.getItemType(placeId)); } - record.id = guid; record.parentid = this._getParentGUIDForId(placeId); record.predecessorid = this._getPredecessorGUIDForId(placeId); - record.encryption = cryptoMetaURL; record.sortindex = this._calculateIndex(record); return record; diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 43f795b627fa..4cf266e7c5ec 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -211,10 +211,9 @@ ClientStore.prototype = { this.update(record); }, - createRecord: function ClientStore_createRecord(id) { + createRecord: function createRecord(guid) { let record = new ClientRecord(); - record.id = id; - record.payload = this.clients[id]; + record.payload = this.clients[guid]; return record; }, diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index ea5913acf41a..c8106db68581 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -155,13 +155,11 @@ FormStore.prototype = { return (id in this._formItems); }, - createRecord: function FormStore_createRecord(guid, cryptoMetaURL) { + createRecord: function createRecord(guid) { let record = new FormRec(); - record.id = guid; if (guid in this._formItems) { let item = this._formItems[guid]; - record.encryption = cryptoMetaURL; record.name = item.name; record.value = item.value; } else { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index c1ef22a2073a..04378d91ff51 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -249,16 +249,14 @@ HistoryStore.prototype = { return url ? this._hsvc.isVisited(url) : false; }, - createRecord: function HistStore_createRecord(guid, cryptoMetaURL) { + createRecord: function createRecord(guid) { let foo = this._findURLByGUID(guid); let record = new HistoryRec(); - record.id = guid; if (foo) { record.histUri = foo.url; record.title = foo.title; record.sortindex = foo.frecency; record.visits = this._getVisits(record.histUri); - record.encryption = cryptoMetaURL; } else record.deleted = true; diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 00c506adfca0..067575f0d31b 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -170,13 +170,11 @@ PasswordStore.prototype = { return false; }, - createRecord: function PasswordStore__createRecord(guid, cryptoMetaURL) { + createRecord: function createRecord(guid) { let record = new LoginRec(); let login = this._getLoginFromGUID(guid); - record.id = guid; if (login) { - record.encryption = cryptoMetaURL; record.hostname = login.hostname; record.formSubmitURL = login.formSubmitURL; record.httpRealm = login.httpRealm; diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index ca4b7249e2c9..1d95c143ab71 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -182,12 +182,10 @@ PrefStore.prototype = { return (id === WEAVE_PREFS_GUID); }, - createRecord: function FormStore_createRecord(guid, cryptoMetaURL) { + createRecord: function createRecord(guid) { let record = new PrefRec(); - record.id = guid; if (guid == WEAVE_PREFS_GUID) { - record.encryption = cryptoMetaURL; record.value = this._getAllPrefs(); } else { record.deleted = true; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 4b4422ea15b3..525a4933caff 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -149,7 +149,7 @@ TabStore.prototype = { return allTabs; }, - createRecord: function TabStore_createRecord(id, cryptoMetaURL) { + createRecord: function createRecord(guid) { let record = new TabSetRecord(); record.clientName = Clients.clientName; @@ -161,8 +161,6 @@ TabStore.prototype = { this._log.trace("Wrapping tab: " + JSON.stringify(tab)); }, this); - record.id = id; - record.encryption = cryptoMetaURL; return record; }, From fb4f13fa213dd42ac9cce89010e192def3db18fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Tue, 16 Mar 2010 12:00:40 -0700 Subject: [PATCH 1578/1860] Bug 552532 - Sync form data to mobile [r=Mardak] Add the "Form" engine to the list of engines used on Fennec. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 22fecf458e3f..f6b675b7f421 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -346,7 +346,7 @@ WeaveSvc.prototype = { let engines = []; switch (Svc.AppInfo.ID) { case FENNEC_ID: - engines = ["Bookmarks", "History", "Password", "Tab"]; + engines = ["Bookmarks", "Form", "History", "Password", "Tab"]; break; case FIREFOX_ID: From 4fdb1c603e71f89ec5d09ea600e92c1bc604006d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Tue, 16 Mar 2010 15:14:32 -0700 Subject: [PATCH 1579/1860] Bug 548939 - Use SessionStore in tab engine [r=Mardak] Created a fake SessionStore service for Fennec that imitates the parts of Firefox's SessionStore API that we need. Then used the now "consistent" SessionStore service in the Tabs engine. --- services/sync/modules/engines/tabs.js | 53 +++++++++++++-------------- services/sync/modules/util.js | 52 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 28 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 525a4933caff..44e824bc88ae 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -104,13 +104,6 @@ TabStore.prototype = { __proto__: Store.prototype, _remoteClients: {}, - get _sessionStore() { - let sessionStore = Cc["@mozilla.org/browser/sessionstore;1"]. - getService(Ci.nsISessionStore); - this.__defineGetter__("_sessionStore", function() { return sessionStore;}); - return this._sessionStore; - }, - itemExists: function TabStore_itemExists(id) { return id == Clients.clientID; }, @@ -119,33 +112,36 @@ TabStore.prototype = { getAllTabs: function getAllTabs(filter) { let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i"); - // Iterate through each tab of each window let allTabs = []; - let wins = Svc.WinMediator.getEnumerator("navigator:browser"); - while (wins.hasMoreElements()) { - // Get the tabs for both Firefox and Fennec - let window = wins.getNext(); - let tabs = window.gBrowser && window.gBrowser.tabContainer.childNodes; - tabs = tabs || window.Browser._tabs; - - // Extract various pieces of tab data - Array.forEach(tabs, function(tab) { - let browser = tab.linkedBrowser || tab.browser; - let url = browser.currentURI.spec; - // Filter out some urls if necessary - if (filter && filteredUrls.test(url)) + let currentState = JSON.parse(Svc.Session.getBrowserState()); + currentState.windows.forEach(function(window) { + window.tabs.forEach(function(tab) { + // Make sure there are history entries to look at. + if (!tab.entries.length) + return; + // Until we store full or partial history, just grab the current entry. + // index is 1 based, so make sure we adjust. + let entry = tab.entries[tab.index - 1]; + + // Filter out some urls if necessary. SessionStore can return empty + // tabs in some cases - easiest thing is to just ignore them for now. + if (!entry.url || filter && filteredUrls.test(entry.url)) return; + // weaveLastUsed will only be set if the tab was ever selected (or + // opened after Weave was running). So it might not ever be set. + // I think it's also possible that attributes[.image] might not be set + // so handle that as well. allTabs.push({ - title: browser.contentTitle || "", - urlHistory: [url], - icon: browser.mIconURL || "", - lastUsed: tab.lastUsed || 0 + title: entry.title || "", + urlHistory: [entry.url], + icon: tab.attributes && tab.attributes.image || "", + lastUsed: tab.extData && tab.extData.weaveLastUsed || 0 }); }); - } - + }); + return allTabs; }, @@ -248,7 +244,8 @@ TabTracker.prototype = { this._changedIDs[Clients.clientID] = true; // Store a timestamp in the tab to track when it was last used - event.originalTarget.lastUsed = Math.floor(Date.now() / 1000); + Svc.Session.setTabValue(event.originalTarget, "weaveLastUsed", + Math.floor(Date.now() / 1000)); }, get changedIDs() this._changedIDs, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 50da4eba0ac4..a6fe44019e33 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -787,6 +787,57 @@ let FakeSvc = { "@mozilla.org/privatebrowsing;1": { autoStarted: false, privateBrowsingEnabled: false + }, + // Session Restore + "@mozilla.org/browser/sessionstore;1": { + setTabValue: function(tab, key, value) { + if (!tab.__SS_extdata) + tab.__SS_extdata = {}; + tab.__SS_extData[key] = value; + }, + getBrowserState: function() { + // Fennec should have only one window. Not more, not less. + let state = { windows: [{ tabs: [] }] }; + let window = Svc.WinMediator.getMostRecentWindow("navigator:browser"); + + // Extract various pieces of tab data + window.Browser._tabs.forEach(function(tab) { + let tabState = { entries: [{}] }; + let browser = tab.browser; + + // Cases when we want to skip the tab. Could come up if we get + // state as a tab is opening or closing. + if (!browser || !browser.currentURI || !browser.sessionHistory) + return; + + let history = browser.sessionHistory; + if (history.count > 0) { + // We're only grabbing the current history entry for now. + let entry = history.getEntryAtIndex(history.index, false); + tabState.entries[0].url = entry.URI.spec; + // Like SessionStore really does it... + if (entry.title && entry.title != entry.url) + tabState.entries[0].title = entry.title; + } + // index is 1-based + tabState.index = 1; + + // Get the image for the tab. Fennec doesn't quite work the same + // way as Firefox, so we'll just get this from the browser object. + tabState.attributes = { image: browser.mIconURL }; + + // Collect the extdata + if (tab.__SS_extdata) { + tabState.extData = {}; + for (let key in tab.__SS_extdata) + tabState.extData[key] = tab.__SS_extdata[key]; + } + + // Add the tab to the window + state.windows[0].tabs.push(tabState); + }); + return JSON.stringify(state); + } } }; @@ -818,6 +869,7 @@ Svc.Obs = Observers; ["Version", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator"], ["WinMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"], ["WinWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"], + ["Session", "@mozilla.org/browser/sessionstore;1", "nsISessionStore"], ].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], Ci[lazy[2]])); let Str = {}; From ce857eb6de6c5b4ff7e7b055293b15f8ad8f6517 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 19 Mar 2010 11:35:01 -0700 Subject: [PATCH 1580/1860] Bug 551874 - Figure out if we can avoid getting into partial synced state [r=mconnor] For not-mobile clients, sync everything by specifying Infinity to keep existing math/logic working. Don't add a limit ?query if it's Infinity. --- services/sync/modules/base_records/collection.js | 2 +- services/sync/modules/engines.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index e59e67e73a69..35d8f7d2f775 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -75,7 +75,7 @@ Collection.prototype = { args.push('sort=' + this.sort); if (this.ids != null) args.push("ids=" + this.ids); - if (this.limit > 0) + if (this.limit > 0 && this.limit != Infinity) args.push("limit=" + this.limit); this.uri.query = (args.length > 0)? '?' + args.join('&') : ''; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e65c6ca40aec..b44ab22c6e32 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -406,7 +406,7 @@ SyncEngine.prototype = { this._log.trace("Downloading & applying server changes"); // Figure out how many total items to fetch this sync; do less on mobile - let fetchNum = 1500; + let fetchNum = Infinity; if (Svc.Prefs.get("client.type") == "mobile") fetchNum = 50; From d48405db443a498b409a2e09eb57a0287ee5c277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Fri, 19 Mar 2010 12:40:56 -0700 Subject: [PATCH 1581/1860] Bug 539591 - Not localized string [r=mconnor] Localized a string in fx-prefs.xul as confirm.client.moreinfo.label. --- services/sync/locales/en-US/fx-prefs.dtd | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 0d84f453d433..7681c7bdbf41 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -72,6 +72,7 @@ + From c24d53ca542575dc4af2c1a7f0f76ebb887564ee Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 23 Mar 2010 18:51:47 -0700 Subject: [PATCH 1582/1860] Bug 554472 - Only sync prefs under extensions.weave.prefs.sync. (with a trailing dot) [r=mconnor] Add a "." to the end of WEAVE_SYNC_PREFS value and fix up uses that did extra work to remove the dot (or something that might not have been a dot). --- services/sync/modules/engines/prefs.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 1d95c143ab71..a50c7f9ee05e 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -40,7 +40,7 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -const WEAVE_SYNC_PREFS = "extensions.weave.prefs.sync"; +const WEAVE_SYNC_PREFS = "extensions.weave.prefs.sync."; const WEAVE_PREFS_GUID = "preferences"; Cu.import("resource://weave/util.js"); @@ -91,8 +91,7 @@ PrefStore.prototype = { get _syncPrefs() { let service = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService); - let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}). - map(function(elem) { return elem.substr(1); }); + let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}); this.__defineGetter__("_syncPrefs", function() syncPrefs); return syncPrefs; @@ -104,7 +103,7 @@ PrefStore.prototype = { let pref; for (let i = 0; i < toSync.length; i++) { - if (!this._prefs.getBoolPref(WEAVE_SYNC_PREFS + "." + toSync[i])) + if (!this._prefs.getBoolPref(WEAVE_SYNC_PREFS + toSync[i])) continue; pref = {}; @@ -230,8 +229,7 @@ PrefTracker.prototype = { get _syncPrefs() { let service = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService); - let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}). - map(function(elem) { return elem.substr(1); }); + let syncPrefs = service.getBranch(WEAVE_SYNC_PREFS).getChildList("", {}); this.__defineGetter__("_syncPrefs", function() syncPrefs); return syncPrefs; From 4cbf072b5bd3e7d232d7cc9ac511e22e591a4c95 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:31:55 -0700 Subject: [PATCH 1583/1860] Bug 545517 - Make the remote version check compare storage versions and not weave versions [r=mconnor sr=mhanson] Move back to a model where multiple client versions can read the same data of the same storageVersion. The only time meta/global is written is on a freshStart/server wipe. Initialize the version to 1.2pre1.1 so that individual storage-incompatible changes can bump the value. Old versions are strings, so estimate with a parseFloat, but future versions will be integers. --- services/sync/modules/constants.js | 7 +++-- services/sync/modules/service.js | 45 ++++++++++-------------------- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index df7f61879999..3dafb43b395e 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -40,9 +40,10 @@ let EXPORTED_SYMBOLS = [((this[key] = val), key) for ([key, val] in Iterator({ WEAVE_VERSION: "@weave_version@", WEAVE_ID: "@weave_id@", -// Last client version this client can read. If the server contains an older -// version, this client will wipe the data on the server first. -COMPATIBLE_VERSION: "@compatible_version@", +// Version of the data format this client supports. The data format describes +// how records are packaged; this is separate from the Server API version and +// the per-engine cleartext formats. +STORAGE_VERSION: @storage_version@, DEFAULT_SERVER: "@server_url@", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f6b675b7f421..7d002de93bcb 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -821,11 +821,13 @@ WeaveSvc.prototype = { let remoteVersion = (meta && meta.payload.storageVersion)? meta.payload.storageVersion : ""; - this._log.debug(["Weave Version:", WEAVE_VERSION, "Compatible:", - COMPATIBLE_VERSION, "Remote:", remoteVersion].join(" ")); + this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:", + STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" ")); + // Check for cases that require a fresh start. When comparing remoteVersion, + // we need to convert it to a number as older clients used it as a string. if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || - Svc.Version.compare(COMPATIBLE_VERSION, remoteVersion) > 0) { + STORAGE_VERSION > parseFloat(remoteVersion)) { // abort the server wipe if the GET status was anything other than 404 or 200 let status = Records.response.status; @@ -841,8 +843,6 @@ WeaveSvc.prototype = { this._log.info("No metadata record, server wipe needed"); if (meta && !meta.payload.syncID) this._log.warn("No sync id, server wipe needed"); - if (Svc.Version.compare(COMPATIBLE_VERSION, remoteVersion) > 0) - this._log.info("Server data is older than what Weave supports, server wipe needed"); if (!this._keyGenEnabled) { this._log.info("...and key generation is disabled. Not wiping. " + @@ -858,28 +858,22 @@ WeaveSvc.prototype = { this._log.info("Metadata record not found, server wiped to ensure " + "consistency."); else // 200 - this._log.info("Server data wiped to ensure consistency after client " + - "upgrade (" + remoteVersion + " -> " + WEAVE_VERSION + ")"); + this._log.info("Wiped server; incompatible metadata: " + remoteVersion); - } else if (Svc.Version.compare(remoteVersion, WEAVE_VERSION) > 0) { + } + else if (remoteVersion > STORAGE_VERSION) { Status.sync = VERSION_OUT_OF_DATE; - this._log.warn("Server data is of a newer Weave version, this client " + - "needs to be upgraded. Aborting sync."); + this._log.warn("Upgrade required to access newer storage version."); return false; - } else if (meta.payload.syncID != Clients.syncID) { this.resetService(); Clients.syncID = meta.payload.syncID; this._log.debug("Clear cached values and take syncId: " + Clients.syncID); - this._updateRemoteVersion(meta); // XXX Bug 531005 Wait long enough to allow potentially another concurrent // sync to finish generating the keypair and uploading them Sync.sleep(15000); } - // We didn't wipe the server and we're not out of date, so update remote - else - this._updateRemoteVersion(meta); let needKeys = true; let pubkey = PubKeys.getDefaultKey(); @@ -1242,10 +1236,14 @@ WeaveSvc.prototype = { _freshStart: function WeaveSvc__freshStart() { this.resetClient(); - this._log.debug("Uploading new metadata record from freshStart"); let meta = new WBORecord(this.metaURL); meta.payload.syncID = Clients.syncID; - this._updateRemoteVersion(meta); + meta.payload.storageVersion = STORAGE_VERSION; + + this._log.debug("New metadata record: " + JSON.stringify(meta.payload)); + let resp = new Resource(meta.uri).put(meta); + if (!resp.success) + throw resp; // Wipe everything we know about except meta because we just uploaded it let collections = [Clients].concat(Engines.getAll()).map(function(engine) { @@ -1254,19 +1252,6 @@ WeaveSvc.prototype = { this.wipeServer(["crypto", "keys"].concat(collections)); }, - _updateRemoteVersion: function WeaveSvc__updateRemoteVersion(meta) { - // Don't update if the remote version is already newer - if (Svc.Version.compare(meta.payload.storageVersion, WEAVE_VERSION) >= 0) - return; - - this._log.debug("Setting meta payload storage version to " + WEAVE_VERSION); - meta.payload.storageVersion = WEAVE_VERSION; - let resp = new Resource(meta.uri).put(meta); - if (!resp.success) - throw resp; - }, - - /** * Check to see if this is a failure * From 52228b549f0670fbb7ed2c54585d536939bd6cc7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:31:55 -0700 Subject: [PATCH 1584/1860] Bug 546772 - Encrypt the clients records [r=mconnor] Store data in cleartext instead of directly in the payload to have it encrypted with the CryptoWrapper. This cleans up some hacks needed to get the plain WBO client record to behave nicely with other encrypted data. --- services/sync/modules/engines/clients.js | 4 +-- services/sync/modules/type_records/clients.js | 26 +++---------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 4cf266e7c5ec..9616cebc9485 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -213,7 +213,7 @@ ClientStore.prototype = { createRecord: function createRecord(guid) { let record = new ClientRecord(); - record.payload = this.clients[guid]; + record.cleartext = this.clients[guid]; return record; }, @@ -228,7 +228,7 @@ ClientStore.prototype = { update: function ClientStore_update(record) { this._log.debug("Updating client " + record.id); - this.clients[record.id] = record.payload; + this.clients[record.id] = record.cleartext; }, wipe: function ClientStore_wipe() { diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index a097722599c7..9b92b841b9f3 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -42,32 +42,12 @@ const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/crypto.js"); function ClientRecord(uri) { - WBORecord.call(this, uri); + CryptoWrapper.call(this, uri); } ClientRecord.prototype = { - __proto__: WBORecord.prototype, + __proto__: CryptoWrapper.prototype, _logName: "Record.Client", - - deserialize: function ClientRecord_deserialize(json) { - let data = JSON.parse(json, function(key, val) key == "payload" ? - unescape(val) : val); - WBORecord.prototype.deserialize.call(this, data); - }, - - toJSON: function toJSON() { - let obj = WBORecord.prototype.toJSON.call(this); - obj.payload = escape(obj.payload); - return obj; - }, - - // engines.js uses cleartext to determine if records _isEqual - // XXX Bug 482669 Implement .equals() for SyncEngine to compare records - get cleartext() JSON.stringify(this), - - // XXX engines.js calls encrypt/decrypt for all records, so define these: - encrypt: function ClientRecord_encrypt(passphrase) {}, - decrypt: function ClientRecord_decrypt(passphrase) {} }; From 2cd02b3c7524bbe93e03b3d3b07a96c56afc0103 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:31:56 -0700 Subject: [PATCH 1585/1860] Bug 544069 - Move bookmark parentid into the encrypted payload [r=mconnor] Tweak the get/setters to refer to the PlacesItem instead of WBORecord. --- services/sync/modules/base_records/crypto.js | 1 - services/sync/modules/base_records/wbo.js | 4 +--- services/sync/modules/engines.js | 3 +-- services/sync/modules/type_records/bookmark.js | 3 ++- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 689090aa4bd5..fe594798cf87 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -92,7 +92,6 @@ CryptoWrapper.prototype = { toString: function CryptoWrap_toString() "{ " + [ "id: " + this.id, - "parent: " + this.parentid, "index: " + this.sortindex, "modified: " + this.modified, "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.cleartext)) diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 60f29954199e..5c0483e974e4 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -96,15 +96,13 @@ WBORecord.prototype = { toString: function WBORec_toString() "{ " + [ "id: " + this.id, - "parent: " + this.parentid, "index: " + this.sortindex, "modified: " + this.modified, "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.payload)) ].join("\n ") + " }", }; -Utils.deferGetSet(WBORecord, "data", ["id", "parentid", "modified", "sortindex", - "payload"]); +Utils.deferGetSet(WBORecord, "data", ["id", "modified", "sortindex", "payload"]); Utils.lazy(this, 'Records', RecordManager); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b44ab22c6e32..d07a54302831 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -514,8 +514,7 @@ SyncEngine.prototype = { let local = this._createRecord(item.id); if (this._log.level <= Log4Moz.Level.Trace) this._log.trace("Local record: " + local); - if (item.parentid == local.parentid && - item.deleted == local.deleted && + if (item.deleted == local.deleted && Utils.deepEquals(item.cleartext, local.cleartext)) { this._log.trace("Local record is the same"); return true; diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 4e173f970900..e2d4ac91ca04 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -85,7 +85,8 @@ PlacesItem.prototype = { _logName: "Record.PlacesItem", }; -Utils.deferGetSet(PlacesItem, "cleartext", ["parentName", "predecessorid", "type"]); +Utils.deferGetSet(PlacesItem, "cleartext", ["parentid", "parentName", + "predecessorid", "type"]); function Bookmark(uri, type) { PlacesItem.call(this, uri, type || "bookmark"); From 52a7c15b7668bad98667047199fb9483ab169b86 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:31:56 -0700 Subject: [PATCH 1586/1860] Bug 549636 - Don't unnecessarily [wrap] cleartext for JSON.stringify [r=mconnor] Remove the unnecessary indirection now that JSON.stringify can take strings. --- services/sync/modules/base_records/crypto.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index fe594798cf87..4810d7959765 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -66,7 +66,7 @@ CryptoWrapper.prototype = { let meta = CryptoMetas.get(this.encryption); let symkey = meta.getKey(privkey, passphrase); - this.ciphertext = Svc.Crypto.encrypt(JSON.stringify([this.cleartext]), + this.ciphertext = Svc.Crypto.encrypt(JSON.stringify(this.cleartext), symkey, meta.bulkIV); this.cleartext = null; }, @@ -82,9 +82,8 @@ CryptoWrapper.prototype = { let meta = CryptoMetas.get(this.encryption); let symkey = meta.getKey(privkey, passphrase); - // note: payload is wrapped in an array, see _encrypt this.cleartext = JSON.parse(Svc.Crypto.decrypt(this.ciphertext, - symkey, meta.bulkIV))[0]; + symkey, meta.bulkIV)); this.ciphertext = null; return this.cleartext; From a7190733d3924787ef06f9f3779afb1aa5e922a3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:31:56 -0700 Subject: [PATCH 1587/1860] Bug 547049 - Verify that encrypted payloads correspond to the requested record [r=mconnor] Write the record's id to both .data and .cleartext and check that they match when decrypting. --- .../sync/modules/base_records/collection.js | 3 +-- services/sync/modules/base_records/crypto.js | 15 ++++++++++++- .../sync/tests/unit/test_records_crypto.js | 21 ++++++++++++++----- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 35d8f7d2f775..002d0fb25f41 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -151,8 +151,7 @@ Collection.prototype = { // Deserialize a record from json and give it to the callback let record = new coll._recordObj(); record.deserialize(json); - record.baseURI = coll.uri; - record.id = record.data.id; + record.baseUri = coll.uri; onRecord(record); } }; diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 4810d7959765..4b7b7aec974c 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -46,10 +46,10 @@ Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/keys.js"); function CryptoWrapper(uri) { + this.cleartext = {}; WBORecord.call(this, uri); this.encryption = ""; this.ciphertext = null; - this.cleartext = {}; } CryptoWrapper.prototype = { __proto__: WBORecord.prototype, @@ -86,6 +86,10 @@ CryptoWrapper.prototype = { symkey, meta.bulkIV)); this.ciphertext = null; + // Verify that the encrypted id matches the requested record's id + if (this.cleartext.id != this.id) + throw "Server attack?! Id mismatch: " + [this.cleartext.id, this.id]; + return this.cleartext; }, @@ -95,6 +99,15 @@ CryptoWrapper.prototype = { "modified: " + this.modified, "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.cleartext)) ].join("\n ") + " }", + + // The custom setter below masks the parent's getter, so explicitly call it :( + get id() WBORecord.prototype.__lookupGetter__("id").call(this), + + // Keep both plaintext and encrypted versions of the id to verify integrity + set id(val) { + WBORecord.prototype.__lookupSetter__("id").call(this, val); + return this.cleartext.id = val; + }, }; Utils.deferGetSet(CryptoWrapper, "payload", ["encryption", "ciphertext"]); diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index aab47922b1dd..612a71fd4064 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -78,24 +78,35 @@ function run_test() { cryptoWrap = new CryptoWrapper("http://localhost:8080/crypted-resource", auth); cryptoWrap.encryption = "http://localhost:8080/crypto-meta"; - cryptoWrap.cleartext = "my payload here"; + cryptoWrap.cleartext.stuff = "my payload here"; cryptoWrap.encrypt(passphrase); log.info("Decrypting the record"); let payload = cryptoWrap.decrypt(passphrase); - do_check_eq(payload, "my payload here"); + do_check_eq(payload.stuff, "my payload here"); do_check_neq(payload, cryptoWrap.payload); // wrap.data.payload is the encrypted one log.info("Re-encrypting the record with alternate payload"); - cryptoWrap.cleartext = "another payload"; + cryptoWrap.cleartext.stuff = "another payload"; cryptoWrap.encrypt(passphrase); payload = cryptoWrap.decrypt(passphrase); - do_check_eq(payload, "another payload"); + do_check_eq(payload.stuff, "another payload"); + + log.info("Make sure differing ids cause failures"); + cryptoWrap.encrypt(passphrase); + cryptoWrap.data.id = "other"; + let error = ""; + try { + cryptoWrap.decrypt(passphrase); + } + catch(ex) { + error = ex; + } + do_check_eq(error, "Server attack?! Id mismatch: crypted-resource,other"); log.info("Done!"); } - catch (e) { do_throw(e); } finally { server.stop(); } } From 71669edf9e68f602b3853aa7d32c3387d4ad5261 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:31:56 -0700 Subject: [PATCH 1588/1860] Bug 547048 - Only allow clients to issue delete records [r=mconnor] Don't specially serialize/not encrypt delete records and store the deleted flag as part of the cleartext payload. --- services/sync/modules/base_records/crypto.js | 9 +-------- services/sync/modules/base_records/wbo.js | 17 +++++++---------- services/sync/modules/engines.js | 3 +-- .../sync/tests/unit/test_collection_inc_get.js | 6 +++--- 4 files changed, 12 insertions(+), 23 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 4b7b7aec974c..a76a791e31f6 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -56,10 +56,6 @@ CryptoWrapper.prototype = { _logName: "Record.CryptoWrapper", encrypt: function CryptoWrapper_encrypt(passphrase) { - // No need to encrypt deleted records - if (this.deleted) - return; - let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); @@ -72,10 +68,6 @@ CryptoWrapper.prototype = { }, decrypt: function CryptoWrapper_decrypt(passphrase) { - // Deleted records aren't encrypted - if (this.deleted) - return; - let pubkey = PubKeys.getDefaultKey(); let privkey = PrivKeys.get(pubkey.privateKeyUri); @@ -111,6 +103,7 @@ CryptoWrapper.prototype = { }; Utils.deferGetSet(CryptoWrapper, "payload", ["encryption", "ciphertext"]); +Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted"); function CryptoMeta(uri) { WBORecord.call(this, uri); diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 5c0483e974e4..710c50ae0dee 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -52,7 +52,6 @@ function WBORecord(uri) { this.uri = uri; } WBORecord.prototype = { - deleted: false, _logName: "Record.WBO", // NOTE: baseUri must have a trailing slash, or baseUri.resolve() will omit @@ -77,20 +76,18 @@ WBORecord.prototype = { deserialize: function deserialize(json) { this.data = json.constructor.toString() == String ? JSON.parse(json) : json; - // Empty string payloads are deleted records - if (this.payload === "") - this.deleted = true; - else + try { + // The payload is likely to be JSON, but if not, keep it as a string this.payload = JSON.parse(this.payload); + } + catch(ex) {} }, toJSON: function toJSON() { - // Copy fields from data to except payload which needs to be a string + // Copy fields from data to be stringified, making sure payload is a string let obj = {}; for (let [key, val] in Iterator(this.data)) - if (key != "payload") - obj[key] = val; - obj.payload = this.deleted ? "" : JSON.stringify(this.payload); + obj[key] = key == "payload" ? JSON.stringify(val) : val; return obj; }, @@ -98,7 +95,7 @@ WBORecord.prototype = { "id: " + this.id, "index: " + this.sortindex, "modified: " + this.modified, - "payload: " + (this.deleted ? "DELETED" : JSON.stringify(this.payload)) + "payload: " + JSON.stringify(this.payload) ].join("\n ") + " }", }; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index d07a54302831..11de5110dee7 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -514,8 +514,7 @@ SyncEngine.prototype = { let local = this._createRecord(item.id); if (this._log.level <= Log4Moz.Level.Trace) this._log.trace("Local record: " + local); - if (item.deleted == local.deleted && - Utils.deepEquals(item.cleartext, local.cleartext)) { + if (Utils.deepEquals(item.cleartext, local.cleartext)) { this._log.trace("Local record is the same"); return true; } else { diff --git a/services/sync/tests/unit/test_collection_inc_get.js b/services/sync/tests/unit/test_collection_inc_get.js index 0c89f1a6c8be..8550a5e5fb1c 100644 --- a/services/sync/tests/unit/test_collection_inc_get.js +++ b/services/sync/tests/unit/test_collection_inc_get.js @@ -7,13 +7,13 @@ function run_test() { let stream = { _data: "" }; let called, recCount, sum; - _("Parse empty string payload as deleted"); + _("Not-JSON, string payloads are strings"); called = false; - stream._data = '{"payload":""}\n'; + stream._data = '{"payload":"hello"}\n'; coll.recordHandler = function(rec) { called = true; _("Got record:", JSON.stringify(rec)); - do_check_true(rec.deleted); + do_check_eq(rec.payload, "hello"); }; coll._onProgress.call(stream); do_check_eq(stream._data, ''); From 19a5e7d513e580879f2bf45d4f99c18f75e8e437 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:31:56 -0700 Subject: [PATCH 1589/1860] Bug 547007 - Use a per-record IV instead of one for each symkey [r=mconnor] Generate a random IV on every encrypt instead of taking it from the CryptoMeta. Don't bother generating a bulkIV per CryptoMeta. --- services/sync/modules/base_records/crypto.js | 16 ++++++---------- services/sync/modules/engines.js | 1 - services/sync/tests/unit/test_records_crypto.js | 6 +++++- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index a76a791e31f6..18b5efcc256d 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -62,8 +62,9 @@ CryptoWrapper.prototype = { let meta = CryptoMetas.get(this.encryption); let symkey = meta.getKey(privkey, passphrase); + this.IV = Svc.Crypto.generateRandomIV(); this.ciphertext = Svc.Crypto.encrypt(JSON.stringify(this.cleartext), - symkey, meta.bulkIV); + symkey, this.IV); this.cleartext = null; }, @@ -74,8 +75,8 @@ CryptoWrapper.prototype = { let meta = CryptoMetas.get(this.encryption); let symkey = meta.getKey(privkey, passphrase); - this.cleartext = JSON.parse(Svc.Crypto.decrypt(this.ciphertext, - symkey, meta.bulkIV)); + this.cleartext = JSON.parse(Svc.Crypto.decrypt(this.ciphertext, symkey, + this.IV)); this.ciphertext = null; // Verify that the encrypted id matches the requested record's id @@ -102,22 +103,17 @@ CryptoWrapper.prototype = { }, }; -Utils.deferGetSet(CryptoWrapper, "payload", ["encryption", "ciphertext"]); +Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "encryption", "IV"]); Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted"); function CryptoMeta(uri) { WBORecord.call(this, uri); - this.bulkIV = null; this.keyring = {}; } CryptoMeta.prototype = { __proto__: WBORecord.prototype, _logName: "Record.CryptoMeta", - generateIV: function CryptoMeta_generateIV() { - this.bulkIV = Svc.Crypto.generateRandomIV(); - }, - getKey: function CryptoMeta_getKey(privkey, passphrase) { // We cache the unwrapped key, as it's expensive to generate if (this._unwrappedKey) @@ -161,7 +157,7 @@ CryptoMeta.prototype = { } }; -Utils.deferGetSet(CryptoMeta, "payload", ["bulkIV", "keyring"]); +Utils.deferGetSet(CryptoMeta, "payload", "keyring"); Utils.lazy(this, 'CryptoMetas', CryptoRecordManager); diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 11de5110dee7..fc8b0470b1f6 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -370,7 +370,6 @@ SyncEngine.prototype = { let symkey = Svc.Crypto.generateRandomKey(); let pubkey = PubKeys.getDefaultKey(); meta = new CryptoMeta(this.cryptoMetaURL); - meta.generateIV(); meta.addUnwrappedKey(pubkey, symkey); let res = new Resource(meta.uri); let resp = res.put(meta); diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 612a71fd4064..f24c0914cebc 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -71,7 +71,6 @@ function run_test() { log.info("Setting up keyring"); cryptoMeta = new CryptoMeta("http://localhost:8080/crypto-meta", auth); - cryptoMeta.generateIV(); cryptoMeta.addUnwrappedKey(keys.pubkey, keys.symkey); log.info("Creating and encrypting a record"); @@ -80,6 +79,7 @@ function run_test() { cryptoWrap.encryption = "http://localhost:8080/crypto-meta"; cryptoWrap.cleartext.stuff = "my payload here"; cryptoWrap.encrypt(passphrase); + let firstIV = cryptoWrap.IV; log.info("Decrypting the record"); @@ -91,9 +91,13 @@ function run_test() { cryptoWrap.cleartext.stuff = "another payload"; cryptoWrap.encrypt(passphrase); + let secondIV = cryptoWrap.IV; payload = cryptoWrap.decrypt(passphrase); do_check_eq(payload.stuff, "another payload"); + log.info("Make sure multiple encrypts use different IVs"); + do_check_neq(firstIV, secondIV); + log.info("Make sure differing ids cause failures"); cryptoWrap.encrypt(passphrase); cryptoWrap.data.id = "other"; From a97ee9583d4b3977cc35dd9bdbc6fa0e36d81013 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 25 Mar 2010 16:52:45 +0000 Subject: [PATCH 1590/1860] Bug 554427 - Move syncID from clients.js to service.js [r=mconnor] Switch from Clients.syncID to this.syncID for service.js. Don't special case resetSyncID and just use = "". --- services/sync/modules/engines/clients.js | 12 ------------ services/sync/modules/service.js | 22 ++++++++++++++++------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 9616cebc9485..fe01ae2b6624 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -87,18 +87,6 @@ ClientEngine.prototype = { return Svc.Prefs.get("client.GUID"); }, - get syncID() { - if (!Svc.Prefs.get("client.syncID")) - Svc.Prefs.set("client.syncID", Utils.makeGUID()); - return Svc.Prefs.get("client.syncID"); - }, - set syncID(value) { - Svc.Prefs.set("client.syncID", value); - }, - resetSyncID: function ClientEngine_resetSyncID() { - Svc.Prefs.reset("client.syncID"); - }, - get clientName() { if (Svc.Prefs.isSet("client.name")) return Svc.Prefs.get("client.name"); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7d002de93bcb..5f62f8736642 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -165,6 +165,15 @@ WeaveSvc.prototype = { return user + "1/"; }, + get syncID() { + // Generate a random syncID id we don't have one + let syncID = Svc.Prefs.get("client.syncID", ""); + return syncID == "" ? this.syncID = Utils.makeGUID() : syncID; + }, + set syncID(value) { + Svc.Prefs.set("client.syncID", value); + }, + get isLoggedIn() { return this._loggedIn; }, get keyGenEnabled() { return this._keyGenEnabled; }, @@ -865,10 +874,11 @@ WeaveSvc.prototype = { Status.sync = VERSION_OUT_OF_DATE; this._log.warn("Upgrade required to access newer storage version."); return false; - } else if (meta.payload.syncID != Clients.syncID) { + } + else if (meta.payload.syncID != this.syncID) { this.resetService(); - Clients.syncID = meta.payload.syncID; - this._log.debug("Clear cached values and take syncId: " + Clients.syncID); + this.syncID = meta.payload.syncID; + this._log.debug("Clear cached values and take syncId: " + this.syncID); // XXX Bug 531005 Wait long enough to allow potentially another concurrent // sync to finish generating the keypair and uploading them @@ -1237,7 +1247,7 @@ WeaveSvc.prototype = { this.resetClient(); let meta = new WBORecord(this.metaURL); - meta.payload.syncID = Clients.syncID; + meta.payload.syncID = this.syncID; meta.payload.storageVersion = STORAGE_VERSION; this._log.debug("New metadata record: " + JSON.stringify(meta.payload)); @@ -1338,7 +1348,7 @@ WeaveSvc.prototype = { wipeRemote: function WeaveSvc_wipeRemote(engines) this._catch(this._notify("wipe-remote", "", function() { // Make sure stuff gets uploaded - Clients.resetSyncID(); + this.syncID = ""; // Clear out any server data this.wipeServer(engines); @@ -1364,7 +1374,7 @@ WeaveSvc.prototype = { this._log.info("Logs reinitialized for service reset"); // Pretend we've never synced to the server and drop cached data - Clients.resetSyncID(); + this.syncID = ""; Svc.Prefs.reset("lastSync"); for each (let cache in [PubKeys, PrivKeys, CryptoMetas, Records]) cache.clearCache(); From 162234af368d0754638e153cbd02234348222084 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:39:08 -0700 Subject: [PATCH 1591/1860] Bug 548066 - JavaScript strict warning: clientData.js, line 194: reference to undefined property this.clients[id] [r=mconnor] Get rid of get/setInfo on ClientEngine and ClientStore and expose functions to read/modify client data: stats, clearCommands, sendCommand. Also expose the local client information as local[ID,Name,Type,Commands] and rework the storage to use these instead of trying to keep the JS object clients entry in sync with prefs, etc. Update users of the old interface (service/tabs/chrome) to use the new local*. Set the client type based on app id instead of from each app's overlay. --- services/sync/modules/engines/clients.js | 214 +++++++++--------- services/sync/modules/engines/tabs.js | 12 +- services/sync/modules/service.js | 60 +---- services/sync/modules/type_records/clients.js | 10 +- services/sync/services-sync.js | 1 - .../sync/tests/unit/test_clients_escape.js | 38 +++- services/sync/tests/unit/test_load_modules.js | 4 +- 7 files changed, 154 insertions(+), 185 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index fe01ae2b6624..c806643a67fb 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -34,62 +34,94 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['Clients']; +const EXPORTED_SYMBOLS = ["Clients"]; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/type_records/clientData.js"); +Cu.import("resource://weave/type_records/clients.js"); -Utils.lazy(this, 'Clients', ClientEngine); +Utils.lazy(this, "Clients", ClientEngine); function ClientEngine() { SyncEngine.call(this, "Clients"); // Reset the client on every startup so that we fetch recent clients this._resetClient(); - Utils.prefs.addObserver("", this, false); } ClientEngine.prototype = { __proto__: SyncEngine.prototype, _storeObj: ClientStore, - _trackerObj: ClientTracker, - _recordObj: ClientRecord, + _recordObj: ClientsRec, - // get and set info for clients + // Aggregate some stats on the composition of clients on this account + get stats() { + let stats = { + hasMobile: this.localType == "mobile", + names: [this.localName], + numClients: 1, + }; - // FIXME: callers must use the setInfo interface or changes won't get synced, - // which is unintuitive + for each (let {name, type} in this._store._remoteClients) { + stats.hasMobile = stats.hasMobile || type == "mobile"; + stats.names.push(name); + stats.numClients++; + } - getClients: function ClientEngine_getClients() { - return this._store.clients; + return stats; }, - getInfo: function ClientEngine_getInfo(id) { - return this._store.getInfo(id); + // Remove any commands for the local client and mark it for upload + clearCommands: function clearCommands() { + delete this.localCommands; + this._tracker.addChangedID(this.localID); }, - setInfo: function ClientEngine_setInfo(id, info) { - this._store.setInfo(id, info); - this._tracker.addChangedID(id); + // Send a command+args pair to each remote client + sendCommand: function sendCommand(command, args) { + // Helper to determine if the client already has this command + let notDupe = function(other) other.command != command || + JSON.stringify(other.args) != JSON.stringify(args); + + // Package the command/args pair into an object + let action = { + command: command, + args: args, + }; + + // Send the command to each remote client + for (let [id, client] in Iterator(this._store._remoteClients)) { + // Set the action to be a new commands array if none exists + if (client.commands == null) + client.commands = [action]; + // Add the new action if there are no duplicates + else if (client.commands.every(notDupe)) + client.commands.push(action); + // Must have been a dupe.. skip! + else + continue; + + this._log.trace("Client " + id + " got a new action: " + [command, args]); + this._tracker.addChangedID(id); + } }, - // helpers for getting/setting this client's info directly - - get clientID() { - if (!Svc.Prefs.get("client.GUID")) - Svc.Prefs.set("client.GUID", Utils.makeGUID()); - return Svc.Prefs.get("client.GUID"); + 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 clientName() { - if (Svc.Prefs.isSet("client.name")) - return Svc.Prefs.get("client.name"); + get localName() { + let localName = Svc.Prefs.get("client.name", ""); + if (localName != "") + return localName; // Generate a client name if we don't have a useful one yet let user = Svc.Env.get("USER") || Svc.Env.get("USERNAME"); @@ -109,38 +141,26 @@ ClientEngine.prototype = { } } - return this.clientName = Str.sync.get("client.name", [user, app, host]); + return this.localName = Str.sync.get("client.name", [user, app, host]); }, - set clientName(value) { Svc.Prefs.set("client.name", value); }, + set localName(value) Svc.Prefs.set("client.name", value), - get clientType() { return Svc.Prefs.get("client.type", "desktop"); }, - set clientType(value) { Svc.Prefs.set("client.type", value); }, - - updateLocalInfo: function ClientEngine_updateLocalInfo(info) { - // Grab data from the store if we weren't given something to start with - if (!info) - info = this.getInfo(this.clientID); - - // Overwrite any existing values with the ones from the pref - info.name = this.clientName; - info.type = this.clientType; - - return info; - }, - - observe: function ClientEngine_observe(subject, topic, data) { - switch (topic) { - case "nsPref:changed": - switch (data) { - case "client.name": - case "client.type": - // Update the store and tracker on pref changes - this.setInfo(this.clientID, this.updateLocalInfo()); - break; + get localType() { + // Figure out if we have a type previously set + let localType = Svc.Prefs.get("client.type", ""); + if (localType == "") { + // Assume we're desktop-like unless the app is for mobiles + localType = "desktop"; + switch (Svc.AppInfo.ID) { + case FENNEC_ID: + localType = "mobile"; + break; } - break; + this.localType = localType; } + return localType; }, + set localType(value) Svc.Prefs.set("client.type", value), // Always process incoming items because they might have commands _reconcile: function _reconcile() { @@ -153,9 +173,6 @@ ClientEngine.prototype = { _wipeClient: function _wipeClient() { SyncEngine.prototype._resetClient.call(this); this._store.wipe(); - - // Make sure the local client exists after wiping - this.setInfo(this.clientID, this.updateLocalInfo({})); } }; @@ -163,72 +180,47 @@ function ClientStore(name) { Store.call(this, name); } ClientStore.prototype = { - ////////////////////////////////////////////////////////////////////////////// - // ClientStore Attributes - - clients: {}, - __proto__: Store.prototype, - ////////////////////////////////////////////////////////////////////////////// - // ClientStore Methods + create: function create(record) this.update(record), - /** - * Get the client by guid - */ - getInfo: function ClientStore_getInfo(id) this.clients[id], - - /** - * Set the client data for a guid. Use Engine.setInfo to update tracker. - */ - setInfo: function ClientStore_setInfo(id, info) { - this._log.debug("Setting client " + id + ": " + JSON.stringify(info)); - this.clients[id] = info; - }, - - ////////////////////////////////////////////////////////////////////////////// - // Store.prototype Methods - - changeItemID: function ClientStore_changeItemID(oldID, newID) { - this._log.debug("Changing id from " + oldId + " to " + newID); - this.clients[newID] = this.clients[oldID]; - delete this.clients[oldID]; - }, - - create: function ClientStore_create(record) { - this.update(record); + update: function update(record) { + // Unpack the individual components of the local client + if (record.id == Clients.localID) { + Clients.localName = record.name; + Clients.localType = record.type; + Clients.localCommands = record.commands; + } + else + this._remoteClients[record.id] = record.cleartext; }, createRecord: function createRecord(guid) { - let record = new ClientRecord(); - record.cleartext = this.clients[guid]; + let record = new ClientsRec(); + + // Package the individual components into a record for the local client + if (guid == Clients.localID) { + record.name = Clients.localName; + record.type = Clients.localType; + record.commands = Clients.localCommands; + } + else + record.cleartext = this._remoteClients[guid]; + return record; }, - getAllIDs: function ClientStore_getAllIDs() this.clients, + itemExists: function itemExists(id) id in this.getAllIDs(), - itemExists: function ClientStore_itemExists(id) id in this.clients, - - remove: function ClientStore_remove(record) { - this._log.debug("Removing client " + record.id); - delete this.clients[record.id]; + getAllIDs: function getAllIDs() { + let ids = {}; + ids[Clients.localID] = true; + for (let id in this._remoteClients) + ids[id] = true; + return ids; }, - update: function ClientStore_update(record) { - this._log.debug("Updating client " + record.id); - this.clients[record.id] = record.cleartext; - }, - - wipe: function ClientStore_wipe() { - this._log.debug("Wiping local clients store") - this.clients = {}; + wipe: function wipe() { + this._remoteClients = {}; }, }; - -function ClientTracker(name) { - Tracker.call(this, name); -} -ClientTracker.prototype = { - __proto__: Tracker.prototype, - get score() 100 // always sync -}; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 44e824bc88ae..0770a66b69a1 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -47,7 +47,7 @@ Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/tabs.js"); -Cu.import("resource://weave/engines/clientData.js"); +Cu.import("resource://weave/engines/clients.js"); function TabEngine() { SyncEngine.call(this, "Tabs"); @@ -102,13 +102,11 @@ function TabStore(name) { } TabStore.prototype = { __proto__: Store.prototype, - _remoteClients: {}, itemExists: function TabStore_itemExists(id) { - return id == Clients.clientID; + return id == Clients.localID; }, - getAllTabs: function getAllTabs(filter) { let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i"); @@ -147,7 +145,7 @@ TabStore.prototype = { createRecord: function createRecord(guid) { let record = new TabSetRecord(); - record.clientName = Clients.clientName; + record.clientName = Clients.localName; // Sort tabs in descending-used order to grab the most recently used record.tabs = this.getAllTabs(true).sort(function(a, b) { @@ -162,7 +160,7 @@ TabStore.prototype = { getAllIDs: function TabStore_getAllIds() { let ids = {}; - ids[Clients.clientID] = true; + ids[Clients.localID] = true; return ids; }, @@ -241,7 +239,7 @@ TabTracker.prototype = { onTab: function onTab(event) { this._log.trace(event.type); this.score += 1; - this._changedIDs[Clients.clientID] = true; + this._changedIDs[Clients.localID] = true; // Store a timestamp in the tab to track when it was last used Svc.Session.setTabValue(event.originalTarget, "weaveLastUsed", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5f62f8736642..fe5ce1dfc1a8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -62,7 +62,7 @@ Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/status.js"); -Cu.import("resource://weave/engines/clientData.js"); +Cu.import("resource://weave/engines/clients.js"); // for export let Weave = {}; @@ -78,7 +78,7 @@ Cu.import("resource://weave/stores.js", Weave); Cu.import("resource://weave/engines.js", Weave); Cu.import("resource://weave/engines/bookmarks.js", Weave); -Cu.import("resource://weave/engines/clientData.js", Weave); +Cu.import("resource://weave/engines/clients.js", Weave); Cu.import("resource://weave/engines/forms.js", Weave); Cu.import("resource://weave/engines/history.js", Weave); Cu.import("resource://weave/engines/prefs.js", Weave); @@ -1143,7 +1143,7 @@ WeaveSvc.prototype = { Clients.sync(); // Process the incoming commands if we have any - if (Clients.getClients()[Clients.clientID].commands) { + if (Clients.localCommands) { try { if (!(this.processCommands())) { Status.sync = ABORT_SYNC_COMMAND; @@ -1189,16 +1189,8 @@ WeaveSvc.prototype = { * Process the locally stored clients list to figure out what mode to be in */ _updateClientMode: function _updateClientMode() { - let numClients = 0; - let hasMobile = false; - - // Check how many and what type of clients we have - for each (let {type} in Clients.getClients()) { - numClients++; - hasMobile = hasMobile || type == "mobile"; - } - // Nothing to do if it's the same amount + let {numClients, hasMobile} = Clients.stats; if (this.numClients == numClients) return; @@ -1441,12 +1433,9 @@ WeaveSvc.prototype = { */ processCommands: function WeaveSvc_processCommands() this._notify("process-commands", "", function() { - let info = Clients.getInfo(Clients.clientID); - let commands = info.commands; - // Immediately clear out the commands as we've got them locally - delete info.commands; - Clients.setInfo(Clients.clientID, info); + let commands = Clients.localCommands; + Clients.clearCommands(); // Process each command in order for each ({command: command, args: args} in commands) { @@ -1502,40 +1491,9 @@ WeaveSvc.prototype = { return; } - // Package the command/args pair into an object - let action = { - command: command, - args: args, - }; - let actionStr = command + "(" + args + ")"; - - // Convert args into a string to simplify array comparisons - let jsonArgs = JSON.stringify(args); - let notDupe = function(action) action.command != command || - JSON.stringify(action.args) != jsonArgs; - - this._log.info("Sending clients: " + actionStr + "; " + commandData.desc); - - // Add the action to each remote client - for (let guid in Clients.getClients()) { - // Don't send commands to the local client - if (guid == Clients.clientID) - continue; - - let info = Clients.getInfo(guid); - // Set the action to be a new commands array if none exists - if (info.commands == null) - info.commands = [action]; - // Add the new action if there are no duplicates - else if (info.commands.every(notDupe)) - info.commands.push(action); - // Must have been a dupe.. skip! - else - continue; - - Clients.setInfo(guid, info); - this._log.trace("Client " + guid + " got a new action: " + actionStr); - } + // Send the command to all remote clients + this._log.debug("Sending clients: " + [command, args, commandData.desc]); + Clients.sendCommand(command, args); }, }; diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index 9b92b841b9f3..a9ff330609b4 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -const EXPORTED_SYMBOLS = ['ClientRecord']; +const EXPORTED_SYMBOLS = ["ClientsRec"]; const Cc = Components.classes; const Ci = Components.interfaces; @@ -44,10 +44,12 @@ const Cu = Components.utils; Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/crypto.js"); -function ClientRecord(uri) { +function ClientsRec(uri) { CryptoWrapper.call(this, uri); } -ClientRecord.prototype = { +ClientsRec.prototype = { __proto__: CryptoWrapper.prototype, - _logName: "Record.Client", + _logName: "Record.Clients", }; + +Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands"]); diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 55f7b3ee698d..8224483a8b5e 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -5,7 +5,6 @@ pref("extensions.weave.miscURL", "misc/"); pref("extensions.weave.pwChangeURL", "https://auth.services.mozilla.com/weave-password-reset"); pref("extensions.weave.termsURL", "https://mozillalabs.com/weave/tos/"); -pref("extensions.weave.client.name", "Firefox"); pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.autoconnect", true); diff --git a/services/sync/tests/unit/test_clients_escape.js b/services/sync/tests/unit/test_clients_escape.js index 936cf41806c8..bbc63835f5e0 100644 --- a/services/sync/tests/unit/test_clients_escape.js +++ b/services/sync/tests/unit/test_clients_escape.js @@ -1,16 +1,35 @@ -Cu.import("resource://weave/engines/clientData.js"); +Cu.import("resource://weave/engines/clients.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/base_records/keys.js"); +Cu.import("resource://weave/base_records/crypto.js"); function run_test() { + let passphrase = { password: "passphrase" }; + let baseUri = "http://fakebase/"; + let pubUri = baseUri + "pubkey"; + let privUri = baseUri + "privkey"; + let cryptoUri = baseUri + "crypto"; + + _("Setting up fake pub/priv keypair and symkey for encrypt/decrypt"); + PubKeys.defaultKeyUri = baseUri + "pubkey"; + let {pubkey, privkey} = PubKeys.createKeypair(passphrase, pubUri, privUri); + PubKeys.set(pubUri, pubkey); + PrivKeys.set(privUri, privkey); + let cryptoMeta = new CryptoMeta(cryptoUri); + cryptoMeta.addUnwrappedKey(pubkey, Svc.Crypto.generateRandomKey()); + CryptoMetas.set(cryptoUri, cryptoMeta); + _("Test that serializing client records results in uploadable ascii"); - Clients.setInfo("ascii", { - name: "wéävê" - }); + Clients.__defineGetter__("cryptoMetaURL", function() cryptoUri); + Clients.localID = "ascii"; + Clients.localName = "wéävê"; _("Make sure we have the expected record"); - let record = Clients._store.createRecord("ascii"); + let record = Clients._createRecord("ascii"); do_check_eq(record.id, "ascii"); - do_check_eq(record.payload.name, "wéävê"); + do_check_eq(record.name, "wéävê"); + record.encrypt(passphrase) let serialized = JSON.stringify(record); let checkCount = 0; _("Checking for all ASCII:", serialized); @@ -25,11 +44,12 @@ function run_test() { do_check_eq(checkCount, serialized.length); _("Making sure the record still looks like it did before"); + record.decrypt(passphrase) do_check_eq(record.id, "ascii"); - do_check_eq(record.payload.name, "wéävê"); + do_check_eq(record.name, "wéävê"); _("Sanity check that creating the record also gives the same"); - record = Clients._store.createRecord("ascii"); + record = Clients._createRecord("ascii"); do_check_eq(record.id, "ascii"); - do_check_eq(record.payload.name, "wéävê"); + do_check_eq(record.name, "wéävê"); } diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js index 5efff1fd96cb..c01c358661c6 100644 --- a/services/sync/tests/unit/test_load_modules.js +++ b/services/sync/tests/unit/test_load_modules.js @@ -6,7 +6,7 @@ const modules = [ "base_records/wbo.js", "constants.js", "engines/bookmarks.js", - "engines/clientData.js", + "engines/clients.js", "engines/forms.js", "engines/history.js", "engines/passwords.js", @@ -23,7 +23,7 @@ const modules = [ "stores.js", "trackers.js", "type_records/bookmark.js", - "type_records/clientData.js", + "type_records/clients.js", "type_records/forms.js", "type_records/history.js", "type_records/passwords.js", From adf6834d172a3cf832b1f41754c0c1af3cbde196 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 25 Mar 2010 10:05:21 -0700 Subject: [PATCH 1592/1860] Bug 503964 - Have per-engine versioning to avoid wiping all engine data [r=mconnor] Add an engines object to meta/global to track version and syncID for each engine. If the server is outdated, wipe the data and set a new version and syncID. If the client is oudated, ask for an upgrade. Differing syncIDs cause a reupload. All engines are right now the default version 1. --- services/sync/modules/engines.js | 61 +++++++++++++++++++++++++++++--- services/sync/modules/service.js | 6 ++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index fc8b0470b1f6..eefc5f6c26e1 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -295,8 +295,8 @@ function SyncEngine(name) { } SyncEngine.prototype = { __proto__: Engine.prototype, - _recordObj: CryptoWrapper, + version: 1, get storageURL() Svc.Prefs.get("clusterURL") + Svc.Prefs.get("storageAPI") + "/" + ID.get("WeaveID").username + "/storage/", @@ -305,6 +305,17 @@ SyncEngine.prototype = { get cryptoMetaURL() this.storageURL + "crypto/" + this.name, + get metaURL() this.storageURL + "meta/global", + + get syncID() { + // Generate a random syncID if we don't have one + let syncID = Svc.Prefs.get(this.name + ".syncID", ""); + return syncID == "" ? this.syncID = Utils.makeGUID() : syncID; + }, + set syncID(value) { + Svc.Prefs.set(this.name + ".syncID", value); + }, + get lastSync() { return parseFloat(Svc.Prefs.get(this.name + ".lastSync", "0")); }, @@ -346,6 +357,42 @@ SyncEngine.prototype = { _syncStartup: function SyncEngine__syncStartup() { this._log.trace("Ensuring server crypto records are there"); + // Determine if we need to wipe on outdated versions + let wipeServerData = false; + let metaGlobal = Records.get(this.metaURL); + let engines = metaGlobal.payload.engines || {}; + let engineData = engines[this.name] || {}; + + // Assume missing versions are 0 and wipe the server + if ((engineData.version || 0) < this.version) { + this._log.debug("Old engine data: " + [engineData.version, this.version]); + + // Prepare to clear the server and upload everything + wipeServerData = true; + this.syncID = ""; + + // Set the newer version and newly generated syncID + engineData.version = this.version; + engineData.syncID = this.syncID; + + // Put the new data back into meta/global and mark for upload + engines[this.name] = engineData; + metaGlobal.payload.engines = engines; + metaGlobal.changed = true; + } + // Don't sync this engine if the server has newer data + else if (engineData.version > this.version) { + let error = new String("New data: " + [engineData.version, this.version]); + error.failureCode = VERSION_OUT_OF_DATE; + throw error; + } + // Changes to syncID mean we'll need to upload everything + else if (engineData.syncID != this.syncID) { + this._log.debug("Engine syncIDs: " + [engineData.syncID, this.syncID]); + this.syncID = engineData.syncID; + this._resetClient(); + }; + // Try getting/unwrapping the crypto record let meta = CryptoMetas.get(this.cryptoMetaURL); if (meta) { @@ -355,16 +402,20 @@ SyncEngine.prototype = { meta.getKey(privkey, ID.get("WeaveCryptoID")); } catch(ex) { - // Remove traces of this bad cryptometa + // Remove traces of this bad cryptometa and tainted data this._log.debug("Purging bad data after failed unwrap crypto: " + ex); CryptoMetas.del(this.cryptoMetaURL); meta = null; - - // Remove any potentially tained data - new Resource(this.engineURL).delete(); + wipeServerData = true; } } + // Delete all server data and reupload on bad version or meta + if (wipeServerData) { + new Resource(this.engineURL).delete(); + this._resetClient(); + } + // Generate a new crypto record if (!meta) { let symkey = Svc.Crypto.generateRandomKey(); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index fe5ce1dfc1a8..3de998ca8db3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1172,6 +1172,11 @@ WeaveSvc.prototype = { } } + // Upload meta/global if any engines changed anything + let meta = Records.get(this.metaURL); + if (meta.changed) + new Resource(meta.uri).put(meta); + if (this._syncError) throw "Some engines did not sync correctly"; else { @@ -1246,6 +1251,7 @@ WeaveSvc.prototype = { let resp = new Resource(meta.uri).put(meta); if (!resp.success) throw resp; + Records.set(meta.uri, meta); // Wipe everything we know about except meta because we just uploaded it let collections = [Clients].concat(Engines.getAll()).map(function(engine) { From ce7b99c51aea8a5d19cca9af0cc64772345b636e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Thu, 25 Mar 2010 13:58:27 -0700 Subject: [PATCH 1593/1860] Bug 555015 - JavaScript strict warning: bookmarks.js: undeclared variable record [r=Mardak] Just make sure the record variable is declared before using it. --- services/sync/modules/engines/bookmarks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index f4edb03cb60f..c619bd60430a 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -706,6 +706,7 @@ BookmarksStore.prototype = { // Create a record starting from the weave id (places guid) createRecord: function createRecord(guid) { let placeId = idForGUID(guid); + let record; if (placeId <= 0) { // deleted item record = new PlacesItem(); record.deleted = true; From 583a4a4cb5be1ed9ba67d830db6a4c5fc8d1d165 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 25 Mar 2010 14:21:42 -0700 Subject: [PATCH 1594/1860] Bug 550267 - Sync tabs before other data for Fennec [r=mconnor] Move tabs to be first instead of last to sync for fennec. --- services/sync/modules/service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3de998ca8db3..f2132c7918ad 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -355,7 +355,7 @@ WeaveSvc.prototype = { let engines = []; switch (Svc.AppInfo.ID) { case FENNEC_ID: - engines = ["Bookmarks", "Form", "History", "Password", "Tab"]; + engines = ["Tab", "Bookmarks", "Form", "History", "Password"]; break; case FIREFOX_ID: From 628ea56ef369297f4d98f0274b7cc6ec41a52f45 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 12 Mar 2010 15:49:10 -0800 Subject: [PATCH 1595/1860] bug 508112 - captcha will not work with noscript enabled, r=Mardak --- services/sync/locales/en-US/fx-prefs.dtd | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 7681c7bdbf41..8e7171742372 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -40,8 +40,6 @@ - - From 5b0f8bcc0dd25c04a9b52230e804b0b1f0ad63e0 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 25 Mar 2010 17:24:41 -0400 Subject: [PATCH 1596/1860] bug 535136 - improve transition from single-client to multiple-client mode, r=Mardak --- services/sync/modules/service.js | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f2132c7918ad..8b3fe88272d5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -978,6 +978,8 @@ WeaveSvc.prototype = { // Clear out any scheduled syncs if (this._syncTimer) this._syncTimer.clear(); + if (this._heartbeatTimer) + this._heartbeatTimer.clear(); // Clear out a sync that's just waiting for idle if we happen to have one try { @@ -1050,6 +1052,63 @@ WeaveSvc.prototype = { // Save the next sync time in-case sync is disabled (logout/offline/etc.) this.nextSync = Date.now() + interval; + + // if we're a single client, set up a heartbeat to detect new clients sooner + if (this.numClients == 1) + this._scheduleHeartbeat(); + }, + + /** + * Hits info/collections on the server to see if there are new clients. + * This is only called when the account has one active client, and if more + * are found will trigger a sync to change client sync frequency and update data. + */ + + _doHeartbeat: function WeaveSvc__doHeartbeat() { + if (this._heartbeatTimer) + this._heartbeatTimer.clear(); + + let info = null; + try { + info = new Resource(this.infoURL).get(); + if (info && info.success) { + // if clients.lastModified doesn't match what the server has, + // we have another client in play + this._log.trace("Remote timestamp:" + info.obj["clients"] + + " Local timestamp: " + Clients.lastSync); + if (info.obj["clients"] > Clients.lastSync) { + this._log.debug("New clients detected, triggering a full sync"); + this.syncOnIdle(); + return; + } + } + else { + this._checkServerError(info); + this._log.debug("Heartbeat failed. HTTP Error: " + info.status); + } + } catch(ex) { + // if something throws unexpectedly, not a big deal + this._log.debug("Heartbeat failed unexpectedly: " + ex); + } + + // no joy, schedule the next heartbeat + this._scheduleHeartbeat(); + }, + + /** + * Sets up a heartbeat ping to check for new clients. This is not a critical + * behaviour for the client, so if we hit server/network issues, we'll just drop + * this until the next sync. + */ + _scheduleHeartbeat: function WeaveSvc__scheduleNextHeartbeat() { + let interval = MULTI_DESKTOP_SYNC; + if (this.nextSync < Date.now() + interval || + Status.enforceBackoff) + return; + + this._log.trace("Setting up heartbeat, next ping in " + + Math.ceil(interval / 1000) + " sec."); + Utils.delay(function() this._doHeartbeat(), interval, this, "_heartbeatTimer"); }, _syncErrors: 0, From 3eb7e7af21690624513d68dd4ba3b245f4ac1d8d Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 25 Mar 2010 17:24:41 -0400 Subject: [PATCH 1597/1860] bug 550597 - explicit server backoff is buggy, r=Mardak --- services/sync/modules/service.js | 7 +++++-- services/sync/modules/status.js | 8 +------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8b3fe88272d5..5affc06c5a72 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -396,7 +396,7 @@ WeaveSvc.prototype = { this._syncErrors = 0; break; case "weave:service:backoff:interval": - let interval = data + Math.random() * data * 0.25; // required backoff + up to 25% + let interval = (data + Math.random() * data * 0.25) * 1000; // required backoff + up to 25% Status.backoffInterval = interval; Status.minimumNextSync = Date.now() + data; break; @@ -1175,6 +1175,10 @@ WeaveSvc.prototype = { this._clearSyncTriggers(); this.nextSync = 0; + // reset backoff info, if the server tells us to continue backing off, + // we'll handle that later + Status.resetBackoff(); + this.globalScore = 0; if (!(this._remoteSetup())) @@ -1261,7 +1265,6 @@ WeaveSvc.prototype = { this._log.debug("Client count: " + this.numClients + " -> " + numClients); this.numClients = numClients; - let tabEngine = Engines.get("tabs"); if (numClients == 1) { this.syncInterval = SINGLE_USER_SYNC; this.syncThreshold = SINGLE_USER_THRESHOLD; diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js index f6162ce4dcb2..fc9526b2bb09 100644 --- a/services/sync/modules/status.js +++ b/services/sync/modules/status.js @@ -55,13 +55,7 @@ let Status = { get sync() this._sync, set sync(code) { this._sync = code; - - if (code != SYNC_SUCCEEDED) - this.service = SYNC_FAILED; - else { - this.service = STATUS_OK; - this.resetBackoff(); - } + this.service = code == SYNC_SUCCEEDED ? STATUS_OK : SYNC_FAILED; }, get engines() this._engines, From 4bacd81c6529ef686ed9fd534825ff9677873c45 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 16 Mar 2010 16:31:56 -0700 Subject: [PATCH 1598/1860] Bug 549638 - Ensure that encrypted payloads haven't been tampered with [r=mconnor r=dolske] Generate a SHA256 HAMC hex string from the base64 ciphertext and base64 symmetric key. Generate a HMAC key from the symmetric key and cache them together when unwrapping. Refactor the Utils.sha1 to share the same digest code. --- services/sync/modules/base_records/crypto.js | 22 ++++++++---- services/sync/modules/util.js | 35 ++++++++++++------- .../sync/tests/unit/test_records_crypto.js | 12 +++++++ 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 18b5efcc256d..34a8a32041bc 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -65,6 +65,7 @@ CryptoWrapper.prototype = { this.IV = Svc.Crypto.generateRandomIV(); this.ciphertext = Svc.Crypto.encrypt(JSON.stringify(this.cleartext), symkey, this.IV); + this.hmac = Utils.sha256HMAC(this.ciphertext, symkey.hmacKey); this.cleartext = null; }, @@ -75,6 +76,10 @@ CryptoWrapper.prototype = { let meta = CryptoMetas.get(this.encryption); let symkey = meta.getKey(privkey, passphrase); + // Authenticate the encrypted blob with the expected HMAC + if (Utils.sha256HMAC(this.ciphertext, symkey.hmacKey) != this.hmac) + throw "Server attack?! SHA256 HMAC mismatch: " + this.hmac; + this.cleartext = JSON.parse(Svc.Crypto.decrypt(this.ciphertext, symkey, this.IV)); this.ciphertext = null; @@ -103,7 +108,8 @@ CryptoWrapper.prototype = { }, }; -Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "encryption", "IV"]); +Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "encryption", "IV", + "hmac"]); Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted"); function CryptoMeta(uri) { @@ -115,10 +121,6 @@ CryptoMeta.prototype = { _logName: "Record.CryptoMeta", getKey: function CryptoMeta_getKey(privkey, passphrase) { - // We cache the unwrapped key, as it's expensive to generate - if (this._unwrappedKey) - return this._unwrappedKey; - // get the uri to our public key let pubkeyUri = privkey.publicKeyUri.spec; @@ -131,8 +133,14 @@ CryptoMeta.prototype = { if (!wrapped_key) throw "keyring doesn't contain a key for " + pubkeyUri; - return this._unwrappedKey = Svc.Crypto.unwrapSymmetricKey(wrapped_key, - privkey.keyData, passphrase.password, privkey.salt, privkey.iv); + let unwrappedKey = new String(Svc.Crypto.unwrapSymmetricKey(wrapped_key, + privkey.keyData, passphrase.password, privkey.salt, privkey.iv)); + + unwrappedKey.hmacKey = Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, + unwrappedKey); + + // Cache the result after the first get and just return it + return (this.getKey = function() unwrappedKey)(); }, addKey: function CryptoMeta_addKey(new_pubkey, privkey, passphrase) { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index a6fe44019e33..4a8eabdd70c9 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -475,26 +475,34 @@ let Utils = { throw 'checkStatus failed'; }, - sha1: function Weave_sha1(string) { + digest: function digest(message, hasher) { let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA1); - - let data = converter.convertToByteArray(string, {}); + let data = converter.convertToByteArray(message, {}); hasher.update(data, data.length); - let rawHash = hasher.finish(false); - // return the two-digit hexadecimal code for a byte - function toHexString(charCode) { - return ("0" + charCode.toString(16)).slice(-2); - } + // Convert each hashed byte into 2-hex strings then combine them + return [("0" + byte.charCodeAt().toString(16)).slice(-2) for each (byte in + hasher.finish(false))].join(""); + }, - let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); - return hash; + sha1: function sha1(message) { + let hasher = Cc["@mozilla.org/security/hash;1"]. + createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + return Utils.digest(message, hasher); + }, + + /** + * Generate a sha256 HMAC for a string message and a given nsIKeyObject + */ + sha256HMAC: function sha256HMAC(message, key) { + let hasher = Cc["@mozilla.org/security/hmac;1"]. + createInstance(Ci.nsICryptoHMAC); + hasher.init(hasher.SHA256, key); + return Utils.digest(message, hasher); }, makeURI: function Weave_makeURI(URIString) { @@ -858,6 +866,7 @@ Svc.Obs = Observers; ["History", "@mozilla.org/browser/nav-history-service;1", "nsPIPlacesDatabase"], ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], + ["KeyFactory", "@mozilla.org/security/keyobjectfactory;1", "nsIKeyObjectFactory"], ["Login", "@mozilla.org/login-manager;1", "nsILoginManager"], ["Memory", "@mozilla.org/xpcom/memory-service;1", "nsIMemory"], ["Observer", "@mozilla.org/observer-service;1", "nsIObserverService"], diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index f24c0914cebc..335f10fc1886 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -110,6 +110,18 @@ function run_test() { } do_check_eq(error, "Server attack?! Id mismatch: crypted-resource,other"); + log.info("Make sure wrong hmacs cause failures"); + cryptoWrap.encrypt(passphrase); + cryptoWrap.hmac = "foo"; + error = ""; + try { + cryptoWrap.decrypt(passphrase); + } + catch(ex) { + error = ex; + } + do_check_eq(error, "Server attack?! SHA256 HMAC mismatch: foo"); + log.info("Done!"); } finally { server.stop(); } From 4f38873f695bfa86a0170313920a2dd34f64b0f6 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 25 Mar 2010 19:23:44 -0700 Subject: [PATCH 1599/1860] Bug 552134 - Ensure that keyring/symmetric key haven't been tampered with [r=mconnor] Store a HMAC with the encrypted symmetric key instead of just the wrapped key and verify that the HMAC matches before unwrapping. Test that normal getting works and a tampered payload/HMAC fails but succeeds on restoring the correct HMAC. --- services/sync/modules/base_records/crypto.js | 31 +++++++++++-- services/sync/tests/unit/head_first.js | 6 +++ .../sync/tests/unit/test_clients_escape.js | 1 - .../sync/tests/unit/test_records_crypto.js | 1 - .../tests/unit/test_records_cryptometa.js | 43 +++++++++++++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 services/sync/tests/unit/test_records_cryptometa.js diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 34a8a32041bc..85bd4927f8f1 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -41,6 +41,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/base_records/wbo.js"); Cu.import("resource://weave/base_records/keys.js"); @@ -133,8 +134,21 @@ CryptoMeta.prototype = { if (!wrapped_key) throw "keyring doesn't contain a key for " + pubkeyUri; - let unwrappedKey = new String(Svc.Crypto.unwrapSymmetricKey(wrapped_key, - privkey.keyData, passphrase.password, privkey.salt, privkey.iv)); + // Make sure the wrapped key hasn't been tampered with + let localHMAC = Utils.sha256HMAC(wrapped_key.wrapped, this.hmacKey); + if (localHMAC != wrapped_key.hmac) + throw "Server attack?! SHA256 HMAC key fail: " + wrapped_key.hmac; + + // Decrypt the symmetric key and make it a String object to add properties + let unwrappedKey = new String( + Svc.Crypto.unwrapSymmetricKey( + wrapped_key.wrapped, + privkey.keyData, + passphrase.password, + privkey.salt, + privkey.iv + ) + ); unwrappedKey.hmacKey = Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, unwrappedKey); @@ -160,8 +174,17 @@ CryptoMeta.prototype = { delete this.keyring[relUri]; } - this.keyring[new_pubkey.uri.spec] = - Svc.Crypto.wrapSymmetricKey(symkey, new_pubkey.keyData); + // Wrap the symmetric key and generate a HMAC for it + let wrapped = Svc.Crypto.wrapSymmetricKey(symkey, new_pubkey.keyData); + this.keyring[new_pubkey.uri.spec] = { + wrapped: wrapped, + hmac: Utils.sha256HMAC(wrapped, this.hmacKey) + }; + }, + + get hmacKey() { + let passphrase = ID.get("WeaveCryptoID").password; + return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, passphrase); } }; diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index d18688ce661a..fa37973e1f3e 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -330,3 +330,9 @@ function SyncTestingInfrastructure(engineFactory) { * @usage _(1, 2, 3) -> prints "1 2 3" */ let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" ")); + +_("Setting the identity for passphrase"); +Cu.import("resource://weave/identity.js"); +let passphrase = ID.set("WeaveCryptoID", new Identity()); +passphrase.password = "passphrase"; + diff --git a/services/sync/tests/unit/test_clients_escape.js b/services/sync/tests/unit/test_clients_escape.js index bbc63835f5e0..a165b3b8e8f4 100644 --- a/services/sync/tests/unit/test_clients_escape.js +++ b/services/sync/tests/unit/test_clients_escape.js @@ -4,7 +4,6 @@ Cu.import("resource://weave/base_records/keys.js"); Cu.import("resource://weave/base_records/crypto.js"); function run_test() { - let passphrase = { password: "passphrase" }; let baseUri = "http://fakebase/"; let pubUri = baseUri + "pubkey"; let privUri = baseUri + "privkey"; diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 335f10fc1886..04309d9ce2ff 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -59,7 +59,6 @@ function run_test() { log.info("Generating keypair + symmetric key"); PubKeys.defaultKeyUri = "http://localhost:8080/pubkey"; - let passphrase = { password: "my passphrase" }; keys = PubKeys.createKeypair(passphrase, "http://localhost:8080/pubkey", "http://localhost:8080/privkey"); diff --git a/services/sync/tests/unit/test_records_cryptometa.js b/services/sync/tests/unit/test_records_cryptometa.js new file mode 100644 index 000000000000..57fb62bec6be --- /dev/null +++ b/services/sync/tests/unit/test_records_cryptometa.js @@ -0,0 +1,43 @@ +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://weave/base_records/keys.js"); + +function run_test() { + _("Generating keypair to encrypt/decrypt symkeys"); + let {pubkey, privkey} = PubKeys.createKeypair( + passphrase, + "http://site/pubkey", + "http://site/privkey" + ); + PubKeys.set(pubkey.uri, pubkey); + PrivKeys.set(privkey.uri, privkey); + + _("Generating a crypto meta with a random key"); + let crypto = new CryptoMeta("http://site/crypto"); + let symkey = Svc.Crypto.generateRandomKey(); + crypto.addUnwrappedKey(pubkey, symkey); + + _("Verifying correct HMAC by getting the key"); + crypto.getKey(privkey, passphrase); + + _("Generating a new crypto meta as the previous caches the unwrapped key"); + let crypto = new CryptoMeta("http://site/crypto"); + let symkey = Svc.Crypto.generateRandomKey(); + crypto.addUnwrappedKey(pubkey, symkey); + + _("Changing the HMAC to force a mismatch"); + let goodHMAC = crypto.keyring[pubkey.uri.spec].hmac; + crypto.keyring[pubkey.uri.spec].hmac = "failme!"; + let error = ""; + try { + crypto.getKey(privkey, passphrase); + } + catch(ex) { + error = ex; + } + do_check_eq(error, "Server attack?! SHA256 HMAC key fail: failme!"); + + _("Switching back to the correct HMAC and trying again"); + crypto.keyring[pubkey.uri.spec].hmac = goodHMAC; + crypto.getKey(privkey, passphrase); +} From 5ed218837af6c2acc50d204c8036764c8a724bca Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 12 Mar 2010 16:14:09 -0800 Subject: [PATCH 1600/1860] bug 539056 - Better Tab sync UI and discoverability, r=Mardak --- services/sync/Weave.js | 23 ++++++++++++++++++++- services/sync/locales/en-US/fx-tabs.dtd | 9 ++++++++ services/sync/locales/en-US/sync.dtd | 1 - services/sync/locales/en-US/sync.properties | 2 ++ services/sync/modules/engines/clients.js | 4 ++++ 5 files changed, 37 insertions(+), 2 deletions(-) mode change 100644 => 100755 services/sync/Weave.js create mode 100644 services/sync/locales/en-US/fx-tabs.dtd diff --git a/services/sync/Weave.js b/services/sync/Weave.js old mode 100644 new mode 100755 index cfa151d25e7a..8b18fea5053f --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -71,7 +71,28 @@ WeaveService.prototype = { } }; +function AboutWeaveTabs() {} +AboutWeaveTabs.prototype = { + classDescription: "about:weave-tabs", + contractID: "@mozilla.org/network/protocol/about;1?what=weave-tabs", + classID: Components.ID("{ecb6987d-9d71-475d-a44d-a5ff2099b08c}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, + Ci.nsISupportsWeakReference]), + + getURIFlags: function(aURI) { + return (Ci.nsIAboutModule.ALLOW_SCRIPT); + }, + + newChannel: function(aURI) { + let ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let ch = ios.newChannel("chrome://weave/content/fx-tabs.xul", null, null); + ch.originalURI = aURI; + return ch; + } +}; function NSGetModule(compMgr, fileSpec) { - return XPCOMUtils.generateModule([WeaveService]); + return XPCOMUtils.generateModule([WeaveService, AboutWeaveTabs]); } diff --git a/services/sync/locales/en-US/fx-tabs.dtd b/services/sync/locales/en-US/fx-tabs.dtd new file mode 100644 index 000000000000..76f3eacd728c --- /dev/null +++ b/services/sync/locales/en-US/fx-tabs.dtd @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 94c915e7e001..8d7ec8412271 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -14,4 +14,3 @@ - diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index f5bd51f37074..5836d0e378f9 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -37,3 +37,5 @@ error.sync.needUpdate.label = Update Weave error.sync.needUpdate.accesskey = U error.sync.tryAgainButton.label = Sync Now error.sync.tryAgainButton.accesskey = S + +tabs.fromOtherComputers.label = Tabs From Other Computers diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index c806643a67fb..8864920c4988 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -162,6 +162,10 @@ ClientEngine.prototype = { }, set localType(value) Svc.Prefs.set("client.type", value), + isMobile: function isMobile(id) { + return this._store._remoteClients[id].type == "mobile"; + }, + // Always process incoming items because they might have commands _reconcile: function _reconcile() { return true; From 85ad90156d8b41a019d94c66f0bfecf3e92507d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Wed, 31 Mar 2010 23:05:50 -0700 Subject: [PATCH 1601/1860] Bug 532932 - No error prompting for invalid email on weave sign up [r=Mardak] --- services/sync/locales/en-US/fx-prefs.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties index db589ba61c9d..4379ef355f79 100644 --- a/services/sync/locales/en-US/fx-prefs.properties +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -10,6 +10,8 @@ passwordTooWeak.label = Password is too weak entriesMustMatch.label = Phrases do not match cannotMatchPassword.label = Secret phrase cannot match password +invalidEmail.label = Invalid email address + errorCreatingAccount.title = Error Creating Account serverInvalid.label = Please enter a valid server URL From 1268cfa9751b4d5c74769fb920ef9c7703fe6885 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 17 Feb 2010 18:24:22 -0800 Subject: [PATCH 1602/1860] Bug 535326 - Need to rethink tab sync limit of 25 [r=mconnor] Fit as many tabs as possible in 20000 characters by linearly estimating how many will fit then remove extras one by one. --- services/sync/modules/engines/tabs.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 0770a66b69a1..3cae17bd2eb4 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -148,13 +148,31 @@ TabStore.prototype = { record.clientName = Clients.localName; // Sort tabs in descending-used order to grab the most recently used - record.tabs = this.getAllTabs(true).sort(function(a, b) { + let tabs = this.getAllTabs(true).sort(function(a, b) { return b.lastUsed - a.lastUsed; - }).slice(0, 25); - record.tabs.forEach(function(tab) { + }); + + // 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; }, From 053d03fd57b3be7766ebe452848775b89eb7cbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Thu, 1 Apr 2010 13:43:09 -0700 Subject: [PATCH 1603/1860] Bug 539057 - better flow with start over [r=mconnor] * Adds an option to the manage account to change sync option. * Changes "start over" to "use a different account". * Hides the "start over" button on the sync options page when not running through the whole process. * Adds a prompt when starting over (which fixes bug 524186). --- services/sync/locales/en-US/fx-prefs.dtd | 2 ++ services/sync/locales/en-US/fx-prefs.properties | 6 +++++- services/sync/modules/service.js | 12 ++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 8e7171742372..9405945be317 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -88,6 +88,8 @@ + + diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties index 4379ef355f79..7991da080a02 100644 --- a/services/sync/locales/en-US/fx-prefs.properties +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -25,4 +25,8 @@ additionalClients.label = and %S additional devices bookmarkCount.label = %S bookmarks historyCount.label = %S days of history -passwordCount.label = %S passwords \ No newline at end of file +passwordCount.label = %S passwords + +differentAccount.title = Use a Different Account? +differentAccount.label = This will reset all of your sync account information and preferences. +differentAccountConfirm.label = Reset All Information diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5affc06c5a72..16d8a965b3d8 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -641,6 +641,18 @@ WeaveSvc.prototype = { return false; }, + startOver: function() { + this.logout(); + // Reset all engines + this.resetClient(); + // Reset Weave prefs + Svc.Prefs.resetBranch(""); + // Find weave logins and remove them. + Svc.Login.findLogins({}, PWDMGR_HOST, "", "").map(function(login) { + Svc.Login.removeLogin(login); + }); + }, + _autoConnect: let (attempts = 0) function _autoConnect() { let reason = ""; if (this._mpLocked()) From 1b406c2bc1ead8d25424bd66bdcd101c7fe8ab64 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 1 Apr 2010 15:11:42 -0700 Subject: [PATCH 1604/1860] Bug 556509 - folders description not synchronised [r=mconnor] Allow descriptions to be set on folder records and set description on creation. --- services/sync/modules/engines/bookmarks.js | 4 ++++ services/sync/modules/type_records/bookmark.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index c619bd60430a..e9a53350f307 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -509,6 +509,9 @@ BookmarksStore.prototype = { record._insertPos); this._log.debug(["created folder", newId, "under", record._parent, "at", record._insertPos, "as", record.title].join(" ")); + + if (record.description) + Utils.anno(newId, "bookmarkProperties/description", record.description); break; case "livemark": let siteURI = null; @@ -767,6 +770,7 @@ BookmarksStore.prototype = { record.parentName = Svc.Bookmark.getItemTitle(parent); record.title = this._bms.getItemTitle(placeId); + record.description = this._getDescription(placeId); break; case this._bms.TYPE_SEPARATOR: diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index e2d4ac91ca04..701c174d284b 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -127,7 +127,7 @@ BookmarkFolder.prototype = { _logName: "Record.Folder", }; -Utils.deferGetSet(BookmarkFolder, "cleartext", "title"); +Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title"]); function Livemark(uri) { BookmarkFolder.call(this, uri, "livemark"); From 4f64cd093ef34648d90758e5789ce4c9489ea062 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 1 Apr 2010 15:16:19 -0700 Subject: [PATCH 1605/1860] Bug 554924 - Weave should not sync during session restore [r=mconnor] Get rid of STATUS_DELAYED and initialize Weave listeners, etc immediately. At the end of onStartup, wait a little bit to let sessionstore restore tabs and then count how many busy tabs to delay autoconnecting to avoid doing network while tabs are doing network. --- services/sync/modules/constants.js | 1 - services/sync/modules/service.js | 47 ++++++++++++++---------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 3dafb43b395e..d3bae0dd072f 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -85,7 +85,6 @@ SYNC_FAILED: "error.sync.failed", LOGIN_FAILED: "error.login.failed", SYNC_FAILED_PARTIAL: "error.sync.failed_partial", STATUS_DISABLED: "service.disabled", -STATUS_DELAYED: "service.startup.delayed", // success states LOGIN_SUCCEEDED: "success.login", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 16d8a965b3d8..0a2435d2b639 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -247,31 +247,9 @@ WeaveSvc.prototype = { * Prepare to initialize the rest of Weave after waiting a little bit */ onStartup: function onStartup() { - Status.service = STATUS_DELAYED; - - // Figure out how many seconds to delay loading Weave based on the app - let wait = 0; - switch (Svc.AppInfo.ID) { - case FIREFOX_ID: - // Add one second delay for each tab in every window - let enum = Svc.WinMediator.getEnumerator("navigator:browser"); - while (enum.hasMoreElements()) - wait += enum.getNext().gBrowser.mTabs.length; - break; - } - - // Make sure we wait a little but but not too long in the worst case - wait = Math.ceil(Math.max(5, Math.min(20, wait))); - this._initLogs(); - this._log.info("Loading Weave " + WEAVE_VERSION + " in " + wait + " sec."); - Utils.delay(this._onStartup, wait * 1000, this, "_startupTimer"); - }, + this._log.info("Loading Weave " + WEAVE_VERSION); - // one-time initialization like setting up observers and the like - // xxx we might need to split some of this out into something we can call - // again when username/server/etc changes - _onStartup: function _onStartup() { Status.service = STATUS_OK; this.enabled = true; @@ -309,8 +287,27 @@ WeaveSvc.prototype = { // Send an event now that Weave service is ready Svc.Obs.notify("weave:service:ready"); - if (Svc.Prefs.get("autoconnect")) - this._autoConnect(); + // Wait a little before checking how long to wait to autoconnect + if (Svc.Prefs.get("autoconnect")) { + Utils.delay(function() { + // Figure out how many seconds to delay autoconnect based on the app + let wait = 3; + switch (Svc.AppInfo.ID) { + case FIREFOX_ID: + // Add one second delay for each busy tab in every window + let enum = Svc.WinMediator.getEnumerator("navigator:browser"); + while (enum.hasMoreElements()) { + Array.forEach(enum.getNext().gBrowser.mTabs, function(tab) { + wait += tab.hasAttribute("busy"); + }); + } + break; + } + + this._log.debug("Autoconnecting in " + wait + " seconds"); + Utils.delay(this._autoConnect, wait * 1000, this, "_autoTimer"); + }, 2000, this, "_autoTimer"); + } }, _initLogs: function WeaveSvc__initLogs() { From 6aeba92aa975c68e5c1459aa83ca69f2fa413383 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 1 Apr 2010 15:21:54 -0700 Subject: [PATCH 1606/1860] Bug 556361 - New searches from the searchbar don't get synced [r=mconnor] Add a component that notifies when satchel methods are getting called. The notifications come as "form-notifier" with JSON data of the function name, arguments, and type (before vs after). --- services/sync/FormNotifier.js | 51 ++++++++++++++++++++++++++ services/sync/modules/engines/forms.js | 32 ++++++++++++++-- 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 services/sync/FormNotifier.js diff --git a/services/sync/FormNotifier.js b/services/sync/FormNotifier.js new file mode 100644 index 000000000000..2b8096649d3a --- /dev/null +++ b/services/sync/FormNotifier.js @@ -0,0 +1,51 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function FormNotifier() { + let baseForm = Components.classesByID["{a2059c0e-5a58-4c55-ab7c-26f0557546ef}"]. + getService(Ci.nsIFormHistory2) + let obs = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + + for (let keyval in Iterator(baseForm)) { + // Make a local copy of these values + let [key, val] = keyval; + + // Don't overwrite something we already have + if (key in this) + continue; + + // Make a getter to grab non-functions + if (typeof val != "function") { + this.__defineGetter__(key, function() baseForm[key]); + continue; + } + + // Wrap the function with notifications + this[key] = function() { + let args = Array.slice(arguments); + let notify = function(type) { + obs.notifyObservers(null, "form-notifier", JSON.stringify({ + args: args, + func: key, + type: type + })); + }; + + notify("before"); + val.apply(this, arguments); + notify("after"); + }; + } +} +FormNotifier.prototype = { + classDescription: "Form Notifier Wrapper", + contractID: "@mozilla.org/satchel/form-history;1", + classID: Components.ID("{be5a097b-6ee6-4c6a-8eca-6bce87d570e9}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormHistory2]), +}; + +let components = [FormNotifier]; +function NSGetModule(compMgr, fileSpec) XPCOMUtils.generateModule(components); diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index c8106db68581..afe49d9dec0f 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -197,14 +197,39 @@ FormStore.prototype = { function FormTracker(name) { Tracker.call(this, name); + Svc.Obs.add("form-notifier", this); Svc.Observer.addObserver(this, "earlyformsubmit", false); } FormTracker.prototype = { __proto__: Tracker.prototype, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIFormSubmitObserver, + Ci.nsIObserver]), + + trackEntry: function trackEntry(name, value) { + this.addChangedID(Utils.sha1(name + value)); + this.score += 10; + }, + + observe: function observe(subject, topic, data) { + let name, value; + + // Figure out if it's a function that we care about tracking + let formCall = JSON.parse(data); + let func = formCall.func; + if ((func == "addEntry" && formCall.type == "after") || + (func == "removeEntry" && formCall.type == "before")) + [name, value] = formCall.args; + + // Skip if there's nothing of interest + if (name == null || value == null) + return; + + this._log.trace("Logging form action: " + [func, name, value]); + this.trackEntry(name, value); + }, - /* 10 points per form element */ notify: function FormTracker_notify(formElement, aWindow, actionURI) { if (this.ignoreAll) return; @@ -268,8 +293,7 @@ FormTracker.prototype = { } this._log.trace("Logging form element: " + name + " :: " + el.value); - this.addChangedID(Utils.sha1(name + el.value)); - this.score += 10; + this.trackEntry(name, el.value); } } }; From 2d8626cf1193e90b8aff0f78800f54e7a5d0484c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 1 Apr 2010 15:25:47 -0700 Subject: [PATCH 1607/1860] Bug 544532 - Weave won't sync after resume from standby [r=mconnor] Start an abortTimer onStartRequest and refresh the timer on each onDataAvailable only to cancel on an onStopRequest. If the timer triggers, the sync/async call will be aborted. --- services/sync/modules/resource.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index ccbc0863f905..e25db9b755c3 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -206,7 +206,9 @@ Resource.prototype = { catch(ex) { // Combine the channel stack with this request stack let error = Error(ex.message); - let chanStack = ex.stack.trim().split(/\n/).slice(1); + let chanStack = []; + if (ex.stack) + chanStack = ex.stack.trim().split(/\n/).slice(1); let requestStack = error.stack.split(/\n/).slice(1); // Strip out the args for the last 2 frames because they're usually HUGE! @@ -301,13 +303,22 @@ function ChannelListener(onComplete, onProgress, logger) { this._log = logger; } ChannelListener.prototype = { + // Wait 5 minutes before killing a request + ABORT_TIMEOUT: 300000, + onStartRequest: function Channel_onStartRequest(channel) { channel.QueryInterface(Ci.nsIHttpChannel); this._log.trace(channel.requestMethod + " " + channel.URI.spec); this._data = ''; + + // Create an abort timer to kill dangling requests + Utils.delay(this.abortRequest, this.ABORT_TIMEOUT, this, "abortTimer"); }, onStopRequest: function Channel_onStopRequest(channel, context, status) { + // Clear the abort timer now that the channel is done + this.abortTimer.clear(); + if (this._data == '') this._data = null; @@ -325,6 +336,16 @@ ChannelListener.prototype = { this._data += siStream.read(count); this._onProgress(); + + // Update the abort timer to wait an extra timeout + Utils.delay(this.abortRequest, this.ABORT_TIMEOUT, this, "abortTimer"); + }, + + abortRequest: function abortRequest() { + // Ignore any callbacks if we happen to get any now + this.onStopRequest = function() {}; + this.onDataAvailable = function() {}; + this._onComplete.throw(Error("Aborting due to channel inactivity.")); } }; From bfdde2c35f5ddb02ee9c029b32d53899b96c30c8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 1 Apr 2010 15:29:16 -0700 Subject: [PATCH 1608/1860] Bug 546768 - form history uses hashes for GUIDs [r=mconnor] Write a FormWrapper that knows about GUIDs and get/sets them in moz_formhistory as needed. It lazily adds the columns on failure and lazily generates GUIDs for entries that are missing it. Don't eagerly create a sha1 formItem mapping -- don't create it at all, so empty syncs will be much faster too. --- services/sync/modules/engines.js | 10 +- services/sync/modules/engines/forms.js | 208 +++++++++++++------------ services/sync/modules/util.js | 1 + 3 files changed, 118 insertions(+), 101 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index eefc5f6c26e1..f46726144d47 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -584,14 +584,18 @@ SyncEngine.prototype = { }, _handleDupe: function _handleDupe(item, dupeId) { - // The local dupe is the lower id, so pretend the incoming is for it - if (dupeId < item.id) { + // Prefer shorter guids; for ties, just do an ASCII compare + let preferLocal = dupeId.length < item.id.length || + (dupeId.length == item.id.length && dupeId < item.id); + + if (preferLocal) { + this._log.trace("Preferring local id: " + [dupeId, item.id]); this._deleteId(item.id); item.id = dupeId; this._tracker.changedIDs[dupeId] = true; } - // The incoming item has the lower id, so change the dupe to it else { + this._log.trace("Switching local id to incoming: " + [item.id, dupeId]); this._store.changeItemID(dupeId, item.id); this._deleteId(dupeId); } diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index afe49d9dec0f..13e08c22fab8 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -47,6 +47,89 @@ Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/forms.js"); +let FormWrapper = { + getAllEntries: function getAllEntries() { + let entries = []; + let query = Svc.Form.DBConnection.createStatement( + "SELECT fieldname, value FROM moz_formhistory"); + while (query.executeStep()) { + entries.push({ + name: query.row.fieldname, + value: query.row.value + }); + } + return entries; + }, + + getEntry: function getEntry(guid) { + let query = Svc.Form.DBConnection.createStatement( + "SELECT fieldname, value FROM moz_formhistory WHERE guid = :guid"); + query.params.guid = guid; + if (!query.executeStep()) + return; + + return { + name: query.row.fieldname, + value: query.row.value + }; + }, + + getGUID: function getGUID(name, value) { + let getQuery = "SELECT guid FROM moz_formhistory " + + "WHERE fieldname = :name AND value = :value"; + try { + getQuery = Svc.Form.DBConnection.createStatement(getQuery); + } + catch(ex) { + // The column must not exist yet, so add it with an index + Svc.Form.DBConnection.executeSimpleSQL( + "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT"); + Svc.Form.DBConnection.executeSimpleSQL( + "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index " + + "ON moz_formhistory (guid)"); + + // Try creating the query now that the column exists + getQuery = Svc.Form.DBConnection.createStatement(getQuery); + } + + // Query for the provided entry + getQuery.params.name = name; + getQuery.params.value = value; + getQuery.executeStep(); + + // Give the guid if we found one + if (getQuery.row.guid != null) + return getQuery.row.guid; + + // We need to create a guid for this entry + let setQuery = Svc.Form.DBConnection.createStatement( + "UPDATE moz_formhistory SET guid = :guid " + + "WHERE fieldname = :name AND value = :value"); + let guid = Utils.makeGUID(); + setQuery.params.guid = guid; + setQuery.params.name = name; + setQuery.params.value = value; + setQuery.execute(); + + return guid; + }, + + hasGUID: function hasGUID(guid) { + let query = Svc.Form.DBConnection.createStatement( + "SELECT 1 FROM moz_formhistory WHERE guid = :guid"); + query.params.guid = guid; + return query.executeStep(); + }, + + replaceGUID: function replaceGUID(oldGUID, newGUID) { + let query = Svc.Form.DBConnection.createStatement( + "UPDATE moz_formhistory SET guid = :newGUID WHERE guid = :oldGUID"); + query.params.oldGUID = oldGUID; + query.params.newGUID = newGUID; + query.execute(); + } +}; + function FormEngine() { SyncEngine.call(this, "Forms"); } @@ -57,133 +140,59 @@ FormEngine.prototype = { _recordObj: FormRec, get prefName() "history", - _syncStartup: function FormEngine__syncStartup() { - this._store.cacheFormItems(); - SyncEngine.prototype._syncStartup.call(this); - }, - - /* Wipe cache when sync finishes */ - _syncFinish: function FormEngine__syncFinish(error) { - this._store.clearFormCache(); - SyncEngine.prototype._syncFinish.call(this); - }, - _findDupe: function _findDupe(item) { - // Search through the items to find a matching name/value - for (let [guid, {name, value}] in Iterator(this._store._formItems)) - if (name == item.name && value == item.value) - return guid; + if (Svc.Form.entryExists(item.name, item.value)) + return FormWrapper.getGUID(item.name, item.value); } }; - function FormStore(name) { Store.call(this, name); } FormStore.prototype = { __proto__: Store.prototype, - _formItems: null, - - get _formDB() { - let file = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties). - get("ProfD", Ci.nsIFile); - file.append("formhistory.sqlite"); - let stor = Cc["@mozilla.org/storage/service;1"]. - getService(Ci.mozIStorageService); - let formDB = stor.openDatabase(file); - - this.__defineGetter__("_formDB", function() formDB); - return formDB; - }, - - get _formHistory() { - let formHistory = Cc["@mozilla.org/satchel/form-history;1"]. - getService(Ci.nsIFormHistory2); - this.__defineGetter__("_formHistory", function() formHistory); - return formHistory; - }, - - get _formStatement() { - // This is essentially: - // SELECT * FROM moz_formhistory ORDER BY 1.0 * (lastUsed - minLast) / - // (maxLast - minLast) * timesUsed / minTimes DESC LIMIT 200 - let stmnt = this._formDB.createStatement( - "SELECT * FROM moz_formhistory ORDER BY 1.0 * (lastUsed - \ - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) / \ - ((SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed DESC LIMIT 1) - \ - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) * \ - timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) \ - DESC LIMIT 200" - ); - - this.__defineGetter__("_formStatement", function() stmnt); - return stmnt; - }, - - cacheFormItems: function FormStore_cacheFormItems() { - this._log.trace("Caching all form items"); - this._formItems = this.getAllIDs(); - }, - - clearFormCache: function FormStore_clearFormCache() { - this._log.trace("Clearing form cache"); - this._formItems = null; - }, getAllIDs: function FormStore_getAllIDs() { - let items = {}; - let stmnt = this._formStatement; - - while (stmnt.executeStep()) { - let nam = stmnt.getUTF8String(1); - let val = stmnt.getUTF8String(2); - let key = Utils.sha1(nam + val); - - items[key] = { name: nam, value: val }; - } - stmnt.reset(); - - return items; + let guids = {}; + for each (let {name, value} in FormWrapper.getAllEntries()) + guids[FormWrapper.getGUID(name, value)] = true; + return guids; }, changeItemID: function FormStore_changeItemID(oldID, newID) { - this._log.warn("FormStore IDs are data-dependent, cannot change!"); + FormWrapper.replaceGUID(oldID, newID); }, itemExists: function FormStore_itemExists(id) { - return (id in this._formItems); + return FormWrapper.hasGUID(id); }, createRecord: function createRecord(guid) { let record = new FormRec(); - - if (guid in this._formItems) { - let item = this._formItems[guid]; - record.name = item.name; - record.value = item.value; - } else { - record.deleted = true; + let entry = FormWrapper.getEntry(guid); + if (entry != null) { + record.name = entry.name; + record.value = entry.value } - + else + record.deleted = true; return record; }, create: function FormStore_create(record) { - this._log.debug("Adding form record for " + record.name); - this._formHistory.addEntry(record.name, record.value); + this._log.trace("Adding form record for " + record.name); + Svc.Form.addEntry(record.name, record.value); }, remove: function FormStore_remove(record) { this._log.trace("Removing form record: " + record.id); - if (record.id in this._formItems) { - let item = this._formItems[record.id]; - this._formHistory.removeEntry(item.name, item.value); + // Just skip remove requests for things already gone + let entry = FormWrapper.getEntry(record.id); + if (entry == null) return; - } - this._log.trace("Invalid GUID found, ignoring remove request."); + Svc.Form.removeEntry(entry.name, entry.value); }, update: function FormStore_update(record) { @@ -191,7 +200,7 @@ FormStore.prototype = { }, wipe: function FormStore_wipe() { - this._formHistory.removeAllEntries(); + Svc.Form.removeAllEntries(); } }; @@ -208,7 +217,7 @@ FormTracker.prototype = { Ci.nsIObserver]), trackEntry: function trackEntry(name, value) { - this.addChangedID(Utils.sha1(name + value)); + this.addChangedID(FormWrapper.getGUID(name, value)); this.score += 10; }, @@ -292,8 +301,11 @@ FormTracker.prototype = { continue; } - this._log.trace("Logging form element: " + name + " :: " + el.value); - this.trackEntry(name, el.value); + // Get the GUID on a delay so that it can be added to the DB first... + Utils.delay(function() { + this._log.trace("Logging form element: " + [name, el.value]); + this.trackEntry(name, el.value); + }, 0, this); } } }; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 4a8eabdd70c9..63728a949fdc 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -863,6 +863,7 @@ Svc.Obs = Observers; ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], ["Env", "@mozilla.org/process/environment;1", "nsIEnvironment"], ["Favicon", "@mozilla.org/browser/favicon-service;1", "nsIFaviconService"], + ["Form", "@mozilla.org/satchel/form-history;1", "nsIFormHistory2"], ["History", "@mozilla.org/browser/nav-history-service;1", "nsPIPlacesDatabase"], ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["IO", "@mozilla.org/network/io-service;1", "nsIIOService"], From cf8d361e6ada9036298b331a3fea497c053b3000 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 1 Apr 2010 15:54:53 -0700 Subject: [PATCH 1609/1860] Bug 550627 - Default reconciliation to server wins for older changed items [r=mconnor] Save the time the tracker adds a new changed id and use that to compare the age of the record on the server vs the age of the local change to decide if it's server wins or client wins. Fix up various direct uses of changedIDs to use the API and make the save-to-disk lazy to avoid excessive writes. Add a test to make sure addChangedID only increases in time. --- services/sync/modules/engines.js | 51 ++++++++----------- services/sync/modules/engines/bookmarks.js | 2 +- services/sync/modules/engines/tabs.js | 14 +---- services/sync/modules/resource.js | 7 +++ services/sync/modules/service.js | 2 +- services/sync/modules/trackers.js | 43 +++++++--------- .../tests/unit/test_tracker_addChanged.js | 25 +++++++++ 7 files changed, 72 insertions(+), 72 deletions(-) create mode 100644 services/sync/tests/unit/test_tracker_addChanged.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index f46726144d47..17cd50e85a11 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -434,14 +434,11 @@ SyncEngine.prototype = { CryptoMetas.set(meta.uri, meta); } - // first sync special case: upload all items - // NOTE: we use a backdoor (of sorts) to the tracker so it - // won't save to disk this list over and over + // Mark all items to be uploaded, but treat them as changed from long ago if (!this.lastSync) { this._log.debug("First sync, uploading all items"); - this._tracker.clearChangedIDs(); - [i for (i in this._store.getAllIDs())] - .forEach(function(id) this._tracker.changedIDs[id] = true, this); + for (let id in this._store.getAllIDs()) + this._tracker.addChangedID(id, 0); } let outnum = [i for (i in this._tracker.changedIDs)].length; @@ -592,7 +589,7 @@ SyncEngine.prototype = { this._log.trace("Preferring local id: " + [dupeId, item.id]); this._deleteId(item.id); item.id = dupeId; - this._tracker.changedIDs[dupeId] = true; + this._tracker.addChangedID(dupeId, 0); } else { this._log.trace("Switching local id to incoming: " + [item.id, dupeId]); @@ -601,44 +598,36 @@ SyncEngine.prototype = { } }, - // Reconciliation has three steps: - // 1) Check for the same item (same ID) on both the incoming and outgoing - // queues. This means the same item was modified on this profile and - // another at the same time. In this case, this client wins (which really - // means, the last profile you sync wins). - // 2) Check if the incoming item's ID exists locally. In that case it's an - // update and we should not try a similarity check (step 3) - // 3) Check if any incoming & outgoing items are actually the same, even - // though they have different IDs. This happens when the same item is - // added on two different machines at the same time. It's also the common - // case when syncing for the first time two machines that already have the - // same bookmarks. In this case we change the IDs to match. _reconcile: function SyncEngine__reconcile(item) { if (this._log.level <= Log4Moz.Level.Trace) this._log.trace("Incoming: " + item); - // Step 1: Check for conflicts - // If same as local record, do not upload - this._log.trace("Reconcile step 1"); + this._log.trace("Reconcile step 1: Check for conflicts"); if (item.id in this._tracker.changedIDs) { - if (this._isEqual(item)) + // If the incoming and local changes are the same, skip + if (this._isEqual(item)) { this._tracker.removeChangedID(item.id); - return false; + return false; + } + + // Records differ so figure out which to take + let recordAge = Resource.serverTime - item.modified; + let localAge = Date.now() / 1000 - this._tracker.changedIDs[item.id]; + this._log.trace("Record age vs local age: " + [recordAge, localAge]); + + // Apply the record if the record is newer (server wins) + return recordAge < localAge; } - // Step 2: Check for updates - // If different from local record, apply server update - this._log.trace("Reconcile step 2"); + this._log.trace("Reconcile step 2: Check for updates"); if (this._store.itemExists(item.id)) return !this._isEqual(item); - // If the incoming item has been deleted, skip step 3 - this._log.trace("Reconcile step 2.5"); + this._log.trace("Reconcile step 2.5: Don't dupe deletes"); if (item.deleted) return true; - // Step 3: Check for similar items - this._log.trace("Reconcile step 3"); + this._log.trace("Reconcile step 3: Find dupes"); let dupeId = this._findDupe(item); if (dupeId) this._handleDupe(item, dupeId); diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index e9a53350f307..d12079518f0f 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -206,7 +206,7 @@ BookmarksEngine.prototype = { // Trigger id change from dupe to winning and update the server this._store.changeItemID(dupeId, item.id); this._deleteId(dupeId); - this._tracker.changedIDs[item.id] = true; + this._tracker.addChangedID(item.id, 0); } }; diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 3cae17bd2eb4..e3bf1c7bc863 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -75,11 +75,6 @@ TabEngine.prototype = { this._store.wipe(); }, - _syncFinish: function _syncFinish() { - SyncEngine.prototype._syncFinish.call(this); - this._tracker.resetChanged(); - }, - /* The intent is not to show tabs in the menu if they're already * open locally. There are a couple ways to interpret this: for * instance, we could do it by removing a tab from the list when @@ -209,8 +204,6 @@ TabStore.prototype = { function TabTracker(name) { Tracker.call(this, name); - this.resetChanged(); - // Make sure "this" pointer is always set correctly for event listeners this.onTab = Utils.bind2(this, this.onTab); @@ -257,15 +250,10 @@ TabTracker.prototype = { onTab: function onTab(event) { this._log.trace(event.type); this.score += 1; - this._changedIDs[Clients.localID] = true; + this.addChangedID(Clients.localID); // Store a timestamp in the tab to track when it was last used Svc.Session.setTabValue(event.originalTarget, "weaveLastUsed", Math.floor(Date.now() / 1000)); }, - - get changedIDs() this._changedIDs, - - // Provide a way to empty out the changed ids - resetChanged: function resetChanged() this._changedIDs = {} } diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index e25db9b755c3..0b0b698c249b 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -308,6 +308,13 @@ ChannelListener.prototype = { onStartRequest: function Channel_onStartRequest(channel) { channel.QueryInterface(Ci.nsIHttpChannel); + + // Save the latest server timestamp when possible + try { + Resource.serverTime = channel.getResponseHeader("X-Weave-Timestamp") - 0; + } + catch(ex) {} + this._log.trace(channel.requestMethod + " " + channel.URI.spec); this._data = ''; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0a2435d2b639..48046a0bca46 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -885,7 +885,7 @@ WeaveSvc.prototype = { return false; } else if (meta.payload.syncID != this.syncID) { - this.resetService(); + this.resetClient(); this.syncID = meta.payload.syncID; this._log.debug("Clear cached values and take syncId: " + this.syncID); diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 301cc7b995bf..53c2c67b5343 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -68,6 +68,7 @@ function Tracker(name) { this._score = 0; this._ignored = []; this.ignoreAll = false; + this.changedIDs = {}; this.loadChangedIDs(); } Tracker.prototype = { @@ -95,29 +96,15 @@ Tracker.prototype = { this._score = 0; }, - /* - * Changed IDs are in an object (hash) to make it easy to check if - * one is already set or not. - * Note that it would be nice to make these methods asynchronous so - * as to not block when writing to disk. However, these will often - * get called from observer callbacks, and so it is better to make - * them synchronous. - */ - - get changedIDs() { - let items = {}; - this.__defineGetter__("changedIDs", function() items); - return items; - }, - saveChangedIDs: function T_saveChangedIDs() { - Utils.jsonSave("changes/" + this.file, this, this.changedIDs); + Utils.delay(function() { + Utils.jsonSave("changes/" + this.file, this, this.changedIDs); + }, 1000, this, "_lazySave"); }, loadChangedIDs: function T_loadChangedIDs() { Utils.jsonLoad("changes/" + this.file, this, function(json) { - for (let id in json) - this.changedIDs[id] = 1; + this.changedIDs = json; }); }, @@ -136,16 +123,22 @@ Tracker.prototype = { this._ignored.splice(index, 1); }, - addChangedID: function T_addChangedID(id) { + addChangedID: function addChangedID(id, when) { if (!id) { this._log.warn("Attempted to add undefined ID to tracker"); return false; } if (this.ignoreAll || (id in this._ignored)) return false; - if (!this.changedIDs[id]) { - this._log.trace("Adding changed ID " + id); - this.changedIDs[id] = true; + + // Default to the current time in seconds if no time is provided + if (when == null) + when = Math.floor(Date.now() / 1000); + + // Add/update the entry if we have a newer time + if ((this.changedIDs[id] || -Infinity) < when) { + this._log.trace("Adding changed ID: " + [id, when]); + this.changedIDs[id] = when; this.saveChangedIDs(); } return true; @@ -158,7 +151,7 @@ Tracker.prototype = { } if (this.ignoreAll || (id in this._ignored)) return false; - if (this.changedIDs[id]) { + if (this.changedIDs[id] != null) { this._log.trace("Removing changed ID " + id); delete this.changedIDs[id]; this.saveChangedIDs(); @@ -168,9 +161,7 @@ Tracker.prototype = { clearChangedIDs: function T_clearChangedIDs() { this._log.trace("Clearing changed ID list"); - for (let id in this.changedIDs) { - delete this.changedIDs[id]; - } + this.changedIDs = {}; this.saveChangedIDs(); } }; diff --git a/services/sync/tests/unit/test_tracker_addChanged.js b/services/sync/tests/unit/test_tracker_addChanged.js new file mode 100644 index 000000000000..7f4af1bf9875 --- /dev/null +++ b/services/sync/tests/unit/test_tracker_addChanged.js @@ -0,0 +1,25 @@ +Cu.import("resource://weave/trackers.js"); + +function run_test() { + let tracker = new Tracker(); + let id = "the_id!"; + + _("Make sure nothing exists yet.."); + do_check_eq(tracker.changedIDs[id], null); + + _("Make sure adding of time 0 works"); + tracker.addChangedID(id, 0); + do_check_eq(tracker.changedIDs[id], 0); + + _("A newer time will replace the old 0"); + tracker.addChangedID(id, 10); + do_check_eq(tracker.changedIDs[id], 10); + + _("An older time will not replace the newer 10"); + tracker.addChangedID(id, 5); + do_check_eq(tracker.changedIDs[id], 10); + + _("Adding without time defaults to current time"); + tracker.addChangedID(id); + do_check_true(tracker.changedIDs[id] > 10); +} From 8bee415f2e3b6b0bb60e784e11554b6f8fd8f8db Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 31 Mar 2010 21:58:07 -0400 Subject: [PATCH 1610/1860] bug 545725 - Changing passphrase should prevent other clients from syncing, r=Mardak --- services/sync/locales/en-US/fx-prefs.dtd | 3 +- .../locales/en-US/generic-change.properties | 44 ++++++++--------- services/sync/locales/en-US/sync.properties | 6 ++- services/sync/modules/constants.js | 7 ++- services/sync/modules/service.js | 48 ++++++------------- 5 files changed, 42 insertions(+), 66 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 9405945be317..8be31f156147 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -53,7 +53,7 @@ - + @@ -87,7 +87,6 @@ - diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties index e97277727a77..26eb6a319260 100644 --- a/services/sync/locales/en-US/generic-change.properties +++ b/services/sync/locales/en-US/generic-change.properties @@ -1,33 +1,29 @@ -noPassword.alert = You must enter a password. -noPassphrase.alert = You must enter a passphrase. -passwordNoMatch.alert = Your passwords do not match. Try again! -passphraseNoMatch.alert = Your passphrases do not match. Try again! - -incorrectPassword.alert = Your current password is incorrect! -incorrectPassphrase.alert = Your current passphrase is incorrect! - change.password.title = Change your Password +change.password.acceptButton = Change Password change.password.status.active = Changing your password… change.password.status.success = Your password has been changed. change.password.status.error = There was an error changing your password. -change.password.status.passwordSameAsPassphrase = The password cannot be the same as the passphrase. -change.password.status.passwordSameAsUsername = The password cannot be the same as the username. -change.password.status.passwordsDoNotMatch = The passwords you entered do not match. -change.password.status.badOldPassword = Your current password is incorrect. +change.password.status.pwSameAsPassphrase = The password cannot be the same as your secret phrase. +change.password.status.pwSameAsUsername = The password cannot be the same as the username. -change.passphrase.title = Change your Passphrase -change.passphrase.label = Changing passphrase, please wait… -change.passphrase.error = There was an error while changing your passphrase! -change.passphrase.success = Your passphrase was successfully changed! +change.password.introText = Your password must be at least 12 characters long. It cannot be the same as either your user name or your secret phrase. +change.password.warningText = Note: All of your other devices will be unable to connect to your account once you change this password. -reset.passphrase.title = Reset your Passphrase -reset.passphrase.label = Resetting passphrase, please wait… -reset.passphrase.error = There was an error while resetting your passphrase! -reset.passphrase.success = Your passphrase was successfully reset! -new.passphrase.old = Enter your current passphrase -new.passphrase.label = Enter your new passphrase -new.passphrase.confirm = Confirm your new passphrase -new.password.old = Enter your current password +change.passphrase.title = Change your Secret Phrase +change.passphrase.acceptButton = Change Secret Phrase +change.passphrase.label = Changing secret phrase and uploading local data, please wait… +change.passphrase.error = There was an error while changing your secret phrase! +change.passphrase.success = Your secret phrase was successfully changed! +change.passphrase.status.ppSameAsPassword = The secret phrase cannot be the same as your password. +change.passphrase.status.ppSameAsUsername = The secret phrase cannot be the same as the username. + +new.passphrase.label = New secret phrase +new.passphrase.confirm = Confirm secret phrase + +change.passphrase.introText = Your secret phrase must be at least 12 characters long. Weave uses this phrase as part of encrypting your data. +change.passphrase.introText2 = You may wish to write this down, as this is never sent over the Internet and is not backed up or synced by Weave for your security. +change.passphrase.warningText = Note: This will erase all data stored on the Weave server and upload new data secured by this phrase. Your other devices will not sync until the secret phrase is entered for that device. + new.password.label = Enter your new password new.password.confirm = Confirm your new password diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 5836d0e378f9..4e1675a10215 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -24,10 +24,12 @@ remote.opened.label = All remote tabs are already open remote.notification.label = Recent desktop tabs will be available once they sync error.login.title = Error While Signing In -error.login.description = Weave encountered an error while signing you in: %1$S. Please try again. +error.login.description = Weave encountered an error while connecting: %1$S. Please try again. +error.login.prefs.label = Preferences… +error.login.prefs.accesskey = P # should decide if we're going to show this error.logout.title = Error While Signing Out -error.logout.description = Weave encountered an error while signing you out. It's probably ok, and you don't have to do anything about it. +error.logout.description = Weave encountered an error while connecting. It's probably ok, and you don't have to do anything about it. error.sync.title = Error While Syncing error.sync.description = Weave encountered an error while syncing: %1$S. Weave will automatically retry this action. error.sync.no_node_found = The Weave server is a little busy right now, but you don't need to do anything about it. We'll start syncing your data as soon as we can! diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index d3bae0dd072f..885a6ecb1b4f 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -107,6 +107,7 @@ KEYS_DOWNLOAD_FAIL: "error.sync.reason.keys_download_fail", NO_KEYS_NO_KEYGEN: "error.sync.reason.no_keys_no_keygen", KEYS_UPLOAD_FAIL: "error.sync.reason.keys_upload_fail", SETUP_FAILED_NO_PASSPHRASE: "error.sync.reason.setup_failed_no_passphrase", +CREDENTIALS_CHANGED: "error.sync.reason.credentials_changed", ABORT_SYNC_COMMAND: "aborting sync, process commands said so", NO_SYNC_NODE_FOUND: "error.sync.reason.no_node_found", @@ -128,9 +129,7 @@ FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", FENNEC_ID: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}", SEAMONKEY_ID: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", -// UI constants - -// How many data types (bookmarks, history, etc) to display per row -UI_DATA_TYPES_PER_ROW: 3, +MIN_PP_LENGTH: 12, +MIN_PASS_LENGTH: 8 }))]; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 48046a0bca46..8c44b5b44a36 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -387,6 +387,10 @@ WeaveSvc.prototype = { break; case "weave:service:sync:error": this._handleSyncError(); + if (Status.sync == CREDENTIALS_CHANGED) { + this.logout(); + Utils.delay(function() this.login(), 0, this); + } break; case "weave:service:sync:finish": this._scheduleNextSync(); @@ -557,29 +561,6 @@ WeaveSvc.prototype = { } }))(), - changePassphrase: function WeaveSvc_changePassphrase(newphrase) - this._catch(this._notify("changepph", "", function() { - let pubkey = PubKeys.getDefaultKey(); - let privkey = PrivKeys.get(pubkey.privateKeyUri); - - /* Re-encrypt with new passphrase. - * FIXME: verifyPassphrase first! - */ - let newkey = Svc.Crypto.rewrapPrivateKey(privkey.payload.keyData, - this.passphrase, privkey.payload.salt, - privkey.payload.iv, newphrase); - privkey.payload.keyData = newkey; - - let resp = new Resource(privkey.uri).put(privkey); - if (!resp.success) - throw resp; - - // Save the new passphrase to the login manager for it to sync - this.passphrase = newphrase; - this.persistLogin(); - return true; - }))(), - changePassword: function WeaveSvc_changePassword(newpass) this._notify("changepwd", "", function() { let url = this.userAPI + this.username + "/password"; @@ -602,28 +583,20 @@ WeaveSvc.prototype = { return true; })(), - resetPassphrase: function WeaveSvc_resetPassphrase(newphrase) - this._catch(this._notify("resetpph", "", function() { - /* Make remote commands ready so we have a list of clients beforehand */ - this.prepCommand("logout", []); - let clientsBackup = Clients._store.clients; - + changePassphrase: function WeaveSvc_changePassphrase(newphrase) + this._catch(this._notify("changepph", "", function() { /* Wipe */ this.wipeServer(); PubKeys.clearCache(); PrivKeys.clearCache(); - /* Set remote commands before syncing */ - Clients._store.clients = clientsBackup; - let username = this.username; - let password = this.password; this.logout(); /* Set this so UI is updated on next run */ this.passphrase = newphrase; /* Login in sync: this also generates new keys */ - this.login(username, password, newphrase); + this.login(); this.sync(true); return true; }))(), @@ -892,6 +865,13 @@ WeaveSvc.prototype = { // XXX Bug 531005 Wait long enough to allow potentially another concurrent // sync to finish generating the keypair and uploading them Sync.sleep(15000); + + // bug 545725 - re-verify creds and fail sanely + if (!this._verifyLogin()) { + Status.sync = CREDENTIALS_CHANGED; + this._log.info("Credentials have changed, aborting sync and forcing re-login."); + return false; + } } let needKeys = true; From b1e2b85f0c66cee11e5f040481278529ccd34a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Fri, 2 Apr 2010 16:30:09 -0700 Subject: [PATCH 1611/1860] Bug 554936 - Make statusbar text "Set Up Weave..." until Weave has been configured [r=Mardak] * Added a "Set Up Weave..." status message which is determined by Status.login states * The "Set Up Weave..." message opens the pref window directly instead of showing the menu * Added an additional error state for NO_PASSPHRASE * Added checks to onStartup to check some status (no username/password/passphrase) for when autoconnect=false * When the prefpane is opened and there's an error, it will open to the correct page so that can be corrected * If using a custom server, that will be reflected when shown that page in the prefpane --- services/sync/locales/en-US/errors.properties | 1 + services/sync/locales/en-US/sync.properties | 1 + services/sync/modules/constants.js | 1 + services/sync/modules/service.js | 28 ++++++++++++++----- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties index 0ccb8df1a29d..7995fe806002 100644 --- a/services/sync/locales/en-US/errors.properties +++ b/services/sync/locales/en-US/errors.properties @@ -2,6 +2,7 @@ error.login.reason.network = Failed to connect to the server error.login.reason.passphrase = Wrong secret phrase error.login.reason.password = Incorrect username or password error.login.reason.no_password= No saved password to use +error.login.reason.no_passphrase= No saved secret phrase to use error.login.reason.server = Server incorrectly configured error.sync.failed_partial = One or more data types could not be synced diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 4e1675a10215..008a01225eb2 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -15,6 +15,7 @@ syncing.label = Weave is syncing: %S status.offline = Not Signed In status.privateBrowsing = Disabled (Private Browsing) +status.needsSetup = Set Up Weave… mobile.label = Mobile Bookmarks diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 885a6ecb1b4f..b1da6a292e00 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -94,6 +94,7 @@ ENGINE_SUCCEEDED: "success.engine", // login failure status codes: LOGIN_FAILED_NO_USERNAME: "error.login.reason.no_username", LOGIN_FAILED_NO_PASSWORD: "error.login.reason.no_password", +LOGIN_FAILED_NO_PASSPHRASE: "error.login.reason.no_passphrase", LOGIN_FAILED_NETWORK_ERROR: "error.login.reason.network", LOGIN_FAILED_SERVER_ERROR: "error.login.reason.server", LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.passphrase", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8c44b5b44a36..7643669db308 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -287,6 +287,9 @@ WeaveSvc.prototype = { // Send an event now that Weave service is ready Svc.Obs.notify("weave:service:ready"); + // Do some preliminary Status setting, in case autoconnect is false. + this._setBasicLoginStatus(); + // Wait a little before checking how long to wait to autoconnect if (Svc.Prefs.get("autoconnect")) { Utils.delay(function() { @@ -612,6 +615,8 @@ WeaveSvc.prototype = { }, startOver: function() { + // Set a username error so the status message shows "set up..." + Status.login = Weave.LOGIN_FAILED_NO_USERNAME; this.logout(); // Reset all engines this.resetClient(); @@ -662,6 +667,17 @@ WeaveSvc.prototype = { return true; }, + _setBasicLoginStatus: function _setBasicLoginStatus() { + // Some funky logic to make sure if there's a MP, we don't give the error + // until we actually try to login. + if (!this.username) + Status.login = Weave.LOGIN_FAILED_NO_USERNAME; + else if (!(this._mpLocked() || this.password)) + Status.login = Weave.LOGIN_FAILED_NO_PASSWORD; + else if (!(this._mpLocked() || this.passphrase)) + Status.login = Weave.LOGIN_FAILED_NO_PASSPHRASE; + }, + persistLogin: function persistLogin() { // Canceled master password prompt can prevent these from succeeding try { @@ -684,18 +700,16 @@ WeaveSvc.prototype = { if (passphrase) this.passphrase = passphrase; - if (!this.username) { - Status.login = LOGIN_FAILED_NO_USERNAME; + this._setBasicLoginStatus(); + if (!this.username) throw "No username set, login failed"; - } - if (!this.password) { - Status.login = LOGIN_FAILED_NO_PASSWORD; + if (!this.password) throw "No password given or found in password manager"; - } this._log.info("Logging in user " + this.username); if (!this._verifyLogin()) { - // verifyLogin sets the failure states here + // verifyLogin sets the failure states here. They might also be set by + // _setBasicLoginStatus for ERROR_FAILED_NO_* throw "Login failed: " + Status.login; } From 0c57046c0c6719f777aafe390d8697034a08a429 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 2 Apr 2010 16:37:53 -0700 Subject: [PATCH 1612/1860] Replace tabs in files under source/ with appropriate number of spaces. --- services/sync/modules/engines/bookmarks.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index d12079518f0f..88cf2553f7fe 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -493,15 +493,15 @@ BookmarksStore.prototype = { this._log.debug(" \-> is a microsummary"); Utils.anno(newId, "bookmarks/staticTitle", record.staticTitle || ""); let genURI = Utils.makeURI(record.generatorUri); - if (this._ms) { + if (this._ms) { try { let micsum = this._ms.createMicrosummary(uri, genURI); this._ms.setMicrosummary(newId, micsum); } catch(ex) { /* ignore "missing local generator" exceptions */ } - } else { - this._log.warn("Can't create microsummary -- not supported."); - } + } + else + this._log.warn("Can't create microsummary -- not supported."); } } break; case "folder": @@ -620,12 +620,12 @@ BookmarksStore.prototype = { try { let micsumURI = this._bms.getBookmarkURI(itemId); let genURI = Utils.makeURI(val); - if (this._ms == SERVICE_NOT_SUPPORTED) { - this._log.warn("Can't create microsummary -- not supported."); - } else { + if (this._ms == SERVICE_NOT_SUPPORTED) + this._log.warn("Can't create microsummary -- not supported."); + else { let micsum = this._ms.createMicrosummary(micsumURI, genURI); this._ms.setMicrosummary(itemId, micsum); - } + } } catch (e) { this._log.debug("Could not set microsummary generator URI: " + e); } From cb1048b80fe4bd3c617fb41c8e0773f832d54a2c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 2 Apr 2010 16:38:05 -0700 Subject: [PATCH 1613/1860] Remove trailing spaces in files under source/. --- services/sync/modules/service.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7643669db308..0f9f0ff04f01 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -879,7 +879,7 @@ WeaveSvc.prototype = { // XXX Bug 531005 Wait long enough to allow potentially another concurrent // sync to finish generating the keypair and uploading them Sync.sleep(15000); - + // bug 545725 - re-verify creds and fail sanely if (!this._verifyLogin()) { Status.sync = CREDENTIALS_CHANGED; @@ -1063,7 +1063,7 @@ WeaveSvc.prototype = { /** * Hits info/collections on the server to see if there are new clients. - * This is only called when the account has one active client, and if more + * This is only called when the account has one active client, and if more * are found will trigger a sync to change client sync frequency and update data. */ @@ -1077,7 +1077,7 @@ WeaveSvc.prototype = { if (info && info.success) { // if clients.lastModified doesn't match what the server has, // we have another client in play - this._log.trace("Remote timestamp:" + info.obj["clients"] + + this._log.trace("Remote timestamp:" + info.obj["clients"] + " Local timestamp: " + Clients.lastSync); if (info.obj["clients"] > Clients.lastSync) { this._log.debug("New clients detected, triggering a full sync"); @@ -1092,7 +1092,7 @@ WeaveSvc.prototype = { } catch(ex) { // if something throws unexpectedly, not a big deal this._log.debug("Heartbeat failed unexpectedly: " + ex); - } + } // no joy, schedule the next heartbeat this._scheduleHeartbeat(); @@ -1100,7 +1100,7 @@ WeaveSvc.prototype = { /** * Sets up a heartbeat ping to check for new clients. This is not a critical - * behaviour for the client, so if we hit server/network issues, we'll just drop + * behaviour for the client, so if we hit server/network issues, we'll just drop * this until the next sync. */ _scheduleHeartbeat: function WeaveSvc__scheduleNextHeartbeat() { @@ -1109,7 +1109,7 @@ WeaveSvc.prototype = { Status.enforceBackoff) return; - this._log.trace("Setting up heartbeat, next ping in " + + this._log.trace("Setting up heartbeat, next ping in " + Math.ceil(interval / 1000) + " sec."); Utils.delay(function() this._doHeartbeat(), interval, this, "_heartbeatTimer"); }, @@ -1178,7 +1178,7 @@ WeaveSvc.prototype = { this._clearSyncTriggers(); this.nextSync = 0; - // reset backoff info, if the server tells us to continue backing off, + // reset backoff info, if the server tells us to continue backing off, // we'll handle that later Status.resetBackoff(); From 41dd2b923272662b63de915758bde741b795984d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 6 Apr 2010 11:59:50 -0700 Subject: [PATCH 1614/1860] Bug 557503 - bookmark restore from backup and server-wins interact badly [r=mconnor] Don't resetLastSync as that will make local changes look old and instead mark each as changed now that addChangedID doesn't jsonSave on each call. --- services/sync/modules/engines/bookmarks.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 88cf2553f7fe..58e18356b933 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -108,9 +108,12 @@ BookmarksEngine.prototype = { }, this); Observers.add("bookmarks-restore-success", function() { - this._log.debug("Triggering fresh start on successful import"); - this.resetLastSync(); + this._log.debug("Tracking all items on successful import"); this._tracker.ignoreAll = false; + + // Mark all the items as changed so they get uploaded + for (let id in this._store.getAllIDs()) + this._tracker.addChangedID(id); }, this); Observers.add("bookmarks-restore-failed", function() { From e9717efb2514e89b9731d86631bbcded62995fae Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 6 Apr 2010 12:07:34 -0700 Subject: [PATCH 1615/1860] Bug 549789 - Point user/misc urls to 1.0/ instead of 1/ [r=mconnor] Fix up both url getters to have the extra dot oh. --- services/sync/modules/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0f9f0ff04f01..6edfbfec7bbe 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -154,7 +154,7 @@ WeaveSvc.prototype = { let misc = Svc.Prefs.get("miscURL"); if (misc.indexOf(":") == -1) misc = this.serverURL + misc; - return misc + "1/"; + return misc + "1.0/"; }, get userAPI() { @@ -162,7 +162,7 @@ WeaveSvc.prototype = { let user = Svc.Prefs.get("userURL"); if (user.indexOf(":") == -1) user = this.serverURL + user; - return user + "1/"; + return user + "1.0/"; }, get syncID() { From af5b5e1ad6a993e01e28d0d2c77c854d50360f9c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 6 Apr 2010 14:13:21 -0700 Subject: [PATCH 1616/1860] Bug 557623 - Tab sync broken on fennec since session-store was added [r=zpao] Check for existance of both cid and iface in Cc/Ci to decide to use the platform's service or fake one. Remove unused lazyInstance to clean up API (iface == string not object). --- services/sync/modules/util.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 63728a949fdc..097e8c7abf47 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -323,8 +323,10 @@ let Utils = { delete dest[prop]; let svc = null; - // Try creating a fake service if we can handle that - if (!Cc[cid]) { + // Use the platform's service if it exists + if (cid in Cc && iface in Ci) + svc = Cc[cid].getService(Ci[iface]); + else { svc = FakeSvc[cid]; let log = Log4Moz.repository.getLogger("Service.Util"); @@ -333,23 +335,12 @@ let Utils = { else log.debug("Using a fake svc object for " + cid); } - else - svc = Cc[cid].getService(iface); return dest[prop] = svc; }; dest.__defineGetter__(prop, getter); }, - lazyInstance: function Weave_lazyInstance(dest, prop, cid, iface) { - let getter = function() { - delete dest[prop]; - dest[prop] = Cc[cid].createInstance(iface); - return dest[prop]; - }; - dest.__defineGetter__(prop, getter); - }, - lazyStrings: function Weave_lazyStrings(name) { let bundle = "chrome://weave/locale/" + name + ".properties"; return function() new StringBundle(bundle); @@ -880,7 +871,7 @@ Svc.Obs = Observers; ["WinMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"], ["WinWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"], ["Session", "@mozilla.org/browser/sessionstore;1", "nsISessionStore"], -].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], Ci[lazy[2]])); +].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], lazy[2])); let Str = {}; ["engines", "errors", "sync"] From 6098dd49b65f124cc2252d57e46e0eff5691986f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 6 Apr 2010 16:49:47 -0700 Subject: [PATCH 1617/1860] Bug 543858 - Weave's change-password/passphrase dialogs allow me to leave password/passphrase unchanged [r=mconnor] Check if the new value is the same as the current one and provide a warning. --- services/sync/locales/en-US/generic-change.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties index 26eb6a319260..97e1e65c85b3 100644 --- a/services/sync/locales/en-US/generic-change.properties +++ b/services/sync/locales/en-US/generic-change.properties @@ -4,6 +4,7 @@ change.password.status.active = Changing your password… change.password.status.success = Your password has been changed. change.password.status.error = There was an error changing your password. change.password.status.pwSameAsPassphrase = The password cannot be the same as your secret phrase. +change.password.status.pwSameAsPassword = The password cannot be the same as your current password. change.password.status.pwSameAsUsername = The password cannot be the same as the username. change.password.introText = Your password must be at least 12 characters long. It cannot be the same as either your user name or your secret phrase. @@ -15,6 +16,7 @@ change.passphrase.acceptButton = Change Secret Phrase change.passphrase.label = Changing secret phrase and uploading local data, please wait… change.passphrase.error = There was an error while changing your secret phrase! change.passphrase.success = Your secret phrase was successfully changed! +change.passphrase.status.ppSameAsPassphrase = The secret phrase cannot be the same as your current secret phrase. change.passphrase.status.ppSameAsPassword = The secret phrase cannot be the same as your password. change.passphrase.status.ppSameAsUsername = The secret phrase cannot be the same as the username. From b5a012c909d85400f39c4e35ea1254c8d0ebbfd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Tue, 6 Apr 2010 16:50:46 -0700 Subject: [PATCH 1618/1860] Bug 557314 - Need cancel button after clicking Reset Sync [r=mconnor] Added a cancel button that only shows on the sync options page when resetting. Also removed the pref usage, so that resetting is properly cancelable (by pressing cancel, closing the window, or quitting). --- services/sync/locales/en-US/fx-prefs.dtd | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 8be31f156147..56a4d13e9cb0 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -88,6 +88,7 @@ + From 6327af78d8a04a8fa8e0e89904d4e0ab45511d9a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 7 Apr 2010 17:52:22 -0700 Subject: [PATCH 1619/1860] Bug 557891 - Wipe local triggers deletions on all other clients! [r=mconnor] Ignore tracker changes triggered by wiping the local client store. --- services/sync/modules/engines.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 17cd50e85a11..9e70909c1672 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -281,7 +281,9 @@ Engine.prototype = { _wipeClient: function Engine__wipeClient() { this.resetClient(); this._log.debug("Deleting all local data"); + this._tracker.ignoreAll = true; this._store.wipe(); + this._tracker.ignoreAll = false; }, wipeClient: function Engine_wipeClient() { From d18c5611a601f5c0c4ff01476b85426bb4d66980 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 8 Apr 2010 21:57:21 -0400 Subject: [PATCH 1620/1860] bug 558209 - Change password text says minimum is 12 characters --- services/sync/locales/en-US/generic-change.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties index 97e1e65c85b3..a5051e8ee063 100644 --- a/services/sync/locales/en-US/generic-change.properties +++ b/services/sync/locales/en-US/generic-change.properties @@ -7,7 +7,7 @@ change.password.status.pwSameAsPassphrase = The password cannot be the same as y change.password.status.pwSameAsPassword = The password cannot be the same as your current password. change.password.status.pwSameAsUsername = The password cannot be the same as the username. -change.password.introText = Your password must be at least 12 characters long. It cannot be the same as either your user name or your secret phrase. +change.password.introText = Your password must be at least 8 characters long. It cannot be the same as either your user name or your secret phrase. change.password.warningText = Note: All of your other devices will be unable to connect to your account once you change this password. From 6a982393d5b43925808e4a73a87937b3aba02841 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 9 Apr 2010 10:20:58 -0700 Subject: [PATCH 1621/1860] Bug 558264 - Form data fails to sync when there's nothing to upload [r=mconnor] Wrap the createStatement calls to guard against missing guid columns and lazily add it. --- services/sync/modules/engines/forms.js | 48 ++++++++++++++------------ 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 13e08c22fab8..2a38090c5f5e 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -50,7 +50,7 @@ Cu.import("resource://weave/type_records/forms.js"); let FormWrapper = { getAllEntries: function getAllEntries() { let entries = []; - let query = Svc.Form.DBConnection.createStatement( + let query = this.createStatement( "SELECT fieldname, value FROM moz_formhistory"); while (query.executeStep()) { entries.push({ @@ -62,7 +62,7 @@ let FormWrapper = { }, getEntry: function getEntry(guid) { - let query = Svc.Form.DBConnection.createStatement( + let query = this.createStatement( "SELECT fieldname, value FROM moz_formhistory WHERE guid = :guid"); query.params.guid = guid; if (!query.executeStep()) @@ -75,24 +75,10 @@ let FormWrapper = { }, getGUID: function getGUID(name, value) { - let getQuery = "SELECT guid FROM moz_formhistory " + - "WHERE fieldname = :name AND value = :value"; - try { - getQuery = Svc.Form.DBConnection.createStatement(getQuery); - } - catch(ex) { - // The column must not exist yet, so add it with an index - Svc.Form.DBConnection.executeSimpleSQL( - "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT"); - Svc.Form.DBConnection.executeSimpleSQL( - "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index " + - "ON moz_formhistory (guid)"); - - // Try creating the query now that the column exists - getQuery = Svc.Form.DBConnection.createStatement(getQuery); - } - // Query for the provided entry + let getQuery = this.createStatement( + "SELECT guid FROM moz_formhistory " + + "WHERE fieldname = :name AND value = :value"); getQuery.params.name = name; getQuery.params.value = value; getQuery.executeStep(); @@ -102,7 +88,7 @@ let FormWrapper = { return getQuery.row.guid; // We need to create a guid for this entry - let setQuery = Svc.Form.DBConnection.createStatement( + let setQuery = this.createStatement( "UPDATE moz_formhistory SET guid = :guid " + "WHERE fieldname = :name AND value = :value"); let guid = Utils.makeGUID(); @@ -115,18 +101,36 @@ let FormWrapper = { }, hasGUID: function hasGUID(guid) { - let query = Svc.Form.DBConnection.createStatement( + let query = this.createStatement( "SELECT 1 FROM moz_formhistory WHERE guid = :guid"); query.params.guid = guid; return query.executeStep(); }, replaceGUID: function replaceGUID(oldGUID, newGUID) { - let query = Svc.Form.DBConnection.createStatement( + let query = this.createStatement( "UPDATE moz_formhistory SET guid = :newGUID WHERE guid = :oldGUID"); query.params.oldGUID = oldGUID; query.params.newGUID = newGUID; query.execute(); + }, + + createStatement: function createStatement(query) { + try { + // Just return the statement right away if it's okay + return Svc.Form.DBConnection.createStatement(query); + } + catch(ex) { + // Assume guid column must not exist yet, so add it with an index + Svc.Form.DBConnection.executeSimpleSQL( + "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT"); + Svc.Form.DBConnection.executeSimpleSQL( + "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index " + + "ON moz_formhistory (guid)"); + + // Try creating the query now that the column exists + return Svc.Form.DBConnection.createStatement(query); + } } }; From c9ebf3a97ac15794644fb2b5090bb78aedbff533 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Sun, 11 Apr 2010 12:34:27 -0700 Subject: [PATCH 1622/1860] Bug 558654 - Firefox crashes every ~2min after update to 1.2 [r=mconnor] Limit the number of initial form entries to 500 ordered by "frecency". --- services/sync/modules/engines/forms.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 2a38090c5f5e..11acd44db366 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -50,8 +50,13 @@ Cu.import("resource://weave/type_records/forms.js"); let FormWrapper = { getAllEntries: function getAllEntries() { let entries = []; + // Sort by (lastUsed - minLast) / (maxLast - minLast) * timesUsed / maxTimes let query = this.createStatement( - "SELECT fieldname, value FROM moz_formhistory"); + "SELECT fieldname, value FROM moz_formhistory " + + "ORDER BY 1.0 * (lastUsed - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) / " + + "((SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed DESC LIMIT 1) - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) * " + + "timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) DESC " + + "LIMIT 500"); while (query.executeStep()) { entries.push({ name: query.row.fieldname, From 3cfe77d583200deb3dc92fe74275ae9342e6009c Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 5 Apr 2010 23:53:31 -0400 Subject: [PATCH 1623/1860] bug 551572 - 100% CPU when sitting on merge-choice screen, r=Mardak --- services/sync/modules/constants.js | 1 + services/sync/modules/service.js | 39 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index b1da6a292e00..e75a0e51e166 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -124,6 +124,7 @@ kSyncNotLoggedIn: "User is not logged in", kSyncNetworkOffline: "Network is offline", kSyncInPrivateBrowsing: "Private browsing is enabled", kSyncBackoffNotMet: "Trying to sync before the server said it's okay", +kFirstSyncChoiceNotMade: "User has not selected an action for first sync", // Application IDs FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 6edfbfec7bbe..f5e14e6eeb7f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -968,6 +968,8 @@ WeaveSvc.prototype = { else if (Svc.Private && Svc.Private.privateBrowsingEnabled) // Svc.Private doesn't exist on Fennec -- don't assume it's there. reason = kSyncInPrivateBrowsing; + else if (Svc.Prefs.get("firstSync") == "notReady") + reason = kFirstSyncChoiceNotMade; else if (Status.minimumNextSync > Date.now()) reason = kSyncBackoffNotMet; @@ -1144,26 +1146,8 @@ WeaveSvc.prototype = { */ sync: function sync() this._catch(this._lock(this._notify("sync", "", function() { - Status.resetSync(); - if (Svc.Prefs.isSet("firstSync")) { - switch(Svc.Prefs.get("firstSync")) { - case "wipeClient": - this.wipeClient(); - break; - case "wipeRemote": - this.wipeRemote(Engines.getAll().map(function(e) e.name)); - break; - default: - this._scheduleNextSync(); - return; - } - } - // if we don't have a node, get one. if that fails, retry in 10 minutes - if (this.clusterURL == "" && !this._setCluster()) { - this._scheduleNextSync(10 * 60 * 1000); - return; - } + Status.resetSync(); // Make sure we should sync or record why we shouldn't let reason = this._checkSync(); @@ -1174,6 +1158,23 @@ WeaveSvc.prototype = { throw reason; } + // if we don't have a node, get one. if that fails, retry in 10 minutes + if (this.clusterURL == "" && !this._setCluster()) { + this._scheduleNextSync(10 * 60 * 1000); + return; + } + + if (Svc.Prefs.isSet("firstSync")) { + switch(Svc.Prefs.get("firstSync")) { + case "wipeClient": + this.wipeClient(); + break; + case "wipeRemote": + this.wipeRemote(Engines.getAll().map(function(e) e.name)); + break; + } + } + // Clear out any potentially pending syncs now that we're syncing this._clearSyncTriggers(); this.nextSync = 0; From 0c2d221bb08668fbbdbac89bbe31f46d90e35e3c Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 7 Apr 2010 20:06:37 -0400 Subject: [PATCH 1624/1860] bug 496485 - make Service.wipeServer work properly, r=Mardak --- services/sync/modules/engines.js | 11 ----------- services/sync/modules/service.js | 23 ++++++++++++----------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 9e70909c1672..67ebd013384a 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -262,12 +262,6 @@ Engine.prototype = { } }, - wipeServer: function Engine_wipeServer() { - if (!this._wipeServer) - throw "engine does not implement _wipeServer method"; - this._notify("wipe-server", this.name, this._wipeServer)(); - }, - /** * Get rid of any local meta-data */ @@ -737,11 +731,6 @@ SyncEngine.prototype = { } }, - _wipeServer: function SyncEngine__wipeServer() { - new Resource(this.engineURL).delete(); - new Resource(this.cryptoMetaURL).delete(); - }, - _testDecrypt: function _testDecrypt() { // Report failure even if there's nothing to decrypt let canDecrypt = false; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index f5e14e6eeb7f..c342aadcced1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1351,22 +1351,23 @@ WeaveSvc.prototype = { }, /** - * Wipe all user data from the server. + * Wipe user data from the server. * - * @param engines [optional] - * Array of engine names to wipe. If not given, all engines are used. + * @param collections [optional] + * Array of collections to wipe. If not given, all collections are wiped. */ - wipeServer: function WeaveSvc_wipeServer(engines) + wipeServer: function WeaveSvc_wipeServer(collections) this._catch(this._notify("wipe-server", "", function() { - // Grab all the collections for the user and delete each one - let info = new Resource(this.infoURL).get(); - for (let name in info.obj) { + if (!collections) { + collections = []; + let info = new Resource(this.infoURL).get(); + for (let name in info.obj) + collections.push(name); + } + for each (let name in collections) { try { - // If we have a list of engines, make sure it's one we want - if (engines && engines.indexOf(name) == -1) - continue; - new Resource(this.storageURL + name).delete(); + new Resource(this.storageURL + "crypto/" + name).delete(); } catch(ex) { this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); From c15c7359ae1adf64d947c733d7c540c3142a99ac Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 8 Apr 2010 17:03:10 -0400 Subject: [PATCH 1625/1860] bug 556683 - password reset URL is hardcoded, r=Mardak --- services/sync/locales/en-US/fx-prefs.dtd | 2 +- services/sync/modules/service.js | 14 ++++---------- services/sync/services-sync.js | 1 - 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 56a4d13e9cb0..9941413dbb13 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -85,7 +85,7 @@ - + diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c342aadcced1..a52957ec807a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -165,6 +165,10 @@ WeaveSvc.prototype = { return user + "1.0/"; }, + get pwResetURL() { + return this.serverURL + "weave-password-reset"; + }, + get syncID() { // Generate a random syncID id we don't have one let syncID = Svc.Prefs.get("client.syncID", ""); @@ -604,16 +608,6 @@ WeaveSvc.prototype = { return true; }))(), - requestPasswordReset: function WeaveSvc_requestPasswordReset(username) { - let res = new Resource(Svc.Prefs.get("pwChangeURL")); - res.authenticator = new NoOpAuthenticator(); - res.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - let ret = res.post('uid=' + username); - if (ret.indexOf("Further instructions have been sent") >= 0) - return true; - return false; - }, - startOver: function() { // Set a username error so the status message shows "set up..." Status.login = Weave.LOGIN_FAILED_NO_USERNAME; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 8224483a8b5e..dc3e0d9c7e25 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -2,7 +2,6 @@ pref("extensions.weave.serverURL", "@server_url@"); pref("extensions.weave.storageAPI", "1.0"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); -pref("extensions.weave.pwChangeURL", "https://auth.services.mozilla.com/weave-password-reset"); pref("extensions.weave.termsURL", "https://mozillalabs.com/weave/tos/"); pref("extensions.weave.lastversion", "firstrun"); From 9cdc29b86bcafe502912eb6662327ec1c16811e8 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 8 Apr 2010 17:03:10 -0400 Subject: [PATCH 1626/1860] bug 556710 - Make mpLocked part of Utils, r=Mardak --- services/sync/modules/service.js | 23 +++-------------------- services/sync/modules/util.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index a52957ec807a..b2e5e6ffc112 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -624,7 +624,7 @@ WeaveSvc.prototype = { _autoConnect: let (attempts = 0) function _autoConnect() { let reason = ""; - if (this._mpLocked()) + if (Utils.mpLocked()) reason = "master password still locked"; // Can't autoconnect if we're missing these values @@ -644,31 +644,14 @@ WeaveSvc.prototype = { Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer"); }, - _mpLocked: function _mpLocked() { - let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]. - getService(Ci.nsIPKCS11ModuleDB); - let sdrSlot = modules.findSlotByName(""); - let status = sdrSlot.status; - let slots = Ci.nsIPKCS11Slot; - - if (status == slots.SLOT_READY || status == slots.SLOT_LOGGED_IN) - return false; - - if (status == slots.SLOT_NOT_LOGGED_IN) - return true; - - this._log.debug("something wacky happened, pretending MP is locked"); - return true; - }, - _setBasicLoginStatus: function _setBasicLoginStatus() { // Some funky logic to make sure if there's a MP, we don't give the error // until we actually try to login. if (!this.username) Status.login = Weave.LOGIN_FAILED_NO_USERNAME; - else if (!(this._mpLocked() || this.password)) + else if (!(Utils.mpLocked() || this.password)) Status.login = Weave.LOGIN_FAILED_NO_PASSWORD; - else if (!(this._mpLocked() || this.passphrase)) + else if (!(Utils.mpLocked() || this.passphrase)) Status.login = Weave.LOGIN_FAILED_NO_PASSPHRASE; }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 097e8c7abf47..574168d17f2e 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -769,6 +769,23 @@ let Utils = { return function innerBind() { return method.apply(object, arguments); }; }, + mpLocked: function mpLocked() { + let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"]. + getService(Ci.nsIPKCS11ModuleDB); + let sdrSlot = modules.findSlotByName(""); + let status = sdrSlot.status; + let slots = Ci.nsIPKCS11Slot; + + if (status == slots.SLOT_READY || status == slots.SLOT_LOGGED_IN) + return false; + + if (status == slots.SLOT_NOT_LOGGED_IN) + return true; + + // something wacky happened, pretend MP is locked + return true; + }, + __prefs: null, get prefs() { if (!this.__prefs) { From a3380cee1343464547865c8ba6010c2e866c3dec Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 8 Apr 2010 17:03:10 -0400 Subject: [PATCH 1627/1860] bug 543851 - autoconnect should call _checkSync(), r=Mardak --- services/sync/modules/service.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b2e5e6ffc112..2cf9c3e92bc4 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -623,9 +623,9 @@ WeaveSvc.prototype = { }, _autoConnect: let (attempts = 0) function _autoConnect() { - let reason = ""; - if (Utils.mpLocked()) - reason = "master password still locked"; + let reason = + Utils.mpLocked() ? "master password still locked" + : this._checkSync([kSyncNotLoggedIn, kFirstSyncChoiceNotMade]); // Can't autoconnect if we're missing these values if (!reason) { @@ -931,24 +931,30 @@ WeaveSvc.prototype = { /** * Determine if a sync should run. + * + * @param ignore [optional] + * array of reasons to ignore when checking * * @return Reason for not syncing; not-truthy if sync should run */ - _checkSync: function WeaveSvc__checkSync() { + _checkSync: function WeaveSvc__checkSync(ignore) { let reason = ""; if (!this.enabled) reason = kSyncWeaveDisabled; - else if (!this._loggedIn) - reason = kSyncNotLoggedIn; else if (Svc.IO.offline) reason = kSyncNetworkOffline; else if (Svc.Private && Svc.Private.privateBrowsingEnabled) // Svc.Private doesn't exist on Fennec -- don't assume it's there. reason = kSyncInPrivateBrowsing; - else if (Svc.Prefs.get("firstSync") == "notReady") - reason = kFirstSyncChoiceNotMade; else if (Status.minimumNextSync > Date.now()) reason = kSyncBackoffNotMet; + else if (!this._loggedIn) + reason = kSyncNotLoggedIn; + else if (Svc.Prefs.get("firstSync") == "notReady") + reason = kFirstSyncChoiceNotMade; + + if (ignore && ignore.indexOf(reason) != -1) + return ""; return reason; }, @@ -977,8 +983,7 @@ WeaveSvc.prototype = { _checkSyncStatus: function WeaveSvc__checkSyncStatus() { // Should we be syncing now, if not, cancel any sync timers and return // if we're in backoff, we'll schedule the next sync - let reason = this._checkSync(); - if (reason && reason != kSyncBackoffNotMet) { + if (this._checkSync([kSyncBackoffNotMet])) { this._clearSyncTriggers(); Status.service = STATUS_DISABLED; return; From 69b7aba1585be0a7c9c1688cfb5f13a5d8aa8527 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 14 Apr 2010 15:00:29 -0700 Subject: [PATCH 1628/1860] Bug 534218 - Changing sync direction from outdated clients loses client/server data [r=mconnor] Check remoteSetup first to determine if we're okay to sync before wiping client/remote. Additionally only wipe enabled engines instead of everything. --- services/sync/modules/service.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2cf9c3e92bc4..4065b7ad53bb 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1146,17 +1146,6 @@ WeaveSvc.prototype = { return; } - if (Svc.Prefs.isSet("firstSync")) { - switch(Svc.Prefs.get("firstSync")) { - case "wipeClient": - this.wipeClient(); - break; - case "wipeRemote": - this.wipeRemote(Engines.getAll().map(function(e) e.name)); - break; - } - } - // Clear out any potentially pending syncs now that we're syncing this._clearSyncTriggers(); this.nextSync = 0; @@ -1170,6 +1159,16 @@ WeaveSvc.prototype = { if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; + // Wipe data in the desired direction if necessary + switch (Svc.Prefs.get("firstSync")) { + case "wipeClient": + this.wipeClient(Engines.getEnabled().map(function(e) e.name)); + break; + case "wipeRemote": + this.wipeRemote(Engines.getEnabled().map(function(e) e.name)); + break; + } + // Ping the server with a special info request once a day let infoURL = this.infoURL; let now = Math.floor(Date.now() / 1000); From cbf8b014c68903e900bf1774b7f55bbede83255a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 15 Apr 2010 13:13:49 -0700 Subject: [PATCH 1629/1860] Bug 559130 - Can't clear search history with Weave 1.2.1 [r=mconnor] Return any value provided by the base form implementation while making sure we still notify before and after the call. --- services/sync/FormNotifier.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/FormNotifier.js b/services/sync/FormNotifier.js index 2b8096649d3a..ec1bf7628974 100644 --- a/services/sync/FormNotifier.js +++ b/services/sync/FormNotifier.js @@ -35,8 +35,12 @@ function FormNotifier() { }; notify("before"); - val.apply(this, arguments); - notify("after"); + try { + return val.apply(this, arguments); + } + finally { + notify("after"); + } }; } } From 6e395be7c477c6646bd70f9c44119b7fe5eadc92 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 15 Apr 2010 16:33:09 -0700 Subject: [PATCH 1630/1860] Bug 559674 - Put the xpi type (dev/rel) in the updated url [r=mconnor] Add two constants: WEAVE_CHANNEL and UPDATED_URL that get preprocessed with @xpi_type@. --- services/sync/modules/constants.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index e75a0e51e166..58d2f74fd075 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -37,6 +37,7 @@ // Process each item in the "constants hash" to add to "global" and give a name let EXPORTED_SYMBOLS = [((this[key] = val), key) for ([key, val] in Iterator({ +WEAVE_CHANNEL: "@xpi_type@", WEAVE_VERSION: "@weave_version@", WEAVE_ID: "@weave_id@", @@ -46,6 +47,7 @@ WEAVE_ID: "@weave_id@", STORAGE_VERSION: @storage_version@, DEFAULT_SERVER: "@server_url@", +UPDATED_URL: "https://services.mozilla.com/sync/updated/?version=@weave_version@&channel=@xpi_type@", PREFS_BRANCH: "extensions.weave.", From 0f7583cc5806cc5d802fb1196e5e06520118f0bd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 21 Apr 2010 11:10:21 -0700 Subject: [PATCH 1631/1860] Bug 560184 - Proxy authentication: Initial Weave connection attempt doesn't time out [r=mconnor] Delay the abort timer from the constructor in addition to onStartRequest and onDataAvailable in-case the callbacks never get called. --- services/sync/modules/resource.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index 0b0b698c249b..b523bd448cab 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -301,6 +301,7 @@ function ChannelListener(onComplete, onProgress, logger) { this._onComplete = onComplete; this._onProgress = onProgress; this._log = logger; + this.delayAbort(); } ChannelListener.prototype = { // Wait 5 minutes before killing a request @@ -317,9 +318,7 @@ ChannelListener.prototype = { this._log.trace(channel.requestMethod + " " + channel.URI.spec); this._data = ''; - - // Create an abort timer to kill dangling requests - Utils.delay(this.abortRequest, this.ABORT_TIMEOUT, this, "abortTimer"); + this.delayAbort(); }, onStopRequest: function Channel_onStopRequest(channel, context, status) { @@ -343,8 +342,13 @@ ChannelListener.prototype = { this._data += siStream.read(count); this._onProgress(); + this.delayAbort(); + }, - // Update the abort timer to wait an extra timeout + /** + * Create or push back the abort timer that kills this request + */ + delayAbort: function delayAbort() { Utils.delay(this.abortRequest, this.ABORT_TIMEOUT, this, "abortTimer"); }, From d4811c98d2d0157c75a213087fafe5da6cd68f2c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 21 Apr 2010 11:10:32 -0700 Subject: [PATCH 1632/1860] Bug 437277 - update daily backup before first bookmark sync [r=mconnor] Share an archiveBookmarks function that wraps the PlacesUtils call to be used from storage.wipe and first sync. --- services/sync/modules/engines/bookmarks.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 58e18356b933..093eb5e978a5 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -55,6 +55,14 @@ Cu.import("resource://weave/stores.js"); Cu.import("resource://weave/trackers.js"); Cu.import("resource://weave/type_records/bookmark.js"); +function archiveBookmarks() { + // Some nightly builds of 3.7 don't have this function + try { + PlacesUtils.archiveBookmarksFile(null, true); + } + catch(ex) {} +} + // Lazily initialize the special top level folders let kSpecialIds = {}; [["menu", "bookmarksMenuFolder"], @@ -126,6 +134,10 @@ BookmarksEngine.prototype = { _syncStartup: function _syncStart() { SyncEngine.prototype._syncStartup.call(this); + // For first-syncs, make a backup for the user to restore + if (this.lastSync == 0) + archiveBookmarks(); + // Lazily create a mapping of folder titles and separator positions to GUID this.__defineGetter__("_lazyMap", function() { delete this._lazyMap; @@ -936,10 +948,8 @@ BookmarksStore.prototype = { }, wipe: function BStore_wipe() { - // some nightly builds of 3.7 don't have this function - try { - PlacesUtils.archiveBookmarksFile(null, true); - } catch(e) {} + // Save a backup before clearing out all bookmarks + archiveBookmarks(); for (let [guid, id] in Iterator(kSpecialIds)) if (guid != "places") From 4271c92ccd7d2af326a3c4c72bbf3ada62adbfc3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 21 Apr 2010 11:10:32 -0700 Subject: [PATCH 1633/1860] Bug 480448 - Get rid of code to delete old snapshots [r=mconnor] SnapshotStore got removed with bug 524916 and hasn't been getting called for a long time now. --- services/sync/modules/service.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 4065b7ad53bb..8f7354f7d4a0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1448,18 +1448,6 @@ WeaveSvc.prototype = { // Have each engine drop any temporary meta data for each (let engine in engines) engine.resetClient(); - - // XXX Bug 480448: Delete any snapshots from old code - try { - let cruft = Svc.Directory.get("ProfD", Ci.nsIFile); - cruft.QueryInterface(Ci.nsILocalFile); - cruft.append("weave"); - cruft.append("snapshots"); - if (cruft.exists()) - cruft.remove(true); - } catch (e) { - this._log.debug("Could not remove old snapshots: " + Utils.exceptionStr(e)); - } }))(), /** From 322877bf2ff51c09b1c6ced0b8fd8a2fd71aa3ed Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 21 Apr 2010 13:41:18 -0700 Subject: [PATCH 1634/1860] Bug 558191 - Theme/persona sync sometimes doesn't happen until restart of client [r=mconnor] Wait until all lightweight theme prefs have synced before poking at the lightweight theme manager. --- services/sync/modules/engines/prefs.js | 29 ++++++++++++-------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index a50c7f9ee05e..52231d0b3c76 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -140,22 +140,6 @@ PrefStore.prototype = { break; case "string": this._prefs.setCharPref(values[i]["name"], values[i]["value"]); - - // Notify the lightweight theme manager of the new value - if (values[i].name == "lightweightThemes.usedThemes") { - try { - let ltm = {}; - Cu.import("resource://gre/modules/LightweightThemeManager.jsm", ltm); - ltm = ltm.LightweightThemeManager; - if (ltm.currentTheme) { - ltm.currentTheme = null; - ltm.currentTheme = ltm.usedThemes[0]; - } - } - // LightweightThemeManager only exists in Firefox 3.6+ - catch (ex) {} - } - break; case "boolean": this._prefs.setBoolPref(values[i]["name"], values[i]["value"]); @@ -164,6 +148,19 @@ PrefStore.prototype = { this._log.trace("Unexpected preference type: " + values[i]["type"]); } } + + // Notify the lightweight theme manager of all the new values + try { + let ltm = {}; + Cu.import("resource://gre/modules/LightweightThemeManager.jsm", ltm); + ltm = ltm.LightweightThemeManager; + if (ltm.currentTheme) { + ltm.currentTheme = null; + ltm.currentTheme = ltm.usedThemes[0]; + } + } + // LightweightThemeManager only exists in Firefox 3.6+ + catch (ex) {} }, getAllIDs: function PrefStore_getAllIDs() { From 011a424994cbed30a62d26776ad324abeddfe8e7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 21 Apr 2010 16:35:51 -0700 Subject: [PATCH 1635/1860] Bug 553402 - New pages in a tab aren't synced [r=mconnor] Trace pageshow events to indicate that new tab data should be uploaded. Because scores must be integers, still increment the score by 1, but only do that 10% of the time for pageshows. --- services/sync/modules/engines/tabs.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index e3bf1c7bc863..83b645a38978 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -224,7 +224,7 @@ TabTracker.prototype = { this._log.trace("Registering tab listeners in new window"); // For each topic, add or remove onTab as the listener - let topics = ["TabOpen", "TabClose", "TabSelect"]; + let topics = ["pageshow", "TabOpen", "TabClose", "TabSelect"]; let onTab = this.onTab; let addRem = function(add) topics.forEach(function(topic) { window[(add ? "add" : "remove") + "EventListener"](topic, onTab, false); @@ -248,12 +248,23 @@ TabTracker.prototype = { }, onTab: function onTab(event) { - this._log.trace(event.type); - this.score += 1; + this._log.trace("onTab event: " + event.type); this.addChangedID(Clients.localID); - // Store a timestamp in the tab to track when it was last used - Svc.Session.setTabValue(event.originalTarget, "weaveLastUsed", - Math.floor(Date.now() / 1000)); + // For pageshow events, only give a partial score bump (~.1) + let chance = .1; + + // For regular Tab events, do a full score bump and remember when it changed + if (event.type != "pageshow") { + chance = 1; + + // Store a timestamp in the tab to track when it was last used + Svc.Session.setTabValue(event.originalTarget, "weaveLastUsed", + Math.floor(Date.now() / 1000)); + } + + // Only increase the score by whole numbers, so use random for partial score + if (Math.random() < chance) + this.score++; }, } From 2cd5749d13fd8ad6c651037e5a0f1a1cbae48f9f Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Wed, 21 Apr 2010 19:02:16 -0700 Subject: [PATCH 1636/1860] Bug 513798 - Rewrite WeaveCrypto in JS. r=mconnor, r=dwitte --- services/crypto/IWeaveCrypto.h | 359 +++++ .../crypto/{components => }/IWeaveCrypto.xpt | Bin services/crypto/Makefile | 75 +- services/crypto/WeaveCrypto.cpp | 1198 ----------------- services/crypto/WeaveCrypto.h | 92 -- services/crypto/WeaveCrypto.js | 1126 ++++++++++++++++ services/crypto/WeaveCrypto.rc.in | 94 -- services/crypto/WeaveCryptoModule.cpp | 54 - .../Darwin/components/WeaveCrypto.dylib | Bin 65944 -> 0 bytes .../platform/Linux/components/WeaveCrypto.so | Bin 85574 -> 0 bytes .../Linux_x86-gcc3/components/WeaveCrypto.so | Bin 55737 -> 0 bytes .../components/WeaveCrypto.so | Bin 73368 -> 0 bytes .../platform/SunOS/components/WeaveCrypto.so | Bin 80256 -> 0 bytes .../platform/WINCE/components/WeaveCrypto.dll | Bin 33280 -> 0 bytes .../WINNT_x86-msvc/components/WeaveCrypto.dll | Bin 79872 -> 0 bytes services/sync/modules/util.js | 19 +- services/sync/services-sync.js | 1 + services/sync/tests/unit/head_first.js | 17 + services/sync/tests/unit/test_crypto_crypt.js | 2 +- .../sync/tests/unit/test_crypto_keypair.js | 2 +- .../sync/tests/unit/test_crypto_random.js | 2 +- .../sync/tests/unit/test_crypto_rewrap.js | 2 +- .../sync/tests/unit/test_crypto_verify.js | 2 +- .../sync/tests/unit/test_records_crypto.js | 2 +- 24 files changed, 1581 insertions(+), 1466 deletions(-) create mode 100644 services/crypto/IWeaveCrypto.h rename services/crypto/{components => }/IWeaveCrypto.xpt (100%) delete mode 100644 services/crypto/WeaveCrypto.cpp delete mode 100644 services/crypto/WeaveCrypto.h create mode 100644 services/crypto/WeaveCrypto.js delete mode 100644 services/crypto/WeaveCrypto.rc.in delete mode 100644 services/crypto/WeaveCryptoModule.cpp delete mode 100755 services/crypto/platform/Darwin/components/WeaveCrypto.dylib delete mode 100755 services/crypto/platform/Linux/components/WeaveCrypto.so delete mode 100755 services/crypto/platform/Linux_x86-gcc3/components/WeaveCrypto.so delete mode 100644 services/crypto/platform/Linux_x86_64-gcc3/components/WeaveCrypto.so delete mode 100755 services/crypto/platform/SunOS/components/WeaveCrypto.so delete mode 100755 services/crypto/platform/WINCE/components/WeaveCrypto.dll delete mode 100755 services/crypto/platform/WINNT_x86-msvc/components/WeaveCrypto.dll diff --git a/services/crypto/IWeaveCrypto.h b/services/crypto/IWeaveCrypto.h new file mode 100644 index 000000000000..8b8886ca98e1 --- /dev/null +++ b/services/crypto/IWeaveCrypto.h @@ -0,0 +1,359 @@ +/* + * DO NOT EDIT. THIS FILE IS GENERATED FROM IWeaveCrypto.idl + */ + +#ifndef __gen_IWeaveCrypto_h__ +#define __gen_IWeaveCrypto_h__ + + +#ifndef __gen_nsISupports_h__ +#include "nsISupports.h" +#endif + +/* For IDL files that don't want to include root IDL files. */ +#ifndef NS_NO_VTABLE +#define NS_NO_VTABLE +#endif + +/* starting interface: IWeaveCrypto */ +#define IWEAVECRYPTO_IID_STR "f4463043-315e-41f3-b779-82e900e6fffa" + +#define IWEAVECRYPTO_IID \ + {0xf4463043, 0x315e, 0x41f3, \ + { 0xb7, 0x79, 0x82, 0xe9, 0x00, 0xe6, 0xff, 0xfa }} + +class NS_NO_VTABLE NS_SCRIPTABLE IWeaveCrypto : public nsISupports { + public: + + NS_DECLARE_STATIC_IID_ACCESSOR(IWEAVECRYPTO_IID) + + /** + * Shortcuts for some algorithm SEC OIDs. Full list available here: + * http://lxr.mozilla.org/seamonkey/source/security/nss/lib/util/secoidt.h + */ + enum { DES_EDE3_CBC = 156U }; + + enum { AES_128_CBC = 184U }; + + enum { AES_192_CBC = 186U }; + + enum { AES_256_CBC = 188U }; + + /** + * One of the above constants. Used as the mechanism for encrypting bulk + * data and wrapping keys. + * + * Default is AES_256_CBC. + */ + /* attribute unsigned long algorithm; */ + NS_SCRIPTABLE NS_IMETHOD GetAlgorithm(PRUint32 *aAlgorithm) = 0; + NS_SCRIPTABLE NS_IMETHOD SetAlgorithm(PRUint32 aAlgorithm) = 0; + + /** + * The size of the RSA key to create with generateKeypair(). + * + * Default is 2048. + */ + /* attribute unsigned long keypairBits; */ + NS_SCRIPTABLE NS_IMETHOD GetKeypairBits(PRUint32 *aKeypairBits) = 0; + NS_SCRIPTABLE NS_IMETHOD SetKeypairBits(PRUint32 aKeypairBits) = 0; + + /** + * Encrypt data using a symmetric key. + * The algorithm attribute specifies how the encryption is performed. + * + * @param clearText + * The data to be encrypted (not base64 encoded). + * @param symmetricKey + * A base64-encoded symmetric key (eg, one from generateRandomKey). + * @param iv + * A base64-encoded initialization vector + * @returns Encrypted data, base64 encoded + */ + /* ACString encrypt (in AUTF8String clearText, in ACString symmetricKey, in ACString iv); */ + NS_SCRIPTABLE NS_IMETHOD Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) = 0; + + /** + * Encrypt data using a symmetric key. + * The algorithm attribute specifies how the encryption is performed. + * + * @param cipherText + * The base64-encoded data to be decrypted + * @param symmetricKey + * A base64-encoded symmetric key (eg, one from unwrapSymmetricKey) + * @param iv + * A base64-encoded initialization vector + * @returns Decrypted data (not base64-encoded) + */ + /* AUTF8String decrypt (in ACString cipherText, in ACString symmetricKey, in ACString iv); */ + NS_SCRIPTABLE NS_IMETHOD Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) = 0; + + /** + * Generate a RSA public/private keypair. + * + * @param aPassphrase + * User's passphrase. Used with PKCS#5 to generate a symmetric key + * for wrapping the private key. + * @param aSalt + * Salt for the user's passphrase. + * @param aIV + * Random IV, used when wrapping the private key. + * @param aEncodedPublicKey + * The public key, base-64 encoded. + * @param aWrappedPrivateKey + * The public key, encrypted with the user's passphrase, and base-64 encoded. + */ + /* void generateKeypair (in ACString aPassphrase, in ACString aSalt, in ACString aIV, out ACString aEncodedPublicKey, out ACString aWrappedPrivateKey); */ + NS_SCRIPTABLE NS_IMETHOD GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM) = 0; + + /* ACString generateRandomKey (); */ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomKey(nsACString & _retval NS_OUTPARAM) = 0; + + /* ACString generateRandomIV (); */ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomIV(nsACString & _retval NS_OUTPARAM) = 0; + + /* ACString generateRandomBytes (in unsigned long aByteCount); */ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM) = 0; + + /** + * Encrypts a symmetric key with a user's public key. + * + * @param aSymmetricKey + * The base64 encoded string holding a symmetric key. + * @param aEncodedPublicKey + * The base64 encoded string holding a public key. + * @returns The wrapped symmetric key, base64 encoded + * + * For RSA, the unencoded public key is a PKCS#1 object. + */ + /* ACString wrapSymmetricKey (in ACString aSymmetricKey, in ACString aEncodedPublicKey); */ + NS_SCRIPTABLE NS_IMETHOD WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM) = 0; + + /** + * Decrypts a symmetric key with a user's private key. + * + * @param aWrappedSymmetricKey + * The base64 encoded string holding an encrypted symmetric key. + * @param aWrappedPrivateKey + * The base64 encoded string holdering an encrypted private key. + * @param aPassphrase + * The passphrase to decrypt the private key. + * @param aSalt + * The salt for the passphrase. + * @param aIV + * The random IV used when unwrapping the private key. + * @returns The unwrapped symmetric key, base64 encoded + * + * For RSA, the unencoded, decrypted key is a PKCS#1 object. + */ + /* ACString unwrapSymmetricKey (in ACString aWrappedSymmetricKey, in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV); */ + NS_SCRIPTABLE NS_IMETHOD UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM) = 0; + + /** + * Rewrap a private key with a new user passphrase. + * + * @param aWrappedPrivateKey + * The base64 encoded string holding an encrypted private key. + * @param aPassphrase + * The passphrase to decrypt the private key. + * @param aSalt + * The salt for the passphrase. + * @param aIV + * The random IV used when unwrapping the private key. + * @param aNewPassphrase + * The new passphrase to wrap the private key with. + * @returns The (re)wrapped private key, base64 encoded + * + */ + /* ACString rewrapPrivateKey (in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV, in ACString aNewPassphrase); */ + NS_SCRIPTABLE NS_IMETHOD RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM) = 0; + + /** + * Verify a user's passphrase against a private key. + * + * @param aWrappedPrivateKey + * The base64 encoded string holding an encrypted private key. + * @param aPassphrase + * The passphrase to decrypt the private key. + * @param aSalt + * The salt for the passphrase. + * @param aIV + * The random IV used when unwrapping the private key. + * @returns Boolean true if the passphrase decrypted the key correctly. + * + */ + /* boolean verifyPassphrase (in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV); */ + NS_SCRIPTABLE NS_IMETHOD VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM) = 0; + +}; + + NS_DEFINE_STATIC_IID_ACCESSOR(IWeaveCrypto, IWEAVECRYPTO_IID) + +/* Use this macro when declaring classes that implement this interface. */ +#define NS_DECL_IWEAVECRYPTO \ + NS_SCRIPTABLE NS_IMETHOD GetAlgorithm(PRUint32 *aAlgorithm); \ + NS_SCRIPTABLE NS_IMETHOD SetAlgorithm(PRUint32 aAlgorithm); \ + NS_SCRIPTABLE NS_IMETHOD GetKeypairBits(PRUint32 *aKeypairBits); \ + NS_SCRIPTABLE NS_IMETHOD SetKeypairBits(PRUint32 aKeypairBits); \ + NS_SCRIPTABLE NS_IMETHOD Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomKey(nsACString & _retval NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomIV(nsACString & _retval NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM); \ + NS_SCRIPTABLE NS_IMETHOD VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM); + +/* Use this macro to declare functions that forward the behavior of this interface to another object. */ +#define NS_FORWARD_IWEAVECRYPTO(_to) \ + NS_SCRIPTABLE NS_IMETHOD GetAlgorithm(PRUint32 *aAlgorithm) { return _to GetAlgorithm(aAlgorithm); } \ + NS_SCRIPTABLE NS_IMETHOD SetAlgorithm(PRUint32 aAlgorithm) { return _to SetAlgorithm(aAlgorithm); } \ + NS_SCRIPTABLE NS_IMETHOD GetKeypairBits(PRUint32 *aKeypairBits) { return _to GetKeypairBits(aKeypairBits); } \ + NS_SCRIPTABLE NS_IMETHOD SetKeypairBits(PRUint32 aKeypairBits) { return _to SetKeypairBits(aKeypairBits); } \ + NS_SCRIPTABLE NS_IMETHOD Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) { return _to Encrypt(clearText, symmetricKey, iv, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) { return _to Decrypt(cipherText, symmetricKey, iv, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM) { return _to GenerateKeypair(aPassphrase, aSalt, aIV, aEncodedPublicKey, aWrappedPrivateKey); } \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomKey(nsACString & _retval NS_OUTPARAM) { return _to GenerateRandomKey(_retval); } \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomIV(nsACString & _retval NS_OUTPARAM) { return _to GenerateRandomIV(_retval); } \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM) { return _to GenerateRandomBytes(aByteCount, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM) { return _to WrapSymmetricKey(aSymmetricKey, aEncodedPublicKey, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM) { return _to UnwrapSymmetricKey(aWrappedSymmetricKey, aWrappedPrivateKey, aPassphrase, aSalt, aIV, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM) { return _to RewrapPrivateKey(aWrappedPrivateKey, aPassphrase, aSalt, aIV, aNewPassphrase, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM) { return _to VerifyPassphrase(aWrappedPrivateKey, aPassphrase, aSalt, aIV, _retval); } + +/* Use this macro to declare functions that forward the behavior of this interface to another object in a safe way. */ +#define NS_FORWARD_SAFE_IWEAVECRYPTO(_to) \ + NS_SCRIPTABLE NS_IMETHOD GetAlgorithm(PRUint32 *aAlgorithm) { return !_to ? NS_ERROR_NULL_POINTER : _to->GetAlgorithm(aAlgorithm); } \ + NS_SCRIPTABLE NS_IMETHOD SetAlgorithm(PRUint32 aAlgorithm) { return !_to ? NS_ERROR_NULL_POINTER : _to->SetAlgorithm(aAlgorithm); } \ + NS_SCRIPTABLE NS_IMETHOD GetKeypairBits(PRUint32 *aKeypairBits) { return !_to ? NS_ERROR_NULL_POINTER : _to->GetKeypairBits(aKeypairBits); } \ + NS_SCRIPTABLE NS_IMETHOD SetKeypairBits(PRUint32 aKeypairBits) { return !_to ? NS_ERROR_NULL_POINTER : _to->SetKeypairBits(aKeypairBits); } \ + NS_SCRIPTABLE NS_IMETHOD Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->Encrypt(clearText, symmetricKey, iv, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->Decrypt(cipherText, symmetricKey, iv, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->GenerateKeypair(aPassphrase, aSalt, aIV, aEncodedPublicKey, aWrappedPrivateKey); } \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomKey(nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->GenerateRandomKey(_retval); } \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomIV(nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->GenerateRandomIV(_retval); } \ + NS_SCRIPTABLE NS_IMETHOD GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->GenerateRandomBytes(aByteCount, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->WrapSymmetricKey(aSymmetricKey, aEncodedPublicKey, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->UnwrapSymmetricKey(aWrappedSymmetricKey, aWrappedPrivateKey, aPassphrase, aSalt, aIV, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->RewrapPrivateKey(aWrappedPrivateKey, aPassphrase, aSalt, aIV, aNewPassphrase, _retval); } \ + NS_SCRIPTABLE NS_IMETHOD VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->VerifyPassphrase(aWrappedPrivateKey, aPassphrase, aSalt, aIV, _retval); } + +#if 0 +/* Use the code below as a template for the implementation class for this interface. */ + +/* Header file */ +class _MYCLASS_ : public IWeaveCrypto +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_IWEAVECRYPTO + + _MYCLASS_(); + +private: + ~_MYCLASS_(); + +protected: + /* additional members */ +}; + +/* Implementation file */ +NS_IMPL_ISUPPORTS1(_MYCLASS_, IWeaveCrypto) + +_MYCLASS_::_MYCLASS_() +{ + /* member initializers and constructor code */ +} + +_MYCLASS_::~_MYCLASS_() +{ + /* destructor code */ +} + +/* attribute unsigned long algorithm; */ +NS_IMETHODIMP _MYCLASS_::GetAlgorithm(PRUint32 *aAlgorithm) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP _MYCLASS_::SetAlgorithm(PRUint32 aAlgorithm) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* attribute unsigned long keypairBits; */ +NS_IMETHODIMP _MYCLASS_::GetKeypairBits(PRUint32 *aKeypairBits) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP _MYCLASS_::SetKeypairBits(PRUint32 aKeypairBits) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* ACString encrypt (in AUTF8String clearText, in ACString symmetricKey, in ACString iv); */ +NS_IMETHODIMP _MYCLASS_::Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* AUTF8String decrypt (in ACString cipherText, in ACString symmetricKey, in ACString iv); */ +NS_IMETHODIMP _MYCLASS_::Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void generateKeypair (in ACString aPassphrase, in ACString aSalt, in ACString aIV, out ACString aEncodedPublicKey, out ACString aWrappedPrivateKey); */ +NS_IMETHODIMP _MYCLASS_::GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* ACString generateRandomKey (); */ +NS_IMETHODIMP _MYCLASS_::GenerateRandomKey(nsACString & _retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* ACString generateRandomIV (); */ +NS_IMETHODIMP _MYCLASS_::GenerateRandomIV(nsACString & _retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* ACString generateRandomBytes (in unsigned long aByteCount); */ +NS_IMETHODIMP _MYCLASS_::GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* ACString wrapSymmetricKey (in ACString aSymmetricKey, in ACString aEncodedPublicKey); */ +NS_IMETHODIMP _MYCLASS_::WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* ACString unwrapSymmetricKey (in ACString aWrappedSymmetricKey, in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV); */ +NS_IMETHODIMP _MYCLASS_::UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* ACString rewrapPrivateKey (in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV, in ACString aNewPassphrase); */ +NS_IMETHODIMP _MYCLASS_::RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean verifyPassphrase (in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV); */ +NS_IMETHODIMP _MYCLASS_::VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* End of implementation class template. */ +#endif + + +#endif /* __gen_IWeaveCrypto_h__ */ diff --git a/services/crypto/components/IWeaveCrypto.xpt b/services/crypto/IWeaveCrypto.xpt similarity index 100% rename from services/crypto/components/IWeaveCrypto.xpt rename to services/crypto/IWeaveCrypto.xpt diff --git a/services/crypto/Makefile b/services/crypto/Makefile index 84a971726bc3..7257b438212c 100644 --- a/services/crypto/Makefile +++ b/services/crypto/Makefile @@ -14,13 +14,13 @@ # # The Original Code is Weave code. # -# The Initial Developer of the Original Code is -# Mozilla Corporation +# The Initial Developer of the Original Code is Mozilla Foundation. # Portions created by the Initial Developer are Copyright (C) 2008 # the Initial Developer. All Rights Reserved. # # Contributor(s): # Dan Mills (original author) +# Justin Dolske # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -39,29 +39,62 @@ stage_dir=../dist/stage sdkdir ?= ${MOZSDKDIR} + +idl = IWeaveCrypto.idl +idl_typelib = $(idl:.idl=.xpt) +idl_header = $(idl:.idl=.h) + +# +# The only thing to actually build here is the IDL's .xpt/.h form, which +# requires an SDK. So don't do that unless explicitly requested, and use +# the files checked into Mercurial instead. +# +all: stage + +build: $(idl_typelib) $(idl_header) stage + +# No SDK is needed unless you're modifying the IDL interface, in which +# case we'll need to rebuild the .h and .xpt files. +xpidl = $(sdkdir)/bin/xpidl +ifdef CROSS_COMPILE +xpidl = $(sdkdir)/host/bin/host_xpidl +endif +$(idl_typelib): $(idl) ifeq ($(sdkdir),) - $(warning No 'sdkdir' variable given) - $(warning It should point to the location of the Gecko SDK) - $(warning For example: "make sdkdir=/foo/bar/baz") - $(warning Or set the MOZSDKDIR environment variable to point to it) - $(error) -endif - -ifdef platform_target - platform=$(platform_target) + $(warning No 'sdkdir' variable given) + $(warning It should point to the location of the Gecko SDK) + $(warning For example: "make sdkdir=/foo/bar/baz") + $(warning Or set the MOZSDKDIR environment variable to point to it) + $(error) else - platform=* + $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) endif -all: build +$(idl_header): $(idl) +ifeq ($(sdkdir),) + $(warning No 'sdkdir' variable given) + $(warning It should point to the location of the Gecko SDK) + $(warning For example: "make sdkdir=/foo/bar/baz") + $(warning Or set the MOZSDKDIR environment variable to point to it) + $(error) +else + $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) +endif -.PHONY: build crypto rebuild_all +stage: + mkdir -p $(stage_dir)/components +ifdef NO_SYMLINK + cp -v $(idl_typelib) $(stage_dir)/components + cp -v $(idl_header) $(TOPSRCDIR)/crypto-obsolete/src + cp -v WeaveCrypto.js $(stage_dir)/components +else + ln -vsf `pwd`/$(idl_typelib) $(stage_dir)/components + ln -vsf `pwd`/$(idl_header) $(TOPSRCDIR)/crypto-obsolete/src + ln -vsf `pwd`/WeaveCrypto.js $(stage_dir)/components +endif -crypto: - $(MAKE) -C src install +clean: + rm -f $(TOPSRCDIR)/crypto-obsolete/src/$(idl_header) + # maybe hg revert the .xpt/.h? -build: - cp -R -v components $(stage_dir) - cd platform;mkdir -p ../$(stage_dir)/platform;cp -R -v $(platform) ../$(stage_dir)/platform - -rebuild_all: crypto build +.PHONY: all build stage clean diff --git a/services/crypto/WeaveCrypto.cpp b/services/crypto/WeaveCrypto.cpp deleted file mode 100644 index 4aa39bea5484..000000000000 --- a/services/crypto/WeaveCrypto.cpp +++ /dev/null @@ -1,1198 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Weave code. - * - * The Initial Developer of the Original Code is - * Mozilla Corporation - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills (original author) - * Honza Bambas - * Justin Dolske - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -#include "WeaveCrypto.h" - -#include "nsStringAPI.h" -#include "nsAutoPtr.h" -#include "plbase64.h" -#include "prmem.h" -#include "secerr.h" - -#include "pk11func.h" -#include "keyhi.h" -#include "nss.h" - -/* - * In a number of places we use stack buffers to hold smallish temporary data. - * 4K is plenty big for the exptected uses, and avoids poking holes in the - * heap for small allocations. (Yes, we still check for overflow.) - */ -#define STACK_BUFFER_SIZE 4096 - -NS_IMPL_ISUPPORTS1(WeaveCrypto, IWeaveCrypto) - -WeaveCrypto::WeaveCrypto() : - mAlgorithm(SEC_OID_AES_256_CBC), - mKeypairBits(2048) -{ - // Ensure that PSM (and thus NSS) is initialized. - nsCOMPtr psm(nsGetServiceByContractID("@mozilla.org/psm;1")); -} - -WeaveCrypto::~WeaveCrypto() -{ -} - -/* - * Base 64 encoding and decoding... - */ - -nsresult -WeaveCrypto::EncodeBase64(const char *aData, PRUint32 aLength, nsACString& retval) -{ - // Empty input? Nothing to do. - if (!aLength) { - retval.Assign(EmptyCString()); - return NS_OK; - } - - PRUint32 encodedLength = (aLength + 2) / 3 * 4; - char *encoded = (char *)PR_Malloc(encodedLength); - if (!encoded) - return NS_ERROR_OUT_OF_MEMORY; - - PL_Base64Encode(aData, aLength, encoded); - - retval.Assign(encoded, encodedLength); - - PR_Free(encoded); - return NS_OK; -} - -nsresult -WeaveCrypto::EncodeBase64(const nsACString& binary, nsACString& retval) -{ - PromiseFlatCString fBinary(binary); - return EncodeBase64(fBinary.get(), fBinary.Length(), retval); -} - -nsresult -WeaveCrypto::DecodeBase64(const nsACString& base64, - char *decoded, PRUint32 *decodedSize) -{ - PromiseFlatCString fBase64(base64); - - if (fBase64.Length() == 0) { - *decodedSize = 0; - return NS_OK; - } - - // We expect at least 4 bytes of input - if (fBase64.Length() < 4) - return NS_ERROR_FAILURE; - - PRUint32 size = (fBase64.Length() * 3) / 4; - // Adjust for padding. - if (*(fBase64.get() + fBase64.Length() - 1) == '=') - size--; - if (*(fBase64.get() + fBase64.Length() - 2) == '=') - size--; - - // Just to be paranoid about buffer overflow. - if (*decodedSize < size) - return NS_ERROR_FAILURE; - *decodedSize = size; - - if (!PL_Base64Decode(fBase64.get(), fBase64.Length(), decoded)) - return NS_ERROR_ILLEGAL_VALUE; - - return NS_OK; -} - -nsresult -WeaveCrypto::DecodeBase64(const nsACString& base64, nsACString& retval) -{ - PRUint32 decodedLength = base64.Length(); - char *decoded = (char *)PR_Malloc(decodedLength); - if (!decoded) - return NS_ERROR_OUT_OF_MEMORY; - - nsresult rv = DecodeBase64(base64, decoded, &decodedLength); - if (NS_FAILED(rv)) { - PR_Free(decoded); - return rv; - } - - retval.Assign(decoded, decodedLength); - - PR_Free(decoded); - return NS_OK; -} - - -////////////////////////////////////////////////////////////////////////////// - -NS_IMETHODIMP -WeaveCrypto::GetAlgorithm(PRUint32 *aAlgorithm) -{ - *aAlgorithm = mAlgorithm; - return NS_OK; -} - -NS_IMETHODIMP -WeaveCrypto::SetAlgorithm(PRUint32 aAlgorithm) -{ - mAlgorithm = (SECOidTag)aAlgorithm; - return NS_OK; -} - -NS_IMETHODIMP -WeaveCrypto::GetKeypairBits(PRUint32 *aBits) -{ - *aBits = mKeypairBits; - return NS_OK; -} - -NS_IMETHODIMP -WeaveCrypto::SetKeypairBits(PRUint32 aBits) -{ - mKeypairBits = aBits; - return NS_OK; -} - - -/* - * Encrypt - */ -NS_IMETHODIMP -WeaveCrypto::Encrypt(const nsACString& aClearText, - const nsACString& aSymmetricKey, - const nsACString& aIV, - nsACString& aCipherText) -{ - nsresult rv = NS_OK; - - // When using CBC padding, the output is 1 block larger than the input. - CK_MECHANISM_TYPE mech = PK11_AlgtagToMechanism(mAlgorithm); - PRUint32 blockSize = PK11_GetBlockSize(mech, nsnull); - - PRUint32 outputBufferSize = aClearText.Length() + blockSize; - char *outputBuffer = (char *)PR_Malloc(outputBufferSize); - if (!outputBuffer) - return NS_ERROR_OUT_OF_MEMORY; - - PromiseFlatCString input(aClearText); - - rv = CommonCrypt(input.get(), input.Length(), - outputBuffer, &outputBufferSize, - aSymmetricKey, aIV, CKA_ENCRYPT); - if (NS_FAILED(rv)) - goto encrypt_done; - - rv = EncodeBase64(outputBuffer, outputBufferSize, aCipherText); - if (NS_FAILED(rv)) - goto encrypt_done; - -encrypt_done: - PR_Free(outputBuffer); - return rv; -} - - -/* - * Decrypt - */ -NS_IMETHODIMP -WeaveCrypto::Decrypt(const nsACString& aCipherText, - const nsACString& aSymmetricKey, - const nsACString& aIV, - nsACString& aClearText) -{ - nsresult rv = NS_OK; - - PRUint32 inputBufferSize = aCipherText.Length(); - PRUint32 outputBufferSize = aCipherText.Length(); - char *outputBuffer = (char *)PR_Malloc(outputBufferSize); - char *inputBuffer = (char *)PR_Malloc(inputBufferSize); - if (!inputBuffer || !outputBuffer) - return NS_ERROR_OUT_OF_MEMORY; - - rv = DecodeBase64(aCipherText, inputBuffer, &inputBufferSize); - if (NS_FAILED(rv)) - goto decrypt_done; - - rv = CommonCrypt(inputBuffer, inputBufferSize, - outputBuffer, &outputBufferSize, - aSymmetricKey, aIV, CKA_DECRYPT); - if (NS_FAILED(rv)) - goto decrypt_done; - - aClearText.Assign(outputBuffer, outputBufferSize); - -decrypt_done: - PR_Free(outputBuffer); - PR_Free(inputBuffer); - return rv; -} - - -/* - * CommonCrypt - */ -nsresult -WeaveCrypto::CommonCrypt(const char *input, PRUint32 inputSize, - char *output, PRUint32 *outputSize, - const nsACString& aSymmetricKey, - const nsACString& aIV, - CK_ATTRIBUTE_TYPE aOperation) -{ - nsresult rv = NS_OK; - PK11SymKey *symKey = nsnull; - PK11Context *ctx = nsnull; - PK11SlotInfo *slot = nsnull; - SECItem *ivParam = nsnull; - PRUint32 maxOutputSize; - - char keyData[STACK_BUFFER_SIZE]; - PRUint32 keyDataSize = sizeof(keyData); - char ivData[STACK_BUFFER_SIZE]; - PRUint32 ivDataSize = sizeof(ivData); - - rv = DecodeBase64(aSymmetricKey, keyData, &keyDataSize); - NS_ENSURE_SUCCESS(rv, rv); - rv = DecodeBase64(aIV, ivData, &ivDataSize); - NS_ENSURE_SUCCESS(rv, rv); - - SECItem keyItem = {siBuffer, (unsigned char*)keyData, keyDataSize}; - SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; - - - // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD - CK_MECHANISM_TYPE mechanism = PK11_AlgtagToMechanism(mAlgorithm); - mechanism = PK11_GetPadMechanism(mechanism); - if (mechanism == CKM_INVALID_MECHANISM) { - NS_WARNING("Unknown key mechanism"); - rv = NS_ERROR_FAILURE; - goto crypt_done; - } - - ivParam = PK11_ParamFromIV(mechanism, &ivItem); - if (!ivParam) { - NS_WARNING("Couldn't create IV param"); - rv = NS_ERROR_FAILURE; - goto crypt_done; - } - - slot = PK11_GetInternalKeySlot(); - if (!slot) { - NS_WARNING("PK11_GetInternalKeySlot failed"); - rv = NS_ERROR_FAILURE; - goto crypt_done; - } - - symKey = PK11_ImportSymKey(slot, mechanism, PK11_OriginUnwrap, aOperation, &keyItem, NULL); - if (!symKey) { - NS_WARNING("PK11_ImportSymKey failed"); - rv = NS_ERROR_FAILURE; - goto crypt_done; - } - - ctx = PK11_CreateContextBySymKey(mechanism, aOperation, symKey, ivParam); - if (!ctx) { - NS_WARNING("PK11_CreateContextBySymKey failed"); - rv = NS_ERROR_FAILURE; - goto crypt_done; - } - - int tmpOutSize; // XXX retarded. why is an signed int suddenly needed? - maxOutputSize = *outputSize; - rv = PK11_CipherOp(ctx, - (unsigned char *)output, &tmpOutSize, maxOutputSize, - (unsigned char *)input, inputSize); - if (NS_FAILED(rv)) { - NS_WARNING("PK11_CipherOp failed"); - rv = NS_ERROR_FAILURE; - goto crypt_done; - } - - *outputSize = tmpOutSize; - output += tmpOutSize; - maxOutputSize -= tmpOutSize; - - // PK11_DigestFinal sure sounds like the last step for *hashing*, but it - // just seems to be an odd name -- NSS uses this to finish the current - // cipher operation. You'd think it would be called PK11_CipherOpFinal... - PRUint32 tmpOutSize2; // XXX WTF? Now unsigned again? What kind of crazy API is this? - rv = PK11_DigestFinal(ctx, (unsigned char *)output, &tmpOutSize2, maxOutputSize); - if (NS_FAILED(rv)) { - NS_WARNING("PK11_DigestFinal failed"); - rv = NS_ERROR_FAILURE; - goto crypt_done; - } - - *outputSize += tmpOutSize2; - -crypt_done: - if (ctx) - PK11_DestroyContext(ctx, PR_TRUE); - if (symKey) - PK11_FreeSymKey(symKey); - if (slot) - PK11_FreeSlot(slot); - if (ivParam) - SECITEM_FreeItem(ivParam, PR_TRUE); - - return rv; -} - - -/* - * GenerateKeypair - * - * Generates an RSA key pair, encrypts (wraps) the private key with the - * supplied passphrase, and returns both to the caller. - * - * Based on code from: - * http://www.mozilla.org/projects/security/pki/nss/sample-code/sample1.html - */ -NS_IMETHODIMP -WeaveCrypto::GenerateKeypair(const nsACString& aPassphrase, - const nsACString& aSalt, - const nsACString& aIV, - nsACString& aEncodedPublicKey, - nsACString& aWrappedPrivateKey) -{ - nsresult rv; - SECStatus s; - SECKEYPrivateKey *privKey = nsnull; - SECKEYPublicKey *pubKey = nsnull; - PK11SlotInfo *slot = nsnull; - PK11RSAGenParams rsaParams; - // Attributes for the private key. We're just going to wrap and extract the - // value, so they're not critical. The _PUBLIC attribute just indicates the - // object can be accessed without being logged into the token. - PK11AttrFlags attrFlags = (PK11_ATTR_SESSION | - PK11_ATTR_PUBLIC | - PK11_ATTR_SENSITIVE); - - - rsaParams.keySizeInBits = mKeypairBits; // 1024, 2048, etc. - rsaParams.pe = 65537; // public exponent. - - slot = PK11_GetInternalKeySlot(); - if (!slot) { - NS_WARNING("PK11_GetInternalKeySlot failed"); - rv = NS_ERROR_FAILURE; - goto keygen_done; - } - - // Generate the keypair. - privKey = PK11_GenerateKeyPairWithFlags(slot, - CKM_RSA_PKCS_KEY_PAIR_GEN, - &rsaParams, &pubKey, - attrFlags, nsnull); - - if (!privKey) { - NS_WARNING("PK11_GenerateKeyPair failed"); - rv = NS_ERROR_FAILURE; - goto keygen_done; - } - - s = PK11_SetPrivateKeyNickname(privKey, "Weave User PrivKey"); - if (s != SECSuccess) { - NS_WARNING("PK11_SetPrivateKeyNickname failed"); - rv = NS_ERROR_FAILURE; - goto keygen_done; - } - - - rv = WrapPrivateKey(privKey, aPassphrase, aSalt, aIV, aWrappedPrivateKey); - if (NS_FAILED(rv)) { - NS_WARNING("WrapPrivateKey failed"); - rv = NS_ERROR_FAILURE; - goto keygen_done; - } - - rv = EncodePublicKey(pubKey, aEncodedPublicKey); - if (NS_FAILED(rv)) { - NS_WARNING("EncodePublicKey failed"); - rv = NS_ERROR_FAILURE; - goto keygen_done; - } - -keygen_done: - // Cleanup allocated resources. - if (pubKey) - SECKEY_DestroyPublicKey(pubKey); - if (privKey) - SECKEY_DestroyPrivateKey(privKey); - if (slot) - PK11_FreeSlot(slot); - - return rv; -} - - -/* - * DeriveKeyFromPassphrase - * - * Creates a symmertic key from a passphrase, using PKCS#5. - */ -nsresult -WeaveCrypto::DeriveKeyFromPassphrase(const nsACString& aPassphrase, - const nsACString& aSalt, - PK11SymKey **aSymKey) -{ - nsresult rv; - - PromiseFlatCString fPass(aPassphrase); - SECItem passphrase = {siBuffer, (unsigned char *)fPass.get(), fPass.Length()}; - - char saltBytes[STACK_BUFFER_SIZE]; - PRUint32 saltBytesLength = sizeof(saltBytes); - rv = DecodeBase64(aSalt, saltBytes, &saltBytesLength); - NS_ENSURE_SUCCESS(rv, rv); - SECItem salt = {siBuffer, (unsigned char*)saltBytes, saltBytesLength}; - - // http://mxr.mozilla.org/seamonkey/source/security/nss/lib/pk11wrap/pk11pbe.c#1261 - - // Bug 436577 prevents us from just using SEC_OID_PKCS5_PBKDF2 here - SECOidTag pbeAlg = mAlgorithm; - SECOidTag cipherAlg = mAlgorithm; // ignored by callee when pbeAlg != a pkcs5 mech. - SECOidTag prfAlg = SEC_OID_HMAC_SHA1; // callee picks if SEC_OID_UNKNOWN, but only SHA1 is supported - - PRInt32 keyLength = 0; // Callee will pick. - PRInt32 iterations = 4096; // PKCS#5 recommends at least 1000. - - SECAlgorithmID *algid = PK11_CreatePBEV2AlgorithmID(pbeAlg, cipherAlg, prfAlg, - keyLength, iterations, &salt); - if (!algid) - return NS_ERROR_FAILURE; - - PK11SlotInfo *slot = PK11_GetInternalSlot(); - if (!slot) - return NS_ERROR_FAILURE; - - *aSymKey = PK11_PBEKeyGen(slot, algid, &passphrase, PR_FALSE, nsnull); - - SECOID_DestroyAlgorithmID(algid, PR_TRUE); - PK11_FreeSlot(slot); - - return (*aSymKey ? NS_OK : NS_ERROR_FAILURE); -} - - -/* - * WrapPrivateKey - * - * Extract the private key by wrapping it (ie, converting it to a binary - * blob and encoding it with a symmetric key) - * - * See: - * http://www.mozilla.org/projects/security/pki/nss/tech-notes/tn5.html, - * "Symmetric Key Wrapping/Unwrapping of a Private Key" - * - * The NSS softtoken uses sftk_PackagePrivateKey() to prepare the private key - * for wrapping. For RSA, that's an ASN1 encoded PKCS1 object. - */ -nsresult -WeaveCrypto::WrapPrivateKey(SECKEYPrivateKey *aPrivateKey, - const nsACString& aPassphrase, - const nsACString& aSalt, - const nsACString& aIV, - nsACString& aWrappedPrivateKey) - -{ - nsresult rv; - SECStatus s; - PK11SymKey *pbeKey = nsnull; - - // Convert our passphrase to a symkey and get the IV in the form we want. - rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); - NS_ENSURE_SUCCESS(rv, rv); - - char ivData[STACK_BUFFER_SIZE]; - PRUint32 ivDataSize = sizeof(ivData); - rv = DecodeBase64(aIV, ivData, &ivDataSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; - - // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD - CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); - wrapMech = PK11_GetPadMechanism(wrapMech); - if (wrapMech == CKM_INVALID_MECHANISM) { - NS_WARNING("Unknown key mechanism"); - return NS_ERROR_FAILURE; - } - - SECItem *ivParam = PK11_ParamFromIV(wrapMech, &ivItem); - if (!ivParam) { - NS_WARNING("Couldn't create IV param"); - return NS_ERROR_FAILURE; - } - - - // Use a stack buffer to hold the wrapped key. NSS says about 1200 bytes for - // a 2048-bit RSA key, so our 4096 byte buffer should be plenty. - unsigned char stackBuffer[STACK_BUFFER_SIZE]; - SECItem wrappedKey = {siBuffer, stackBuffer, sizeof(stackBuffer)}; - - s = PK11_WrapPrivKey(aPrivateKey->pkcs11Slot, - pbeKey, aPrivateKey, - wrapMech, ivParam, - &wrappedKey, nsnull); - - SECITEM_FreeItem(ivParam, PR_TRUE); - PK11_FreeSymKey(pbeKey); - - if (s != SECSuccess) { - NS_WARNING("PK11_WrapPrivKey failed"); - return(NS_ERROR_FAILURE); - } - - rv = EncodeBase64((char *)wrappedKey.data, wrappedKey.len, aWrappedPrivateKey); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; -} - - -/* - * EncodePublicKey - * - * http://svn.mozilla.org/projects/mccoy/trunk/components/src/KeyPair.cpp - */ -nsresult -WeaveCrypto::EncodePublicKey(SECKEYPublicKey *aPublicKey, - nsACString& aEncodedPublicKey) -{ - //SECItem *derKey = PK11_DEREncodePublicKey(aPublicKey); - SECItem *derKey = SECKEY_EncodeDERSubjectPublicKeyInfo(aPublicKey); - if (!derKey) - return NS_ERROR_FAILURE; - - nsresult rv = EncodeBase64((char *)derKey->data, derKey->len, aEncodedPublicKey); - NS_ENSURE_SUCCESS(rv, rv); - - // XXX destroy derKey? - - return NS_OK; -} - - -/* - * GenerateRandomBytes - */ -NS_IMETHODIMP -WeaveCrypto::GenerateRandomBytes(PRUint32 aByteCount, - nsACString& aEncodedBytes) -{ - nsresult rv; - char random[STACK_BUFFER_SIZE]; - - if (aByteCount > STACK_BUFFER_SIZE) - return NS_ERROR_OUT_OF_MEMORY; - - rv = PK11_GenerateRandom((unsigned char *)random, aByteCount); - NS_ENSURE_SUCCESS(rv, rv); - - rv = EncodeBase64(random, aByteCount, aEncodedBytes); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; -} - - -/* - * GenerateRandomIV - */ -NS_IMETHODIMP -WeaveCrypto::GenerateRandomIV(nsACString& aEncodedBytes) -{ - nsresult rv; - - CK_MECHANISM_TYPE mech = PK11_AlgtagToMechanism(mAlgorithm); - PRUint32 size = PK11_GetIVLength(mech); - - char random[STACK_BUFFER_SIZE]; - - if (size > STACK_BUFFER_SIZE) - return NS_ERROR_OUT_OF_MEMORY; - - rv = PK11_GenerateRandom((unsigned char *)random, size); - NS_ENSURE_SUCCESS(rv, rv); - - rv = EncodeBase64(random, size, aEncodedBytes); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; -} - - -/* - * GenerateRandomKey - * - * Generates a random symmetric key, and returns it in base64 encoded form. - * - * Note that we could just use GenerateRandomBytes to do all this (at least - * for AES), but ideally we'd be able to switch to passing around key handles - * insead of the actual key bits, which would require generating an actual - * key. - */ -NS_IMETHODIMP -WeaveCrypto::GenerateRandomKey(nsACString& aEncodedKey) -{ - nsresult rv = NS_OK; - PRUint32 keySize; - CK_MECHANISM_TYPE keygenMech; - SECItem *keydata = nsnull; - - // XXX doesn't NSS have a lookup function to do this? - switch (mAlgorithm) { - case AES_128_CBC: - keygenMech = CKM_AES_KEY_GEN; - keySize = 16; - break; - - case AES_192_CBC: - keygenMech = CKM_AES_KEY_GEN; - keySize = 24; - break; - - case AES_256_CBC: - keygenMech = CKM_AES_KEY_GEN; - keySize = 32; - break; - - default: - NS_WARNING("Unknown random keygen algorithm"); - return NS_ERROR_FAILURE; - } - - PK11SlotInfo *slot = PK11_GetInternalSlot(); - if (!slot) - return NS_ERROR_FAILURE; - - PK11SymKey* randKey = PK11_KeyGen(slot, keygenMech, NULL, keySize, NULL); - if (!randKey) { - NS_WARNING("PK11_KeyGen failed"); - rv = NS_ERROR_FAILURE; - goto keygen_done; - } - - if (PK11_ExtractKeyValue(randKey)) { - NS_WARNING("PK11_ExtractKeyValue failed"); - rv = NS_ERROR_FAILURE; - goto keygen_done; - } - - keydata = PK11_GetKeyData(randKey); - if (!keydata) { - NS_WARNING("PK11_GetKeyData failed"); - rv = NS_ERROR_FAILURE; - goto keygen_done; - } - - rv = EncodeBase64((char *)keydata->data, keydata->len, aEncodedKey); - if (NS_FAILED(rv)) { - NS_WARNING("EncodeBase64 failed"); - goto keygen_done; - } - -keygen_done: - // XXX does keydata need freed? - if (randKey) - PK11_FreeSymKey(randKey); - if (slot) - PK11_FreeSlot(slot); - - return rv; -} - - -/* - * WrapSymmetricKey - */ -NS_IMETHODIMP -WeaveCrypto::WrapSymmetricKey(const nsACString& aSymmetricKey, - const nsACString& aPublicKey, - nsACString& aWrappedKey) -{ - nsresult rv = NS_OK; - SECStatus s; - PK11SlotInfo *slot = nsnull; - PK11SymKey *symKey = nsnull; - SECKEYPublicKey *pubKey = nsnull; - CERTSubjectPublicKeyInfo *pubKeyInfo = nsnull; - CK_MECHANISM_TYPE keyMech, wrapMech; - - // Step 1. Get rid of the base64 encoding on the inputs. - - char publicKeyBuffer[STACK_BUFFER_SIZE]; - PRUint32 publicKeyBufferSize = sizeof(publicKeyBuffer); - rv = DecodeBase64(aPublicKey, publicKeyBuffer, &publicKeyBufferSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem pubKeyData = {siBuffer, (unsigned char *)publicKeyBuffer, publicKeyBufferSize}; - - char symKeyBuffer[STACK_BUFFER_SIZE]; - PRUint32 symKeyBufferSize = sizeof(symKeyBuffer); - rv = DecodeBase64(aSymmetricKey, symKeyBuffer, &symKeyBufferSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem symKeyData = {siBuffer, (unsigned char *)symKeyBuffer, symKeyBufferSize}; - - char wrappedBuffer[STACK_BUFFER_SIZE]; - SECItem wrappedKey = {siBuffer, (unsigned char *)wrappedBuffer, sizeof(wrappedBuffer)}; - - - // Step 2. Put the symmetric key bits into a P11 key object. - - slot = PK11_GetInternalSlot(); - if (!slot) { - NS_WARNING("Can't get internal PK11 slot"); - rv = NS_ERROR_FAILURE; - goto wrap_done; - } - - // ImportSymKey wants a mechanism, from which it derives the key type. - keyMech = PK11_AlgtagToMechanism(mAlgorithm); - if (keyMech == CKM_INVALID_MECHANISM) { - NS_WARNING("Unknown key mechanism"); - rv = NS_ERROR_FAILURE; - goto wrap_done; - } - - // This imports a key with the usage set for encryption, but that doesn't - // really matter because we're just going to wrap it up and not use it. - symKey = PK11_ImportSymKey(slot, - keyMech, - PK11_OriginUnwrap, - CKA_ENCRYPT, - &symKeyData, - NULL); - if (!symKey) { - NS_WARNING("PK11_ImportSymKey failed"); - rv = NS_ERROR_FAILURE; - goto wrap_done; - } - - - // Step 3. Put the public key bits into a P11 key object. - - // Can't just do this directly, it's expecting a minimal ASN1 blob - // pubKey = SECKEY_ImportDERPublicKey(&pubKeyData, CKK_RSA); - pubKeyInfo = SECKEY_DecodeDERSubjectPublicKeyInfo(&pubKeyData); - if (!pubKeyInfo) { - NS_WARNING("SECKEY_DecodeDERSubjectPublicKeyInfo failed"); - rv = NS_ERROR_FAILURE; - goto wrap_done; - } - - pubKey = SECKEY_ExtractPublicKey(pubKeyInfo); - if (!pubKey) { - NS_WARNING("SECKEY_ExtractPublicKey failed"); - rv = NS_ERROR_FAILURE; - goto wrap_done; - } - - - // Step 4. Wrap the symmetric key with the public key. - - wrapMech = PK11_AlgtagToMechanism(SEC_OID_PKCS1_RSA_ENCRYPTION); - - s = PK11_PubWrapSymKey(wrapMech, pubKey, symKey, &wrappedKey); - if (s != SECSuccess) { - NS_WARNING("PK11_PubWrapSymKey failed"); - rv = NS_ERROR_FAILURE; - goto wrap_done; - } - - - // Step 5. Base64 encode the wrapped key, cleanup, and return to caller. - - rv = EncodeBase64((char *)wrappedKey.data, wrappedKey.len, aWrappedKey); - if (NS_FAILED(rv)) { - NS_WARNING("EncodeBase64 failed"); - goto wrap_done; - } - -wrap_done: - if (pubKey) - SECKEY_DestroyPublicKey(pubKey); - if (pubKeyInfo) - SECKEY_DestroySubjectPublicKeyInfo(pubKeyInfo); - if (symKey) - PK11_FreeSymKey(symKey); - if (slot) - PK11_FreeSlot(slot); - - return rv; -} - - -/* - * UnwrapSymmetricKey - */ -NS_IMETHODIMP -WeaveCrypto::UnwrapSymmetricKey(const nsACString& aWrappedSymmetricKey, - const nsACString& aWrappedPrivateKey, - const nsACString& aPassphrase, - const nsACString& aSalt, - const nsACString& aIV, - nsACString& aSymmetricKey) -{ - nsresult rv = NS_OK; - PK11SlotInfo *slot = nsnull; - PK11SymKey *pbeKey = nsnull; - PK11SymKey *symKey = nsnull; - SECKEYPrivateKey *privKey = nsnull; - SECItem *ivParam = nsnull; - SECItem *symKeyData = nsnull; - SECItem *keyID = nsnull; - - CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; - PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); - - - // Step 1. Get rid of the base64 encoding on the inputs. - - char privateKeyBuffer[STACK_BUFFER_SIZE]; - PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); - rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; - - char wrappedKeyBuffer[STACK_BUFFER_SIZE]; - PRUint32 wrappedKeyBufferSize = sizeof(wrappedKeyBuffer); - rv = DecodeBase64(aWrappedSymmetricKey, wrappedKeyBuffer, &wrappedKeyBufferSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem wrappedSymKey = {siBuffer, (unsigned char *)wrappedKeyBuffer, wrappedKeyBufferSize}; - - - // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. - rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); - NS_ENSURE_SUCCESS(rv, rv); - - char ivData[STACK_BUFFER_SIZE]; - PRUint32 ivDataSize = sizeof(ivData); - rv = DecodeBase64(aIV, ivData, &ivDataSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; - - // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD - CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); - wrapMech = PK11_GetPadMechanism(wrapMech); - if (wrapMech == CKM_INVALID_MECHANISM) { - NS_WARNING("Unknown key mechanism"); - rv = NS_ERROR_FAILURE; - goto unwrap_done; - } - - ivParam = PK11_ParamFromIV(wrapMech, &ivItem); - if (!ivParam) { - NS_WARNING("Couldn't create IV param"); - rv = NS_ERROR_FAILURE; - goto unwrap_done; - } - - - // Step 3. Unwrap the private key with the key from the passphrase. - - slot = PK11_GetInternalSlot(); - if (!slot) { - NS_WARNING("Can't get internal PK11 slot"); - rv = NS_ERROR_FAILURE; - goto unwrap_done; - } - - - // Normally, one wants to associate a private key with a public key. - // P11_UnwrapPrivKey() passes its keyID arg to PK11_MakeIDFromPubKey(), - // which hashes the public key to create an ID (or, for small inputs, - // assumes it's already hashed and does nothing). - // We don't really care about this, because our unwrapped private key will - // just live long enough to unwrap the bulk data key. So, we'll just jam in - // a random value... We have an IV handy, so that will suffice. - keyID = &ivItem; - - privKey = PK11_UnwrapPrivKey(slot, - pbeKey, wrapMech, ivParam, &wrappedPrivKey, - nsnull, // label (SECItem) - keyID, - PR_FALSE, // isPerm (token object) - PR_TRUE, // isSensitive - CKK_RSA, - privKeyUsage, privKeyUsageLength, - nsnull); // wincx - if (!privKey) { - NS_WARNING("PK11_UnwrapPrivKey failed"); - rv = NS_ERROR_FAILURE; - goto unwrap_done; - } - - // Step 4. Unwrap the symmetric key with the user's private key. - - // XXX also have PK11_PubUnwrapSymKeyWithFlags() if more control is needed. - // (last arg is keySize, 0 seems to work) - symKey = PK11_PubUnwrapSymKey(privKey, &wrappedSymKey, wrapMech, - CKA_DECRYPT, 0); - if (!symKey) { - NS_WARNING("PK11_PubUnwrapSymKey failed"); - rv = NS_ERROR_FAILURE; - goto unwrap_done; - } - - // Step 5. Base64 encode the unwrapped key, cleanup, and return to caller. - - if (PK11_ExtractKeyValue(symKey)) { - NS_WARNING("PK11_ExtractKeyValue failed"); - rv = NS_ERROR_FAILURE; - goto unwrap_done; - } - - // XXX need to free this? - symKeyData = PK11_GetKeyData(symKey); - if (!symKeyData) { - NS_WARNING("PK11_GetKeyData failed"); - rv = NS_ERROR_FAILURE; - goto unwrap_done; - } - - rv = EncodeBase64((char *)symKeyData->data, symKeyData->len, aSymmetricKey); - if (NS_FAILED(rv)) { - NS_WARNING("EncodeBase64 failed"); - goto unwrap_done; - } - -unwrap_done: - if (privKey) - SECKEY_DestroyPrivateKey(privKey); - if (symKey) - PK11_FreeSymKey(symKey); - if (pbeKey) - PK11_FreeSymKey(pbeKey); - if (slot) - PK11_FreeSlot(slot); - if (ivParam) - SECITEM_FreeItem(ivParam, PR_TRUE); - - return rv; -} - -/* - * RewrapPrivateKey - */ -NS_IMETHODIMP -WeaveCrypto::RewrapPrivateKey(const nsACString& aWrappedPrivateKey, - const nsACString& aPassphrase, - const nsACString& aSalt, - const nsACString& aIV, - const nsACString& aNewPassphrase, - nsACString& aPrivateKey) -{ - nsresult rv = NS_OK; - PK11SlotInfo *slot = nsnull; - PK11SymKey *pbeKey = nsnull; - SECKEYPrivateKey *privKey = nsnull; - SECItem *ivParam = nsnull; - SECItem *keyID = nsnull; - - CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; - PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); - - // Step 1. Get rid of the base64 encoding on the inputs. - char privateKeyBuffer[STACK_BUFFER_SIZE]; - PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); - rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; - - - // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. - rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); - NS_ENSURE_SUCCESS(rv, rv); - - char ivData[STACK_BUFFER_SIZE]; - PRUint32 ivDataSize = sizeof(ivData); - rv = DecodeBase64(aIV, ivData, &ivDataSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; - - // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD - CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); - wrapMech = PK11_GetPadMechanism(wrapMech); - if (wrapMech == CKM_INVALID_MECHANISM) { - NS_WARNING("Unknown key mechanism"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - - ivParam = PK11_ParamFromIV(wrapMech, &ivItem); - if (!ivParam) { - NS_WARNING("Couldn't create IV param"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - - // Step 3. Unwrap the private key with the key from the passphrase. - slot = PK11_GetInternalSlot(); - if (!slot) { - NS_WARNING("Can't get internal PK11 slot"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - - keyID = &ivItem; - privKey = PK11_UnwrapPrivKey(slot, - pbeKey, wrapMech, ivParam, &wrappedPrivKey, - nsnull, - keyID, - PR_FALSE, - PR_TRUE, - CKK_RSA, - privKeyUsage, privKeyUsageLength, - nsnull); - if (!privKey) { - NS_WARNING("PK11_UnwrapPrivKey failed"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - - // Step 4. Rewrap the private key with the new passphrase. - rv = WrapPrivateKey(privKey, aNewPassphrase, aSalt, aIV, aPrivateKey); - if (NS_FAILED(rv)) { - NS_WARNING("RewrapPrivateKey failed"); - rv = NS_ERROR_FAILURE; - goto rewrap_done; - } - -rewrap_done: - if (privKey) - SECKEY_DestroyPrivateKey(privKey); - if (pbeKey) - PK11_FreeSymKey(pbeKey); - if (slot) - PK11_FreeSlot(slot); - if (ivParam) - SECITEM_FreeItem(ivParam, PR_TRUE); - - return rv; -} - -/* - * VerifyPassphrase - */ -NS_IMETHODIMP -WeaveCrypto::VerifyPassphrase(const nsACString& aWrappedPrivateKey, - const nsACString& aPassphrase, - const nsACString& aSalt, - const nsACString& aIV, - PRBool *result) -{ - *result = PR_FALSE; - nsresult rv = NS_OK; - PK11SlotInfo *slot = nsnull; - PK11SymKey *pbeKey = nsnull; - SECKEYPrivateKey *privKey = nsnull; - SECItem *ivParam = nsnull; - SECItem *keyID = nsnull; - - CK_ATTRIBUTE_TYPE privKeyUsage[] = { CKA_UNWRAP }; - PRUint32 privKeyUsageLength = sizeof(privKeyUsage) / sizeof(CK_ATTRIBUTE_TYPE); - - // Step 1. Get rid of the base64 encoding on the input. - char privateKeyBuffer[STACK_BUFFER_SIZE]; - PRUint32 privateKeyBufferSize = sizeof(privateKeyBuffer); - rv = DecodeBase64(aWrappedPrivateKey, privateKeyBuffer, &privateKeyBufferSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem wrappedPrivKey = {siBuffer, (unsigned char *)privateKeyBuffer, privateKeyBufferSize}; - - // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. - rv = DeriveKeyFromPassphrase(aPassphrase, aSalt, &pbeKey); - NS_ENSURE_SUCCESS(rv, rv); - - char ivData[STACK_BUFFER_SIZE]; - PRUint32 ivDataSize = sizeof(ivData); - rv = DecodeBase64(aIV, ivData, &ivDataSize); - NS_ENSURE_SUCCESS(rv, rv); - SECItem ivItem = {siBuffer, (unsigned char*)ivData, ivDataSize}; - - // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD - CK_MECHANISM_TYPE wrapMech = PK11_AlgtagToMechanism(mAlgorithm); - wrapMech = PK11_GetPadMechanism(wrapMech); - if (wrapMech == CKM_INVALID_MECHANISM) { - NS_WARNING("Unknown key mechanism"); - rv = NS_ERROR_FAILURE; - goto verify_done; - } - - ivParam = PK11_ParamFromIV(wrapMech, &ivItem); - if (!ivParam) { - NS_WARNING("Couldn't create IV param"); - rv = NS_ERROR_FAILURE; - goto verify_done; - } - - // Step 3. Unwrap the private key with the key from the passphrase. - slot = PK11_GetInternalSlot(); - if (!slot) { - NS_WARNING("Can't get internal PK11 slot"); - rv = NS_ERROR_FAILURE; - goto verify_done; - } - - keyID = &ivItem; - privKey = PK11_UnwrapPrivKey(slot, - pbeKey, wrapMech, ivParam, &wrappedPrivKey, - nsnull, - keyID, - PR_FALSE, - PR_TRUE, - CKK_RSA, - privKeyUsage, privKeyUsageLength, - nsnull); - if (!privKey) { - NS_WARNING("PK11_UnwrapPrivKey failed"); - } else { - *result = PR_TRUE; - } - -verify_done: - if (privKey) - SECKEY_DestroyPrivateKey(privKey); - if (pbeKey) - PK11_FreeSymKey(pbeKey); - if (slot) - PK11_FreeSlot(slot); - if (ivParam) - SECITEM_FreeItem(ivParam, PR_TRUE); - - return rv; -} diff --git a/services/crypto/WeaveCrypto.h b/services/crypto/WeaveCrypto.h deleted file mode 100644 index 1e45f5f37308..000000000000 --- a/services/crypto/WeaveCrypto.h +++ /dev/null @@ -1,92 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is WeaveCrypto code. - * - * The Initial Developer of the Original Code is - * Mozilla Corporation - * Portions created by the Initial Developer are Copyright (C) 2007 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills (original author) - * Honza Bambas - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -#ifndef WeaveCrypto_h_ -#define WeaveCrypto_h_ - -#include "IWeaveCrypto.h" -#include "pk11pub.h" - -#define WEAVE_CRYPTO_CONTRACTID "@labs.mozilla.com/Weave/Crypto;1" -#define WEAVE_CRYPTO_CLASSNAME "Weave crypto module" -#define WEAVE_CRYPTO_CID { 0xd3b0f750, 0xc976, 0x46d0, \ - { 0xbe, 0x20, 0x96, 0xb2, 0x4f, 0x46, 0x84, 0xbc } } - -class WeaveCrypto : public IWeaveCrypto -{ -public: - WeaveCrypto(); - - NS_DECL_ISUPPORTS - NS_DECL_IWEAVECRYPTO - -private: - ~WeaveCrypto(); - - SECOidTag mAlgorithm; - PRUint32 mKeypairBits; - - nsresult DecodeBase64(const nsACString& base64, nsACString& retval); - nsresult DecodeBase64(const nsACString& base64, char *aData, PRUint32 *aLength); - nsresult EncodeBase64(const nsACString& binary, nsACString& retval); - nsresult EncodeBase64(const char *aData, PRUint32 aLength, nsACString& retval); - - nsresult CommonCrypt(const char *input, PRUint32 inputSize, - char *output, PRUint32 *outputSize, - const nsACString& aSymmetricKey, - const nsACString& aIV, - CK_ATTRIBUTE_TYPE aOperation); - - - nsresult DeriveKeyFromPassphrase(const nsACString& aPassphrase, - const nsACString& aSalt, - PK11SymKey **aSymKey); - - nsresult WrapPrivateKey(SECKEYPrivateKey *aPrivateKey, - const nsACString& aPassphrase, - const nsACString& aSalt, - const nsACString& aIV, - nsACString& aEncodedPublicKey); - nsresult EncodePublicKey(SECKEYPublicKey *aPublicKey, - nsACString& aEncodedPublicKey); - - -}; - -#endif // WeaveCrypto_h_ diff --git a/services/crypto/WeaveCrypto.js b/services/crypto/WeaveCrypto.js new file mode 100644 index 000000000000..831db5ca2e2d --- /dev/null +++ b/services/crypto/WeaveCrypto.js @@ -0,0 +1,1126 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Justin Dolske (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/ctypes.jsm"); + +function WeaveCrypto() { + this.init(); +} + +WeaveCrypto.prototype = { + classDescription: "WeaveCrypto", + contractID: "@labs.mozilla.com/Weave/Crypto;2", + classID: Components.ID("{7fa20841-c90e-4432-a1a1-ba3b20cb6b37}"), + QueryInterface: XPCOMUtils.generateQI([Ci.IWeaveCrypto]), + + prefBranch : null, + debug : true, // extensions.weave.log.cryptoDebug + nss : null, + nss_t : null, + + observer : { + _self : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + observe : function (subject, topic, data) { + let self = this._self; + self.log("Observed " + topic + " topic."); + if (topic == "nsPref:changed") { + self.debug = self.prefBranch.getBoolPref("cryptoDebug"); + } + } + }, + + init : function() { + try { + // Preferences. Add observer so we get notified of changes. + this.prefBranch = Services.prefs.getBranch("extensions.weave.log."); + this.prefBranch.QueryInterface(Ci.nsIPrefBranch2); + this.prefBranch.addObserver("cryptoDebug", this.observer, false); + this.observer._self = this; + this.debug = this.prefBranch.getBoolPref("cryptoDebug"); + + this.initNSS(); + } catch (e) { + this.log("init failed: " + e); + throw e; + } + }, + + log : function (message) { + if (!this.debug) + return; + dump("WeaveCrypto: " + message + "\n"); + Services.console.logStringMessage("WeaveCrypto: " + message); + }, + + initNSS : function() { + // We use NSS for the crypto ops, which needs to be initialized before + // use. By convention, PSM is required to be the module that + // initializes NSS. So, make sure PSM is initialized in order to + // implicitly initialize NSS. + Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + + // Open the NSS library. + let nssfile = Services.dirsvc.get("GreD", Ci.nsILocalFile); + let os = Services.appinfo.OS; + switch (os) { + case "WINNT": + case "WINMO": + case "WINCE": + nssfile.append("nss3.dll"); + break; + case "Darwin": + nssfile.append("libnss3.dylib"); + break; + case "Linux": + case "SunOS": + case "WebOS": // Palm Pre + nssfile.append("libnss3.so"); + break; + case "Android": + // Android uses a $GREDIR/lib/ subdir. + nssfile.append("lib"); + nssfile.append("libnss3.so"); + break; + default: + throw Components.Exception("unsupported platform: " + os, Cr.NS_ERROR_UNEXPECTED); + } + this.log("Using NSS library " + nssfile.path); + + // XXX really want to be able to pass specific dlopen flags here. + let nsslib = ctypes.open(nssfile.path); + + this.log("Initializing NSS types and function declarations..."); + + this.nss = {}; + this.nss_t = {}; + + // nsprpub/pr/include/prtypes.h#435 + // typedef PRIntn PRBool; --> int + this.nss_t.PRBool = ctypes.int; + // security/nss/lib/util/seccomon.h#91 + // typedef enum + this.nss_t.SECStatus = ctypes.int; + // security/nss/lib/softoken/secmodt.h#59 + // typedef struct PK11SlotInfoStr PK11SlotInfo; (defined in secmodti.h) + this.nss_t.PK11SlotInfo = ctypes.void_t; + // security/nss/lib/util/pkcs11t.h + this.nss_t.CK_MECHANISM_TYPE = ctypes.unsigned_long; + this.nss_t.CK_ATTRIBUTE_TYPE = ctypes.unsigned_long; + this.nss_t.CK_KEY_TYPE = ctypes.unsigned_long; + this.nss_t.CK_OBJECT_HANDLE = ctypes.unsigned_long; + // security/nss/lib/softoken/secmodt.h#359 + // typedef enum PK11Origin + this.nss_t.PK11Origin = ctypes.int; + // PK11Origin enum values... + this.nss.PK11_OriginUnwrap = 4; + // security/nss/lib/softoken/secmodt.h#61 + // typedef struct PK11SymKeyStr PK11SymKey; (defined in secmodti.h) + this.nss_t.PK11SymKey = ctypes.void_t; + // security/nss/lib/util/secoidt.h#454 + // typedef enum + this.nss_t.SECOidTag = ctypes.int; + // security/nss/lib/util/seccomon.h#64 + // typedef enum + this.nss_t.SECItemType = ctypes.int; + // SECItemType enum values... + this.nss.SIBUFFER = 0; + // security/nss/lib/softoken/secmodt.h#62 (defined in secmodti.h) + // typedef struct PK11ContextStr PK11Context; + this.nss_t.PK11Context = ctypes.void_t; + // Needed for SECKEYPrivateKey struct def'n, but I don't think we need to actually access it. + this.nss_t.PLArenaPool = ctypes.void_t; + // security/nss/lib/cryptohi/keythi.h#45 + // typedef enum + this.nss_t.KeyType = ctypes.int; + // security/nss/lib/softoken/secmodt.h#201 + // typedef PRUint32 PK11AttrFlags; + this.nss_t.PK11AttrFlags = ctypes.unsigned_int; + // security/nss/lib/util/secoidt.h#454 + // typedef enum + this.nss_t.SECOidTag = ctypes.int; + // security/nss/lib/util/seccomon.h#83 + // typedef struct SECItemStr SECItem; --> SECItemStr defined right below it + this.nss_t.SECItem = ctypes.StructType( + "SECItem", [{ type: this.nss_t.SECItemType }, + { data: ctypes.unsigned_char.ptr }, + { len : ctypes.int }]); + // security/nss/lib/softoken/secmodt.h#65 + // typedef struct PK11RSAGenParamsStr --> def'n on line 139 + this.nss_t.PK11RSAGenParams = ctypes.StructType( + "PK11RSAGenParams", [{ keySizeInBits: ctypes.int }, + { pe : ctypes.unsigned_long }]); + // security/nss/lib/cryptohi/keythi.h#233 + // typedef struct SECKEYPrivateKeyStr SECKEYPrivateKey; --> def'n right above it + this.nss_t.SECKEYPrivateKey = ctypes.StructType( + "SECKEYPrivateKey", [{ arena: this.nss_t.PLArenaPool.ptr }, + { keyType: this.nss_t.KeyType }, + { pkcs11Slot: this.nss_t.PK11SlotInfo.ptr }, + { pkcs11ID: this.nss_t.CK_OBJECT_HANDLE }, + { pkcs11IsTemp: this.nss_t.PRBool }, + { wincx: ctypes.voidptr_t }, + { staticflags: ctypes.unsigned_int }]); + // security/nss/lib/cryptohi/keythi.h#78 + // typedef struct SECKEYRSAPublicKeyStr --> def'n right above it + this.nss_t.SECKEYRSAPublicKey = ctypes.StructType( + "SECKEYRSAPublicKey", [{ arena: this.nss_t.PLArenaPool.ptr }, + { modulus: this.nss_t.SECItem }, + { publicExponent: this.nss_t.SECItem }]); + // security/nss/lib/cryptohi/keythi.h#189 + // typedef struct SECKEYPublicKeyStr SECKEYPublicKey; --> def'n right above it + this.nss_t.SECKEYPublicKey = ctypes.StructType( + "SECKEYPublicKey", [{ arena: this.nss_t.PLArenaPool.ptr }, + { keyType: this.nss_t.KeyType }, + { pkcs11Slot: this.nss_t.PK11SlotInfo.ptr }, + { pkcs11ID: this.nss_t.CK_OBJECT_HANDLE }, + { rsa: this.nss_t.SECKEYRSAPublicKey } ]); + // XXX: "rsa" et al into a union here! + // { dsa: SECKEYDSAPublicKey }, + // { dh: SECKEYDHPublicKey }, + // { kea: SECKEYKEAPublicKey }, + // { fortezza: SECKEYFortezzaPublicKey }, + // { ec: SECKEYECPublicKey } ]); + // security/nss/lib/util/secoidt.h#52 + // typedef struct SECAlgorithmIDStr --> def'n right below it + this.nss_t.SECAlgorithmID = ctypes.StructType( + "SECAlgorithmID", [{ algorithm: this.nss_t.SECItem }, + { parameters: this.nss_t.SECItem }]); + // security/nss/lib/certdb/certt.h#98 + // typedef struct CERTSubjectPublicKeyInfoStrA --> def'n on line 160 + this.nss_t.CERTSubjectPublicKeyInfo = ctypes.StructType( + "CERTSubjectPublicKeyInfo", [{ arena: this.nss_t.PLArenaPool.ptr }, + { algorithm: this.nss_t.SECAlgorithmID }, + { subjectPublicKey: this.nss_t.SECItem }]); + + + // security/nss/lib/util/pkcs11t.h + this.nss.CKK_RSA = 0x0; + this.nss.CKM_RSA_PKCS_KEY_PAIR_GEN = 0x0000; + this.nss.CKM_AES_KEY_GEN = 0x1080; + this.nss.CKA_ENCRYPT = 0x104; + this.nss.CKA_DECRYPT = 0x105; + this.nss.CKA_UNWRAP = 0x107; + + // security/nss/lib/softoken/secmodt.h + this.nss.PK11_ATTR_SESSION = 0x02; + this.nss.PK11_ATTR_PUBLIC = 0x08; + this.nss.PK11_ATTR_SENSITIVE = 0x40; + + // security/nss/lib/util/secoidt.h + this.nss.SEC_OID_HMAC_SHA1 = 294; + this.nss.SEC_OID_PKCS1_RSA_ENCRYPTION = 16; + + + // security/nss/lib/pk11wrap/pk11pub.h#286 + // SECStatus PK11_GenerateRandom(unsigned char *data,int len); + this.nss.PK11_GenerateRandom = nsslib.declare("PK11_GenerateRandom", + ctypes.default_abi, this.nss_t.SECStatus, + ctypes.unsigned_char.ptr, ctypes.int); + // security/nss/lib/pk11wrap/pk11pub.h#74 + // PK11SlotInfo *PK11_GetInternalSlot(void); + this.nss.PK11_GetInternalSlot = nsslib.declare("PK11_GetInternalSlot", + ctypes.default_abi, this.nss_t.PK11SlotInfo.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#73 + // PK11SlotInfo *PK11_GetInternalKeySlot(void); + this.nss.PK11_GetInternalKeySlot = nsslib.declare("PK11_GetInternalKeySlot", + ctypes.default_abi, this.nss_t.PK11SlotInfo.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#328 + // PK11SymKey *PK11_KeyGen(PK11SlotInfo *slot,CK_MECHANISM_TYPE type, SECItem *param, int keySize,void *wincx); + this.nss.PK11_KeyGen = nsslib.declare("PK11_KeyGen", + ctypes.default_abi, this.nss_t.PK11SymKey.ptr, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.CK_MECHANISM_TYPE, + this.nss_t.SECItem.ptr, ctypes.int, ctypes.voidptr_t); + // security/nss/lib/pk11wrap/pk11pub.h#477 + // SECStatus PK11_ExtractKeyValue(PK11SymKey *symKey); + this.nss.PK11_ExtractKeyValue = nsslib.declare("PK11_ExtractKeyValue", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.PK11SymKey.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#478 + // SECItem * PK11_GetKeyData(PK11SymKey *symKey); + this.nss.PK11_GetKeyData = nsslib.declare("PK11_GetKeyData", + ctypes.default_abi, this.nss_t.SECItem.ptr, + this.nss_t.PK11SymKey.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#278 + // CK_MECHANISM_TYPE PK11_AlgtagToMechanism(SECOidTag algTag); + this.nss.PK11_AlgtagToMechanism = nsslib.declare("PK11_AlgtagToMechanism", + ctypes.default_abi, this.nss_t.CK_MECHANISM_TYPE, + this.nss_t.SECOidTag); + // security/nss/lib/pk11wrap/pk11pub.h#270 + // int PK11_GetIVLength(CK_MECHANISM_TYPE type); + this.nss.PK11_GetIVLength = nsslib.declare("PK11_GetIVLength", + ctypes.default_abi, ctypes.int, + this.nss_t.CK_MECHANISM_TYPE); + // security/nss/lib/pk11wrap/pk11pub.h#269 + // int PK11_GetBlockSize(CK_MECHANISM_TYPE type,SECItem *params); + this.nss.PK11_GetBlockSize = nsslib.declare("PK11_GetBlockSize", + ctypes.default_abi, ctypes.int, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#293 + // CK_MECHANISM_TYPE PK11_GetPadMechanism(CK_MECHANISM_TYPE); + this.nss.PK11_GetPadMechanism = nsslib.declare("PK11_GetPadMechanism", + ctypes.default_abi, this.nss_t.CK_MECHANISM_TYPE, + this.nss_t.CK_MECHANISM_TYPE); + // security/nss/lib/pk11wrap/pk11pub.h#271 + // SECItem *PK11_ParamFromIV(CK_MECHANISM_TYPE type,SECItem *iv); + this.nss.PK11_ParamFromIV = nsslib.declare("PK11_ParamFromIV", + ctypes.default_abi, this.nss_t.SECItem.ptr, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#301 + // PK11SymKey *PK11_ImportSymKey(PK11SlotInfo *slot, CK_MECHANISM_TYPE type, PK11Origin origin, + // CK_ATTRIBUTE_TYPE operation, SECItem *key, void *wincx); + this.nss.PK11_ImportSymKey = nsslib.declare("PK11_ImportSymKey", + ctypes.default_abi, this.nss_t.PK11SymKey.ptr, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.CK_MECHANISM_TYPE, this.nss_t.PK11Origin, + this.nss_t.CK_ATTRIBUTE_TYPE, this.nss_t.SECItem.ptr, ctypes.voidptr_t); + // security/nss/lib/pk11wrap/pk11pub.h#672 + // PK11Context *PK11_CreateContextBySymKey(CK_MECHANISM_TYPE type, CK_ATTRIBUTE_TYPE operation, + // PK11SymKey *symKey, SECItem *param); + this.nss.PK11_CreateContextBySymKey = nsslib.declare("PK11_CreateContextBySymKey", + ctypes.default_abi, this.nss_t.PK11Context.ptr, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.CK_ATTRIBUTE_TYPE, + this.nss_t.PK11SymKey.ptr, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#685 + // SECStatus PK11_CipherOp(PK11Context *context, unsigned char *out + // int *outlen, int maxout, unsigned char *in, int inlen); + this.nss.PK11_CipherOp = nsslib.declare("PK11_CipherOp", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.PK11Context.ptr, ctypes.unsigned_char.ptr, + ctypes.int.ptr, ctypes.int, ctypes.unsigned_char.ptr, ctypes.int); + // security/nss/lib/pk11wrap/pk11pub.h#688 + // SECStatus PK11_DigestFinal(PK11Context *context, unsigned char *data, + // unsigned int *outLen, unsigned int length); + this.nss.PK11_DigestFinal = nsslib.declare("PK11_DigestFinal", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.PK11Context.ptr, ctypes.unsigned_char.ptr, + ctypes.unsigned_int.ptr, ctypes.unsigned_int); + // security/nss/lib/pk11wrap/pk11pub.h#507 + // SECKEYPrivateKey *PK11_GenerateKeyPairWithFlags(PK11SlotInfo *slot, + // CK_MECHANISM_TYPE type, void *param, SECKEYPublicKey **pubk, + // PK11AttrFlags attrFlags, void *wincx); + this.nss.PK11_GenerateKeyPairWithFlags = nsslib.declare("PK11_GenerateKeyPairWithFlags", + ctypes.default_abi, this.nss_t.SECKEYPrivateKey.ptr, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.CK_MECHANISM_TYPE, ctypes.voidptr_t, + this.nss_t.SECKEYPublicKey.ptr.ptr, this.nss_t.PK11AttrFlags, ctypes.voidptr_t); + // security/nss/lib/pk11wrap/pk11pub.h#466 + // SECStatus PK11_SetPrivateKeyNickname(SECKEYPrivateKey *privKey, const char *nickname); + this.nss.PK11_SetPrivateKeyNickname = nsslib.declare("PK11_SetPrivateKeyNickname", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.SECKEYPrivateKey.ptr, ctypes.char.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#731 + // SECAlgorithmID * PK11_CreatePBEV2AlgorithmID(SECOidTag pbeAlgTag, SECOidTag cipherAlgTag, + // SECOidTag prfAlgTag, int keyLength, int iteration, + // SECItem *salt); + this.nss.PK11_CreatePBEV2AlgorithmID = nsslib.declare("PK11_CreatePBEV2AlgorithmID", + ctypes.default_abi, this.nss_t.SECAlgorithmID.ptr, + this.nss_t.SECOidTag, this.nss_t.SECOidTag, this.nss_t.SECOidTag, + ctypes.int, ctypes.int, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#736 + // PK11SymKey * PK11_PBEKeyGen(PK11SlotInfo *slot, SECAlgorithmID *algid, SECItem *pwitem, PRBool faulty3DES, void *wincx); + this.nss.PK11_PBEKeyGen = nsslib.declare("PK11_PBEKeyGen", + ctypes.default_abi, this.nss_t.PK11SymKey.ptr, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.SECAlgorithmID.ptr, + this.nss_t.SECItem.ptr, this.nss_t.PRBool, ctypes.voidptr_t); + // security/nss/lib/pk11wrap/pk11pub.h#574 + // SECStatus PK11_WrapPrivKey(PK11SlotInfo *slot, PK11SymKey *wrappingKey, + // SECKEYPrivateKey *privKey, CK_MECHANISM_TYPE wrapType, + // SECItem *param, SECItem *wrappedKey, void *wincx); + this.nss.PK11_WrapPrivKey = nsslib.declare("PK11_WrapPrivKey", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.PK11SymKey.ptr, + this.nss_t.SECKEYPrivateKey.ptr, this.nss_t.CK_MECHANISM_TYPE, + this.nss_t.SECItem.ptr, this.nss_t.SECItem.ptr, ctypes.voidptr_t); + // security/nss/lib/cryptohi/keyhi.h#159 + // SECItem* SECKEY_EncodeDERSubjectPublicKeyInfo(SECKEYPublicKey *pubk); + this.nss.SECKEY_EncodeDERSubjectPublicKeyInfo = nsslib.declare("SECKEY_EncodeDERSubjectPublicKeyInfo", + ctypes.default_abi, this.nss_t.SECItem.ptr, + this.nss_t.SECKEYPublicKey.ptr); + // security/nss/lib/cryptohi/keyhi.h#165 + // CERTSubjectPublicKeyInfo * SECKEY_DecodeDERSubjectPublicKeyInfo(SECItem *spkider); + this.nss.SECKEY_DecodeDERSubjectPublicKeyInfo = nsslib.declare("SECKEY_DecodeDERSubjectPublicKeyInfo", + ctypes.default_abi, this.nss_t.CERTSubjectPublicKeyInfo.ptr, + this.nss_t.SECItem.ptr); + // security/nss/lib/cryptohi/keyhi.h#179 + // SECKEYPublicKey * SECKEY_ExtractPublicKey(CERTSubjectPublicKeyInfo *); + this.nss.SECKEY_ExtractPublicKey = nsslib.declare("SECKEY_ExtractPublicKey", + ctypes.default_abi, this.nss_t.SECKEYPublicKey.ptr, + this.nss_t.CERTSubjectPublicKeyInfo.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#377 + // SECStatus PK11_PubWrapSymKey(CK_MECHANISM_TYPE type, SECKEYPublicKey *pubKey, + // PK11SymKey *symKey, SECItem *wrappedKey); + this.nss.PK11_PubWrapSymKey = nsslib.declare("PK11_PubWrapSymKey", + ctypes.default_abi, this.nss_t.SECStatus, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.SECKEYPublicKey.ptr, + this.nss_t.PK11SymKey.ptr, this.nss_t.SECItem.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#568 + // SECKEYPrivateKey *PK11_UnwrapPrivKey(PK11SlotInfo *slot, + // PK11SymKey *wrappingKey, CK_MECHANISM_TYPE wrapType, + // SECItem *param, SECItem *wrappedKey, SECItem *label, + // SECItem *publicValue, PRBool token, PRBool sensitive, + // CK_KEY_TYPE keyType, CK_ATTRIBUTE_TYPE *usage, int usageCount, + // void *wincx); + this.nss.PK11_UnwrapPrivKey = nsslib.declare("PK11_UnwrapPrivKey", + ctypes.default_abi, this.nss_t.SECKEYPrivateKey.ptr, + this.nss_t.PK11SlotInfo.ptr, this.nss_t.PK11SymKey.ptr, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.SECItem.ptr, + this.nss_t.SECItem.ptr, this.nss_t.SECItem.ptr, + this.nss_t.SECItem.ptr, this.nss_t.PRBool, + this.nss_t.PRBool, this.nss_t.CK_KEY_TYPE, + this.nss_t.CK_ATTRIBUTE_TYPE.ptr, ctypes.int, + ctypes.voidptr_t); + // security/nss/lib/pk11wrap/pk11pub.h#447 + // PK11SymKey *PK11_PubUnwrapSymKey(SECKEYPrivateKey *key, SECItem *wrapppedKey, + // CK_MECHANISM_TYPE target, CK_ATTRIBUTE_TYPE operation, int keySize); + this.nss.PK11_PubUnwrapSymKey = nsslib.declare("PK11_PubUnwrapSymKey", + ctypes.default_abi, this.nss_t.PK11SymKey.ptr, + this.nss_t.SECKEYPrivateKey.ptr, this.nss_t.SECItem.ptr, + this.nss_t.CK_MECHANISM_TYPE, this.nss_t.CK_ATTRIBUTE_TYPE, ctypes.int); + // security/nss/lib/pk11wrap/pk11pub.h#675 + // void PK11_DestroyContext(PK11Context *context, PRBool freeit); + this.nss.PK11_DestroyContext = nsslib.declare("PK11_DestroyContext", + ctypes.default_abi, ctypes.void_t, + this.nss_t.PK11Context.ptr, this.nss_t.PRBool); + // security/nss/lib/pk11wrap/pk11pub.h#299 + // void PK11_FreeSymKey(PK11SymKey *key); + this.nss.PK11_FreeSymKey = nsslib.declare("PK11_FreeSymKey", + ctypes.default_abi, ctypes.void_t, + this.nss_t.PK11SymKey.ptr); + // security/nss/lib/pk11wrap/pk11pub.h#70 + // void PK11_FreeSlot(PK11SlotInfo *slot); + this.nss.PK11_FreeSlot = nsslib.declare("PK11_FreeSlot", + ctypes.default_abi, ctypes.void_t, + this.nss_t.PK11SlotInfo.ptr); + // security/nss/lib/util/secitem.h#114 + // extern void SECITEM_FreeItem(SECItem *zap, PRBool freeit); + this.nss.SECITEM_FreeItem = nsslib.declare("SECITEM_FreeItem", + ctypes.default_abi, ctypes.void_t, + this.nss_t.SECItem.ptr, this.nss_t.PRBool); + // security/nss/lib/cryptohi/keyhi.h#193 + // extern void SECKEY_DestroyPublicKey(SECKEYPublicKey *key); + this.nss.SECKEY_DestroyPublicKey = nsslib.declare("SECKEY_DestroyPublicKey", + ctypes.default_abi, ctypes.void_t, + this.nss_t.SECKEYPublicKey.ptr); + // security/nss/lib/cryptohi/keyhi.h#186 + // extern void SECKEY_DestroyPrivateKey(SECKEYPrivateKey *key); + this.nss.SECKEY_DestroyPrivateKey = nsslib.declare("SECKEY_DestroyPrivateKey", + ctypes.default_abi, ctypes.void_t, + this.nss_t.SECKEYPrivateKey.ptr); + // security/nss/lib/util/secoid.h#103 + // extern void SECOID_DestroyAlgorithmID(SECAlgorithmID *aid, PRBool freeit); + this.nss.SECOID_DestroyAlgorithmID = nsslib.declare("SECOID_DestroyAlgorithmID", + ctypes.default_abi, ctypes.void_t, + this.nss_t.SECAlgorithmID.ptr, this.nss_t.PRBool); + // security/nss/lib/cryptohi/keyhi.h#58 + // extern void SECKEY_DestroySubjectPublicKeyInfo(CERTSubjectPublicKeyInfo *spki); + this.nss.SECKEY_DestroySubjectPublicKeyInfo = nsslib.declare("SECKEY_DestroySubjectPublicKeyInfo", + ctypes.default_abi, ctypes.void_t, + this.nss_t.CERTSubjectPublicKeyInfo.ptr); + }, + + + // + // IWeaveCrypto interfaces + // + + + algorithm : Ci.IWeaveCrypto.AES_256_CBC, + + keypairBits : 2048, + + encrypt : function(clearTextUCS2, symmetricKey, iv) { + this.log("encrypt() called"); + + // js-ctypes autoconverts to a UTF8 buffer, but also includes a null + // at the end which we don't want. Cast to make the length 1 byte shorter. + let inputBuffer = new ctypes.ArrayType(ctypes.unsigned_char)(clearTextUCS2); + inputBuffer = ctypes.cast(inputBuffer, ctypes.unsigned_char.array(inputBuffer.length - 1)); + + // When using CBC padding, the output size is the input size rounded + // up to the nearest block. If the input size is exactly on a block + // boundary, the output is 1 extra block long. + let mech = this.nss.PK11_AlgtagToMechanism(this.algorithm); + let blockSize = this.nss.PK11_GetBlockSize(mech, null); + let outputBufferSize = inputBuffer.length + blockSize; + let outputBuffer = new ctypes.ArrayType(ctypes.unsigned_char, outputBufferSize)(); + + outputBuffer = this._commonCrypt(inputBuffer, outputBuffer, symmetricKey, iv, this.nss.CKA_ENCRYPT); + + return this.encodeBase64(outputBuffer.address(), outputBuffer.length); + }, + + + decrypt : function(cipherText, symmetricKey, iv) { + this.log("decrypt() called"); + + let inputUCS2 = ""; + if (cipherText.length) + inputUCS2 = atob(cipherText); + + // We can't have js-ctypes create the buffer directly from the string + // (as in encrypt()), because we do _not_ want it to do UTF8 + // conversion... We've got random binary data in the input's low byte. + let input = new ctypes.ArrayType(ctypes.unsigned_char, inputUCS2.length)(); + this.byteCompress(inputUCS2, input); + + let outputBuffer = new ctypes.ArrayType(ctypes.unsigned_char, input.length)(); + + outputBuffer = this._commonCrypt(input, outputBuffer, symmetricKey, iv, this.nss.CKA_DECRYPT); + + // outputBuffer contains UTF-8 data, let js-ctypes autoconvert that to a JS string. + return outputBuffer.readString(); + }, + + + _commonCrypt : function (input, output, symmetricKey, iv, operation) { + this.log("_commonCrypt() called"); + // Get rid of the base64 encoding and convert to SECItems. + let keyItem = this.makeSECItem(symmetricKey, true); + let ivItem = this.makeSECItem(iv, true); + + // Determine which (padded) PKCS#11 mechanism to use. + // EG: AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + let mechanism = this.nss.PK11_AlgtagToMechanism(this.algorithm); + mechanism = this.nss.PK11_GetPadMechanism(mechanism); + if (mechanism == this.nss.CKM_INVALID_MECHANISM) + throw Components.Exception("invalid algorithm (can't pad)", Cr.NS_ERROR_FAILURE); + + let ctx, symKey, slot, ivParam; + try { + ivParam = this.nss.PK11_ParamFromIV(mechanism, ivItem.address()); + if (ivParam.isNull()) + throw Components.Exception("can't convert IV to param", Cr.NS_ERROR_FAILURE); + + slot = this.nss.PK11_GetInternalKeySlot(); + if (slot.isNull()) + throw Components.Exception("can't get internal key slot", Cr.NS_ERROR_FAILURE); + + symKey = this.nss.PK11_ImportSymKey(slot, mechanism, this.nss.PK11_OriginUnwrap, operation, keyItem.address(), null); + if (symKey.isNull()) + throw Components.Exception("symkey import failed", Cr.NS_ERROR_FAILURE); + + ctx = this.nss.PK11_CreateContextBySymKey(mechanism, operation, symKey, ivParam); + if (ctx.isNull()) + throw Components.Exception("couldn't create context for symkey", Cr.NS_ERROR_FAILURE); + + let maxOutputSize = output.length; + let tmpOutputSize = new ctypes.int(); // Note 1: NSS uses a signed int here... + + if (this.nss.PK11_CipherOp(ctx, output, tmpOutputSize.address(), maxOutputSize, input, input.length)) + throw Components.Exception("cipher operation failed", Cr.NS_ERROR_FAILURE); + + let actualOutputSize = tmpOutputSize.value; + let finalOutput = output.addressOfElement(actualOutputSize); + maxOutputSize -= actualOutputSize; + + // PK11_DigestFinal sure sounds like the last step for *hashing*, but it + // just seems to be an odd name -- NSS uses this to finish the current + // cipher operation. You'd think it would be called PK11_CipherOpFinal... + let tmpOutputSize2 = new ctypes.unsigned_int(); // Note 2: ...but an unsigned here! + if (this.nss.PK11_DigestFinal(ctx, finalOutput, tmpOutputSize2.address(), maxOutputSize)) + throw Components.Exception("cipher finalize failed", Cr.NS_ERROR_FAILURE); + + actualOutputSize += tmpOutputSize2.value; + let newOutput = ctypes.cast(output, ctypes.unsigned_char.array(actualOutputSize)); + return newOutput; + } catch (e) { + this.log("_commonCrypt: failed: " + e); + throw e; + } finally { + if (ctx && !ctx.isNull()) + this.nss.PK11_DestroyContext(ctx, true); + if (symKey && !symKey.isNull()) + this.nss.PK11_FreeSymKey(symKey); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + if (ivParam && !ivParam.isNull()) + this.nss.SECITEM_FreeItem(ivParam, true); + } + }, + + + generateKeypair : function(passphrase, salt, iv, out_encodedPublicKey, out_wrappedPrivateKey) { + this.log("generateKeypair() called."); + + let pubKey, privKey, slot; + try { + // Attributes for the private key. We're just going to wrap and extract the + // value, so they're not critical. The _PUBLIC attribute just indicates the + // object can be accessed without being logged into the token. + let attrFlags = (this.nss.PK11_ATTR_SESSION | this.nss.PK11_ATTR_PUBLIC | this.nss.PK11_ATTR_SENSITIVE); + + pubKey = new this.nss_t.SECKEYPublicKey.ptr(); + + let rsaParams = new this.nss_t.PK11RSAGenParams(); + rsaParams.keySizeInBits = this.keypairBits; // 1024, 2048, etc. + rsaParams.pe = 65537; // public exponent. + + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + // Generate the keypair. + privKey = this.nss.PK11_GenerateKeyPairWithFlags(slot, + this.nss.CKM_RSA_PKCS_KEY_PAIR_GEN, + rsaParams.address(), + pubKey.address(), + attrFlags, null); + if (privKey.isNull()) + throw Components.Exception("keypair generation failed", Cr.NS_ERROR_FAILURE); + + let s = this.nss.PK11_SetPrivateKeyNickname(privKey, "Weave User PrivKey"); + if (s) + throw Components.Exception("key nickname failed", Cr.NS_ERROR_FAILURE); + + let wrappedPrivateKey = this._wrapPrivateKey(privKey, passphrase, salt, iv); + out_wrappedPrivateKey.value = wrappedPrivateKey; // outparam + + let derKey = this.nss.SECKEY_EncodeDERSubjectPublicKeyInfo(pubKey); + if (derKey.isNull()) + throw Components.Exception("SECKEY_EncodeDERSubjectPublicKeyInfo failed", Cr.NS_ERROR_FAILURE); + + let encodedPublicKey = this.encodeBase64(derKey.contents.data, derKey.contents.len); + out_encodedPublicKey.value = encodedPublicKey; // outparam + } catch (e) { + this.log("generateKeypair: failed: " + e); + throw e; + } finally { + if (pubKey && !pubKey.isNull()) + this.nss.SECKEY_DestroyPublicKey(pubKey); + if (privKey && !privKey.isNull()) + this.nss.SECKEY_DestroyPrivateKey(privKey); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + } + }, + + + generateRandomKey : function() { + this.log("generateRandomKey() called"); + let encodedKey, keygenMech, keySize; + + // Doesn't NSS have a lookup function to do this? + switch(this.algorithm) { + case Ci.IWeaveCrypto.AES_128_CBC: + keygenMech = this.nss.CKM_AES_KEY_GEN; + keySize = 16; + break; + + case Ci.IWeaveCrypto.AES_192_CBC: + keygenMech = this.nss.CKM_AES_KEY_GEN; + keySize = 24; + break; + + case Ci.IWeaveCrypto.AES_256_CBC: + keygenMech = this.nss.CKM_AES_KEY_GEN; + keySize = 32; + break; + + default: + throw Components.Exception("unknown algorithm", Cr.NS_ERROR_FAILURE); + } + + let slot, randKey, keydata; + try { + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + randKey = this.nss.PK11_KeyGen(slot, keygenMech, null, keySize, null); + if (randKey.isNull()) + throw Components.Exception("PK11_KeyGen failed.", Cr.NS_ERROR_FAILURE); + + // Slightly odd API, this call just prepares the key value for + // extraction, we get the actual bits from the call to PK11_GetKeyData(). + if (this.nss.PK11_ExtractKeyValue(randKey)) + throw Components.Exception("PK11_ExtractKeyValue failed.", Cr.NS_ERROR_FAILURE); + + keydata = this.nss.PK11_GetKeyData(randKey); + if (keydata.isNull()) + throw Components.Exception("PK11_GetKeyData failed.", Cr.NS_ERROR_FAILURE); + + return this.encodeBase64(keydata.contents.data, keydata.contents.len); + } catch (e) { + this.log("generateRandomKey: failed: " + e); + throw e; + } finally { + if (randKey && !randKey.isNull()) + this.nss.PK11_FreeSymKey(randKey); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + } + }, + + + generateRandomIV : function() { + this.log("generateRandomIV() called"); + + let mech = this.nss.PK11_AlgtagToMechanism(this.algorithm); + let size = this.nss.PK11_GetIVLength(mech); + + return this.generateRandomBytes(size); + }, + + + generateRandomBytes : function(byteCount) { + this.log("generateRandomBytes() called"); + + // Temporary buffer to hold the generated data. + let scratch = new ctypes.ArrayType(ctypes.unsigned_char, byteCount)(); + if (this.nss.PK11_GenerateRandom(scratch, byteCount)) + throw Components.Exception("PK11_GenrateRandom failed", Cr.NS_ERROR_FAILURE); + + return this.encodeBase64(scratch.address(), scratch.length); + }, + + + wrapSymmetricKey : function(symmetricKey, encodedPublicKey) { + this.log("wrapSymmetricKey() called"); + + // Step 1. Get rid of the base64 encoding on the inputs. + + let pubKeyData = this.makeSECItem(encodedPublicKey, true); + let symKeyData = this.makeSECItem(symmetricKey, true); + + // This buffer is much larger than needed, but that's ok. + let keyData = new ctypes.ArrayType(ctypes.unsigned_char, 4096)(); + let wrappedKey = new this.nss_t.SECItem(this.nss.SIBUFFER, keyData, keyData.length); + + // Step 2. Put the symmetric key bits into a P11 key object. + let slot, symKey, pubKeyInfo, pubKey; + try { + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + // ImportSymKey wants a mechanism, from which it derives the key type. + let keyMech = this.nss.PK11_AlgtagToMechanism(this.algorithm); + + // This imports a key with the usage set for encryption, but that doesn't + // really matter because we're just going to wrap it up and not use it. + symKey = this.nss.PK11_ImportSymKey(slot, keyMech, this.nss.PK11_OriginUnwrap, this.nss.CKA_ENCRYPT, symKeyData.address(), null); + if (symKey.isNull()) + throw Components.Exception("symkey import failed", Cr.NS_ERROR_FAILURE); + + // Step 3. Put the public key bits into a P11 key object. + + // Can't just do this directly, it's expecting a minimal ASN1 blob + // pubKey = SECKEY_ImportDERPublicKey(&pubKeyData, CKK_RSA); + pubKeyInfo = this.nss.SECKEY_DecodeDERSubjectPublicKeyInfo(pubKeyData.address()); + if (pubKeyInfo.isNull()) + throw Components.Exception("SECKEY_DecodeDERSubjectPublicKeyInfo failed", Cr.NS_ERROR_FAILURE); + + pubKey = this.nss.SECKEY_ExtractPublicKey(pubKeyInfo); + if (pubKey.isNull()) + throw Components.Exception("SECKEY_ExtractPublicKey failed", Cr.NS_ERROR_FAILURE); + + // Step 4. Wrap the symmetric key with the public key. + + let wrapMech = this.nss.PK11_AlgtagToMechanism(this.nss.SEC_OID_PKCS1_RSA_ENCRYPTION); + + let s = this.nss.PK11_PubWrapSymKey(wrapMech, pubKey, symKey, wrappedKey.address()); + if (s) + throw Components.Exception("PK11_PubWrapSymKey failed", Cr.NS_ERROR_FAILURE); + + // Step 5. Base64 encode the wrapped key, cleanup, and return to caller. + return this.encodeBase64(wrappedKey.data, wrappedKey.len); + } catch (e) { + this.log("wrapSymmetricKey: failed: " + e); + throw e; + } finally { + if (pubKey && !pubKey.isNull()) + this.nss.SECKEY_DestroyPublicKey(pubKey); + if (pubKeyInfo && !pubKeyInfo.isNull()) + this.nss.SECKEY_DestroySubjectPublicKeyInfo(pubKeyInfo); + if (symKey && !symKey.isNull()) + this.nss.PK11_FreeSymKey(symKey); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + } + }, + + + unwrapSymmetricKey : function(wrappedSymmetricKey, wrappedPrivateKey, passphrase, salt, iv) { + this.log("unwrapSymmetricKey() called"); + let privKeyUsageLength = 1; + let privKeyUsage = new ctypes.ArrayType(this.nss_t.CK_ATTRIBUTE_TYPE, privKeyUsageLength)(); + privKeyUsage[0] = this.nss.CKA_UNWRAP; + + // Step 1. Get rid of the base64 encoding on the inputs. + let wrappedPrivKey = this.makeSECItem(wrappedPrivateKey, true); + let wrappedSymKey = this.makeSECItem(wrappedSymmetricKey, true); + + let ivParam, slot, pbeKey, symKey, privKey, symKeyData; + try { + // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. + pbeKey = this._deriveKeyFromPassphrase(passphrase, salt); + let ivItem = this.makeSECItem(iv, true); + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + let wrapMech = this.nss.PK11_AlgtagToMechanism(this.algorithm); + wrapMech = this.nss.PK11_GetPadMechanism(wrapMech); + if (wrapMech == this.nss.CKM_INVALID_MECHANISM) + throw Components.Exception("unwrapSymKey: unknown key mech", Cr.NS_ERROR_FAILURE); + + ivParam = this.nss.PK11_ParamFromIV(wrapMech, ivItem.address()); + if (ivParam.isNull()) + throw Components.Exception("unwrapSymKey: PK11_ParamFromIV failed", Cr.NS_ERROR_FAILURE); + + // Step 3. Unwrap the private key with the key from the passphrase. + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + // Normally, one wants to associate a private key with a public key. + // P11_UnwrapPrivKey() passes its keyID arg to PK11_MakeIDFromPubKey(), + // which hashes the public key to create an ID (or, for small inputs, + // assumes it's already hashed and does nothing). + // We don't really care about this, because our unwrapped private key will + // just live long enough to unwrap the bulk data key. So, we'll just jam in + // a random value... We have an IV handy, so that will suffice. + let keyID = ivItem.address(); + + privKey = this.nss.PK11_UnwrapPrivKey(slot, + pbeKey, wrapMech, ivParam, wrappedPrivKey.address(), + null, // label + keyID, + false, // isPerm (token object) + true, // isSensitive + this.nss.CKK_RSA, + privKeyUsage.addressOfElement(0), privKeyUsageLength, + null); // wincx + if (privKey.isNull()) + throw Components.Exception("PK11_UnwrapPrivKey failed", Cr.NS_ERROR_FAILURE); + + // Step 4. Unwrap the symmetric key with the user's private key. + + // XXX also have PK11_PubUnwrapSymKeyWithFlags() if more control is needed. + // (last arg is keySize, 0 seems to work) + symKey = this.nss.PK11_PubUnwrapSymKey(privKey, wrappedSymKey.address(), wrapMech, + this.nss.CKA_DECRYPT, 0); + if (symKey.isNull()) + throw Components.Exception("PK11_PubUnwrapSymKey failed", Cr.NS_ERROR_FAILURE); + + // Step 5. Base64 encode the unwrapped key, cleanup, and return to caller. + if (this.nss.PK11_ExtractKeyValue(symKey)) + throw Components.Exception("PK11_ExtractKeyValue failed.", Cr.NS_ERROR_FAILURE); + + symKeyData = this.nss.PK11_GetKeyData(symKey); + if (symKeyData.isNull()) + throw Components.Exception("PK11_GetKeyData failed.", Cr.NS_ERROR_FAILURE); + + return this.encodeBase64(symKeyData.contents.data, symKeyData.contents.len); + } catch (e) { + this.log("unwrapSymmetricKey: failed: " + e); + throw e; + } finally { + if (privKey && !privKey.isNull()) + this.nss.SECKEY_DestroyPrivateKey(privKey); + if (symKey && !symKey.isNull()) + this.nss.PK11_FreeSymKey(symKey); + if (pbeKey && !pbeKey.isNull()) + this.nss.PK11_FreeSymKey(pbeKey); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + if (ivParam && !ivParam.isNull()) + this.nss.SECITEM_FreeItem(ivParam, true); + } + }, + + + rewrapPrivateKey : function(wrappedPrivateKey, oldPassphrase, salt, iv, newPassphrase) { + this.log("rewrapPrivateKey() called"); + let privKeyUsageLength = 1; + let privKeyUsage = new ctypes.ArrayType(this.nss_t.CK_ATTRIBUTE_TYPE, privKeyUsageLength)(); + privKeyUsage[0] = this.nss.CKA_UNWRAP; + + // Step 1. Get rid of the base64 encoding on the inputs. + let wrappedPrivKey = this.makeSECItem(wrappedPrivateKey, true); + + let pbeKey, ivParam, slot, privKey; + try { + // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. + let pbeKey = this._deriveKeyFromPassphrase(oldPassphrase, salt); + let ivItem = this.makeSECItem(iv, true); + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + let wrapMech = this.nss.PK11_AlgtagToMechanism(this.algorithm); + wrapMech = this.nss.PK11_GetPadMechanism(wrapMech); + if (wrapMech == this.nss.CKM_INVALID_MECHANISM) + throw Components.Exception("rewrapSymKey: unknown key mech", Cr.NS_ERROR_FAILURE); + + ivParam = this.nss.PK11_ParamFromIV(wrapMech, ivItem.address()); + if (ivParam.isNull()) + throw Components.Exception("rewrapSymKey: PK11_ParamFromIV failed", Cr.NS_ERROR_FAILURE); + + // Step 3. Unwrap the private key with the key from the passphrase. + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + let keyID = ivItem.address(); + + privKey = this.nss.PK11_UnwrapPrivKey(slot, + pbeKey, wrapMech, ivParam, wrappedPrivKey.address(), + null, // label + keyID, + false, // isPerm (token object) + true, // isSensitive + this.nss.CKK_RSA, + privKeyUsage.addressOfElement(0), privKeyUsageLength, + null); // wincx + if (privKey.isNull()) + throw Components.Exception("PK11_UnwrapPrivKey failed", Cr.NS_ERROR_FAILURE); + + // Step 4. Rewrap the private key with the new passphrase. + return this._wrapPrivateKey(privKey, newPassphrase, salt, iv); + } catch (e) { + this.log("rewrapPrivateKey: failed: " + e); + throw e; + } finally { + if (privKey && !privKey.isNull()) + this.nss.SECKEY_DestroyPrivateKey(privKey); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + if (ivParam && !ivParam.isNull()) + this.nss.SECITEM_FreeItem(ivParam, true); + if (pbeKey && !pbeKey.isNull()) + this.nss.PK11_FreeSymKey(pbeKey); + } + }, + + + verifyPassphrase : function(wrappedPrivateKey, passphrase, salt, iv) { + this.log("verifyPassphrase() called"); + let privKeyUsageLength = 1; + let privKeyUsage = new ctypes.ArrayType(this.nss_t.CK_ATTRIBUTE_TYPE, privKeyUsageLength)(); + privKeyUsage[0] = this.nss.CKA_UNWRAP; + + // Step 1. Get rid of the base64 encoding on the inputs. + let wrappedPrivKey = this.makeSECItem(wrappedPrivateKey, true); + + let pbeKey, ivParam, slot, privKey; + try { + // Step 2. Convert the passphrase to a symmetric key and get the IV in the proper form. + pbeKey = this._deriveKeyFromPassphrase(passphrase, salt); + let ivItem = this.makeSECItem(iv, true); + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + let wrapMech = this.nss.PK11_AlgtagToMechanism(this.algorithm); + wrapMech = this.nss.PK11_GetPadMechanism(wrapMech); + if (wrapMech == this.nss.CKM_INVALID_MECHANISM) + throw Components.Exception("rewrapSymKey: unknown key mech", Cr.NS_ERROR_FAILURE); + + ivParam = this.nss.PK11_ParamFromIV(wrapMech, ivItem.address()); + if (ivParam.isNull()) + throw Components.Exception("rewrapSymKey: PK11_ParamFromIV failed", Cr.NS_ERROR_FAILURE); + + // Step 3. Unwrap the private key with the key from the passphrase. + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + let keyID = ivItem.address(); + + privKey = this.nss.PK11_UnwrapPrivKey(slot, + pbeKey, wrapMech, ivParam, wrappedPrivKey.address(), + null, // label + keyID, + false, // isPerm (token object) + true, // isSensitive + this.nss.CKK_RSA, + privKeyUsage.addressOfElement(0), privKeyUsageLength, + null); // wincx + return (!privKey.isNull()); + } catch (e) { + this.log("verifyPassphrase: failed: " + e); + throw e; + } finally { + if (privKey && !privKey.isNull()) + this.nss.SECKEY_DestroyPrivateKey(privKey); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + if (ivParam && !ivParam.isNull()) + this.nss.SECITEM_FreeItem(ivParam, true); + if (pbeKey && !pbeKey.isNull()) + this.nss.PK11_FreeSymKey(pbeKey); + } + }, + + + // + // Utility functions + // + + + // Compress a JS string (2-byte chars) into a normal C string (1-byte chars) + // EG, for "ABC", 0x0041, 0x0042, 0x0043 --> 0x41, 0x42, 0x43 + byteCompress : function (jsString, charArray) { + let intArray = ctypes.cast(charArray, ctypes.uint8_t.array(charArray.length)); + for (let i = 0; i < jsString.length; i++) + intArray[i] = jsString.charCodeAt(i); + }, + + // Expand a normal C string (1-byte chars) into a JS string (2-byte chars) + // EG, for "ABC", 0x41, 0x42, 0x43 --> 0x0041, 0x0042, 0x0043 + byteExpand : function (charArray) { + let expanded = ""; + let len = charArray.length; + let intData = ctypes.cast(charArray, ctypes.uint8_t.array(len)); + for (let i = 0; i < len; i++) + expanded += String.fromCharCode(intData[i]); + return expanded; + }, + + encodeBase64 : function (data, len) { + // Byte-expand the buffer, so we can treat it as a UCS-2 string + // consisting of u0000 - u00FF. + let expanded = ""; + let intData = ctypes.cast(data, ctypes.uint8_t.array(len).ptr).contents; + for (let i = 0; i < len; i++) + expanded += String.fromCharCode(intData[i]); + return btoa(expanded); + }, + + + makeSECItem : function(input, isEncoded) { + if (isEncoded) + input = atob(input); + let outputData = new ctypes.ArrayType(ctypes.unsigned_char, input.length)(); + this.byteCompress(input, outputData); + + return new this.nss_t.SECItem(this.nss.SIBUFFER, outputData, outputData.length); + }, + + _deriveKeyFromPassphrase : function (passphrase, salt) { + this.log("_deriveKeyFromPassphrase() called."); + let passItem = this.makeSECItem(passphrase, false); + let saltItem = this.makeSECItem(salt, true); + + // http://mxr.mozilla.org/seamonkey/source/security/nss/lib/pk11wrap/pk11pbe.c#1261 + + // Bug 436577 prevents us from just using SEC_OID_PKCS5_PBKDF2 here + let pbeAlg = this.algorithm; + let cipherAlg = this.algorithm; // ignored by callee when pbeAlg != a pkcs5 mech. + let prfAlg = this.nss.SEC_OID_HMAC_SHA1; // callee picks if SEC_OID_UNKNOWN, but only SHA1 is supported + + let keyLength = 0; // Callee will pick. + let iterations = 4096; // PKCS#5 recommends at least 1000. + + let algid, slot, symKey; + try { + algid = this.nss.PK11_CreatePBEV2AlgorithmID(pbeAlg, cipherAlg, prfAlg, + keyLength, iterations, saltItem.address()); + if (algid.isNull()) + throw Components.Exception("PK11_CreatePBEV2AlgorithmID failed", Cr.NS_ERROR_FAILURE); + + slot = this.nss.PK11_GetInternalSlot(); + if (slot.isNull()) + throw Components.Exception("couldn't get internal slot", Cr.NS_ERROR_FAILURE); + + symKey = this.nss.PK11_PBEKeyGen(slot, algid, passItem.address(), false, null); + if (symKey.isNull()) + throw Components.Exception("PK11_PBEKeyGen failed", Cr.NS_ERROR_FAILURE); + } catch (e) { + this.log("_deriveKeyFromPassphrase: failed: " + e); + throw e; + } finally { + if (algid && !algid.isNull()) + this.nss.SECOID_DestroyAlgorithmID(algid, true); + if (slot && !slot.isNull()) + this.nss.PK11_FreeSlot(slot); + } + + return symKey; + }, + + + _wrapPrivateKey : function(privKey, passphrase, salt, iv) { + this.log("_wrapPrivateKey() called."); + let ivParam, pbeKey, wrappedKey; + try { + // Convert our passphrase to a symkey and get the IV in the form we want. + pbeKey = this._deriveKeyFromPassphrase(passphrase, salt); + + let ivItem = this.makeSECItem(iv, true); + + // AES_128_CBC --> CKM_AES_CBC --> CKM_AES_CBC_PAD + let wrapMech = this.nss.PK11_AlgtagToMechanism(this.algorithm); + wrapMech = this.nss.PK11_GetPadMechanism(wrapMech); + if (wrapMech == this.nss.CKM_INVALID_MECHANISM) + throw Components.Exception("wrapPrivKey: unknown key mech", Cr.NS_ERROR_FAILURE); + + let ivParam = this.nss.PK11_ParamFromIV(wrapMech, ivItem.address()); + if (ivParam.isNull()) + throw Components.Exception("wrapPrivKey: PK11_ParamFromIV failed", Cr.NS_ERROR_FAILURE); + + // Use a buffer to hold the wrapped key. NSS says about 1200 bytes for + // a 2048-bit RSA key, so a 4096 byte buffer should be plenty. + let keyData = new ctypes.ArrayType(ctypes.unsigned_char, 4096)(); + wrappedKey = new this.nss_t.SECItem(this.nss.SIBUFFER, keyData, keyData.length); + + let s = this.nss.PK11_WrapPrivKey(privKey.contents.pkcs11Slot, + pbeKey, privKey, + wrapMech, ivParam, + wrappedKey.address(), null); + if (s) + throw Components.Exception("wrapPrivKey: PK11_WrapPrivKey failed", Cr.NS_ERROR_FAILURE); + + return this.encodeBase64(wrappedKey.data, wrappedKey.len); + } catch (e) { + this.log("_wrapPrivateKey: failed: " + e); + throw e; + } finally { + if (ivParam && !ivParam.isNull()) + this.nss.SECITEM_FreeItem(ivParam, true); + if (pbeKey && !pbeKey.isNull()) + this.nss.PK11_FreeSymKey(pbeKey); + } + } +}; + +let component = [WeaveCrypto]; +function NSGetModule (compMgr, fileSpec) { + return XPCOMUtils.generateModule(component); +} diff --git a/services/crypto/WeaveCrypto.rc.in b/services/crypto/WeaveCrypto.rc.in deleted file mode 100644 index 3f67f00b9ec3..000000000000 --- a/services/crypto/WeaveCrypto.rc.in +++ /dev/null @@ -1,94 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Weave code. - * - * The Initial Developer of the Original Code is - * Mozilla Corporation - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills (original author) - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -/* See http://msdn.microsoft.com/en-us/library/aa381058.aspx for format docs, - * and mozilla/config/version_win.pl for what Mozilla uses - */ - -#include - -#define VER_BUILDID_STR "@buildid@" -#define VER_FILEVERSION 1,9,0,@buildid_short@ -#define VER_PRODUCTVERSION 1,9,0,@buildid_short@ - -#define VER_FILEFLAGS 0 | VS_FF_PRIVATEBUILD | VS_FF_PRERELEASE - -#define VER_PRODUCTNAME_STR "Weave" -#define VER_INTERNALNAME_STR "WeaveCrypto" -#define VER_FILEVERSION_STR "1.9.0.@buildid_short@" -#define VER_PRODUCTVERSION_STR "1.9.0.@buildid_short@" - -#define VER_COMPANYNAME_STR "Mozilla Corporation" -#define VER_LEGALTRADEMARKS_STR "Mozilla" -#define VER_LEGALCOPYRIGHT_STR "License: MPL 1.1/GPL 2.0/LGPL 2.1" - -#define VER_COMMENTS_STR "" -#define VER_FILEDESCRIPTION_STR "" -#define VER_ORIGINALFILENAME_STR "" - -VS_VERSION_INFO VERSIONINFO -FILEVERSION VER_FILEVERSION -PRODUCTVERSION VER_PRODUCTVERSION -FILEFLAGSMASK 0x3fL -FILEFLAGS VER_FILEFLAGS -FILEOS VOS__WINDOWS32 -FILETYPE VFT_DLL -FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "000004b0" - BEGIN - VALUE "Comments", VER_COMMENTS_STR - VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR - VALUE "CompanyName", VER_COMPANYNAME_STR - VALUE "FileDescription", VER_FILEDESCRIPTION_STR - VALUE "FileVersion", VER_FILEVERSION_STR - VALUE "ProductVersion", VER_PRODUCTVERSION_STR - VALUE "InternalName", VER_INTERNALNAME_STR - VALUE "LegalTrademarks", VER_LEGALTRADEMARKS_STR - VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR - VALUE "ProductName", VER_PRODUCTNAME_STR - VALUE "BuildID", VER_BUILDID_STR - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x0, 1200 - END -END diff --git a/services/crypto/WeaveCryptoModule.cpp b/services/crypto/WeaveCryptoModule.cpp deleted file mode 100644 index 16a7315df900..000000000000 --- a/services/crypto/WeaveCryptoModule.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Weave code. - * - * The Initial Developer of the Original Code is - * Mozilla Corporation - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills (original author) - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -#include "nsIGenericFactory.h" -#include "WeaveCrypto.h" - -NS_GENERIC_FACTORY_CONSTRUCTOR(WeaveCrypto) - -static nsModuleComponentInfo components[] = -{ - { - WEAVE_CRYPTO_CLASSNAME, - WEAVE_CRYPTO_CID, - WEAVE_CRYPTO_CONTRACTID, - WeaveCryptoConstructor, - } -}; - -NS_IMPL_NSGETMODULE(WeaveCryptoModule, components) diff --git a/services/crypto/platform/Darwin/components/WeaveCrypto.dylib b/services/crypto/platform/Darwin/components/WeaveCrypto.dylib deleted file mode 100755 index c1cd643278a53a75787c97f19758c3b10df1a2fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65944 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 F{|yNn1T+8u diff --git a/services/crypto/platform/Linux/components/WeaveCrypto.so b/services/crypto/platform/Linux/components/WeaveCrypto.so deleted file mode 100755 index 7a161e9740fc8543b3bbf8437075c06d9b32f186..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85574 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 diff --git a/services/crypto/platform/Linux_x86-gcc3/components/WeaveCrypto.so b/services/crypto/platform/Linux_x86-gcc3/components/WeaveCrypto.so deleted file mode 100755 index 7a45ec7e7414ecd9b479882e959c4afdd753f843..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55737 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 U8QM9p|J!J$!Ua97HC=!358sx&lK=n! diff --git a/services/crypto/platform/Linux_x86_64-gcc3/components/WeaveCrypto.so b/services/crypto/platform/Linux_x86_64-gcc3/components/WeaveCrypto.so deleted file mode 100644 index 2e79b67ca8e4d5152072066153acc66995960dc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73368 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 diff --git a/services/crypto/platform/WINCE/components/WeaveCrypto.dll b/services/crypto/platform/WINCE/components/WeaveCrypto.dll deleted file mode 100755 index b2cddef1bf6dcf58b70b4f5a690dc750f9073270..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33280 zcmeHw4}4VBo$r}skRe9AsZmp!+KUY~(g1fR0VWXO0s#XC7)j9ROG}1iNK%K%IGJFu zw2pw9Rnm=0E$xa-w?5ZrSvRk>Ep@Svw(cvnw9B@At!>@cN#b-dxNTl*mF?QH-|xBi zOm40Lwfg${KFjbqoO}NL&hP*4cg~$8>uwql`9cUU%9%4l97fHLDgQtBe`6S){=TQC zizlajcjjSF{dZ?Jc64^@@kCd9BD_s+4aZ_#NqtL1PxQp}&X``ip+Vo))fSn1(M1Ji zHt3tpPi>nw^WT=L^6=b8md-)@NB&2beh{_4`y)%sKyoo5b)-8}T3UBds>xc#hA?q=A4+~>g z)@%3Y8R;Pp#)Nh-`_&7?s1_F**ZGWrER-5B?~v<#fXOz;3SDD`jS+p7Axp92{EYPv=kd?anVe(VsVV$J0Kqo*tT_rH7^hc1MvoWdMe6 zCg?}s_$*>9HK;%T_~VbCM(suWHPmC~oEJ4kBk1#l z(nGz=2}{wpFWWYBFiRZRC(MVlq|rN;9;)+zMjdKsup%G!G^M!fWlx0E` zl={c#j>CQcYeFONUb8Rrr$;<%c8^qnPh;er4c9q0Wu!7}zyG@Q(2u8xQ^z>ZwDGd% zTl@!H%>5^$LE^kbG>mvctMZ=38Y!1^toc6HaK11e%yP{X(J=JmGiMexyKoYryCfgP z7oO^hpY7dVPB>{TWI!Y|KiM>^b6G zX8?~f>{X7vDvbVYX{bM|>;2g_O`O_{!o58rIRcsHL#FxbD1WVgm^3$VJU%$2#^4kS z*0K-wrTP}3FK1`EnZ9fx;hGkop~D4Ahx4JsLf@a&j04$xV^>BPeXz&tM?6~k62K_>-NoWP zLfi=0>7w7r=S?G^tLP>9e1n_^doOwcZQ`HzEaY%>?+A5a>@S}_dHm;R&d@g{zi#VC z9CKPM+ZCzwP!sI8P$*s7m#ww!mwitA74Wm{D}xO4p}QqTu%{4gm$qcym-Rtr9507_ zmCa+#5M@r?qu%*|pP_>{8hjaKGWv&c-7BX)iTzn$=s?y}0ho}bk?9{UF`>+L#d;lkc}>V5)t}8=Z0SWYc>5DR!#@_`nQH`aU|oj2E#H(LI?g%3k2>Pf zIOp!{H_=BPeo~Ho3UYvL!yb_$%uw8@X%d$-mcy1`3!-vvFo%VU+SC9d0)KoIa zKn5oEO@XG6MHlQ%+T3&b3hgHMBpoHrlyG~KpE z&;z~?m+Rn>2Kxi-KG3wAbhXglw$qPVG)>z*auswC^Q6vh=N#DFt7py>P#%S_aWMd! zh72n*pBR~*>Cbo`2fqf>BgK%-1>m6%I2RA}!%iTVbbr=2Xv-zupZz%DnO009pBJHB zC=O&z_KU`me?D_&2JN>I_FD@4ORyhln{CsD*qcwCu2B4iPX7=(Z6Fqrhvc6J^E)KG zkmX0F?ant+Py2j2;*5p@90~JM2TY8gj01+}r|X9E2!pU%P!}|gAg=C4T;1*YY5%Tx z>ghhpgE71Tdyv-`fOk^>)2pN3=Gz~BZ^?o9V%;#-<=}e&mw3`oUIiGdh%5Fx*Iq%# zUgWXbi*$@kvKMXKi`eW*VXrv$GCq|(4E-&D4LpS!I2Rm2?esl3B8=*0nth`ssF7xk zYN)3XKls3xp#!uV+7Eds^z_Jz_oe|WJ*0Dv!w_;qvUz@@9bi19?H^l8J z*hk662e`~vo&Zhc3DzKELk=sjWOCS%vkdGV;Sp)_nL10IHeR|@qa>0`3= zGxRajwe-;UZJ3O09|Ar;_*m>A{fZ8nJRh9hQpZ&T6M!kX| z0K;A*GT`3~w6#0ni(wmUrR`P^z`Vk9;_7E2ARK-CZYf7M_-($Z+E;t5Bxje&u$P~wp0uA_c zJ$%maR2}gQun%+G%CB;k1>t<4X{D8{Epr!ziTG3lUuzF~y%s1DD> za|Pg1hWW7JoR7Tw>60h^(ejCX&b-Bh99t~?_0&M`Lg|sIncX9vrz~DHLaww$#+VB5 z6>^tz=%304`?J?OWOX-WRc6b(G1EU>OL^zu+$-fxx~Y3Lkc|m=gt5v21IITBBY$@P zaIb#9)SVFEG-mFVey|+;V?IC_b%0S181Hw)w5?d5j~rvC?Yk+@yxHlIMmwkCI++_e z_PmjMN2vikK`NunV{aEWj=*GD_ab{UQC&p;71xU6FHRA#+E>Km@D%r z59bY7KK)&MPFc3i7(rX9!MqZ{^Z`EnS(d)tlYyQ==Fqnx(n!0|2Q9lOM$On$h}w&? zgfO978gz?tn*w~ft`2Z%#{zQ<&VgQ1|E8d?kZp&b@Y-!B4e(i``G^fE#0KRr$g|JA zX88)GgO@sV@bQbNk2k^ZrHlSXJ~xd3_oB3Xz7aN>zJ$5dezcWdHacR%c#$i!I1KP4F&`{uFH8#*x0K&7q5Rwk}e4E(DLJfVZ?0 zrGpPRbm4le%X_?kIGX;9%%Qj+Wl^VpLh3KqQGfP3_F9wEpHEBusWbXsr#p9EBu=GJ zr0%3%uRC)cx|85M_!;H{yZLO#g|q5T$kH9=)YJhV@TCqgS7Xjrj9TgrYA?zYgbBSU zgkCToPr_F6U~h5Q+l7Ro^=B22M7lqF$iW}vWjSA^e9-+4nSVe&TYgHH&*S-Wi7kKT z7IhB!A7;LcTd`nIrzz3-h&tKgWg)|19%j5_!FfI%9mW~*az`tf-k8v?tPsuZQAi z7mmZ+`WiYMicNuj=LAym%J;g+nqlLHgg) z$Ba#7sCizSqFa8DvYO-I#U1Tizqlr!!w0z7To=AHjT-<7;VOm?L!P19(xW<6Z&!OdJfiz3aBdD3d5C*n z(lECC>62FgehRwAvwX98Fxv=xu>Ytq_DMKcR|Fi2yyl+aDWY-cH11X_Ft6-Q%ri_2 zehJ_gqv$B5D2fL{TsI<8>7kPbcozR=_I;q?IlvXX%LJahJ9?%CZQSj8&6UVw$MK?^ z$7zD>%-UhxWemM?=FDu|Ny`{QUn7jWp!auXdx3NOP2gOIdFGp#C&UdS@QqWDTX|OC zz5%#+z$cI5>FHO+m;qWVP|8utP?9LUC_7OY7a8{`AKX`rd=+<1-pqj)C=WRX-hkc< zJs8XNA05{%;nFQH+7}&#UWqv`iqM=FeTRDFy0&NcLy=9)mVb`+0Z*e4n7N;oANzWHmaS?+dEw zujs$W_r@Km-1|?lFLCwCcTg5Rw6k2FP8x{suCwBI@dVx?CTqBpMjZaA%qzf4tk3gY zGD4qc`6tsJpo^g)q$@PKe2oGL;2#=Gx|Y!|4;rXybnp6*(KXP%zNha8M&qq z&&V(2vu5K*esP^fJV7Jvh3_w%eTHM~Vtb79^6h8xh`e^v^Sbrmvqht|pPSZbuh90R zxP3u|%l`3R4zeM>mQKi;w&~zo(B)H@gY(|F2>dDmzY6WOvbNXqo{+xeNf!=j%$NQE z{AEqO=(G9Dwv%tTJScGXl{xqZpE&85@@vj@PJX-B$uVj3maY?LH}5p~QRcC{!=n7x zQvTqZ|{Scn_pqMC12S`Ub=%}*kC;=VF>9f+$Sai%# zbi4%oUH}fy+jN|8;h7t_)@Ja!n`n%7jp$d5Jam|yuYuUyOkH+_v7H`L1J#&iB zl0NnU4rQrw8O~+?h<8h;bnHc6R)#ssR`^UGP;B>cA3hgjO}Wjhli-owgIvevRWY8u zC`WBxDIcNqd_7_^W$h7`Pbc5+!d<{*`mW*^dCi=?+{PU`lH+X$c+mphG}*kZckx!f z-$INe54pZfN5Nbj``&__R_Pwcow~q$nYwTSHb))U3z~MK1W}x^hB*Q0QMOj%u&W8N z9s5l-PuZCC9i-3Zb)l5;MBmZq@us-9lc$uA@>j}FD88wjh5LGOPoQutci_kz*@}6V z&6a>a7EH!$3pVbsMy?zUy*I+C!hsX{cDDW0yvg=sjy)OvZaftKR4(SE zr`Ul9&w9l!-pO(CK=ymcJ(SEm@ME}7vT_RWPx=7(WLevB3-M@%Jf8-wd~G^#ofNkA zfbByb$l^qGih17IXmaKb&K#Aw5zoiYTt(rkbVbGS7htzeotlg%D)usua^uWA^^mzls!=&owllQCJf%0FbA`8=E-f_IiG&hslQd!U&LV2t@2WZ z_hfsz{g*nEIDM5n=D%*t5%c93@(_!kyC(R&KSloLbz`;hW%-I=2~+jo|mU z;Gcu!E#mD$`cC8}KgIo(iv8{wYuyzb$OaJOR6N&hA5UEQ%($!K58=|zOz1(4#(w%^ z-j7(d@_S{7*JX0dKBHhA^Iy)#-JImRiu;7gT+IV}ka-T`UOCn?9#%MT;WPb>(@*jI zreeNf=b^l#aq^V08Sit44`*Mev&a;)JgS+H>o@Jyh;w;1QVziZQuw8<-6(_9mQrCJN zZAqiOz6@=hG}~>-8|a3z?eZPtIV$77qTypUuI~Aiqlz6GXq5RO&l7<89o~f_2Xex6 z=I`XeO@Nz|6ZDMv`){THdUN?ZVVo;}F9)nYCV#KkKAyjq;@OLG)Xv>VuQPuq4&|Uj z*)Z*eIi{lH&E)Td@lNLNgeQPg6?F>wN?mdL7TWVWk-w+G88MSLvSepORP5U6sGr!XB8vdv6um%b;n{MVC5@Qs&Ij)Omrt zz7oE~2Y=1k7yD2%m)Y*wXLHbe5SN=@xsr)VXq+gRT zb@~y@&bd~~9#3LT5*lE z^ZW8_6IaF@=3AtL_iGV+2b-}&t*3l=pUFO%S7DxxbL$Q0;~nKO>|H1Mjqi`5Jr&;{ ztw*~a^F!Dp1fI+S{*CD4{X-+#24Zg$+D({4o^ajo0Y}k?^O(GgDaRQ1xF2Jra{zU@ z!~@^u0iA9<>908dA26>O@SCw8WwH|X+JbhAy&u1W&v}23d2Q$??1f%&ssqm*m=ndm zZK$cE+=D!l`3L!gyGGLC)$SOM0(LZmzRbgTxA%z&->HoQRvc@cGFNml$Kd|WHeENN zuE1RC0`eD$+a~mJ?;!T33trpYJlbJ_eYKC4>q67$u#wQ|j+mpl7_d;u!U&&ejoiZ$)`B#J;915Wz~EZmo3sCyz)SI>4tu9CP94~ZnzHESeAL`q`OHG_ zmOhi;HK)(4BTq8ugU>8=`OJFoxgKlD6Ad-xi94kN!c%dU?=)09gn6|2CY(w6oa3YI zXW-uooPCY<{4&hvGxrDqKiWp2=V10a&__C7f$id4JId$Z*gn$fzbbM#SUlJX7`=dz z1a17888=-`po_82Nf&vl=&)@%*KW4)OhS+N?4*Hn-E_6s^G(d>GwEsuPA$NxTpY~4 z2XujNquh^rN`2Y}J>|SAJkze)?09h)IpQIdM^HLYT2Pu$s!)_aqW)Gve`za9Z=1kB z5B6bSPVWGldQ07)-qJ3qw`_ABb%f_vm3!dlA!ZBoJM${WO!n1dUY&LzTMJx826J4x z!~CfhiEm7=UYFOvNhdEWZa!A1Md}y3agLgYFA_x32>7_zLWcG*K4PUa&vD8+_`^kO}!k zKU0D|t8AH6;7snd&*duLm9dwg}h%}TV z*Kfx=6BQ$r&T=2VcjQ@2**4|Kw$uUIoZXi1T+yxq{e}82`Tn&^v+SQdU|ycZxkbsL z9P3o<<}-6V>Zr62n?7|upkGBi8>L@i3{W`L_?a%yR%)Vb%F?2D**>S zN%~~QYl#QO0DJgD7(q}NA++r4V3v15e1FIH!6g~+ z-SIw{a988Jquahip8IdZ_oGk_+xKG1?wxv*B3~pOXV(|b=Uwg_ogJL;Ets?o@Bp;Q zHqH@@J(TGb(5LjI#ic_^?wrH?kNXDCGKaP+=fggw4PvfSAC=#uEh@QBz!SJlW{>C6 z>uShsGM@1c)xC$DXZsk%lga!Q^Car;A;?gjEtJove&V~9_VFrb$*u@-@mr) zNaf_ryM43)%+c|jcIiwR`pazExgI}@K|T(D1#ndkK;2|+z<&J9)`W9OJ=QWmV7|dK zije^yk@}@7G@Vz+rrKc!Qz_sF* zm%3y6+x2DepOgEt+sA#G)1Jru zQ2+4VxTA+&pVJTZg11haR`}946l<2R`Z?whj*Jb^L(=G6qj(|x3C5na=f6?jLY-qi zO}%!;1h*a0=6Sy4cMXUW_x~*TGMO#$I|I(|U>|X{)2{X-uJ&WD_90h$(A9p@)jsTM z?{~G2y4ugU+Q(e&=Q(hm1Lrw#o&)DO@c$VHj#~JxE|1%18dckLtTa}?{9*rZmVaf(t{25G z&ipQ@;pcHsO0N`J2z3ie6lEvMGw9RG@J@3Ue!sxG@pDZj__T!?UeB;1A$edkU+ z+?WV=M$rKDHL=Lnt&!GbC#Y&nbZ+B@v${~)Pr)es3MT01DDtz8NIwn(CDPBap0Pj=|tQkZkW$3Mj$ z|EcAFO(YgcbhfSzw!D+A@*Hb%Ctj3uwumoCxqK0v@XMB2kqJrUX3 z8pAJ4h;>g~j45wjUK2sc{SAHQ65ZaMFxz_K*fSalcSk1O^PSo^ye8J2498j{p(IUM zT-Osx>;NkwiLK#Q{MPiyEj^uyNVgu=V_mU1>o?Wc>ur(lWM?d#?CgrozwXdbYRjVy(bpl5{>A|E{NKKhVF4_`jVyktlmjrQSOt$Q#k1zU0pXzA2$J9 zB9iP$#1I}Lj2SuT_(BlqVf;8ZMU(G2PD4))!sJv4acBd+^MvvwN=-m$Cs3LykvCK! z9?uurNtBWWn1kBu$M{_QJp4T54|p!Y+-8i`FU0R4x&G{h?}f=9AKr_xEhO2j z%Qhe7=k{+e{`v9m7d>_OOLdXj@M|g;IBJcqK2a3iZ~o?oMFaR*jz|7w$!DIa5!%2a z;JX-bRsy$Lw68&)vj%wCd%5=roVdUHmWCe`eR{n}ZT^1OFK_JX;U7i#KoVvQpN0b4 zhjmNKj&-lxunvwuzqJzqsVg2ygyA6PEG@s9VM%P;7`~OG@Kkh-u>0m7<}1x%<}1yu z$XA+O`AW0QSDKlxG%M(HJFxZ--?_Z(ZzGS^J^AHRdwzEL%KKe@Cl-mcMUb9|Tj-BA z!vpn1lXKf6$?~i9C3KV$6C%NnCokVS<$|fcX$2P+UNrq;t>|5s6kmGTE&aZSUy($j#Ah zv99*-1c9ZKlI_vEoUG9=+2LQ{PxrxyLRun^RB!5{^Fi{_x69{F9+_s z|ABp(4#%UmyLO$Nt;npFi}sPyF5BfB(W4pZwC7zjFAi zPyPLor@!_;j(+_c-+bmD{^z%jef!z(9RKe3{_(^=eg6kP{9ix%@pC`<>Cc}3=l}il z%+T=23)x@%%gGltM|M~TAUjFSXub%$5-~Ib*zyHITw@v@@6)S63t-hvi z&Dv}0*R9_Wx^81buy}0NQ8H6-oj`}A2G50mi@K0T*hUU z+b&NgBirJLd+qd<;VAw9L^v7IBfYJW?rtR3(H(j;oM?wB5tBb6COP(;xu$!0PqItW zR1W_gMni(tyF0piqHTHy&nH_ule(cJMc78KPcwKsbauwtBE5Re61`q#quf@k=xXm_ zIDw5b*y9Ov-tIn0s;MMeJjqi zV2lh;R!q>1-dRn5j83kLY*V?vaR&g{A9I%wWdlMKI{HHB6X<)%(O17)hz$B_9DPrq z&p=AB z+=UWFX+gOjg`am}-`DOGVi@HJ$`dFLqEw(ij`}8)0Lm380_Ef$;LS0VK9q-1zJPK9 zWe>)0!uXF+jsyPlD8EGUqFsVgj;kno*9U@Z%B7mQ|WJE^i7gZ!&LU&$49!w9#C{Hgb;E$mZ^jo>+2j zTQn+GZrHf0w!U6IUqEeIXvD#>#==2G_%iG#;>J;LA zcCTvBvfFBvw2b8pJwm*jV<4y@lGxta8d--FtUZDgoXoc*<#&qE#?7k}k%;(LKCP26 z3u=sKtXtWWK;RT+;&S#bPj+qVY+Vy;O~{ZU#AV~XwUJzJtUI2lk~=KUYgn~% zP2;L{a+@^>kT|z`Lf88JvZEo|l@#rc_Kt15N;9pVTAU&iT|3A>{#B?tySK9){orLd ziZg-Lx3V)1f4(6u{B|2CCnNU46+0R%u;NK;bPdiyiKN~6F{=;s$|EhH0t1QHFRYPJ z7=I;fb)swA8hknMzTE6kxNTjewId9Xf~GR7e|fY$8E$Xva*W(X4s2Lc3yDi=Fr_Pj z+>1dZ4*0HZ*tA`R=5}9 z_FNwpODox6^{nc}y<=+4Miw*`LHgI-md&~8R)L3!;e#8cE0Zm;FW>l;9h;|3)M z_W#L4+4-3Fx|sJyf_NTx387a9f3W@ACsuz=fA9+%R_{4-7=6uGx$YR=2+*IyxCWf_ zg;{9VC9Ly=sk6R7n0(KJpl#N(_6U>jdIY|R-ohH|qIR3_ec0|pomY+iBGg-j>Agf~ z`F+AHyA*X;n2%kCx>1;?FUR|rB4P5qP+o~J`Hm>>J;LPsBChAVqWq*V`MxOsQDO3( zQGNnlMI?#aCKg;{~SGOpvD8R7BX%v&lpvF3V zDQfT|%KFRT({j`o0WNFIKg=5YKeobZpG2Jx{_+mZ3;FUM%{z$waF^z7f&A8>2E1n0 z%dr1i%zrQBfZAIOxnGMKbTqOCoqJeA{wG<3e{J=svHyP7^Du86YR4mI@R7;EfXyAd_^6Ah@rzjD@q zzn3-OKF1n(6*i&<+#1#x|8sBw<2ZO=B=S#wrpEjD&^kp_L;yJ^whCNjh(mIY0o<*BRbmV2kblskMIUV9kz^q44y9fgoXT-%0xkEcf@fr%FouXB)>Bj$ygf7;J5PEgt z7jseOiEHe30Br;9`UyRJIbA1;v5&QX96QCZ$9mMng`aiU_clpQ6ur8fnE)nTz$6ST zI`M?GR+OW7a&*^XbvJM$KjMyAI3tQn9P_TnyaZ+{C_-XhEwP;|0+LtN_sz*W*}$@W%fK>lU5Lkw zL&l)-q;c3dVjMO4{CoWa{(b)a{*J0-b#T$%MKz1f#kGs;7S}HhEpA-gQ4_6+*CcCt zYj)P8YU-BMF9|JaT+*}zfK1$Zph_<6UAS{$YT=%ReGB(4^i>yDA6rziIJ5YL#Vs|p zOY9X(o0cA4dSodP;63Y@aoB&vf7Jhs-wcceUY7jWQ`uMfP}QNTBUQ(%p09eT>b0uE z`K9wK=GTD!=K0b2JLm76|IqwH^N-9wKL7doFU@~#e&K@B1r-Zw7Su0jUJzZdbHQG4 z^U#7L3yv>%e!)u%URzMOuykR?!W#S{1Sq#iq0)>JBWTnZrcrCu8TCfUXf&FPW~0Su zGdhf@5jT=Xuko^R+K2|@!DO&ExHFgv?g{n<_XY=o`-1y}4+Yb~M}m(94+RHB7S?DT%oxh%@p2octDq;8x z;-nkph5^h>U=;#JEx;xYOj1UlF<|UB(#B(u*ZE$Njzjl)n$s-S1EPAM+3T4};^!{3ra+`7{2L{xSb)7>6&Q1xf;X zpgdp%f&nv77YGHK0xf}#Ks?YJNCo-=1A+a4bl|bTAawI+;8@^9;JH91a1wGm4ITDX uYLz9GdS!W~Q5mc>E9)vll}(i`l^vDw%HB$nc>Z&q1Lrw#o&*0GIq)Bms#yO3 diff --git a/services/crypto/platform/WINNT_x86-msvc/components/WeaveCrypto.dll b/services/crypto/platform/WINNT_x86-msvc/components/WeaveCrypto.dll deleted file mode 100755 index d68700e47da44e02a8cc28880ad9a812e7134b0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79872 zcmeFae|%KcnLmCfGf8e>!VD51N`N3iX@eRKC~*je9}@x+oES49g#fmKSO9vgh7Yp8bPI zA6R*FdiszYtLh^s@BW9{Ur#z8|LI5fo&O2K`q5vU{}bN-d)>10Pm1tM=XH@U=X|yZ z?>zr2@&5Pob>jVN>%CvR?|tZd0jl$xdf@X)(n4FZH2mieE{Nypl#(V4wWUc?D`K{o zvA>&zw;R7s5q693BuPpZ!GX6_Ycr9EcqAi$LR2pCHiJFmY|HfjIRt0TV}UhlMBIaZy>0|371?`CD?dIgD-kKjS@QD15B2mNJB(u$kcJaE5q zza-7P3kjY072$Ubzd?URh`rg&CLPR0Lblm$X$anf{)!~2>E<+lB72LBaF((J+E|Nr~{9RsXc%a&T$+#H8)JM55{t)(m| zg%R|X86%?~N^b3lyTxxPIWjx1-5~w8ncIx!X20^ABJ=%~L3>sH>F}JSi|1KS^T$c3 zHVertS`{+X)W%^_YHegfWx1is{N9@+$zN$uT|LNBX^fmAD$2@lTE1+_QFK(m;4Y_3 za=5gIFE9*)O(Qhszh55q%D%9-$7j5)9dU-5)N7g7t!IjukpxlI@@1?=RC8rkE$UdC z`f_rSR2w-xD1}A8Z`e^2+G+kgiBB9ZNxD}?hM3g~E0vL4yHPd@RoguTjsb@MhCKb0 z2LKgJU-OJU9ksXzU`0{Z!w;jdp*sCO?wcY>q6@CjOmy(&O5mt_D!wYhdvoS?8b;%c zY)Rr}$hLECw#}XYnjUH#50VKT!Bh9Xz(U8x#~v2yz(=|6-Ro~8X;jHxIVn>1>)w4~ z?|vV9mxbR#OxWA#D`T(ozw0-QL!kEBP^&BSAjCr0yU!O6HM$^34RtSaY(Nf5T!2Jl zt*ouwDrqZgsRG@*jd@Y=GiH9>ThF}vbZ-Oi`8G=PE<9OBRh4$xP=^R~cRNG_wZ3xR zjKr!c-Mb&-W+na(VDMdt2lNB6`w$BTD&1OU95@TWgN||^Ux!@UkxbpY8yT&ke8N!o z`^-G$+$S=)(dkf=GRHX3j*@J=p}r6<-Cka1sC#_;MzfAkt2&W+x7)mXay#@?jMEPE zdyk>+;}SBk4zO$J1I211nR$2EQni|WlHv^7DAVu0Mg7ofc1uc{q3-6dQdq0mD}sCZ zKbpb)R{s6`Z$y69!ooCw%a`T9v4npY4b{DN5aFYTvm_exg*_PasTe#r+3?sh}U%I-LFCRwO+~yB^_k#MigMo>4Sm|z1 z8;zTY-bWpNnW#mt-pjY3e5h%|w^)x^Krj*uQ`5^K7YN>eMIY*3+m9GxQ4ow6`Mu%N z+A>gH1M}{NVDblUz+n4)x?0PllTA!!dOlE74++Nm2mX6R>fSw`R^`urKaDTOwFPlO zTe+Uf{|#RNi$6p~@6e89U;JQ9U){%-Uo|$@wNee;)fRu;NBl8`_~TZwqda+|n)5K;LCVZIWb^#^oUfYZC(Zd;%H5Ks{h^-=SGQk@s z=AIMAYS~!r;O8$O&mw-dJ6Y)08|l?#C^0U*LzF3x{H# z8-|a6d4nVgiGxnJs$(HkA>0$GKN2m)!CuBnWBG3^=S672nhqLeYO?r>CYje+Udd`? zU`P)|(F%VAjrjO>JPEmVmkFW)XiQK*Z;xF8%Xs4e6^^H}H~FRLU?dVW%ZUMgjbcZE z0T9wg%+T}Xn@G!7MwgT*F8nMMZVqNgQi7S0q@a6QIsXjUV;z`Dt<)7k$sd#gwE(P~ z+X&De?MOzbPfdL_8RQ+g!_WVOA~YmJ{roNK(@GqH(aB(MTBB1=P)7ZH4rMCmx6_NY zEmGp8x-m-8h7#lOn5k!`~ zN*MI9*Y%pc{6Vyrm^1Rg<>q`~i2;zkkSie)86;ZFLbst1O(POb0U}LIAA2E&Ag)6D zK#q`Dy1E0?nO?J-AHsr0h==&}@S4eG{BMv@MT}%k8@mxN0)JynEgQw1pX^S`Ay(odAEpqt-@P`GplW{JI(KE7Vml!0l)%6|>9 z+3R3U;=YbPpuPhIQ!9B1a-v=()z9ys&q1@mL<9+}zoQQl{)Bsz8&@JLgek&Dp%9Sp zpF+cR9}U+Eq$P&yB(ktDr)Q`WPY8S#J|{jf6ZD9W{Vd#zk8-`VG5;uuxw9g=NiRL3 zm$rvXk7H0zVtQqvE+mu*q3>gFws{Yx0(W}#an8`8#MnM7#&#=25(;6iKQ88s+!@$R%!j91;I4k2S5unZTb2ANFo2OkgY2q(p_zprQ|BloghnU zIbTYRX*JOIFn;^_uYp1Y8%3~@?=XV{+GB_!#CX!+nB#dLs;GO76qopT4y*v~L0V!w z#R?!yn4K?i;jx*g>LYpQte(HIlXu&ZoIj za5gP6#5nHD?<3id!F*{P4dy@M33*(l8HSn$n8bv59}Q4Bl4yD+U)3%#D7f{6{ve?r1tPsGqjsRyZME&B5kk9 zcYFEoDO`oF4_4d%2}R;+d!~eyx5Jq4V6z+M_bIbxSy#|mL8Y+(|{)k zL>YLqQYW-|XoSz8kiE@^_2}2ExE}#cV`baRWh^a7$#jC)X+>|EBq)h>@LQ53Nlc@L znh`FQu`(~KEcdY!eC>HL&%M9iZK#>-1Xkmf0!Y?jE!qd{0KiiVbPv)kO{f8&zmoTT zh8jwpx;M%r^f`@;0sywxc-v5;x;JLLO+${U^aol^Oi~_%Vu;mP)JNJO??vBO$04Ao zHuQmdJJN(mCNNQh>IN~!P)#L2hVEfP{y>HFMFJu-P2&P^gdm$n76#+(O5<$-XCzq| zBnE>EV*^x4HqV*ghY3fG0mL?M)CRqUr(_Ye$)KX8{@?-(5ZI@;3=4P-3A9wi0!Nt@ z=)jnk^M??J{xqq1hMGl;l0k+J)mhGqA<@jPw5lTrpGb`HwPJ=y$P{i&*|<@ms# z7-Ly4jS^ACe5^E@uyZ+?H^%-{JvjIA<0eh$6}>3Jyisksq>RIGma&T7dl6Rt6>-Xq&$N8}&uC^$rP00U%`#RIev}O&h4cOfQWgg+LvF zB>)=^Kx1H?PLPszL`IZWASu_diWmT=bSB-)N@U{{b$&2>0znnM;4J>yXi0*eF4Zd3 zKYW0XV_1Mj`OuO0{!?|lql3#cZ!ZBchk;oE1gas+HwN|wq@b+5#6h51aFj8h!#It# z5o*?95{z669BYSV;68Ey(QVYK)R;uLMSHS|SkWHFlX{F6nANCQHxUbG^7KaiI114AM)*2lmqB%cJL!2bF5A3`IIaaG04y( z=9kti^Iv=ls-=uhzJxNOi(XluO&PTfZb`X~VrILw^?8zVgJ2yDE7c1s8-PXRVI_GW zF?~3}eFbDDU0LI8jLa66l9+BYGsdadN=NM+214`?AUh)f*`oD9>3#zGY8Hs6Q@N|U zS`pKJDEfjjC2nm)4iTpXa}KC!po=av&&u3vcAhzi=*VFBFm8mj+HTCtO*L7;1%d4! zOYXo~di3G})78i>LA)9HP3kE#JJ>cy#5urq!Hj_86LhK~23nUv>m4G=>SdfZ6a0s9 z5G@W9EvnRFCW;4^(&J$~zNy8@i53IvuW0Wrk>I4ER11SPtNs@`kpZZu7o@CLM75fz zq_`+K%CWS9aPFbe?!l*op1lR4x0@r706C6-jNfCh^q_ZIr%fRqDdQ(bk(NNBV1P0x zzgFXxM5neNaTwIDr&1VKj5HvdPBjYRGwOJmcl3NXkf7;RZZTz$3FVC!0ORI2i$Jx!Z`#f#Z0w z!SKY(XGqFI(FZ3h$tdH0%oNzwnruE^jKYNJ3MUC&C!|_@E}9f?DT8nTF~@_~QeHWd zmXKM4JB zXcO%P!6q0{C&W(~e;6|+O%h-2Y0}_sd=37Z2|dgP1kG~jv!Pi+;K^*hQ(u7Ho5YBV z{y~=0cnpgEYJF7Mtf~6dt)hGxz}416@{AF^b!$I_6m^L{xLH|VoPIFTIrS3A1UW9I zOtLjaE>sQ(D_J}2&@0SIn+POP38P>FXGryN zh3ZjpFhgvbVbq|XX*Inh1~8TM@*S`*=w41@fSV9-=$t}Olnju)sb}2FYz8bFF)?9G zh7i$9&#{VgdPR>`(_;yi9)1jFUftV8HFi;r^bw`n5GY^gNQ)Cx%?rrGL`NP^;1=GRa%O|(vdC(JJKDV2z|EQ6{ob?YUW zm<8HqLxmKx9V^YyOR{M$@@DF@bIi7(>&FQ$-u|F=d4mH*ttpDqG1@fccm7e#PvZl$ z?hj;gnlR|0;Bn=Ial$Nd+oN*%T$l!)>+rh-gu@THH>Jou$XQztZBMDJptJ zdtWz2Id~G9Kx&MLQU+ot0!8r-ZzSLjSA#BjBr5h`qX>Ms8wQ3|m>aqVrzffC#J?VH4`* z#|02kQ&%4)WW-sD@4_OEdCzv=OHEa@v)R~Vmcb^Sp}~X!caCGuu`FF(3(-a-Kvj(VDV?-y8OJ`1pA+CQTjf{0{Wq$9F;yA*_4P zg-bj58Ik>*B~!);x%C#+vjC70gtG;NZ*}iO{a{mVeJ1n)%20rk=xjUB27#frNC9P^ zgIL{rgfe8B8K7d4G7@97I*UQFBX9%-k3bB~7V_#GD~$s%MgVYrUOsiXNj~++aY^^6quck;|V16^03Hu{0e$V=tEGZ(nV+FfmuGzFC?sF z9rG*%lar=PM0{nkfshjPl0hTm?a|g}Ny==D6)VePvq|qYzp+9BHODF2(l5)=G9e4n z{~SR}Sq1gdVhUA(^sYx-LY($!KZF90X|x9@Lkl^^Y2w`hP2H53ari;w6U@F1=%X_Q z0mQpvu6zgOzn_dUUsIoUE?cX?U=L}uLm%xHbmz@%BcK7btZi^g>I5>KPZ@~%A1SBMSH<*l2q3}GTOUF+IY``0 zl3&nt4`}+FkB=P^r|It{6HUK?Z6QI^(QxTGZiwtri>61=I<10JJ&%)F2ebMd3Ot8g zy0;S(5HOno+Ux{jcEWNXECyYarC8+ef~E_?H%V?LreUE(6Iom_Uuoy2Bvz}W(v$_Z zLUi)mzy(5cgBETwcN`B8hQ!>EUcgqbVD3i;nA=r zNoh0nB#`2Ig*grQ zOfRH%$ESNNZD9~3Wp+T1b-=12^}VZ!-l}{!yXXV<3!?XT=f^Lp8{(7?+1w z1A;Dj4Hmofo<#g8@+DUOSIS$`PwDS#>JOBh6Ko?Y+(8u%swmzb+PIb4_!{{;U@wHG znFZDkxKUw1qQ?>{*%v_UD?^lh( zpOM_VLan*FJR0Hr8_WO8*OjLt5RsBlss{RRj+pjvj>FOYi-=*E6lI1#K6hofFOhD- z6WI0{C?SdeEo`Jjru^UGiDu37P|rZzVh$lm0y=v_Bswi2MB~O!S*p*~>}Ap`Hm9Qe zG~_OkDb&U?{u+3ZChK-S6mu=u;&{TcNNVLT&^;asEdr!T%V@ui<|n@+y&J!S}WNy$!huX(oTq7C4IW@1U9gJO1wY zSWu!S-vj)j{*4_qt>pJucS5+O`7)gLApL+X`p^&fxbVes6Zm6Qw0fwAkZ(E zKf^Mq(j@HT&k98+u|s7{+6{U!CcPk@JH&Ilcs?VZ+r)FLcs7VxBhh4KRh0{p3E^BGC(N}j;2FQ<^6r0=> z#Xm>BKCf&eSC>b?5LL)dn_Ff!Y$T(uM+VoGNQCowB%R#Ee+08UZC|=5uoK&36!`dP zOJDN-TWoIIp*?u2*J|k^gw5*v&`coXEi^uiGQ$%9>q?4iO1HUbqncNv#^VA4kcz)j zi`vwUKF)6xRjXt{7zTO5CG4+~OC;|{N~D`VftNAT0le;NyxZpHQ*V%@T?f%RugIpH zHK}|9_Vprf^5qcUp{9-5{DbSo$7t9A)5#5hJcW$pMwIE^1AgPHksdjkZ+P0&TMG&) zep8*1PVa)k>G|kavhW%ysBx=L@HxoLN23M)0|F?hMU&Jjej1$#WF$d*v+-=0>BuL_ zbJ10QJG$&QrtQOc8Pg=x#cv`+8Yh9h(UpEa6ctrf86ze1M_c4D^pr@ZpWlkXuIm1R z#7oq!!mTX~^EbHFg$@px$CP*;z1>0C&~DMsj6k zO!D+7$y$Gsdh^a6kOG>OEB)nU=j_sIx+GWq|3W^*@HJ?Ss=&fgv!d!yKjrJUsX4$| zb|rkqe}K5kAlxNqBY(Wg6pBHg`lu^lb=I1RfDx^@&mcp45d#} za<#kz^e~hrkuHS=eI8YT@fcl$cuI zr`&k)u)x+e^cLu9(?{b_jl8^NmHE*+FG)%SmbWxmsd0J8?h|Kj9HG<7L-|cxP_n*B z9aZ-QZLq8vrWMvNuwkbpx%-lbT<dhuQDL%ZV4J=Ks3khN}`+Q-_PJ7s$_ zz1(;m)Bb30?oLutF8mWJ9%APEl=2DC=+w`E{k{@@KG+d)lN(AiJ7sV6^gk|pqOK=* z1BSXw0+R7@NJdrqwNG5y$1YF*vgNA0Y0fk+L8!{UnFvEGxKPzHi8gHLux}J6?Oa* z+T2b+;Mag(Eq?3qTa8};KQ{NrUHaUoyYS|CbG*;teGcy)ynFEO#k&{pDBe-LV|d5H zjxf3|*?a6!3mwd$P9Mjw1HW@rw-G7(=_hc{Dd)d>5FO@QJ$OV{*wHlUy2;`-;VwQ08q9;JX38efVuDwog3O zFwd@iCb0Lpz}~+Os+wT4@(hgCXGz-UViXz%<=i$XA9{cuT4VN^UHhCy*7c+xf^tv^ z)a?L3PW&?P%fc@QzbW`l#cv*dz;zaIorQNM-kEr3;GKcD8*fT?;_bv+##_eQfw$wz zu`zM90`&#(TaVvXsyjY5#dcs|&{&8tdOK(w&^P2U56es#$#L zRB$x?wMm#KCEk@!0y1N0OJsImu~Spvk>7yTo*#a4_t2Y<#?!~=LJ-g)p{AN-)D#)c zinBaz$`Bv_M26heH)ZLYoU8>tTGcI>Jg3v-xpxAJly;HbMwK!5Uvxd&1O;F$e-}-f ztqVZ-{U!)nyAg2$h9bpDm3h1PuTVpapMQ5IHBl4u^KWC2;1$_rOAhy?8j?Hj`sxaIFQ>N}U+7H_Z_n&u>fsXO7sfZWU;Q26?BA8k5mQHJwW$ z8?_(l^0aM~J;~dEkTvQ26Xb7`M8Y9~GZPUkLtxBxXl{}tzpr>=tMa>uZB}hTw(`Xm z+l>1a=ZrN<=8Sc&&=craFlkBtX$;EHvGthmBLVh7k0Vbsrh88b%}=K^FyImx=)Q@cQ>R z0G3Fa_ED_v^;&dg))pu?>a%gc@|&jHT+e`zUG;wg?)X|Dm-XnX6WrzI{dv^UOcd+> z15!b>M)G=qt1kmV%X%@HAjKdJMHbe|-xx=F7mi2r9-^$4U}j)Djlycg=cD*p&^Bvr zjwXk*LPRs#$K#tnPGR;fbMqVx-cR;GRSVBEBG>bWio|XKofsLz;U8u}!E5oUPX~ST z?^06$D3}zqRnpEID|FHd>K%BM$~*!Zs`61_q|`}TtP1~H8R-$MM>-6(lOHB3f{F|s zE6v9}sJAKtXW>pk*kPw2X>(Xzhblg){p07Xpy9q$2OA#Vn3TgtY`O2YoTSqyzqoC5 z?H@Pijb>@#xwafObj!k9b8eGsKitel+_vzxoCX<2dyo`1%S=37gKw?otmLXE6EK?V z&c~Z#aHz`L3CzH>Z}J~=2o#0q%8pv>`2pmc?h-h42O0^dSz}QZ*vTT-G5mpB5RonM z`FNYuNpj#>v`8zJ1o=+j9%Rs`^9hN3CKvn?c1Z9}(HfL%)D!4;WhJ6;xZo zc1SuM7Nh@Ned;5K#&6Q69QR;uj?dkL^{LE7V(!kMbjQh!7m(eENMctkFb>MdrzjpkZ@N2{mbMXIwK1Gc&gF2ms-(>t| zP)vNz{{Kv$dcjBS3Vq6l&u`GDb^{iS1!dm0AKxALNeIMsps&@ZDEEI&pAz-P>q=-H ze@&mV>b?(6EXD5u{2sl}qcov>xLB~QjT9T(V1iM!6iBGQ(863;Y;)YO*z7QRS~i(@=` zr~^fImOv}gLuc`cOBFE3?8-(-SaV*Uhu)*_Bcq56^umT>jEvz^E2IpPDkIO&C{KXu zHb&B{g=s1SucIawuzoC++a&1%20Be!AxnCRgFNs|1sKK~VW<eNT;z>t7v=+~131PKu_KL+hc6-Powh-i+H6!Q zPSZT*;2*#u4=rmJN~_6@GU83(&^AAbKF7_2_h1uYz&wc4Gug<>Z=eni6oCI)qQJN- z3XtWOZi<+;hir4RF#5){-S}itOfURage}Y1lN#{PKFIw;c#Z>iL9BuHdI*iMD8f~q z=-QG-3936jl31I)&C|BV1w|mn-I#5}fsRq(d2YlOiP$el%O~^7t<9H{eBr6v`9eb0 zE&_sPq?rimDO#(;&+|os0Ld#?;Rr?(o!mgz{6R-Bn?Hb>SdX7Okqwv?HSD6?Fs&sA zGZ!yw!F?h5P0L7?j}o`7gmsfY0H}zysI90qZN~X2K0VeUV+)^ypovGy`|rRbo#npR zQB@*VCYdf&c@1)EE4zq^J<}e()8<}xJ-BcNOhp#|&Y6wRLFL?(^R;;zc^3gLO9^MfrtZxoFmlY&;O}Dv~kbbT^y3?-t~T)n8M&+XHk;cD8q+Ai{}8VhZ5Sts z1d;)2bqjo0&0_GwK}lSQB3B*C0V#)TIxoCPUK?KLa_mYhLkVY}B-06yUa9Cgn>a(I z_84jxR5aYR0(8+SbHS@HvBrDfD;pP7i0R40)jvsyZX5xMcu zeP|TduW%Fr%7DGU!>$Jbni-gjB#4SGk)+_H7rKC>O*9*T8YKQB!n4_p|GTDR-_Ue+ zqUqItt!eCM10=COV6}@aT*R;*4EPvhT8uEil7IuNSlu4kK^d?Mk-*3(V8lTdZx7`( zPHO4G3ciz2NEjQ2uD^vs*fT(k(f#e)L5oR+6ObjS?8LwrEm;K&}WU(Brm931=!t-DSN3@?grB)nsZOEBS`!1JI@1WwXi z5Lza#FyOC|%(-$GAD*--nitqdzn=fR_AxGa3@@$U*oT!MlLOJuR?T5ayqC zLjaQY;HrOL;wffmn;DvFhMqJ-Gt7`;hKkJ4Dl;_K3@tW8C1z-W z8CqIMLrIMkPz2EBBP@|XWt##xjCdWo(2gzu>#jP3MvRX7J1KJ<=auFtji!Q#!{DOq z!zeqkl7Bb{k)A>tE+wPRaMRnV-36zZO8)0rW&y%JABR$Yz8{C*v7zI_B~REP6T|m0 z79U_ld^`Dj<~MDyX@z1)l)Hsn?u}pN(Gl@M6BG`~0PbE!deoqy91D#DLrF4nJ)lUi zM;gwS;>ZuWO_I!C`Biqfm!>kDel}e7%g|vqjc7mc0&t0wm4RpIy(;6Tog@KvQOZu@+O}jWcvqzaJ&tmT zvZmnJWpTKxo{UzexbTsjDgh6+qn^;bB<8QWHj1q?R(vW+>p&FB2g|LV@RJiTrohM8Y#Q4}V4u&$(FUPZ zK=dDRS{VZeL45JqP#GE6i{QW%m`qKbw6aol?H~_a+JW|)0Lby6%%@?{AxUv>8JT#j z!N6JCp)3UeVS5c)o=26}^dyiBw#9KP7iiechJ}tI&nc0|x#nCv4;UFL9!`D*=*{1_ z8&#vWA);l9s^ZCr3uNI?aX`j{?QSRjXcSP=c8YOA3l5O$Bzf1`@uRLowILyo|a=;xGUGIR#^9fU3;xTzxFhaQ3jCU**ev^o`Dtin8jAcPhmzrGQ?No+Qoi!cg2TU!nnvTfd z2d1F=joE~Q*eftfGV# zK66N1c?JS;^Oww9P@^C&+D(PV^lxTZ%yTbeNJy1G1g}UxpHND&ec5&NT#B~+ToN%H z%;M*xp!WLt>-Y|wT@FaoQC2&Wvx^8BbK`x1Jv2NUplFlXGbS!58@Z3fplJvU76r4H z`S}eb%d5!vO0EFA$Rw&yBO`OTfmSn-yw)PD%w(grD0zs!UT!VQky?u;4QVa90m135 zMN=iMsfeXhxw>Yt{%Kxwd{sfs30FPcJ_$d5s@ot@Ha)x;TQweNLx)7h{*6hjzs=i; zY*J(i!Na!;Nd`Lh!w6}aKy{lYtlrt-Z33ci4}yr6rnM__YU3bjE2aZH6Q<$zw>^i~ zoHo3j1%=e1jn79?nE29Gc1j^*(+^z5Q2g4Zex+=SeUDAKdylLRh5z4vU>2r`V!>NZj3yLmLPYAWA+&2%s{5@b*X}kF{KOWX9AF0!#@(kahF+nV~z+RNEWa_!|b z_3u%Wqjk0+miy?LtG*jxY)P4stvF_+sVT*g3yUHjiG-6#5FV&Fvta`0Hf)e;`9~v3 z#j|Q>E>$iOv^VrM+tl&GJI)y{w$)C*UUgtTOV4l0e;?gIC(wse-I%zpnJ*eoS~?pY zI;x(hzF-WM3IU@G5bmClrp&`F6@}P!@*(p?ey+sqWcrpAbkFRZkvh=RNIE}iWw93< zsYt4D7I*tW#wecJnj{g%sDQhB7$RwDx&%10YuS=vv+P@RL23T`{>p-}M1-KhCyO9Q z>GqXFMHoYWdBfPb!fv<+1ih1-w)AOJkbs+@Qqi~RnW`Oom~aawN7C~0a7`~C0-l0b zDyXUut=_eE0X#Y(Th`8I(dJK*!VZ_GdCl#(I!=iADqI5fEvvY_ztT??9K{S`R#39b zjkfe@8F+#pNfWayKG5KImH;qpsUP(ADb&J>dkZ!is!Q8Y)hsbL~4= zu2+WylNRrygh;YkL)7L9)ghDTvg$I63J11hh+=K43dSqLii%bhWU80U*MG2S+tFHi zd3kPI`Kf4;ud=)xClHLcVVD3`Zt-zC42Y#}4510-xHcy0!&NHCMQSwdkH&a9{JE?2 z>KLnzL5l4lS4K_NZz|In$h7334kK`v5Fs zFldo6dQ(2eFMzPt3K#!cC{dE}c+42tfSQckX~887*Y9Yp8FM_M;Kq(+hA1(RxO_?6 zzP%O2v`NB7)?#XyWo3RokB&3J0^+JK20bwU(w*)J1OXg`j+GY_D8pFC_ut2PbEo}1 z&+BVG`+k$te%RB#RxT(2LpPJ95m*|+7Rwkkx`Ghf)0aUX+T~=`NmDQS+5M@Au(|3# zAj(;^f?V!#;Cs;uR^lK(5OHr4OLw4QRw&c+l>L(Dotm8QA9b=#OSK0IV5%w<$zv&b zw*3zP$a7}nA3-JzOX3F#e~f#paYab+0~(r$EGyg=4-@@H78U*^9=v){>7GR1~SKO#1HJEaxFL!^MIaCpH<5t1>i_tMjMr4g3-=p(ml(5 zwpPxcMsB*5ETz?JV6V5A_&@vv7L@%;I$PwhznSlx&hy>^JuGsvV%ZF5Q`jr_6**S9 zLZgU8O5Hd{s+Y&u68N8UoNL!E_bbb>j}>+-VGEzi2+w`W4bOIHKi(+hdj@-lm(0cG z8igMA6B)m^XHJ%orM@{EZky9 zwUtpZ)0S7hy6EGgmtj7DV|1I69%@n2E}q;)dp403xLFtCapZb%EmLFpYSb9H5#RYe zrdut&Yem?+mAantd3*D{J^4NGiby?l4{DAK4P8)Ep`3QQhxX`V_%FV#PooBwEf@O( zw6j4zfquRl%8ia2eTtn`d!AHBbGnt*+Mr+L%qDf~7L5L$A|;)NMbh;kpU6iLT2P5c z>B}5@u%7!7;@BeGd(y8D(~GGxt@CrOJqh2)0TJ;_@&{3;@F{Tp)ta*5#_QzS;=9Px zBlv``SturV6{gW~^m(z^N%zE6y6S7G_F|dM442sSVQf)mZgjrqvcAZ%X>28C5+}a~ z>NRFpQjbct_H49DniftKI!T2eLn{0z+@jt&o}GdZk?;z#+=nc$AV;>65lkvCFXK_BW^(dr~MLn~RGghp&w9>}P<$U%vwQ+(en_;{tz{+W`4%jDJ% zWy2ei44dnvric@I6>gvWoUE_n`cr#aN{f+V$}m5)HoDYhi&r+lJO@C_+DrpALAl)g zN!-*xu3FGGdf28`#DED*On1_xC0RA;ZE^9h-bsIG*0!WUG?Ju&m8R)${^{_ zAHmDDbtvMt9G;RrWAVD-u0w}ihiXfDG3$4_;9Ug|tNbR{q59?kqJpW-QAd>?dIrTj z&8{E)GmHRm3tjR<(0k_oybG%-JO$2O6tzUckLJ7R;NG(Gsl^2 zj(Pg^R|pzc_!5i{X6=rP&r?)4`9qVVkOso)a$$1X)G4g7fGCgehB;=7eU8~|Jq#PWVJ*5M z$CJp>U5fgNP@)gGp5~C|uKEq&aV=Wzdip;w?51qj_$8PB#5A(J}My%~8l4y?Zin9$b^xy`Qn8&Nm)`RA1Q)h3iY*T2t;`2y6>aEQ9> z3x`N7#Qi166g9prMC?k9L`H0x7i$n1+eAK!Gz*$gbx%V>45tb#*22}39_Ztq6UtPx zdqlpjS0z16&?_0w00mO9wf)FeOPLbIJWTwPK#KmT* zmO%X_KxL_{(xJ7<-M>RX9bWszgX&6?oDh~GRRw~yHl^s|UH1wR+AgGQx{C?AtZS`C zMnB($6O3Dg(1y4r3662H2tz+Wl*&3HL(Gb)czz#^2`-y&prm;*J+u`c7E>BRCWDU* z(S%8S_|FtnO<0sD$M$p{^as8JH#K9+h`mA-4@c#?PtY@H$R^XMnO2SE#Tvp~dyGEJvUC5cR)o7Cafn4pv3exi}`ZaW6l>h>ZJ z1`P#Vp}VQ(Hb-A`wvuULrmq<)kL%TWv5141=Q$`c^I{Zfe+~=R(*v2tc;4PTdZPoY z{^1upwA0eX_Xav=F~c@c#Ct5NCgs1ciLsVyoP)^WI!#F6i$V@vNgAweXp5RMK>!_A zFEBwbh7c;Drp+motlR_-FZArWdwu1V{Ky;-Xmg%+sX%{T5J;50w<*Ogu-PzdQRJ1d z_d;%Ku>~0Lpo_x#0l}#EgT7MPiQ0lZ#cv4S#eLhTyVaIg6qz}H72;EhiaaOO-drJ< zPDLY=g}@qMU{hd4Qs>v=+K*()`xi5>*^u^16Y2xTmtqK+U{)*(Zo}y^5LlWyR5wL3 zu`I9xxsjoJl9b_QO13$C$SW~1Yv(>sUC({~B%UT2IMvaHQ0~Hlk;`HAUk#HERBc13 zAow2}$ZIBs!$cYUaip7tWo%5DUk>$}%JN{InUCLW)}XyYA`4(sqsppK)28n-fjDd0 z`1ceeOi1?yQ^;v19NLaRF$}BCvoI=j7(<<0AR?7);{YXEaNs29dEB$By+Wm>D)j&y zi-h)Ofb7HwpfZw6{1~!TS&G}nIIIf6nr-EHk}5jl)LtQZf%AFB0s7!QZzK9e5m&{; zJdf=KF)PDfibkG6CS1&q=a!S*AELisqsf{A5ohGhE5TTD3-UKe!q$q4@m; zzgdq-?uGdM6u%~<3B4RD{VpF2SP3JUx^*oK=s;!|T;`L&a zc>OaWWyjNk7Ma%(E_JktmdIJs6wrFO7g^Z+m^q)d3HQj+jhT$FRaCGbOBCGVUMP0KbZ=0M&4XcO)=N|Hq^IFyU38&6q|=<3r!asDn4{ z)kY_Z-$})*yZ;qJD~?J@@MVR@O8t2>5GAL#~V3Ft8Pi2 z3m}8TsysMZJ+WD9!YOuMcUBxrO=)bn-sWyWTnm3B9<|ksa$4&-j$7pD8o4rE=1<}i z-cI}=-g;hqF=zU>3QGn49h5iLm%I&C*!fA|)fJtd$h4bR6iS0v=>FE?>F+^DzR;qBy!#az$ zQ$1NE;2Z@1ZwX!J69?k0B2n7+aIoX)ul% zBlnmDd_%y8dIFa*arpVmi5e2ST;t6;{CsPRVH5$z8IMnge-#4SXC8ZN(Egi;akwV1 z5S8S%!*1}Sy9kI305Lqj&jjNvRDML!Rp5aVT5fIC}e8C zI@8~nnF8)?n-s`KO_kgOk8kuja$B(1+W73i)#y3+S76F`x+omCc1-<}>ch6;F>oOc z*xX)jj8xEhu|opfZvn)Fdu1d7H535eaqLy#T^$u5l=E+aGfnT&@%W;%+vFTYGLViw zTeojJ$afB=@=|AWcj_wF=S?_xY@Wg1w#u~`F`MVP>5)y3JbDz-!$*%5^jIB!wHB?- zS5Gq3geytVhIYl8as%B8M`JF9pD$JaUhvy$IMe`E!WcMQ5o2iNK}|Z0azc zN1%q=f$tHK03>rr@^-gY>9B8;@l)NHlKuAZl|ZqBbu_)T674@OZ?B$KxyT z*t8l64Yx{IOBL4%s5*YpAmvAKm~o6oU!pgI3o?lJ8{ z7rm|=ZFXwtJL&=k@17Idhnw|M?m4fF$URRx)3j0q2hbpua#+~cv1+<7zTjFE~J&pc0`~wE`I~vc)NV}mhpv& z79!WhGlTGO-sHlIIB@=`5*8>P9{p0U)(>1S$@qxp-ls>aRP2HV#mW z6KHfT=YSaU=l9`s?N~0Mu#mde+2RJ_&6Gjix3eO5d{U>OfjNGUri|Mo<6q9;Uw!O6 zZWwi|oEITu;i;gfz#-T|9<5WQbP^U~jHuGjrjixoJuz z{v*;uAeSm1e+JfQ&?Sx=yU`ISzX4+-sbCb`oytrn!{3TtR&=SFO*h8_C~rYp;s)K`QP*nV_lA3BG_mZ#wy z5EEILV;}C??rYYRTM8;5Z^q~d7&nF(DkMUG*OQUZh z6uX_&2Y)aF{{6W7j!Z$36i;gnc6fGC3jRYG-+(njRn;!)Au^2%I{domIbU%_LUQmr zochQ={Tr$m`~8$(cP44g*|?0g?$XIgaKZUB*uM-pRvJF$_U7elFn{{y*s5@m1XxnO^>0j4_0kCTrPVcG;+ zfw{~)zzoV4+hvQ&E5kEF8=-cM@K;htgp09jMeLihSTD{56XSmG zyYx-6K2u+oiH$cPFKIaKb1W}fV)2Z?yl~{X5fD0w$zVlMuJ!YN$ODK4gIuy!WlC_% z(pJe*74exnkVkwr??ZBA6r2VE-9R13tab&Py*Zf8GLTd4D85O zv2;Plj$xvZy|o}ynJ?Vj7;QHCxGl%TzevVE$eEhLceJ6LN&Z? zqAYj9ab{HAgMzB`#W^q`=)e+^v-iTZt{7b}M9_%wl zJ^-p@sd`olG#?yj{tK?iHK#KEB|b5`L4YRwvN6?Tv)1#vzjEUePdd3ft}SOo?d&|L zizaGV9>pm4ZOSxlx}oI7)#9u}G5*eqk_S$Ep9Z6@H<9DW^Dp2sD;toaY} zUg8aKXu-TpQ%4e$h-#eyA5ra3KEUh`1+a~}eDMlrO-rMN|IIR4y&bFE7qQ49nx^n| z1CEl-6HNMnYgcs_d?n*jJP!>QRCg(3MdfknXq}9oPF{~Bdq@c`0}w6n^Q@t@^hyZu z0qXc_S8-aO%9^qV-qfq98^?Z+{?QE|nCQ+1hw?k95{O?JJ?{Q4W*5YLOmU?3)^un_ zkt;j{(QN7~G{)rV^IL$6DUmrujOmBjlU$p4OJhpB!(ZOmP-#{W%*?L(r3# zYmUeX=^Q!FjB+-1FR5I?Bp*{XI8eqCG2eE*A#{ z(kQ2D_Zerj)_D5I%a$Yr5LVh$ZGxnnf$c<6>)t2531*5e@<1aGG4SVG;3FKOydDR2 z&1G<*Ksjc!@KgbC!8jbEY>PY+%;c}qeh;}8sxH17Zm#iDj*&79aTOtncxjwEMIfWn zw4D)lM_2MOv_|K5CQ5;`B183=5gcQZC@Ick#N5O|E&$ttY9|Z5D84B7hs~~OaLXOx z~8cD$QRw7HD7G^|(EF*LT?64$s}PI{RhxvR3;dd+oLC*qKOp zVUpwGnF&i9=k}-Je~FjM6RwS9g%@-tQg$>RK>;Mf4_2N~<-mNw%4N310W+k%v{(O* z_(v~z-Ye_n;)+{1z6@%$I;wqS)2n-3p{G$#E>?5Z(dNsJ;9a;|#m-R(5kc`y}JfQLKS8!hVJy(4#8iCOw>^#M) zkXt5qB9vc*r;Mj(k*u!R0(3Q$y84?N@IgqNR9oYc)M4zHK^1UiQ4W48d9@f#P*lWF*B67 zYd6f1)HH2{qhG{m>v6VrWjS0uS38ZjMU=6cZWyeEQ4gk{DYnBC98v@;>g+ejsNE?%1`OdIYN%#p{LCKjGlGQ^6PfQ-!}Oyo-C( zsnD9g-;{w3NNNR#<%M_Z$fdvqmV~6`uGf;O6c9wEb_wR|%dm_5fg`gpb;r>#<>*MDtvcg z3o4`k4}<@9gthHsgANdGLYBx-04=UiC+ zgtx`;(%pDNw6+z+dMM~~wAIob)f7l3O?r~tTv#gR=?8yK^s=RR&X)H6=K3ZKRl}k=ScI;32oeKJetPlr1fMJ}3LEfk7l?4v zBA=Xno$Q_5GDx)1&7Zp1v8yCnFS{p(v+(mh=jrA-HeScS?x~Ksep~^-`dT37y!f!c z3!m(#J@|s?X};uu42)`*?s_~Wcvltq`!08QmdolK!D8B_?6ubsH%7w?Z9>3pBLQ~< ziHmC^EoP3X`iPWcjz?>$b?I-yI7t&4;bc-Q4e3gxLcTTEFPhvOb&& zc7_4^tER`C*fQm z(0YQQ5 z(ONR}YT47VhI=~H&1@`H{0({DMV@!nB@w3vSFu4Qdrzwy zqh5V(;?jn>I)42w{6kRuqqJ&y!qCVRaF3a7tC$ka+!LMuIXI$T~9MKe8^sQ?}q*W>V(R{$@~R1E{oSVr|V(NgXOk z4_xzknuiS#X)JAH!(37tkR^S9K+~Qeohy9M1a(oTP>RlcmUagO8Eo`J^&$Gg;l%Wi zg=&xe6!A5pZUbBQvSHy3Hhm^eKV9b;OYx1iK0lX>T-CtLu_JclU9K6 z;Q251q{PDBU+H-rjvu&q@$$Mc{8N}{f}@t{c{q(zUe4Ddh($5(Jjny|dM>DrhHE45 zF}ip)X?F3QEsFgf^#as)cf+;(S5)-+DqMhmKGG~Mk`ZfY{L^1IFU1-lIE+6fJQ#?q zU?N`RK>u#p*$E(jdzO?0aRt-x0vS%gn6nnZD0#?1Qhk4B!?#p%H`d<0sVXVd_Y3Zmu(Mi?%}Wg2EgT`$;x3y!l{Yh`U)n$}ub z*=B0HT_jyEX@zBF<+fC=y*6%Pxk^dS|8t)A9T-U4`nJ2@-~PXi9-jN{Jm)#j?L6nr zhm{iU6%wW+=M>KxnzzSa#vco>LIls6qp-hRhd<*EVvokWT0~b2%WN-Pd>>dLY2$tA zI9#6IrrD4^b6%_4NVRg|q+K!^%P0;1@v~H>cctZEMkJ0Pxz(%MBS{IyKu>QpwI8Yg z-5W1bBwaxVzM(t?FUSJkkCHd8Lm`~*I+)MjOU|w{Dj-tT0)*2mC@7uq{EC{3g9dD@ z;UMgFI;c*aj=fzDZ$W3ZekoL|k0(rw=>8=>Lf#kJumbLVtRBdk8XQ4cqM)woP&G&$ z23=iH)eceB{R9kTuh}EEG{(m(SB{JM6^gK(WbX zrkDh8Ip)xT3cmV{+hL|=ibm9BTmtasKY}#cHnUd`lkLtM+$$?M=w=;5-u4Py4Mjr! ztKua}eSjZ$gm(W?d^k=gm@)zHT!EF~mr6;SXQ*-mK2xSxA)2%g15qus>alfD4YU2uw_wMT%mP z;t6Z(GN>3Fj5r6>W%n8`S-UQRvh@w58??dXB_%Ip)VQt}q01L}pq^PcYU)eAd;yCG z8%`mghfrhX;TxIVb2s^T>jzM(uomwvQun2ywNZ8115nB_NRa!Y-QNtYMX>`=#<9B3 zq>U0^*a7I7HN4??1v6^_$!w{9M>1PDE{+`t+FS@N9sI+)L4@rBZBDT}hr3sSHm~*6 zmWgEfHnz`1mdg-%A5{EQ;uuR%@l#s{u5M7VXFF<)ieb33qJfLuA_j$^?FSE>o<{u5 z6sc)SnTN$jGDIcz6PP1G9Gvgh#P0j^3Va<$WcLsg!FhfzeXeUGMn%)`C>^Np@@3bE zgArr}Dj@Wejf}MOJDeFav#rJ{xatD?jkO)Z$?+Wzor?k*i z-cGMzJO{eg*U}(*#ZHB8xn{A)Jdjzr(6k~FBBcX7`}p1HP3&rZhsvN~con-R0ga8W z$9qf;SE%m2GE*C@j!#$n`ENC>m{2wr8 z=8(2L;YsoHji?(&jOWrX$vI`M3YhVNX7dS9FK8J{rF#OWOx5S}+p0vmIxTq@*b-Ljvy(^#m!v3SCw*4fH8FP<}GL|wNx#dSUZ`rCmMdO?yUA8ssJF-+e^F40fp9QIrj zPF*j$!k{2r`2kMkp=`0XJg}U-SOLgz&*C|I330%O-osS_-Cr*u?EDH2ETFNw#$9U^|JajA zb>=8gh+M$fF}1CEjJ$TkOKfy>!*AeVSKdtVm}Tko${&j`B^ynBlKVoFvgSCk!d*K2=yGWU~rt`X96?1d_MBSO%+lv zht0h|mednvqEC9EPk{Q)J|MNE!bxizMu(86v}c|nlqYHDp2#wb33Ll&cX3Z<6{rYnILM2&m}l$I8>e2NgsStj(A?>1JG?^XcfkNKkN&7^$(_3f-6=k@4~#jimf z61n;FZwAN$pVv9P7hTSO1g|s8K`ux?gntXJuFAOKf@)t}Vqv$4J3PO4cP#h`1V=wX zc6dvIn!5c74jwDDz4LYZnaa5Va=&5Sa9?dSuA3OG@{ zsE|(+IbYkH7+Ao{dHmxcl%1vE46p|N=BB@l`ML)??H1f>n3!zzD^4|*h+3~7K+i;f{$ z%**J}vezC*{U#BY+>!v^-FdmWB@wPIGWwH5f3*11C3N8FPkJr#b(U}_1vq!>g{Ux` z+V73Rz&i^q6Ja@#3k=OQ5fluWviZ>2LwWG|RLs6b(y|j&*J-G$C|`C0L&1Hi->Piq zV9&lqp>xJHJ8u9)0*c2qyVChDKtVKNLTee`W#f9l3c7fSs<@f{_zL~C^E_|y^MPl` zfE(Ho_dL@x0(LB$@OoN5L>v6_O@D%}!bcmJHMU^h2)zurq&kfl4>L7ZCo$?<(MpJ3B>u_I+ zboNE#Ab)o$+;yF28WeUMZ<Y78^`atNqI_dj>{|2lT@A1Uh5JLDYm+Sqt#ku~Ot#f=s>1YFT z3sBaL_XXe&IF6dLkA-!w7{0_As8uiM5Nn!Jj6r|y1&~PKQg}-W@X$p9Thf^mvp_rb z=4wP`)W*Kd;UAnLuutDaQLGuiiJn)lXeBpxq|n`W6!CkAIM;NR zj>S3VIa-@{fM}z4L9s&4`L#1+>hnT|L!yUNz~PLq&C_%U2#DStp8AUnJQiolR3Jf} zQiq!iEW3pzyLf2>P0pl@5GcVC4OD=Z9X*PHj$<;YagUF|QCaNF8J6kO%eFq%sZ&H) zrSEUTx1frf=v;}kjL5)U5Ia#h)MJmrjsaslFFwir&1ERr`Ev0DZ_;A#yk{kzeo%zr z{$)7lI?5KtSWEE_kKj0xVC2ZL6As^w;yY-6f{kD-7}P#w8-6i0oX%sI$hK37^1$&T zm52DZ@9^WevMiSFEA0}$J%%^4hD6P0anOvW&^>h22eEA&`+08k-xlY;E!KZq0zX)c z2;HSK^JnFHJ|jI+$~Wnyh5s1tBhks9dCqy7A#&v(PAegV9!9v|e;U_*KoaTEE4%q= z4uX37;yI4-(wUx$_{bU4{m%Fpe&=OEX;h~IbKDf-^L<$4;vx`hiCOs=7rfyEM0&#y zDP`9{zVRpxP9~Lb(DSbECJb|Ykajv#YBX5OXsI0$uMvco!P@Wvk7YXlh9LHaeeB4rw5AUkO0na;U=qwFO zzTmHD{No5lo0k6ohEQ2y$yWq7@sUH{{}m0_Xnv=V!TmilxIxGO7bc!pd~>nwdLJ#r zLMR~O94CA)2g@s34L-)Oi^a*wrkSn2VSILHZhRig-<@-k?K1*w0g4!~Q3MP58c%FjmgOOYdLB;)7zDC%#ASy5-wP!5FIq|Sk)IFpTTekHa> z7~6!ka!9h$Vyd2`usppkr1uW5i>_Y}2jBPiG+|aK59=~xybW=v0eFv*8*BM!lU6}2kV-7ix6Ip|ja!FJ|}Is|2U zAme=hZW*_L$GKmmtU{vKy#a^6f%5mWe9zFL*od{IG1N6Odbd(}xWs)KsBTV6bF<2X zHa$n}6S`T9IK$8U_)*ZLGpEGBFd5xYM&+V!77!2RD~yt^Zl1*IOEE$it_=I4 z*Xfxb6+Mo%#DFnzdp7buLnj=*WW7+ zGS;1*ed7*Ty!9$de5C>Gj0$^>A{stib;K7X%)$cY!)Kl_u!YtO1Df@5U*N!(43hCu zQcUI&|8vBy{IUSSV73)O;5om#iO!xfVFOnbUs&K!^;yA}IiddjyBh(ecTX2da z3Te-VwMphn7Dw+qIOq7};<#Y^J_VZbS3;3cn>@ZJKrlqyS`e(r70?(gL(tA!UA(~+d)d|Qj^;khXiwIaj+}9aT zSV4@W^grzR(=V~j4qh7!A+!n$g9+>r6%=;S_qf!ZMet@;=#@loC1?a&4*Z#N6_x|S zYY8Tv#V2inH#X^zmBN~`hXQsg|9GPGnMQX%E z8eGm6r}!Ep3uGI&C_-G?AK1nXQYJy$v{76gT_s#$cg7UOKaVoz;L#8}qsgaTSwHBD zz;1?iXi;p4N29hhyNeHV-2f5>xy%&=p=DX`Cj4}B&WagLoi_QA2wj-BXt*meHm zSDrS+!Uru8BDr`v?3iPK0r6@h9nl8FUEF8tI0Rgm7aybi0JccjqAtDx6@ zdETRP7Q6y65`o%q_B4yH#zvBMz;07Es1X_uq8sS_V_C~3(q^Ad3num*hOsmk2;X7Y zk09JV)A}d|z83!Z`#>z=@jJtm%($+mU(bt zw8oTG2-#csh0K38{5R7dbpJHnaEj)fV&V59(61sFbQe5JF(G`8JGMmJN++-{*bMdN zv`=4zZaj+xA-=GP%zIsc#!FY9)j41VQqHH_O!Y#(G7E2~ED&WBeafN(mblGSKLkQN zB3BHAux$qm$GIp1d4uQ=G7>3?MW9VQ8zX^32;9*@GzUt71EM)}iWDT8bK*rdW-wx* zNL&!jiG^rRi*}eZe9|-*>FfP`?SRI<5Ysu*C8iV0M^ZMTn9g(J=^!hU_(&x9h;Dui z%TI!F;~FH!z{u^a$N`ibRFP?&6?vPIhBuDzy^6QthPRmYm>a71JO^CSYx_1vrc6Rz zMnGi9H?-3kIn5d2&xvt;PKiSsVZM%<7aoDzv4gr`rR0eVW zLC{3ETKgAV43QYft~=4g>;cmJb(7}Rk99wru z=InDYBc?Bk^d+tBZCv8)Z=B7%u=nMBBM}DP!{G&MyTo-3qPqsLSmDOmuJ9>T#4}W# zJs2an!oKK=iQaW2dUw>+3(euq$mo}jxX;LZ*Ws)6D9I}IN$kg~JKFfb$H{Jyz(#2@|i{Ft!! zKq1t057>%-)i#{_;ybq{cWbD#&uUC}nDlD4 z>G1oa;e&IG*$|BjI{-$Cjs-$Po3!Nb47)hLqgw~!HH9xW5J%Q62+tWil_+Ct-^pQh z5j;sp_AYCKpp7rY!awmXBMfom6XT=zZL5JHk~Z9n10s6pzK5An{0?SH_-)LL;Wsfe zmfuLGn!l4drF;`J@Fx~r8{EJg~ zGxJL4)y&M~ConUcAI(e)KZ2Qg{19f&<_9oy9^Z?Zh5Y4p)PiFE0yE3_&zV`lpJt|= z|B#tZ{vBp6;a_Lwa{d5Jssd9;!S5m8UG?ZG{z+zSBO2_#8z0A6vtm~L{16fxy z>qfFJW>#80^0zYUCbAYX>sGSPWY%qDy$M$L{=@(~7CObL^7Jx9FmXh%<1vUbhpDxV zBLPMD3}!xy@o7LQ^U3GK$frjpE1_4w5D_n?`N+d^kr*NpM4vYVpBO_#qUiIY;1g?z zkcmD|2tIL!h$PYHKEX$7h|r2YzhXX%;|(e!^U3F36#JC!GSeoXy>*mKoc(*sieVQ_2A$o*dA)Yf|N_zR}bL?0hd&B>Xkpy8xeEyt$ zIUu_J3Ak4pSNq$*Bx;kq>B*5M4(DLC*YFF#@;&>p(1|<99X$#*2XzV4ehsX_tIt7> zW@0?0A-ND|xi>u7vIrvWpDia+iJC=4qCEQg4N3yjfRI28-G<|ug_?^)_+0*a>=m)0 znB#LPFX-mS36+m&%82XM62B{s&p}m~Dh6E8`lc8%Vx1%K0frn2_Vaj8i?@v~s%smr zEU+BH!8*>UyY>Ufz&|)1h2lLU7EAg3GHi=*N9?to#k!7dtoeSmo!Y&+E_$wc6r;=) zkF5jbIVhLFN8?c{KZI>E@Z_P}&LNnZekeV8{EUzw03Z<3IqziPu#=ML% zIO4hhy2g|^t~t|?5wjvBr70t3GsR~#w;h=i&25FrXpV#{7|lu1ZM_)HNxNt+o@g$H z(Hzur1ZYkgpt(DccxBq6R^^+sFJ46c4oxuKoyKWWPr7@TMr${^gDxRjU7gqAtz*3z z8Da%;Ug&nBI+R6QnorPJkBV@T3(;$eK-U+j$059FFdPvi#x7<=5y?Am1QABp??4hm zPF(aIzlTNW_286eh^ZQmtjrLqe~zk;M=pKDVKamuL)AYIN?UZW7Y!R+1^b49AAY9= z(8CRNT8Nm+zGKl8>v^C-0iKkTCfL!nGE3vqA*VfwmyG3 z*qdSHAj68EhGMsV6_f%Y;!a=%8pJkYhXIZP2YLa6y0Z&~5}^azHwwrBPsQ+r7$J?+ zv%HOpYin{5OSt&qbr*htYB8CZ>_Vm_|ISO?SSL&_V6NbEs~71B#R8(vD^y%|Y17 z&t%t|iAS+*M^Am+o8#tFv80x%=Y6aK2M95q%ns!tJ_8lPeXPV|NtofW#LwVk&=>Cg zk>9zFrAua#GJDSa zIdgG?>~9HdM1;>VuxfScmf`c9wguoyg~41QkQg^?f4lrUD0tU=4$H^CeD+b_5CX(pg&s zZjYKE{z+$c7!Gu*RrwH&dpZtuT`xp}wzFLW5G`2J7dLahSLwqN(YtzC_)pG)*64s_ z1<40D(w@rwb;!z5xF}dIRlW(n5qD!pd{K4K-*p0qLA*TMK@3+-ya!T z=rUCtuKaN}_XY$(=z}N-^XBlp^+A+BKhm2PVxsA=7AF!(lKalY?mx`P)uh?UN4R1z= zWjfPKNPZUn&rCs=6q_lDHsd_)XWNARtS=^dcSgrAGNS76>K*<$Z6vUbt`nCD)4h<2 z7>BP6M29tBcer2koHO@V(Zz}A+dJS}4_``x(MVz*l75G1B)Iy(9e(j6oB`%u{75{8 z#F1i{a3J|n*jsZ0$AgRd3U|@Vzhum`^JFxtYh@v5$1MxTh8Dd+&G!#ZWS#C}TD$rZ zA@fh6LvR=m`sGhl264Znu8e5+pM$C51Qrue3E1M{AZl?E=!Q66Q_fMGSTwCT4$;-< z`T}&3N0o;{(03Sm?|6m_WaeDpUcraBW>Y^Aho%K2K3)s249FE9%s5WN1q_KM^Ar3o zj3az9b+X~&%DGUXLF?FDZ@OBXdM5JsfLgGgIOx=h>)7$LFio69Qd%hBvp@Ly)I}Hg z(HXk@v+%dN(MCk?TI14fMi`W7Kw{H*!W*upY33Av`8I}39LqzMNl?+UvqmYap zP6fr~QVdvdWknnU!G%rpTd`3ePEOs5f%NF8Y`xt-3t0X&o=+uZ#)!0j^%`$;;cKI+V%C0pp=B`=!x!@1| z4m)$OQ^J|V?$l>**(IfY)absa*7kOe!ItS4Sn0YChUhtEs1F#9O2CxLM2rv&Y36KaF1o#FD zi(3J&0{#Hl1h@-uJHQTD2)G3>37`NB14sa&fD33F4|oUg3Sb9dD_}jq0muiO7>f1) zIM{Chr~#7z^poJzWN`omAQR9FK)-OrKN*WW0owt00V)6s0Fwbp04bm^;NxpUxYq$Y z08*q|19K6;0?-2D0et}%koO6|A;3<+gMf8_<$yAP7HOvf76R%387FTnmdfPSY2 zBM-nKz#hQEfOUZ7fI`4zfC3-|L;-sK6nUIv^lDBLx`vZPJWAKw!tb6pImxr|3%JQ# zXf2jnD=Mmstj_Z4s;-c|f_1VTUoj?Yrf3S@_^g9$^7#ewuTpm}z5NV#+W$?qgp(u# z+OLOp8<@)gYXJ`c@}-<4Y9!QC1I}IxJ&Ss)*e0D>V{=F?j`GDf+m?!cX^y3KXLU|>v8%#{N~Kg-7uHM& zWKdLHIgzEDC`6;20(lj&gwjeer3JK%wuM7fFc_0P@lvFpNR_CWm|A76a!yREF1Ae+ zOB*krpqn6{xWrDSmR7iI6RT=+osROV(o{>E?+LS<~gk86>z{m ztIAeVVk>f%qm_A%@=A(3sv1xXSi*5XwcHGcquPNyXI43EMb)KM<;!fvQU|JnyhoM) zyUQ5)lk5Lu@b*)2m20y)ipr>yN~#^wWj06k_zGK9sk2O4!@83m#QZ19_&?nLQ*2c> zM|sg?Ymu|su~Zy9mKj-9PNgEDa)zxWt;%_$)NGQ<&==qpwo+?_)L|&EljX%uRRM97}-; zo1?^9WJ48hbCo-6HBzgzs=8|Y)S21Y(qdbUv%HG&@{yF}U()^`jsH%r{I4nh2jO2V z^GA-)EB5zK#rxQ?W2G}}rR6o~DdHxW7wpwlwkl_hw7g0$9aV!{;DjnyMa7L>W+&|m z?LZ{G)Kz6&SYeYotI^d$Zt(8PO=>brM=iNRUexbb$}dnSX<2pkt&GQY%g$kQx*S!Q z9&9vcbmq=+)E=5YzQ09RF0-o*uFf2;+zO6s)pL>-KpUVg0UwtEa5r+2^#F*RN$i02 zfD}N>7{nQkxBwaA*#SG?FB^x0vq@J6i%(e36pE%DCJyYsv%cY09X9;+=$ixoxaYY{ zTe`I)ur}CB$c66imVp%;Q@%PraJ`F@Z2i=9zkLEHk&Z|G0Cft~MF~>{P;-)PnjYHrJYYv6rTxw?Wkt3RJp?UEK*tHwAnjKR|Y82cFAY-Cr}q)p2KBaqgXV?wI7 z+ZaIGOq~hm3iIPEA!ZTWge|p<^}Se zP{v(8a&7#RwnsB}?()@taBbRMJ$O!)%~ou~(uBK>cu@hkkE_Btq15I~xKU~%Cdq0+ z6$l9p3yQQCU@Ozs*tObS{^i5Jwtn>SC#U{)`cwYQXTGz4|NM)#b6i3Y+JE)%|0mjS1qt+ALs!-0r8*qe zr3DLV<|Ka6-TfnZw~$zzqXX;QRHxHcX~!H`N_zR@pSqPfGOH%l<*a6HN&qjn!ojkY)|6GdDvG6Lw09{fcS>baEEg(i2B4wwQ`%Wx zRcu=#HJYT^Y-LKta+9h{T{N{|ywfHQHpKa<<*=sFnC{-lis~Atlp2asO0ZA)DQ&5C zmf0La_EZpaPUv17_pdB(BzACJJvPaOjh6#_YxHR8NTKX2hJ%g~8tBr6HmR$IsLNU< zP#5OQg)Ap5!fJmSKFa?e#rb;+p7kg`89({XnKyA`pF2F;u^4YO08?`dGN$IvoRLwG zJ2NdUBR4l;R81l#_Zy`p*xv#%Y^oCG2&rtzsM$ZBCUb0+UHgE&I9(1&<|IRNg*m(h zhnfj8DBI{CA{h-o@}F6CEBc{|4S28tcBGMq@L33>@4*M=aa4s114tu&gLhHFFQba4 zNlSG(EwSNsGnc{5;HKhlHmBqi+ypKiwrmzT9lPn^xRWbtCOd344rZ>+nQz6kO7d;5uYJYL;PYZdz4 z6J+XaZSmp&~gHEU{@&kVIpnbf~;Dn_T> zUQsR_Yk?8hNGq+y;QH_vW(b-dDynh%1tug-lLpGJC|~HXI+lXF6;(TMWa6w?I-#$H z?e(zou5w~y%2EX7KBh)mjx zKKMR0nC%D`x=P`eF0FPsf-x~cn&p%tY9+Dpit<}&J|qN)gS5H?3(yKWnj%b5oyODV zg$?F}iA5C-3~g5xPOC7>uzF(a8K@NYsty-z+okx=0@Euha;)W5tnJip1+}}X+9|bF zRl7>TQth;jrLz_~f5c9QrW6rj!T6H*z*lG6u{Wa5n~{h10N>6sRJs~#<_SL>A5;$2 zO-5xM-`2O znrd`cv2AgAQBYhFN46z)j2p0j!c5o5WO$Y{{J2n}7^f2Z2W)5@I2|P_xV2~etK&sX zRBUxxQQiJRmj?|?Ht6Y$1V4s!JWr>jWS20jn$d?M z3fX#lWZ%A+#3-6xN{R97RadvBZgm|dh_E$vYi_TLiHN`q(a_LYr>YITPxDNnM<~ zXMyEb*XBAI6DEJGARW^dE&}~$b@8%PAx*Zm#+k}a`YqLVm!0J!xP@%E9IOVcrM5}c zOH#R+xfwH*iV5l2SZ+wuz=JDnpj9DjX*ONgR6E8XUujwerU_|gInD!cc)e7bZmTJB zl;diI1#{9#*lUTA*hU43g8jOpV`7vhaMHY`c3Y233K@IGAvl^@gn`5cYpKiXC?3NV zR^ou66ys|#+6+!!RBd0%7(`$)D91<>D_bbWSxo!C?~MsM_#ycfF9mg?N+Ro`!%uMv zqDh6&!xaYNbTVGtB#EJgwz5FA|7n$i;(lKfzk`pUIt5_@_4*Fpy5`T|h=ukFOp-M2 z!ET8L=FXQEVN(Q-FSM^`cu;6zb!AmKo6u{@OR3)jIS}1c*=py`M?HkTU0lNWez%_M zET(%L^!bduyez_PMkZJ2Dt2P^KqrS-H6ZLe*y+rM3&LRl-&bsOP`oRmFDRU*qClSq zqYd;UPKU+iHC-sGwAz7(0BwC=B?57-SOIE#Cw*SI0);i0eyt7x<+Psa%1ES3TZt8O z-<2`ml|SpUE2S4I-!0cd;hIXwzuTA<2P@UviSb=sBE(rtO9b@U;)SgD$+J+T1hOvx zvs(@r4KNx_oY`#RM!`N2Fdi@tFcz?UIpU0lNofEp0dz2t2op&-OdA}Mb4Hk>022Y@ z&0{$v;l`1j0Ds28F_wjKT;PBcPLAkwB+{DzBjGk8BQr7;>6y_o#VaVlLZza-6ld75 zV@Zo!yCU2R6%^Q=4s{YttYE3hEN%(dvEV2upyhVJj`db&9J`ggg}iYbSyX{MhkdCtwF)J761ND_|4g0l-GU2Ecm2IzS_!9#98Z4p;(k z0_=baKpCJIPzaa@m<`ASWCDzUBtQZ{0tg4R`Ed>p*a27%umdcBM1TZv_A)0q3fKy0 z1S|nK0TqCG04+cUhzG;~+P~!_CjdJDjetTx5+D|E9yn_OYy~U<%mWZkgj^qC8@4K$P@%pd0@B6RE|JCmk|10hrGc)y-m3lf&hkOox ziP9YWq-0r&AD5FevAB35EcjV+pxhsTGklz{vZH=(YW3ny`!wui$vf9($7P}rA=QOD zHmoIYa#gTAZo(NB<&|o8l+yw+hj!mEC)*Y}urn6oSgV*7Z29Z82r+vO`SF~ zFP_UJ$N%t0Pf<7FZien*f`57NKaRVefFC_GJq33gAPVuxpW-ma2RljjlV1ve(%BJ4 zZu$j>C5UOspVDgK4&+CEfghz0j!XU<;aAop{SvtAL_Z1-qz^7LkVdwMS&2iBr_a9O@$vyz( zccpOh_tLcZJ>idKbq_xgls~1*xw(7zp&sF@vb%@3^ax*;(>*-6{*?dC)4xCbKFB~* zysNdZlZ3x-9qf209UK;opqe# zNkDLTPkeQ_yN4eQ%AeAG)*ytFeHZM3w)M>C$)@i4^u+sNPxtVi?YzU=RnC!hUFG!5 zr{5jj^XVD>@%`Pyd)Bk>1Kq=WrcZk4`@=79?jGI~Uq>J59^NzmVZZMl-m`wIwsj8= zrkmeB&PkpIP+tW4AvpZ+2s_s!9Qx`z>4nmLy1je);PT6!giJjkIRBpQyR@TwcyM}3 z*Z2AF4jysMwEL>}+vzz7Tw=n;=-@^1F$&|EQCbN$_nRMI`;=8$BerdR);Gmtm%LgJG zELK>MUmD%^mqQ#q3s<%{m*|S!88R7{3N>ZiKnhE@;mUmyu10Rf8717=)un>K<_u9% zT-wbywYV74>>PK!=o@g465WAROv)xzkQ^CGVQ5gU%|REhIqc>If+|dsb3!rBH5)j~ zi~=Ty@^`Z3;H(oOC+o?MbZIU~G*mhB*aZ%EE%~N8t8synRaN9*cMKdiq{|m8y?}33 zjop#NN-%`xW~621W#q78vamkJdOO5&vs}(Rax1ExTxpPdX(c_`NfCU~=}5MEDdEqy z#K~ofzU8Ix2QKNPb*$i*R&IxE*fcvQ7u`Ut( zbe6+v?|k)?N9Ep}F{eNfQ%cX6k?UeNjTYBJ2)dv^x^jdxQJ_SSE^s!(iYv-WZh64h znUyby4szV8PCq1O^gm7T$ynlaK+pxgf*=$3ir_CcYI>k1A_lHpk7+$(byk!6QpgP+ z)WASr&J4sCOYe$*Qy>Hs2??32wmiEl9_txG9Wa1ebKDo5WzMXs4Jvh@fN8G|j#;3K$raYp8pyiVDA@qQ z&cl*jK)15$B%voUcXj#*E#&MKMZ$37o($R-&HtgENLxP3&cuFnQGp!mj5zu!-b9bjxEe=}*R)5bMlIUn@GL#*G@7^~vAP0S?@eQs8GzHrgXaGe*% z-V0dpLWu;qAd1D1gR_(pYJVPvi-@hqsWfpy63GpOJ&j>$DFk*Z^B@VBTEp-M8|Mw5 z1w7fv&82Z^xB(B)^?O_f$!qpRBlkrpo?I@reaF9pdM>D+@&YXo>1Zo!-wd1Je3ZEu z1K{SeI$IcSnfC=|ksxh^^Q#94hY9feXQMq>qxiN&mk9hLQt_EG% z1ajk2Lnc?aYRV`dLG&4;$s|xENf#28qJG1|Iw>NrIzT2|O80o6iH}0F({jMP*&+++ zJ_&Xs{C1(gFk_RbSSoj4Pz|TKoOYL!>bM*^q*vrOg$7 z10O8CiM(&Pp6;B4GaWKx;f@1H0mpCPI0ejZuZCHMQ*(hK@zofD|I2^ck8zLs3GQDn z?s#kQ;g-oSN$>f?w8`}^aPV7pgRob-S`#gZv*B}2GWB%IpPm`|ayrGM{G?wA=_Vwn z=Ho3#ft6O1DYzd`N#TOS=y3&`8pN<$(JIj72#d8sI)Is*_fBKUk zA)O=k2GH9F%Bt`@@Bx68(Z>*&bgo7~amgfk>R3Popg#ce&Fs59ln|>WurxD7|6I|;kYf4Ifk|c4 zfS`OT0aAb+5C_0^fYqPcLG`HvP@Pwc=It=4Ep7mX*8?aY$b{pm9mhQ=x;Mk5`fUYJ zdA|oxyxjn5)9U~Ve-}XEu|NvpD}Xb?Fa0P^+t-|gaJ3&MmAw%r>uQ*Ur{nFMg!0@9 zlWKX~<(n1Ns-=FHHdiJa{?=dA;{R!ns zuloD<)aO5)>(zx4gqmFa()>SN(|=!iS0w?F_O40-(tlr_{ugroFDC`!6(OPFkz27a zE4+Q_?IT4LKKfU|viT9gT>VfhErqz0m37SAhWO1}-rph>g1_^3^<}{;MKquLf$>wt z_`4AQjfYP?JdFF>!~DZ@ub%(P%`C-qEYzpJzu&Sl+ymGNcnk0)fRH#5kOQay{AYh5Y;lCWQ(gCRg>w?w75s^INS3G20)2cKlRba- zuq1fpbGcjr{?fbXTrP{7hS$*rFsE{pVW;1V;a^~!hG35w!j1ns?KK568DX^NLowf> zofZ&N2=a4c_gap3$EC=v96MYaa<1Y^@G_d?wzD`gF7PjjTL`F` zCYFx{sjKnc+=a57=-2MK{m{I$*!737zH+i0s?f5EAS}#+ua#SjziB9MDQgk+sTL z1WXX+6{9uj3{O|X;doAsa%^Zr2`I4$c(nq%NesJ`5?U$Y1DQUSUoa)(Zjp@wp|w?L zBT+^dB~Ypq!=!f$>zxZAbU~vq0A@yhb%^}k(=ek@?Xgd6?sVyCAB2g zsoK@IY97{3*WYDaXa-5LGoS&o(XxrMHL@MDuVmlKZj^73Z<2>8-cv*70ylcvv1b>;^1TJv4zC(R&G#ykJ4^d@afdOqm} zl}R;K^=s7(^*qfBn*Ex=+F{!3wPUomX}{98YroY-=$7i%>rU!U>3SO)4YwKJGM+I0 z#rQX4oGIS4*0jg;l4-x`Ra1-U9hCF2i8p<2I&b<0&zE|cqs)WMvE~uxc=K4Z%&ayW z&6CWT=Beg9^DX8=bBVda>@Y7euQb=AX1_ImWd7XTZtjmy$`j!;TQV(~>30v@)YCMV2nhlx52-vioJ5 zWLst1WZPx!vcB>;@&dVCeuw;S`ETW$<-eCdDc>poll&F=VR?)EUHOOd)AG;d7v!Oe zehP_Vh(fBkQIVubQDi7)DsE8}DoPZ~6>i1dik*r+%8|-!rCa%HWn|LGq??kaC6$0O zjwD^D8VMXOQ8lSfsQ#w9R(+HD7U1eub**}x`Zwy|tDjQ8t^PoLTK%QEw`PFm2F;C{ zR85X1S5u&=(l|9MG)W%#vWtKn(GPQ%NF{f0LT#|x}!1M?uL0O~Xv1O*ffxO?9Tc zM$R_#v*y>#?}1LwnR_KqPR>nUk$iiyH~B8~8CM87QqZSSmL;1ZE0O(5=9Rq++8iNY zATN`zmp>-|Og>DJs933}S8P)pQ1FVc6roDJGF`br`H=E4bk&Q{yh73wX(aD+w!YAnzcYZ^3bH7{xQX^v^$14hqk{sD{*(vHBGov77m zleHPzQf;gDwAPQY`?l^&-6VY+@MSdQ8J;lw*}wx6A;vz&fyUv+RAZJg-!$LUigEib zX!Z;9oykupznzRp13VJv=AgPF*;C2~lM>Xk)XUWc+PV4yeUZLYf2+P)U!$+p*X!@m zml+(u`Td5?hUX1`L*DU5jj`Ie%=kN_)l_A2o9;IK!F0wH0)8Mf>&z+UEa3Ck=3V9w z%v|y{$>Wku$ySVZt_})OP-d+xQNB}gPH{O{3p zouQtIaZ8j~5B~T^^&yPjPu2Z3DVk{*ugf)0X#TG8YwAH`&uBl@Uew0x5_LE0=ISbS z4&7mJv%l*G>2vh+^-J^{^`Gngdc7gv@QmRF!)Jzev>?@(Ykbc5it#hX8#l}i=lP@AcW*@{~gcYq!*DF!H4Du1i|L^&;~D(RD? zFOou4F{+6w4KSats#bYa8&vnJwy3tLcB?*A^;XBKN2_mF-wT|7qW(giuBpWcAEuRJ z&Zq^Sd`bHy#&kIFU95}6c%BGMTR}4|`rd{?2B~3`VXR@IA=yx6SZ>&7IArK=9Aq47 z9AO+~9BaJK_^@%CQDI8Om@F{eVY<<*GN&d#k-P_d@rz_`hd`I(W%Fc%(WVJ#gIQUs z+^d|bnxMX@PSBV&t27U44BBe#gWBtKx9gtPy`p(?5j#s=e^#y+NBm=aBSriG?Ordo`chfLc{&zcUI-Y~sw@|k*> zpD+)l{sfargyF_4a8wo{A0$5~=M-xc>lAk??o~VpN|GcENg9@PJ#8Oag?dMU_s1w= z74eD$^mn164m_w4Db_1CC^jk{P;63c1y90j!F) z%D2h4%Xff!_sCz6@5Q(}BtMGT?Kno)NqMXMl$-}Wx5>}T+vOd=MYtkT5omE7W;|+j zq5>LO6k3H5b6>h5Q<1H(DDr@ddB8@oqD)btuq&L3C72EWId1kV4k?aeq#jqCP@Gh> zDufobp-nrDdq7Vu##ZAgBX4XoMw+6qLP|1eP1z=ksnAqxa+;Q4mf48)(oxfK(+P0h z^QI1y#GDAeXffN(>#;6qF}Il|$+Bci^1S3F$@R&bl3z&Pn|w046|WCM*qd%nCY2?E zFPF(0WgD=9Y>~CeByySDf|bB}hAYBPo1Cy9Lrd-GX-*mfi5m2SEL;{Vi(@Mai!4uO zm#s%08)bX2a*dTg07}>nDtH0h=qOrv60M`&>QIC$qm(h|t$1akG6`Hc9jgV4a<;M! zYos#G5>2CKqh_1tsOE&`q^4DKO2cc~Fw;wov8Fha6j(?AZOa%gjKD>nX|`z|!$=uM zoSoriIWSXiYBa4gtv77|jvg>=GHt~e*bctC6RXS@OnZU1L%>`M!`(?>?-cm)SyLOZ z*bY2$W(hd6u;NN!I88EZvF6MKK4WO?{QUbF13zQnXAJy|fuAw(GX{Rfz|R=?83R9K R;Aaf{jDepq@FOws{{iUBpdA1J diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 574168d17f2e..c22f5e671162 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -860,6 +860,23 @@ let FakeSvc = { /* * Commonly-used services */ +let cryptoContractID = "@labs.mozilla.com/Weave/Crypto"; + +{ + let versSvc = Cc["@mozilla.org/xpcom/version-comparator;1"]. + getService(Ci.nsIVersionComparator); + let appinfo = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo); + let platVers = appinfo.platformVersion; + + if (versSvc.compare(platVers, "1.9.3a3") < 0) { + // use old binary component + cryptoContractID += ";1"; + } else { + // use new JS-CTypes component + cryptoContractID += ";2"; + } +} let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); @@ -867,7 +884,7 @@ Svc.Obs = Observers; [["Annos", "@mozilla.org/browser/annotation-service;1", "nsIAnnotationService"], ["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], - ["Crypto", "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"], + ["Crypto", cryptoContractID, "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], ["Env", "@mozilla.org/process/environment;1", "nsIEnvironment"], ["Favicon", "@mozilla.org/browser/favicon-service;1", "nsIFaviconService"], diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index dc3e0d9c7e25..4e8f9ec0bba2 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -28,6 +28,7 @@ pref("extensions.weave.log.logger.engine.history", "Debug"); pref("extensions.weave.log.logger.engine.passwords", "Debug"); pref("extensions.weave.log.logger.engine.prefs", "Debug"); pref("extensions.weave.log.logger.engine.tabs", "Debug"); +pref("extensions.weave.log.cryptoDebug", false); // Preferences to be synced by default pref("extensions.weave.prefs.sync.accessibility.blockautorefresh", true); diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index fa37973e1f3e..83403fd72bfc 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -3,6 +3,23 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +let versSvc = Cc["@mozilla.org/xpcom/version-comparator;1"]. + getService(Ci.nsIVersionComparator); +let appinfo = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo); +let platVers = appinfo.platformVersion; + +let cryptoContractID = "@labs.mozilla.com/Weave/Crypto"; +if (versSvc.compare(platVers, "1.9.3a3") < 0) { + // use old binary component + cryptoContractID += ";1"; +} else { + // use new JS-CTypes component + cryptoContractID += ";2"; +} +dump("Using crypto contract " + cryptoContractID + "\n"); + + // initialize nss let ch = Cc["@mozilla.org/security/hash;1"]. createInstance(Ci.nsICryptoHash); diff --git a/services/sync/tests/unit/test_crypto_crypt.js b/services/sync/tests/unit/test_crypto_crypt.js index 6ea0af0574f6..0f42c8d71c1f 100644 --- a/services/sync/tests/unit/test_crypto_crypt.js +++ b/services/sync/tests/unit/test_crypto_crypt.js @@ -1,5 +1,5 @@ function run_test() { - var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + var cryptoSvc = Cc[cryptoContractID]. getService(Ci.IWeaveCrypto); // First, do a normal run with expected usage... Generate a random key and diff --git a/services/sync/tests/unit/test_crypto_keypair.js b/services/sync/tests/unit/test_crypto_keypair.js index 174f158c3035..a82ccbfe9d9b 100644 --- a/services/sync/tests/unit/test_crypto_keypair.js +++ b/services/sync/tests/unit/test_crypto_keypair.js @@ -1,5 +1,5 @@ function run_test() { - var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + var cryptoSvc = Cc[cryptoContractID]. getService(Ci.IWeaveCrypto); var salt = cryptoSvc.generateRandomBytes(16); diff --git a/services/sync/tests/unit/test_crypto_random.js b/services/sync/tests/unit/test_crypto_random.js index 426e35319bd1..f5873dd28174 100644 --- a/services/sync/tests/unit/test_crypto_random.js +++ b/services/sync/tests/unit/test_crypto_random.js @@ -1,5 +1,5 @@ function run_test() { - var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + var cryptoSvc = Cc[cryptoContractID]. getService(Ci.IWeaveCrypto); // Test salt generation. diff --git a/services/sync/tests/unit/test_crypto_rewrap.js b/services/sync/tests/unit/test_crypto_rewrap.js index a8ecde0e3586..7bd5eea3d5d1 100644 --- a/services/sync/tests/unit/test_crypto_rewrap.js +++ b/services/sync/tests/unit/test_crypto_rewrap.js @@ -1,5 +1,5 @@ function run_test() { - var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + var cryptoSvc = Cc[cryptoContractID]. getService(Ci.IWeaveCrypto); var salt = cryptoSvc.generateRandomBytes(16); diff --git a/services/sync/tests/unit/test_crypto_verify.js b/services/sync/tests/unit/test_crypto_verify.js index f55f7c004d1c..610a3fbc38e7 100644 --- a/services/sync/tests/unit/test_crypto_verify.js +++ b/services/sync/tests/unit/test_crypto_verify.js @@ -1,5 +1,5 @@ function run_test() { - var cryptoSvc = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + var cryptoSvc = Cc[cryptoContractID]. getService(Ci.IWeaveCrypto); var salt = cryptoSvc.generateRandomBytes(16); diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 04309d9ce2ff..05b1d363a607 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -62,7 +62,7 @@ function run_test() { keys = PubKeys.createKeypair(passphrase, "http://localhost:8080/pubkey", "http://localhost:8080/privkey"); - let crypto = Cc["@labs.mozilla.com/Weave/Crypto;1"]. + let crypto = Cc[cryptoContractID]. getService(Ci.IWeaveCrypto); keys.symkey = crypto.generateRandomKey(); keys.wrappedkey = crypto.wrapSymmetricKey(keys.symkey, keys.pubkey.keyData); From 644c06badbe5cdf0b001255b91dbadc4309a50e2 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 9 Apr 2010 13:20:41 -0400 Subject: [PATCH 1637/1860] Bug 556930 - Wrong secret phrase warning shows up when logging in, r=Mardak --- services/sync/modules/constants.js | 1 + services/sync/modules/service.js | 46 ++++++++++++++++-------------- services/sync/modules/status.js | 4 ++- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 58d2f74fd075..28908d3d0291 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -86,6 +86,7 @@ STATUS_OK: "success.status_ok", SYNC_FAILED: "error.sync.failed", LOGIN_FAILED: "error.login.failed", SYNC_FAILED_PARTIAL: "error.sync.failed_partial", +CLIENT_NOT_CONFIGURED: "service.client_not_configured", STATUS_DISABLED: "service.disabled", // success states diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 8f7354f7d4a0..bd95a562954f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -291,11 +291,8 @@ WeaveSvc.prototype = { // Send an event now that Weave service is ready Svc.Obs.notify("weave:service:ready"); - // Do some preliminary Status setting, in case autoconnect is false. - this._setBasicLoginStatus(); - // Wait a little before checking how long to wait to autoconnect - if (Svc.Prefs.get("autoconnect")) { + if (this._checkSetup() == STATUS_OK && Svc.Prefs.get("autoconnect")) { Utils.delay(function() { // Figure out how many seconds to delay autoconnect based on the app let wait = 3; @@ -317,6 +314,25 @@ WeaveSvc.prototype = { } }, + _checkSetup: function WeaveSvc__checkSetup() { + if (!this.username) { + this._log.debug("checkSetup: no username set"); + Status.login = Weave.LOGIN_FAILED_NO_USERNAME; + } + else if (!Utils.mpLocked() && !this.password) { + this._log.debug("checkSetup: no password set"); + Status.login = Weave.LOGIN_FAILED_NO_PASSWORD; + } + else if (!Utils.mpLocked() && !this.passphrase) { + this._log.debug("checkSetup: no passphrase set"); + Status.login = Weave.LOGIN_FAILED_NO_PASSPHRASE; + } + else + Status.service = STATUS_OK; + + return Status.service; + }, + _initLogs: function WeaveSvc__initLogs() { this._log = Log4Moz.repository.getLogger("Service.Main"); this._log.level = @@ -644,17 +660,6 @@ WeaveSvc.prototype = { Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer"); }, - _setBasicLoginStatus: function _setBasicLoginStatus() { - // Some funky logic to make sure if there's a MP, we don't give the error - // until we actually try to login. - if (!this.username) - Status.login = Weave.LOGIN_FAILED_NO_USERNAME; - else if (!(Utils.mpLocked() || this.password)) - Status.login = Weave.LOGIN_FAILED_NO_PASSWORD; - else if (!(Utils.mpLocked() || this.passphrase)) - Status.login = Weave.LOGIN_FAILED_NO_PASSPHRASE; - }, - persistLogin: function persistLogin() { // Canceled master password prompt can prevent these from succeeding try { @@ -677,16 +682,13 @@ WeaveSvc.prototype = { if (passphrase) this.passphrase = passphrase; - this._setBasicLoginStatus(); - if (!this.username) - throw "No username set, login failed"; - if (!this.password) - throw "No password given or found in password manager"; + if (this._checkSetup() == CLIENT_NOT_CONFIGURED) + throw "aborting login, client not configured"; + this._log.info("Logging in user " + this.username); if (!this._verifyLogin()) { - // verifyLogin sets the failure states here. They might also be set by - // _setBasicLoginStatus for ERROR_FAILED_NO_* + // verifyLogin sets the failure states here. throw "Login failed: " + Status.login; } diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js index fc9526b2bb09..95278fc6f2a7 100644 --- a/services/sync/modules/status.js +++ b/services/sync/modules/status.js @@ -46,8 +46,10 @@ let Status = { set login(code) { this._login = code; - if (code != LOGIN_SUCCEEDED) + if (code == LOGIN_FAILED_NETWORK_ERROR || code == LOGIN_FAILED_SERVER_ERROR) this.service = LOGIN_FAILED; + else if (code != LOGIN_SUCCEEDED) // missing/wrong user/pass/pp + this.service = CLIENT_NOT_CONFIGURED; else this.service = STATUS_OK; }, From 6301c2403d756d43a840dad58a7995772da424b6 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 20 Apr 2010 21:56:44 -0400 Subject: [PATCH 1638/1860] Bug 526012 - audit observer service usage, r=mardak --- services/sync/modules/engines/forms.js | 2 +- services/sync/modules/service.js | 16 ++++++++-------- services/sync/modules/util.js | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 11acd44db366..aa1df28b543a 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -216,7 +216,7 @@ FormStore.prototype = { function FormTracker(name) { Tracker.call(this, name); Svc.Obs.add("form-notifier", this); - Svc.Observer.addObserver(this, "earlyformsubmit", false); + Svc.Obs.add("earlyformsubmit", this); } FormTracker.prototype = { __proto__: Tracker.prototype, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bd95a562954f..82034fa225da 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -269,12 +269,12 @@ WeaveSvc.prototype = { "Weave, since it will not work correctly."); } - Svc.Observer.addObserver(this, "network:offline-status-changed", true); - Svc.Observer.addObserver(this, "private-browsing", true); - Svc.Observer.addObserver(this, "weave:service:sync:finish", true); - Svc.Observer.addObserver(this, "weave:service:sync:error", true); - Svc.Observer.addObserver(this, "weave:service:backoff:interval", true); - Svc.Observer.addObserver(this, "weave:engine:score:updated", true); + Svc.Obs.add("network:offline-status-changed", this); + Svc.Obs.add("private-browsing", this); + Svc.Obs.add("weave:service:sync:finish", this); + Svc.Obs.add("weave:service:sync:error", this); + Svc.Obs.add("weave:service:backoff:interval", this); + Svc.Obs.add("weave:engine:score:updated", this); if (!this.enabled) this._log.info("Weave Sync disabled"); @@ -522,7 +522,7 @@ WeaveSvc.prototype = { // to succeed, since that probably means we just don't have storage if (this.clusterURL == "" && !this._setCluster()) { Status.sync = NO_SYNC_NODE_FOUND; - Svc.Observer.notifyObservers(null, "weave:service:sync:delayed", ""); + Svc.Obs.notify("weave:service:sync:delayed"); return true; } @@ -717,7 +717,7 @@ WeaveSvc.prototype = { this._checkSyncStatus(); Svc.Prefs.set("autoconnect", false); - Svc.Observer.notifyObservers(null, "weave:service:logout:finish", ""); + Svc.Obs.notify("weave:service:logout:finish"); }, _errorStr: function WeaveSvc__errorStr(code) { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index c22f5e671162..63102f618820 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -895,7 +895,6 @@ Svc.Obs = Observers; ["KeyFactory", "@mozilla.org/security/keyobjectfactory;1", "nsIKeyObjectFactory"], ["Login", "@mozilla.org/login-manager;1", "nsILoginManager"], ["Memory", "@mozilla.org/xpcom/memory-service;1", "nsIMemory"], - ["Observer", "@mozilla.org/observer-service;1", "nsIObserverService"], ["Private", "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService"], ["Profiles", "@mozilla.org/toolkit/profile-service;1", "nsIToolkitProfileService"], ["Prompt", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"], From 60faf01133aacbbf93c0b1c1fe24caf13e931134 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 21 Apr 2010 20:40:42 -0400 Subject: [PATCH 1639/1860] Bug 560937 - move all setup into a single wizard, and clean up interactions, r=mardak --- services/sync/locales/en-US/fx-prefs.dtd | 31 ++++++++++++++----- .../sync/locales/en-US/fx-prefs.properties | 3 ++ .../locales/en-US/generic-change.properties | 11 ++++++- services/sync/modules/service.js | 13 +++++--- services/sync/modules/status.js | 8 +++-- 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 9941413dbb13..d83e2598e366 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -1,9 +1,12 @@ + + + - + - + @@ -24,19 +27,29 @@ - + + + + - + + + + - - - - + + + + + + + + @@ -86,6 +99,8 @@ + + diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties index 7991da080a02..fb7a8dddcc9f 100644 --- a/services/sync/locales/en-US/fx-prefs.properties +++ b/services/sync/locales/en-US/fx-prefs.properties @@ -4,6 +4,9 @@ startSyncing.label = Start Syncing usernameNotAvailable.label = Already in use +later.label = Later +cancelSetup.label = Cancel Setup + passwordMismatch.label = Passwords do not match passwordTooWeak.label = Password is too weak diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties index a5051e8ee063..ede890364843 100644 --- a/services/sync/locales/en-US/generic-change.properties +++ b/services/sync/locales/en-US/generic-change.properties @@ -10,7 +10,6 @@ change.password.status.pwSameAsUsername = The password cannot be the same as the change.password.introText = Your password must be at least 8 characters long. It cannot be the same as either your user name or your secret phrase. change.password.warningText = Note: All of your other devices will be unable to connect to your account once you change this password. - change.passphrase.title = Change your Secret Phrase change.passphrase.acceptButton = Change Secret Phrase change.passphrase.label = Changing secret phrase and uploading local data, please wait… @@ -29,3 +28,13 @@ change.passphrase.warningText = Note: This will erase all data stored on the Wea new.password.label = Enter your new password new.password.confirm = Confirm your new password + +new.password.title = Update Password +new.password.introText = Your password was rejected by the server, please update your password. +new.password.acceptButton = Update Password +new.password.status.incorrect = Password incorrect, please try again. + +new.passphrase.title = Update Secret Phrase +new.passphrase.introText = Your secret phrase has changed, please enter your new secret phrase +new.passphrase.acceptButton = Update Secret Phrase +new.passphrase.status.incorrect = Secret phrase incorrect, please try again. \ No newline at end of file diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 82034fa225da..58a72e6f2a02 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -317,15 +317,15 @@ WeaveSvc.prototype = { _checkSetup: function WeaveSvc__checkSetup() { if (!this.username) { this._log.debug("checkSetup: no username set"); - Status.login = Weave.LOGIN_FAILED_NO_USERNAME; + Status.login = LOGIN_FAILED_NO_USERNAME; } else if (!Utils.mpLocked() && !this.password) { this._log.debug("checkSetup: no password set"); - Status.login = Weave.LOGIN_FAILED_NO_PASSWORD; + Status.login = LOGIN_FAILED_NO_PASSWORD; } else if (!Utils.mpLocked() && !this.passphrase) { this._log.debug("checkSetup: no passphrase set"); - Status.login = Weave.LOGIN_FAILED_NO_PASSPHRASE; + Status.login = LOGIN_FAILED_NO_PASSPHRASE; } else Status.service = STATUS_OK; @@ -626,7 +626,7 @@ WeaveSvc.prototype = { startOver: function() { // Set a username error so the status message shows "set up..." - Status.login = Weave.LOGIN_FAILED_NO_USERNAME; + Status.login = LOGIN_FAILED_NO_USERNAME; this.logout(); // Reset all engines this.resetClient(); @@ -636,6 +636,7 @@ WeaveSvc.prototype = { Svc.Login.findLogins({}, PWDMGR_HOST, "", "").map(function(login) { Svc.Login.removeLogin(login); }); + Svc.Obs.notify("weave:service:start-over"); }, _autoConnect: let (attempts = 0) function _autoConnect() { @@ -987,7 +988,6 @@ WeaveSvc.prototype = { // if we're in backoff, we'll schedule the next sync if (this._checkSync([kSyncBackoffNotMet])) { this._clearSyncTriggers(); - Status.service = STATUS_DISABLED; return; } @@ -1163,6 +1163,9 @@ WeaveSvc.prototype = { // Wipe data in the desired direction if necessary switch (Svc.Prefs.get("firstSync")) { + case "resetClient": + this.resetClient(Engines.getEnabled().map(function(e) e.name)); + break; case "wipeClient": this.wipeClient(Engines.getEnabled().map(function(e) e.name)); break; diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js index 95278fc6f2a7..e59128dc4c84 100644 --- a/services/sync/modules/status.js +++ b/services/sync/modules/status.js @@ -46,10 +46,12 @@ let Status = { set login(code) { this._login = code; - if (code == LOGIN_FAILED_NETWORK_ERROR || code == LOGIN_FAILED_SERVER_ERROR) + if (code == LOGIN_FAILED_NO_USERNAME || + code == LOGIN_FAILED_NO_PASSWORD || + code == LOGIN_FAILED_NO_PASSPHRASE) + this.service = CLIENT_NOT_CONFIGURED; + else if (code != LOGIN_SUCCEEDED) this.service = LOGIN_FAILED; - else if (code != LOGIN_SUCCEEDED) // missing/wrong user/pass/pp - this.service = CLIENT_NOT_CONFIGURED; else this.service = STATUS_OK; }, From e8f5c3ab7fbb63ca0adce7750c7f710bc4d6cac8 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 26 Apr 2010 11:37:12 -0700 Subject: [PATCH 1640/1860] Bug 561638 - Weave Error in Trunk Nightly (Status Bar Icon Missing) [r=mconnor] Remove the extra name from get/set declarations now that Spidermonkey doesn't want them. --- services/sync/modules/identity.js | 8 +++----- services/sync/modules/service.js | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 82e690046e9f..2a3ef3b9aac7 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -81,7 +81,7 @@ function Identity(realm, username, password) { this._password = password; } Identity.prototype = { - get password password() { + get password() { // Look up the password then cache it if (this._password == null) for each (let login in this._logins) @@ -90,9 +90,7 @@ Identity.prototype = { return this._password; }, - set password password(value) { - this._password = value; - }, + set password(value) this._password = value, persist: function persist() { // Clean up any existing passwords unless it's what we're persisting @@ -120,5 +118,5 @@ Identity.prototype = { Svc.Login.addLogin(newLogin); }, - get _logins _logins() Svc.Login.findLogins({}, PWDMGR_HOST, null, this.realm) + get _logins() Svc.Login.findLogins({}, PWDMGR_HOST, null, this.realm) }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 58a72e6f2a02..b39135a44cc0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -126,11 +126,11 @@ WeaveSvc.prototype = { this._updateCachedURLs(); }, - get password password() ID.get("WeaveID").password, - set password password(value) ID.get("WeaveID").password = value, + get password() ID.get("WeaveID").password, + set password(value) ID.get("WeaveID").password = value, - get passphrase passphrase() ID.get("WeaveCryptoID").password, - set passphrase passphrase(value) ID.get("WeaveCryptoID").password = value, + get passphrase() ID.get("WeaveCryptoID").password, + set passphrase(value) ID.get("WeaveCryptoID").password = value, get serverURL() Svc.Prefs.get("serverURL"), set serverURL(value) { From f706ee8ac58e6babeb2cc64b13b252b1db125061 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 28 Apr 2010 16:36:41 -0700 Subject: [PATCH 1641/1860] Bug 561005 - Use FakeSvc to grab binary crypto if js-ctypes doesn't work [r=zpao r=mconnor] Lazily load the binary component service onto FakeSvc if ;2 fails to register on Svc. --- services/sync/modules/util.js | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 63102f618820..36aef43d26a3 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -857,34 +857,21 @@ let FakeSvc = { } }; +// Use the binary WeaveCrypto (;1) if the js-ctypes version (;2) fails to load +// by adding an alias on FakeSvc from ;2 to ;1 +Utils.lazySvc(FakeSvc, "@labs.mozilla.com/Weave/Crypto;2", + "@labs.mozilla.com/Weave/Crypto;1", "IWeaveCrypto"); + /* * Commonly-used services */ -let cryptoContractID = "@labs.mozilla.com/Weave/Crypto"; - -{ - let versSvc = Cc["@mozilla.org/xpcom/version-comparator;1"]. - getService(Ci.nsIVersionComparator); - let appinfo = Cc["@mozilla.org/xre/app-info;1"]. - getService(Ci.nsIXULAppInfo); - let platVers = appinfo.platformVersion; - - if (versSvc.compare(platVers, "1.9.3a3") < 0) { - // use old binary component - cryptoContractID += ";1"; - } else { - // use new JS-CTypes component - cryptoContractID += ";2"; - } -} - let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); Svc.Obs = Observers; [["Annos", "@mozilla.org/browser/annotation-service;1", "nsIAnnotationService"], ["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], ["Bookmark", "@mozilla.org/browser/nav-bookmarks-service;1", "nsINavBookmarksService"], - ["Crypto", cryptoContractID, "IWeaveCrypto"], + ["Crypto", "@labs.mozilla.com/Weave/Crypto;2", "IWeaveCrypto"], ["Directory", "@mozilla.org/file/directory_service;1", "nsIProperties"], ["Env", "@mozilla.org/process/environment;1", "nsIEnvironment"], ["Favicon", "@mozilla.org/browser/favicon-service;1", "nsIFaviconService"], From 8faf2bb8c90d171e858c012d984cc889cfa50b1d Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 28 Apr 2010 18:06:44 -0400 Subject: [PATCH 1642/1860] Bug 562183 - unify passphrase matching/validation code, r=Mardak --- services/sync/locales/en-US/errors.properties | 12 ++++++++++++ .../sync/locales/en-US/generic-change.properties | 6 ------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties index 7995fe806002..03a4d06a1428 100644 --- a/services/sync/locales/en-US/errors.properties +++ b/services/sync/locales/en-US/errors.properties @@ -12,3 +12,15 @@ weak-password = Use a stronger password # this is the fallback, if we hit an error we didn't bother to localize error.reason.unknown = Unknown error + +change.passphrase.ppSameAsPassphrase = The secret phrase cannot be the same as your current secret phrase +change.passphrase.ppSameAsPassword = The secret phrase cannot be the same as your password +change.passphrase.ppSameAsUsername = The secret phrase cannot be the same as the username +change.passphrase.mismatch = The phrases entered do not match +change.passphrase.tooShort = The secret phrase entered is too short + +change.password.pwSameAsPassphrase = Your password cannot match your secret phrase +change.password.pwSameAsPassword = Your password cannot match your current password +change.password.pwSameAsUsername = Your password cannot match your username +change.password.mismatch = The passwords entered do not match +change.password.tooShort = The password entered is too short diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties index ede890364843..362201997497 100644 --- a/services/sync/locales/en-US/generic-change.properties +++ b/services/sync/locales/en-US/generic-change.properties @@ -3,9 +3,6 @@ change.password.acceptButton = Change Password change.password.status.active = Changing your password… change.password.status.success = Your password has been changed. change.password.status.error = There was an error changing your password. -change.password.status.pwSameAsPassphrase = The password cannot be the same as your secret phrase. -change.password.status.pwSameAsPassword = The password cannot be the same as your current password. -change.password.status.pwSameAsUsername = The password cannot be the same as the username. change.password.introText = Your password must be at least 8 characters long. It cannot be the same as either your user name or your secret phrase. change.password.warningText = Note: All of your other devices will be unable to connect to your account once you change this password. @@ -15,9 +12,6 @@ change.passphrase.acceptButton = Change Secret Phrase change.passphrase.label = Changing secret phrase and uploading local data, please wait… change.passphrase.error = There was an error while changing your secret phrase! change.passphrase.success = Your secret phrase was successfully changed! -change.passphrase.status.ppSameAsPassphrase = The secret phrase cannot be the same as your current secret phrase. -change.passphrase.status.ppSameAsPassword = The secret phrase cannot be the same as your password. -change.passphrase.status.ppSameAsUsername = The secret phrase cannot be the same as the username. new.passphrase.label = New secret phrase new.passphrase.confirm = Confirm secret phrase From c26c254a333b41c582f5b73ebef3c79eeeb9134d Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 28 Apr 2010 22:14:12 -0400 Subject: [PATCH 1643/1860] Bug 551612 - Changes to Weave Sync Client to comply with European Privacy Policy, r=Mardak --- services/sync/locales/en-US/fx-prefs.dtd | 4 +++- services/sync/services-sync.js | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index d83e2598e366..887143645667 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -24,7 +24,9 @@ - + + + diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 4e8f9ec0bba2..40ba32007cb3 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -2,7 +2,8 @@ pref("extensions.weave.serverURL", "@server_url@"); pref("extensions.weave.storageAPI", "1.0"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); -pref("extensions.weave.termsURL", "https://mozillalabs.com/weave/tos/"); +pref("extensions.weave.termsURL", "http://mozillalabs.com/weave/tos/"); +pref("extensions.weave.privacyURL", "http://mozillalabs.com/weave/weave-privacy-policy/"); pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.autoconnect", true); From 1a4a1ed64e765f0f379b853bc5cb453c3a979294 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Wed, 28 Apr 2010 22:20:08 -0400 Subject: [PATCH 1644/1860] Bug 562159 - Tabs from other computers don't show at about:weave-tabs, r=Mardak --- services/sync/modules/engines/clients.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index 8864920c4988..f2495b3a239c 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -163,7 +163,9 @@ ClientEngine.prototype = { set localType(value) Svc.Prefs.set("client.type", value), isMobile: function isMobile(id) { - return this._store._remoteClients[id].type == "mobile"; + if (this._store._remoteClients[id]) + return this._store._remoteClients[id].type == "mobile"; + return false; }, // Always process incoming items because they might have commands From f55225d0a4358578df62a2e5a4287e493a4c3568 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 29 Apr 2010 11:50:46 -0400 Subject: [PATCH 1645/1860] Bug 560887 - Heartbeat doesn't detect new clients if the browser is closed before 1 hour, r=Mardak --- services/sync/modules/service.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index b39135a44cc0..492664e658ff 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -183,9 +183,11 @@ WeaveSvc.prototype = { get keyGenEnabled() { return this._keyGenEnabled; }, set keyGenEnabled(value) { this._keyGenEnabled = value; }, - // nextSync is in milliseconds, but prefs can't hold that much + // nextSync and nextHeartbeat are in milliseconds, but prefs can't hold that much get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), + get nextHeartbeat() Svc.Prefs.get("nextHeartbeat", 0) * 1000, + set nextHeartbeat(value) Svc.Prefs.set("nextHeartbeat", Math.floor(value / 1000)), get syncInterval() { // If we have a partial download, sync sooner if we're not mobile @@ -1057,6 +1059,7 @@ WeaveSvc.prototype = { if (this._heartbeatTimer) this._heartbeatTimer.clear(); + this.nextHeartbeat = 0; let info = null; try { info = new Resource(this.infoURL).get(); @@ -1090,11 +1093,26 @@ WeaveSvc.prototype = { * this until the next sync. */ _scheduleHeartbeat: function WeaveSvc__scheduleNextHeartbeat() { + if (this._heartbeatTimer) + return; + + let now = Date.now(); + if (this.nextHeartbeat && this.nextHeartbeat < now) { + this._doHeartbeat(); + return; + } + + // if the next sync is in less than an hour, don't bother let interval = MULTI_DESKTOP_SYNC; if (this.nextSync < Date.now() + interval || Status.enforceBackoff) return; + if (this.nextHeartbeat) + interval = this.nextHeartbeat - now; + else + this.nextHeartbeat = now + interval; + this._log.trace("Setting up heartbeat, next ping in " + Math.ceil(interval / 1000) + " sec."); Utils.delay(function() this._doHeartbeat(), interval, this, "_heartbeatTimer"); @@ -1151,6 +1169,7 @@ WeaveSvc.prototype = { // Clear out any potentially pending syncs now that we're syncing this._clearSyncTriggers(); this.nextSync = 0; + this.nextHeartbeat = 0; // reset backoff info, if the server tells us to continue backing off, // we'll handle that later From 05367fc28d3909ff6c68ad1a264e25f5913acc85 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 29 Apr 2010 14:36:09 -0700 Subject: [PATCH 1646/1860] Bug 558077 - Bookmark folder and its contents lost under certain conditions of syncing same named folder [r=mconnor] Mark entries as dupe if they're known to be dupes locally so that receiving ends won't bother looking for dupes for these items. --- services/sync/modules/engines/bookmarks.js | 19 ++++++++++++++++++- .../sync/modules/type_records/bookmark.js | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 093eb5e978a5..586f97b545c1 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -168,8 +168,13 @@ BookmarksEngine.prototype = { if (lazyMap[parentName] == null) lazyMap[parentName] = {}; + // If the entry already exists, remember that there are explicit dupes + let entry = new String(guid); + entry.hasDupe = lazyMap[parentName][key] != null; + // Remember this item's guid for its parent-name/key pair - lazyMap[parentName][key] = guid; + lazyMap[parentName][key] = entry; + this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]); } // Expose a helper function to get a dupe guid for an item @@ -209,7 +214,19 @@ BookmarksEngine.prototype = { this._tracker._ensureMobileQuery(); }, + _createRecord: function _createRecord(id) { + // Create the record like normal but mark it as having dupes if necessary + let record = SyncEngine.prototype._createRecord.call(this, id); + let entry = this._lazyMap(record); + if (entry != null && entry.hasDupe) + record.hasDupe = true; + return record; + }, + _findDupe: function _findDupe(item) { + // Don't bother finding a dupe if the incoming item has duplicates + if (item.hasDupe) + return; return this._lazyMap(item); }, diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index 701c174d284b..e410a0de0c00 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -85,7 +85,7 @@ PlacesItem.prototype = { _logName: "Record.PlacesItem", }; -Utils.deferGetSet(PlacesItem, "cleartext", ["parentid", "parentName", +Utils.deferGetSet(PlacesItem, "cleartext", ["hasDupe", "parentid", "parentName", "predecessorid", "type"]); function Bookmark(uri, type) { From bd9fbf0efd4db653a1d047627133f8d6bf200da9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 29 Apr 2010 14:36:15 -0700 Subject: [PATCH 1647/1860] Bug 553709 - Syncing "Browsing History" uses 100% of a CPU core for extended periods [r=mconnor] Sync asyncExecute to avoid forcing synchronous waits on disk but keep existing calling conventions (no callbacks) for callers by using Sync. --- services/sync/modules/engines/history.js | 70 ++++++++++++++---------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 04378d91ff51..a3209a416c23 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -43,6 +43,7 @@ const Cu = Components.utils; const GUID_ANNO = "weave/guid"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); @@ -136,39 +137,52 @@ HistoryStore.prototype = { // See bug 320831 for why we use SQL here _getVisits: function HistStore__getVisits(uri) { - let visits = []; - if (typeof(uri) != "string") - uri = uri.spec; - - try { - this._visitStm.params.url = uri; - while (this._visitStm.step()) { - visits.push({date: this._visitStm.row.date, - type: this._visitStm.row.type}); + this._visitStm.params.url = uri; + let [exec, execCb] = Sync.withCb(this._visitStm.executeAsync, this._visitStm); + return exec({ + visits: [], + handleResult: function handleResult(results) { + let row; + while ((row = results.getNextRow()) != null) { + this.visits.push({ + date: row.getResultByName("date"), + type: row.getResultByName("type") + }); + } + }, + handleError: function handleError(error) { + execCb.throw(error); + }, + handleCompletion: function handleCompletion(reason) { + execCb(this.visits); } - } finally { - this._visitStm.reset(); - } - - return visits; + }); }, // See bug 468732 for why we use SQL here _findURLByGUID: function HistStore__findURLByGUID(guid) { - try { - this._urlStm.params.guid = guid; - if (!this._urlStm.step()) - return null; - - return { - url: this._urlStm.row.url, - title: this._urlStm.row.title, - frecency: this._urlStm.row.frecency - }; - } - finally { - this._urlStm.reset(); - } + this._urlStm.params.guid = guid; + let [exec, execCb] = Sync.withCb(this._urlStm.executeAsync, this._urlStm); + return exec({ + handleResult: function(results) { + // Save the one result and its columns + let row = results.getNextRow(); + this.urlInfo = { + url: row.getResultByName("url"), + title: row.getResultByName("title"), + frecency: row.getResultByName("frecency"), + }; + }, + handleError: function(error) { + execCb.throw(error); + }, + handleCompletion: function(reason) { + // Throw in-case for some reason we didn't find the GUID + if (this.urlInfo == null) + execCb.throw(reason); + execCb(this.urlInfo); + } + }); }, changeItemID: function HStore_changeItemID(oldID, newID) { From 7e366b555bbd5ba6ba8c70f534de96b73e5ed5d5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 29 Apr 2010 15:18:05 -0700 Subject: [PATCH 1648/1860] Bustage fix from sync-asyncExecute: don't throw as the old code would catch and implicit return undefined. --- services/sync/modules/engines/history.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index a3209a416c23..6c13a30dd009 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -177,9 +177,6 @@ HistoryStore.prototype = { execCb.throw(error); }, handleCompletion: function(reason) { - // Throw in-case for some reason we didn't find the GUID - if (this.urlInfo == null) - execCb.throw(reason); execCb(this.urlInfo); } }); From fc0c33cd003e7679100504b761591338275c9fb1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 29 Apr 2010 16:42:21 -0700 Subject: [PATCH 1649/1860] Bug 556454 - engine.sync should always check engine.enabled [r=mconnor] Just check if the engine is enabled at the beginning of sync. For now keep getEnabled as it's used for other behavior and not just sync. --- services/sync/modules/engines.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 67ebd013384a..cacf360a0e7d 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -177,6 +177,9 @@ Engine.prototype = { }, sync: function Engine_sync() { + if (!this.enabled) + return; + if (!this._sync) throw "engine does not implement _sync method"; From 982e131a954a2c3d70b3f4665b0bc37229bd87e7 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 29 Apr 2010 16:42:39 -0700 Subject: [PATCH 1650/1860] Bug 554836 - On idle occurs 'JavaScript component does not have a method named: "onDeleteVisits"' [r=mconnor] Add onDeleteVisits in addition to onPageExpired to maintain compatibility with old and new API. --- services/sync/modules/engines/history.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 6c13a30dd009..0b951550a741 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -311,6 +311,8 @@ HistoryTracker.prototype = { if (this.addChangedID(GUIDForUri(uri, true))) this._upScore(); }, + onDeleteVisits: function onDeleteVisits() { + }, onPageExpired: function HT_onPageExpired(uri, time, entry) { }, onBeforeDeleteURI: function onBeforeDeleteURI(uri) { From cfee72dc49531937961120e2a878d38839ddf161 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 29 Apr 2010 14:36:15 -0700 Subject: [PATCH 1651/1860] Bug 559163 - Don't select from slow indexless views for just one item [r=mconnor] Just select from moz_places instead of _view where data might be slightly stale but good enough. --- services/sync/modules/engines/bookmarks.js | 2 +- services/sync/modules/engines/history.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 586f97b545c1..9e27067fd076 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -834,7 +834,7 @@ BookmarksStore.prototype = { this._log.trace("Creating SQL statement: _frecencyStm"); let stm = Svc.History.DBConnection.createStatement( "SELECT frecency " + - "FROM moz_places_view " + + "FROM moz_places " + "WHERE url = :url"); this.__defineGetter__("_frecencyStm", function() stm); return stm; diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 0b951550a741..39e997191aec 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -109,10 +109,10 @@ HistoryStore.prototype = { this._log.trace("Creating SQL statement: _visitStm"); let stm = this._db.createStatement( "SELECT visit_type type, visit_date date " + - "FROM moz_historyvisits_view " + + "FROM moz_historyvisits " + "WHERE place_id = (" + "SELECT id " + - "FROM moz_places_view " + + "FROM moz_places " + "WHERE url = :url) " + "ORDER BY date DESC LIMIT 10"); this.__defineGetter__("_visitStm", function() stm); @@ -123,7 +123,7 @@ HistoryStore.prototype = { this._log.trace("Creating SQL statement: _urlStm"); let stm = this._db.createStatement( "SELECT url, title, frecency " + - "FROM moz_places_view " + + "FROM moz_places " + "WHERE id = (" + "SELECT place_id " + "FROM moz_annos " + From a77bb0bb1fd77ff925172831be3670a55332dca6 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 3 May 2010 16:44:18 -0400 Subject: [PATCH 1652/1860] Bug 561382 - Theme doesn't dynamically switch on sync to default theme, r=Mardak --- services/sync/modules/engines/prefs.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 52231d0b3c76..aaedc56bb565 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -133,6 +133,20 @@ PrefStore.prototype = { }, _setAllPrefs: function PrefStore__setAllPrefs(values) { + // cache + let ltmExists = true; + let ltm = {}; + let enabledBefore = false; + let prevTheme = ""; + try { + Cu.import("resource://gre/modules/LightweightThemeManager.jsm", ltm); + ltm = ltm.LightweightThemeManager; + enabledBefore = this._prefs.getBoolPref("lightweightThemes.isThemeSelected"); + prevTheme = ltm.currentTheme; + } catch(ex) { + ltmExists = false; + } // LightweightThemeManager only exists in Firefox 3.6+ + for (let i = 0; i < values.length; i++) { switch (values[i]["type"]) { case "int": @@ -150,17 +164,15 @@ PrefStore.prototype = { } // Notify the lightweight theme manager of all the new values - try { - let ltm = {}; - Cu.import("resource://gre/modules/LightweightThemeManager.jsm", ltm); - ltm = ltm.LightweightThemeManager; - if (ltm.currentTheme) { + if (ltmExists) { + let enabledNow = this._prefs.getBoolPref("lightweightThemes.isThemeSelected"); + if (enabledBefore && !enabledNow) + ltm.currentTheme = null; + else if (enabledNow && ltm.usedThemes[0] != prevTheme) { ltm.currentTheme = null; ltm.currentTheme = ltm.usedThemes[0]; } } - // LightweightThemeManager only exists in Firefox 3.6+ - catch (ex) {} }, getAllIDs: function PrefStore_getAllIDs() { From 19ac3f6c7b0a2a5fa287b620165171da253d5c5d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 3 May 2010 14:01:08 -0700 Subject: [PATCH 1653/1860] Bug 562100 - need enhanced activity logging to measure sync performance from client [r=mconnor] Track a start time and print the total sync time to hundredths of a second. --- services/sync/modules/service.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 492664e658ff..0fdcf8f398fc 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1149,6 +1149,8 @@ WeaveSvc.prototype = { sync: function sync() this._catch(this._lock(this._notify("sync", "", function() { + let syncStartTime = Date.now(); + Status.resetSync(); // Make sure we should sync or record why we shouldn't @@ -1254,7 +1256,8 @@ WeaveSvc.prototype = { else { Svc.Prefs.set("lastSync", new Date().toString()); Status.sync = SYNC_SUCCEEDED; - this._log.info("Sync completed successfully"); + let syncTime = ((Date.now() - syncStartTime) / 1000).toFixed(2); + this._log.info("Sync completed successfully in " + syncTime + " secs."); } } finally { this._syncError = false; From 9d462bc16e9dd7f0e05f4f074fb54feaec08dec9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 3 May 2010 14:39:32 -0700 Subject: [PATCH 1654/1860] Bug 562515 - Reset sync option "Replace all data on this computer with your Weave data" is not handling local deletes correctly [r=mconnor] Clear out any changed ids when wiping the client to prevent delete records from getting uploaded for these items. --- services/sync/modules/engines.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index cacf360a0e7d..69082dd805e5 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -281,6 +281,7 @@ Engine.prototype = { this._tracker.ignoreAll = true; this._store.wipe(); this._tracker.ignoreAll = false; + this._tracker.clearChangedIDs(); }, wipeClient: function Engine_wipeClient() { From c1e0d846a6ed7cdc0d93c30bd2b511d665182f3d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 4 May 2010 12:14:48 -0700 Subject: [PATCH 1655/1860] Bug 561839 - import PlacesUtils.jsm instead of utils.js [r=mconnor] Try the new PlacesUtils file before the old utils.js. --- services/sync/modules/engines/bookmarks.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index 9e27067fd076..a69a5b650096 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -46,7 +46,12 @@ const PARENT_ANNO = "weave/parent"; const PREDECESSOR_ANNO = "weave/predecessor"; const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; -Cu.import("resource://gre/modules/utils.js"); +try { + Cu.import("resource://gre/modules/PlacesUtils.jsm"); +} +catch(ex) { + Cu.import("resource://gre/modules/utils.js"); +} Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/util.js"); From ea126d5effef0d15d5e6c67c302cfd7ef0237a85 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 4 May 2010 12:15:43 -0700 Subject: [PATCH 1656/1860] Bug 561480 - Errors when loading WeaveCrypto.js on older platforms [r=mconnor] Only register WeaveCrypto if it was able to import scripts. --- services/crypto/WeaveCrypto.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/crypto/WeaveCrypto.js b/services/crypto/WeaveCrypto.js index 831db5ca2e2d..52aa4ed24ffe 100644 --- a/services/crypto/WeaveCrypto.js +++ b/services/crypto/WeaveCrypto.js @@ -40,8 +40,11 @@ const Ci = Components.interfaces; const Cr = Components.results; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/ctypes.jsm"); +try { + Components.utils.import("resource://gre/modules/Services.jsm"); + Components.utils.import("resource://gre/modules/ctypes.jsm"); +} +catch(ex) {} function WeaveCrypto() { this.init(); @@ -1120,7 +1123,7 @@ WeaveCrypto.prototype = { } }; -let component = [WeaveCrypto]; +let component = Services == null || ctypes == null ? [] : [WeaveCrypto]; function NSGetModule (compMgr, fileSpec) { return XPCOMUtils.generateModule(component); } From 2968c2e47f77ed0cad957605bd74b27ecc18d7e3 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 4 May 2010 16:55:34 -0400 Subject: [PATCH 1657/1860] Bug 561391 - Add hidden pref to let createAccount include X-Weave-Secret header to bypass captcha, r=Mardak --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0fdcf8f398fc..35f7dee2eec0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -777,6 +777,8 @@ WeaveSvc.prototype = { let url = this.userAPI + username; let res = new Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); + if (Svc.Prefs.isSet("admin-secret")) + res.setHeader("X-Weave-Secret", Svc.Prefs.get("admin-secret", "")); let error = "generic-server-error"; try { From 45b294a596e44bfc6a1d951faf339d27747f8e28 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 4 May 2010 16:56:12 -0400 Subject: [PATCH 1658/1860] Bug 563682 - Latest Secret phrase does not update in password manager, r=Mardak --- services/sync/modules/service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 35f7dee2eec0..07b414693f9a 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -619,6 +619,7 @@ WeaveSvc.prototype = { /* Set this so UI is updated on next run */ this.passphrase = newphrase; + this.persistLogin(); /* Login in sync: this also generates new keys */ this.login(); From 19cbaa6301f3eb62f6383690921074767ea64abb Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Tue, 4 May 2010 18:46:10 -0400 Subject: [PATCH 1659/1860] Bug 563794 - Certain preferences are not syncing across browsers, r=Mardak --- services/sync/services-sync.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 40ba32007cb3..83752d951957 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -43,6 +43,7 @@ pref("extensions.weave.prefs.sync.browser.download.manager.scanWhenDone", true); pref("extensions.weave.prefs.sync.browser.download.manager.showWhenStarting", true); pref("extensions.weave.prefs.sync.browser.formfill.enable", true); pref("extensions.weave.prefs.sync.browser.history_expire_days", true); +pref("extensions.weave.prefs.sync.browser.history_expire_days_min", true); pref("extensions.weave.prefs.sync.browser.link.open_newwindow", true); pref("extensions.weave.prefs.sync.browser.offline-apps.notify", true); pref("extensions.weave.prefs.sync.browser.safebrowsing.enabled", true); @@ -72,6 +73,7 @@ pref("extensions.weave.prefs.sync.extensions.personas.current", true); pref("extensions.weave.prefs.sync.extensions.update.enabled", true); pref("extensions.weave.prefs.sync.general.autoScroll", true); pref("extensions.weave.prefs.sync.general.smoothScroll", true); +pref("extensions.weave.prefs.sync.intl.accept_languages", true); pref("extensions.weave.prefs.sync.javascript.enabled", true); pref("extensions.weave.prefs.sync.layout.spellcheckDefault", true); pref("extensions.weave.prefs.sync.lightweightThemes.isThemeSelected", true); @@ -107,3 +109,5 @@ pref("extensions.weave.prefs.sync.security.warn_submit_insecure", true); pref("extensions.weave.prefs.sync.security.warn_viewing_mixed", true); pref("extensions.weave.prefs.sync.signon.rememberSignons", true); pref("extensions.weave.prefs.sync.spellchecker.dictionary", true); +pref("extensions.weave.prefs.sync.xpinstall.whitelist.required", true); + From 5e764401ea0356f91982ee752e6156852b48f5fc Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 5 May 2010 17:16:17 -0700 Subject: [PATCH 1660/1860] Bug 563989 - Reset Sync option 3, doesn't properly propagate to other clients [r=mconnor] Always sync client data by making sure it's always enabled and only update local cilent with remote commands. Make sure to sync clients to get a list of clients that need to receive commands. Also, make sure to upload data after wiping remote now that remoteSetup happens before wipeRemote. --- services/sync/modules/engines/clients.js | 10 +++---- services/sync/modules/service.js | 35 ++++++++++++------------ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index f2495b3a239c..ae77d4ef1560 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -59,6 +59,9 @@ ClientEngine.prototype = { _storeObj: ClientStore, _recordObj: ClientsRec, + // Always sync client data as it controls other sync behavior + get enabled() true, + // Aggregate some stats on the composition of clients on this account get stats() { let stats = { @@ -191,12 +194,9 @@ ClientStore.prototype = { create: function create(record) this.update(record), update: function update(record) { - // Unpack the individual components of the local client - if (record.id == Clients.localID) { - Clients.localName = record.name; - Clients.localType = record.type; + // Only grab commands from the server; local name/type always wins + if (record.id == Clients.localID) Clients.localCommands = record.commands; - } else this._remoteClients[record.id] = record.cleartext; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 07b414693f9a..84b510fbe51f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1182,22 +1182,6 @@ WeaveSvc.prototype = { this.globalScore = 0; - if (!(this._remoteSetup())) - throw "aborting sync, remote setup failed"; - - // Wipe data in the desired direction if necessary - switch (Svc.Prefs.get("firstSync")) { - case "resetClient": - this.resetClient(Engines.getEnabled().map(function(e) e.name)); - break; - case "wipeClient": - this.wipeClient(Engines.getEnabled().map(function(e) e.name)); - break; - case "wipeRemote": - this.wipeRemote(Engines.getEnabled().map(function(e) e.name)); - break; - } - // Ping the server with a special info request once a day let infoURL = this.infoURL; let now = Math.floor(Date.now() / 1000); @@ -1216,9 +1200,26 @@ WeaveSvc.prototype = { for each (let engine in [Clients].concat(Engines.getAll())) engine.lastModified = info.obj[engine.name] || 0; + if (!(this._remoteSetup())) + throw "aborting sync, remote setup failed"; + + // Make sure we have an up-to-date list of clients before sending commands this._log.trace("Refreshing client list"); Clients.sync(); + // Wipe data in the desired direction if necessary + switch (Svc.Prefs.get("firstSync")) { + case "resetClient": + this.resetClient(Engines.getEnabled().map(function(e) e.name)); + break; + case "wipeClient": + this.wipeClient(Engines.getEnabled().map(function(e) e.name)); + break; + case "wipeRemote": + this.wipeRemote(Engines.getEnabled().map(function(e) e.name)); + break; + } + // Process the incoming commands if we have any if (Clients.localCommands) { try { @@ -1424,7 +1425,7 @@ WeaveSvc.prototype = { wipeRemote: function WeaveSvc_wipeRemote(engines) this._catch(this._notify("wipe-remote", "", function() { // Make sure stuff gets uploaded - this.syncID = ""; + this.resetClient(engines); // Clear out any server data this.wipeServer(engines); From 74b28dce92ea49a1b25b7d27acb9d4c59bbd3727 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 7 May 2010 00:02:00 -0400 Subject: [PATCH 1661/1860] Bug 563868 - Provide better feedback when server is unreachable or doesn't have a server instance during setup. also fixes bug 564329. r=Mardak --- services/sync/modules/service.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 84b510fbe51f..0357ed929a84 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -757,8 +757,13 @@ WeaveSvc.prototype = { let data = ""; try { data = res.get(); - if (data.status == 200 && data == "0") - return "available"; + if (data.status == 200) { + if (data == "0") + return "available"; + else if (data == "1") + return "notAvailable"; + } + } catch(ex) {} From dc4227611261bd7e497bea4a6d26e9d309c76dc5 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 7 May 2010 00:02:40 -0400 Subject: [PATCH 1662/1860] Bug 564095 - wizard cleanup, r=Mardak --- services/sync/locales/en-US/fx-prefs.dtd | 1 + 1 file changed, 1 insertion(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 887143645667..9d839cb3d122 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -32,6 +32,7 @@ + From d3649e6a5d542bcbac3ef85dcd99bcf263857755 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 7 May 2010 11:25:59 -0400 Subject: [PATCH 1663/1860] Bug 561192 - Rename add-on to Firefox Sync, update visuals, r=Mardak --- .../sync/locales/en-US/fennec-firstrun.dtd | 11 +++--- services/sync/locales/en-US/fx-prefs.dtd | 35 +++++++++---------- services/sync/locales/en-US/sync.dtd | 4 ++- services/sync/locales/en-US/sync.properties | 16 ++++----- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/services/sync/locales/en-US/fennec-firstrun.dtd b/services/sync/locales/en-US/fennec-firstrun.dtd index dafd599f5474..2b1aaed1b677 100644 --- a/services/sync/locales/en-US/fennec-firstrun.dtd +++ b/services/sync/locales/en-US/fennec-firstrun.dtd @@ -1,6 +1,9 @@ - + + + + - - + + - + diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 9d839cb3d122..54711520fd85 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -1,7 +1,7 @@ - - + + - + @@ -29,12 +29,12 @@ - + - - + + @@ -45,17 +45,14 @@ - + - + - - - @@ -74,26 +71,26 @@ - - + + - - + + - + - + - + - + diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd index 8d7ec8412271..bc2e08976fb7 100644 --- a/services/sync/locales/en-US/sync.dtd +++ b/services/sync/locales/en-US/sync.dtd @@ -1,4 +1,6 @@ - + + + diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index 008a01225eb2..c0b6895cc1a2 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -11,11 +11,11 @@ disconnected.label = Disconnected connecting.label = Connecting… # %S is the engine being synced -syncing.label = Weave is syncing: %S +syncing.label = Firefox Sync is syncing: %S status.offline = Not Signed In status.privateBrowsing = Disabled (Private Browsing) -status.needsSetup = Set Up Weave… +status.needsSetup = Set Up Firefox Sync… mobile.label = Mobile Bookmarks @@ -25,18 +25,18 @@ remote.opened.label = All remote tabs are already open remote.notification.label = Recent desktop tabs will be available once they sync error.login.title = Error While Signing In -error.login.description = Weave encountered an error while connecting: %1$S. Please try again. +error.login.description = Sync encountered an error while connecting: %1$S. Please try again. error.login.prefs.label = Preferences… error.login.prefs.accesskey = P # should decide if we're going to show this error.logout.title = Error While Signing Out -error.logout.description = Weave encountered an error while connecting. It's probably ok, and you don't have to do anything about it. +error.logout.description = Sync encountered an error while connecting. It's probably ok, and you don't have to do anything about it. error.sync.title = Error While Syncing -error.sync.description = Weave encountered an error while syncing: %1$S. Weave will automatically retry this action. -error.sync.no_node_found = The Weave server is a little busy right now, but you don't need to do anything about it. We'll start syncing your data as soon as we can! +error.sync.description = Sync encountered an error while syncing: %1$S. Sync will automatically retry this action. +error.sync.no_node_found = The Sync server is a little busy right now, but you don't need to do anything about it. We'll start syncing your data as soon as we can! error.sync.no_node_found.title = Sync Delay -error.sync.needUpdate.description = You need to update Weave to continue syncing your data. -error.sync.needUpdate.label = Update Weave +error.sync.needUpdate.description = You need to update Firefox Sync to continue syncing your data. +error.sync.needUpdate.label = Update Firefox Sync error.sync.needUpdate.accesskey = U error.sync.tryAgainButton.label = Sync Now error.sync.tryAgainButton.accesskey = S From 0dbda1a43b39e7d24e0e7275c18721c31fdf5da7 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 7 May 2010 16:16:37 -0400 Subject: [PATCH 1664/1860] Bug 564365 - canceling setup, then setting up Weave, doesn't set firstrun pref, causing problems later, r=mardak --- services/sync/modules/service.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0357ed929a84..2f7786195ffa 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -635,6 +635,8 @@ WeaveSvc.prototype = { this.resetClient(); // Reset Weave prefs Svc.Prefs.resetBranch(""); + // set lastversion pref + Svc.Prefs.set("lastversion", WEAVE_VERSION); // Find weave logins and remove them. Svc.Login.findLogins({}, PWDMGR_HOST, "", "").map(function(login) { Svc.Login.removeLogin(login); From ac20ff667f8d2f23365a68c623064220488aced4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 7 May 2010 13:29:04 -0700 Subject: [PATCH 1665/1860] Bustage fix for bug 561480 to check typeof == undefined instead of == null. --- services/crypto/WeaveCrypto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/crypto/WeaveCrypto.js b/services/crypto/WeaveCrypto.js index 52aa4ed24ffe..6e76b3986780 100644 --- a/services/crypto/WeaveCrypto.js +++ b/services/crypto/WeaveCrypto.js @@ -1123,7 +1123,7 @@ WeaveCrypto.prototype = { } }; -let component = Services == null || ctypes == null ? [] : [WeaveCrypto]; +let component = typeof Services == "undefined" || typeof ctypes == "undefined" ? [] : [WeaveCrypto]; function NSGetModule (compMgr, fileSpec) { return XPCOMUtils.generateModule(component); } From 4272a85fa7e60acff7819803ad6eaddfbc7384ff Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 7 May 2010 15:24:51 -0700 Subject: [PATCH 1666/1860] Bug 564494 - Only show the activity log menu item for dev-channel releases [r=mconnor] Hide the item by default and show only for the dev channel. Also only show the separator and time if there's a time. Register about:weave-log to access the activity log. --- services/sync/Weave.js | 64 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 8b18fea5053f..bd07fd2d9cef 100755 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -92,7 +92,69 @@ AboutWeaveTabs.prototype = { return ch; } }; + +function AboutWeaveLog() {} +AboutWeaveLog.prototype = { + classDescription: "about:weave-log", + contractID: "@mozilla.org/network/protocol/about;1?what=weave-log", + classID: Components.ID("{d28f8a0b-95da-48f4-b712-caf37097be41}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, + Ci.nsISupportsWeakReference]), + + getURIFlags: function(aURI) { + return 0; + }, + + newChannel: function(aURI) { + let dir = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + let file = dir.get("ProfD", Ci.nsILocalFile); + file.append("weave"); + file.append("logs"); + file.append("verbose-log.txt"); + let ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let ch = ios.newChannel(ios.newFileURI(file).spec, null, null); + ch.originalURI = aURI; + return ch; + } +}; + +function AboutWeaveLog1() {} +AboutWeaveLog1.prototype = { + classDescription: "about:weave-log.1", + contractID: "@mozilla.org/network/protocol/about;1?what=weave-log.1", + classID: Components.ID("{a08ee179-df50-48e0-9c87-79e4dd5caeb1}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, + Ci.nsISupportsWeakReference]), + + getURIFlags: function(aURI) { + return 0; + }, + + newChannel: function(aURI) { + let dir = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + let file = dir.get("ProfD", Ci.nsILocalFile); + file.append("weave"); + file.append("logs"); + file.append("verbose-log.txt.1"); + let ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let ch = ios.newChannel(ios.newFileURI(file).spec, null, null); + ch.originalURI = aURI; + return ch; + } +}; + function NSGetModule(compMgr, fileSpec) { - return XPCOMUtils.generateModule([WeaveService, AboutWeaveTabs]); + return XPCOMUtils.generateModule([ + WeaveService, + AboutWeaveTabs, + AboutWeaveLog, + AboutWeaveLog1 + ]); } From 654b6fba12a8e11de9e8a507b29e2ed471eb9b2a Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 7 May 2010 18:39:58 -0400 Subject: [PATCH 1667/1860] Bug 564523 - passphrase can match password for new accounts, r=Mardak --- services/sync/locales/en-US/errors.properties | 8 ++++---- services/sync/modules/service.js | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/services/sync/locales/en-US/errors.properties b/services/sync/locales/en-US/errors.properties index 03a4d06a1428..4cf107c41007 100644 --- a/services/sync/locales/en-US/errors.properties +++ b/services/sync/locales/en-US/errors.properties @@ -15,12 +15,12 @@ error.reason.unknown = Unknown error change.passphrase.ppSameAsPassphrase = The secret phrase cannot be the same as your current secret phrase change.passphrase.ppSameAsPassword = The secret phrase cannot be the same as your password -change.passphrase.ppSameAsUsername = The secret phrase cannot be the same as the username +change.passphrase.ppSameAsUsername = The secret phrase cannot be the same as your user name change.passphrase.mismatch = The phrases entered do not match change.passphrase.tooShort = The secret phrase entered is too short -change.password.pwSameAsPassphrase = Your password cannot match your secret phrase -change.password.pwSameAsPassword = Your password cannot match your current password -change.password.pwSameAsUsername = Your password cannot match your username +change.password.pwSameAsPassphrase = Password can't match secret phrase +change.password.pwSameAsPassword = Password can't match current password +change.password.pwSameAsUsername = Password can't match your user name change.password.mismatch = The passwords entered do not match change.password.tooShort = The password entered is too short diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2f7786195ffa..bcf5e65522cb 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -638,6 +638,8 @@ WeaveSvc.prototype = { // set lastversion pref Svc.Prefs.set("lastversion", WEAVE_VERSION); // Find weave logins and remove them. + this.password = ""; + this.passphrase = ""; Svc.Login.findLogins({}, PWDMGR_HOST, "", "").map(function(login) { Svc.Login.removeLogin(login); }); From 5601a911a0d952ccebebcb32769ea98a38bdef18 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 7 May 2010 18:40:41 -0400 Subject: [PATCH 1668/1860] Bug 563441 - Need better text for end of setup, r=mardak --- services/sync/locales/en-US/fx-prefs.dtd | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 54711520fd85..388215bc2ea1 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -31,21 +31,23 @@ - + - + - + @@ -115,7 +117,7 @@ - + From 07c6c136fd3b622c524dade007970d56fd03df24 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 7 May 2010 15:42:50 -0700 Subject: [PATCH 1669/1860] Bug 564533 - Register about: pages as sync instead of weave [r=mconnor] Switch the component and uses in the UI to about:sync-*. --- services/sync/Weave.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index bd07fd2d9cef..4057d7f5b0d2 100755 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -73,8 +73,8 @@ WeaveService.prototype = { function AboutWeaveTabs() {} AboutWeaveTabs.prototype = { - classDescription: "about:weave-tabs", - contractID: "@mozilla.org/network/protocol/about;1?what=weave-tabs", + classDescription: "about:sync-tabs", + contractID: "@mozilla.org/network/protocol/about;1?what=sync-tabs", classID: Components.ID("{ecb6987d-9d71-475d-a44d-a5ff2099b08c}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, @@ -95,8 +95,8 @@ AboutWeaveTabs.prototype = { function AboutWeaveLog() {} AboutWeaveLog.prototype = { - classDescription: "about:weave-log", - contractID: "@mozilla.org/network/protocol/about;1?what=weave-log", + classDescription: "about:sync-log", + contractID: "@mozilla.org/network/protocol/about;1?what=sync-log", classID: Components.ID("{d28f8a0b-95da-48f4-b712-caf37097be41}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, @@ -123,8 +123,8 @@ AboutWeaveLog.prototype = { function AboutWeaveLog1() {} AboutWeaveLog1.prototype = { - classDescription: "about:weave-log.1", - contractID: "@mozilla.org/network/protocol/about;1?what=weave-log.1", + classDescription: "about:sync-log.1", + contractID: "@mozilla.org/network/protocol/about;1?what=sync-log.1", classID: Components.ID("{a08ee179-df50-48e0-9c87-79e4dd5caeb1}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, From 8e8c0b11e4d5975d70efdc4842e48ed4b2179125 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Sat, 8 May 2010 12:33:40 -0400 Subject: [PATCH 1670/1860] Bug 564564 - remove crufty descriptions from Weave setup wizard, r=Mardak --- services/sync/locales/en-US/fx-prefs.dtd | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 388215bc2ea1..4d7f7174d0e5 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -32,7 +32,6 @@ - From 1a8801e27eb56c4417abbe73878426095dd1b7cf Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 10 May 2010 13:27:02 -0400 Subject: [PATCH 1671/1860] Bug 564637 - Don't reuse Terms of Service and Privacy Policy strings, r=Mardak --- services/sync/locales/en-US/fx-prefs.dtd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd index 4d7f7174d0e5..b1ff0f51cbf0 100644 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ b/services/sync/locales/en-US/fx-prefs.dtd @@ -29,6 +29,9 @@ + + + From d2eba44cb53417843439f3ddd508e3ce40eaef38 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 10 May 2010 16:22:32 -0700 Subject: [PATCH 1672/1860] Bug 557591 - Add tests for Utils.catch. --- services/sync/tests/unit/test_utils_catch.js | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_catch.js diff --git a/services/sync/tests/unit/test_utils_catch.js b/services/sync/tests/unit/test_utils_catch.js new file mode 100644 index 000000000000..deff0bb24777 --- /dev/null +++ b/services/sync/tests/unit/test_utils_catch.js @@ -0,0 +1,42 @@ +_("Make sure catch when copied to an object will correctly catch stuff"); +Cu.import("resource://weave/util.js"); + +function run_test() { + let ret, rightThis, didCall, didThrow; + let obj = { + catch: Utils.catch, + _log: { + debug: function(str) { + didThrow = str.search(/^Exception: /) == 0; + } + }, + + func: function() this.catch(function() { + rightThis = this == obj; + didCall = true; + return 5; + })(), + + throwy: function() this.catch(function() { + rightThis = this == obj; + didCall = true; + throw 10; + })() + }; + + _("Make sure a normal call will call and return"); + rightThis = didCall = didThrow = false; + ret = obj.func(); + do_check_eq(ret, 5); + do_check_true(rightThis); + do_check_true(didCall); + do_check_false(didThrow); + + _("Make sure catch/throw results in debug call and caller doesn't need to handle exception"); + rightThis = didCall = didThrow = false; + ret = obj.throwy(); + do_check_eq(ret, undefined); + do_check_true(rightThis); + do_check_true(didCall); + do_check_true(didThrow); +} From 927b74fbab848bbd2aa55b4489c6fee1f85cf217 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 10 May 2010 16:42:04 -0700 Subject: [PATCH 1673/1860] Bug 557591 - Add tests for Utils.lock. --- services/sync/tests/unit/test_utils_lock.js | 65 +++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_lock.js diff --git a/services/sync/tests/unit/test_utils_lock.js b/services/sync/tests/unit/test_utils_lock.js new file mode 100644 index 000000000000..f5fd4b88d31d --- /dev/null +++ b/services/sync/tests/unit/test_utils_lock.js @@ -0,0 +1,65 @@ +_("Make sure lock prevents calling with a shared lock"); +Cu.import("resource://weave/util.js"); + +function run_test() { + let ret, rightThis, didCall; + let state, lockState, lockedState, unlockState; + let obj = { + _lock: Utils.lock, + lock: function() { + lockState = ++state; + if (this._locked) { + lockedState = ++state; + return false; + } + this._locked = true; + return true; + }, + unlock: function() { + unlockState = ++state; + this._locked = false; + }, + + func: function() this._lock(function() { + rightThis = this == obj; + didCall = true; + return 5; + })(), + + throwy: function() this._lock(function() { + rightThis = this == obj; + didCall = true; + this.throwy(); + })() + }; + + _("Make sure a normal call will call and return"); + rightThis = didCall = false; + state = 0; + ret = obj.func(); + do_check_eq(ret, 5); + do_check_true(rightThis); + do_check_true(didCall); + do_check_eq(lockState, 1); + do_check_eq(unlockState, 2); + do_check_eq(state, 2); + + _("Make sure code that calls locked code throws"); + ret = null; + rightThis = didCall = false; + try { + ret = obj.throwy(); + do_throw("throwy internal call should have thrown!"); + } + catch(ex) { + do_check_eq(ex, "Could not acquire lock"); + } + do_check_eq(ret, null); + do_check_true(rightThis); + do_check_true(didCall); + _("Lock should be called twice so state 3 is skipped"); + do_check_eq(lockState, 4); + do_check_eq(lockedState, 5); + do_check_eq(unlockState, 6); + do_check_eq(state, 6); +} From 2632396b52450118adaf3c5e321cc44472248b7e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 10 May 2010 17:05:50 -0700 Subject: [PATCH 1674/1860] Bug 557591 - Add tests for Utils.notify. --- services/sync/tests/unit/test_utils_notify.js | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_notify.js diff --git a/services/sync/tests/unit/test_utils_notify.js b/services/sync/tests/unit/test_utils_notify.js new file mode 100644 index 000000000000..962468fa9600 --- /dev/null +++ b/services/sync/tests/unit/test_utils_notify.js @@ -0,0 +1,90 @@ +_("Make sure notify sends out the right notifications"); +Cu.import("resource://weave/util.js"); + +function run_test() { + let ret, rightThis, didCall; + let obj = { + notify: Utils.notify("foo:"), + _log: { + trace: function() {} + }, + + func: function() this.notify("bar", "baz", function() { + rightThis = this == obj; + didCall = true; + return 5; + })(), + + throwy: function() this.notify("bad", "one", function() { + rightThis = this == obj; + didCall = true; + throw 10; + })() + }; + + let state = 0; + let makeObs = function(topic) { + let obj = { + observe: function(subject, topic, data) { + this.state = ++state; + this.subject = subject; + this.topic = topic; + this.data = data; + } + }; + + Svc.Obs.add(topic, obj); + return obj; + }; + + _("Make sure a normal call will call and return with notifications"); + rightThis = didCall = false; + let fs = makeObs("foo:bar:start"); + let ff = makeObs("foo:bar:finish"); + let fe = makeObs("foo:bar:error"); + ret = obj.func(); + do_check_eq(ret, 5); + do_check_true(rightThis); + do_check_true(didCall); + do_check_eq(fs.state, 1); + do_check_eq(fs.subject, "baz"); + do_check_eq(fs.topic, "foo:bar:start"); + do_check_eq(fs.data, undefined); + do_check_eq(ff.state, 2); + do_check_eq(ff.subject, "baz"); + do_check_eq(ff.topic, "foo:bar:finish"); + do_check_eq(ff.data, undefined); + do_check_eq(fe.state, undefined); + do_check_eq(fe.subject, undefined); + do_check_eq(fe.topic, undefined); + do_check_eq(fe.data, undefined); + + _("Make sure a throwy call will call and throw with notifications"); + ret = null; + rightThis = didCall = false; + let ts = makeObs("foo:bad:start"); + let tf = makeObs("foo:bad:finish"); + let te = makeObs("foo:bad:error"); + try { + ret = obj.throwy(); + do_throw("throwy should have thrown!"); + } + catch(ex) { + do_check_eq(ex, 10); + } + do_check_eq(ret, null); + do_check_true(rightThis); + do_check_true(didCall); + do_check_eq(ts.state, 3); + do_check_eq(ts.subject, "one"); + do_check_eq(ts.topic, "foo:bad:start"); + do_check_eq(ts.data, undefined); + do_check_eq(tf.state, undefined); + do_check_eq(tf.subject, undefined); + do_check_eq(tf.topic, undefined); + do_check_eq(tf.data, undefined); + do_check_eq(te.state, 4); + do_check_eq(te.subject, "one"); + do_check_eq(te.topic, "foo:bad:error"); + do_check_eq(te.data, undefined); +} From 3f7981ffcfddd52b7cd26d795dda9691b7eee013 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 10 May 2010 17:22:48 -0700 Subject: [PATCH 1675/1860] Bug 557591 - Add tests for Utils.makeGUID. --- services/sync/tests/unit/test_utils_makeGUID.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_makeGUID.js diff --git a/services/sync/tests/unit/test_utils_makeGUID.js b/services/sync/tests/unit/test_utils_makeGUID.js new file mode 100644 index 000000000000..3dc43803e4c4 --- /dev/null +++ b/services/sync/tests/unit/test_utils_makeGUID.js @@ -0,0 +1,15 @@ +_("Make sure makeGUID makes guids of the right length/characters"); +Cu.import("resource://weave/util.js"); + +function run_test() { + // XXX this could cause random failures as guids arent always unique... + _("Create a bunch of guids to make sure they don't conflict"); + let guids = []; + for (let i = 0; i < 1000; i++) { + let newGuid = Utils.makeGUID(); + _("Making sure guid has the right length without special characters:", newGuid); + do_check_eq(encodeURIComponent(newGuid).length, 10); + do_check_true(guids.every(function(g) g != newGuid)); + guids.push(newGuid); + } +} From 7304a8d8afb5420411de63f29ab66d7d14b2cac0 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 10 May 2010 18:08:52 -0700 Subject: [PATCH 1676/1860] Bug 557591 - Add tests for Utils.anno. --- services/sync/tests/unit/test_utils_anno.js | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_anno.js diff --git a/services/sync/tests/unit/test_utils_anno.js b/services/sync/tests/unit/test_utils_anno.js new file mode 100644 index 000000000000..4a1e2f393af6 --- /dev/null +++ b/services/sync/tests/unit/test_utils_anno.js @@ -0,0 +1,23 @@ +_("Make sure various combinations of anno arguments do the right get/set for pages/items"); +Cu.import("resource://weave/util.js"); + +function run_test() { + _("set an anno on an item 1"); + Utils.anno(1, "anno", "hi"); + do_check_eq(Utils.anno(1, "anno"), "hi"); + + _("set an anno on a url"); + Utils.anno("about:", "tation", "hello"); + do_check_eq(Utils.anno("about:", "tation"), "hello"); + + _("make sure getting it also works with a nsIURI"); + let uri = Utils.makeURI("about:"); + do_check_eq(Utils.anno(uri, "tation"), "hello"); + + _("make sure annotations get updated"); + Utils.anno(uri, "tation", "bye!"); + do_check_eq(Utils.anno("about:", "tation"), "bye!"); + + _("sanity check that the item anno is still there"); + do_check_eq(Utils.anno(1, "anno"), "hi"); +} From b8a8c4f876d14ddf30b37a579d1aac2643aeda59 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 14 May 2010 17:31:24 -0700 Subject: [PATCH 1677/1860] Bug 557591 - Add tests for Utils.deferGetSet. --- .../sync/tests/unit/test_utils_deferGetSet.js | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_deferGetSet.js diff --git a/services/sync/tests/unit/test_utils_deferGetSet.js b/services/sync/tests/unit/test_utils_deferGetSet.js new file mode 100644 index 000000000000..e12502cef489 --- /dev/null +++ b/services/sync/tests/unit/test_utils_deferGetSet.js @@ -0,0 +1,52 @@ +_("Make sure various combinations of deferGetSet arguments correctly defer getting/setting properties to another object"); +Cu.import("resource://weave/util.js"); + +function run_test() { + let base = function() {}; + base.prototype = { + dst: {}, + + get a() "a", + set b(val) this.dst.b = val + "!!!" + }; + let src = new base(); + + _("get/set a single property"); + Utils.deferGetSet(base, "dst", "foo"); + src.foo = "bar"; + do_check_eq(src.dst.foo, "bar"); + do_check_eq(src.foo, "bar"); + + _("editing the target also updates the source"); + src.dst.foo = "baz"; + do_check_eq(src.dst.foo, "baz"); + do_check_eq(src.foo, "baz"); + + _("handle multiple properties"); + Utils.deferGetSet(base, "dst", ["p1", "p2"]); + src.p1 = "v1"; + src.p2 = "v2"; + do_check_eq(src.p1, "v1"); + do_check_eq(src.dst.p1, "v1"); + do_check_eq(src.p2, "v2"); + do_check_eq(src.dst.p2, "v2"); + + _("handle dotted properties"); + src.dst.nest = {}; + Utils.deferGetSet(base, "dst.nest", "prop"); + src.prop = "val"; + do_check_eq(src.prop, "val"); + do_check_eq(src.dst.nest.prop, "val"); + + _("make sure existing getter keeps its functionality"); + Utils.deferGetSet(base, "dst", "a"); + src.a = "not a"; + do_check_eq(src.dst.a, "not a"); + do_check_eq(src.a, "a"); + + _("make sure existing setter keeps its functionality"); + Utils.deferGetSet(base, "dst", "b"); + src.b = "b"; + do_check_eq(src.dst.b, "b!!!"); + do_check_eq(src.b, "b!!!"); +} From 3b324695c7bee70d2c2724cdfe1145d3ab8e163c Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Mon, 17 May 2010 13:58:33 -0400 Subject: [PATCH 1678/1860] Bug 565164 - move privacy policy and terms of service to services.mozilla.com, r=Mardak --- services/sync/services-sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 83752d951957..72f49dd0ec32 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -2,8 +2,8 @@ pref("extensions.weave.serverURL", "@server_url@"); pref("extensions.weave.storageAPI", "1.0"); pref("extensions.weave.userURL", "user/"); pref("extensions.weave.miscURL", "misc/"); -pref("extensions.weave.termsURL", "http://mozillalabs.com/weave/tos/"); -pref("extensions.weave.privacyURL", "http://mozillalabs.com/weave/weave-privacy-policy/"); +pref("extensions.weave.termsURL", "https://services.mozilla.com/tos/"); +pref("extensions.weave.privacyURL", "https://services.mozilla.com/privacy-policy/"); pref("extensions.weave.lastversion", "firstrun"); pref("extensions.weave.autoconnect", true); From 19bdd4b12def705ca71f137d309b62444e794d2c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 17 May 2010 12:43:20 -0700 Subject: [PATCH 1679/1860] Bug 557591 - Add tests for Utils.lazy/cb. --- services/sync/tests/unit/test_utils_lazy.js | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_lazy.js diff --git a/services/sync/tests/unit/test_utils_lazy.js b/services/sync/tests/unit/test_utils_lazy.js new file mode 100644 index 000000000000..31dffb76cd52 --- /dev/null +++ b/services/sync/tests/unit/test_utils_lazy.js @@ -0,0 +1,40 @@ +_("Make sure lazy constructor calling/assignment works"); +Cu.import("resource://weave/util.js"); + +function run_test() { + let count = 0; + let Foo = function() { + this.num = ++count; + } + + _("Make a thing instance of Foo but make sure it isn't initialized yet"); + let obj = {}; + Utils.lazy(obj, "thing", Foo); + do_check_eq(count, 0); + + _("Access the property to make it construct"); + do_check_eq(typeof obj.thing, "object"); + do_check_eq(obj.thing.constructor, Foo); + do_check_eq(count, 1); + do_check_eq(obj.thing.num, 1); + + _("Additional accesses don't construct again (nothing should change"); + do_check_eq(typeof obj.thing, "object"); + do_check_eq(obj.thing.constructor, Foo); + do_check_eq(count, 1); + do_check_eq(obj.thing.num, 1); + + _("More lazy properties will constrct more"); + do_check_eq(typeof obj.other, "undefined"); + Utils.lazy(obj, "other", Foo); + do_check_eq(typeof obj.other, "object"); + do_check_eq(obj.other.constructor, Foo); + do_check_eq(count, 2); + do_check_eq(obj.other.num, 2); + + _("Sanity check that the original property didn't change"); + do_check_eq(typeof obj.thing, "object"); + do_check_eq(obj.thing.constructor, Foo); + do_check_eq(count, 2); + do_check_eq(obj.thing.num, 1); +} From 287134b5d75bcd10bf80d1b52465723ff86d3f20 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 17 May 2010 12:47:29 -0700 Subject: [PATCH 1680/1860] Bug 557591 - Add tests for Utils.lazy2/cb. --- services/sync/tests/unit/test_utils_lazy2.js | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_lazy2.js diff --git a/services/sync/tests/unit/test_utils_lazy2.js b/services/sync/tests/unit/test_utils_lazy2.js new file mode 100644 index 000000000000..1120d15feb98 --- /dev/null +++ b/services/sync/tests/unit/test_utils_lazy2.js @@ -0,0 +1,36 @@ +_("Make sure lazy2 function calling/assignment works"); +Cu.import("resource://weave/util.js"); + +function run_test() { + let count = 0; + let Foo = function() { + return ++count; + } + + _("Make a thing instance of Foo but make sure it isn't initialized yet"); + let obj = {}; + Utils.lazy2(obj, "thing", Foo); + do_check_eq(count, 0); + + _("Access the property to make it evaluates"); + do_check_eq(typeof obj.thing, "number"); + do_check_eq(count, 1); + do_check_eq(obj.thing, 1); + + _("Additional accesses don't evaluate again (nothing should change"); + do_check_eq(typeof obj.thing, "number"); + do_check_eq(count, 1); + do_check_eq(obj.thing, 1); + + _("More lazy properties will constrct more"); + do_check_eq(typeof obj.other, "undefined"); + Utils.lazy2(obj, "other", Foo); + do_check_eq(typeof obj.other, "number"); + do_check_eq(count, 2); + do_check_eq(obj.other, 2); + + _("Sanity check that the original property didn't change"); + do_check_eq(typeof obj.thing, "number"); + do_check_eq(count, 2); + do_check_eq(obj.thing, 1); +} From 88a169a34d85e2ef3f0ac0884be52423eccc83a4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 17 May 2010 13:46:19 -0700 Subject: [PATCH 1681/1860] Bug 557591 - Add tests for Utils.lazySvc. [r=mconnor] Add a fake service that definitely won't exist for testing purposes. --- services/sync/modules/util.js | 4 +++ .../sync/tests/unit/test_utils_lazySvc.js | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_lazySvc.js diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 36aef43d26a3..e0b7bab5b9ad 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -854,6 +854,10 @@ let FakeSvc = { }); return JSON.stringify(state); } + }, + // A fake service only used for testing + "@labs.mozilla.com/Fake/Thing;1": { + isFake: true } }; diff --git a/services/sync/tests/unit/test_utils_lazySvc.js b/services/sync/tests/unit/test_utils_lazySvc.js new file mode 100644 index 000000000000..dda2db7e6f35 --- /dev/null +++ b/services/sync/tests/unit/test_utils_lazySvc.js @@ -0,0 +1,31 @@ +_("Make sure lazySvc get the desired services"); +Cu.import("resource://weave/util.js"); + +function run_test() { + _("Load the xul app info service as obj.app"); + let obj = {} + do_check_eq(typeof obj.app, "undefined"); + Utils.lazySvc(obj, "app", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"); + do_check_eq(typeof obj.app.QueryInterface, "function"); + do_check_eq(typeof obj.app.vendor, "string"); + do_check_eq(typeof obj.app.name, "string"); + + _("Check other types of properties on profiles"); + Utils.lazySvc(obj, "prof", "@mozilla.org/toolkit/profile-service;1", "nsIToolkitProfileService"); + do_check_eq(typeof obj.prof.QueryInterface, "function"); + do_check_eq(typeof obj.prof.startOffline, "boolean"); + do_check_eq(typeof obj.prof.profileCount, "number"); + do_check_eq(typeof obj.prof.createProfile, "function"); + + _("Make sure fake services get loaded correctly (private browsing doesnt exist on all platforms)"); + Utils.lazySvc(obj, "priv", "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService"); + do_check_eq(typeof obj.priv.privateBrowsingEnabled, "boolean"); + + _("Definitely make sure services that should never exist will use fake service if available"); + Utils.lazySvc(obj, "fake", "@labs.mozilla.com/Fake/Thing;1", "fake"); + do_check_eq(obj.fake.isFake, true); + + _("Nonexistant services that aren't fake-implemented will get nothing"); + Utils.lazySvc(obj, "nonexist", "@something?@", "doesnt exist"); + do_check_eq(obj.nonexist, undefined); +} From 11a1a512bd7971fe882836c00e3aef4776c2f8d5 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 17 May 2010 14:17:32 -0700 Subject: [PATCH 1682/1860] Bug 557591 - Add tests for Utils.stackTrace. --- .../sync/tests/unit/test_utils_stackTrace.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_stackTrace.js diff --git a/services/sync/tests/unit/test_utils_stackTrace.js b/services/sync/tests/unit/test_utils_stackTrace.js new file mode 100644 index 000000000000..66645e979a27 --- /dev/null +++ b/services/sync/tests/unit/test_utils_stackTrace.js @@ -0,0 +1,32 @@ +_("Define some functions in well defined line positions for the test"); +function foo(v) bar(v + 1); // line 2 +function bar(v) baz(v + 1); // line 3 +function baz(v) { throw new Error(v + 1); } // line 4 + +_("Make sure lazy constructor calling/assignment works"); +Cu.import("resource://weave/util.js"); + +function run_test() { + _("Make sure functions, arguments, files are pretty printed in the trace"); + let trace = ""; + try { + foo(0); + } + catch(ex) { + trace = Utils.stackTrace(ex); + } + _("Got trace:", trace); + do_check_neq(trace, ""); + + let errorPos = trace.indexOf('Error("3")@:0'); + let bazPos = trace.indexOf("baz(2)@test_utils_stackTrace.js:4"); + let barPos = trace.indexOf("bar(1)@test_utils_stackTrace.js:3"); + let fooPos = trace.indexOf("foo(0)@test_utils_stackTrace.js:2"); + _("String positions:", errorPos, bazPos, barPos, fooPos); + + _("Make sure the desired messages show up"); + do_check_true(errorPos >= 0); + do_check_true(bazPos > errorPos); + do_check_true(barPos > bazPos); + do_check_true(fooPos > barPos); +} From b5e49fc3ddf0969f3095f6d8ed7c21bf787beb33 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 18 May 2010 13:40:12 -0700 Subject: [PATCH 1683/1860] Bug 557591 - Add tests for Utils.sha256HMAC. --- .../sync/tests/unit/test_utils_sha256HMAC.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_sha256HMAC.js diff --git a/services/sync/tests/unit/test_utils_sha256HMAC.js b/services/sync/tests/unit/test_utils_sha256HMAC.js new file mode 100644 index 000000000000..bb3b721bd1c2 --- /dev/null +++ b/services/sync/tests/unit/test_utils_sha256HMAC.js @@ -0,0 +1,26 @@ +_("Make sure sha256 hmac works with various messages and keys"); +Cu.import("resource://weave/util.js"); + +function run_test() { + let key1 = Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, "key1"); + let key2 = Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, "key2"); + + let mes1 = "message 1"; + let mes2 = "message 2"; + + _("Make sure right sha256 hmacs are generated"); + let hmac11 = Utils.sha256HMAC(mes1, key1); + do_check_eq(hmac11, "54f035bfbd6b44445d771c7c430e0753f1c00823974108fb4723703782552296"); + let hmac12 = Utils.sha256HMAC(mes1, key2); + do_check_eq(hmac12, "1dbeae48de1b12f69517d828fb32969c74c8adc0715babc41b8f50254a980e70"); + let hmac21 = Utils.sha256HMAC(mes2, key1); + do_check_eq(hmac21, "e00e91db4e86973868de8b3e818f4c968894d4135a3209bfea7b9e699484f07a"); + let hmac22 = Utils.sha256HMAC(mes2, key2); + do_check_eq(hmac22, "4624312da31ada485b87beeecef0e5f0311cd5de60ea12291ce34cab158e0cc7"); + + _("Repeated hmacs shouldn't change the digest"); + do_check_eq(Utils.sha256HMAC(mes1, key1), hmac11); + do_check_eq(Utils.sha256HMAC(mes1, key2), hmac12); + do_check_eq(Utils.sha256HMAC(mes2, key1), hmac21); + do_check_eq(Utils.sha256HMAC(mes2, key2), hmac22); +} From fcc624d63f5f7882462ccb37b2159367240ad891 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 19 May 2010 09:40:45 -0700 Subject: [PATCH 1684/1860] Bug 557591 - Add tests for Utils.makeURI. --- .../sync/tests/unit/test_utils_makeURI.js | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_makeURI.js diff --git a/services/sync/tests/unit/test_utils_makeURI.js b/services/sync/tests/unit/test_utils_makeURI.js new file mode 100644 index 000000000000..0a194159a861 --- /dev/null +++ b/services/sync/tests/unit/test_utils_makeURI.js @@ -0,0 +1,59 @@ +_("Make sure uri strings are converted to nsIURIs"); +Cu.import("resource://weave/util.js"); + +function run_test() { + _("Check http uris"); + let uri1 = "http://mozillalabs.com/"; + do_check_eq(Utils.makeURI(uri1).spec, uri1); + let uri2 = "http://www.mozillalabs.com/"; + do_check_eq(Utils.makeURI(uri2).spec, uri2); + let uri3 = "http://mozillalabs.com/path"; + do_check_eq(Utils.makeURI(uri3).spec, uri3); + let uri4 = "http://mozillalabs.com/multi/path"; + do_check_eq(Utils.makeURI(uri4).spec, uri4); + let uri5 = "http://mozillalabs.com/?query"; + do_check_eq(Utils.makeURI(uri5).spec, uri5); + let uri6 = "http://mozillalabs.com/#hash"; + do_check_eq(Utils.makeURI(uri6).spec, uri6); + + _("Check https uris"); + let uris1 = "https://mozillalabs.com/"; + do_check_eq(Utils.makeURI(uris1).spec, uris1); + let uris2 = "https://www.mozillalabs.com/"; + do_check_eq(Utils.makeURI(uris2).spec, uris2); + let uris3 = "https://mozillalabs.com/path"; + do_check_eq(Utils.makeURI(uris3).spec, uris3); + let uris4 = "https://mozillalabs.com/multi/path"; + do_check_eq(Utils.makeURI(uris4).spec, uris4); + let uris5 = "https://mozillalabs.com/?query"; + do_check_eq(Utils.makeURI(uris5).spec, uris5); + let uris6 = "https://mozillalabs.com/#hash"; + do_check_eq(Utils.makeURI(uris6).spec, uris6); + + _("Check chrome uris"); + let uric1 = "chrome://browser/content/browser.xul"; + do_check_eq(Utils.makeURI(uric1).spec, uric1); + let uric2 = "chrome://browser/skin/browser.css"; + do_check_eq(Utils.makeURI(uric2).spec, uric2); + let uric3 = "chrome://browser/locale/browser.dtd"; + do_check_eq(Utils.makeURI(uric3).spec, uric3); + + _("Check about uris"); + let uria1 = "about:weave"; + do_check_eq(Utils.makeURI(uria1).spec, uria1); + let uria2 = "about:weave/"; + do_check_eq(Utils.makeURI(uria2).spec, uria2); + let uria3 = "about:weave/path"; + do_check_eq(Utils.makeURI(uria3).spec, uria3); + let uria4 = "about:weave/multi/path"; + do_check_eq(Utils.makeURI(uria4).spec, uria4); + let uria5 = "about:weave/?query"; + do_check_eq(Utils.makeURI(uria5).spec, uria5); + let uria6 = "about:weave/#hash"; + do_check_eq(Utils.makeURI(uria6).spec, uria6); + + _("Invalid uris are undefined"); + do_check_eq(Utils.makeURI("mozillalabs.com"), undefined); + do_check_eq(Utils.makeURI("chrome://badstuff"), undefined); + do_check_eq(Utils.makeURI("this is a test"), undefined); +} From 3f47312fca6de1a73ad02c22ed67defcfe28dfbb Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 20 May 2010 18:03:19 -0700 Subject: [PATCH 1685/1860] Add tests for Utils.jsonSave and Utils.jsonLoad. --- services/sync/tests/unit/test_utils_json.js | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_json.js diff --git a/services/sync/tests/unit/test_utils_json.js b/services/sync/tests/unit/test_utils_json.js new file mode 100644 index 000000000000..17f2a997cfa3 --- /dev/null +++ b/services/sync/tests/unit/test_utils_json.js @@ -0,0 +1,46 @@ +_("Make sure json saves and loads from disk"); +Cu.import("resource://weave/util.js"); + +function run_test() { + _("Do a simple write of an array to json and read"); + let foo; + Utils.jsonSave("foo", {}, ["v1", "v2"]); + Utils.jsonLoad("foo", {}, function(val) { + foo = val; + }); + do_check_eq(typeof foo, "object"); + do_check_eq(foo.length, 2); + do_check_eq(foo[0], "v1"); + do_check_eq(foo[1], "v2"); + + _("Use the function callback version of jsonSave"); + let bar; + Utils.jsonSave("bar", {}, function() ["v1", "v2"]); + Utils.jsonLoad("bar", {}, function(val) { + bar = val; + }); + do_check_eq(typeof bar, "object"); + do_check_eq(bar.length, 2); + do_check_eq(bar[0], "v1"); + do_check_eq(bar[1], "v2"); + + _("Try saving simple strings"); + let str; + Utils.jsonSave("str", {}, "hi"); + Utils.jsonLoad("str", {}, function(val) { + str = val; + }); + do_check_eq(typeof str, "string"); + do_check_eq(str.length, 2); + do_check_eq(str[0], "h"); + do_check_eq(str[1], "i"); + + _("Try saving a number"); + let num; + Utils.jsonSave("num", {}, function() 42); + Utils.jsonLoad("num", {}, function(val) { + num = val; + }); + do_check_eq(typeof num, "number"); + do_check_eq(num, 42); +} From e44e0bd9eed1917f244f691140deba232243452b Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 21 May 2010 12:15:58 -0700 Subject: [PATCH 1686/1860] Bug 567371 - replace server and replace local options does not sync certain Passwords/bookmarks [r=mconnor] Make sure to clear local cache when deleting crypto records from the server. Handle missing crypto by deleting any existing data and reuploading. Fix broken records by uploading new ones. --- services/sync/modules/engines.js | 47 ++++++++++++++++---------------- services/sync/modules/service.js | 6 +++- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 69082dd805e5..85ccbac8a59b 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -357,8 +357,22 @@ SyncEngine.prototype = { _syncStartup: function SyncEngine__syncStartup() { this._log.trace("Ensuring server crypto records are there"); + // Try getting/unwrapping the crypto record + let meta = CryptoMetas.get(this.cryptoMetaURL); + if (meta) { + try { + let pubkey = PubKeys.getDefaultKey(); + let privkey = PrivKeys.get(pubkey.privateKeyUri); + meta.getKey(privkey, ID.get("WeaveCryptoID")); + } + catch(ex) { + // Indicate that we don't have a cryptometa to delete and reupload + this._log.debug("Purging bad data after failed unwrap crypto: " + ex); + meta = null; + } + } + // Determine if we need to wipe on outdated versions - let wipeServerData = false; let metaGlobal = Records.get(this.metaURL); let engines = metaGlobal.payload.engines || {}; let engineData = engines[this.name] || {}; @@ -368,7 +382,7 @@ SyncEngine.prototype = { this._log.debug("Old engine data: " + [engineData.version, this.version]); // Prepare to clear the server and upload everything - wipeServerData = true; + meta = null; this.syncID = ""; // Set the newer version and newly generated syncID @@ -393,31 +407,12 @@ SyncEngine.prototype = { this._resetClient(); }; - // Try getting/unwrapping the crypto record - let meta = CryptoMetas.get(this.cryptoMetaURL); - if (meta) { - try { - let pubkey = PubKeys.getDefaultKey(); - let privkey = PrivKeys.get(pubkey.privateKeyUri); - meta.getKey(privkey, ID.get("WeaveCryptoID")); - } - catch(ex) { - // Remove traces of this bad cryptometa and tainted data - this._log.debug("Purging bad data after failed unwrap crypto: " + ex); - CryptoMetas.del(this.cryptoMetaURL); - meta = null; - wipeServerData = true; - } - } - - // Delete all server data and reupload on bad version or meta - if (wipeServerData) { + // Delete any existing data and reupload on bad version or missing meta + if (meta == null) { new Resource(this.engineURL).delete(); this._resetClient(); - } - // Generate a new crypto record - if (!meta) { + // Generate a new crypto record let symkey = Svc.Crypto.generateRandomKey(); let pubkey = PubKeys.getDefaultKey(); meta = new CryptoMeta(this.cryptoMetaURL); @@ -486,6 +481,10 @@ SyncEngine.prototype = { } catch(ex) { this._log.warn("Error processing record: " + Utils.exceptionStr(ex)); + + // Upload a new record to replace the bad one if we have it + if (this._store.itemExists(item.id)) + this._tracker.addChangedID(item.id, 0); } this._tracker.ignoreAll = false; Sync.sleep(0); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index bcf5e65522cb..faa9ca20e701 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1388,7 +1388,11 @@ WeaveSvc.prototype = { for each (let name in collections) { try { new Resource(this.storageURL + name).delete(); - new Resource(this.storageURL + "crypto/" + name).delete(); + + // Remove the crypto record from the server and local cache + let crypto = this.storageURL + "crypto/" + name; + new Resource(crypto).delete(); + CryptoMetas.del(crypto); } catch(ex) { this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); From 31d36213422e7b1b2b75f601964b80a871d08f63 Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Fri, 21 May 2010 17:41:19 -0400 Subject: [PATCH 1687/1860] Bug 567364 - final tweaks, r=Mardak --- services/sync/modules/constants.js | 3 ++- services/sync/modules/service.js | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 28908d3d0291..49ea4ef56ee7 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -47,7 +47,8 @@ WEAVE_ID: "@weave_id@", STORAGE_VERSION: @storage_version@, DEFAULT_SERVER: "@server_url@", -UPDATED_URL: "https://services.mozilla.com/sync/updated/?version=@weave_version@&channel=@xpi_type@", +UPDATED_DEV_URL: "https://services.mozilla.com/sync/updated/?version=@weave_version@&channel=@xpi_type@", +UPDATED_REL_URL: "http://www.mozilla.com/firefox/sync/updated.html", PREFS_BRANCH: "extensions.weave.", diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index faa9ca20e701..2192ab1158d5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -169,6 +169,10 @@ WeaveSvc.prototype = { return this.serverURL + "weave-password-reset"; }, + get updatedURL() { + return WEAVE_CHANNEL == "dev" ? UPDATED_DEV_URL : UPDATED_REL_URL; + }, + get syncID() { // Generate a random syncID id we don't have one let syncID = Svc.Prefs.get("client.syncID", ""); From 5f9065d8befb10dbb73a4db8eca6a6b2cec82234 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 26 May 2010 11:56:04 -0700 Subject: [PATCH 1688/1860] Bug 568256 - form history changes breaks form sync, FF search bar, probably more [r=mconnor] Check for both old and new class IDs and use the one that exists when wrapping. Also add FormTracker as the observer object so that its notify function is correctly called by nsHTMLFormElement. --- services/sync/FormNotifier.js | 5 +++-- services/sync/modules/engines/forms.js | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/services/sync/FormNotifier.js b/services/sync/FormNotifier.js index ec1bf7628974..7042d99f5b7a 100644 --- a/services/sync/FormNotifier.js +++ b/services/sync/FormNotifier.js @@ -4,8 +4,9 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); function FormNotifier() { - let baseForm = Components.classesByID["{a2059c0e-5a58-4c55-ab7c-26f0557546ef}"]. - getService(Ci.nsIFormHistory2) + let formClass = Components.classesByID["{a2059c0e-5a58-4c55-ab7c-26f0557546ef}"] || + Components.classesByID["{0c1bb408-71a2-403f-854a-3a0659829ded}"]; + let baseForm = formClass.getService(Ci.nsIFormHistory2); let obs = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index aa1df28b543a..0e844a5c5493 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -216,7 +216,11 @@ FormStore.prototype = { function FormTracker(name) { Tracker.call(this, name); Svc.Obs.add("form-notifier", this); - Svc.Obs.add("earlyformsubmit", this); + + // nsHTMLFormElement doesn't use the normal observer/observe pattern and looks + // up nsIFormSubmitObservers to .notify() them so add manually to observers + Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService). + addObserver(this, "earlyformsubmit", false); } FormTracker.prototype = { __proto__: Tracker.prototype, From fa93d3505ce51ec42a7c5d8792860e1103a2e650 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 27 May 2010 11:04:26 -0700 Subject: [PATCH 1689/1860] Bug 568440 - sync errors when resetting sync at the same time as other syncs are occurring [r=mconnor] Clear out cached crypto and keys if info collections says they've been modified. --- services/sync/modules/service.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2192ab1158d5..5895d2b14767 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1213,6 +1213,21 @@ WeaveSvc.prototype = { for each (let engine in [Clients].concat(Engines.getAll())) engine.lastModified = info.obj[engine.name] || 0; + // If the modified time of crypto records ever changes, clear the cache + if (info.obj.crypto != this.cryptoModified) { + this._log.debug("Clearing cached crypto records"); + CryptoMetas.clearCache(); + this.cryptoModified = info.obj.crypto; + } + + // If the modified time of keys records ever changes, clear the cache + if (info.obj.keys != this.keysModified) { + this._log.debug("Clearing cached keys records"); + PubKeys.clearCache(); + PrivKeys.clearCache(); + this.keysModified = info.obj.keys; + } + if (!(this._remoteSetup())) throw "aborting sync, remote setup failed"; From c6f67042d09a6faadf24e53abc0aa8ed5ed8f785 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 27 May 2010 11:04:30 -0700 Subject: [PATCH 1690/1860] Bug 568518 - Unhelpful log messages [r=mconnor] Remove "Server attack" from exceptions. --- services/sync/modules/base_records/crypto.js | 6 +++--- services/sync/tests/unit/test_records_crypto.js | 5 +++-- services/sync/tests/unit/test_records_cryptometa.js | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 85bd4927f8f1..14fb7b8fe557 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -79,7 +79,7 @@ CryptoWrapper.prototype = { // Authenticate the encrypted blob with the expected HMAC if (Utils.sha256HMAC(this.ciphertext, symkey.hmacKey) != this.hmac) - throw "Server attack?! SHA256 HMAC mismatch: " + this.hmac; + throw "Record SHA256 HMAC mismatch: " + this.hmac; this.cleartext = JSON.parse(Svc.Crypto.decrypt(this.ciphertext, symkey, this.IV)); @@ -87,7 +87,7 @@ CryptoWrapper.prototype = { // Verify that the encrypted id matches the requested record's id if (this.cleartext.id != this.id) - throw "Server attack?! Id mismatch: " + [this.cleartext.id, this.id]; + throw "Record id mismatch: " + [this.cleartext.id, this.id]; return this.cleartext; }, @@ -137,7 +137,7 @@ CryptoMeta.prototype = { // Make sure the wrapped key hasn't been tampered with let localHMAC = Utils.sha256HMAC(wrapped_key.wrapped, this.hmacKey); if (localHMAC != wrapped_key.hmac) - throw "Server attack?! SHA256 HMAC key fail: " + wrapped_key.hmac; + throw "Key SHA256 HMAC mismatch: " + wrapped_key.hmac; // Decrypt the symmetric key and make it a String object to add properties let unwrappedKey = new String( diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 05b1d363a607..35636571206e 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -71,6 +71,7 @@ function run_test() { cryptoMeta = new CryptoMeta("http://localhost:8080/crypto-meta", auth); cryptoMeta.addUnwrappedKey(keys.pubkey, keys.symkey); + CryptoMetas.set(cryptoMeta.uri, cryptoMeta); log.info("Creating and encrypting a record"); @@ -107,7 +108,7 @@ function run_test() { catch(ex) { error = ex; } - do_check_eq(error, "Server attack?! Id mismatch: crypted-resource,other"); + do_check_eq(error, "Record id mismatch: crypted-resource,other"); log.info("Make sure wrong hmacs cause failures"); cryptoWrap.encrypt(passphrase); @@ -119,7 +120,7 @@ function run_test() { catch(ex) { error = ex; } - do_check_eq(error, "Server attack?! SHA256 HMAC mismatch: foo"); + do_check_eq(error, "Record SHA256 HMAC mismatch: foo"); log.info("Done!"); } diff --git a/services/sync/tests/unit/test_records_cryptometa.js b/services/sync/tests/unit/test_records_cryptometa.js index 57fb62bec6be..e3bf1dce223d 100644 --- a/services/sync/tests/unit/test_records_cryptometa.js +++ b/services/sync/tests/unit/test_records_cryptometa.js @@ -35,7 +35,7 @@ function run_test() { catch(ex) { error = ex; } - do_check_eq(error, "Server attack?! SHA256 HMAC key fail: failme!"); + do_check_eq(error, "Key SHA256 HMAC mismatch: failme!"); _("Switching back to the correct HMAC and trying again"); crypto.keyring[pubkey.uri.spec].hmac = goodHMAC; From 905e9fb04a96f8f35b494bcd4348ac22dc3cd2cb Mon Sep 17 00:00:00 2001 From: Mike Connor Date: Thu, 27 May 2010 14:11:16 -0400 Subject: [PATCH 1691/1860] Bug 567650 - missed weave rebranding, r=Mardak --- services/sync/locales/en-US/fennec-firstrun.dtd | 2 +- services/sync/locales/en-US/generic-change.properties | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/sync/locales/en-US/fennec-firstrun.dtd b/services/sync/locales/en-US/fennec-firstrun.dtd index 2b1aaed1b677..b3c56936f634 100644 --- a/services/sync/locales/en-US/fennec-firstrun.dtd +++ b/services/sync/locales/en-US/fennec-firstrun.dtd @@ -3,7 +3,7 @@ - + diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties index 362201997497..fbe969c2e2e8 100644 --- a/services/sync/locales/en-US/generic-change.properties +++ b/services/sync/locales/en-US/generic-change.properties @@ -16,9 +16,9 @@ change.passphrase.success = Your secret phrase was successfully changed! new.passphrase.label = New secret phrase new.passphrase.confirm = Confirm secret phrase -change.passphrase.introText = Your secret phrase must be at least 12 characters long. Weave uses this phrase as part of encrypting your data. -change.passphrase.introText2 = You may wish to write this down, as this is never sent over the Internet and is not backed up or synced by Weave for your security. -change.passphrase.warningText = Note: This will erase all data stored on the Weave server and upload new data secured by this phrase. Your other devices will not sync until the secret phrase is entered for that device. +change.passphrase.introText = Your secret phrase must be at least 12 characters long. Sync uses this phrase as part of encrypting your data. +change.passphrase.introText2 = You may wish to write this down, as this is never sent over the Internet and is not backed up or synced by Firefox Sync for your security. +change.passphrase.warningText = Note: This will erase all data stored on the Sync server and upload new data secured by this phrase. Your other devices will not sync until the secret phrase is entered for that device. new.password.label = Enter your new password new.password.confirm = Confirm your new password From 75a8018e4abfdafb6780ebb3a76d18917d92859d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 27 May 2010 17:32:15 -0700 Subject: [PATCH 1692/1860] Bug 568707 - "key is not defined" results in broken form history service [r=mconnor] Pretend to call a function with "key" to keep it around. --- services/sync/FormNotifier.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/sync/FormNotifier.js b/services/sync/FormNotifier.js index 7042d99f5b7a..9ce8b2f7d997 100644 --- a/services/sync/FormNotifier.js +++ b/services/sync/FormNotifier.js @@ -24,6 +24,9 @@ function FormNotifier() { continue; } + // XXX Bug 568707 Make use of "key" to prevent it from disappearing + (function(){})(key); + // Wrap the function with notifications this[key] = function() { let args = Array.slice(arguments); From 4e96e34b8612c98037f9b912a0d0373d122b142f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 1 Jun 2010 11:30:11 -0700 Subject: [PATCH 1693/1860] Bug 557591 - Add tests for Utils.anno with invalid uris. --- services/sync/tests/unit/test_utils_anno.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/sync/tests/unit/test_utils_anno.js b/services/sync/tests/unit/test_utils_anno.js index 4a1e2f393af6..3e33f4334d18 100644 --- a/services/sync/tests/unit/test_utils_anno.js +++ b/services/sync/tests/unit/test_utils_anno.js @@ -20,4 +20,14 @@ function run_test() { _("sanity check that the item anno is still there"); do_check_eq(Utils.anno(1, "anno"), "hi"); + + _("invalid uris don't get annos"); + let didThrow = false; + try { + Utils.anno("foo/bar/baz", "bad"); + } + catch(ex) { + didThrow = true; + } + do_check_true(didThrow); } From 8b6571496175ace76be28e00329a4bbb4917fea3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 1 Jun 2010 11:59:21 -0700 Subject: [PATCH 1694/1860] Bug 557591 - Add tests for Utils.sha1. --- services/sync/tests/unit/test_utils_sha1.js | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 services/sync/tests/unit/test_utils_sha1.js diff --git a/services/sync/tests/unit/test_utils_sha1.js b/services/sync/tests/unit/test_utils_sha1.js new file mode 100644 index 000000000000..d7a6a59d38b5 --- /dev/null +++ b/services/sync/tests/unit/test_utils_sha1.js @@ -0,0 +1,29 @@ +_("Make sure sha1 digests works with various messages"); +Cu.import("resource://weave/util.js"); + +function run_test() { + let mes1 = "hello"; + let mes2 = "world"; + + _("Make sure right sha1 digests are generated"); + let dig1 = Utils.sha1(mes1); + do_check_eq(dig1, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"); + let dig2 = Utils.sha1(mes2); + do_check_eq(dig2, "7c211433f02071597741e6ff5a8ea34789abbf43"); + let dig12 = Utils.sha1(mes1 + mes2); + do_check_eq(dig12, "6adfb183a4a2c94a2f92dab5ade762a47889a5a1"); + let dig21 = Utils.sha1(mes2 + mes1); + do_check_eq(dig21, "5715790a892990382d98858c4aa38d0617151575"); + + _("Repeated sha1s shouldn't change the digest"); + do_check_eq(Utils.sha1(mes1), dig1); + do_check_eq(Utils.sha1(mes2), dig2); + do_check_eq(Utils.sha1(mes1 + mes2), dig12); + do_check_eq(Utils.sha1(mes2 + mes1), dig21); + + _("Nested sha1 should work just fine"); + let nest1 = Utils.sha1(Utils.sha1(Utils.sha1(Utils.sha1(Utils.sha1(mes1))))); + do_check_eq(nest1, "23f340d0cff31e299158b3181b6bcc7e8c7f985a"); + let nest2 = Utils.sha1(Utils.sha1(Utils.sha1(Utils.sha1(Utils.sha1(mes2))))); + do_check_eq(nest2, "1f6453867e3fb9876ae429918a64cdb8dc5ff2d0"); +} From d5383943bcfb519b8f793f850b0d0130df7b9bb4 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 1 Jun 2010 12:44:51 -0700 Subject: [PATCH 1695/1860] Bug 569428 - Add tests for FormEngine (Store) --- .../tests/unit/test_engines_forms_store.js | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 services/sync/tests/unit/test_engines_forms_store.js diff --git a/services/sync/tests/unit/test_engines_forms_store.js b/services/sync/tests/unit/test_engines_forms_store.js new file mode 100644 index 000000000000..bf41295452d7 --- /dev/null +++ b/services/sync/tests/unit/test_engines_forms_store.js @@ -0,0 +1,81 @@ +_("Make sure the form store follows the Store api and correctly accesses the backend form storage"); +Cu.import("resource://weave/engines/forms.js"); +Cu.import("resource://weave/type_records/forms.js"); + +function run_test() { + let store = new FormEngine()._store; + + _("Remove any existing entries"); + store.wipe(); + for (let id in store.getAllIDs()) { + do_throw("Shouldn't get any ids!"); + } + + _("Add a form entry"); + store.create({ + name: "name!!", + value: "value??" + }); + + _("Should have 1 entry now"); + let id = ""; + for (let _id in store.getAllIDs()) { + if (id == "") + id = _id; + else + do_throw("Should have only gotten one!"); + } + do_check_true(store.itemExists(id)); + + let rec = store.createRecord(id); + _("Got record for id", id, rec); + do_check_eq(rec.name, "name!!"); + do_check_eq(rec.value, "value??"); + + _("Create a non-existant id for delete"); + do_check_true(store.createRecord("deleted!!").deleted); + + _("Try updating.. doesn't do anything yet"); + store.update({}); + + _("Remove all entries"); + store.wipe(); + for (let id in store.getAllIDs()) { + do_throw("Shouldn't get any ids!"); + } + + _("Add another entry"); + store.create({ + name: "another", + value: "entry" + }); + id = ""; + for (let _id in store.getAllIDs()) { + if (id == "") + id = _id; + else + do_throw("Should have only gotten one!"); + } + + _("Change the id of the new entry to something else"); + store.changeItemID(id, "newid"); + + _("Make sure it's there"); + do_check_true(store.itemExists("newid")); + + _("Remove the entry"); + store.remove({ + id: "newid" + }); + for (let id in store.getAllIDs()) { + do_throw("Shouldn't get any ids!"); + } + + _("Removing the entry again shouldn't matter"); + store.remove({ + id: "newid" + }); + for (let id in store.getAllIDs()) { + do_throw("Shouldn't get any ids!"); + } +} From 6a544f5e42b1f41c4a6c2374f7862ee99f220a78 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Tue, 1 Jun 2010 15:06:16 -0700 Subject: [PATCH 1696/1860] Bug 557588 - code audit and create unit test plan for engines.js [r=mconnor] Tests for EngineMangerSvc, Engine and SyncEngine sans sync(). --- services/sync/modules/engines.js | 2 +- services/sync/tests/unit/test_engine.js | 169 ++++++++++++++++++ .../sync/tests/unit/test_engine_lastSync.js | 8 - .../sync/tests/unit/test_enginemanager.js | 80 +++++++++ services/sync/tests/unit/test_syncengine.js | 113 ++++++++++++ 5 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 services/sync/tests/unit/test_engine.js delete mode 100644 services/sync/tests/unit/test_engine_lastSync.js create mode 100644 services/sync/tests/unit/test_enginemanager.js create mode 100644 services/sync/tests/unit/test_syncengine.js diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 85ccbac8a59b..25317262173e 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -151,7 +151,7 @@ Engine.prototype = { _trackerObj: Tracker, get prefName() this.name, - get enabled() Svc.Prefs.get("engine." + this.prefName, null), + get enabled() Svc.Prefs.get("engine." + this.prefName, false), set enabled(val) Svc.Prefs.set("engine." + this.prefName, !!val), get score() this._tracker.score, diff --git a/services/sync/tests/unit/test_engine.js b/services/sync/tests/unit/test_engine.js new file mode 100644 index 000000000000..00b31ea66182 --- /dev/null +++ b/services/sync/tests/unit/test_engine.js @@ -0,0 +1,169 @@ +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/engines.js"); +Cu.import("resource://weave/ext/Observers.js"); + + +function SteamStore() { + Store.call(this, "Steam"); + this.wasWiped = false; +} +SteamStore.prototype = { + __proto__: Store.prototype, + + wipe: function() { + this.wasWiped = true; + } +}; + +function SteamTracker() { + Tracker.call(this, "Steam"); +} +SteamTracker.prototype = { + __proto__: Tracker.prototype +}; + +function SteamEngine() { + Engine.call(this, "Steam"); + this.wasReset = false; + this.wasSynced = false; +} +SteamEngine.prototype = { + __proto__: Engine.prototype, + _storeObj: SteamStore, + _trackerObj: SteamTracker, + + _resetClient: function () { + this.wasReset = true; + }, + + _sync: function () { + this.wasSynced = true; + } +}; + +let engineObserver = { + topics: [], + + observe: function(subject, topic, data) { + do_check_eq(subject, "steam"); + this.topics.push(topic); + }, + + reset: function() { + this.topics = []; + } +}; +Observers.add("weave:engine:reset-client:start", engineObserver); +Observers.add("weave:engine:reset-client:finish", engineObserver); +Observers.add("weave:engine:wipe-client:start", engineObserver); +Observers.add("weave:engine:wipe-client:finish", engineObserver); +Observers.add("weave:engine:sync:start", engineObserver); +Observers.add("weave:engine:sync:finish", engineObserver); + + +function do_check_throws(func) { + var raised = false; + try { + func(); + } catch (ex) { + raised = true; + } + do_check_true(raised); +} + + +function test_members() { + _("Engine object members"); + let engine = new SteamEngine(); + do_check_eq(engine.displayName, "Steam"); + do_check_eq(engine.prefName, "steam"); + do_check_true(engine._store instanceof SteamStore); + do_check_true(engine._tracker instanceof SteamTracker); +} + +function test_score() { + _("Engine.score corresponds to tracker.score and is readonly"); + let engine = new SteamEngine(); + do_check_eq(engine.score, 0); + engine._tracker.score += 5; + do_check_eq(engine.score, 5); + + do_check_throws(function() { + engine.score = 10; + }); +} + +function test_resetClient() { + _("Engine.resetClient calls _resetClient"); + let engine = new SteamEngine(); + do_check_false(engine.wasReset); + + engine.resetClient(); + do_check_true(engine.wasReset); + do_check_eq(engineObserver.topics[0], "weave:engine:reset-client:start"); + do_check_eq(engineObserver.topics[1], "weave:engine:reset-client:finish"); + + engine.wasReset = false; + engineObserver.reset(); +} + +function test_wipeClient() { + _("Engine.wipeClient calls resetClient, wipes store, clears changed IDs"); + let engine = new SteamEngine(); + do_check_false(engine.wasReset); + do_check_false(engine._store.wasWiped); + do_check_true(engine._tracker.addChangedID("a-changed-id")); + do_check_true("a-changed-id" in engine._tracker.changedIDs); + + engine.wipeClient(); + do_check_true(engine.wasReset); + do_check_true(engine._store.wasWiped); + do_check_eq(JSON.stringify(engine._tracker.changedIDs), "{}"); + do_check_eq(engineObserver.topics[0], "weave:engine:wipe-client:start"); + do_check_eq(engineObserver.topics[1], "weave:engine:reset-client:start"); + do_check_eq(engineObserver.topics[2], "weave:engine:reset-client:finish"); + do_check_eq(engineObserver.topics[3], "weave:engine:wipe-client:finish"); + + engine.wasReset = false; + engine._store.wasWiped = false; + engineObserver.reset(); +} + +function test_enabled() { + _("Engine.enabled corresponds to preference"); + let engine = new SteamEngine(); + try { + do_check_false(engine.enabled); + Svc.Prefs.set("engine.steam", true); + do_check_true(engine.enabled); + + engine.enabled = false; + do_check_false(Svc.Prefs.get("engine.steam")); + } finally { + Svc.Prefs.reset("engine.steam"); + } +} + +function test_sync() { + let engine = new SteamEngine(); + try { + _("Engine.sync doesn't call _sync if it's not enabled"); + do_check_false(engine.enabled); + do_check_false(engine.wasSynced); + engine.sync(); + do_check_false(engine.wasSynced); + + _("Engine.sync calls _sync if it's enabled"); + engine.enabled = true; + engine.sync(); + do_check_true(engine.wasSynced); + do_check_eq(engineObserver.topics[0], "weave:engine:sync:start"); + do_check_eq(engineObserver.topics[1], "weave:engine:sync:finish"); + } finally { + Svc.Prefs.reset("engine.steam"); + engine.wasSynced = false; + engineObserver.reset(); + } +} diff --git a/services/sync/tests/unit/test_engine_lastSync.js b/services/sync/tests/unit/test_engine_lastSync.js deleted file mode 100644 index f243022d46e4..000000000000 --- a/services/sync/tests/unit/test_engine_lastSync.js +++ /dev/null @@ -1,8 +0,0 @@ -Cu.import("resource://weave/engines.js"); - -function run_test() { - // Make sure storing floats for lastSync stay as floats - let engine = new SyncEngine(); - engine.lastSync = 123.45; - do_check_eq(engine.lastSync, 123.45); -} diff --git a/services/sync/tests/unit/test_enginemanager.js b/services/sync/tests/unit/test_enginemanager.js new file mode 100644 index 000000000000..2ac0bab07238 --- /dev/null +++ b/services/sync/tests/unit/test_enginemanager.js @@ -0,0 +1,80 @@ +Cu.import("resource://weave/engines.js"); + +function run_test() { + _("We start out with a clean slate"); + let engines = Engines.getAll(); + do_check_eq(engines.length, 0); + do_check_eq(Engines.get('dummy'), undefined); + + _("Register an engine"); + function DummyEngine() {} + DummyEngine.prototype.name = "dummy"; + Engines.register(DummyEngine); + let dummy = Engines.get('dummy'); + do_check_true(dummy instanceof DummyEngine); + + engines = Engines.getAll(); + do_check_eq(engines.length, 1); + do_check_eq(engines[0], dummy); + + _("Register an already registered engine is ignored"); + Engines.register(DummyEngine); + do_check_eq(Engines.get('dummy'), dummy); + + _("Register multiple engines in one go"); + function PetrolEngine() {} + PetrolEngine.prototype.name = "petrol"; + function DieselEngine() {} + DieselEngine.prototype.name = "diesel"; + + Engines.register([PetrolEngine, DieselEngine]); + let petrol = Engines.get('petrol'); + let diesel = Engines.get('diesel'); + do_check_true(petrol instanceof PetrolEngine); + do_check_true(diesel instanceof DieselEngine); + + engines = Engines.getAll(); + do_check_eq(engines.length, 3); + do_check_neq(engines.indexOf(petrol), -1); + do_check_neq(engines.indexOf(diesel), -1); + + _("Retrieve multiple engines in one go"); + engines = Engines.get(["dummy", "diesel"]); + do_check_eq(engines.length, 2); + do_check_neq(engines.indexOf(dummy), -1); + do_check_neq(engines.indexOf(diesel), -1); + + _("getEnabled() only returns enabled engines"); + engines = Engines.getEnabled(); + do_check_eq(engines.length, 0); + + petrol.enabled = true; + engines = Engines.getEnabled(); + do_check_eq(engines.length, 1); + do_check_eq(engines[0], petrol); + + dummy.enabled = true; + diesel.enabled = true; + engines = Engines.getEnabled(); + do_check_eq(engines.length, 3); + + _("Unregister an engine by name"); + Engines.unregister('dummy'); + do_check_eq(Engines.get('dummy'), undefined); + engines = Engines.getAll(); + do_check_eq(engines.length, 2); + do_check_eq(engines.indexOf(dummy), -1); + + _("Unregister an engine by value"); + // Engines.unregister() checks for instanceof Engine, so let's make one: + function ActualEngine() {} + ActualEngine.prototype = {__proto__: Engine.prototype, + name: 'actual'}; + Engines.register(ActualEngine); + let actual = Engines.get('actual'); + do_check_true(actual instanceof ActualEngine); + do_check_true(actual instanceof Engine); + + Engines.unregister(actual); + do_check_eq(Engines.get('actual'), undefined); +} diff --git a/services/sync/tests/unit/test_syncengine.js b/services/sync/tests/unit/test_syncengine.js new file mode 100644 index 000000000000..439961fcdb87 --- /dev/null +++ b/services/sync/tests/unit/test_syncengine.js @@ -0,0 +1,113 @@ +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/engines.js"); + +function makeSteamEngine() { + return new SyncEngine('Steam'); +} + +var syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + +function test_url_attributes() { + _("SyncEngine url attributes"); + Svc.Prefs.set("clusterURL", "https://cluster/"); + let engine = makeSteamEngine(); + try { + do_check_eq(engine.storageURL, "https://cluster/1.0/foo/storage/"); + do_check_eq(engine.engineURL, "https://cluster/1.0/foo/storage/steam"); + do_check_eq(engine.cryptoMetaURL, + "https://cluster/1.0/foo/storage/crypto/steam"); + do_check_eq(engine.metaURL, "https://cluster/1.0/foo/storage/meta/global"); + } finally { + Svc.Prefs.reset("clusterURL"); + } +} + +function test_syncID() { + _("SyncEngine.syncID corresponds to preference"); + let engine = makeSteamEngine(); + try { + // Ensure pristine environment + do_check_eq(Svc.Prefs.get("steam.syncID"), undefined); + + // Performing the first get on the attribute will generate a new GUID. + do_check_eq(engine.syncID, "fake-guid-0"); + do_check_eq(Svc.Prefs.get("steam.syncID"), "fake-guid-0"); + + Svc.Prefs.set("steam.syncID", Utils.makeGUID()); + do_check_eq(Svc.Prefs.get("steam.syncID"), "fake-guid-1"); + do_check_eq(engine.syncID, "fake-guid-1"); + } finally { + Svc.Prefs.reset("steam.syncID"); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + +function test_lastSync() { + _("SyncEngine.lastSync corresponds to preference and stores floats"); + let engine = makeSteamEngine(); + try { + // Ensure pristine environment + do_check_eq(Svc.Prefs.get("steam.lastSync"), undefined); + do_check_eq(engine.lastSync, 0); + + // Floats are properly stored as floats and synced with the preference + engine.lastSync = 123.45; + do_check_eq(engine.lastSync, 123.45); + do_check_eq(Svc.Prefs.get("steam.lastSync"), "123.45"); + + // resetLastSync() resets the value (and preference) to 0 + engine.resetLastSync(); + do_check_eq(engine.lastSync, 0); + do_check_eq(Svc.Prefs.get("steam.lastSync"), "0"); + } finally { + Svc.Prefs.reset("steam.lastSync"); + } +} + +function test_toFetch() { + _("SyncEngine.toFetch corresponds to file on disk"); + let engine = makeSteamEngine(); + try { + // Ensure pristine environment + do_check_eq(engine.toFetch.length, 0); + + // Write file to disk + let toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()]; + engine.toFetch = toFetch; + do_check_eq(engine.toFetch, toFetch); + let fakefile = syncTesting.fakeFilesystem.fakeContents[ + "weave/toFetch/steam.json"]; + do_check_eq(fakefile, JSON.stringify(toFetch)); + + // Read file from disk + toFetch = [Utils.makeGUID(), Utils.makeGUID()]; + syncTesting.fakeFilesystem.fakeContents["weave/toFetch/steam.json"] + = JSON.stringify(toFetch); + engine.loadToFetch(); + do_check_eq(engine.toFetch.length, 2); + do_check_eq(engine.toFetch[0], toFetch[0]); + do_check_eq(engine.toFetch[1], toFetch[1]); + } finally { + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + +function test_resetClient() { + _("SyncEngine.resetClient resets lastSync and toFetch"); + let engine = makeSteamEngine(); + try { + // Ensure pristine environment + do_check_eq(Svc.Prefs.get("steam.lastSync"), undefined); + do_check_eq(engine.toFetch.length, 0); + + engine.toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()]; + engine.lastSync = 123.45; + + engine.resetClient(); + do_check_eq(engine.lastSync, 0); + do_check_eq(engine.toFetch.length, 0); + } finally { + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + Svc.Prefs.reset("steam.lastSync"); + } +} From db8a66e2965ae69930f4e0184a0aeeac40e537fc Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Tue, 1 Jun 2010 15:07:50 -0700 Subject: [PATCH 1697/1860] Bug 557588 - code audit and create unit test plan for engines.js [r=mconnor] Tests for SyncEngine.sync(), incl some additions to harness. --- services/sync/modules/constants.js | 3 +- services/sync/modules/engines.js | 6 +- services/sync/tests/unit/head_first.js | 84 +- services/sync/tests/unit/head_http_server.js | 197 ++++ .../sync/tests/unit/test_syncengine_sync.js | 934 ++++++++++++++++++ 5 files changed, 1219 insertions(+), 5 deletions(-) create mode 100644 services/sync/tests/unit/test_syncengine_sync.js diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 49ea4ef56ee7..3658c7e332d4 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -79,7 +79,8 @@ PERMS_PASSFILE: 0600, PERMS_DIRECTORY: 0755, // Number of records to upload in a single POST (multiple POSTS if exceeded) -// Record size limit is currently 10K, so 100 is a bit over 1MB +// FIXME: Record size limit is 256k (new cluster), so this can be quite large! +// (Bug 569295) MAX_UPLOAD_RECORDS: 100, // Top-level statuses: diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 25317262173e..b64a877f7994 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -447,7 +447,9 @@ SyncEngine.prototype = { _processIncoming: function SyncEngine__processIncoming() { this._log.trace("Downloading & applying server changes"); - // Figure out how many total items to fetch this sync; do less on mobile + // Figure out how many total items to fetch this sync; do less on mobile. + // 50 is hardcoded here because of URL length restrictions. + // (GUIDs can be up to 64 chars long) let fetchNum = Infinity; if (Svc.Prefs.get("client.type") == "mobile") fetchNum = 50; @@ -597,6 +599,8 @@ SyncEngine.prototype = { } }, + // Reconcile incoming and existing records. Return true if server + // data should be applied. _reconcile: function SyncEngine__reconcile(item) { if (this._log.level <= Log4Moz.Level.Trace) this._log.trace("Incoming: " + item); diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 83403fd72bfc..585b13fe271b 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -87,13 +87,12 @@ function FakeTimerService() { Utils.makeTimerForCall = self.makeTimerForCall; }; +Cu.import("resource://weave/log4moz.js"); function getTestLogger(component) { return Log4Moz.repository.getLogger("Testing"); } function initTestLogging(level) { - Cu.import("resource://weave/log4moz.js"); - function LogStats() { this.errorsLogged = 0; } @@ -117,7 +116,8 @@ function initTestLogging(level) { log.level = Log4Moz.Level.Trace; appender.level = Log4Moz.Level.Trace; - log.addAppender(appender); + // Overwrite any other appenders (e.g. from previous incarnations) + log._appenders = [appender]; return logStats; } @@ -221,6 +221,83 @@ function FakeGUIDService() { }; } + +/* + * Mock implementation of IWeaveCrypto. It does not encrypt or + * decrypt, just returns the input verbatimly. + */ +function FakeCryptoService() { + this.counter = 0; + + delete Svc.Crypto; // get rid of the getter first + Svc.Crypto = this; + Utils.sha256HMAC = this.sha256HMAC; +} +FakeCryptoService.prototype = { + + sha256HMAC: function(message, key) { + message = message.substr(0, 64); + while (message.length < 64) { + message += " "; + } + return message; + }, + + encrypt: function(aClearText, aSymmetricKey, aIV) { + return aClearText; + }, + + decrypt: function(aCipherText, aSymmetricKey, aIV) { + return aCipherText; + }, + + generateKeypair: function(aPassphrase, aSalt, aIV, + aEncodedPublicKey, aWrappedPrivateKey) { + aEncodedPublicKey.value = aPassphrase; + aWrappedPrivateKey.value = aPassphrase; + }, + + generateRandomKey: function() { + return "fake-symmetric-key-" + this.counter++; + }, + + generateRandomIV: function() { + return "fake-random-iv"; + }, + + generateRandomBytes: function(aByteCount) { + var s = ""; + for (var i=0; i < aByteCount; i++) { + s += String.fromCharCode(Math.floor(Math.random() * 256)); + } + return btoa(s); + }, + + wrapSymmetricKey: function(aSymmetricKey, aEncodedPublicKey) { + return aSymmetricKey; + }, + + unwrapSymmetricKey: function(aWrappedSymmetricKey, aWrappedPrivateKey, + aPassphrase, aSalt, aIV) { + if (!this.verifyPassphrase(aWrappedPrivateKey, aPassphrase)) { + throw Components.Exception("Unwrapping the private key failed", + Cr.NS_ERROR_FAILURE); + } + return aWrappedSymmetricKey; + }, + + rewrapPrivateKey: function(aWrappedPrivateKey, aPassphrase, aSalt, aIV, + aNewPassphrase) { + return aNewPassphrase; + }, + + verifyPassphrase: function(aWrappedPrivateKey, aPassphrase, aSalt, aIV) { + return aWrappedPrivateKey == aPassphrase; + } + +}; + + function SyncTestingInfrastructure(engineFactory) { let __fakePasswords = { 'Mozilla Services Password': {foo: "bar"}, @@ -249,6 +326,7 @@ function SyncTestingInfrastructure(engineFactory) { this.logStats = initTestLogging(); this.fakeFilesystem = new FakeFilesystemService({}); this.fakeGUIDService = new FakeGUIDService(); + this.fakeCryptoService = new FakeCryptoService(); this._logger = getTestLogger(); this._engineFactory = engineFactory; diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js index 55108fe45088..9a1a0eab3dfd 100644 --- a/services/sync/tests/unit/head_http_server.js +++ b/services/sync/tests/unit/head_http_server.js @@ -23,3 +23,200 @@ function httpd_basic_auth_handler(body, metadata, response) { } response.bodyOutputStream.write(body, body.length); } + + + +/* + * Represent a WBO on the server + */ +function ServerWBO(id, initialPayload) { + this.id = id; + if (!initialPayload) { + return; + } + + if (typeof initialPayload == "object") { + initialPayload = JSON.stringify(initialPayload); + } + this.payload = initialPayload; + this.modified = Date.now() / 1000; +} +ServerWBO.prototype = { + + get data() { + return JSON.parse(this.payload); + }, + + get: function() { + return JSON.stringify(this, ['id', 'modified', 'payload']); + }, + + put: function(input) { + input = JSON.parse(input); + this.payload = input.payload; + this.modified = Date.now() / 1000; + }, + + delete: function() { + delete this.payload; + delete this.modified; + }, + + handler: function() { + let self = this; + + return function(request, response) { + var statusCode = 200; + var status = "OK"; + var body; + + switch(request.method) { + case "GET": + if (self.payload) { + body = self.get(); + } else { + statusCode = 404; + status = "Not Found"; + body = "Not Found"; + } + break; + + case "PUT": + self.put(readBytesFromInputStream(request.bodyInputStream)); + body = JSON.stringify(self.modified); + break; + + case "DELETE": + self.delete(); + body = JSON.stringify(Date.now() / 1000); + break; + } + response.setHeader('X-Weave-Timestamp', ''+Date.now()/1000, false); + response.setStatusLine(request.httpVersion, statusCode, status); + response.bodyOutputStream.write(body, body.length); + }; + } + +}; + + +/* + * Represent a collection on the server. The 'wbo' attribute is a + * mapping of id -> ServerWBO objects. + * + * Note that if you want these records to be accessible individually, + * you need to register their handlers with the server separately! + */ +function ServerCollection(wbos) { + this.wbos = wbos || {}; +} +ServerCollection.prototype = { + + _inResultSet: function(wbo, options) { + return ((!options.ids || (options.ids.indexOf(wbo.id) != -1)) + && (!options.newer || (wbo.modified > options.newer))); + }, + + get: function(options) { + let result; + if (options.full) { + let data = [wbo.get() for ([id, wbo] in Iterator(this.wbos)) + if (this._inResultSet(wbo, options))]; + if (options.limit) { + data = data.slice(0, options.limit); + } + // Our implementation of application/newlines + result = data.join("\n") + "\n"; + } else { + let data = [id for ([id, wbo] in Iterator(this.wbos)) + if (this._inResultSet(wbo, options))]; + if (options.limit) { + data = data.slice(0, options.limit); + } + result = JSON.stringify(data); + } + return result; + }, + + post: function(input) { + input = JSON.parse(input); + let success = []; + let failed = []; + + // This will count records where we have an existing ServerWBO + // registered with us as successful and all other records as failed. + for each (let record in input) { + let wbo = this.wbos[record.id]; + if (wbo) { + wbo.payload = record.payload; + wbo.modified = Date.now() / 1000; + success.push(record.id); + } else { + failed.push(record.id); + } + } + return {success: success, + failed: failed}; + }, + + delete: function(options) { + for (let [id, wbo] in Iterator(this.wbos)) { + if (this._inResultSet(wbo, options)) { + wbo.delete(); + } + } + }, + + handler: function() { + let self = this; + + return function(request, response) { + var statusCode = 200; + var status = "OK"; + var body; + + // Parse queryString + let options = {}; + for each (let chunk in request.queryString.split('&')) { + if (!chunk) { + continue; + } + chunk = chunk.split('='); + if (chunk.length == 1) { + options[chunk[0]] = ""; + } else { + options[chunk[0]] = chunk[1]; + } + } + if (options.ids) { + options.ids = options.ids.split(','); + } + if (options.newer) { + options.newer = parseFloat(options.newer); + } + if (options.limit) { + options.limit = parseInt(options.limit, 10); + } + + switch(request.method) { + case "GET": + body = self.get(options); + break; + + case "POST": + let res = self.post(readBytesFromInputStream(request.bodyInputStream)); + body = JSON.stringify(res); + break; + + case "DELETE": + self.delete(options); + body = JSON.stringify(Date.now() / 1000); + break; + } + response.setHeader('X-Weave-Timestamp', ''+Date.now()/1000, false); + response.setStatusLine(request.httpVersion, statusCode, status); + response.bodyOutputStream.write(body, body.length); + }; + } + +}; diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js new file mode 100644 index 000000000000..6c18041dc507 --- /dev/null +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -0,0 +1,934 @@ +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/resource.js"); +Cu.import("resource://weave/stores.js"); +Cu.import("resource://weave/trackers.js"); +Cu.import("resource://weave/engines.js"); + +Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://weave/base_records/keys.js"); +Cu.import("resource://weave/base_records/crypto.js"); + +/* + * A fake engine implementation. + * + * Complete with record, store, and tracker implementations. + */ + +function SteamRecord(uri) { + CryptoWrapper.call(this, uri); +} +SteamRecord.prototype = { + __proto__: CryptoWrapper.prototype +}; +Utils.deferGetSet(SteamRecord, "cleartext", ["denomination"]); + +function SteamStore() { + Store.call(this, "Steam"); + this.items = {}; +} +SteamStore.prototype = { + __proto__: Store.prototype, + + create: function Store_create(record) { + this.items[record.id] = record.denomination; + }, + + remove: function Store_remove(record) { + delete this.items[record.id]; + }, + + update: function Store_update(record) { + this.items[record.id] = record.denomination; + }, + + itemExists: function Store_itemExists(id) { + return (id in this.items); + }, + + createRecord: function(id) { + var record = new SteamRecord(); + record.id = id; + record.denomination = this.items[id] || "Data for new record: " + id; + return record; + }, + + changeItemID: function(oldID, newID) { + this.items[newID] = this.items[oldID]; + delete this.items[oldID]; + }, + + getAllIDs: function() { + let ids = {}; + for (var id in this.items) { + ids[id] = true; + } + return ids; + }, + + wipe: function() { + this.items = {}; + } +}; + +function SteamTracker() { + Tracker.call(this, "Steam"); +} +SteamTracker.prototype = { + __proto__: Tracker.prototype +}; + + +function SteamEngine() { + SyncEngine.call(this, "Steam"); +} +SteamEngine.prototype = { + __proto__: SyncEngine.prototype, + _storeObj: SteamStore, + _trackerObj: SteamTracker, + _recordObj: SteamRecord, + + _findDupe: function(item) { + for (let [id, value] in Iterator(this._store.items)) { + if (item.denomination == value) { + return id; + } + } + } +}; + + +function makeSteamEngine() { + return new SteamEngine(); +} + +var syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + + +/* + * Test setup helpers + */ + +function sync_httpd_setup(handlers) { + handlers["/1.0/foo/storage/meta/global"] + = (new ServerWBO('global', {})).handler(); + handlers["/1.0/foo/storage/keys/pubkey"] + = (new ServerWBO('pubkey')).handler(); + handlers["/1.0/foo/storage/keys/privkey"] + = (new ServerWBO('privkey')).handler(); + return httpd_setup(handlers); +} + +function createAndUploadKeypair() { + let storageURL = Svc.Prefs.get("clusterURL") + Svc.Prefs.get("storageAPI") + + "/" + ID.get("WeaveID").username + "/storage/"; + + PubKeys.defaultKeyUri = storageURL + "keys/pubkey"; + PrivKeys.defaultKeyUri = storageURL + "keys/privkey"; + let keys = PubKeys.createKeypair(ID.get("WeaveCryptoID"), + PubKeys.defaultKeyUri, + PrivKeys.defaultKeyUri); + PubKeys.uploadKeypair(keys); +} + +function createAndUploadSymKey(url) { + let symkey = Svc.Crypto.generateRandomKey(); + let pubkey = PubKeys.getDefaultKey(); + let meta = new CryptoMeta(url); + meta.addUnwrappedKey(pubkey, symkey); + let res = new Resource(meta.uri); + res.put(meta); +} + +// Turn WBO cleartext into "encrypted" payload as it goes over the wire +function encryptPayload(cleartext) { + if (typeof cleartext == "object") { + cleartext = JSON.stringify(cleartext); + } + + return {encryption: "http://localhost:8080/1.0/foo/storage/crypto/steam", + ciphertext: cleartext, // ciphertext == cleartext with fake crypto + IV: "irrelevant", + hmac: Utils.sha256HMAC(cleartext, null)}; +} + + +/* + * Tests + * + * SyncEngine._sync() is divided into four rather independent steps: + * + * - _syncStartup() + * - _processIncoming() + * - _uploadOutgoing() + * - _syncFinish() + * + * In the spirit of unit testing, these are tested individually for + * different scenarios below. + */ + +function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() { + _("SyncEngine._syncStartup resets sync and wipes server data if there's no or an oudated global record"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let crypto_steam = new ServerWBO('steam'); + + // Some server side data that's going to be wiped + let collection = new ServerCollection(); + collection.wbos.flying = new ServerWBO( + 'flying', encryptPayload({id: 'flying', + denomination: "LNER Class A3 4472"})); + collection.wbos.scotsman = new ServerWBO( + 'scotsman', encryptPayload({id: 'scotsman', + denomination: "Flying Scotsman"})); + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler() + }); + createAndUploadKeypair(); + + let engine = makeSteamEngine(); + engine._store.items = {rekolok: "Rekonstruktionslokomotive"}; + try { + + // Confirm initial environment + do_check_eq(crypto_steam.payload, undefined); + do_check_eq(engine._tracker.changedIDs["rekolok"], undefined); + let metaGlobal = Records.get(engine.metaURL); + do_check_eq(metaGlobal.payload.engines, undefined); + do_check_true(!!collection.wbos.flying.payload); + do_check_true(!!collection.wbos.scotsman.payload); + + engine.lastSync = Date.now() / 1000; + engine._syncStartup(); + + // The meta/global WBO has been filled with data about the engine + let engineData = metaGlobal.payload.engines["steam"]; + do_check_eq(engineData.version, engine.version); + do_check_eq(engineData.syncID, engine.syncID); + + // Sync was reset and server data was wiped + do_check_eq(engine.lastSync, 0); + do_check_eq(collection.wbos.flying.payload, undefined); + do_check_eq(collection.wbos.scotsman.payload, undefined); + + // Bulk key was uploaded + do_check_true(!!crypto_steam.payload); + do_check_true(!!crypto_steam.data.keyring); + + // WBO IDs are added to tracker (they're all marked for uploading) + do_check_eq(engine._tracker.changedIDs["rekolok"], 0); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.syncID"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_syncStartup_serverHasNewerVersion() { + _("SyncEngine._syncStartup "); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let global = new ServerWBO('global', {engines: {steam: {version: 23456}}}); + let server = httpd_setup({ + "/1.0/foo/storage/meta/global": global.handler() + }); + + let engine = makeSteamEngine(); + try { + + // The server has a newer version of the data and our engine can + // handle. That should give us an exception. + let error; + try { + engine._syncStartup(); + } catch (ex) { + error = ex; + } + do_check_eq(error.failureCode, VERSION_OUT_OF_DATE); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Records.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_syncStartup_syncIDMismatchResetsClient() { + _("SyncEngine._syncStartup resets sync if syncIDs don't match"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let crypto_steam = new ServerWBO('steam'); + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler() + }); + + // global record with a different syncID than our engine has + let engine = makeSteamEngine(); + let global = new ServerWBO('global', + {engines: {steam: {version: engine.version, + syncID: 'foobar'}}}); + server.registerPathHandler("/1.0/foo/storage/meta/global", global.handler()); + + createAndUploadKeypair(); + + try { + + // Confirm initial environment + do_check_eq(engine.syncID, 'fake-guid-0'); + do_check_eq(crypto_steam.payload, undefined); + do_check_eq(engine._tracker.changedIDs["rekolok"], undefined); + + engine.lastSync = Date.now() / 1000; + engine._syncStartup(); + + // The engine has assumed the server's syncID + do_check_eq(engine.syncID, 'foobar'); + + // Sync was reset + do_check_eq(engine.lastSync, 0); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.syncID"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_syncStartup_badKeyWipesServerData() { + _("SyncEngine._syncStartup resets sync and wipes server data if there's something wrong with the symmetric key"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + + // A symmetric key with an incorrect HMAC + let crypto_steam = new ServerWBO('steam'); + crypto_steam.payload = JSON.stringify({ + keyring: { + "http://localhost:8080/1.0/foo/storage/keys/pubkey": { + wrapped: Svc.Crypto.generateRandomKey(), + hmac: "this-hmac-is-incorrect" + } + } + }); + + // A proper global record with matching version and syncID + let engine = makeSteamEngine(); + let global = new ServerWBO('global', + {engines: {steam: {version: engine.version, + syncID: engine.syncID}}}); + + // Some server side data that's going to be wiped + let collection = new ServerCollection(); + collection.wbos.flying = new ServerWBO( + 'flying', encryptPayload({id: 'flying', + denomination: "LNER Class A3 4472"})); + collection.wbos.scotsman = new ServerWBO( + 'scotsman', encryptPayload({id: 'scotsman', + denomination: "Flying Scotsman"})); + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler() + }); + createAndUploadKeypair(); + + try { + + // Confirm initial environment + let key = crypto_steam.data.keyring["http://localhost:8080/1.0/foo/storage/keys/pubkey"]; + do_check_eq(key.wrapped, "fake-symmetric-key-0"); + do_check_eq(key.hmac, "this-hmac-is-incorrect"); + do_check_true(!!collection.wbos.flying.payload); + do_check_true(!!collection.wbos.scotsman.payload); + + engine.lastSync = Date.now() / 1000; + engine._syncStartup(); + + // Sync was reset and server data was wiped + do_check_eq(engine.lastSync, 0); + do_check_eq(collection.wbos.flying.payload, undefined); + do_check_eq(collection.wbos.scotsman.payload, undefined); + + // New bulk key was uploaded + key = crypto_steam.data.keyring["http://localhost:8080/1.0/foo/storage/keys/pubkey"]; + do_check_eq(key.wrapped, "fake-symmetric-key-1"); + do_check_eq(key.hmac, "fake-symmetric-key-1 "); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.syncID"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_processIncoming_emptyServer() { + _("SyncEngine._processIncoming working with an empty server backend"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let crypto_steam = new ServerWBO('steam'); + let collection = new ServerCollection(); + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler() + }); + createAndUploadKeypair(); + + let engine = makeSteamEngine(); + try { + + // Merely ensure that this code path is run without any errors + engine._processIncoming(); + do_check_eq(engine.lastSync, 0); + do_check_eq(engine.toFetch.length, 0); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_processIncoming_createFromServer() { + _("SyncEngine._processIncoming creates new records from server data"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let crypto_steam = new ServerWBO('steam'); + + // Some server records that will be downloaded + let collection = new ServerCollection(); + collection.wbos.flying = new ServerWBO( + 'flying', encryptPayload({id: 'flying', + denomination: "LNER Class A3 4472"})); + collection.wbos.scotsman = new ServerWBO( + 'scotsman', encryptPayload({id: 'scotsman', + denomination: "Flying Scotsman"})); + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler(), + "/1.0/foo/storage/steam/flying": collection.wbos.flying.handler(), + "/1.0/foo/storage/steam/scotsman": collection.wbos.scotsman.handler() + }); + createAndUploadKeypair(); + createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam"); + + let engine = makeSteamEngine(); + try { + + // Confirm initial environment + do_check_eq(engine.lastSync, 0); + do_check_eq(engine.lastModified, null); + do_check_eq(engine._store.items.flying, undefined); + do_check_eq(engine._store.items.scotsman, undefined); + + engine._processIncoming(); + + // Timestamps of last sync and last server modification are set. + do_check_true(engine.lastSync > 0); + do_check_true(engine.lastModified > 0); + + // Local records have been created from the server data. + do_check_eq(engine._store.items.flying, "LNER Class A3 4472"); + do_check_eq(engine._store.items.scotsman, "Flying Scotsman"); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_processIncoming_reconcile() { + _("SyncEngine._processIncoming updates local records"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let crypto_steam = new ServerWBO('steam'); + let collection = new ServerCollection(); + + // This server record is newer than the corresponding client one, + // so it'll update its data. + collection.wbos.newrecord = new ServerWBO( + 'newrecord', encryptPayload({id: 'newrecord', + denomination: "New stuff..."})); + + // This server record is newer than the corresponding client one, + // so it'll update its data. + collection.wbos.newerserver = new ServerWBO( + 'newerserver', encryptPayload({id: 'newerserver', + denomination: "New data!"})); + + // This server record is 2 mins older than the client counterpart + // but identical to it, so we're expecting the client record's + // changedID to be reset. + collection.wbos.olderidentical = new ServerWBO( + 'olderidentical', encryptPayload({id: 'olderidentical', + denomination: "Older but identical"})); + collection.wbos.olderidentical.modified -= 120; + + // This item simply has different data than the corresponding client + // record (which is unmodified), so it will update the client as well + collection.wbos.updateclient = new ServerWBO( + 'updateclient', encryptPayload({id: 'updateclient', + denomination: "Get this!"})); + + // This is a dupe of 'original' but with a longer GUID, so we're + // expecting it to be marked for deletion from the server + collection.wbos.duplication = new ServerWBO( + 'duplication', encryptPayload({id: 'duplication', + denomination: "Original Entry"})); + + // This is a dupe of 'long_original' but with a shorter GUID, so we're + // expecting it to replace 'long_original'. + collection.wbos.dupe = new ServerWBO( + 'dupe', encryptPayload({id: 'dupe', + denomination: "Long Original Entry"})); + + // This record is marked as deleted, so we're expecting the client + // record to be removed. + collection.wbos.nukeme = new ServerWBO( + 'nukeme', encryptPayload({id: 'nukeme', + denomination: "Nuke me!", + deleted: true})); + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler() + }); + createAndUploadKeypair(); + createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam"); + + let engine = makeSteamEngine(); + engine._store.items = {newerserver: "New data, but not as new as server!", + olderidentical: "Older but identical", + updateclient: "Got data?", + original: "Original Entry", + long_original: "Long Original Entry", + nukeme: "Nuke me!"}; + // Make this record 1 min old, thus older than the one on the server + engine._tracker.addChangedID('newerserver', Date.now()/1000 - 60); + // This record has been changed 2 mins later than the one on the server + engine._tracker.addChangedID('olderidentical', Date.now()/1000); + + try { + + // Confirm initial environment + do_check_eq(engine._store.items.newrecord, undefined); + do_check_eq(engine._store.items.newerserver, "New data, but not as new as server!"); + do_check_eq(engine._store.items.olderidentical, "Older but identical"); + do_check_eq(engine._store.items.updateclient, "Got data?"); + do_check_eq(engine._store.items.nukeme, "Nuke me!"); + do_check_true(engine._tracker.changedIDs['olderidentical'] > 0); + + engine._delete = {}; // normally set up by _syncStartup + engine._processIncoming(); + + // Timestamps of last sync and last server modification are set. + do_check_true(engine.lastSync > 0); + do_check_true(engine.lastModified > 0); + + // The new record is created. + do_check_eq(engine._store.items.newrecord, "New stuff..."); + + // The 'newerserver' record is updated since the server data is newer. + do_check_eq(engine._store.items.newerserver, "New data!"); + + // The data for 'olderidentical' is identical on the server, so + // it's no longer marked as changed anymore. + do_check_eq(engine._store.items.olderidentical, "Older but identical"); + do_check_eq(engine._tracker.changedIDs['olderidentical'], undefined); + + // Updated with server data. + do_check_eq(engine._store.items.updateclient, "Get this!"); + + // The dupe with the shorter ID is kept, the longer one is slated + // for deletion. + do_check_eq(engine._store.items.long_original, undefined); + do_check_eq(engine._store.items.dupe, "Long Original Entry"); + do_check_neq(engine._delete.ids.indexOf('duplication'), -1); + + // The 'nukeme' record marked as deleted is removed. + do_check_eq(engine._store.items.nukeme, undefined); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_processIncoming_fetchNum() { + _("SyncEngine._processIncoming doesn't fetch everything at ones on mobile clients"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + Svc.Prefs.set("client.type", "mobile"); + let crypto_steam = new ServerWBO('steam'); + let collection = new ServerCollection(); + + // Let's create some 1000 server side records. They're all at least + // 10 minutes old. + for (var i=0; i < 1234; i++) { + let id = 'record-no-' + i; + let payload = encryptPayload({id: id, denomination: "Record No. " + i}); + let wbo = new ServerWBO(id, payload); + wbo.modified = Date.now()/1000 - 60*(i+10); + collection.wbos[id] = wbo; + } + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler() + }); + createAndUploadKeypair(); + createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam"); + + let engine = makeSteamEngine(); + + try { + + // On a mobile client, the first sync will only get the first 50 + // objects from the server + engine._processIncoming(); + do_check_eq([id for (id in engine._store.items)].length, 50); + do_check_true('record-no-0' in engine._store.items); + do_check_true('record-no-49' in engine._store.items); + do_check_eq(engine.toFetch.length, 1234-50); + + + // The next sync will get another 50 objects, assuming the server + // hasn't got any new data. + engine._processIncoming(); + do_check_eq([id for (id in engine._store.items)].length, 100); + do_check_true('record-no-50' in engine._store.items); + do_check_true('record-no-99' in engine._store.items); + do_check_eq(engine.toFetch.length, 1234-100); + + + // Now let's say there are some new items on the server + for (i=0; i < 5; i++) { + let id = 'new-record-no-' + i; + let payload = encryptPayload({id: id, denomination: "New record No. " + i}); + let wbo = new ServerWBO(id, payload); + wbo.modified = Date.now()/1000 - 60*i; + collection.wbos[id] = wbo; + } + // Let's tell the engine the server has got newer data. This is + // normally done by the WeaveSvc after retrieving info/collections. + engine.lastModified = Date.now() / 1000 + 1; + + // Now we'll fetch another 50 items, but 5 of those are the new + // ones, so we've only fetched another 45 of the older ones. + engine._processIncoming(); + do_check_eq([id for (id in engine._store.items)].length, 150); + do_check_true('new-record-no-0' in engine._store.items); + do_check_true('new-record-no-4' in engine._store.items); + do_check_true('record-no-100' in engine._store.items); + do_check_true('record-no-144' in engine._store.items); + do_check_eq(engine.toFetch.length, 1234-100-45); + + + // Now let's modify a few existing records on the server so that + // they have to be refetched. + collection.wbos['record-no-3'].modified = Date.now()/1000 + 1; + collection.wbos['record-no-41'].modified = Date.now()/1000 + 1; + collection.wbos['record-no-122'].modified = Date.now()/1000 + 1; + + // Once again we'll tell the engine that the server's got newer data + // and once again we'll fetch 50 items, but 3 of those are the + // existing records, so we're only fetching 47 new ones. + engine.lastModified = Date.now() / 1000 + 2; + engine._processIncoming(); + do_check_eq([id for (id in engine._store.items)].length, 197); + do_check_true('record-no-145' in engine._store.items); + do_check_true('record-no-191' in engine._store.items); + do_check_eq(engine.toFetch.length, 1234-100-45-47); + + + // Finally let's fetch the rest, making sure that will fetch + // everything up to the last record. + while(engine.toFetch.length) { + engine._processIncoming(); + } + do_check_eq([id for (id in engine._store.items)].length, 1234+5); + do_check_true('record-no-1233' in engine._store.items); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("client.type"); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_uploadOutgoing_toEmptyServer() { + _("SyncEngine._uploadOutgoing uploads new records to server"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let crypto_steam = new ServerWBO('steam'); + let collection = new ServerCollection(); + collection.wbos.flying = new ServerWBO('flying'); + collection.wbos.scotsman = new ServerWBO('scotsman'); + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler(), + "/1.0/foo/storage/steam/flying": collection.wbos.flying.handler(), + "/1.0/foo/storage/steam/scotsman": collection.wbos.scotsman.handler() + }); + createAndUploadKeypair(); + createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam"); + + let engine = makeSteamEngine(); + engine._store.items = {flying: "LNER Class A3 4472", + scotsman: "Flying Scotsman"}; + // Mark one of these records as changed + engine._tracker.addChangedID('scotsman', 0); + + try { + + // Confirm initial environment + do_check_eq(collection.wbos.flying.payload, undefined); + do_check_eq(collection.wbos.scotsman.payload, undefined); + do_check_eq(engine._tracker.changedIDs['scotsman'], 0); + + engine._uploadOutgoing(); + + // Ensure the marked record ('scotsman') has been uploaded and is + // no longer marked. + do_check_eq(collection.wbos.flying.payload, undefined); + do_check_true(!!collection.wbos.scotsman.payload); + do_check_eq(JSON.parse(collection.wbos.scotsman.data.ciphertext).id, + 'scotsman'); + do_check_eq(engine._tracker.changedIDs['scotsman'], undefined); + + // The 'flying' record wasn't marked so it wasn't uploaded + do_check_eq(collection.wbos.flying.payload, undefined); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_uploadOutgoing_MAX_UPLOAD_RECORDS() { + _("SyncEngine._uploadOutgoing uploads in batches of MAX_UPLOAD_RECORDS"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let crypto_steam = new ServerWBO('steam'); + let collection = new ServerCollection(); + + // Let's count how many times the client posts to the server + var noOfUploads = 0; + collection.post = (function(orig) { + return function() { + noOfUploads++; + return orig.apply(this, arguments); + }; + }(collection.post)); + + // Create a bunch of records (and server side handlers) + let engine = makeSteamEngine(); + for (var i=0; i < 2345; i++) { + let id = 'record-no-' + i; + engine._store.items[id] = "Record No. " + i; + engine._tracker.addChangedID(id, 0); + collection.wbos[id] = new ServerWBO(id); + } + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler() + }); + createAndUploadKeypair(); + createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam"); + + try { + + // Confirm initial environment + do_check_eq(noOfUploads, 0); + + engine._uploadOutgoing(); + + // Ensure all records have been uploaded + for (i=0; i < 2345; i++) { + do_check_true(!!collection.wbos['record-no-'+i].payload); + } + + // Ensure that the uploads were performed in batches of MAX_UPLOAD_RECORDS + do_check_eq(noOfUploads, Math.ceil(2345/MAX_UPLOAD_RECORDS)); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Svc.Prefs.reset("steam.lastSync"); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_syncFinish_noDelete() { + _("SyncEngine._syncFinish resets tracker's score"); + let engine = makeSteamEngine(); + engine._delete = {}; // Nothing to delete + engine._tracker.score = 100; + + // _syncFinish() will reset the engine's score. + engine._syncFinish(); + do_check_eq(engine.score, 0); +} + + +function test_syncFinish_deleteByIds() { + _("SyncEngine._syncFinish deletes server records slated for deletion (list of record IDs)."); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let collection = new ServerCollection(); + collection.wbos.flying = new ServerWBO( + 'flying', encryptPayload({id: 'flying', + denomination: "LNER Class A3 4472"})); + collection.wbos.scotsman = new ServerWBO( + 'scotsman', encryptPayload({id: 'scotsman', + denomination: "Flying Scotsman"})); + collection.wbos.rekolok = new ServerWBO( + 'rekolok', encryptPayload({id: 'rekolok', + denomination: "Rekonstruktionslokomotive"})); + + let server = httpd_setup({ + "/1.0/foo/storage/steam": collection.handler() + }); + + let engine = makeSteamEngine(); + try { + engine._delete = {ids: ['flying', 'rekolok']}; + engine._syncFinish(); + + // The 'flying' and 'rekolok' records were deleted while the + // 'scotsman' one wasn't. + do_check_eq(collection.wbos.flying.payload, undefined); + do_check_true(!!collection.wbos.scotsman.payload); + do_check_eq(collection.wbos.rekolok.payload, undefined); + + // The deletion todo list has been reset. + do_check_eq(engine._delete.ids, undefined); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Records.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + + +function test_syncFinish_deleteLotsInBatches() { + _("SyncEngine._syncFinish deletes server records in batches of 100 (list of record IDs)."); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let collection = new ServerCollection(); + + // Let's count how many times the client does a DELETE request to the server + var noOfUploads = 0; + collection.delete = (function(orig) { + return function() { + noOfUploads++; + return orig.apply(this, arguments); + }; + }(collection.delete)); + + // Create a bunch of records on the server + for (var i=0; i < 2345; i++) { + let id = 'record-no-' + i; + let payload = encryptPayload({id: id, denomination: "Record No. " + i}); + let wbo = new ServerWBO(id, payload); + wbo.modified = Date.now()/1000 - 60*(i+10); + collection.wbos[id] = wbo; + } + + let server = httpd_setup({ + "/1.0/foo/storage/steam": collection.handler() + }); + + let engine = makeSteamEngine(); + try { + + // Confirm initial environment + do_check_eq(noOfUploads, 0); + + // Declare what we want to have deleted: all records no. 200 and + // up and all records that are less than 200 mins old (which are + // records 0 thru 190). + engine._delete = {ids: [], + newer: Date.now()/1000 - 60*200.5}; + for (i=200; i < 2345; i++) { + engine._delete.ids.push('record-no-' + i); + } + + engine._syncFinish(); + + // Ensure that the appropriate server data has been wiped while + // preserving records 190 thru 200. + for (i=0; i < 2345; i++) { + let id = 'record-no-' + i; + if (i<=190 || i>=200) { + do_check_eq(collection.wbos[id].payload, undefined); + } else { + do_check_true(!!collection.wbos[id].payload); + } + } + + // The deletion was done in batches + do_check_eq(noOfUploads, 22+1); + + // The deletion todo list has been reset. + do_check_eq(engine._delete.ids, undefined); + + } finally { + server.stop(function() {}); + Svc.Prefs.reset("clusterURL"); + Records.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} From fa67851500a4a0c369c5559badf4c9387e60e61e Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Tue, 1 Jun 2010 15:12:25 -0700 Subject: [PATCH 1698/1860] Bug 557590 - code audit and create unit test plan for status.js [r=mconnor] Initialize status values in status.js + tests for status.js. --- services/sync/modules/service.js | 3 - services/sync/modules/status.js | 3 + services/sync/tests/unit/test_status.js | 91 +++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 services/sync/tests/unit/test_status.js diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 5895d2b14767..3a4ce60aa623 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -260,7 +260,6 @@ WeaveSvc.prototype = { this._initLogs(); this._log.info("Loading Weave " + WEAVE_VERSION); - Status.service = STATUS_OK; this.enabled = true; this._registerEngines(); @@ -333,8 +332,6 @@ WeaveSvc.prototype = { this._log.debug("checkSetup: no passphrase set"); Status.login = LOGIN_FAILED_NO_PASSPHRASE; } - else - Status.service = STATUS_OK; return Status.service; }, diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js index e59128dc4c84..2312e1bc07f4 100644 --- a/services/sync/modules/status.js +++ b/services/sync/modules/status.js @@ -76,6 +76,9 @@ let Status = { this.minimumNextSync = 0; }, resetSync: function resetSync() { + this.service = STATUS_OK; + this._login = LOGIN_SUCCEEDED; + this._sync = SYNC_SUCCEEDED; this._engines = {}; this.partial = false; }, diff --git a/services/sync/tests/unit/test_status.js b/services/sync/tests/unit/test_status.js new file mode 100644 index 000000000000..8beadc76b7a9 --- /dev/null +++ b/services/sync/tests/unit/test_status.js @@ -0,0 +1,91 @@ +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/status.js"); + +function run_test() { + + // Check initial states + do_check_false(Status.enforceBackoff); + do_check_eq(Status.backoffInterval, 0); + do_check_eq(Status.minimumNextSync, 0); + + do_check_eq(Status.service, STATUS_OK); + do_check_eq(Status.sync, SYNC_SUCCEEDED); + do_check_eq(Status.login, LOGIN_SUCCEEDED); + for (let name in Status.engines) { + do_throw('Status.engines should be empty.'); + } + do_check_eq(Status.partial, false); + + + // Check login status + for each (let code in [LOGIN_FAILED_NO_USERNAME, + LOGIN_FAILED_NO_PASSWORD, + LOGIN_FAILED_NO_PASSPHRASE]) { + Status.login = code; + do_check_eq(Status.login, code); + do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); + Status.resetSync(); + } + + Status.login = LOGIN_FAILED; + do_check_eq(Status.login, LOGIN_FAILED); + do_check_eq(Status.service, LOGIN_FAILED); + Status.resetSync(); + + Status.login = LOGIN_SUCCEEDED; + do_check_eq(Status.login, LOGIN_SUCCEEDED); + do_check_eq(Status.service, STATUS_OK); + Status.resetSync(); + + + // Check sync status + Status.sync = SYNC_FAILED; + do_check_eq(Status.sync, SYNC_FAILED); + do_check_eq(Status.service, SYNC_FAILED); + + Status.sync = SYNC_SUCCEEDED; + do_check_eq(Status.sync, SYNC_SUCCEEDED); + do_check_eq(Status.service, STATUS_OK); + + Status.resetSync(); + + + // Check engine status + Status.engines = ["testEng1", ENGINE_SUCCEEDED]; + do_check_eq(Status.engines["testEng1"], ENGINE_SUCCEEDED); + do_check_eq(Status.service, STATUS_OK); + + Status.engines = ["testEng2", ENGINE_DOWNLOAD_FAIL]; + do_check_eq(Status.engines["testEng1"], ENGINE_SUCCEEDED); + do_check_eq(Status.engines["testEng2"], ENGINE_DOWNLOAD_FAIL); + do_check_eq(Status.service, SYNC_FAILED_PARTIAL); + + Status.engines = ["testEng3", ENGINE_SUCCEEDED]; + do_check_eq(Status.engines["testEng1"], ENGINE_SUCCEEDED); + do_check_eq(Status.engines["testEng2"], ENGINE_DOWNLOAD_FAIL); + do_check_eq(Status.engines["testEng3"], ENGINE_SUCCEEDED); + do_check_eq(Status.service, SYNC_FAILED_PARTIAL); + + + // Check resetSync + Status.sync = SYNC_FAILED; + Status.resetSync(); + + do_check_eq(Status.service, STATUS_OK); + do_check_eq(Status.sync, SYNC_SUCCEEDED); + for (name in Status.engines) { + do_throw('Status.engines should be empty.'); + } + + + // Check resetBackoff + Status.enforceBackoff = true; + Status.backOffInterval = 4815162342; + Status.backOffInterval = 42; + Status.resetBackoff(); + + do_check_false(Status.enforceBackoff); + do_check_eq(Status.backoffInterval, 0); + do_check_eq(Status.minimumNextSync, 0); + +} From d33e73e4f6170f9e6bc657f5cfbbfba27b753097 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Tue, 1 Jun 2010 15:15:53 -0700 Subject: [PATCH 1699/1860] Bug 557596 - code audit and create unit test plan for resource.js [r=mconnor] Lots of resource tests, Resource.serverTime initialized to null, Resource.headers normalized to lowercase. --- services/sync/modules/engines.js | 2 +- services/sync/modules/resource.js | 33 ++- services/sync/modules/service.js | 4 +- services/sync/tests/unit/head_http_server.js | 15 + services/sync/tests/unit/test_resource.js | 284 +++++++++++++++++-- 5 files changed, 303 insertions(+), 35 deletions(-) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index b64a877f7994..23b600f4388c 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -660,7 +660,7 @@ SyncEngine.prototype = { } // Record the modified time of the upload - let modified = resp.headers["X-Weave-Timestamp"]; + let modified = resp.headers["x-weave-timestamp"]; if (modified > this.lastSync) this.lastSync = modified; diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index b523bd448cab..f07ca11254c6 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -58,11 +58,16 @@ function Resource(uri) { this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; this.uri = uri; - this._headers = {'Content-type': 'text/plain'}; + this._headers = {}; } Resource.prototype = { _logName: "Net.Resource", + // ** {{{ Resource.serverTime }}} ** + // + // Caches the latest server timestamp (X-Weave-Timestamp header). + serverTime: null, + // ** {{{ Resource.authenticator }}} ** // // Getter and setter for the authenticator module @@ -81,9 +86,9 @@ Resource.prototype = { // ** {{{ Resource.headers }}} ** // - // Getter for access to received headers after the request - // for the resource has been made, setter for headers to be included - // while making a request for the resource. + // Headers to be included when making a request for the resource. + // Note: Header names should be all lower case, there's no explicit + // check for duplicates due to case! get headers() { return this.authenticator.onRequest(this._headers); }, @@ -94,7 +99,7 @@ Resource.prototype = { if (arguments.length % 2) throw "setHeader only accepts arguments in multiples of 2"; for (let i = 0; i < arguments.length; i += 2) { - this._headers[arguments[i]] = arguments[i + 1]; + this._headers[arguments[i].toLowerCase()] = arguments[i + 1]; } }, @@ -144,7 +149,7 @@ Resource.prototype = { channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; // Setup a callback to handle bad HTTPS certificates. - channel.notificationCallbacks = new badCertListener(); + channel.notificationCallbacks = new BadCertListener(); // Avoid calling the authorizer more than once let headers = this.headers; @@ -182,8 +187,8 @@ Resource.prototype = { this._log.debug(action + " Length: " + this._data.length); this._log.trace(action + " Body: " + this._data); - let type = ('Content-Type' in this._headers)? - this._headers['Content-Type'] : 'text/plain'; + let type = ('content-type' in this._headers) ? + this._headers['content-type'] : 'text/plain'; let stream = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); @@ -227,7 +232,7 @@ Resource.prototype = { // Read out the response headers if available channel.visitResponseHeaders({ visitHeader: function visitHeader(header, value) { - headers[header] = value; + headers[header.toLowerCase()] = value; } }); status = channel.responseStatus; @@ -244,8 +249,8 @@ Resource.prototype = { this._log.trace(action + " body: " + this._data); // this is a server-side safety valve to allow slowing down clients without hurting performance - if (headers["X-Weave-Backoff"]) - Observers.notify("weave:service:backoff:interval", parseInt(headers["X-Weave-Backoff"], 10)) + if (headers["x-weave-backoff"]) + Observers.notify("weave:service:backoff:interval", parseInt(headers["x-weave-backoff"], 10)) } // Got a response but no header; must be cached (use default values) catch(ex) { @@ -360,16 +365,16 @@ ChannelListener.prototype = { } }; -// = badCertListener = +// = BadCertListener = // // We use this listener to ignore bad HTTPS // certificates and continue a request on a network // channel. Probably not a very smart thing to do, // but greatly simplifies debugging and is just very // convenient. -function badCertListener() { +function BadCertListener() { } -badCertListener.prototype = { +BadCertListener.prototype = { getInterface: function(aIID) { return this.QueryInterface(aIID); }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3a4ce60aa623..2cdbadce7035 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1370,8 +1370,8 @@ WeaveSvc.prototype = { _checkServerError: function WeaveSvc__checkServerError(resp) { if (Utils.checkStatus(resp.status, null, [500, [502, 504]])) { Status.enforceBackoff = true; - if (resp.status == 503 && resp.headers["Retry-After"]) - Observers.notify("weave:service:backoff:interval", parseInt(resp.headers["Retry-After"], 10)); + if (resp.status == 503 && resp.headers["retry-after"]) + Observers.notify("weave:service:backoff:interval", parseInt(resp.headers["retry-after"], 10)); } }, /** diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js index 9a1a0eab3dfd..af777e8d9560 100644 --- a/services/sync/tests/unit/head_http_server.js +++ b/services/sync/tests/unit/head_http_server.js @@ -24,6 +24,21 @@ function httpd_basic_auth_handler(body, metadata, response) { response.bodyOutputStream.write(body, body.length); } +/* + * Read bytes string from an nsIInputStream. If 'count' is omitted, + * all available input is read. + */ +function readBytesFromInputStream(inputStream, count) { + var BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + if (!count) { + count = inputStream.available(); + } + return new BinaryInputStream(inputStream).readBytes(count); +} + /* diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index 8218070e69a6..0fb33d80c797 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -3,14 +3,21 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/resource.js"); Cu.import("resource://weave/auth.js"); Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/ext/Observers.js"); let logger; let Httpd = {}; Cu.import("resource://harness/modules/httpd.js", Httpd); function server_open(metadata, response) { - let body = "This path exists"; - response.setStatusLine(metadata.httpVersion, 200, "OK"); + let body; + if (metadata.method == "GET") { + body = "This path exists"; + response.setStatusLine(metadata.httpVersion, 200, "OK"); + } else { + body = "Wrong request method"; + response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed"); + } response.bodyOutputStream.write(body, body.length); } @@ -23,7 +30,6 @@ function server_protected(metadata, response) { body = "This path exists and is protected"; response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); - } else { body = "This path exists and is protected - failed"; response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); @@ -39,6 +45,88 @@ function server_404(metadata, response) { response.bodyOutputStream.write(body, body.length); } + +let sample_data = { + some: "sample_data", + injson: "format", + number: 42 +}; + +function server_upload(metadata, response) { + let body; + + let input = readBytesFromInputStream(metadata.bodyInputStream); + if (input == JSON.stringify(sample_data)) { + body = "Valid data upload via " + metadata.method; + response.setStatusLine(metadata.httpVersion, 200, "OK"); + } else { + body = "Invalid data upload via " + metadata.method + ': ' + input; + response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error"); + } + + response.bodyOutputStream.write(body, body.length); +} + +function server_delete(metadata, response) { + let body; + if (metadata.method == "DELETE") { + body = "This resource has been deleted"; + response.setStatusLine(metadata.httpVersion, 200, "OK"); + } else { + body = "Wrong request method"; + response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed"); + } + response.bodyOutputStream.write(body, body.length); +} + +function server_json(metadata, response) { + let body = JSON.stringify(sample_data); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +const TIMESTAMP = 1274380461; + +function server_timestamp(metadata, response) { + let body = "Thank you for your request"; + response.setHeader("X-Weave-Timestamp", ''+TIMESTAMP, false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function server_backoff(metadata, response) { + let body = "Hey, back off!"; + response.setHeader("X-Weave-Backoff", '600', false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + + +function server_headers(metadata, response) { + let ignore_headers = ["host", "user-agent", "accept", "accept-language", + "accept-encoding", "accept-charset", "keep-alive", + "connection", "pragma", "cache-control", + "content-length"]; + let headers = metadata.headers; + let header_names = []; + while (headers.hasMoreElements()) { + let header = headers.getNext().toString(); + if (ignore_headers.indexOf(header) == -1) { + header_names.push(header); + } + } + header_names = header_names.sort(); + + headers = {}; + for each (let header in header_names) { + headers[header] = metadata.getHeader(header); + } + let body = JSON.stringify(headers); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + + function run_test() { logger = Log4Moz.repository.getLogger('Test'); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); @@ -47,52 +135,212 @@ function run_test() { server.registerPathHandler("/open", server_open); server.registerPathHandler("/protected", server_protected); server.registerPathHandler("/404", server_404); + server.registerPathHandler("/upload", server_upload); + server.registerPathHandler("/delete", server_delete); + server.registerPathHandler("/json", server_json); + server.registerPathHandler("/timestamp", server_timestamp); + server.registerPathHandler("/headers", server_headers); + server.registerPathHandler("/backoff", server_backoff); server.start(8080); Utils.prefs.setIntPref("network.numRetries", 1); // speed up test - // 1. A regular non-password-protected resource - + _("Resource object memebers"); let res = new Resource("http://localhost:8080/open"); + do_check_true(res.uri instanceof Ci.nsIURI); + do_check_eq(res.uri.spec, "http://localhost:8080/open"); + do_check_eq(res.spec, "http://localhost:8080/open"); + do_check_eq(typeof res.headers, "object"); + do_check_eq(typeof res.authenticator, "object"); + // Initially res.data is null since we haven't performed a GET or + // PUT/POST request yet. + do_check_eq(res.data, null); + + _("GET a non-password-protected resource"); let content = res.get(); do_check_eq(content, "This path exists"); do_check_eq(content.status, 200); do_check_true(content.success); + // res.data has been updated with the result from the request + do_check_eq(res.data, content); - // 2. A password protected resource (test that it'll fail w/o pass, no throw) + // Since we didn't receive proper JSON data, accessing content.obj + // will result in a SyntaxError from JSON.parse + let didThrow = false; + try { + content.obj; + } catch (ex) { + didThrow = true; + } + do_check_true(didThrow); + + _("GET a password protected resource (test that it'll fail w/o pass, no throw)"); let res2 = new Resource("http://localhost:8080/protected"); content = res2.get(); do_check_eq(content, "This path exists and is protected - failed") do_check_eq(content.status, 401); do_check_false(content.success); - // 3. A password protected resource - + _("GET a password protected resource"); let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest")); let res3 = new Resource("http://localhost:8080/protected"); res3.authenticator = auth; + do_check_eq(res3.authenticator, auth); content = res3.get(); do_check_eq(content, "This path exists and is protected"); do_check_eq(content.status, 200); do_check_true(content.success); - // 4. A non-existent resource (test that it'll fail, but not throw) - + _("GET a non-existent resource (test that it'll fail, but not throw)"); let res4 = new Resource("http://localhost:8080/404"); content = res4.get(); do_check_eq(content, "File not found"); do_check_eq(content.status, 404); do_check_false(content.success); - _("5. Check some headers of the 404 response"); - do_check_eq(content.headers.Connection, "close"); - do_check_eq(content.headers.Server, "httpd.js"); - do_check_eq(content.headers["Content-Length"], 14); + // Check some headers of the 404 response + do_check_eq(content.headers.connection, "close"); + do_check_eq(content.headers.server, "httpd.js"); + do_check_eq(content.headers["content-length"], 14); - // FIXME: additional coverage needed: - // * PUT requests - // * DELETE requests - // * JsonFilter + _("PUT to a resource (string)"); + let res5 = new Resource("http://localhost:8080/upload"); + content = res5.put(JSON.stringify(sample_data)); + do_check_eq(content, "Valid data upload via PUT"); + do_check_eq(content.status, 200); + do_check_eq(res5.data, content); + + _("PUT to a resource (object)"); + content = res5.put(sample_data); + do_check_eq(content, "Valid data upload via PUT"); + do_check_eq(content.status, 200); + do_check_eq(res5.data, content); + + _("PUT without data arg (uses resource.data) (string)"); + res5.data = JSON.stringify(sample_data); + content = res5.put(); + do_check_eq(content, "Valid data upload via PUT"); + do_check_eq(content.status, 200); + do_check_eq(res5.data, content); + + _("PUT without data arg (uses resource.data) (object)"); + res5.data = sample_data; + content = res5.put(); + do_check_eq(content, "Valid data upload via PUT"); + do_check_eq(content.status, 200); + do_check_eq(res5.data, content); + + _("POST to a resource (string)"); + content = res5.post(JSON.stringify(sample_data)); + do_check_eq(content, "Valid data upload via POST"); + do_check_eq(content.status, 200); + do_check_eq(res5.data, content); + + _("POST to a resource (object)"); + content = res5.post(sample_data); + do_check_eq(content, "Valid data upload via POST"); + do_check_eq(content.status, 200); + do_check_eq(res5.data, content); + + _("POST without data arg (uses resource.data) (string)"); + res5.data = JSON.stringify(sample_data); + content = res5.post(); + do_check_eq(content, "Valid data upload via POST"); + do_check_eq(content.status, 200); + do_check_eq(res5.data, content); + + _("POST without data arg (uses resource.data) (object)"); + res5.data = sample_data; + content = res5.post(); + do_check_eq(content, "Valid data upload via POST"); + do_check_eq(content.status, 200); + do_check_eq(res5.data, content); + + _("DELETE a resource"); + let res6 = new Resource("http://localhost:8080/delete"); + content = res6.delete(); + do_check_eq(content, "This resource has been deleted") + do_check_eq(content.status, 200); + + _("JSON conversion of response body"); + let res7 = new Resource("http://localhost:8080/json"); + content = res7.get(); + do_check_eq(content, JSON.stringify(sample_data)); + do_check_eq(content.status, 200); + do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data)); + + _("X-Weave-Timestamp header updates Resource.serverTime"); + // Before having received any response containing the + // X-Weave-Timestamp header, Resource.serverTime is null. + do_check_eq(Resource.serverTime, null); + let res8 = new Resource("http://localhost:8080/timestamp"); + content = res8.get(); + do_check_eq(Resource.serverTime, TIMESTAMP); + + + _("GET: no special request headers"); + let res9 = new Resource("http://localhost:8080/headers"); + content = res9.get(); + do_check_eq(content, '{}'); + + _("PUT: Content-Type defaults to text/plain"); + content = res9.put('data'); + do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); + + _("POST: Content-Type defaults to text/plain"); + content = res9.post('data'); + do_check_eq(content, JSON.stringify({"content-type": "text/plain"})); + + _("setHeader(): setting simple header"); + res9.setHeader('X-What-Is-Weave', 'awesome'); + do_check_eq(res9.headers['x-what-is-weave'], 'awesome'); + content = res9.get(); + do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"})); + + _("setHeader(): setting multiple headers, overwriting existing header"); + res9.setHeader('X-WHAT-is-Weave', 'more awesomer', + 'X-Another-Header', 'hello world'); + do_check_eq(res9.headers['x-what-is-weave'], 'more awesomer'); + do_check_eq(res9.headers['x-another-header'], 'hello world'); + content = res9.get(); + do_check_eq(content, JSON.stringify({"x-another-header": "hello world", + "x-what-is-weave": "more awesomer"})); + + _("Setting headers object"); + res9.headers = {}; + content = res9.get(); + do_check_eq(content, "{}"); + + _("PUT/POST: override default Content-Type"); + res9.setHeader('Content-Type', 'application/foobar'); + do_check_eq(res9.headers['content-type'], 'application/foobar'); + content = res9.put('data'); + do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); + content = res9.post('data'); + do_check_eq(content, JSON.stringify({"content-type": "application/foobar"})); + + + _("X-Weave-Backoff header notifies observer"); + let backoffInterval; + function onBackoff(subject, data) { + backoffInterval = subject; + } + Observers.add("weave:service:backoff:interval", onBackoff); + + let res10 = new Resource("http://localhost:8080/backoff"); + content = res10.get(); + do_check_eq(backoffInterval, 600); + + _("Error handling in _request() preserves exception information"); + let error; + let res11 = new Resource("http://localhost:12345/does/not/exist"); + try { + content = res11.get(); + } catch(ex) { + error = ex; + } + do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED"); + do_check_eq(typeof error.stack, "string"); server.stop(); } From b7df9c8825f394526eb73060a31263f0684c7c8f Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Tue, 1 Jun 2010 15:25:23 -0700 Subject: [PATCH 1700/1860] Bug 566575 - Some tests FAIL on first run and PASS or hang on subsequent runs [r=mconnor] Update to httpd.js from mozilla-central. server.stop() now expects a callback parameter, so pass a no-op where it's used. --- services/sync/tests/unit/test_auth_manager.js | 2 +- services/sync/tests/unit/test_records_crypto.js | 2 +- services/sync/tests/unit/test_records_keys.js | 2 +- services/sync/tests/unit/test_records_wbo.js | 2 +- services/sync/tests/unit/test_resource.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js index 170cc6260523..d6dde51cda42 100644 --- a/services/sync/tests/unit/test_auth_manager.js +++ b/services/sync/tests/unit/test_auth_manager.js @@ -43,5 +43,5 @@ function run_test() { do_check_eq(content, "This path exists and is protected"); do_check_eq(content.status, 200); - server.stop(); + server.stop(function() {}); } diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index 35636571206e..b34ff9ae7a3b 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -124,5 +124,5 @@ function run_test() { log.info("Done!"); } - finally { server.stop(); } + finally { server.stop(function() {}); } } diff --git a/services/sync/tests/unit/test_records_keys.js b/services/sync/tests/unit/test_records_keys.js index 90b56ca35c9f..5d713b1dc327 100644 --- a/services/sync/tests/unit/test_records_keys.js +++ b/services/sync/tests/unit/test_records_keys.js @@ -54,5 +54,5 @@ function run_test() { log.info("Done!"); } catch (e) { do_throw(e); } - finally { server.stop(); } + finally { server.stop(function() {}); } } diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index 994a60ffe4c9..782f9b0a5b66 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -74,5 +74,5 @@ function run_test() { log.info("Done!"); } catch (e) { do_throw(e); } - finally { server.stop(); } + finally { server.stop(function() {}); } } diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index 0fb33d80c797..66093e84e590 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -342,5 +342,5 @@ function run_test() { do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED"); do_check_eq(typeof error.stack, "string"); - server.stop(); + server.stop(function() {}); } From 2e3d8b25742b42e178ef6dbffb31414974cb35c3 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 13 May 2010 11:44:19 -0700 Subject: [PATCH 1701/1860] Bug 565411 - Sync 5000 most frecent pages instead of 1000 recent for first sync [r=mconnor] Use an async sql query to get uris by frecency instead of query result nodes, and wrap it with sync/async to keep the sync interface without blocking UI. Rough timings of getAllIDs: old 1k = 1.6sec blocking; old 5k = 2.7sec blocking; new 1k = .2sec non-block; new 5k = .6sec non-block. --- services/sync/modules/engines/history.js | 50 +++++++++++++++--------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 39e997191aec..5d45918a6055 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -135,6 +135,18 @@ HistoryStore.prototype = { return stm; }, + get _allUrlStm() { + this._log.trace("Creating SQL statement: _allUrlStm"); + let stm = this._db.createStatement( + "SELECT url " + + "FROM moz_places " + + "WHERE last_visit_date > :cutoff_date " + + "ORDER BY frecency DESC " + + "LIMIT 5000"); + this.__defineGetter__("_allUrlStm", function() stm); + return stm; + }, + // See bug 320831 for why we use SQL here _getVisits: function HistStore__getVisits(uri) { this._visitStm.params.url = uri; @@ -188,25 +200,25 @@ HistoryStore.prototype = { getAllIDs: function HistStore_getAllIDs() { - let query = this._hsvc.getNewQuery(), - options = this._hsvc.getNewQueryOptions(); - - query.minVisits = 1; - options.maxResults = 1000; - options.sortingMode = options.SORT_BY_DATE_DESCENDING; - options.queryType = options.QUERY_TYPE_HISTORY; - - let root = this._hsvc.executeQuery(query, options).root; - root.QueryInterface(Ci.nsINavHistoryQueryResultNode); - root.containerOpen = true; - - let items = {}; - for (let i = 0; i < root.childCount; i++) { - let item = root.getChild(i); - let guid = GUIDForUri(item.uri, true); - items[guid] = item.uri; - } - return items; + // Only get places visited within the last 30 days (30*24*60*60*1000ms) + this._allUrlStm.params.cutoff_date = (Date.now() - 2592000000) * 1000; + let [exec, execCb] = Sync.withCb(this._allUrlStm.executeAsync, this._allUrlStm); + return exec({ + ids: {}, + handleResult: function handleResult(results) { + let row; + while ((row = results.getNextRow()) != null) { + let url = row.getResultByName("url"); + this.ids[GUIDForUri(url, true)] = url; + } + }, + handleError: function handleError(error) { + execCb.throw(error); + }, + handleCompletion: function handleCompletion(reason) { + execCb(this.ids); + } + }); }, create: function HistStore_create(record) { From c0cb64dafe27b769b2c5c783277652264e74910c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Jun 2010 15:18:01 -0700 Subject: [PATCH 1702/1860] Bug 565411 - Accept max_results as a parameter to allow configuring the number of history items. [r=mconnor] --- services/sync/modules/engines/history.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 5d45918a6055..19adba6e5964 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -142,7 +142,7 @@ HistoryStore.prototype = { "FROM moz_places " + "WHERE last_visit_date > :cutoff_date " + "ORDER BY frecency DESC " + - "LIMIT 5000"); + "LIMIT :max_results"); this.__defineGetter__("_allUrlStm", function() stm); return stm; }, @@ -202,6 +202,8 @@ HistoryStore.prototype = { 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 = 5000; + let [exec, execCb] = Sync.withCb(this._allUrlStm.executeAsync, this._allUrlStm); return exec({ ids: {}, From 977cde1e6cc5240e0514f52b804d2dc5d93a586d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 2 Jun 2010 16:51:48 -0700 Subject: [PATCH 1703/1860] Bug 569746 - Test with sync with smaller numbers to speed up testing Reduce test run time (w/ coverage tool) from 25 minutes to 2 minutes while maintaining the same code coverage by processing hundreds instead of thousands of records. --- .../sync/tests/unit/test_syncengine_sync.js | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js index 6c18041dc507..390bf079e210 100644 --- a/services/sync/tests/unit/test_syncengine_sync.js +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -597,9 +597,9 @@ function test_processIncoming_fetchNum() { let crypto_steam = new ServerWBO('steam'); let collection = new ServerCollection(); - // Let's create some 1000 server side records. They're all at least + // Let's create some 234 server side records. They're all at least // 10 minutes old. - for (var i=0; i < 1234; i++) { + for (var i = 0; i < 234; i++) { let id = 'record-no-' + i; let payload = encryptPayload({id: id, denomination: "Record No. " + i}); let wbo = new ServerWBO(id, payload); @@ -624,7 +624,7 @@ function test_processIncoming_fetchNum() { do_check_eq([id for (id in engine._store.items)].length, 50); do_check_true('record-no-0' in engine._store.items); do_check_true('record-no-49' in engine._store.items); - do_check_eq(engine.toFetch.length, 1234-50); + do_check_eq(engine.toFetch.length, 234 - 50); // The next sync will get another 50 objects, assuming the server @@ -633,7 +633,7 @@ function test_processIncoming_fetchNum() { do_check_eq([id for (id in engine._store.items)].length, 100); do_check_true('record-no-50' in engine._store.items); do_check_true('record-no-99' in engine._store.items); - do_check_eq(engine.toFetch.length, 1234-100); + do_check_eq(engine.toFetch.length, 234 - 100); // Now let's say there are some new items on the server @@ -656,7 +656,7 @@ function test_processIncoming_fetchNum() { do_check_true('new-record-no-4' in engine._store.items); do_check_true('record-no-100' in engine._store.items); do_check_true('record-no-144' in engine._store.items); - do_check_eq(engine.toFetch.length, 1234-100-45); + do_check_eq(engine.toFetch.length, 234 - 100 - 45); // Now let's modify a few existing records on the server so that @@ -673,7 +673,7 @@ function test_processIncoming_fetchNum() { do_check_eq([id for (id in engine._store.items)].length, 197); do_check_true('record-no-145' in engine._store.items); do_check_true('record-no-191' in engine._store.items); - do_check_eq(engine.toFetch.length, 1234-100-45-47); + do_check_eq(engine.toFetch.length, 234 - 100 - 45 - 47); // Finally let's fetch the rest, making sure that will fetch @@ -681,8 +681,8 @@ function test_processIncoming_fetchNum() { while(engine.toFetch.length) { engine._processIncoming(); } - do_check_eq([id for (id in engine._store.items)].length, 1234+5); - do_check_true('record-no-1233' in engine._store.items); + do_check_eq([id for (id in engine._store.items)].length, 234 + 5); + do_check_true('record-no-233' in engine._store.items); } finally { server.stop(function() {}); @@ -769,7 +769,7 @@ function test_uploadOutgoing_MAX_UPLOAD_RECORDS() { // Create a bunch of records (and server side handlers) let engine = makeSteamEngine(); - for (var i=0; i < 2345; i++) { + for (var i = 0; i < 234; i++) { let id = 'record-no-' + i; engine._store.items[id] = "Record No. " + i; engine._tracker.addChangedID(id, 0); @@ -791,12 +791,12 @@ function test_uploadOutgoing_MAX_UPLOAD_RECORDS() { engine._uploadOutgoing(); // Ensure all records have been uploaded - for (i=0; i < 2345; i++) { + for (i = 0; i < 234; i++) { do_check_true(!!collection.wbos['record-no-'+i].payload); } // Ensure that the uploads were performed in batches of MAX_UPLOAD_RECORDS - do_check_eq(noOfUploads, Math.ceil(2345/MAX_UPLOAD_RECORDS)); + do_check_eq(noOfUploads, Math.ceil(234/MAX_UPLOAD_RECORDS)); } finally { server.stop(function() {}); @@ -879,11 +879,12 @@ function test_syncFinish_deleteLotsInBatches() { }(collection.delete)); // Create a bunch of records on the server - for (var i=0; i < 2345; i++) { + let now = Date.now(); + for (var i = 0; i < 234; i++) { let id = 'record-no-' + i; let payload = encryptPayload({id: id, denomination: "Record No. " + i}); let wbo = new ServerWBO(id, payload); - wbo.modified = Date.now()/1000 - 60*(i+10); + wbo.modified = now / 1000 - 60 * (i + 110); collection.wbos[id] = wbo; } @@ -897,22 +898,22 @@ function test_syncFinish_deleteLotsInBatches() { // Confirm initial environment do_check_eq(noOfUploads, 0); - // Declare what we want to have deleted: all records no. 200 and + // Declare what we want to have deleted: all records no. 100 and // up and all records that are less than 200 mins old (which are - // records 0 thru 190). + // records 0 thru 90). engine._delete = {ids: [], - newer: Date.now()/1000 - 60*200.5}; - for (i=200; i < 2345; i++) { + newer: now / 1000 - 60 * 200.5}; + for (i = 100; i < 234; i++) { engine._delete.ids.push('record-no-' + i); } engine._syncFinish(); // Ensure that the appropriate server data has been wiped while - // preserving records 190 thru 200. - for (i=0; i < 2345; i++) { + // preserving records 90 thru 200. + for (i = 0; i < 234; i++) { let id = 'record-no-' + i; - if (i<=190 || i>=200) { + if (i <= 90 || i >= 100) { do_check_eq(collection.wbos[id].payload, undefined); } else { do_check_true(!!collection.wbos[id].payload); @@ -920,7 +921,7 @@ function test_syncFinish_deleteLotsInBatches() { } // The deletion was done in batches - do_check_eq(noOfUploads, 22+1); + do_check_eq(noOfUploads, 2 + 1); // The deletion todo list has been reset. do_check_eq(engine._delete.ids, undefined); From aa78231812b68ecbf8b16f777288e42c5652221c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 3 Jun 2010 18:37:07 -0700 Subject: [PATCH 1704/1860] Bug 568136 - Migration requires client to check for a node again after a 401 [r=mconnor] Allow observers to watch for resource 401s and to specify a new uri to request. Test to make sure "weave:resource:status:401" gets notified and resource handles uri changes. --- services/sync/modules/resource.js | 17 +++++++++++ services/sync/modules/service.js | 19 ++++++++++++ services/sync/tests/unit/test_resource.js | 36 +++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index f07ca11254c6..cbc055636dfd 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -265,6 +265,23 @@ Resource.prototype = { // Make a lazy getter to convert the json response into an object Utils.lazy2(ret, "obj", function() JSON.parse(ret)); + // Notify if we get a 401 to maybe try again with a new uri + if (status == 401) { + // Create an object to allow observers to decide if we should try again + let subject = { + newUri: "", + resource: this, + response: ret + } + Observers.notify("weave:resource:status:401", subject); + + // Do the same type of request but with the new uri + if (subject.newUri != "") { + this.uri = subject.newUri; + return this._request.apply(this, arguments); + } + } + return ret; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 2cdbadce7035..e78a93dda537 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -280,6 +280,7 @@ WeaveSvc.prototype = { Svc.Obs.add("weave:service:sync:error", this); Svc.Obs.add("weave:service:backoff:interval", this); Svc.Obs.add("weave:engine:score:updated", this); + Svc.Obs.add("weave:resource:status:401", this); if (!this.enabled) this._log.info("Weave Sync disabled"); @@ -430,6 +431,9 @@ WeaveSvc.prototype = { case "weave:engine:score:updated": this._handleScoreUpdate(); break; + case "weave:resource:status:401": + this._handleResource401(subject); + break; case "idle": this._log.trace("Idle time hit, trying to sync"); Svc.Idle.removeIdleObserver(this, this._idleTime); @@ -456,6 +460,21 @@ WeaveSvc.prototype = { this._checkSyncStatus(); }, + _handleResource401: function _handleResource401(request) { + // Only handle 401s that are hitting the current cluster + let spec = request.resource.spec; + let cluster = this.clusterURL; + if (spec.indexOf(cluster) != 0) + return; + + // Nothing to do if the cluster isn't changing + if (!this._setCluster()) + return; + + // Replace the old cluster with the new one to retry the request + request.newUri = this.clusterURL + spec.slice(cluster.length); + }, + // gets cluster from central LDAP server and returns it, or null on error _findCluster: function _findCluster() { this._log.debug("Finding cluster for user " + this.username); diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index 66093e84e590..81da7492dccd 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -174,9 +174,13 @@ function run_test() { } do_check_true(didThrow); + let did401 = false; + Observers.add("weave:resource:status:401", function() did401 = true); + _("GET a password protected resource (test that it'll fail w/o pass, no throw)"); let res2 = new Resource("http://localhost:8080/protected"); content = res2.get(); + do_check_true(did401); do_check_eq(content, "This path exists and is protected - failed") do_check_eq(content.status, 401); do_check_false(content.success); @@ -342,5 +346,37 @@ function run_test() { do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED"); do_check_eq(typeof error.stack, "string"); + let redirRequest; + let redirToOpen = function(subject) { + subject.newUri = "http://localhost:8080/open"; + redirRequest = subject; + }; + Observers.add("weave:resource:status:401", redirToOpen); + + _("Notification of 401 can redirect to another uri"); + did401 = false; + let res12 = new Resource("http://localhost:8080/protected"); + content = res12.get(); + do_check_eq(res12.spec, "http://localhost:8080/open"); + do_check_eq(content, "This path exists"); + do_check_eq(content.status, 200); + do_check_true(content.success); + do_check_eq(res.data, content); + do_check_true(did401); + do_check_eq(redirRequest.response, "This path exists and is protected - failed") + do_check_eq(redirRequest.response.status, 401); + do_check_false(redirRequest.response.success); + + Observers.remove("weave:resource:status:401", redirToOpen); + + _("Removing the observer should result in the original 401"); + did401 = false; + let res13 = new Resource("http://localhost:8080/protected"); + content = res13.get(); + do_check_true(did401); + do_check_eq(content, "This path exists and is protected - failed") + do_check_eq(content.status, 401); + do_check_false(content.success); + server.stop(function() {}); } From 7f4b8c9909aec9f1b7a6a16b356fc8502689bde6 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 4 Jun 2010 14:44:04 +0100 Subject: [PATCH 1705/1860] Bug 569730 - Can't log in a 2nd computer [r=Mardak] Fix a regression introduced with the status.js refactoring (bug 557590). --- services/sync/modules/service.js | 4 +- .../sync/tests/unit/test_service_login.js | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 services/sync/tests/unit/test_service_login.js diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index e78a93dda537..c35e706507f5 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -333,7 +333,9 @@ WeaveSvc.prototype = { this._log.debug("checkSetup: no passphrase set"); Status.login = LOGIN_FAILED_NO_PASSPHRASE; } - + else + Status.service = STATUS_OK; + return Status.service; }, diff --git a/services/sync/tests/unit/test_service_login.js b/services/sync/tests/unit/test_service_login.js new file mode 100644 index 000000000000..851dd28c51dc --- /dev/null +++ b/services/sync/tests/unit/test_service_login.js @@ -0,0 +1,52 @@ +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/service.js"); +Cu.import("resource://weave/status.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); + +function login_handler(request, response) { + // btoa('johndoe:ilovejane') == am9obmRvZTppbG92ZWphbmU= + let body; + if (request.hasHeader("Authorization") && + request.getHeader("Authorization") == "Basic am9obmRvZTppbG92ZWphbmU=") { + body = "{}"; + response.setStatusLine(request.httpVersion, 200, "OK"); + } else { + body = "Unauthorized"; + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + } + response.bodyOutputStream.write(body, body.length); +} + +function run_test() { + let logger = Log4Moz.repository.rootLogger; + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + + let server = httpd_setup({ + "/1.0/johndoe/info/collections": login_handler + }); + + try { + Weave.Service.serverURL = "http://localhost:8080/"; + Weave.Service.clusterURL = "http://localhost:8080/"; + + _("Try logging in. It wont' work because we're not configured yet."); + do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); + Weave.Service.login(); + do_check_false(Weave.Service.isLoggedIn); + + _("Try again with username and password set."); + Weave.Service.username = "johndoe"; + Weave.Service.password = "ilovejane"; + // We need a non-empty passphrase for login to work. Even the + // setup wizard just sets this to 'foo' if there isn't a + // passphrase yet. + Weave.Service.passphrase = "foo"; + Weave.Service.login(); + do_check_true(Weave.Service.isLoggedIn); + + } finally { + Svc.Prefs.resetBranch(""); + server.stop(function() {}); + } +} From 6c024f0a1a2f6305d181ff7e4650d73efdd9e902 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 4 Jun 2010 15:14:27 +0100 Subject: [PATCH 1706/1860] Bug 570137 - test_engine fails on Minefield [r=mconnor] Setting an attribute that has a getter no longer throws an exception in Minefield, it's just ignored. --- services/sync/tests/unit/test_engine.js | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/services/sync/tests/unit/test_engine.js b/services/sync/tests/unit/test_engine.js index 00b31ea66182..a5acf90ad5b7 100644 --- a/services/sync/tests/unit/test_engine.js +++ b/services/sync/tests/unit/test_engine.js @@ -63,17 +63,6 @@ Observers.add("weave:engine:sync:start", engineObserver); Observers.add("weave:engine:sync:finish", engineObserver); -function do_check_throws(func) { - var raised = false; - try { - func(); - } catch (ex) { - raised = true; - } - do_check_true(raised); -} - - function test_members() { _("Engine object members"); let engine = new SteamEngine(); @@ -90,9 +79,14 @@ function test_score() { engine._tracker.score += 5; do_check_eq(engine.score, 5); - do_check_throws(function() { - engine.score = 10; - }); + try { + engine.score = 10; + } catch(ex) { + // Setting an attribute that has a getter produces an error in + // Firefox <= 3.6 and is ignored in later versions. Either way, + // the attribute's value won't change. + } + do_check_eq(engine.score, 5); } function test_resetClient() { From 0c33e7320a27827cbad62ba3f6aab66af7f62fd9 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 4 Jun 2010 16:38:38 +0100 Subject: [PATCH 1707/1860] Bug 570152 - engine.js tests should completely reset preferences [r=mconnor] Switch to resetBranch("") instead of resetting individual prefs. --- services/sync/tests/unit/test_engine.js | 4 +- services/sync/tests/unit/test_syncengine.js | 8 ++-- .../sync/tests/unit/test_syncengine_sync.js | 37 ++++++------------- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/services/sync/tests/unit/test_engine.js b/services/sync/tests/unit/test_engine.js index a5acf90ad5b7..e9fc36b87c71 100644 --- a/services/sync/tests/unit/test_engine.js +++ b/services/sync/tests/unit/test_engine.js @@ -136,7 +136,7 @@ function test_enabled() { engine.enabled = false; do_check_false(Svc.Prefs.get("engine.steam")); } finally { - Svc.Prefs.reset("engine.steam"); + Svc.Prefs.resetBranch(""); } } @@ -156,7 +156,7 @@ function test_sync() { do_check_eq(engineObserver.topics[0], "weave:engine:sync:start"); do_check_eq(engineObserver.topics[1], "weave:engine:sync:finish"); } finally { - Svc.Prefs.reset("engine.steam"); + Svc.Prefs.resetBranch(""); engine.wasSynced = false; engineObserver.reset(); } diff --git a/services/sync/tests/unit/test_syncengine.js b/services/sync/tests/unit/test_syncengine.js index 439961fcdb87..67f7bd4c0108 100644 --- a/services/sync/tests/unit/test_syncengine.js +++ b/services/sync/tests/unit/test_syncengine.js @@ -18,7 +18,7 @@ function test_url_attributes() { "https://cluster/1.0/foo/storage/crypto/steam"); do_check_eq(engine.metaURL, "https://cluster/1.0/foo/storage/meta/global"); } finally { - Svc.Prefs.reset("clusterURL"); + Svc.Prefs.resetBranch(""); } } @@ -37,7 +37,7 @@ function test_syncID() { do_check_eq(Svc.Prefs.get("steam.syncID"), "fake-guid-1"); do_check_eq(engine.syncID, "fake-guid-1"); } finally { - Svc.Prefs.reset("steam.syncID"); + Svc.Prefs.resetBranch(""); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); } } @@ -60,7 +60,7 @@ function test_lastSync() { do_check_eq(engine.lastSync, 0); do_check_eq(Svc.Prefs.get("steam.lastSync"), "0"); } finally { - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); } } @@ -108,6 +108,6 @@ function test_resetClient() { do_check_eq(engine.toFetch.length, 0); } finally { syncTesting = new SyncTestingInfrastructure(makeSteamEngine); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); } } diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js index 390bf079e210..342f4711977e 100644 --- a/services/sync/tests/unit/test_syncengine_sync.js +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -223,9 +223,7 @@ function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.syncID"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -257,7 +255,7 @@ function test_syncStartup_serverHasNewerVersion() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); + Svc.Prefs.resetBranch(""); Records.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); } @@ -300,9 +298,7 @@ function test_syncStartup_syncIDMismatchResetsClient() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.syncID"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -371,9 +367,7 @@ function test_syncStartup_badKeyWipesServerData() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.syncID"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -404,8 +398,7 @@ function test_processIncoming_emptyServer() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -458,8 +451,7 @@ function test_processIncoming_createFromServer() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -580,8 +572,7 @@ function test_processIncoming_reconcile() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -686,9 +677,7 @@ function test_processIncoming_fetchNum() { } finally { server.stop(function() {}); - Svc.Prefs.reset("client.type"); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -742,8 +731,7 @@ function test_uploadOutgoing_toEmptyServer() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -800,8 +788,7 @@ function test_uploadOutgoing_MAX_UPLOAD_RECORDS() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); - Svc.Prefs.reset("steam.lastSync"); + Svc.Prefs.resetBranch(""); Records.clearCache(); CryptoMetas.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); @@ -856,7 +843,7 @@ function test_syncFinish_deleteByIds() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); + Svc.Prefs.resetBranch(""); Records.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); } @@ -928,7 +915,7 @@ function test_syncFinish_deleteLotsInBatches() { } finally { server.stop(function() {}); - Svc.Prefs.reset("clusterURL"); + Svc.Prefs.resetBranch(""); Records.clearCache(); syncTesting = new SyncTestingInfrastructure(makeSteamEngine); } From f09c89e80a5aba9a874e87f170b0abf40e4bae82 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 4 Jun 2010 16:38:38 +0100 Subject: [PATCH 1708/1860] Bug 557589 - code audit and create unit test plan for service.js [r=mconnor] Part 1: Get rid of unused '_syncInProgress' attribute, unnecessary getters and setters. --- services/sync/modules/service.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c35e706507f5..70c6c26bacd0 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -100,8 +100,7 @@ WeaveSvc.prototype = { _lock: Utils.lock, _catch: Utils.catch, _loggedIn: false, - _syncInProgress: false, - _keyGenEnabled: true, + keyGenEnabled: true, // object for caching public and private keys _keyPair: {}, @@ -184,9 +183,6 @@ WeaveSvc.prototype = { get isLoggedIn() { return this._loggedIn; }, - get keyGenEnabled() { return this._keyGenEnabled; }, - set keyGenEnabled(value) { this._keyGenEnabled = value; }, - // nextSync and nextHeartbeat are in milliseconds, but prefs can't hold that much get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), @@ -865,7 +861,7 @@ WeaveSvc.prototype = { if (meta && !meta.payload.syncID) this._log.warn("No sync id, server wipe needed"); - if (!this._keyGenEnabled) { + if (!this.keyGenEnabled) { this._log.info("...and key generation is disabled. Not wiping. " + "Aborting sync."); Status.sync = DESKTOP_VERSION_OUT_OF_DATE; @@ -932,7 +928,7 @@ WeaveSvc.prototype = { return false; } - if (!this._keyGenEnabled) { + if (!this.keyGenEnabled) { this._log.warn("Couldn't download keys from server, and key generation" + "is disabled. Aborting sync"); Status.sync = NO_KEYS_NO_KEYGEN; From 6774af746d8ff43a531eca80da6000094d08c2a3 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 4 Jun 2010 17:05:55 +0100 Subject: [PATCH 1709/1860] Bug 557589 - code audit and create unit test plan for service.js [r=mconnor] Part 2: Tests for module startup, Weave.Service attributes. --- services/sync/modules/constants.js | 1 + services/sync/modules/service.js | 2 + .../tests/unit/test_service_attributes.js | 186 ++++++++++++++++++ .../sync/tests/unit/test_service_startup.js | 35 ++++ 4 files changed, 224 insertions(+) create mode 100644 services/sync/tests/unit/test_service_attributes.js create mode 100644 services/sync/tests/unit/test_service_startup.js diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 3658c7e332d4..b1e446d1dae1 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -135,6 +135,7 @@ kFirstSyncChoiceNotMade: "User has not selected an action for firs FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", FENNEC_ID: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}", SEAMONKEY_ID: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", +TEST_HARNESS_ID: "xuth@mozilla.org", MIN_PP_LENGTH: 12, MIN_PASS_LENGTH: 8 diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 70c6c26bacd0..23a7f14fdc49 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -99,6 +99,7 @@ WeaveSvc.prototype = { _lock: Utils.lock, _catch: Utils.catch, + _locked: false, _loggedIn: false, keyGenEnabled: true, @@ -381,6 +382,7 @@ WeaveSvc.prototype = { break; case FIREFOX_ID: + case TEST_HARNESS_ID: engines = ["Bookmarks", "Form", "History", "Password", "Prefs", "Tab"]; break; diff --git a/services/sync/tests/unit/test_service_attributes.js b/services/sync/tests/unit/test_service_attributes.js new file mode 100644 index 000000000000..000224489f88 --- /dev/null +++ b/services/sync/tests/unit/test_service_attributes.js @@ -0,0 +1,186 @@ +Cu.import("resource://weave/service.js"); +Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/status.js"); +Cu.import("resource://weave/base_records/keys.js"); + +function test_urlsAndIdentities() { + _("Various Weave.Service properties correspond to preference settings and update other object properties upon being set."); + + try { + _("Verify initial state"); + do_check_eq(Svc.Prefs.get("username"), undefined); + do_check_eq(PubKeys.defaultKeyUri, undefined); + do_check_eq(PrivKeys.defaultKeyUri, undefined); + do_check_eq(ID.get("WeaveID").username, ""); + do_check_eq(ID.get("WeaveCryptoID").username, ""); + + do_check_true(!!Weave.Service.serverURL); // actual value may change + do_check_eq(Weave.Service.clusterURL, ""); + do_check_eq(Weave.Service.infoURL, undefined); + do_check_eq(Weave.Service.storageURL, undefined); + do_check_eq(Weave.Service.metaURL, undefined); + + _("The 'username' attribute is normalized to lower case, updates preferences and identities."); + Weave.Service.username = "JohnDoe"; + do_check_eq(Svc.Prefs.get("username"), "johndoe"); + do_check_eq(ID.get("WeaveID").username, "johndoe"); + do_check_eq(ID.get("WeaveCryptoID").username, "johndoe"); + + // Since we don't have a cluster URL yet, these will still not be defined. + do_check_eq(Weave.Service.infoURL, undefined); + do_check_eq(Weave.Service.storageURL, undefined); + do_check_eq(Weave.Service.metaURL, undefined); + do_check_eq(PubKeys.defaultKeyUri, undefined); + do_check_eq(PrivKeys.defaultKeyUri, undefined); + + _("The 'clusterURL' attribute updates preferences and cached URLs."); + Weave.Service.serverURL = "http://weave.server/"; + Weave.Service.clusterURL = "http://weave.cluster/"; + do_check_eq(Svc.Prefs.get("clusterURL"), "http://weave.cluster/"); + + do_check_eq(Weave.Service.infoURL, + "http://weave.cluster/1.0/johndoe/info/collections"); + do_check_eq(Weave.Service.storageURL, + "http://weave.cluster/1.0/johndoe/storage/"); + do_check_eq(Weave.Service.metaURL, + "http://weave.cluster/1.0/johndoe/storage/meta/global"); + do_check_eq(PubKeys.defaultKeyUri, + "http://weave.cluster/1.0/johndoe/storage/keys/pubkey"); + do_check_eq(PrivKeys.defaultKeyUri, + "http://weave.cluster/1.0/johndoe/storage/keys/privkey"); + + _("The 'miscURL' and 'userURL' attributes can be relative to 'serverURL' or absolute."); + Svc.Prefs.set("miscURL", "relative/misc/"); + Svc.Prefs.set("userURL", "relative/user/"); + do_check_eq(Weave.Service.miscAPI, + "http://weave.server/relative/misc/1.0/"); + do_check_eq(Weave.Service.userAPI, + "http://weave.server/relative/user/1.0/"); + + Svc.Prefs.set("miscURL", "http://weave.misc.services/"); + Svc.Prefs.set("userURL", "http://weave.user.services/"); + do_check_eq(Weave.Service.miscAPI, "http://weave.misc.services/1.0/"); + do_check_eq(Weave.Service.userAPI, "http://weave.user.services/1.0/"); + + do_check_eq(Weave.Service.pwResetURL, + "http://weave.server/weave-password-reset"); + + _("Empty/false value for 'username' resets preference."); + Weave.Service.username = ""; + do_check_eq(Svc.Prefs.get("username"), undefined); + do_check_eq(ID.get("WeaveID").username, ""); + do_check_eq(ID.get("WeaveCryptoID").username, ""); + + _("The 'serverURL' attributes updates/resets preferences."); + // Identical value doesn't do anything + Weave.Service.serverURL = Weave.Service.serverURL; + do_check_eq(Svc.Prefs.get("clusterURL"), "http://weave.cluster/"); + + Weave.Service.serverURL = "http://different.auth.node/"; + do_check_eq(Svc.Prefs.get("serverURL"), "http://different.auth.node/"); + do_check_eq(Svc.Prefs.get("clusterURL"), undefined); + + } finally { + Svc.Prefs.resetBranch(""); + } +} + + +function test_syncID() { + _("Weave.Service.syncID is auto-generated, corresponds to preference."); + new FakeGUIDService(); + + try { + // Ensure pristine environment + do_check_eq(Svc.Prefs.get("client.syncID"), undefined); + + // Performing the first get on the attribute will generate a new GUID. + do_check_eq(Weave.Service.syncID, "fake-guid-0"); + do_check_eq(Svc.Prefs.get("client.syncID"), "fake-guid-0"); + + Svc.Prefs.set("client.syncID", Utils.makeGUID()); + do_check_eq(Svc.Prefs.get("client.syncID"), "fake-guid-1"); + do_check_eq(Weave.Service.syncID, "fake-guid-1"); + } finally { + Svc.Prefs.resetBranch(""); + new FakeGUIDService(); + } +} + + +function test_prefAttributes() { + _("Test various attributes corresponding to preferences."); + + const TIMESTAMP1 = 1275493471649; + const TIMESTAMP2 = 1275493741122; + const INTERVAL = 42 * 60 * 1000; // 42 minutes + const THRESHOLD = 3142; + const SCORE = 2718; + const NUMCLIENTS = 42; + + try { + _("The 'nextSync' and 'nextHeartbeat' attributes store a millisecond timestamp to the nearest second."); + do_check_eq(Weave.Service.nextSync, 0); + do_check_eq(Weave.Service.nextHeartbeat, 0); + Weave.Service.nextSync = TIMESTAMP1; + Weave.Service.nextHeartbeat = TIMESTAMP2; + do_check_eq(Weave.Service.nextSync, Math.floor(TIMESTAMP1/1000)*1000); + do_check_eq(Weave.Service.nextHeartbeat, Math.floor(TIMESTAMP2/1000)*1000); + + _("'syncInterval' has a non-zero default value."); + do_check_eq(Svc.Prefs.get('syncInterval'), undefined); + do_check_true(Weave.Service.syncInterval > 0); + + _("'syncInterval' corresponds to a preference setting."); + Weave.Service.syncInterval = INTERVAL; + do_check_eq(Weave.Service.syncInterval, INTERVAL); + do_check_eq(Svc.Prefs.get('syncInterval'), INTERVAL); + + _("'syncInterval' ignored preference setting after partial sync.."); + Status.partial = true; + do_check_eq(Weave.Service.syncInterval, PARTIAL_DATA_SYNC); + + _("'syncThreshold' corresponds to preference, has non-zero default."); + do_check_eq(Svc.Prefs.get('syncThreshold'), undefined); + do_check_true(Weave.Service.syncThreshold > 0); + Weave.Service.syncThreshold = THRESHOLD; + do_check_eq(Weave.Service.syncThreshold, THRESHOLD); + do_check_eq(Svc.Prefs.get('syncThreshold'), THRESHOLD); + + _("'globalScore' corresponds to preference, defaults to zero."); + do_check_eq(Svc.Prefs.get('globalScore'), undefined); + do_check_eq(Weave.Service.globalScore, 0); + Weave.Service.globalScore = SCORE; + do_check_eq(Weave.Service.globalScore, SCORE); + do_check_eq(Svc.Prefs.get('globalScore'), SCORE); + + _("'numClients' corresponds to preference, defaults to zero."); + do_check_eq(Svc.Prefs.get('numClients'), undefined); + do_check_eq(Weave.Service.numClients, 0); + Weave.Service.numClients = NUMCLIENTS; + do_check_eq(Weave.Service.numClients, NUMCLIENTS); + do_check_eq(Svc.Prefs.get('numClients'), NUMCLIENTS); + + } finally { + Svc.Prefs.resetBranch(""); + } +} + + +function test_locked() { + _("The 'locked' attribute can be toggled with lock() and unlock()"); + + // Defaults to false + do_check_eq(Weave.Service.locked, false); + + do_check_eq(Weave.Service.lock(), true); + do_check_eq(Weave.Service.locked, true); + + // Locking again will return false + do_check_eq(Weave.Service.lock(), false); + + Weave.Service.unlock(); + do_check_eq(Weave.Service.locked, false); +} diff --git a/services/sync/tests/unit/test_service_startup.js b/services/sync/tests/unit/test_service_startup.js new file mode 100644 index 000000000000..3d35780ebd9b --- /dev/null +++ b/services/sync/tests/unit/test_service_startup.js @@ -0,0 +1,35 @@ +Cu.import("resource://weave/util.js"); +Cu.import("resource://weave/identity.js"); +Cu.import("resource://weave/ext/Observers.js"); + +function run_test() { + _("When imported, Weave.Service.onStartup is called"); + + // Test fixtures + let observerCalled = false; + Observers.add("weave:service:ready", function (subject, data) { + observerCalled = true; + }); + Svc.Prefs.set("username", "johndoe"); + + try { + Cu.import("resource://weave/service.js"); + + _("Service is enabled."); + do_check_eq(Weave.Service.enabled, true); + + _("Engines are registered."); + let engines = Weave.Engines.getAll(); + do_check_true(!!engines.length); + + _("Identities are registered."); + do_check_eq(ID.get('WeaveID').username, "johndoe"); + do_check_eq(ID.get('WeaveCryptoID').username, "johndoe"); + + _("Observers are notified of startup"); + do_check_true(observerCalled); + + } finally { + Svc.Prefs.resetBranch(""); + } +} From 8cd4cd54a2b126c168c53613901d943868e57fef Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Tue, 8 Jun 2010 19:05:05 -0700 Subject: [PATCH 1710/1860] Bug 569740 - Tab Characters within Usernames Can Cause Connectivity Issues & Security Events [r=mconnor] Strip tabs from username since tabs are stripped from URIs and the username is part of all URIs. --- services/sync/modules/service.js | 3 +++ .../sync/tests/unit/test_service_attributes.js | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 23a7f14fdc49..ec46348efb41 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -113,6 +113,9 @@ WeaveSvc.prototype = { if (value) { // Make sure all uses of this new username is lowercase value = value.toLowerCase(); + // Tab characters are stripped from URIs, so make sure that the + // username doesn't contain any tabs. + value = value.replace("\t", "", "g"); Svc.Prefs.set("username", value); } else diff --git a/services/sync/tests/unit/test_service_attributes.js b/services/sync/tests/unit/test_service_attributes.js index 000224489f88..17ff0b238d83 100644 --- a/services/sync/tests/unit/test_service_attributes.js +++ b/services/sync/tests/unit/test_service_attributes.js @@ -23,10 +23,11 @@ function test_urlsAndIdentities() { do_check_eq(Weave.Service.metaURL, undefined); _("The 'username' attribute is normalized to lower case, updates preferences and identities."); - Weave.Service.username = "JohnDoe"; - do_check_eq(Svc.Prefs.get("username"), "johndoe"); - do_check_eq(ID.get("WeaveID").username, "johndoe"); - do_check_eq(ID.get("WeaveCryptoID").username, "johndoe"); + Weave.Service.username = "TarZan"; + do_check_eq(Weave.Service.username, "tarzan"); + do_check_eq(Svc.Prefs.get("username"), "tarzan"); + do_check_eq(ID.get("WeaveID").username, "tarzan"); + do_check_eq(ID.get("WeaveCryptoID").username, "tarzan"); // Since we don't have a cluster URL yet, these will still not be defined. do_check_eq(Weave.Service.infoURL, undefined); @@ -35,6 +36,14 @@ function test_urlsAndIdentities() { do_check_eq(PubKeys.defaultKeyUri, undefined); do_check_eq(PrivKeys.defaultKeyUri, undefined); + _("Tabs are stripped from the 'username' attribute as they can't be part of a URI."); + Weave.Service.username = "jo\thn\tdoe"; + + do_check_eq(Weave.Service.username, "johndoe"); + do_check_eq(Svc.Prefs.get("username"), "johndoe"); + do_check_eq(ID.get("WeaveID").username, "johndoe"); + do_check_eq(ID.get("WeaveCryptoID").username, "johndoe"); + _("The 'clusterURL' attribute updates preferences and cached URLs."); Weave.Service.serverURL = "http://weave.server/"; Weave.Service.clusterURL = "http://weave.cluster/"; From f045e6839b991dce40c706d7fef82408b6d6fe94 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 9 Jun 2010 09:35:01 -0700 Subject: [PATCH 1711/1860] Bug 570635 - Use async queries for fetching form data [r=mconnor] Create a Utils.queryAsync to wrap executeAsync that fetches all rows and columns by name. Update form and history engines to use it. --- services/sync/modules/engines/forms.js | 33 +++------ services/sync/modules/engines/history.js | 62 ++--------------- services/sync/modules/util.js | 28 ++++++++ .../sync/tests/unit/test_utils_queryAsync.js | 69 +++++++++++++++++++ 4 files changed, 114 insertions(+), 78 deletions(-) create mode 100644 services/sync/tests/unit/test_utils_queryAsync.js diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 0e844a5c5493..3a19cf04b11a 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -49,34 +49,21 @@ Cu.import("resource://weave/type_records/forms.js"); let FormWrapper = { getAllEntries: function getAllEntries() { - let entries = []; // Sort by (lastUsed - minLast) / (maxLast - minLast) * timesUsed / maxTimes let query = this.createStatement( - "SELECT fieldname, value FROM moz_formhistory " + + "SELECT fieldname name, value FROM moz_formhistory " + "ORDER BY 1.0 * (lastUsed - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) / " + "((SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed DESC LIMIT 1) - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) * " + "timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) DESC " + "LIMIT 500"); - while (query.executeStep()) { - entries.push({ - name: query.row.fieldname, - value: query.row.value - }); - } - return entries; + return Utils.queryAsync(query, ["name", "value"]); }, getEntry: function getEntry(guid) { let query = this.createStatement( - "SELECT fieldname, value FROM moz_formhistory WHERE guid = :guid"); + "SELECT fieldname name, value FROM moz_formhistory WHERE guid = :guid"); query.params.guid = guid; - if (!query.executeStep()) - return; - - return { - name: query.row.fieldname, - value: query.row.value - }; + return Utils.queryAsync(query, ["name", "value"])[0]; }, getGUID: function getGUID(name, value) { @@ -86,11 +73,11 @@ let FormWrapper = { "WHERE fieldname = :name AND value = :value"); getQuery.params.name = name; getQuery.params.value = value; - getQuery.executeStep(); // Give the guid if we found one - if (getQuery.row.guid != null) - return getQuery.row.guid; + let item = Utils.queryAsync(getQuery, "guid")[0]; + if (item != null) + return item.guid; // We need to create a guid for this entry let setQuery = this.createStatement( @@ -100,7 +87,7 @@ let FormWrapper = { setQuery.params.guid = guid; setQuery.params.name = name; setQuery.params.value = value; - setQuery.execute(); + Utils.queryAsync(setQuery); return guid; }, @@ -109,7 +96,7 @@ let FormWrapper = { let query = this.createStatement( "SELECT 1 FROM moz_formhistory WHERE guid = :guid"); query.params.guid = guid; - return query.executeStep(); + return Utils.queryAsync(query).length == 1; }, replaceGUID: function replaceGUID(oldGUID, newGUID) { @@ -117,7 +104,7 @@ let FormWrapper = { "UPDATE moz_formhistory SET guid = :newGUID WHERE guid = :oldGUID"); query.params.oldGUID = oldGUID; query.params.newGUID = newGUID; - query.execute(); + Utils.queryAsync(query); }, createStatement: function createStatement(query) { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 19adba6e5964..8385cda8ce78 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -43,7 +43,6 @@ const Cu = Components.utils; const GUID_ANNO = "weave/guid"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); Cu.import("resource://weave/stores.js"); @@ -150,48 +149,13 @@ HistoryStore.prototype = { // See bug 320831 for why we use SQL here _getVisits: function HistStore__getVisits(uri) { this._visitStm.params.url = uri; - let [exec, execCb] = Sync.withCb(this._visitStm.executeAsync, this._visitStm); - return exec({ - visits: [], - handleResult: function handleResult(results) { - let row; - while ((row = results.getNextRow()) != null) { - this.visits.push({ - date: row.getResultByName("date"), - type: row.getResultByName("type") - }); - } - }, - handleError: function handleError(error) { - execCb.throw(error); - }, - handleCompletion: function handleCompletion(reason) { - execCb(this.visits); - } - }); + return Utils.queryAsync(this._visitStm, ["date", "type"]); }, // See bug 468732 for why we use SQL here _findURLByGUID: function HistStore__findURLByGUID(guid) { this._urlStm.params.guid = guid; - let [exec, execCb] = Sync.withCb(this._urlStm.executeAsync, this._urlStm); - return exec({ - handleResult: function(results) { - // Save the one result and its columns - let row = results.getNextRow(); - this.urlInfo = { - url: row.getResultByName("url"), - title: row.getResultByName("title"), - frecency: row.getResultByName("frecency"), - }; - }, - handleError: function(error) { - execCb.throw(error); - }, - handleCompletion: function(reason) { - execCb(this.urlInfo); - } - }); + return Utils.queryAsync(this._urlStm, ["url", "title", "frecency"])[0]; }, changeItemID: function HStore_changeItemID(oldID, newID) { @@ -204,23 +168,11 @@ HistoryStore.prototype = { this._allUrlStm.params.cutoff_date = (Date.now() - 2592000000) * 1000; this._allUrlStm.params.max_results = 5000; - let [exec, execCb] = Sync.withCb(this._allUrlStm.executeAsync, this._allUrlStm); - return exec({ - ids: {}, - handleResult: function handleResult(results) { - let row; - while ((row = results.getNextRow()) != null) { - let url = row.getResultByName("url"); - this.ids[GUIDForUri(url, true)] = url; - } - }, - handleError: function handleError(error) { - execCb.throw(error); - }, - handleCompletion: function handleCompletion(reason) { - execCb(this.ids); - } - }); + let urls = Utils.queryAsync(this._allUrlStm, "url"); + return urls.reduce(function(ids, item) { + ids[GUIDForUri(item.url, true)] = item.url; + return ids; + }, {}); }, create: function HistStore_create(record) { diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index e0b7bab5b9ad..56a8f922fb87 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -44,6 +44,7 @@ const Cu = Components.utils; Cu.import("resource://weave/ext/Preferences.js"); Cu.import("resource://weave/ext/Observers.js"); Cu.import("resource://weave/ext/StringBundle.js"); +Cu.import("resource://weave/ext/Sync.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/log4moz.js"); @@ -146,6 +147,33 @@ let Utils = { }; }, + queryAsync: function(query, names) { + // Allow array of names, single name, and no name + if (!Utils.isArray(names)) + names = names == null ? [] : [names]; + + // Synchronously asyncExecute fetching all results by name + let [exec, execCb] = Sync.withCb(query.executeAsync, query); + return exec({ + items: [], + handleResult: function handleResult(results) { + let row; + while ((row = results.getNextRow()) != null) { + this.items.push(names.reduce(function(item, name) { + item[name] = row.getResultByName(name); + return item; + }, {})); + } + }, + handleError: function handleError(error) { + execCb.throw(error); + }, + handleCompletion: function handleCompletion(reason) { + execCb(this.items); + } + }); + }, + // Generates a brand-new globally unique identifier (GUID). makeGUID: function makeGUID() { // 70 characters that are not-escaped URL-friendly diff --git a/services/sync/tests/unit/test_utils_queryAsync.js b/services/sync/tests/unit/test_utils_queryAsync.js new file mode 100644 index 000000000000..fa15abab0802 --- /dev/null +++ b/services/sync/tests/unit/test_utils_queryAsync.js @@ -0,0 +1,69 @@ +_("Make sure queryAsync will synchronously fetch rows for a query asyncly"); +Cu.import("resource://weave/util.js"); + +function run_test() { + _("Using the form service to test queries"); + function c(query) Svc.Form.DBConnection.createStatement(query); + + _("Make sure the call is async and allows other events to process"); + let isAsync = false; + setTimeout(function() isAsync = true, 0); + do_check_false(isAsync); + + _("Empty out the formhistory table"); + let r0 = Utils.queryAsync(c("DELETE FROM moz_formhistory")); + do_check_true(isAsync); + do_check_eq(r0.length, 0); + + _("Make sure there's nothing there"); + let r1 = Utils.queryAsync(c("SELECT 1 FROM moz_formhistory")); + do_check_eq(r1.length, 0); + + _("Insert a row"); + let r2 = Utils.queryAsync(c("INSERT INTO moz_formhistory (fieldname, value) VALUES ('foo', 'bar')")); + do_check_eq(r2.length, 0); + + _("Request a known value for the one row"); + let r3 = Utils.queryAsync(c("SELECT 42 num FROM moz_formhistory"), "num"); + do_check_eq(r3.length, 1); + do_check_eq(r3[0].num, 42); + + _("Get multiple columns"); + let r4 = Utils.queryAsync(c("SELECT fieldname, value FROM moz_formhistory"), ["fieldname", "value"]); + do_check_eq(r4.length, 1); + do_check_eq(r4[0].fieldname, "foo"); + do_check_eq(r4[0].value, "bar"); + + _("Get multiple columns with a different order"); + let r5 = Utils.queryAsync(c("SELECT fieldname, value FROM moz_formhistory"), ["value", "fieldname"]); + do_check_eq(r5.length, 1); + do_check_eq(r5[0].fieldname, "foo"); + do_check_eq(r5[0].value, "bar"); + + _("Add multiple entries (sqlite doesn't support multiple VALUES)"); + let r6 = Utils.queryAsync(c("INSERT INTO moz_formhistory (fieldname, value) SELECT 'foo', 'baz' UNION SELECT 'more', 'values'")); + do_check_eq(r6.length, 0); + + _("Get multiple rows"); + let r7 = Utils.queryAsync(c("SELECT fieldname, value FROM moz_formhistory WHERE fieldname = 'foo'"), ["fieldname", "value"]); + do_check_eq(r7.length, 2); + do_check_eq(r7[0].fieldname, "foo"); + do_check_eq(r7[1].fieldname, "foo"); + + _("Make sure updates work"); + let r8 = Utils.queryAsync(c("UPDATE moz_formhistory SET value = 'updated' WHERE fieldname = 'more'")); + do_check_eq(r8.length, 0); + + _("Get the updated"); + let r9 = Utils.queryAsync(c("SELECT value, fieldname FROM moz_formhistory WHERE fieldname = 'more'"), ["fieldname", "value"]); + do_check_eq(r9.length, 1); + do_check_eq(r9[0].fieldname, "more"); + do_check_eq(r9[0].value, "updated"); + + _("Grabbing fewer fields than queried is fine"); + let r10 = Utils.queryAsync(c("SELECT value, fieldname FROM moz_formhistory"), "fieldname"); + do_check_eq(r10.length, 3); + + _("Cleaning up"); + Utils.queryAsync(c("DELETE FROM moz_formhistory")); +} From 8490869c1dee31c414980236382420dcdf4be70e Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 9 Jun 2010 15:38:17 -0700 Subject: [PATCH 1712/1860] Bustage fix for bug 570635 to allow more time for the timeout to trigger. --- services/sync/tests/unit/test_utils_queryAsync.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_utils_queryAsync.js b/services/sync/tests/unit/test_utils_queryAsync.js index fa15abab0802..a431cff3783c 100644 --- a/services/sync/tests/unit/test_utils_queryAsync.js +++ b/services/sync/tests/unit/test_utils_queryAsync.js @@ -12,7 +12,6 @@ function run_test() { _("Empty out the formhistory table"); let r0 = Utils.queryAsync(c("DELETE FROM moz_formhistory")); - do_check_true(isAsync); do_check_eq(r0.length, 0); _("Make sure there's nothing there"); @@ -66,4 +65,7 @@ function run_test() { _("Cleaning up"); Utils.queryAsync(c("DELETE FROM moz_formhistory")); + + _("Make sure the timeout got to run before this function ends"); + do_check_true(isAsync); } From 608d41e1ef03839cf98f1e0ff6bc2e9cbfcdae2f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 9 Jun 2010 16:10:38 -0700 Subject: [PATCH 1713/1860] Bustage fix for bug 570635 to check null for item.guid instead of item. --- services/sync/modules/engines/forms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index 3a19cf04b11a..e19f7e647dd7 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -76,7 +76,7 @@ let FormWrapper = { // Give the guid if we found one let item = Utils.queryAsync(getQuery, "guid")[0]; - if (item != null) + if (item.guid != null) return item.guid; // We need to create a guid for this entry From 1acadeb58d2b176ad7e626bb5402bd18eb445e2a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 9 Jun 2010 17:03:31 -0700 Subject: [PATCH 1714/1860] Bug 569355 - source code reorg, redux [r=mconnor] Split the code structure to service/{crypto,sync} and ui/{fennec,firefox}. Update the top level makefile to build the addon staging directory to merge various parts of directories. --- services/crypto/IWeaveCrypto.h | 359 ------------------ services/crypto/IWeaveCrypto.xpt | Bin 601 -> 0 bytes services/crypto/Makefile | 100 ----- services/sync/Weave.js | 2 +- .../sync/locales/en-US/fennec-firstrun.dtd | 9 - services/sync/locales/en-US/fennec-tabs.dtd | 1 - services/sync/locales/en-US/fx-prefs.dtd | 133 ------- .../sync/locales/en-US/fx-prefs.properties | 35 -- services/sync/locales/en-US/fx-tabs.dtd | 9 - .../locales/en-US/generic-change.properties | 34 -- services/sync/locales/en-US/sync.dtd | 18 - services/sync/modules/util.js | 2 +- services/sync/tests/unit/Makefile | 58 --- 13 files changed, 2 insertions(+), 758 deletions(-) delete mode 100644 services/crypto/IWeaveCrypto.h delete mode 100644 services/crypto/IWeaveCrypto.xpt delete mode 100644 services/crypto/Makefile delete mode 100644 services/sync/locales/en-US/fennec-firstrun.dtd delete mode 100644 services/sync/locales/en-US/fennec-tabs.dtd delete mode 100644 services/sync/locales/en-US/fx-prefs.dtd delete mode 100644 services/sync/locales/en-US/fx-prefs.properties delete mode 100644 services/sync/locales/en-US/fx-tabs.dtd delete mode 100644 services/sync/locales/en-US/generic-change.properties delete mode 100644 services/sync/locales/en-US/sync.dtd delete mode 100644 services/sync/tests/unit/Makefile diff --git a/services/crypto/IWeaveCrypto.h b/services/crypto/IWeaveCrypto.h deleted file mode 100644 index 8b8886ca98e1..000000000000 --- a/services/crypto/IWeaveCrypto.h +++ /dev/null @@ -1,359 +0,0 @@ -/* - * DO NOT EDIT. THIS FILE IS GENERATED FROM IWeaveCrypto.idl - */ - -#ifndef __gen_IWeaveCrypto_h__ -#define __gen_IWeaveCrypto_h__ - - -#ifndef __gen_nsISupports_h__ -#include "nsISupports.h" -#endif - -/* For IDL files that don't want to include root IDL files. */ -#ifndef NS_NO_VTABLE -#define NS_NO_VTABLE -#endif - -/* starting interface: IWeaveCrypto */ -#define IWEAVECRYPTO_IID_STR "f4463043-315e-41f3-b779-82e900e6fffa" - -#define IWEAVECRYPTO_IID \ - {0xf4463043, 0x315e, 0x41f3, \ - { 0xb7, 0x79, 0x82, 0xe9, 0x00, 0xe6, 0xff, 0xfa }} - -class NS_NO_VTABLE NS_SCRIPTABLE IWeaveCrypto : public nsISupports { - public: - - NS_DECLARE_STATIC_IID_ACCESSOR(IWEAVECRYPTO_IID) - - /** - * Shortcuts for some algorithm SEC OIDs. Full list available here: - * http://lxr.mozilla.org/seamonkey/source/security/nss/lib/util/secoidt.h - */ - enum { DES_EDE3_CBC = 156U }; - - enum { AES_128_CBC = 184U }; - - enum { AES_192_CBC = 186U }; - - enum { AES_256_CBC = 188U }; - - /** - * One of the above constants. Used as the mechanism for encrypting bulk - * data and wrapping keys. - * - * Default is AES_256_CBC. - */ - /* attribute unsigned long algorithm; */ - NS_SCRIPTABLE NS_IMETHOD GetAlgorithm(PRUint32 *aAlgorithm) = 0; - NS_SCRIPTABLE NS_IMETHOD SetAlgorithm(PRUint32 aAlgorithm) = 0; - - /** - * The size of the RSA key to create with generateKeypair(). - * - * Default is 2048. - */ - /* attribute unsigned long keypairBits; */ - NS_SCRIPTABLE NS_IMETHOD GetKeypairBits(PRUint32 *aKeypairBits) = 0; - NS_SCRIPTABLE NS_IMETHOD SetKeypairBits(PRUint32 aKeypairBits) = 0; - - /** - * Encrypt data using a symmetric key. - * The algorithm attribute specifies how the encryption is performed. - * - * @param clearText - * The data to be encrypted (not base64 encoded). - * @param symmetricKey - * A base64-encoded symmetric key (eg, one from generateRandomKey). - * @param iv - * A base64-encoded initialization vector - * @returns Encrypted data, base64 encoded - */ - /* ACString encrypt (in AUTF8String clearText, in ACString symmetricKey, in ACString iv); */ - NS_SCRIPTABLE NS_IMETHOD Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) = 0; - - /** - * Encrypt data using a symmetric key. - * The algorithm attribute specifies how the encryption is performed. - * - * @param cipherText - * The base64-encoded data to be decrypted - * @param symmetricKey - * A base64-encoded symmetric key (eg, one from unwrapSymmetricKey) - * @param iv - * A base64-encoded initialization vector - * @returns Decrypted data (not base64-encoded) - */ - /* AUTF8String decrypt (in ACString cipherText, in ACString symmetricKey, in ACString iv); */ - NS_SCRIPTABLE NS_IMETHOD Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) = 0; - - /** - * Generate a RSA public/private keypair. - * - * @param aPassphrase - * User's passphrase. Used with PKCS#5 to generate a symmetric key - * for wrapping the private key. - * @param aSalt - * Salt for the user's passphrase. - * @param aIV - * Random IV, used when wrapping the private key. - * @param aEncodedPublicKey - * The public key, base-64 encoded. - * @param aWrappedPrivateKey - * The public key, encrypted with the user's passphrase, and base-64 encoded. - */ - /* void generateKeypair (in ACString aPassphrase, in ACString aSalt, in ACString aIV, out ACString aEncodedPublicKey, out ACString aWrappedPrivateKey); */ - NS_SCRIPTABLE NS_IMETHOD GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM) = 0; - - /* ACString generateRandomKey (); */ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomKey(nsACString & _retval NS_OUTPARAM) = 0; - - /* ACString generateRandomIV (); */ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomIV(nsACString & _retval NS_OUTPARAM) = 0; - - /* ACString generateRandomBytes (in unsigned long aByteCount); */ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM) = 0; - - /** - * Encrypts a symmetric key with a user's public key. - * - * @param aSymmetricKey - * The base64 encoded string holding a symmetric key. - * @param aEncodedPublicKey - * The base64 encoded string holding a public key. - * @returns The wrapped symmetric key, base64 encoded - * - * For RSA, the unencoded public key is a PKCS#1 object. - */ - /* ACString wrapSymmetricKey (in ACString aSymmetricKey, in ACString aEncodedPublicKey); */ - NS_SCRIPTABLE NS_IMETHOD WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM) = 0; - - /** - * Decrypts a symmetric key with a user's private key. - * - * @param aWrappedSymmetricKey - * The base64 encoded string holding an encrypted symmetric key. - * @param aWrappedPrivateKey - * The base64 encoded string holdering an encrypted private key. - * @param aPassphrase - * The passphrase to decrypt the private key. - * @param aSalt - * The salt for the passphrase. - * @param aIV - * The random IV used when unwrapping the private key. - * @returns The unwrapped symmetric key, base64 encoded - * - * For RSA, the unencoded, decrypted key is a PKCS#1 object. - */ - /* ACString unwrapSymmetricKey (in ACString aWrappedSymmetricKey, in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV); */ - NS_SCRIPTABLE NS_IMETHOD UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM) = 0; - - /** - * Rewrap a private key with a new user passphrase. - * - * @param aWrappedPrivateKey - * The base64 encoded string holding an encrypted private key. - * @param aPassphrase - * The passphrase to decrypt the private key. - * @param aSalt - * The salt for the passphrase. - * @param aIV - * The random IV used when unwrapping the private key. - * @param aNewPassphrase - * The new passphrase to wrap the private key with. - * @returns The (re)wrapped private key, base64 encoded - * - */ - /* ACString rewrapPrivateKey (in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV, in ACString aNewPassphrase); */ - NS_SCRIPTABLE NS_IMETHOD RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM) = 0; - - /** - * Verify a user's passphrase against a private key. - * - * @param aWrappedPrivateKey - * The base64 encoded string holding an encrypted private key. - * @param aPassphrase - * The passphrase to decrypt the private key. - * @param aSalt - * The salt for the passphrase. - * @param aIV - * The random IV used when unwrapping the private key. - * @returns Boolean true if the passphrase decrypted the key correctly. - * - */ - /* boolean verifyPassphrase (in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV); */ - NS_SCRIPTABLE NS_IMETHOD VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM) = 0; - -}; - - NS_DEFINE_STATIC_IID_ACCESSOR(IWeaveCrypto, IWEAVECRYPTO_IID) - -/* Use this macro when declaring classes that implement this interface. */ -#define NS_DECL_IWEAVECRYPTO \ - NS_SCRIPTABLE NS_IMETHOD GetAlgorithm(PRUint32 *aAlgorithm); \ - NS_SCRIPTABLE NS_IMETHOD SetAlgorithm(PRUint32 aAlgorithm); \ - NS_SCRIPTABLE NS_IMETHOD GetKeypairBits(PRUint32 *aKeypairBits); \ - NS_SCRIPTABLE NS_IMETHOD SetKeypairBits(PRUint32 aKeypairBits); \ - NS_SCRIPTABLE NS_IMETHOD Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomKey(nsACString & _retval NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomIV(nsACString & _retval NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM); \ - NS_SCRIPTABLE NS_IMETHOD VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM); - -/* Use this macro to declare functions that forward the behavior of this interface to another object. */ -#define NS_FORWARD_IWEAVECRYPTO(_to) \ - NS_SCRIPTABLE NS_IMETHOD GetAlgorithm(PRUint32 *aAlgorithm) { return _to GetAlgorithm(aAlgorithm); } \ - NS_SCRIPTABLE NS_IMETHOD SetAlgorithm(PRUint32 aAlgorithm) { return _to SetAlgorithm(aAlgorithm); } \ - NS_SCRIPTABLE NS_IMETHOD GetKeypairBits(PRUint32 *aKeypairBits) { return _to GetKeypairBits(aKeypairBits); } \ - NS_SCRIPTABLE NS_IMETHOD SetKeypairBits(PRUint32 aKeypairBits) { return _to SetKeypairBits(aKeypairBits); } \ - NS_SCRIPTABLE NS_IMETHOD Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) { return _to Encrypt(clearText, symmetricKey, iv, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) { return _to Decrypt(cipherText, symmetricKey, iv, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM) { return _to GenerateKeypair(aPassphrase, aSalt, aIV, aEncodedPublicKey, aWrappedPrivateKey); } \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomKey(nsACString & _retval NS_OUTPARAM) { return _to GenerateRandomKey(_retval); } \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomIV(nsACString & _retval NS_OUTPARAM) { return _to GenerateRandomIV(_retval); } \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM) { return _to GenerateRandomBytes(aByteCount, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM) { return _to WrapSymmetricKey(aSymmetricKey, aEncodedPublicKey, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM) { return _to UnwrapSymmetricKey(aWrappedSymmetricKey, aWrappedPrivateKey, aPassphrase, aSalt, aIV, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM) { return _to RewrapPrivateKey(aWrappedPrivateKey, aPassphrase, aSalt, aIV, aNewPassphrase, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM) { return _to VerifyPassphrase(aWrappedPrivateKey, aPassphrase, aSalt, aIV, _retval); } - -/* Use this macro to declare functions that forward the behavior of this interface to another object in a safe way. */ -#define NS_FORWARD_SAFE_IWEAVECRYPTO(_to) \ - NS_SCRIPTABLE NS_IMETHOD GetAlgorithm(PRUint32 *aAlgorithm) { return !_to ? NS_ERROR_NULL_POINTER : _to->GetAlgorithm(aAlgorithm); } \ - NS_SCRIPTABLE NS_IMETHOD SetAlgorithm(PRUint32 aAlgorithm) { return !_to ? NS_ERROR_NULL_POINTER : _to->SetAlgorithm(aAlgorithm); } \ - NS_SCRIPTABLE NS_IMETHOD GetKeypairBits(PRUint32 *aKeypairBits) { return !_to ? NS_ERROR_NULL_POINTER : _to->GetKeypairBits(aKeypairBits); } \ - NS_SCRIPTABLE NS_IMETHOD SetKeypairBits(PRUint32 aKeypairBits) { return !_to ? NS_ERROR_NULL_POINTER : _to->SetKeypairBits(aKeypairBits); } \ - NS_SCRIPTABLE NS_IMETHOD Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->Encrypt(clearText, symmetricKey, iv, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->Decrypt(cipherText, symmetricKey, iv, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->GenerateKeypair(aPassphrase, aSalt, aIV, aEncodedPublicKey, aWrappedPrivateKey); } \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomKey(nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->GenerateRandomKey(_retval); } \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomIV(nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->GenerateRandomIV(_retval); } \ - NS_SCRIPTABLE NS_IMETHOD GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->GenerateRandomBytes(aByteCount, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->WrapSymmetricKey(aSymmetricKey, aEncodedPublicKey, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->UnwrapSymmetricKey(aWrappedSymmetricKey, aWrappedPrivateKey, aPassphrase, aSalt, aIV, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->RewrapPrivateKey(aWrappedPrivateKey, aPassphrase, aSalt, aIV, aNewPassphrase, _retval); } \ - NS_SCRIPTABLE NS_IMETHOD VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM) { return !_to ? NS_ERROR_NULL_POINTER : _to->VerifyPassphrase(aWrappedPrivateKey, aPassphrase, aSalt, aIV, _retval); } - -#if 0 -/* Use the code below as a template for the implementation class for this interface. */ - -/* Header file */ -class _MYCLASS_ : public IWeaveCrypto -{ -public: - NS_DECL_ISUPPORTS - NS_DECL_IWEAVECRYPTO - - _MYCLASS_(); - -private: - ~_MYCLASS_(); - -protected: - /* additional members */ -}; - -/* Implementation file */ -NS_IMPL_ISUPPORTS1(_MYCLASS_, IWeaveCrypto) - -_MYCLASS_::_MYCLASS_() -{ - /* member initializers and constructor code */ -} - -_MYCLASS_::~_MYCLASS_() -{ - /* destructor code */ -} - -/* attribute unsigned long algorithm; */ -NS_IMETHODIMP _MYCLASS_::GetAlgorithm(PRUint32 *aAlgorithm) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} -NS_IMETHODIMP _MYCLASS_::SetAlgorithm(PRUint32 aAlgorithm) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* attribute unsigned long keypairBits; */ -NS_IMETHODIMP _MYCLASS_::GetKeypairBits(PRUint32 *aKeypairBits) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} -NS_IMETHODIMP _MYCLASS_::SetKeypairBits(PRUint32 aKeypairBits) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* ACString encrypt (in AUTF8String clearText, in ACString symmetricKey, in ACString iv); */ -NS_IMETHODIMP _MYCLASS_::Encrypt(const nsACString & clearText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* AUTF8String decrypt (in ACString cipherText, in ACString symmetricKey, in ACString iv); */ -NS_IMETHODIMP _MYCLASS_::Decrypt(const nsACString & cipherText, const nsACString & symmetricKey, const nsACString & iv, nsACString & _retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* void generateKeypair (in ACString aPassphrase, in ACString aSalt, in ACString aIV, out ACString aEncodedPublicKey, out ACString aWrappedPrivateKey); */ -NS_IMETHODIMP _MYCLASS_::GenerateKeypair(const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & aEncodedPublicKey NS_OUTPARAM, nsACString & aWrappedPrivateKey NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* ACString generateRandomKey (); */ -NS_IMETHODIMP _MYCLASS_::GenerateRandomKey(nsACString & _retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* ACString generateRandomIV (); */ -NS_IMETHODIMP _MYCLASS_::GenerateRandomIV(nsACString & _retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* ACString generateRandomBytes (in unsigned long aByteCount); */ -NS_IMETHODIMP _MYCLASS_::GenerateRandomBytes(PRUint32 aByteCount, nsACString & _retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* ACString wrapSymmetricKey (in ACString aSymmetricKey, in ACString aEncodedPublicKey); */ -NS_IMETHODIMP _MYCLASS_::WrapSymmetricKey(const nsACString & aSymmetricKey, const nsACString & aEncodedPublicKey, nsACString & _retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* ACString unwrapSymmetricKey (in ACString aWrappedSymmetricKey, in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV); */ -NS_IMETHODIMP _MYCLASS_::UnwrapSymmetricKey(const nsACString & aWrappedSymmetricKey, const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, nsACString & _retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* ACString rewrapPrivateKey (in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV, in ACString aNewPassphrase); */ -NS_IMETHODIMP _MYCLASS_::RewrapPrivateKey(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, const nsACString & aNewPassphrase, nsACString & _retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* boolean verifyPassphrase (in ACString aWrappedPrivateKey, in ACString aPassphrase, in ACString aSalt, in ACString aIV); */ -NS_IMETHODIMP _MYCLASS_::VerifyPassphrase(const nsACString & aWrappedPrivateKey, const nsACString & aPassphrase, const nsACString & aSalt, const nsACString & aIV, PRBool *_retval NS_OUTPARAM) -{ - return NS_ERROR_NOT_IMPLEMENTED; -} - -/* End of implementation class template. */ -#endif - - -#endif /* __gen_IWeaveCrypto_h__ */ diff --git a/services/crypto/IWeaveCrypto.xpt b/services/crypto/IWeaveCrypto.xpt deleted file mode 100644 index 2ed7438b519b70a0a5bc2ab3444d9d0e404b96bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 601 zcmZvYO-lkn7{{O8Yy-idtKGVIO{FNhq^p8dByCh)(n%*>YG0ULH}*DklD2ZK!<|7|Lib9aErkZDeoe zrq8?8Wc{>z_{TjD`|zk9$Idal%B#509m1P{2%#_F5#dX~6DOA~hnarcf(c#F&?P3z zoA@%*OznnvA!t%Lx2R1S@#&7-52JiQ?51O(oiXL38Icct>cJHwt`S-m^_kfaRUP|} xVpOOKGXvQV0?N$uutGe~Z8PFgC=`uau~5v`^t=u^4C&0qgf}x|&aABu_!puOuPp!o diff --git a/services/crypto/Makefile b/services/crypto/Makefile deleted file mode 100644 index 7257b438212c..000000000000 --- a/services/crypto/Makefile +++ /dev/null @@ -1,100 +0,0 @@ -# -# ***** BEGIN LICENSE BLOCK ***** -# Version: MPL 1.1/GPL 2.0/LGPL 2.1 -# -# The contents of this file are subject to the Mozilla Public License Version -# 1.1 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS IS" basis, -# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -# for the specific language governing rights and limitations under the -# License. -# -# The Original Code is Weave code. -# -# The Initial Developer of the Original Code is Mozilla Foundation. -# Portions created by the Initial Developer are Copyright (C) 2008 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Dan Mills (original author) -# Justin Dolske -# -# Alternatively, the contents of this file may be used under the terms of -# either the GNU General Public License Version 2 or later (the "GPL"), or -# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), -# in which case the provisions of the GPL or the LGPL are applicable instead -# of those above. If you wish to allow use of your version of this file only -# under the terms of either the GPL or the LGPL, and not to allow others to -# use your version of this file under the terms of the MPL, indicate your -# decision by deleting the provisions above and replace them with the notice -# and other provisions required by the GPL or the LGPL. If you do not delete -# the provisions above, a recipient may use your version of this file under -# the terms of any one of the MPL, the GPL or the LGPL. -# -# ***** END LICENSE BLOCK ***** - -stage_dir=../dist/stage - -sdkdir ?= ${MOZSDKDIR} - -idl = IWeaveCrypto.idl -idl_typelib = $(idl:.idl=.xpt) -idl_header = $(idl:.idl=.h) - -# -# The only thing to actually build here is the IDL's .xpt/.h form, which -# requires an SDK. So don't do that unless explicitly requested, and use -# the files checked into Mercurial instead. -# -all: stage - -build: $(idl_typelib) $(idl_header) stage - -# No SDK is needed unless you're modifying the IDL interface, in which -# case we'll need to rebuild the .h and .xpt files. -xpidl = $(sdkdir)/bin/xpidl -ifdef CROSS_COMPILE -xpidl = $(sdkdir)/host/bin/host_xpidl -endif -$(idl_typelib): $(idl) -ifeq ($(sdkdir),) - $(warning No 'sdkdir' variable given) - $(warning It should point to the location of the Gecko SDK) - $(warning For example: "make sdkdir=/foo/bar/baz") - $(warning Or set the MOZSDKDIR environment variable to point to it) - $(error) -else - $(xpidl) -m typelib -I$(sdkdir)/idl $(@:.xpt=.idl) -endif - -$(idl_header): $(idl) -ifeq ($(sdkdir),) - $(warning No 'sdkdir' variable given) - $(warning It should point to the location of the Gecko SDK) - $(warning For example: "make sdkdir=/foo/bar/baz") - $(warning Or set the MOZSDKDIR environment variable to point to it) - $(error) -else - $(xpidl) -m header -I$(sdkdir)/idl $(@:.h=.idl) -endif - -stage: - mkdir -p $(stage_dir)/components -ifdef NO_SYMLINK - cp -v $(idl_typelib) $(stage_dir)/components - cp -v $(idl_header) $(TOPSRCDIR)/crypto-obsolete/src - cp -v WeaveCrypto.js $(stage_dir)/components -else - ln -vsf `pwd`/$(idl_typelib) $(stage_dir)/components - ln -vsf `pwd`/$(idl_header) $(TOPSRCDIR)/crypto-obsolete/src - ln -vsf `pwd`/WeaveCrypto.js $(stage_dir)/components -endif - -clean: - rm -f $(TOPSRCDIR)/crypto-obsolete/src/$(idl_header) - # maybe hg revert the .xpt/.h? - -.PHONY: all build stage clean diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 4057d7f5b0d2..e4bcad3a2d97 100755 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -87,7 +87,7 @@ AboutWeaveTabs.prototype = { newChannel: function(aURI) { let ios = Cc["@mozilla.org/network/io-service;1"] .getService(Ci.nsIIOService); - let ch = ios.newChannel("chrome://weave/content/fx-tabs.xul", null, null); + let ch = ios.newChannel("chrome://weave/content/firefox/tabs.xul", null, null); ch.originalURI = aURI; return ch; } diff --git a/services/sync/locales/en-US/fennec-firstrun.dtd b/services/sync/locales/en-US/fennec-firstrun.dtd deleted file mode 100644 index b3c56936f634..000000000000 --- a/services/sync/locales/en-US/fennec-firstrun.dtd +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/services/sync/locales/en-US/fennec-tabs.dtd b/services/sync/locales/en-US/fennec-tabs.dtd deleted file mode 100644 index 41387be9c39c..000000000000 --- a/services/sync/locales/en-US/fennec-tabs.dtd +++ /dev/null @@ -1 +0,0 @@ - diff --git a/services/sync/locales/en-US/fx-prefs.dtd b/services/sync/locales/en-US/fx-prefs.dtd deleted file mode 100644 index b1ff0f51cbf0..000000000000 --- a/services/sync/locales/en-US/fx-prefs.dtd +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/services/sync/locales/en-US/fx-prefs.properties b/services/sync/locales/en-US/fx-prefs.properties deleted file mode 100644 index fb7a8dddcc9f..000000000000 --- a/services/sync/locales/en-US/fx-prefs.properties +++ /dev/null @@ -1,35 +0,0 @@ -connect.label = Connect -disconnect.label = Disconnect -startSyncing.label = Start Syncing - -usernameNotAvailable.label = Already in use - -later.label = Later -cancelSetup.label = Cancel Setup - -passwordMismatch.label = Passwords do not match -passwordTooWeak.label = Password is too weak - -entriesMustMatch.label = Phrases do not match -cannotMatchPassword.label = Secret phrase cannot match password - -invalidEmail.label = Invalid email address - -errorCreatingAccount.title = Error Creating Account - -serverInvalid.label = Please enter a valid server URL - -loginFailed = Login failed: %1$S - -recoverPasswordSuccess.title = Recover Password Success! -recoverPasswordSuccess.label = We've sent you an email to your address on file. Please check it and follow the instructions to reset your password. - -additionalClients.label = and %S additional devices - -bookmarkCount.label = %S bookmarks -historyCount.label = %S days of history -passwordCount.label = %S passwords - -differentAccount.title = Use a Different Account? -differentAccount.label = This will reset all of your sync account information and preferences. -differentAccountConfirm.label = Reset All Information diff --git a/services/sync/locales/en-US/fx-tabs.dtd b/services/sync/locales/en-US/fx-tabs.dtd deleted file mode 100644 index 76f3eacd728c..000000000000 --- a/services/sync/locales/en-US/fx-tabs.dtd +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/services/sync/locales/en-US/generic-change.properties b/services/sync/locales/en-US/generic-change.properties deleted file mode 100644 index fbe969c2e2e8..000000000000 --- a/services/sync/locales/en-US/generic-change.properties +++ /dev/null @@ -1,34 +0,0 @@ -change.password.title = Change your Password -change.password.acceptButton = Change Password -change.password.status.active = Changing your password… -change.password.status.success = Your password has been changed. -change.password.status.error = There was an error changing your password. - -change.password.introText = Your password must be at least 8 characters long. It cannot be the same as either your user name or your secret phrase. -change.password.warningText = Note: All of your other devices will be unable to connect to your account once you change this password. - -change.passphrase.title = Change your Secret Phrase -change.passphrase.acceptButton = Change Secret Phrase -change.passphrase.label = Changing secret phrase and uploading local data, please wait… -change.passphrase.error = There was an error while changing your secret phrase! -change.passphrase.success = Your secret phrase was successfully changed! - -new.passphrase.label = New secret phrase -new.passphrase.confirm = Confirm secret phrase - -change.passphrase.introText = Your secret phrase must be at least 12 characters long. Sync uses this phrase as part of encrypting your data. -change.passphrase.introText2 = You may wish to write this down, as this is never sent over the Internet and is not backed up or synced by Firefox Sync for your security. -change.passphrase.warningText = Note: This will erase all data stored on the Sync server and upload new data secured by this phrase. Your other devices will not sync until the secret phrase is entered for that device. - -new.password.label = Enter your new password -new.password.confirm = Confirm your new password - -new.password.title = Update Password -new.password.introText = Your password was rejected by the server, please update your password. -new.password.acceptButton = Update Password -new.password.status.incorrect = Password incorrect, please try again. - -new.passphrase.title = Update Secret Phrase -new.passphrase.introText = Your secret phrase has changed, please enter your new secret phrase -new.passphrase.acceptButton = Update Secret Phrase -new.passphrase.status.incorrect = Secret phrase incorrect, please try again. \ No newline at end of file diff --git a/services/sync/locales/en-US/sync.dtd b/services/sync/locales/en-US/sync.dtd deleted file mode 100644 index bc2e08976fb7..000000000000 --- a/services/sync/locales/en-US/sync.dtd +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 56a8f922fb87..386645e224c2 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -370,7 +370,7 @@ let Utils = { }, lazyStrings: function Weave_lazyStrings(name) { - let bundle = "chrome://weave/locale/" + name + ".properties"; + let bundle = "chrome://weave/locale/services/" + name + ".properties"; return function() new StringBundle(bundle); }, diff --git a/services/sync/tests/unit/Makefile b/services/sync/tests/unit/Makefile deleted file mode 100644 index 346940183b32..000000000000 --- a/services/sync/tests/unit/Makefile +++ /dev/null @@ -1,58 +0,0 @@ -# profiledir needs to be an absolute path on Mac OS X (FIXME: file bug). -profiledir = $(abspath ../profile) -sys := $(shell uname -s) - -# OS detection - -ifeq ($(sys), Darwin) - os = Darwin -else -ifeq ($(sys), Linux) - os = Linux -else -ifeq ($(sys), MINGW32_NT-6.1) - os = WINNT -else -ifeq ($(sys), MINGW32_NT-5.1) - os = WINNT -else -ifeq ($(sys), SunOS) - os = SunOS -else - $(error Sorry, your os is unknown/unsupported: $(sys)) -endif -endif -endif -endif -endif - -topsrcdir ?= ${TOPSRCDIR} -native_topsrcdir ?= ${NATIVE_TOPSRCDIR} - -ifeq ($(topsrcdir),) -topsrcdir = ../.. -endif - -ifeq ($(native_topsrcdir),) -ifeq ($(os), WINNT) -native_topsrcdir = ..\.. -else -native_topsrcdir = ../.. -endif -endif - -# The path to the extension, in the native format, as required by the extension -# manager when it installs an extension via a file in the /extensions/ -# directory that contains the path to the extension. -ifeq ($(os), WINNT) -extensiondir = $(native_topsrcdir)\dist\stage -else -extensiondir = $(topsrcdir)/dist/stage -endif - -# A command that installs the extension into the profile when the harness -# creates a new profile. - -configure_profile = echo "$(extensiondir)" > $(profiledir)/extensions/\{340c2bbc-ce74-4362-90b5-7c26312808ef\}; - -include ../harness/Makefile From 7041263ab69e3c2beca920c7e30c9c4d2252b4de Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Thu, 10 Jun 2010 10:57:49 -0700 Subject: [PATCH 1715/1860] Bug 566061 - After server syncing, lightweight themes do not get applied to Chrome [r=mconnor] Make sure the pref exists (is a bool) before trying to access its value. --- services/sync/modules/engines/prefs.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index aaedc56bb565..924363900573 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -141,8 +141,12 @@ PrefStore.prototype = { try { Cu.import("resource://gre/modules/LightweightThemeManager.jsm", ltm); ltm = ltm.LightweightThemeManager; - enabledBefore = this._prefs.getBoolPref("lightweightThemes.isThemeSelected"); - prevTheme = ltm.currentTheme; + + let enabledPref = "lightweightThemes.isThemeSelected"; + if (this._prefs.getPrefType(enabledPref) == this._prefs.PREF_BOOL) { + enabledBefore = this._prefs.getBoolPref(enabledPref); + prevTheme = ltm.currentTheme; + } } catch(ex) { ltmExists = false; } // LightweightThemeManager only exists in Firefox 3.6+ From bbd2e9e2d49a47593ca1dd76763b51885c93382d Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 14 Jun 2010 10:30:37 -0700 Subject: [PATCH 1716/1860] Bug 546551 - Weave status bar notifications are too busy [r=mconnor] Remove status bar icon/text for setup/connect/sync status. Add menuitem for setting up sync instead of drilling down a 1-item submenu that leads to a button. Remove unused Engine.displayName and related strings. --- services/sync/locales/en-US/engines.properties | 8 -------- services/sync/locales/en-US/sync.properties | 7 ------- services/sync/modules/engines.js | 8 -------- services/sync/modules/util.js | 2 +- services/sync/tests/unit/test_engine.js | 2 +- 5 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 services/sync/locales/en-US/engines.properties diff --git a/services/sync/locales/en-US/engines.properties b/services/sync/locales/en-US/engines.properties deleted file mode 100644 index ff2fb7fe55a5..000000000000 --- a/services/sync/locales/en-US/engines.properties +++ /dev/null @@ -1,8 +0,0 @@ -#bookmarks -clients = Client Data -bookmarks = Bookmarks -tabs = Tabs -forms = Form History -history = Browsing History -prefs = Preferences -passwords = Passwords diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index c0b6895cc1a2..a978b55a85f0 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -10,13 +10,6 @@ connected.label = Connected: %S disconnected.label = Disconnected connecting.label = Connecting… -# %S is the engine being synced -syncing.label = Firefox Sync is syncing: %S - -status.offline = Not Signed In -status.privateBrowsing = Disabled (Private Browsing) -status.needsSetup = Set Up Firefox Sync… - mobile.label = Mobile Bookmarks remote.pending.label = Remote tabs are being synced… diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 23b600f4388c..201ee73c5df6 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -168,14 +168,6 @@ Engine.prototype = { return tracker; }, - get displayName() { - try { - return Str.engines.get(this.name); - } catch (e) {} - - return this.Name; - }, - sync: function Engine_sync() { if (!this.enabled) return; diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 386645e224c2..67f6cf2f48e0 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -926,5 +926,5 @@ Svc.Obs = Observers; ].forEach(function(lazy) Utils.lazySvc(Svc, lazy[0], lazy[1], lazy[2])); let Str = {}; -["engines", "errors", "sync"] +["errors", "sync"] .forEach(function(lazy) Utils.lazy2(Str, lazy, Utils.lazyStrings(lazy))); diff --git a/services/sync/tests/unit/test_engine.js b/services/sync/tests/unit/test_engine.js index e9fc36b87c71..7ff0e0147076 100644 --- a/services/sync/tests/unit/test_engine.js +++ b/services/sync/tests/unit/test_engine.js @@ -66,7 +66,7 @@ Observers.add("weave:engine:sync:finish", engineObserver); function test_members() { _("Engine object members"); let engine = new SteamEngine(); - do_check_eq(engine.displayName, "Steam"); + do_check_eq(engine.Name, "Steam"); do_check_eq(engine.prefName, "steam"); do_check_true(engine._store instanceof SteamStore); do_check_true(engine._tracker instanceof SteamTracker); From 5feea34af8a8ed3adbae3c8833b70bf91e67c7da Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Wed, 9 Jun 2010 17:22:03 -0700 Subject: [PATCH 1717/1860] Bug 570573 - Clean up uses of switch (Svc.AppInfo.ID) for app-specific hacks (Part 1) [r=mconnor] Weave.Service._registerEngines now reads the list of engines from a preference so that apps can specify it this way. The switch logic stays as fallback for the addon case. --- services/sync/modules/service.js | 29 ++++++++++++------- .../sync/tests/unit/test_service_startup.js | 4 ++- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ec46348efb41..7cf122c34f6f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -379,19 +379,26 @@ WeaveSvc.prototype = { */ _registerEngines: function WeaveSvc__registerEngines() { let engines = []; - switch (Svc.AppInfo.ID) { - case FENNEC_ID: - engines = ["Tab", "Bookmarks", "Form", "History", "Password"]; - break; + // Applications can provide this preference (comma-separated list) + // to specify which engines should be registered on startup. + let pref = Svc.Prefs.get("registerEngines"); + if (pref) { + engines = pref.split(","); + } else { + // Fallback for the add-on case + switch (Svc.AppInfo.ID) { + case FENNEC_ID: + engines = ["Tab", "Bookmarks", "Form", "History", "Password"]; + break; - case FIREFOX_ID: - case TEST_HARNESS_ID: - engines = ["Bookmarks", "Form", "History", "Password", "Prefs", "Tab"]; - break; + case FIREFOX_ID: + engines = ["Bookmarks", "Form", "History", "Password", "Prefs", "Tab"]; + break; - case SEAMONKEY_ID: - engines = ["Form", "History", "Password", "Tab"]; - break; + case SEAMONKEY_ID: + engines = ["Form", "History", "Password", "Tab"]; + break; + } } // Grab the actual engine and register them diff --git a/services/sync/tests/unit/test_service_startup.js b/services/sync/tests/unit/test_service_startup.js index 3d35780ebd9b..6b6a10acaced 100644 --- a/services/sync/tests/unit/test_service_startup.js +++ b/services/sync/tests/unit/test_service_startup.js @@ -10,6 +10,7 @@ function run_test() { Observers.add("weave:service:ready", function (subject, data) { observerCalled = true; }); + Svc.Prefs.set("registerEngines", "Tab,Bookmarks,Form,History"); Svc.Prefs.set("username", "johndoe"); try { @@ -20,7 +21,8 @@ function run_test() { _("Engines are registered."); let engines = Weave.Engines.getAll(); - do_check_true(!!engines.length); + do_check_true(Utils.deepEquals([engine.name for each (engine in engines)], + ['tabs', 'bookmarks', 'forms', 'history'])); _("Identities are registered."); do_check_eq(ID.get('WeaveID').username, "johndoe"); From 9d672683c5fabe0f8ab24a50b677a775ec476f74 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Thu, 10 Jun 2010 17:04:49 -0700 Subject: [PATCH 1718/1860] Bug 570573 - Clean up uses of switch (Svc.AppInfo.ID) for app-specific hacks (Part 2) [r=mconnor] Weave.Service.onStartup now triggers autoconnect after a fixed delay specified in a preference. If that preference is absent, nothing happens and apps are responsible for triggering autoconnect in a weave:service:ready observer themselves. Provide such observers for Firefox and Fennec. --- services/sync/modules/service.js | 42 +++++++++---------- .../sync/tests/unit/test_service_login.js | 17 ++++++-- .../sync/tests/unit/test_service_startup.js | 3 ++ 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 7cf122c34f6f..eed77f2963dd 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -294,30 +294,17 @@ WeaveSvc.prototype = { this._updateCachedURLs(); - // Send an event now that Weave service is ready - Svc.Obs.notify("weave:service:ready"); - - // Wait a little before checking how long to wait to autoconnect - if (this._checkSetup() == STATUS_OK && Svc.Prefs.get("autoconnect")) { - Utils.delay(function() { - // Figure out how many seconds to delay autoconnect based on the app - let wait = 3; - switch (Svc.AppInfo.ID) { - case FIREFOX_ID: - // Add one second delay for each busy tab in every window - let enum = Svc.WinMediator.getEnumerator("navigator:browser"); - while (enum.hasMoreElements()) { - Array.forEach(enum.getNext().gBrowser.mTabs, function(tab) { - wait += tab.hasAttribute("busy"); - }); - } - break; - } - - this._log.debug("Autoconnecting in " + wait + " seconds"); - Utils.delay(this._autoConnect, wait * 1000, this, "_autoTimer"); - }, 2000, this, "_autoTimer"); + // Applications can specify this preference if they want autoconnect + // to happen after a fixed delay. + let delay = Svc.Prefs.get("autoconnectDelay"); + if (delay) { + this.delayedAutoConnect(delay); } + + // Send an event now that Weave service is ready. We don't do this + // synchronously so that observers will definitely have access to the + // 'Weave' namespace. + Utils.delay(function() Svc.Obs.notify("weave:service:ready"), 0); }, _checkSetup: function WeaveSvc__checkSetup() { @@ -676,6 +663,15 @@ WeaveSvc.prototype = { Svc.Obs.notify("weave:service:start-over"); }, + delayedAutoConnect: function delayedAutoConnect(delay) { + if (this._loggedIn) + return; + + if (this._checkSetup() == STATUS_OK && Svc.Prefs.get("autoconnect")) { + Utils.delay(this._autoConnect, delay * 1000, this, "_autoTimer"); + } + }, + _autoConnect: let (attempts = 0) function _autoConnect() { let reason = Utils.mpLocked() ? "master password still locked" diff --git a/services/sync/tests/unit/test_service_login.js b/services/sync/tests/unit/test_service_login.js index 851dd28c51dc..2b17edfbef69 100644 --- a/services/sync/tests/unit/test_service_login.js +++ b/services/sync/tests/unit/test_service_login.js @@ -30,19 +30,28 @@ function run_test() { Weave.Service.serverURL = "http://localhost:8080/"; Weave.Service.clusterURL = "http://localhost:8080/"; + _("Initial state is ok."); + do_check_eq(Status.service, STATUS_OK); + _("Try logging in. It wont' work because we're not configured yet."); - do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); Weave.Service.login(); + do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); + do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME); do_check_false(Weave.Service.isLoggedIn); _("Try again with username and password set."); Weave.Service.username = "johndoe"; Weave.Service.password = "ilovejane"; - // We need a non-empty passphrase for login to work. Even the - // setup wizard just sets this to 'foo' if there isn't a - // passphrase yet. + Weave.Service.login(); + do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); + do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE); + do_check_false(Weave.Service.isLoggedIn); + + _("Success if passphrase is set."); Weave.Service.passphrase = "foo"; Weave.Service.login(); + do_check_eq(Status.service, STATUS_OK); + do_check_eq(Status.login, LOGIN_SUCCEEDED); do_check_true(Weave.Service.isLoggedIn); } finally { diff --git a/services/sync/tests/unit/test_service_startup.js b/services/sync/tests/unit/test_service_startup.js index 6b6a10acaced..f5ddfc5af368 100644 --- a/services/sync/tests/unit/test_service_startup.js +++ b/services/sync/tests/unit/test_service_startup.js @@ -1,6 +1,7 @@ Cu.import("resource://weave/util.js"); Cu.import("resource://weave/identity.js"); Cu.import("resource://weave/ext/Observers.js"); +Cu.import("resource://weave/ext/Sync.js"); function run_test() { _("When imported, Weave.Service.onStartup is called"); @@ -29,6 +30,8 @@ function run_test() { do_check_eq(ID.get('WeaveCryptoID').username, "johndoe"); _("Observers are notified of startup"); + // Synchronize with Weave.Service.onStartup's async notification + Sync.sleep(0); do_check_true(observerCalled); } finally { From c67af812e52667d561b7585bc612b7560c90507c Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Mon, 14 Jun 2010 21:03:39 +0100 Subject: [PATCH 1719/1860] Bug 570180 - Setup wizard sets passphrase='foo' [r=mconnor] Promote _verifyLogin to a public method so we have a way to query login status even with a non-existent or invalid passphrase. --- services/sync/modules/service.js | 21 +++++-- .../sync/tests/unit/test_service_login.js | 4 ++ .../tests/unit/test_service_verifyLogin.js | 59 +++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 services/sync/tests/unit/test_service_verifyLogin.js diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index eed77f2963dd..572f64b322b6 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -534,7 +534,7 @@ WeaveSvc.prototype = { return false; }, - _verifyLogin: function _verifyLogin() + verifyLogin: function verifyLogin() this._notify("verify-login", "", function() { // Make sure we have a cluster to verify against // this is a little weird, if we don't get a node we pretend @@ -545,11 +545,22 @@ WeaveSvc.prototype = { return true; } + if (!this.username) { + Status.login = LOGIN_FAILED_NO_USERNAME; + return false; + } + try { let test = new Resource(this.infoURL).get(); switch (test.status) { case 200: - // The user is authenticated, so check the passphrase now + // The user is authenticated. + if (!this.passphrase) { + Status.login = LOGIN_FAILED_NO_PASSPHRASE; + return false; + } + + // We also have a passphrase, so check it now. if (!this._verifyPassphrase()) { Status.login = LOGIN_FAILED_INVALID_PASSPHRASE; return false; @@ -563,7 +574,7 @@ WeaveSvc.prototype = { case 404: // Check that we're verifying with the correct cluster if (this._setCluster()) - return this._verifyLogin(); + return this.verifyLogin(); // We must have the right cluster, but the server doesn't expect us Status.login = LOGIN_FAILED_LOGIN_REJECTED; @@ -721,7 +732,7 @@ WeaveSvc.prototype = { this._log.info("Logging in user " + this.username); - if (!this._verifyLogin()) { + if (!this.verifyLogin()) { // verifyLogin sets the failure states here. throw "Login failed: " + Status.login; } @@ -901,7 +912,7 @@ WeaveSvc.prototype = { Sync.sleep(15000); // bug 545725 - re-verify creds and fail sanely - if (!this._verifyLogin()) { + if (!this.verifyLogin()) { Status.sync = CREDENTIALS_CHANGED; this._log.info("Credentials have changed, aborting sync and forcing re-login."); return false; diff --git a/services/sync/tests/unit/test_service_login.js b/services/sync/tests/unit/test_service_login.js index 2b17edfbef69..2f0e1fc58a84 100644 --- a/services/sync/tests/unit/test_service_login.js +++ b/services/sync/tests/unit/test_service_login.js @@ -29,6 +29,7 @@ function run_test() { try { Weave.Service.serverURL = "http://localhost:8080/"; Weave.Service.clusterURL = "http://localhost:8080/"; + Svc.Prefs.set("autoconnect", false); _("Initial state is ok."); do_check_eq(Status.service, STATUS_OK); @@ -38,6 +39,7 @@ function run_test() { do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME); do_check_false(Weave.Service.isLoggedIn); + do_check_false(Svc.Prefs.get("autoconnect")); _("Try again with username and password set."); Weave.Service.username = "johndoe"; @@ -46,6 +48,7 @@ function run_test() { do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE); do_check_false(Weave.Service.isLoggedIn); + do_check_false(Svc.Prefs.get("autoconnect")); _("Success if passphrase is set."); Weave.Service.passphrase = "foo"; @@ -53,6 +56,7 @@ function run_test() { do_check_eq(Status.service, STATUS_OK); do_check_eq(Status.login, LOGIN_SUCCEEDED); do_check_true(Weave.Service.isLoggedIn); + do_check_true(Svc.Prefs.get("autoconnect")); } finally { Svc.Prefs.resetBranch(""); diff --git a/services/sync/tests/unit/test_service_verifyLogin.js b/services/sync/tests/unit/test_service_verifyLogin.js new file mode 100644 index 000000000000..f3590f85b7f4 --- /dev/null +++ b/services/sync/tests/unit/test_service_verifyLogin.js @@ -0,0 +1,59 @@ +Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://weave/service.js"); +Cu.import("resource://weave/status.js"); +Cu.import("resource://weave/constants.js"); +Cu.import("resource://weave/util.js"); + +function login_handler(request, response) { + // btoa('johndoe:ilovejane') == am9obmRvZTppbG92ZWphbmU= + let body; + if (request.hasHeader("Authorization") && + request.getHeader("Authorization") == "Basic am9obmRvZTppbG92ZWphbmU=") { + body = "{}"; + response.setStatusLine(request.httpVersion, 200, "OK"); + } else { + body = "Unauthorized"; + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + } + response.bodyOutputStream.write(body, body.length); +} + +function run_test() { + let logger = Log4Moz.repository.rootLogger; + Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); + + let server = httpd_setup({ + "/1.0/johndoe/info/collections": login_handler + }); + + try { + Weave.Service.serverURL = "http://localhost:8080/"; + Weave.Service.clusterURL = "http://localhost:8080/"; + + _("Initial state is ok."); + do_check_eq(Status.service, STATUS_OK); + + _("Credentials won't check out because we're not configured yet."); + do_check_false(Weave.Service.verifyLogin()); + do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); + do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME); + + _("Try again with username and password set."); + Weave.Service.username = "johndoe"; + Weave.Service.password = "ilovejane"; + do_check_false(Weave.Service.verifyLogin()); + do_check_eq(Status.service, CLIENT_NOT_CONFIGURED); + do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE); + + _("Success if passphrase is set."); + Weave.Service.passphrase = "foo"; + Weave.Service.login(); + do_check_eq(Status.service, STATUS_OK); + do_check_eq(Status.login, LOGIN_SUCCEEDED); + do_check_true(Weave.Service.isLoggedIn); + + } finally { + Svc.Prefs.resetBranch(""); + server.stop(function() {}); + } +} From 3d97d049bafa03bc22d1b019e0228dafd5d5e6bd Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 11 Jun 2010 11:36:51 -0700 Subject: [PATCH 1720/1860] Bug 562878 - Override for machine name [r=mconnor] Allow setting of the computer name during Firefox account setup and from prefs. Also allow setting device name from Fennec prefs. Fix up strings for Fennec. --- services/sync/locales/en-US/sync.properties | 4 ++-- services/sync/modules/engines/clients.js | 25 +++++++-------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/services/sync/locales/en-US/sync.properties b/services/sync/locales/en-US/sync.properties index a978b55a85f0..ed7ce4a88e48 100644 --- a/services/sync/locales/en-US/sync.properties +++ b/services/sync/locales/en-US/sync.properties @@ -1,5 +1,5 @@ -# %1: the user name (Ed), %2: the app name (Firefox), %3: the hostname (Comp) -client.name = %1$S's %2$S on %3$S +# %1: the user name (Ed), %2: the app name (Firefox), %3: the operating system (Android) +client.name2 = %1$S's %2$S on %3$S # %S is the date and time at which the last sync successfully completed lastSync.label = Last Update: %S diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index ae77d4ef1560..a06fdb3ad955 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -40,6 +40,7 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +Cu.import("resource://weave/ext/StringBundle.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/engines.js"); @@ -127,24 +128,14 @@ ClientEngine.prototype = { return localName; // Generate a client name if we don't have a useful one yet - let user = Svc.Env.get("USER") || Svc.Env.get("USERNAME"); - let app = Svc.AppInfo.name; - let host = Svc.SysInfo.get("host"); + let user = Svc.Env.get("USER") || Svc.Env.get("USERNAME") || + Svc.Prefs.get("username"); + let brand = new StringBundle("chrome://branding/locale/brand.properties"); + let app = brand.get("brandShortName"); + let os = Cc["@mozilla.org/network/protocol;1?name=http"]. + getService(Ci.nsIHttpProtocolHandler).oscpu; - // Try figuring out the name of the current profile - let prof = Svc.Directory.get("ProfD", Components.interfaces.nsIFile).path; - let profiles = Svc.Profiles.profiles; - while (profiles.hasMoreElements()) { - let profile = profiles.getNext().QueryInterface(Ci.nsIToolkitProfile); - if (prof == profile.rootDir.path) { - // Only bother adding the profile name if it's not "default" - if (profile.name != "default") - host = profile.name + "-" + host; - break; - } - } - - return this.localName = Str.sync.get("client.name", [user, app, host]); + return this.localName = Str.sync.get("client.name2", [user, app, os]); }, set localName(value) Svc.Prefs.set("client.name", value), From b4446a52a8efaa078cbec4683fcd1535759bebc9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 14 Jun 2010 15:16:53 -0700 Subject: [PATCH 1721/1860] Bug 568677 - Failure to get CryptoMeta assumes it's missing [r=mconnor] Explicitly check the status code to make sure we stop syncing on non-404 crypto meta failures. Add tests to check correct behavior of 404 and non-404 errors during syncStartup. --- services/sync/modules/constants.js | 1 + services/sync/modules/engines.js | 6 ++ .../sync/tests/unit/test_syncengine_sync.js | 88 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index b1e446d1dae1..127795669d30 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -121,6 +121,7 @@ NO_SYNC_NODE_FOUND: "error.sync.reason.no_node_found", ENGINE_UPLOAD_FAIL: "error.engine.reason.record_upload_fail", ENGINE_DOWNLOAD_FAIL: "error.engine.reason.record_download_fail", ENGINE_UNKNOWN_FAIL: "error.engine.reason.unknown_fail", +ENGINE_METARECORD_DOWNLOAD_FAIL: "error.engine.reason.metarecord_download_fail", ENGINE_METARECORD_UPLOAD_FAIL: "error.engine.reason.metarecord_upload_fail", // Ways that a sync can be disabled (messages only to be printed in debug log) diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index 201ee73c5df6..e8f7bef03373 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -363,6 +363,12 @@ SyncEngine.prototype = { meta = null; } } + // Don't proceed if we failed to get the crypto meta for reasons not 404 + else if (CryptoMetas.response.status != 404) { + let resp = CryptoMetas.response; + resp.failureCode = ENGINE_METARECORD_DOWNLOAD_FAIL; + throw resp; + } // Determine if we need to wipe on outdated versions let metaGlobal = Records.get(this.metaURL); diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js index 342f4711977e..b2414ff2a633 100644 --- a/services/sync/tests/unit/test_syncengine_sync.js +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -230,6 +230,94 @@ function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() { } } +function test_syncStartup_metaGet404() { + _("SyncEngine._syncStartup resets sync and wipes server data if the symmetric key is missing 404"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + + // A symmetric key with an incorrect HMAC + let crypto_steam = new ServerWBO("steam"); + + // A proper global record with matching version and syncID + let engine = makeSteamEngine(); + let global = new ServerWBO("global", + {engines: {steam: {version: engine.version, + syncID: engine.syncID}}}); + + // Some server side data that's going to be wiped + let collection = new ServerCollection(); + collection.wbos.flying = new ServerWBO( + "flying", encryptPayload({id: "flying", + denomination: "LNER Class A3 4472"})); + collection.wbos.scotsman = new ServerWBO( + "scotsman", encryptPayload({id: "scotsman", + denomination: "Flying Scotsman"})); + + let server = sync_httpd_setup({ + "/1.0/foo/storage/crypto/steam": crypto_steam.handler(), + "/1.0/foo/storage/steam": collection.handler() + }); + createAndUploadKeypair(); + + try { + + _("Confirm initial environment"); + do_check_false(!!crypto_steam.payload); + do_check_true(!!collection.wbos.flying.payload); + do_check_true(!!collection.wbos.scotsman.payload); + + engine.lastSync = Date.now() / 1000; + engine._syncStartup(); + + _("Sync was reset and server data was wiped"); + do_check_eq(engine.lastSync, 0); + do_check_eq(collection.wbos.flying.payload, undefined); + do_check_eq(collection.wbos.scotsman.payload, undefined); + + _("New bulk key was uploaded"); + key = crypto_steam.data.keyring["http://localhost:8080/1.0/foo/storage/keys/pubkey"]; + do_check_eq(key.wrapped, "fake-symmetric-key-0"); + do_check_eq(key.hmac, "fake-symmetric-key-0 "); + + } finally { + server.stop(function() {}); + Svc.Prefs.resetBranch(""); + Records.clearCache(); + CryptoMetas.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} + +function test_syncStartup_failedMetaGet() { + _("SyncEngine._syncStartup non-404 failures for getting cryptometa should stop sync"); + + Svc.Prefs.set("clusterURL", "http://localhost:8080/"); + let server = httpd_setup({ + "/1.0/foo/storage/crypto/steam": function(request, response) { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + response.bodyOutputStream.write("Fail!", 5); + } + }); + + let engine = makeSteamEngine(); + try { + + _("Getting the cryptometa will fail and should set the appropriate failure"); + let error; + try { + engine._syncStartup(); + } catch (ex) { + error = ex; + } + do_check_eq(error.failureCode, ENGINE_METARECORD_DOWNLOAD_FAIL); + + } finally { + server.stop(function() {}); + Svc.Prefs.resetBranch(""); + Records.clearCache(); + syncTesting = new SyncTestingInfrastructure(makeSteamEngine); + } +} function test_syncStartup_serverHasNewerVersion() { _("SyncEngine._syncStartup "); From 830b170d9646637de850b3c089198be7b4baae04 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Jun 2010 14:30:08 -0700 Subject: [PATCH 1722/1860] Bug 570636 - Decide how to co-exist as a sync add-on and built-in sync [r=mconnor] Map the modules directory to services-sync instead of weave and update imports. --- services/sync/Weave.js | 2 +- services/sync/modules/auth.js | 2 +- .../sync/modules/base_records/collection.js | 4 +- services/sync/modules/base_records/crypto.js | 8 +-- services/sync/modules/base_records/keys.js | 10 +-- services/sync/modules/base_records/wbo.js | 6 +- services/sync/modules/engines.js | 27 ++++---- services/sync/modules/engines/bookmarks.js | 12 ++-- services/sync/modules/engines/clients.js | 12 ++-- services/sync/modules/engines/forms.js | 10 +-- services/sync/modules/engines/history.js | 10 +-- services/sync/modules/engines/passwords.js | 16 ++--- services/sync/modules/engines/prefs.js | 10 +-- services/sync/modules/engines/tabs.js | 12 ++-- services/sync/modules/identity.js | 8 +-- services/sync/modules/notifications.js | 6 +- services/sync/modules/resource.js | 14 ++--- services/sync/modules/service.js | 61 +++++++++---------- services/sync/modules/status.js | 2 +- services/sync/modules/stores.js | 6 +- services/sync/modules/trackers.js | 8 +-- .../sync/modules/type_records/bookmark.js | 4 +- services/sync/modules/type_records/clients.js | 4 +- services/sync/modules/type_records/forms.js | 4 +- services/sync/modules/type_records/history.js | 4 +- .../sync/modules/type_records/passwords.js | 4 +- services/sync/modules/type_records/prefs.js | 4 +- services/sync/modules/type_records/tabs.js | 4 +- services/sync/modules/util.js | 12 ++-- .../tests/unit/attic/test_bookmark_sharing.js | 8 +-- .../sync/tests/unit/attic/test_sharing.js | 6 +- services/sync/tests/unit/attic/test_xmpp.js | 2 +- .../sync/tests/unit/attic/test_xmpp_simple.js | 2 +- .../unit/attic/test_xmpp_transport_http.js | 2 +- .../sync/tests/unit/fake_login_manager.js | 2 +- services/sync/tests/unit/head_first.js | 14 ++--- .../unit/need-work/test_bookmark_syncing.js | 6 +- .../tests/unit/need-work/test_cookie_store.js | 2 +- .../need-work/test_passphrase_checking.js | 8 +-- .../unit/need-work/test_password_syncing.js | 2 +- .../tests/unit/need-work/test_passwords.js | 2 +- .../sync/tests/unit/need-work/test_service.js | 8 +-- .../unit/need-work/test_util_tracebacks.js | 2 +- services/sync/tests/unit/test_auth_manager.js | 10 +-- .../tests/unit/test_bookmark_batch_fail.js | 2 +- .../sync/tests/unit/test_bookmark_order.js | 6 +- .../tests/unit/test_bookmark_predecessor.js | 4 +- .../sync/tests/unit/test_clients_escape.js | 8 +-- .../tests/unit/test_collection_inc_get.js | 4 +- services/sync/tests/unit/test_engine.js | 10 +-- .../sync/tests/unit/test_enginemanager.js | 2 +- .../tests/unit/test_engines_forms_store.js | 4 +- services/sync/tests/unit/test_load_modules.js | 4 +- services/sync/tests/unit/test_log4moz.js | 2 +- .../sync/tests/unit/test_notifications.js | 2 +- .../sync/tests/unit/test_records_crypto.js | 12 ++-- .../tests/unit/test_records_cryptometa.js | 6 +- services/sync/tests/unit/test_records_keys.js | 10 +-- services/sync/tests/unit/test_records_wbo.js | 14 ++--- services/sync/tests/unit/test_resource.js | 12 ++-- .../tests/unit/test_service_attributes.js | 12 ++-- .../sync/tests/unit/test_service_login.js | 10 +-- .../sync/tests/unit/test_service_startup.js | 10 +-- .../tests/unit/test_service_verifyLogin.js | 10 +-- services/sync/tests/unit/test_status.js | 4 +- services/sync/tests/unit/test_syncengine.js | 4 +- .../sync/tests/unit/test_syncengine_sync.js | 21 +++---- .../tests/unit/test_tracker_addChanged.js | 2 +- services/sync/tests/unit/test_utils_anno.js | 2 +- services/sync/tests/unit/test_utils_catch.js | 2 +- .../sync/tests/unit/test_utils_deepEquals.js | 2 +- .../sync/tests/unit/test_utils_deferGetSet.js | 2 +- services/sync/tests/unit/test_utils_json.js | 2 +- services/sync/tests/unit/test_utils_lazy.js | 2 +- services/sync/tests/unit/test_utils_lazy2.js | 2 +- .../sync/tests/unit/test_utils_lazySvc.js | 2 +- services/sync/tests/unit/test_utils_lock.js | 2 +- .../sync/tests/unit/test_utils_makeGUID.js | 2 +- .../sync/tests/unit/test_utils_makeURI.js | 2 +- services/sync/tests/unit/test_utils_notify.js | 2 +- .../sync/tests/unit/test_utils_queryAsync.js | 2 +- services/sync/tests/unit/test_utils_sha1.js | 2 +- .../sync/tests/unit/test_utils_sha256HMAC.js | 2 +- .../sync/tests/unit/test_utils_stackTrace.js | 2 +- 84 files changed, 283 insertions(+), 286 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index e4bcad3a2d97..197a48e7ce93 100755 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -63,7 +63,7 @@ WeaveService.prototype = { this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this.timer.initWithCallback({ notify: function() { - Cu.import("resource://weave/service.js"); + Cu.import("resource://services-sync/service.js"); } }, 10000, Ci.nsITimer.TYPE_ONE_SHOT); break; diff --git a/services/sync/modules/auth.js b/services/sync/modules/auth.js index 0a5f47545d4a..89a190214bda 100644 --- a/services/sync/modules/auth.js +++ b/services/sync/modules/auth.js @@ -41,7 +41,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); Utils.lazy(this, 'Auth', AuthMgr); diff --git a/services/sync/modules/base_records/collection.js b/services/sync/modules/base_records/collection.js index 002d0fb25f41..8cb1fb1b089f 100644 --- a/services/sync/modules/base_records/collection.js +++ b/services/sync/modules/base_records/collection.js @@ -41,8 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/resource.js"); +Cu.import("resource://services-sync/resource.js"); +Cu.import("resource://services-sync/util.js"); function Collection(uri, recordObj) { Resource.call(this, uri); diff --git a/services/sync/modules/base_records/crypto.js b/services/sync/modules/base_records/crypto.js index 14fb7b8fe557..ba313968ac74 100644 --- a/services/sync/modules/base_records/crypto.js +++ b/services/sync/modules/base_records/crypto.js @@ -41,10 +41,10 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/wbo.js"); -Cu.import("resource://weave/base_records/keys.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/base_records/wbo.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/util.js"); function CryptoWrapper(uri) { this.cleartext = {}; diff --git a/services/sync/modules/base_records/keys.js b/services/sync/modules/base_records/keys.js index 7bc7eafa2b23..82d8c7e3336b 100644 --- a/services/sync/modules/base_records/keys.js +++ b/services/sync/modules/base_records/keys.js @@ -42,11 +42,11 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://services-sync/base_records/wbo.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/resource.js"); +Cu.import("resource://services-sync/util.js"); function PubKey(uri) { WBORecord.call(this, uri); diff --git a/services/sync/modules/base_records/wbo.js b/services/sync/modules/base_records/wbo.js index 710c50ae0dee..ffc2da5fccfe 100644 --- a/services/sync/modules/base_records/wbo.js +++ b/services/sync/modules/base_records/wbo.js @@ -41,9 +41,9 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/resource.js"); +Cu.import("resource://services-sync/util.js"); function WBORecord(uri) { this.data = {}; diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js index e8f7bef03373..0d55fe8e01bb 100644 --- a/services/sync/modules/engines.js +++ b/services/sync/modules/engines.js @@ -42,20 +42,19 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/ext/Observers.js"); -Cu.import("resource://weave/ext/Sync.js"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); - -Cu.import("resource://weave/base_records/wbo.js"); -Cu.import("resource://weave/base_records/keys.js"); -Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/collection.js"); +Cu.import("resource://services-sync/base_records/collection.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/base_records/wbo.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/ext/Sync.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/resource.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/util.js"); // Singleton service, holds registered engines diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js index a69a5b650096..886aa36eda60 100644 --- a/services/sync/modules/engines/bookmarks.js +++ b/services/sync/modules/engines/bookmarks.js @@ -53,12 +53,12 @@ catch(ex) { Cu.import("resource://gre/modules/utils.js"); } Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/ext/Observers.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/type_records/bookmark.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/type_records/bookmark.js"); +Cu.import("resource://services-sync/util.js"); function archiveBookmarks() { // Some nightly builds of 3.7 don't have this function diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index a06fdb3ad955..e9cefd2906d6 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -40,12 +40,12 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -Cu.import("resource://weave/ext/StringBundle.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/type_records/clients.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/ext/StringBundle.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/type_records/clients.js"); +Cu.import("resource://services-sync/util.js"); Utils.lazy(this, "Clients", ClientEngine); diff --git a/services/sync/modules/engines/forms.js b/services/sync/modules/engines/forms.js index e19f7e647dd7..69bec968a95c 100644 --- a/services/sync/modules/engines/forms.js +++ b/services/sync/modules/engines/forms.js @@ -41,11 +41,11 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/type_records/forms.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/type_records/forms.js"); +Cu.import("resource://services-sync/util.js"); let FormWrapper = { getAllEntries: function getAllEntries() { diff --git a/services/sync/modules/engines/history.js b/services/sync/modules/engines/history.js index 8385cda8ce78..e117f4e100a5 100644 --- a/services/sync/modules/engines/history.js +++ b/services/sync/modules/engines/history.js @@ -43,11 +43,11 @@ const Cu = Components.utils; const GUID_ANNO = "weave/guid"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/type_records/history.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/type_records/history.js"); +Cu.import("resource://services-sync/util.js"); // Create some helper functions to handle GUIDs function setGUID(uri, guid) { diff --git a/services/sync/modules/engines/passwords.js b/services/sync/modules/engines/passwords.js index 067575f0d31b..8721436d2ff4 100644 --- a/services/sync/modules/engines/passwords.js +++ b/services/sync/modules/engines/passwords.js @@ -41,14 +41,14 @@ const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/base_records/collection.js"); -Cu.import("resource://weave/ext/Observers.js"); -Cu.import("resource://weave/type_records/passwords.js"); +Cu.import("resource://services-sync/base_records/collection.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/type_records/passwords.js"); +Cu.import("resource://services-sync/util.js"); function PasswordEngine() { SyncEngine.call(this, "Passwords"); diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index 924363900573..c150bf6cbccd 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -43,11 +43,11 @@ const Cu = Components.utils; const WEAVE_SYNC_PREFS = "extensions.weave.prefs.sync."; const WEAVE_PREFS_GUID = "preferences"; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/type_records/prefs.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/type_records/prefs.js"); +Cu.import("resource://services-sync/util.js"); function PrefsEngine() { SyncEngine.call(this, "Prefs"); diff --git a/services/sync/modules/engines/tabs.js b/services/sync/modules/engines/tabs.js index 83b645a38978..5f120e116935 100644 --- a/services/sync/modules/engines/tabs.js +++ b/services/sync/modules/engines/tabs.js @@ -42,12 +42,12 @@ const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/type_records/tabs.js"); -Cu.import("resource://weave/engines/clients.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/engines/clients.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/type_records/tabs.js"); +Cu.import("resource://services-sync/util.js"); function TabEngine() { SyncEngine.call(this, "Tabs"); diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 2a3ef3b9aac7..6c32a0478100 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -41,10 +41,10 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/ext/Sync.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/ext/Sync.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/util.js"); Utils.lazy(this, 'ID', IDManager); diff --git a/services/sync/modules/notifications.js b/services/sync/modules/notifications.js index dcb8d25c3a49..e8e596ee42d4 100644 --- a/services/sync/modules/notifications.js +++ b/services/sync/modules/notifications.js @@ -41,9 +41,9 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/ext/Observers.js"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/util.js"); let Notifications = { // Match the referenced values in toolkit/content/widgets/notification.xml. diff --git a/services/sync/modules/resource.js b/services/sync/modules/resource.js index cbc055636dfd..a97d8f83042b 100644 --- a/services/sync/modules/resource.js +++ b/services/sync/modules/resource.js @@ -42,13 +42,13 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/ext/Observers.js"); -Cu.import("resource://weave/ext/Preferences.js"); -Cu.import("resource://weave/ext/Sync.js"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/auth.js"); +Cu.import("resource://services-sync/auth.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/ext/Preferences.js"); +Cu.import("resource://services-sync/ext/Sync.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/util.js"); // = Resource = // diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 572f64b322b6..d6ac48d2c2e1 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -50,40 +50,39 @@ const IDLE_TIME = 5; // xxxmpc: in seconds, should be preffable const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://weave/ext/Sync.js"); -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/auth.js"); -Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/base_records/wbo.js"); -Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/status.js"); -Cu.import("resource://weave/engines/clients.js"); +Cu.import("resource://services-sync/auth.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/base_records/wbo.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/engines/clients.js"); +Cu.import("resource://services-sync/ext/Sync.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/resource.js"); +Cu.import("resource://services-sync/status.js"); +Cu.import("resource://services-sync/util.js"); // for export let Weave = {}; -Cu.import("resource://weave/constants.js", Weave); -Cu.import("resource://weave/util.js", Weave); -Cu.import("resource://weave/auth.js", Weave); -Cu.import("resource://weave/resource.js", Weave); -Cu.import("resource://weave/base_records/keys.js", Weave); -Cu.import("resource://weave/notifications.js", Weave); -Cu.import("resource://weave/identity.js", Weave); -Cu.import("resource://weave/status.js", Weave); -Cu.import("resource://weave/stores.js", Weave); -Cu.import("resource://weave/engines.js", Weave); - -Cu.import("resource://weave/engines/bookmarks.js", Weave); -Cu.import("resource://weave/engines/clients.js", Weave); -Cu.import("resource://weave/engines/forms.js", Weave); -Cu.import("resource://weave/engines/history.js", Weave); -Cu.import("resource://weave/engines/prefs.js", Weave); -Cu.import("resource://weave/engines/passwords.js", Weave); -Cu.import("resource://weave/engines/tabs.js", Weave); +Cu.import("resource://services-sync/auth.js", Weave); +Cu.import("resource://services-sync/constants.js", Weave); +Cu.import("resource://services-sync/base_records/keys.js", Weave); +Cu.import("resource://services-sync/engines.js", Weave); +Cu.import("resource://services-sync/engines/bookmarks.js", Weave); +Cu.import("resource://services-sync/engines/clients.js", Weave); +Cu.import("resource://services-sync/engines/forms.js", Weave); +Cu.import("resource://services-sync/engines/history.js", Weave); +Cu.import("resource://services-sync/engines/prefs.js", Weave); +Cu.import("resource://services-sync/engines/passwords.js", Weave); +Cu.import("resource://services-sync/engines/tabs.js", Weave); +Cu.import("resource://services-sync/identity.js", Weave); +Cu.import("resource://services-sync/notifications.js", Weave); +Cu.import("resource://services-sync/resource.js", Weave); +Cu.import("resource://services-sync/status.js", Weave); +Cu.import("resource://services-sync/stores.js", Weave); +Cu.import("resource://services-sync/util.js", Weave); Utils.lazy(Weave, 'Service', WeaveSvc); diff --git a/services/sync/modules/status.js b/services/sync/modules/status.js index 2312e1bc07f4..58248c6da779 100644 --- a/services/sync/modules/status.js +++ b/services/sync/modules/status.js @@ -39,7 +39,7 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/constants.js"); +Cu.import("resource://services-sync/constants.js"); let Status = { get login() this._login, diff --git a/services/sync/modules/stores.js b/services/sync/modules/stores.js index da15572f034f..b0866cf93429 100644 --- a/services/sync/modules/stores.js +++ b/services/sync/modules/stores.js @@ -41,9 +41,9 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/util.js"); /* * Data Stores diff --git a/services/sync/modules/trackers.js b/services/sync/modules/trackers.js index 53c2c67b5343..644248bda85c 100644 --- a/services/sync/modules/trackers.js +++ b/services/sync/modules/trackers.js @@ -41,10 +41,10 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/ext/Observers.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/util.js"); /* * Trackers are associated with a single engine and deal with diff --git a/services/sync/modules/type_records/bookmark.js b/services/sync/modules/type_records/bookmark.js index e410a0de0c00..37d8290ddfce 100644 --- a/services/sync/modules/type_records/bookmark.js +++ b/services/sync/modules/type_records/bookmark.js @@ -42,8 +42,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/util.js"); function PlacesItem(uri, type) { CryptoWrapper.call(this, uri); diff --git a/services/sync/modules/type_records/clients.js b/services/sync/modules/type_records/clients.js index a9ff330609b4..a8f23b337675 100644 --- a/services/sync/modules/type_records/clients.js +++ b/services/sync/modules/type_records/clients.js @@ -41,8 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/util.js"); function ClientsRec(uri) { CryptoWrapper.call(this, uri); diff --git a/services/sync/modules/type_records/forms.js b/services/sync/modules/type_records/forms.js index 818ec06d8bb6..5f1c37233185 100644 --- a/services/sync/modules/type_records/forms.js +++ b/services/sync/modules/type_records/forms.js @@ -41,8 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/util.js"); function FormRec(uri) { CryptoWrapper.call(this, uri); diff --git a/services/sync/modules/type_records/history.js b/services/sync/modules/type_records/history.js index 3b102af44a79..a9b0d7c802d8 100644 --- a/services/sync/modules/type_records/history.js +++ b/services/sync/modules/type_records/history.js @@ -41,8 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/util.js"); function HistoryRec(uri) { CryptoWrapper.call(this, uri); diff --git a/services/sync/modules/type_records/passwords.js b/services/sync/modules/type_records/passwords.js index c4229993fc94..2ea1cc4ace5c 100644 --- a/services/sync/modules/type_records/passwords.js +++ b/services/sync/modules/type_records/passwords.js @@ -41,8 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/util.js"); function LoginRec(uri) { CryptoWrapper.call(this, uri); diff --git a/services/sync/modules/type_records/prefs.js b/services/sync/modules/type_records/prefs.js index 043a200c1d6a..842aba8ec306 100644 --- a/services/sync/modules/type_records/prefs.js +++ b/services/sync/modules/type_records/prefs.js @@ -41,8 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/util.js"); function PrefRec(uri) { CryptoWrapper.call(this, uri); diff --git a/services/sync/modules/type_records/tabs.js b/services/sync/modules/type_records/tabs.js index 7a85cdd4e42f..81aafa968f2b 100644 --- a/services/sync/modules/type_records/tabs.js +++ b/services/sync/modules/type_records/tabs.js @@ -41,8 +41,8 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/util.js"); function TabSetRecord(uri) { CryptoWrapper.call(this, uri); diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 67f6cf2f48e0..0f046b312de8 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -41,12 +41,12 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; -Cu.import("resource://weave/ext/Preferences.js"); -Cu.import("resource://weave/ext/Observers.js"); -Cu.import("resource://weave/ext/StringBundle.js"); -Cu.import("resource://weave/ext/Sync.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/ext/Preferences.js"); +Cu.import("resource://services-sync/ext/StringBundle.js"); +Cu.import("resource://services-sync/ext/Sync.js"); +Cu.import("resource://services-sync/log4moz.js"); /* * Utility functions diff --git a/services/sync/tests/unit/attic/test_bookmark_sharing.js b/services/sync/tests/unit/attic/test_bookmark_sharing.js index 4ce15428ec5e..da0e4703d118 100644 --- a/services/sync/tests/unit/attic/test_bookmark_sharing.js +++ b/services/sync/tests/unit/attic/test_bookmark_sharing.js @@ -1,7 +1,7 @@ -Cu.import("resource://weave/engines/bookmarks.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/sharing.js"); +Cu.import("resource://services-sync/async.js"); +Cu.import("resource://services-sync/engines/bookmarks.js"); +Cu.import("resource://services-sync/sharing.js"); +Cu.import("resource://services-sync/util.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/tests/unit/attic/test_sharing.js b/services/sync/tests/unit/attic/test_sharing.js index c3944a871688..1afe639eea4f 100644 --- a/services/sync/tests/unit/attic/test_sharing.js +++ b/services/sync/tests/unit/attic/test_sharing.js @@ -1,6 +1,6 @@ -Cu.import("resource://weave/sharing.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/identity.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/sharing.js"); +Cu.import("resource://services-sync/util.js"); function runTestGenerator() { let self = yield; diff --git a/services/sync/tests/unit/attic/test_xmpp.js b/services/sync/tests/unit/attic/test_xmpp.js index 9c76eed9308a..fa682bbde920 100644 --- a/services/sync/tests/unit/attic/test_xmpp.js +++ b/services/sync/tests/unit/attic/test_xmpp.js @@ -1,4 +1,4 @@ -Cu.import( "resource://weave/xmpp/xmppClient.js" ); +Cu.import( "resource://services-sync/xmpp/xmppClient.js" ); function LOG(aMsg) { dump("TEST_XMPP: " + aMsg + "\n"); diff --git a/services/sync/tests/unit/attic/test_xmpp_simple.js b/services/sync/tests/unit/attic/test_xmpp_simple.js index b6b54eabd04b..1516a0417744 100644 --- a/services/sync/tests/unit/attic/test_xmpp_simple.js +++ b/services/sync/tests/unit/attic/test_xmpp_simple.js @@ -2,7 +2,7 @@ function LOG(aMsg) { dump("TEST_XMPP_SIMPLE: " + aMsg + "\n"); } -Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); +Components.utils.import( "resource://services-sync/xmpp/xmppClient.js" ); var serverUrl = "http://localhost:5280/http-poll"; var jabberDomain = "localhost"; diff --git a/services/sync/tests/unit/attic/test_xmpp_transport_http.js b/services/sync/tests/unit/attic/test_xmpp_transport_http.js index 73b2896465fe..88082398afbe 100644 --- a/services/sync/tests/unit/attic/test_xmpp_transport_http.js +++ b/services/sync/tests/unit/attic/test_xmpp_transport_http.js @@ -2,7 +2,7 @@ function LOG(aMsg) { dump("TEST_XMPP_TRANSPORT_HTTP: " + aMsg + "\n"); } -Components.utils.import( "resource://weave/xmpp/xmppClient.js" ); +Components.utils.import( "resource://services-sync/xmpp/xmppClient.js" ); var tests = []; diff --git a/services/sync/tests/unit/fake_login_manager.js b/services/sync/tests/unit/fake_login_manager.js index ceba356aebb9..e45a13aca284 100644 --- a/services/sync/tests/unit/fake_login_manager.js +++ b/services/sync/tests/unit/fake_login_manager.js @@ -1,4 +1,4 @@ -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); // ---------------------------------------- // Fake Sample Data diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_first.js index 585b13fe271b..cac8d8077f53 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_first.js @@ -61,7 +61,7 @@ function loadInSandbox(aUri) { } function FakeTimerService() { - Cu.import("resource://weave/util.js"); + Cu.import("resource://services-sync/util.js"); this.callbackQueue = []; @@ -87,7 +87,7 @@ function FakeTimerService() { Utils.makeTimerForCall = self.makeTimerForCall; }; -Cu.import("resource://weave/log4moz.js"); +Cu.import("resource://services-sync/log4moz.js"); function getTestLogger(component) { return Log4Moz.repository.getLogger("Testing"); } @@ -123,7 +123,7 @@ function initTestLogging(level) { } function FakePrefService(contents) { - Cu.import("resource://weave/util.js"); + Cu.import("resource://services-sync/util.js"); this.fakeContents = contents; Utils.__prefs = this; } @@ -146,7 +146,7 @@ FakePrefService.prototype = { }; function FakePasswordService(contents) { - Cu.import("resource://weave/util.js"); + Cu.import("resource://services-sync/util.js"); this.fakeContents = contents; let self = this; @@ -312,8 +312,8 @@ function SyncTestingInfrastructure(engineFactory) { "xmpp.enabled" : false }; - Cu.import("resource://weave/identity.js"); - Cu.import("resource://weave/util.js"); + Cu.import("resource://services-sync/identity.js"); + Cu.import("resource://services-sync/util.js"); ID.set('WeaveID', new Identity('Mozilla Services Encryption Passphrase', 'foo')); @@ -427,7 +427,7 @@ function SyncTestingInfrastructure(engineFactory) { let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" ")); _("Setting the identity for passphrase"); -Cu.import("resource://weave/identity.js"); +Cu.import("resource://services-sync/identity.js"); let passphrase = ID.set("WeaveCryptoID", new Identity()); passphrase.password = "passphrase"; diff --git a/services/sync/tests/unit/need-work/test_bookmark_syncing.js b/services/sync/tests/unit/need-work/test_bookmark_syncing.js index 26dbf6c090d9..937d88eefa6a 100644 --- a/services/sync/tests/unit/need-work/test_bookmark_syncing.js +++ b/services/sync/tests/unit/need-work/test_bookmark_syncing.js @@ -1,6 +1,6 @@ -Cu.import("resource://weave/engines/bookmarks.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/async.js"); +Cu.import("resource://services-sync/async.js"); +Cu.import("resource://services-sync/engines/bookmarks.js"); +Cu.import("resource://services-sync/util.js"); Function.prototype.async = Async.sugar; diff --git a/services/sync/tests/unit/need-work/test_cookie_store.js b/services/sync/tests/unit/need-work/test_cookie_store.js index 691eb3817f56..c80343bc39e6 100644 --- a/services/sync/tests/unit/need-work/test_cookie_store.js +++ b/services/sync/tests/unit/need-work/test_cookie_store.js @@ -1,4 +1,4 @@ -Components.utils.import("resource://weave/engines/cookies.js"); +Components.utils.import("resource://services-sync/engines/cookies.js"); function FakeCookie( host, path, name, value, isSecure, isHttpOnly, isSession, expiry ) { diff --git a/services/sync/tests/unit/need-work/test_passphrase_checking.js b/services/sync/tests/unit/need-work/test_passphrase_checking.js index a8ccabb706b4..1c2e238b1a09 100644 --- a/services/sync/tests/unit/need-work/test_passphrase_checking.js +++ b/services/sync/tests/unit/need-work/test_passphrase_checking.js @@ -1,7 +1,7 @@ -Cu.import("resource://weave/dav.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/crypto.js"); -Cu.import("resource://weave/identity.js"); +Cu.import("resource://services-sync/async.js"); +Cu.import("resource://services-sync/crypto.js"); +Cu.import("resource://services-sync/dav.js"); +Cu.import("resource://services-sync/identity.js"); Function.prototype.async = Async.sugar; let __fakeCryptoID = { diff --git a/services/sync/tests/unit/need-work/test_password_syncing.js b/services/sync/tests/unit/need-work/test_password_syncing.js index e65e60ebab86..0d57b08561fc 100644 --- a/services/sync/tests/unit/need-work/test_password_syncing.js +++ b/services/sync/tests/unit/need-work/test_password_syncing.js @@ -1,4 +1,4 @@ -Cu.import("resource://weave/engines/passwords.js"); +Cu.import("resource://services-sync/engines/passwords.js"); load("fake_login_manager.js"); diff --git a/services/sync/tests/unit/need-work/test_passwords.js b/services/sync/tests/unit/need-work/test_passwords.js index 9639aad66d1c..9b75fccc91ca 100644 --- a/services/sync/tests/unit/need-work/test_passwords.js +++ b/services/sync/tests/unit/need-work/test_passwords.js @@ -3,7 +3,7 @@ load("fake_login_manager.js"); var loginMgr = new FakeLoginManager(fakeSampleLogins); // The JS module we're testing, with all members exposed. -var passwords = loadInSandbox("resource://weave/engines/passwords.js"); +var passwords = loadInSandbox("resource://services-sync/engines/passwords.js"); function test_hashLoginInfo_works() { var pwStore = new passwords.PasswordStore(); diff --git a/services/sync/tests/unit/need-work/test_service.js b/services/sync/tests/unit/need-work/test_service.js index 70911bc00737..953a2285e684 100644 --- a/services/sync/tests/unit/need-work/test_service.js +++ b/services/sync/tests/unit/need-work/test_service.js @@ -1,6 +1,6 @@ -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/async.js"); -Cu.import("resource://weave/crypto.js"); +Cu.import("resource://services-sync/async.js"); +Cu.import("resource://services-sync/crypto.js"); +Cu.import("resource://services-sync/log4moz.js"); Function.prototype.async = Async.sugar; @@ -19,7 +19,7 @@ let __fakeDAVContents = { "public/pubkey" : '{"version":1,"algorithm":"RSA"}' }; -let Service = loadInSandbox("resource://weave/service.js"); +let Service = loadInSandbox("resource://services-sync/service.js"); function TestService() { this.__superclassConstructor = Service.WeaveSvc; diff --git a/services/sync/tests/unit/need-work/test_util_tracebacks.js b/services/sync/tests/unit/need-work/test_util_tracebacks.js index c304768173ca..57824819219b 100644 --- a/services/sync/tests/unit/need-work/test_util_tracebacks.js +++ b/services/sync/tests/unit/need-work/test_util_tracebacks.js @@ -1,4 +1,4 @@ -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function _reportException(e) { dump("Exception caught.\n"); diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js index d6dde51cda42..e685c95161d3 100644 --- a/services/sync/tests/unit/test_auth_manager.js +++ b/services/sync/tests/unit/test_auth_manager.js @@ -1,8 +1,8 @@ -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/auth.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/resource.js"); +Cu.import("resource://services-sync/auth.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/resource.js"); +Cu.import("resource://services-sync/util.js"); let logger; let Httpd = {}; diff --git a/services/sync/tests/unit/test_bookmark_batch_fail.js b/services/sync/tests/unit/test_bookmark_batch_fail.js index 8833c1ee5cb0..ceed3c0f911d 100644 --- a/services/sync/tests/unit/test_bookmark_batch_fail.js +++ b/services/sync/tests/unit/test_bookmark_batch_fail.js @@ -1,5 +1,5 @@ _("Making sure a failing sync reports a useful error"); -Cu.import("resource://weave/engines/bookmarks.js"); +Cu.import("resource://services-sync/engines/bookmarks.js"); function run_test() { let engine = new BookmarksEngine(); diff --git a/services/sync/tests/unit/test_bookmark_order.js b/services/sync/tests/unit/test_bookmark_order.js index 486bb58a57b8..dba583eea0cd 100644 --- a/services/sync/tests/unit/test_bookmark_order.js +++ b/services/sync/tests/unit/test_bookmark_order.js @@ -1,7 +1,7 @@ _("Making sure after processing incoming bookmarks, they show up in the right order"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines/bookmarks.js"); -Cu.import("resource://weave/type_records/bookmark.js"); +Cu.import("resource://services-sync/engines/bookmarks.js"); +Cu.import("resource://services-sync/type_records/bookmark.js"); +Cu.import("resource://services-sync/util.js"); function getBookmarks(folderId) { let bookmarks = []; diff --git a/services/sync/tests/unit/test_bookmark_predecessor.js b/services/sync/tests/unit/test_bookmark_predecessor.js index a80bcee47c7d..6007d53b76a6 100644 --- a/services/sync/tests/unit/test_bookmark_predecessor.js +++ b/services/sync/tests/unit/test_bookmark_predecessor.js @@ -1,6 +1,6 @@ _("Make sure bad bookmarks can still get their predecessors"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines/bookmarks.js"); +Cu.import("resource://services-sync/engines/bookmarks.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("Starting with a clean slate of no bookmarks"); diff --git a/services/sync/tests/unit/test_clients_escape.js b/services/sync/tests/unit/test_clients_escape.js index a165b3b8e8f4..e6ceb5ee8057 100644 --- a/services/sync/tests/unit/test_clients_escape.js +++ b/services/sync/tests/unit/test_clients_escape.js @@ -1,7 +1,7 @@ -Cu.import("resource://weave/engines/clients.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/keys.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/engines/clients.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let baseUri = "http://fakebase/"; diff --git a/services/sync/tests/unit/test_collection_inc_get.js b/services/sync/tests/unit/test_collection_inc_get.js index 8550a5e5fb1c..2987b2d9a8a6 100644 --- a/services/sync/tests/unit/test_collection_inc_get.js +++ b/services/sync/tests/unit/test_collection_inc_get.js @@ -1,6 +1,6 @@ _("Make sure Collection can correctly incrementally parse GET requests"); -Cu.import("resource://weave/base_records/collection.js"); -Cu.import("resource://weave/base_records/wbo.js"); +Cu.import("resource://services-sync/base_records/collection.js"); +Cu.import("resource://services-sync/base_records/wbo.js"); function run_test() { let coll = new Collection("", WBORecord); diff --git a/services/sync/tests/unit/test_engine.js b/services/sync/tests/unit/test_engine.js index 7ff0e0147076..4fb4c44007de 100644 --- a/services/sync/tests/unit/test_engine.js +++ b/services/sync/tests/unit/test_engine.js @@ -1,8 +1,8 @@ -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/engines.js"); -Cu.import("resource://weave/ext/Observers.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/util.js"); function SteamStore() { diff --git a/services/sync/tests/unit/test_enginemanager.js b/services/sync/tests/unit/test_enginemanager.js index 2ac0bab07238..4822e4c12ba2 100644 --- a/services/sync/tests/unit/test_enginemanager.js +++ b/services/sync/tests/unit/test_enginemanager.js @@ -1,4 +1,4 @@ -Cu.import("resource://weave/engines.js"); +Cu.import("resource://services-sync/engines.js"); function run_test() { _("We start out with a clean slate"); diff --git a/services/sync/tests/unit/test_engines_forms_store.js b/services/sync/tests/unit/test_engines_forms_store.js index bf41295452d7..9caa4c68c2b6 100644 --- a/services/sync/tests/unit/test_engines_forms_store.js +++ b/services/sync/tests/unit/test_engines_forms_store.js @@ -1,6 +1,6 @@ _("Make sure the form store follows the Store api and correctly accesses the backend form storage"); -Cu.import("resource://weave/engines/forms.js"); -Cu.import("resource://weave/type_records/forms.js"); +Cu.import("resource://services-sync/engines/forms.js"); +Cu.import("resource://services-sync/type_records/forms.js"); function run_test() { let store = new FormEngine()._store; diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js index c01c358661c6..cbe399ef99d5 100644 --- a/services/sync/tests/unit/test_load_modules.js +++ b/services/sync/tests/unit/test_load_modules.js @@ -34,8 +34,8 @@ const modules = [ function run_test() { for each (let m in modules) { - _("Attempting to load resource://weave/" + m); - Cu.import("resource://weave/" + m, {}); + _("Attempting to load resource://services-sync/" + m); + Cu.import("resource://services-sync/" + m, {}); } } diff --git a/services/sync/tests/unit/test_log4moz.js b/services/sync/tests/unit/test_log4moz.js index 1c5feb864ab0..f48ee8b9f2fb 100644 --- a/services/sync/tests/unit/test_log4moz.js +++ b/services/sync/tests/unit/test_log4moz.js @@ -1,4 +1,4 @@ -Components.utils.import("resource://weave/log4moz.js"); +Components.utils.import("resource://services-sync/log4moz.js"); function MockAppender(formatter) { this._formatter = formatter; diff --git a/services/sync/tests/unit/test_notifications.js b/services/sync/tests/unit/test_notifications.js index 3c27ded47291..9d6da1d2db36 100644 --- a/services/sync/tests/unit/test_notifications.js +++ b/services/sync/tests/unit/test_notifications.js @@ -1,4 +1,4 @@ -Cu.import("resource://weave/notifications.js"); +Cu.import("resource://services-sync/notifications.js"); function run_test() { var logStats = initTestLogging("Info"); diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index b34ff9ae7a3b..cd374a9b4ce7 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -1,10 +1,10 @@ try { - Cu.import("resource://weave/log4moz.js"); - Cu.import("resource://weave/util.js"); - Cu.import("resource://weave/auth.js"); - Cu.import("resource://weave/identity.js"); - Cu.import("resource://weave/base_records/keys.js"); - Cu.import("resource://weave/base_records/crypto.js"); + Cu.import("resource://services-sync/base_records/crypto.js"); + Cu.import("resource://services-sync/base_records/keys.js"); + Cu.import("resource://services-sync/auth.js"); + Cu.import("resource://services-sync/log4moz.js"); + Cu.import("resource://services-sync/identity.js"); + Cu.import("resource://services-sync/util.js"); } catch (e) { do_throw(e); } diff --git a/services/sync/tests/unit/test_records_cryptometa.js b/services/sync/tests/unit/test_records_cryptometa.js index e3bf1dce223d..1fe680d63196 100644 --- a/services/sync/tests/unit/test_records_cryptometa.js +++ b/services/sync/tests/unit/test_records_cryptometa.js @@ -1,6 +1,6 @@ -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/base_records/crypto.js"); -Cu.import("resource://weave/base_records/keys.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("Generating keypair to encrypt/decrypt symkeys"); diff --git a/services/sync/tests/unit/test_records_keys.js b/services/sync/tests/unit/test_records_keys.js index 5d713b1dc327..607309bc59ae 100644 --- a/services/sync/tests/unit/test_records_keys.js +++ b/services/sync/tests/unit/test_records_keys.js @@ -1,9 +1,9 @@ try { - Cu.import("resource://weave/log4moz.js"); - Cu.import("resource://weave/util.js"); - Cu.import("resource://weave/auth.js"); - Cu.import("resource://weave/identity.js"); - Cu.import("resource://weave/base_records/keys.js"); + Cu.import("resource://services-sync/base_records/keys.js"); + Cu.import("resource://services-sync/auth.js"); + Cu.import("resource://services-sync/log4moz.js"); + Cu.import("resource://services-sync/identity.js"); + Cu.import("resource://services-sync/util.js"); } catch (e) { do_throw(e); } function pubkey_handler(metadata, response) { diff --git a/services/sync/tests/unit/test_records_wbo.js b/services/sync/tests/unit/test_records_wbo.js index 782f9b0a5b66..317c6046d168 100644 --- a/services/sync/tests/unit/test_records_wbo.js +++ b/services/sync/tests/unit/test_records_wbo.js @@ -1,11 +1,11 @@ try { - Cu.import("resource://weave/log4moz.js"); - Cu.import("resource://weave/util.js"); - Cu.import("resource://weave/auth.js"); - Cu.import("resource://weave/identity.js"); - Cu.import("resource://weave/resource.js"); - Cu.import("resource://weave/base_records/wbo.js"); - Cu.import("resource://weave/base_records/collection.js"); + Cu.import("resource://services-sync/auth.js"); + Cu.import("resource://services-sync/base_records/wbo.js"); + Cu.import("resource://services-sync/base_records/collection.js"); + Cu.import("resource://services-sync/identity.js"); + Cu.import("resource://services-sync/log4moz.js"); + Cu.import("resource://services-sync/resource.js"); + Cu.import("resource://services-sync/util.js"); } catch (e) { do_throw(e); } function record_handler(metadata, response) { diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index 81da7492dccd..7e1e87423c02 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -1,9 +1,9 @@ -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/auth.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/ext/Observers.js"); +Cu.import("resource://services-sync/auth.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/resource.js"); +Cu.import("resource://services-sync/util.js"); let logger; let Httpd = {}; diff --git a/services/sync/tests/unit/test_service_attributes.js b/services/sync/tests/unit/test_service_attributes.js index 17ff0b238d83..04a05ce526f4 100644 --- a/services/sync/tests/unit/test_service_attributes.js +++ b/services/sync/tests/unit/test_service_attributes.js @@ -1,9 +1,9 @@ -Cu.import("resource://weave/service.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/status.js"); -Cu.import("resource://weave/base_records/keys.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/status.js"); +Cu.import("resource://services-sync/util.js"); function test_urlsAndIdentities() { _("Various Weave.Service properties correspond to preference settings and update other object properties upon being set."); diff --git a/services/sync/tests/unit/test_service_login.js b/services/sync/tests/unit/test_service_login.js index 2f0e1fc58a84..b6b58a1daa69 100644 --- a/services/sync/tests/unit/test_service_login.js +++ b/services/sync/tests/unit/test_service_login.js @@ -1,8 +1,8 @@ -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/service.js"); -Cu.import("resource://weave/status.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/status.js"); +Cu.import("resource://services-sync/util.js"); function login_handler(request, response) { // btoa('johndoe:ilovejane') == am9obmRvZTppbG92ZWphbmU= diff --git a/services/sync/tests/unit/test_service_startup.js b/services/sync/tests/unit/test_service_startup.js index f5ddfc5af368..42f830a7c1e1 100644 --- a/services/sync/tests/unit/test_service_startup.js +++ b/services/sync/tests/unit/test_service_startup.js @@ -1,7 +1,7 @@ -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/ext/Observers.js"); -Cu.import("resource://weave/ext/Sync.js"); +Cu.import("resource://services-sync/ext/Observers.js"); +Cu.import("resource://services-sync/ext/Sync.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("When imported, Weave.Service.onStartup is called"); @@ -15,7 +15,7 @@ function run_test() { Svc.Prefs.set("username", "johndoe"); try { - Cu.import("resource://weave/service.js"); + Cu.import("resource://services-sync/service.js"); _("Service is enabled."); do_check_eq(Weave.Service.enabled, true); diff --git a/services/sync/tests/unit/test_service_verifyLogin.js b/services/sync/tests/unit/test_service_verifyLogin.js index f3590f85b7f4..d7201f3621ee 100644 --- a/services/sync/tests/unit/test_service_verifyLogin.js +++ b/services/sync/tests/unit/test_service_verifyLogin.js @@ -1,8 +1,8 @@ -Cu.import("resource://weave/log4moz.js"); -Cu.import("resource://weave/service.js"); -Cu.import("resource://weave/status.js"); -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/status.js"); +Cu.import("resource://services-sync/util.js"); function login_handler(request, response) { // btoa('johndoe:ilovejane') == am9obmRvZTppbG92ZWphbmU= diff --git a/services/sync/tests/unit/test_status.js b/services/sync/tests/unit/test_status.js index 8beadc76b7a9..bc2d67f425f7 100644 --- a/services/sync/tests/unit/test_status.js +++ b/services/sync/tests/unit/test_status.js @@ -1,5 +1,5 @@ -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/status.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/status.js"); function run_test() { diff --git a/services/sync/tests/unit/test_syncengine.js b/services/sync/tests/unit/test_syncengine.js index 67f7bd4c0108..a20f8410dedc 100644 --- a/services/sync/tests/unit/test_syncengine.js +++ b/services/sync/tests/unit/test_syncengine.js @@ -1,5 +1,5 @@ -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/engines.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/util.js"); function makeSteamEngine() { return new SyncEngine('Steam'); diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js index b2414ff2a633..16059d3c5c8b 100644 --- a/services/sync/tests/unit/test_syncengine_sync.js +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -1,14 +1,13 @@ -Cu.import("resource://weave/constants.js"); -Cu.import("resource://weave/util.js"); -Cu.import("resource://weave/identity.js"); -Cu.import("resource://weave/resource.js"); -Cu.import("resource://weave/stores.js"); -Cu.import("resource://weave/trackers.js"); -Cu.import("resource://weave/engines.js"); - -Cu.import("resource://weave/base_records/wbo.js"); -Cu.import("resource://weave/base_records/keys.js"); -Cu.import("resource://weave/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/base_records/wbo.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/engines.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/resource.js"); +Cu.import("resource://services-sync/stores.js"); +Cu.import("resource://services-sync/trackers.js"); +Cu.import("resource://services-sync/util.js"); /* * A fake engine implementation. diff --git a/services/sync/tests/unit/test_tracker_addChanged.js b/services/sync/tests/unit/test_tracker_addChanged.js index 7f4af1bf9875..114712150e23 100644 --- a/services/sync/tests/unit/test_tracker_addChanged.js +++ b/services/sync/tests/unit/test_tracker_addChanged.js @@ -1,4 +1,4 @@ -Cu.import("resource://weave/trackers.js"); +Cu.import("resource://services-sync/trackers.js"); function run_test() { let tracker = new Tracker(); diff --git a/services/sync/tests/unit/test_utils_anno.js b/services/sync/tests/unit/test_utils_anno.js index 3e33f4334d18..bbbb63c26cd8 100644 --- a/services/sync/tests/unit/test_utils_anno.js +++ b/services/sync/tests/unit/test_utils_anno.js @@ -1,5 +1,5 @@ _("Make sure various combinations of anno arguments do the right get/set for pages/items"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("set an anno on an item 1"); diff --git a/services/sync/tests/unit/test_utils_catch.js b/services/sync/tests/unit/test_utils_catch.js index deff0bb24777..8233b297d393 100644 --- a/services/sync/tests/unit/test_utils_catch.js +++ b/services/sync/tests/unit/test_utils_catch.js @@ -1,5 +1,5 @@ _("Make sure catch when copied to an object will correctly catch stuff"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let ret, rightThis, didCall, didThrow; diff --git a/services/sync/tests/unit/test_utils_deepEquals.js b/services/sync/tests/unit/test_utils_deepEquals.js index ec8bad523de0..c75fa0cfa256 100644 --- a/services/sync/tests/unit/test_utils_deepEquals.js +++ b/services/sync/tests/unit/test_utils_deepEquals.js @@ -1,5 +1,5 @@ _("Make sure Utils.deepEquals correctly finds items that are deeply equal"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let data = '[NaN, undefined, null, true, false, Infinity, 0, 1, "a", "b", {a: 1}, {a: "a"}, [{a: 1}], [{a: true}], {a: 1, b: 2}, [1, 2], [1, 2, 3]]'; diff --git a/services/sync/tests/unit/test_utils_deferGetSet.js b/services/sync/tests/unit/test_utils_deferGetSet.js index e12502cef489..9b6277baa286 100644 --- a/services/sync/tests/unit/test_utils_deferGetSet.js +++ b/services/sync/tests/unit/test_utils_deferGetSet.js @@ -1,5 +1,5 @@ _("Make sure various combinations of deferGetSet arguments correctly defer getting/setting properties to another object"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let base = function() {}; diff --git a/services/sync/tests/unit/test_utils_json.js b/services/sync/tests/unit/test_utils_json.js index 17f2a997cfa3..d373ea0fbb61 100644 --- a/services/sync/tests/unit/test_utils_json.js +++ b/services/sync/tests/unit/test_utils_json.js @@ -1,5 +1,5 @@ _("Make sure json saves and loads from disk"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("Do a simple write of an array to json and read"); diff --git a/services/sync/tests/unit/test_utils_lazy.js b/services/sync/tests/unit/test_utils_lazy.js index 31dffb76cd52..4df5dc7450fa 100644 --- a/services/sync/tests/unit/test_utils_lazy.js +++ b/services/sync/tests/unit/test_utils_lazy.js @@ -1,5 +1,5 @@ _("Make sure lazy constructor calling/assignment works"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let count = 0; diff --git a/services/sync/tests/unit/test_utils_lazy2.js b/services/sync/tests/unit/test_utils_lazy2.js index 1120d15feb98..094c9dd8d1a0 100644 --- a/services/sync/tests/unit/test_utils_lazy2.js +++ b/services/sync/tests/unit/test_utils_lazy2.js @@ -1,5 +1,5 @@ _("Make sure lazy2 function calling/assignment works"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let count = 0; diff --git a/services/sync/tests/unit/test_utils_lazySvc.js b/services/sync/tests/unit/test_utils_lazySvc.js index dda2db7e6f35..f1add8a86a72 100644 --- a/services/sync/tests/unit/test_utils_lazySvc.js +++ b/services/sync/tests/unit/test_utils_lazySvc.js @@ -1,5 +1,5 @@ _("Make sure lazySvc get the desired services"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("Load the xul app info service as obj.app"); diff --git a/services/sync/tests/unit/test_utils_lock.js b/services/sync/tests/unit/test_utils_lock.js index f5fd4b88d31d..fc4c04be0a93 100644 --- a/services/sync/tests/unit/test_utils_lock.js +++ b/services/sync/tests/unit/test_utils_lock.js @@ -1,5 +1,5 @@ _("Make sure lock prevents calling with a shared lock"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let ret, rightThis, didCall; diff --git a/services/sync/tests/unit/test_utils_makeGUID.js b/services/sync/tests/unit/test_utils_makeGUID.js index 3dc43803e4c4..0fb80b427183 100644 --- a/services/sync/tests/unit/test_utils_makeGUID.js +++ b/services/sync/tests/unit/test_utils_makeGUID.js @@ -1,5 +1,5 @@ _("Make sure makeGUID makes guids of the right length/characters"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { // XXX this could cause random failures as guids arent always unique... diff --git a/services/sync/tests/unit/test_utils_makeURI.js b/services/sync/tests/unit/test_utils_makeURI.js index 0a194159a861..9962925ab566 100644 --- a/services/sync/tests/unit/test_utils_makeURI.js +++ b/services/sync/tests/unit/test_utils_makeURI.js @@ -1,5 +1,5 @@ _("Make sure uri strings are converted to nsIURIs"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("Check http uris"); diff --git a/services/sync/tests/unit/test_utils_notify.js b/services/sync/tests/unit/test_utils_notify.js index 962468fa9600..50eb0d84a1ec 100644 --- a/services/sync/tests/unit/test_utils_notify.js +++ b/services/sync/tests/unit/test_utils_notify.js @@ -1,5 +1,5 @@ _("Make sure notify sends out the right notifications"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let ret, rightThis, didCall; diff --git a/services/sync/tests/unit/test_utils_queryAsync.js b/services/sync/tests/unit/test_utils_queryAsync.js index a431cff3783c..4a7ee754600d 100644 --- a/services/sync/tests/unit/test_utils_queryAsync.js +++ b/services/sync/tests/unit/test_utils_queryAsync.js @@ -1,5 +1,5 @@ _("Make sure queryAsync will synchronously fetch rows for a query asyncly"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("Using the form service to test queries"); diff --git a/services/sync/tests/unit/test_utils_sha1.js b/services/sync/tests/unit/test_utils_sha1.js index d7a6a59d38b5..5a7d928e1c27 100644 --- a/services/sync/tests/unit/test_utils_sha1.js +++ b/services/sync/tests/unit/test_utils_sha1.js @@ -1,5 +1,5 @@ _("Make sure sha1 digests works with various messages"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let mes1 = "hello"; diff --git a/services/sync/tests/unit/test_utils_sha256HMAC.js b/services/sync/tests/unit/test_utils_sha256HMAC.js index bb3b721bd1c2..1ae32784d517 100644 --- a/services/sync/tests/unit/test_utils_sha256HMAC.js +++ b/services/sync/tests/unit/test_utils_sha256HMAC.js @@ -1,5 +1,5 @@ _("Make sure sha256 hmac works with various messages and keys"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { let key1 = Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, "key1"); diff --git a/services/sync/tests/unit/test_utils_stackTrace.js b/services/sync/tests/unit/test_utils_stackTrace.js index 66645e979a27..b4030ea57d33 100644 --- a/services/sync/tests/unit/test_utils_stackTrace.js +++ b/services/sync/tests/unit/test_utils_stackTrace.js @@ -4,7 +4,7 @@ function bar(v) baz(v + 1); // line 3 function baz(v) { throw new Error(v + 1); } // line 4 _("Make sure lazy constructor calling/assignment works"); -Cu.import("resource://weave/util.js"); +Cu.import("resource://services-sync/util.js"); function run_test() { _("Make sure functions, arguments, files are pretty printed in the trace"); From f8625fe9d8047c14567c9d2f1df501cb80446d70 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Wed, 16 Jun 2010 14:30:13 -0700 Subject: [PATCH 1723/1860] Bug 570636 - Decide how to co-exist as a sync add-on and built-in sync [r=mconnor] Migrate prefs from extensions.weave. to services.sync. when loading the service for the first time before migration. --- services/crypto/WeaveCrypto.js | 4 +- services/sync/modules/constants.js | 2 +- services/sync/modules/engines/prefs.js | 2 +- services/sync/modules/service.js | 24 ++ services/sync/services-sync.js | 214 +++++++++--------- .../tests/unit/test_service_migratePrefs.js | 48 ++++ 6 files changed, 183 insertions(+), 111 deletions(-) create mode 100644 services/sync/tests/unit/test_service_migratePrefs.js diff --git a/services/crypto/WeaveCrypto.js b/services/crypto/WeaveCrypto.js index 6e76b3986780..13ea88be8e03 100644 --- a/services/crypto/WeaveCrypto.js +++ b/services/crypto/WeaveCrypto.js @@ -57,7 +57,7 @@ WeaveCrypto.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.IWeaveCrypto]), prefBranch : null, - debug : true, // extensions.weave.log.cryptoDebug + debug : true, // services.sync.log.cryptoDebug nss : null, nss_t : null, @@ -79,7 +79,7 @@ WeaveCrypto.prototype = { init : function() { try { // Preferences. Add observer so we get notified of changes. - this.prefBranch = Services.prefs.getBranch("extensions.weave.log."); + this.prefBranch = Services.prefs.getBranch("services.sync.log."); this.prefBranch.QueryInterface(Ci.nsIPrefBranch2); this.prefBranch.addObserver("cryptoDebug", this.observer, false); this.observer._self = this; diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 127795669d30..b1ad27f773de 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -50,7 +50,7 @@ DEFAULT_SERVER: "@server_url@", UPDATED_DEV_URL: "https://services.mozilla.com/sync/updated/?version=@weave_version@&channel=@xpi_type@", UPDATED_REL_URL: "http://www.mozilla.com/firefox/sync/updated.html", -PREFS_BRANCH: "extensions.weave.", +PREFS_BRANCH: "services.sync.", // Host "key" to access Weave Identity in the password manager PWDMGR_HOST: "chrome://weave", diff --git a/services/sync/modules/engines/prefs.js b/services/sync/modules/engines/prefs.js index c150bf6cbccd..757954b63fdf 100644 --- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -40,7 +40,7 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -const WEAVE_SYNC_PREFS = "extensions.weave.prefs.sync."; +const WEAVE_SYNC_PREFS = "services.sync.prefs.sync."; const WEAVE_PREFS_GUID = "preferences"; Cu.import("resource://services-sync/engines.js"); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index d6ac48d2c2e1..3f98235f4deb 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -77,6 +77,7 @@ Cu.import("resource://services-sync/engines/history.js", Weave); Cu.import("resource://services-sync/engines/prefs.js", Weave); Cu.import("resource://services-sync/engines/passwords.js", Weave); Cu.import("resource://services-sync/engines/tabs.js", Weave); +Cu.import("resource://services-sync/ext/Preferences.js"); Cu.import("resource://services-sync/identity.js", Weave); Cu.import("resource://services-sync/notifications.js", Weave); Cu.import("resource://services-sync/resource.js", Weave); @@ -256,6 +257,7 @@ WeaveSvc.prototype = { * Prepare to initialize the rest of Weave after waiting a little bit */ onStartup: function onStartup() { + this._migratePrefs(); this._initLogs(); this._log.info("Loading Weave " + WEAVE_VERSION); @@ -325,6 +327,28 @@ WeaveSvc.prototype = { return Status.service; }, + _migratePrefs: function _migratePrefs() { + // No need to re-migrate + if (Svc.Prefs.get("migrated", false)) + return; + + // Grab the list of old pref names + let oldPrefBranch = "extensions.weave."; + let oldPrefNames = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(oldPrefBranch). + getChildList("", {}); + + // Map each old pref to the current pref branch + let oldPref = new Preferences(oldPrefBranch); + for each (let pref in oldPrefNames) + Svc.Prefs.set(pref, oldPref.get(pref)); + + // Remove all the old prefs and remember that we've migrated + oldPref.resetBranch(""); + Svc.Prefs.set("migrated", true); + }, + _initLogs: function WeaveSvc__initLogs() { this._log = Log4Moz.repository.getLogger("Service.Main"); this._log.level = diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 72f49dd0ec32..d7ff3ed15fb9 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,113 +1,113 @@ -pref("extensions.weave.serverURL", "@server_url@"); -pref("extensions.weave.storageAPI", "1.0"); -pref("extensions.weave.userURL", "user/"); -pref("extensions.weave.miscURL", "misc/"); -pref("extensions.weave.termsURL", "https://services.mozilla.com/tos/"); -pref("extensions.weave.privacyURL", "https://services.mozilla.com/privacy-policy/"); +pref("services.sync.serverURL", "@server_url@"); +pref("services.sync.storageAPI", "1.0"); +pref("services.sync.userURL", "user/"); +pref("services.sync.miscURL", "misc/"); +pref("services.sync.termsURL", "https://services.mozilla.com/tos/"); +pref("services.sync.privacyURL", "https://services.mozilla.com/privacy-policy/"); -pref("extensions.weave.lastversion", "firstrun"); -pref("extensions.weave.autoconnect", true); +pref("services.sync.lastversion", "firstrun"); +pref("services.sync.autoconnect", true); -pref("extensions.weave.engine.bookmarks", true); -pref("extensions.weave.engine.history", true); -pref("extensions.weave.engine.passwords", true); -pref("extensions.weave.engine.prefs", true); -pref("extensions.weave.engine.tabs", true); -pref("extensions.weave.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$"); +pref("services.sync.engine.bookmarks", true); +pref("services.sync.engine.history", true); +pref("services.sync.engine.passwords", true); +pref("services.sync.engine.prefs", true); +pref("services.sync.engine.tabs", true); +pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$"); -pref("extensions.weave.log.appender.console", "Warn"); -pref("extensions.weave.log.appender.dump", "Error"); -pref("extensions.weave.log.appender.debugLog", "Trace"); -pref("extensions.weave.log.rootLogger", "Debug"); -pref("extensions.weave.log.logger.service.main", "Debug"); -pref("extensions.weave.log.logger.authenticator", "Debug"); -pref("extensions.weave.log.logger.network.resources", "Debug"); -pref("extensions.weave.log.logger.engine.bookmarks", "Debug"); -pref("extensions.weave.log.logger.engine.clients", "Debug"); -pref("extensions.weave.log.logger.engine.forms", "Debug"); -pref("extensions.weave.log.logger.engine.history", "Debug"); -pref("extensions.weave.log.logger.engine.passwords", "Debug"); -pref("extensions.weave.log.logger.engine.prefs", "Debug"); -pref("extensions.weave.log.logger.engine.tabs", "Debug"); -pref("extensions.weave.log.cryptoDebug", false); +pref("services.sync.log.appender.console", "Warn"); +pref("services.sync.log.appender.dump", "Error"); +pref("services.sync.log.appender.debugLog", "Trace"); +pref("services.sync.log.rootLogger", "Debug"); +pref("services.sync.log.logger.service.main", "Debug"); +pref("services.sync.log.logger.authenticator", "Debug"); +pref("services.sync.log.logger.network.resources", "Debug"); +pref("services.sync.log.logger.engine.bookmarks", "Debug"); +pref("services.sync.log.logger.engine.clients", "Debug"); +pref("services.sync.log.logger.engine.forms", "Debug"); +pref("services.sync.log.logger.engine.history", "Debug"); +pref("services.sync.log.logger.engine.passwords", "Debug"); +pref("services.sync.log.logger.engine.prefs", "Debug"); +pref("services.sync.log.logger.engine.tabs", "Debug"); +pref("services.sync.log.cryptoDebug", false); // Preferences to be synced by default -pref("extensions.weave.prefs.sync.accessibility.blockautorefresh", true); -pref("extensions.weave.prefs.sync.accessibility.browsewithcaret", true); -pref("extensions.weave.prefs.sync.accessibility.typeaheadfind", true); -pref("extensions.weave.prefs.sync.accessibility.typeaheadfind.linksonly", true); -pref("extensions.weave.prefs.sync.app.update.mode", true); -pref("extensions.weave.prefs.sync.browser.download.manager.closeWhenDone", true); -pref("extensions.weave.prefs.sync.browser.download.manager.retention", true); -pref("extensions.weave.prefs.sync.browser.download.manager.scanWhenDone", true); -pref("extensions.weave.prefs.sync.browser.download.manager.showWhenStarting", true); -pref("extensions.weave.prefs.sync.browser.formfill.enable", true); -pref("extensions.weave.prefs.sync.browser.history_expire_days", true); -pref("extensions.weave.prefs.sync.browser.history_expire_days_min", true); -pref("extensions.weave.prefs.sync.browser.link.open_newwindow", true); -pref("extensions.weave.prefs.sync.browser.offline-apps.notify", true); -pref("extensions.weave.prefs.sync.browser.safebrowsing.enabled", true); -pref("extensions.weave.prefs.sync.browser.safebrowsing.malware.enabled", true); -pref("extensions.weave.prefs.sync.browser.search.selectedEngine", true); -pref("extensions.weave.prefs.sync.browser.search.update", true); -pref("extensions.weave.prefs.sync.browser.startup.homepage", true); -pref("extensions.weave.prefs.sync.browser.startup.page", true); -pref("extensions.weave.prefs.sync.browser.tabs.autoHide", true); -pref("extensions.weave.prefs.sync.browser.tabs.closeButtons", true); -pref("extensions.weave.prefs.sync.browser.tabs.loadInBackground", true); -pref("extensions.weave.prefs.sync.browser.tabs.tabMaxWidth", true); -pref("extensions.weave.prefs.sync.browser.tabs.tabMinWidth", true); -pref("extensions.weave.prefs.sync.browser.tabs.warnOnClose", true); -pref("extensions.weave.prefs.sync.browser.tabs.warnOnOpen", true); -pref("extensions.weave.prefs.sync.browser.urlbar.autocomplete.enabled", true); -pref("extensions.weave.prefs.sync.browser.urlbar.autoFill", true); -pref("extensions.weave.prefs.sync.browser.urlbar.default.behavior", true); -pref("extensions.weave.prefs.sync.browser.urlbar.maxRichResults", true); -pref("extensions.weave.prefs.sync.dom.disable_open_during_load", true); -pref("extensions.weave.prefs.sync.dom.disable_window_flip", true); -pref("extensions.weave.prefs.sync.dom.disable_window_move_resize", true); -pref("extensions.weave.prefs.sync.dom.disable_window_open_feature.status", true); -pref("extensions.weave.prefs.sync.dom.disable_window_status_change", true); -pref("extensions.weave.prefs.sync.dom.event.contextmenu.enabled", true); -pref("extensions.weave.prefs.sync.extensions.personas.current", true); -pref("extensions.weave.prefs.sync.extensions.update.enabled", true); -pref("extensions.weave.prefs.sync.general.autoScroll", true); -pref("extensions.weave.prefs.sync.general.smoothScroll", true); -pref("extensions.weave.prefs.sync.intl.accept_languages", true); -pref("extensions.weave.prefs.sync.javascript.enabled", true); -pref("extensions.weave.prefs.sync.layout.spellcheckDefault", true); -pref("extensions.weave.prefs.sync.lightweightThemes.isThemeSelected", true); -pref("extensions.weave.prefs.sync.lightweightThemes.usedThemes", true); -pref("extensions.weave.prefs.sync.network.cookie.cookieBehavior", true); -pref("extensions.weave.prefs.sync.network.cookie.lifetimePolicy", true); -pref("extensions.weave.prefs.sync.permissions.default.image", true); -pref("extensions.weave.prefs.sync.pref.advanced.images.disable_button.view_image", true); -pref("extensions.weave.prefs.sync.pref.advanced.javascript.disable_button.advanced", true); -pref("extensions.weave.prefs.sync.pref.downloads.disable_button.edit_actions", true); -pref("extensions.weave.prefs.sync.pref.privacy.disable_button.cookie_exceptions", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.cache", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.cookies", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.downloads", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.formdata", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.history", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.offlineApps", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.passwords", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.sessions", true); -pref("extensions.weave.prefs.sync.privacy.clearOnShutdown.siteSettings", true); -pref("extensions.weave.prefs.sync.privacy.sanitize.sanitizeOnShutdown", true); -pref("extensions.weave.prefs.sync.security.OCSP.disable_button.managecrl", true); -pref("extensions.weave.prefs.sync.security.OCSP.enabled", true); -pref("extensions.weave.prefs.sync.security.OCSP.require", true); -pref("extensions.weave.prefs.sync.security.default_personal_cert", true); -pref("extensions.weave.prefs.sync.security.enable_java", true); -pref("extensions.weave.prefs.sync.security.enable_ssl3", true); -pref("extensions.weave.prefs.sync.security.enable_tls", true); -pref("extensions.weave.prefs.sync.security.warn_entering_secure", true); -pref("extensions.weave.prefs.sync.security.warn_entering_weak", true); -pref("extensions.weave.prefs.sync.security.warn_leaving_secure", true); -pref("extensions.weave.prefs.sync.security.warn_submit_insecure", true); -pref("extensions.weave.prefs.sync.security.warn_viewing_mixed", true); -pref("extensions.weave.prefs.sync.signon.rememberSignons", true); -pref("extensions.weave.prefs.sync.spellchecker.dictionary", true); -pref("extensions.weave.prefs.sync.xpinstall.whitelist.required", true); +pref("services.sync.prefs.sync.accessibility.blockautorefresh", true); +pref("services.sync.prefs.sync.accessibility.browsewithcaret", true); +pref("services.sync.prefs.sync.accessibility.typeaheadfind", true); +pref("services.sync.prefs.sync.accessibility.typeaheadfind.linksonly", true); +pref("services.sync.prefs.sync.app.update.mode", true); +pref("services.sync.prefs.sync.browser.download.manager.closeWhenDone", true); +pref("services.sync.prefs.sync.browser.download.manager.retention", true); +pref("services.sync.prefs.sync.browser.download.manager.scanWhenDone", true); +pref("services.sync.prefs.sync.browser.download.manager.showWhenStarting", true); +pref("services.sync.prefs.sync.browser.formfill.enable", true); +pref("services.sync.prefs.sync.browser.history_expire_days", true); +pref("services.sync.prefs.sync.browser.history_expire_days_min", true); +pref("services.sync.prefs.sync.browser.link.open_newwindow", true); +pref("services.sync.prefs.sync.browser.offline-apps.notify", true); +pref("services.sync.prefs.sync.browser.safebrowsing.enabled", true); +pref("services.sync.prefs.sync.browser.safebrowsing.malware.enabled", true); +pref("services.sync.prefs.sync.browser.search.selectedEngine", true); +pref("services.sync.prefs.sync.browser.search.update", true); +pref("services.sync.prefs.sync.browser.startup.homepage", true); +pref("services.sync.prefs.sync.browser.startup.page", true); +pref("services.sync.prefs.sync.browser.tabs.autoHide", true); +pref("services.sync.prefs.sync.browser.tabs.closeButtons", true); +pref("services.sync.prefs.sync.browser.tabs.loadInBackground", true); +pref("services.sync.prefs.sync.browser.tabs.tabMaxWidth", true); +pref("services.sync.prefs.sync.browser.tabs.tabMinWidth", true); +pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true); +pref("services.sync.prefs.sync.browser.tabs.warnOnOpen", true); +pref("services.sync.prefs.sync.browser.urlbar.autocomplete.enabled", true); +pref("services.sync.prefs.sync.browser.urlbar.autoFill", true); +pref("services.sync.prefs.sync.browser.urlbar.default.behavior", true); +pref("services.sync.prefs.sync.browser.urlbar.maxRichResults", true); +pref("services.sync.prefs.sync.dom.disable_open_during_load", true); +pref("services.sync.prefs.sync.dom.disable_window_flip", true); +pref("services.sync.prefs.sync.dom.disable_window_move_resize", true); +pref("services.sync.prefs.sync.dom.disable_window_open_feature.status", true); +pref("services.sync.prefs.sync.dom.disable_window_status_change", true); +pref("services.sync.prefs.sync.dom.event.contextmenu.enabled", true); +pref("services.sync.prefs.sync.extensions.personas.current", true); +pref("services.sync.prefs.sync.extensions.update.enabled", true); +pref("services.sync.prefs.sync.general.autoScroll", true); +pref("services.sync.prefs.sync.general.smoothScroll", true); +pref("services.sync.prefs.sync.intl.accept_languages", true); +pref("services.sync.prefs.sync.javascript.enabled", true); +pref("services.sync.prefs.sync.layout.spellcheckDefault", true); +pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true); +pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true); +pref("services.sync.prefs.sync.network.cookie.cookieBehavior", true); +pref("services.sync.prefs.sync.network.cookie.lifetimePolicy", true); +pref("services.sync.prefs.sync.permissions.default.image", true); +pref("services.sync.prefs.sync.pref.advanced.images.disable_button.view_image", true); +pref("services.sync.prefs.sync.pref.advanced.javascript.disable_button.advanced", true); +pref("services.sync.prefs.sync.pref.downloads.disable_button.edit_actions", true); +pref("services.sync.prefs.sync.pref.privacy.disable_button.cookie_exceptions", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.cache", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.cookies", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.downloads", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.formdata", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.history", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.offlineApps", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.passwords", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.sessions", true); +pref("services.sync.prefs.sync.privacy.clearOnShutdown.siteSettings", true); +pref("services.sync.prefs.sync.privacy.sanitize.sanitizeOnShutdown", true); +pref("services.sync.prefs.sync.security.OCSP.disable_button.managecrl", true); +pref("services.sync.prefs.sync.security.OCSP.enabled", true); +pref("services.sync.prefs.sync.security.OCSP.require", true); +pref("services.sync.prefs.sync.security.default_personal_cert", true); +pref("services.sync.prefs.sync.security.enable_java", true); +pref("services.sync.prefs.sync.security.enable_ssl3", true); +pref("services.sync.prefs.sync.security.enable_tls", true); +pref("services.sync.prefs.sync.security.warn_entering_secure", true); +pref("services.sync.prefs.sync.security.warn_entering_weak", true); +pref("services.sync.prefs.sync.security.warn_leaving_secure", true); +pref("services.sync.prefs.sync.security.warn_submit_insecure", true); +pref("services.sync.prefs.sync.security.warn_viewing_mixed", true); +pref("services.sync.prefs.sync.signon.rememberSignons", true); +pref("services.sync.prefs.sync.spellchecker.dictionary", true); +pref("services.sync.prefs.sync.xpinstall.whitelist.required", true); diff --git a/services/sync/tests/unit/test_service_migratePrefs.js b/services/sync/tests/unit/test_service_migratePrefs.js new file mode 100644 index 000000000000..22b01734298b --- /dev/null +++ b/services/sync/tests/unit/test_service_migratePrefs.js @@ -0,0 +1,48 @@ +Cu.import("resource://services-sync/ext/Preferences.js"); + +function run_test() { + _("Set some prefs on the old branch"); + let globalPref = new Preferences(""); + globalPref.set("extensions.weave.hello", "world"); + globalPref.set("extensions.weave.number", 42); + globalPref.set("extensions.weave.yes", true); + globalPref.set("extensions.weave.no", false); + + _("Make sure the old prefs are there"); + do_check_eq(globalPref.get("extensions.weave.hello"), "world"); + do_check_eq(globalPref.get("extensions.weave.number"), 42); + do_check_eq(globalPref.get("extensions.weave.yes"), true); + do_check_eq(globalPref.get("extensions.weave.no"), false); + + _("New prefs shouldn't exist yet"); + do_check_eq(globalPref.get("services.sync.hello"), null); + do_check_eq(globalPref.get("services.sync.number"), null); + do_check_eq(globalPref.get("services.sync.yes"), null); + do_check_eq(globalPref.get("services.sync.no"), null); + + _("Loading service should migrate"); + Cu.import("resource://services-sync/service.js"); + do_check_eq(globalPref.get("services.sync.hello"), "world"); + do_check_eq(globalPref.get("services.sync.number"), 42); + do_check_eq(globalPref.get("services.sync.yes"), true); + do_check_eq(globalPref.get("services.sync.no"), false); + do_check_eq(globalPref.get("extensions.weave.hello"), null); + do_check_eq(globalPref.get("extensions.weave.number"), null); + do_check_eq(globalPref.get("extensions.weave.yes"), null); + do_check_eq(globalPref.get("extensions.weave.no"), null); + + _("Migrating should set a pref to make sure to not re-migrate"); + do_check_true(globalPref.get("services.sync.migrated")); + + _("Make sure re-migrate doesn't happen"); + globalPref.set("extensions.weave.tooLate", "already migrated!"); + do_check_eq(globalPref.get("extensions.weave.tooLate"), "already migrated!"); + do_check_eq(globalPref.get("services.sync.tooLate"), null); + Weave.Service._migratePrefs(); + do_check_eq(globalPref.get("extensions.weave.tooLate"), "already migrated!"); + do_check_eq(globalPref.get("services.sync.tooLate"), null); + + _("Clearing out pref changes for other tests"); + globalPref.resetBranch("extensions.weave."); + globalPref.resetBranch("services.sync."); +} From c7181f045f7a8a67796d92d9345d452e39ecb0f9 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 18 Jun 2010 09:13:02 -0700 Subject: [PATCH 1724/1860] Bug 572970 - Hardcode some pre-processed values for .in files like prefs and constants [r=mconnor] * Rename prefs.js.in to prefs.js * Hardcode server_url --- services/sync/services-sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index d7ff3ed15fb9..acec0c0fc7ef 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -1,4 +1,4 @@ -pref("services.sync.serverURL", "@server_url@"); +pref("services.sync.serverURL", "https://auth.services.mozilla.com/"); pref("services.sync.storageAPI", "1.0"); pref("services.sync.userURL", "user/"); pref("services.sync.miscURL", "misc/"); From f60a9f70c0111f9a8f7f8483ddcf2dc539dd4124 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 18 Jun 2010 09:13:02 -0700 Subject: [PATCH 1725/1860] Bug 572970 - Hardcode some pre-processed values for .in files like prefs and constants [r=mconnor] * Rename constants.js.in to constants.js. * Get rid of unused DEFAULT_SERVER variable * Hardcode STORAGE_VERSION. The three other variable substitutions are add-on specific. --- services/sync/modules/constants.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index b1ad27f773de..850bc0b6b101 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -44,9 +44,8 @@ WEAVE_ID: "@weave_id@", // Version of the data format this client supports. The data format describes // how records are packaged; this is separate from the Server API version and // the per-engine cleartext formats. -STORAGE_VERSION: @storage_version@, +STORAGE_VERSION: 2, -DEFAULT_SERVER: "@server_url@", UPDATED_DEV_URL: "https://services.mozilla.com/sync/updated/?version=@weave_version@&channel=@xpi_type@", UPDATED_REL_URL: "http://www.mozilla.com/firefox/sync/updated.html", From ed752d8fcbda830830290d5b33e1f26a434a1780 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 18 Jun 2010 09:39:03 -0700 Subject: [PATCH 1726/1860] Bug 573015 - Get rid of 'attic' and 'need-work' tests [r=mconnor] --- .../tests/unit/attic/test_bookmark_sharing.js | 121 ------ .../sync/tests/unit/attic/test_sharing.js | 29 -- services/sync/tests/unit/attic/test_xmpp.js | 104 ------ .../sync/tests/unit/attic/test_xmpp_simple.js | 71 ---- .../unit/attic/test_xmpp_transport_http.js | 55 --- .../unit/need-work/test_bookmark_syncing.js | 131 ------- .../test_bookmark_syncing.log.expected | 350 ------------------ .../tests/unit/need-work/test_cookie_store.js | 150 -------- .../need-work/test_passphrase_checking.js | 46 --- .../unit/need-work/test_password_syncing.js | 37 -- .../test_password_syncing.log.expected | 174 --------- .../tests/unit/need-work/test_passwords.js | 23 -- .../sync/tests/unit/need-work/test_service.js | 58 --- .../unit/need-work/test_service.log.expected | 18 - .../unit/need-work/test_util_tracebacks.js | 36 -- .../test_util_tracebacks.log.expected | 36 -- 16 files changed, 1439 deletions(-) delete mode 100644 services/sync/tests/unit/attic/test_bookmark_sharing.js delete mode 100644 services/sync/tests/unit/attic/test_sharing.js delete mode 100644 services/sync/tests/unit/attic/test_xmpp.js delete mode 100644 services/sync/tests/unit/attic/test_xmpp_simple.js delete mode 100644 services/sync/tests/unit/attic/test_xmpp_transport_http.js delete mode 100644 services/sync/tests/unit/need-work/test_bookmark_syncing.js delete mode 100644 services/sync/tests/unit/need-work/test_bookmark_syncing.log.expected delete mode 100644 services/sync/tests/unit/need-work/test_cookie_store.js delete mode 100644 services/sync/tests/unit/need-work/test_passphrase_checking.js delete mode 100644 services/sync/tests/unit/need-work/test_password_syncing.js delete mode 100644 services/sync/tests/unit/need-work/test_password_syncing.log.expected delete mode 100644 services/sync/tests/unit/need-work/test_passwords.js delete mode 100644 services/sync/tests/unit/need-work/test_service.js delete mode 100644 services/sync/tests/unit/need-work/test_service.log.expected delete mode 100644 services/sync/tests/unit/need-work/test_util_tracebacks.js delete mode 100644 services/sync/tests/unit/need-work/test_util_tracebacks.log.expected diff --git a/services/sync/tests/unit/attic/test_bookmark_sharing.js b/services/sync/tests/unit/attic/test_bookmark_sharing.js deleted file mode 100644 index da0e4703d118..000000000000 --- a/services/sync/tests/unit/attic/test_bookmark_sharing.js +++ /dev/null @@ -1,121 +0,0 @@ -Cu.import("resource://services-sync/async.js"); -Cu.import("resource://services-sync/engines/bookmarks.js"); -Cu.import("resource://services-sync/sharing.js"); -Cu.import("resource://services-sync/util.js"); - -Function.prototype.async = Async.sugar; - -load("bookmark_setup.js"); - -function FakeMicrosummaryService() { - return {hasMicrosummary: function() { return false; }}; -} - -function FakeAnnotationService() { - this._annotations = {}; -} -FakeAnnotationService.prototype = { - EXPIRE_NEVER: 0, - getItemAnnotation: function (aItemId, aName) { - if (this._annotations[aItemId] != undefined) - if (this._annotations[aItemId][aName]) - return this._annotations[aItemId][aName]; - return null; - }, - setItemAnnotation: function (aItemId, aName, aValue, aFlags, aExpiration) { - if (this._annotations[aItemId] == undefined) - this._annotations[aItemId] = {}; - this._annotations[aItemId][aName] = aValue; - dump( "Annotated item " + aItemId + " with " + aName + " = " + aValue + "\n"); - //ignore flags and expiration - }, - getItemsWithAnnotation: function(aName, resultCount, results) { - var list = []; - for ( var x in this._annotations) { - if (this._annotations[x][aName] != undefined) { - return x; - } - } - return list; - } -} - - -function FakeSharingApi() { -} -FakeSharingApi.prototype = { - shareWithUsers: function FakeSharingApi_shareWith(path, users, onComplete) { - // TODO just set a flag on the fake DAV thing. - } -} -Sharing.Api = FakeSharingApi; - - -var annoSvc = new FakeAnnotationService(); - -function makeBookmarksEngine() { - let engine = new BookmarksEngine(); - engine._store.__ms = new FakeMicrosummaryService(); - let shareManager = engine._sharing; - shareManager.__annoSvc = annoSvc; // use fake annotation service - return engine; -} - -function run_test() { - let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - - - var syncTesting = new SyncTestingInfrastructure( makeBookmarksEngine ); - - let folderName = "Funny Pictures of Manatees and Walruses"; - let folderToShare = bms.createFolder( bms.bookmarksMenuFolder, - folderName, -1 ); - let lolrusBm = bms.insertBookmark(folderToShare, - uri("http://www.lolrus.com"), - -1, "LOLrus" ); - let lolateeBm = bms.insertBookmark(folderToShare, - uri("http://www.lolatee.com"), - -1, "LOLatee" ); - - // Note xmpp.enabled is set to false by the SyncTestingInfrastructure. - - let username = "rusty"; - let engine = makeBookmarksEngine(); - let shareManager = engine._sharing; - - function setupShare(cb) { - // TODO: Passing in folderToShare won't work at the time of writing - // this because folderToShare is expected to be a DOM node, not a - // Places ID. - shareManager._share.async( shareManager, cb, folderToShare, "jonas" ); - } - - /* - syncTesting.runAsyncFunc("Share folder with Jonas", setupShare); - - - dump( "folderToShare = " + folderToShare + "\n"); - // Get the server path from folder annotation... - let serverPath = annoSvc.getItemAnnotation( folderToShare, - "weave/shared-server-path" ); - dump( "Shared it to server path " + serverPath + "\n"); - - // get off rusty's computer, switch to Jonas's computer: - syncTesting.saveClientState( "rusty computer 1" ); - syncTesting.resetClientState(); - - // These next two lines simulate what would happen when jonas received - // the xmpp message and clicked "accept". - shareManager._createIncomingShare(username, serverPath, folderName); - shareManager._updateAllIncomingShares(); - - // now look for a bookmark folder with an incoming-share annotation - let a = annoSvc.getItemsWithAnnotation("weave/shared-incoming", - {}); - do_check_eq( a.length, 1); // should be just one - // TODO next look at its children: - */ -} - - diff --git a/services/sync/tests/unit/attic/test_sharing.js b/services/sync/tests/unit/attic/test_sharing.js deleted file mode 100644 index 1afe639eea4f..000000000000 --- a/services/sync/tests/unit/attic/test_sharing.js +++ /dev/null @@ -1,29 +0,0 @@ -Cu.import("resource://services-sync/identity.js"); -Cu.import("resource://services-sync/sharing.js"); -Cu.import("resource://services-sync/util.js"); - -function runTestGenerator() { - let self = yield; - - ID.set("blarg", new Identity("realm", "myusername", "mypass")); - let fakeDav = { - identity: "blarg", - POST: function fakeDav_POST(url, data, callback) { - do_check_true(data.indexOf("uid=myusername") != -1); - do_check_true(data.indexOf("password=mypass") != -1); - do_check_true(data.indexOf("/fake/dir") != -1); - do_check_true(data.indexOf("johndoe") != -1); - let result = {status: 200, responseText: "OK"}; - Utils.makeTimerForCall(function() { callback(result); }); - } - }; - - let api = new Sharing.Api(fakeDav); - api.shareWithUsers("/fake/dir", ["johndoe"], self.cb); - let result = yield; - - do_check_eq(result.wasSuccessful, true); - self.done(); -} - -var run_test = makeAsyncTestRunner(runTestGenerator); diff --git a/services/sync/tests/unit/attic/test_xmpp.js b/services/sync/tests/unit/attic/test_xmpp.js deleted file mode 100644 index fa682bbde920..000000000000 --- a/services/sync/tests/unit/attic/test_xmpp.js +++ /dev/null @@ -1,104 +0,0 @@ -Cu.import( "resource://services-sync/xmpp/xmppClient.js" ); - -function LOG(aMsg) { - dump("TEST_XMPP: " + aMsg + "\n"); -} - -var serverUrl = "http://localhost:5280/http-poll"; -var jabberDomain = "localhost"; - -var timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer ); -var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); - -function run_test() { - // FIXME: this test hangs when you don't have a server, disabling for now - return; - - /* First, just see if we can connect: */ - var transport = new HTTPPollingTransport(serverUrl, false, 4000); - var auth = new PlainAuthenticator(); - var alice = new XmppClient("alice", jabberDomain, "iamalice", - transport, auth); - - // test connection - LOG("connecting"); - alice.connect( jabberDomain ); - alice.waitForConnection(); - do_check_eq( alice._connectionStatus, alice.CONNECTED); - LOG("connected"); - - // test disconnection - LOG("disconnecting"); - alice.disconnect(); - do_check_eq( alice._connectionStatus, alice.NOT_CONNECTED); - LOG("disconnected"); - - // test re-connection - LOG("reconnecting"); - alice.connect( jabberDomain ); - alice.waitForConnection(); - LOG("reconnected"); - do_check_eq( alice._connectionStatus, alice.CONNECTED); - alice.disconnect(); - - // test connection failure - bad domain - alice.connect( "bad domain" ); - alice.waitForConnection(); - do_check_eq( alice._connectionStatus, alice.FAILED ); - - /* - // re-connect and move on - alice.connect( jabberDomain ); - alice.waitForConnection(); - do_check_eq( alice._connectionStatus, alice.CONNECTED); - - // The talking-to-myself test: - var testIsOver = false; - var sometext = "bla bla how you doin bla"; - var transport2 = new HTTPPollingTransport( serverUrl, false, 4000 ); - var auth2 = new PlainAuthenticator(); - var bob = new XmppClient( "bob", jabberDomain, "iambob", transport2, auth2 ); - - // Timer that will make the test fail if message is not received after - // a certain amount of time - var timerResponder = { - notify: function( timer ) { - testIsOver = true; - do_throw( "Timed out waiting for message." ); - } - }; - timer.initWithCallback( timerResponder, 20000, timer.TYPE_ONE_SHOT ); - - - // Handler that listens for the incoming message: - var aliceMessageHandler = { - handle: function( msgText, from ) { - dump( "Alice got a message.\n" ); - do_check_eq( msgText, sometext ); - do_check_eq( from, "bob@" + jabberDomain ); - timer.cancel(); - testIsOver = true; - } - }; - alice.registerMessageHandler( aliceMessageHandler ); - - // Start both clients - bob.connect( jabberDomain ); - bob.waitForConnection(); - do_check_neq( bob._connectionStatus, bob.FAILED ); - alice.connect( jabberDomain ); - alice.waitForConnection(); - do_check_neq( alice._connectionStatus, alice.FAILED ); - - // Send the message - bob.sendMessage( "alice@" + jabberDomain, sometext ); - // Wait until either the message is received, or the timeout expires. - var currentThread = threadManager.currentThread; - while( !testIsOver ) { - currentThread.processNextEvent( true ); - } - - alice.disconnect(); - bob.disconnect(); - */ -}; diff --git a/services/sync/tests/unit/attic/test_xmpp_simple.js b/services/sync/tests/unit/attic/test_xmpp_simple.js deleted file mode 100644 index 1516a0417744..000000000000 --- a/services/sync/tests/unit/attic/test_xmpp_simple.js +++ /dev/null @@ -1,71 +0,0 @@ -function LOG(aMsg) { - dump("TEST_XMPP_SIMPLE: " + aMsg + "\n"); -} - -Components.utils.import( "resource://services-sync/xmpp/xmppClient.js" ); - -var serverUrl = "http://localhost:5280/http-poll"; -var jabberDomain = "localhost"; - -function run_test() { - // FIXME: this test hangs when you don't have a server, disabling for now - return; - - // async test - do_test_pending(); - - var testMessage = "Hello Bob."; - - var aliceHandler = { - handle: function(msgText, from) { - LOG("ALICE RCVD from " + from + ": " + msgText); - } - }; - var aliceClient = getClientForUser("alice", "iamalice", aliceHandler); - - var bobHandler = { - handle: function(msgText, from) { - LOG("BOB RCVD from " + from + ": " + msgText); - do_check_eq(from.split("/")[0], "alice@" + jabberDomain); - do_check_eq(msgText, testMessage); - LOG("messages checked out"); - - aliceClient.disconnect(); - bobClient.disconnect(); - LOG("disconnected"); - - do_test_finished(); - } - }; - var bobClient = getClientForUser("bob", "iambob", bobHandler); - bobClient.announcePresence(); - - - // Send a message - aliceClient.sendMessage("bob@" + jabberDomain, testMessage); -} - -function getClientForUser(aName, aPassword, aHandler) { - // "false" tells the transport not to use session keys. 4000 is the number of - // milliseconds to wait between attempts to poll the server. - var transport = new HTTPPollingTransport(serverUrl, false, 4000); - - var auth = new PlainAuthenticator(); - - var client = new XmppClient(aName, jabberDomain, aPassword, - transport, auth); - - client.registerMessageHandler(aHandler); - - // Connect - client.connect(jabberDomain); - client.waitForConnection(); - - // this will block until our connection attempt has either succeeded or failed. - // Check if connection succeeded: - if ( client._connectionStatus == client.FAILED ) { - do_throw("connection failed"); - } - - return client; -} diff --git a/services/sync/tests/unit/attic/test_xmpp_transport_http.js b/services/sync/tests/unit/attic/test_xmpp_transport_http.js deleted file mode 100644 index 88082398afbe..000000000000 --- a/services/sync/tests/unit/attic/test_xmpp_transport_http.js +++ /dev/null @@ -1,55 +0,0 @@ -function LOG(aMsg) { - dump("TEST_XMPP_TRANSPORT_HTTP: " + aMsg + "\n"); -} - -Components.utils.import( "resource://services-sync/xmpp/xmppClient.js" ); - -var tests = []; - -// test connection failure - no server -tests.push(function run_test_bad_server() { - LOG("starting test: bad server"); - - var transport = new HTTPPollingTransport("this is not a server URL", false, 4000); - transport.connect(); - transport.setCallbackObject({ - onIncomingData: function(aData) { - do_throw("onIncomingData was called instead of onTransportError, for a bad URL"); - }, - onTransportError: function(aErrorMessage) { - do_check_true(/^Unable to send message to server:/.test(aErrorMessage)); - // continue test suite - tests.shift()(); - } - }); - transport.send(); -}); - -tests.push(function run_test_bad_url() { - LOG("starting test: bad url"); - // test connection failure - server up, bad URL - var serverUrl = "http://localhost:5280/http-polly-want-a-cracker"; - var transport = new HTTPPollingTransport(serverUrl, false, 4000); - transport.connect(); - transport.setCallbackObject({ - onIncomingData: function(aData) { - do_throw("onIncomingData was called instead of onTransportError, for a bad URL"); - }, - onTransportError: function(aErrorMessage) { - LOG("ERROR: " + aErrorMessage); - do_check_true(/^Provided URL is not valid./.test(aErrorMessage)); - do_test_finished(); - } - }); - transport.send(); -}); - -function run_test() { - // FIXME: this test hangs when you don't have a server, disabling for now - return; - - // async test - do_test_pending(); - - tests.shift()(); -} diff --git a/services/sync/tests/unit/need-work/test_bookmark_syncing.js b/services/sync/tests/unit/need-work/test_bookmark_syncing.js deleted file mode 100644 index 937d88eefa6a..000000000000 --- a/services/sync/tests/unit/need-work/test_bookmark_syncing.js +++ /dev/null @@ -1,131 +0,0 @@ -Cu.import("resource://services-sync/async.js"); -Cu.import("resource://services-sync/engines/bookmarks.js"); -Cu.import("resource://services-sync/util.js"); - -Function.prototype.async = Async.sugar; - -load("bookmark_setup.js"); - -// ---------------------------------------- -// Test Logic -// ---------------------------------------- - -function FakeMicrosummaryService() { - return {hasMicrosummary: function() { return false; }}; -} - -function makeBookmarksEngine() { - let engine = new BookmarksEngine(); - engine._store.__ms = new FakeMicrosummaryService(); - return engine; -} - -function run_test() { - // ----- - // Setup - // ----- - - var syncTesting = new SyncTestingInfrastructure(makeBookmarksEngine); - - let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); - - function bmId(url) { - var bookmarks = bms.getBookmarkIdsForURI(uri(url), {}); - do_check_eq(bookmarks.length, 1); - return bookmarks[0]; - } - - cleanUp(); - - // ----------- - // Test Proper - // ----------- - - let boogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, - uri("http://www.boogle.com"), - -1, - "Boogle"); - bms.setItemGUID(boogleBm, "boogle-bookmark-guid"); - - syncTesting.doSync("initial sync w/ one bookmark"); - - syncTesting.doSync("trivial re-sync"); - - let yoogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, - uri("http://www.yoogle.com"), - -1, - "Yoogle"); - bms.setItemGUID(yoogleBm, "yoogle-bookmark-guid"); - - syncTesting.doSync("add bookmark and re-sync"); - - bms.moveItem(yoogleBm, - bms.bookmarksMenuFolder, - 0); - - syncTesting.doSync("swap bookmark order and re-sync"); - - syncTesting.saveClientState("first computer"); - - do_check_true(bms.isBookmarked(uri("http://www.boogle.com"))); - do_check_true(bms.isBookmarked(uri("http://www.yoogle.com"))); - - syncTesting.resetClientState(); - - do_check_false(bms.isBookmarked(uri("http://www.boogle.com"))); - do_check_false(bms.isBookmarked(uri("http://www.yoogle.com"))); - - syncTesting.doSync("re-sync on second computer"); - - do_check_true(bms.isBookmarked(uri("http://www.boogle.com"))); - do_check_true(bms.isBookmarked(uri("http://www.yoogle.com"))); - - let zoogleBm = bms.insertBookmark(bms.bookmarksMenuFolder, - uri("http://www.zoogle.com"), - -1, - "Zoogle"); - bms.setItemGUID(zoogleBm, "zoogle-bookmark-guid"); - - syncTesting.doSync("add bookmark on second computer and resync"); - - syncTesting.saveClientState("second computer"); - - do_check_true(bms.isBookmarked(uri("http://www.zoogle.com"))); - - syncTesting.restoreClientState("first computer"); - - do_check_false(bms.isBookmarked(uri("http://www.zoogle.com"))); - - syncTesting.doSync("re-sync on first computer"); - - do_check_true(bms.isBookmarked(uri("http://www.zoogle.com"))); - - let binkBm1 = bms.insertBookmark(bms.bookmarksMenuFolder, - uri("http://www.bink.com"), - -1, - "Bink"); - bms.setItemGUID(binkBm1, "bink-bookmark-guid-1"); - - syncTesting.doSync("add bookmark 'bink' on first computer and resync"); - syncTesting.restoreClientState("second computer"); - - let binkBm2 = bms.insertBookmark(bms.bookmarksMenuFolder, - uri("http://www.bink.com"), - -1, - "Bink"); - - bms.setItemGUID(binkBm2, "bink-bookmark-guid-2"); - - syncTesting.doSync("Manually add same bookmark 'bink', but with " + - "different GUID, to second computer and resync"); - - do_check_eq(bms.getItemGUID(bmId("http://www.bink.com")), - "bink-bookmark-guid-1"); - - // -------- - // Teardown - // -------- - - cleanUp(); -} diff --git a/services/sync/tests/unit/need-work/test_bookmark_syncing.log.expected b/services/sync/tests/unit/need-work/test_bookmark_syncing.log.expected deleted file mode 100644 index e8a5d88844ec..000000000000 --- a/services/sync/tests/unit/need-work/test_bookmark_syncing.log.expected +++ /dev/null @@ -1,350 +0,0 @@ -*** test pending -Testing INFO ----------------------------------------- -Testing INFO Step 'initial sync w/ one bookmark' starting. -Testing INFO ----------------------------------------- -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 404 -Service.BmkEngine INFO Initial upload to server -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/keys.json with data: {"ring":{},"bulkIV":null} -Service.Resource DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/bookmarks/snapshot.json with data: {"boogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}} -Service.Resource DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":4} -Service.Resource DEBUG PUT request successful -Service.RemoteStore INFO Full upload to server successful -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":0,"GUID":"fake-guid-0","snapshot":{"boogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} -Testing INFO Step 'initial sync w/ one bookmark' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'trivial re-sync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.BmkEngine INFO Local snapshot version: 0 -Service.BmkEngine INFO Server maxVersion: 0 -Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) -Service.RemoteStore TRACE Local snapshot version == server maxVersion -Service.BmkEngine INFO Sync complete: no changes needed on client or server -Testing INFO Step 'trivial re-sync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'add bookmark and re-sync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.BmkEngine INFO Local snapshot version: 0 -Service.BmkEngine INFO Server maxVersion: 0 -Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) -Service.RemoteStore TRACE Local snapshot version == server maxVersion -Service.BmkEngine INFO Reconciling client/server updates -Service.BMSync DEBUG Reconciling 1 against 0 commands -Service.BmkEngine INFO Changes for client: 0 -Service.BmkEngine INFO Predicted changes for server: 1 -Service.BmkEngine INFO Client conflicts: 0 -Service.BmkEngine INFO Server conflicts: 0 -Service.BmkEngine INFO Actual changes for server: 1 -Service.BmkEngine INFO Uploading changes to server -Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/bookmarks/deltas/1 with data: [{"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}}] -Service.ResourceSet DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":5} -Service.Resource DEBUG PUT request successful -Service.BmkEngine INFO Successfully updated deltas and status on server -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":1,"GUID":"fake-guid-0","snapshot":{"boogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"yoogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} -Service.BmkEngine INFO Sync complete -Testing INFO Step 'add bookmark and re-sync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'swap bookmark order and re-sync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.BmkEngine INFO Local snapshot version: 1 -Service.BmkEngine INFO Server maxVersion: 1 -Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) -Service.RemoteStore TRACE Local snapshot version == server maxVersion -Service.BmkEngine INFO Reconciling client/server updates -Service.BMSync DEBUG Reconciling 2 against 0 commands -Service.BmkEngine INFO Changes for client: 0 -Service.BmkEngine INFO Predicted changes for server: 2 -Service.BmkEngine INFO Client conflicts: 0 -Service.BmkEngine INFO Server conflicts: 0 -Service.BmkEngine INFO Actual changes for server: 2 -Service.BmkEngine INFO Uploading changes to server -Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/bookmarks/deltas/2 with data: [{"action":"edit","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":1,"type":"bookmark"}},{"action":"edit","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":0,"type":"bookmark"}}] -Service.ResourceSet DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":5} -Service.Resource DEBUG PUT request successful -Service.BmkEngine INFO Successfully updated deltas and status on server -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} -Service.BmkEngine INFO Sync complete -Testing INFO Step 'swap bookmark order and re-sync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 're-sync on second computer' starting. -Testing INFO ----------------------------------------- -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.BmkEngine DEBUG Remote/local sync GUIDs do not match. Forcing initial sync. -Service.BmkEngine INFO Local snapshot version: -1 -Service.BmkEngine INFO Server maxVersion: 2 -Service.RemoteStore TRACE Getting latest from snap --> scratch -Service.RemoteStore INFO Downloading all server data from scratch -Testing INFO HTTP GET from user-data/bookmarks/snapshot.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data -Testing INFO HTTP GET from user-data/bookmarks/deltas/1, returning status 200 -Service.ResourceSet DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data -Service.SnapStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} -Testing INFO HTTP GET from user-data/bookmarks/deltas/2, returning status 200 -Service.ResourceSet DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data -Service.SnapStore TRACE Processing command: {"action":"edit","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":1,"type":"bookmark"}} -Service.SnapStore TRACE Processing command: {"action":"edit","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"index":0,"type":"bookmark"}} -Service.BmkEngine INFO Reconciling client/server updates -Service.BMSync DEBUG Reconciling 3 against 5 commands -Service.BmkEngine INFO Changes for client: 2 -Service.BmkEngine INFO Predicted changes for server: 0 -Service.BmkEngine INFO Client conflicts: 0 -Service.BmkEngine INFO Server conflicts: 0 -Service.BmkEngine INFO Applying changes locally -Service.SnapStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} -Service.SnapStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} -Service.BStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} -Service.BStore DEBUG -> creating bookmark "Boogle" -Service.BStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} -Service.BStore DEBUG -> creating bookmark "Yoogle" -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}}} -Service.BmkEngine INFO Actual changes for server: 0 -Service.BmkEngine INFO Sync complete -Testing INFO Step 're-sync on second computer' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'add bookmark on second computer and resync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.BmkEngine INFO Local snapshot version: 2 -Service.BmkEngine INFO Server maxVersion: 2 -Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) -Service.RemoteStore TRACE Local snapshot version == server maxVersion -Service.BmkEngine INFO Reconciling client/server updates -Service.BMSync DEBUG Reconciling 1 against 0 commands -Service.BmkEngine INFO Changes for client: 0 -Service.BmkEngine INFO Predicted changes for server: 1 -Service.BmkEngine INFO Client conflicts: 0 -Service.BmkEngine INFO Server conflicts: 0 -Service.BmkEngine INFO Actual changes for server: 1 -Service.BmkEngine INFO Uploading changes to server -Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/bookmarks/deltas/3 with data: [{"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}}] -Service.ResourceSet DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":3,"snapEncryption":"none","deltasEncryption":"none","itemCount":6} -Service.Resource DEBUG PUT request successful -Service.BmkEngine INFO Successfully updated deltas and status on server -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":3,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} -Service.BmkEngine INFO Sync complete -Testing INFO Step 'add bookmark on second computer and resync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'restore client state of first computer' starting. -Testing INFO ----------------------------------------- -Service.BStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} -Service.BStore DEBUG -> creating bookmark "Yoogle" -Service.BStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} -Service.BStore DEBUG -> creating bookmark "Boogle" -Testing INFO Step 'restore client state of first computer' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 're-sync on first computer' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.BmkEngine INFO Local snapshot version: 2 -Service.BmkEngine INFO Server maxVersion: 3 -Service.RemoteStore DEBUG Using last sync snapshot as starting point for server snapshot -Service.RemoteStore INFO Downloading server deltas -Testing INFO HTTP GET from user-data/bookmarks/deltas/3, returning status 200 -Service.ResourceSet DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data -Service.SnapStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}} -Service.BmkEngine INFO Reconciling client/server updates -Service.BMSync DEBUG Reconciling 0 against 1 commands -Service.BmkEngine INFO Changes for client: 1 -Service.BmkEngine INFO Predicted changes for server: 0 -Service.BmkEngine INFO Client conflicts: 0 -Service.BmkEngine INFO Server conflicts: 0 -Service.BmkEngine INFO Applying changes locally -Service.SnapStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}} -Service.BStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}} -Service.BStore DEBUG -> creating bookmark "Zoogle" -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":3,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}}} -Service.BmkEngine INFO Actual changes for server: 0 -Service.BmkEngine INFO Sync complete -Testing INFO Step 're-sync on first computer' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'add bookmark 'bink' on first computer and resync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.BmkEngine INFO Local snapshot version: 3 -Service.BmkEngine INFO Server maxVersion: 3 -Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) -Service.RemoteStore TRACE Local snapshot version == server maxVersion -Service.BmkEngine INFO Reconciling client/server updates -Service.BMSync DEBUG Reconciling 1 against 0 commands -Service.BmkEngine INFO Changes for client: 0 -Service.BmkEngine INFO Predicted changes for server: 1 -Service.BmkEngine INFO Client conflicts: 0 -Service.BmkEngine INFO Server conflicts: 0 -Service.BmkEngine INFO Actual changes for server: 1 -Service.BmkEngine INFO Uploading changes to server -Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/bookmarks/deltas/4 with data: [{"action":"create","GUID":"bink-bookmark-guid-1","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}}] -Service.ResourceSet DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/bookmarks/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":4,"snapEncryption":"none","deltasEncryption":"none","itemCount":7} -Service.Resource DEBUG PUT request successful -Service.BmkEngine INFO Successfully updated deltas and status on server -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":4,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null},"bink-bookmark-guid-1":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"}}} -Service.BmkEngine INFO Sync complete -Testing INFO Step 'add bookmark 'bink' on first computer and resync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'restore client state of second computer' starting. -Testing INFO ----------------------------------------- -Service.BStore TRACE Processing command: {"action":"create","GUID":"yoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null}} -Service.BStore DEBUG -> creating bookmark "Yoogle" -Service.BStore TRACE Processing command: {"action":"create","GUID":"boogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null}} -Service.BStore DEBUG -> creating bookmark "Boogle" -Service.BStore TRACE Processing command: {"action":"create","GUID":"zoogle-bookmark-guid","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null}} -Service.BStore DEBUG -> creating bookmark "Zoogle" -Testing INFO Step 'restore client state of second computer' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'Manually add same bookmark 'bink', but with different GUID, to second computer and resync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/bookmarks.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.BmkEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/bookmarks/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/bookmarks/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.BmkEngine INFO Local snapshot version: 3 -Service.BmkEngine INFO Server maxVersion: 4 -Service.RemoteStore DEBUG Using last sync snapshot as starting point for server snapshot -Service.RemoteStore INFO Downloading server deltas -Testing INFO HTTP GET from user-data/bookmarks/deltas/4, returning status 200 -Service.ResourceSet DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data -Service.SnapStore TRACE Processing command: {"action":"create","GUID":"bink-bookmark-guid-1","depth":1,"parents":["menu"],"data":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}} -Service.BmkEngine INFO Reconciling client/server updates -Service.BMSync DEBUG Reconciling 1 against 1 commands -Service.BmkEngine INFO Changes for client: 1 -Service.BmkEngine INFO Predicted changes for server: 0 -Service.BmkEngine INFO Client conflicts: 0 -Service.BmkEngine INFO Server conflicts: 0 -Service.BmkEngine INFO Applying changes locally -Service.SnapStore TRACE Processing command: {"action":"edit","GUID":"bink-bookmark-guid-2","data":{"GUID":"bink-bookmark-guid-1"}} -Service.BStore TRACE Processing command: {"action":"edit","GUID":"bink-bookmark-guid-2","data":{"GUID":"bink-bookmark-guid-1"}} -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/bookmarks.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/bookmarks.json': {"version":4,"GUID":"fake-guid-0","snapshot":{"yoogle-bookmark-guid":{"parentGUID":"menu","index":0,"type":"bookmark","title":"Yoogle","URI":"http://www.yoogle.com/","tags":[],"keyword":null},"boogle-bookmark-guid":{"parentGUID":"menu","index":1,"type":"bookmark","title":"Boogle","URI":"http://www.boogle.com/","tags":[],"keyword":null},"zoogle-bookmark-guid":{"parentGUID":"menu","index":2,"type":"bookmark","title":"Zoogle","URI":"http://www.zoogle.com/","tags":[],"keyword":null},"menu":{"type":"folder"},"toolbar":{"type":"folder"},"unfiled":{"type":"folder"},"bink-bookmark-guid-1":{"parentGUID":"menu","index":3,"type":"bookmark","title":"Bink","URI":"http://www.bink.com/","tags":[],"keyword":null}}} -Service.BmkEngine INFO Actual changes for server: 0 -Service.BmkEngine INFO Sync complete -Testing INFO Step 'Manually add same bookmark 'bink', but with different GUID, to second computer and resync' succeeded. -*** test finished -*** exiting -*** PASS *** diff --git a/services/sync/tests/unit/need-work/test_cookie_store.js b/services/sync/tests/unit/need-work/test_cookie_store.js deleted file mode 100644 index c80343bc39e6..000000000000 --- a/services/sync/tests/unit/need-work/test_cookie_store.js +++ /dev/null @@ -1,150 +0,0 @@ -Components.utils.import("resource://services-sync/engines/cookies.js"); - -function FakeCookie( host, path, name, value, - isSecure, isHttpOnly, isSession, expiry ) { - this._init( host, path, name, value, - isSecure, isHttpOnly, isSession, expiry ); -} -FakeCookie.prototype = { - _init: function( host, path, name, value, - isSecure, isHttpOnly, isSession, expiry) { - this.host = host; - this.path = path; - this.name = name; - this.value = value; - this.isSecure = isSecure; - this.isHttpOnly = isHttpOnly; - this.isSession = isSession; - }, - - QueryInterface: function( aIID ) { - if ( !aIID.equals( Components.interfaces.nsICookie2 ) ) { - throw Components.results.NS_ERROR_NO_INTERFACE; - } - return this; - } -}; - -function StubEnumerator( list ) { - this._init( list ); -} -StubEnumerator.prototype = { - _init: function( list ) { - this._list = list; - this._pointer = 0; - }, - hasMoreElements: function() { - return ( this._list.length > this._pointer ); - }, - getNext: function() { - var theThing = this._list[ this._pointer ]; - this._pointer++; - return theThing; - } -}; - -function FakeCookieManager() { - this._init(); -} -FakeCookieManager.prototype = { - _init: function() { - this._cookieList = []; - }, - - add: function( host, path, name, value, - isSecure, isHttpOnly, isSession, expiry) { - var newCookie = new FakeCookie( host, - path, - name, - value, - isSecure, - isHttpOnly, - isSession, - expiry ); - this._cookieList.push( newCookie ); - }, - remove: function( host, name, path, alwaysBlock ) { - for (var x in this._cookieList ) { - var cookie = this._cookieList[x]; - if ( cookie.host == host && - cookie.name == name && - cookie.path == path ) { - this._cookieList.splice( x, 1 ); - break; - } - } - }, - - get enumerator() { - var stubEnum = new StubEnumerator( this._cookieList ); - return stubEnum; - }, - - removeAll: function() { - this._cookieList = []; - } -}; - -function sub_test_cookie_tracker() { - var ct = new CookieTracker(); - - // gonna have to use the real cookie manager here... - var cookieManager = Cc["@mozilla.org/cookiemanager;1"]. - getService(Ci.nsICookieManager2); - var d = new Date(); - d.setDate( d.getDate() + 1 ); - cookieManager.add( "www.evilbrainjono.net", - "/blog/", - "comments", - "on", - false, - true, - false, - d.getTime() ); - cookieManager.add( "www.evilbrainjono.net", - "/comic/", - "lang", - "jp", - false, - true, - false, - d.getTime() ); - cookieManager.add( "www.evilbrainjono.net", - "/blog/", - "comments", - "off", - false, - true, - true, // session - d.getTime() ); - // score is 10 per cookie changed, but we should be ignoring the - // session cookie, so we should be at 20 now. - do_check_eq( ct.score, 20 ); -}; - -function run_test() { - /* Set a persistent cookie and a non-persistent cookie - then call cookieStore.wrap() and make sure it returns the persistent - one and not the non-persistent one */ - - // My stub object to replace the real cookieManager: - var fakeCookieManager = new FakeCookieManager(); - - // add a persistent cookie: - var d = new Date(); - d.setDate( d.getDate() + 1 ); - fakeCookieManager.add( "evilbrainjono.net", "/", "login", "jono", - false, true, false, d.getTime() ); - // and a session cookie: - fakeCookieManager.add( "humanized.com", "/", "langauge", "en", - false, true, true, 0 ); - var myStore = new CookieStore( fakeCookieManager ); - var json = myStore.wrap(); - // The json should include only the persistent cookie, not the session - // cookie: - - var jsonGuids = [ guid for ( guid in json ) ]; - do_check_eq( jsonGuids.length, 1 ); - do_check_eq( jsonGuids[0], "evilbrainjono.net:/:login" ); - sub_test_cookie_tracker(); -} diff --git a/services/sync/tests/unit/need-work/test_passphrase_checking.js b/services/sync/tests/unit/need-work/test_passphrase_checking.js deleted file mode 100644 index 1c2e238b1a09..000000000000 --- a/services/sync/tests/unit/need-work/test_passphrase_checking.js +++ /dev/null @@ -1,46 +0,0 @@ -Cu.import("resource://services-sync/async.js"); -Cu.import("resource://services-sync/crypto.js"); -Cu.import("resource://services-sync/dav.js"); -Cu.import("resource://services-sync/identity.js"); -Function.prototype.async = Async.sugar; - -let __fakeCryptoID = { - keypairAlg: "RSA", - // This private key is encrypted with the passphrase 'passphrase', - // contained in our testing infrastructure. - privkey: "3ytj94K6Wo0mBjAVsiIwjm5x2+ENvpKTDUqLCz19iXbESf8RT6O8PmY7Pqcndpn+adqaQdvmr0T1JQ5bfLEHev0WBfo8oWJb+OS4rKoCWxDNzGwrOlW5hCfxSekw0KrKjqZyDZ0hT1Qt9vn6thlV2v9YWfmyn0OIxNC9hUqGwU3Wb2F2ejM0Tw40+IIW4eLEvFxLGv0vnEXpZvesPt413proL6FGQJe6vyapBg+sdX1JMYGaKZY84PUGIiDPxTbQg7yIWTSe3WlDhJ001khFiyEoTZvPhiAGXfML9ycrCRZUWkHp/cfS7QiusJXs6co0tLjrIk/rTk8h4mHBnyPkFIxh4YrfC7Bwf9npwomhaZCEQ32VK+a8grTDsGYHPZexDm3TcD2+d+hZ/u4lUOHFscQKX4w83tq942yqFtElCD2yQoqEDr1Z9zge5XBnLcYiH9hL0ozfpxBlTtpR1kSH663JHqlYim0qhuk0zrGAPkHna07UMFufxvgQBSd/YUqWCimJFGi+5QeOOFO20Skj882Bh1QDYsmbxZ/JED5ocGNHWSqpaOL2ML1F9nD5rdtffI0BsTe+j9h+HV4GlvzUz0Jd6RRf9xN4RyxqfENb8iGH5Pwbry7Qyk16rfm0s6JgG8pNb/8quKD+87RAtQFybZtdQ9NfGg+gyRiU9pbb6FPuPnGp+KpktaHu/K3HnomrVUoyLQALfCSbPXg2D9ta6dRV0JRqOZz4w52hlHIa62iJO6QecbdBzPYGT0QfOy/vp6ndRDR+2xMD/BmlaQwm3+58cqhIw9SVV5h/Z5PVaXxAOqg5vpU1NjrbF4uIFo5rmR0PyA/6qtxZaBY6w3I4sUWdDkIer8QsyrFrO7MIEdxksvDoFIeIM5eN8BufLu3ymS5ZXBiFr/iRxlYcQVHK2hz0/7syWUYsrz5/l1mj+qbWGx+6daWOk3xt4SH+p0hUpMC4FbJ9m/xr4im+X5m5ZYiajaF1QPOXTTny2plE0vfqMVlwX1HFFTJrAP+E85sZI8LPHAYO80qhSi3tV/LHjxCnC1LHJXaRkG202pQFWF1yVT/o82HBt9OC1xY6TVcy4Uh+6piNIQ9FxXGWrzjz0AUkxwkSN3Foqlfiq+mqJmNwzIdEQTmNAcBBsN3vWngU4elHjYI5qFZBzxJIkH8tfvivOshrOZIZB9TD9GIRhQwIBWc6i4fnqE9GUK2Jle6werdFATiMU4msQg7ClURaMn/p3MOLoxTmsPd1iBYPQkqnJgEAdNfKj3KRqSc6M/x09hGDSzK2d9Y03pyDGPh2sopcMFdCQbMy8VOld2/hEGakMJv6BPoRfhKmJbgGVf5x4B9dWZNa8WCmlcxaZ7KG43UA0zLm1VgfTcDW5qazDFoxIcfhmO5KoRI3q8vNs+Wh+smLC6yFODdF9HzrPimEYSc6OWHWgUcuiIBRjKeo5gBTbExWmri2VG+cn005vQNxK+0s7JVyFB8TzZ96pV3nFjkYy9OUkaiJxGd6OVGcvhbbrcNsKkaZff7OsLqczf6x0bhwh+y8+bLjLkuusGYUdBvdeiuv12IfeRupvwD8Z3aZOgcD7d+8VTyTyd/KX9fu8P7tD5SojJ5joRPjcv4Q8/mhRgtwx1McMIL3YnKHG+U=", - privkeyWrapIV: "fZ7CB/KQAUjEhkmrEkns4Q==", - passphraseSalt: "JFs5h2RKX9m0Op9DlQIcCOSfOH1MuDrrrHxCx+CpCUU=", - pubkey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxxwObnXIoYeQKMG9RbvLkgsu/idDo25qOX87jIEiXkgW1wLKp/1D/DBLUEW303tVszNGVt6bTyAXOIj6skpmYoDs9Z48kvU3+g7Vi4QXEw5moSS4fr+yFpKiYd2Kx1+jCFGvGZjBzAAvnjsWmWrSA+LHJSrFlKY6SM3kNg8KrE8dxUi3wztlZnhZgo1ZYe7/VeBOXUfThtoadIl1VdREw2e79eiMQpPa0XLv4grCaMd/wLRs0be1/nPt7li4NyT0fnYFWg75SU3ni/xSaq/zR4NmW/of5vB2EcKyUG+/mvNplQ0CX+v3hRBCdhpCyPmcbHKUluyKzj7Ms9pKyCkwxwIDAQAB" - }; - -function testGenerator() { - let self = yield; - - let id = ID.get("WeaveCryptoID"); - for (name in __fakeCryptoID) - id[name] = __fakeCryptoID[name]; - - Crypto.isPassphraseValid.async(Crypto, self.cb, id); - let result = yield; - - do_check_eq(result, true); - - id.setTempPassword("incorrect passphrase"); - - Crypto.isPassphraseValid.async(Crypto, self.cb, id); - result = yield; - - do_check_eq(result, false); - - self.done(); -} - -function run_test() { - let syncTesting = new SyncTestingInfrastructure(); - - syncTesting.runAsyncFunc( - "Ensuring isPassphraseValid() works", - function runTest(cb) { testGenerator.async({}, cb); } - ); -} diff --git a/services/sync/tests/unit/need-work/test_password_syncing.js b/services/sync/tests/unit/need-work/test_password_syncing.js deleted file mode 100644 index 0d57b08561fc..000000000000 --- a/services/sync/tests/unit/need-work/test_password_syncing.js +++ /dev/null @@ -1,37 +0,0 @@ -Cu.import("resource://services-sync/engines/passwords.js"); - -load("fake_login_manager.js"); - -// ---------------------------------------- -// Test Logic -// ---------------------------------------- - -function run_test() { - function passwdFactory() { return new PasswordEngine(); } - var syncTesting = new SyncTestingInfrastructure(passwdFactory); - var fakeLoginManager = new FakeLoginManager(fakeSampleLogins); - - syncTesting.doSync("initial sync"); - - syncTesting.doSync("trivial re-sync"); - - fakeLoginManager.fakeLogins.push( - {hostname: "www.yoogle.com", - formSubmitURL: "http://www.yoogle.com/search", - httpRealm: "", - username: "", - password: "", - usernameField: "test_person2", - passwordField: "test_password2"} - ); - - syncTesting.doSync("add user and re-sync"); - - fakeLoginManager.fakeLogins.pop(); - - syncTesting.doSync("remove user and re-sync"); - - syncTesting.resetClientState(); - - syncTesting.doSync("resync on second computer"); -} diff --git a/services/sync/tests/unit/need-work/test_password_syncing.log.expected b/services/sync/tests/unit/need-work/test_password_syncing.log.expected deleted file mode 100644 index c47fb009ef7b..000000000000 --- a/services/sync/tests/unit/need-work/test_password_syncing.log.expected +++ /dev/null @@ -1,174 +0,0 @@ -*** test pending -Testing INFO ----------------------------------------- -Testing INFO Step 'initial sync' starting. -Testing INFO ----------------------------------------- -Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/passwords/status.json, returning status 404 -Service.PasswordEngine INFO Initial upload to server -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/keys.json with data: {"ring":{},"bulkIV":null} -Service.Resource DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/passwords/snapshot.json with data: {"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} -Service.Resource DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":0,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} -Service.Resource DEBUG PUT request successful -Service.RemoteStore INFO Full upload to server successful -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/passwords.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":0,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} -Testing INFO Step 'initial sync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'trivial re-sync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/passwords.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.PasswordEngine INFO Local snapshot version: 0 -Service.PasswordEngine INFO Server maxVersion: 0 -Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) -Service.RemoteStore TRACE Local snapshot version == server maxVersion -Service.PasswordEngine INFO Sync complete: no changes needed on client or server -Testing INFO Step 'trivial re-sync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'add user and re-sync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/passwords.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.PasswordEngine INFO Local snapshot version: 0 -Service.PasswordEngine INFO Server maxVersion: 0 -Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) -Service.RemoteStore TRACE Local snapshot version == server maxVersion -Service.PasswordEngine INFO Reconciling client/server updates -Service.PasswordSync DEBUG Reconciling 1 against 0 commands -Service.PasswordEngine INFO Changes for client: 0 -Service.PasswordEngine INFO Predicted changes for server: 1 -Service.PasswordEngine INFO Client conflicts: 0 -Service.PasswordEngine INFO Server conflicts: 0 -Service.PasswordEngine INFO Actual changes for server: 1 -Service.PasswordEngine INFO Uploading changes to server -Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/passwords/deltas/1 with data: [{"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}] -Service.ResourceSet DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":1,"snapEncryption":"none","deltasEncryption":"none","itemCount":2} -Service.Resource DEBUG PUT request successful -Service.PasswordEngine INFO Successfully updated deltas and status on server -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/passwords.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":1,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"},"1b3869fc36234b39cd354f661ed1d7d148394ca3":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}}} -Service.PasswordEngine INFO Sync complete -Testing INFO Step 'add user and re-sync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'remove user and re-sync' starting. -Testing INFO ----------------------------------------- -Testing INFO Opening 'weave/snapshots/passwords.json' for reading. -Testing INFO Reading from stream. -Service.SnapStore INFO Read saved snapshot from disk -Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.PasswordEngine INFO Local snapshot version: 1 -Service.PasswordEngine INFO Server maxVersion: 1 -Service.RemoteStore DEBUG Using last sync snapshot as server snapshot (snap version == max version) -Service.RemoteStore TRACE Local snapshot version == server maxVersion -Service.PasswordEngine INFO Reconciling client/server updates -Service.PasswordSync DEBUG Reconciling 1 against 0 commands -Service.PasswordEngine INFO Changes for client: 0 -Service.PasswordEngine INFO Predicted changes for server: 1 -Service.PasswordEngine INFO Client conflicts: 0 -Service.PasswordEngine INFO Server conflicts: 0 -Service.PasswordEngine INFO Actual changes for server: 1 -Service.PasswordEngine INFO Uploading changes to server -Service.JsonFilter DEBUG Encoding data as JSON -Service.CryptoFilter DEBUG Encrypting data -Service.Crypto DEBUG NOT encrypting data -Testing INFO HTTP PUT to user-data/passwords/deltas/2 with data: [{"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]}] -Service.ResourceSet DEBUG PUT request successful -Service.JsonFilter DEBUG Encoding data as JSON -Testing INFO HTTP PUT to user-data/passwords/status.json with data: {"GUID":"fake-guid-0","formatVersion":3,"snapVersion":0,"maxVersion":2,"snapEncryption":"none","deltasEncryption":"none","itemCount":1} -Service.Resource DEBUG PUT request successful -Service.PasswordEngine INFO Successfully updated deltas and status on server -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/passwords.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} -Service.PasswordEngine INFO Sync complete -Testing INFO Step 'remove user and re-sync' succeeded. -Testing INFO ----------------------------------------- -Testing INFO Step 'resync on second computer' starting. -Testing INFO ----------------------------------------- -Service.PasswordEngine INFO Beginning sync -Testing INFO HTTP MKCOL on user-data/passwords/deltas -Service.RemoteStore DEBUG Downloading status file -Testing INFO HTTP GET from user-data/passwords/status.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.JsonFilter DEBUG Decoding JSON data -Service.RemoteStore DEBUG Downloading status file... done -Service.PasswordEngine DEBUG Remote/local sync GUIDs do not match. Forcing initial sync. -Service.PasswordEngine INFO Local snapshot version: -1 -Service.PasswordEngine INFO Server maxVersion: 2 -Service.RemoteStore TRACE Getting latest from snap --> scratch -Service.RemoteStore INFO Downloading all server data from scratch -Testing INFO HTTP GET from user-data/passwords/snapshot.json, returning status 200 -Service.Resource DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data -Testing INFO HTTP GET from user-data/passwords/deltas/1, returning status 200 -Service.ResourceSet DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data -Service.SnapStore TRACE Processing command: {"action":"create","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[],"data":{"hostname":"www.yoogle.com","formSubmitURL":"http://www.yoogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person2","passwordField":"test_password2"}} -Testing INFO HTTP GET from user-data/passwords/deltas/2, returning status 200 -Service.ResourceSet DEBUG GET request successful -Service.CryptoFilter DEBUG Decrypting data -Service.Crypto DEBUG NOT decrypting data -Service.JsonFilter DEBUG Decoding JSON data -Service.SnapStore TRACE Processing command: {"action":"remove","GUID":"1b3869fc36234b39cd354f661ed1d7d148394ca3","depth":0,"parents":[]} -Service.PasswordEngine INFO Reconciling client/server updates -Service.PasswordSync DEBUG Reconciling 0 against 1 commands -Service.PasswordEngine INFO Changes for client: 1 -Service.PasswordEngine INFO Predicted changes for server: 0 -Service.PasswordEngine INFO Client conflicts: 0 -Service.PasswordEngine INFO Server conflicts: 0 -Service.PasswordEngine INFO Applying changes locally -Service.SnapStore TRACE Processing command: {"action":"create","GUID":"805ec58eb8dcded602999967e139be21acd0f194","depth":0,"parents":[],"data":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} -Service.PasswordStore TRACE Processing command: {"action":"create","GUID":"805ec58eb8dcded602999967e139be21acd0f194","depth":0,"parents":[],"data":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}} -Service.PasswordStore INFO PasswordStore got createCommand: [object Object] -Testing INFO nsILoginManager.addLogin() called with hostname 'www.boogle.com'. -Service.SnapStore INFO Saving snapshot to disk -Testing INFO Opening 'weave/snapshots/passwords.json' for writing. -Testing INFO Writing data to local file 'weave/snapshots/passwords.json': {"version":2,"GUID":"fake-guid-0","snapshot":{"805ec58eb8dcded602999967e139be21acd0f194":{"hostname":"www.boogle.com","formSubmitURL":"http://www.boogle.com/search","httpRealm":"","username":"","password":"","usernameField":"test_person","passwordField":"test_password"}}} -Service.PasswordEngine INFO Actual changes for server: 0 -Service.PasswordEngine INFO Sync complete -Testing INFO Step 'resync on second computer' succeeded. -*** test finished -*** exiting -*** PASS *** diff --git a/services/sync/tests/unit/need-work/test_passwords.js b/services/sync/tests/unit/need-work/test_passwords.js deleted file mode 100644 index 9b75fccc91ca..000000000000 --- a/services/sync/tests/unit/need-work/test_passwords.js +++ /dev/null @@ -1,23 +0,0 @@ -load("fake_login_manager.js"); - -var loginMgr = new FakeLoginManager(fakeSampleLogins); - -// The JS module we're testing, with all members exposed. -var passwords = loadInSandbox("resource://services-sync/engines/passwords.js"); - -function test_hashLoginInfo_works() { - var pwStore = new passwords.PasswordStore(); - var fakeUserHash = pwStore._hashLoginInfo(fakeSampleLogins[0]); - do_check_eq(typeof fakeUserHash, 'string'); - do_check_eq(fakeUserHash.length, 40); -} - -function test_synccore_itemexists_works() { - var pwStore = new passwords.PasswordStore(); - var fakeUserHash = pwStore._hashLoginInfo(fakeSampleLogins[0]); - var psc = new passwords.PasswordSyncCore(); - /* wrap needs to be called before _itemExists */ - pwStore.wrap(); - do_check_false(pwStore._itemExists("invalid guid")); - do_check_true(pwStore._itemExists(fakeUserHash)); -} diff --git a/services/sync/tests/unit/need-work/test_service.js b/services/sync/tests/unit/need-work/test_service.js deleted file mode 100644 index 953a2285e684..000000000000 --- a/services/sync/tests/unit/need-work/test_service.js +++ /dev/null @@ -1,58 +0,0 @@ -Cu.import("resource://services-sync/async.js"); -Cu.import("resource://services-sync/crypto.js"); -Cu.import("resource://services-sync/log4moz.js"); - -Function.prototype.async = Async.sugar; - -let __fakePrefs = { - "log.logger.async" : "Debug", - "username" : "foo", - "serverURL" : "https://example.com/", - "encryption" : "aes-256-cbc", - "enabled" : true, - "schedule" : 0 -}; - -let __fakeDAVContents = { - "meta/version" : "3", - "private/privkey" : '{"version":1,"algorithm":"RSA"}', - "public/pubkey" : '{"version":1,"algorithm":"RSA"}' -}; - -let Service = loadInSandbox("resource://services-sync/service.js"); - -function TestService() { - this.__superclassConstructor = Service.WeaveSvc; - this.__superclassConstructor([]); -} - -TestService.prototype = { - _initLogs: function TS__initLogs() { - this._log = Log4Moz.Service.getLogger("Service.Main"); - } -}; -TestService.prototype.__proto__ = Service.WeaveSvc.prototype; - -Crypto.isPassphraseValid = function fake_isPassphraseValid(id) { - let self = yield; - - do_check_eq(id.password, "passphrase"); - - self.done(true); -}; - -function test_login_works() { - var syncTesting = new SyncTestingInfrastructure(); - - syncTesting.fakeDAVService.fakeContents = __fakeDAVContents; - for (name in __fakePrefs) - syncTesting.fakePrefService.fakeContents[name] = __fakePrefs[name]; - - var testService = new TestService(); - - function login(cb) { - testService.login(cb); - } - - syncTesting.runAsyncFunc("Logging in", login); -} diff --git a/services/sync/tests/unit/need-work/test_service.log.expected b/services/sync/tests/unit/need-work/test_service.log.expected deleted file mode 100644 index 6ab71c065a84..000000000000 --- a/services/sync/tests/unit/need-work/test_service.log.expected +++ /dev/null @@ -1,18 +0,0 @@ -*** test pending -Running test: test_login_works -Service.Main INFO Weave Sync Service Initializing -Testing INFO ----------------------------------------- -Testing INFO Step 'Logging in' starting. -Testing INFO ----------------------------------------- -Service.Main DEBUG Logging in user foo -Service.Main INFO Using server URL: https://example.com/user/foo -Testing INFO HTTP GET from meta/version, returning status 200 -Service.Main TRACE Retrieving keypair from server -Testing INFO HTTP GET from private/privkey, returning status 200 -Testing INFO HTTP GET from public/pubkey, returning status 200 -Service.Main INFO Weave scheduler disabled -Testing INFO Step 'Logging in' succeeded. -1 of 1 tests passed. -*** test finished -*** exiting -*** PASS *** diff --git a/services/sync/tests/unit/need-work/test_util_tracebacks.js b/services/sync/tests/unit/need-work/test_util_tracebacks.js deleted file mode 100644 index 57824819219b..000000000000 --- a/services/sync/tests/unit/need-work/test_util_tracebacks.js +++ /dev/null @@ -1,36 +0,0 @@ -Cu.import("resource://services-sync/util.js"); - -function _reportException(e) { - dump("Exception caught.\n"); - dump("Exception: " + Utils.exceptionStr(e) + "\n"); - dump("Traceback:\n\n" + Utils.stackTrace(e) + "\n"); -} - -function test_stackTrace_works_with_error_object() { - try { - dump("Throwing new Error object.\n"); - throw new Error("Error!"); - } catch (e) { - _reportException(e); - } -} - -function test_stackTrace_works_with_bare_string() { - try { - dump("Throwing bare string.\n"); - throw "Error!"; - } catch (e) { - _reportException(e); - } -} - -function test_stackTrace_works_with_nsIException() { - try { - dump("Throwing a wrapped nsIException.\n"); - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - dirSvc.get("nonexistentPropertyName"); - } catch (e) { - _reportException(e); - } -} diff --git a/services/sync/tests/unit/need-work/test_util_tracebacks.log.expected b/services/sync/tests/unit/need-work/test_util_tracebacks.log.expected deleted file mode 100644 index ad816f85b6b9..000000000000 --- a/services/sync/tests/unit/need-work/test_util_tracebacks.log.expected +++ /dev/null @@ -1,36 +0,0 @@ -*** test pending -Running test: test_stackTrace_works_with_error_object -Throwing new Error object. -Exception caught. -Exception: Error! (file 'test_util_tracebacks.js', line 12) -Traceback: - -Error("Error!")@:0 -test_stackTrace_works_with_error_object()@test_util_tracebacks.js:12 -_find_and_run_tests()@../harness/head.js:106 -_execute_test(_find_and_run_tests)@../harness/head.js:144 -@../harness/tail.js:38 - -Running test: test_stackTrace_works_with_bare_string -Throwing bare string. -Exception caught. -Exception: Error! -Traceback: - -No traceback available. - -Running test: test_stackTrace_works_with_nsIException -Throwing a wrapped nsIException. -Exception caught. -Exception: Not enough arguments [nsIProperties.get] (JS frame :: test_util_tracebacks.js :: test_stackTrace_works_with_nsIException :: line 32) -Traceback: - -JS frame :: test_util_tracebacks.js :: test_stackTrace_works_with_nsIException :: line 32 -JS frame :: ../harness/head.js :: _find_and_run_tests :: line 106 -JS frame :: ../harness/head.js :: _execute_test :: line 144 -JS frame :: ../harness/tail.js :: :: line 38 - -3 of 3 tests passed. -*** test finished -*** exiting -*** PASS *** From df91da96440c4126ec7bc1dd3ac34d66d8b1639a Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Wed, 16 Jun 2010 23:11:40 +0100 Subject: [PATCH 1727/1860] Bug 572436 - Get rid of app-specific hacks (switch (Svc.AppInfo.ID)) in sync library [r=mconnor] Set relevant default preferences programmatically in app specific overlays, making app-specific code paths in the sync library unnecessary. --- services/sync/modules/engines/clients.js | 16 +--------------- services/sync/modules/ext/Preferences.js | 15 +++++++++++---- services/sync/modules/service.js | 17 +---------------- services/sync/modules/util.js | 1 + 4 files changed, 14 insertions(+), 35 deletions(-) diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js index e9cefd2906d6..1e79050c2275 100644 --- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -139,21 +139,7 @@ ClientEngine.prototype = { }, set localName(value) Svc.Prefs.set("client.name", value), - get localType() { - // Figure out if we have a type previously set - let localType = Svc.Prefs.get("client.type", ""); - if (localType == "") { - // Assume we're desktop-like unless the app is for mobiles - localType = "desktop"; - switch (Svc.AppInfo.ID) { - case FENNEC_ID: - localType = "mobile"; - break; - } - this.localType = localType; - } - return localType; - }, + get localType() Svc.Prefs.get("client.type", "desktop"), set localType(value) Svc.Prefs.set("client.type", value), isMobile: function isMobile(id) { diff --git a/services/sync/modules/ext/Preferences.js b/services/sync/modules/ext/Preferences.js index c712bb4f0429..0149fde6d005 100644 --- a/services/sync/modules/ext/Preferences.js +++ b/services/sync/modules/ext/Preferences.js @@ -55,6 +55,8 @@ function Preferences(args) { if (isObject(args)) { if (args.branch) this._prefBranch = args.branch; + if (args.defaultBranch) + this._defaultBranch = args.defaultBranch; if (args.site) this._site = args.site; } @@ -429,10 +431,15 @@ Preferences.prototype = { * @private */ get _prefSvc() { - let prefSvc = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefService). - getBranch(this._prefBranch). - QueryInterface(Ci.nsIPrefBranch2); + let prefSvc = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService); + if (this._defaultBranch) { + prefSvc = prefSvc.getDefaultBranch(this._prefBranch); + } else { + prefSvc = prefSvc.getBranch(this._prefBranch) + .QueryInterface(Ci.nsIPrefBranch2); + } + this.__defineGetter__("_prefSvc", function() prefSvc); return this._prefSvc; }, diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 3f98235f4deb..c91ac5bc64f3 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -394,24 +394,9 @@ WeaveSvc.prototype = { let pref = Svc.Prefs.get("registerEngines"); if (pref) { engines = pref.split(","); - } else { - // Fallback for the add-on case - switch (Svc.AppInfo.ID) { - case FENNEC_ID: - engines = ["Tab", "Bookmarks", "Form", "History", "Password"]; - break; - - case FIREFOX_ID: - engines = ["Bookmarks", "Form", "History", "Password", "Prefs", "Tab"]; - break; - - case SEAMONKEY_ID: - engines = ["Form", "History", "Password", "Tab"]; - break; - } } - // Grab the actual engine and register them + // Grab the actual engines and register them Engines.register(engines.map(function(name) Weave[name + "Engine"])); }, diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 0f046b312de8..2cf1f496171d 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -899,6 +899,7 @@ Utils.lazySvc(FakeSvc, "@labs.mozilla.com/Weave/Crypto;2", */ let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); +Svc.DefaultPrefs = new Preferences({branch: PREFS_BRANCH, defaultBranch: true}); Svc.Obs = Observers; [["Annos", "@mozilla.org/browser/annotation-service;1", "nsIAnnotationService"], ["AppInfo", "@mozilla.org/xre/app-info;1", "nsIXULAppInfo"], From a9d37e76be582e4fc9a9b1e39ec5788018542108 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Wed, 9 Jun 2010 11:07:54 -0700 Subject: [PATCH 1728/1860] Bug 557589 - code audit and create unit test plan for service.js [r=mconnor] Part 3: Tests for Weave.Service._{find|set|update}Cluster() --- services/sync/modules/service.js | 2 +- .../sync/tests/unit/test_service_cluster.js | 146 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 services/sync/tests/unit/test_service_cluster.js diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index c91ac5bc64f3..0e2180783012 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -502,7 +502,7 @@ WeaveSvc.prototype = { node = null; return node; default: - this._log.debug("Unexpected response code: " + node.status); + fail = "Unexpected response code: " + node.status; break; } } catch (e) { diff --git a/services/sync/tests/unit/test_service_cluster.js b/services/sync/tests/unit/test_service_cluster.js new file mode 100644 index 000000000000..f414718d5ef2 --- /dev/null +++ b/services/sync/tests/unit/test_service_cluster.js @@ -0,0 +1,146 @@ +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/util.js"); + +function do_check_throws(func) { + var raised = false; + try { + func(); + } catch (ex) { + raised = true; + } + do_check_true(raised); +} + +function send(statusCode, status, body) { + return function(request, response) { + response.setStatusLine(request.httpVersion, statusCode, status); + response.bodyOutputStream.write(body, body.length); + }; +} + +function test_findCluster() { + _("Test Weave.Service._findCluster()"); + let server; + try { + Weave.Service.serverURL = "http://localhost:8080/"; + Weave.Service.username = "johndoe"; + + _("_findCluster() throws on network errors (e.g. connection refused)."); + do_check_throws(function() { + Weave.Service._findCluster(); + }); + + server = httpd_setup({ + "/user/1.0/johndoe/node/weave": send(200, "OK", "http://weave.user.node/"), + "/user/1.0/jimdoe/node/weave": send(200, "OK", "null"), + "/user/1.0/janedoe/node/weave": send(404, "Not Found", "Not Found"), + "/user/1.0/juliadoe/node/weave": send(400, "Bad Request", "Bad Request"), + "/user/1.0/joedoe/node/weave": send(500, "Server Error", "Server Error") + }); + + _("_findCluster() returns the user's cluster node"); + let cluster = Weave.Service._findCluster(); + do_check_eq(cluster, "http://weave.user.node/"); + + _("A 'null' response is converted to null."); + Weave.Service.username = "jimdoe"; + cluster = Weave.Service._findCluster(); + do_check_eq(cluster, null); + + _("If a 404 is encountered, the server URL is taken as the cluster URL"); + Weave.Service.username = "janedoe"; + cluster = Weave.Service._findCluster(); + do_check_eq(cluster, Weave.Service.serverURL); + + _("A 400 response will throw an error."); + Weave.Service.username = "juliadoe"; + do_check_throws(function() { + Weave.Service._findCluster(); + }); + + _("Any other server response (e.g. 500) will throw an error."); + Weave.Service.username = "joedoe"; + do_check_throws(function() { + Weave.Service._findCluster(); + }); + + } finally { + Svc.Prefs.resetBranch(""); + if (server) { + server.stop(function() {}); + } + } +} + + +function test_setCluster() { + _("Test Weave.Service._setCluster()"); + let server = httpd_setup({ + "/user/1.0/johndoe/node/weave": send(200, "OK", "http://weave.user.node/"), + "/user/1.0/jimdoe/node/weave": send(200, "OK", "null") + }); + try { + Weave.Service.serverURL = "http://localhost:8080/"; + Weave.Service.username = "johndoe"; + + _("Check initial state."); + do_check_eq(Weave.Service.clusterURL, ""); + + _("Set the cluster URL."); + do_check_true(Weave.Service._setCluster()); + do_check_eq(Weave.Service.clusterURL, "http://weave.user.node/"); + + _("Setting it again won't make a difference if it's the same one."); + do_check_false(Weave.Service._setCluster()); + do_check_eq(Weave.Service.clusterURL, "http://weave.user.node/"); + + _("A 'null' response won't make a difference either."); + Weave.Service.username = "jimdoe"; + do_check_false(Weave.Service._setCluster()); + do_check_eq(Weave.Service.clusterURL, "http://weave.user.node/"); + + } finally { + Svc.Prefs.resetBranch(""); + server.stop(function() {}); + } +} + +function test_updateCluster() { + _("Test Weave.Service._updateCluster()"); + let server = httpd_setup({ + "/user/1.0/johndoe/node/weave": send(200, "OK", "http://weave.user.node/"), + "/user/1.0/janedoe/node/weave": send(200, "OK", "http://weave.cluster.url/") + }); + try { + Weave.Service.serverURL = "http://localhost:8080/"; + Weave.Service.username = "johndoe"; + + _("Check initial state."); + do_check_eq(Weave.Service.clusterURL, ""); + do_check_eq(Svc.Prefs.get("lastClusterUpdate"), null); + + _("Set the cluster URL."); + do_check_true(Weave.Service._updateCluster()); + do_check_eq(Weave.Service.clusterURL, "http://weave.user.node/"); + let lastUpdate = parseInt(Svc.Prefs.get("lastClusterUpdate"), 10); + do_check_true(lastUpdate > Date.now() - 1000); + + _("Trying to update the cluster URL within the backoff timeout won't do anything."); + do_check_false(Weave.Service._updateCluster()); + do_check_eq(Weave.Service.clusterURL, "http://weave.user.node/"); + do_check_eq(parseInt(Svc.Prefs.get("lastClusterUpdate"), 10), lastUpdate); + + _("Time travel 30 mins into the past and the update will work."); + Weave.Service.username = "janedoe"; + Svc.Prefs.set("lastClusterUpdate", (lastUpdate - 30*60*1000).toString()); + + do_check_true(Weave.Service._updateCluster()); + do_check_eq(Weave.Service.clusterURL, "http://weave.cluster.url/"); + lastUpdate = parseInt(Svc.Prefs.get("lastClusterUpdate"), 10); + do_check_true(lastUpdate > Date.now() - 1000); + + } finally { + Svc.Prefs.resetBranch(""); + server.stop(function() {}); + } +} From 326c1c8707b60ebbd97c8c6d6e9f6269a1c82427 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Thu, 17 Jun 2010 04:12:38 +0100 Subject: [PATCH 1729/1860] Bug 557589 - code audit and create unit test plan for service.js [r=mconnor] Part 4: Get rid of superfluous attribute, introduce constants for password/passphrase realms, add/improve tests for login(), logout(), persistLogin(). --- services/sync/modules/constants.js | 2 + services/sync/modules/service.js | 8 +--- .../sync/tests/unit/test_service_login.js | 35 +++++++++++++++-- .../tests/unit/test_service_persistLogin.js | 39 +++++++++++++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 services/sync/tests/unit/test_service_persistLogin.js diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 850bc0b6b101..ee6dc86408ee 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -53,6 +53,8 @@ PREFS_BRANCH: "services.sync.", // Host "key" to access Weave Identity in the password manager PWDMGR_HOST: "chrome://weave", +PWDMGR_PASSWORD_REALM: "Mozilla Services Password", +PWDMGR_PASSPHRASE_REALM: "Mozilla Services Encryption Passphrase", // Sync intervals for various clients configurations SINGLE_USER_SYNC: 24 * 60 * 60 * 1000, // 1 day diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 0e2180783012..ebc1df547b9e 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -103,9 +103,6 @@ WeaveSvc.prototype = { _loggedIn: false, keyGenEnabled: true, - // object for caching public and private keys - _keyPair: {}, - get username() { return Svc.Prefs.get("username", "").toLowerCase(); }, @@ -287,11 +284,11 @@ WeaveSvc.prototype = { this._log.info("Weave Sync disabled"); // Create Weave identities (for logging in, and for encryption) - ID.set('WeaveID', new Identity('Mozilla Services Password', this.username)); + ID.set('WeaveID', new Identity(PWDMGR_PASSWORD_REALM, this.username)); Auth.defaultAuthenticator = new BasicAuthenticator(ID.get('WeaveID')); ID.set('WeaveCryptoID', - new Identity('Mozilla Services Encryption Passphrase', this.username)); + new Identity(PWDMGR_PASSPHRASE_REALM, this.username)); this._updateCachedURLs(); @@ -764,7 +761,6 @@ WeaveSvc.prototype = { this._log.info("Logging out"); this._loggedIn = false; - this._keyPair = {}; // Cancel the sync timer now that we're logged out this._checkSyncStatus(); diff --git a/services/sync/tests/unit/test_service_login.js b/services/sync/tests/unit/test_service_login.js index b6b58a1daa69..8efdbfef5dfd 100644 --- a/services/sync/tests/unit/test_service_login.js +++ b/services/sync/tests/unit/test_service_login.js @@ -6,9 +6,11 @@ Cu.import("resource://services-sync/util.js"); function login_handler(request, response) { // btoa('johndoe:ilovejane') == am9obmRvZTppbG92ZWphbmU= + // btoa('janedoe:ilovejohn') == amFuZWRvZTppbG92ZWpvaG4= let body; - if (request.hasHeader("Authorization") && - request.getHeader("Authorization") == "Basic am9obmRvZTppbG92ZWphbmU=") { + let header = request.getHeader("Authorization"); + if (header == "Basic am9obmRvZTppbG92ZWphbmU=" + || header == "Basic amFuZWRvZTppbG92ZWpvaG4=") { body = "{}"; response.setStatusLine(request.httpVersion, 200, "OK"); } else { @@ -23,7 +25,8 @@ function run_test() { Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); let server = httpd_setup({ - "/1.0/johndoe/info/collections": login_handler + "/1.0/johndoe/info/collections": login_handler, + "/1.0/janedoe/info/collections": login_handler }); try { @@ -58,6 +61,32 @@ function run_test() { do_check_true(Weave.Service.isLoggedIn); do_check_true(Svc.Prefs.get("autoconnect")); + _("We can also pass username, password and passphrase to login()."); + Weave.Service.login("janedoe", "incorrectpassword", "bar"); + do_check_eq(Weave.Service.username, "janedoe"); + do_check_eq(Weave.Service.password, "incorrectpassword"); + do_check_eq(Weave.Service.passphrase, "bar"); + do_check_eq(Status.service, LOGIN_FAILED); + do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED); + do_check_false(Weave.Service.isLoggedIn); + + _("Try again with correct password."); + Weave.Service.login("janedoe", "ilovejohn"); + do_check_eq(Status.service, STATUS_OK); + do_check_eq(Status.login, LOGIN_SUCCEEDED); + do_check_true(Weave.Service.isLoggedIn); + do_check_true(Svc.Prefs.get("autoconnect")); + + _("Logout."); + Weave.Service.logout(); + do_check_false(Weave.Service.isLoggedIn); + do_check_false(Svc.Prefs.get("autoconnect")); + + _("Logging out again won't do any harm."); + Weave.Service.logout(); + do_check_false(Weave.Service.isLoggedIn); + do_check_false(Svc.Prefs.get("autoconnect")); + } finally { Svc.Prefs.resetBranch(""); server.stop(function() {}); diff --git a/services/sync/tests/unit/test_service_persistLogin.js b/services/sync/tests/unit/test_service_persistLogin.js new file mode 100644 index 000000000000..7c54c3ee0617 --- /dev/null +++ b/services/sync/tests/unit/test_service_persistLogin.js @@ -0,0 +1,39 @@ +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/constants.js"); + +function run_test() { + try { + Weave.Service.username = "johndoe"; + Weave.Service.password = "ilovejane"; + Weave.Service.passphrase = "my preciousss"; + + _("Confirm initial environment is empty."); + let logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null, + PWDMGR_PASSWORD_REALM); + do_check_eq(logins.length, 0); + logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null, + PWDMGR_PASSPHRASE_REALM); + do_check_eq(logins.length, 0); + + _("Persist logins to the login service"); + Weave.Service.persistLogin(); + + _("The password has been persisted in the login service."); + logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null, + PWDMGR_PASSWORD_REALM); + do_check_eq(logins.length, 1); + do_check_eq(logins[0].username, "johndoe"); + do_check_eq(logins[0].password, "ilovejane"); + + _("The passphrase has been persisted in the login service."); + logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null, + PWDMGR_PASSPHRASE_REALM); + do_check_eq(logins.length, 1); + do_check_eq(logins[0].username, "johndoe"); + do_check_eq(logins[0].password, "my preciousss"); + + } finally { + Weave.Svc.Prefs.resetBranch(""); + Weave.Svc.Login.removeAllLogins(); + } +} From 5f806f563b0868e87274ab8438106470a2bf61d1 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Thu, 17 Jun 2010 16:47:13 +0100 Subject: [PATCH 1730/1860] Bug 557589 - code audit and create unit test plan for service.js [r=mconnor] Part 5: Tests for checkUsername, createAccount, changePassword --- services/sync/modules/service.js | 3 + .../tests/unit/test_service_changePassword.js | 57 +++++++++++++++++++ .../tests/unit/test_service_checkUsername.js | 31 ++++++++++ .../tests/unit/test_service_createAccount.js | 56 ++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 services/sync/tests/unit/test_service_changePassword.js create mode 100644 services/sync/tests/unit/test_service_checkUsername.js create mode 100644 services/sync/tests/unit/test_service_createAccount.js diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index ebc1df547b9e..8f95a4b08699 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -828,6 +828,9 @@ WeaveSvc.prototype = { let url = this.userAPI + username; let res = new Resource(url); res.authenticator = new Weave.NoOpAuthenticator(); + + // Hint to server to allow scripted user creation or otherwise + // ignore captcha. if (Svc.Prefs.isSet("admin-secret")) res.setHeader("X-Weave-Secret", Svc.Prefs.get("admin-secret", "")); diff --git a/services/sync/tests/unit/test_service_changePassword.js b/services/sync/tests/unit/test_service_changePassword.js new file mode 100644 index 000000000000..9da6a33569e0 --- /dev/null +++ b/services/sync/tests/unit/test_service_changePassword.js @@ -0,0 +1,57 @@ +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/constants.js"); + +function run_test() { + var requestBody; + function send(statusCode, status, body) { + return function(request, response) { + requestBody = readBytesFromInputStream(request.bodyInputStream); + response.setStatusLine(request.httpVersion, statusCode, status); + response.bodyOutputStream.write(body, body.length); + }; + } + + let server; + + try { + + Weave.Service.serverURL = "http://localhost:8080/"; + Weave.Service.username = "johndoe"; + Weave.Service.password = "ilovejane"; + + _("changePassword() returns false for a network error, the password won't change."); + let res = Weave.Service.changePassword("ILoveJane83"); + do_check_false(res); + do_check_eq(Weave.Service.password, "ilovejane"); + + _("Let's fire up the server and actually change the password."); + server = httpd_setup({ + "/user/1.0/johndoe/password": send(200, "OK", ""), + "/user/1.0/janedoe/password": send(401, "Unauthorized", "Forbidden!") + }); + + res = Weave.Service.changePassword("ILoveJane83"); + do_check_true(res); + do_check_eq(Weave.Service.password, "ILoveJane83"); + + _("Make sure the password has been persisted in the login manager."); + let logins = Weave.Svc.Login.findLogins({}, PWDMGR_HOST, null, + PWDMGR_PASSWORD_REALM); + do_check_eq(logins[0].password, "ILoveJane83"); + + _("changePassword() returns false for a server error, the password won't change."); + Weave.Svc.Login.removeAllLogins(); + Weave.Service.username = "janedoe"; + Weave.Service.password = "ilovejohn"; + res = Weave.Service.changePassword("ILoveJohn86"); + do_check_false(res); + do_check_eq(Weave.Service.password, "ilovejohn"); + + } finally { + Weave.Svc.Prefs.resetBranch(""); + Weave.Svc.Login.removeAllLogins(); + if (server) { + server.stop(function() {}); + } + } +} diff --git a/services/sync/tests/unit/test_service_checkUsername.js b/services/sync/tests/unit/test_service_checkUsername.js new file mode 100644 index 000000000000..ef98a83c9a73 --- /dev/null +++ b/services/sync/tests/unit/test_service_checkUsername.js @@ -0,0 +1,31 @@ +Cu.import("resource://services-sync/service.js"); + +function send(statusCode, status, body) { + return function(request, response) { + response.setStatusLine(request.httpVersion, statusCode, status); + response.bodyOutputStream.write(body, body.length); + }; +} + +function run_test() { + let server = httpd_setup({ + "/user/1.0/johndoe": send(200, "OK", "1"), + "/user/1.0/janedoe": send(200, "OK", "0") + }); + try { + Weave.Service.serverURL = "http://localhost:8080/"; + + _("A 404 will be recorded as 'generic-server-error'"); + do_check_eq(Weave.Service.checkUsername("jimdoe"), "generic-server-error"); + + _("Username that's not available."); + do_check_eq(Weave.Service.checkUsername("johndoe"), "notAvailable"); + + _("Username that's available."); + do_check_eq(Weave.Service.checkUsername("janedoe"), "available"); + + } finally { + Weave.Svc.Prefs.resetBranch(""); + server.stop(function() {}); + } +} diff --git a/services/sync/tests/unit/test_service_createAccount.js b/services/sync/tests/unit/test_service_createAccount.js new file mode 100644 index 000000000000..c899915bb425 --- /dev/null +++ b/services/sync/tests/unit/test_service_createAccount.js @@ -0,0 +1,56 @@ +Cu.import("resource://services-sync/service.js"); + +function run_test() { + var requestBody; + var secretHeader; + function send(statusCode, status, body) { + return function(request, response) { + requestBody = readBytesFromInputStream(request.bodyInputStream); + if (request.hasHeader("X-Weave-Secret")) { + secretHeader = request.getHeader("X-Weave-Secret"); + } + + response.setStatusLine(request.httpVersion, statusCode, status); + response.bodyOutputStream.write(body, body.length); + }; + } + + let server = httpd_setup({ + "/user/1.0/johndoe": send(200, "OK", "0"), + "/user/1.0/janedoe": send(400, "Bad Request", "2"), + "/user/1.0/jimdoe": send(500, "Server Error", "Server Error") + }); + try { + Weave.Service.serverURL = "http://localhost:8080/"; + + _("Create an account."); + let res = Weave.Service.createAccount("johndoe", "mysecretpw", "john@doe", + "challenge", "response"); + do_check_eq(res, null); + let payload = JSON.parse(requestBody); + do_check_eq(payload.password, "mysecretpw"); + do_check_eq(payload.email, "john@doe"); + do_check_eq(payload["captcha-challenge"], "challenge"); + do_check_eq(payload["captcha-response"], "response"); + + _("Invalid captcha or other user-friendly error."); + res = Weave.Service.createAccount("janedoe", "anothersecretpw", "jane@doe", + "challenge", "response"); + do_check_eq(res, "invalid-captcha"); + + _("Generic server error."); + res = Weave.Service.createAccount("jimdoe", "preciousss", "jim@doe", + "challenge", "response"); + do_check_eq(res, "generic-server-error"); + + _("Admin secret preference is passed as HTTP header token."); + Weave.Svc.Prefs.set("admin-secret", "my-server-secret"); + res = Weave.Service.createAccount("johndoe", "mysecretpw", "john@doe", + "challenge", "response"); + do_check_eq(secretHeader, "my-server-secret"); + + } finally { + Weave.Svc.Prefs.resetBranch(""); + server.stop(function() {}); + } +} From d8ec7c54ec1f1d7a6ec6b82bf7b02007e652f571 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Fri, 18 Jun 2010 13:59:30 -0700 Subject: [PATCH 1731/1860] Bug 573108 - Remove references to chrome://weave from services [r=mconnor] Remove openWindow/Dialog helpers and inline into ui bits for prefs-common and pass arguments into generic-change. --- services/sync/modules/util.js | 44 ----------------------------------- 1 file changed, 44 deletions(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 2cf1f496171d..917201f4219b 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -713,50 +713,6 @@ let Utils = { return [stream, file]; }, - /** - * Open/reshow a window/dialog based on its name and type. - * - * @param name - * Name of the window/dialog to reshow if already open - * @param type - * Opening behavior: "Window" or "Dialog" - * @param args - * More arguments go here depending on the type - */ - _openWin: function Utils__openWin(name, type /*, args... */) { - // Just re-show the window if it's already open - let openedWindow = Svc.WinMediator.getMostRecentWindow("Weave:" + name); - if (openedWindow) { - openedWindow.focus(); - return; - } - - // Open up the window/dialog! - let win = Svc.WinWatcher; - if (type == "Dialog") - win = win.activeWindow; - win["open" + type].apply(win, Array.slice(arguments, 2)); - }, - - _openChromeWindow: function Utils_openCWindow(name, uri, options, args) { - Utils.openWindow(name, "chrome://weave/content/" + uri, options, args); - }, - - openWindow: function Utils_openWindow(name, uri, options, args) { - Utils._openWin(name, "Window", null, uri, "", - options || "centerscreen,chrome,dialog,resizable=yes", args); - }, - - openDialog: function Utils_openDialog(name, uri, options, args) { - Utils._openWin(name, "Dialog", "chrome://weave/content/" + uri, "", - options || "centerscreen,chrome,dialog,modal,resizable=no", args); - }, - - openGenericDialog: function Utils_openGenericDialog(type) { - this._genericDialogType = type; - this.openDialog("ChangeSomething", "generic-change.xul"); - }, - getIcon: function(iconUri, defaultIcon) { try { let iconURI = Utils.makeURI(iconUri); From 87fef60feb344ae4ab88488e513460c57b213465 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Fri, 18 Jun 2010 14:11:14 -0700 Subject: [PATCH 1732/1860] Bug 571902 - Land sync and crypto components on trunk [r=mconnor] If resource://services-sync isn't defined yet, alias it to resource://gre/modules/services-sync. --- services/sync/Weave.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 197a48e7ce93..0b4300a6adc7 100755 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -56,6 +56,7 @@ WeaveService.prototype = { let os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); os.addObserver(this, "final-ui-startup", true); + this.addResourceAlias(); break; case "final-ui-startup": @@ -68,6 +69,26 @@ WeaveService.prototype = { }, 10000, Ci.nsITimer.TYPE_ONE_SHOT); break; } + }, + + addResourceAlias: function() { + let ioService = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioService.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + + // Only create alias if resource://services-sync doesn't already exist. + if (resProt.hasSubstitution("services-sync")) + return; + + let uri = ioService.newURI("resource://gre/modules/services-sync", + null, null); + let file = uri.QueryInterface(Ci.nsIFileURL) + .file.QueryInterface(Ci.nsILocalFile); + + let aliasURI = ioService.newFileURI(file); + resProt.setSubstitution("services-sync", aliasURI); } }; From 867641d6ca6e2e0714f34019343840bfacaa4817 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 21 Jun 2010 16:46:57 -0700 Subject: [PATCH 1733/1860] Bug 573194 - AboutWeaveTabs still references chrome://weave/content/firefox/tabs.xul [r=Mardak] Split off about: chrome aliasing from other services aliases. --- services/sync/AboutWeaveTabs.js | 68 +++++++++++++++++++++++++++++++++ services/sync/Weave.js | 23 ----------- 2 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 services/sync/AboutWeaveTabs.js diff --git a/services/sync/AboutWeaveTabs.js b/services/sync/AboutWeaveTabs.js new file mode 100644 index 000000000000..1597abbb1bc3 --- /dev/null +++ b/services/sync/AboutWeaveTabs.js @@ -0,0 +1,68 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function AboutWeaveTabs() {} +AboutWeaveTabs.prototype = { + classDescription: "about:sync-tabs", + contractID: "@mozilla.org/network/protocol/about;1?what=sync-tabs", + classID: Components.ID("{ecb6987d-9d71-475d-a44d-a5ff2099b08c}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, + Ci.nsISupportsWeakReference]), + + getURIFlags: function(aURI) { + return (Ci.nsIAboutModule.ALLOW_SCRIPT); + }, + + newChannel: function(aURI) { + let ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let ch = ios.newChannel("chrome://weave/content/firefox/tabs.xul", null, null); + ch.originalURI = aURI; + return ch; + } +}; + +function NSGetModule(compMgr, fileSpec) { + return XPCOMUtils.generateModule([ AboutWeaveTabs ]); +} + diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 0b4300a6adc7..9e8436e6bba6 100755 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -92,28 +92,6 @@ WeaveService.prototype = { } }; -function AboutWeaveTabs() {} -AboutWeaveTabs.prototype = { - classDescription: "about:sync-tabs", - contractID: "@mozilla.org/network/protocol/about;1?what=sync-tabs", - classID: Components.ID("{ecb6987d-9d71-475d-a44d-a5ff2099b08c}"), - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, - Ci.nsISupportsWeakReference]), - - getURIFlags: function(aURI) { - return (Ci.nsIAboutModule.ALLOW_SCRIPT); - }, - - newChannel: function(aURI) { - let ios = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let ch = ios.newChannel("chrome://weave/content/firefox/tabs.xul", null, null); - ch.originalURI = aURI; - return ch; - } -}; - function AboutWeaveLog() {} AboutWeaveLog.prototype = { classDescription: "about:sync-log", @@ -173,7 +151,6 @@ AboutWeaveLog1.prototype = { function NSGetModule(compMgr, fileSpec) { return XPCOMUtils.generateModule([ WeaveService, - AboutWeaveTabs, AboutWeaveLog, AboutWeaveLog1 ]); From ab2cf9130f46be86c6ba7cc29ee1a2041b3a4eac Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Mon, 21 Jun 2010 23:56:56 -0700 Subject: [PATCH 1734/1860] Bug 573679 - Fix tests to pass on trunk Create a bookmark so that the url will exist when setting annotations. --- services/sync/tests/unit/test_utils_anno.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/services/sync/tests/unit/test_utils_anno.js b/services/sync/tests/unit/test_utils_anno.js index bbbb63c26cd8..2c1972c75c64 100644 --- a/services/sync/tests/unit/test_utils_anno.js +++ b/services/sync/tests/unit/test_utils_anno.js @@ -6,17 +6,22 @@ function run_test() { Utils.anno(1, "anno", "hi"); do_check_eq(Utils.anno(1, "anno"), "hi"); + _("create a bookmark to a url so it exists"); + let url = "about:"; + let bmkid = Svc.Bookmark.insertBookmark(Svc.Bookmark.unfiledBookmarksFolder, + Utils.makeURI(url), -1, ""); + _("set an anno on a url"); - Utils.anno("about:", "tation", "hello"); - do_check_eq(Utils.anno("about:", "tation"), "hello"); + Utils.anno(url, "tation", "hello"); + do_check_eq(Utils.anno(url, "tation"), "hello"); _("make sure getting it also works with a nsIURI"); - let uri = Utils.makeURI("about:"); + let uri = Utils.makeURI(url); do_check_eq(Utils.anno(uri, "tation"), "hello"); _("make sure annotations get updated"); Utils.anno(uri, "tation", "bye!"); - do_check_eq(Utils.anno("about:", "tation"), "bye!"); + do_check_eq(Utils.anno(url, "tation"), "bye!"); _("sanity check that the item anno is still there"); do_check_eq(Utils.anno(1, "anno"), "hi"); @@ -30,4 +35,7 @@ function run_test() { didThrow = true; } do_check_true(didThrow); + + _("cleaning up the bookmark we created"); + Svc.Bookmark.removeItem(bmkid); } From 2402444a5b3c02b8670038f2d9565e08c191b26a Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 00:20:31 -0700 Subject: [PATCH 1735/1860] Bug 573668 - Register appinfo with correct OS for tests Register a fake AppInfo with correct OS if it doesn't exist yet. Just use Svc.Crypto instead of trying to dynamically pick the contract id. Name the head files so they load in appinfo -> helper -> http order. --- services/sync/tests/unit/bookmark_setup.js | 131 ------------------ services/sync/tests/unit/head_appinfo.js | 51 +++++++ .../unit/{head_first.js => head_helpers.js} | 22 --- services/sync/tests/unit/test_crypto_crypt.js | 5 +- .../sync/tests/unit/test_crypto_keypair.js | 5 +- .../sync/tests/unit/test_crypto_random.js | 5 +- .../sync/tests/unit/test_crypto_rewrap.js | 5 +- .../sync/tests/unit/test_crypto_verify.js | 5 +- .../sync/tests/unit/test_records_crypto.js | 19 +-- 9 files changed, 73 insertions(+), 175 deletions(-) delete mode 100644 services/sync/tests/unit/bookmark_setup.js create mode 100644 services/sync/tests/unit/head_appinfo.js rename services/sync/tests/unit/{head_first.js => head_helpers.js} (94%) diff --git a/services/sync/tests/unit/bookmark_setup.js b/services/sync/tests/unit/bookmark_setup.js deleted file mode 100644 index 5d777a5ef781..000000000000 --- a/services/sync/tests/unit/bookmark_setup.js +++ /dev/null @@ -1,131 +0,0 @@ -/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -// This file was originally taken from mozilla-central at: -// http://hg.mozilla.org/index.cgi/mozilla-central/file/f171c57e016e/browser/components/places/tests/unit/head_bookmarks.js -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Places. - * - * The Initial Developer of the Original Code is - * Google Inc. - * Portions created by the Initial Developer are Copyright (C) 2005 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Brian Ryner - * Dietrich Ayala - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const NS_APP_USER_PROFILE_50_DIR = "ProfD"; - -// If there's no location registered for the profile direcotry, register one now. -var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); -var profileDir = null; -try { - profileDir = dirSvc.get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile); -} catch (e) {} -if (!profileDir) { - // Register our own provider for the profile directory. - // It will simply return the current directory. - var provider = { - getFile: function(prop, persistent) { - persistent.value = true; - if (prop == NS_APP_USER_PROFILE_50_DIR) { - return dirSvc.get("CurProcD", Ci.nsIFile); - } - throw Cr.NS_ERROR_FAILURE; - }, - QueryInterface: function(iid) { - if (iid.equals(Ci.nsIDirectoryServiceProvider) || - iid.equals(Ci.nsISupports)) { - return this; - } - throw Cr.NS_ERROR_NO_INTERFACE; - } - }; - dirSvc.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider); -} - -var XULAppInfo = { - vendor: "Mozilla", - name: "PlacesTest", - ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}", - version: "1", - appBuildID: "2007010101", - platformVersion: "", - platformBuildID: "2007010101", - inSafeMode: false, - logConsoleErrors: true, - OS: "XPCShell", - XPCOMABI: "noarch-spidermonkey", - - QueryInterface: function QueryInterface(iid) { - if (iid.equals(Ci.nsIXULAppInfo) || - iid.equals(Ci.nsIXULRuntime) || - iid.equals(Ci.nsISupports)) - return this; - throw Cr.NS_ERROR_NO_INTERFACE; - } -}; - -var XULAppInfoFactory = { - createInstance: function (outer, iid) { - if (outer != null) - throw Cr.NS_ERROR_NO_AGGREGATION; - return XULAppInfo.QueryInterface(iid); - } -}; - -var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); -registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"), - "XULAppInfo", "@mozilla.org/xre/app-info;1", - XULAppInfoFactory); - -var updateSvc = Cc["@mozilla.org/updates/update-service;1"]. - getService(Ci.nsISupports); - -var iosvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); - -function uri(spec) { - return iosvc.newURI(spec, null, null); -} - -function cleanUp() { - try { - // Delete a previously created sqlite file - var file = dirSvc.get('ProfD', Ci.nsIFile); - file.append("places.sqlite"); - if (file.exists()) - file.remove(false); - - // Delete exported bookmarks html file - file = dirSvc.get('ProfD', Ci.nsIFile); - file.append("bookmarks.exported.html"); - if (file.exists()) - file.remove(false); - } catch(ex) { dump("Exception: " + ex); } -} -cleanUp(); diff --git a/services/sync/tests/unit/head_appinfo.js b/services/sync/tests/unit/head_appinfo.js new file mode 100644 index 000000000000..949c6885e86d --- /dev/null +++ b/services/sync/tests/unit/head_appinfo.js @@ -0,0 +1,51 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +try { + // In the context of xpcshell tests, there won't be a default AppInfo + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo); +} +catch(ex) { + +// Make sure to provide the right OS so crypto loads the right binaries +let OS = "XPCShell"; +if ("@mozilla.org/windows-registry-key;1" in Cc) + OS = "WINNT"; +else if ("nsILocalFileMac" in Ci) + OS = "Darwin"; +else + OS = "Linux"; + +let XULAppInfo = { + vendor: "Mozilla", + name: "XPCShell", + ID: "{3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}", + version: "1", + appBuildID: "20100621", + platformVersion: "", + platformBuildID: "20100621", + inSafeMode: false, + logConsoleErrors: true, + OS: OS, + XPCOMABI: "noarch-spidermonkey", + QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]) +}; + +let XULAppInfoFactory = { + createInstance: function (outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return XULAppInfo.QueryInterface(iid); + } +}; + +let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"), + "XULAppInfo", "@mozilla.org/xre/app-info;1", + XULAppInfoFactory); + +} diff --git a/services/sync/tests/unit/head_first.js b/services/sync/tests/unit/head_helpers.js similarity index 94% rename from services/sync/tests/unit/head_first.js rename to services/sync/tests/unit/head_helpers.js index cac8d8077f53..30b7abc1a653 100644 --- a/services/sync/tests/unit/head_first.js +++ b/services/sync/tests/unit/head_helpers.js @@ -1,25 +1,3 @@ -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -let versSvc = Cc["@mozilla.org/xpcom/version-comparator;1"]. - getService(Ci.nsIVersionComparator); -let appinfo = Cc["@mozilla.org/xre/app-info;1"]. - getService(Ci.nsIXULAppInfo); -let platVers = appinfo.platformVersion; - -let cryptoContractID = "@labs.mozilla.com/Weave/Crypto"; -if (versSvc.compare(platVers, "1.9.3a3") < 0) { - // use old binary component - cryptoContractID += ";1"; -} else { - // use new JS-CTypes component - cryptoContractID += ";2"; -} -dump("Using crypto contract " + cryptoContractID + "\n"); - - // initialize nss let ch = Cc["@mozilla.org/security/hash;1"]. createInstance(Ci.nsICryptoHash); diff --git a/services/sync/tests/unit/test_crypto_crypt.js b/services/sync/tests/unit/test_crypto_crypt.js index 0f42c8d71c1f..46cdc37d8d70 100644 --- a/services/sync/tests/unit/test_crypto_crypt.js +++ b/services/sync/tests/unit/test_crypto_crypt.js @@ -1,6 +1,7 @@ +Cu.import("resource://services-sync/util.js"); + function run_test() { - var cryptoSvc = Cc[cryptoContractID]. - getService(Ci.IWeaveCrypto); + let cryptoSvc = Svc.Crypto; // First, do a normal run with expected usage... Generate a random key and // iv, encrypt and decrypt a string. diff --git a/services/sync/tests/unit/test_crypto_keypair.js b/services/sync/tests/unit/test_crypto_keypair.js index a82ccbfe9d9b..2761c44b8c1d 100644 --- a/services/sync/tests/unit/test_crypto_keypair.js +++ b/services/sync/tests/unit/test_crypto_keypair.js @@ -1,6 +1,7 @@ +Cu.import("resource://services-sync/util.js"); + function run_test() { - var cryptoSvc = Cc[cryptoContractID]. - getService(Ci.IWeaveCrypto); + let cryptoSvc = Svc.Crypto; var salt = cryptoSvc.generateRandomBytes(16); do_check_eq(salt.length, 24); diff --git a/services/sync/tests/unit/test_crypto_random.js b/services/sync/tests/unit/test_crypto_random.js index f5873dd28174..4eeff02b7f81 100644 --- a/services/sync/tests/unit/test_crypto_random.js +++ b/services/sync/tests/unit/test_crypto_random.js @@ -1,6 +1,7 @@ +Cu.import("resource://services-sync/util.js"); + function run_test() { - var cryptoSvc = Cc[cryptoContractID]. - getService(Ci.IWeaveCrypto); + let cryptoSvc = Svc.Crypto; // Test salt generation. var salt; diff --git a/services/sync/tests/unit/test_crypto_rewrap.js b/services/sync/tests/unit/test_crypto_rewrap.js index 7bd5eea3d5d1..bc2aa01d2192 100644 --- a/services/sync/tests/unit/test_crypto_rewrap.js +++ b/services/sync/tests/unit/test_crypto_rewrap.js @@ -1,6 +1,7 @@ +Cu.import("resource://services-sync/util.js"); + function run_test() { - var cryptoSvc = Cc[cryptoContractID]. - getService(Ci.IWeaveCrypto); + let cryptoSvc = Svc.Crypto; var salt = cryptoSvc.generateRandomBytes(16); var iv = cryptoSvc.generateRandomIV(); diff --git a/services/sync/tests/unit/test_crypto_verify.js b/services/sync/tests/unit/test_crypto_verify.js index 610a3fbc38e7..eefbd3a23ef0 100644 --- a/services/sync/tests/unit/test_crypto_verify.js +++ b/services/sync/tests/unit/test_crypto_verify.js @@ -1,6 +1,7 @@ +Cu.import("resource://services-sync/util.js"); + function run_test() { - var cryptoSvc = Cc[cryptoContractID]. - getService(Ci.IWeaveCrypto); + let cryptoSvc = Svc.Crypto; var salt = cryptoSvc.generateRandomBytes(16); var iv = cryptoSvc.generateRandomIV(); diff --git a/services/sync/tests/unit/test_records_crypto.js b/services/sync/tests/unit/test_records_crypto.js index cd374a9b4ce7..5e698639b19b 100644 --- a/services/sync/tests/unit/test_records_crypto.js +++ b/services/sync/tests/unit/test_records_crypto.js @@ -1,13 +1,9 @@ -try { - Cu.import("resource://services-sync/base_records/crypto.js"); - Cu.import("resource://services-sync/base_records/keys.js"); - Cu.import("resource://services-sync/auth.js"); - Cu.import("resource://services-sync/log4moz.js"); - Cu.import("resource://services-sync/identity.js"); - Cu.import("resource://services-sync/util.js"); -} catch (e) { - do_throw(e); -} +Cu.import("resource://services-sync/base_records/crypto.js"); +Cu.import("resource://services-sync/base_records/keys.js"); +Cu.import("resource://services-sync/auth.js"); +Cu.import("resource://services-sync/log4moz.js"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/util.js"); let keys, cryptoMeta, cryptoWrap; @@ -62,8 +58,7 @@ function run_test() { keys = PubKeys.createKeypair(passphrase, "http://localhost:8080/pubkey", "http://localhost:8080/privkey"); - let crypto = Cc[cryptoContractID]. - getService(Ci.IWeaveCrypto); + let crypto = Svc.Crypto; keys.symkey = crypto.generateRandomKey(); keys.wrappedkey = crypto.wrapSymmetricKey(keys.symkey, keys.pubkey.keyData); From 56829088b3c6129e1c3e943d82af92bb14eab45f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 08:14:15 -0700 Subject: [PATCH 1736/1860] Bug 573691 - Flatten/split components and prefs for services vs ui separation [r=mconnor] Move about:weave-tabs component to ui/firefox and flatten structure of the components. Split prefs for services vs firefox. --- services/sync/AboutWeaveTabs.js | 68 --------------------------- services/sync/Weave.js | 0 services/sync/services-sync.js | 81 --------------------------------- 3 files changed, 149 deletions(-) delete mode 100644 services/sync/AboutWeaveTabs.js mode change 100755 => 100644 services/sync/Weave.js diff --git a/services/sync/AboutWeaveTabs.js b/services/sync/AboutWeaveTabs.js deleted file mode 100644 index 1597abbb1bc3..000000000000 --- a/services/sync/AboutWeaveTabs.js +++ /dev/null @@ -1,68 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Weave. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dan Mills - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -function AboutWeaveTabs() {} -AboutWeaveTabs.prototype = { - classDescription: "about:sync-tabs", - contractID: "@mozilla.org/network/protocol/about;1?what=sync-tabs", - classID: Components.ID("{ecb6987d-9d71-475d-a44d-a5ff2099b08c}"), - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, - Ci.nsISupportsWeakReference]), - - getURIFlags: function(aURI) { - return (Ci.nsIAboutModule.ALLOW_SCRIPT); - }, - - newChannel: function(aURI) { - let ios = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - let ch = ios.newChannel("chrome://weave/content/firefox/tabs.xul", null, null); - ch.originalURI = aURI; - return ch; - } -}; - -function NSGetModule(compMgr, fileSpec) { - return XPCOMUtils.generateModule([ AboutWeaveTabs ]); -} - diff --git a/services/sync/Weave.js b/services/sync/Weave.js old mode 100755 new mode 100644 diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index acec0c0fc7ef..01afdb57b693 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -30,84 +30,3 @@ pref("services.sync.log.logger.engine.passwords", "Debug"); pref("services.sync.log.logger.engine.prefs", "Debug"); pref("services.sync.log.logger.engine.tabs", "Debug"); pref("services.sync.log.cryptoDebug", false); - -// Preferences to be synced by default -pref("services.sync.prefs.sync.accessibility.blockautorefresh", true); -pref("services.sync.prefs.sync.accessibility.browsewithcaret", true); -pref("services.sync.prefs.sync.accessibility.typeaheadfind", true); -pref("services.sync.prefs.sync.accessibility.typeaheadfind.linksonly", true); -pref("services.sync.prefs.sync.app.update.mode", true); -pref("services.sync.prefs.sync.browser.download.manager.closeWhenDone", true); -pref("services.sync.prefs.sync.browser.download.manager.retention", true); -pref("services.sync.prefs.sync.browser.download.manager.scanWhenDone", true); -pref("services.sync.prefs.sync.browser.download.manager.showWhenStarting", true); -pref("services.sync.prefs.sync.browser.formfill.enable", true); -pref("services.sync.prefs.sync.browser.history_expire_days", true); -pref("services.sync.prefs.sync.browser.history_expire_days_min", true); -pref("services.sync.prefs.sync.browser.link.open_newwindow", true); -pref("services.sync.prefs.sync.browser.offline-apps.notify", true); -pref("services.sync.prefs.sync.browser.safebrowsing.enabled", true); -pref("services.sync.prefs.sync.browser.safebrowsing.malware.enabled", true); -pref("services.sync.prefs.sync.browser.search.selectedEngine", true); -pref("services.sync.prefs.sync.browser.search.update", true); -pref("services.sync.prefs.sync.browser.startup.homepage", true); -pref("services.sync.prefs.sync.browser.startup.page", true); -pref("services.sync.prefs.sync.browser.tabs.autoHide", true); -pref("services.sync.prefs.sync.browser.tabs.closeButtons", true); -pref("services.sync.prefs.sync.browser.tabs.loadInBackground", true); -pref("services.sync.prefs.sync.browser.tabs.tabMaxWidth", true); -pref("services.sync.prefs.sync.browser.tabs.tabMinWidth", true); -pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true); -pref("services.sync.prefs.sync.browser.tabs.warnOnOpen", true); -pref("services.sync.prefs.sync.browser.urlbar.autocomplete.enabled", true); -pref("services.sync.prefs.sync.browser.urlbar.autoFill", true); -pref("services.sync.prefs.sync.browser.urlbar.default.behavior", true); -pref("services.sync.prefs.sync.browser.urlbar.maxRichResults", true); -pref("services.sync.prefs.sync.dom.disable_open_during_load", true); -pref("services.sync.prefs.sync.dom.disable_window_flip", true); -pref("services.sync.prefs.sync.dom.disable_window_move_resize", true); -pref("services.sync.prefs.sync.dom.disable_window_open_feature.status", true); -pref("services.sync.prefs.sync.dom.disable_window_status_change", true); -pref("services.sync.prefs.sync.dom.event.contextmenu.enabled", true); -pref("services.sync.prefs.sync.extensions.personas.current", true); -pref("services.sync.prefs.sync.extensions.update.enabled", true); -pref("services.sync.prefs.sync.general.autoScroll", true); -pref("services.sync.prefs.sync.general.smoothScroll", true); -pref("services.sync.prefs.sync.intl.accept_languages", true); -pref("services.sync.prefs.sync.javascript.enabled", true); -pref("services.sync.prefs.sync.layout.spellcheckDefault", true); -pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true); -pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true); -pref("services.sync.prefs.sync.network.cookie.cookieBehavior", true); -pref("services.sync.prefs.sync.network.cookie.lifetimePolicy", true); -pref("services.sync.prefs.sync.permissions.default.image", true); -pref("services.sync.prefs.sync.pref.advanced.images.disable_button.view_image", true); -pref("services.sync.prefs.sync.pref.advanced.javascript.disable_button.advanced", true); -pref("services.sync.prefs.sync.pref.downloads.disable_button.edit_actions", true); -pref("services.sync.prefs.sync.pref.privacy.disable_button.cookie_exceptions", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.cache", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.cookies", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.downloads", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.formdata", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.history", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.offlineApps", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.passwords", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.sessions", true); -pref("services.sync.prefs.sync.privacy.clearOnShutdown.siteSettings", true); -pref("services.sync.prefs.sync.privacy.sanitize.sanitizeOnShutdown", true); -pref("services.sync.prefs.sync.security.OCSP.disable_button.managecrl", true); -pref("services.sync.prefs.sync.security.OCSP.enabled", true); -pref("services.sync.prefs.sync.security.OCSP.require", true); -pref("services.sync.prefs.sync.security.default_personal_cert", true); -pref("services.sync.prefs.sync.security.enable_java", true); -pref("services.sync.prefs.sync.security.enable_ssl3", true); -pref("services.sync.prefs.sync.security.enable_tls", true); -pref("services.sync.prefs.sync.security.warn_entering_secure", true); -pref("services.sync.prefs.sync.security.warn_entering_weak", true); -pref("services.sync.prefs.sync.security.warn_leaving_secure", true); -pref("services.sync.prefs.sync.security.warn_submit_insecure", true); -pref("services.sync.prefs.sync.security.warn_viewing_mixed", true); -pref("services.sync.prefs.sync.signon.rememberSignons", true); -pref("services.sync.prefs.sync.spellchecker.dictionary", true); -pref("services.sync.prefs.sync.xpinstall.whitelist.required", true); - From 9183491be17695f6613e650eeddbcc36e2fa4ac1 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 13:18:10 -0700 Subject: [PATCH 1737/1860] Bug 573679 - Fix tests to pass on trunk Use do_load_httpd_js for xpcshell tests and have consumers directly call new nsHttpServer(). --- services/sync/tests/unit/head_appinfo.js | 15 +++++++++++---- services/sync/tests/unit/head_http_server.js | 5 +---- services/sync/tests/unit/test_auth_manager.js | 4 +--- services/sync/tests/unit/test_engine.js | 9 +++++++++ services/sync/tests/unit/test_resource.js | 4 +--- .../sync/tests/unit/test_service_attributes.js | 7 +++++++ .../sync/tests/unit/test_service_cluster.js | 6 ++++++ services/sync/tests/unit/test_syncengine.js | 8 ++++++++ .../sync/tests/unit/test_syncengine_sync.js | 18 ++++++++++++++++++ 9 files changed, 62 insertions(+), 14 deletions(-) diff --git a/services/sync/tests/unit/head_appinfo.js b/services/sync/tests/unit/head_appinfo.js index 949c6885e86d..30a9cbce9012 100644 --- a/services/sync/tests/unit/head_appinfo.js +++ b/services/sync/tests/unit/head_appinfo.js @@ -1,7 +1,14 @@ -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; +// Load httpd from the add-on test harness or from xpcshell and declare Cc, etc. +// without a var/const so they don't get hoisted and conflict with load_httpd. +if (this.do_load_httpd_js == null) { + Cc = Components.classes; + Ci = Components.interfaces; + Cr = Components.results; + Cu = Components.utils; + Cu.import("resource://harness/modules/httpd.js"); +} +else + do_load_httpd_js(); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js index af777e8d9560..c77fd86597e0 100644 --- a/services/sync/tests/unit/head_http_server.js +++ b/services/sync/tests/unit/head_http_server.js @@ -1,8 +1,5 @@ -let Httpd = {}; -Cu.import("resource://harness/modules/httpd.js", Httpd); - function httpd_setup (handlers) { - let server = new Httpd.nsHttpServer(); + let server = new nsHttpServer(); for (let path in handlers) { server.registerPathHandler(path, handlers[path]); } diff --git a/services/sync/tests/unit/test_auth_manager.js b/services/sync/tests/unit/test_auth_manager.js index e685c95161d3..8a950979ab31 100644 --- a/services/sync/tests/unit/test_auth_manager.js +++ b/services/sync/tests/unit/test_auth_manager.js @@ -5,8 +5,6 @@ Cu.import("resource://services-sync/resource.js"); Cu.import("resource://services-sync/util.js"); let logger; -let Httpd = {}; -Cu.import("resource://harness/modules/httpd.js", Httpd); function server_handler(metadata, response) { let body; @@ -31,7 +29,7 @@ function run_test() { logger = Log4Moz.repository.getLogger('Test'); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); - let server = new Httpd.nsHttpServer(); + let server = new nsHttpServer(); server.registerPathHandler("/foo", server_handler); server.start(8080); diff --git a/services/sync/tests/unit/test_engine.js b/services/sync/tests/unit/test_engine.js index 4fb4c44007de..35c4e6197b0a 100644 --- a/services/sync/tests/unit/test_engine.js +++ b/services/sync/tests/unit/test_engine.js @@ -161,3 +161,12 @@ function test_sync() { engineObserver.reset(); } } + +function run_test() { + test_members(); + test_score(); + test_resetClient(); + test_wipeClient(); + test_enabled(); + test_sync(); +} diff --git a/services/sync/tests/unit/test_resource.js b/services/sync/tests/unit/test_resource.js index 7e1e87423c02..ad96e38ffb4c 100644 --- a/services/sync/tests/unit/test_resource.js +++ b/services/sync/tests/unit/test_resource.js @@ -6,8 +6,6 @@ Cu.import("resource://services-sync/resource.js"); Cu.import("resource://services-sync/util.js"); let logger; -let Httpd = {}; -Cu.import("resource://harness/modules/httpd.js", Httpd); function server_open(metadata, response) { let body; @@ -131,7 +129,7 @@ function run_test() { logger = Log4Moz.repository.getLogger('Test'); Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender()); - let server = new Httpd.nsHttpServer(); + let server = new nsHttpServer(); server.registerPathHandler("/open", server_open); server.registerPathHandler("/protected", server_protected); server.registerPathHandler("/404", server_404); diff --git a/services/sync/tests/unit/test_service_attributes.js b/services/sync/tests/unit/test_service_attributes.js index 04a05ce526f4..1da589b1bdf3 100644 --- a/services/sync/tests/unit/test_service_attributes.js +++ b/services/sync/tests/unit/test_service_attributes.js @@ -193,3 +193,10 @@ function test_locked() { Weave.Service.unlock(); do_check_eq(Weave.Service.locked, false); } + +function run_test() { + test_urlsAndIdentities(); + test_syncID(); + test_prefAttributes(); + test_locked(); +} diff --git a/services/sync/tests/unit/test_service_cluster.js b/services/sync/tests/unit/test_service_cluster.js index f414718d5ef2..5a6fbc71ddac 100644 --- a/services/sync/tests/unit/test_service_cluster.js +++ b/services/sync/tests/unit/test_service_cluster.js @@ -144,3 +144,9 @@ function test_updateCluster() { server.stop(function() {}); } } + +function run_test() { + test_findCluster(); + test_setCluster(); + test_updateCluster(); +} diff --git a/services/sync/tests/unit/test_syncengine.js b/services/sync/tests/unit/test_syncengine.js index a20f8410dedc..e82cc70b7a9b 100644 --- a/services/sync/tests/unit/test_syncengine.js +++ b/services/sync/tests/unit/test_syncengine.js @@ -111,3 +111,11 @@ function test_resetClient() { Svc.Prefs.resetBranch(""); } } + +function run_test() { + test_url_attributes(); + test_syncID(); + test_lastSync(); + test_toFetch(); + test_resetClient(); +} diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js index 16059d3c5c8b..4b9b0dcdb641 100644 --- a/services/sync/tests/unit/test_syncengine_sync.js +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -1007,3 +1007,21 @@ function test_syncFinish_deleteLotsInBatches() { syncTesting = new SyncTestingInfrastructure(makeSteamEngine); } } + +function run_test() { + test_syncStartup_emptyOrOutdatedGlobalsResetsSync(); + test_syncStartup_metaGet404(); + test_syncStartup_failedMetaGet(); + test_syncStartup_serverHasNewerVersion(); + test_syncStartup_syncIDMismatchResetsClient(); + test_syncStartup_badKeyWipesServerData(); + test_processIncoming_emptyServer(); + test_processIncoming_createFromServer(); + test_processIncoming_reconcile(); + test_processIncoming_fetchNum(); + test_uploadOutgoing_toEmptyServer(); + test_uploadOutgoing_MAX_UPLOAD_RECORDS(); + test_syncFinish_noDelete(); + test_syncFinish_deleteByIds(); + test_syncFinish_deleteLotsInBatches(); +} From 77e791b878e750edf3972cc41b5c47a0af3d483f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 16:47:44 -0700 Subject: [PATCH 1738/1860] Bug 573679 - Fix tests to pass on trunk Just take parts of a static string instead of randomly generating bytes and btoa-ing. --- services/sync/tests/unit/head_helpers.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js index 30b7abc1a653..82b5f2eb9a76 100644 --- a/services/sync/tests/unit/head_helpers.js +++ b/services/sync/tests/unit/head_helpers.js @@ -244,11 +244,7 @@ FakeCryptoService.prototype = { }, generateRandomBytes: function(aByteCount) { - var s = ""; - for (var i=0; i < aByteCount; i++) { - s += String.fromCharCode(Math.floor(Math.random() * 256)); - } - return btoa(s); + return "not-so-random-now-are-we-HA-HA-HA! >:)".slice(aByteCount); }, wrapSymmetricKey: function(aSymmetricKey, aEncodedPublicKey) { From b18d839d9910fc5e96285526e86892b1748a646c Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 16:48:55 -0700 Subject: [PATCH 1739/1860] Bug 573679 - Fix tests to pass on trunk Switch around which lazy services we use to test with but cover even more data types. --- services/sync/tests/unit/test_utils_lazySvc.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/services/sync/tests/unit/test_utils_lazySvc.js b/services/sync/tests/unit/test_utils_lazySvc.js index f1add8a86a72..cbef43c4af76 100644 --- a/services/sync/tests/unit/test_utils_lazySvc.js +++ b/services/sync/tests/unit/test_utils_lazySvc.js @@ -10,12 +10,17 @@ function run_test() { do_check_eq(typeof obj.app.vendor, "string"); do_check_eq(typeof obj.app.name, "string"); - _("Check other types of properties on profiles"); - Utils.lazySvc(obj, "prof", "@mozilla.org/toolkit/profile-service;1", "nsIToolkitProfileService"); - do_check_eq(typeof obj.prof.QueryInterface, "function"); - do_check_eq(typeof obj.prof.startOffline, "boolean"); - do_check_eq(typeof obj.prof.profileCount, "number"); - do_check_eq(typeof obj.prof.createProfile, "function"); + _("Check other types of properties on other services"); + Utils.lazySvc(obj, "io", "@mozilla.org/network/io-service;1", "nsIIOService"); + do_check_eq(typeof obj.io.newURI, "function"); + do_check_eq(typeof obj.io.offline, "boolean"); + + Utils.lazySvc(obj, "thread", "@mozilla.org/thread-manager;1", "nsIThreadManager"); + do_check_true(obj.thread.currentThread instanceof Ci.nsIThread); + + Utils.lazySvc(obj, "net", "@mozilla.org/network/util;1", "nsINetUtil"); + do_check_eq(typeof obj.net.ESCAPE_ALL, "number"); + do_check_eq(obj.net.ESCAPE_URL_SCHEME, 1); _("Make sure fake services get loaded correctly (private browsing doesnt exist on all platforms)"); Utils.lazySvc(obj, "priv", "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService"); From 8eca19edf7f2b67408b2ea41d3283b14d9fc07a9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 16:49:33 -0700 Subject: [PATCH 1740/1860] Bug 573679 - Fix tests to pass on trunk Use Utils.delay instead of setTimeout. --- services/sync/tests/unit/test_utils_queryAsync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/tests/unit/test_utils_queryAsync.js b/services/sync/tests/unit/test_utils_queryAsync.js index 4a7ee754600d..52d0c8597522 100644 --- a/services/sync/tests/unit/test_utils_queryAsync.js +++ b/services/sync/tests/unit/test_utils_queryAsync.js @@ -7,7 +7,7 @@ function run_test() { _("Make sure the call is async and allows other events to process"); let isAsync = false; - setTimeout(function() isAsync = true, 0); + Utils.delay(function() isAsync = true, 0); do_check_false(isAsync); _("Empty out the formhistory table"); From 9e58b96167c132209754071a3b1e0473e14d74c9 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 18:28:37 -0700 Subject: [PATCH 1741/1860] Bug 573679 - Fix tests to pass on trunk Get a profile for xpcshell tests so that login manager works (and probably other stuff). Fix up anno test from this change to use a real bookmark id. --- services/sync/tests/unit/head_appinfo.js | 4 +++- services/sync/tests/unit/test_utils_anno.js | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/services/sync/tests/unit/head_appinfo.js b/services/sync/tests/unit/head_appinfo.js index 30a9cbce9012..d6a2ef5f9296 100644 --- a/services/sync/tests/unit/head_appinfo.js +++ b/services/sync/tests/unit/head_appinfo.js @@ -7,8 +7,10 @@ if (this.do_load_httpd_js == null) { Cu = Components.utils; Cu.import("resource://harness/modules/httpd.js"); } -else +else { do_load_httpd_js(); + do_get_profile(); +} Cu.import("resource://gre/modules/XPCOMUtils.jsm"); diff --git a/services/sync/tests/unit/test_utils_anno.js b/services/sync/tests/unit/test_utils_anno.js index 2c1972c75c64..008ce6714e76 100644 --- a/services/sync/tests/unit/test_utils_anno.js +++ b/services/sync/tests/unit/test_utils_anno.js @@ -2,15 +2,15 @@ _("Make sure various combinations of anno arguments do the right get/set for pag Cu.import("resource://services-sync/util.js"); function run_test() { - _("set an anno on an item 1"); - Utils.anno(1, "anno", "hi"); - do_check_eq(Utils.anno(1, "anno"), "hi"); - _("create a bookmark to a url so it exists"); let url = "about:"; let bmkid = Svc.Bookmark.insertBookmark(Svc.Bookmark.unfiledBookmarksFolder, Utils.makeURI(url), -1, ""); + _("set an anno on the bookmark "); + Utils.anno(bmkid, "anno", "hi"); + do_check_eq(Utils.anno(bmkid, "anno"), "hi"); + _("set an anno on a url"); Utils.anno(url, "tation", "hello"); do_check_eq(Utils.anno(url, "tation"), "hello"); @@ -24,7 +24,7 @@ function run_test() { do_check_eq(Utils.anno(url, "tation"), "bye!"); _("sanity check that the item anno is still there"); - do_check_eq(Utils.anno(1, "anno"), "hi"); + do_check_eq(Utils.anno(bmkid, "anno"), "hi"); _("invalid uris don't get annos"); let didThrow = false; From 27865e06bd0d54fd8eef3a4c34e3e97b225c1728 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 19:09:29 -0700 Subject: [PATCH 1742/1860] Bug 573842 - Work around non-null terminated string issue for decrypted strings [r=mconnor] Just wrap with empty strings until bug 573841 is fixed. --- services/crypto/WeaveCrypto.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/crypto/WeaveCrypto.js b/services/crypto/WeaveCrypto.js index 13ea88be8e03..a8d7773bccb4 100644 --- a/services/crypto/WeaveCrypto.js +++ b/services/crypto/WeaveCrypto.js @@ -512,7 +512,9 @@ WeaveCrypto.prototype = { outputBuffer = this._commonCrypt(input, outputBuffer, symmetricKey, iv, this.nss.CKA_DECRYPT); // outputBuffer contains UTF-8 data, let js-ctypes autoconvert that to a JS string. - return outputBuffer.readString(); + // XXX Bug 573842: wrap the string from ctypes to get a new string, so + // we don't hit bug 573841. + return "" + outputBuffer.readString() + ""; }, From 576289b547cabba2dbc5d73cf27267178a6a4261 Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 16:29:47 -0700 Subject: [PATCH 1743/1860] Bug 573870 - Be less strict about what the path/root of an exception stack file can be [r=mconnor] Just match anything that doesn't look like another stack file entry. --- services/sync/modules/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 917201f4219b..2225be14c8a2 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -463,7 +463,7 @@ let Utils = { // Standard JS exception if (e.stack) return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < "). - replace(/@(?:chrome|file):.*?([^\/\.]+\.\w+:)/g, "@$1"); + replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1"); return "No traceback available"; }, From 7c0dad8c897ca7cd77f6284f87cf43ffdd261c2f Mon Sep 17 00:00:00 2001 From: Edward Lee Date: Tue, 22 Jun 2010 19:11:20 -0700 Subject: [PATCH 1744/1860] Bug 573740 - Register resource://services-sync before xpcshell tests get run [r=mconnor] Add the alias to resource://services-sync when loading the component instead of waiting for app-startup, which doesn't fire for xpcshell tests. --- services/sync/Weave.js | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index 9e8436e6bba6..b5bbb03b31cb 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -56,7 +56,6 @@ WeaveService.prototype = { let os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); os.addObserver(this, "final-ui-startup", true); - this.addResourceAlias(); break; case "final-ui-startup": @@ -69,26 +68,6 @@ WeaveService.prototype = { }, 10000, Ci.nsITimer.TYPE_ONE_SHOT); break; } - }, - - addResourceAlias: function() { - let ioService = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - let resProt = ioService.getProtocolHandler("resource") - .QueryInterface(Ci.nsIResProtocolHandler); - - - // Only create alias if resource://services-sync doesn't already exist. - if (resProt.hasSubstitution("services-sync")) - return; - - let uri = ioService.newURI("resource://gre/modules/services-sync", - null, null); - let file = uri.QueryInterface(Ci.nsIFileURL) - .file.QueryInterface(Ci.nsILocalFile); - - let aliasURI = ioService.newFileURI(file); - resProt.setSubstitution("services-sync", aliasURI); } }; @@ -156,3 +135,19 @@ function NSGetModule(compMgr, fileSpec) { ]); } +(function addResourceAlias() { + let ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let resProt = ioService.getProtocolHandler("resource"). + QueryInterface(Ci.nsIResProtocolHandler); + + // Only create alias if resource://services-sync doesn't already exist. + if (resProt.hasSubstitution("services-sync")) + return; + + let uri = ioService.newURI("resource://gre/modules/services-sync", null, null); + let file = uri.QueryInterface(Ci.nsIFileURL).file.QueryInterface(Ci.nsILocalFile); + + let aliasURI = ioService.newFileURI(file); + resProt.setSubstitution("services-sync", aliasURI); +})(); From 5c792aa9d2053cdc551ce036b07a612e67166a79 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Wed, 23 Jun 2010 12:36:48 +0200 Subject: [PATCH 1745/1860] Bug 569744 - Delayed loading of service.js causes test failures [r=Mardak] Have FakeCryptoService.generateRandomIV() return 24 bytes so that Weave.Service._checkCrypto() believes it's the real deal. Fix undeclared variable. --- services/sync/tests/unit/head_helpers.js | 3 ++- services/sync/tests/unit/test_syncengine_sync.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js index 82b5f2eb9a76..596498f5408e 100644 --- a/services/sync/tests/unit/head_helpers.js +++ b/services/sync/tests/unit/head_helpers.js @@ -240,7 +240,8 @@ FakeCryptoService.prototype = { }, generateRandomIV: function() { - return "fake-random-iv"; + // A base64-encoded IV is 24 characters long + return "fake-fake-fake-random-iv"; }, generateRandomBytes: function(aByteCount) { diff --git a/services/sync/tests/unit/test_syncengine_sync.js b/services/sync/tests/unit/test_syncengine_sync.js index 4b9b0dcdb641..3fb8dee82e07 100644 --- a/services/sync/tests/unit/test_syncengine_sync.js +++ b/services/sync/tests/unit/test_syncengine_sync.js @@ -274,7 +274,7 @@ function test_syncStartup_metaGet404() { do_check_eq(collection.wbos.scotsman.payload, undefined); _("New bulk key was uploaded"); - key = crypto_steam.data.keyring["http://localhost:8080/1.0/foo/storage/keys/pubkey"]; + let key = crypto_steam.data.keyring["http://localhost:8080/1.0/foo/storage/keys/pubkey"]; do_check_eq(key.wrapped, "fake-symmetric-key-0"); do_check_eq(key.hmac, "fake-symmetric-key-0 "); From 65b59a97d758dbd7909d07c93972f2ce1918a89e Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Wed, 23 Jun 2010 16:28:10 +0200 Subject: [PATCH 1746/1860] Bug 573740 - Register resource://services-sync before xpcshell tests get run [r=Mardak] Don't try to create the alias too early, add-on chrome registration might not have happened yet, so do it during testing. --- services/sync/Weave.js | 41 +++++++++++++----------- services/sync/tests/unit/head_appinfo.js | 4 +++ 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/services/sync/Weave.js b/services/sync/Weave.js index b5bbb03b31cb..0e5e2bbaf408 100644 --- a/services/sync/Weave.js +++ b/services/sync/Weave.js @@ -40,7 +40,9 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -function WeaveService() {} +function WeaveService() { + this.wrappedJSObject = this; +} WeaveService.prototype = { classDescription: "Weave Service", contractID: "@mozilla.org/weave/service;1", @@ -56,6 +58,7 @@ WeaveService.prototype = { let os = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); os.addObserver(this, "final-ui-startup", true); + this.addResourceAlias(); break; case "final-ui-startup": @@ -68,6 +71,25 @@ WeaveService.prototype = { }, 10000, Ci.nsITimer.TYPE_ONE_SHOT); break; } + }, + + addResourceAlias: function() { + let ioService = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + let resProt = ioService.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + // Only create alias if resource://services-sync doesn't already exist. + if (resProt.hasSubstitution("services-sync")) + return; + + let uri = ioService.newURI("resource://gre/modules/services-sync", + null, null); + let file = uri.QueryInterface(Ci.nsIFileURL) + .file.QueryInterface(Ci.nsILocalFile); + + let aliasURI = ioService.newFileURI(file); + resProt.setSubstitution("services-sync", aliasURI); } }; @@ -134,20 +156,3 @@ function NSGetModule(compMgr, fileSpec) { AboutWeaveLog1 ]); } - -(function addResourceAlias() { - let ioService = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - let resProt = ioService.getProtocolHandler("resource"). - QueryInterface(Ci.nsIResProtocolHandler); - - // Only create alias if resource://services-sync doesn't already exist. - if (resProt.hasSubstitution("services-sync")) - return; - - let uri = ioService.newURI("resource://gre/modules/services-sync", null, null); - let file = uri.QueryInterface(Ci.nsIFileURL).file.QueryInterface(Ci.nsILocalFile); - - let aliasURI = ioService.newFileURI(file); - resProt.setSubstitution("services-sync", aliasURI); -})(); diff --git a/services/sync/tests/unit/head_appinfo.js b/services/sync/tests/unit/head_appinfo.js index d6a2ef5f9296..278ee6dc0a12 100644 --- a/services/sync/tests/unit/head_appinfo.js +++ b/services/sync/tests/unit/head_appinfo.js @@ -58,3 +58,7 @@ registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}" XULAppInfoFactory); } + +// Provide resource://services-sync if it isn't already available +let weaveService = Cc["@mozilla.org/weave/service;1"].getService(); +weaveService.wrappedJSObject.addResourceAlias(); From d8e90a2d7ced7c04dbeb7f553949cf5bc48cdcde Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Wed, 23 Jun 2010 22:59:04 +0200 Subject: [PATCH 1747/1860] Bug 573926 - Increase the main toolbar's padding-top so that less of the locationbar's focus ring is cut off in small-icons mode. r=dao --- browser/themes/pinstripe/browser/browser.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/browser/themes/pinstripe/browser/browser.css b/browser/themes/pinstripe/browser/browser.css index bb33525c742b..7f2df57d6c41 100644 --- a/browser/themes/pinstripe/browser/browser.css +++ b/browser/themes/pinstripe/browser/browser.css @@ -1338,10 +1338,6 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker { display: none; } -#nav-bar { - padding-top: 0 !important; -} - #nav-bar[collapsed="true"] + toolbar[customindex] { border-top: 2px solid; -moz-border-top-colors: rgba(0,0,0,0.35) rgba(255,255,255,0.45); From 233e451a98850cc01508c6bc540286f228334725 Mon Sep 17 00:00:00 2001 From: Stephen Horlander Date: Wed, 23 Jun 2010 23:01:51 +0200 Subject: [PATCH 1748/1860] Bug 574090 - Refine OS X Toolbar Icons. r=dao --- browser/themes/pinstripe/browser/Toolbar.png | Bin 11176 -> 9544 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/browser/themes/pinstripe/browser/Toolbar.png b/browser/themes/pinstripe/browser/Toolbar.png index 428c5da24cf12fe406e465892eec73a6fa8479a9..69d8810574a0145cc4d95a4515c63416d4c10896 100644 GIT binary patch literal 9544 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 H%dr0gUF3IW literal 11176 zcmXwfbyQp37i@w%1zNOdaSF5qDNakVqAkU}6bQxLLI|bBwYW=hcZvmfcXxsZcYXPO z@4afNjtNQ(NvwU^g% z0s!#I{(AudNhy>700Thb z%LG5SRBBL^uR9$~1PwSkI!Y{6nhoR$8f5HuQ5fAT{rItG^eKkjVJFS&&?8qRRRkM- zY<}%*d!+1sEr8yTgoZ{ms@&1_-`OXq7>%Iy4uasUrA(t}8vOL;fmO9Ws(&ynqMdo* z2!6*?2p-ESVc#fENJuEh7GN7WI5@}&AfolW-WmG~-1j>r$X+Zf)~Pa22iYt&xD2SX zxiLjN4j&si8Wn3-Nap0mi65S%T)Y%>Kh|`|#JCrmpWOH&KzvB7LZgz#sZq%9cy+j> z%D8~uwo}l-XFN@2tl28zwDy~#Vd{e67Lxg0X8AB1Nt|s1Z+6-qZowK@@!{lQYLx?X z;INQ|?MfVOJgO(&tBM7L8$`%PD_{!bz0%xFMB@Zb+w->&?(Xg;_Vx9l;}YXuPZen$ zF~aaCadw>xEJo6h7{(D{+19;c6|Ttnjg9qnTTKx_#d?Fwe#1#hblY`?fGS4dz#qd^ z+R$pLmra;ol{@|9wWNMqP+?_`DJUqUx`$s7h|BE)QdxUXR5tJ4B9Jl(Rz2U#|KSfk zVu|#8E^SQn+$-q8``*n9vw)~qnGcOMdtS_+pHHZ^tWqHFsAx@fczUkU^lkHDKkXe5Aur(+ld4Y7Y^bhlo(}A1j4xOP4T(IAlqzsosiu!MBw87@1HnM4bHI3 zCy5dHzfJVOIBE28a$b3wtqt$*y|yEH=}?i>Q+*YW@EfL=LY*u>btXz1_6mt#wqe?8 z%=~C^{Nr@C)sij(y{Ack&OiWSU}Q95&z4!cVEC7ax3f~3jp&aZ80_!I3wmWD!OB$0 z{^Iq^__#P%ISAzb8O9gG51&P-t-kM&4EsHH@bA53Q%TuLM%9#pMPL<|{#?eGCtz!W zKcSxg&=v3Qa>33oj>>o+_W?|OR~SYkR4b!$15hzmT_gZgJ2X=$@5`JJB4Wauygx}K zt#Lb!dmV@LS;+C%!{+V(%7!^|=ye*~Z7aBhy%ZaW=jBy%J=I99gvg}zGj{mdEYv>S z`_tHJFa2}a@EeHZR(#luR^3xdmuT+ji)Pa}eC=+qUgokVOh_ZB-v^7z@)Ftp#=lm2 zJf)gm^!ac1xN@@KP4%}a;zvO1oaM#dlo4iOvWUyxR;fWFt0ahirYRX!Lz|^KRnuX) zdCK$I3ejVa9+ev@TKw0|U^kjkRx4Uz$L#j;9L3bCkhcW3^{%uc6LVLYX_>cbOMk!q zL(G882NRe~d%_<`&OVK$`#j!I-xdfoe$d&Gr%%Hxr$vrtO8EM@2|W)UywWWC zV&)P}&Yepy>KgiuPFVRa)MV=q+USc!kj3y&g6sHnse#`6YS`l*IkXyl$1&*B z6fH+%DUnfjnkV^S`OZbs*FstL?f!d-+hAfF)9YHBh2^X;!+>x0J#zHklL4!=!A?%> z+;GpRP^zzaU&5`Hz#X{zi}h!(=RK_&;rUf{s3ucGctb$&)vfJ%oM($P-H({KTAZ#X z0-UzezeV$-Iic&eNmaLm%3psLc7ooY^gLz`5@fo6 zi6h}}YRnwSlpgtruRUL3+G7AFd~q_QRiZl;>_4GmM&A0%5u9uF-=kSls^6a&HUFX@ zF-ieG^TJ9k{d}_CRrN-%n5awj1WkiKeeo$}j zD_X0uoY-P%0k5d?M^3sedsu|?!jjF0lES9XVy0KdGE93%u&*5fi*CFv_or19`u1(# zc3ja`<+O^mKR;hBTdcAenR8omnUMypc5Uh;onJgQVO)-vpTERf-H4gdQW;~adL`{o zKsAXX4&pVEvNvO29{C!GH}|HBdWRZ$B(9mZeup(H3QVdqQ&x zc}|}bmzbJNGhpoWw$)tV>zFxmWd+Ps&5kLdiwL@Z@6|0=RF1PmlvWd~voO+1jq&aO?YkU22lyPS%CIG~Pw)3Zs{VwwBZ_ zQ@5CObd&!usu!LU2lvDXs|81{QD3p@>4vRx)PPgZ~pby3$BruXBm)$qQ z3ZCw_B?Xe}KOnT|auEKG*?O|C`kGK&hD9p@`S-{Yug^zD*vK8mOJfLfPs6-?B^%mn zJ(|%rlX)H0!#dfP7gJY#x)#%Dzzcv198$dJQuZm3RK-r+PP9ntJB@I=A5ME?(vH?+ z2ZY;|+xzd++1!!CZb(Ju+bq((YbA{(gH}PoXVV3_vnwR<#OuF+u_C~#OK&#}_+5QOY+s*j#EyZ* z0Y7ZU7Md>fuCnH8th5AT0_jIRi{?&-Ew~8O1767^jYewAmPLi7W;AoOkfBXn8?rpB;bs9J+&t?iO03DS-j^sg#(EUsgo zM!>W?lvDHy8KclgmWpGefsNDwmuP+v-Z-7uatJ(ldbnD7IyqkJILh!I3=aWx{Z=53-PCrL3xIx%U(aZ+bC)B1ty*aaGR5$*r^fiLI#wvZW+IGnxbNsGn!fqQh z;Yk|3VSF+{N2OuvN*VlDlJ_CExw(kT#phMLaT1*F)zD*lmRkcFj2PTG_U`a+huOH! zmu!-Jk~^Vt`__>ghC8}Pn}76ZIK7&-Cqz?Y2i3zxOHpH&y6sj(5DJnN;?`tDHoj{_ z?Io<=m;6@PSQ2f%Sx}t8TgdFGieI_$z+~&}xrc|_Wd7&00D7qVv#QA3V)Vi6(~&RB zvM|%>m8VBH-v?2#{a+7abzC0*gjrY+Hm6Y;p5a zu5)I79|ni{ku@o)Dq3}wAjKOHZU02I<%DYaf(mO?9nC~_Us*@$XEA5m?cvk~S{p;w za0sld689Xroj+vYxtlXVN!g`!sCQ3g&Y)`lBG7}*ebqM^{&>58+2Gc28@5*$JRC$P zOPBP{L&Wm_=xlR;4CEAr7~k1R35e%45|~lw$y%AGhhArfw;w=6HjuZO%DJG~ynP{1 zdW{OTD7QIJU&bhrM?HZUQ1P8-j7aTuiP@p?(qY<HujBQdrQqE=U83DdA?OQF`1MKbmsCJch{Zl9{yAEh8 zTK)UJ6V3C4ocM%iaFvlklWyt+Jj(Z0pvOm{!|>xQ1MKNc?PlX9KBNZ`Z&b=rM|J>O zD>ERH$av0Mr$V<%MZQB!BRF`r^EW~_3BL2a`+dhSa|B}>oiRM%{kQDu=Z$n|Vgq>O zgk}mEN=H$bP}pdlU0UfCH`gU6bQZgmp=Y-Pzruqu1SZ@d3l|HIl{E>DZ(cCvL1&(k?G zuczJt4;9MuOZlRCGQ`jBP^w0`lUs}tn2w!RI#nirx7Y@o2&`nWgCyqsxkOJ%hmkx! zOm?FE7F!PE0Hx`#rYOGa&o1(lV{g53-kaPtfR(<=ttm=;@c@j3>X?)g7@#Evi3dXH zPh}YE%m(6(GFH@cl{g4jeGnmMjhQ8?rsPjj*VSg+x?`qiEwS*r7@bvF$y!Wke{o=+ z8J;&wP=gIM7p5{y)ezx!^a#hy?MxucQ(ZhmskAyYiqjf(HtALj^9Q#@$3gmx)b5MXOwRC8Nz zin!Huv5NEHH%vMNVY#SzhP$Z#i}v=u0dTFj&FBd>Nu7#S9lCj zo+>&m4NBqKVV&Il^hx4V{>vQ2_~99QAhm`)9BKV%*>UZ?_nX4-nxKIV97-;X5DV(t zJZpJC+PxR>B0-<6s$UF>XgKZXlI9^Vyq|T+5hxVq?z~Ic+-Eh-G1*I1o#=$`-@YDe z__w~UXi$6}q5iJ*f?zA+(UpxXRft~ZVfSL8AZ)>9!08Bh-hy`S4MGz+Za!|1Mz7pk zTw&>CiwWcYGt-vU>uO6Nsu%nQ@II*saEsSK?jAYAHi`9(-+Mc+m~`D^>tL?(5pq*< z`+8N4^7Yzavd~CP^bH#hgCc!m|BL?L>ubtSWT4Q#VlMt%I%?=i7e)HT;nH26S`*S8 z;I_39fug;Wbs1V_P+oE3oJ)3uKxQgW%`TFQ+Qn+m?ioC|ohj37%ZnCtL=wXrLbax> z821_%@Ae?0kzP$abdbn|s|V{yLrtTudX93hQnrTr?%Rf`YOy}Nl76nQ`fS(np_|6s za_g^Lghba7^|e{duF7+N-|E=9wt(_FZ6;~0*Y92H|K#Psv^!uu9lE0olIVocdUSKibl9C{}A_dWUY5m z4Nu=a6Z%!63-~V!54cCZeYO;G9j?a2Cx@U(VHkkV9b-6F1@05Xr25fq5Hmv|venD{ zH-k9T#?sj*u>zx2ogFKTEq~F>&H1JesV&6t|9Ml%FBlJ``IQJ6IAoEDU$`zR6nRbM zq^drUr`Cm!5WIY+w$Dk&LEP}IUq{hko81s`>L#-u49@gYar`o#863-5s;HtM(aEqH zq^fkx%*wn@R$TiB{J<*4xa_H^RBRqrUBq-tyFDVfYKM@1j2D?#D#aqMG4)vDDlUik zTs5xLB=TF1m$Rk0f5WNjpDT8{(9r&OU)sd?V}ROqj+uqU>&u^LaRnmGkkjsrPQtqV zk785_6USsbAxFS@7k0lXa5)Kfc8=lcSQfL*Cq>1Hc6S+aKS_*ps|G~8ZzlOQf8@4v zGwL+Cvi#3-L*ry{d@?*(Eub*s-GfTtne{?Fk`uhFz@fQbXwID^KCwPdezE9&`&;4g zOG@|Jj9P9c3`|9yN^d<~+?OoKjsCtI`0;zGc9yF=mH6@bsDwC5n5qNs%4=anv?9(t z3u44=demhiu!$Z{75-lo_LyD{bbYc1v38k35`F6%Qsm;(qHR-f-qs;;sh`O{N6lA6 z537E}hb@RWJwr@@8%{>*vbg1{nuYFC$p5Q_AmVwQa!@Im@#wn-Z0H|3!9B^b6HctZ z4&CpQKbfWj83ajZt6niV9j~?}@((W^xE2y83=xo>bJwDM0A@6u4jhIUo;K8VECJ?* zqm?&&TogATagxnLBXG1Gy;yAO66@d_{fFBNqcqgvPU?GIjU%F;nDdMC`p8!wJ&A8n z_C?&Jb*>Te?i}!y-t2n;`xUr%fb(w%OI4kjV3;KD?19txC@Tk1NjQZ)+MtynY4EQFNJeba{m1Wnrq^$ob?uMJK<6W7`oA38 z%z}b1O)_r`26_F|{4cqb9I{Ak7?xq)rv>WqEnc_V>V&;LQ4JS+L!l0vUCAR2jjpu+ z!V>l0@=rgWcidDTaamG0R6pf+b~1-Fm&WwGUznBCD|F@v5#-<~OF0iwe%(s+-N06F zEG8y6W(E7UDeu_gZMM%;$HA%CjP-QCe>2LoV)>L4cN^u~g?%a06_Ygl3a42{8g?!QS#kYZPBJ@$q5M@>&)D%M|& z=i$GYIa+Zqnc(6bIQ?wPxSy2JWfteG%P1z5BpT`#n(TX`*N zZbS~sgx(_MUvLm}Brv)6l4sSO?~HBG+!ItEY5sQz6gZgArxr`}Di--A_IFdW_KS+l z#Asmdm~*37@Xt*C+bhY(>kV>1M+ZM_Bte4bG%pKS`wiw5a%{?9zK2i2BetWuanD_z z7d4#GCiJ0k$n&(14K$~4PQ)(^gIn}QGWxmGR7i|G->K^&uy0LVMlR!8XOji-JJyc{ zM?0z1l>mAqEj*`aLMGF{o0~VxvlNgGbb`R68CJ7$J|%8jtNrwE(%Q0|(E%5Xt)>SD zb5#+-bX-+a{N#jJj$su4Muq7}XDdSrV3r?UI7VkyhnVW@kg#L!!C=id(x24DDu=Sq zTumE~(V#}Tl@&CsE1?Rm?k83LckBrHM$1tfcG0pgoPh1BbuU6OO76+*X4Z83`8vX3 zSK6JzCW>hfWyiGr(#GM+5mUChJtlqV7pDBO`q@@V;AWA!P>?nL%MVYnR2IwhPuD%$ zZ`H*x8MI`=17^)?z9JV|2)}($l5onsXzc65<)+GxsDRzP_UlBe?(T3k{Hp1$hB@}2 zk{%9akl=YllNxL%n~I+3Dqnj0cKSUHvCU!&7?UNO*#dZHX>E6z!G2*;WF=Bf|Hc1` z%KoANO!Fm5j!bYhz9`t*U~;vR2Ngd1Ceob;}5f zKeDY2FX(G&1inqeu<0RhpxfcJ-2C|8$>*elIxN|2iGyUINkQ6TSx#aB5ijevPD2M) zQiLp)k+$bh6Fwo;6MX!Ai^n7f)vRv5i*V6H%HZx5iXj{7mXX2@@^KrsB{{%(m0i@lp!%<;j$SRT>J(XUg-SWabt+o+GIFNOh?Q0)x8xRlzw|KC_@KF6 z*<1zMT_&0lh;$m?p>vw4A^3yHtivX<7=If~7Um5-_K)# zV78Xn1l!UK`iP6|oJws5WZ0;kM|G@iFDIdWMYVwe)${RMyOrgsToc7j@9e4PGmhhj zlN@F}avcUM@Q2t9_ubr?j5a^|qw)H)kP;9dtMl$6qAN6XdRoH)xllu+m1~`N!M7i& zWpy&WMX_@`XOVtE${u29c!h`mkzV$njc%2~gU`ZRsn+}njnI+dRIM-`Zm8W*s|s*Ci4ldsaA12jU|He)NjS-?2M<*3l$aPl!nKPvp*pExgKw4 zT8Q3xoPNdCw(Kq&)As9hhLB5G%h9Ba_=m6+X%^M0;om1D7heY6w{NX6Z(@=Yvldti z{K)40su!F1mV>cCE?LmV&~;qKb42P202PFiLy6#J(Q?fB&6s4p!##9f)&lq>_}yVN zZXwa184ul}%x8v#R_+C$w%NOw=hmE~7^@Fw>~pGHYZ59LYG3&UvMCBl4IxXq2vODGA>{QZ4w*419ARVg<* zI+LTEhEcw4?^zw;5r!9A{nw-)*EPef3|Hb0>(<7&&(k7OkYH)cgJ$5DsbtIq06bvk zu*NS-vQg;usND*@8S|2_XjY2LQ|bbTc57A4aOu=l9`A-&`q`3UDHPME(1h8K zW_dEU+Qx4%KW%joLML~N=4c{C>lc(6=04HZ!fd0OL%7v+L#+splNO@jWoh3|xEYeu zvnc>}TH#f#FJciRNTB^kXx8oA-b(F_75iZutNL-8wV^#iJaO4w=5Gx$Q6WrCtGsy3yuF9PsQRY6=_NOtv5D)(_ z;v2c{HumLB0zmWVtoTwzXN$z$P4e^8l-1WKv}p7AHa~2sMnYZori3yjj}i+_Jq6XW z$1WI9)zv`)gTW`VZTqb`O~s#}y(j})mQ?-(HL^qjc!yyB2|L4H9_CBGRl=(!w-IEc z+tEHAKx8N!6YPhX@myQb>w>s3JcfPIdsK4sFZt%AiAFDyL2vD+(zoT$sVvJNv zIS*cW3sKh|)}WiZ&et@IS1ZQ4+O53S=Fp+tPf_*$*Qf3!f(m|rT&w%N5T}y+oY7~R z?y&rU{V=-$<3q~QS8krF}ui9r*GJESzxh559>Wxs0P>kVB41(2kX%lKqS+xIFi#hHl z6~51qFu%wDpS!+eBaO5fc=yWJ=r>0Bja95>vY7MvPo)oQ?9B6y>=R?3?-izrD(k5& zn2niNnSiF_4kW?2!TM;h8uH-ajT+?V8TY2ziXUr78Ktlj%hJ;DbD2Q-hXJEUM=D;r z8Z)~(_-M|V?7ds!^>~#!Q}B||*nwkj5^5G=aqwJMe)DY_*_$xliu$*t?}%B90Z(GZ z=T17`%QOuM(x&F{X2Z0GeY8+Rq|=vqYH?)TS5;Gv^mhF`t6lLYqt@N@Co}D9j3tll zKy)l;1oxsWB=Vj>6gpj@`ar3-s^3PFv3-BR)Z<k;VFLiC)c;Xw0(;Nj`k3SOKI1rmoL{ylKu6u|yw=mK@5sEh$b*TN?rQ zhAOU$J-$W*=zNYJ?Z2!WUS_pm?W_d(KS@BUONM$Dp#Jqqmp1cKhBP=mvY+VrhUFPr zFh=_^Cv)5lT0p!d%@{i~n>jUq6ExtPl)-BsE%t(w3+%hjTu-=~=2v{3yP9{qzS@+( z_Ir=Ti?+)p{y8OVqai)1YDtL07dbvLF>u!GE9qz(YEJ2xE56;=b}vL~9}Cl|c6kwk zOCXJgoAUN6g_O^8FE758pG#fIfgK(Tn!MpLnExcPCPhzxM-g)2FFiN{DlZU$%l(Uz z{UO@>nPd3Eo?3nea!2f~)D*t5x>R43mH`~O9+f_RTUwO44 z&aKCHSa=uC)fq~Ztx9Flwi_`*Wu4)5un#dSMXB*yH8+xHv}2CrtHPo?N3aLySq1#M zHm#Wn`w(_xcYPXlifs8 zQ4}l+)`+U$#(}P1Sn;rVWsl(*OU_E4`we=dGhA_GhniY6&d|c6eR}7UAAhj2@K)ZK#dnxG^x8XTMq8H_TMd*TP%;WNFjzkRi{h~%8 zg0fY}CZnA6UB9A+&ZkAxcmwEZ*)$&wcnY{V+g8Os_9{a;(o{y>drI17!#lP;Qyus^ zpWHrPWxj+69xXN25o~3|QyOnM(RL{@>a5h)Oq>$Mnd329;0rd_Q{$$Tk-mZXz^Ctd zx$5|D&>`H4dT+!bTs0oLy1F1+vxcY%54`-aSt`Isgi^^{iJ zF`tryEi6x7{(eIzCBe{&m&rrZHXJTB3TdKLKlIGuP}Y^Q5n6b9BU-`;&Ul+)hu^IqhH=kq3|LWR*s{9F3ZD^YXRWEi|2?XCehqj0bI@m!8}@e{;rIqhL1|MOq}GVO|Qzt&}D@>J8egY?SI z-A_oQtSD6Fj{|aHrgo1GSV`6rs={INsR@=TsM?oez&#!h7FO)(Wmp%K@){Qw;kceh z4Z;sPXljxz6;Vy<#{Nuvbmla-YPf05Fr>L=)=4tTF5BE@@t?yJYM>4$>g^?AEZ-Kb zw~Ja{o+(R}^u*;_KTY75HQ$z|d6Q@M^I5kH56}=Y&Q#zH^`<)HZSQpAT3}Ch(lr;h znVaOH<33PHx7QS%C;<2|HL&F0 zlK%0jOK4uDbiBmAO*@#6r;ZI+3*M%F@atV+UEVjw5zkQngTo9&zxHJ|ZSVL4qbSe$ zjh}%zFKCZnY@AjhX}-si0CQ2DBd4U!EMPsEM}a(FmxMSbA;k;FezD-+#Au$ZBR zVVp8&bn&48i^mNt)3o)Ti2EDTc+!nLLWPqwBse$E|Ciq_8;5b!9r&l zu$AiO^uf6wv_8o&h{Jiv8XsIj`rZ|RV;nz&+Yrzz>AYVC;RvGuyAl^K|gd@~04ldRY^e}yU_Uy63%@f<5WD}o1AFXB{qgi@)ZgtDAiv8C=&YFPFbFU4NaA#CB{-jPY@;q6DtW>{Hy-TirJxva`D@_f}8tlh}#s zuePL-fGqzhKfU&L_w@q8*arQ)p_n~TCQnRO|Ogj7WAF0iG z7Q|matzOlPl?_{TR0Jod>+!ccqs$F^!VlWCgx$aLMp~5n#pO>8{ zl*JPa671$b^`eB>M+OwMNpJ=tx1yy41uvrUOt8a=lqRS|(7Y0zUH==qiqE|=dPI3d z?qOBOIXUsi;~wQx0T0T0$hvJO|9$zRyIdqHkfnKAq9=}$vcOlqIH=w%iDp7I4k3N# zd3|h>3fo;==S<&U@xlot>eQbx z)pCwzH9FAYR`1$P3yMzcD7*{!-}{GG7D#w^F9ulIR^BrjgUg(j&eG0``WHIK4xYZ|&u98O1 zZ3WajZHI5~{6a@zw(DBz2Jq(QM|;t#*#gD&k2)3SAX1h+m;LFIvYqk`yF>x2>EvHl z_RrEY!ry0A5MByqLQ(ElOVmShm0Mw>x90&Q@}5t=iy}e5(waY=;|4;Nx)sffo)+ z+*WzY$(Ldaujo4rjdI+MmU)X&cPwGIpn^WzB8Oh4UuM`$1V(V{ta3y5CJV?QgZCce zKk~u=x{z$=ly=tijtV>xZV*!s-P1-_2*t?OS>S{`7;Z@ubxv?{!wPhY8@|!p*v@Zi z+WAgz24Un?uR+dMn9>io7mx=w^Lq`CQ`K9H(52@5H4tZe+wr|aE~HsQ)MbyKQeBy= z8S;AxiY>}l)5w-Tu~6rAsv8DRj^(IQIb+eAkuL^OZ3an);_`!{0 z;Lf~=@Yc+IS*Ap<4qk%_7Y&X58?YS6bv*(5#t}#JCW(7pijK-xp-zJjZd&1McFfPD z(6Eltak?=A;$Z0KF68i=TpMr{72Ye~r^5fpL#ykl<;k9RXMl*;#%PPmW6A0&jNJBu tFD_g=a;Ev_@%4u{!}6;wjJ{9U=sj^%qq-kwP|;g}f{e Date: Wed, 23 Jun 2010 14:35:57 -0700 Subject: [PATCH 1749/1860] Fixing bug 572609. Use faster version of GetElementById() from our C++ code. r=jonas@sicking.cc --- content/base/src/nsReferencedElement.cpp | 11 +-- content/events/src/nsXMLEventsManager.cpp | 25 ++---- .../html/content/src/nsHTMLLabelElement.cpp | 3 +- content/html/content/src/nsImageMapUtils.cpp | 20 ++--- content/xbl/src/nsXBLWindowKeyHandler.cpp | 7 +- .../src/xpath/txMozillaXPathTreeWalker.cpp | 8 +- content/xul/document/src/nsXULDocument.cpp | 89 +++++++------------ dom/base/nsDOMClassInfo.cpp | 11 +-- layout/base/nsPresShell.cpp | 12 +-- layout/xul/base/src/nsMenuFrame.cpp | 13 +-- layout/xul/base/src/nsResizerFrame.cpp | 7 +- layout/xul/base/src/nsXULTooltipListener.cpp | 15 +--- widget/src/cocoa/nsMenuItemX.mm | 19 ++-- 13 files changed, 77 insertions(+), 163 deletions(-) diff --git a/content/base/src/nsReferencedElement.cpp b/content/base/src/nsReferencedElement.cpp index 636b66fb9f23..6cb3f69ce66e 100644 --- a/content/base/src/nsReferencedElement.cpp +++ b/content/base/src/nsReferencedElement.cpp @@ -213,15 +213,10 @@ nsReferencedElement::HaveNewDocument(nsIDocument* aDocument, PRBool aWatch, if (!aDocument) { return; } - nsCOMPtr domDoc = do_QueryInterface(aDocument); - NS_ASSERTION(domDoc, "Content doesn't reference a dom Document"); - // XXXbz we should really have a sane GetElementById on nsIDocument. - nsCOMPtr element; - domDoc->GetElementById(aRef, getter_AddRefs(element)); - if (element) { - nsCOMPtr content = do_QueryInterface(element); - mElement = content->AsElement(); + Element *e = aDocument->GetElementById(aRef); + if (e) { + mElement = e; } } diff --git a/content/events/src/nsXMLEventsManager.cpp b/content/events/src/nsXMLEventsManager.cpp index fc06c50631eb..7a015ddb75dc 100644 --- a/content/events/src/nsXMLEventsManager.cpp +++ b/content/events/src/nsXMLEventsManager.cpp @@ -47,6 +47,7 @@ #include "nsIDOMEventListener.h" #include "nsINameSpaceManager.h" #include "nsINodeInfo.h" +#include "mozilla/dom/Element.h" PRBool nsXMLEventsListener::InitXMLEventsListener(nsIDocument * aDocument, nsXMLEventsManager * aManager, @@ -67,7 +68,7 @@ PRBool nsXMLEventsListener::InitXMLEventsListener(nsIDocument * aDocument, return PR_FALSE; nsAutoString handlerURIStr; PRBool hasHandlerURI = PR_FALSE; - nsCOMPtr handler; + nsIContent *handler = nsnull; nsAutoString observerID; nsAutoString targetIdref; @@ -87,13 +88,8 @@ PRBool nsXMLEventsListener::InitXMLEventsListener(nsIDocument * aDocument, //We support only XML Events Basic. docURI->Equals(handlerURL, &equals); if (equals) { - nsCOMPtr doc(do_QueryInterface(aDocument)); - if (doc) { - nsCOMPtr domhandler; - doc->GetElementById(NS_ConvertUTF8toUTF16(handlerRef), - getter_AddRefs(domhandler)); - handler = do_QueryInterface(domhandler); - } + handler = + aDocument->GetElementById(NS_ConvertUTF8toUTF16(handlerRef)); } } } @@ -120,7 +116,7 @@ PRBool nsXMLEventsListener::InitXMLEventsListener(nsIDocument * aDocument, aContent->AttrValueIs(nameSpaceID, nsGkAtoms::defaultAction, nsGkAtoms::cancel, eCaseMatters); - nsCOMPtr observer; + nsIContent *observer; if (!hasObserver) { if (!hasHandlerURI) //Parent should be the observer observer = aContent->GetParent(); @@ -128,16 +124,9 @@ PRBool nsXMLEventsListener::InitXMLEventsListener(nsIDocument * aDocument, observer = aContent; } else if (!observerID.IsEmpty()) { - nsCOMPtr doc(do_QueryInterface(aDocument)); - if (doc) { - nsCOMPtr el; - doc->GetElementById(observerID, getter_AddRefs(el)); - observer = do_QueryInterface(el); - } + observer = aDocument->GetElementById(observerID); } - nsCOMPtr eventObserver; - if (observer) - eventObserver = do_QueryInterface(observer); + nsCOMPtr eventObserver(do_QueryInterface(observer)); if (eventObserver) { nsXMLEventsListener * eli = new nsXMLEventsListener(aManager, aContent, diff --git a/content/html/content/src/nsHTMLLabelElement.cpp b/content/html/content/src/nsHTMLLabelElement.cpp index d5ab010b3ff5..e7d29410ee0c 100644 --- a/content/html/content/src/nsHTMLLabelElement.cpp +++ b/content/html/content/src/nsHTMLLabelElement.cpp @@ -443,7 +443,8 @@ nsHTMLLabelElement::GetControlContent() nsCOMPtr element = do_QueryInterface(content); if (element && element->IsLabelableControl()) { - NS_ADDREF(content); + // Transfer the reference count of element to the returned value. + element.forget(); return content; } diff --git a/content/html/content/src/nsImageMapUtils.cpp b/content/html/content/src/nsImageMapUtils.cpp index dcc48070dc15..ceb9240bf398 100644 --- a/content/html/content/src/nsImageMapUtils.cpp +++ b/content/html/content/src/nsImageMapUtils.cpp @@ -39,17 +39,15 @@ #include "nsReadableUtils.h" #include "nsCOMPtr.h" #include "nsIDocument.h" +#include "nsIContent.h" #include "nsIHTMLDocument.h" -#include "nsIDOMDocument.h" -#include "nsIDOMNode.h" -#include "nsIDOMElement.h" #include "nsIDOMHTMLMapElement.h" #include "nsImageMapUtils.h" /*static*/ already_AddRefed nsImageMapUtils::FindImageMap(nsIDocument *aDocument, - const nsAString &aUsemap) + const nsAString &aUsemap) { if (!aDocument) return nsnull; @@ -91,16 +89,12 @@ nsImageMapUtils::FindImageMap(nsIDocument *aDocument, // XHTML. The attribute "name" is officially deprecated. This // simplifies our life becase we can simply get the map with // getElementById(). - nsCOMPtr domDoc(do_QueryInterface(aDocument)); - if (domDoc) { - nsCOMPtr element; - domDoc->GetElementById(usemap, getter_AddRefs(element)); + nsIContent *element = aDocument->GetElementById(usemap); - if (element) { - nsIDOMHTMLMapElement* map; - CallQueryInterface(element, &map); - return map; - } + if (element) { + nsIDOMHTMLMapElement* map; + CallQueryInterface(element, &map); + return map; } } diff --git a/content/xbl/src/nsXBLWindowKeyHandler.cpp b/content/xbl/src/nsXBLWindowKeyHandler.cpp index 9e9be6c2a64c..7f7695f082d4 100644 --- a/content/xbl/src/nsXBLWindowKeyHandler.cpp +++ b/content/xbl/src/nsXBLWindowKeyHandler.cpp @@ -536,10 +536,9 @@ nsXBLWindowKeyHandler::WalkHandlersAndExecute(nsIDOMKeyEvent* aKeyEvent, // Locate the command element in question. Note that we // know "elt" is in a doc if we're dealing with it here. NS_ASSERTION(elt->IsInDoc(), "elt must be in document"); - nsCOMPtr domDoc( - do_QueryInterface(elt->GetCurrentDoc())); - if (domDoc) - domDoc->GetElementById(command, getter_AddRefs(commandElt)); + nsIDocument *doc = elt->GetCurrentDoc(); + if (doc) + commandElt = do_QueryInterface(doc->GetElementById(command)); if (!commandElt) { NS_ERROR("A XUL is observing a command that doesn't exist. Unable to execute key binding!"); diff --git a/content/xslt/src/xpath/txMozillaXPathTreeWalker.cpp b/content/xslt/src/xpath/txMozillaXPathTreeWalker.cpp index 7eb0c2b8c342..3ef6b62a82ea 100644 --- a/content/xslt/src/xpath/txMozillaXPathTreeWalker.cpp +++ b/content/xslt/src/xpath/txMozillaXPathTreeWalker.cpp @@ -107,13 +107,7 @@ txXPathTreeWalker::moveToElementById(const nsAString& aID) nsCOMPtr content; if (doc) { - nsCOMPtr document = do_QueryInterface(doc); - NS_ASSERTION(document, "QI failed"); - - nsCOMPtr element; - document->GetElementById(aID, getter_AddRefs(element)); - - content = do_QueryInterface(element); + content = doc->GetElementById(aID); } else { // We're in a disconnected subtree, search only that subtree. diff --git a/content/xul/document/src/nsXULDocument.cpp b/content/xul/document/src/nsXULDocument.cpp index 0897e35303ee..e090fe51bb16 100644 --- a/content/xul/document/src/nsXULDocument.cpp +++ b/content/xul/document/src/nsXULDocument.cpp @@ -1302,17 +1302,9 @@ nsXULDocument::Persist(const nsAString& aID, nsresult rv; - nsCOMPtr domelement; - rv = nsDocument::GetElementById(aID, getter_AddRefs(domelement)); - if (NS_FAILED(rv)) return rv; - - if (! domelement) - return NS_OK; - - nsCOMPtr element = do_QueryInterface(domelement); - NS_ASSERTION(element != nsnull, "null ptr"); + nsIContent *element = nsDocument::GetElementById(aID); if (! element) - return NS_ERROR_UNEXPECTED; + return NS_OK; nsCOMPtr tag; PRInt32 nameSpaceID; @@ -3925,14 +3917,10 @@ nsXULDocument::OverlayForwardReference::Resolve() else { // The hook-up element has an id, try to match it with an element // with the same id in the base document. - nsCOMPtr domtarget; - rv = mDocument->GetElementById(id, getter_AddRefs(domtarget)); - if (NS_FAILED(rv)) return eResolve_Error; + target = mDocument->GetElementById(id); // If we can't find the element in the document, defer the hookup // until later. - target = do_QueryInterface(domtarget); - NS_ASSERTION(!domtarget || target, "not an nsIContent"); if (!target) return eResolve_Later; @@ -4064,17 +4052,18 @@ nsXULDocument::OverlayForwardReference::Merge(nsIContent* aTargetNode, for (i = 0; i < childCount; ++i) { currContent = aOverlayNode->GetChildAt(0); - nsAutoString id; - currContent->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id); + nsIAtom *idAtom = currContent->GetID(); - nsCOMPtr nodeInDocument; - if (!id.IsEmpty()) { - nsCOMPtr domDocument( - do_QueryInterface(aTargetNode->GetDocument())); - if (!domDocument) return NS_ERROR_FAILURE; + nsIContent *elementInDocument = nsnull; + if (idAtom) { + nsDependentAtomString id(idAtom); - rv = domDocument->GetElementById(id, getter_AddRefs(nodeInDocument)); - if (NS_FAILED(rv)) return rv; + if (!id.IsEmpty()) { + nsIDocument *doc = aTargetNode->GetDocument(); + if (!doc) return NS_ERROR_FAILURE; + + elementInDocument = doc->GetElementById(id); + } } // The item has an 'id' attribute set, and we need to check with @@ -4082,24 +4071,21 @@ nsXULDocument::OverlayForwardReference::Merge(nsIContent* aTargetNode, // this locale. If so, we want to merge the subtree under that // node. Otherwise, we just do an append as if the element had // no id attribute. - if (nodeInDocument) { + if (elementInDocument) { // Given two parents, aTargetNode and aOverlayNode, we want // to call merge on currContent if we find an associated // node in the document with the same id as currContent that // also has aTargetNode as its parent. - nsCOMPtr nodeParent; - rv = nodeInDocument->GetParentNode(getter_AddRefs(nodeParent)); - if (NS_FAILED(rv)) return rv; - nsCOMPtr elementParent(do_QueryInterface(nodeParent)); + nsIContent *elementParent = elementInDocument->GetParent(); - nsAutoString parentID; - elementParent->GetAttribute(NS_LITERAL_STRING("id"), parentID); - if (aTargetNode->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id, - parentID, eCaseMatters)) { + nsIAtom *parentID = elementParent->GetID(); + if (parentID && + aTargetNode->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id, + nsDependentAtomString(parentID), + eCaseMatters)) { // The element matches. "Go Deep!" - nsCOMPtr childDocumentContent(do_QueryInterface(nodeInDocument)); - rv = Merge(childDocumentContent, currContent, aNotify); + rv = Merge(elementInDocument, currContent, aNotify); if (NS_FAILED(rv)) return rv; rv = aOverlayNode->RemoveChildAt(0, PR_FALSE); if (NS_FAILED(rv)) return rv; @@ -4363,11 +4349,11 @@ nsXULDocument::CheckBroadcasterHookup(Element* aElement, } nsresult -nsXULDocument::InsertElement(nsIContent* aParent, nsIContent* aChild, PRBool aNotify) +nsXULDocument::InsertElement(nsIContent* aParent, nsIContent* aChild, + PRBool aNotify) { // Insert aChild appropriately into aParent, accounting for a // 'pos' attribute set on aChild. - nsresult rv; nsAutoString posStr; PRBool wasInserted = PR_FALSE; @@ -4382,39 +4368,30 @@ nsXULDocument::InsertElement(nsIContent* aParent, nsIContent* aChild, PRBool aNo } if (!posStr.IsEmpty()) { - nsCOMPtr domDocument( - do_QueryInterface(aParent->GetDocument())); - if (!domDocument) return NS_ERROR_FAILURE; + nsIDocument *document = aParent->GetOwnerDoc(); + if (!document) return NS_ERROR_FAILURE; - nsCOMPtr domElement; + nsIContent *content = nsnull; char* str = ToNewCString(posStr); char* rest; char* token = nsCRT::strtok(str, ", ", &rest); while (token) { - rv = domDocument->GetElementById(NS_ConvertASCIItoUTF16(token), - getter_AddRefs(domElement)); - if (domElement) + content = document->GetElementById(NS_ConvertASCIItoUTF16(token)); + if (content) break; token = nsCRT::strtok(rest, ", ", &rest); } nsMemory::Free(str); - if (NS_FAILED(rv)) - return rv; - - if (domElement) { - nsCOMPtr content(do_QueryInterface(domElement)); - NS_ASSERTION(content != nsnull, "null ptr"); - if (!content) - return NS_ERROR_UNEXPECTED; + if (content) { PRInt32 pos = aParent->IndexOf(content); if (pos != -1) { pos = isInsertAfter ? pos + 1 : pos; - rv = aParent->InsertChildAt(aChild, pos, aNotify); + nsresult rv = aParent->InsertChildAt(aChild, pos, aNotify); if (NS_FAILED(rv)) return rv; @@ -4427,6 +4404,7 @@ nsXULDocument::InsertElement(nsIContent* aParent, nsIContent* aChild, PRBool aNo aChild->GetAttr(kNameSpaceID_None, nsGkAtoms::position, posStr); if (!posStr.IsEmpty()) { + nsresult rv; // Positions are one-indexed. PRInt32 pos = posStr.ToInteger(reinterpret_cast(&rv)); // Note: if the insertion index (which is |pos - 1|) would be less @@ -4446,9 +4424,8 @@ nsXULDocument::InsertElement(nsIContent* aParent, nsIContent* aChild, PRBool aNo } } - if (! wasInserted) { - rv = aParent->AppendChildTo(aChild, aNotify); - if (NS_FAILED(rv)) return rv; + if (!wasInserted) { + return aParent->AppendChildTo(aChild, aNotify); } return NS_OK; } diff --git a/dom/base/nsDOMClassInfo.cpp b/dom/base/nsDOMClassInfo.cpp index 5bcee6b0ecf3..1d96bdcb02a5 100644 --- a/dom/base/nsDOMClassInfo.cpp +++ b/dom/base/nsDOMClassInfo.cpp @@ -4891,16 +4891,7 @@ nsWindowSH::GlobalScopePolluterNewResolve(JSContext *cx, JSObject *obj, } nsDependentJSString str(jsstr); - nsCOMPtr result; - - { - nsCOMPtr dom_doc(do_QueryInterface(doc)); - nsCOMPtr element; - - dom_doc->GetElementById(str, getter_AddRefs(element)); - - result = element; - } + nsCOMPtr result = document->GetElementById(str); if (!result) { doc->ResolveName(str, nsnull, getter_AddRefs(result)); diff --git a/layout/base/nsPresShell.cpp b/layout/base/nsPresShell.cpp index a6aff52cce02..d4602e8d3564 100644 --- a/layout/base/nsPresShell.cpp +++ b/layout/base/nsPresShell.cpp @@ -3723,20 +3723,13 @@ PresShell::GoToAnchor(const nsAString& aAnchorName, PRBool aScroll) return NS_OK; } - nsCOMPtr doc = do_QueryInterface(mDocument); nsCOMPtr htmlDoc = do_QueryInterface(mDocument); nsresult rv = NS_OK; nsCOMPtr content; // Search for an element with a matching "id" attribute - if (doc) { - nsCOMPtr element; - rv = doc->GetElementById(aAnchorName, getter_AddRefs(element)); - if (NS_SUCCEEDED(rv) && element) { - // Get the nsIContent interface, because that's what we need to - // get the primary frame - content = do_QueryInterface(element); - } + if (mDocument) { + content = mDocument->GetElementById(aAnchorName); } // Search for an anchor element with a matching "name" attribute @@ -3768,6 +3761,7 @@ PresShell::GoToAnchor(const nsAString& aAnchorName, PRBool aScroll) // Search for anchor in the HTML namespace with a matching name if (!content && !htmlDoc) { + nsCOMPtr doc = do_QueryInterface(mDocument); nsCOMPtr list; NS_NAMED_LITERAL_STRING(nameSpace, "http://www.w3.org/1999/xhtml"); // Get the list of anchor elements diff --git a/layout/xul/base/src/nsMenuFrame.cpp b/layout/xul/base/src/nsMenuFrame.cpp index bd2c26273269..672e4933f86d 100644 --- a/layout/xul/base/src/nsMenuFrame.cpp +++ b/layout/xul/base/src/nsMenuFrame.cpp @@ -1015,13 +1015,12 @@ nsMenuFrame::BuildAcceleratorText() return; // Turn the document into a DOM document so we can use getElementById - nsCOMPtr domDocument(do_QueryInterface(mContent->GetDocument())); - if (!domDocument) + nsIDocument *document = mContent->GetDocument(); + if (!document) return; - nsCOMPtr keyDOMElement; - domDocument->GetElementById(keyValue, getter_AddRefs(keyDOMElement)); - if (!keyDOMElement) { + nsIContent *keyElement = document->GetElementById(keyValue); + if (!keyElement) { #ifdef DEBUG nsAutoString label; mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, label); @@ -1035,10 +1034,6 @@ nsMenuFrame::BuildAcceleratorText() return; } - nsCOMPtr keyElement(do_QueryInterface(keyDOMElement)); - if (!keyElement) - return; - // get the string to display as accelerator text // check the key element's attributes in this order: // |keytext|, |key|, |keycode| diff --git a/layout/xul/base/src/nsResizerFrame.cpp b/layout/xul/base/src/nsResizerFrame.cpp index 314b6b816137..446e95d37c21 100644 --- a/layout/xul/base/src/nsResizerFrame.cpp +++ b/layout/xul/base/src/nsResizerFrame.cpp @@ -382,12 +382,7 @@ nsResizerFrame::GetContentToResize(nsIPresShell* aPresShell, nsIBaseWindow** aWi return parent ? parent->FindFirstNonNativeAnonymous() : nsnull; } - nsCOMPtr doc = do_QueryInterface(aPresShell->GetDocument()); - nsCOMPtr element; - doc->GetElementById(elementid, getter_AddRefs(element)); - - nsCOMPtr content = do_QueryInterface(element); - return content.get(); + return aPresShell->GetDocument()->GetElementById(elementid); } /* adjust the window position and size according to the mouse movement and diff --git a/layout/xul/base/src/nsXULTooltipListener.cpp b/layout/xul/base/src/nsXULTooltipListener.cpp index 261bd42803ae..545db68ecafa 100644 --- a/layout/xul/base/src/nsXULTooltipListener.cpp +++ b/layout/xul/base/src/nsXULTooltipListener.cpp @@ -608,12 +608,12 @@ nsXULTooltipListener::FindTooltip(nsIContent* aTarget, nsIContent** aTooltip) return NS_ERROR_NULL_POINTER; // before we go on, make sure that target node still has a window - nsCOMPtr document = aTarget->GetDocument(); + nsIDocument *document = aTarget->GetDocument(); if (!document) { NS_WARNING("Unable to retrieve the tooltip node document."); return NS_ERROR_FAILURE; } - nsCOMPtr window = document->GetWindow(); + nsPIDOMWindow *window = document->GetWindow(); if (!window) { return NS_OK; } @@ -650,20 +650,13 @@ nsXULTooltipListener::FindTooltip(nsIContent* aTarget, nsIContent** aTooltip) if (!tooltipId.IsEmpty()) { // tooltip must be an id, use getElementById to find it - nsCOMPtr domDocument = - do_QueryInterface(document); - if (!domDocument) { - return NS_ERROR_FAILURE; - } - - nsCOMPtr tooltipEl; - domDocument->GetElementById(tooltipId, getter_AddRefs(tooltipEl)); + nsCOMPtr tooltipEl = document->GetElementById(tooltipId); if (tooltipEl) { #ifdef MOZ_XUL mNeedTitletip = PR_FALSE; #endif - CallQueryInterface(tooltipEl, aTooltip); + *aTooltip = tooltipEl.forget().get(); return NS_OK; } } diff --git a/widget/src/cocoa/nsMenuItemX.mm b/widget/src/cocoa/nsMenuItemX.mm index 944f3940451c..e766c0eb5c21 100644 --- a/widget/src/cocoa/nsMenuItemX.mm +++ b/widget/src/cocoa/nsMenuItemX.mm @@ -48,7 +48,7 @@ #include "nsWidgetAtoms.h" #include "nsGUIEvent.h" -#include "nsIContent.h" +#include "mozilla/dom/Element.h" #include "nsIWidget.h" #include "nsIDocument.h" #include "nsIDOMDocument.h" @@ -104,20 +104,19 @@ nsresult nsMenuItemX::Create(nsMenuX* aParent, const nsString& aLabel, EMenuItem mMenuGroupOwner->RegisterForContentChanges(mContent, this); - nsCOMPtr domDoc(do_QueryInterface(mContent->GetCurrentDoc())); + nsIDocument *doc = mContent->GetCurrentDoc(); // if we have a command associated with this menu item, register for changes // to the command DOM node - if (domDoc) { + if (doc) { nsAutoString ourCommand; mContent->GetAttr(kNameSpaceID_None, nsWidgetAtoms::command, ourCommand); if (!ourCommand.IsEmpty()) { - nsCOMPtr commandElement; - domDoc->GetElementById(ourCommand, getter_AddRefs(commandElement)); + nsIContent *commandElement = doc->GetElementById(ourCommand); if (commandElement) { - mCommandContent = do_QueryInterface(commandElement); + mCommandContent = commandElement; // register to observe the command DOM element mMenuGroupOwner->RegisterForContentChanges(mCommandContent, this); } @@ -146,14 +145,12 @@ nsresult nsMenuItemX::Create(nsMenuX* aParent, const nsString& aLabel, EMenuItem nsWidgetAtoms::_true, eCaseMatters)); // Set key shortcut and modifiers - if (domDoc) { + if (doc) { nsAutoString keyValue; mContent->GetAttr(kNameSpaceID_None, nsWidgetAtoms::key, keyValue); if (!keyValue.IsEmpty()) { - nsCOMPtr keyElement; - domDoc->GetElementById(keyValue, getter_AddRefs(keyElement)); - if (keyElement) { - nsCOMPtr keyContent(do_QueryInterface(keyElement)); + nsIContent *keyContent = doc->GetElementById(keyValue); + if (keyContent) { nsAutoString keyChar(NS_LITERAL_STRING(" ")); keyContent->GetAttr(kNameSpaceID_None, nsWidgetAtoms::key, keyChar); From 99f63340e8208c426c8d30fbeaa4d2d839eda6bb Mon Sep 17 00:00:00 2001 From: Blake Kaplan Date: Wed, 23 Jun 2010 17:09:00 -0500 Subject: [PATCH 1750/1860] Fix bug 572130. r=jst --- js/src/xpconnect/src/xpcvariant.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/js/src/xpconnect/src/xpcvariant.cpp b/js/src/xpconnect/src/xpcvariant.cpp index c8f42aa596a8..9fefc33de39b 100644 --- a/js/src/xpconnect/src/xpcvariant.cpp +++ b/js/src/xpconnect/src/xpcvariant.cpp @@ -41,6 +41,7 @@ /* nsIVariant implementation for xpconnect. */ #include "xpcprivate.h" +#include "XPCWrapper.h" NS_IMPL_CYCLE_COLLECTION_CLASS(XPCVariant) @@ -431,15 +432,17 @@ XPCVariant::VariantDataToJS(XPCLazyCallContext& lccx, NS_ASSERTION(type == nsIDataType::VTYPE_INTERFACE || type == nsIDataType::VTYPE_INTERFACE_IS, "Weird variant"); - *pJSVal = realVal; - return JS_TRUE; - // else, it's an object and we really need to double wrap it if we've - // already decided that its 'natural' type is as some sort of interface. - - // We just fall through to the code below and let it do what it does. + return XPCWrapper::RewrapObject(lccx.GetJSContext(), scope, + JSVAL_TO_OBJECT(realVal), + XPCWrapper::UNKNOWN, pJSVal); } + // else, it's an object and we really need to double wrap it if we've + // already decided that its 'natural' type is as some sort of interface. + + // We just fall through to the code below and let it do what it does. + // The nsIVariant is not a XPCVariant (or we act like it isn't). // So we extract the data and do the Right Thing. From 51c55ef92be1e3669294c3b0fa226c06de81e812 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Mon, 21 Jun 2010 17:05:08 -0700 Subject: [PATCH 1751/1860] Bug 571902 - Land sync and crypto components on trunk [r=mconnor r=ted] Makefiles and build helpers for 'services'. --- services/Makefile.in | 49 +++++++++++++++++++++++++ services/crypto/Makefile.in | 56 ++++++++++++++++++++++++++++ services/makefiles.sh | 52 ++++++++++++++++++++++++++ services/sync/Makefile.in | 61 +++++++++++++++++++++++++++++++ services/sync/locales/Makefile.in | 48 ++++++++++++++++++++++++ services/sync/locales/jar.mn | 6 +++ services/sync/locales/l10n.ini | 5 +++ services/sync/tests/Makefile.in | 48 ++++++++++++++++++++++++ 8 files changed, 325 insertions(+) create mode 100644 services/Makefile.in create mode 100644 services/crypto/Makefile.in create mode 100644 services/makefiles.sh create mode 100644 services/sync/Makefile.in create mode 100644 services/sync/locales/Makefile.in create mode 100644 services/sync/locales/jar.mn create mode 100644 services/sync/locales/l10n.ini create mode 100644 services/sync/tests/Makefile.in diff --git a/services/Makefile.in b/services/Makefile.in new file mode 100644 index 000000000000..538aa50e5440 --- /dev/null +++ b/services/Makefile.in @@ -0,0 +1,49 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = .. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +ifdef MOZ_SERVICES_SYNC +PARALLEL_DIRS += crypto sync +endif + +include $(topsrcdir)/config/rules.mk diff --git a/services/crypto/Makefile.in b/services/crypto/Makefile.in new file mode 100644 index 000000000000..7be9da3ce60a --- /dev/null +++ b/services/crypto/Makefile.in @@ -0,0 +1,56 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = services-crypto +XPIDL_MODULE = services-crypto + +XPIDLSRCS = \ + IWeaveCrypto.idl \ + $(NULL) + +EXTRA_COMPONENTS = \ + WeaveCrypto.js \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/services/makefiles.sh b/services/makefiles.sh new file mode 100644 index 000000000000..7eceecaf0db5 --- /dev/null +++ b/services/makefiles.sh @@ -0,0 +1,52 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +MAKEFILES_crypto=" + services/crypto/Makefile + " + +MAKEFILES_sync=" + services/sync/Makefile + services/sync/locales/Makefile + services/sync/tests/Makefile + " + +add_makefiles " + services/Makefile + $MAKEFILES_crypto + $MAKEFILES_sync + " diff --git a/services/sync/Makefile.in b/services/sync/Makefile.in new file mode 100644 index 000000000000..ba4dbcfe6f86 --- /dev/null +++ b/services/sync/Makefile.in @@ -0,0 +1,61 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +DIRS = locales + +EXTRA_COMPONENTS = \ + FormNotifier.js \ + Weave.js \ + $(NULL) + +PREF_JS_EXPORTS = $(srcdir)/services-sync.js + +libs:: + $(PYTHON) $(topsrcdir)/config/nsinstall.py $(srcdir)/modules/* $(FINAL_TARGET)/modules/services-sync + +ifdef ENABLE_TESTS +DIRS += tests +endif + +include $(topsrcdir)/config/rules.mk diff --git a/services/sync/locales/Makefile.in b/services/sync/locales/Makefile.in new file mode 100644 index 000000000000..adc74a1013d0 --- /dev/null +++ b/services/sync/locales/Makefile.in @@ -0,0 +1,48 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = services/sync/locales + +include $(DEPTH)/config/autoconf.mk + +DEFINES += -DAB_CD=$(AB_CD) + +include $(topsrcdir)/config/rules.mk diff --git a/services/sync/locales/jar.mn b/services/sync/locales/jar.mn new file mode 100644 index 000000000000..7df67d81db13 --- /dev/null +++ b/services/sync/locales/jar.mn @@ -0,0 +1,6 @@ +#filter substitution + +@AB_CD@.jar: +% locale weave @AB_CD@ %locale/@AB_CD@/ + locale/@AB_CD@/services/errors.properties (%errors.properties) + locale/@AB_CD@/services/sync.properties (%sync.properties) diff --git a/services/sync/locales/l10n.ini b/services/sync/locales/l10n.ini new file mode 100644 index 000000000000..f9eb2b4f01af --- /dev/null +++ b/services/sync/locales/l10n.ini @@ -0,0 +1,5 @@ +[general] +depth = ../../.. + +[compare] +dirs = services/sync diff --git a/services/sync/tests/Makefile.in b/services/sync/tests/Makefile.in new file mode 100644 index 000000000000..304efdbbb1a1 --- /dev/null +++ b/services/sync/tests/Makefile.in @@ -0,0 +1,48 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = test_services_sync +XPCSHELL_TESTS = unit + +include $(topsrcdir)/config/rules.mk From dac4713e8c7a4ad70b6334ff2e439d20690df448 Mon Sep 17 00:00:00 2001 From: Philipp von Weitershausen Date: Mon, 21 Jun 2010 17:07:13 -0700 Subject: [PATCH 1752/1860] Bug 571902 - Land sync and crypto components on trunk [r=mconnor r=ted] Hook 'services' into the build system with MOZ_SERVICES_SYNC not-yet-set as a browser confvar. --- allmakefiles.sh | 3 +++ browser/confvars.sh | 1 + browser/installer/package-manifest.in | 11 +++++++++++ browser/locales/Makefile.in | 1 + browser/locales/l10n.ini | 1 + config/autoconf.mk.in | 2 ++ configure.in | 6 ++++++ toolkit/toolkit-tiers.mk | 5 +++++ 8 files changed, 30 insertions(+) diff --git a/allmakefiles.sh b/allmakefiles.sh index d35a3beff81a..5f80ab41c7d7 100755 --- a/allmakefiles.sh +++ b/allmakefiles.sh @@ -113,3 +113,6 @@ done if test -z "$LIBXUL_SDK"; then . "${srcdir}/toolkit/toolkit-makefiles.sh" fi + +# Services makefiles +. "${srcdir}/services/makefiles.sh" diff --git a/browser/confvars.sh b/browser/confvars.sh index 0906955a749e..376abfd955eb 100755 --- a/browser/confvars.sh +++ b/browser/confvars.sh @@ -45,6 +45,7 @@ MOZ_STATIC_BUILD_UNSUPPORTED=1 # always enabled for form history MOZ_MORKREADER=1 MOZ_SAFE_BROWSING=1 +MOZ_SERVICES_SYNC= MOZ_APP_VERSION=$FIREFOX_VERSION MOZ_EXTENSIONS_DEFAULT=" gnomevfs reporter" # MOZ_APP_DISPLAYNAME will be set by branding/configure.sh diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index ea795fbe91c8..a72e5cd41e8e 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -219,6 +219,9 @@ @BINPATH@/components/satchel.xpt @BINPATH@/components/saxparser.xpt @BINPATH@/components/sessionstore.xpt +#ifdef MOZ_SERVICES_SYNC +@BINPATH@/components/services-crypto.xpt +#endif @BINPATH@/components/shellservice.xpt @BINPATH@/components/shistory.xpt @BINPATH@/components/spellchecker.xpt @@ -334,6 +337,11 @@ #endif @BINPATH@/components/nsINIProcessor.js @BINPATH@/components/nsPrompter.js +#ifdef MOZ_SERVICES_SYNC +@BINPATH@/components/FormNotifier.js +@BINPATH@/components/Weave.js +@BINPATH@/components/WeaveCrypto.js +#endif ; Modules @BINPATH@/modules/* @@ -380,6 +388,9 @@ @BINPATH@/@PREF_DIR@/firefox.js @BINPATH@/@PREF_DIR@/firefox-branding.js @BINPATH@/@PREF_DIR@/channel-prefs.js +#ifdef MOZ_SERVICES_SYNC +@BINPATH@/@PREF_DIR@/services-sync.js +#endif @BINPATH@/greprefs.js @BINPATH@/defaults/autoconfig/platform.js @BINPATH@/defaults/autoconfig/prefcalls.js diff --git a/browser/locales/Makefile.in b/browser/locales/Makefile.in index 5ea86dda47e3..ebe933e7c4c7 100644 --- a/browser/locales/Makefile.in +++ b/browser/locales/Makefile.in @@ -184,6 +184,7 @@ install:: $(addsuffix .xml,$(SEARCH_PLUGINS)) libs-%: $(NSINSTALL) -D $(DIST)/install @$(MAKE) -C ../../toolkit/locales libs-$* BOTH_MANIFESTS=1 + @$(MAKE) -C ../../services/sync/locales AB_CD=$* XPI_NAME=locale-$* BOTH_MANIFESTS=1 @$(MAKE) -C ../../extensions/reporter/locales libs AB_CD=$* XPI_NAME=locale-$* BOTH_MANIFESTS=1 @$(MAKE) -C ../../extensions/spellcheck/locales AB_CD=$* XPI_NAME=locale-$* BOTH_MANIFESTS=1 @$(MAKE) libs AB_CD=$* XPI_NAME=locale-$* PREF_DIR=defaults/pref BOTH_MANIFESTS=1 diff --git a/browser/locales/l10n.ini b/browser/locales/l10n.ini index f8735bc74cf9..5f20be6973f4 100644 --- a/browser/locales/l10n.ini +++ b/browser/locales/l10n.ini @@ -11,6 +11,7 @@ dirs = browser # non-central apps might want to use %(topsrcdir)s here, or other vars # RFE: that needs to be supported by compare-locales, too, though toolkit = toolkit/locales/l10n.ini +services_sync = services/sync/locales/l10n.ini [extras] dirs = extensions/spellcheck diff --git a/config/autoconf.mk.in b/config/autoconf.mk.in index e0ae4fb2f374..17fdffeabea0 100644 --- a/config/autoconf.mk.in +++ b/config/autoconf.mk.in @@ -657,6 +657,8 @@ MOZ_SPLASHSCREEN = @MOZ_SPLASHSCREEN@ MOZ_THEME_FASTSTRIPE = @MOZ_THEME_FASTSTRIPE@ +MOZ_SERVICES_SYNC = @MOZ_SERVICES_SYNC@ + MOZ_OFFICIAL_BRANDING = @MOZ_OFFICIAL_BRANDING@ HAVE_CLOCK_MONOTONIC = @HAVE_CLOCK_MONOTONIC@ diff --git a/configure.in b/configure.in index aae4e3c0d3b3..8f931bc5b28e 100644 --- a/configure.in +++ b/configure.in @@ -8618,6 +8618,12 @@ if test "$MOZ_PLACES"; then AC_DEFINE(MOZ_PLACES) fi +dnl Build Sync Services if required +AC_SUBST(MOZ_SERVICES_SYNC) +if test -n "$MOZ_SERVICES_SYNC"; then + AC_DEFINE(MOZ_SERVICES_SYNC) +fi + dnl ======================================================== if test "$MOZ_DEBUG" || test "$NS_TRACE_MALLOC"; then MOZ_COMPONENTS_VERSION_SCRIPT_LDFLAGS= diff --git a/toolkit/toolkit-tiers.mk b/toolkit/toolkit-tiers.mk index 3a5d33014da0..53f8f336f6b1 100644 --- a/toolkit/toolkit-tiers.mk +++ b/toolkit/toolkit-tiers.mk @@ -271,6 +271,11 @@ ifdef MOZ_MAPINFO tier_platform_dirs += tools/codesighs endif +ifdef MOZ_SERVICES_SYNC +tier_platform_dirs += services/crypto +tier_platform_dirs += services/sync +endif + ifdef ENABLE_TESTS tier_platform_dirs += testing/mochitest tier_platform_dirs += testing/xpcshell From 52e8537cd4c7f293f7429e68a859ddb709652762 Mon Sep 17 00:00:00 2001 From: Jonathan Kew Date: Wed, 23 Jun 2010 19:38:04 -0400 Subject: [PATCH 1753/1860] Bug 569531 - Enable harfbuzz by default for simple scripts on Mac OS X only; r=roc --- modules/libpref/src/init/all.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 3d65cd201b8d..11dd63708ccb 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -177,7 +177,11 @@ pref("gfx.color_management.rendering_intent", 0); pref("gfx.downloadable_fonts.enabled", true); +#ifdef XP_MACOSX +pref("gfx.font_rendering.harfbuzz.level", 1); +#else pref("gfx.font_rendering.harfbuzz.level", 0); +#endif #ifdef XP_WIN #ifndef WINCE From 0b6f34019e7bd611ef6f2dd52e99c716a0077d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Corr=C3=AAa=20da=20Silva=20Sanches?= Date: Wed, 23 Jun 2010 17:30:55 -0700 Subject: [PATCH 1754/1860] Bug 554704: Rename "aCanCache" param (for nsISMILAttr::ValueFromString), r=dholbert, r=jwatt --- content/smil/nsISMILAttr.h | 11 ++--- content/smil/nsSMILAnimationFunction.cpp | 40 +++++++++++-------- content/smil/nsSMILAnimationFunction.h | 3 +- content/smil/nsSMILCSSProperty.cpp | 13 +++--- content/smil/nsSMILCSSProperty.h | 2 +- content/smil/nsSMILMappedAttribute.cpp | 4 +- content/smil/nsSMILMappedAttribute.h | 2 +- content/smil/nsSMILParserUtils.cpp | 22 +++++----- content/smil/nsSMILParserUtils.h | 2 +- content/svg/content/src/SVGMotionSMILAttr.cpp | 2 +- content/svg/content/src/SVGMotionSMILAttr.h | 2 +- content/svg/content/src/nsSVGAngle.cpp | 4 +- content/svg/content/src/nsSVGAngle.h | 2 +- content/svg/content/src/nsSVGBoolean.cpp | 4 +- content/svg/content/src/nsSVGBoolean.h | 2 +- content/svg/content/src/nsSVGEnum.cpp | 4 +- content/svg/content/src/nsSVGEnum.h | 2 +- content/svg/content/src/nsSVGInteger.cpp | 4 +- content/svg/content/src/nsSVGInteger.h | 2 +- content/svg/content/src/nsSVGLength2.cpp | 9 +++-- content/svg/content/src/nsSVGLength2.h | 2 +- content/svg/content/src/nsSVGNumber2.cpp | 4 +- content/svg/content/src/nsSVGNumber2.h | 2 +- .../content/src/nsSVGPreserveAspectRatio.cpp | 4 +- .../content/src/nsSVGPreserveAspectRatio.h | 2 +- .../content/src/nsSVGTransformSMILAttr.cpp | 4 +- .../svg/content/src/nsSVGTransformSMILAttr.h | 2 +- content/svg/content/src/nsSVGViewBox.cpp | 4 +- content/svg/content/src/nsSVGViewBox.h | 2 +- 29 files changed, 86 insertions(+), 76 deletions(-) diff --git a/content/smil/nsISMILAttr.h b/content/smil/nsISMILAttr.h index 3f9d60be5aed..0904d0216eb3 100644 --- a/content/smil/nsISMILAttr.h +++ b/content/smil/nsISMILAttr.h @@ -69,16 +69,17 @@ public: * animateTransform where the 'type' attribute is needed to * parse the value. * @param[out] aValue Outparam for storing the parsed value. - * @param[out] aCanCache Outparam for indicating whether the parsed value - * can be reused in future samples -- i.e. whether the - * given string is always guaranteed to compute - * to the same nsSMILValue. + * @param[out] aPreventCachingOfSandwich + * Outparam to indicate whether the attribute contains + * dependencies on its context that should prevent the + * result of the animation sandwich from being cached and + * reused in future samples. * @return NS_OK on success or an error code if creation failed. */ virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const = 0; + PRBool& aPreventCachingOfSandwich) const = 0; /** * Gets the underlying value of this attribute. diff --git a/content/smil/nsSMILAnimationFunction.cpp b/content/smil/nsSMILAnimationFunction.cpp index d47bf8847f81..c85bc0c6a2ba 100644 --- a/content/smil/nsSMILAnimationFunction.cpp +++ b/content/smil/nsSMILAnimationFunction.cpp @@ -694,10 +694,12 @@ nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName, nsAString& aResult) const * @param aAttName The attribute name (in the global namespace). * @param aSMILAttr The SMIL attribute to perform the parsing. * @param[out] aResult The resulting nsSMILValue. - * @param[out] aCanCacheSoFar If |aResult| cannot be cached (as reported by - * nsISMILAttr::ValueFromString), then this outparam - * will be set to PR_FALSE. Otherwise, this outparam - * won't be modified. + * @param[out] aPreventCachingOfSandwich + * If |aResult| contains dependencies on its context that + * should prevent the result of the animation sandwich from + * being cached and reused in future samples (as reported + * by nsISMILAttr::ValueFromString), then this outparam + * will be set to PR_TRUE. Otherwise it is left unmodified. * * Returns PR_FALSE if a parse error occurred, otherwise returns PR_TRUE. */ @@ -705,18 +707,18 @@ PRBool nsSMILAnimationFunction::ParseAttr(nsIAtom* aAttName, const nsISMILAttr& aSMILAttr, nsSMILValue& aResult, - PRBool& aCanCacheSoFar) const + PRBool& aPreventCachingOfSandwich) const { nsAutoString attValue; if (GetAttr(aAttName, attValue)) { - PRBool canCache; + PRBool preventCachingOfSandwich; nsresult rv = aSMILAttr.ValueFromString(attValue, mAnimationElement, - aResult, canCache); + aResult, preventCachingOfSandwich); if (NS_FAILED(rv)) return PR_FALSE; - if (!canCache) { - aCanCacheSoFar = PR_FALSE; + if (preventCachingOfSandwich) { + aPreventCachingOfSandwich = PR_TRUE; } } return PR_TRUE; @@ -750,25 +752,29 @@ nsSMILAnimationFunction::GetValues(const nsISMILAttr& aSMILAttr, if (HasAttr(nsGkAtoms::values)) { nsAutoString attValue; GetAttr(nsGkAtoms::values, attValue); - PRBool canCache; + PRBool preventCachingOfSandwich; nsresult rv = nsSMILParserUtils::ParseValues(attValue, mAnimationElement, - aSMILAttr, result, canCache); + aSMILAttr, result, + preventCachingOfSandwich); if (NS_FAILED(rv)) return rv; - if (!canCache) { + if (preventCachingOfSandwich) { mValueNeedsReparsingEverySample = PR_TRUE; } // Else try to/from/by } else { - PRBool canCacheSoFar = PR_TRUE; + PRBool preventCachingOfSandwich = PR_FALSE; PRBool parseOk = PR_TRUE; nsSMILValue to, from, by; - parseOk &= ParseAttr(nsGkAtoms::to, aSMILAttr, to, canCacheSoFar); - parseOk &= ParseAttr(nsGkAtoms::from, aSMILAttr, from, canCacheSoFar); - parseOk &= ParseAttr(nsGkAtoms::by, aSMILAttr, by, canCacheSoFar); + parseOk &= ParseAttr(nsGkAtoms::to, aSMILAttr, to, + preventCachingOfSandwich); + parseOk &= ParseAttr(nsGkAtoms::from, aSMILAttr, from, + preventCachingOfSandwich); + parseOk &= ParseAttr(nsGkAtoms::by, aSMILAttr, by, + preventCachingOfSandwich); - if (!canCacheSoFar) { + if (preventCachingOfSandwich) { mValueNeedsReparsingEverySample = PR_TRUE; } diff --git a/content/smil/nsSMILAnimationFunction.h b/content/smil/nsSMILAnimationFunction.h index d43844dafe01..7f51b123db07 100644 --- a/content/smil/nsSMILAnimationFunction.h +++ b/content/smil/nsSMILAnimationFunction.h @@ -321,7 +321,8 @@ protected: nsAString& aResult) const; PRBool ParseAttr(nsIAtom* aAttName, const nsISMILAttr& aSMILAttr, - nsSMILValue& aResult, PRBool& aCanCacheSoFar) const; + nsSMILValue& aResult, + PRBool& aPreventCachingOfSandwich) const; virtual nsresult GetValues(const nsISMILAttr& aSMILAttr, nsSMILValueArray& aResult); diff --git a/content/smil/nsSMILCSSProperty.cpp b/content/smil/nsSMILCSSProperty.cpp index b93a5ac9b40f..b9cdf3dd103b 100644 --- a/content/smil/nsSMILCSSProperty.cpp +++ b/content/smil/nsSMILCSSProperty.cpp @@ -155,7 +155,7 @@ nsresult nsSMILCSSProperty::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); @@ -168,11 +168,12 @@ nsSMILCSSProperty::ValueFromString(const nsAString& aStr, // reparsed every sample. This prevents us from doing the "nothing's changed // so don't recompose" optimization (bug 533291) for CSS properties & mapped // attributes. If it ends up being expensive to always recompose those, we - // can be a little smarter here. We really only need to disable aCanCache - // for "inherit" & "currentColor" (whose values could change at any time), as - // well as for length-valued types (particularly those with em/ex/percent - // units, since their conversion ratios can change at any time). - aCanCache = PR_FALSE; + // can be a little smarter here. We really only need to set + // aPreventCachingOfSandwich to true for "inherit" & "currentColor" (whose + // values could change at any time), for length-valued types (particularly + // those with em/ex/percent units, since their conversion ratios can change + // at any time), and for any value for 'font-family'. + aPreventCachingOfSandwich = PR_TRUE; return NS_OK; } diff --git a/content/smil/nsSMILCSSProperty.h b/content/smil/nsSMILCSSProperty.h index 26929d8c07cb..c7c5ae62c9f3 100644 --- a/content/smil/nsSMILCSSProperty.h +++ b/content/smil/nsSMILCSSProperty.h @@ -73,7 +73,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual nsresult SetAnimValue(const nsSMILValue& aValue); virtual void ClearAnimValue(); diff --git a/content/smil/nsSMILMappedAttribute.cpp b/content/smil/nsSMILMappedAttribute.cpp index 859d877f4d29..bff6852faa33 100644 --- a/content/smil/nsSMILMappedAttribute.cpp +++ b/content/smil/nsSMILMappedAttribute.cpp @@ -62,7 +62,7 @@ nsresult nsSMILMappedAttribute::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); @@ -73,7 +73,7 @@ nsSMILMappedAttribute::ValueFromString(const nsAString& aStr, // XXXdholbert: For simplicity, just assume that all CSS values have to // reparsed every sample. See note in nsSMILCSSProperty::ValueFromString. - aCanCache = PR_FALSE; + aPreventCachingOfSandwich = PR_TRUE; return NS_OK; } diff --git a/content/smil/nsSMILMappedAttribute.h b/content/smil/nsSMILMappedAttribute.h index 7661db3f4a11..b6f04a9e318d 100644 --- a/content/smil/nsSMILMappedAttribute.h +++ b/content/smil/nsSMILMappedAttribute.h @@ -73,7 +73,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual nsresult SetAnimValue(const nsSMILValue& aValue); virtual void ClearAnimValue(); diff --git a/content/smil/nsSMILParserUtils.cpp b/content/smil/nsSMILParserUtils.cpp index f5e452358b98..4eb0bf9a3746 100644 --- a/content/smil/nsSMILParserUtils.cpp +++ b/content/smil/nsSMILParserUtils.cpp @@ -548,26 +548,26 @@ public: SMILValueParser(const nsISMILAnimationElement* aSrcElement, const nsISMILAttr* aSMILAttr, nsTArray* aValuesArray, - PRBool* aCanCache) : + PRBool* aPreventCachingOfSandwich) : mSrcElement(aSrcElement), mSMILAttr(aSMILAttr), mValuesArray(aValuesArray), - mCanCache(aCanCache) + mPreventCachingOfSandwich(aPreventCachingOfSandwich) {} virtual nsresult Parse(const nsAString& aValueStr) { nsSMILValue newValue; - PRBool tmpCanCache; - nsresult rv = mSMILAttr->ValueFromString(aValueStr, mSrcElement, - newValue, tmpCanCache); + PRBool tmpPreventCachingOfSandwich; + nsresult rv = mSMILAttr->ValueFromString(aValueStr, mSrcElement, newValue, + tmpPreventCachingOfSandwich); if (NS_FAILED(rv)) return rv; if (!mValuesArray->AppendElement(newValue)) { return NS_ERROR_OUT_OF_MEMORY; } - if (!tmpCanCache) { - *mCanCache = PR_FALSE; + if (tmpPreventCachingOfSandwich) { + *mPreventCachingOfSandwich = PR_TRUE; } return NS_OK; } @@ -575,7 +575,7 @@ protected: const nsISMILAnimationElement* mSrcElement; const nsISMILAttr* mSMILAttr; nsTArray* mValuesArray; - PRBool* mCanCache; + PRBool* mPreventCachingOfSandwich; }; nsresult @@ -583,12 +583,12 @@ nsSMILParserUtils::ParseValues(const nsAString& aSpec, const nsISMILAnimationElement* aSrcElement, const nsISMILAttr& aAttribute, nsTArray& aValuesArray, - PRBool& aCanCache) + PRBool& aPreventCachingOfSandwich) { // Assume all results can be cached, until we find one that can't. - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; SMILValueParser valueParser(aSrcElement, &aAttribute, - &aValuesArray, &aCanCache); + &aValuesArray, &aPreventCachingOfSandwich); return ParseValuesGeneric(aSpec, valueParser); } diff --git a/content/smil/nsSMILParserUtils.h b/content/smil/nsSMILParserUtils.h index ec5915640d90..99119a655d8b 100644 --- a/content/smil/nsSMILParserUtils.h +++ b/content/smil/nsSMILParserUtils.h @@ -75,7 +75,7 @@ public: const nsISMILAnimationElement* aSrcElement, const nsISMILAttr& aAttribute, nsTArray& aValuesArray, - PRBool& aCanCache); + PRBool& aPreventCachingOfSandwich); // Generic method that will run some code on each sub-section of an animation // element's "values" list. diff --git a/content/svg/content/src/SVGMotionSMILAttr.cpp b/content/svg/content/src/SVGMotionSMILAttr.cpp index 8cbff9725484..7b1090efafaf 100644 --- a/content/svg/content/src/SVGMotionSMILAttr.cpp +++ b/content/svg/content/src/SVGMotionSMILAttr.cpp @@ -54,7 +54,7 @@ nsresult SVGMotionSMILAttr::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { NS_NOTREACHED("Shouldn't using nsISMILAttr::ValueFromString for parsing " "animateMotion's SMIL values."); diff --git a/content/svg/content/src/SVGMotionSMILAttr.h b/content/svg/content/src/SVGMotionSMILAttr.h index eabccec924f2..38fe036e6fdc 100644 --- a/content/svg/content/src/SVGMotionSMILAttr.h +++ b/content/svg/content/src/SVGMotionSMILAttr.h @@ -62,7 +62,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual nsresult SetAnimValue(const nsSMILValue& aValue); virtual void ClearAnimValue(); diff --git a/content/svg/content/src/nsSVGAngle.cpp b/content/svg/content/src/nsSVGAngle.cpp index b9994d5c433d..a02377ace4d8 100644 --- a/content/svg/content/src/nsSVGAngle.cpp +++ b/content/svg/content/src/nsSVGAngle.cpp @@ -440,7 +440,7 @@ nsresult nsSVGAngle::SMILOrient::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* /*aSrcElement*/, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { nsSMILValue val(&SVGOrientSMILType::sSingleton); if (aStr.EqualsLiteral("auto")) { @@ -457,7 +457,7 @@ nsSVGAngle::SMILOrient::ValueFromString(const nsAString& aStr, val.mU.mOrient.mOrientType = nsIDOMSVGMarkerElement::SVG_MARKER_ORIENT_ANGLE; } aValue.Swap(val); - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; return NS_OK; } diff --git a/content/svg/content/src/nsSVGAngle.h b/content/svg/content/src/nsSVGAngle.h index 0210919673bd..0e5287b10aa0 100644 --- a/content/svg/content/src/nsSVGAngle.h +++ b/content/svg/content/src/nsSVGAngle.h @@ -226,7 +226,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); diff --git a/content/svg/content/src/nsSVGBoolean.cpp b/content/svg/content/src/nsSVGBoolean.cpp index 7371fb8b454e..4e97268ae17a 100644 --- a/content/svg/content/src/nsSVGBoolean.cpp +++ b/content/svg/content/src/nsSVGBoolean.cpp @@ -146,7 +146,7 @@ nsresult nsSVGBoolean::SMILBool::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* /*aSrcElement*/, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { nsSMILValue val(&SMILBoolType::sSingleton); @@ -158,7 +158,7 @@ nsSVGBoolean::SMILBool::ValueFromString(const nsAString& aStr, return NS_ERROR_FAILURE; aValue = val; - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; return NS_OK; } diff --git a/content/svg/content/src/nsSVGBoolean.h b/content/svg/content/src/nsSVGBoolean.h index f34174c6d238..f6593636367f 100644 --- a/content/svg/content/src/nsSVGBoolean.h +++ b/content/svg/content/src/nsSVGBoolean.h @@ -124,7 +124,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); diff --git a/content/svg/content/src/nsSVGEnum.cpp b/content/svg/content/src/nsSVGEnum.cpp index 3625e208fd4a..8abfe2a3d3b1 100644 --- a/content/svg/content/src/nsSVGEnum.cpp +++ b/content/svg/content/src/nsSVGEnum.cpp @@ -177,7 +177,7 @@ nsresult nsSVGEnum::SMILEnum::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* /*aSrcElement*/, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { nsCOMPtr valAtom = do_GetAtom(aStr); nsSVGEnumMapping *mapping = mVal->GetMapping(mSVGElement); @@ -187,7 +187,7 @@ nsSVGEnum::SMILEnum::ValueFromString(const nsAString& aStr, nsSMILValue val(&SMILEnumType::sSingleton); val.mU.mUint = mapping->mVal; aValue = val; - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; return NS_OK; } mapping++; diff --git a/content/svg/content/src/nsSVGEnum.h b/content/svg/content/src/nsSVGEnum.h index a408c77087fa..0926a4d8ce6a 100644 --- a/content/svg/content/src/nsSVGEnum.h +++ b/content/svg/content/src/nsSVGEnum.h @@ -134,7 +134,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); diff --git a/content/svg/content/src/nsSVGInteger.cpp b/content/svg/content/src/nsSVGInteger.cpp index 9f0b154ab07a..5261b8f69216 100644 --- a/content/svg/content/src/nsSVGInteger.cpp +++ b/content/svg/content/src/nsSVGInteger.cpp @@ -140,7 +140,7 @@ nsresult nsSVGInteger::SMILInteger::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* /*aSrcElement*/, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { NS_ConvertUTF16toUTF8 value(aStr); const char *str = value.get(); @@ -157,7 +157,7 @@ nsSVGInteger::SMILInteger::ValueFromString(const nsAString& aStr, nsSMILValue smilVal(&SMILIntegerType::sSingleton); smilVal.mU.mInt = val; aValue = smilVal; - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; return NS_OK; } diff --git a/content/svg/content/src/nsSVGInteger.h b/content/svg/content/src/nsSVGInteger.h index 165a23fdaf9d..119a347b1721 100644 --- a/content/svg/content/src/nsSVGInteger.h +++ b/content/svg/content/src/nsSVGInteger.h @@ -124,7 +124,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); diff --git a/content/svg/content/src/nsSVGLength2.cpp b/content/svg/content/src/nsSVGLength2.cpp index 9760dc8d5566..f2be479131c6 100644 --- a/content/svg/content/src/nsSVGLength2.cpp +++ b/content/svg/content/src/nsSVGLength2.cpp @@ -522,7 +522,7 @@ nsresult nsSVGLength2::SMILLength::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* /*aSrcElement*/, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { float value; PRUint16 unitType; @@ -535,9 +535,10 @@ nsSVGLength2::SMILLength::ValueFromString(const nsAString& aStr, nsSMILValue val(&nsSMILFloatType::sSingleton); val.mU.mDouble = value / mVal->GetUnitScaleFactor(mSVGElement, unitType); aValue = val; - aCanCache = (unitType != nsIDOMSVGLength::SVG_LENGTHTYPE_PERCENTAGE && - unitType != nsIDOMSVGLength::SVG_LENGTHTYPE_EMS && - unitType != nsIDOMSVGLength::SVG_LENGTHTYPE_EXS); + aPreventCachingOfSandwich = + (unitType == nsIDOMSVGLength::SVG_LENGTHTYPE_PERCENTAGE || + unitType == nsIDOMSVGLength::SVG_LENGTHTYPE_EMS || + unitType == nsIDOMSVGLength::SVG_LENGTHTYPE_EXS); return NS_OK; } diff --git a/content/svg/content/src/nsSVGLength2.h b/content/svg/content/src/nsSVGLength2.h index 24f233b3da84..05172e2e0271 100644 --- a/content/svg/content/src/nsSVGLength2.h +++ b/content/svg/content/src/nsSVGLength2.h @@ -282,7 +282,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue &aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); diff --git a/content/svg/content/src/nsSVGNumber2.cpp b/content/svg/content/src/nsSVGNumber2.cpp index bc2fbdaae132..c2342806f59d 100644 --- a/content/svg/content/src/nsSVGNumber2.cpp +++ b/content/svg/content/src/nsSVGNumber2.cpp @@ -175,7 +175,7 @@ nsresult nsSVGNumber2::SMILNumber::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* /*aSrcElement*/, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { float value; @@ -187,7 +187,7 @@ nsSVGNumber2::SMILNumber::ValueFromString(const nsAString& aStr, nsSMILValue val(&nsSMILFloatType::sSingleton); val.mU.mDouble = value; aValue = val; - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; return NS_OK; } diff --git a/content/svg/content/src/nsSVGNumber2.h b/content/svg/content/src/nsSVGNumber2.h index 21bd7e8e38b3..cd078adcd723 100644 --- a/content/svg/content/src/nsSVGNumber2.h +++ b/content/svg/content/src/nsSVGNumber2.h @@ -134,7 +134,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); diff --git a/content/svg/content/src/nsSVGPreserveAspectRatio.cpp b/content/svg/content/src/nsSVGPreserveAspectRatio.cpp index 61666bdf1a0f..02b64753a8e4 100644 --- a/content/svg/content/src/nsSVGPreserveAspectRatio.cpp +++ b/content/svg/content/src/nsSVGPreserveAspectRatio.cpp @@ -350,7 +350,7 @@ nsSVGPreserveAspectRatio::SMILPreserveAspectRatio ::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* /*aSrcElement*/, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { PreserveAspectRatio par; nsresult res = ToPreserveAspectRatio(aStr, &par); @@ -359,7 +359,7 @@ nsSVGPreserveAspectRatio::SMILPreserveAspectRatio nsSMILValue val(&SMILEnumType::sSingleton); val.mU.mUint = PackPreserveAspectRatio(par); aValue = val; - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; return NS_OK; } diff --git a/content/svg/content/src/nsSVGPreserveAspectRatio.h b/content/svg/content/src/nsSVGPreserveAspectRatio.h index 72dbab8d9e83..a3e31e5133c5 100644 --- a/content/svg/content/src/nsSVGPreserveAspectRatio.h +++ b/content/svg/content/src/nsSVGPreserveAspectRatio.h @@ -225,7 +225,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); diff --git a/content/svg/content/src/nsSVGTransformSMILAttr.cpp b/content/svg/content/src/nsSVGTransformSMILAttr.cpp index 58df66ba54e1..7b744c55deaa 100644 --- a/content/svg/content/src/nsSVGTransformSMILAttr.cpp +++ b/content/svg/content/src/nsSVGTransformSMILAttr.cpp @@ -55,7 +55,7 @@ nsresult nsSVGTransformSMILAttr::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { NS_ENSURE_TRUE(aSrcElement, NS_ERROR_FAILURE); NS_ASSERTION(aValue.IsNull(), @@ -67,7 +67,7 @@ nsSVGTransformSMILAttr::ValueFromString(const nsAString& aStr, : nsGkAtoms::translate; ParseValue(aStr, transformType, aValue); - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; return aValue.IsNull() ? NS_ERROR_FAILURE : NS_OK; } diff --git a/content/svg/content/src/nsSVGTransformSMILAttr.h b/content/svg/content/src/nsSVGTransformSMILAttr.h index 4ff439016f38..feb1b53d8422 100644 --- a/content/svg/content/src/nsSVGTransformSMILAttr.h +++ b/content/svg/content/src/nsSVGTransformSMILAttr.h @@ -60,7 +60,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); diff --git a/content/svg/content/src/nsSVGViewBox.cpp b/content/svg/content/src/nsSVGViewBox.cpp index 12c6c6b7f092..aeef51c17979 100644 --- a/content/svg/content/src/nsSVGViewBox.cpp +++ b/content/svg/content/src/nsSVGViewBox.cpp @@ -282,7 +282,7 @@ nsSVGViewBox::SMILViewBox ::ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* /*aSrcElement*/, nsSMILValue& aValue, - PRBool& aCanCache) const + PRBool& aPreventCachingOfSandwich) const { nsSVGViewBoxRect viewBox; nsresult res = ToSVGViewBoxRect(aStr, &viewBox); @@ -292,7 +292,7 @@ nsSVGViewBox::SMILViewBox nsSMILValue val(&SVGViewBoxSMILType::sSingleton); *static_cast(val.mU.mPtr) = viewBox; aValue.Swap(val); - aCanCache = PR_TRUE; + aPreventCachingOfSandwich = PR_FALSE; return NS_OK; } diff --git a/content/svg/content/src/nsSVGViewBox.h b/content/svg/content/src/nsSVGViewBox.h index 05c328f741a9..f636c5c7ec9e 100644 --- a/content/svg/content/src/nsSVGViewBox.h +++ b/content/svg/content/src/nsSVGViewBox.h @@ -211,7 +211,7 @@ public: virtual nsresult ValueFromString(const nsAString& aStr, const nsISMILAnimationElement* aSrcElement, nsSMILValue& aValue, - PRBool& aCanCache) const; + PRBool& aPreventCachingOfSandwich) const; virtual nsSMILValue GetBaseValue() const; virtual void ClearAnimValue(); virtual nsresult SetAnimValue(const nsSMILValue& aValue); From 18cc6a03e8ba5d9bf6d1acde4ce10b36d610d2bc Mon Sep 17 00:00:00 2001 From: Daniel Holbert Date: Wed, 23 Jun 2010 17:30:57 -0700 Subject: [PATCH 1755/1860] Bug 574049: Remove unused variable 'tabChildC'. r=smaug --- content/base/src/nsInProcessTabChildGlobal.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/content/base/src/nsInProcessTabChildGlobal.cpp b/content/base/src/nsInProcessTabChildGlobal.cpp index d2e333821c04..2784152826a5 100644 --- a/content/base/src/nsInProcessTabChildGlobal.cpp +++ b/content/base/src/nsInProcessTabChildGlobal.cpp @@ -107,7 +107,6 @@ bool SendAsyncMessageToParent(void* aCallbackData, return true; } -static int tabChildC = 0; nsInProcessTabChildGlobal::nsInProcessTabChildGlobal(nsIDocShell* aShell, nsIContent* aOwner, nsFrameMessageManager* aChrome) From 107505b8afdd01b2e1dc3da58501840ec4f677cf Mon Sep 17 00:00:00 2001 From: Daniel Holbert Date: Wed, 23 Jun 2010 17:30:59 -0700 Subject: [PATCH 1756/1860] Bug 573530: Remove unused variable in nsXPComInit.cpp. r=bsmedberg --- xpcom/build/nsXPComInit.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xpcom/build/nsXPComInit.cpp b/xpcom/build/nsXPComInit.cpp index a95360c187f2..75a0c25d18da 100644 --- a/xpcom/build/nsXPComInit.cpp +++ b/xpcom/build/nsXPComInit.cpp @@ -697,8 +697,8 @@ NS_InitXPCOM3(nsIServiceManager* *result, NS_TIME_FUNCTION_MARK("Next: interface info manager init"); // The iimanager constructor searches and registers XPT files. - nsIInterfaceInfoManager* iim = - xptiInterfaceInfoManager::GetSingleton(); + // (We trigger the singleton's lazy construction here to make that happen.) + (void) xptiInterfaceInfoManager::GetSingleton(); NS_TIME_FUNCTION_MARK("Next: try to load compreg.dat"); From c5029b351a034add5496712b705df38accef01c5 Mon Sep 17 00:00:00 2001 From: Justin Dolske Date: Wed, 23 Jun 2010 17:54:05 -0700 Subject: [PATCH 1757/1860] Bug 573808 - "domWin is null" error with new prompting backend and no active window. r=gavin --- toolkit/components/prompts/src/nsPrompter.js | 17 ++----------- .../prompts/test/test_modal_prompts.html | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/toolkit/components/prompts/src/nsPrompter.js b/toolkit/components/prompts/src/nsPrompter.js index 19cfa7254b3a..8f6ac6e667b0 100644 --- a/toolkit/components/prompts/src/nsPrompter.js +++ b/toolkit/components/prompts/src/nsPrompter.js @@ -204,18 +204,6 @@ let PromptUtils = { return [buttonLabels[0], buttonLabels[1], buttonLabels[2], defaultButtonNum, isDelayEnabled]; }, - // Returns true if some listener requested that the default action be prevented. - fireEvent : function (domWin, eventType) { - // XXX main use of DOMWillOpenModalDialog is so tabbrowser can focus - // the tab opening the modal prompt. DOMModalDialogClosed is - // unused (until bug 429287). - // XXX Maybe make these observer notifications instead? - // oh, content can see these? That seems unfortunate. Esp. for auth. - let event = domWin.document.createEvent("Events"); - event.initEvent(eventType, true, true); - return !domWin.dispatchEvent(event); - }, - getAuthInfo : function (authInfo) { let username, password; @@ -384,13 +372,12 @@ ModalPrompter.prototype = { if (!domWin) domWin = Services.ww.activeWindow; + // XXX domWin may still be null here if there are _no_ windows open. + // Note that we don't need to fire DOMWillOpenModalDialog and // DOMModalDialogClosed events here, wwatcher's OpenWindowJSInternal // will do that. Similarly for enterModalState / leaveModalState. - let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - Services.ww.openWindow(domWin, uri, "_blank", "centerscreen,chrome,modal,titlebar", args); }, diff --git a/toolkit/components/prompts/test/test_modal_prompts.html b/toolkit/components/prompts/test/test_modal_prompts.html index 4299bd4fccf3..b12015a91041 100644 --- a/toolkit/components/prompts/test/test_modal_prompts.html +++ b/toolkit/components/prompts/test/test_modal_prompts.html @@ -637,6 +637,24 @@ function handleDialog(doc, testNum) { clickOK = false; break; + case 29: + // Alert, null window + state = { + msg : "This is the alert text.", + title : "TestTitle", + iconClass : "alert-icon", + textHidden : true, + passHidden : true, + checkHidden : true, + textValue : "", + passValue : "", + checkMsg : "", + checked : false, + }; + checkExpectedState(doc, state); + break; + + case 100: // PromptAuth (no realm, ok, with checkbox) state = { @@ -1023,6 +1041,13 @@ is(clickedButton, 2, "checked expected button num click"); is(checkVal.value, true, "expected checkbox setting"); ok(didDialog, "handleDialog was invoked"); +// ===== test 29 ===== +// Alert, no window +testNum++; +startCallbackTimer(); +prompter.alert(null, "TestTitle", "This is the alert text."); +ok(didDialog, "handleDialog was invoked"); + // promptAuth already tested via password manager but do a few specific things here. From ce4b68edf85f53a6e3402bb0743eb4be5042e7b0 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 23 Jun 2010 22:52:48 -0500 Subject: [PATCH 1758/1860] Followup to bug 572939: Use X11Util and the already_AddRefed type coercion operator in GLContextProviderGLX and fix a potential leak-on-error. r=karlt --- gfx/public/X11Util.h | 21 ++++++--- gfx/thebes/src/GLContextProviderGLX.cpp | 62 +++++++++---------------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/gfx/public/X11Util.h b/gfx/public/X11Util.h index 4983c806009d..3cb4bed8d990 100644 --- a/gfx/public/X11Util.h +++ b/gfx/public/X11Util.h @@ -53,6 +53,8 @@ # error Unknown toolkit #endif +#include "nsDebug.h" + namespace mozilla { /** @@ -75,20 +77,27 @@ DefaultXDisplay() template struct ScopedXFree { + ScopedXFree() : mPtr(NULL) {} ScopedXFree(T* aPtr) : mPtr(aPtr) {} - ~ScopedXFree() - { - if (mPtr) - XFree(mPtr); - } + ~ScopedXFree() { Assign(NULL); } + + ScopedXFree& operator=(T* aPtr) { Assign(aPtr); return *this; } operator T*() const { return get(); } T* operator->() const { return get(); } T* get() const { return mPtr; } - private: + void Assign(T* aPtr) + { + NS_ASSERTION(!mPtr || mPtr != aPtr, "double-XFree() imminent"); + + if (mPtr) + XFree(mPtr); + mPtr = aPtr; + } + T* mPtr; // disable these diff --git a/gfx/thebes/src/GLContextProviderGLX.cpp b/gfx/thebes/src/GLContextProviderGLX.cpp index 6d3e13c6ed37..faf9f6c5ef32 100644 --- a/gfx/thebes/src/GLContextProviderGLX.cpp +++ b/gfx/thebes/src/GLContextProviderGLX.cpp @@ -37,20 +37,18 @@ #ifdef MOZ_WIDGET_GTK2 #include #include -// we're using default display for now #define GET_NATIVE_WINDOW(aWidget) GDK_WINDOW_XID((GdkWindow *) aWidget->GetNativeData(NS_NATIVE_WINDOW)) -#define DISPLAY gdk_x11_get_default_xdisplay #elif defined(MOZ_WIDGET_QT) #include #include -// we're using default display for now #define GET_NATIVE_WINDOW(aWidget) static_cast(aWidget->GetNativeData(NS_NATIVE_SHELLWIDGET))->handle() -#define DISPLAY QX11Info().display #endif #include #include +#include "mozilla/X11Util.h" + #include "GLContextProvider.h" #include "nsDebug.h" #include "nsIWidget.h" @@ -116,7 +114,8 @@ ctxErrorHandler(Display *dpy, XErrorEvent *ev) class GLContextGLX : public GLContext { public: - static GLContextGLX *CreateGLContext(Display *display, GLXDrawable drawable, GLXFBConfig cfg, PRBool pbuffer) + static already_AddRefed + CreateGLContext(Display *display, GLXDrawable drawable, GLXFBConfig cfg, PRBool pbuffer) { int db = 0, err; err = sGLXLibrary.xGetFBConfigAttrib(display, cfg, @@ -144,16 +143,16 @@ public: return nsnull; } - GLContextGLX *glContext = new GLContextGLX(display, - drawable, - context, - pbuffer, - db); + nsRefPtr glContext(new GLContextGLX(display, + drawable, + context, + pbuffer, + db)); if (!glContext->Init()) { return nsnull; } - return glContext; + return glContext.forget(); } ~GLContextGLX() @@ -273,7 +272,7 @@ GLContextProvider::CreateForWindow(nsIWidget *aWidget) const char *vendor = sGLXLibrary.xQueryServerString(display, xscreen, GLX_VENDOR); PRBool isATI = vendor && strstr(vendor, "ATI"); int numConfigs; - GLXFBConfig *cfgs; + ScopedXFree cfgs; if (isATI) { const int attribs[] = { GLX_DOUBLEBUFFER, False, @@ -309,7 +308,7 @@ GLContextProvider::CreateForWindow(nsIWidget *aWidget) printf("[GLX] widget has VisualID 0x%lx\n", widgetVisualID); #endif - XVisualInfo *vi = NULL; + ScopedXFree vi; if (isATI) { XVisualInfo vinfo_template; int nvisuals; @@ -324,33 +323,25 @@ GLContextProvider::CreateForWindow(nsIWidget *aWidget) int matchIndex = -1; for (int i = 0; i < numConfigs; i++) { - XVisualInfo *info = sGLXLibrary.xGetVisualFromFBConfig(display, cfgs[i]); + ScopedXFree info(sGLXLibrary.xGetVisualFromFBConfig(display, cfgs[i])); if (!info) { continue; } if (isATI) { if (AreCompatibleVisuals(vi, info)) { matchIndex = i; - XFree(info); break; } } else { if (widgetVisualID == info->visualid) { matchIndex = i; - XFree(info); break; } } - XFree(info); - } - - if (isATI) { - XFree(vi); } if (matchIndex == -1) { NS_WARNING("[GLX] Couldn't find a FBConfig matching widget visual"); - XFree(cfgs); return nsnull; } @@ -358,9 +349,7 @@ GLContextProvider::CreateForWindow(nsIWidget *aWidget) window, cfgs[matchIndex], PR_FALSE); - XFree(cfgs); - - return glContext.forget().get(); + return glContext.forget(); } already_AddRefed @@ -379,7 +368,7 @@ GLContextProvider::CreatePBuffer(const gfxIntSize &aSize, const ContextFormat& a } while(0) int numFormats; - Display *display = DISPLAY(); + Display *display = DefaultXDisplay(); int xscreen = DefaultScreen(display); A2_(GLX_DOUBLEBUFFER, False); @@ -392,15 +381,15 @@ GLContextProvider::CreatePBuffer(const gfxIntSize &aSize, const ContextFormat& a A2_(GLX_DEPTH_SIZE, aFormat.depth); A1_(0); - GLXFBConfig *cfg = sGLXLibrary.xChooseFBConfig(display, - xscreen, - attribs.Elements(), - &numFormats); - + ScopedXFree cfg(sGLXLibrary.xChooseFBConfig(display, + xscreen, + attribs.Elements(), + &numFormats)); if (!cfg) { return nsnull; } - NS_ASSERTION(numFormats > 0, ""); + NS_ASSERTION(numFormats > 0, + "glXChooseFBConfig() failed to match our requested format and violated its spec (!)"); nsTArray pbattribs; pbattribs.AppendElement(GLX_PBUFFER_WIDTH); @@ -415,7 +404,6 @@ GLContextProvider::CreatePBuffer(const gfxIntSize &aSize, const ContextFormat& a pbattribs.Elements()); if (pbuffer == 0) { - XFree(cfg); return nsnull; } @@ -423,13 +411,7 @@ GLContextProvider::CreatePBuffer(const gfxIntSize &aSize, const ContextFormat& a pbuffer, cfg[0], PR_TRUE); - XFree(cfg); - - if (!glContext) { - return nsnull; - } - - return glContext.forget().get(); + return glContext.forget(); } already_AddRefed From 84c86436a05bd11fbabbb330c31f540d832530e5 Mon Sep 17 00:00:00 2001 From: Karl Tomlinson Date: Thu, 24 Jun 2010 15:59:10 +1200 Subject: [PATCH 1759/1860] revert b8ddc4e35f65 to remove temporary logging --- tools/rb/fix-linux-stack.pl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tools/rb/fix-linux-stack.pl b/tools/rb/fix-linux-stack.pl index aa03a77dd130..903cc15a81bd 100755 --- a/tools/rb/fix-linux-stack.pl +++ b/tools/rb/fix-linux-stack.pl @@ -270,12 +270,6 @@ while (<>) { print $line; } - } elsif ($line =~ "Test timed out") { - # Temporary logging for debugging mochitests bug 569237 et al - print $line; - system qw(xprop -root); - system qw(xwininfo -tree -root); - } else { print $line; } From 3684d7775eac4d073fed0324c87c7279dbbc8169 Mon Sep 17 00:00:00 2001 From: Robert O'Callahan Date: Thu, 24 Jun 2010 16:02:12 +1200 Subject: [PATCH 1760/1860] b=573944 invalidate windowless plugins on instantiation r=josh --- layout/generic/nsObjectFrame.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/layout/generic/nsObjectFrame.cpp b/layout/generic/nsObjectFrame.cpp index 5f4e122e889e..075700cc357c 100644 --- a/layout/generic/nsObjectFrame.cpp +++ b/layout/generic/nsObjectFrame.cpp @@ -2054,6 +2054,9 @@ nsObjectFrame::Instantiate(nsIChannel* aChannel, nsIStreamListener** aStreamList // This must be done before instantiating the plugin FixupWindow(GetContentRect().Size()); + // Ensure we redraw when a plugin is instantiated + Invalidate(GetContentRect() - GetPosition()); + nsWeakFrame weakFrame(this); NS_ASSERTION(!mPreventInstantiation, "Say what?"); @@ -2094,6 +2097,9 @@ nsObjectFrame::Instantiate(const char* aMimeType, nsIURI* aURI) // This must be done before instantiating the plugin FixupWindow(GetContentRect().Size()); + // Ensure we redraw when a plugin is instantiated + Invalidate(GetContentRect() - GetPosition()); + // get the nsIPluginHost service nsCOMPtr pluginHost(do_GetService(MOZ_PLUGIN_HOST_CONTRACTID, &rv)); if (NS_FAILED(rv)) From 970dbfbc8ae68dd5538990feff53b97bb8e7d0c5 Mon Sep 17 00:00:00 2001 From: Oleg Romashin Date: Wed, 23 Jun 2010 10:01:29 -0400 Subject: [PATCH 1761/1860] Bug 571832 - GL ThebesLayer rendering layout on X-System terribly slow. ThebesLayer. r=vladimir. --- gfx/layers/opengl/ThebesLayerOGL.cpp | 80 +++++++++++++++++++++------- gfx/layers/opengl/ThebesLayerOGL.h | 7 +++ 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/gfx/layers/opengl/ThebesLayerOGL.cpp b/gfx/layers/opengl/ThebesLayerOGL.cpp index 8e9f0f2c0905..deb77c94a1f5 100644 --- a/gfx/layers/opengl/ThebesLayerOGL.cpp +++ b/gfx/layers/opengl/ThebesLayerOGL.cpp @@ -43,10 +43,13 @@ #ifdef XP_WIN #include "gfxWindowsSurface.h" #endif +#include "GLContextProvider.h" namespace mozilla { namespace layers { +using namespace mozilla::gl; + // Returns true if it's OK to save the contents of aLayer in an // opaque surface (a surface without an alpha channel). // If we can use a surface without an alpha channel, we should, because @@ -74,6 +77,7 @@ ThebesLayerOGL::ThebesLayerOGL(LayerManagerOGL *aManager) : ThebesLayer(aManager, NULL) , LayerOGL(aManager) , mTexture(0) + , mOffscreenFormat(gfxASurface::ImageFormatUnknown) { mImplData = static_cast(this); } @@ -81,20 +85,41 @@ ThebesLayerOGL::ThebesLayerOGL(LayerManagerOGL *aManager) ThebesLayerOGL::~ThebesLayerOGL() { mOGLManager->MakeCurrent(); + if (mOffscreenSurfaceAsGLContext) + mOffscreenSurfaceAsGLContext->ReleaseTexImage(); if (mTexture) { gl()->fDeleteTextures(1, &mTexture); } } -void -ThebesLayerOGL::SetVisibleRegion(const nsIntRegion &aRegion) +PRBool +ThebesLayerOGL::EnsureSurface() { - if (aRegion.IsEqual(mVisibleRegion)) - return; + gfxASurface::gfxImageFormat imageFormat = gfxASurface::ImageFormatARGB32; + if (UseOpaqueSurface(this)) + imageFormat = gfxASurface::ImageFormatRGB24; - ThebesLayer::SetVisibleRegion(aRegion); + if (mInvalidatedRect.IsEmpty()) + return mOffScreenSurface ? PR_TRUE : PR_FALSE; + + if ((mOffscreenSize == gfxIntSize(mInvalidatedRect.width, mInvalidatedRect.height) + && imageFormat == mOffscreenFormat) + || mInvalidatedRect.IsEmpty()) + return mOffScreenSurface ? PR_TRUE : PR_FALSE; + + mOffScreenSurface = + gfxPlatform::GetPlatform()-> + CreateOffscreenSurface(gfxIntSize(mInvalidatedRect.width, mInvalidatedRect.height), + imageFormat); + if (!mOffScreenSurface) + return PR_FALSE; + + if (mOffScreenSurface) { + mOffscreenSize.width = mInvalidatedRect.width; + mOffscreenSize.height = mInvalidatedRect.height; + mOffscreenFormat = imageFormat; + } - mInvalidatedRect = mVisibleRegion.GetBounds(); mOGLManager->MakeCurrent(); @@ -108,6 +133,13 @@ ThebesLayerOGL::SetVisibleRegion(const nsIntRegion &aRegion) gl()->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, LOCAL_GL_CLAMP_TO_EDGE); gl()->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, LOCAL_GL_CLAMP_TO_EDGE); + // Try bind our offscreen surface directly to texture + mOffscreenSurfaceAsGLContext = sGLContextProvider.CreateForNativePixmapSurface(mOffScreenSurface); + // Bind GL surface to the texture, and return + if (mOffscreenSurfaceAsGLContext) + return mOffscreenSurfaceAsGLContext->BindTexImage(); + + // Otherwise allocate new texture gl()->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGBA, @@ -119,6 +151,18 @@ ThebesLayerOGL::SetVisibleRegion(const nsIntRegion &aRegion) NULL); DEBUG_GL_ERROR_CHECK(gl()); + return PR_TRUE; +} + +void +ThebesLayerOGL::SetVisibleRegion(const nsIntRegion &aRegion) +{ + if (aRegion.IsEqual(mVisibleRegion)) + return; + + ThebesLayer::SetVisibleRegion(aRegion); + + mInvalidatedRect = mVisibleRegion.GetBounds(); } void @@ -140,6 +184,9 @@ void ThebesLayerOGL::RenderLayer(int aPreviousFrameBuffer, const nsIntPoint& aOffset) { + if (!EnsureSurface()) + return; + if (!mTexture) return; @@ -149,27 +196,19 @@ ThebesLayerOGL::RenderLayer(int aPreviousFrameBuffer, bool needsTextureBind = true; nsIntRect visibleRect = mVisibleRegion.GetBounds(); + nsRefPtr surface = mOffScreenSurface; + gfxASurface::gfxImageFormat imageFormat = mOffscreenFormat; + if (!mInvalidatedRect.IsEmpty()) { - gfxASurface::gfxImageFormat imageFormat; - - if (UseOpaqueSurface(this)) { - imageFormat = gfxASurface::ImageFormatRGB24; - } else { - imageFormat = gfxASurface::ImageFormatARGB32; - } - - nsRefPtr surface = - gfxPlatform::GetPlatform()-> - CreateOffscreenSurface(gfxIntSize(mInvalidatedRect.width, - mInvalidatedRect.height), - imageFormat); - nsRefPtr ctx = new gfxContext(surface); ctx->Translate(gfxPoint(-mInvalidatedRect.x, -mInvalidatedRect.y)); /* Call the thebes layer callback */ mOGLManager->CallThebesLayerDrawCallback(this, ctx, mInvalidatedRect); + } + // If draw callback happend and we don't have native surface + if (!mInvalidatedRect.IsEmpty() && !mOffscreenSurfaceAsGLContext) { /* Then take its results and put it in an image surface, * in preparation for a texture upload */ nsRefPtr imageSurface; @@ -202,6 +241,7 @@ ThebesLayerOGL::RenderLayer(int aPreviousFrameBuffer, break; } + // Upload image to texture (slow) gl()->fBindTexture(LOCAL_GL_TEXTURE_2D, mTexture); gl()->fTexSubImage2D(LOCAL_GL_TEXTURE_2D, 0, diff --git a/gfx/layers/opengl/ThebesLayerOGL.h b/gfx/layers/opengl/ThebesLayerOGL.h index 4f4f7e059f80..7a7bee6a18fb 100644 --- a/gfx/layers/opengl/ThebesLayerOGL.h +++ b/gfx/layers/opengl/ThebesLayerOGL.h @@ -41,6 +41,7 @@ #include "Layers.h" #include "LayerManagerOGL.h" #include "gfxImageSurface.h" +#include "GLContext.h" namespace mozilla { @@ -50,6 +51,7 @@ class ThebesLayerOGL : public ThebesLayer, public LayerOGL { public: + typedef mozilla::gl::GLContext GLContext; ThebesLayerOGL(LayerManagerOGL *aManager); virtual ~ThebesLayerOGL(); @@ -71,6 +73,7 @@ public: const nsIntRect &GetInvalidatedRect(); private: + PRBool EnsureSurface(); /** * Currently invalidated rectangular area. */ @@ -80,6 +83,10 @@ private: * OpenGL Texture */ GLuint mTexture; + nsRefPtr mOffscreenSurfaceAsGLContext; + nsRefPtr mOffScreenSurface; + gfxASurface::gfxImageFormat mOffscreenFormat; + gfxIntSize mOffscreenSize; }; } /* layers */ From a03715ba0ece4319e518bbb9843fe56885bdd046 Mon Sep 17 00:00:00 2001 From: Oleg Romashin Date: Wed, 23 Jun 2010 10:02:32 -0400 Subject: [PATCH 1762/1860] Bug 571832 - GL ThebesLayer rendering layout on X-System terribly slow. CanvasLayer. r=vladimir. --- gfx/layers/opengl/CanvasLayerOGL.cpp | 26 ++++++++++++++++++++++++-- gfx/layers/opengl/CanvasLayerOGL.h | 1 + 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/gfx/layers/opengl/CanvasLayerOGL.cpp b/gfx/layers/opengl/CanvasLayerOGL.cpp index 4b9f6b5550a5..08ae935b3d36 100644 --- a/gfx/layers/opengl/CanvasLayerOGL.cpp +++ b/gfx/layers/opengl/CanvasLayerOGL.cpp @@ -39,6 +39,7 @@ #include "gfxImageSurface.h" #include "gfxContext.h" +#include "GLContextProvider.h" #ifdef XP_WIN #include "gfxWindowsSurface.h" @@ -72,6 +73,8 @@ CanvasLayerOGL::Initialize(const Data& aData) NS_ASSERTION(aData.mGLContext == nsnull, "CanvasLayerOGL can't have both surface and GLContext"); mNeedsYFlip = PR_FALSE; + if (mCanvasSurface->GetType() == gfxASurface::SurfaceTypeXlib) + mCanvasSurfaceAsGLContext = sGLContextProvider.CreateForNativePixmapSurface(mCanvasSurface); } else if (aData.mGLContext) { // this must be a pbuffer context void *pbuffer = aData.mGLContext->GetNativeData(GLContext::NativePBuffer); @@ -101,7 +104,26 @@ CanvasLayerOGL::Updated(const nsIntRect& aRect) mUpdatedRect.UnionRect(mUpdatedRect, aRect); - if (mCanvasSurface) { + if (mCanvasSurfaceAsGLContext) { + PRBool newTexture = mTexture == 0; + if (newTexture) { + gl()->fGenTextures(1, &mTexture); + + gl()->fActiveTexture(LOCAL_GL_TEXTURE0); + gl()->fBindTexture(LOCAL_GL_TEXTURE_2D, mTexture); + + gl()->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, LOCAL_GL_LINEAR); + gl()->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, LOCAL_GL_LINEAR); + gl()->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, LOCAL_GL_CLAMP_TO_EDGE); + gl()->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, LOCAL_GL_CLAMP_TO_EDGE); + + mUpdatedRect = mBounds; + } else { + gl()->fActiveTexture(LOCAL_GL_TEXTURE0); + gl()->fBindTexture(LOCAL_GL_TEXTURE_2D, mTexture); + } + mCanvasSurfaceAsGLContext->BindTexImage(); + } else if (mCanvasSurface) { PRBool newTexture = mTexture == 0; if (newTexture) { gl()->fGenTextures(1, &mTexture); @@ -236,7 +258,7 @@ CanvasLayerOGL::RenderLayer(int aPreviousDestination, gl()->fActiveTexture(LOCAL_GL_TEXTURE0); gl()->fBindTexture(LOCAL_GL_TEXTURE_2D, mTexture); - if (mCanvasGLContext) { + if (mCanvasGLContext || mCanvasSurfaceAsGLContext) { program = mOGLManager->GetRGBALayerProgram(); } else { program = mOGLManager->GetBGRALayerProgram(); diff --git a/gfx/layers/opengl/CanvasLayerOGL.h b/gfx/layers/opengl/CanvasLayerOGL.h index e651b410ee6a..d29c09751300 100644 --- a/gfx/layers/opengl/CanvasLayerOGL.h +++ b/gfx/layers/opengl/CanvasLayerOGL.h @@ -80,6 +80,7 @@ protected: PRPackedBool mGLBufferIsPremultiplied; PRPackedBool mNeedsYFlip; + nsRefPtr mCanvasSurfaceAsGLContext; }; } /* layers */ From c07a4627aa01cfaa974add9dc2395e2203ff2c4c Mon Sep 17 00:00:00 2001 From: Oleg Romashin Date: Wed, 23 Jun 2010 10:03:31 -0400 Subject: [PATCH 1763/1860] Bug 571832 - GL ThebesLayer rendering layout on X-System terribly slow. ImageLayer. r=vladimir. --- gfx/layers/opengl/ImageLayerOGL.cpp | 30 ++++++++++++++++++++--------- gfx/layers/opengl/ImageLayerOGL.h | 1 + 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/gfx/layers/opengl/ImageLayerOGL.cpp b/gfx/layers/opengl/ImageLayerOGL.cpp index 223a74efc699..b67c2b111af5 100644 --- a/gfx/layers/opengl/ImageLayerOGL.cpp +++ b/gfx/layers/opengl/ImageLayerOGL.cpp @@ -38,6 +38,7 @@ #include "ImageLayerOGL.h" #include "gfxImageSurface.h" +#include "GLContextProvider.h" using namespace mozilla::gl; @@ -322,7 +323,11 @@ ImageLayerOGL::RenderLayer(int, gl()->fActiveTexture(LOCAL_GL_TEXTURE0); gl()->fBindTexture(LOCAL_GL_TEXTURE_2D, cairoImage->mTexture.GetTextureID()); - ColorTextureLayerProgram *program = mOGLManager->GetBGRALayerProgram(); + ColorTextureLayerProgram *program; + if (cairoImage->mASurfaceAsGLContext) + program = mOGLManager->GetRGBALayerProgram(); + else + program = mOGLManager->GetBGRALayerProgram(); program->Activate(); program->SetLayerQuadRect(nsIntRect(0, 0, @@ -539,6 +544,21 @@ CairoImageOGL::SetData(const CairoImage::Data &aData) mSize = aData.mSize; + gl->fActiveTexture(LOCAL_GL_TEXTURE0); + gl->fBindTexture(LOCAL_GL_TEXTURE_2D, mTexture.GetTextureID()); + gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, LOCAL_GL_LINEAR); + gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, LOCAL_GL_LINEAR); + gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, LOCAL_GL_CLAMP_TO_EDGE); + gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, LOCAL_GL_CLAMP_TO_EDGE); + + if (!mASurfaceAsGLContext) { + mASurfaceAsGLContext = sGLContextProvider.CreateForNativePixmapSurface(aData.mSurface); + if (mASurfaceAsGLContext) + mASurfaceAsGLContext->BindTexImage(); + } + if (mASurfaceAsGLContext) + return; + // XXX This could be a lot more efficient if we already have an image-compatible // surface // XXX if we ever create an ImageFormatRGB24 surface, make sure that we use @@ -550,14 +570,6 @@ CairoImageOGL::SetData(const CairoImage::Data &aData) context->SetSource(aData.mSurface); context->Paint(); - gl->fActiveTexture(LOCAL_GL_TEXTURE0); - gl->fBindTexture(LOCAL_GL_TEXTURE_2D, mTexture.GetTextureID()); - - gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, LOCAL_GL_LINEAR); - gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, LOCAL_GL_LINEAR); - gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, LOCAL_GL_CLAMP_TO_EDGE); - gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, LOCAL_GL_CLAMP_TO_EDGE); - gl->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGBA, diff --git a/gfx/layers/opengl/ImageLayerOGL.h b/gfx/layers/opengl/ImageLayerOGL.h index cc0aab941f2c..55fcd7407fd1 100644 --- a/gfx/layers/opengl/ImageLayerOGL.h +++ b/gfx/layers/opengl/ImageLayerOGL.h @@ -222,6 +222,7 @@ public: GLTexture mTexture; gfxIntSize mSize; + nsRefPtr mASurfaceAsGLContext; }; } /* layers */ From 404ca1eca460bd04996be8f070f76d0d6c1443ab Mon Sep 17 00:00:00 2001 From: Oleg Romashin Date: Thu, 24 Jun 2010 06:04:28 -0400 Subject: [PATCH 1764/1860] Bug 571832 - Bind(Release)TexImage, fix complex logic commented by vlad --- gfx/thebes/src/GLContextProviderEGL.cpp | 29 ++++++++++--------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/gfx/thebes/src/GLContextProviderEGL.cpp b/gfx/thebes/src/GLContextProviderEGL.cpp index 30f80cd4e8a7..5f49c3f8a039 100644 --- a/gfx/thebes/src/GLContextProviderEGL.cpp +++ b/gfx/thebes/src/GLContextProviderEGL.cpp @@ -270,17 +270,15 @@ public: PRBool BindTexImage() { - if (mBound) - if (!ReleaseTexImage()) - return PR_FALSE; + if (!mSurface) + return PR_FALSE; - if (mSurface) { - EGLBoolean success; - success = sEGLLibrary.fBindTexImage(mDisplay, (EGLSurface)mSurface, - LOCAL_EGL_BACK_BUFFER); - if (success == LOCAL_EGL_FALSE) - return PR_FALSE; - } else + if (mBound && !ReleaseTexImage()) + return PR_FALSE; + + EGLBoolean success = sEGLLibrary.fBindTexImage(mDisplay, + (EGLSurface)mSurface, LOCAL_EGL_BACK_BUFFER); + if (success == LOCAL_EGL_FALSE) return PR_FALSE; mBound = PR_TRUE; @@ -292,15 +290,12 @@ public: if (!mBound) return PR_TRUE; - if (!mDisplay) + if (!mDisplay || !mSurface) return PR_FALSE; - if (mSurface) { - EGLBoolean success; - success = sEGLLibrary.fReleaseTexImage(mDisplay, (EGLSurface)mSurface, LOCAL_EGL_BACK_BUFFER); - if (success == LOCAL_EGL_FALSE) - return PR_FALSE; - } else + EGLBoolean success; + success = sEGLLibrary.fReleaseTexImage(mDisplay, (EGLSurface)mSurface, LOCAL_EGL_BACK_BUFFER); + if (success == LOCAL_EGL_FALSE) return PR_FALSE; mBound = PR_FALSE; From 81b53be60ddbbb01feb285e08b0cc964d009e235 Mon Sep 17 00:00:00 2001 From: Oleg Romashin Date: Thu, 24 Jun 2010 06:07:57 -0400 Subject: [PATCH 1765/1860] Bug 560537 - Implement Top level GL layer manager for Qt widget port. r=vladimir. --- widget/src/qt/mozqwidget.h | 10 ++-- widget/src/qt/nsWindow.cpp | 107 +++++++++++++++++++++++++++++-------- widget/src/qt/nsWindow.h | 5 ++ 3 files changed, 97 insertions(+), 25 deletions(-) diff --git a/widget/src/qt/mozqwidget.h b/widget/src/qt/mozqwidget.h index a4a4ba764d19..a880db54c911 100644 --- a/widget/src/qt/mozqwidget.h +++ b/widget/src/qt/mozqwidget.h @@ -4,7 +4,6 @@ #include #include #include -#include #include "nsIWidget.h" #include "prenv.h" @@ -148,12 +147,16 @@ public: MozQGraphicsView(MozQWidget* aTopLevel, QWidget * aParent = nsnull) : QGraphicsView (new QGraphicsScene(), aParent) , mEventHandler(this, aTopLevel) + , mTopLevelWidget(aTopLevel) { - if (PR_GetEnv("MOZ_QT_GL")) - setViewport(new QGLWidget()); scene()->addItem(aTopLevel); } + MozQWidget* GetTopLevelWidget() + { + return mTopLevelWidget; + } + protected: virtual bool event(QEvent* aEvent) @@ -176,6 +179,7 @@ protected: private: MozQGraphicsViewEvents mEventHandler; + MozQWidget* mTopLevelWidget; }; #endif diff --git a/widget/src/qt/nsWindow.cpp b/widget/src/qt/nsWindow.cpp index 913a4c486236..fca79fd79c39 100644 --- a/widget/src/qt/nsWindow.cpp +++ b/widget/src/qt/nsWindow.cpp @@ -99,6 +99,10 @@ #include "nsIDOMSimpleGestureEvent.h" //Gesture support +#include +#include "Layers.h" +#include "LayerManagerOGL.h" + // imported in nsWidgetFactory.cpp PRBool gDisableNativeTheme = PR_FALSE; @@ -758,9 +762,12 @@ nsWindow::GetNativeData(PRUint32 aDataType) break; } - case NS_NATIVE_SHELLWIDGET: - return (void *) GetViewWidget(); - + case NS_NATIVE_SHELLWIDGET: { + QWidget* widget = nsnull; + if (mWidget && mWidget->scene()) + widget = mWidget->scene()->views()[0]->viewport(); + return (void *) widget; + } default: NS_WARNING("nsWindow::GetNativeData called with bad value"); return nsnull; @@ -955,20 +962,6 @@ nsWindow::GetAttention(PRInt32 aCycleCount) return NS_ERROR_NOT_IMPLEMENTED; } -#ifdef MOZ_X11 -static already_AddRefed -GetSurfaceForQPixmap(QPixmap* aDrawable) -{ - gfxASurface* result = - new gfxXlibSurface(aDrawable->x11Info().display(), - aDrawable->handle(), - (Visual*)aDrawable->x11Info().visual(), - gfxIntSize(aDrawable->size().width(), aDrawable->size().height())); - NS_IF_ADDREF(result); - return result; -} -#endif - nsEventStatus nsWindow::DoPaint(QPainter* aPainter, const QStyleOptionGraphicsItem* aOption) { @@ -993,6 +986,19 @@ nsWindow::DoPaint(QPainter* aPainter, const QStyleOptionGraphicsItem* aOption) if (!mDirtyScrollArea.isEmpty()) mDirtyScrollArea = QRegion(); + nsEventStatus status; + nsIntRect rect(r.x(), r.y(), r.width(), r.height()); + + if (GetLayerManager()->GetBackendType() == LayerManager::LAYERS_OPENGL) { + nsPaintEvent event(PR_TRUE, NS_PAINT, this); + event.refPoint.x = r.x(); + event.refPoint.y = r.y(); + event.region = nsIntRegion(rect); + static_cast(GetLayerManager())-> + SetClippingRegion(event.region); + return DispatchEvent(&event); + } + gfxQtPlatform::RenderMode renderMode = gfxQtPlatform::GetPlatform()->GetRenderMode(); int depth = aPainter->device()->depth(); @@ -1018,13 +1024,9 @@ nsWindow::DoPaint(QPainter* aPainter, const QStyleOptionGraphicsItem* aOption) ctx->Translate(gfxPoint(-r.x(), -r.y())); nsPaintEvent event(PR_TRUE, NS_PAINT, this); - - nsIntRect rect(r.x(), r.y(), r.width(), r.height()); event.refPoint.x = r.x(); event.refPoint.y = r.y(); event.region = nsIntRegion(rect); - - nsEventStatus status; { AutoLayerManagerSetup setupLayerManager(this, ctx); status = DispatchEvent(&event); @@ -1844,7 +1846,19 @@ nsWindow::GetHasTransparentBackground(PRBool& aTransparent) void nsWindow::GetToplevelWidget(MozQWidget **aWidget) { - *aWidget = mWidget; + MozQGraphicsView *view = static_cast(GetViewWidget()); + if (view) + *aWidget = view->GetTopLevelWidget(); +} + +nsWindow * +nsWindow::GetTopLevelNsWindow() +{ + MozQWidget *widget = nsnull; + GetToplevelWidget(&widget); + if (widget) + return widget->getReceiver(); + return nsnull; } void * @@ -2109,6 +2123,55 @@ nsWindow::createQWidget(MozQWidget *parent, nsWidgetInitData *aInitData) return widget; } +PRBool +nsWindow::IsAcceleratedQView(QGraphicsView *view) +{ + if (view && view->viewport()) { + QPaintEngine::Type type = view->viewport()->paintEngine()->type(); + return (type == QPaintEngine::OpenGL || type == QPaintEngine::OpenGL2); + } + return PR_FALSE; +} + +NS_IMETHODIMP +nsWindow::SetAcceleratedRendering(PRBool aEnabled) +{ + if (mUseAcceleratedRendering == aEnabled) + return NS_OK; + + mUseAcceleratedRendering = aEnabled; + mLayerManager = NULL; + + QGraphicsView* view = static_cast(GetViewWidget()); + if (view) { + if (aEnabled && !IsAcceleratedQView(view)) + view->setViewport(new QGLWidget()); + if (!aEnabled && IsAcceleratedQView(view)) + view->setViewport(new QWidget()); + view->viewport()->setAttribute(Qt::WA_PaintOnScreen, aEnabled); + view->viewport()->setAttribute(Qt::WA_NoSystemBackground, aEnabled); + } + + return NS_OK; +} + + +mozilla::layers::LayerManager* +nsWindow::GetLayerManager() +{ + nsWindow *topWindow = GetTopLevelNsWindow(); + if (!topWindow) + return nsBaseWidget::GetLayerManager(); + + if (mUseAcceleratedRendering != topWindow->GetAcceleratedRendering() + && IsAcceleratedQView(static_cast(GetViewWidget()))) { + mLayerManager = NULL; + mUseAcceleratedRendering = topWindow->GetAcceleratedRendering(); + } + + return nsBaseWidget::GetLayerManager(); +} + // return the gfxASurface for rendering to this widget gfxASurface* nsWindow::GetThebesSurface() diff --git a/widget/src/qt/nsWindow.h b/widget/src/qt/nsWindow.h index b7e8bf803f7d..cad992920693 100644 --- a/widget/src/qt/nsWindow.h +++ b/widget/src/qt/nsWindow.h @@ -97,6 +97,7 @@ extern PRLogModuleInfo *gWidgetDrawLog; #endif /* MOZ_LOGGING */ class QEvent; +class QGraphicsView; class MozQWidget; @@ -191,6 +192,7 @@ public: NS_IMETHODIMP SetIMEEnabled(PRUint32 aState); NS_IMETHODIMP GetIMEEnabled(PRUint32* aState); + NS_IMETHOD SetAcceleratedRendering(PRBool aEnabled); // // utility methods @@ -303,10 +305,12 @@ protected: void ThemeChanged(void); + virtual LayerManager* GetLayerManager(); gfxASurface* GetThebesSurface(); private: void GetToplevelWidget(MozQWidget **aWidget); + nsWindow* GetTopLevelNsWindow(); void* SetupPluginPort(void); nsresult SetWindowIconList(const nsTArray &aIconList); void SetDefaultIcon(void); @@ -315,6 +319,7 @@ private: MozQWidget* createQWidget(MozQWidget *parent, nsWidgetInitData *aInitData); QWidget* GetViewWidget(); + PRBool IsAcceleratedQView(QGraphicsView* aView); MozQWidget* mWidget; From 1e9a0e1253aaf823073b455b9718d907ebc6dca0 Mon Sep 17 00:00:00 2001 From: Oleg Romashin Date: Thu, 24 Jun 2010 09:16:27 -0400 Subject: [PATCH 1766/1860] Bug 560537 - Qt build bustage due to missing change. r=bas.schouten. --- widget/src/qt/nsWindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/widget/src/qt/nsWindow.cpp b/widget/src/qt/nsWindow.cpp index fca79fd79c39..b19f100a9b10 100644 --- a/widget/src/qt/nsWindow.cpp +++ b/widget/src/qt/nsWindow.cpp @@ -100,6 +100,7 @@ #include "nsIDOMSimpleGestureEvent.h" //Gesture support #include +#define GLdouble_defined 1 #include "Layers.h" #include "LayerManagerOGL.h" From fe1d1e38f1d2e1ccb4fce49ae76d4a39e263fb60 Mon Sep 17 00:00:00 2001 From: Henri Sivonen Date: Wed, 12 May 2010 10:57:12 +0300 Subject: [PATCH 1767/1860] Bug 534071 - Treat carriage returns as tab-like whitespace in layout. r=roc. --- content/base/src/nsTextFragment.h | 2 +- layout/generic/nsTextFrameThebes.cpp | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/content/base/src/nsTextFragment.h b/content/base/src/nsTextFragment.h index a64068663d30..b26050a2abbc 100644 --- a/content/base/src/nsTextFragment.h +++ b/content/base/src/nsTextFragment.h @@ -55,7 +55,7 @@ class nsCString; // XXX these need I18N spankage #define XP_IS_SPACE(_ch) \ - (((_ch) == ' ') || ((_ch) == '\t') || ((_ch) == '\n')) + (((_ch) == ' ') || ((_ch) == '\t') || ((_ch) == '\n') || ((_ch) == '\r')) #define XP_IS_UPPERCASE(_ch) \ (((_ch) >= 'A') && ((_ch) <= 'Z')) diff --git a/layout/generic/nsTextFrameThebes.cpp b/layout/generic/nsTextFrameThebes.cpp index 110131cce61e..c32909e38aa5 100644 --- a/layout/generic/nsTextFrameThebes.cpp +++ b/layout/generic/nsTextFrameThebes.cpp @@ -538,6 +538,7 @@ static PRBool IsCSSWordSpacingSpace(const nsTextFragment* aFrag, case ' ': case CH_NBSP: return !IsSpaceCombiningSequenceTail(aFrag, aPos + 1); + case '\r': case '\t': return !aStyleText->WhiteSpaceIsSignificant(); case '\n': return !aStyleText->NewlineIsSignificant(); default: return PR_FALSE; @@ -553,14 +554,14 @@ static PRBool IsTrimmableSpace(const PRUnichar* aChars, PRUint32 aLength) PRUnichar ch = *aChars; if (ch == ' ') return !nsTextFrameUtils::IsSpaceCombiningSequenceTail(aChars + 1, aLength - 1); - return ch == '\t' || ch == '\f' || ch == '\n'; + return ch == '\t' || ch == '\f' || ch == '\n' || ch == '\r'; } // Check whether the character aCh is trimmable according to CSS // 'white-space:normal/nowrap' static PRBool IsTrimmableSpace(char aCh) { - return aCh == ' ' || aCh == '\t' || aCh == '\f' || aCh == '\n'; + return aCh == ' ' || aCh == '\t' || aCh == '\f' || aCh == '\n' || aCh == '\r'; } static PRBool IsTrimmableSpace(const nsTextFragment* aFrag, PRUint32 aPos, @@ -573,6 +574,7 @@ static PRBool IsTrimmableSpace(const nsTextFragment* aFrag, PRUint32 aPos, !IsSpaceCombiningSequenceTail(aFrag, aPos + 1); case '\n': return !aStyleText->NewlineIsSignificant(); case '\t': + case '\r': case '\f': return !aStyleText->WhiteSpaceIsSignificant(); default: return PR_FALSE; } @@ -584,7 +586,7 @@ static PRBool IsSelectionSpace(const nsTextFragment* aFrag, PRUint32 aPos) PRUnichar ch = aFrag->CharAt(aPos); if (ch == ' ' || ch == CH_NBSP) return !IsSpaceCombiningSequenceTail(aFrag, aPos + 1); - return ch == '\t' || ch == '\n' || ch == '\f'; + return ch == '\t' || ch == '\n' || ch == '\f' || ch == '\r'; } // Count the amount of trimmable whitespace (as per CSS @@ -627,7 +629,7 @@ IsAllWhitespace(const nsTextFragment* aFrag, PRBool aAllowNewline) const char* str = aFrag->Get1b(); for (PRInt32 i = 0; i < len; ++i) { char ch = str[i]; - if (ch == ' ' || ch == '\t' || (ch == '\n' && aAllowNewline)) + if (ch == ' ' || ch == '\t' || ch == '\r' || (ch == '\n' && aAllowNewline)) continue; return PR_FALSE; } @@ -2108,7 +2110,7 @@ static PRBool IsJustifiableCharacter(const nsTextFragment* aFrag, PRInt32 aPos, PRBool aLangIsCJ) { PRUnichar ch = aFrag->CharAt(aPos); - if (ch == '\n' || ch == '\t') + if (ch == '\n' || ch == '\t' || ch == '\r') return PR_TRUE; if (ch == ' ' || ch == CH_NBSP) { // Don't justify spaces that are combined with diacriticals From a1c686a1ea4270c92971dd5a37c670bb672e689a Mon Sep 17 00:00:00 2001 From: "Tanner M. Young" Date: Wed, 23 Jun 2010 14:24:02 -0500 Subject: [PATCH 1768/1860] Bug 377349 - Page info : Media Tab. The broken image indicator is often wrong. r=db48x --- browser/base/content/pageinfo/pageInfo.js | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js index ed7907f5f984..c19102ee592f 100644 --- a/browser/base/content/pageinfo/pageInfo.js +++ b/browser/base/content/pageinfo/pageInfo.js @@ -25,6 +25,7 @@ # Daniel Brooks # Florian QUEZE # Erik Fabert +# Tanner M. Young # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -175,8 +176,11 @@ gImageView._ltrAtom = atomSvc.getAtom("ltr"); gImageView._brokenAtom = atomSvc.getAtom("broken"); gImageView.getCellProperties = function(row, col, props) { - if (gImageView.data[row][COL_IMAGE_SIZE] == gStrings.unknown && - !/^https:/.test(gImageView.data[row][COL_IMAGE_ADDRESS])) + var data = gImageView.data[row]; + var item = gImageView.data[row][COL_IMAGE_NODE]; + if (!checkProtocol(data) || + item instanceof HTMLEmbedElement || + (item instanceof HTMLObjectElement && !/^image\//.test(item.type))) props.AppendElement(this._brokenAtom); if (col.element.id == "image-address") @@ -928,10 +932,7 @@ function makePreview(row) var imageContainer = document.getElementById("theimagecontainer"); var oldImage = document.getElementById("thepreviewimage"); - const regex = /^(https?|ftp|file|about|chrome|resource):/; - var isProtocolAllowed = regex.test(url); - if (/^data:/.test(url) && /^image\//.test(mimeType)) - isProtocolAllowed = true; + var isProtocolAllowed = checkProtocol(gImageView.data[row]); var newImage = new Image; newImage.id = "thepreviewimage"; @@ -1215,7 +1216,8 @@ function doSelectAll() elem.view.selection.selectAll(); } -function selectImage() { +function selectImage() +{ if (!gImageElement) return; @@ -1230,3 +1232,12 @@ function selectImage() { } } } + +function checkProtocol(img) +{ + var url = img[COL_IMAGE_ADDRESS]; + if (/^data:/.test(url) && /^image\//.test(img[COL_IMAGE_NODE].type)) + return true; + const regex = /^(https?|ftp|file|about|chrome|resource):/; + return regex.test(url); +} From 76fc1c03fad8dda013dd4e6e34201d2bbae1610d Mon Sep 17 00:00:00 2001 From: Clint Talbert Date: Thu, 24 Jun 2010 02:32:01 -0700 Subject: [PATCH 1769/1860] Bug 573263 - Refactor remote reftest to work on android, create shared remoteautomation class r=jmaher --- build/automation.py.in | 46 ++++++ build/mobile/remoteautomation.py | 144 +++++++++++++++++ layout/tools/reftest/Makefile.in | 1 + layout/tools/reftest/remotereftest.py | 215 +++++++++++--------------- layout/tools/reftest/runreftest.py | 22 ++- testing/mochitest/Makefile.in | 1 + testing/mochitest/runtests.py.in | 5 +- testing/mochitest/runtestsremote.py | 153 ++++++------------ 8 files changed, 342 insertions(+), 245 deletions(-) create mode 100644 build/mobile/remoteautomation.py diff --git a/build/automation.py.in b/build/automation.py.in index 0eb1d9cb1662..03bcde8aa889 100644 --- a/build/automation.py.in +++ b/build/automation.py.in @@ -50,6 +50,7 @@ import subprocess import sys import threading import tempfile +import zipfile SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) sys.path.insert(0, SCRIPT_DIR) @@ -825,3 +826,48 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t ssltunnelProcess.kill() return status + + """ + Copies an "installed" extension into the extensions directory of the given profile + extensionSource - the source location of the extension files. This can be either + a directory or a path to an xpi file. + profileDir - the profile directory we are copying into. We will create the + "extensions" directory there if it doesn't exist + extensionID - the id of the extension to be used as the containing directory for the + extension, i.e. + this is the name of the folder in the /extensions/ + """ + def installExtension(self, extensionSource, profileDir, extensionID): + if (not os.path.exists(extensionSource)): + self.log.info("INFO | automation.py | Cannot install extension no source at: %s", extensionSource) + + if (not os.path.exists(profileDir)): + self.log.info("INFO | automation.py | Cannot install extension invalid profileDir at: %s", profileDir) + + # See if we have an XPI or a directory + if (os.path.isfile(extensionSource)): + tmpd = tempfile.mkdtemp() + extrootdir = self.extractZip(extensionSource, tmpd) + else: + extrootdir = extensionSource + extnsdir = os.path.join(profileDir, "extensions") + extnshome = os.path.join(extnsdir, extensionID) + + # Now we copy the extension source into the extnshome + shutil.copytree(extrootdir, extnshome) + + def extractZip(self, filename, dest): + z = zipfile.ZipFile(filename, 'r') + for n in z.namelist(): + fullpath = os.path.join(dest, n) + parentdir = os.path.dirname(fullpath) + if not os.path.isdir(parentdir): + os.makedirs(parentdir) + if (not n.endswith(os.sep)): + data = z.read(n) + f = open(fullpath, 'w') + f.write(data) + f.close() + z.close() + return dest + diff --git a/build/mobile/remoteautomation.py b/build/mobile/remoteautomation.py new file mode 100644 index 000000000000..bbb1612c2192 --- /dev/null +++ b/build/mobile/remoteautomation.py @@ -0,0 +1,144 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is Joel Maher. +# +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Joel Maher (Original Developer) +# Clint Talbert +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import time +import sys +import os + +from automation import Automation +from devicemanager import DeviceManager + +class RemoteAutomation(Automation): + _devicemanager = None + + def __init__(self, deviceManager, appName = ''): + self._devicemanager = deviceManager + self._appName = appName + self._remoteProfile = None + # Default our product to fennec + self._product = "fennec" + Automation.__init__(self) + + def setDeviceManager(self, deviceManager): + self._devicemanager = deviceManager + + def setAppName(self, appName): + self._appName = appName + + def setRemoteProfile(self, remoteProfile): + self._remoteProfile = remoteProfile + + def setProduct(self, product): + self._product = product + + def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo): + # maxTime is used to override the default timeout, we should honor that + status = proc.wait(timeout = maxTime) + + print proc.stdout + + if (status == 1 and self._devicemanager.processExist(proc.procName)): + # Then we timed out, make sure Fennec is dead + proc.kill() + + return status + + def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): + # If remote profile is specified, use that instead + if (self._remoteProfile): + profileDir = self._remoteProfile + + cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) + # Remove -foreground if it exists, if it doesn't this just returns + try: + args.remove('-foreground') + except: + pass +#TODO: figure out which platform require NO_EM_RESTART +# return app, ['--environ:NO_EM_RESTART=1'] + args + return app, args + + def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = '.'): + return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd) + + # be careful here as this inner class doesn't have access to outer class members + class RProcess(object): + # device manager process + dm = None + def __init__(self, dm, cmd, stdout = None, stderr = None, env = None, cwd = '.'): + self.dm = dm + print "going to launch process: " + str(self.dm.host) + self.proc = dm.launchProcess(cmd) + exepath = cmd[0] + name = exepath.split('/')[-1] + self.procName = name + + # Setting timeout at 1 hour since on a remote device this takes much longer + self.timeout = 3600 + time.sleep(15) + + @property + def pid(self): + hexpid = self.dm.processExist(self.procName) + if (hexpid == '' or hexpid == None): + hexpid = "0x0" + return int(hexpid, 0) + + @property + def stdout(self): + return self.dm.getFile(self.proc) + + def wait(self, timeout = None): + timer = 0 + interval = 5 + + if timeout == None: + timeout = self.timeout + + while (self.dm.processExist(self.procName)): + time.sleep(interval) + timer += interval + if (timer > timeout): + break + + if (timer >= timeout): + return 1 + return 0 + + def kill(self): + self.dm.killProcess(self.procName) diff --git a/layout/tools/reftest/Makefile.in b/layout/tools/reftest/Makefile.in index 545cf86b282f..d079470fef48 100644 --- a/layout/tools/reftest/Makefile.in +++ b/layout/tools/reftest/Makefile.in @@ -79,6 +79,7 @@ _HARNESS_FILES = \ $(topsrcdir)/build/mobile/devicemanager.py \ $(topsrcdir)/build/automationutils.py \ $(topsrcdir)/build/poster.zip \ + $(topsrcdir)/build/mobile/remoteautomation.py \ $(NULL) $(_DEST_DIR): diff --git a/layout/tools/reftest/remotereftest.py b/layout/tools/reftest/remotereftest.py index cc99505b2801..225cf19ab409 100644 --- a/layout/tools/reftest/remotereftest.py +++ b/layout/tools/reftest/remotereftest.py @@ -47,83 +47,7 @@ from runreftest import RefTest from runreftest import ReftestOptions from automation import Automation from devicemanager import DeviceManager - -class RemoteAutomation(Automation): - _devicemanager = None - - def __init__(self, deviceManager, product = ''): - self._devicemanager = deviceManager - self._product = product - Automation.__init__(self) - - def setDeviceManager(self, deviceManager): - self._devicemanager = deviceManager - - def setProduct(self, productName): - self._product = productName - - def setRemoteApp(self, remoteAppName): - self._remoteAppName = remoteAppName - - def setTestRoot(self, testRoot): - self._testRoot = testRoot - - def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime): - status = proc.wait() - print proc.stdout - # todo: consider pulling log file from remote - return status - - def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): - remoteProfileDir = self._testRoot + 'profile' - cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, remoteProfileDir, testURL, extraArgs) - return app, ['--environ:NO_EM_RESTART=1'] + args - - def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = '.'): - return self.RProcess(self._devicemanager, self._remoteAppName, cmd, stdout, stderr, env, cwd) - - class RProcess(object): - #device manager process - dm = None - def __init__(self, dm, appName, cmd, stdout = None, stderr = None, env = None, cwd = '.'): - self.dm = dm - print "going to launch process: " + str(self.dm.host) - self.proc = dm.launchProcess(cmd) - self.procName = appName - - # Setting this at 1 hour since remote testing is much slower - self.timeout = 3600 - time.sleep(5) - - @property - def pid(self): - hexpid = self.dm.processExist(self.procName) - if (hexpid == '' or hexpid == None): - hexpid = 0 - return int(hexpid, 0) - - @property - def stdout(self): - return self.dm.getFile(self.proc) - - def wait(self, timeout = None): - timer = 0 - if timeout == None: - timeout = self.timeout - - while (self.dm.process.isAlive()): - time.sleep(1) - timer += 1 - if (timer > timeout): - break - - if (timer >= timeout): - return 1 - return 0 - - def kill(self): - self.dm.killProcess(self.procName) - +from remoteautomation import RemoteAutomation class RemoteOptions(ReftestOptions): def __init__(self, automation): @@ -132,38 +56,80 @@ class RemoteOptions(ReftestOptions): defaults = {} defaults["logFile"] = "reftest.log" # app, xrePath and utilityPath variables are set in main function - defaults["testRoot"] = "/tests/" + defaults["remoteTestRoot"] = None defaults["app"] = "" defaults["xrePath"] = "" defaults["utilityPath"] = "" - self.add_option("--device", action="store", - type = "string", dest = "device", + self.add_option("--remote-app-path", action="store", + type = "string", dest = "remoteAppPath", + help = "Path to remote executable relative to device root using only forward slashes. Either this or app must be specified, but not both.") + defaults["remoteAppPath"] = None + + self.add_option("--deviceIP", action="store", + type = "string", dest = "deviceIP", help = "ip address of remote device to test") - defaults["device"] = None + defaults["deviceIP"] = None self.add_option("--devicePort", action="store", type = "string", dest = "devicePort", help = "port of remote device to test") - defaults["devicePort"] = 27020 + defaults["devicePort"] = 20701 - self.add_option("--remoteProductName", action="store", + self.add_option("--remote-product-name", action="store", type = "string", dest = "remoteProductName", - help = "Name of remote product to test - either fennec or firefox, defaults to fennec") + help = "Name of product to test - either fennec or firefox, defaults to fennec") defaults["remoteProductName"] = "fennec" - self.add_option("--remoteAppName", action="store", - type = "string", dest = "remoteAppName", - help = "Executable name for remote device, OS dependent, defaults to fennec.exe") - defaults["remoteAppName"] = "fennec.exe" - self.add_option("--remote-webserver", action="store", type = "string", dest = "remoteWebServer", help = "IP Address of the webserver hosting the reftest content") - defaults["remoteWebServer"] = "127.0.0.1" + defaults["remoteWebServer"] = None + + self.add_option("--http-port", action = "store", + type = "string", dest = "httpPort", + help = "port of the web server for http traffic") + defaults["httpPort"] = automation.DEFAULT_HTTP_PORT + + self.add_option("--ssl-port", action = "store", + type = "string", dest = "sslPort", + help = "Port for https traffic to the web server") + defaults["sslPort"] = automation.DEFAULT_SSL_PORT + + self.add_option("--remote-logfile", action="store", + type = "string", dest = "remoteLogFile", + help = "Name of log file on the device relative to device root. PLEASE USE ONLY A FILENAME.") + defaults["remoteLogFile"] = "reftest.log" self.set_defaults(**defaults) + def verifyRemoteOptions(self, options): + # Ensure our defaults are set properly for everything we can infer + options.remoteTestRoot = self._automation._devicemanager.getDeviceRoot() + '/reftest' + options.remoteProfile = options.remoteTestRoot + "/profile" + + # One of remoteAppPath (relative path to application) or the app (executable) must be + # set, but not both. If both are set, we destroy the user's selection for app + # so instead of silently destroying a user specificied setting, we error. + if (options.remoteAppPath and options.app): + print "ERROR: You cannot specify both the remoteAppPath and the app" + return None + elif (options.remoteAppPath): + options.app = options.remoteTestRoot + "/" + options.remoteAppPath + elif (options.app == None): + # Neither remoteAppPath nor app are set -- error + print "ERROR: You must specify either appPath or app" + return None + + if (options.xrePath == None): + print "ERROR: You must specify the path to the controller xre directory" + return None + + # TODO: Copied from main, but I think these are no longer used in a post xulrunner world + #options.xrePath = options.remoteTestRoot + self._automation._product + '/xulrunner' + #options.utilityPath = options.testRoot + self._automation._product + '/bin' + return options + class RemoteReftest(RefTest): remoteApp = '' @@ -172,39 +138,32 @@ class RemoteReftest(RefTest): self._devicemanager = devicemanager self.scriptDir = scriptDir self.remoteApp = options.app - self.remoteTestRoot = options.testRoot - self.remoteProfileDir = options.testRoot + 'profile' + self.remoteTestRoot = options.remoteTestRoot def createReftestProfile(self, options, profileDir): RefTest.createReftestProfile(self, options, profileDir) - self.remoteTestRoot += "reftest/" - - # install the reftest extension bits into the remoteProfile - profileExtensionsPath = os.path.join(profileDir, "extensions") - reftestExtensionPath = self.remoteTestRoot.replace('/', '\\') - extFile = open(os.path.join(profileExtensionsPath, "reftest@mozilla.org"), "w") - extFile.write(reftestExtensionPath) - extFile.close() - - if (self._devicemanager.pushDir(profileDir, self.remoteProfileDir) == None): - raise devicemanager.FileError("Failed to copy profiledir to device") - - if (self._devicemanager.pushDir(self.scriptDir + '/reftest', self.remoteTestRoot) == None): - raise devicemanager.FileError("Failed to copy extension dir to device") - + if (self._devicemanager.pushDir(profileDir, options.remoteProfile) == None): + raise devicemanager.FileError("Failed to copy profiledir to device") def copyExtraFilesToProfile(self, options, profileDir): RefTest.copyExtraFilesToProfile(self, options, profileDir) - if (self._devicemanager.pushDir(profileDir, self.remoteProfileDir) == None): - raise devicemanager.FileError("Failed to copy extra files in profile dir to device") + if (self._devicemanager.pushDir(profileDir, options.remoteProfile) == None): + raise devicemanager.FileError("Failed to copy extra files to device") - def registerExtension(self, browserEnv, options, profileDir): - """ - It appears that we do not need to do the extension registration on winmo. - This is something we should look into for winmo as the -silent option isn't working - """ - pass + def registerExtension(self, browserEnv, options, profileDir, extraArgs = ['-silent'] ): + self.automation.log.info("REFTEST INFO | runreftest.py | Performing extension manager registration: start.\n") + # Because our startProcess code doesn't return until fennec starts we just give it + # a maxTime of 20 secs before timing it out and ensuring it is dead. + # Besides registering the extension, this works around fennec bug 570027 + status = self.automation.runApp(None, browserEnv, options.app, profileDir, + extraArgs, + utilityPath = options.utilityPath, + xrePath=options.xrePath, + symbolsPath=options.symbolsPath, + maxTime = 20) + # We don't care to call |processLeakLog()| for this step. + self.automation.log.info("\nREFTEST INFO | runreftest.py | Performing extension manager registration: end.") def getManifestPath(self, path): return path @@ -220,22 +179,24 @@ def main(): parser = RemoteOptions(automation) options, args = parser.parse_args() - if (options.device == None): + if (options.deviceIP == None): print "Error: you must provide a device IP to connect to via the --device option" sys.exit(1) - if options.remoteAppName.rfind('/') < 0: - options.app = options.testRoot + options.remoteProductName + '/' - options.app += str(options.remoteAppName) - - options.xrePath = options.testRoot + options.remoteProductName + '/xulrunner' - options.utilityPath = options.testRoot + options.remoteProductName + '/bin' - - dm = DeviceManager(options.device, options.devicePort) + dm = DeviceManager(options.deviceIP, options.devicePort) automation.setDeviceManager(dm) - automation.setProduct(options.remoteProductName) - automation.setRemoteApp(options.remoteAppName) - automation.setTestRoot(options.testRoot) + + if (options.remoteProductName != None): + automation.setProduct(options.remoteProductName) + + # Set up the defaults and ensure options are set + options = parser.verifyRemoteOptions(options) + if (options == None): + print "ERROR: Invalid options specified, use --help for a list of valid options" + sys.exit(1) + + automation.setAppName(options.app) + automation.setRemoteProfile(options.remoteProfile) reftest = RemoteReftest(automation, dm, options, SCRIPT_DIRECTORY) if (options.remoteWebServer == "127.0.0.1"): diff --git a/layout/tools/reftest/runreftest.py b/layout/tools/reftest/runreftest.py index 41712ef5790a..97384e74f127 100644 --- a/layout/tools/reftest/runreftest.py +++ b/layout/tools/reftest/runreftest.py @@ -93,12 +93,10 @@ class RefTest(object): prefsFile.close() # install the reftest extension bits into the profile - profileExtensionsPath = os.path.join(profileDir, "extensions") - os.mkdir(profileExtensionsPath) - reftestExtensionPath = os.path.join(SCRIPT_DIRECTORY, "reftest") - extFile = open(os.path.join(profileExtensionsPath, "reftest@mozilla.org"), "w") - extFile.write(reftestExtensionPath) - extFile.close() + self.automation.installExtension(os.path.join(SCRIPT_DIRECTORY, "reftest"), + profileDir, + "reftest@mozilla.org") + def registerExtension(self, browserEnv, options, profileDir, extraArgs = ['-silent']): # run once with -silent to let the extension manager do its thing @@ -149,7 +147,6 @@ class RefTest(object): # then again to actually run reftest self.automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n") reftestlist = self.getManifestPath(manifest) - status = self.automation.runApp(None, browserEnv, options.app, profileDir, ["-reftest", reftestlist], utilityPath = options.utilityPath, @@ -179,14 +176,15 @@ class RefTest(object): class ReftestOptions(OptionParser): def __init__(self, automation): + self._automation = automation OptionParser.__init__(self) defaults = {} # we want to pass down everything from automation.__all__ addCommonOptions(self, - defaults=dict(zip(automation.__all__, - [getattr(automation, x) for x in automation.__all__]))) - automation.addCommonOptions(self) + defaults=dict(zip(self._automation.__all__, + [getattr(self._automation, x) for x in self._automation.__all__]))) + self._automation.addCommonOptions(self) self.add_option("--appname", action = "store", type = "string", dest = "app", default = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP), @@ -208,10 +206,10 @@ class ReftestOptions(OptionParser): "than the given number") self.add_option("--utility-path", action = "store", type = "string", dest = "utilityPath", - default = automation.DIST_BIN, + default = self._automation.DIST_BIN, help = "absolute path to directory containing utility " "programs (xpcshell, ssltunnel, certutil)") - defaults["utilityPath"] = automation.DIST_BIN + defaults["utilityPath"] = self._automation.DIST_BIN self.add_option("--total-chunks", type = "int", dest = "totalChunks", diff --git a/testing/mochitest/Makefile.in b/testing/mochitest/Makefile.in index 5324c79c09c4..fd832d89a7c1 100644 --- a/testing/mochitest/Makefile.in +++ b/testing/mochitest/Makefile.in @@ -66,6 +66,7 @@ _SERV_FILES = \ $(topsrcdir)/build/mobile/devicemanager.py \ $(topsrcdir)/build/automationutils.py \ $(topsrcdir)/build/poster.zip \ + $(topsrcdir)/build/mobile/remoteautomation.py \ gen_template.pl \ server.js \ harness-a11y.xul \ diff --git a/testing/mochitest/runtests.py.in b/testing/mochitest/runtests.py.in index 259bf97272d0..7626efb7dd77 100644 --- a/testing/mochitest/runtests.py.in +++ b/testing/mochitest/runtests.py.in @@ -341,7 +341,10 @@ class MochitestServer: c = urllib2.urlopen(self.shutdownURL) c.read() c.close() - self._process.wait() + + rtncode = self._process.poll() + if (rtncode == None): + self._process.terminate() except: self._process.kill() diff --git a/testing/mochitest/runtestsremote.py b/testing/mochitest/runtestsremote.py index 59b2097db68b..fd461de29e9b 100644 --- a/testing/mochitest/runtestsremote.py +++ b/testing/mochitest/runtestsremote.py @@ -44,100 +44,24 @@ import tempfile sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))) from automation import Automation +from remoteautomation import RemoteAutomation from runtests import Mochitest from runtests import MochitestOptions from runtests import MochitestServer import devicemanager -class RemoteAutomation(Automation): - _devicemanager = None - - def __init__(self, deviceManager, product): - self._devicemanager = deviceManager - self._product = product - Automation.__init__(self) - - def setDeviceManager(self, deviceManager): - self._devicemanager = deviceManager - - def setProduct(self, productName): - self._product = productName - - def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo): - status = proc.wait() - print proc.stdout - # todo: consider pulling log file from remote - return status - - def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): - cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) - # Remove -foreground if it exists, if it doesn't this just returns - try: - args.remove('-foreground') - except: - pass -#TODO: figure out which platform require NO_EM_RESTART -# return app, ['--environ:NO_EM_RESTART=1'] + args - return app, args - - def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = '.'): - return self.RProcess(self._devicemanager, self._product, cmd, stdout, stderr, env, cwd) - - # be careful here as this inner class doesn't have access to outer class members - class RProcess(object): - # device manager process - dm = None - def __init__(self, dm, product, cmd, stdout = None, stderr = None, env = None, cwd = '.'): - self.dm = dm - print "going to launch process: " + str(self.dm.host) - self.proc = dm.launchProcess(cmd) - exepath = cmd[0] - name = exepath.split('/')[-1] - self.procName = name - - # Setting timeout at 1 hour since on a remote device this takes much longer - self.timeout = 3600 - time.sleep(15) - - @property - def pid(self): - hexpid = self.dm.processExist(self.procName) - if (hexpid == '' or hexpid == None): - hexpid = "0x0" - return int(hexpid, 0) - - @property - def stdout(self): - return self.dm.getFile(self.proc) - - def wait(self, timeout = None): - timer = 0 - interval = 5 - - if timeout == None: - timeout = self.timeout - - while (self.dm.processExist(self.procName)): - time.sleep(interval) - timer += interval - if (timer > timeout): - break - - if (timer >= timeout): - return 1 - return 0 - - def kill(self): - self.dm.killProcess(self.procName) - - class RemoteOptions(MochitestOptions): def __init__(self, automation, scriptdir, **kwargs): defaults = {} MochitestOptions.__init__(self, automation, scriptdir) + self.add_option("--remote-app-path", action="store", + type = "string", dest = "remoteAppPath", + help = "Path to remote executable relative to device root using only forward slashes. Either this or app must be specified but not both") + defaults["remoteAppPath"] = None + self.add_option("--deviceIP", action="store", type = "string", dest = "deviceIP", help = "ip address of remote device to test") @@ -148,7 +72,7 @@ class RemoteOptions(MochitestOptions): help = "port of remote device to test") defaults["devicePort"] = 20701 - self.add_option("--remoteProductName", action="store", + self.add_option("--remote-product-name", action="store", type = "string", dest = "remoteProductName", help = "The executable's name of remote product to test - either fennec or firefox, defaults to fennec") defaults["remoteProductName"] = "fennec" @@ -185,41 +109,46 @@ class RemoteOptions(MochitestOptions): def verifyRemoteOptions(self, options, automation): options.remoteTestRoot = automation._devicemanager.getDeviceRoot() + options.utilityPath = options.remoteTestRoot + "/bin" options.certPath = options.remoteTestRoot + "/certs" - if options.remoteWebServer == None: - if os.name != "nt": + + if options.remoteWebServer == None and os.name != "nt": options.remoteWebServer = get_lan_ip() - else: + elif os.name == "nt": print "ERROR: you must specify a remoteWebServer ip address\n" return None options.webServer = options.remoteWebServer if (options.deviceIP == None): - print "ERROR: you must provide a device IP" - return None + print "ERROR: you must provide a device IP" + return None if (options.remoteLogFile == None): - options.remoteLogFile = automation._devicemanager.getDeviceRoot() + '/test.log' + options.remoteLogFile = automation._devicemanager.getDeviceRoot() + '/test.log' # Set up our options that we depend on based on the above productRoot = options.remoteTestRoot + "/" + automation._product + options.utilityPath = productRoot + "/bin" - # Set this only if the user hasn't set it - if (options.utilityPath == None): - options.utilityPath = productRoot + "/bin" - - # If provided, use cli value, otherwise reset as remoteTestRoot - if (options.app == None): - options.app = productRoot + "/" + options.remoteProductName + # remoteAppPath or app must be specified to find the product to launch + if (options.remoteAppPath and options.app): + print "ERROR: You cannot specify both the remoteAppPath and the app setting" + return None + elif (options.remoteAppPath): + options.app = options.remoteTestRoot + "/" + options.remoteAppPath + elif (options.app == None): + # Neither remoteAppPath nor app are set -- error + print "ERROR: You must specify either appPath or app" + return None # Only reset the xrePath if it wasn't provided if (options.xrePath == None): - if (automation._product == "fennec"): - options.xrePath = productRoot + "/xulrunner" - else: - options.xrePath = options.utilityPath + if (automation._product == "fennec"): + options.xrePath = productRoot + "/xulrunner" + else: + options.xrePath = options.utilityPath return options @@ -283,7 +212,7 @@ class MochiRemote(Mochitest): xpcshell = "xpcshell" if (os.name == "nt"): xpcshell += ".exe" - + if (options.utilityPath): paths.insert(0, options.utilityPath) options.utilityPath = self.findPath(paths, xpcshell) @@ -303,9 +232,6 @@ class MochiRemote(Mochitest): def stopWebServer(self, options): self.server.stop() - def runExtensionRegistration(self, options, browserEnv): - pass - def buildProfile(self, options): manifest = Mochitest.buildProfile(self, options) self.localProfile = options.profilePath @@ -314,7 +240,24 @@ class MochiRemote(Mochitest): options.profilePath = self.remoteProfile return manifest - + + def runExtensionRegistration(self, options, browserEnv): + """ run once with -silent to let the extension manager do its thing + and then exit the app + We do this on every run because we need to work around bug 570027 + """ + self._automation.log.info("INFO | runtestsremote.py | Performing extension manager registration: start.\n") + # Don't care about this |status|: |runApp()| reporting it should be enough. + # Because process() doesn't return until fennec starts, we just give it a fudge + # factor of 20s before timing it out and killing it. + status = self._automation.runApp(None, browserEnv, options.app, + options.profilePath, ["-silent"], + utilityPath = options.utilityPath, + xrePath = options.xrePath, + symbolsPath=options.symbolsPath, + maxTime = 20) + # We don't care to call |processLeakLog()| for this step. + self._automation.log.info("\nINFO | runtestsremote.py | Performing extension manager registration: end.") def buildURLOptions(self, options): self.localLog = options.logFile options.logFile = self.remoteLog From a02f55a8d40780d7e02d58555aae49d05855e2f7 Mon Sep 17 00:00:00 2001 From: Clint Talbert Date: Thu, 24 Jun 2010 02:32:01 -0700 Subject: [PATCH 1770/1860] Bug 573263 - Refactor remote reftest to work on android, create shared remoteautomation class r=jmaher --- testing/mochitest/runtestsremote.py | 120 ++++++++++++++-------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/testing/mochitest/runtestsremote.py b/testing/mochitest/runtestsremote.py index fd461de29e9b..893243ea3350 100644 --- a/testing/mochitest/runtestsremote.py +++ b/testing/mochitest/runtestsremote.py @@ -186,48 +186,48 @@ class MochiRemote(Mochitest): self._dm.removeDir(self.remoteProfile) def findPath(self, paths, filename = None): - for path in paths: - p = path - if filename: - p = os.path.join(p, filename) - if os.path.exists(self.getFullPath(p)): - return path - return None + for path in paths: + p = path + if filename: + p = os.path.join(p, filename) + if os.path.exists(self.getFullPath(p)): + return path + return None def startWebServer(self, options): - """ Create the webserver on the host and start it up """ - remoteXrePath = options.xrePath - remoteProfilePath = options.profilePath - remoteUtilityPath = options.utilityPath - localAutomation = Automation() + """ Create the webserver on the host and start it up """ + remoteXrePath = options.xrePath + remoteProfilePath = options.profilePath + remoteUtilityPath = options.utilityPath + localAutomation = Automation() - paths = [options.xrePath, localAutomation.DIST_BIN, self._automation._product, os.path.join('..', self._automation._product)] - options.xrePath = self.findPath(paths) - if options.xrePath == None: - print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name) - sys.exit(1) - paths.append("bin") - paths.append(os.path.join("..", "bin")) + paths = [options.xrePath, localAutomation.DIST_BIN, self._automation._product, os.path.join('..', self._automation._product)] + options.xrePath = self.findPath(paths) + if options.xrePath == None: + print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name) + sys.exit(1) + paths.append("bin") + paths.append(os.path.join("..", "bin")) - xpcshell = "xpcshell" - if (os.name == "nt"): - xpcshell += ".exe" + xpcshell = "xpcshell" + if (os.name == "nt"): + xpcshell += ".exe" - if (options.utilityPath): - paths.insert(0, options.utilityPath) - options.utilityPath = self.findPath(paths, xpcshell) - if options.utilityPath == None: - print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name) - sys.exit(1) + if (options.utilityPath): + paths.insert(0, options.utilityPath) + options.utilityPath = self.findPath(paths, xpcshell) + if options.utilityPath == None: + print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name) + sys.exit(1) - options.profilePath = tempfile.mkdtemp() - self.server = MochitestServer(localAutomation, options) - self.server.start() + options.profilePath = tempfile.mkdtemp() + self.server = MochitestServer(localAutomation, options) + self.server.start() - self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT) - options.xrePath = remoteXrePath - options.utilityPath = remoteUtilityPath - options.profilePath = remoteProfilePath + self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT) + options.xrePath = remoteXrePath + options.utilityPath = remoteUtilityPath + options.profilePath = remoteProfilePath def stopWebServer(self, options): self.server.stop() @@ -282,27 +282,27 @@ class MochiRemote(Mochitest): # utilities to get the local ip address # if os.name != "nt": - import fcntl - import struct - def get_interface_ip(ifname): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - return socket.inet_ntoa(fcntl.ioctl( - s.fileno(), - 0x8915, # SIOCGIFADDR - struct.pack('256s', ifname[:15]) - )[20:24]) + import fcntl + import struct + def get_interface_ip(ifname): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + return socket.inet_ntoa(fcntl.ioctl( + s.fileno(), + 0x8915, # SIOCGIFADDR + struct.pack('256s', ifname[:15]) + )[20:24]) def get_lan_ip(): - ip = socket.gethostbyname(socket.gethostname()) - if ip.startswith("127.") and os.name != "nt": - interfaces = ["eth0","eth1","eth2","wlan0","wlan1","wifi0","ath0","ath1","ppp0"] - for ifname in interfaces: - try: - ip = get_interface_ip(ifname) - break; - except IOError: - pass - return ip + ip = socket.gethostbyname(socket.gethostname()) + if ip.startswith("127.") and os.name != "nt": + interfaces = ["eth0","eth1","eth2","wlan0","wlan1","wifi0","ath0","ath1","ppp0"] + for ifname in interfaces: + try: + ip = get_interface_ip(ifname) + break; + except IOError: + pass + return ip def main(): @@ -316,24 +316,24 @@ def main(): auto.setDeviceManager(dm) options = parser.verifyRemoteOptions(options, auto) if (options == None): - print "ERROR: Invalid options specified, use --help for a list of valid options" - sys.exit(1) + print "ERROR: Invalid options specified, use --help for a list of valid options" + sys.exit(1) productPieces = options.remoteProductName.split('.') if (productPieces != None): - auto.setProduct(productPieces[0]) + auto.setProduct(productPieces[0]) else: - auto.setProduct(options.remoteProductName) + auto.setProduct(options.remoteProductName) mochitest = MochiRemote(auto, dm, options) options = parser.verifyOptions(options, mochitest) if (options == None): - sys.exit(1) + sys.exit(1) auto.setServerInfo(options.webServer, options.httpPort, options.sslPort) sys.exit(mochitest.runTests(options)) if __name__ == "__main__": - main() + main() From 735ba7544c9df3216654fb8bcc89951feb1bb9aa Mon Sep 17 00:00:00 2001 From: Clint Talbert Date: Thu, 24 Jun 2010 02:32:01 -0700 Subject: [PATCH 1771/1860] Bug 573478 - Allow remote reftests to start their own httpd.js webserver r=jmaher --- layout/tools/reftest/remotereftest.py | 128 +++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 4 deletions(-) diff --git a/layout/tools/reftest/remotereftest.py b/layout/tools/reftest/remotereftest.py index 225cf19ab409..151f43a0bb95 100644 --- a/layout/tools/reftest/remotereftest.py +++ b/layout/tools/reftest/remotereftest.py @@ -38,10 +38,10 @@ import sys import os import time +import tempfile +# We need to know our current directory so that we can serve our test files from it. SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) -sys.path.append(SCRIPT_DIRECTORY) -#os.chdir(SCRIPT_DIRECTORY) from runreftest import RefTest from runreftest import ReftestOptions @@ -130,6 +130,73 @@ class RemoteOptions(ReftestOptions): #options.utilityPath = options.testRoot + self._automation._product + '/bin' return options +class ReftestServer: + """ Web server used to serve Reftests, for closer fidelity to the real web. + It is virtually identical to the server used in mochitest and will only + be used for running reftests remotely. + Bug xxx has been filed to refactor this wrapper around httpd.js into + it's own class and use it in both remote and non-remote testing. """ + + def __init__(self, automation, options): + self._automation = automation + self._utilityPath = options.utilityPath + self._xrePath = options.xrePath + self._profileDir = options.serverProfilePath + self.webServer = options.remoteWebServer + self.httpPort = options.httpPort + self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server" : self.webServer, "port" : self.httpPort } + + def start(self): + "Run the Refest server, returning the process ID of the server." + + env = self._automation.environment(xrePath = self._xrePath) + env["XPCOM_DEBUG_BREAK"] = "warn" + if self._automation.IS_WIN32: + env["PATH"] = env["PATH"] + ";" + self._xrePath + + args = ["-g", self._xrePath, + "-v", "170", + "-f", "./" + "httpd.js", + "-e", "const _PROFILE_PATH = '%(profile)s';const _SERVER_PORT = '%(port)s'; const _SERVER_ADDR ='%(server)s';" % + {"profile" : self._profileDir.replace('\\', '\\\\'), "port" : self.httpPort, "server" : self.webServer }, + "-f", "./" + "server.js"] + + xpcshell = os.path.join(self._utilityPath, + "xpcshell" + self._automation.BIN_SUFFIX) + self._process = self._automation.Process([xpcshell] + args, env = env) + pid = self._process.pid + if pid < 0: + print "Error starting server." + sys.exit(2) + self._automation.log.info("INFO | remotereftests.py | Server pid: %d", pid) + + def ensureReady(self, timeout): + assert timeout >= 0 + + aliveFile = os.path.join(self._profileDir, "server_alive.txt") + i = 0 + while i < timeout: + if os.path.exists(aliveFile): + break + time.sleep(1) + i += 1 + else: + print "Timed out while waiting for server startup." + self.stop() + sys.exit(1) + + def stop(self): + try: + c = urllib2.urlopen(self.shutdownURL) + c.read() + c.close() + + rtncode = self._process.poll() + if (rtncode == None): + self._process.terminate() + except: + self._process.kill() + class RemoteReftest(RefTest): remoteApp = '' @@ -138,7 +205,57 @@ class RemoteReftest(RefTest): self._devicemanager = devicemanager self.scriptDir = scriptDir self.remoteApp = options.app + self.remoteProfile = options.remoteProfile self.remoteTestRoot = options.remoteTestRoot + if self.automation.IS_DEBUG_BUILD: + self.SERVER_STARTUP_TIMEOUT = 180 + else: + self.SERVER_STARTUP_TIMEOUT = 90 + + def findPath(self, paths, filename = None): + for path in paths: + p = path + if filename: + p = os.path.join(p, filename) + if os.path.exists(self.getFullPath(p)): + return path + return None + + def startWebServer(self, options): + """ Create the webserver on the host and start it up """ + remoteXrePath = options.xrePath + remoteUtilityPath = options.utilityPath + localAutomation = Automation() + + paths = [options.xrePath, localAutomation.DIST_BIN, self.automation._product, os.path.join('..', self.automation._product)] + options.xrePath = self.findPath(paths) + if options.xrePath == None: + print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name) + sys.exit(1) + paths.append("bin") + paths.append(os.path.join("..", "bin")) + + xpcshell = "xpcshell" + if (os.name == "nt"): + xpcshell += ".exe" + + if (options.utilityPath): + paths.insert(0, options.utilityPath) + options.utilityPath = self.findPath(paths, xpcshell) + if options.utilityPath == None: + print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name) + sys.exit(1) + + options.serverProfilePath = tempfile.mkdtemp() + self.server = ReftestServer(localAutomation, options) + self.server.start() + + self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT) + options.xrePath = remoteXrePath + options.utilityPath = remoteUtilityPath + + def stopWebServer(self, options): + self.server.stop() def createReftestProfile(self, options, profileDir): RefTest.createReftestProfile(self, options, profileDir) @@ -169,7 +286,7 @@ class RemoteReftest(RefTest): return path def cleanup(self, profileDir): - self._devicemanager.removeDir(self.remoteProfileDir) + self._devicemanager.removeDir(self.remoteProfile) self._devicemanager.removeDir(self.remoteTestRoot) RefTest.cleanup(self, profileDir) @@ -202,10 +319,13 @@ def main(): if (options.remoteWebServer == "127.0.0.1"): print "Error: remoteWebServer must be non localhost" sys.exit(1) - + + # Start the webserver + reftest.startWebServer(options) #an example manifest name to use on the cli # manifest = "http://" + options.remoteWebServer + "/reftests/layout/reftests/reftest-sanity/reftest.list" reftest.runTests(args[0], options) + reftest.stopWebServer(options) if __name__ == "__main__": main() From 4119bfcdcd163dac2c86bd9efc62f3066d4ea762 Mon Sep 17 00:00:00 2001 From: Blair McBride Date: Thu, 24 Jun 2010 11:35:21 +1200 Subject: [PATCH 1772/1860] Bug 562890 - Preferences button for the add-on should be placed in the entry of the digest view. r=mossop --- .../chrome/mozapps/extensions/extensions.dtd | 2 + .../mozapps/extensions/content/extensions.js | 9 +-- .../mozapps/extensions/content/extensions.xml | 16 +++++ .../extensions/test/browser/Makefile.in | 1 + .../extensions/test/browser/addon_prefs.xul | 5 ++ .../test/browser/browser_bug562890.js | 58 +++++++++++++++++++ 6 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 toolkit/mozapps/extensions/test/browser/addon_prefs.xul create mode 100644 toolkit/mozapps/extensions/test/browser/browser_bug562890.js diff --git a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd index d8d6b25caed9..83ebe633fd42 100644 --- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd +++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd @@ -43,6 +43,8 @@ + + diff --git a/toolkit/mozapps/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js index 6dd8f4f0ca7f..8bc052b8b802 100644 --- a/toolkit/mozapps/extensions/content/extensions.js +++ b/toolkit/mozapps/extensions/content/extensions.js @@ -541,14 +541,15 @@ var gViewController = { } }, - doCommand: function(aCommand) { + doCommand: function(aCommand, aAddon) { if (!this.supportsCommand(aCommand)) return; - var addon = this.currentViewObj.getSelectedAddon(); var cmd = this.commands[aCommand]; - if (!cmd.isEnabled(addon)) + if (!aAddon) + aAddon = this.currentViewObj.getSelectedAddon(); + if (!cmd.isEnabled(aAddon)) return; - cmd.doCommand(addon); + cmd.doCommand(aAddon); }, onEvent: function() {} diff --git a/toolkit/mozapps/extensions/content/extensions.xml b/toolkit/mozapps/extensions/content/extensions.xml index 566de05dbe15..2f28317d9013 100644 --- a/toolkit/mozapps/extensions/content/extensions.xml +++ b/toolkit/mozapps/extensions/content/extensions.xml @@ -723,6 +723,10 @@ + + + document.getAnonymousElementByAttribute(this, "anonid", + "preferences-btn"); + document.getAnonymousElementByAttribute(this, "anonid", "enable-btn"); @@ -946,6 +954,8 @@ + + + + + + diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug562890.js b/toolkit/mozapps/extensions/test/browser/browser_bug562890.js new file mode 100644 index 000000000000..c555cb337323 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_bug562890.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Tests the Preferences button for addons in list view + */ + +function test() { + waitForExplicitFinish(); + + var addonPrefsURI = TESTROOT + "addon_prefs.xul"; + + var gProvider = new MockProvider(); + gProvider.createAddons([{ + id: "test1@tests.mozilla.org", + name: "Test add-on 1", + description: "foo" + }, + { + id: "test2@tests.mozilla.org", + name: "Test add-on 2", + description: "bar", + optionsURL: addonPrefsURI + }]); + + open_manager(null, function(aWindow) { + var addonList = aWindow.document.getElementById("addon-list"); + var addonItem = addonList.childNodes[0]; + var prefsBtn = aWindow.document.getAnonymousElementByAttribute(addonItem, + "anonid", + "preferences-btn"); + is(prefsBtn.hidden, true, "Prefs button should be hidden for addon with no optionsURL set") + + addonItem = addonList.childNodes[1]; + prefsBtn = aWindow.document.getAnonymousElementByAttribute(addonItem, + "anonid", + "preferences-btn"); + is(prefsBtn.hidden, false, "Prefs button should be shown for addon with a optionsURL set") + + Services.ww.registerNotification(function(aSubject, aTopic, aData) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(arguments.callee); + } else if (aTopic == "domwindowopened") { + let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + win.documentURI, addonPrefsURI, "The correct addon pref window should open" + waitForFocus(function() { + win.close(); + aWindow.close(); + finish(); + }, win); + } + }); + + EventUtils.synthesizeMouse(prefsBtn, 2, 2, { }, aWindow); + }); + +} From 65fdc865ff8f3bf4b7a710269179c365babe9a39 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Thu, 24 Jun 2010 13:47:31 +0200 Subject: [PATCH 1773/1860] Bug 573302 - Folder name is not update when create a folder and/or edit name in Properties. r=dietrich --- .../places/content/browserPlacesViews.js | 38 +++++---- .../places/content/editBookmarkOverlay.js | 9 +- .../browser_library_views_liveupdate.js | 39 +++++++-- .../tests/browser/browser_views_liveupdate.js | 84 +++++++++++++------ .../places/src/nsNavHistoryResult.cpp | 5 +- 5 files changed, 118 insertions(+), 57 deletions(-) diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js index f38cfd6fec4c..11a7e3432063 100644 --- a/browser/components/places/content/browserPlacesViews.js +++ b/browser/components/places/content/browserPlacesViews.js @@ -480,6 +480,24 @@ PlacesViewBase.prototype = { } }, + nodeTitleChanged: + function PM_nodeTitleChanged(aPlacesNode, aNewTitle) { + let elt = aPlacesNode._DOMElement; + if (!elt) + throw "aPlacesNode must have _DOMElement set"; + + // There's no UI representation for the root node, thus there's + // nothing to be done when the title changes. + if (elt == this._rootElt) + return; + + // Here we need the . + if (elt.localName == "menupopup") + elt = elt.parentNode; + + elt.label = aNewTitle || PlacesUIUtils.getBestTitle(aPlacesNode); + }, + nodeRemoved: function PVB_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) { let parentElt = aParentPlacesNode._DOMElement; @@ -1128,19 +1146,16 @@ PlacesToolbar.prototype = { if (elt == this._rootElt) return; + PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments); + // Here we need the . if (elt.localName == "menupopup") elt = elt.parentNode; if (elt.parentNode == this._rootElt) { // Node is on the toolbar - elt.label = aNewTitle; this.updateChevron(); } - else { - // Node is within a built menu. - elt.label = aNewTitle || PlacesUIUtils.getBestTitle(aPlacesNode); - } }, nodeReplaced: @@ -1660,19 +1675,6 @@ PlacesMenu.prototype = { this._endMarker--; }, - nodeTitleChanged: function PM_nodeTitleChanged(aPlacesNode, aNewTitle) { - let elt = aPlacesNode._DOMElement; - if (!elt) - throw "aPlacesNode must have _DOMElement set"; - - // There's no UI representation for the root node, thus there's - // nothing to be done when the title changes. - if (elt == this._rootElt) - return; - - elt.label = aNewTitle || PlacesUIUtils.getBestTitle(aPlacesNode); - }, - uninit: function PM_uninit() { this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true); diff --git a/browser/components/places/content/editBookmarkOverlay.js b/browser/components/places/content/editBookmarkOverlay.js index a03c333677cc..e1ff15710222 100644 --- a/browser/components/places/content/editBookmarkOverlay.js +++ b/browser/components/places/content/editBookmarkOverlay.js @@ -846,8 +846,8 @@ var gEditItemOverlay = { function EIO__getFolderMenuItem(aFolderId) { var menupopup = this._folderMenuList.menupopup; - for (var i=0; i < menupopup.childNodes.length; i++) { - if (menupopup.childNodes[i].folderId && + for (let i = 0; i < menupopup.childNodes.length; i++) { + if ("folderId" in menupopup.childNodes[i] && menupopup.childNodes[i].folderId == aFolderId) return menupopup.childNodes[i]; } @@ -1074,8 +1074,9 @@ var gEditItemOverlay = { // menulist has been changed, we need to update the label of its // representing element. var menupopup = this._folderMenuList.menupopup; - for (var i=0; i < menupopup.childNodes.length; i++) { - if (menupopup.childNodes[i].folderId == aItemId) { + for (let i = 0; i < menupopup.childNodes.length; i++) { + if ("folderId" in menupopup.childNodes[i] && + menupopup.childNodes[i].folderId == aItemId) { menupopup.childNodes[i].label = aValue; break; } diff --git a/browser/components/places/tests/browser/browser_library_views_liveupdate.js b/browser/components/places/tests/browser/browser_library_views_liveupdate.js index 3c8df9ba951b..588134c3e523 100644 --- a/browser/components/places/tests/browser/browser_library_views_liveupdate.js +++ b/browser/components/places/tests/browser/browser_library_views_liveupdate.js @@ -96,12 +96,14 @@ function startTest() { PlacesUtils._uri("place:"), bs.DEFAULT_INDEX, "bm2"); + bs.setItemTitle(id, "bm2_edited"); addedBookmarks.push(id); id = bs.insertSeparator(bs.bookmarksMenuFolder, bs.DEFAULT_INDEX); addedBookmarks.push(id); id = bs.createFolder(bs.bookmarksMenuFolder, "bmf", bs.DEFAULT_INDEX); + bs.setItemTitle(id, "bmf_edited"); addedBookmarks.push(id); id = bs.insertBookmark(id, PlacesUtils._uri("http://bmf1.mozilla.org/"), @@ -116,17 +118,20 @@ function startTest() { PlacesUtils._uri("http://tb1.mozilla.org/"), bs.DEFAULT_INDEX, "tb1"); + bs.setItemTitle(id, "tb1_edited"); addedBookmarks.push(id); id = bs.insertBookmark(bs.toolbarFolder, PlacesUtils._uri("place:"), bs.DEFAULT_INDEX, "tb2"); + bs.setItemTitle(id, "tb2_edited"); addedBookmarks.push(id); id = bs.insertSeparator(bs.toolbarFolder, bs.DEFAULT_INDEX); addedBookmarks.push(id); id = bs.createFolder(bs.toolbarFolder, "tbf", bs.DEFAULT_INDEX); + bs.setItemTitle(id, "tbf_edited"); addedBookmarks.push(id); id = bs.insertBookmark(id, PlacesUtils._uri("http://tbf1.mozilla.org/"), @@ -141,17 +146,20 @@ function startTest() { PlacesUtils._uri("http://ub1.mozilla.org/"), bs.DEFAULT_INDEX, "ub1"); + bs.setItemTitle(id, "ub1_edited"); addedBookmarks.push(id); id = bs.insertBookmark(bs.unfiledBookmarksFolder, PlacesUtils._uri("place:"), bs.DEFAULT_INDEX, "ub2"); + bs.setItemTitle(id, "ub2_edited"); addedBookmarks.push(id); id = bs.insertSeparator(bs.unfiledBookmarksFolder, bs.DEFAULT_INDEX); addedBookmarks.push(id); id = bs.createFolder(bs.unfiledBookmarksFolder, "ubf", bs.DEFAULT_INDEX); + bs.setItemTitle(id, "ubf_edited"); addedBookmarks.push(id); id = bs.insertBookmark(id, PlacesUtils._uri("http://ubf1.mozilla.org/"), @@ -260,7 +268,19 @@ var bookmarksObserver = { onBeforeItemRemoved: function PSB_onBeforeItemRemoved(aItemId) {}, onItemVisited: function() {}, onItemChanged: function PSB_onItemChanged(aItemId, aProperty, - aIsAnnotationProperty, aValue) {} + aIsAnnotationProperty, aNewValue) { + if (aProperty == "title") { + let validator = function(aTreeRowIndex) { + let tree = gLibrary.PlacesOrganizer._places; + let cellText = tree.view.getCellText(aTreeRowIndex, + tree.columns.getColumnAt(0)); + return cellText == aNewValue; + } + let [node, index, valid] = getNodeForTreeItem(aItemId, gLibrary.PlacesOrganizer._places, validator); + if (node) // Only visible nodes. + ok(valid, "Title cell value has been correctly updated"); + } + } }; @@ -269,13 +289,17 @@ var bookmarksObserver = { * * @param aItemId * item id of the item to search. - * @returns [node, index] or [null, null] if not found. + * @param aTree + * Tree to search in. + * @param aValidator [optional] + * function to check row validity if found. Defaults to {return true;}. + * @returns [node, index, valid] or [null, null, false] if not found. */ -function getNodeForTreeItem(aItemId, aTree) { +function getNodeForTreeItem(aItemId, aTree, aValidator) { function findNode(aContainerIndex) { if (aTree.view.isContainerEmpty(aContainerIndex)) - return [null, null]; + return [null, null, false]; // The rowCount limit is just for sanity, but we will end looping when // we have checked the last child of this container or we have found node. @@ -284,7 +308,8 @@ function getNodeForTreeItem(aItemId, aTree) { if (node.itemId == aItemId) { // Minus one because we want relative index inside the container. - return [node, i - aTree.view.getParentIndex(i) - 1]; + let valid = aValidator ? aValidator(i) : true; + return [node, i - aTree.view.getParentIndex(i) - 1, valid]; } if (PlacesUtils.nodeIsFolder(node)) { @@ -303,7 +328,7 @@ function getNodeForTreeItem(aItemId, aTree) { if (!aTree.view.hasNextSibling(aContainerIndex + 1, i)) break; } - return [null, null] + return [null, null, false] } // Root node is hidden, so we need to manually walk the first level. @@ -318,5 +343,5 @@ function getNodeForTreeItem(aItemId, aTree) { if (foundNode[0] != null) return foundNode; } - return [null, null]; + return [null, null, false]; } diff --git a/browser/components/places/tests/browser/browser_views_liveupdate.js b/browser/components/places/tests/browser/browser_views_liveupdate.js index 8e3417d55e5a..9d25d3105686 100644 --- a/browser/components/places/tests/browser/browser_views_liveupdate.js +++ b/browser/components/places/tests/browser/browser_views_liveupdate.js @@ -91,24 +91,26 @@ function startTest() { PlacesUtils._uri("http://bm1.mozilla.org/"), bs.DEFAULT_INDEX, "bm1"); - addedBookmarks.push(id); - // Test live update of title. bs.setItemTitle(id, "bm1_edited"); + addedBookmarks.push(id); id = bs.insertBookmark(bs.bookmarksMenuFolder, PlacesUtils._uri("place:"), bs.DEFAULT_INDEX, "bm2"); + bs.setItemTitle(id, "bm2_edited"); addedBookmarks.push(id); id = bs.insertSeparator(bs.bookmarksMenuFolder, bs.DEFAULT_INDEX); addedBookmarks.push(id); id = bs.createFolder(bs.bookmarksMenuFolder, "bmf", bs.DEFAULT_INDEX); + bs.setItemTitle(id, "bmf_edited"); addedBookmarks.push(id); id = bs.insertBookmark(id, PlacesUtils._uri("http://bmf1.mozilla.org/"), bs.DEFAULT_INDEX, "bmf1"); + bs.setItemTitle(id, "bmf1_edited"); addedBookmarks.push(id); bs.moveItem(id, bs.bookmarksMenuFolder, 0); @@ -118,6 +120,7 @@ function startTest() { PlacesUtils._uri("http://tb1.mozilla.org/"), bs.DEFAULT_INDEX, "tb1"); + bs.setItemTitle(id, "tb1_edited"); addedBookmarks.push(id); // Test live update of title. bs.setItemTitle(id, "tb1_edited"); @@ -125,17 +128,20 @@ function startTest() { PlacesUtils._uri("place:"), bs.DEFAULT_INDEX, "tb2"); + bs.setItemTitle(id, "tb2_edited"); addedBookmarks.push(id); id = bs.insertSeparator(bs.toolbarFolder, bs.DEFAULT_INDEX); addedBookmarks.push(id); id = bs.createFolder(bs.toolbarFolder, "tbf", bs.DEFAULT_INDEX); + bs.setItemTitle(id, "tbf_edited"); addedBookmarks.push(id); id = bs.insertBookmark(id, PlacesUtils._uri("http://tbf1.mozilla.org/"), bs.DEFAULT_INDEX, - "bmf1"); + "tbf1"); + bs.setItemTitle(id, "tbf1_edited"); addedBookmarks.push(id); bs.moveItem(id, bs.toolbarFolder, 0); @@ -145,24 +151,26 @@ function startTest() { PlacesUtils._uri("http://ub1.mozilla.org/"), bs.DEFAULT_INDEX, "ub1"); - addedBookmarks.push(id); - // Test live update of title. bs.setItemTitle(id, "ub1_edited"); + addedBookmarks.push(id); id = bs.insertBookmark(bs.unfiledBookmarksFolder, PlacesUtils._uri("place:"), bs.DEFAULT_INDEX, "ub2"); + bs.setItemTitle(id, "ub2_edited"); addedBookmarks.push(id); id = bs.insertSeparator(bs.unfiledBookmarksFolder, bs.DEFAULT_INDEX); addedBookmarks.push(id); id = bs.createFolder(bs.unfiledBookmarksFolder, "ubf", bs.DEFAULT_INDEX); + bs.setItemTitle(id, "ubf_edited"); addedBookmarks.push(id); id = bs.insertBookmark(id, PlacesUtils._uri("http://ubf1.mozilla.org/"), bs.DEFAULT_INDEX, "bubf1"); + bs.setItemTitle(id, "bubf1_edited"); addedBookmarks.push(id); bs.moveItem(id, bs.unfiledBookmarksFolder, 0); @@ -251,7 +259,8 @@ var bookmarksObserver = { onBeforeItemRemoved: function PSB_onBeforeItemRemoved(aItemId) {}, onItemVisited: function() {}, - onItemChanged: function PSB_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aValue) { + onItemChanged: function PSB_onItemChanged(aItemId, aProperty, + aIsAnnotationProperty, aNewValue) { if (aProperty !== "title") return; @@ -259,10 +268,24 @@ var bookmarksObserver = { ok(views.length > 0, "Found affected views (" + views.length + "): " + views); // Check that item has been moved in the correct position. + let validator = function(aElementOrTreeIndex) { + if (typeof(aElementOrTreeIndex) == "number") { + var sidebar = document.getElementById("sidebar"); + var tree = sidebar.contentDocument.getElementById("bookmarks-view"); + let cellText = tree.view.getCellText(aElementOrTreeIndex, + tree.columns.getColumnAt(0)); + return cellText == aNewValue; + } + else { + return aElementOrTreeIndex.label == aNewValue; + } + }; + for (var i = 0; i < views.length; i++) { - var [node, index] = searchItemInView(aItemId, views[i]); - isnot(node, null, "Found new Places node in " + views[i]); - is(node.title, aValue, "Node has correct title: " + aValue); + var [node, index, valid] = searchItemInView(aItemId, views[i], validator); + isnot(node, null, "Found changed Places node in " + views[i]); + is(node.title, aNewValue, "Node has correct title: " + aNewValue); + ok(valid, "Node element has correct label: " + aNewValue); } } }; @@ -274,19 +297,21 @@ var bookmarksObserver = { * item id of the item to search. * @param aView * either "toolbar", "menu" or "sidebar" - * @returns [node, index] or [null, null] if not found. + * @param aValidator + * function to check validity of the found node element. + * @returns [node, index, valid] or [null, null, false] if not found. */ -function searchItemInView(aItemId, aView) { +function searchItemInView(aItemId, aView, aValidator) { switch (aView) { case "toolbar": - return getNodeForToolbarItem(aItemId); + return getNodeForToolbarItem(aItemId, aValidator); case "menu": - return getNodeForMenuItem(aItemId); + return getNodeForMenuItem(aItemId, aValidator); case "sidebar": - return getNodeForSidebarItem(aItemId); + return getNodeForSidebarItem(aItemId, aValidator); } - return [null, null]; + return [null, null, false]; } /** @@ -296,7 +321,7 @@ function searchItemInView(aItemId, aView) { * item id of the item to search. * @returns [node, index] or [null, null] if not found. */ -function getNodeForToolbarItem(aItemId) { +function getNodeForToolbarItem(aItemId, aValidator) { var toolbar = document.getElementById("PlacesToolbarItems"); function findNode(aContainer) { @@ -310,8 +335,10 @@ function getNodeForToolbarItem(aItemId) { continue; } - if (child._placesNode.itemId == aItemId) - return [child._placesNode, i - staticNodes]; + if (child._placesNode.itemId == aItemId) { + let valid = aValidator ? aValidator(child) : true; + return [child._placesNode, i - staticNodes, valid]; + } // Don't search in queries, they could contain our item in a // different position. Search only folders @@ -337,7 +364,7 @@ function getNodeForToolbarItem(aItemId) { * item id of the item to search. * @returns [node, index] or [null, null] if not found. */ -function getNodeForMenuItem(aItemId) { +function getNodeForMenuItem(aItemId, aValidator) { var menu = document.getElementById("bookmarksMenu"); function findNode(aContainer) { @@ -351,8 +378,10 @@ function getNodeForMenuItem(aItemId) { continue; } - if (child._placesNode.itemId == aItemId) - return [child._placesNode, i - staticNodes]; + if (child._placesNode.itemId == aItemId) { + let valid = aValidator ? aValidator(child) : true; + return [child._placesNode, i - staticNodes, valid]; + } // Don't search in queries, they could contain our item in a // different position. Search only folders @@ -366,7 +395,7 @@ function getNodeForMenuItem(aItemId) { return foundNode; } } - return [null, null]; + return [null, null, false]; } return findNode(menu.lastChild); @@ -379,13 +408,13 @@ function getNodeForMenuItem(aItemId) { * item id of the item to search. * @returns [node, index] or [null, null] if not found. */ -function getNodeForSidebarItem(aItemId) { +function getNodeForSidebarItem(aItemId, aValidator) { var sidebar = document.getElementById("sidebar"); var tree = sidebar.contentDocument.getElementById("bookmarks-view"); function findNode(aContainerIndex) { if (tree.view.isContainerEmpty(aContainerIndex)) - return [null, null]; + return [null, null, false]; // The rowCount limit is just for sanity, but we will end looping when // we have checked the last child of this container or we have found node. @@ -394,7 +423,8 @@ function getNodeForSidebarItem(aItemId) { if (node.itemId == aItemId) { // Minus one because we want relative index inside the container. - return [node, i - tree.view.getParentIndex(i) - 1]; + let valid = aValidator ? aValidator(i) : true; + return [node, i - tree.view.getParentIndex(i) - 1, valid]; } if (PlacesUtils.nodeIsFolder(node)) { @@ -413,7 +443,7 @@ function getNodeForSidebarItem(aItemId) { if (!tree.view.hasNextSibling(aContainerIndex + 1, i)) break; } - return [null, null] + return [null, null, false] } // Root node is hidden, so we need to manually walk the first level. @@ -428,7 +458,7 @@ function getNodeForSidebarItem(aItemId) { if (foundNode[0] != null) return foundNode; } - return [null, null]; + return [null, null, false]; } /** diff --git a/toolkit/components/places/src/nsNavHistoryResult.cpp b/toolkit/components/places/src/nsNavHistoryResult.cpp index 34e1efc840dc..7cbd7c0b7065 100644 --- a/toolkit/components/places/src/nsNavHistoryResult.cpp +++ b/toolkit/components/places/src/nsNavHistoryResult.cpp @@ -3162,7 +3162,10 @@ nsNavHistoryQueryResultNode::OnItemChanged(PRInt64 aItemId, } } else { - NS_WARNING("history observers should not get OnItemChanged, but should get the corresponding history notifications instead"); + // Some node could observe both bookmarks and history. But a node observing + // only history should never get a bookmark notification. + NS_WARN_IF_FALSE(mResult && (mResult->mIsAllBookmarksObserver || mResult->mIsBookmarkFolderObserver), + "history observers should not get OnItemChanged, but should get the corresponding history notifications instead"); } return nsNavHistoryResultNode::OnItemChanged(aItemId, aProperty, From 18fd2c87af02f1821c49479db6ae2cd4522e31bf Mon Sep 17 00:00:00 2001 From: Gervase Markham Date: Thu, 24 Jun 2010 13:09:05 +0100 Subject: [PATCH 1774/1860] Bug 564214 - Add . to IDN whitelist. --- modules/libpref/src/init/all.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 11dd63708ccb..0240dbcc8e68 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -785,6 +785,8 @@ pref("network.IDN.whitelist.tw", true); pref("network.IDN.whitelist.vn", true); // IDN ccTLDs +// ae, UAE, . +pref("network.IDN.whitelist.xn--mgbaam7a8h", true); // sa, Saudi Arabia, . pref("network.IDN.whitelist.xn--mgberp4a5d4ar", true); // ru, Russian Federation, . From 2d72e67d669612cfa1c0335ba006468b21ab5c64 Mon Sep 17 00:00:00 2001 From: Jonathan Kew Date: Thu, 24 Jun 2010 13:11:50 +0100 Subject: [PATCH 1775/1860] bug 550163 - part 20 - prefer Calibri font (on Win7) in reftest to avoid fallback to visible CGJ glyph. r=roc --- layout/reftests/text/cgj-01-ref.html | 2 ++ layout/reftests/text/cgj-01.html | 2 ++ 2 files changed, 4 insertions(+) diff --git a/layout/reftests/text/cgj-01-ref.html b/layout/reftests/text/cgj-01-ref.html index ba606575ce27..07cbbe1060bb 100644 --- a/layout/reftests/text/cgj-01-ref.html +++ b/layout/reftests/text/cgj-01-ref.html @@ -9,6 +9,8 @@ diff --git a/layout/reftests/text/cgj-01.html b/layout/reftests/text/cgj-01.html index ed55f74c3000..6382064f5b04 100644 --- a/layout/reftests/text/cgj-01.html +++ b/layout/reftests/text/cgj-01.html @@ -9,6 +9,8 @@ From 753899261afd102e5853e82e71a00a58d5d81663 Mon Sep 17 00:00:00 2001 From: Jonathan Kew Date: Thu, 24 Jun 2010 13:11:59 +0100 Subject: [PATCH 1776/1860] bug 574076 - correctly get width of digit glyph with dwrite. r=bas --- gfx/thebes/src/gfxDWriteFonts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gfx/thebes/src/gfxDWriteFonts.cpp b/gfx/thebes/src/gfxDWriteFonts.cpp index d39751de3137..21525b2343eb 100644 --- a/gfx/thebes/src/gfxDWriteFonts.cpp +++ b/gfx/thebes/src/gfxDWriteFonts.cpp @@ -206,7 +206,7 @@ gfxDWriteFont::ComputeMetrics() fontMetrics.designUnitsPerEm) * mAdjustedSize; } ucs = L'0'; - if (FAILED(mFontFace->GetGlyphIndicesA(&ucs, 1, &glyph)) && + if (SUCCEEDED(mFontFace->GetGlyphIndicesA(&ucs, 1, &glyph)) && SUCCEEDED(mFontFace->GetDesignGlyphMetrics(&glyph, 1, &metrics))) { mMetrics.zeroOrAveCharWidth = ((gfxFloat)metrics.advanceWidth / From 78e554c98de204aceaf86ff3b6f3d03cc89f3032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A3o=20Gottwald?= Date: Thu, 24 Jun 2010 16:29:02 +0200 Subject: [PATCH 1777/1860] Bug 563730 - Implement pinTab, unpinTab. r=gavin --- browser/base/content/browser.css | 14 ++- browser/base/content/tabbrowser.css | 4 +- browser/base/content/tabbrowser.xml | 106 ++++++++++++++++-- browser/base/content/test/Makefile.in | 1 + .../base/content/test/browser_pinnedTabs.js | 48 ++++++++ .../themes/gnomestripe/browser/browser.css | 8 ++ browser/themes/winstripe/browser/browser.css | 5 +- toolkit/content/widgets/scrollbox.xml | 10 ++ 8 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 browser/base/content/test/browser_pinnedTabs.js diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index bd8fe571185d..5ebbed14169b 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -22,6 +22,7 @@ tabbrowser { .tabbrowser-tab { -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tab"); + -moz-box-flex: 100; } .tabbrowser-tab:not([fadein]) { @@ -29,7 +30,7 @@ tabbrowser { min-width: 1px !important; } -.tabbrowser-tab[fadein] { +.tabbrowser-tab[fadein]:not([pinned]) { -moz-transition: min-width .2s ease-out, max-width .25s ease-out; } @@ -45,6 +46,17 @@ tabbrowser { -moz-transition: opacity .25s; } +.tabbrowser-tab[pinned] { + position: fixed; + -moz-box-flex: 0; + min-width: 0 !important; + max-width: none !important; +} + +.tabbrowser-tab[pinned] > .tab-text { + display: none; +} + #alltabs-popup { -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-alltabs-popup"); } diff --git a/browser/base/content/tabbrowser.css b/browser/base/content/tabbrowser.css index e449e8214eed..4581c775e343 100644 --- a/browser/base/content/tabbrowser.css +++ b/browser/base/content/tabbrowser.css @@ -11,8 +11,8 @@ display: none; } -.tabbrowser-tabs:not([closebuttons="noclose"]):not([closebuttons="closeatend"]) > .tabbrowser-tab[selected="true"] > .tab-close-button, -.tabbrowser-tabs[closebuttons="alltabs"] > .tabbrowser-tab > .tab-close-button { +.tabbrowser-tabs:not([closebuttons="noclose"]):not([closebuttons="closeatend"]) > .tabbrowser-tab[selected="true"]:not([pinned]) > .tab-close-button, +.tabbrowser-tabs[closebuttons="alltabs"] > .tabbrowser-tab:not([pinned]) > .tab-close-button { display: -moz-box; } diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 6096170a4e1b..d04eca66900c 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -158,6 +158,41 @@ false + + + + + + + + + + + + + + @@ -1178,7 +1213,6 @@ t.style.maxWidth = this.tabContainer.mTabMaxWidth + "px"; t.style.minWidth = this.tabContainer.mTabMinWidth + "px"; t.width = 0; - t.setAttribute("flex", "100"); t.setAttribute("validate", "never"); t.setAttribute("onerror", "this.removeAttribute('image');"); t.className = "tabbrowser-tab"; @@ -1204,7 +1238,7 @@ * order to make sure the same set of tabs is visible before and * after the new tab is added. See bug 508816. */ - this.tabContainer.mTabstrip.scrollByPixels(this.tabs[0].clientWidth); + this.tabContainer.mTabstrip.scrollByPixels(t.clientWidth); } // invalidate cache, because tabContainer is about to change @@ -1525,6 +1559,8 @@ if (browser == this.mCurrentBrowser) this.mCurrentBrowser = null; + var wasPinned = aTab.pinned; + // Invalidate browsers cache, as the tab is removed from the // tab container. this._browsers = null; @@ -1536,9 +1572,13 @@ for (let i = aTab._tPos; i < this.tabs.length; i++) this.tabs[i]._tPos = i; - // update tab close buttons state - if (!this._windowIsClosing) + if (!this._windowIsClosing) { + if (wasPinned) + this.tabContainer._positionPinnedTabs(); + + // update tab close buttons state this.tabContainer.adjustTabstrip(); + } // update first-tab/last-tab/beforeselected/afterselected attributes this.selectedTab._selected = true; @@ -1844,8 +1884,16 @@ if (oldPosition == aIndex) return; + // Don't allow mixing pinned and unpinned tabs. + if (aTab.pinned) + aIndex = Math.min(aIndex, this._numPinnedTabs - 1); + else + aIndex = Math.max(aIndex, this._numPinnedTabs); + if (oldPosition == aIndex) + return; + this._lastRelatedTab = null; - this._browsers = null; // invalidate cache + this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(aTab._tPos, 1)[0]); this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(aTab._tPos, 1)[0]); @@ -1864,6 +1912,9 @@ this.mCurrentTab._selected = true; this.tabContainer.mTabstrip.ensureElementIsVisible(this.mCurrentTab, false); + if (aTab.pinned) + this.tabContainer._positionPinnedTabs(); + var evt = document.createEvent("UIEvents"); evt.initUIEvent("TabMove", true, false, window, oldPosition); aTab.dispatchEvent(evt); @@ -2394,6 +2445,12 @@ return document.getBindingParent(this).childNodes; ]]> + + + + return !tab.pinned; + + @@ -2403,6 +2460,7 @@ var tabs = document.getBindingParent(this); tabs.removeAttribute("overflow"); + tabs._positionPinnedTabs(); ]]> @@ -2459,7 +2517,6 @@ tab.style.minWidth = this.mTabMinWidth + "px"; tab.style.maxWidth = this.mTabMaxWidth + "px"; tab.width = 0; - tab.setAttribute("flex", "100"); tab.setAttribute("crop", "end"); tab.setAttribute("validate", "never"); tab.setAttribute("onerror", "this.removeAttribute('image');"); @@ -2574,10 +2631,13 @@ case 1: if (this.childNodes.length == 1 && this._closeWindowWithLastTab) this.setAttribute("closebuttons", "noclose"); - else if (this.firstChild.getBoundingClientRect().width > this.mTabClipWidth) - this.setAttribute("closebuttons", "alltabs"); - else - this.setAttribute("closebuttons", "activetab"); + else { + let tab = this.childNodes.item(this.tabbrowser._numPinnedTabs); + if (tab && tab.getBoundingClientRect().width > this.mTabClipWidth) + this.setAttribute("closebuttons", "alltabs"); + else + this.setAttribute("closebuttons", "activetab"); + } break; case 2: case 3: @@ -2611,6 +2671,21 @@ ]]> + + = 0; i--) { + let tab = this.childNodes[i]; + width += tab.scrollWidth; + tab.style.MozMarginStart = - (width + scrollButtonWidth) + "px"; + } + this.style.MozMarginStart = width + "px"; + this.mTabstrip.ensureElementIsVisible(this.selectedItem, false); + ]]> + + + + + return this.getAttribute("pinned") == "true"; + + + false null diff --git a/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in index b79fd7984bd8..98e3c3f14db1 100644 --- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -146,6 +146,7 @@ _BROWSER_FILES = \ browser_overflowScroll.js \ browser_pageInfo.js \ browser_page_style_menu.js \ + browser_pinnedTabs.js \ browser_plainTextLinks.js \ browser_pluginnotification.js \ browser_popupUI.js \ diff --git a/browser/base/content/test/browser_pinnedTabs.js b/browser/base/content/test/browser_pinnedTabs.js new file mode 100644 index 000000000000..07cd9d966a37 --- /dev/null +++ b/browser/base/content/test/browser_pinnedTabs.js @@ -0,0 +1,48 @@ +var tabs; + +function index(tab) Array.indexOf(gBrowser.tabs, tab); + +function indexTest(tab, expectedIndex, msg) { + var diag = "tab " + tab + " should be at index " + expectedIndex; + if (msg) + msg = msg + " (" + diag + ")"; + else + msg = diag; + is(index(tabs[tab]), expectedIndex, msg); +} + +function test() { + tabs = [gBrowser.selectedTab, gBrowser.addTab(), gBrowser.addTab(), gBrowser.addTab()]; + indexTest(0, 0); + indexTest(1, 1); + indexTest(2, 2); + indexTest(3, 3); + + gBrowser.pinTab(tabs[3]); + indexTest(0, 1); + indexTest(1, 2); + indexTest(2, 3); + indexTest(3, 0); + + gBrowser.pinTab(tabs[1]); + indexTest(0, 2); + indexTest(1, 1); + indexTest(2, 3); + indexTest(3, 0); + + gBrowser.moveTabTo(tabs[3], 3); + indexTest(3, 1, "shouldn't be able to mix a pinned tab into normal tabs"); + + gBrowser.moveTabTo(tabs[2], 0); + indexTest(2, 2, "shouldn't be able to mix a normal tab into pinned tabs"); + + gBrowser.unpinTab(tabs[1]); + indexTest(1, 1, "unpinning a tab should move a tab to the start of normal tabs"); + + gBrowser.unpinTab(tabs[3]); + indexTest(3, 0, "unpinning a tab should move a tab to the start of normal tabs"); + + gBrowser.removeTab(tabs[1]); + gBrowser.removeTab(tabs[2]); + gBrowser.removeTab(tabs[3]); +} diff --git a/browser/themes/gnomestripe/browser/browser.css b/browser/themes/gnomestripe/browser/browser.css index 02a2df230e3a..a22d6744961b 100644 --- a/browser/themes/gnomestripe/browser/browser.css +++ b/browser/themes/gnomestripe/browser/browser.css @@ -1173,6 +1173,10 @@ statusbarpanel#statusbar-display { min-height: 28px; } +.tabbrowser-tab[pinned] + .tabbrowser-tab:not([pinned]) { + -moz-margin-start: 0; +} + .tab-icon-image { width: 16px; height: 16px; @@ -1182,6 +1186,10 @@ statusbarpanel#statusbar-display { -moz-image-region: rect(0px, 16px, 16px, 0px); } +.tabbrowser-tab[pinned] > .tab-icon-image { + margin: 2px 0 0; +} + .tabbrowser-tab[busy] > .tab-icon-image { list-style-image: url("chrome://browser/skin/tabbrowser/progress.png") !important; -moz-image-region: rect(0, 16px, 16px, 0); diff --git a/browser/themes/winstripe/browser/browser.css b/browser/themes/winstripe/browser/browser.css index 07d92f75df81..a45383cba9cd 100644 --- a/browser/themes/winstripe/browser/browser.css +++ b/browser/themes/winstripe/browser/browser.css @@ -1163,13 +1163,16 @@ richlistitem[type="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-i } .tab-icon-image { - -moz-margin-end: 3px; width: 16px; height: 16px; list-style-image: url("chrome://global/skin/icons/folder-item.png"); -moz-image-region: rect(0px, 16px, 16px, 0px); } +.tabbrowser-tab:not([pinned]) > .tab-icon-image { + -moz-margin-end: 3px; +} + /* tabbrowser-tab focus ring */ .tabbrowser-tab > .tab-text { border: 1px dotted transparent; diff --git a/toolkit/content/widgets/scrollbox.xml b/toolkit/content/widgets/scrollbox.xml index 540cb5716f12..be2fb23aaecf 100644 --- a/toolkit/content/widgets/scrollbox.xml +++ b/toolkit/content/widgets/scrollbox.xml @@ -165,10 +165,20 @@ null + + + + return true; + + + Date: Thu, 24 Jun 2010 16:30:46 +0200 Subject: [PATCH 1778/1860] Bug 571992 - Make tabs on top be the default state on Windows. r=gavin --- browser/base/content/browser.xul | 3 +++ 1 file changed, 3 insertions(+) diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index e25c35265e8a..57ba4a62f8ea 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -506,6 +506,9 @@ defaultmode="icons" mode="icons" #ifdef WINCE defaulticonsize="small" iconsize="small" +#endif +#ifdef XP_WIN + tabsontop="true" #endif persist="tabsontop"> From 8ef30e6050a18fe29d1dd295c3e2ae69927f0487 Mon Sep 17 00:00:00 2001 From: Gavin Sharp Date: Thu, 24 Jun 2010 00:54:44 -0400 Subject: [PATCH 1779/1860] Bug 574228: PopupNotifications.show() secondaryActions argument should be optional, r=dtownsend --- .../content/test/browser_popupNotification.js | 17 ++++++++++++++++- toolkit/content/PopupNotifications.jsm | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/browser/base/content/test/browser_popupNotification.js b/browser/base/content/test/browser_popupNotification.js index bd9db76c9566..7614ed409b13 100644 --- a/browser/base/content/test/browser_popupNotification.js +++ b/browser/base/content/test/browser_popupNotification.js @@ -292,7 +292,7 @@ var tests = [ // Test two notifications with different anchors { // Test #9 run: function () { - this.notifyObj = new basicNotification(), + this.notifyObj = new basicNotification(); this.firstNotification = showNotification(this.notifyObj); this.notifyObj2 = new basicNotification(); this.notifyObj2.id += "-2"; @@ -310,6 +310,21 @@ var tests = [ this.firstNotification.remove(); } }, + // Test optional params + { // Test #10 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.secondaryActions = undefined; + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + this.notification.remove(); + } + }, ]; function showNotification(notifyObj) { diff --git a/toolkit/content/PopupNotifications.jsm b/toolkit/content/PopupNotifications.jsm index f15510dd7ea7..b51c37812da1 100644 --- a/toolkit/content/PopupNotifications.jsm +++ b/toolkit/content/PopupNotifications.jsm @@ -179,7 +179,7 @@ PopupNotifications.prototype = { throw "PopupNotifications_show: invalid message"; if (mainAction && isInvalidAction(mainAction)) throw "PopupNotifications_show: invalid mainAction"; - if (secondaryActions.some(isInvalidAction)) + if (secondaryActions && secondaryActions.some(isInvalidAction)) throw "PopupNotifications_show: invalid secondaryActions"; let notification = new Notification(id, message, anchorID, mainAction, From 1b82e69235b05f0a2691a793f3355678a0f1dced Mon Sep 17 00:00:00 2001 From: Gavin Sharp Date: Thu, 24 Jun 2010 00:55:03 -0400 Subject: [PATCH 1780/1860] Bug 574227: don't stretch popup notification icons to full height, r=dtownsend --- toolkit/content/widgets/notification.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/content/widgets/notification.xml b/toolkit/content/widgets/notification.xml index 9276f486c716..78c5ab60415d 100644 --- a/toolkit/content/widgets/notification.xml +++ b/toolkit/content/widgets/notification.xml @@ -467,7 +467,7 @@ - + From 25f8757cc66fe7850503e4f06160eea7b886969c Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Thu, 24 Jun 2010 10:08:43 -0700 Subject: [PATCH 1781/1860] Bug 544817 - Create Bookmarks Widget with placement dependent on Bookmarks Bar status. r=dao --- browser/base/content/browser-menubar.inc | 6 +- browser/base/content/browser-places.js | 303 +++++++++++------- browser/base/content/browser.css | 7 + browser/base/content/browser.js | 7 + browser/base/content/browser.xul | 72 ++++- .../places/content/browserPlacesViews.js | 2 +- browser/components/places/content/menu.xml | 10 + .../components/places/src/PlacesUIUtils.jsm | 4 +- .../browser_drag_bookmarks_on_toolbar.js | 16 +- .../tests/browser/browser_views_liveupdate.js | 11 + .../locales/en-US/chrome/browser/browser.dtd | 6 + .../themes/gnomestripe/browser/browser.css | 22 +- browser/themes/pinstripe/browser/browser.css | 22 +- browser/themes/winstripe/browser/browser.css | 23 +- 14 files changed, 367 insertions(+), 144 deletions(-) diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc index 3656c58b04e5..58fecb836b78 100644 --- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -524,9 +524,9 @@ + ondragenter="PlacesMenuDNDHandler.onDragEnter(event);" + ondragover="PlacesMenuDNDHandler.onDragOver(event);" + ondrop="PlacesMenuDNDHandler.onDrop(event);"> element during a drag. + * @param event + * The DragEnter event that spawned the opening. + */ + onDragEnter: function PMDH_onDragEnter(event) { + // Opening menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) + return; + + this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._loadTimer.initWithCallback(function() { + PlacesMenuDNDHandler._loadTimer = null; + event.target.lastChild.setAttribute("autoopened", "true"); + event.target.lastChild.showPopup(event.target.lastChild); + }, this._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Handles dragleave on the element. + * @returns true if the element is a container element (menu or + * menu-toolbarbutton), false otherwise. + */ + onDragLeave: function PMDH_onDragLeave(event) { + // Closing menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) + return; + + if (this._loadTimer) { + this._loadTimer.cancel(); + this._loadTimer = null; + } + let closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + closeTimer.initWithCallback(function() { + let node = PlacesControllerDragHelper.currentDropTarget; + let inHierarchy = false; + while (node && !inHierarchy) { + inHierarchy = node == event.target; + node = node.parentNode; + } + if (!inHierarchy && event.target.lastChild && + event.target.lastChild.hasAttribute("autoopened")) { + event.target.lastChild.removeAttribute("autoopened"); + event.target.lastChild.hidePopup(); + } + }, this._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); + }, + + /** + * Determines if a XUL element represents a static container. + * @returns true if the element is a container element (menu or + *` menu-toolbarbutton), false otherwise. + */ + _isStaticContainer: function PMDH__isContainer(node) { + let isMenu = node.localName == "menu" || + (node.localName == "toolbarbutton" && + node.getAttribute("type") == "menu"); + let isStatic = !("_placesNode" in node) && node.lastChild && + node.lastChild.hasAttribute("placespopup") && + !node.parentNode.hasAttribute("placespopup"); + return isMenu && isStatic; + }, + + /** + * Called when the user drags over the element. + * @param event + * The DragOver event. + */ + onDragOver: function PMDH_onDragOver(event) { let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX, Ci.nsITreeView.DROP_ON); @@ -894,7 +967,12 @@ let BookmarksMenuDropHandler = { event.stopPropagation(); }, - onDrop: function BMDH_onDrop(event) { + /** + * Called when the user drops on the element. + * @param event + * The Drop event. + */ + onDrop: function PMDH_onDrop(event) { // Put the item at the end of bookmark menu. let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX, @@ -904,107 +982,6 @@ let BookmarksMenuDropHandler = { } }; -/** - * Handles special drag and drop functionality for menus on the Bookmarks - * Toolbar and Bookmarks Menu. - */ -var PlacesMenuDNDController = { - _springLoadDelay: 350, // milliseconds - - /** - * All Drag Timers set for the Places UI - */ - _timers: { }, - - /** - * Called when the user drags over the Bookmarks top level element. - * @param event - * The DragEnter event that spawned the opening. - */ - onBookmarksMenuDragEnter: function PMDC_onDragEnter(event) { - if ("loadTime" in this._timers) - return; - - this._setDragTimer("loadTime", this._openBookmarksMenu, - this._springLoadDelay, [event]); - }, - - /** - * Creates a timer that will fire during a drag and drop operation. - * @param id - * The identifier of the timer being set - * @param callback - * The function to call when the timer "fires" - * @param delay - * The time to wait before calling the callback function - * @param args - * An array of arguments to pass to the callback function - */ - _setDragTimer: function PMDC__setDragTimer(id, callback, delay, args) { - if (!this._dragSupported) - return; - - // Cancel this timer if it's already running. - if (id in this._timers) - this._timers[id].cancel(); - - /** - * An object implementing nsITimerCallback that calls a user-supplied - * method with the specified args in the context of the supplied object. - */ - function Callback(object, method, args) { - this._method = method; - this._args = args; - this._object = object; - } - Callback.prototype = { - notify: function C_notify(timer) { - this._method.apply(this._object, this._args); - } - }; - - var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - timer.initWithCallback(new Callback(this, callback, args), delay, - timer.TYPE_ONE_SHOT); - this._timers[id] = timer; - }, - - /** - * Determines if a XUL element represents a container in the Bookmarks system - * @returns true if the element is a container element (menu or - *` menu-toolbarbutton), false otherwise. - */ - _isContainer: function PMDC__isContainer(node) { - return node.localName == "menu" || - (node.localName == "toolbarbutton" && - node.getAttribute("type") == "menu"); - }, - - /** - * Opens the Bookmarks Menu when it is dragged over. (This is special-cased, - * since the toplevel Bookmarks is not a member of an existing places - * container, as folders on the personal toolbar or submenus are. - * @param event - * The DragEnter event that spawned the opening. - */ - _openBookmarksMenu: function PMDC__openBookmarksMenu(event) { - if ("loadTime" in this._timers) - delete this._timers.loadTime; - if (event.target.id == "bookmarksMenu") { - // If this is the bookmarks menu, tell its menupopup child to show. - event.target.lastChild.setAttribute("autoopened", "true"); - event.target.lastChild.showPopup(event.target.lastChild); - } - }, - - // Whether or not drag and drop to menus is supported on this platform - // Dragging in menus is disabled on OS X due to various repainting issues. -#ifdef XP_MACOSX - _dragSupported: false -#else - _dragSupported: true -#endif -}; var PlacesStarButton = { init: function PSB_init() { @@ -1019,13 +996,7 @@ var PlacesStarButton = { PlacesUtils.bookmarks.removeObserver(this); }, - QueryInterface: function PSB_QueryInterface(aIID) { - if (aIID.equals(Ci.nsINavBookmarkObserver) || - aIID.equals(Ci.nsISupports)) - return this; - - throw Cr.NS_NOINTERFACE; - }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]), _starred: false, _batching: false, @@ -1086,10 +1057,11 @@ var PlacesStarButton = { this.updateState(); }, - onItemVisited: function() { }, - onItemMoved: function() { } + onItemVisited: function() {}, + onItemMoved: function() {} }; + // This object handles the initlization and uninitlization of the bookmarks // toolbar. updateState is called when the browser window is opened and // after closing the toolbar customization dialog. @@ -1121,3 +1093,94 @@ let PlacesToolbarHelper = { } } }; + + +// Handles the bookmarks menu button shown when the main menubar is hidden. +let BookmarksMenuButton = { + get button() { + delete this.button; + return this.button = document.getElementById("bookmarks-menu-button"); + }, + + get navbarButtonContainer() { + delete this.navbarButtonContainer; + return this.navbarButtonContainer = + document.getElementById("bookmarks-menu-button-container"); + }, + + get personalToolbar() { + delete this.personalToolbar; + return this.personalToolbar = document.getElementById("PersonalToolbar"); + }, + + get bookmarksToolbarItem() { + return document.getElementById("personal-bookmarks"); + }, + + init: function BMB_init() { + this.updatePosition(); + + // Any other stuff that does not regard the button itself should be + // handled in the onPopupShowing handler, so it does not hit Ts. + }, + + _popupInitialized: false, + _popupNeedsUpdating: true, + onPopupShowing: function BMB_onPopupShowing(event) { + if (!this._popupNeedsUpdating) + return; + this._popupNeedsUpdating = false; + + let viewToolbar = document.getElementById("BMB_viewBookmarksToolbar"); + if (!this._popupInitialized) { + // First popupshowing event, initialize immutable attributes. + this._popupInitialized = true; + // Update View bookmarks toolbar checkbox menuitem. + viewToolbar.setAttribute("toolbarindex", + Array.indexOf(gNavToolbox.childNodes, + this.personalToolbar)); + + // Need to set the label on Unsorted Bookmarks menu. + let unsortedBookmarksElt = + document.getElementById("BMB_unsortedBookmarksFolderMenu"); + unsortedBookmarksElt.label = + PlacesUtils.getString("UnsortedBookmarksFolderTitle"); + } + + // Update View Bookmarks Toolbar checkbox menuitem. + viewToolbar.setAttribute("checked", !this.personalToolbar.collapsed); + + // Hide Bookmarks Toolbar menu if the button is next to the bookmarks + // toolbar item, show them otherwise. + let bookmarksToolbarElt = + document.getElementById("BMB_bookmarksToolbarFolderMenu"); + bookmarksToolbarElt.collapsed = + this.button.parentNode == this.bookmarksToolbarItem; + }, + + updatePosition: function BMB_updatePosition() { + this._popupNeedsUpdating = true; + + let bookmarksToolbarItem = this.bookmarksToolbarItem; + if (isElementVisible(bookmarksToolbarItem)) { + bookmarksToolbarItem.appendChild(this.button); + this.button.classList.add("bookmark-item"); + this.button.classList.remove("toolbarbutton-1"); + } + else { + this.navbarButtonContainer.appendChild(this.button); + this.button.classList.remove("bookmark-item"); + this.button.classList.add("toolbarbutton-1"); + } + }, + + customizeStart: function BMB_customizeStart() { + var bmToolbarItem = this.bookmarksToolbarItem; + if (this.button.parentNode == bmToolbarItem) + bmToolbarItem.removeChild(this.button); + }, + + customizeDone: function BMB_customizeDone() { + this.updatePosition(); + } +}; diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index 5ebbed14169b..af602d34dd18 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -199,6 +199,13 @@ toolbarbutton.bookmark-item { max-width: 13em; } +%ifdef MENUBAR_CAN_AUTOHIDE +#toolbar-menubar:not([autohide="true"]) ~ #nav-bar > #bookmarks-menu-button-container, +#toolbar-menubar:not([autohide="true"]) ~ toolbar > #personal-bookmarks > #bookmarks-menu-button { + display: none; +} +%endif + #editBMPanel_tagsSelector { /* override default listbox width from xul.css */ width: auto; diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 3294d0f3f7ec..a2c67ed8504c 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1098,6 +1098,8 @@ function BrowserStartup() { TabsOnTop.syncCommand(); + BookmarksMenuButton.init(); + setTimeout(delayedStartup, 0, isLoadingBlank, mustLoadSidebar); } @@ -3334,6 +3336,8 @@ function BrowserCustomizeToolbar() CombinedStopReload.uninit(); + BookmarksMenuButton.customizeStart(); + var customizeURL = "chrome://global/content/customizeToolbar.xul"; gCustomizeSheet = getBoolPref("toolbar.customization.usesheet", false); @@ -3396,6 +3400,7 @@ function BrowserToolboxCustomizeDone(aToolboxChanged) { } PlacesToolbarHelper.updateState(); + BookmarksMenuButton.customizeDone(); UpdateUrlbarSearchSplitterState(); @@ -4601,6 +4606,8 @@ function onViewToolbarCommand(aEvent) { aEvent.originalTarget.getAttribute("checked") != "true"); document.persist(toolbar.id, hidingAttribute); + BookmarksMenuButton.updatePosition(); + #ifdef MENUBAR_CAN_AUTOHIDE updateAppButtonDisplay(); #endif diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 57ba4a62f8ea..38b92c3fe22c 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -533,10 +533,10 @@ fullscreentoolbar="true" mode="icons" customizable="true" #ifdef WINCE iconsize="small" defaulticonsize="small" - defaultset="unified-back-forward-button,reload-button,stop-button,home-button,urlbar-container,search-container,navigator-throbber,fullscreenflex,window-controls" + defaultset="unified-back-forward-button,reload-button,stop-button,home-button,urlbar-container,search-container,bookmarks-menu-button-container,navigator-throbber,fullscreenflex,window-controls" #else iconsize="large" - defaultset="unified-back-forward-button,reload-button,stop-button,home-button,urlbar-container,search-container,fullscreenflex,window-controls" + defaultset="unified-back-forward-button,reload-button,stop-button,home-button,urlbar-container,search-container,bookmarks-menu-button-container,fullscreenflex,window-controls" #endif context="toolbar-context-menu"> @@ -669,6 +669,72 @@ + + + + + + + + + + + + + + + + + + + + + + of which its associated menupopup is a places // view, is the menupopup. if (node.localName == "menu" && !node._placesNode && - node.firstChild._placesView) - return node.firstChild._placesView; + node.lastChild._placesView) + return node.lastChild._placesView; while (node instanceof Ci.nsIDOMElement) { if (node._placesView) diff --git a/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js index e2d39262d6f1..0f0d728518cd 100644 --- a/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js +++ b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js @@ -244,13 +244,23 @@ function nextTest() { setTimeout(nextTest, 0); } - else + else { + // Collapse the personal toolbar if needed. + if (wasCollapsed) + toolbar.collapsed = true; finish(); + } } -function test() { - waitForExplicitFinish(); +let toolbar = document.getElementById("PersonalToolbar"); +let wasCollapsed = toolbar.collapsed; +function test() { + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) + toolbar.collapsed = false; + + waitForExplicitFinish(); nextTest(); } diff --git a/browser/components/places/tests/browser/browser_views_liveupdate.js b/browser/components/places/tests/browser/browser_views_liveupdate.js index 9d25d3105686..602c8b6d4860 100644 --- a/browser/components/places/tests/browser/browser_views_liveupdate.js +++ b/browser/components/places/tests/browser/browser_views_liveupdate.js @@ -42,7 +42,14 @@ const Cc = Components.classes; const Ci = Components.interfaces; +let toolbar = document.getElementById("PersonalToolbar"); +let wasCollapsed = toolbar.collapsed; + function test() { + // Uncollapse the personal toolbar if needed. + if (wasCollapsed) + toolbar.collapsed = false; + waitForExplicitFinish(); // Sanity checks. @@ -195,6 +202,10 @@ function finishTest() { // Close bookmarks sidebar. toggleSidebar("viewBookmarksSidebar", false); + // Collapse the personal toolbar if needed. + if (wasCollapsed) + toolbar.collapsed = true; + finish(); } diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd index af128da58284..be2ea63ee465 100644 --- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -129,6 +129,12 @@ + + + + + + diff --git a/browser/themes/gnomestripe/browser/browser.css b/browser/themes/gnomestripe/browser/browser.css index a22d6744961b..c7fa7c4e0d0e 100644 --- a/browser/themes/gnomestripe/browser/browser.css +++ b/browser/themes/gnomestripe/browser/browser.css @@ -440,10 +440,15 @@ menuitem:not([type]) { -moz-image-region: rect(0px 48px 16px 32px); } -#bookmarksToolbarFolderMenu { +#bookmarksToolbarFolderMenu, +#BMB_bookmarksToolbarFolderMenu { list-style-image: url("chrome://browser/skin/places/bookmarksToolbar.png"); } +#unsortedBookmarksFolderMenu { + list-style-image: url("chrome://browser/skin/places/unsortedBookmarks.png"); +} + #menu_openDownloads { list-style-image: url("chrome://browser/skin/Toolbar-small.png"); -moz-image-region: rect(0px 16px 16px 0px); @@ -566,10 +571,19 @@ toolbar[mode="full"] .toolbarbutton-menubutton-button { -moz-image-region: rect(0px 48px 24px 24px); } -#bookmarks-button { +#bookmarks-button, +#bookmarks-menu-button { -moz-image-region: rect(0px 72px 24px 48px); } +#bookmarks-menu-button.bookmark-item { + list-style-image: url("chrome://browser/skin/Toolbar-small.png"); +} + +toolbar[mode="icons"] #bookmarks-menu-button.toolbarbutton-1 { + -moz-box-orient: horizontal; +} + #print-button { list-style-image: url("moz-icon://stock/gtk-print?size=toolbar"); } @@ -692,7 +706,9 @@ toolbar[iconsize="small"] #history-button { -moz-image-region: rect(0px 32px 16px 16px); } -toolbar[iconsize="small"] #bookmarks-button { +toolbar[iconsize="small"] #bookmarks-button, +toolbar[iconsize="small"] #bookmarks-menu-button, +#bookmarks-menu-button.bookmark-item { -moz-image-region: rect(0px 48px 16px 32px); } diff --git a/browser/themes/pinstripe/browser/browser.css b/browser/themes/pinstripe/browser/browser.css index 7f2df57d6c41..492892cccd4f 100644 --- a/browser/themes/pinstripe/browser/browser.css +++ b/browser/themes/pinstripe/browser/browser.css @@ -286,8 +286,13 @@ toolbarbutton.bookmark-item > menupopup { height: 16px; } -#bookmarksToolbarFolderMenu { - list-style-image: url("chrome://browser/skin/places/bookmarksToolbar.png"); +#bookmarksToolbarFolderMenu, +#BMB_bookmarksToolbarFolderMenu { + list-style-image: url("chrome://browser/skin/places/bookmarksToolbar.png"); +} + +#BMB_unsortedBookmarksFolderMenu { + list-style-image: url("chrome://browser/skin/places/unsortedBookmarks.png"); } /* ----- PRIMARY TOOLBAR BUTTONS ----- */ @@ -554,9 +559,10 @@ toolbar[iconsize="small"][mode="icons"] #forward-button:-moz-locale-dir(rtl) { -moz-image-region: rect(40px, 160px, 60px, 140px); } -/* bookmark sidebar button */ +/* bookmark sidebar & menu buttons */ -#bookmarks-button { +#bookmarks-button, +#bookmarks-menu-button { -moz-image-region: rect(0, 180px, 20px, 160px); } @@ -568,6 +574,14 @@ toolbar[iconsize="small"][mode="icons"] #forward-button:-moz-locale-dir(rtl) { -moz-image-region: rect(40px, 180px, 60px, 160px); } +#bookmarks-menu-button.bookmark-item { + list-style-image: url("chrome://browser/skin/Toolbar.png"); +} + +toolbar[mode="icons"] #bookmarks-menu-button.toolbarbutton-1 { + -moz-box-orient: horizontal; +} + /* print button */ #print-button { diff --git a/browser/themes/winstripe/browser/browser.css b/browser/themes/winstripe/browser/browser.css index a45383cba9cd..2bd02353c192 100644 --- a/browser/themes/winstripe/browser/browser.css +++ b/browser/themes/winstripe/browser/browser.css @@ -568,12 +568,21 @@ toolbar:not([iconsize="small"])[mode="icons"] #forward-button:not([disabled="tru -moz-image-region: rect(0, 126px, 18px, 108px); } -/* bookmark sidebar button */ +/* bookmark sidebar & menu buttons */ -#bookmarks-button { +#bookmarks-button, +#bookmarks-menu-button { -moz-image-region: rect(0, 144px, 18px, 126px); } +#bookmarks-menu-button.bookmark-item { + list-style-image: url("chrome://browser/skin/Toolbar.png"); +} + +toolbar[mode="icons"] #bookmarks-menu-button.toolbarbutton-1 { + -moz-box-orient: horizontal; +} + /* print button */ #print-button { @@ -1586,12 +1595,18 @@ toolbarbutton.bookmark-item[dragover="true"][open="true"] { -moz-image-region: rect(0, 108px, 18px, 90px); } -/* Bookmarks Toolbar menu-item */ -#bookmarksToolbarFolderMenu { +/* Bookmarks roots menu-items */ +#bookmarksToolbarFolderMenu, +#BMB_bookmarksToolbarFolderMenu { list-style-image: url("chrome://browser/skin/places/bookmarksToolbar.png"); -moz-image-region: auto; } +#BMB_unsortedBookmarksFolderMenu { + list-style-image: url("chrome://browser/skin/places/unsortedBookmarks.png"); + -moz-image-region: auto; +} + /* ::::: Keyboard UI Panel ::::: */ .KUI-panel { From 032e88ff27fd4a4625fe161490d414f31bf74aae Mon Sep 17 00:00:00 2001 From: Dao Gottwald Date: Thu, 24 Jun 2010 10:08:45 -0700 Subject: [PATCH 1782/1860] Bug 571992 - Switch Tabs-on-top to be the Default State on Windows r=gavin --- browser/base/content/browser.xul | 3 +++ 1 file changed, 3 insertions(+) diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 38b92c3fe22c..5977b72e3260 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -507,6 +507,9 @@ #ifdef WINCE defaulticonsize="small" iconsize="small" #endif +#ifdef XP_WIN + tabsontop="true" +#endif #ifdef XP_WIN tabsontop="true" #endif From ef72b7e945f14e0d6c90442838da7deb0d82c8ba Mon Sep 17 00:00:00 2001 From: Jacek Caban Date: Thu, 24 Jun 2010 10:08:46 -0700 Subject: [PATCH 1783/1860] Bug 569581 - Better __stdcall check for win64. r=ted --- configure.in | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/configure.in b/configure.in index 8f931bc5b28e..b0f6b6aa3f0c 100644 --- a/configure.in +++ b/configure.in @@ -2898,17 +2898,19 @@ fi if test -z "$SKIP_COMPILER_CHECKS"; then dnl Checks for typedefs, structures, and compiler characteristics. dnl ======================================================== -AC_LANG_C AC_HEADER_STDC AC_C_CONST AC_TYPE_MODE_T AC_TYPE_OFF_T AC_TYPE_PID_T AC_TYPE_SIZE_T +AC_LANG_CPLUSPLUS AC_MSG_CHECKING(for __stdcall) AC_CACHE_VAL(ac_cv___stdcall, - [AC_TRY_COMPILE([], - [void foo(); void __stdcall foo();], + [AC_TRY_COMPILE([template struct foo; + template <> struct foo {}; + template <> struct foo {};], + [], [ac_cv___stdcall=true], [ac_cv___stdcall=false])]) if test "$ac_cv___stdcall" = true ; then @@ -2917,6 +2919,7 @@ if test "$ac_cv___stdcall" = true ; then else AC_MSG_RESULT(no) fi +AC_LANG_C AC_MSG_CHECKING(for ssize_t) AC_CACHE_VAL(ac_cv_type_ssize_t, [AC_TRY_COMPILE([#include From 492bcec88ba90030c424459fdd9251602ad0e9f2 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 24 Jun 2010 10:11:29 -0700 Subject: [PATCH 1784/1860] Bug 573791 - Allow dragging the window with the empty space in the tab bar r=Enn --- toolkit/content/WindowDraggingUtils.jsm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/content/WindowDraggingUtils.jsm b/toolkit/content/WindowDraggingUtils.jsm index 84ae8e8b19ce..7c800e5b53b8 100644 --- a/toolkit/content/WindowDraggingUtils.jsm +++ b/toolkit/content/WindowDraggingUtils.jsm @@ -46,7 +46,7 @@ WindowDraggingElement.prototype = { mouseDownCheck: function(e) { return true; }, dragTags: ["box", "hbox", "vbox", "spacer", "label", "statusbarpanel", "stack", "toolbaritem", "toolbarseparator", "toolbarspring", "toolbarspacer", - "radiogroup", "deck", "scrollbox"], + "radiogroup", "deck", "scrollbox", "arrowscrollbox", "tabs"], handleEvent: function(aEvent) { switch (aEvent.type) { case "mousedown": From 6554351e6091d8846c57a7a3aab116e629c6ccaf Mon Sep 17 00:00:00 2001 From: Felipe Gomes Date: Thu, 24 Jun 2010 10:13:59 -0700 Subject: [PATCH 1785/1860] Bug 555081 - Can't move the window when using custom titlebar drawing r=dao --- browser/themes/winstripe/browser/browser-aero.css | 9 +++++++++ browser/themes/winstripe/browser/browser.css | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/browser/themes/winstripe/browser/browser-aero.css b/browser/themes/winstripe/browser/browser-aero.css index 7eb869bae548..b405090541de 100644 --- a/browser/themes/winstripe/browser/browser-aero.css +++ b/browser/themes/winstripe/browser/browser-aero.css @@ -43,6 +43,15 @@ border-right: 1px solid ThreeDShadow; } + /* Make the window draggable by glassed toolbars (bug 555081) */ + #toolbar-menubar:not([autohide="true"]), + #navigator-toolbox[tabsontop="true"] > #TabsToolbar, + #navigator-toolbox:not([tabsontop="true"]) > #nav-bar, + #navigator-toolbox:not([tabsontop="true"]) > #nav-bar + #customToolbars + #PersonalToolbar[collapsed="true"] + #TabsToolbar:last-child, + #navigator-toolbox > toolbar:not(#toolbar-menubar):not(#fullscr-toggler):-moz-lwtheme { + -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbar-drag"); + } + #browser:not(:-moz-lwtheme), #browser-bottombox:not(:-moz-lwtheme) { background-color: -moz-dialog; diff --git a/browser/themes/winstripe/browser/browser.css b/browser/themes/winstripe/browser/browser.css index 2bd02353c192..ccffe7d8fc9f 100644 --- a/browser/themes/winstripe/browser/browser.css +++ b/browser/themes/winstripe/browser/browser.css @@ -112,6 +112,10 @@ statusbarpanel#statusbar-display { } %endif +#appmenu-button-container { + -moz-binding: url("chrome://global/content/bindings/general.xml#windowdragbox"); +} + #appmenu-button { -moz-appearance: none; background-color: rgb(228,120,14); From 3480fb96c8a33039afc6d1421847e8936af5f33a Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Thu, 24 Jun 2010 10:38:37 -0700 Subject: [PATCH 1786/1860] Backed out changeset 88142593698a (bug 571992) because it already landed apparently... --- browser/base/content/browser.xul | 3 --- 1 file changed, 3 deletions(-) diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 5977b72e3260..38b92c3fe22c 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -507,9 +507,6 @@ #ifdef WINCE defaulticonsize="small" iconsize="small" #endif -#ifdef XP_WIN - tabsontop="true" -#endif #ifdef XP_WIN tabsontop="true" #endif From a6c33b57e9715bb166d87e5e3499051a99a1a53c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A3o=20Gottwald?= Date: Thu, 24 Jun 2010 19:55:39 +0200 Subject: [PATCH 1787/1860] Make browser_bug462289.js work with tabs on top --- browser/base/content/test/browser_bug462289.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/browser/base/content/test/browser_bug462289.js b/browser/base/content/test/browser_bug462289.js index bf1fc6babad4..4e576133ad55 100644 --- a/browser/base/content/test/browser_bug462289.js +++ b/browser/base/content/test/browser_bug462289.js @@ -22,8 +22,13 @@ function step3() { isnot(document.activeElement, tab1, "mouse on tab again activeElement"); - document.getElementById("searchbar").focus(); - EventUtils.synthesizeKey("VK_TAB", { }); + if (gNavToolbox.getAttribute("tabsontop") == "true") { + gURLBar.focus(); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}); + } else { + document.getElementById("searchbar").focus(); + EventUtils.synthesizeKey("VK_TAB", {}); + } is(document.activeElement, tab1, "tab key to tab activeElement"); EventUtils.synthesizeMouse(tab1, 2, 2, {}); From d74c3385a3219c0c5890c8d6d2a427c7781b0891 Mon Sep 17 00:00:00 2001 From: Bobby Holley Date: Thu, 24 Jun 2010 11:33:34 -0700 Subject: [PATCH 1788/1860] Bug 573629 - gDiscardable and gDecodeOnDraw should be prefs. r=vlad --- modules/libpr0n/src/imgRequest.cpp | 82 ++++++++++++++++++++++++++++++ modules/libpref/src/init/all.js | 12 +++++ 2 files changed, 94 insertions(+) diff --git a/modules/libpr0n/src/imgRequest.cpp b/modules/libpr0n/src/imgRequest.cpp index 2538ac0f9622..3c6786294790 100644 --- a/modules/libpr0n/src/imgRequest.cpp +++ b/modules/libpr0n/src/imgRequest.cpp @@ -71,9 +71,75 @@ #include "nsNetUtil.h" #include "nsIProtocolHandler.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch2.h" + +#define DISCARD_PREF "image.mem.discardable" +#define DECODEONDRAW_PREF "image.mem.decodeondraw" + +/* Kept up to date by a pref observer. */ static PRBool gDecodeOnDraw = PR_FALSE; static PRBool gDiscardable = PR_FALSE; +/* + * Pref observer goop. Yuck. + */ + +// Flag +static PRBool gRegisteredPrefObserver = PR_FALSE; + +// Reloader +static void +ReloadPrefs(nsIPrefBranch *aBranch) +{ + // Discardable + PRBool discardable; + nsresult rv = aBranch->GetBoolPref(DISCARD_PREF, &discardable); + if (NS_SUCCEEDED(rv)) + gDiscardable = discardable; + + // Decode-on-draw + PRBool decodeondraw; + rv = aBranch->GetBoolPref(DECODEONDRAW_PREF, &decodeondraw); + if (NS_SUCCEEDED(rv)) + gDecodeOnDraw = decodeondraw; +} + +// Observer +class imgRequestPrefObserver : public nsIObserver { +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER +}; +NS_IMPL_ISUPPORTS1(imgRequestPrefObserver, nsIObserver) + +// Callback +NS_IMETHODIMP +imgRequestPrefObserver::Observe(nsISupports *aSubject, + const char *aTopic, + const PRUnichar *aData) +{ + // Right topic + NS_ABORT_IF_FALSE(!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID), "invalid topic"); + + // Right pref + if (strcmp(NS_LossyConvertUTF16toASCII(aData).get(), DISCARD_PREF) && + strcmp(NS_LossyConvertUTF16toASCII(aData).get(), DECODEONDRAW_PREF)) + return NS_OK; + + // Get the pref branch + nsCOMPtr branch = do_QueryInterface(aSubject); + if (!branch) { + NS_WARNING("Couldn't get pref branch within imgRequestPrefObserver::Observe!"); + return NS_OK; + } + + // Process the change + ReloadPrefs(branch); + + return NS_OK; +} + #if defined(PR_LOGGING) PRLogModuleInfo *gImgLog = PR_NewLogModule("imgRequest"); #endif @@ -150,6 +216,22 @@ nsresult imgRequest::Init(nsIURI *aURI, SetLoadId(aLoadId); + // Register our pref observer if it hasn't been done yet. + if (NS_UNLIKELY(!gRegisteredPrefObserver)) { + imgRequestPrefObserver *observer = new imgRequestPrefObserver(); + if (observer) { + nsCOMPtr branch = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (branch) { + branch->AddObserver(DISCARD_PREF, observer, PR_FALSE); + branch->AddObserver(DECODEONDRAW_PREF, observer, PR_FALSE); + ReloadPrefs(branch); + gRegisteredPrefObserver = PR_TRUE; + } + } + else + delete observer; + } + return NS_OK; } diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 0240dbcc8e68..c72b2e9a972a 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -3107,6 +3107,18 @@ pref("image.cache.timeweight", 500); // The default Accept header sent for images loaded over HTTP(S) pref("image.http.accept", "image/png,image/*;q=0.8,*/*;q=0.5"); +// +// Image memory management prefs +// + +// Discards inactive image frames and re-decodes them on demand from +// compressed data. +pref("image.mem.discardable", false); + +// Prevents images from automatically being decoded on load, instead allowing +// them to be decoded on demand when they are drawn. +pref("image.mem.decodeondraw", false); + // WebGL prefs pref("webgl.enabled_for_all_sites", false); pref("webgl.software_render", false); From 86d65565bbee7e6314783687c5fd6d4b26d1ce0d Mon Sep 17 00:00:00 2001 From: Brad Lassey Date: Thu, 24 Jun 2010 15:00:42 -0400 Subject: [PATCH 1789/1860] bug 574412 - Add Logging to android life cycle callbacks r=mwu --- embedding/android/GeckoApp.java | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/embedding/android/GeckoApp.java b/embedding/android/GeckoApp.java index 0d3487814b95..f5b5d786780b 100644 --- a/embedding/android/GeckoApp.java +++ b/embedding/android/GeckoApp.java @@ -1,5 +1,4 @@ -/* -*- Mode: Java; tab-width: 20; indent-tabs-mode: nil; -*- - * ***** BEGIN LICENSE BLOCK ***** +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-/ * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version @@ -97,6 +96,7 @@ abstract public class GeckoApp @Override public void onCreate(Bundle savedInstanceState) { + Log.i("GeckoApp", "create"); super.onCreate(savedInstanceState); mAppContext = this; @@ -190,6 +190,7 @@ abstract public class GeckoApp @Override public void onStop() { + Log.i("GeckoApp", "stop"); // We're about to be stopped, potentially in preparation for // being destroyed. We're killable after this point -- as I // understand it, in extreme cases the process can be terminated @@ -206,9 +207,24 @@ abstract public class GeckoApp super.onStop(); } + @Override + public void onRestart() + { + Log.i("GeckoApp", "restart"); + super.onRestart(); + } + + @Override + public void onStart() + { + Log.i("GeckoApp", "start"); + super.onStart(); + } + @Override public void onDestroy() { + Log.i("GeckoApp", "destroy"); // Tell Gecko to shutting down; we'll end up calling System.exit() // in onXreExit. GeckoAppShell.sendEventToGecko(new GeckoEvent(GeckoEvent.ACTIVITY_STOPPING)); @@ -219,6 +235,7 @@ abstract public class GeckoApp @Override public void onConfigurationChanged(android.content.res.Configuration newConfig) { + Log.i("GeckoApp", "configuration changed"); // nothing, just ignore super.onConfigurationChanged(newConfig); } @@ -226,6 +243,7 @@ abstract public class GeckoApp @Override public void onLowMemory() { + Log.i("GeckoApp", "low memory"); // XXX TODO super.onLowMemory(); } From e25e6e220dccec4cb5a254b84eed93e83c4d9cd4 Mon Sep 17 00:00:00 2001 From: Ben Parr Date: Thu, 24 Jun 2010 12:50:04 -0700 Subject: [PATCH 1790/1860] Bug 572561: Addons Manager hides Language category if there are no locale packs installed but some are pending install. r=Unfocused, r=dtownsend --- .../mozapps/extensions/content/extensions.js | 109 +++++++---- .../extensions/test/browser/Makefile.in | 1 + .../test/browser/browser_bug562899.js | 8 +- .../test/browser/browser_bug572561.js | 94 +++++++++ .../test/browser/browser_dragdrop.js | 6 +- .../test/browser/browser_sorting.js | 6 +- .../mozapps/extensions/test/browser/head.js | 184 +++++++++++++++++- 7 files changed, 358 insertions(+), 50 deletions(-) create mode 100644 toolkit/mozapps/extensions/test/browser/browser_bug572561.js diff --git a/toolkit/mozapps/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js index 8bc052b8b802..b9c930e3ee1b 100644 --- a/toolkit/mozapps/extensions/content/extensions.js +++ b/toolkit/mozapps/extensions/content/extensions.js @@ -88,6 +88,9 @@ XPCOMUtils.defineLazyGetter(gStrings, "appVersion", function() { window.addEventListener("load", initialize, false); window.addEventListener("unload", shutdown, false); +var gPendingInitializations = 1; +__defineGetter__("gIsInitializing", function() gPendingInitializations > 0); + function initialize() { gCategories.initialize(); gHeader.initialize(); @@ -105,6 +108,19 @@ function initialize() { } gViewController.loadView(view); + notifyInitialized(); +} + +function notifyInitialized() { + if (!gIsInitializing) + return; + + gPendingInitializations--; + if (!gIsInitializing) { + var event = document.createEvent("Events"); + event.initEvent("Initialized", true, true); + document.dispatchEvent(event); + } } function shutdown() { @@ -258,6 +274,10 @@ var gViewController = { return {type: viewType, param: decodeURIComponent(viewParam)}; }, + get isLoading() { + return this.currentViewObj.node.hasAttribute("loading"); + }, + loadView: function(aViewId) { if (aViewId == this.currentViewId) return; @@ -616,6 +636,39 @@ function createItem(aObj, aIsInstall, aRequiresRestart) { return item; } +function getAddonsAndInstalls(aType, aCallback) { + var addonTypes = null, installTypes = null; + if (aType != null) { + addonTypes = [aType]; + installTypes = [aType]; + if (aType == "extension") { + addonTypes.push("bootstrapped"); + installTypes = addonTypes.concat(""); + } + } + + var addons = null, installs = null; + + AddonManager.getAddonsByTypes(addonTypes, function(aAddonsList) { + addons = aAddonsList; + if (installs != null) + aCallback(addons, installs); + }); + + AddonManager.getInstallsByTypes(installTypes, function(aInstallsList) { + // skip over upgrade installs and non-active installs + installs = aInstallsList.filter(function(aInstall) { + return !(aInstall.existingAddon || + aInstall.state == AddonManager.STATE_AVAILABLE); + }); + + if (addons != null) + aCallback(addons, installs) + }); + + return {addon: addonTypes, install: installTypes}; +} + var gCategories = { node: null, @@ -648,16 +701,18 @@ var gCategories = { }, false); var maybeHidden = ["addons://list/locale", "addons://list/searchengine"]; + gPendingInitializations += maybeHidden.length; maybeHidden.forEach(function(aId) { var type = gViewController.parseViewId(aId).param; - AddonManager.getAddonsByTypes([type], function(aAddonsList) { - if (aAddonsList.length > 0) { + getAddonsAndInstalls(type, function(aAddonsList, aInstallsList) { + if (aAddonsList.length > 0 || aInstallsList.length > 0) { self.get(aId).hidden = false; + notifyInitialized(); return; } gEventManager.registerInstallListener({ - onNewInstall: function(aInstall) { + onDownloadStarted: function(aInstall) { this._maybeShowCategory(aInstall); }, @@ -680,6 +735,8 @@ var gCategories = { } } }); + + notifyInitialized(); }); }); }, @@ -795,6 +852,7 @@ var gDiscoverView = { .getService(Ci.nsIURLFormatter) .formatURLPref(PREF_DISCOVERURL); + gPendingInitializations++; AddonManager.getAllAddons(function(aAddons) { var list = {}; aAddons.forEach(function(aAddon) { @@ -809,6 +867,7 @@ var gDiscoverView = { }); gDiscoverView._browser.homePage = url + "#" + JSON.stringify(list); + notifyInitialized(); }); }, @@ -996,33 +1055,21 @@ var gListView = { gHeader.setName(gStrings.ext.GetStringFromName("header-" + aType)); this.showEmptyNotice(false); - this._types = [aType]; - this._installTypes = [aType]; - if (aType == "extension") { - this._types.push("bootstrapped"); - this._installTypes = this._types.concat(""); - } - while (this._listBox.itemCount > 0) this._listBox.removeItemAt(0); var self = this; - var addons = null, installs = null; - - function updateList() { - if (addons == null || installs == null) + var types = getAddonsAndInstalls(aType, function(aAddonsList, aInstallsList) { + if (gViewController && aRequest != gViewController.currentViewRequest) return; - for (let i = 0; i < addons.length; i++) { - let item = createItem(addons[i]); + for (let i = 0; i < aAddonsList.length; i++) { + let item = createItem(aAddonsList[i]); self._listBox.appendChild(item); } - for (let i = 0; i < installs.length; i++) { - // skip over upgrade installs - if (installs[i].existingAddon) - continue; - let item = createItem(installs[i], true); + for (let i = 0; i < aInstallsList.length; i++) { + let item = createItem(aInstallsList[i], true); self._listBox.appendChild(item); } @@ -1031,25 +1078,13 @@ var gListView = { else self.showEmptyNotice(true); + gEventManager.registerInstallListener(self); gViewController.updateCommands(); gViewController.notifyViewChanged(); - } - - - AddonManager.getAddonsByTypes(this._types, function(aAddonsList) { - if (gViewController && aRequest != gViewController.currentViewRequest) - return; - addons = aAddonsList; - updateList(); }); - AddonManager.getInstallsByTypes(this._installTypes, function(aInstallsList) { - if (gViewController && aRequest != gViewController.currentViewRequest) - return; - installs = aInstallsList; - updateList(); - gEventManager.registerInstallListener(self); - }); + this._types = types.addon; + this._installTypes = types.install; }, hide: function() { @@ -1277,7 +1312,7 @@ var gDragDrop = { }, onDrop: function(aEvent) { - var dataTransfer = aEvent.dataTransfer; + var dataTransfer = aEvent.dataTransfer; var urls = []; // Convert every dropped item into a url diff --git a/toolkit/mozapps/extensions/test/browser/Makefile.in b/toolkit/mozapps/extensions/test/browser/Makefile.in index 592d14dd25db..cb33e1ffd64c 100644 --- a/toolkit/mozapps/extensions/test/browser/Makefile.in +++ b/toolkit/mozapps/extensions/test/browser/Makefile.in @@ -49,6 +49,7 @@ _TEST_FILES = \ head.js \ browser_bug562890.js \ browser_bug562899.js \ + browser_bug572561.js \ browser_dragdrop.js \ browser_sorting.js \ browser_updatessl.js \ diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug562899.js b/toolkit/mozapps/extensions/test/browser/browser_bug562899.js index 60311e7ea094..1d83ec17f22e 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_bug562899.js +++ b/toolkit/mozapps/extensions/test/browser/browser_bug562899.js @@ -28,10 +28,10 @@ function test() { } function end_test() { - gManagerWindow.close(); - LightweightThemeManager.forgetUsedTheme("test"); - - finish(); + close_manager(gManagerWindow, function() { + LightweightThemeManager.forgetUsedTheme("test"); + finish(); + }); } // Tests that loading a second view before the first has not finished loading diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug572561.js b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js new file mode 100644 index 000000000000..75aae1b80ef8 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that the locale category is shown if there are no locale packs +// installed but some are pending install + +var gManagerWindow; +var gProvider; +var gInstallProperties = [{ + name: "Locale Category Test", + type: "locale" +}]; +var gInstall; +var gExpectedCancel = false; +var gTestInstallListener = { + onInstallStarted: function(aInstall) { + check_hidden(false); + }, + + onInstallEnded: function(aInstall) { + check_hidden(false); + run_next_test(); + }, + + onInstallCancelled: function(aInstall) { + ok(gExpectedCancel, "Should expect install cancel"); + check_hidden(false); + run_next_test(); + }, + + onInstallFailed: function(aInstall) { + ok(false, "Did not expect onInstallFailed"); + } +}; + +function test() { + waitForExplicitFinish(); + + gProvider = new MockProvider(); + + open_manager(null, function(aWindow) { + gManagerWindow = aWindow; + run_next_test(); + }); +} + +function end_test() { + close_manager(gManagerWindow, function() { + finish(); + }); +} + +function check_hidden(aExpectedHidden) { + var category = gManagerWindow.gCategories.get("addons://list/locale"); + is(category.hidden, aExpectedHidden, "Should have correct hidden state"); +} + +// Tests that a non-active install does not make the locale category show +add_test(function() { + check_hidden(true); + gInstall = gProvider.createInstalls(gInstallProperties)[0]; + gInstall.addTestListener(gTestInstallListener); + check_hidden(true); + run_next_test(); +}); + +// Test that restarting the add-on manager with a non-active install +// does not cause the locale category to show +add_test(function() { + restart_manager(gManagerWindow, null, function(aWindow) { + gManagerWindow = aWindow; + check_hidden(true); + run_next_test(); + }); +}); + +// Test that installing the install shows the locale category +add_test(function() { + gInstall.install(); +}); + +// Test that restarting the add-on manager does not cause the locale category +// to become hidden +add_test(function() { + restart_manager(gManagerWindow, null, function(aWindow) { + gManagerWindow = aWindow; + check_hidden(false); + + gExpectedCancel = true; + gInstall.cancel(); + }); +}); + diff --git a/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js index b7e68530f918..8ed01ef672d6 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js +++ b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js @@ -79,9 +79,9 @@ function test() { } function end_test() { - gManagerWindow.close(); - - finish(); + close_manager(gManagerWindow, function() { + finish(); + }); } function test_confirmation(aWindow, aExpectedURLs) { diff --git a/toolkit/mozapps/extensions/test/browser/browser_sorting.js b/toolkit/mozapps/extensions/test/browser/browser_sorting.js index 34efa1f6a95a..eecbb262ccfa 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_sorting.js +++ b/toolkit/mozapps/extensions/test/browser/browser_sorting.js @@ -49,9 +49,9 @@ function test() { } function end_test() { - gManagerWindow.close(); - - finish(); + close_manager(gManagerWindow, function() { + finish(); + }); } function check_order(aExpectedOrder) { diff --git a/toolkit/mozapps/extensions/test/browser/head.js b/toolkit/mozapps/extensions/test/browser/head.js index 011a99dafd4b..443e48aaa82c 100644 --- a/toolkit/mozapps/extensions/test/browser/head.js +++ b/toolkit/mozapps/extensions/test/browser/head.js @@ -48,7 +48,7 @@ function get_addon_file_url(aFilename) { } function wait_for_view_load(aManagerWindow, aCallback) { - if (!aManagerWindow.gViewController.currentViewObj.node.hasAttribute("loading")) { + if (!aManagerWindow.gViewController.isLoading) { aCallback(aManagerWindow); return; } @@ -67,7 +67,15 @@ function open_manager(aView, aCallback) { ok(aManagerWindow != null, "Should have an add-ons manager window"); is(aManagerWindow.location, MANAGER_URI, "Should be displaying the correct UI"); - wait_for_view_load(aManagerWindow, aCallback); + if (!aManagerWindow.gIsInitializing) { + wait_for_view_load(aManagerWindow, aCallback); + return; + } + + aManagerWindow.document.addEventListener("Initialized", function() { + aManagerWindow.document.removeEventListener("Initialized", arguments.callee, false); + wait_for_view_load(aManagerWindow, aCallback); + }, false); } if ("switchToTabHavingURI" in window) { @@ -77,12 +85,28 @@ function open_manager(aView, aCallback) { return; } - openDialog("about:addons").addEventListener("load", function() { + openDialog(MANAGER_URI).addEventListener("load", function() { this.removeEventListener("load", arguments.callee, false); setup_manager(this); }, false); } +function close_manager(aManagerWindow, aCallback) { + ok(aManagerWindow != null, "Should have an add-ons manager window to close"); + is(aManagerWindow.location, MANAGER_URI, "Should be closing window with correct URI"); + + aManagerWindow.addEventListener("unload", function() { + this.removeEventListener("unload", arguments.callee, false); + aCallback(); + }, false); + + aManagerWindow.close(); +} + +function restart_manager(aManagerWindow, aView, aCallback) { + close_manager(aManagerWindow, function() { open_manager(aView, aCallback); }); +} + function CertOverrideListener(host, bits) { this.host = host; this.bits = bits; @@ -186,14 +210,32 @@ MockProvider.prototype = { null, false) }, + /** + * Adds an add-on install to the list of installs that this provider exposes + * to the AddonManager, dispatching appropriate events in the process. + * + * @param aInstall + * The add-on install to add + */ + addInstall: function MP_addInstall(aInstall) { + this.installs.push(aInstall); + + if (!this.started) + return; + + aInstall.callListeners("onNewInstall"); + }, + /** * Creates a set of mock add-on objects and adds them to the list of add-ons * managed by this provider. * * @param aAddonProperties * An array of objects containing properties describing the add-ons + * @return Array of the new MockAddons */ createAddons: function MP_createAddons(aAddonProperties) { + var newAddons = []; aAddonProperties.forEach(function(aAddonProp) { var addon = new MockAddon(aAddonProp.id); for (var prop in aAddonProp) { @@ -202,7 +244,32 @@ MockProvider.prototype = { addon[prop] = aAddonProp[prop]; } this.addAddon(addon); + newAddons.push(addon); }, this); + + return newAddons; + }, + + /** + * Creates a set of mock add-on install objects and adds them to the list + * of installs managed by this provider. + * + * @param aInstallProperties + * An array of objects containing properties describing the installs + * @return Array of the new MockInstalls + */ + createInstalls: function MP_createInstalls(aInstallProperties) { + var newInstalls = []; + aInstallProperties.forEach(function(aInstallProp) { + var install = new MockInstall(); + for (var prop in aInstallProp) { + install[prop] = aInstallProp[prop]; + } + this.addInstall(install); + newInstalls.push(install); + }, this); + + return newInstalls; }, /***** AddonProvider implementation *****/ @@ -287,8 +354,13 @@ MockProvider.prototype = { */ getInstallsByTypes: function MP_getInstallsByTypes(aTypes, aCallback) { var installs = this.installs.filter(function(aInstall) { + // Appear to have actually removed cancelled installs from the provider + if (aInstall.state == AddonManager.STATE_CANCELLED) + return false; + if (aTypes && aTypes.length > 0 && aTypes.indexOf(aInstall.type) == -1) return false; + return true; }); this._delayCallback(aCallback, installs); @@ -454,3 +526,109 @@ MockAddon.prototype = { // To be implemented when needed } }; + +/***** Mock AddonInstall object for the Mock Provider *****/ + +function MockInstall(aName, aType) { + this.name = aName || ""; + this.type = aType || "extension"; + this.version = "1.0"; + this.iconURL = ""; + this.infoURL = ""; + this.state = AddonManager.STATE_AVAILABLE; + this.error = 0; + this.sourceURL = ""; + this.file = null; + this.progress = 0; + this.maxProgress = -1; + this.certificate = null; + this.certName = ""; + this.existingAddon = null; + this.addon = null; + this.listeners = []; + + // Another type of install listener for tests that want to check the results + // of code run from standard install listeners + this.testListeners = []; +} + +MockInstall.prototype = { + install: function() { + switch (this.state) { + case AddonManager.STATE_AVAILABLE: + case AddonManager.STATE_DOWNLOADED: + // Downloading to be implemented when needed + + this.state = AddonManager.STATE_INSTALLING; + if (!this.callListeners("onInstallStarted")) { + // Reverting to STATE_DOWNLOADED instead to be implemented when needed + this.state = AddonManager.STATE_CANCELLED; + this.callListeners("onInstallCancelled"); + return; + } + + // Adding addon to MockProvider to be implemented when needed + this.addon = new MockAddon("", this.name, this.type); + this.state = AddonManager.STATE_INSTALLED; + this.callListeners("onInstallEnded"); + break; + case AddonManager.STATE_DOWNLOADING: + case AddonManager.STATE_CHECKING: + case AddonManger.STATE_INSTALLING: + // Installation is already running + return; + default: + ok(false, "Cannot start installing when state = " + this.state); + } + }, + + cancel: function() { + switch (this.state) { + case AddonManager.STATE_AVAILABLE: + this.state = AddonManager.STATE_CANCELLED; + break; + case AddonManager.STATE_INSTALLED: + this.state = AddonManager.STATE_CANCELLED; + this.callListeners("onInstallCancelled"); + break; + default: + // Handling cancelling when downloading to be implemented when needed + ok(false, "Cannot cancel when state = " + this.state); + } + }, + + + addListener: function(aListener) { + if (!this.listeners.some(function(i) i == aListener)) + this.listeners.push(aListener); + }, + + removeListener: function(aListener) { + this.listeners = this.listeners.filter(function(i) i != aListener); + }, + + addTestListener: function(aListener) { + if (!this.testListeners.some(function(i) i == aListener)) + this.testListeners.push(aListener); + }, + + removeTestListener: function(aListener) { + this.testListeners = this.testListeners.filter(function(i) i != aListener); + }, + + callListeners: function(aMethod) { + var result = AddonManagerPrivate.callInstallListeners(aMethod, this.listeners, + this, this.addon); + + // Call test listeners after standard listeners to remove race condition + // between standard and test listeners + this.testListeners.forEach(function(aListener) { + if (aMethod in aListener) + if (aListener[aMethod].call(aListener, this, this.addon) === false) + result = false; + }); + + return result; + } +}; + From b0ecb592b1933bb0ef54e3c1fb96bb99d45448e2 Mon Sep 17 00:00:00 2001 From: Joel Maher Date: Thu, 24 Jun 2010 13:30:50 -0700 Subject: [PATCH 1791/1860] Bug 567417 - bug to get mochitests running on fennec + e10s r=smaug,ctalbert --- testing/mochitest/Makefile.in | 2 + testing/mochitest/ipc-overlay.xul | 75 ++++++ testing/mochitest/ipc.js | 215 ++++++++++++++++++ testing/mochitest/runtests.py.in | 6 + .../tests/SimpleTest/MozillaFileLogger.js | 52 ++++- .../mochitest/tests/SimpleTest/SimpleTest.js | 23 ++ .../mochitest/tests/SimpleTest/TestRunner.js | 41 ++++ testing/mochitest/tests/SimpleTest/quit.js | 12 +- 8 files changed, 418 insertions(+), 8 deletions(-) create mode 100644 testing/mochitest/ipc-overlay.xul create mode 100644 testing/mochitest/ipc.js diff --git a/testing/mochitest/Makefile.in b/testing/mochitest/Makefile.in index fd832d89a7c1..e2017013ecc5 100644 --- a/testing/mochitest/Makefile.in +++ b/testing/mochitest/Makefile.in @@ -74,6 +74,8 @@ _SERV_FILES = \ harness.xul \ browser-test-overlay.xul \ browser-test.js \ + ipc-overlay.xul \ + ipc.js \ browser-harness.xul \ redirect-a11y.html \ redirect.html \ diff --git a/testing/mochitest/ipc-overlay.xul b/testing/mochitest/ipc-overlay.xul new file mode 100644 index 000000000000..c3480415b468 --- /dev/null +++ b/testing/mochitest/ipc-overlay.xul @@ -0,0 +1,75 @@ + + + + + + + + + + + diff --git a/testing/mochitest/ipc.js b/testing/mochitest/ipc.js new file mode 100644 index 000000000000..0a95b6f43d6a --- /dev/null +++ b/testing/mochitest/ipc.js @@ -0,0 +1,215 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla's layout acceptance tests. + * + * The Initial Developer of the Original Code is the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joel Maher , Mozilla Corporation (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +if (Cc === undefined) { + var Cc = Components.classes; + var Ci = Components.interfaces; +} + +function ipcEvent(e) { + var sync = e.getData("sync"); + var type = e.getData("type"); + var data = JSON.parse(e.getData("data")); + + switch(type) { + case 'LoggerInit': + MozillaFileLogger.init(data.filename); + break; + case 'Logger': + var logger = MozillaFileLogger.getLogCallback(); + logger({"num":data.num, "level":data.level, "info": Array(data.info)}); + break; + case 'LoggerClose': + MozillaFileLogger.close(); + break; + case 'waitForFocus': + if (content) + var wrapper = content.wrappedJSObject.frames[0].SimpleTest; + else + var wrapper = SimpleTest; + ipctest.waitForFocus(wrapper[data.callback], data.targetWindow, data.expectBlankPage); + break; + default: + if (type == 'QuitApplication') { + removeEventListener("contentEvent", function (e) { ipcEvent(e); }, false, true); + } + + if (sync == 1) { + return sendSyncMessage("chromeEvent", {"type":type, "data":data}); + } else { + sendAsyncMessage("chromeEvent", {"type":type, "data":data}); + } + } +}; + +var xulRuntime = Components.classes["@mozilla.org/xre/app-info;1"] + .getService(Components.interfaces.nsIXULRuntime); + +//check if we are in content process +if (xulRuntime.processType == 2) { + addEventListener("contentEvent", function (e) { ipcEvent(e); }, false, true); +} + +var ipctest = {}; + +ipctest.waitForFocus_started = false; +ipctest.waitForFocus_loaded = false; +ipctest.waitForFocus_focused = false; + +ipctest.waitForFocus = function (callback, targetWindow, expectBlankPage) { + + if (targetWindow && targetWindow != undefined) { + var tempID = targetWindow; + targetWindow = null; + var wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + var wm_enum = wm.getXULWindowEnumerator(null); + + while(wm_enum.hasMoreElements()) { + var win = wm_enum.getNext().QueryInterface(Ci.nsIXULWindow) + .docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow) + .content.wrappedJSObject; + + var domutils = win.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + + if (domutils.outerWindowID == tempID) { + targetWindow = win; + } + } + } + + if (targetWindow == null || targetWindow == undefined) + if (content) + targetWindow = content.document.defaultView; + else + targetWindow = window; + + ipctest.waitForFocus_started = false; + expectBlankPage = !!expectBlankPage; + + var fm = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + + var childTargetWindow = { }; + fm.getFocusedElementForWindow(targetWindow, true, childTargetWindow); + childTargetWindow = childTargetWindow.value; + + function debugFocusLog(prefix) { + if (content) + var wrapper = content.wrappedJSObject.frames[0].SimpleTest; + else + var wrapper = SimpleTest; + wrapper.ok(true, prefix + " -- loaded: " + targetWindow.document.readyState + + " active window: " + + (fm.activeWindow ? "(" + fm.activeWindow + ") " + fm.activeWindow.location : "") + + " focused window: " + + (fm.focusedWindow ? "(" + fm.focusedWindow + ") " + fm.focusedWindow.location : "") + + " desired window: (" + targetWindow + ") " + targetWindow.location + + " child window: (" + childTargetWindow + ") " + childTargetWindow.location); + } + + debugFocusLog("before wait for focus"); + + function maybeRunTests() { + debugFocusLog("maybe run tests "); + if (ipctest.waitForFocus_loaded && + ipctest.waitForFocus_focused && + !ipctest.waitForFocus_started) { + ipctest.waitForFocus_started = true; + if (content) + content.setTimeout(function() { callback(); }, 0, targetWindow); + else + setTimeout(function() { callback(); }, 0, targetWindow); + } + } + + function waitForEvent(event) { + try { + debugFocusLog("waitForEvent called "); + + // Check to make sure that this isn't a load event for a blank or + // non-blank page that wasn't desired. + if (event.type == "load" && (expectBlankPage != (event.target.location == "about:blank"))) + return; + + ipctest["waitForFocus_" + event.type + "ed"] = true; + var win = (event.type == "load") ? targetWindow : childTargetWindow; + win.removeEventListener(event.type, waitForEvent, true); + maybeRunTests(); + } catch (e) { + } + } + + // If the current document is about:blank and we are not expecting a blank + // page (or vice versa), and the document has not yet loaded, wait for the + // page to load. A common situation is to wait for a newly opened window + // to load its content, and we want to skip over any intermediate blank + // pages that load. This issue is described in bug 554873. + ipctest.waitForFocus_loaded = + (expectBlankPage == (targetWindow.location == "about:blank")) && + targetWindow.document.readyState == "complete"; + if (!ipctest.waitForFocus_loaded) { + targetWindow.addEventListener("load", waitForEvent, true); + } + + // Check if the desired window is already focused. + var focusedChildWindow = { }; + if (fm.activeWindow) { + fm.getFocusedElementForWindow(fm.activeWindow, true, focusedChildWindow); + focusedChildWindow = focusedChildWindow.value; + } + + // If this is a child frame, ensure that the frame is focused. + ipctest.waitForFocus_focused = (focusedChildWindow == childTargetWindow); + if (ipctest.waitForFocus_focused) { + maybeRunTests(); + } + else { + //TODO: is this really the childTargetWindow + // and are we really doing something with it here? + if (content) { + var wr = childTargetWindow.wrappedJSObject; + wr.addEventListener("focus", waitForEvent, true); + sendAsyncMessage("chromeEvent", {"type":"Focus", "data":{}}); + wr.focus(); + } else { + childTargetWindow.addEventListener("focus", waitForEvent, true); + childTargetWindow.focus(); + } + } +}; diff --git a/testing/mochitest/runtests.py.in b/testing/mochitest/runtests.py.in index 7626efb7dd77..0936a40b21f9 100644 --- a/testing/mochitest/runtests.py.in +++ b/testing/mochitest/runtests.py.in @@ -167,6 +167,7 @@ class MochitestOptions(optparse.OptionParser): self.add_option("--a11y", action = "store_true", dest = "a11y", help = "run accessibility Mochitests"); + defaults["a11y"] = False self.add_option("--setenv", action = "append", type = "string", @@ -695,6 +696,11 @@ toolbar#nav-bar { manifestFile.write("""overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul """) + elif ((options.chrome == False) and (options.a11y == False)): + #only do the ipc-overlay.xul for mochitest-plain. + #Currently there are focus issues in chrome tests and issues with new windows and dialogs when using ipc + manifestFile.write("overlay chrome://browser/content/browser.xul chrome://mochikit/content/ipc-overlay.xul") + manifestFile.close() return self.installChromeFile(temp_file, options) diff --git a/testing/mochitest/tests/SimpleTest/MozillaFileLogger.js b/testing/mochitest/tests/SimpleTest/MozillaFileLogger.js index 4bc1cff9ceef..76322351ec58 100644 --- a/testing/mochitest/tests/SimpleTest/MozillaFileLogger.js +++ b/testing/mochitest/tests/SimpleTest/MozillaFileLogger.js @@ -1,10 +1,17 @@ /** * MozillaFileLogger, a log listener that can write to a local file. */ + try { netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); - const Cc = Components.classes; - const Ci = Components.interfaces; + + if (Cc === undefined) { + var Cc = Components.classes; + var Ci = Components.interfaces; + } +} catch (ex) {} //running in ipcMode-chrome + +try { const FOSTREAM_CID = "@mozilla.org/network/file-output-stream;1"; const LF_CID = "@mozilla.org/file/local;1"; @@ -32,15 +39,29 @@ try { // exists, no action and NULL is returned. const PR_EXCL = 0x80; } catch (ex) { - // probably not running in the test harness + // probably not running in the test harness } /** Init the file logger with the absolute path to the file. It will create and append if the file already exists **/ -var MozillaFileLogger = {} +var MozillaFileLogger = {}; + +var ipcMode = false; +try { + if (typeof(TestRunner) != undefined) + ipcMode = TestRunner.ipcMode; +} catch(e) { }; MozillaFileLogger.init = function(path) { - netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + if (ipcMode) { + contentAsyncEvent("LoggerInit", {"filename": path}); + return; + } + + try { + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + } catch (ex) {} //running in ipcMode-chrome + MozillaFileLogger._file = Cc[LF_CID].createInstance(Ci.nsILocalFile); MozillaFileLogger._file.initWithPath(path); MozillaFileLogger._foStream = Cc[FOSTREAM_CID].createInstance(Ci.nsIFileOutputStream); @@ -49,8 +70,17 @@ MozillaFileLogger.init = function(path) { } MozillaFileLogger.getLogCallback = function() { + if (ipcMode) { + return function(msg) { + contentAsyncEvent("Logger", {"num": msg.num, "level": msg.level, "info": msg.info.join(' ')}); + } + } + return function (msg) { - netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + try { + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + } catch(ex) {} //running in ipcMode-chrome + var data = msg.num + " " + msg.level + " " + msg.info.join(' ') + "\n"; MozillaFileLogger._foStream.write(data, data.length); if (data.indexOf("SimpleTest FINISH") >= 0) { @@ -60,7 +90,15 @@ MozillaFileLogger.getLogCallback = function() { } MozillaFileLogger.close = function() { - netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + if (ipcMode) { + contentAsyncEvent("LoggerClose"); + return; + } + + try { + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + } catch(ex) {} //running in ipcMode-chrome + MozillaFileLogger._foStream.close(); MozillaFileLogger._foStream = null; MozillaFileLogger._file = null; diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js index 753a4cf0f58d..8991a211e94d 100644 --- a/testing/mochitest/tests/SimpleTest/SimpleTest.js +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -26,6 +26,12 @@ if (typeof(parent) != "undefined" && parent.TestRunner) { parentRunner = parent.wrappedJSObject.TestRunner; } +//Simple test to see if we are running in e10s IPC +var ipcMode = false; +if (parentRunner) { + ipcMode = parentRunner.ipcMode; +} + // Check to see if the TestRunner is present and has logging if (parentRunner) { SimpleTest._logEnabled = parentRunner.logEnabled; @@ -258,6 +264,17 @@ SimpleTest.waitForFocus = function (callback, targetWindow, expectBlankPage) { if (!targetWindow) targetWindow = window; + if (ipcMode) { + var domutils = targetWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + + //TODO: make this support scenarios where we run test standalone and not inside of TestRunner only + if (parent && parent.ipcWaitForFocus != undefined) { + parent.contentAsyncEvent("waitForFocus", {"callback":callback, "targetWindow":domutils.outerWindowID}); + } + return; + } + SimpleTest.waitForFocus_started = false; expectBlankPage = !!expectBlankPage; @@ -372,6 +389,12 @@ SimpleTest.waitForClipboard_polls = 0; * within 5s. It can also be called if the known value can't be found. */ SimpleTest.waitForClipboard = function(aExpectedVal, aSetupFn, aSuccessFn, aFailureFn) { + if (ipcMode) { + //TODO: support waitForClipboard via events to chrome + dump("E10S_TODO: bug 573735 addresses adding support for this"); + return; + } + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); var cbSvc = Components.classes["@mozilla.org/widget/clipboard;1"]. diff --git a/testing/mochitest/tests/SimpleTest/TestRunner.js b/testing/mochitest/tests/SimpleTest/TestRunner.js index 73641f64bf3e..ef27592303e6 100644 --- a/testing/mochitest/tests/SimpleTest/TestRunner.js +++ b/testing/mochitest/tests/SimpleTest/TestRunner.js @@ -1,3 +1,30 @@ +/* + * e10s event dispatcher from content->chrome + * + * type = eventName (QuitApplication, LoggerInit, LoggerClose, Logger, GetPref, SetPref) + * data = json object {"filename":filename} <- for LoggerInit + */ +function contentDispatchEvent(type, data, sync) { + if (typeof(data) == "undefined") { + data = {}; + } + + var element = document.createEvent("datacontainerevent"); + element.initEvent("contentEvent", true, false); + element.setData("sync", sync); + element.setData("type", type); + element.setData("data", JSON.stringify(data)); + document.dispatchEvent(element); +} + +function contentSyncEvent(type, data) { + contentDispatchEvent(type, data, 1); +} + +function contentAsyncEvent(type, data) { + contentDispatchEvent(type, data, 0); +} + /** * TestRunner: A test runner for SimpleTest * TODO: @@ -15,6 +42,16 @@ TestRunner._urls = []; TestRunner.timeout = 5 * 60 * 1000; // 5 minutes. TestRunner.maxTimeouts = 4; // halt testing after too many timeouts +TestRunner.ipcMode = false; // running in e10s build and need to use IPC? +try { + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + var ipcsanity = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + ipcsanity.setIntPref("mochitest.ipcmode", 0); +} catch (e) { + TestRunner.ipcMode = true; +} + /** * Make sure the tests don't hang indefinitely. **/ @@ -89,6 +126,10 @@ TestRunner._makeIframe = function (url, retry) { ("activeElement" in document && document.activeElement != iframe))) { // typically calling ourselves from setTimeout is sufficient // but we'll try focus() just in case that's needed + + if (TestRunner.ipcMode) { + contentAsyncEvent("Focus"); + } window.focus(); iframe.focus(); if (retry < 3) { diff --git a/testing/mochitest/tests/SimpleTest/quit.js b/testing/mochitest/tests/SimpleTest/quit.js index 360ba2df56f8..ebb62ae2b9c4 100644 --- a/testing/mochitest/tests/SimpleTest/quit.js +++ b/testing/mochitest/tests/SimpleTest/quit.js @@ -42,6 +42,12 @@ These files did not have a license */ +//Simple test to see if we are running in e10s IPC +var ipcMode = false; +if (typeof(TestRunner) != "undefined") { + ipcMode = TestRunner.ipcMode; +} + function quitHook() { var xhr = new XMLHttpRequest(); @@ -83,6 +89,11 @@ function canQuitApplication() function goQuitApplication() { + if (ipcMode) { + contentAsyncEvent("QuitApplication"); + return; + } + const privs = 'UniversalXPConnect'; try @@ -109,7 +120,6 @@ function goQuitApplication() appService = Components.classes[kAppStartup]. getService(Components.interfaces.nsIAppStartup); forceQuit = Components.interfaces.nsIAppStartup.eForceQuit; - } else if (kAppShell in Components.classes) { From c03385b08622066ea2d7d7881fa85bb3f97ffd02 Mon Sep 17 00:00:00 2001 From: Saint Wesonga Date: Thu, 24 Jun 2010 13:32:07 -0700 Subject: [PATCH 1792/1860] Bug 512447 - JSON.stringify does not correct handle replacer functions. r=sayrer --- dom/src/json/test/unit/test_replacer.js | 73 +++++++++++++++++++++++++ js/src/json.cpp | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/dom/src/json/test/unit/test_replacer.js b/dom/src/json/test/unit/test_replacer.js index 130f1255136b..10178d9f547a 100644 --- a/dom/src/json/test/unit/test_replacer.js +++ b/dom/src/json/test/unit/test_replacer.js @@ -1,3 +1,38 @@ +/** + * These return* functions are used by the + * replacer tests taken from bug 512447 + */ +function returnObjectFor1(k, v) { + if (k == "1") + return {}; + return v; +} +function returnArrayFor1(k, v) { + if (k == "1") + return []; + return v; +} +function returnNullFor1(k, v) { + if (k == "1") + return null; + return v; +} +function returnStringForUndefined(k, v) { + if (v === undefined) + return "undefined value"; + return v; +} +function returnCycleObjectFor1(k, v) { + if (k == "1") + return object; + return v; +} +function returnCycleArrayFor1(k, v) { + if (k == "1") + return array; + return v; +} + function run_test() { var x = JSON.stringify({key:2},function(k,v){return k?undefined:v;}) do_check_eq("{}", x); @@ -52,5 +87,43 @@ function run_test() { foo = ['e']; var x = JSON.stringify(foo, null, '\t'); do_check_eq(x, '[\n\t"e"\n]'); + + foo = {0:0, 1:1, 2:2, 3:undefined}; + var x = JSON.stringify(foo, returnObjectFor1); + do_check_eq(x, '{"0":0,"1":{},"2":2}'); + + var x = JSON.stringify(foo, returnArrayFor1); + do_check_eq(x, '{"0":0,"1":[],"2":2}'); + + var x = JSON.stringify(foo, returnNullFor1); + do_check_eq(x, '{"0":0,"1":null,"2":2}'); + + var x = JSON.stringify(foo, returnStringForUndefined); + do_check_eq(x, '{"0":0,"1":1,"2":2,"3":"undefined value"}'); + + var thrown = false; + try { + var x = JSON.stringify(foo, returnCycleObjectFor1); + } catch (e) { + thrown = true; + } + do_check_eq(thrown, true); + + var thrown = false; + try { + var x = JSON.stringify(foo, returnCycleArrayFor1); + } catch (e) { + thrown = true; + } + do_check_eq(thrown, true); + + var thrown = false; + foo = [0, 1, 2, undefined]; + try { + var x = JSON.stringify(foo, returnCycleObjectFor1); + } catch (e) { + thrown = true; + } + do_check_eq(thrown, true); } diff --git a/js/src/json.cpp b/js/src/json.cpp index 9fc4ef1467b0..669aa7270d62 100644 --- a/js/src/json.cpp +++ b/js/src/json.cpp @@ -337,7 +337,7 @@ JO(JSContext *cx, jsval *vp, StringifyContext *scx) s->getCharsAndLength(chars, length); if (!write_string(cx, scx->cb, chars, length) || !scx->cb.append(':') || - !Str(cx, id, obj, scx, &outputValue, false)) { + !Str(cx, id, obj, scx, &outputValue, true)) { return JS_FALSE; } } From 51439b51945d3f1239153309357414dfc006abd8 Mon Sep 17 00:00:00 2001 From: Jonathan Kew Date: Thu, 24 Jun 2010 21:38:57 +0100 Subject: [PATCH 1793/1860] bug 574287 - honor the bad-underline blacklist in dwrite font backend. r=bas --- gfx/thebes/src/gfxDWriteFontList.cpp | 11 ++++++++--- gfx/thebes/src/gfxDWriteFonts.cpp | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/gfx/thebes/src/gfxDWriteFontList.cpp b/gfx/thebes/src/gfxDWriteFontList.cpp index ec7c803dc8e1..3118c327111a 100644 --- a/gfx/thebes/src/gfxDWriteFontList.cpp +++ b/gfx/thebes/src/gfxDWriteFontList.cpp @@ -136,6 +136,9 @@ gfxDWriteFontFamily::FindStyleVariations() NS_WARNING("Family with no font faces in it."); } + if (mIsBadUnderlineFamily) { + SetBadUnderlineFonts(); + } } void @@ -531,9 +534,11 @@ gfxDWriteFontList::InitFontList() if (!mFontFamilies.GetWeak(name)) { nsRefPtr fam = - new gfxDWriteFontFamily( - nsDependentString(famName.Elements()), - family); + new gfxDWriteFontFamily(nsDependentString(famName.Elements()), + family); + if (mBadUnderlineFamilyNames.Contains(name)) { + fam->SetBadUnderlineFamily(); + } mFontFamilies.Put(name, fam); } } diff --git a/gfx/thebes/src/gfxDWriteFonts.cpp b/gfx/thebes/src/gfxDWriteFonts.cpp index 21525b2343eb..7f53b9a273c9 100644 --- a/gfx/thebes/src/gfxDWriteFonts.cpp +++ b/gfx/thebes/src/gfxDWriteFonts.cpp @@ -231,7 +231,7 @@ gfxDWriteFont::ComputeMetrics() mFUnitsConvFactor = GetAdjustedSize() / fontMetrics.designUnitsPerEm; - SanitizeMetrics(&mMetrics, PR_FALSE); + SanitizeMetrics(&mMetrics, GetFontEntry()->mIsBadUnderlineFont); #if 0 printf("Font: %p (%s) size: %f\n", this, From 27074f4840412e701942daaf6784bdbdc93eb3b6 Mon Sep 17 00:00:00 2001 From: "L. David Baron" Date: Thu, 24 Jun 2010 14:53:44 -0700 Subject: [PATCH 1794/1860] Don't bother with 16-bit reference count and index in nsCSSValue::Array. (Bug 574059) r=bzbarsky --- layout/base/nsPresContext.cpp | 2 +- layout/style/CSSCalc.h | 2 +- layout/style/nsCSSValue.h | 28 ++++++++++++++-------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp index d748edcf1709..c2a46c6c2794 100644 --- a/layout/base/nsPresContext.cpp +++ b/layout/base/nsPresContext.cpp @@ -1755,7 +1755,7 @@ InsertFontFaceRule(nsCSSFontFaceRule *aRule, gfxUserFontSet* aFontSet, unit = val.GetUnit(); if (unit == eCSSUnit_Array) { nsCSSValue::Array *srcArr = val.GetArrayValue(); - PRUint32 i, numSrc = srcArr->Count(); + size_t i, numSrc = srcArr->Count(); for (i = 0; i < numSrc; i++) { val = srcArr->Item(i); diff --git a/layout/style/CSSCalc.h b/layout/style/CSSCalc.h index e3ad35662004..4365fab2c69a 100644 --- a/layout/style/CSSCalc.h +++ b/layout/style/CSSCalc.h @@ -126,7 +126,7 @@ ComputeCalc(const nsCSSValue& aValue, CalcOps &aOps) case eCSSUnit_Calc_Maximum: { nsCSSValue::Array *arr = aValue.GetArrayValue(); typename CalcOps::result_type result = ComputeCalc(arr->Item(0), aOps); - for (PRUint32 i = 1, i_end = arr->Count(); i < i_end; ++i) { + for (size_t i = 1, i_end = arr->Count(); i < i_end; ++i) { typename CalcOps::result_type tmp = ComputeCalc(arr->Item(i), aOps); result = aOps.MergeAdditive(aValue.GetUnit(), result, tmp); } diff --git a/layout/style/nsCSSValue.h b/layout/style/nsCSSValue.h index 5f239c690c12..1754c8b8f8cd 100644 --- a/layout/style/nsCSSValue.h +++ b/layout/style/nsCSSValue.h @@ -527,39 +527,39 @@ private: struct nsCSSValue::Array { // return |Array| with reference count of zero - static Array* Create(PRUint16 aItemCount) { + static Array* Create(size_t aItemCount) { return new (aItemCount) Array(aItemCount); } - nsCSSValue& operator[](PRUint16 aIndex) { + nsCSSValue& operator[](size_t aIndex) { NS_ASSERTION(aIndex < mCount, "out of range"); return mArray[aIndex]; } - const nsCSSValue& operator[](PRUint16 aIndex) const { + const nsCSSValue& operator[](size_t aIndex) const { NS_ASSERTION(aIndex < mCount, "out of range"); return mArray[aIndex]; } - nsCSSValue& Item(PRUint16 aIndex) { return (*this)[aIndex]; } - const nsCSSValue& Item(PRUint16 aIndex) const { return (*this)[aIndex]; } + nsCSSValue& Item(size_t aIndex) { return (*this)[aIndex]; } + const nsCSSValue& Item(size_t aIndex) const { return (*this)[aIndex]; } - PRUint16 Count() const { return mCount; } + size_t Count() const { return mCount; } PRBool operator==(const Array& aOther) const { if (mCount != aOther.mCount) return PR_FALSE; - for (PRUint16 i = 0; i < mCount; ++i) + for (size_t i = 0; i < mCount; ++i) if ((*this)[i] != aOther[i]) return PR_FALSE; return PR_TRUE; } - // XXXdholbert This uses a 16-bit ref count to save space. Should we use + // XXXdholbert This uses a size_t ref count to save space. Should we use // a variant of NS_INLINE_DECL_REFCOUNTING that takes a type as an argument? void AddRef() { - if (mRefCnt == PR_UINT16_MAX) { + if (mRefCnt == size_t(-1)) { // really want SIZE_MAX NS_WARNING("refcount overflow, leaking nsCSSValue::Array"); return; } @@ -567,7 +567,7 @@ struct nsCSSValue::Array { NS_LOG_ADDREF(this, mRefCnt, "nsCSSValue::Array", sizeof(*this)); } void Release() { - if (mRefCnt == PR_UINT16_MAX) { + if (mRefCnt == size_t(-1)) { // really want SIZE_MAX NS_WARNING("refcount overflow, leaking nsCSSValue::Array"); return; } @@ -579,14 +579,14 @@ struct nsCSSValue::Array { private: - PRUint16 mRefCnt; - const PRUint16 mCount; + size_t mRefCnt; + const size_t mCount; // This must be the last sub-object, since we extend this array to // be of size mCount; it needs to be a sub-object so it gets proper // alignment. nsCSSValue mArray[1]; - void* operator new(size_t aSelfSize, PRUint16 aItemCount) CPP_THROW_NEW { + void* operator new(size_t aSelfSize, size_t aItemCount) CPP_THROW_NEW { NS_ABORT_IF_FALSE(aItemCount > 0, "cannot have a 0 item count"); return ::operator new(aSelfSize + sizeof(nsCSSValue) * (aItemCount - 1)); } @@ -601,7 +601,7 @@ private: for (nsCSSValue *var = First() + 1, *var##_end = First() + mCount; \ var != var##_end; ++var) - Array(PRUint16 aItemCount) + Array(size_t aItemCount) : mRefCnt(0) , mCount(aItemCount) { From 4157ac95dd236b5954334f5890d2ca5bec1f28a9 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Thu, 24 Jun 2010 15:01:27 -0700 Subject: [PATCH 1795/1860] Bug 573246: Fix tests that are failing with the Feedback extension installed. r=gavin --- browser/base/content/test/browser_bug462289.js | 12 +++++++++++- browser/fuel/test/browser_ApplicationPrefs.js | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/browser/base/content/test/browser_bug462289.js b/browser/base/content/test/browser_bug462289.js index 4e576133ad55..12aaf3c9df98 100644 --- a/browser/base/content/test/browser_bug462289.js +++ b/browser/base/content/test/browser_bug462289.js @@ -1,5 +1,13 @@ var tab1, tab2; +function focus_in_navbar() { + var parent = document.activeElement.parentNode; + while (parent && parent.id != "nav-bar") + parent = parent.parentNode; + + return (parent != null); +} + function test() { waitForExplicitFinish(); @@ -27,7 +35,9 @@ function step3() EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}); } else { document.getElementById("searchbar").focus(); - EventUtils.synthesizeKey("VK_TAB", {}); + + while (focus_in_navbar()) + EventUtils.synthesizeKey("VK_TAB", { }); } is(document.activeElement, tab1, "tab key to tab activeElement"); diff --git a/browser/fuel/test/browser_ApplicationPrefs.js b/browser/fuel/test/browser_ApplicationPrefs.js index d505f708392c..e88bc22828df 100644 --- a/browser/fuel/test/browser_ApplicationPrefs.js +++ b/browser/fuel/test/browser_ApplicationPrefs.js @@ -148,6 +148,9 @@ function test() { pref.locked = false; ok(!pref.locked, "A single preference is unlocked."); + // Preference events tests disabled until bug 533290 is fixed + return; + // check for change event when setting a value waitForExplicitFinish(); Application.prefs.events.addListener("change", onPrefChange); From cc070f81d535ca9bf5e3a3974fc32b0da1039a89 Mon Sep 17 00:00:00 2001 From: Andreas Gal Date: Thu, 24 Jun 2010 15:18:07 -0700 Subject: [PATCH 1796/1860] Bug 571698 - TM: turn off int/double speculation when we record many peer trees --- js/src/jstracer.cpp | 133 +++++++++++++++++++++++--------------------- js/src/jstracer.h | 14 +++-- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/js/src/jstracer.cpp b/js/src/jstracer.cpp index cc9dbba977a0..5609af07a5c8 100644 --- a/js/src/jstracer.cpp +++ b/js/src/jstracer.cpp @@ -1153,10 +1153,6 @@ Oracle::Oracle() JS_REQUIRES_STACK void Oracle::markGlobalSlotUndemotable(JSContext* cx, unsigned slot) { - #ifdef DEBUG_dvander - printf("MGSU: %d [%08x]: %d\n", slot, GlobalSlotHash(cx, slot), - _globalDontDemote.get(GlobalSlotHash(cx, slot))); - #endif _globalDontDemote.set(GlobalSlotHash(cx, slot)); } @@ -1164,10 +1160,6 @@ Oracle::markGlobalSlotUndemotable(JSContext* cx, unsigned slot) JS_REQUIRES_STACK bool Oracle::isGlobalSlotUndemotable(JSContext* cx, unsigned slot) const { - #ifdef DEBUG_dvander - printf("IGSU: %d [%08x]: %d\n", slot, GlobalSlotHash(cx, slot), - _globalDontDemote.get(GlobalSlotHash(cx, slot))); - #endif return _globalDontDemote.get(GlobalSlotHash(cx, slot)); } @@ -1175,10 +1167,6 @@ Oracle::isGlobalSlotUndemotable(JSContext* cx, unsigned slot) const JS_REQUIRES_STACK void Oracle::markStackSlotUndemotable(JSContext* cx, unsigned slot, const void* pc) { - #ifdef DEBUG_dvander - printf("MSSU: %p:%d [%08x]: %d\n", pc, slot, StackSlotHash(cx, slot, pc), - _stackDontDemote.get(StackSlotHash(cx, slot, pc))); - #endif _stackDontDemote.set(StackSlotHash(cx, slot, pc)); } @@ -1192,10 +1180,6 @@ Oracle::markStackSlotUndemotable(JSContext* cx, unsigned slot) JS_REQUIRES_STACK bool Oracle::isStackSlotUndemotable(JSContext* cx, unsigned slot, const void* pc) const { - #ifdef DEBUG_dvander - printf("ISSU: %p:%d [%08x]: %d\n", pc, slot, StackSlotHash(cx, slot, pc), - _stackDontDemote.get(StackSlotHash(cx, slot, pc))); - #endif return _stackDontDemote.get(StackSlotHash(cx, slot, pc)); } @@ -1231,34 +1215,34 @@ JS_REQUIRES_STACK void TraceRecorder::markSlotUndemotable(LinkableFragment* f, unsigned slot) { if (slot < f->nStackTypes) { - oracle->markStackSlotUndemotable(cx, slot); + traceMonitor->oracle->markStackSlotUndemotable(cx, slot); return; } uint16* gslots = f->globalSlots->data(); - oracle->markGlobalSlotUndemotable(cx, gslots[slot - f->nStackTypes]); + traceMonitor->oracle->markGlobalSlotUndemotable(cx, gslots[slot - f->nStackTypes]); } JS_REQUIRES_STACK void TraceRecorder::markSlotUndemotable(LinkableFragment* f, unsigned slot, const void* pc) { if (slot < f->nStackTypes) { - oracle->markStackSlotUndemotable(cx, slot, pc); + traceMonitor->oracle->markStackSlotUndemotable(cx, slot, pc); return; } uint16* gslots = f->globalSlots->data(); - oracle->markGlobalSlotUndemotable(cx, gslots[slot - f->nStackTypes]); + traceMonitor->oracle->markGlobalSlotUndemotable(cx, gslots[slot - f->nStackTypes]); } static JS_REQUIRES_STACK bool IsSlotUndemotable(Oracle* oracle, JSContext* cx, LinkableFragment* f, unsigned slot, const void* ip) { if (slot < f->nStackTypes) - return oracle->isStackSlotUndemotable(cx, slot, ip); + return !oracle || oracle->isStackSlotUndemotable(cx, slot, ip); uint16* gslots = f->globalSlots->data(); - return oracle->isGlobalSlotUndemotable(cx, gslots[slot - f->nStackTypes]); + return !oracle || oracle->isGlobalSlotUndemotable(cx, gslots[slot - f->nStackTypes]); } class FrameInfoCache @@ -1465,14 +1449,14 @@ AddNewPeerToPeerList(TraceMonitor* tm, TreeFragment* peer) } JS_REQUIRES_STACK void -TreeFragment::initialize(JSContext* cx, SlotList *globalSlots) +TreeFragment::initialize(JSContext* cx, SlotList *globalSlots, bool speculate) { this->dependentTrees.clear(); this->linkedTrees.clear(); this->globalSlots = globalSlots; /* Capture the coerced type of each active slot in the type map. */ - this->typeMap.captureTypes(cx, globalObj, *globalSlots, 0 /* callDepth */); + this->typeMap.captureTypes(cx, globalObj, *globalSlots, 0 /* callDepth */, speculate); this->nStackTypes = this->typeMap.length() - globalSlots->length(); this->spOffsetAtEntry = cx->regs->sp - StackBase(cx->fp); @@ -1961,19 +1945,19 @@ class CaptureTypesVisitor : public SlotVisitorBase JSContext* mCx; TraceType* mTypeMap; TraceType* mPtr; + Oracle * mOracle; public: - JS_ALWAYS_INLINE CaptureTypesVisitor(JSContext* cx, TraceType* typeMap) : + JS_ALWAYS_INLINE CaptureTypesVisitor(JSContext* cx, TraceType* typeMap, bool speculate) : mCx(cx), mTypeMap(typeMap), - mPtr(typeMap) - {} + mPtr(typeMap), + mOracle(speculate ? JS_TRACE_MONITOR(cx).oracle : NULL) {} JS_REQUIRES_STACK JS_ALWAYS_INLINE void visitGlobalSlot(jsval *vp, unsigned n, unsigned slot) { TraceType type = getCoercedType(*vp); - if (type == TT_INT32 && - JS_TRACE_MONITOR(mCx).oracle->isGlobalSlotUndemotable(mCx, slot)) + if (type == TT_INT32 && (!mOracle || mOracle->isGlobalSlotUndemotable(mCx, slot))) type = TT_DOUBLE; JS_ASSERT(type != TT_JSVAL); debug_only_printf(LC_TMTracer, @@ -1986,8 +1970,7 @@ public: visitStackSlots(jsval *vp, int count, JSStackFrame* fp) { for (int i = 0; i < count; ++i) { TraceType type = getCoercedType(vp[i]); - if (type == TT_INT32 && - JS_TRACE_MONITOR(mCx).oracle->isStackSlotUndemotable(mCx, length())) + if (type == TT_INT32 && (!mOracle || mOracle->isStackSlotUndemotable(mCx, length()))) type = TT_DOUBLE; JS_ASSERT(type != TT_JSVAL); debug_only_printf(LC_TMTracer, @@ -2017,22 +2000,24 @@ TypeMap::set(unsigned stackSlots, unsigned ngslots, * stack frames. */ JS_REQUIRES_STACK void -TypeMap::captureTypes(JSContext* cx, JSObject* globalObj, SlotList& slots, unsigned callDepth) +TypeMap::captureTypes(JSContext* cx, JSObject* globalObj, SlotList& slots, unsigned callDepth, + bool speculate) { setLength(NativeStackSlots(cx, callDepth) + slots.length()); - CaptureTypesVisitor visitor(cx, data()); + CaptureTypesVisitor visitor(cx, data(), speculate); VisitSlots(visitor, cx, globalObj, callDepth, slots); JS_ASSERT(visitor.length() == length()); } JS_REQUIRES_STACK void -TypeMap::captureMissingGlobalTypes(JSContext* cx, JSObject* globalObj, SlotList& slots, unsigned stackSlots) +TypeMap::captureMissingGlobalTypes(JSContext* cx, JSObject* globalObj, SlotList& slots, unsigned stackSlots, + bool speculate) { unsigned oldSlots = length() - stackSlots; int diff = slots.length() - oldSlots; JS_ASSERT(diff >= 0); setLength(length() + diff); - CaptureTypesVisitor visitor(cx, data() + stackSlots + oldSlots); + CaptureTypesVisitor visitor(cx, data() + stackSlots + oldSlots, speculate); VisitGlobalSlots(visitor, cx, globalObj, diff, slots.data() + oldSlots); } @@ -2099,9 +2084,14 @@ SpecializeTreesToLateGlobals(JSContext* cx, TreeFragment* root, TraceType* globa static JS_REQUIRES_STACK void SpecializeTreesToMissingGlobals(JSContext* cx, JSObject* globalObj, TreeFragment* root) { - root->typeMap.captureMissingGlobalTypes(cx, globalObj, *root->globalSlots, root->nStackTypes); - JS_ASSERT(root->globalSlots->length() == root->typeMap.length() - root->nStackTypes); + /* If we already have a bunch of peer trees, try to be as generic as possible. */ + size_t count = 0; + for (TreeFragment *f = root->first; f; f = f->peer, ++count); + bool speculate = count < MAXPEERS-1; + root->typeMap.captureMissingGlobalTypes(cx, globalObj, *root->globalSlots, root->nStackTypes, + speculate); + JS_ASSERT(root->globalSlots->length() == root->typeMap.length() - root->nStackTypes); SpecializeTreesToLateGlobals(cx, root, root->globalTypeMap(), root->nGlobalTypes()); } @@ -2140,10 +2130,10 @@ JS_REQUIRES_STACK TraceRecorder::TraceRecorder(JSContext* cx, VMSideExit* anchor, VMFragment* fragment, unsigned stackSlots, unsigned ngslots, TraceType* typeMap, VMSideExit* innermost, jsbytecode* outer, uint32 outerArgc, - RecordReason recordReason) + RecordReason recordReason, bool speculate) : cx(cx), traceMonitor(&JS_TRACE_MONITOR(cx)), - oracle(JS_TRACE_MONITOR(cx).oracle), + oracle(speculate ? JS_TRACE_MONITOR(cx).oracle : NULL), fragment(fragment), tree(fragment->root), recordReason(recordReason), @@ -3552,7 +3542,7 @@ TraceRecorder::importGlobalSlot(unsigned slot) int index = tree->globalSlots->offsetOf(uint16(slot)); if (index == -1) { type = getCoercedType(*vp); - if (type == TT_INT32 && oracle->isGlobalSlotUndemotable(cx, slot)) + if (type == TT_INT32 && (!oracle || oracle->isGlobalSlotUndemotable(cx, slot))) type = TT_DOUBLE; index = (int)tree->globalSlots->length(); tree->globalSlots->add(uint16(slot)); @@ -3783,7 +3773,7 @@ public: * Aggressively undo speculation so the inner tree will compile * if this fails. */ - mRecorder.oracle->markGlobalSlotUndemotable(mCx, slot); + JS_TRACE_MONITOR(mCx).oracle->markGlobalSlotUndemotable(mCx, slot); } JS_ASSERT(!(!isPromote && *mTypeMap == TT_INT32)); ++mTypeMap; @@ -3827,7 +3817,7 @@ public: * Aggressively undo speculation so the inner tree will compile * if this fails. */ - mRecorder.oracle->markStackSlotUndemotable(mCx, mSlotnum); + JS_TRACE_MONITOR(mCx).oracle->markStackSlotUndemotable(mCx, mSlotnum); } JS_ASSERT(!(!isPromote && *mTypeMap == TT_INT32)); ++vp; @@ -5354,14 +5344,16 @@ bool JS_REQUIRES_STACK TraceRecorder::startRecorder(JSContext* cx, VMSideExit* anchor, VMFragment* f, unsigned stackSlots, unsigned ngslots, TraceType* typeMap, VMSideExit* expectedInnerExit, - jsbytecode* outer, uint32 outerArgc, RecordReason recordReason) + jsbytecode* outer, uint32 outerArgc, RecordReason recordReason, + bool speculate) { TraceMonitor *tm = &JS_TRACE_MONITOR(cx); JS_ASSERT(!tm->needFlush); JS_ASSERT_IF(cx->fp->imacpc, f->root != f); tm->recorder = new TraceRecorder(cx, anchor, f, stackSlots, ngslots, typeMap, - expectedInnerExit, outer, outerArgc, recordReason); + expectedInnerExit, outer, outerArgc, recordReason, + speculate); if (!tm->recorder || tm->outOfMemory() || OverfullJITCache(tm)) { ResetJIT(cx, FR_OOM); @@ -5564,19 +5556,26 @@ SynthesizeSlowNativeFrame(TracerState& state, JSContext *cx, VMSideExit *exit) } static JS_REQUIRES_STACK bool -RecordTree(JSContext* cx, TreeFragment* peer, jsbytecode* outer, +RecordTree(JSContext* cx, TreeFragment* first, jsbytecode* outer, uint32 outerArgc, SlotList* globalSlots, RecordReason reason) { TraceMonitor* tm = &JS_TRACE_MONITOR(cx); /* Try to find an unused peer fragment, or allocate a new one. */ - TreeFragment* f = peer; - while (f->code() && f->peer) - f = f->peer; - if (f->code()) - f = AddNewPeerToPeerList(tm, f); + JS_ASSERT(first->first == first); + TreeFragment* f = NULL; + size_t count = 0; + for (TreeFragment* peer = first; peer; peer = peer->peer, ++count) { + if (!peer->code()) + f = peer; + } + if (!f) + f = AddNewPeerToPeerList(tm, first); JS_ASSERT(f->root == f); + /* Disable speculation if we are starting to accumulate a lot of trees. */ + bool speculate = count < MAXPEERS-1; + /* save a local copy for use after JIT flush */ const void* localRootIP = f->root->ip; @@ -5598,7 +5597,7 @@ RecordTree(JSContext* cx, TreeFragment* peer, jsbytecode* outer, JS_ASSERT(!f->code()); - f->initialize(cx, globalSlots); + f->initialize(cx, globalSlots, speculate); #ifdef DEBUG AssertTreeIsUnique(tm, f); @@ -5620,7 +5619,8 @@ RecordTree(JSContext* cx, TreeFragment* peer, jsbytecode* outer, return TraceRecorder::startRecorder(cx, NULL, f, f->nStackTypes, f->globalSlots->length(), f->typeMap.data(), NULL, - outer, outerArgc, reason); + outer, outerArgc, reason, + speculate); } static JS_REQUIRES_STACK TypeConsensus @@ -5856,7 +5856,8 @@ AttemptToExtendTree(JSContext* cx, VMSideExit* anchor, VMSideExit* exitedFrom, j } JS_ASSERT(ngslots >= anchor->numGlobalSlots); bool rv = TraceRecorder::startRecorder(cx, anchor, c, stackSlots, ngslots, typeMap, - exitedFrom, outer, cx->fp->argc, Record_Branch); + exitedFrom, outer, cx->fp->argc, Record_Branch, + hits < maxHits); #ifdef MOZ_TRACEVIS if (!rv && tvso) tvso->r = R_FAIL_EXTEND_START; @@ -6042,7 +6043,7 @@ TraceRecorder::attemptTreeCall(TreeFragment* f, uintN& inlineCallCount) } case OVERFLOW_EXIT: - oracle->markInstructionUndemotable(cx->regs->pc); + traceMonitor->oracle->markInstructionUndemotable(cx->regs->pc); /* FALL THROUGH */ case RECURSIVE_SLURP_FAIL_EXIT: case RECURSIVE_SLURP_MISMATCH_EXIT: @@ -6050,9 +6051,9 @@ TraceRecorder::attemptTreeCall(TreeFragment* f, uintN& inlineCallCount) case RECURSIVE_EMPTY_RP_EXIT: case BRANCH_EXIT: case CASE_EXIT: { - /* Abort recording the outer tree, extend the inner tree. */ - AbortRecording(cx, "Inner tree is trying to grow, abort outer recording"); - return AttemptToExtendTree(localCx, lr, NULL, outer) ? ARECORD_CONTINUE : ARECORD_ABORTED; + /* Abort recording the outer tree, extend the inner tree. */ + AbortRecording(cx, "Inner tree is trying to grow, abort outer recording"); + return AttemptToExtendTree(localCx, lr, NULL, outer) ? ARECORD_CONTINUE : ARECORD_ABORTED; } case NESTED_EXIT: @@ -6136,6 +6137,7 @@ class TypeCompatibilityVisitor : public SlotVisitorBase { TraceRecorder &mRecorder; JSContext *mCx; + Oracle *mOracle; TraceType *mTypeMap; unsigned mStackSlotNum; bool mOk; @@ -6144,6 +6146,7 @@ public: TraceType *typeMap) : mRecorder(recorder), mCx(mRecorder.cx), + mOracle(JS_TRACE_MONITOR(mCx).oracle), mTypeMap(typeMap), mStackSlotNum(0), mOk(true) @@ -6155,10 +6158,10 @@ public: if (!IsEntryTypeCompatible(vp, mTypeMap)) { mOk = false; } else if (!isPromoteInt(mRecorder.get(vp)) && *mTypeMap == TT_INT32) { - mRecorder.oracle->markGlobalSlotUndemotable(mCx, slot); + mOracle->markGlobalSlotUndemotable(mCx, slot); mOk = false; } else if (JSVAL_IS_INT(*vp) && *mTypeMap == TT_DOUBLE) { - mRecorder.oracle->markGlobalSlotUndemotable(mCx, slot); + mOracle->markGlobalSlotUndemotable(mCx, slot); } mTypeMap++; } @@ -6170,10 +6173,10 @@ public: if (!IsEntryTypeCompatible(vp, mTypeMap)) { mOk = false; } else if (!isPromoteInt(mRecorder.get(vp)) && *mTypeMap == TT_INT32) { - mRecorder.oracle->markStackSlotUndemotable(mCx, mStackSlotNum); + mOracle->markStackSlotUndemotable(mCx, mStackSlotNum); mOk = false; } else if (JSVAL_IS_INT(*vp) && *mTypeMap == TT_DOUBLE) { - mRecorder.oracle->markStackSlotUndemotable(mCx, mStackSlotNum); + mOracle->markStackSlotUndemotable(mCx, mStackSlotNum); } vp++; mTypeMap++; @@ -8102,7 +8105,8 @@ TraceRecorder::alu(LOpcode v, jsdouble v0, jsdouble v1, LIns* s0, LIns* s1) * integers and the oracle must not give us a negative hint for the * instruction. */ - if (oracle->isInstructionUndemotable(cx->regs->pc) || !isPromoteInt(s0) || !isPromoteInt(s1)) { + if (!oracle || oracle->isInstructionUndemotable(cx->regs->pc) || + !isPromoteInt(s0) || !isPromoteInt(s1)) { out: if (v == LIR_modd) { LIns* args[] = { s1, s0 }; @@ -10344,7 +10348,8 @@ TraceRecorder::record_JSOP_NEG() * a double. Only follow this path if we're not an integer that's 0 and * we're not a double that's zero. */ - if (!oracle->isInstructionUndemotable(cx->regs->pc) && + if (oracle && + !oracle->isInstructionUndemotable(cx->regs->pc) && isPromoteInt(a) && (!JSVAL_IS_INT(v) || JSVAL_TO_INT(v) != 0) && (!JSVAL_IS_DOUBLE(v) || !JSDOUBLE_IS_NEGZERO(*JSVAL_TO_DOUBLE(v))) && @@ -15594,7 +15599,7 @@ StopTraceVisNative(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval JS_REQUIRES_STACK void TraceRecorder::captureStackTypes(unsigned callDepth, TraceType* typeMap) { - CaptureTypesVisitor capVisitor(cx, typeMap); + CaptureTypesVisitor capVisitor(cx, typeMap, !!oracle); VisitStackSlots(capVisitor, cx, callDepth); } diff --git a/js/src/jstracer.h b/js/src/jstracer.h index 360fa2000459..3b657de5176b 100644 --- a/js/src/jstracer.h +++ b/js/src/jstracer.h @@ -375,13 +375,15 @@ const uint32 TT_INVALID = uint32(-1); typedef Queue SlotList; class TypeMap : public Queue { + Oracle *oracle; public: TypeMap(nanojit::Allocator* alloc) : Queue(alloc) {} void set(unsigned stackSlots, unsigned ngslots, const TraceType* stackTypeMap, const TraceType* globalTypeMap); - JS_REQUIRES_STACK void captureTypes(JSContext* cx, JSObject* globalObj, SlotList& slots, unsigned callDepth); + JS_REQUIRES_STACK void captureTypes(JSContext* cx, JSObject* globalObj, SlotList& slots, unsigned callDepth, + bool speculate); JS_REQUIRES_STACK void captureMissingGlobalTypes(JSContext* cx, JSObject* globalObj, SlotList& slots, - unsigned stackSlots); + unsigned stackSlots, bool speculate); bool matches(TypeMap& other) const; void fromRaw(TraceType* other, unsigned numSlots); }; @@ -740,7 +742,7 @@ struct TreeFragment : public LinkableFragment return typeMap.data(); } - JS_REQUIRES_STACK void initialize(JSContext* cx, SlotList *globalSlots); + JS_REQUIRES_STACK void initialize(JSContext* cx, SlotList *globalSlots, bool speculate); UnstableExit* removeUnstableExit(VMSideExit* exit); }; @@ -1432,7 +1434,7 @@ class TraceRecorder TraceRecorder(JSContext* cx, VMSideExit*, VMFragment*, unsigned stackSlots, unsigned ngslots, TraceType* typeMap, VMSideExit* expectedInnerExit, jsbytecode* outerTree, - uint32 outerArgc, RecordReason reason); + uint32 outerArgc, RecordReason reason, bool speculate); /* The destructor should only be called through finish*, not directly. */ ~TraceRecorder(); @@ -1459,12 +1461,14 @@ public: startRecorder(JSContext*, VMSideExit*, VMFragment*, unsigned stackSlots, unsigned ngslots, TraceType* typeMap, VMSideExit* expectedInnerExit, jsbytecode* outerTree, - uint32 outerArgc, RecordReason reason); + uint32 outerArgc, RecordReason reason, + bool speculate); /* Accessors. */ VMFragment* getFragment() const { return fragment; } TreeFragment* getTree() const { return tree; } bool outOfMemory() const { return traceMonitor->outOfMemory(); } + Oracle* getOracle() const { return oracle; } /* Entry points / callbacks from the interpreter. */ JS_REQUIRES_STACK AbortableRecordingStatus monitorRecording(JSOp op); From 100dcef3b50ef4d720d285ded6381e9bb18dc722 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Thu, 24 Jun 2010 16:36:15 -0700 Subject: [PATCH 1797/1860] Bug 573079: Import the Beta Feedback extension into the tree. r=beltzner --- .../chrome.manifest | 10 + .../components/TestPilot.js | 84 + .../content/all-studies-window.js | 446 ++ .../content/all-studies-window.xul | 138 + .../content/browser.css | 240 + .../content/browser.js | 176 + .../content/debug.html | 223 + .../content/experiment-page.js | 457 ++ .../content/feedback-browser.xul | 81 + .../content/flot/jquery.colorhelpers.js | 174 + .../content/flot/jquery.colorhelpers.min.js | 1 + .../content/flot/jquery.flot.crosshair.js | 156 + .../content/flot/jquery.flot.crosshair.min.js | 1 + .../content/flot/jquery.flot.image.js | 237 + .../content/flot/jquery.flot.image.min.js | 1 + .../content/flot/jquery.flot.js | 2119 ++++++++ .../content/flot/jquery.flot.min.js | 1 + .../content/flot/jquery.flot.navigate.js | 272 + .../content/flot/jquery.flot.navigate.min.js | 1 + .../content/flot/jquery.flot.selection.js | 299 ++ .../content/flot/jquery.flot.selection.min.js | 1 + .../content/flot/jquery.flot.stack.js | 152 + .../content/flot/jquery.flot.stack.min.js | 1 + .../content/flot/jquery.flot.threshold.js | 103 + .../content/flot/jquery.flot.threshold.min.js | 1 + .../content/flot/jquery.js | 4376 +++++++++++++++++ .../content/flot/jquery.min.js | 19 + .../content/raw-data-dialog.js | 88 + .../content/raw-data-dialog.xul | 23 + .../content/screen.css | 258 + .../content/status-quit.html | 57 + .../content/status.html | 54 + .../content/survey-generator.js | 326 ++ .../content/take-survey.html | 53 + .../content/tp-browser.xul | 95 + .../content/welcome-page.js | 105 + .../content/welcome.html | 55 + .../content/window-utils.js | 139 + .../defaults/preferences/preferences.js | 20 + .../testpilot@labs.mozilla.com/install.rdf | 27 + .../instrument/chrome.manifest | 3 + .../instrument/install.rdf | 19 + .../instrument/instrument.jsm | 44 + .../instrument/instrument.xul | 12 + .../modules/Observers.js | 183 + .../modules/dbutils.js | 96 + .../modules/experiment_data_store.js | 334 ++ .../modules/extension-update.js | 89 + .../modules/feedback.js | 83 + .../modules/jar-code-store.js | 254 + .../modules/lib/cuddlefish.js | 151 + .../modules/lib/memory.js | 115 + .../modules/lib/observer-service.js | 191 + .../modules/lib/plain-text-console.js | 100 + .../modules/lib/preferences-service.js | 138 + .../modules/lib/securable-module.js | 320 ++ .../modules/lib/timer.js | 88 + .../modules/lib/traceback.js | 140 + .../modules/lib/unit-test.js | 231 + .../modules/lib/unload.js | 15 + .../modules/lib/url.js | 110 + .../modules/log4moz.js | 565 +++ .../modules/metadata.js | 175 + .../modules/remote-experiment-loader.js | 339 ++ .../modules/setup.js | 858 ++++ .../modules/string_sanitizer.js | 56 + .../modules/tasks.js | 1100 +++++ .../skin/all/badge-default.png | Bin 0 -> 19738 bytes .../skin/all/bg.jpg | Bin 0 -> 98705 bytes .../skin/all/css/screen-standalone.css | 316 ++ .../skin/all/dino_32x32.png | Bin 0 -> 4443 bytes .../skin/all/fonts/DroidSans-Bold.ttf | Bin 0 -> 150804 bytes .../skin/all/fonts/DroidSans.ttf | Bin 0 -> 149076 bytes .../skin/all/fonts/DroidSerif-Bold.ttf | Bin 0 -> 172012 bytes .../skin/all/fonts/DroidSerif-BoldItalic.ttf | Bin 0 -> 137212 bytes .../skin/all/fonts/DroidSerif-Italic.ttf | Bin 0 -> 155220 bytes .../skin/all/fonts/DroidSerif-Regular.ttf | Bin 0 -> 162864 bytes .../skin/all/images/bg-status.jpg | Bin 0 -> 86130 bytes .../skin/all/images/callout.png | Bin 0 -> 18197 bytes .../skin/all/images/callout_continue.png | Bin 0 -> 15049 bytes .../skin/all/images/data1.jpg | Bin 0 -> 24556 bytes .../skin/all/images/data2.jpg | Bin 0 -> 24506 bytes .../skin/all/images/home_comments.png | Bin 0 -> 2648 bytes .../skin/all/images/home_computer.png | Bin 0 -> 4623 bytes .../skin/all/images/home_continue.png | Bin 0 -> 693 bytes .../skin/all/images/home_quit.png | Bin 0 -> 1030 bytes .../skin/all/images/home_results.png | Bin 0 -> 4086 bytes .../skin/all/images/home_twitter.png | Bin 0 -> 3502 bytes .../skin/all/images/home_upcoming.png | Bin 0 -> 3638 bytes .../skin/all/logo.png | Bin 0 -> 3600 bytes .../skin/all/mozilla-logo.png | Bin 0 -> 989 bytes .../skin/all/status-completed.png | Bin 0 -> 4233 bytes .../skin/all/status-ejected.png | Bin 0 -> 2363 bytes .../skin/all/status-missed.png | Bin 0 -> 2864 bytes .../skin/all/testPilot_200x200.png | Bin 0 -> 45469 bytes .../skin/all/testpilot_16x16.png | Bin 0 -> 710 bytes .../skin/all/testpilot_32x32.png | Bin 0 -> 2691 bytes .../skin/all/tp-completedstudies-32x32.png | Bin 0 -> 1781 bytes .../skin/all/tp-currentstudies-32x32.png | Bin 0 -> 1365 bytes .../skin/all/tp-generic-32x32.png | Bin 0 -> 1601 bytes .../skin/all/tp-learned-32x32.png | Bin 0 -> 1361 bytes .../skin/all/tp-results-48x48.png | Bin 0 -> 2803 bytes .../skin/all/tp-settings-32x32.png | Bin 0 -> 1156 bytes .../skin/all/tp-study-48x48.png | Bin 0 -> 1408 bytes .../skin/all/tp-submit-48x48.png | Bin 0 -> 2764 bytes .../skin/linux/close_button.png | Bin 0 -> 1377 bytes .../skin/linux/feedback-frown-16x16.png | Bin 0 -> 922 bytes .../skin/linux/feedback-smile-16x16.png | Bin 0 -> 951 bytes .../skin/linux/feedback.css | 27 + .../skin/mac/close_button.png | Bin 0 -> 1576 bytes .../skin/mac/feedback-frown-16x16.png | Bin 0 -> 805 bytes .../skin/mac/feedback-smile-16x16.png | Bin 0 -> 933 bytes .../skin/mac/feedback.css | 34 + .../skin/mac/notification-tail-down.png | Bin 0 -> 2286 bytes .../skin/mac/notification-tail-up.png | Bin 0 -> 6964 bytes .../skin/win/close_button.png | Bin 0 -> 1377 bytes .../skin/win/feedback-frown-16x16.png | Bin 0 -> 810 bytes .../skin/win/feedback-smile-16x16.png | Bin 0 -> 844 bytes .../skin/win/feedback.css | 0 .../skin/win/notification-tail-down.png | Bin 0 -> 4231 bytes .../skin/win/notification-tail-up.png | Bin 0 -> 5463 bytes .../tests/test_data_store.js | 309 ++ browser/locales/en-US/feedback/main.dtd | 30 + .../locales/en-US/feedback/main.properties | 92 + 124 files changed, 17687 insertions(+) create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/chrome.manifest create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/components/TestPilot.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.xul create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/browser.css create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/browser.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/debug.html create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/experiment-page.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/feedback-browser.xul create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.colorhelpers.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.colorhelpers.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.crosshair.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.crosshair.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.image.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.image.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.navigate.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.navigate.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.selection.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.selection.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.stack.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.stack.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.threshold.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.threshold.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.min.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/raw-data-dialog.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/raw-data-dialog.xul create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/screen.css create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/status-quit.html create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/status.html create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/survey-generator.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/take-survey.html create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/tp-browser.xul create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/welcome-page.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/welcome.html create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/content/window-utils.js create mode 100755 browser/app/profile/extensions/testpilot@labs.mozilla.com/defaults/preferences/preferences.js create mode 100755 browser/app/profile/extensions/testpilot@labs.mozilla.com/install.rdf create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/chrome.manifest create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/install.rdf create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/instrument.jsm create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/instrument.xul create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/Observers.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/dbutils.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/experiment_data_store.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/extension-update.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/feedback.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/jar-code-store.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/cuddlefish.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/memory.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/observer-service.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/plain-text-console.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/preferences-service.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/securable-module.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/timer.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/traceback.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/unit-test.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/unload.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/url.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/log4moz.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/metadata.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/remote-experiment-loader.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/setup.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/string_sanitizer.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/tasks.js create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/badge-default.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/bg.jpg create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/css/screen-standalone.css create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/dino_32x32.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSans-Bold.ttf create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSans.ttf create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Bold.ttf create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-BoldItalic.ttf create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Italic.ttf create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Regular.ttf create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/bg-status.jpg create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout_continue.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/data1.jpg create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/data2.jpg create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_comments.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_computer.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_continue.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_quit.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_results.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_twitter.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_upcoming.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/logo.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/mozilla-logo.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-completed.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-ejected.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-missed.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testPilot_200x200.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_16x16.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_32x32.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-completedstudies-32x32.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-currentstudies-32x32.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-generic-32x32.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-learned-32x32.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-results-48x48.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-settings-32x32.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-study-48x48.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-submit-48x48.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/close_button.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback-frown-16x16.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback-smile-16x16.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback.css create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/close_button.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback-frown-16x16.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback-smile-16x16.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback.css create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/notification-tail-down.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/notification-tail-up.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/close_button.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback-frown-16x16.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback-smile-16x16.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback.css create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/notification-tail-down.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/notification-tail-up.png create mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/tests/test_data_store.js create mode 100644 browser/locales/en-US/feedback/main.dtd create mode 100644 browser/locales/en-US/feedback/main.properties diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/chrome.manifest b/browser/app/profile/extensions/testpilot@labs.mozilla.com/chrome.manifest new file mode 100644 index 000000000000..fc7ffbaeb9a5 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/chrome.manifest @@ -0,0 +1,10 @@ +resource testpilot ./ +content testpilot content/ +skin testpilot skin skin/all/ +skin testpilot-os skin skin/linux/ os=Linux +skin testpilot-os skin skin/mac/ os=Darwin +skin testpilot-os skin skin/win/ os=WINNT +overlay chrome://browser/content/browser.xul chrome://testpilot/content/tp-browser.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion<=3.6.* +overlay chrome://browser/content/browser.xul chrome://testpilot/content/feedback-browser.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion>3.7a1pre +# For the menubar on Mac +overlay chrome://testpilot/content/all-studies-window.xul chrome://browser/content/macBrowserOverlay.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} os=Darwin diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/components/TestPilot.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/components/TestPilot.js new file mode 100644 index 000000000000..b32ccc1688c7 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/components/TestPilot.js @@ -0,0 +1,84 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Weave. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dan Mills + * Jono X + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function TestPilotComponent() {} +TestPilotComponent.prototype = { + classDescription: "Test Pilot Component", + contractID: "@mozilla.org/testpilot/service;1", + classID: Components.ID("{e6e5e58f-7977-485a-b076-2f74bee2677b}"), + _xpcom_categories: [{ category: "app-startup", service: true }], + _startupTimer: null, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + observe: function TPC__observe(subject, topic, data) { + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + switch (topic) { + case "app-startup": + os.addObserver(this, "sessionstore-windows-restored", true); + break; + case "sessionstore-windows-restored": + /* Stop oberver, to ensure that globalStartup doesn't get + * called more than once. */ + os.removeObserver(this, "sessionstore-windows-restored", false); + /* Call global startup on a timer so that it's off of the main + * thread... delay a few seconds to give firefox time to finish + * starting up. + */ + this._startupTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._startupTimer.initWithCallback( + {notify: function(timer) { + Cu.import("resource://testpilot/modules/setup.js"); + TestPilotSetup.globalStartup(); + }}, 10000, Ci.nsITimer.TYPE_ONE_SHOT); + break; + } + } +}; + +function NSGetModule(compMgr, fileSpec) { + return XPCOMUtils.generateModule([TestPilotComponent]); +} + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.js new file mode 100644 index 000000000000..cc42cbe20e53 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.js @@ -0,0 +1,446 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * Raymond Lee + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// TODO Show individual status page in new chromeless window as html with +// background color set to "moz-dialog". + +const NO_STUDIES_IMG = "chrome://testpilot/skin/testPilot_200x200.png"; +const PROPOSE_STUDY_URL = + "https://wiki.mozilla.org/Labs/Test_Pilot#For_researchers"; + +var TestPilotXulWindow = { + _stringBundle : null, + + onSubmitButton: function(experimentId) { + Components.utils.import("resource://testpilot/modules/setup.js"); + let task = TestPilotSetup.getTaskById(experimentId); + let button = document.getElementById("submit-button-" + task.id); + + // Hide the upload button so it doesn't get clicked again... + let parent = button.parentNode; + while (parent.firstChild) { + parent.removeChild(parent.firstChild); + } + // Replace it with a message: + this.addLabel( + parent, + this._stringBundle.getString("testpilot.studiesWindow.uploading")); + let self = this; + + task.upload( function(success) { + while (parent.firstChild) { + parent.removeChild(parent.firstChild); + } + if (success) { + self.addThanksMessage(parent); + // TODO or should we move it to 'finished studies' immediately? + } else { + // TODO a better error message? + self.addLabel( + parent, + self._stringBundle.getString( + "testpilot.studiesWindow.unableToReachServer")); + } + }); + + }, + + addThanksMessage: function(container) { + // Fill in status box with icon and message to show success + let hbox = document.createElement("hbox"); + container.appendChild(this.makeSpacer()); + container.appendChild(hbox); + this.addLabel( + container, + this._stringBundle.getString( + "testpilot.studiesWindow.thanksForContributing")); + container.appendChild(this.makeSpacer()); + hbox.appendChild(this.makeSpacer()); + this.addImg(hbox, "study-submitted"); + hbox.appendChild(this.makeSpacer()); + }, + + addXulLink: function (container, text, url, openInTab) { + let linkContainer = document.createElement("hbox"); + let link = document.createElement("label"); + let spacer = document.createElement("spacer"); + link.setAttribute("value", text); + link.setAttribute("class", "text-link"); + if (openInTab) { + link.setAttribute( + "onclick", + "if (event.button==0) { " + + "TestPilotWindowUtils.openInTab('" + url + "'); }"); + } else { + link.setAttribute( + "onclick", + "if (event.button==0) { " + + "TestPilotWindowUtils.openChromeless('" + url + "'); }"); + } + linkContainer.appendChild(link); + spacer.setAttribute("flex", "1"); + linkContainer.appendChild(spacer); + container.appendChild(linkContainer); + }, + + addLabel: function(container, text, styleClass) { + let label = document.createElement("label"); + label.setAttribute("value", text); + if (styleClass) { + label.setAttribute("class", styleClass); + } + container.appendChild(label); + }, + + addImg: function(container, iconClass) { + let newImg = document.createElement("image"); + newImg.setAttribute("class", iconClass); + container.appendChild(newImg); + }, + + makeSpacer: function() { + let spacer = document.createElement("spacer"); + spacer.setAttribute("flex", "1"); + return spacer; + }, + + addThumbnail: function(container, imgUrl) { + let boundingBox = document.createElement("vbox"); + boundingBox.setAttribute("class", "results-thumbnail"); + let bBox2 = document.createElement("hbox"); + + boundingBox.appendChild(this.makeSpacer()); + boundingBox.appendChild(bBox2); + boundingBox.appendChild(this.makeSpacer()); + + bBox2.appendChild(this.makeSpacer()); + let newImg = document.createElement("image"); + newImg.setAttribute("src", imgUrl); + newImg.setAttribute("class", "results-thumbnail"); + bBox2.appendChild(newImg); + bBox2.appendChild(this.makeSpacer()); + + container.appendChild(boundingBox); + }, + + addProgressBar: function(container, percent) { + let progBar = document.createElement("progressmeter"); + progBar.setAttribute("mode", "determined"); + progBar.setAttribute("value", Math.ceil(percent).toString()); + container.appendChild(progBar); + }, + + addDescription: function(container, title, paragraph) { + let desc = document.createElement("description"); + desc.setAttribute("class", "study-title"); + let txtNode = document.createTextNode(title); + desc.appendChild(txtNode); + container.appendChild(desc); + + desc = document.createElement("description"); + desc.setAttribute("class", "study-description"); + desc.setAttribute("crop", "none"); + txtNode = document.createTextNode(paragraph); + desc.appendChild(txtNode); + container.appendChild(desc); + }, + + addButton: function(container, label, id, onClickHandler) { + let button = document.createElement("button"); + button.setAttribute("label", label); + button.setAttribute("id", id); + button.setAttribute("oncommand", onClickHandler); + container.appendChild(button); + }, + + _sortNewestFirst: function(experiments) { + experiments.sort( + function sortFunc(a, b) { + if (a.endDate && b.endDate) { + return b.endDate - a.endDate; + } + if (a.publishDate && b.publishDate) { + if (isNaN(a.publishDate) || isNaN(b.publishDate)) { + return 0; + } + return b.publishDate - a.publishDate; + } + return 0; + }); + return experiments; + }, + + onLoad: function () { + Components.utils.import("resource://testpilot/modules/Observers.js"); + Components.utils.import("resource://testpilot/modules/setup.js"); + Components.utils.import("resource://testpilot/modules/tasks.js"); + + this._stringBundle = document.getElementById("testpilot-stringbundle"); + this._init(false); + Observers.add("testpilot:task:changed", this._onTaskStatusChanged, this); + }, + + onUnload: function() { + Observers.remove("testpilot:task:changed", this._onTaskStatusChanged, this); + }, + + _onTaskStatusChanged : function() { + this._init(true); + }, + + onTakeSurveyButton: function(taskId) { + let task = TestPilotSetup.getTaskById(taskId); + TestPilotWindowUtils.openChromeless(task.defaultUrl); + task.onDetailPageOpened(); + }, + + _init: function(aReload) { + let experiments; + let ready = false; + + // Are we done loading tasks? + if (TestPilotSetup.startupComplete) { + experiments = TestPilotSetup.getAllTasks(); + if (experiments.length > 0 ) { + ready = true; + } + } + + if (!ready) { + // If you opened the window before tasks are done loading, exit now but try + // again in a few seconds. + window.setTimeout( + function() { TestPilotXulWindow._init(aReload); }, 2000); + return; + } + + let numFinishedStudies = 0; + let numCurrentStudies = 0; + + /* Remove 'loading' message */ + let msg = window.document.getElementById("still-loading-msg"); + msg.setAttribute("hidden", "true"); + + if (aReload) { + /* If we're reloading, start by clearing out any old stuff already + * present in the listboxes. */ + let listboxIds = + ["current-studies-listbox", "finished-studies-listbox", + "study-results-listbox"]; + for (let i = 0; i < listboxIds.length; i++) { + let listbox = document.getElementById(listboxIds[i]); + + while (listbox.lastChild) { + listbox.removeChild(listbox.lastChild); + } + } + } + + experiments = this._sortNewestFirst(experiments); + + for (let i = 0; i < experiments.length; i++) { + let task = experiments[i]; + let newRow = document.createElement("richlistitem"); + newRow.setAttribute("class", "tp-study-list"); + + this.addThumbnail(newRow, task.thumbnail); + + let textVbox = document.createElement("vbox"); + newRow.appendChild(textVbox); + + let openInTab = (task.taskType == TaskConstants.TYPE_LEGACY); + + this.addDescription(textVbox, task.title, task.summary); + if (task.showMoreInfoLink) { + this.addXulLink( + textVbox, this._stringBundle.getString("testpilot.moreInfo"), + task.defaultUrl, openInTab); + } + + // Create the rightmost status area, depending on status: + let statusVbox = document.createElement("vbox"); + if (task.status == TaskConstants.STATUS_FINISHED) { + this.addLabel( + statusVbox, + this._stringBundle.getFormattedString( + "testpilot.studiesWindow.finishedOn", + [(new Date(task.endDate)).toLocaleDateString()])); + this.addButton(statusVbox, + this._stringBundle.getString("testpilot.submit"), + "submit-button-" + task.id, + "TestPilotXulWindow.onSubmitButton(" + task.id + ");"); + } + if (task.status == TaskConstants.STATUS_CANCELLED) { + let hbox = document.createElement("hbox"); + newRow.setAttribute("class", "tp-opted-out"); + statusVbox.appendChild(this.makeSpacer()); + statusVbox.appendChild(hbox); + this.addLabel( + statusVbox, + this._stringBundle.getString("testpilot.studiesWindow.canceledStudy")); + statusVbox.appendChild(this.makeSpacer()); + hbox.appendChild(this.makeSpacer()); + this.addImg(hbox, "study-canceled"); + hbox.appendChild(this.makeSpacer()); + } + if (task.status == TaskConstants.STATUS_NEW || + task.status == TaskConstants.STATUS_PENDING ) { + newRow.setAttribute("class", "tp-new-results"); + + if (task.taskType == TaskConstants.TYPE_SURVEY) { + this.addButton( + statusVbox, + this._stringBundle.getString("testpilot.takeSurvey"), + "survey-button", + "TestPilotXulWindow.onTakeSurveyButton('" + task.id + "');"); + } else if (task.taskType == TaskConstants.TYPE_EXPERIMENT) { + if (task.startDate) { + this.addLabel( + statusVbox, + this._stringBundle.getFormattedString( + "testpilot.studiesWindow.willStart", + [(new Date(task.startDate)).toLocaleDateString()])); + } + } + } + if (task.status == TaskConstants.STATUS_IN_PROGRESS || + task.status == TaskConstants.STATUS_STARTING) { + + if (task.taskType == TaskConstants.TYPE_SURVEY) { + this.addButton( + statusVbox, + this._stringBundle.getString("testpilot.takeSurvey"), + "survey-button", + "TestPilotXulWindow.onTakeSurveyButton('" + task.id + "');"); + } else if (task.taskType == TaskConstants.TYPE_EXPERIMENT) { + this.addLabel( + statusVbox, + this._stringBundle.getString( + "testpilot.studiesWindow.gatheringData")); + let now = (new Date()).getTime(); + let progress = + 100 * (now - task.startDate) / (task.endDate - task.startDate); + this.addProgressBar(statusVbox, progress); + this.addLabel( + statusVbox, + this._stringBundle.getFormattedString( + "testpilot.studiesWindow.willFinish", + [(new Date(task.endDate)).toLocaleDateString()])); + } + } + if (task.status >= TaskConstants.STATUS_SUBMITTED) { + if (task.taskType == TaskConstants.TYPE_RESULTS) { + let maintask = TestPilotSetup.getTaskById(task.relatedStudyId); + if (maintask && maintask.status >= TaskConstants.STATUS_SUBMITTED) { + this.addThanksMessage(statusVbox); + } + } else { + if (task.status == TaskConstants.STATUS_MISSED) { + // TODO use Sean's icon for missed studies + } else { + this.addThanksMessage(statusVbox); + numFinishedStudies ++; + } + } + } + let spacer = document.createElement("spacer"); + spacer.setAttribute("flex", "1"); + newRow.appendChild(spacer); + newRow.appendChild(statusVbox); + + // Use status to decide which panel to add this to: + let rowset; + if (task.taskType == TaskConstants.TYPE_RESULTS) { + rowset = document.getElementById("study-results-listbox"); + } else if (task.status > TaskConstants.STATUS_FINISHED) { + rowset = document.getElementById("finished-studies-listbox"); + } else { + rowset = document.getElementById("current-studies-listbox"); + numCurrentStudies++; + } + + // TODO further distinguish by background colors. + rowset.appendChild(newRow); + } + + // If there are no current studies, show a message about upcoming + // studies: + if (numCurrentStudies == 0) { + let newRow = document.createElement("richlistitem"); + newRow.setAttribute("class", "tp-study-list"); + this.addThumbnail(newRow, NO_STUDIES_IMG); + let textVbox = document.createElement("vbox"); + textVbox.setAttribute("class", "pilot-largetext"); + newRow.appendChild(textVbox); + this.addDescription( + textVbox, "", + this._stringBundle.getString("testpilot.studiesWindow.noStudies")); + this.addXulLink( + textVbox, + this._stringBundle.getString("testpilot.studiesWindow.proposeStudy"), + PROPOSE_STUDY_URL, true); + document.getElementById("current-studies-listbox").appendChild(newRow); + } + + // Show number of studies the user finished on badge: + document.getElementById("num-finished-badge").setAttribute( + "value", numFinishedStudies); + + window.sizeToContent(); + }, + + focusPane: function(paneIndex) { + document.getElementById("tp-xulwindow-deck").selectedIndex = paneIndex; + + // When you focus the 'study findings' tab, any results there which + // are still marked "new" should have their status changed as the user + // is considered to have seen them. + if (paneIndex == 2) { + Components.utils.import("resource://testpilot/modules/setup.js"); + Components.utils.import("resource://testpilot/modules/tasks.js"); + + let experiments = TestPilotSetup.getAllTasks(); + for each (let experiment in experiments) { + if (experiment.taskType == TaskConstants.TYPE_RESULTS) { + if (experiment.status == TaskConstants.STATUS_NEW) { + experiment.changeStatus(TaskConstants.STATUS_ARCHIVED, true); + } + } + } + } + } +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.xul b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.xul new file mode 100644 index 000000000000..c7ab91068a18 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.xul @@ -0,0 +1,138 @@ + + + + + + + + + %testpilotDTD; +]> + + + + + + + + + + + + +
+

Current Status = . + +or set it to + + + + + + +

+
+

+ + + +

+
+

+ + + + + + + + + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/experiment-page.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/experiment-page.js new file mode 100644 index 000000000000..89a448aa7b5d --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/experiment-page.js @@ -0,0 +1,457 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * Raymond Lee + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const PAGE_TYPE_STATUS = 0; +const PAGE_TYPE_QUIT = 1; +var stringBundle; + + function showRawData(experimentId) { + window.openDialog( + "chrome://testpilot/content/raw-data-dialog.xul", + "TestPilotRawDataDialog", "chrome,centerscreen,resizable,scrollbars", + experimentId); + } + + function getUrlParam(name) { + // from http://www.netlobo.com/url_query_string_javascript.html + name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); + var regexS = "[\\?&]"+name+"=([^&#]*)"; + var regex = new RegExp(regexS); + var results = regex.exec(window.location.href); + if( results == null ) + return ""; + else + return results[1]; + } + + function uploadData() { + Components.utils.import("resource://testpilot/modules/setup.js"); + let eid = parseInt(getUrlParam("eid")); + let task = TestPilotSetup.getTaskById(eid); + + // If always-submit-checkbox is checked, set the pref + if (task._recursAutomatically) { + let checkBox = document.getElementById("always-submit-checkbox"); + if (checkBox && checkBox.checked) { + task.setRecurPref(TaskConstants.ALWAYS_SUBMIT); + } + } + + let uploadStatus = document.getElementById("upload-status"); + uploadStatus.innerHTML = + stringBundle.GetStringFromName("testpilot.statusPage.uploadingData"); + task.upload( function(success) { + if (success) { + window.location = + "chrome://testpilot/content/status.html?eid=" + eid; + } else { + uploadStatus.innerHTML = + "

" + + stringBundle.GetStringFromName("testpilot.statusPage.uploadError") + + "

"; + } + }); + } + + function deleteData() { + Components.utils.import("resource://testpilot/modules/setup.js"); + Components.utils.import("resource://testpilot/modules/tasks.js"); + let eid = parseInt(getUrlParam("eid")); + let task = TestPilotSetup.getTaskById(eid); + task.dataStore.wipeAllData(); + // reload the URL after wiping all data. + window.location = "chrome://testpilot/content/status.html?eid=" + eid; + } + + function saveCanvas(canvas) { + const nsIFilePicker = Components.interfaces.nsIFilePicker; + let filePicker = Components.classes["@mozilla.org/filepicker;1"]. + createInstance(nsIFilePicker); + filePicker.init(window, null, nsIFilePicker.modeSave); + filePicker.appendFilters( + nsIFilePicker.filterImages | nsIFilePicker.filterAll); + filePicker.defaultString = "canvas.png"; + + let response = filePicker.show(); + if (response == nsIFilePicker.returnOK || + response == nsIFilePicker.returnReplace) { + const nsIWebBrowserPersist = Components.interfaces.nsIWebBrowserPersist; + let file = filePicker.file; + + // create a data url from the canvas and then create URIs of the source + // and targets + let io = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + let source = io.newURI(canvas.toDataURL("image/png", ""), "UTF8", null); + let target = io.newFileURI(file); + + // prepare to save the canvas data + let persist = Components.classes[ + "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]. + createInstance(nsIWebBrowserPersist); + persist.persistFlags = nsIWebBrowserPersist. + PERSIST_FLAGS_REPLACE_EXISTING_FILES; + persist.persistFlags |= nsIWebBrowserPersist. + PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; + + // displays a download dialog (remove these 3 lines for silent download) + let xfer = Components.classes["@mozilla.org/transfer;1"]. + createInstance(Components.interfaces.nsITransfer); + xfer.init(source, target, "", null, null, null, persist); + persist.progressListener = xfer; + + // save the canvas data to the file + persist.saveURI(source, null, null, null, null, file); + } + } + + function exportData() { + const nsIFilePicker = Components.interfaces.nsIFilePicker; + let filePicker = Components.classes["@mozilla.org/filepicker;1"]. + createInstance(nsIFilePicker); + let eid = parseInt(getUrlParam("eid")); + let task = TestPilotSetup.getTaskById(eid); + + filePicker.init(window, null, nsIFilePicker.modeSave); + filePicker.appendFilters( + nsIFilePicker.filterImages | nsIFilePicker.filterAll); + filePicker.defaultString = task.title + ".csv"; + + let response = filePicker.show(); + if (response == nsIFilePicker.returnOK || + response == nsIFilePicker.returnReplace) { + const nsIWebBrowserPersist = Components.interfaces.nsIWebBrowserPersist; + let foStream = + Components.classes["@mozilla.org/network/file-output-stream;1"]. + createInstance(Components.interfaces.nsIFileOutputStream); + let converter = + Components.classes["@mozilla.org/intl/converter-output-stream;1"]. + createInstance(Components.interfaces.nsIConverterOutputStream); + let file = filePicker.file; + let dataStore = task.dataStore; + let columnNames = dataStore.getHumanReadableColumnNames(); + let propertyNames = dataStore.getPropertyNames(); + let csvString = ""; + + // titles + for (let i = 0; i < columnNames.length; i++) { + csvString += "\"" + columnNames[i] + "\","; + } + if (csvString.length > 0) { + csvString = csvString.substring(0, (csvString.length - 1)); + csvString += "\n"; + } + + dataStore.getAllDataAsJSON(true, function(rawData) { + // data + for (let i = 0; i < rawData.length; i++) { + for (let j = 0; j < columnNames.length; j++) { + csvString += "\"" + rawData[i][propertyNames[j]] + "\","; + } + csvString = csvString.substring(0, (csvString.length - 1)); + csvString += "\n"; + } + + // write, create, truncate + foStream.init(file, 0x02 | 0x08 | 0x20, 0664, 0); + converter.init(foStream, "UTF-8", 0, 0); + converter.writeString(csvString); + converter.close(); + }); + } + } + + function openLink(url) { + // open the link in the chromeless window + let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + let recentWindow = wm.getMostRecentWindow("navigator:browser"); + + if (recentWindow) { + recentWindow.TestPilotWindowUtils.openInTab(url); + } else { + window.open(url); + } + } + + function getTestEndingDate(experimentId) { + Components.utils.import("resource://testpilot/modules/setup.js"); + var task = TestPilotSetup.getTaskById(experimentId); + var endDate = new Date(task.endDate); + var diff = (endDate - Date.now()); + var span = document.getElementById("test-end-time"); + if (!span) { + return; + } + if (diff < 0) { + span.innerHTML = + stringBundle.GetStringFromName("testpilot.statusPage.endedAlready"); + return; + } + var hours = diff / (60 * 60 * 1000); + if (hours < 24) { + span.innerHTML = + stringBundle.formatStringFromName( + "testpilot.statusPage.todayAt", [endDate.toLocaleTimeString()], 1); + } else { + span.innerHTML = + stringBundle.formatStringFromName( + "testpilot.statusPage.endOn", [endDate.toLocaleString()], 1); + } + } + + function showMetaData() { + Components.utils.import("resource://testpilot/modules/metadata.js"); + MetadataCollector.getMetadata(function(md) { + var mdLocale = document.getElementById("md-locale"); + if (mdLocale) + mdLocale.innerHTML = md.location; + var mdVersion = document.getElementById("md-version"); + if (mdVersion) + mdVersion.innerHTML = md.version; + var mdOs = document.getElementById("md-os"); + if (mdOs) + mdOs.innerHTML = md.operatingSystem; + var mdNumExt = document.getElementById("md-num-ext"); + if (mdNumExt) { + var numExt = md.extensions.length; + if (numExt == 1) { + mdNumExt.innerHTML = + stringBundle.GetStringFromName("testpilot.statusPage.extension"); + } else { + mdNumExt.innerHTML = + stringBundle.formatStringFromName( + "testpilot.statusPage.extensions", [numExt], 1); + } + } + }); + } + + function onQuitPageLoad() { + Components.utils.import("resource://testpilot/modules/setup.js"); + setStrings(PAGE_TYPE_QUIT); + let eid = parseInt(getUrlParam("eid")); + let task = TestPilotSetup.getTaskById(eid); + let header = document.getElementById("about-quit-title"); + header.innerHTML = + stringBundle.formatStringFromName( + "testpilot.quitPage.aboutToQuit", [task.title], 1); + + if (task._recursAutomatically) { + document.getElementById("recur-options").setAttribute("style", ""); + document.getElementById("recur-checkbox-container"). + setAttribute("style", ""); + } + } + + function quitExperiment() { + Components.utils.import("resource://testpilot/modules/setup.js"); + Components.utils.import("resource://testpilot/modules/tasks.js"); + let eid = parseInt(getUrlParam("eid")); + let reason = document.getElementById("reason-for-quit").value; + let task = TestPilotSetup.getTaskById(eid); + task.optOut(reason, function(success) { + // load the you-are-canceleed page. + window.location = "chrome://testpilot/content/status.html?eid=" + eid; + }); + + // If opt-out-forever checkbox is checked, opt out forever! + if (task._recursAutomatically) { + let checkBox = document.getElementById("opt-out-forever"); + if (checkBox.checked) { + task.setRecurPref(TaskConstants.NEVER_SUBMIT); + } + // quit test so rescheduling + task._reschedule(); + } + } + + function updateRecurSettings() { + Components.utils.import("resource://testpilot/modules/setup.js"); + let eid = parseInt(getUrlParam("eid")); + let experiment = TestPilotSetup.getTaskById(eid); + let recurSelector = document.getElementById("recur-selector"); + let newValue = recurSelector.options[recurSelector.selectedIndex].value; + experiment.setRecurPref(parseInt(newValue)); + } + + function showRecurControls(experiment) { + Components.utils.import("resource://testpilot/modules/tasks.js"); + let recurPrefSpan = document.getElementById("recur-pref"); + if (!recurPrefSpan) { + return; + } + let days = experiment._recurrenceInterval; + recurPrefSpan.innerHTML = + stringBundle.formatStringFromName( + "testpilot.statusPage.recursEveryNumberOfDays", [days], 1); + + let controls = document.getElementById("recur-controls"); + let selector = document.createElement("select"); + controls.appendChild(selector); + selector.setAttribute("onchange", "updateRecurSettings();"); + selector.setAttribute("id", "recur-selector"); + + let option = document.createElement("option"); + option.setAttribute("value", TaskConstants.ASK_EACH_TIME); + if (experiment.recurPref == TaskConstants.ASK_EACH_TIME) { + option.setAttribute("selected", "true"); + } + option.innerHTML = + stringBundle.GetStringFromName( + "testpilot.statusPage.askMeBeforeSubmitData"); + selector.appendChild(option); + + option = document.createElement("option"); + option.setAttribute("value", TaskConstants.ALWAYS_SUBMIT); + if (experiment.recurPref == TaskConstants.ALWAYS_SUBMIT) { + option.setAttribute("selected", "true"); + } + option.innerHTML = + stringBundle.GetStringFromName( + "testpilot.statusPage.alwaysSubmitData"); + selector.appendChild(option); + + option = document.createElement("option"); + option.setAttribute("value", TaskConstants.NEVER_SUBMIT); + if (experiment.recurPref == TaskConstants.NEVER_SUBMIT) { + option.setAttribute("selected", "true"); + } + option.innerHTML = + stringBundle.GetStringFromName( + "testpilot.statusPage.neverSubmitData"); + selector.appendChild(option); + } + + function loadExperimentPage() { + Components.utils.import("resource://testpilot/modules/setup.js"); + Components.utils.import("resource://testpilot/modules/tasks.js"); + var contentDiv = document.getElementById("experiment-specific-text"); + var dataPrivacyDiv = document.getElementById("data-privacy-text"); + // Get experimentID from the GET args of page + var eid = parseInt(getUrlParam("eid")); + var experiment = TestPilotSetup.getTaskById(eid); + if (!experiment) { + // Possible that experiments aren't done loading yet. Try again in + // a few seconds. + contentDiv.innerHTML = + stringBundle.GetStringFromName("testpilot.statusPage.loading"); + window.setTimeout(function() { loadExperimentPage(); }, 2000); + return; + } + experiment.getWebContent(function(webContent) { + contentDiv.innerHTML = webContent; + }); + + experiment.getDataPrivacyContent(function(dataPrivacyContent) { + if (dataPrivacyContent && dataPrivacyContent.length > 0) { + dataPrivacyDiv.innerHTML = dataPrivacyContent; + dataPrivacyDiv.removeAttribute("hidden"); + } + }); + + // Metadata and start/end date should be filled in for every experiment: + showMetaData(); + getTestEndingDate(eid); + if (experiment._recursAutomatically && + experiment.status != TaskConstants.STATUS_FINISHED) { + showRecurControls(experiment); + } + + // Do whatever the experiment's web content wants done on load: + experiment.webContent.onPageLoad(experiment, document, jQuery); + } + + function onStatusPageLoad() { + setStrings(PAGE_TYPE_STATUS); + /* If an experiment ID (eid) is provided in the url params, show status + * for that experiment. If not, show the main menu with status for all + * installed experiments. */ + let eidString = getUrlParam("eid"); + if (eidString == "") { + showStatusMenuPage(); + } else { + loadExperimentPage(); + } + } + + function setStrings(pageType) { + stringBundle = + Components.classes["@mozilla.org/intl/stringbundle;1"]. + getService(Components.interfaces.nsIStringBundleService). + createBundle("chrome://testpilot/locale/main.properties"); + let map; + let mapLength; + + if (pageType == PAGE_TYPE_STATUS) { + map = [ + { id: "page-title", stringKey: "testpilot.fullBrandName" }, + { id: "comments-and-discussions-link", + stringKey: "testpilot.page.commentsAndDiscussions" }, + { id: "propose-test-link", + stringKey: "testpilot.page.proposeATest" }, + { id: "testpilot-twitter-link", + stringKey: "testpilot.page.testpilotOnTwitter" } + ]; + } else if (pageType == PAGE_TYPE_QUIT) { + map = [ + { id: "page-title", stringKey: "testpilot.fullBrandName" }, + { id: "comments-and-discussions-link", + stringKey: "testpilot.page.commentsAndDiscussions" }, + { id: "propose-test-link", + stringKey: "testpilot.page.proposeATest" }, + { id: "testpilot-twitter-link", + stringKey: "testpilot.page.testpilotOnTwitter" }, + { id: "optional-message", + stringKey: "testpilot.quitPage.optionalMessage" }, + { id: "reason-text", + stringKey: "testpilot.quitPage.reason" }, + { id: "recur-options", + stringKey: "testpilot.quitPage.recurringStudy" }, + { id: "quit-forever-text", + stringKey: "testpilot.quitPage.quitFoever" }, + { id: "quit-study-link", + stringKey: "testpilot.quitPage.quitStudy" } + ]; + } + mapLength = map.length; + for (let i = 0; i < mapLength; i++) { + let entry = map[i]; + document.getElementById(entry.id).innerHTML = + stringBundle.GetStringFromName(entry.stringKey); + } + } diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/feedback-browser.xul b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/feedback-browser.xul new file mode 100644 index 000000000000..7e4d98b7e4d1 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/feedback-browser.xul @@ -0,0 +1,81 @@ + + + + + + %testpilotDTD; +]> + + + + + +
+ + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/status.html b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/status.html new file mode 100644 index 000000000000..67a30ead1b7e --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/status.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + +
+
+
+ + +
+
+ +
+
+ + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/survey-generator.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/survey-generator.js new file mode 100644 index 000000000000..3d42e8d4a0ce --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/survey-generator.js @@ -0,0 +1,326 @@ +const MULTIPLE_CHOICE = 0; +const CHECK_BOXES_WITH_FREE_ENTRY = 1; +const SCALE = 2; +const FREE_ENTRY = 3; +const CHECK_BOXES = 4; +const MULTIPLE_CHOICE_WITH_FREE_ENTRY = 5; + +var stringBundle; + +function onBuiltinSurveyLoad() { + Components.utils.import("resource://testpilot/modules/setup.js"); + Components.utils.import("resource://testpilot/modules/tasks.js"); + setStrings(); + let eid = getUrlParam("eid"); + let task = TestPilotSetup.getTaskById(eid); + let contentDiv = document.getElementById("survey-contents"); + let explanation = document.getElementById("survey-explanation"); + if (!task) { + // Tasks haven't all loaded yet. Try again in a few seconds. + contentDiv.innerHTML = + stringBundle.GetStringFromName("testpilot.surveyPage.loading"); + window.setTimeout(function() { onBuiltinSurveyLoad(); }, 2000); + return; + } + + let title = document.getElementById("survey-title"); + title.innerHTML = task.title; + + let submitButton = document.getElementById("survey-submit"); + if (task.relatedStudyId) { + submitButton.innerHTML = + stringBundle.GetStringFromName("testpilot.surveyPage.submitAnswers"); + } else { + submitButton.innerHTML = + stringBundle.GetStringFromName("testpilot.surveyPage.saveAnswers"); + } + + if (task.status == TaskConstants.STATUS_SUBMITTED) { + contentDiv.innerHTML = + "

" + + stringBundle.GetStringFromName( + "testpilot.surveyPage.thankYouForFinishingSurvey") + "

" + + stringBundle.GetStringFromName( + "testpilot.surveyPage.reviewOrChangeYourAnswers") + "

"; + explanation.innerHTML = ""; + submitButton.setAttribute("style", "display:none"); + let changeButton = document.getElementById("change-answers"); + changeButton.setAttribute("style", ""); + } else { + contentDiv.innerHTML = ""; + if (task.surveyExplanation) { + explanation.innerHTML = task.surveyExplanation; + } else { + explanation.innerHTML = ""; + } + drawSurveyForm(task, contentDiv); + } +} + +function drawSurveyForm(task, contentDiv) { + let oldAnswers = task.oldAnswers; + let surveyQuestions = task.surveyQuestions; + + let submitButton = document.getElementById("survey-submit"); + submitButton.setAttribute("style", ""); + let changeButton = document.getElementById("change-answers"); + changeButton.setAttribute("style", "display:none"); + // Loop through questions and render html form input elements for each + // one. + for (let i = 0; i < surveyQuestions.length; i++) { + let question = surveyQuestions[i].question; + let explanation = surveyQuestions[i].explanation; + let elem; + + elem = document.createElement("h3"); + elem.innerHTML = (i+1) + ". " + question; + contentDiv.appendChild(elem); + if (explanation) { + elem = document.createElement("p"); + elem.setAttribute("class", "survey-question-explanation"); + elem.innerHTML = explanation; + contentDiv.appendChild(elem); + } + // If you've done this survey before, preset all inputs using old answers + let choices = surveyQuestions[i].choices; + switch (surveyQuestions[i].type) { + case MULTIPLE_CHOICE: + for (let j = 0; j < choices.length; j++) { + let newRadio = document.createElement("input"); + newRadio.setAttribute("type", "radio"); + newRadio.setAttribute("name", "answer_to_" + i); + newRadio.setAttribute("value", j); + if (oldAnswers && oldAnswers[i] == String(j)) { + newRadio.setAttribute("checked", "true"); + } + let label = document.createElement("span"); + label.innerHTML = choices[j]; + contentDiv.appendChild(newRadio); + contentDiv.appendChild(label); + contentDiv.appendChild(document.createElement("br")); + } + break; + case CHECK_BOXES: + case CHECK_BOXES_WITH_FREE_ENTRY: + let checkboxName = "answer_to_" + i; + // Check boxes: + for (let j = 0; j < choices.length; j++) { + let newCheck = document.createElement("input"); + newCheck.setAttribute("type", "checkbox"); + newCheck.setAttribute("name", checkboxName); + newCheck.setAttribute("value", j); + if (oldAnswers && oldAnswers[i]) { + for each (let an in oldAnswers[i]) { + if (an == String(j)) { + newCheck.setAttribute("checked", "true"); + break; + } + } + } + let label = document.createElement("span"); + label.innerHTML = choices[j]; + contentDiv.appendChild(newCheck); + contentDiv.appendChild(label); + contentDiv.appendChild(document.createElement("br")); + } + // Text area: + if (surveyQuestions[i].type == CHECK_BOXES_WITH_FREE_ENTRY && + surveyQuestions[i].free_entry) { + let freeformId = "freeform_" + i; + let newCheck = document.createElement("input"); + newCheck.setAttribute("type", "checkbox"); + newCheck.setAttribute("name", checkboxName); + newCheck.setAttribute("value", freeformId); + newCheck.addEventListener( + "click", function(event) { + if (!event.target.checked) { + document.getElementById(freeformId).value = ""; + } + }, false); + let label = document.createElement("span"); + label.innerHTML = surveyQuestions[i].free_entry + " : "; + let inputBox = document.createElement("textarea"); + inputBox.setAttribute("id", freeformId); + inputBox.addEventListener( + "keypress", function() { + let elements = document.getElementsByName(checkboxName); + for (let j = (elements.length - 1); j >= 0; j--) { + if (elements[j].value == freeformId) { + elements[j].checked = true; + break; + } + } + }, false); + if (oldAnswers && oldAnswers[i]) { + for each (let an in oldAnswers[i]) { + if (isNaN(parseInt(an))) { + newCheck.setAttribute("checked", "true"); + inputBox.value = an; + break; + } + } + } + contentDiv.appendChild(newCheck); + contentDiv.appendChild(label); + contentDiv.appendChild(inputBox); + } + break; + case SCALE: + let label = document.createElement("span"); + label.innerHTML = surveyQuestions[i].min_label; + contentDiv.appendChild(label); + for (let j = surveyQuestions[i].scale_minimum; + j <= surveyQuestions[i].scale_maximum; + j++) { + let newRadio = document.createElement("input"); + newRadio.setAttribute("type", "radio"); + newRadio.setAttribute("name", "answer_to_" + i); + newRadio.setAttribute("value", j); + if (oldAnswers && oldAnswers[i] == String(j)) { + newRadio.setAttribute("checked", "true"); + } + contentDiv.appendChild(newRadio); + } + label = document.createElement("span"); + label.innerHTML = surveyQuestions[i].max_label; + contentDiv.appendChild(label); + break; + case FREE_ENTRY: + let inputBox = document.createElement("textarea"); + inputBox.setAttribute("id", "freeform_" + i); + + if (oldAnswers && oldAnswers[i] && (oldAnswers[i].length > 0)) { + inputBox.value = oldAnswers[i]; + } + contentDiv.appendChild(inputBox); + break; + case MULTIPLE_CHOICE_WITH_FREE_ENTRY: + let checked = false; + let freeformId = "freeform_" + i; + let radioName = "answer_to_" + i; + + for (let j = 0; j < choices.length; j++) { + let newRadio = document.createElement("input"); + newRadio.setAttribute("type", "radio"); + newRadio.setAttribute("name", radioName); + newRadio.setAttribute("value", j); + newRadio.addEventListener( + "click", function() { + let inputBox = document.getElementById(freeformId); + if (inputBox) { + inputBox.value = ""; + } + }, false); + let label = document.createElement("span"); + label.innerHTML = choices[j]; + if (oldAnswers && oldAnswers[i] == String(j)) { + newRadio.setAttribute("checked", "true"); + checked = true; + } + contentDiv.appendChild(newRadio); + contentDiv.appendChild(label); + contentDiv.appendChild(document.createElement("br")); + } + + // Text area: + if (surveyQuestions[i].free_entry) { + let newRadio = document.createElement("input"); + newRadio.setAttribute("type", "radio"); + newRadio.setAttribute("name", radioName); + newRadio.setAttribute("value", freeformId); + let label = document.createElement("span"); + label.innerHTML = surveyQuestions[i].free_entry + " : "; + let inputBox = document.createElement("textarea"); + inputBox.setAttribute("id", freeformId); + inputBox.addEventListener( + "keypress", function() { + let elements = document.getElementsByName(radioName); + for (let j = 0; j < elements.length; j++) { + if (elements[j].value == freeformId) { + elements[j].checked = true; + } else { + elements[j].checked = false; + } + } + }, false); + if (oldAnswers && oldAnswers[i] && (oldAnswers[i].length > 0) && + !checked) { + newRadio.setAttribute("checked", "true"); + inputBox.value = oldAnswers[i]; + } + contentDiv.appendChild(newRadio); + contentDiv.appendChild(label); + contentDiv.appendChild(inputBox); + } + break; + } + } +} + +function onBuiltinSurveySubmit() { + Components.utils.import("resource://testpilot/modules/setup.js"); + Components.utils.import("resource://testpilot/modules/log4moz.js"); + let logger = Log4Moz.repository.getLogger("TestPilot.Survey"); + let eid = getUrlParam("eid"); + let task = TestPilotSetup.getTaskById(eid); + logger.info("Storing survey answers for survey id " + eid); + + // Read all values from form... + let answers = []; + let surveyQuestions = task.surveyQuestions; + let i; + for (i = 0; i < surveyQuestions.length; i++) { + let elems = document.getElementsByName("answer_to_" + i); + let anAnswer = []; + for each (let elem in elems) { + if (elem.checked && elem.value != ("freeform_" + i)) { + anAnswer.push(elem.value); + } + } + let freeEntry = document.getElementById("freeform_" + i); + if (freeEntry && freeEntry.value) { + anAnswer.push(freeEntry.value); + } + answers.push(anAnswer); + } + let surveyResults = { answers: answers }; + logger.info("Storing survey answers " + JSON.stringify(surveyResults)); + task.store(surveyResults, function(submitted) { + // Reload page to show submitted status: + if (submitted) { + onBuiltinSurveyLoad(); + } + }); +} + +function onBuiltinSurveyChangeAnswers() { + let eid = getUrlParam("eid"); + let task = TestPilotSetup.getTaskById(eid); + let contentDiv = document.getElementById("survey-contents"); + + drawSurveyForm(task, contentDiv); +} + +function setStrings() { + stringBundle = + Components.classes["@mozilla.org/intl/stringbundle;1"]. + getService(Components.interfaces.nsIStringBundleService). + createBundle("chrome://testpilot/locale/main.properties"); + let map = [ + { id: "page-title", stringKey: "testpilot.fullBrandName" }, + { id: "comments-and-discussions-link", + stringKey: "testpilot.page.commentsAndDiscussions" }, + { id: "propose-test-link", + stringKey: "testpilot.page.proposeATest" }, + { id: "testpilot-twitter-link", + stringKey: "testpilot.page.testpilotOnTwitter" }, + { id: "change-answers", + stringKey: "testpilot.surveyPage.changeAnswers" } + ]; + let mapLength = map.length; + for (let i = 0; i < mapLength; i++) { + let entry = map[i]; + document.getElementById(entry.id).innerHTML = + stringBundle.GetStringFromName(entry.stringKey); + } +} diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/take-survey.html b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/take-survey.html new file mode 100644 index 000000000000..be9a169c301b --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/take-survey.html @@ -0,0 +1,53 @@ + + + + + + + + + + + +
+
+
+

+

+
+
+ + +
+ +
+
+ + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/tp-browser.xul b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/tp-browser.xul new file mode 100644 index 000000000000..0c5878aafe5d --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/tp-browser.xul @@ -0,0 +1,95 @@ + + + + + %testpilotDTD; +]> + + + + + + +
+ +
+
+

+
+
+
+

+ +

+ +

+

+
+
+
+
+

+

+ + + +

+

+
+
+
+ +
+ \ No newline at end of file diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/window-utils.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/window-utils.js new file mode 100644 index 000000000000..79bfd2c26ea3 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/window-utils.js @@ -0,0 +1,139 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * Jorge jorge@mozilla.com + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var TestPilotWindowUtils; + +(function() { + const ALL_STUDIES_WINDOW_NAME = "TestPilotAllStudiesWindow"; + const ALL_STUDIES_WINDOW_TYPE = "extensions:testpilot:all_studies_window"; + + TestPilotWindowUtils = { + openAllStudiesWindow: function() { + // If the window is not already open, open it; but if it is open, + // focus it instead. + let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]. + getService(Components.interfaces.nsIWindowMediator); + let allStudiesWindow = wm.getMostRecentWindow(ALL_STUDIES_WINDOW_TYPE); + + if (allStudiesWindow) { + allStudiesWindow.focus(); + } else { + allStudiesWindow = window.openDialog( + "chrome://testpilot/content/all-studies-window.xul", + ALL_STUDIES_WINDOW_NAME, "chrome,titlebar,centerscreen,dialog=no"); + } + }, + + openInTab: function(url) { + let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + let enumerator = wm.getEnumerator("navigator:browser"); + let found = false; + + while(enumerator.hasMoreElements()) { + let win = enumerator.getNext(); + let tabbrowser = win.getBrowser(); + + // Check each tab of this browser instance + let numTabs = tabbrowser.browsers.length; + for (let i = 0; i < numTabs; i++) { + let currentBrowser = tabbrowser.getBrowserAtIndex(i); + if (url == currentBrowser.currentURI.spec) { + tabbrowser.selectedTab = tabbrowser.tabContainer.childNodes[i]; + found = true; + win.focus(); + break; + } + } + } + + if (!found) { + let win = wm.getMostRecentWindow("navigator:browser"); + if (win) { + let browser = win.getBrowser(); + let tab = browser.addTab(url); + browser.selectedTab = tab; + win.focus(); + } else { + window.open(url); + } + } + }, + + getCurrentTabUrl: function() { + let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + let win = wm.getMostRecentWindow("navigator:browser"); + let tabbrowser = win.getBrowser(); + let currentBrowser = tabbrowser.selectedBrowser; + return currentBrowser.currentURI.spec; + }, + + openHomepage: function() { + let Application = Cc["@mozilla.org/fuel/application;1"] + .getService(Ci.fuelIApplication); + let url = Application.prefs.getValue("extensions.testpilot.homepageURL", + ""); + this.openInTab(url); + }, + + openFeedbackPage: function(aIsHappy) { + Components.utils.import("resource://testpilot/modules/feedback.js"); + FeedbackManager.setCurrUrl(this.getCurrentTabUrl()); + if (aIsHappy) { + this.openInTab(FeedbackManager.happyUrl); + } else { + this.openInTab(FeedbackManager.sadUrl); + } + }, + + openChromeless: function(url) { + /* Make the window smaller and dialog-boxier + * Links to discussion group, twitter, etc should open in new + * tab in main browser window, if we have these links here at all!! + * Maybe just one link to the main Test Pilot website. */ + + // TODO this window opening triggers studies' window-open code. + // Is that what we want or not? + + let win = window.open(url, "TestPilotStudyDetailWindow", + "chrome,centerscreen,resizable=yes,scrollbars=yes," + + "status=no,width=1000,height=800"); + win.focus(); + } + }; +}()); \ No newline at end of file diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/defaults/preferences/preferences.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/defaults/preferences/preferences.js new file mode 100755 index 000000000000..241d0d81723d --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/defaults/preferences/preferences.js @@ -0,0 +1,20 @@ +pref("extensions.testpilot.indexFileName", "index.json"); + +pref("extensions.testpilot.popup.delayAfterStartup", 180000); // 3 minutes +pref("extensions.testpilot.popup.timeBetweenChecks", 86400000); // 24 hours +pref("extensions.testpilot.uploadRetryInterval", 3600000); // 1 hour + +pref("extensions.testpilot.popup.showOnNewStudy", false); +pref("extensions.testpilot.popup.showOnStudyFinished", true); +pref("extensions.testpilot.popup.showOnNewResults", false); +pref("extensions.testpilot.alwaysSubmitData", false); +pref("extensions.testpilot.runStudies", true); + +pref("extensions.testpilot.indexBaseURL", "https://testpilot.mozillalabs.com/testcases/"); +pref("extensions.testpilot.firstRunUrl", "chrome://testpilot/content/welcome.html"); +pref("extensions.testpilot.dataUploadURL", "https://testpilot.mozillalabs.com/submit/"); +pref("extensions.testpilot.homepageURL", "https://testpilot.mozillalabs.com/"); + + +pref("extensions.input.happyURL", "http://input.mozilla.com/happy"); +pref("extensions.input.sadURL", "http://input.mozilla.com/sad"); diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/install.rdf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/install.rdf new file mode 100755 index 000000000000..98a182ea3788 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/install.rdf @@ -0,0 +1,27 @@ + + + + + testpilot@labs.mozilla.com + 1.0rc1 + 2 + + + + + {ec8030f7-c20a-464f-9b0e-13a3a9e97384} + 3.5 + 4.0b1 + + + + + Feedback + Help make Firefox better by giving feedback. + Mozilla Corporation + http://testpilot.mozillalabs.com/ + chrome://testpilot/skin/dino_32x32.png + + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/chrome.manifest b/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/chrome.manifest new file mode 100644 index 000000000000..32221f27ed94 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/chrome.manifest @@ -0,0 +1,3 @@ +resource instrument . +content instrument . +overlay chrome://browser/content chrome://instrument/content diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/install.rdf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/install.rdf new file mode 100644 index 000000000000..4dacbb192896 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/install.rdf @@ -0,0 +1,19 @@ + + + + Instrument Test + instrument.test@agadak.net + 1 + Edward Lee (Mardak) + Instrument various browser stuff + http://ed.agadak.net/ + + + {ec8030f7-c20a-464f-9b0e-13a3a9e97384} + 3.0 + 3.6a1pre + + + + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/instrument.jsm b/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/instrument.jsm new file mode 100644 index 000000000000..69dbf0601861 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/instrument.jsm @@ -0,0 +1,44 @@ +let EXPORTED_SYMBOLS = ["Instrument"]; + +let data = {}; +/** + * Track how many times an object's member function is called + * + * @param obj + * Object containing the method to track + * @param func + * Property name of the object that is the function to track + * @param name + * "Pretty" name to log the usage counts + */ +let track = function(obj, func, name) { + // Initialize count data + data[name] = 0; + + // Save the original function to call + let orig = obj[func]; + obj[func] = function() { + // Increment our counter right away (in-case there's an exception) + data[name]++; + + // Make the call just like it was originally was called + return orig.apply(this, arguments); + }; +} + +/** + * Instrument a browser window for various behavior + * + * @param window + * Browser window to instrument + */ +function Instrument(window) { + let $ = function(id) window.document.getElementById(id); + + track(window.gURLBar, "showHistoryPopup", "dropdown"); + track($("back-button"), "_handleClick", "back"); + track($("forward-button"), "_handleClick", "forward"); +} + +// Provide a way to get at the collected data (e.g., from Error Console) +Instrument.report = function() JSON.stringify(data); diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/instrument.xul b/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/instrument.xul new file mode 100644 index 000000000000..8f5230655dce --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/instrument/instrument.xul @@ -0,0 +1,12 @@ + + + + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/Observers.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/Observers.js new file mode 100644 index 000000000000..0700115e5728 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/Observers.js @@ -0,0 +1,183 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Observers. + * + * The Initial Developer of the Original Code is Daniel Aquino. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Daniel Aquino + * Myk Melez + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +let EXPORTED_SYMBOLS = ["Observers"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +/** + * A service for adding, removing and notifying observers of notifications. + * Wraps the nsIObserverService interface. + * + * @version 0.2 + */ +let Observers = { + /** + * Register the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic to observe + * + * @param callback {Object} + * the callback; an Object that implements nsIObserver or a Function + * that gets called when the notification occurs + * + * @param thisObject {Object} [optional] + * the object to use as |this| when calling a Function callback + * + * @returns the observer + */ + add: function(topic, callback, thisObject) { + let observer = new Observer(topic, callback, thisObject); + this._cache.push(observer); + this._service.addObserver(observer, topic, true); + + return observer; + }, + + /** + * Unregister the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic being observed + * + * @param callback {Object} + * the callback doing the observing + * + * @param thisObject {Object} [optional] + * the object being used as |this| when calling a Function callback + */ + remove: function(topic, callback, thisObject) { + // This seems fairly inefficient, but I'm not sure how much better + // we can make it. We could index by topic, but we can't index by callback + // or thisObject, as far as I know, since the keys to JavaScript hashes + // (a.k.a. objects) can apparently only be primitive values. + let [observer] = this._cache.filter(function(v) v.topic == topic && + v.callback == callback && + v.thisObject == thisObject); + if (observer) { + this._service.removeObserver(observer, topic); + this._cache.splice(this._cache.indexOf(observer), 1); + } + }, + + /** + * Notify observers about something. + * + * @param topic {String} + * the topic to notify observers about + * + * @param subject {Object} [optional] + * some information about the topic; can be any JS object or primitive + * + * @param data {String} [optional] [deprecated] + * some more information about the topic; deprecated as the subject + * is sufficient to pass all needed information to the JS observers + * that this module targets; if you have multiple values to pass to + * the observer, wrap them in an object and pass them via the subject + * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) + */ + notify: function(topic, subject, data) { + subject = (typeof subject == "undefined") ? null : new Subject(subject); + data = (typeof data == "undefined") ? null : data; + this._service.notifyObservers(subject, topic, data); + }, + + _service: Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService), + + /** + * A cache of observers that have been added. + * + * We use this to remove observers when a caller calls |remove|. + * + * XXX This might result in reference cycles, causing memory leaks, + * if we hold a reference to an observer that holds a reference to us. + * Could we fix that by making this an independent top-level object + * rather than a property of this object? + */ + _cache: [] +}; + + +function Observer(topic, callback, thisObject) { + this.topic = topic; + this.callback = callback; + this.thisObject = thisObject; +} + +Observer.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + observe: function(subject, topic, data) { + // Extract the wrapped object for subjects that are one of our wrappers + // around a JS object. This way we support both wrapped subjects created + // using this module and those that are real XPCOM components. + if (subject && typeof subject == "object" && + ("wrappedJSObject" in subject) && + ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) + subject = subject.wrappedJSObject.object; + + if (typeof this.callback == "function") { + if (this.thisObject) + this.callback.call(this.thisObject, subject, data); + else + this.callback(subject, data); + } + else // typeof this.callback == "object" (nsIObserver) + this.callback.observe(subject, topic, data); + } +} + + +function Subject(object) { + // Double-wrap the object and set a property identifying the wrappedJSObject + // as one of our wrappers to distinguish between subjects that are one of our + // wrappers (which we should unwrap when notifying our observers) and those + // that are real JS XPCOM components (which we should pass through unaltered). + this.wrappedJSObject = { observersModuleSubjectWrapper: true, object: object }; +} + +Subject.prototype = { + QueryInterface: XPCOMUtils.generateQI([]), + getHelperForLanguage: function() {}, + getInterfaces: function() {} +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/dbutils.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/dbutils.js new file mode 100644 index 000000000000..8371a2d2dbc7 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/dbutils.js @@ -0,0 +1,96 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ubiquity. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Brandon Pung + * Jono X + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; +var EXPORTED_SYMBOLS = ["DbUtils"]; + +Cu.import("resource://testpilot/modules/log4moz.js"); + +/* Make a namespace object called DbUtils, to export, + * which contains each function in this file.*/ +var DbUtils = ([f for each (f in this) if (typeof f === "function")] + .reduce(function(o, f)(o[f.name] = f, o), {})); + +var _dirSvc = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); +var _storSvc = Cc["@mozilla.org/storage/service;1"] + .getService(Ci.mozIStorageService); + +DbUtils.openDatabase = function openDatabase(file) { + /* If the pointed-at file doesn't already exist, it means the database + * has never been initialized */ + let logger = Log4Moz.repository.getLogger("TestPilot.Database"); + let connection = null; + try { + logger.debug("Trying to open file...\n"); + connection = _storSvc.openDatabase(file); + logger.debug("Opening file done...\n"); + } catch(e) { + logger.debug("Opening file failed...\n"); + Components.utils.reportError( + "Opening database failed, database may not have been initialized"); + } + return connection; +}; + +DbUtils.createTable = function createTable(connection, tableName, schema){ + let logger = Log4Moz.repository.getLogger("TestPilot.Database"); + let file = connection.databaseFile; + logger.debug("File is " + file + "\n"); + try{ + if(!connection.tableExists(tableName)){ + connection.executeSimpleSQL(schema); + } + else{ + logger.debug("database table: " + tableName + " already exists\n"); + } + } + catch(e) { + logger.warn("Error creating database: " + e + "\n"); + Cu.reportError("Test Pilot's " + tableName + + " database table appears to be corrupt, resetting it."); + if(file.exists()){ + //remove corrupt database table + file.remove(false); + } + connection = _storSvc.openDatabase(file); + connection.executeSimpleSQL(schema); + } + return connection; +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/experiment_data_store.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/experiment_data_store.js new file mode 100644 index 000000000000..1af8683e3bd5 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/experiment_data_store.js @@ -0,0 +1,334 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * Raymond Lee + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = ["ExperimentDataStore", "TYPE_INT_32", "TYPE_DOUBLE", + "TYPE_STRING"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://testpilot/modules/dbutils.js"); +Cu.import("resource://testpilot/modules/log4moz.js"); +Cu.import("resource://testpilot/modules/string_sanitizer.js"); +var _dirSvc = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); +var _storSvc = Cc["@mozilla.org/storage/service;1"] + .getService(Ci.mozIStorageService); + +const TYPE_INT_32 = 0; +const TYPE_DOUBLE = 1; +const TYPE_STRING = 2; + +function ExperimentDataStore(fileName, tableName, columns) { + this._init(fileName, tableName, columns); +} +ExperimentDataStore.prototype = { + _init: function EDS__init(fileName, tableName, columns) { + this._fileName = fileName; + this._tableName = tableName; + this._columns = columns; + let logger = Log4Moz.repository.getLogger("TestPilot.Database"); + let file = _dirSvc.get("ProfD", Ci.nsIFile); + file.append(this._fileName); + // openDatabase creates the file if it's not there yet: + this._connection = DbUtils.openDatabase(file); + // Create schema based on columns: + let schemaClauses = []; + for (let i = 0; i < this._columns.length; i++) { + let colName = this._columns[i].property; + let colType; + switch( this._columns[i].type) { + case TYPE_INT_32: case TYPE_DOUBLE: + colType = "INTEGER"; + break; + case TYPE_STRING: + colType = "TEXT"; + break; + + } + schemaClauses.push( colName + " " + colType ); + } + let schema = "CREATE TABLE " + this._tableName + "(" + + schemaClauses.join(", ") + ");"; + // CreateTable creates the table only if it does not already exist: + try { + this._connection = DbUtils.createTable(this._connection, + this._tableName, + schema); + } catch(e) { + logger.warn("Error in createTable: " + e + "\n"); + } + }, + + _createStatement: function _createStatement(selectSql) { + try { + var selStmt = this._connection.createStatement(selectSql); + return selStmt; + } catch (e) { + throw new Error(this._connection.lastErrorString); + } + }, + + storeEvent: function EDS_storeEvent(uiEvent, callback) { + // Stores event asynchronously; callback will be called back with + // true or false on success or failure. + let columnNumbers = []; + for (let i = 1; i <= this._columns.length; i++) { + // the i = 1 is so that we'll start with 1 instead of 0... we want + // a string like "...VALUES (?1, ?2, ?3)" + columnNumbers.push( "?" + i); + } + let insertSql = "INSERT INTO " + this._tableName + " VALUES ("; + insertSql += columnNumbers.join(", ") + ")"; + let insStmt = this._createStatement(insertSql); + for (i = 0; i < this._columns.length; i++) { + let datum = uiEvent[this._columns[i].property]; + switch (this._columns[i].type) { + case TYPE_INT_32: + insStmt.bindInt32Parameter(i, datum); + break; + case TYPE_DOUBLE: + insStmt.bindDoubleParameter(i, datum); + break; + case TYPE_STRING: + insStmt.bindUTF8StringParameter(i, sanitizeString(datum)); + break; + } + } + insStmt.executeAsync({ + handleResult: function(aResultSet) { + }, + handleError: function(aError) { + if (callback) { + callback(false); + } + }, + handleCompletion: function(aReason) { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + if (callback) { + callback(true); + } + } else { + if (callback) { + callback(false); + } + } + } + }); + insStmt.finalize(); + }, + + getJSONRows: function EDS_getJSONRows(callback) { + let selectSql = "SELECT * FROM " + this._tableName; + let selStmt = this._createStatement(selectSql); + let records = []; + let self = this; + let numCols = selStmt.columnCount; + + selStmt.executeAsync({ + handleResult: function(aResultSet) { + for (let row = aResultSet.getNextRow(); row; + row = aResultSet.getNextRow()) { + let newRecord = []; + for (let i = 0; i < numCols; i++) { + let column = self._columns[i]; + //let value = row.getResultByIndex(i); + let value = 0; + switch (column.type) { + case TYPE_INT_32: + value = row.getInt32(i); + break; + case TYPE_DOUBLE: + value = row.getDouble(i); + break; + case TYPE_STRING: + value = sanitizeString(row.getUTF8String(i)); + break; + } + newRecord.push(value); + } + records.push(newRecord); + } + }, + handleError: function(aError) { + callback(records); + }, + + handleCompletion: function(aReason) { + callback(records); + } + }); + selStmt.finalize(); + }, + + getAllDataAsJSON: function EDS_getAllDataAsJSON( useDisplayValues, callback ) { + /* if useDisplayValues is true, the values in the returned JSON are translated to + * their human-readable equivalents, using the mechanism provided in the columns + * set. If it's false, the values in the returned JSON are straight numerical + * values. */ + + // Note this works without knowing what the schema is + let selectSql = "SELECT * FROM " + this._tableName; + let selStmt = this._createStatement(selectSql); + let records = []; + let self = this; + let numCols = selStmt.columnCount; + + selStmt.executeAsync({ + handleResult: function(aResultSet) { + for (let row = aResultSet.getNextRow(); row; + row = aResultSet.getNextRow()) { + let newRecord = {}; + for (let i = 0; i < numCols; i++) { + let column = self._columns[i]; + //let value = row.getResultByIndex(i); + let value = 0; + switch (column.type) { + case TYPE_INT_32: + value = row.getInt32(i); + break; + case TYPE_DOUBLE: + value = row.getDouble(i); + break; + case TYPE_STRING: + value = sanitizeString(row.getUTF8String(i)); + break; + } + /* The column may have a property called displayValue, which can be either + * a function returning a string or an array of strings. If we're called + * with useDisplayValues, then take the raw numeric value and either use it as + * an index to the array of strings or use it as input to the function in order + * to get the human-readable display name of the value. */ + if (useDisplayValues && column.displayValue != undefined) { + if (typeof( column.displayValue) == "function") { + newRecord[column.property] = column.displayValue(value); + } else { + newRecord[column.property] = column.displayValue[value]; + } + } else { + newRecord[column.property] = value; + } + } + records.push(newRecord); + } + }, + handleError: function(aError) { + callback(records); + }, + + handleCompletion: function(aReason) { + callback(records); + } + }); + selStmt.finalize(); + }, + + wipeAllData: function EDS_wipeAllData(callback) { + let logger = Log4Moz.repository.getLogger("TestPilot.Database"); + logger.trace("ExperimentDataStore.wipeAllData called.\n"); + let wipeSql = "DELETE FROM " + this._tableName; + let wipeStmt = this._createStatement(wipeSql); + wipeStmt.executeAsync({ + handleResult: function(aResultSet) { + }, + handleError: function(aError) { + if (callback) { + callback(false); + } + }, + handleCompletion: function(aReason) { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + logger.trace("ExperimentDataStore.wipeAllData complete.\n"); + if (callback) { + callback(true); + } + } else { + if (callback) { + callback(false); + } + } + } + }); + wipeStmt.finalize(); + }, + + nukeTable: function EDS_nukeTable() { + // Should never be called, except if schema needs to be changed + // during debugging/development. + let nuke = this._createStatement("DROP TABLE " + this._tableName); + nuke.executeAsync(); + nuke.finalize(); + }, + + haveData: function EDS_haveData(callback) { + let countSql = "SELECT * FROM " + this._tableName; + let countStmt = this._createStatement(countSql); + let hasData = false; + countStmt.executeAsync({ + handleResult: function(aResultSet) { + if (aResultSet.getNextRow()) { + hasData = true; + } + }, + + handleError: function(aError) { + callback(false); + }, + + handleCompletion: function(aReason) { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED && + hasData) { + callback(true); + } else { + callback(false); + } + } + }); + countStmt.finalize(); + }, + + getHumanReadableColumnNames: function EDS_getHumanReadableColumnNames() { + let i; + return [ this._columns[i].displayName for (i in this._columns) ]; + }, + + getPropertyNames: function EDS_getPropertyNames() { + let i; + return [ this._columns[i].property for (i in this._columns) ]; + } +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/extension-update.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/extension-update.js new file mode 100644 index 000000000000..d4130eca0143 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/extension-update.js @@ -0,0 +1,89 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Raymond Lee + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = ["TestPilotExtensionUpdate"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +let TestPilotExtensionUpdate = { + check : function(extensionId) { + let extensionManager = + Cc["@mozilla.org/extensions/manager;1"]. + getService(Ci.nsIExtensionManager); + let listener = new TestPilotExtensionUpdateCheckListener(); + let items = [extensionManager.getItemForID(extensionId)]; + extensionManager.update( + items, items.length, false, listener, + Ci.nsIExtensionManager.UPDATE_WHEN_USER_REQUESTED); + } +}; + +function TestPilotExtensionUpdateCheckListener() { +} + +TestPilotExtensionUpdateCheckListener.prototype = { + _addons: [], + + onUpdateStarted: function() { + }, + + onUpdateEnded: function() { + if (this._addons.length > 0) { + let extensionManager = + Cc["@mozilla.org/extensions/manager;1"]. + getService(Ci.nsIExtensionManager); + let wm = + Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + let win = wm.getMostRecentWindow("navigator:browser"); + + extensionManager.addDownloads(this._addons, this._addons.length, null); + win.BrowserOpenAddonsMgr("extensions"); + } + }, + + onAddonUpdateStarted: function(addon) { + }, + + onAddonUpdateEnded: function(addon, status) { + if (status == Ci.nsIAddonUpdateCheckListener.STATUS_UPDATE) { + this._addons.push(addon); + } + } +}; + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/feedback.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/feedback.js new file mode 100644 index 000000000000..6ce181721d5f --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/feedback.js @@ -0,0 +1,83 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = ["FeedbackManager"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; + +let Application = Cc["@mozilla.org/fuel/application;1"] + .getService(Ci.fuelIApplication); + +var FeedbackManager = { + _lastVisitedUrl: null, + + _happyUrl: null, + get happyUrl() { + if (!this._happyUrl) { + this._happyUrl = Application.prefs.getValue("extensions.input.happyURL", ""); + } + return this._happyUrl; + }, + + _sadUrl: null, + get sadUrl() { + if (!this._sadUrl) { + this._sadUrl = Application.prefs.getValue("extensions.input.sadURL", ""); + } + return this._sadUrl; + }, + + setCurrUrl: function FeedbackManager_setCurrUrl(url) { + this._lastVisitedUrl = url; + }, + + fillInFeedbackPage: function FeedbackManager_fifp(url, window) { + /* If the user activated the happy or sad feedback feature, a page + * gets loaded containing an input field id = id_url + * Fill this field in with the referring URL. + */ + if (url == this.happyUrl || url == this.sadUrl) { + let tabbrowser = window.getBrowser(); + let currentBrowser = tabbrowser.selectedBrowser; + let document = currentBrowser.contentDocument; + let field = document.getElementById("id_url"); + if (field && this._lastVisitedUrl) { + field.value = this._lastVisitedUrl; + } + } + } +}; \ No newline at end of file diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/jar-code-store.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/jar-code-store.js new file mode 100644 index 000000000000..11a0fc65a9b6 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/jar-code-store.js @@ -0,0 +1,254 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +function JarStore() { + try { + let baseDirName = "TestPilotExperimentFiles"; // this should go in pref? + this._baseDir = null; + this._localOverrides = {}; //override with code for debugging purposes + this._index = {}; // tells us which jar file to look in for each module + this._lastModified = {}; // tells us when each jar file was last modified + this._init( baseDirName ); + } catch (e) { + console.warn("Error instantiating jar store: " + e); + } +} +JarStore.prototype = { + + _init: function( baseDirectory ) { + let prefs = require("preferences-service"); + this._localOverrides = JSON.parse( + prefs.get("extensions.testpilot.codeOverride", "{}")); + + let dir = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile); + dir.append(baseDirectory); + this._baseDir = dir; + if( !this._baseDir.exists() || !this._baseDir.isDirectory() ) { + // if jar storage directory doesn't exist, create it: + console.info("Creating: " + this._baseDir.path + "\n"); + this._baseDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0777); + } else { + // Process any jar files already on disk from previous runs: + // Build lookup index of module->jar file and modified dates + let jarFiles = this._baseDir.directoryEntries; + while(jarFiles.hasMoreElements()) { + let jarFile = jarFiles.getNext().QueryInterface(Ci.nsIFile); + // Make sure this is actually a jar file: + if (jarFile.leafName.indexOf(".jar") != jarFile.leafName.length - 4) { + continue; + } + this._lastModified[jarFile.leafName] = jarFile.lastModifiedTime; + this._indexJar(jarFile); + } + } + }, + + _indexJar: function(jarFile) { + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] + .createInstance(Ci.nsIZipReader); + zipReader.open(jarFile); // must already be nsIFile + let entries = zipReader.findEntries(null); + while(entries.hasMore()) { + // Find all .js files inside jar file: + let entry = entries.getNext(); + if (entry.indexOf(".js") == entry.length - 3) { + // add entry to index + let moduleName = entry.slice(0, entry.length - 3); + this._index[moduleName] = jarFile.path; + } + } + zipReader.close(); + }, + + _verifyJar: function(jarFile, expectedHash) { + // Compare the jar file's hash to the expected hash from the + // index file. + // from https://developer.mozilla.org/en/nsICryptoHash#Computing_the_Hash_of_a_File + console.info("Attempting to verify jarfile vs hash = " + expectedHash); + let istream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + // open for reading + istream.init(jarFile, 0x01, 0444, 0); + let ch = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + // Use SHA256, it's more secure than MD5: + ch.init(ch.SHA256); + // this tells updateFromStream to read the entire file + const PR_UINT32_MAX = 0xffffffff; + ch.updateFromStream(istream, PR_UINT32_MAX); + // pass false here to get binary data back + let hash = ch.finish(false); + + // return the two-digit hexadecimal code for a byte + function toHexString(charCode) + { + return ("0" + charCode.toString(16)).slice(-2); + } + + // convert the binary hash data to a hex string. + let s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join(""); + // s now contains your hash in hex + + return (s == expectedHash); + }, + + saveJarFile: function( filename, rawData, expectedHash ) { + console.info("Saving a JAR file as " + filename + " hash = " + expectedHash); + // rawData is a string of binary data representing a jar file + try { + let jarFile = this._baseDir.clone(); + // filename may have directories in it; use just the last part + jarFile.append(filename.split("/").pop()); + + // If a file of that name already exists, remove it! + if (jarFile.exists()) { + jarFile.remove(false); + } + // From https://developer.mozilla.org/en/Code_snippets/File_I%2f%2fO#Getting_special_files + jarFile.create( Ci.nsIFile.NORMAL_FILE_TYPE, 600); + let stream = Cc["@mozilla.org/network/safe-file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(jarFile, 0x04 | 0x08 | 0x20, 0600, 0); // readwrite, create, truncate + + stream.write(rawData, rawData.length); + if (stream instanceof Ci.nsISafeOutputStream) { + stream.finish(); + } else { + stream.close(); + } + // Verify hash; if it's good, index and set last modified time. + // If not good, remove it. + if (this._verifyJar(jarFile, expectedHash)) { + this._indexJar(jarFile); + this._lastModified[jarFile.leafName] = jarFile.lastModifiedTime; + } else { + console.warn("Bad JAR file, doesn't match hash: " + expectedHash); + jarFile.remove(false); + } + } catch(e) { + console.warn("Error in saving jar file: " + e); + } + }, + + resolveModule: function(root, path) { + // Root will be null if require() was done by absolute path. + if (root != null) { + // TODO I don't think we need to do anything special here. + } + // drop ".js" off end of path to get module + let module; + if (path.indexOf(".js") == path.length - 3) { + module = path.slice(0, path.length - 3); + } else { + module = path; + } + if (this._index[module]) { + let resolvedPath = this._index[module] + "!" + module + ".js"; + return resolvedPath; + } + return null; + // must return a path... which gets passed to getFile. + }, + + getFile: function(path) { + // used externally by cuddlefish; takes the path and returns + // {contents: data}. + if (this._localOverrides[path]) { + let code = this._localOverrides[path]; + return {contents: code}; + } + let parts = path.split("!"); + let filePath = parts[0]; + let entryName = parts[1]; + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(filePath); + return this._readEntryFromJarFile(file, entryName); + }, + + _readEntryFromJarFile: function(jarFile, entryName) { + // Reads out content of entry, without unzipping jar file to disk. + // Open the jar file + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] + .createInstance(Ci.nsIZipReader); + zipReader.open(jarFile); // must already be nsIFile + let rawStream = zipReader.getInputStream(entryName); + let stream = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + stream.init(rawStream); + try { + let data = new String(); + let chunk = {}; + do { + chunk = stream.read(-1); + data += chunk; + } while (chunk.length > 0); + return {contents: data}; + } catch(e) { + console.warn("Error reading entry from jar file: " + e ); + } + return null; + }, + + + getFileModifiedDate: function(filename) { + // used by remote experiment loader to know whether we have to redownload + // a thing or not. + filename = filename.split("/").pop(); + if (this._lastModified[filename]) { + return (this._lastModified[filename]); + } else { + return 0; + } + }, + + listAllFiles: function() { + // used by remote experiment loader + let x; + let list = [x for (x in this._index)]; + return list; + }, + + setLocalOverride: function(path, code) { + let prefs = require("preferences-service"); + this._localOverrides[path] = code; + prefs.set("extensions.testpilot.codeOverride", + JSON.stringify(this._localOverrides)); + } +}; + +exports.JarStore = JarStore; \ No newline at end of file diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/cuddlefish.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/cuddlefish.js new file mode 100644 index 000000000000..ab706017189e --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/cuddlefish.js @@ -0,0 +1,151 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +(function(global) { + const Cc = Components.classes; + const Ci = Components.interfaces; + const Cu = Components.utils; + const Cr = Components.results; + + var exports = {}; + + // Load the SecurableModule prerequisite. + var securableModule; + + if (global.require) + // We're being loaded in a SecurableModule. + securableModule = require("securable-module"); + else { + var myURI = Components.stack.filename.split(" -> ").slice(-1)[0]; + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + var securableModuleURI = ios.newURI("securable-module.js", null, + ios.newURI(myURI, null, null)); + if (securableModuleURI.scheme == "chrome") { + // The securable-module module is at a chrome URI, so we can't + // simply load it via Cu.import(). Let's assume we're in a + // chrome-privileged document and use mozIJSSubScriptLoader. + var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader); + + // Import the script, don't pollute the global scope. + securableModule = {__proto__: global}; + loader.loadSubScript(securableModuleURI.spec, securableModule); + securableModule = securableModule.SecurableModule; + } else { + securableModule = {}; + try { + Cu.import(securableModuleURI.spec, securableModule); + } catch (e if e.result == Cr.NS_ERROR_ILLEGAL_VALUE) { + Cu.reportError("Failed to load " + securableModuleURI.spec); + } + } + } + + function unloadLoader() { + this.require("unload").send(); + } + + var cuddlefishSandboxFactory = { + createSandbox: function(options) { + var filename = options.filename ? options.filename : null; + var sandbox = this.__proto__.createSandbox(options); + sandbox.defineProperty("__url__", filename); + return sandbox; + }, + __proto__: new securableModule.SandboxFactory("system") + }; + + function CuddlefishModule(loader) { + this.parentLoader = loader; + this.__proto__ = exports; + } + + var Loader = exports.Loader = function Loader(options) { + var globals = {Cc: Components.classes, + Ci: Components.interfaces, + Cu: Components.utils, + Cr: Components.results}; + + if (options.console) + globals.console = options.console; + if (options.memory) + globals.memory = options.memory; + + var modules = {}; + var loaderOptions = {rootPath: options.rootPath, + rootPaths: options.rootPaths, + fs: options.fs, + sandboxFactory: cuddlefishSandboxFactory, + globals: globals, + modules: modules}; + + var loader = new securableModule.Loader(loaderOptions); + var path = loader.fs.resolveModule(null, "cuddlefish"); + modules[path] = new CuddlefishModule(loader); + + if (!globals.console) { + var console = loader.require("plain-text-console"); + globals.console = new console.PlainTextConsole(options.print); + } + if (!globals.memory) + globals.memory = loader.require("memory"); + + loader.console = globals.console; + loader.memory = globals.memory; + loader.unload = unloadLoader; + + return loader; + }; + + if (global.window) { + // We're being loaded in a chrome window, or a web page with + // UniversalXPConnect privileges. + global.Cuddlefish = exports; + } else if (global.exports) { + // We're being loaded in a SecurableModule. + for (name in exports) { + global.exports[name] = exports[name]; + } + } else { + // We're being loaded in a JS module. + global.EXPORTED_SYMBOLS = []; + for (name in exports) { + global.EXPORTED_SYMBOLS.push(name); + global[name] = exports[name]; + } + } + })(this); diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/memory.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/memory.js new file mode 100644 index 000000000000..aaffd84ffc98 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/memory.js @@ -0,0 +1,115 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var compactTimerId; +var COMPACT_INTERVAL = 5000; +var trackedObjects = {}; + +function scheduleNextCompaction() { + compactTimerId = require("timer").setTimeout(compact, COMPACT_INTERVAL); +} + +function compact() { + var newTrackedObjects = {}; + for (name in trackedObjects) { + var oldBin = trackedObjects[name]; + var newBin = []; + var strongRefs = []; + for (var i = 0; i < oldBin.length; i++) { + var strongRef = oldBin[i].weakref.get(); + if (strongRef && strongRefs.indexOf(strongRef) == -1) { + strongRefs.push(strongRef); + newBin.push(oldBin[i]); + } + } + if (newBin.length) + newTrackedObjects[name] = newBin; + } + trackedObjects = newTrackedObjects; + scheduleNextCompaction(); +} + +var track = exports.track = function track(object, bin, stackFrameNumber) { + if (!compactTimerId) + scheduleNextCompaction(); + var frame = Components.stack.caller; + var weakref = Cu.getWeakReference(object); + if (!bin) + bin = object.constructor.name; + if (bin == "Object") + bin = frame.name; + if (!bin) + bin = "generic"; + if (!(bin in trackedObjects)) + trackedObjects[bin] = []; + + if (stackFrameNumber > 0) + for (var i = 0; i < stackFrameNumber; i++) + frame = frame.caller; + + trackedObjects[bin].push({weakref: weakref, + created: new Date(), + filename: frame.filename, + lineNo: frame.lineNumber}); +}; + +var getBins = exports.getBins = function getBins() { + var names = []; + for (name in trackedObjects) + names.push(name); + return names; +}; + +var getObjects = exports.getObjects = function getObjects(bin) { + function getLiveObjectsInBin(bin, array) { + for (var i = 0; i < bin.length; i++) { + var object = bin[i].weakref.get(); + if (object) + array.push(bin[i]); + } + } + + var results = []; + if (bin) { + if (bin in trackedObjects) + getLiveObjectsInBin(trackedObjects[bin], results); + } else + for (name in trackedObjects) + getLiveObjectsInBin(trackedObjects[name], results); + return results; +}; + +require("unload").when(function() { trackedObjects = {}; }); diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/observer-service.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/observer-service.js new file mode 100644 index 000000000000..523423fc0b46 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/observer-service.js @@ -0,0 +1,191 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Observers. + * + * The Initial Developer of the Original Code is Daniel Aquino. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Daniel Aquino + * Myk Melez + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var jsm = {}; Cu.import("resource://gre/modules/XPCOMUtils.jsm", jsm); +var XPCOMUtils = jsm.XPCOMUtils; + +/** + * A service for adding, removing and notifying observers of notifications. + * Wraps the nsIObserverService interface. + * + * @version 0.2 + */ + +var service = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + +/** + * A cache of observers that have been added. + * + * We use this to remove observers when a caller calls |Observers.remove|. + */ +var cache = []; + +/** + * Register the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic to observe + * + * @param callback {Object} + * the callback; an Object that implements nsIObserver or a Function + * that gets called when the notification occurs + * + * @param thisObject {Object} [optional] + * the object to use as |this| when calling a Function callback + * + * @returns the observer + */ +var add = exports.add = function add(topic, callback, thisObject) { + var observer = new Observer(topic, callback, thisObject); + service.addObserver(observer, topic, true); + cache.push(observer); + + return observer; +}; + +/** + * Unregister the given callback as an observer of the given topic. + * + * @param topic {String} + * the topic being observed + * + * @param callback {Object} + * the callback doing the observing + * + * @param thisObject {Object} [optional] + * the object being used as |this| when calling a Function callback + */ +var remove = exports.remove = function remove(topic, callback, thisObject) { + // This seems fairly inefficient, but I'm not sure how much better + // we can make it. We could index by topic, but we can't index by callback + // or thisObject, as far as I know, since the keys to JavaScript hashes + // (a.k.a. objects) can apparently only be primitive values. + var [observer] = cache.filter(function(v) { + return (v.topic == topic && + v.callback == callback && + v.thisObject == thisObject); + }); + if (observer) { + service.removeObserver(observer, topic); + cache.splice(cache.indexOf(observer), 1); + } +}; + +/** + * Notify observers about something. + * + * @param topic {String} + * the topic to notify observers about + * + * @param subject {Object} [optional] + * some information about the topic; can be any JS object or primitive + * + * @param data {String} [optional] [deprecated] + * some more information about the topic; deprecated as the subject + * is sufficient to pass all needed information to the JS observers + * that this module targets; if you have multiple values to pass to + * the observer, wrap them in an object and pass them via the subject + * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) + */ +var notify = exports.notify = function notify(topic, subject, data) { + subject = (typeof subject == "undefined") ? null : new Subject(subject); + data = (typeof data == "undefined") ? null : data; + service.notifyObservers(subject, topic, data); +}; + +function Observer(topic, callback, thisObject) { + this.topic = topic; + this.callback = callback; + this.thisObject = thisObject; +} + +Observer.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + observe: function(subject, topic, data) { + // Extract the wrapped object for subjects that are one of our + // wrappers around a JS object. This way we support both wrapped + // subjects created using this module and those that are real + // XPCOM components. + if (subject && typeof subject == "object" && + ("wrappedJSObject" in subject) && + ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) + subject = subject.wrappedJSObject.object; + + try { + if (typeof this.callback == "function") { + if (this.thisObject) + this.callback.call(this.thisObject, subject, data); + else + this.callback(subject, data); + } else // typeof this.callback == "object" (nsIObserver) + this.callback.observe(subject, topic, data); + } catch (e) { + console.exception(e); + } + } +}; + +function Subject(object) { + // Double-wrap the object and set a property identifying the + // wrappedJSObject as one of our wrappers to distinguish between + // subjects that are one of our wrappers (which we should unwrap + // when notifying our observers) and those that are real JS XPCOM + // components (which we should pass through unaltered). + this.wrappedJSObject = { + observersModuleSubjectWrapper: true, + object: object + }; +} + +Subject.prototype = { + QueryInterface: XPCOMUtils.generateQI([]), + getHelperForLanguage: function() {}, + getInterfaces: function() {} +}; + +require("unload").when( + function removeAllObservers() { + // Make a copy of cache first, since cache will be changing as we + // iterate through it. + cache.slice().forEach( + function(observer) { + remove(observer.topic, observer.callback, observer.thisObject); + }); + }); diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/plain-text-console.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/plain-text-console.js new file mode 100644 index 000000000000..0e6b99658e8c --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/plain-text-console.js @@ -0,0 +1,100 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +function stringify(arg) { + try { + return String(arg); + } + catch(ex) { + return ""; + } +} + +function stringifyArgs(args) { + return Array.map(args, stringify).join(" "); +} + +function message(print, level, args) { + print(level + ": " + stringifyArgs(args) + "\n"); +} + +var Console = exports.PlainTextConsole = function PlainTextConsole(print) { + if (!print) + print = dump; + if (print === dump) { + // If we're just using dump(), auto-enable preferences so + // that the developer actually sees the console output. + var prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + prefs.setBoolPref("browser.dom.window.dump.enabled", true); + } + this.print = print; +}; + +Console.prototype = { + log: function log() { + message(this.print, "info", arguments); + }, + + info: function info() { + message(this.print, "info", arguments); + }, + + warn: function warn() { + message(this.print, "warning", arguments); + }, + + error: function error() { + message(this.print, "error", arguments); + }, + + debug: function debug() { + message(this.print, "debug", arguments); + }, + + exception: function exception(e) { + var fullString = ("An exception occurred.\n" + + require("traceback").format(e) + "\n" + e); + this.error(fullString); + }, + + trace: function trace() { + var traceback = require("traceback"); + var stack = traceback.get(); + stack.splice(-1, 1); + message(this.print, "info", [traceback.format(stack)]); + } +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/preferences-service.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/preferences-service.js new file mode 100644 index 000000000000..1d0c8aef8798 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/preferences-service.js @@ -0,0 +1,138 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Preferences. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez + * Daniel Aquino + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var jsm = {}; Cu.import("resource://gre/modules/XPCOMUtils.jsm", jsm); +var XPCOMUtils = jsm.XPCOMUtils; + +// The minimum and maximum integers that can be set as preferences. +// The range of valid values is narrower than the range of valid JS values +// because the native preferences code treats integers as NSPR PRInt32s, +// which are 32-bit signed integers on all platforms. +const MAX_INT = Math.pow(2, 31) - 1; +const MIN_INT = -MAX_INT; + +var prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService).getBranch(null); + +var get = exports.get = function get(name, defaultValue) { + switch (prefSvc.getPrefType(name)) { + case Ci.nsIPrefBranch.PREF_STRING: + return prefSvc.getComplexValue(name, Ci.nsISupportsString).data; + + case Ci.nsIPrefBranch.PREF_INT: + return prefSvc.getIntPref(name); + + case Ci.nsIPrefBranch.PREF_BOOL: + return prefSvc.getBoolPref(name); + + case Ci.nsIPrefBranch.PREF_INVALID: + return defaultValue; + + default: + // This should never happen. + throw new Error("Error getting pref " + name + + "; its value's type is " + + prefSvc.getPrefType(name) + + ", which I don't know " + + "how to handle."); + } +}; + +var set = exports.set = function set(name, value) { + var prefType; + if (typeof value != "undefined" && value != null) + prefType = value.constructor.name; + + switch (prefType) { + case "String": + { + var string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = value; + prefSvc.setComplexValue(name, Ci.nsISupportsString, string); + } + break; + + case "Number": + // We throw if the number is outside the range, since the result + // will never be what the consumer wanted to store, but we only warn + // if the number is non-integer, since the consumer might not mind + // the loss of precision. + if (value > MAX_INT || value < MIN_INT) + throw new Error("you cannot set the " + name + + " pref to the number " + value + + ", as number pref values must be in the signed " + + "32-bit integer range -(2^31-1) to 2^31-1. " + + "To store numbers outside that range, store " + + "them as strings."); + if (value % 1 != 0) + throw new Error("cannot store non-integer number: " + value); + prefSvc.setIntPref(name, value); + break; + + case "Boolean": + prefSvc.setBoolPref(name, value); + break; + + default: + throw new Error("can't set pref " + name + " to value '" + value + + "'; it isn't a String, Number, or Boolean"); + } +}; + +var has = exports.has = function has(name) { + return (prefSvc.getPrefType(name) != Ci.nsIPrefBranch.PREF_INVALID); +}; + +var isSet = exports.isSet = function isSet(name) { + return (has(name) && prefSvc.prefHasUserValue(name)); +}; + +var reset = exports.reset = function reset(name) { + try { + prefSvc.clearUserPref(name); + } catch (e if e.result == Cr.NS_ERROR_UNEXPECTED) { + // The pref service throws NS_ERROR_UNEXPECTED when the caller tries + // to reset a pref that doesn't exist or is already set to its default + // value. This interface fails silently in those cases, so callers + // can unconditionally reset a pref without having to check if it needs + // resetting first or trap exceptions after the fact. It passes through + // other exceptions, however, so callers know about them, since we don't + // know what other exceptions might be thrown and what they might mean. + } +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/securable-module.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/securable-module.js new file mode 100644 index 000000000000..eb737567d808 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/securable-module.js @@ -0,0 +1,320 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +(function(global) { + const Cc = Components.classes; + const Ci = Components.interfaces; + const Cu = Components.utils; + const Cr = Components.results; + + var exports = {}; + + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + + var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + + function resolvePrincipal(principal, defaultPrincipal) { + if (principal === undefined) + return defaultPrincipal; + if (principal == "system") + return systemPrincipal; + return principal; + } + + // The base URI to we use when we're given relative URLs, if any. + var baseURI = null; + if (global.window) + baseURI = ios.newURI(global.location.href, null, null); + exports.baseURI = baseURI; + + // The "parent" chrome URI to use if we're loading code that + // needs chrome privileges but may not have a filename that + // matches any of SpiderMonkey's defined system filename prefixes. + // The latter is needed so that wrappers can be automatically + // made for the code. For more information on this, see + // bug 418356: + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=418356 + var parentChromeURIString; + if (baseURI) + // We're being loaded from a chrome-privileged document, so + // use its URL as the parent string. + parentChromeURIString = baseURI.spec; + else + // We're being loaded from a chrome-privileged JS module or + // SecurableModule, so use its filename (which may itself + // contain a reference to a parent). + parentChromeURIString = Components.stack.filename; + + function maybeParentifyFilename(filename) { + var doParentifyFilename = true; + try { + // TODO: Ideally we should just make + // nsIChromeRegistry.wrappersEnabled() available from script + // and use it here. Until that's in the platform, though, + // we'll play it safe and parentify the filename unless + // we're absolutely certain things will be ok if we don't. + var filenameURI = ios.newURI(options.filename, + null, + baseURI); + if (filenameURI.scheme == 'chrome' && + filenameURI.path.indexOf('/content/') == 0) + // Content packages will always have wrappers made for them; + // if automatic wrappers have been disabled for the + // chrome package via a chrome manifest flag, then + // this still works too, to the extent that the + // content package is insecure anyways. + doParentifyFilename = false; + } catch (e) {} + if (doParentifyFilename) + return parentChromeURIString + " -> " + filename; + return filename; + } + + function getRootDir(urlStr) { + // TODO: This feels hacky, and like there will be edge cases. + return urlStr.slice(0, urlStr.lastIndexOf("/") + 1); + } + + exports.SandboxFactory = function SandboxFactory(defaultPrincipal) { + // Unless specified otherwise, use a principal with limited + // privileges. + this._defaultPrincipal = resolvePrincipal(defaultPrincipal, + "http://www.mozilla.org"); + }, + + exports.SandboxFactory.prototype = { + createSandbox: function createSandbox(options) { + var principal = resolvePrincipal(options.principal, + this._defaultPrincipal); + + return { + _sandbox: new Cu.Sandbox(principal), + _principal: principal, + defineProperty: function defineProperty(name, value) { + this._sandbox[name] = value; + }, + evaluate: function evaluate(options) { + if (typeof(options) == 'string') + options = {contents: options}; + options = {__proto__: options}; + if (typeof(options.contents) != 'string') + throw new Error('Expected string for options.contents'); + if (options.lineNo === undefined) + options.lineNo = 1; + if (options.jsVersion === undefined) + options.jsVersion = "1.8"; + if (typeof(options.filename) != 'string') + options.filename = ''; + + if (this._principal == systemPrincipal) + options.filename = maybeParentifyFilename(options.filename); + + return Cu.evalInSandbox(options.contents, + this._sandbox, + options.jsVersion, + options.filename, + options.lineNo); + } + }; + } + }; + + exports.Loader = function Loader(options) { + options = {__proto__: options}; + if (options.fs === undefined) { + var rootPaths = options.rootPath || options.rootPaths; + if (rootPaths) { + if (rootPaths.constructor.name != "Array") + rootPaths = [rootPaths]; + var fses = [new exports.LocalFileSystem(path) + for each (path in rootPaths)]; + options.fs = new exports.CompositeFileSystem(fses); + } else + options.fs = new exports.LocalFileSystem(); + } + if (options.sandboxFactory === undefined) + options.sandboxFactory = new exports.SandboxFactory( + options.defaultPrincipal + ); + if (options.modules === undefined) + options.modules = {}; + if (options.globals === undefined) + options.globals = {}; + + this.fs = options.fs; + this.sandboxFactory = options.sandboxFactory; + this.modules = options.modules; + this.globals = options.globals; + }; + + exports.Loader.prototype = { + _makeRequire: function _makeRequire(rootDir) { + var self = this; + + return function require(module) { + var path = self.fs.resolveModule(rootDir, module); + if (!path) + throw new Error('Module "' + module + '" not found'); + if (!(path in self.modules)) { + var options = self.fs.getFile(path); + if (options.filename === undefined) + options.filename = path; + + var exports = {}; + var sandbox = self.sandboxFactory.createSandbox(options); + for (name in self.globals) + sandbox.defineProperty(name, self.globals[name]); + sandbox.defineProperty('require', self._makeRequire(path)); + sandbox.defineProperty('exports', exports); + self.modules[path] = exports; + sandbox.evaluate(options); + } + return self.modules[path]; + }; + }, + + require: function require(module) { + return (this._makeRequire(null))(module); + }, + + runScript: function runScript(options) { + if (typeof(options) == 'string') + options = {contents: options}; + options = {__proto__: options}; + var sandbox = this.sandboxFactory.createSandbox(options); + for (name in this.globals) + sandbox.defineProperty(name, this.globals[name]); + sandbox.defineProperty('require', this._makeRequire(null)); + return sandbox.evaluate(options); + } + }; + + exports.CompositeFileSystem = function CompositeFileSystem(fses) { + this.fses = fses; + this._pathMap = {}; + }; + + exports.CompositeFileSystem.prototype = { + resolveModule: function resolveModule(base, path) { + for (var i = 0; i < this.fses.length; i++) { + var fs = this.fses[i]; + var absPath = fs.resolveModule(base, path); + if (absPath) { + this._pathMap[absPath] = fs; + return absPath; + } + } + return null; + }, + getFile: function getFile(path) { + return this._pathMap[path].getFile(path); + } + }; + + exports.LocalFileSystem = function LocalFileSystem(root) { + if (root === undefined) { + if (!baseURI) + throw new Error("Need a root path for module filesystem"); + root = baseURI; + } + if (typeof(root) == 'string') + root = ios.newURI(root, null, baseURI); + if (root instanceof Ci.nsIFile) + root = ios.newFileURI(root); + if (!(root instanceof Ci.nsIURI)) + throw new Error('Expected nsIFile, nsIURI, or string for root'); + + this.root = root.spec; + this._rootURI = root; + this._rootURIDir = getRootDir(root.spec); + }; + + exports.LocalFileSystem.prototype = { + resolveModule: function resolveModule(base, path) { + path = path + ".js"; + + var baseURI; + if (!base || path.charAt(0) != '.') + baseURI = this._rootURI; + else + baseURI = ios.newURI(base, null, null); + var newURI = ios.newURI(path, null, baseURI); + if (newURI.spec.indexOf(this._rootURIDir) == 0) { + var channel = ios.newChannelFromURI(newURI); + try { + channel.open().close(); + } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { + return null; + } + return newURI.spec; + } + return null; + }, + getFile: function getFile(path) { + var channel = ios.newChannel(path, null, null); + var iStream = channel.open(); + var siStream = Cc['@mozilla.org/scriptableinputstream;1'] + .createInstance(Ci.nsIScriptableInputStream); + siStream.init(iStream); + var data = new String(); + data += siStream.read(-1); + siStream.close(); + iStream.close(); + return {contents: data}; + } + }; + + if (global.window) { + // We're being loaded in a chrome window, or a web page with + // UniversalXPConnect privileges. + global.SecurableModule = exports; + } else if (global.exports) { + // We're being loaded in a SecurableModule. + for (name in exports) { + global.exports[name] = exports[name]; + } + } else { + // We're being loaded in a JS module. + global.EXPORTED_SYMBOLS = []; + for (name in exports) { + global.EXPORTED_SYMBOLS.push(name); + global[name] = exports[name]; + } + } + })(this); diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/timer.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/timer.js new file mode 100644 index 000000000000..853edaa7c0a3 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/timer.js @@ -0,0 +1,88 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var jsm = {}; Cu.import("resource://gre/modules/XPCOMUtils.jsm", jsm); +var XPCOMUtils = jsm.XPCOMUtils; + +var timerClass = Cc["@mozilla.org/timer;1"]; +var nextID = 1; +var timers = {}; + +function TimerCallback(callback) { + this._callback = callback; + this.QueryInterface = XPCOMUtils.generateQI([Ci.nsITimerCallback]); +}; + +TimerCallback.prototype = { + notify : function notify(timer) { + try { + for (timerID in timers) + if (timers[timerID] === timer) { + delete timers[timerID]; + break; + } + this._callback.apply(null, []); + } catch (e) { + console.exception(e); + } + } +}; + +var setTimeout = exports.setTimeout = function setTimeout(callback, delay) { + var timer = timerClass.createInstance(Ci.nsITimer); + + var timerID = nextID++; + timers[timerID] = timer; + + timer.initWithCallback(new TimerCallback(callback), + delay, + timer.TYPE_ONE_SHOT); + return timerID; +}; + +var clearTimeout = exports.clearTimeout = function clearTimeout(timerID) { + var timer = timers[timerID]; + if (timer) { + timer.cancel(); + delete timers[timerID]; + } +}; + +require("unload").when( + function cancelAllPendingTimers() { + var timerIDs = [timerID for (timerID in timers)]; + timerIDs.forEach(function(timerID) { clearTimeout(timerID); }); + }); diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/traceback.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/traceback.js new file mode 100644 index 000000000000..8ee67e82d0df --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/traceback.js @@ -0,0 +1,140 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// Undo the auto-parentification of URLs done in bug 418356. +function deParentifyURL(url) { + return url ? url.split(" -> ").slice(-1)[0] : url; +} + +// TODO: We might want to move this function to url or some similar +// module. +function getLocalFile(path) { + var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + var channel = ios.newChannel(path, null, null); + var iStream = channel.open(); + var siStream = Cc['@mozilla.org/scriptableinputstream;1'] + .createInstance(Ci.nsIScriptableInputStream); + siStream.init(iStream); + var data = new String(); + data += siStream.read(-1); + siStream.close(); + iStream.close(); + return data; +} + +function safeGetFileLine(path, line) { + try { + var scheme = require("url").parse(path).scheme; + // TODO: There should be an easier, more accurate way to figure out + // what's the case here. + if (!(scheme == "http" || scheme == "https")) + return getLocalFile(path).split("\n")[line - 1]; + } catch (e) {} + return null; +} + +function errorStackToJSON(stack) { + var lines = stack.split("\n"); + + var frames = []; + lines.forEach( + function(line) { + if (!line) + return; + var atIndex = line.indexOf("@"); + var colonIndex = line.lastIndexOf(":"); + var filename = deParentifyURL(line.slice(atIndex + 1, colonIndex)); + var lineNo = parseInt(line.slice(colonIndex + 1)); + var funcSig = line.slice(0, atIndex); + var funcName = funcSig.slice(0, funcSig.indexOf("(")); + frames.unshift({filename: filename, + funcName: funcName, + lineNo: lineNo}); + }); + + return frames; +}; + +function nsIStackFramesToJSON(frame) { + var stack = []; + + while (frame) { + var filename = deParentifyURL(frame.filename); + stack.splice(0, 0, {filename: filename, + lineNo: frame.lineNumber, + funcName: frame.name}); + frame = frame.caller; + } + + return stack; +}; + +var fromException = exports.fromException = function fromException(e) { + if (e instanceof Ci.nsIException) + return nsIStackFramesToJSON(e.location); + return errorStackToJSON(e.stack); +}; + +var get = exports.get = function get() { + return nsIStackFramesToJSON(Components.stack.caller); +}; + +var format = exports.format = function format(tbOrException) { + if (tbOrException === undefined) { + tbOrException = get(); + tbOrException.splice(-1, 1); + } + + var tb; + if (tbOrException.length === undefined) + tb = fromException(tbOrException); + else + tb = tbOrException; + + var lines = ["Traceback (most recent call last):"]; + + tb.forEach( + function(frame) { + lines.push(' File "' + frame.filename + '", line ' + + frame.lineNo + ', in ' + frame.funcName); + var sourceLine = safeGetFileLine(frame.filename, frame.lineNo); + if (sourceLine) + lines.push(' ' + sourceLine.trim()); + }); + + return lines.join("\n"); +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/unit-test.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/unit-test.js new file mode 100644 index 000000000000..c8f4750ae22c --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/unit-test.js @@ -0,0 +1,231 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var timer = require("timer"); +var file = require("file"); + +exports.findAndRunTests = function findAndRunTests(options) { + var finder = new TestFinder(options.dirs); + var runner = new TestRunner(); + runner.startMany({tests: finder.findTests(), + onDone: options.onDone}); +}; + +var TestFinder = exports.TestFinder = function TestFinder(dirs) { + this.dirs = dirs; +}; + +TestFinder.prototype = { + _makeTest: function _makeTest(suite, name, test) { + function runTest(runner) { + console.info("executing '" + suite + "." + name + "'"); + test(runner); + } + return runTest; + }, + + findTests: function findTests() { + var self = this; + var tests = []; + + this.dirs.forEach( + function(dir) { + var suites = [name.slice(0, -3) + for each (name in file.list(dir)) + if (/^test-.*\.js$/.test(name))]; + + suites.forEach( + function(suite) { + var module = require(suite); + for (name in module) + if (name.indexOf("test") == 0) + tests.push(self._makeTest(suite, name, module[name])); + }); + }); + return tests; + } +}; + +var TestRunner = exports.TestRunner = function TestRunner(options) { + this.passed = 0; + this.failed = 0; +}; + +TestRunner.prototype = { + DEFAULT_PAUSE_TIMEOUT: 10000, + + makeSandboxedLoader: function makeSandboxedLoader(options) { + if (!options) + options = {}; + var Cuddlefish = require("cuddlefish"); + + options.fs = Cuddlefish.parentLoader.fs; + return new Cuddlefish.Loader(options); + }, + + pass: function pass(message) { + console.info("pass:", message); + this.passed++; + }, + + fail: function fail(message) { + console.error("fail:", message); + this.failed++; + }, + + exception: function exception(e) { + console.exception(e); + this.failed++; + }, + + assertMatches: function assertMatches(string, regexp, message) { + if (regexp.test(string)) { + if (!message) + message = uneval(string) + " matches " + uneval(regexp); + this.pass(message); + } else { + var no = uneval(string) + " doesn't match " + uneval(regexp); + if (!message) + message = no; + else + message = message + " (" + no + ")"; + this.fail(message); + } + }, + + assertRaises: function assertRaises(func, predicate, message) { + try { + func(); + if (message) + this.fail(message + " (no exception thrown)"); + else + this.fail("function failed to throw exception"); + } catch (e) { + if (typeof(predicate) == "object") + this.assertMatches(e.message, predicate, message); + else + this.assertEqual(e.message, predicate, message); + } + }, + + assertNotEqual: function assertNotEqual(a, b, message) { + if (a != b) { + if (!message) + message = "a != b != " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " == " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertEqual: function assertEqual(a, b, message) { + if (a == b) { + if (!message) + message = "a == b == " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " != " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + done: function done() { + if (!this.isDone) { + this.isDone = true; + if (this.waitTimeout !== null) { + timer.clearTimeout(this.waitTimeout); + this.waitTimeout = null; + } + if (this.onDone !== null) { + var onDone = this.onDone; + this.onDone = null; + onDone(this); + } + } + }, + + waitUntilDone: function waitUntilDone(ms) { + if (ms === undefined) + ms = this.DEFAULT_PAUSE_TIMEOUT; + + var self = this; + + function tiredOfWaiting() { + self.failed++; + self.done(); + } + + this.waitTimeout = timer.setTimeout(tiredOfWaiting, ms); + }, + + startMany: function startMany(options) { + function scheduleNextTest(self) { + function runNextTest() { + var test = options.tests.pop(); + if (test) + self.start({test: test, onDone: scheduleNextTest}); + else + options.onDone(self); + } + timer.setTimeout(runNextTest, 0); + } + scheduleNextTest(this); + }, + + start: function start(options) { + this.test = options.test; + this.isDone = false; + this.onDone = options.onDone; + this.waitTimeout = null; + + try { + this.test(this); + } catch (e) { + this.exception(e); + } + if (this.waitTimeout === null) + this.done(); + } +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/unload.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/unload.js new file mode 100644 index 000000000000..f4c0f479af4c --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/unload.js @@ -0,0 +1,15 @@ +// This module was taken from narwhal: +// +// http://narwhaljs.org + +var observers = []; + +exports.when = function (observer) { + observers.unshift(observer); +}; + +exports.send = function () { + observers.forEach(function (observer) { + observer(); + }); +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/url.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/url.js new file mode 100644 index 000000000000..5ddfc44f88b2 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/lib/url.js @@ -0,0 +1,110 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Jetpack. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + +var resProt = ios.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + +function newURI(uriStr) { + try { + return ios.newURI(uriStr, null, null); + } catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) { + throw new Error("malformed URI: " + uriStr); + } catch (e if (e.result == Cr.NS_ERROR_FAILURE || + e.result == Cr.NS_ERROR_ILLEGAL_VALUE)) { + throw new Error("invalid URI: " + uriStr); + } +} + +function resolveResourceURI(uri) { + var resolved; + try { + resolved = resProt.resolveURI(uri); + } catch (e if e.result == Cr.NS_ERROR_NOT_AVAILABLE) { + throw new Error("resource does not exist: " + uri.spec); + }; + return resolved; +} + +var fromFilename = exports.fromFilename = function fromFilename(path) { + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return ios.newFileURI(file).spec; +}; + +var toFilename = exports.toFilename = function toFilename(url) { + var uri = newURI(url); + if (uri.scheme == "resource") + uri = newURI(resolveResourceURI(uri)); + if (uri.scheme == "file") { + var file = uri.QueryInterface(Ci.nsIFileURL).file; + return file.path; + } + throw new Error("cannot map to filename: " + url); +}; + +var parse = exports.parse = function parse(url) { + var uri = newURI(url); + + var userPass = null; + try { + userPass = uri.userPass ? uri.userPass : null; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + var host = null; + try { + host = uri.host; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + var port = null; + try { + port = uri.port == -1 ? null : uri.port; + } catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + + return {scheme: uri.scheme, + userPass: userPass, + host: host, + port: port, + path: uri.path}; +}; + +var resolve = exports.resolve = function resolve(base, relative) { + var baseURI = newURI(base); + return ios.newURI(relative, null, baseURI).spec; +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/log4moz.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/log4moz.js new file mode 100644 index 000000000000..57cdd5c5c46e --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/log4moz.js @@ -0,0 +1,565 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is log4moz + * + * The Initial Developer of the Original Code is + * Michael Johnston + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Michael Johnston + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ['Log4Moz']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const MODE_RDONLY = 0x01; +const MODE_WRONLY = 0x02; +const MODE_CREATE = 0x08; +const MODE_APPEND = 0x10; +const MODE_TRUNCATE = 0x20; + +const PERMS_FILE = 0644; +const PERMS_DIRECTORY = 0755; + +const ONE_BYTE = 1; +const ONE_KILOBYTE = 1024 * ONE_BYTE; +const ONE_MEGABYTE = 1024 * ONE_KILOBYTE; + +let Log4Moz = { + Level: { + Fatal: 70, + Error: 60, + Warn: 50, + Info: 40, + Config: 30, + Debug: 20, + Trace: 10, + All: 0, + Desc: { + 70: "FATAL", + 60: "ERROR", + 50: "WARN", + 40: "INFO", + 30: "CONFIG", + 20: "DEBUG", + 10: "TRACE", + 0: "ALL" + } + }, + + get repository() { + delete Log4Moz.repository; + Log4Moz.repository = new LoggerRepository(); + return Log4Moz.repository; + }, + set repository(value) { + delete Log4Moz.repository; + Log4Moz.repository = value; + }, + + get LogMessage() { return LogMessage; }, + get Logger() { return Logger; }, + get LoggerRepository() { return LoggerRepository; }, + + get Formatter() { return Formatter; }, + get BasicFormatter() { return BasicFormatter; }, + + get Appender() { return Appender; }, + get DumpAppender() { return DumpAppender; }, + get ConsoleAppender() { return ConsoleAppender; }, + get FileAppender() { return FileAppender; }, + get RotatingFileAppender() { return RotatingFileAppender; }, + + // Logging helper: + // let logger = Log4Moz.repository.getLogger("foo"); + // logger.info(Log4Moz.enumerateInterfaces(someObject).join(",")); + enumerateInterfaces: function Log4Moz_enumerateInterfaces(aObject) { + let interfaces = []; + + for (i in Ci) { + try { + aObject.QueryInterface(Ci[i]); + interfaces.push(i); + } + catch(ex) {} + } + + return interfaces; + }, + + // Logging helper: + // let logger = Log4Moz.repository.getLogger("foo"); + // logger.info(Log4Moz.enumerateProperties(someObject).join(",")); + enumerateProperties: function Log4Moz_enumerateProps(aObject, + aExcludeComplexTypes) { + let properties = []; + + for (p in aObject) { + try { + if (aExcludeComplexTypes && + (typeof aObject[p] == "object" || typeof aObject[p] == "function")) + continue; + properties.push(p + " = " + aObject[p]); + } + catch(ex) { + properties.push(p + " = " + ex); + } + } + + return properties; + } +}; + + +/* + * LogMessage + * Encapsulates a single log event's data + */ +function LogMessage(loggerName, level, message){ + this.loggerName = loggerName; + this.message = message; + this.level = level; + this.time = Date.now(); +} +LogMessage.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + get levelDesc() { + if (this.level in Log4Moz.Level.Desc) + return Log4Moz.Level.Desc[this.level]; + return "UNKNOWN"; + }, + + toString: function LogMsg_toString(){ + return "LogMessage [" + this.time + " " + this.level + " " + + this.message + "]"; + } +}; + +/* + * Logger + * Hierarchical version. Logs to all appenders, assigned or inherited + */ + +function Logger(name, repository) { + this._init(name, repository); +} +Logger.prototype = { + _init: function Logger__init(name, repository) { + if (!repository) + repository = Log4Moz.repository; + this._name = name; + this._appenders = []; + this._repository = repository; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + parent: null, + + get name() { + return this._name; + }, + + _level: null, + get level() { + if (this._level != null) + return this._level; + if (this.parent) + return this.parent.level; + dump("log4moz warning: root logger configuration error: no level defined\n"); + return Log4Moz.Level.All; + }, + set level(level) { + this._level = level; + }, + + _appenders: null, + get appenders() { + if (!this.parent) + return this._appenders; + return this._appenders.concat(this.parent.appenders); + }, + + addAppender: function Logger_addAppender(appender) { + for (let i = 0; i < this._appenders.length; i++) { + if (this._appenders[i] == appender) + return; + } + this._appenders.push(appender); + }, + + removeAppender: function Logger_removeAppender(appender) { + let newAppenders = []; + for (let i = 0; i < this._appenders.length; i++) { + if (this._appenders[i] != appender) + newAppenders.push(this._appenders[i]); + } + this._appenders = newAppenders; + }, + + log: function Logger_log(message) { + if (this.level > message.level) + return; + let appenders = this.appenders; + for (let i = 0; i < appenders.length; i++){ + appenders[i].append(message); + } + }, + + fatal: function Logger_fatal(string) { + this.log(new LogMessage(this._name, Log4Moz.Level.Fatal, string)); + }, + error: function Logger_error(string) { + this.log(new LogMessage(this._name, Log4Moz.Level.Error, string)); + }, + warn: function Logger_warn(string) { + this.log(new LogMessage(this._name, Log4Moz.Level.Warn, string)); + }, + info: function Logger_info(string) { + this.log(new LogMessage(this._name, Log4Moz.Level.Info, string)); + }, + config: function Logger_config(string) { + this.log(new LogMessage(this._name, Log4Moz.Level.Config, string)); + }, + debug: function Logger_debug(string) { + this.log(new LogMessage(this._name, Log4Moz.Level.Debug, string)); + }, + trace: function Logger_trace(string) { + this.log(new LogMessage(this._name, Log4Moz.Level.Trace, string)); + } +}; + +/* + * LoggerRepository + * Implements a hierarchy of Loggers + */ + +function LoggerRepository() {} +LoggerRepository.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + _loggers: {}, + + _rootLogger: null, + get rootLogger() { + if (!this._rootLogger) { + this._rootLogger = new Logger("root", this); + this._rootLogger.level = Log4Moz.Level.All; + } + return this._rootLogger; + }, + // FIXME: need to update all parent values if we do this + //set rootLogger(logger) { + // this._rootLogger = logger; + //}, + + _updateParents: function LogRep__updateParents(name) { + let pieces = name.split('.'); + let cur, parent; + + // find the closest parent + // don't test for the logger name itself, as there's a chance it's already + // there in this._loggers + for (let i = 0; i < pieces.length - 1; i++) { + if (cur) + cur += '.' + pieces[i]; + else + cur = pieces[i]; + if (cur in this._loggers) + parent = cur; + } + + // if we didn't assign a parent above, there is no parent + if (!parent) + this._loggers[name].parent = this.rootLogger; + else + this._loggers[name].parent = this._loggers[parent]; + + // trigger updates for any possible descendants of this logger + for (let logger in this._loggers) { + if (logger != name && logger.indexOf(name) == 0) + this._updateParents(logger); + } + }, + + getLogger: function LogRep_getLogger(name) { + if (!name) + name = this.getLogger.caller.name; + if (name in this._loggers) + return this._loggers[name]; + this._loggers[name] = new Logger(name, this); + this._updateParents(name); + return this._loggers[name]; + } +}; + +/* + * Formatters + * These massage a LogMessage into whatever output is desired + * Only the BasicFormatter is currently implemented + */ + +// Abstract formatter +function Formatter() {} +Formatter.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + format: function Formatter_format(message) {} +}; + +// FIXME: should allow for formatting the whole string, not just the date +function BasicFormatter(dateFormat) { + if (dateFormat) + this.dateFormat = dateFormat; +} +BasicFormatter.prototype = { + __proto__: Formatter.prototype, + + _dateFormat: null, + + get dateFormat() { + if (!this._dateFormat) + this._dateFormat = "%Y-%m-%d %H:%M:%S"; + return this._dateFormat; + }, + + set dateFormat(format) { + this._dateFormat = format; + }, + + format: function BF_format(message) { + // Pad a string to a certain length (20) with a character (space) + let pad = function BF__pad(str, len, chr) str + + new Array(Math.max((len || 20) - str.length + 1, 0)).join(chr || " "); + + // Generate a date string because toLocaleString doesn't work XXX 514803 + let z = function(n) n < 10 ? "0" + n : n; + let d = new Date(message.time); + let dateStr = [d.getFullYear(), "-", z(d.getMonth() + 1), "-", + z(d.getDate()), " ", z(d.getHours()), ":", z(d.getMinutes()), ":", + z(d.getSeconds())].join(""); + + return dateStr + "\t" + pad(message.loggerName) + " " + message.levelDesc + + "\t" + message.message + "\n"; + } +}; + +/* + * Appenders + * These can be attached to Loggers to log to different places + * Simply subclass and override doAppend to implement a new one + */ + +function Appender(formatter) { + this._name = "Appender"; + this._formatter = formatter? formatter : new BasicFormatter(); +} +Appender.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + _level: Log4Moz.Level.All, + get level() { return this._level; }, + set level(level) { this._level = level; }, + + append: function App_append(message) { + if(this._level <= message.level) + this.doAppend(this._formatter.format(message)); + }, + toString: function App_toString() { + return this._name + " [level=" + this._level + + ", formatter=" + this._formatter + "]"; + }, + doAppend: function App_doAppend(message) {} +}; + +/* + * DumpAppender + * Logs to standard out + */ + +function DumpAppender(formatter) { + this._name = "DumpAppender"; + this._formatter = formatter? formatter : new BasicFormatter(); +} +DumpAppender.prototype = { + __proto__: Appender.prototype, + + doAppend: function DApp_doAppend(message) { + dump(message); + } +}; + +/* + * ConsoleAppender + * Logs to the javascript console + */ + +function ConsoleAppender(formatter) { + this._name = "ConsoleAppender"; + this._formatter = formatter; +} +ConsoleAppender.prototype = { + __proto__: Appender.prototype, + + doAppend: function CApp_doAppend(message) { + if (message.level > Log4Moz.Level.Warn) { + Cu.reportError(message); + return; + } + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService).logStringMessage(message); + } +}; + +/* + * FileAppender + * Logs to a file + */ + +function FileAppender(file, formatter) { + this._name = "FileAppender"; + this._file = file; // nsIFile + this._formatter = formatter? formatter : new BasicFormatter(); +} +FileAppender.prototype = { + __proto__: Appender.prototype, + __fos: null, + get _fos() { + if (!this.__fos) + this.openStream(); + return this.__fos; + }, + + openStream: function FApp_openStream() { + try { + let __fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND; + __fos.init(this._file, flags, PERMS_FILE, 0); + + this.__fos = Cc["@mozilla.org/intl/converter-output-stream;1"] + .createInstance(Ci.nsIConverterOutputStream); + this.__fos.init(__fos, "UTF-8", 4096, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + } catch(e) { + dump("Error opening stream:\n" + e); + } + }, + + closeStream: function FApp_closeStream() { + if (!this.__fos) + return; + try { + this.__fos.close(); + this.__fos = null; + } catch(e) { + dump("Failed to close file output stream\n" + e); + } + }, + + doAppend: function FApp_doAppend(message) { + if (message === null || message.length <= 0) + return; + try { + this._fos.writeString(message); + } catch(e) { + dump("Error writing file:\n" + e); + } + }, + + clear: function FApp_clear() { + this.closeStream(); + try { + this._file.remove(false); + } catch (e) { + // XXX do something? + } + } +}; + +/* + * RotatingFileAppender + * Similar to FileAppender, but rotates logs when they become too large + */ + +function RotatingFileAppender(file, formatter, maxSize, maxBackups) { + if (maxSize === undefined) + maxSize = ONE_MEGABYTE * 2; + + if (maxBackups === undefined) + maxBackups = 0; + + this._name = "RotatingFileAppender"; + this._file = file; // nsIFile + this._formatter = formatter? formatter : new BasicFormatter(); + this._maxSize = maxSize; + this._maxBackups = maxBackups; +} +RotatingFileAppender.prototype = { + __proto__: FileAppender.prototype, + + doAppend: function RFApp_doAppend(message) { + if (message === null || message.length <= 0) + return; + try { + this.rotateLogs(); + FileAppender.prototype.doAppend.call(this, message); + } catch(e) { + dump("Error writing file:" + e + "\n"); + } + }, + + rotateLogs: function RFApp_rotateLogs() { + if(this._file.exists() && + this._file.fileSize < this._maxSize) + return; + + this.closeStream(); + + for (let i = this.maxBackups - 1; i > 0; i--){ + let backup = this._file.parent.clone(); + backup.append(this._file.leafName + "." + i); + if (backup.exists()) + backup.moveTo(this._file.parent, this._file.leafName + "." + (i + 1)); + } + + let cur = this._file.clone(); + if (cur.exists()) + cur.moveTo(cur.parent, cur.leafName + ".1"); + + // Note: this._file still points to the same file + } +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/metadata.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/metadata.js new file mode 100644 index 000000000000..a3e4a71eebb8 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/metadata.js @@ -0,0 +1,175 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = ["MetadataCollector"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://testpilot/modules/string_sanitizer.js"); + +const LOCALE_PREF = "general.useragent.locale"; +const EXTENSION_ID = "testpilot@labs.mozilla.com"; +const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; +const PREFIX_ITEM_URI = "urn:mozilla:item:"; + +/* The following preference, if present, stores answers to the basic panel + * survey, which tell us user's general tech level, and so should be included + * with any upload.*/ +const SURVEY_ANS = "extensions.testpilot.surveyAnswers.basic_panel_survey_2"; + +let Application = Cc["@mozilla.org/fuel/application;1"] + .getService(Ci.fuelIApplication); + +// This function copied over from Weave: +function Weave_sha1(string) { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA1); + + let data = converter.convertToByteArray(string, {}); + hasher.update(data, data.length); + let rawHash = hasher.finish(false); + + // return the two-digit hexadecimal code for a byte + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join(""); + return hash; +} + +let MetadataCollector = { + // Collects metadata such as what country you're in, what extensions you have installed, etc. + getExtensions: function MetadataCollector_getExtensions(callback) { + //http://lxr.mozilla.org/aviarybranch/source/toolkit/mozapps/extensions/public/nsIExtensionManager.idl + //http://lxr.mozilla.org/aviarybranch/source/toolkit/mozapps/update/public/nsIUpdateService.idl#45 + let myExtensions = []; + if (Application.extensions) { + for each (let ex in Application.extensions.all) { + myExtensions.push({ id: Weave_sha1(ex.id), isEnabled: ex.enabled }); + } + callback(myExtensions); + } else { + Application.getExtensions(function(extensions) { + for each (let ex in extensions.all) { + myExtensions.push({ id: Weave_sha1(ex.id), isEnabled: ex.enabled }); + } + callback(myExtensions); + }); + } + }, + + getAccessibilities : function MetadataCollector_getAccessibilities() { + let prefs = + Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService); + let branch = prefs.getBranch("accessibility."); + let accessibilities = []; + let children = branch.getChildList("", {}); + let length = children.length; + let prefName; + let prefValue; + + for (let i = 0; i < length; i++) { + prefName = "accessibility." + children[i]; + prefValue = + Application.prefs.getValue(prefName, ""); + accessibilities.push({ name: prefName, value: prefValue }); + } + + return accessibilities; + }, + + getLocation: function MetadataCollector_getLocation() { + //navitagor.geolocation; // or nsIDOMGeoGeolocation + // we don't want the lat/long, we just want the country + + return Application.prefs.getValue(LOCALE_PREF, ""); + }, + + getVersion: function MetadataCollector_getVersion() { + return Application.version; + }, + + getOperatingSystem: function MetadataCollector_getOSVersion() { + let oscpu = Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu; + let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; + return os + " " + oscpu; + }, + + getSurveyAnswers: function MetadataCollector_getSurveyAnswers() { + let answers = Application.prefs.getValue(SURVEY_ANS, ""); + if (answers == "") { + return ""; + } else { + return sanitizeJSONStrings( JSON.parse(answers) ); + } + }, + + getTestPilotVersion: function MetadataCollector_getTPVersion(callback) { + // Application.extensions is undefined if we're in Firefox 4. + if (Application.extensions) { + callback(Application.extensions.get(EXTENSION_ID).version); + } else { + Application.getExtensions(function(extensions) { + callback(extensions.get(EXTENSION_ID).version); + }); + } + }, + + getMetadata: function MetadataCollector_getMetadata(callback) { + let self = this; + self.getTestPilotVersion(function(tpVersion) { + self.getExtensions(function(extensions) { + callback({ extensions: extensions, + accessibilities: self.getAccessibilities(), + location: self.getLocation(), + fxVersion: self.getVersion(), + operatingSystem: self.getOperatingSystem(), + tpVersion: tpVersion, + surveyAnswers: self.getSurveyAnswers()} + ); + }); + }); + } + // TODO if we make a GUID for the user, we keep it here. +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/remote-experiment-loader.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/remote-experiment-loader.js new file mode 100644 index 000000000000..fa8075367031 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/remote-experiment-loader.js @@ -0,0 +1,339 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const BASE_URL_PREF = "extensions.testpilot.indexBaseURL"; +var Cuddlefish = require("cuddlefish"); +var resolveUrl = require("url").resolve; +var SecurableModule = require("securable-module"); +let JarStore = require("jar-code-store").JarStore; + +/* Security info should look like this: + * Security Info: + Security state: secure + Security description: Authenticated by Equifax + Security error message: null + +Certificate Status: + Verification: OK + Common name (CN) = *.mozillalabs.com + Organisation = Mozilla Corporation + Issuer = Equifax + SHA1 fingerprint = E5:CD:91:97:08:E6:88:F2:A2:AE:31:3C:F9:91:8D:14:33:07:C4:EE + Valid from 8/12/09 14:04:39 + Valid until 8/14/11 3:27:26 + */ + +function verifyChannelSecurity(channel) { + // http://mdn.beonex.com/En/How_to_check_the_security_state_of_an_XMLHTTPRequest_over_SSL + // Expect channel to have security state = secure, CN = *.mozillalabs.com, + // Organization = "Mozilla Corporation", verification = OK. + console.info("Verifying SSL channel security info before download..."); + + try { + if (! channel instanceof Ci.nsIChannel) { + console.warn("Not a channel. This should never happen."); + return false; + } + let secInfo = channel.securityInfo; + + if (secInfo instanceof Ci.nsITransportSecurityInfo) { + secInfo.QueryInterface(Ci.nsITransportSecurityInfo); + let secState = secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_SECURE; + if (secState != Ci.nsIWebProgressListener.STATE_IS_SECURE) { + console.warn("Failing security check: Security state is not secure."); + return false; + } + } else { + console.warn("Failing secuity check: No TransportSecurityInfo."); + return false; + } + + // check SSL certificate details + if (secInfo instanceof Ci.nsISSLStatusProvider) { + let cert = secInfo.QueryInterface(Ci.nsISSLStatusProvider). + SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert; + + let verificationResult = cert.verifyForUsage( + Ci.nsIX509Cert.CERT_USAGE_SSLServer); + if (verificationResult != Ci.nsIX509Cert.VERIFIED_OK) { + console.warn("Failing security check: Cert not verified OK."); + return false; + } + if (cert.commonName != "*.mozillalabs.com") { + console.warn("Failing security check: Cert not for *.mozillalabs.com"); + return false; + } + if (cert.organization != "Mozilla Corporation") { + console.warn("Failing security check: Cert not for Mozilla corporation."); + return false; + } + } else { + console.warn("Failing security check: No SSL cert info."); + return false; + } + + // Passed everything + console.info("Channel passed SSL security check."); + return true; + } catch(err) { + console.warn("Failing security check: Error: " + err); + return false; + } +} + +function downloadFile(url, cb, lastModified) { + // lastModified is a timestamp (ms since epoch); if provided, then the file + // will not be downloaded unless it is newer than this. + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance( Ci.nsIXMLHttpRequest ); + req.open('GET', url, true); + if (lastModified != undefined) { + let d = new Date(); + d.setTime(lastModified); + // example header: If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT + req.setRequestHeader("If-Modified-Since", d.toGMTString()); + console.info("Setting if-modified-since header to " + d.toGMTString()); + } + //Use binary mode to download jars TODO find a better switch + if (url.indexOf(".jar") == url.length - 4) { + console.info("Using binary mode to download jar file."); + req.overrideMimeType('text/plain; charset=x-user-defined'); + } + req.onreadystatechange = function(aEvt) { + if (req.readyState == 4) { + if (req.status == 200) { + // check security channel: + if (verifyChannelSecurity(req.channel)) { + cb(req.responseText); + } else { + cb(null); + } + } else if (req.status == 304) { + // 304 is "Not Modified", which we can get because we send an + // If-Modified-Since header. + console.info("File " + url + " not modified; using cached version."); + cb(null); + // calling back with null lets the RemoteExperimentLoader know it should + // keep using the old cached version of the code. + } else { + // Some kind of error. + console.warn("Got a " + req.status + " error code downloading " + url); + cb(null); + } + } + }; + req.send(); +} + +// example contents of extensions.testpilot.experiment.codeFs: +// {'fs': {"bookmark01/experiment": ""}} +// sample code + // example data: + // {'experiments': [{'name': 'Bookmark Experiment', + // 'filename': 'bookmarks.js'}]} + +exports.RemoteExperimentLoader = function(logRepo, fileGetterFunction ) { + /* fileGetterFunction is an optional stub function for unit testing. Pass in + * nothing to have it use the default behavior of downloading the files from the + * Test Pilot server. FileGetterFunction must take (url, callback).*/ + this._init(logRepo, fileGetterFunction); +}; + +exports.RemoteExperimentLoader.prototype = { + _init: function(logRepo, fileGetterFunction) { + this._logger = logRepo.getLogger("TestPilot.Loader"); + this._expLogger = logRepo.getLogger("TestPilot.RemoteCode"); + this._studyResults = []; + this._legacyStudies = []; + let prefs = require("preferences-service"); + this._baseUrl = prefs.get(BASE_URL_PREF, ""); + if (fileGetterFunction != undefined) { + this._fileGetter = fileGetterFunction; + } else { + this._fileGetter = downloadFile; + } + this._logger.trace("About to instantiate preferences store."); + this._jarStore = new JarStore(); + this._experimentFileNames = []; + let self = this; + this._logger.trace("About to instantiate cuddlefish loader."); + this._refreshLoader(); + // set up the unloading + require("unload").when( function() { + self._loader.unload(); + }); + this._logger.trace("Done instantiating remoteExperimentLoader."); + }, + + _refreshLoader: function() { + if (this._loader) { + this._loader.unload(); + } + /* Pass in "TestPilot.experiment" logger as the console object for + * all remote modules loaded through cuddlefish, so they will log their + * stuff to the same file as all other modules. This logger is not + * technically a console object but as long as it has .debug, .info, + * .warn, and .error methods, it will work fine.*/ + + /* Use a composite file system here, compositing codeStorage and a new + * local file system so that securable modules loaded remotely can + * themselves require modules in the cuddlefish lib. */ + let self = this; + this._loader = Cuddlefish.Loader( + {fs: new SecurableModule.CompositeFileSystem( + [self._jarStore, Cuddlefish.parentLoader.fs]), + console: this._expLogger + }); + }, + + checkForUpdates: function(callback) { + /* Callback will be called with true or false + * to let us know whether there are any updates, so that client code can + * restart any experiment whose code has changed. */ + let prefs = require("preferences-service"); + let indexFileName = prefs.get("extensions.testpilot.indexFileName", + "index.json"); + let self = this; + // Unload everything before checking for updates, to be sure we + // get the newest stuff. + this._logger.info("Unloading everything to prepare to check for updates."); + this._refreshLoader(); + + // Check for surveys and studies + let url = resolveUrl(self._baseUrl, indexFileName); + self._fileGetter(url, function onDone(data) { + if (data) { + try { + data = JSON.parse(data); + } catch (e) { + self._logger.warn("Error parsing index.json: " + e ); + callback(false); + return; + } + + // Cache study results... + self._studyResults = data.results; + self._legacyStudies = data.legacy; + + /* Go through each file indicated in index.json, attempt to load it into + * codeStorage (replacing any older version there). + */ + let jarFiles = data.experiment_jars; + let numFilesToDload = jarFiles.length; + + for each (let j in jarFiles) { + let filename = j.jarfile; + let hash = j.hash; + if (j.studyfile) { + self._experimentFileNames.push(j.studyfile); + } + self._logger.trace("I'm gonna go try to get the code for " + filename); + let modDate = self._jarStore.getFileModifiedDate(filename); + + self._fileGetter(resolveUrl(self._baseUrl, filename), + function onDone(code) { + // code will be non-null if there is actually new code to download. + if (code) { + self._logger.info("Downloaded jar file " + filename); + self._jarStore.saveJarFile(filename, code, hash); + self._logger.trace("Saved code for: " + filename); + } else { + self._logger.info("Nothing to download for " + filename); + } + numFilesToDload--; + if (numFilesToDload == 0) { + self._logger.trace("Calling callback."); + callback(true); + } + }, modDate); + } + + } else { + self._logger.warn("Could not download index.json from test pilot server."); + callback(false); + } + }); + }, + + getExperiments: function() { + /* Load up and return all studies/surveys (not libraries) + * already stored in codeStorage. Returns a dict with key = + * the module name and value = the module object. */ + this._logger.trace("GetExperiments called."); + let remoteExperiments = {}; + for each (filename in this._experimentFileNames) { + this._logger.debug("GetExperiments is loading " + filename); + try { + remoteExperiments[filename] = this._loader.require(filename); + this._logger.info("Loaded " + filename + " OK."); + } catch(e) { + this._logger.warn("Error loading " + filename); + this._logger.warn(e); + } + } + return remoteExperiments; + }, + + getStudyResults: function() { + return this._studyResults; + }, + + getLegacyStudies: function() { + return this._legacyStudies; + } +}; + +// TODO purge the pref store of anybody who has one. + +// TODO i realized that right now there is no way for experiments +// on disk to get loaded if the index file is not accessible for +// any reason. getExperiments needs to be able to return names of +// experiment modules on disk even if connection to server fails. But +// we can't just load everything; some modules in the jar are not +// experiments. Right now the information as to which modules are +// experiments lives ONLY in index.json. What if we put it into the .jar +// file itself somehow? Like calling one of the files "study.js". Or +// "survey.js" Hey, that would be neat - one .jar file containing both +// the study.js and the survey.js. Or there could be a mini-manifest in the +// jar telling which files are experiments. + +// TODO Also, if user has a study id foo that is not expired yet, and +// a LegacyStudy appears with the same id, they should keep their "real" +// version of id foo and not load the LegacyStudy version. + +// TODO but once the study is expired, should delete the jar for it and +// just load the LegacyStudy version. + diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/setup.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/setup.js new file mode 100644 index 000000000000..7ddec9292d23 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/setup.js @@ -0,0 +1,858 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Atul Varma + * Jono X + * Raymond Lee + * Jorge Villalobos + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = ["TestPilotSetup", "POPUP_SHOW_ON_NEW", + "POPUP_SHOW_ON_FINISH", "POPUP_SHOW_ON_RESULTS", + "ALWAYS_SUBMIT_DATA", "RUN_AT_ALL_PREF"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const EXTENSION_ID = "testpilot@labs.mozilla.com"; +const VERSION_PREF ="extensions.testpilot.lastversion"; +const FIRST_RUN_PREF ="extensions.testpilot.firstRunUrl"; +const RUN_AT_ALL_PREF = "extensions.testpilot.runStudies"; +const POPUP_SHOW_ON_NEW = "extensions.testpilot.popup.showOnNewStudy"; +const POPUP_SHOW_ON_FINISH = "extensions.testpilot.popup.showOnStudyFinished"; +const POPUP_SHOW_ON_RESULTS = "extensions.testpilot.popup.showOnNewResults"; +const POPUP_CHECK_INTERVAL = "extensions.testpilot.popup.delayAfterStartup"; +const POPUP_REMINDER_INTERVAL = "extensions.testpilot.popup.timeBetweenChecks"; +const ALWAYS_SUBMIT_DATA = "extensions.testpilot.alwaysSubmitData"; +const LOG_FILE_NAME = "TestPilotErrorLog.log"; + +let TestPilotSetup = { + didReminderAfterStartup: false, + startupComplete: false, + _shortTimer: null, + _longTimer: null, + _remoteExperimentLoader: null, + taskList: [], + version: "", + + // Lazy initializers: + __application: null, + get _application() { + if (this.__application == null) { + this.__application = Cc["@mozilla.org/fuel/application;1"] + .getService(Ci.fuelIApplication); + } + return this.__application; + }, + + get _prefs() { + return this._application.prefs; + }, + + __loader: null, + get _loader() { + if (this.__loader == null) { + let Cuddlefish = {}; + Components.utils.import("resource://testpilot/modules/lib/cuddlefish.js", + Cuddlefish); + let repo = this._logRepo; + this.__loader = new Cuddlefish.Loader( + {rootPaths: ["resource://testpilot/modules/", + "resource://testpilot/modules/lib/"], + console: repo.getLogger("TestPilot.Loader") + }); + } + return this.__loader; + }, + + __feedbackManager: null, + get _feedbackManager() { + if (this.__feedbackManager == null) { + let FeedbackModule = {}; + Cu.import("resource://testpilot/modules/feedback.js", FeedbackModule); + this.__feedbackManager = FeedbackModule.FeedbackManager; + } + return this.__feedbackManager; + }, + + __dataStoreModule: null, + get _dataStoreModule() { + if (this.__dataStoreModule == null) { + this.__dataStoreModule = {}; + Cu.import("resource://testpilot/modules/experiment_data_store.js", + this._dataStoreModule); + } + return this.__dataStoreModule; + }, + + __extensionUpdater: null, + get _extensionUpdater() { + if (this.__extensionUpdater == null) { + let ExUpdate = {}; + Cu.import("resource://testpilot/modules/extension-update.js", + ExUpdate); + this.__extensionUpdater = ExUpdate.TestPilotExtensionUpdate; + } + return this.__extensionUpdater; + }, + + __logRepo: null, + get _logRepo() { + // Note: This hits the disk so it's an expensive operation; don't call it + // on startup. + if (this.__logRepo == null) { + let Log4MozModule = {}; + Cu.import("resource://testpilot/modules/log4moz.js", Log4MozModule); + let props = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + let logFile = props.get("ProfD", Components.interfaces.nsIFile); + logFile.append(LOG_FILE_NAME); + let formatter = new Log4MozModule.Log4Moz.BasicFormatter; + let root = Log4MozModule.Log4Moz.repository.rootLogger; + root.level = Log4MozModule.Log4Moz.Level["All"]; + let appender = new Log4MozModule.Log4Moz.RotatingFileAppender(logFile, formatter); + root.addAppender(appender); + this.__logRepo = Log4MozModule.Log4Moz.repository; + } + return this.__logRepo; + }, + + __logger: null, + get _logger() { + if (this.__logger == null) { + this.__logger = this._logRepo.getLogger("TestPilot.Setup"); + } + return this.__logger; + }, + + __taskModule: null, + get _taskModule() { + if (this.__taskModule == null) { + this.__taskModule = {}; + Cu.import("resource://testpilot/modules/tasks.js", this.__taskModule); + } + return this.__taskModule; + }, + + __stringBundle: null, + get _stringBundle() { + if (this.__stringBundle == null) { + this.__stringBundle = + Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle("chrome://testpilot/locale/main.properties"); + } + return this.__stringBundle; + }, + + __obs: null, + get _obs() { + if (this.__obs == null) { + this.__obs = this._loader.require("observer-service"); + } + return this.__obs; + }, + + _isFfx4BetaVersion: function TPS__isFfx4BetaVersion() { + let result = Cc["@mozilla.org/xpcom/version-comparator;1"] + .getService(Ci.nsIVersionComparator) + .compare("3.7a1pre", this._application.version); + if (result < 0) { + return true; + } else { + return false; + } + }, + + _setPrefDefaultsForVersion: function TPS__setPrefDefaultsForVersion() { + /* A couple of preferences need different default values depending on + * whether we're in the Firefox 4 beta version or the standalone TP version + */ + let ps = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService); + let prefBranch = ps.getDefaultBranch(""); + /* note we're setting default values, not current values -- these + * get overridden by any user set values. */ + if (this._isFfx4BetaVersion()) { + prefBranch.setBoolPref(POPUP_SHOW_ON_NEW, true); + prefBranch.setIntPref(POPUP_CHECK_INTERVAL, 600000); + } else { + prefBranch.setBoolPref(POPUP_SHOW_ON_NEW, false); + prefBranch.setIntPref(POPUP_CHECK_INTERVAL, 180000); + } + }, + + _setUpToolbarFeedbackButton: function TPS_toolbarFeedbackButton() { + /* If this is first run, and it's ffx4 beta version, and the feedback + * button is not in the expected place, put it there! + * (copied from MozReporterButtons extension) */ + let logger = this._logger; + try { + let win = this._getFrontBrowserWindow(); + let firefoxnav = win.document.getElementById("nav-bar"); + let curSet = firefoxnav.currentSet; + + if (-1 == curSet.indexOf("feedback-menu-button")) { + logger.info("Feedback toolbar button not present: Adding it."); + // place the buttons after the search box. + let newSet = curSet + ",feedback-menu-button"; + + firefoxnav.setAttribute("currentset", newSet); + firefoxnav.currentSet = newSet; + win.document.persist("nav-bar", "currentset"); + // if you don't do the following call, funny things happen. + try { + BrowserToolboxCustomizeDone(true); + } catch (e) { + } + } + } catch (e) { + logger.warn("Error in setUpToolbarFeedbackButton: " + e); + } + }, + + globalStartup: function TPS__doGlobalSetup() { + // Only ever run this stuff ONCE, on the first window restore. + // Should get called by the Test Pilot component. + let logger = this._logger; + logger.trace("TestPilotSetup.globalStartup was called."); + + try { + this._setPrefDefaultsForVersion(); + if (!this._prefs.getValue(RUN_AT_ALL_PREF, true)) { + logger.trace("Test Pilot globally disabled: Not starting up."); + return; + } + + // Set up observation for task state changes + var self = this; + this._obs.add("testpilot:task:changed", this.onTaskStatusChanged, self); + this._obs.add( + "testpilot:task:dataAutoSubmitted", this._onTaskDataAutoSubmitted, self); + // Set up observation for application shutdown. + this._obs.add("quit-application", this.globalShutdown, self); + // Set up observation for enter/exit private browsing: + this._obs.add("private-browsing", this.onPrivateBrowsingMode, self); + + // Set up timers to remind user x minutes after startup + // and once per day thereafter. Use nsITimer so it doesn't belong to + // any one window. + logger.trace("Setting interval for showing reminders..."); + + this._shortTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._shortTimer.initWithCallback( + { notify: function(timer) { self._doHousekeeping();} }, + this._prefs.getValue(POPUP_CHECK_INTERVAL, 180000), + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + this._longTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._longTimer.initWithCallback( + { notify: function(timer) { + self.reloadRemoteExperiments(function() { + self._notifyUserOfTasks(); + }); + }}, this._prefs.getValue(POPUP_REMINDER_INTERVAL, 86400000), + Ci.nsITimer.TYPE_REPEATING_SLACK); + + this.getVersion(function() { + // Show first run page (in front window) if newly installed or upgraded. + let currVersion = self._prefs.getValue(VERSION_PREF, "firstrun"); + + if (currVersion != self.version) { + if(!self._isFfx4BetaVersion()) { + self._prefs.setValue(VERSION_PREF, self.version); + let browser = self._getFrontBrowserWindow().getBrowser(); + let url = self._prefs.getValue(FIRST_RUN_PREF, ""); + let tab = browser.addTab(url); + browser.selectedTab = tab; + } else { + // Don't show first run page in ffx4 beta version... but do + // set up the Feedback button in the toolbar. + self._setUpToolbarFeedbackButton(); + } + } + + // Install tasks. (This requires knowing the version, so it is + // inside the callback from getVersion.) + self.checkForTasks(function() { + /* Callback to complete startup after we finish + * checking for tasks. */ + self.startupComplete = true; + logger.trace("I'm in the callback from checkForTasks."); + // Send startup message to each task: + for (let i = 0; i < self.taskList.length; i++) { + self.taskList[i].onAppStartup(); + } + self._obs.notify("testpilot:startup:complete", "", null); + /* onWindowLoad gets called once for each window, + * but only after we fire this notification. */ + logger.trace("Testpilot startup complete."); + }); + }); + } catch(e) { + logger.error("Error in testPilot startup: " + e); + } + }, + + globalShutdown: function TPS_globalShutdown() { + let logger = this._logger; + logger.trace("Global shutdown. Unregistering everything."); + let self = this; + for (let i = 0; i < self.taskList.length; i++) { + self.taskList[i].onAppShutdown(); + self.taskList[i].onExperimentShutdown(); + } + this.taskList = []; + this._loader.unload(); + this._obs.remove("testpilot:task:changed", this.onTaskStatusChanged, self); + this._obs.remove( + "testpilot:task:dataAutoSubmitted", this._onTaskDataAutoSubmitted, self); + this._obs.remove("quit-application", this.globalShutdown, self); + this._obs.remove("private-browsing", this.onPrivateBrowsingMode, self); + this._loader.unload(); + this._shortTimer.cancel(); + this._longTimer.cancel(); + logger.trace("Done unregistering everything."); + }, + + _getFrontBrowserWindow: function TPS__getFrontWindow() { + let wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + // TODO Is "most recent" the same as "front"? + return wm.getMostRecentWindow("navigator:browser"); + }, + + onPrivateBrowsingMode: function TPS_onPrivateBrowsingMode(topic, data) { + for (let i = 0; i < this.taskList.length; i++) { + if (data == "enter") { + this.taskList[i].onEnterPrivateBrowsing(); + } else if (data == "exit") { + this.taskList[i].onExitPrivateBrowsing(); + } + } + }, + + onWindowUnload: function TPS__onWindowRegistered(window) { + this._logger.trace("Called TestPilotSetup.onWindow unload!"); + for (let i = 0; i < this.taskList.length; i++) { + this.taskList[i].onWindowClosed(window); + } + }, + + onWindowLoad: function TPS_onWindowLoad(window) { + this._logger.trace("Called TestPilotSetup.onWindowLoad!"); + // Run this stuff once per window... + let self = this; + + // Register listener for URL loads, that will notify all tasks about + // new page: + let appcontent = window.document.getElementById("appcontent"); + if (appcontent) { + appcontent.addEventListener("DOMContentLoaded", function(event) { + let newUrl = event.originalTarget.URL; + self._feedbackManager.fillInFeedbackPage(newUrl, window); + for (i = 0; i < self.taskList.length; i++) { + self.taskList[i].onUrlLoad(newUrl, event); + } + }, true); + } + + // Let each task know about the new window. + for (let i = 0; i < this.taskList.length; i++) { + this.taskList[i].onNewWindow(window); + } + }, + + addTask: function TPS_addTask(testPilotTask) { + // TODO raise some kind of exception if a task with the same ID already + // exists. No excuse to ever be running two copies of the same task. + this.taskList.push(testPilotTask); + }, + + _showNotification: function TPS__showNotification(task, fragile, text, title, + iconClass, showSubmit, + showAlwaysSubmitCheckbox, + linkText, linkUrl, + isExtensionUpdate) { + // If there are multiple windows, show notifications in the frontmost + // window. + let doc = this._getFrontBrowserWindow().document; + let popup = doc.getElementById("pilot-notification-popup"); + + let anchor; + if (this._isFfx4BetaVersion()) { + /* If we're in the Ffx4Beta version, popups come down from feedback + * button, but if we're in the standalone extension version, they + * come up from status bar icon. */ + anchor = doc.getElementById("feedback-menu-button"); + popup.setAttribute("class", "tail-up"); + } else { + anchor = doc.getElementById("pilot-notifications-button"); + popup.setAttribute("class", "tail-down"); + } + let textLabel = doc.getElementById("pilot-notification-text"); + let titleLabel = doc.getElementById("pilot-notification-title"); + let icon = doc.getElementById("pilot-notification-icon"); + let submitBtn = doc.getElementById("pilot-notification-submit"); + let closeBtn = doc.getElementById("pilot-notification-close"); + let link = doc.getElementById("pilot-notification-link"); + let alwaysSubmitCheckbox = + doc.getElementById("pilot-notification-always-submit-checkbox"); + let self = this; + + // Set all appropriate attributes on popup: + if (isExtensionUpdate) { + popup.setAttribute("tpisextensionupdate", "true"); + } + popup.setAttribute("noautohide", !fragile); + titleLabel.setAttribute("value", title); + while (textLabel.lastChild) { + textLabel.removeChild(textLabel.lastChild); + } + textLabel.appendChild(doc.createTextNode(text)); + if (iconClass) { + // css will set the image url based on the class. + icon.setAttribute("class", iconClass); + } + + alwaysSubmitCheckbox.setAttribute("hidden", !showAlwaysSubmitCheckbox); + if (showSubmit) { + if (isExtensionUpdate) { + submitBtn.setAttribute("label", + this._stringBundle.GetStringFromName( + "testpilot.notification.update")); + submitBtn.onclick = function() { + this._extensionUpdater.check(EXTENSION_ID); + self._hideNotification(); + }; + } else { + submitBtn.setAttribute("label", + this._stringBundle.GetStringFromName("testpilot.submit")); + // Functionality for submit button: + submitBtn.onclick = function() { + self._hideNotification(); + if (showAlwaysSubmitCheckbox && alwaysSubmitCheckbox.checked) { + self._prefs.setValue(ALWAYS_SUBMIT_DATA, true); + } + task.upload( function(success) { + if (success) { + self._showNotification( + task, true, + self._stringBundle.GetStringFromName( + "testpilot.notification.thankYouForUploadingData.message"), + self._stringBundle.GetStringFromName( + "testpilot.notification.thankYouForUploadingData"), + "study-submitted", false, false, + self._stringBundle.GetStringFromName("testpilot.moreInfo"), + task.defaultUrl); + } else { + // TODO any point in showing an error message here? + } + }); + }; + } + } + submitBtn.setAttribute("hidden", !showSubmit); + + // Create the link if specified: + if (linkText && (linkUrl || task)) { + link.setAttribute("value", linkText); + link.setAttribute("class", "notification-link"); + link.onclick = function(event) { + if (event.button == 0) { + if (task) { + task.loadPage(); + } else { + self._openChromeless(linkUrl); + } + self._hideNotification(); + } + }; + link.setAttribute("hidden", false); + } else { + link.setAttribute("hidden", true); + } + + closeBtn.onclick = function() { + self._hideNotification(); + }; + + // Show the popup: + popup.hidden = false; + popup.setAttribute("open", "true"); + popup.openPopup( anchor, "after_end"); + }, + + _openChromeless: function TPS__openChromeless(url) { + let window = this._getFrontBrowserWindow(); + window.TestPilotWindowUtils.openChromeless(url); + }, + + _hideNotification: function TPS__hideNotification() { + let window = this._getFrontBrowserWindow(); + let popup = window.document.getElementById("pilot-notification-popup"); + popup.hidden = true; + popup.setAttribute("open", "false"); + popup.removeAttribute("tpisextensionupdate"); + popup.hidePopup(); + }, + + _isShowingUpdateNotification : function() { + let window = this._getFrontBrowserWindow(); + let popup = window.document.getElementById("pilot-notification-popup"); + + return popup.hasAttribute("tpisextensionupdate"); + }, + + _notifyUserOfTasks: function TPS__notifyUser() { + // Check whether there are tasks needing attention, and if any are + // found, show the popup door-hanger thingy. + let i, task; + let TaskConstants = this._taskModule.TaskConstants; + + // if showing extension update notification, don't do anything. + if (this._isShowingUpdateNotification()) { + return; + } + + // Highest priority is if there is a finished test (needs a decision) + if (this._prefs.getValue(POPUP_SHOW_ON_FINISH, false)) { + for (i = 0; i < this.taskList.length; i++) { + task = this.taskList[i]; + if (task.status == TaskConstants.STATUS_FINISHED) { + if (!this._prefs.getValue(ALWAYS_SUBMIT_DATA, false)) { + this._showNotification( + task, false, + this._stringBundle.formatStringFromName( + "testpilot.notification.readyToSubmit.message", [task.title], + 1), + this._stringBundle.GetStringFromName( + "testpilot.notification.readyToSubmit"), + "study-finished", true, true, + this._stringBundle.GetStringFromName("testpilot.moreInfo"), + task.defaultUrl); + // We return after showing something, because it only makes + // sense to show one notification at a time! + return; + } + } + } + } + + // If there's no finished test, next highest priority is new tests that + // are starting... + if (this._prefs.getValue(POPUP_SHOW_ON_NEW, false)) { + for (i = 0; i < this.taskList.length; i++) { + task = this.taskList[i]; + if (task.status == TaskConstants.STATUS_STARTING || + task.status == TaskConstants.STATUS_NEW) { + if (task.taskType == TaskConstants.TYPE_EXPERIMENT) { + this._showNotification( + task, true, + this._stringBundle.formatStringFromName( + "testpilot.notification.newTestPilotStudy.message", + [task.title], 1), + this._stringBundle.GetStringFromName( + "testpilot.notification.newTestPilotStudy"), + "new-study", false, false, + this._stringBundle.GetStringFromName("testpilot.moreInfo"), + task.defaultUrl); + // Having shown the notification, update task status so that this + // notification won't be shown again. + task.changeStatus(TaskConstants.STATUS_IN_PROGRESS, true); + return; + } else if (task.taskType == TaskConstants.TYPE_SURVEY) { + this._showNotification( + task, true, + this._stringBundle.formatStringFromName( + "testpilot.notification.newTestPilotSurvey.message", + [task.title], 1), + this._stringBundle.GetStringFromName( + "testpilot.notification.newTestPilotSurvey"), + "new-study", false, false, + this._stringBundle.GetStringFromName("testpilot.moreInfo"), + task.defaultUrl); + task.changeStatus(TaskConstants.STATUS_IN_PROGRESS, true); + return; + } + } + } + } + + // And finally, new experiment results: + if (this._prefs.getValue(POPUP_SHOW_ON_RESULTS, false)) { + for (i = 0; i < this.taskList.length; i++) { + task = this.taskList[i]; + if (task.taskType == TaskConstants.TYPE_RESULTS && + task.status == TaskConstants.STATUS_NEW) { + this._showNotification( + task, true, + this._stringBundle.formatStringFromName( + "testpilot.notification.newTestPilotResults.message", + [task.title], 1), + this._stringBundle.GetStringFromName( + "testpilot.notification.newTestPilotResults"), + "new-results", false, false, + this._stringBundle.GetStringFromName("testpilot.moreInfo"), + task.defaultUrl); + // Having shown the notification, advance the status of the + // results, so that this notification won't be shown again + task.changeStatus(TaskConstants.STATUS_ARCHIVED, true); + return; + } + } + } + }, + + _doHousekeeping: function TPS__doHousekeeping() { + // check date on all tasks: + for (let i = 0; i < this.taskList.length; i++) { + let task = this.taskList[i]; + task.checkDate(); + } + // Do a full reminder -- but at most once per browser session + if (!this.didReminderAfterStartup) { + this._logger.trace("Doing reminder after startup..."); + this.didReminderAfterStartup = true; + this._notifyUserOfTasks(); + } + }, + + onTaskStatusChanged: function TPS_onTaskStatusChanged() { + this._notifyUserOfTasks(); + }, + + _onTaskDataAutoSubmitted: function(subject, data) { + this._showNotification( + subject, true, + this._stringBundle.formatStringFromName( + "testpilot.notification.autoUploadedData.message", + [subject.title], 1), + this._stringBundle.GetStringFromName( + "testpilot.notification.autoUploadedData"), + "study-submitted", false, false, + this._stringBundle.GetStringFromName("testpilot.moreInfo"), + subject.defaultUrl); + }, + + getVersion: function TPS_getVersion(callback) { + // Application.extensions undefined in Firefox 4; will use the new + // asynchrounous API, store string in this.version, and call the + // callback when done. + if (this._application.extensions) { + this.version = this._application.extensions.get(EXTENSION_ID).version; + callback(); + } else { + let self = this; + self._application.getExtensions(function(extensions) { + self.version = extensions.get(EXTENSION_ID).version; + callback(); + }); + } + }, + + _isNewerThanMe: function TPS__isNewerThanMe(versionString) { + let result = Cc["@mozilla.org/xpcom/version-comparator;1"] + .getService(Ci.nsIVersionComparator) + .compare(this.version, versionString); + if (result < 0) { + return true; // versionString is newer than my version + } else { + return false; // versionString is the same as or older than my version + } + }, + + _isNewerThanFirefox: function TPS__isNewerThanFirefox(versionString) { + let result = Cc["@mozilla.org/xpcom/version-comparator;1"] + .getService(Ci.nsIVersionComparator) + .compare(self._application.version, versionString); + if (result < 0) { + return true; // versionString is newer than Firefox + } else { + return false; // versionString is the same as or older than Firefox + } + }, + + _experimentRequirementsAreMet: function TPS__requirementsMet(experiment) { + // Returns true if we we meet the requirements to run this experiment + // (e.g. meet the minimum Test Pilot version and Firefox version) + // false if not. + // If the experiment doesn't specify minimum versions, attempt to run it. + let logger = this._logger; + try { + let minTpVer, minFxVer, expName; + if (experiment.experimentInfo) { + minTpVer = experiment.experimentInfo.minTPVersion; + minFxVer = experiment.experimentInfo.minFXVersion; + expName = experiment.experimentInfo.testName; + } else if (experiment.surveyInfo) { + minTpVer = experiment.surveyInfo.minTPVersion; + minFxVer = experiment.surveyInfo.minFXVersion; + expName = experiment.surveyInfo.surveyName; + } + + // Minimum test pilot version: + if (minTpVer && this._isNewerThanMe(minTpVer)) { + logger.warn("Not loading " + expName); + logger.warn("Because it requires Test Pilot version " + minTpVer); + + // Let user know there is a newer version of Test Pilot available: + if (!this._isShowingUpdateNotification()) { + this._showNotification( + null, false, + this._stringBundle.GetStringFromName( + "testpilot.notification.extensionUpdate.message"), + this._stringBundle.GetStringFromName( + "testpilot.notification.extensionUpdate"), + "update-extension", true, false, "", "", true); + } + return false; + } + + // Minimum firefox version: + if (minFxVer && this._isNewerThanFirefox(minFxVer)) { + logger.warn("Not loading " + expName); + logger.warn("Because it requires Firefox version " + minFxVer); + return false; + } + } catch (e) { + logger.warn("Error in requirements check " + expName + ": " + e); + } + return true; + }, + + checkForTasks: function TPS_checkForTasks(callback) { + let logger = this._logger; + if (! this._remoteExperimentLoader ) { + logger.trace("Now requiring remote experiment loader:"); + let remoteLoaderModule = this._loader.require("remote-experiment-loader"); + logger.trace("Now instantiating remoteExperimentLoader:"); + let rel = new remoteLoaderModule.RemoteExperimentLoader(this._logRepo); + this._remoteExperimentLoader = rel; + } + + let self = this; + this._remoteExperimentLoader.checkForUpdates( + function(success) { + logger.info("Getting updated experiments... Success? " + success); + // Actually, we do exactly the same thing whether we succeeded in + // downloading new contents or not... + let experiments = self._remoteExperimentLoader.getExperiments(); + + for (let filename in experiments) { + if (!self._experimentRequirementsAreMet(experiments[filename])) { + continue; + } + try { + // The try-catch ensures that if something goes wrong in loading one + // experiment, the other experiments after that one still get loaded. + logger.trace("Attempting to load experiment " + filename); + + let task; + // Could be a survey: check if surveyInfo is exported: + if (experiments[filename].surveyInfo != undefined) { + let sInfo = experiments[filename].surveyInfo; + // If it supplies questions, it's a built-in survey. + // If not, it's a web-based survey. + if (!sInfo.surveyQuestions) { + task = new self._taskModule.TestPilotWebSurvey(sInfo); + } else { + task = new self._taskModule.TestPilotBuiltinSurvey(sInfo); + } + } else { + // This one must be an experiment. + let expInfo = experiments[filename].experimentInfo; + let dsInfo = experiments[filename].dataStoreInfo; + let dataStore = new self._dataStoreModule.ExperimentDataStore( + dsInfo.fileName, dsInfo.tableName, dsInfo.columns ); + let webContent = experiments[filename].webContent; + task = new self._taskModule.TestPilotExperiment(expInfo, + dataStore, + experiments[filename].handlers, + webContent); + } + self.addTask(task); + logger.info("Loaded task " + filename); + } catch (e) { + logger.warn("Failed to load task " + filename + ": " + e); + } + } // end for filename in experiments + + // Handling new results is much simpler: + let results = self._remoteExperimentLoader.getStudyResults(); + for (let r in results) { + let studyResult = new self._taskModule.TestPilotStudyResults(results[r]); + self.addTask(studyResult); + } + + /* Legacy studies = stuff we no longer have the code for, but + * if the user participated in it we want to keep that metadata. */ + let legacyStudies = self._remoteExperimentLoader.getLegacyStudies(); + for (let l in legacyStudies) { + let legacyStudy = new self._taskModule.TestPilotLegacyStudy(legacyStudies[l]); + self.addTask(legacyStudy); + } + + if (callback) { + callback(); + } + } + ); + }, + + reloadRemoteExperiments: function TPS_reloadRemoteExperiments(callback) { + for (let i = 0; i < this.taskList.length; i++) { + this.taskList[i].onExperimentShutdown(); + } + + this.taskList = []; + this._loader.unload(); + + this.checkForTasks(callback); + }, + + getTaskById: function TPS_getTaskById(id) { + for (let i = 0; i < this.taskList.length; i++) { + let task = this.taskList[i]; + if (task.id == id) { + return task; + } + } + return null; + }, + + getAllTasks: function TPS_getAllTasks() { + return this.taskList; + } +}; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/string_sanitizer.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/string_sanitizer.js new file mode 100644 index 000000000000..49924d03635f --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/string_sanitizer.js @@ -0,0 +1,56 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * Dan Mills + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = ["sanitizeString", "sanitizeJSONStrings"]; + +function sanitizeString(input) { + // Only allow alphanumerics, space, period, hypen, and underscore. + // Replace any other characters with question mark. + return input.replace(/[^a-zA-Z0-9 .\-_]/g, '?'); +} + +function sanitizeJSONStrings(jsonBlob) { + // recursively goes through json and sanitizes every string it finds + for (let x in jsonBlob) { + if (typeof jsonBlob[x] == "string") { + jsonBlob[x] = sanitizeString(jsonBlob[x]); + } else if (typeof jsonBlob[x] == "object") { + jsonBlob[x] = sanitizeJSONStrings(jsonBlob[x]); + } + } + return jsonBlob; +} \ No newline at end of file diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/tasks.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/tasks.js new file mode 100644 index 000000000000..3599154e9246 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/tasks.js @@ -0,0 +1,1100 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Test Pilot. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jono X + * Raymond Lee + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = ["TaskConstants", "TestPilotBuiltinSurvey", + "TestPilotExperiment", "TestPilotStudyResults", + "TestPilotLegacyStudy", "TestPilotWebSurvey"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://testpilot/modules/Observers.js"); +Components.utils.import("resource://testpilot/modules/metadata.js"); +Components.utils.import("resource://testpilot/modules/log4moz.js"); +Components.utils.import("resource://testpilot/modules/string_sanitizer.js"); + +const STATUS_PREF_PREFIX = "extensions.testpilot.taskstatus."; +const START_DATE_PREF_PREFIX = "extensions.testpilot.startDate."; +const RECUR_PREF_PREFIX = "extensions.testpilot.reSubmit."; +const RECUR_TIMES_PREF_PREFIX = "extensions.testpilot.recurCount."; +const SURVEY_ANSWER_PREFIX = "extensions.testpilot.surveyAnswers."; +const EXPIRATION_DATE_FOR_DATA_SUBMISSION_PREFIX = + "extensions.testpilot.expirationDateForDataSubmission."; +const DATE_FOR_DATA_DELETION_PREFIX = + "extensions.testpilot.dateForDataDeletion."; +const GUID_PREF_PREFIX = "extensions.testpilot.taskGUID."; +const RETRY_INTERVAL_PREF = "extensions.testpilot.uploadRetryInterval"; +const TIME_FOR_DATA_DELETION = 7 * (24 * 60 * 60 * 1000); // 7 days +const DATA_UPLOAD_PREF = "extensions.testpilot.dataUploadURL"; +const DEFAULT_THUMBNAIL_URL = "chrome://testpilot/skin/badge-default.png"; + + +const TaskConstants = { + // TODO status RESULTS and ARCHIVED don't make sense for studies anymore; + // we need a status MISSED and a status EXPIRED. (can you be cancelled and + // expired?) + STATUS_NEW: 0, // It's new and you haven't seen it yet. + STATUS_PENDING : 1, // You've seen it but it hasn't started. + STATUS_STARTING: 2, // Data collection started but notification not shown. + STATUS_IN_PROGRESS : 3, // Started and notification shown. + STATUS_FINISHED : 4, // Finished and awaiting your choice. + STATUS_CANCELLED : 5, // You've opted out and not submitted anything. + STATUS_SUBMITTED : 6, // You've submitted your data. + STATUS_RESULTS : 7, // Test finished AND final results visible somewhere, deprecated + STATUS_ARCHIVED: 8, // Results seen. Deprecated. TODO: or use for expired? + STATUS_MISSED: 9, // You never ran this study. + + TYPE_EXPERIMENT : 1, + TYPE_SURVEY : 2, + TYPE_RESULTS : 3, + TYPE_LEGACY: 4, + + ALWAYS_SUBMIT: 1, + NEVER_SUBMIT: -1, + ASK_EACH_TIME: 0 +}; +/* Note that experiments use all 9 status codes, but surveys don't have a + * data collection period so they are never STARTING or IN_PROGRESS or + * FINISHED, they go straight from PENDING to SUBMITTED or CANCELED. */ + +let Application = Cc["@mozilla.org/fuel/application;1"] + .getService(Ci.fuelIApplication); + +// Prototype for both TestPilotSurvey and TestPilotExperiment. +var TestPilotTask = { + _id: null, + _title: null, + _status: null, + _url: null, + + _taskInit: function TestPilotTask__taskInit(id, title, url, summary, thumb) { + this._id = id; + this._title = title; + this._status = Application.prefs.getValue(STATUS_PREF_PREFIX + this._id, + TaskConstants.STATUS_NEW); + this._url = url; + this._summary = summary; + this._thumbnail = thumb; + this._logger = Log4Moz.repository.getLogger("TestPilot.Task_"+this._id); + }, + + get title() { + return this._title; + }, + + get id() { + return this._id; + }, + + get taskType() { + return null; + }, + + get status() { + return this._status; + }, + + get webContent() { + return this._webContent; + }, + + get summary() { + if (this._summary) { + return this._summary; + } else { + return this._title; + } + }, + + get thumbnail() { + if (this._thumbnail) { + return this._thumbnail; + } else { + return DEFAULT_THUMBNAIL_URL; + } + }, + + // urls: + get infoPageUrl() { + return this._url; + }, + + get currentStatusUrl() { + return this._url; + }, + + get defaultUrl() { + return this.infoPageUrl; + }, + + get uploadUrl() { + let url = Application.prefs.getValue(DATA_UPLOAD_PREF, ""); + return url + this._id; + }, + + get showMoreInfoLink() { + return true; + }, + + // event handlers: + + onExperimentStartup: function TestPilotTask_onExperimentStartup() { + }, + + onExperimentShutdown: function TestPilotTask_onExperimentShutdown() { + }, + + onAppStartup: function TestPilotTask_onAppStartup() { + // Called by extension core when startup is complete. + }, + + onAppShutdown: function TestPilotTask_onAppShutdown() { + // TODO: not implemented - should be called when firefox is ready to + // shut down. + }, + + onEnterPrivateBrowsing: function TestPilotTask_onEnterPrivate() { + }, + + onExitPrivateBrowsing: function TestPilotTask_onExitPrivate() { + }, + + onNewWindow: function TestPilotTask_onNewWindow(window) { + }, + + onWindowClosed: function TestPilotTask_onWindowClosed(window) { + }, + + onUrlLoad: function TestPilotTask_onUrlLoad(url) { + }, + + onDetailPageOpened: function TestPilotTask_onDetailPageOpened(){ + }, + + checkDate: function TestPilotTask_checkDate() { + }, + + changeStatus: function TPS_changeStatus(newStatus, suppressNotification) { + let logger = Log4Moz.repository.getLogger("TestPilot.Task"); + logger.info("Changing task " + this._id + " status to " + newStatus); + this._status = newStatus; + // Set the pref: + Application.prefs.setValue(STATUS_PREF_PREFIX + this._id, newStatus); + // Notify user of status change: + if (!suppressNotification) { + Observers.notify("testpilot:task:changed", "", null); + } + }, + + loadPage: function TestPilotTask_loadPage() { + // open the link in the chromeless window + var wm = Cc["@mozilla.org/appshell/window-mediator;1"] + .getService(Ci.nsIWindowMediator); + let window = wm.getMostRecentWindow("navigator:browser"); + window.TestPilotWindowUtils.openChromeless(this.defaultUrl); + /* Advance the status when the user sees the page, so that we can stop + * notifying them about stuff they've seen. */ + if (this._status == TaskConstants.STATUS_NEW) { + this.changeStatus(TaskConstants.STATUS_PENDING); + } else if (this._status == TaskConstants.STATUS_STARTING) { + this.changeStatus(TaskConstants.STATUS_IN_PROGRESS); + } else if (this._status == TaskConstants.STATUS_RESULTS) { + this.changeStatus(TaskConstants.STATUS_ARCHIVED); + } + + this.onDetailPageOpened(); + } +}; + +function TestPilotExperiment(expInfo, dataStore, handlers, webContent) { + // All four of these are objects defined in the remote experiment file + this._init(expInfo, dataStore, handlers, webContent); +} +TestPilotExperiment.prototype = { + _init: function TestPilotExperiment__init(expInfo, + dataStore, + handlers, + webContent) { + /* expInfo is a dictionary defined in the remote experiment code, which + * should have the following properties: + * startDate (string representation of date) + * duration (number of days) + * testName (human-readable string) + * testId (int) + * testInfoUrl (url) + * summary (string - couple of sentences explaining study) + * thumbnail (url of an image representing the study) + * optInRequired (boolean) + * recursAutomatically (boolean) + * recurrenceInterval (number of days) + * versionNumber (int) */ + this._taskInit(expInfo.testId, expInfo.testName, expInfo.testInfoUrl, + expInfo.summary, expInfo.thumbnail); + this._webContent = webContent; + this._dataStore = dataStore; + this._versionNumber = expInfo.versionNumber; + this._optInRequired = expInfo.optInRequired; + // TODO implement opt-in interface for tests that require opt-in. + this._recursAutomatically = expInfo.recursAutomatically; + this._recurrenceInterval = expInfo.recurrenceInterval; + + let prefName = START_DATE_PREF_PREFIX + this._id; + let startDateString = Application.prefs.getValue(prefName, false); + if (startDateString) { + // If this isn't the first time we're starting, use the start date + // already stored in prefs. + this._startDate = Date.parse(startDateString); + } else { + // If a start date is provided in expInfo, use that. + // Otherwise, start immediately! + if (expInfo.startDate) { + this._startDate = Date.parse(expInfo.startDate); + Application.prefs.setValue(prefName, expInfo.startDate); + } else { + this._startDate = Date.now(); + Application.prefs.setValue(prefName, (new Date()).toString()); + } + } + + // Duration is specified in days: + let duration = expInfo.duration || 7; // default 1 week + this._endDate = this._startDate + duration * (24 * 60 * 60 * 1000); + this._logger.info("Start date is " + this._startDate.toString()); + this._logger.info("End date is " + this._endDate.toString()); + + this._handlers = handlers; + this._uploadRetryTimer = null; + this._startedUpHandlers = false; + + // checkDate will see what our status is with regards to the start and + // end dates, and set status appropriately. + this.checkDate(); + + if (this.experimentIsRunning) { + this.onExperimentStartup(); + } + }, + + get taskType() { + return TaskConstants.TYPE_EXPERIMENT; + }, + + get endDate() { + return this._endDate; + }, + + get startDate() { + return this._startDate; + }, + + get dataStore() { + return this._dataStore; + }, + + get currentStatusUrl() { + let param = "?eid=" + this._id; + return "chrome://testpilot/content/status.html" + param; + }, + + get defaultUrl() { + return this.currentStatusUrl; + }, + + get recurPref() { + let prefName = RECUR_PREF_PREFIX + this._id; + return Application.prefs.getValue(prefName, TaskConstants.ASK_EACH_TIME); + }, + + getDataStoreAsJSON: function(callback) { + this._dataStore.getAllDataAsJSON(false, callback); + }, + + getWebContent: function TestPilotExperiment_getWebContent(callback) { + let content = ""; + let waitForData = false; + let self = this; + + switch (this._status) { + case TaskConstants.STATUS_NEW: + case TaskConstants.STATUS_PENDING: + content = this.webContent.upcomingHtml; + break; + case TaskConstants.STATUS_STARTING: + case TaskConstants.STATUS_IN_PROGRESS: + content = this.webContent.inProgressHtml; + break; + case TaskConstants.STATUS_FINISHED: + waitForData = true; + this._dataStore.haveData(function(withData) { + if (withData) { + content = self.webContent.completedHtml; + } else { + // for after deleting data manually by user. + let stringBundle = + Components.classes["@mozilla.org/intl/stringbundle;1"]. + getService(Components.interfaces.nsIStringBundleService). + createBundle("chrome://testpilot/locale/main.properties"); + let link = + '"' + this.title + + '"'; + content = + '

' + stringBundle.formatStringFromName( + "testpilot.finishedTask.finishedStudy", [link], 1) + '

' + + '

' + stringBundle.GetStringFromName( + "testpilot.finishedTask.allRelatedDataDeleted") + '

'; + } + callback(content); + }); + break; + case TaskConstants.STATUS_CANCELLED: + if (this._expirationDateForDataSubmission.length == 0) { + content = this.webContent.canceledHtml; + } else { + content = this.webContent.dataExpiredHtml; + } + break; + case TaskConstants.STATUS_SUBMITTED: + if (this._dateForDataDeletion.length > 0) { + content = this.webContent.remainDataHtml; + } else { + content = this.webContent.deletedRemainDataHtml; + } + break; + } + // TODO what to do if status is cancelled, submitted, results, or archived? + if (!waitForData) { + callback(content); + } + }, + + getDataPrivacyContent: function(callback) { + let content = ""; + let waitForData = false; + let self = this; + + switch (this._status) { + case TaskConstants.STATUS_STARTING: + case TaskConstants.STATUS_IN_PROGRESS: + content = this.webContent.inProgressDataPrivacyHtml; + break; + case TaskConstants.STATUS_FINISHED: + waitForData = true; + this._dataStore.haveData(function(withData) { + if (withData) { + content = self.webContent.completedDataPrivacyHtml; + } + callback(content); + }); + break; + case TaskConstants.STATUS_CANCELLED: + if (this._expirationDateForDataSubmission.length == 0) { + content = this.webContent.canceledDataPrivacyHtml; + } else { + content = this.webContent.dataExpiredDataPrivacyHtml; + } + break; + case TaskConstants.STATUS_SUBMITTED: + if (this._dateForDataDeletion.length > 0) { + content = this.webContent.remainDataDataPrivacyHtml; + } else { + content = this.webContent.deletedRemainDataDataPrivacyHtml; + } + break; + } + if (!waitForData) { + callback(content); + } + }, + + experimentIsRunning: function TestPilotExperiment_isRunning() { + if (this._optInRequired) { + return (this._status == TaskConstants.STATUS_STARTING || + this._status == TaskConstants.STATUS_IN_PROGRESS); + } else { + // Tests that don't require extra opt-in should start running even + // if you haven't seen them yet. + return (this._status < TaskConstants.STATUS_FINISHED); + } + }, + + // Pass events along to handlers: + onNewWindow: function TestPilotExperiment_onNewWindow(window) { + this._logger.trace("Experiment.onNewWindow called."); + if (this.experimentIsRunning()) { + this._handlers.onNewWindow(window); + } + }, + + onWindowClosed: function TestPilotExperiment_onWindowClosed(window) { + this._logger.trace("Experiment.onWindowClosed called."); + if (this.experimentIsRunning()) { + this._handlers.onWindowClosed(window); + } + }, + + onAppStartup: function TestPilotExperiment_onAppStartup() { + this._logger.trace("Experiment.onAppStartup called."); + if (this.experimentIsRunning()) { + this._handlers.onAppStartup(); + } + }, + + onAppShutdown: function TestPilotExperiment_onAppShutdown() { + this._logger.trace("Experiment.onAppShutdown called."); + // TODO the caller for this is not yet implemented + if (this.experimentIsRunning()) { + this._handlers.onAppShutdown(); + } + }, + + onExperimentStartup: function TestPilotExperiment_onStartup() { + this._logger.trace("Experiment.onExperimentStartup called."); + // Make sure not to call this if it's already been called: + if (this.experimentIsRunning() && !this._startedUpHandlers) { + this._logger.trace(" ... starting up handlers!"); + this._handlers.onExperimentStartup(this._dataStore); + this._startedUpHandlers = true; + } + }, + + onExperimentShutdown: function TestPilotExperiment_onShutdown() { + this._logger.trace("Experiment.onExperimentShutdown called."); + if (this.experimentIsRunning() && this._startedUpHandlers) { + this._handlers.onExperimentShutdown(); + this._startedUpHandlers = false; + } + }, + + onEnterPrivateBrowsing: function TestPilotExperiment_onEnterPrivate() { + this._logger.trace("Task is entering private browsing."); + if (this.experimentIsRunning()) { + this._handlers.onEnterPrivateBrowsing(); + } + }, + + onExitPrivateBrowsing: function TestPilotExperiment_onExitPrivate() { + this._logger.trace("Task is exiting private browsing."); + if (this.experimentIsRunning()) { + this._handlers.onExitPrivateBrowsing(); + } + }, + + _reschedule: function TestPilotExperiment_reschedule() { + // Schedule next run of test: + // add recurrence interval to start date and store! + let ms = this._recurrenceInterval * (24 * 60 * 60 * 1000); + // recurrenceInterval is in days, convert to milliseconds: + this._startDate += ms; + this._endDate += ms; + let prefName = START_DATE_PREF_PREFIX + this._id; + Application.prefs.setValue(prefName, + (new Date(this._startDate)).toString()); + }, + + get _numTimesRun() { + // For automatically recurring tests, this is the number of times it + // has recurred - it will be 1 on the first run, 2 on the second run, + // etc. + if (this._recursAutomatically) { + return Application.prefs.getValue(RECUR_TIMES_PREF_PREFIX + this._id, + 1); + } else { + return 0; + } + }, + + set _expirationDateForDataSubmission(date) { + if (date) { + Application.prefs.setValue( + EXPIRATION_DATE_FOR_DATA_SUBMISSION_PREFIX + this._id, + (new Date(date)).toString()); + } else { + Application.prefs.setValue( + EXPIRATION_DATE_FOR_DATA_SUBMISSION_PREFIX + this._id, ""); + } + }, + + get _expirationDateForDataSubmission() { + return Application.prefs.getValue( + EXPIRATION_DATE_FOR_DATA_SUBMISSION_PREFIX + this._id, ""); + }, + + set _dateForDataDeletion(date) { + if (date) { + Application.prefs.setValue( + DATE_FOR_DATA_DELETION_PREFIX + this._id, (new Date(date)).toString()); + } else { + Application.prefs.setValue(DATE_FOR_DATA_DELETION_PREFIX + this._id, ""); + } + }, + + get _dateForDataDeletion() { + return Application.prefs.getValue( + DATE_FOR_DATA_DELETION_PREFIX + this._id, ""); + }, + + checkDate: function TestPilotExperiment_checkDate() { + // This method handles all date-related status changes and should be + // called periodically. + let currentDate = Date.now(); + + // Reset automatically recurring tests: + if (this._recursAutomatically && + this._status >= TaskConstants.STATUS_FINISHED && + currentDate >= this._startDate && + currentDate <= this._endDate) { + // if we've done a permanent opt-out, then don't start over- + // just keep rescheduling. + if (this.recurPref == TaskConstants.NEVER_SUBMIT) { + this._logger.info("recurPref is never submit, so I'm rescheduling."); + this._reschedule(); + } else { + // Normal case is reset to new. + this.changeStatus(TaskConstants.STATUS_NEW); + + // increment count of how many times this recurring test has run + let numTimesRun = this._numTimesRun; + numTimesRun++; + this._logger.trace("Test recurring... incrementing " + RECUR_TIMES_PREF_PREFIX + this._id + " to " + numTimesRun); + Application.prefs.setValue( RECUR_TIMES_PREF_PREFIX + this._id, + numTimesRun ); + this._logger.trace("Incremented it."); + } + } + + // No-opt-in required tests skip PENDING and go straight to STARTING. + if (!this._optInRequired && + this._status < TaskConstants.STATUS_STARTING && + currentDate >= this._startDate && + currentDate <= this._endDate) { + let uuidGenerator = + Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + let uuid = uuidGenerator.generateUUID().toString(); + // remove the brackets from the generated UUID + if (uuid.indexOf("{") == 0) { + uuid = uuid.substring(1, (uuid.length - 1)); + } + // clear the data before starting. + this._dataStore.wipeAllData(); + this.changeStatus(TaskConstants.STATUS_STARTING); + Application.prefs.setValue(GUID_PREF_PREFIX + this._id, uuid); + this.onExperimentStartup(); + } + + // What happens when a test finishes: + if (this._status < TaskConstants.STATUS_FINISHED && + currentDate > this._endDate) { + let self = this; + let setDataDeletionDate = true; + this._logger.info("Passed End Date - Switched Task Status to Finished"); + this.changeStatus(TaskConstants.STATUS_FINISHED); + this.onExperimentShutdown(); + + if (this._recursAutomatically) { + this._reschedule(); + // A recurring experiment may have been set to automatically submit. If + // so, submit now! + if (this.recurPref == TaskConstants.ALWAYS_SUBMIT) { + this._logger.info("Automatically Uploading Data"); + this.upload(function(success) { + Observers.notify("testpilot:task:dataAutoSubmitted", self, null); + }); + } else if (this.recurPref == TaskConstants.NEVER_SUBMIT) { + this._logger.info("Automatically opting out of uploading data"); + this.changeStatus(TaskConstants.STATUS_CANCELLED, true); + this._dataStore.wipeAllData(); + setDataDeletionDate = false; + } else { + if (Application.prefs.getValue( + "extensions.testpilot.alwaysSubmitData", false)) { + this.upload(function(success) { + if (success) { + Observers.notify( + "testpilot:task:dataAutoSubmitted", self, null); + } + }); + } + } + } else { + if (Application.prefs.getValue( + "extensions.testpilot.alwaysSubmitData", false)) { + this.upload(function(success) { + if (success) { + Observers.notify("testpilot:task:dataAutoSubmitted", self, null); + } + }); + } + } + if (setDataDeletionDate) { + let date = this._endDate + TIME_FOR_DATA_DELETION; + this._dateForDataDeletion = date; + this._expirationDateForDataSubmission = date; + } else { + this._dateForDataDeletion = null; + this._expirationDateForDataSubmission = null; + } + } else { + // only do this if the state is already finished and the data is expired. + if (this._status == TaskConstants.STATUS_FINISHED) { + if (Application.prefs.getValue( + "extensions.testpilot.alwaysSubmitData", false)) { + this.upload(function(success) { + if (success) { + Observers.notify("testpilot:task:dataAutoSubmitted", self, null); + } + }); + } else if (this._expirationDateForDataSubmission.length > 0) { + let expirationDate = Date.parse(this._expirationDateForDataSubmission); + if (currentDate > expirationDate) { + this.changeStatus(TaskConstants.STATUS_CANCELLED, true); + this._dataStore.wipeAllData(); + this._dateForDataDeletion = null; + // need to keep the expirationDateForDataSubmission value so + // we know that data is expired, not user cancels the task. + } + } + } else if (this._status == TaskConstants.STATUS_SUBMITTED) { + if (this._dateForDataDeletion.length > 0) { + let deleteDate = Date.parse(this._dateForDataDeletion); + if (currentDate > deleteDate) { + this._dataStore.wipeAllData(); + this._dateForDataDeletion = null; + } + } + } + } + }, + + _prependMetadataToJSON: function TestPilotExperiment__prependToJson(callback) { + let json = {}; + let self = this; + MetadataCollector.getMetadata(function(md) { + json.metadata = md; + let guid = Application.prefs.getValue(GUID_PREF_PREFIX + self._id, ""); + json.metadata.task_guid = guid; + json.metadata.event_headers = self._dataStore.getPropertyNames(); + self._dataStore.getJSONRows(function(rows) { + json.events = rows; + callback( JSON.stringify(json) ); + }); + }); + }, + + // Note: When we have multiple experiments running, the uploads + // are separate files. + upload: function TestPilotExperiment_upload(callback, retryCount) { + // Callback is a function that will be called back with true or false + // on success or failure. + + /* If we've already uploaded, and the user tries to upload again for + * some reason (they could navigate back to the status.html page, + * for instance), then proceed without uploading: */ + if (this._status >= TaskConstants.STATUS_SUBMITTED) { + callback(true); + return; + } + + // note the server will reject any upload over 5MB - shouldn't be a problem + let self = this; + let url = self.uploadUrl; + self._logger.info("Posting data to url " + url + "\n"); + self._prependMetadataToJSON( function(dataString) { + let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance( Ci.nsIXMLHttpRequest ); + req.open('POST', url, true); + req.setRequestHeader("Content-type", "application/json"); + req.setRequestHeader("Content-length", dataString.length); + req.setRequestHeader("Connection", "close"); + req.onreadystatechange = function(aEvt) { + if (req.readyState == 4) { + if (req.status == 201) { + let location = req.getResponseHeader("Location"); + self._logger.info("DATA WAS POSTED SUCCESSFULLY " + location); + if (self._uploadRetryTimer) { + self._uploadRetryTimer.cancel(); // Stop retrying - it worked! + } + self.changeStatus(TaskConstants.STATUS_SUBMITTED); + self._dateForDataDeletion = Date.now() + TIME_FOR_DATA_DELETION; + self._expirationDateForDataSubmission = null; + callback(true); + } else { + /* If something went wrong with the upload, give up for now, + * but start a timer to try again later. Maybe the network will + * be better by then. "later" starts at 1 hour, but increases + * using a random exponential function. This serves as a backoff + * in cases where a lot of users are trying to submit data at + * the same time and the network or server can't handle it. + */ + + // TODO don't retry if status code is 401, 404, or... + // any others? + self._logger.warn("ERROR POSTING DATA: " + req.responseText); + self._uploadRetryTimer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + + if (!retryCount) { + retryCount = 0; + } + let interval = + Application.prefs.getValue(RETRY_INTERVAL_PREF, 3600000); // 1 hour + let delay = + parseInt(Math.random() * Math.pow(2, retryCount) * interval); + self._uploadRetryTimer.initWithCallback( + { notify: function(timer) { + self.upload(callback, retryCount++); + } }, (interval + delay), Ci.nsITimer.TYPE_ONE_SHOT); + callback(false); + } + } + }; + req.send(dataString); + }); + }, + + optOut: function TestPilotExperiment_optOut(reason, callback) { + // Regardless of study ID, post the opt-out message to a special + // database table of just opt-out messages; include study ID in metadata. + let url = Application.prefs.getValue(DATA_UPLOAD_PREF, "") + "opt-out"; + let logger = this._logger; + this.changeStatus(TaskConstants.STATUS_CANCELLED); + this._dataStore.wipeAllData(); + this._dateForDataDeletion = null; + this._expirationDateForDataSubmission = null; + logger.info("Opting out of test with reason " + reason); + if (reason) { + // Send us the reason... + // (TODO: include metadata?) + let answer = {id: this._id, + reason: reason}; + let dataString = JSON.stringify(answer); + var req = + Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + logger.trace("Posting " + dataString + " to " + url); + req.open('POST', url, true); + req.setRequestHeader("Content-type", "application/json"); + req.setRequestHeader("Content-length", dataString.length); + req.setRequestHeader("Connection", "close"); + req.onreadystatechange = function(aEvt) { + if (req.readyState == 4) { + if (req.status == 200) { + logger.info("Quit reason posted successfully " + req.responseText); + callback(true); + } else { + logger.warn(req.status + " posting error " + req.responseText); + callback(false); + } + } + }; + logger.trace("Sending quit reason."); + req.send(dataString); + } else { + callback(false); + } + }, + + setRecurPref: function TPE_setRecurPrefs(value) { + // value is NEVER_SUBMIT, ALWAYS_SUBMIT, or ASK_EACH_TIME + let prefName = RECUR_PREF_PREFIX + this._id; + this._logger.info("Setting recur pref to " + value); + Application.prefs.setValue(prefName, value); + } +}; +TestPilotExperiment.prototype.__proto__ = TestPilotTask; + +function TestPilotBuiltinSurvey(surveyInfo) { + this._init(surveyInfo); +} +TestPilotBuiltinSurvey.prototype = { + _init: function TestPilotBuiltinSurvey__init(surveyInfo) { + this._taskInit(surveyInfo.surveyId, + surveyInfo.surveyName, + surveyInfo.surveyUrl, + surveyInfo.summary, + surveyInfo.thumbnail); + this._studyId = surveyInfo.uploadWithExperiment; // what study do we belong to + this._versionNumber = surveyInfo.versionNumber; + this._questions = surveyInfo.surveyQuestions; + this._explanation = surveyInfo.surveyExplanation; + }, + + get taskType() { + return TaskConstants.TYPE_SURVEY; + }, + + get surveyExplanation() { + return this._explanation; + }, + + get surveyQuestions() { + return this._questions; + }, + + get currentStatusUrl() { + let param = "?eid=" + this._id; + return "chrome://testpilot/content/take-survey.html" + param; + }, + + get defaultUrl() { + return this.currentStatusUrl; + }, + + get relatedStudyId() { + return this._studyId; + }, + + onDetailPageOpened: function TPS_onDetailPageOpened() { + if (this._status < TaskConstants.STATUS_IN_PROGRESS) { + this.changeStatus( TaskConstants.STATUS_IN_PROGRESS, true ); + } + }, + + get oldAnswers() { + let surveyResults = + Application.prefs.getValue(SURVEY_ANSWER_PREFIX + this._id, null); + if (!surveyResults) { + return null; + } else { + this._logger.info("Trying to json.parse this: " + surveyResults); + let surveyJson = sanitizeJSONStrings( JSON.parse(surveyResults) ); + return surveyJson["answers"]; + } + }, + + store: function TestPilotSurvey_store(surveyResults, callback) { + /* Store answers in preferences store; then, upload the survey data + * if this is a survey with an associated study (the matching GUIDs + * will be used to associate the two datasets server-side). + * If it's not one with an associated survey, just store and set status + * to submitted. Either way, call the callback with true/false for + * success/failure.*/ + surveyResults = sanitizeJSONStrings(surveyResults); + let prefName = SURVEY_ANSWER_PREFIX + this._id; + // Also store survey version number + if (this._versionNumber) { + surveyResults["version_number"] = this._versionNumber; + } + Application.prefs.setValue(prefName, JSON.stringify(surveyResults)); + if (this._studyId) { + this._upload(callback, 0); + } else { + this.changeStatus(TaskConstants.STATUS_SUBMITTED); + callback(true); + } + }, + + _prependMetadataToJSON: function TestPilotSurvey__prependToJson(callback) { + let json = {}; + let self = this; + MetadataCollector.getMetadata(function(md) { + json.metadata = md; + // Include guid of the study that this survey is related to, so we + // can match them up server-side. + let guid = Application.prefs.getValue(GUID_PREF_PREFIX + self._studyId, ""); + json.metadata.task_guid = guid; + let pref = SURVEY_ANSWER_PREFIX + self._id; + let surveyAnswers = JSON.parse(Application.prefs.getValue(pref, "{}")); + json.survey_data = sanitizeJSONStrings(surveyAnswers); + callback(JSON.stringify(json)); + }); + }, + + // Upload function for survey -- TODO this duplicates a lot of code + // from study._upload(). + _upload: function TestPilotSurvey__upload(callback, retryCount) { + let self = this; + this._prependMetadataToJSON(function(params) { + let req = + Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + let url = self.uploadUrl; + + req.open("POST", url, true); + req.setRequestHeader("Content-type", "application/json"); + req.setRequestHeader("Content-length", params.length); + req.setRequestHeader("Connection", "close"); + req.onreadystatechange = function(aEvt) { + if (req.readyState == 4) { + if (req.status == 200) { + self._logger.info( + "DATA WAS POSTED SUCCESSFULLY " + req.responseText); + if (self._uploadRetryTimer) { + self._uploadRetryTimer.cancel(); // Stop retrying - it worked! + } + self.changeStatus(TaskConstants.STATUS_SUBMITTED); + callback(true); + } else { + self._logger.warn("ERROR POSTING DATA: " + req.responseText); + self._uploadRetryTimer = + Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + if (!retryCount) { + retryCount = 0; + } + let interval = + Application.prefs.getValue(RETRY_INTERVAL_PREF, 3600000); // 1 hour + let delay = + parseInt(Math.random() * Math.pow(2, retryCount) * interval); + self._uploadRetryTimer.initWithCallback( + { notify: function(timer) { + self._upload(callback, retryCount++); + } }, (interval + delay), Ci.nsITimer.TYPE_ONE_SHOT); + callback(false); + } + } + }; + req.send(params); + }); + } +}; +TestPilotBuiltinSurvey.prototype.__proto__ = TestPilotTask; + +function TestPilotWebSurvey(surveyInfo) { + this._init(surveyInfo); +} +TestPilotWebSurvey.prototype = { + _init: function TestPilotWebSurvey__init(surveyInfo) { + this._taskInit(surveyInfo.surveyId, + surveyInfo.surveyName, + surveyInfo.surveyUrl, + surveyInfo.summary, + surveyInfo.thumbnail); + this._logger.info("Initing survey. This._status is " + this._status); + }, + + get taskType() { + return TaskConstants.TYPE_SURVEY; + }, + + get defaultUrl() { + return this.infoPageUrl; + }, + + get showMoreInfoLink() { + return false; + }, + + onDetailPageOpened: function TPWS_onDetailPageOpened() { + /* Once you view the URL of the survey, we'll assume you've taken it. + * There's no reliable way to tell whether you have or not, so let's + * default to not bugging the user about it again. + */ + if (this._status < TaskConstants.STATUS_SUBMITTED) { + this.changeStatus( TaskConstants.STATUS_SUBMITTED, true ); + } + } +}; +TestPilotWebSurvey.prototype.__proto__ = TestPilotTask; + + +function TestPilotStudyResults(resultsInfo) { + this._init(resultsInfo); +}; +TestPilotStudyResults.prototype = { + _init: function TestPilotStudyResults__init(resultsInfo) { + this._taskInit( resultsInfo.id, + resultsInfo.title, + resultsInfo.url, + resultsInfo.summary, + resultsInfo.thumbnail); + this._studyId = resultsInfo.studyId; // what study do we belong to + this._pubDate = Date.parse(resultsInfo.date); + }, + + get taskType() { + return TaskConstants.TYPE_RESULTS; + }, + + get publishDate() { + return this._pubDate; + }, + + get relatedStudyId() { + return this._studyId; + } +}; +TestPilotStudyResults.prototype.__proto__ = TestPilotTask; + +function TestPilotLegacyStudy(studyInfo) { + this._init(studyInfo); +}; +TestPilotLegacyStudy.prototype = { + _init: function TestPilotLegacyStudy__init(studyInfo) { + let stat = Application.prefs.getValue(STATUS_PREF_PREFIX + studyInfo.id, + null); + this._taskInit( studyInfo.id, + studyInfo.name, + studyInfo.url, + studyInfo.summary, + studyInfo.thumbnail ); + + /* Only three statuses are valid for legacy studies: It must be either + * canceled, archived (meaning you submitted it), or missed (you never ran it). + * Set status to one of these. + */ + switch (stat) { + case TaskConstants.STATUS_CANCELLED: + case TaskConstants.STATUS_ARCHIVED: + case TaskConstants.STATUS_MISSED: + // Keep that status, so do nothing + break; + case TaskConstants.STATUS_SUBMITTED: + // Change submitted to archived + this.changeStatus(TaskConstants.STATUS_ARCHIVED, true); + break; + default: + // Anything else means you missed it + this.changeStatus(TaskConstants.STATUS_MISSED, true); + } + + if (studyInfo.duration) { + let prefName = START_DATE_PREF_PREFIX + this._id; + let startDateString = Application.prefs.getValue(prefName, null); + if (startDateString) { + this._startDate = Date.parse(startDateString); + this._endDate = this._startDate + duration * (24 * 60 * 60 * 1000); + } + } + }, + + get taskType() { + return TaskConstants.TYPE_LEGACY; + } + // TODO test that they don't say "thanks for contributing" if the + // user didn't actually complete them... +}; +TestPilotLegacyStudy.prototype.__proto__ = TestPilotTask; \ No newline at end of file diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/badge-default.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/badge-default.png new file mode 100644 index 0000000000000000000000000000000000000000..e4ef896ca7bba075399eed382c5a1e230c263627 GIT binary patch literal 19738 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

x?bg`u%k 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 Ef2h3t`2YX_ literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/css/screen-standalone.css b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/css/screen-standalone.css new file mode 100644 index 000000000000..cde6bfdbdbb2 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/css/screen-standalone.css @@ -0,0 +1,316 @@ +html { + padding-bottom: 40px; + padding-top: 20px; + padding-left: 0px; + padding-right: 0px; +} + +body { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Lucida, Arial, Helvetica, sans-serif; + font-family: 'DroidSans'; + background: #fff url('chrome://testpilot/skin/images/bg-status.jpg') repeat-x top center; padding: 0px; + color:#787878; + font-size:12px; + line-height: 18px; + margin: 0 auto; + +} + + h1 {font-family: 'DroidSans'; color: #3a3a3a; font-size: 28px; font-weight: normal; letter-spacing: -1px} + h2 {font-family: 'DroidSans'; color: #54717b; font-size: 24px; line-height: 24px;font-weight: normal; letter-spacing: -1px} + h3 {font-family: 'DroidSans'; color: #54717b; font-size: 18px; line-height: 18px;font-weight: normal; letter-spacing: -1px} + + p, ul {font-size: 12px;} + + a:link, a:hover, a:active, a:focus, a:visited {color: #9f423b; text-decoration: none;} + a:hover {color: #9f423b; text-decoration: none;} + + .bold {font-family: 'DroidSans-Bold'; color:#3a3a3a;} + .address {margin-left: 20px;} + .inactive {color:#ccc;} + + li {list-style-type: circle;} + + .spacer {height: 40px;} + + .center {text-align: center;} + + .data { + + background-color: #fff; + margin-bottom: 6px; + border: 1px solid rgba(133, 153, 166, 0.2); + border-bottom: 4px solid rgba(133, 153, 166, 0.2); + -moz-border-bottom-colors:rgba(133, 153, 166, 0.3) rgba(133, 153, 166, 0.2) rgba(133, 153, 166, 0.2) rgba(133, 153, 166, 0.2); + padding: 6px; + -moz-box-shadow: + rgba(133, 153, 166, 0.4) 0px 1px 24px; + -webkit-box-shadow: + rgba(133, 153, 166, 0.4) 0px 1px 24px; + + } + + .dataBox { + font-family: 'DroidSans'; + font-size: 16px; + padding: 6px 20px 20px 20px; + -moz-border-radius: 0.5em; + -webkit-border-radius: 0.5em; + background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/images/callout.png') no-repeat top center; + //display: inline; + } + +@font-face{ +font-family: 'DroidSans'; +src: url('chrome://testpilot/skin/fonts/DroidSans.ttf') format('truetype'); +} + +@font-face{ +font-family: 'DroidSans-Bold'; +src: url('chrome://testpilot/skin//DroidSans-Bold.ttf') format('truetype'); +} + +#container { + margin: 0px auto; + width: 950px; + +} + +#logo { + margin-left: 20px; +} + +#contentWelcome { + margin-top: 380px; + padding: 8px 24px 24px 24px; + font-family: 'DroidSans'; + text-align: left; +} + +#content-standalone { + margin-top: 120px; + padding: 8px 24px 24px 24px; + font-family: 'DroidSans'; + text-align: left; +} + +#download { + width: 300px; + margin-left: 410px; + margin-top: 240px; +} + +.downloadButton { + margin-bottom: -10px; + margin-top: -2px; + margin-right: 12px; +} + +.downloadH1 {font-family: 'DroidSans-bold'; color: #3a3a3a; font-size: 24px; font-weight: normal; letter-spacing: -1px; margin-right: 8px;} + +.downloadH2 {font-family: 'DroidSans'; color: #3a3a3a; font-size: 22px; font-weight: normal; letter-spacing: -1px; line-height: 28px; margin-left: 46px;} + +#intro { + float: left; + width: 500px; + margin-top: 30px; +} + +#links { + float: right; + padding: 24px 0px; + margin-right: 0px; + margin-top: 260px; +} + +.button { + font-family: 'DroidSans'; + font-size: 16px; + padding: 8px 12px; + color: rgba(0, 0, 0, 0.8); + -moz-border-radius: 0.5em; + -webkit-border-radius: 0.5em; + -moz-box-shadow: + inset rgba(0, 0, 0, 0.2) 0 1px 1px, + inset rgba(255, 255, 255, 1) 0 3px 1px, + inset rgba(255, 255, 255, 0.3) 0 16px 0px, + inset rgba(0, 0, 0, 0.2) 0 -1px 1px, + inset rgba(0, 0, 0, 0.1) 0 -2px 1px, + rgba(255, 255, 255, 1) 0 1px, + rgba(133, 153, 166, 0.3) 0px 1px 12px; + background-color: #e7eaec; + //display: inline; +} + +.home_button { + font-family: 'DroidSans'; + font-size: 16px; + padding: 8px 12px; + width: 240px; + color: rgba(0, 0, 0, 0.8); + -moz-border-radius: 0.5em; + -webkit-border-radius: 0.5em; + -moz-box-shadow: + inset rgba(0, 0, 0, 0.2) 0 1px 1px, + inset rgba(255, 255, 255, 1) 0 3px 1px, + inset rgba(255, 255, 255, 0.3) 0 16px 0px, + inset rgba(0, 0, 0, 0.2) 0 -1px 1px, + inset rgba(0, 0, 0, 0.1) 0 -2px 1px, + rgba(255, 255, 255, 1) 0 1px, + rgba(133, 153, 166, 0.3) 0px 1px 12px; + background-color: #e7eaec; + //display: inline; +} + +.callout { + font-family: 'DroidSans'; + font-size: 16px; + padding: 8px 24px; + margin: 24px auto; + color: rgba(0, 0, 0, 0.8); + -moz-border-radius: 0.5em; + -webkit-border-radius: 0.5em; + background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/images/callout.png') no-repeat top center; + -moz-box-shadow: + inset rgba(185, 221, 234, 0.2) 0 -10px 12px, + inset rgba(185, 221, 234, 1) 0 0px 1px, + inset rgba(255, 255, 255, 0.2) 0 10px 12px; + //display: inline; +} + +#data-privacy-text { + width: 320px; + margin-bottom: 50px; +} + +.home_callout { + font-family: 'DroidSans'; + font-size: 16px; + vertical-align: middle; + width: 280px; + padding: 8px 24px; + margin: 8px auto; + color: rgba(0, 0, 0, 0.8); + -moz-border-radius: 0.5em; + -webkit-border-radius: 0.5em; + background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/images/callout.png') no-repeat top center; + -moz-box-shadow: + inset rgba(185, 221, 234, 0.2) 0 -10px 12px, + inset rgba(185, 221, 234, 1) 0 0px 1px, + inset rgba(255, 255, 255, 0.2) 0 10px 12px; + //display: inline; +} + +.home_callout_continue { + font-family: 'DroidSans'; + font-size: 16px; + vertical-align: middle; + width: 280px; + padding: 8px 24px; + margin: 8px auto; + color: rgba(0, 0, 0, 0.8); + -moz-border-radius: 0.5em; + -webkit-border-radius: 0.5em; + background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/images/callout_continue.png') no-repeat top center; + -moz-box-shadow: + inset rgba(185, 221, 234, 0.2) 0 -10px 12px, + inset rgba(185, 221, 234, 1) 0 0px 1px, + inset rgba(255, 255, 255, 0.2) 0 10px 12px; + //display: inline; +} + +.home_callout_continue a {color: #6c9735; text-decoration: none;} +.home_callout_continue a:hover {color: #6c9735; text-decoration: none; border-bottom: 1px dotted #6c9735;} + + +.homeIcon { + margin-top: -32px; + margin-bottom: -32px; + margin-right: 12px; +} + +.homeIconSubmit { + margin-top: -32px; + margin-bottom: -32px; + margin-right: 12px; + visibility: hidden; +} + +#footer { + clear: both; + padding: 24px; + font-size: 9px; + +} + +.mozLogo { + margin-bottom: -10px; + margin-right: 6px; +} + +/* ------- MENU ------- + +#menu { + margin: 20px auto; + max-width: 800px; + padding: 4px 40px; + width: 800px; + text-align: left; + -moz-border-radius: 0.25em; + -webkit-border-radius: 0.25em; + border-top: 1px solid #adb6ba; + border-left: 1px solid #adb6ba; + border-right: 1px solid #adb6ba; + border-bottom: 3px solid #adb6ba; + -moz-border-bottom-colors:#adb6ba #e7eaec #e7eaec; + background-color: #fff; +} + +*/ + +.menuItem { + margin-right: 30px; + margin-bottom: 40px; + font-size: 14px; + text-shadow: 1px 1px 1px rgba(173, 182, 186, 0.6); + padding: 9px 8px 8px 8px; +} + +.menuOn { + margin-right: 30px; + margin-bottom: 40px; + font-size: 14px; + text-shadow: 1px 1px 1px rgba(173, 182, 186, 1); + background-color: rgba(173, 182, 186, 0.3); + -moz-box-shadow: + inset rgba(0, 0, 0, 0.2) 0 -10px 12px; + padding: 9px 8px 8px 8px; +} + +.menuItem a {color: #9f423b; text-decoration: none;} +.menuItem a:hover {color: #9f423b; text-decoration: none; border-bottom: 1px dotted #9f423b;} + + +.menuOn a {color: #666666; text-decoration: none;} +.menuOn a:hover {color: #666666; text-decoration: none;} + +canvas { + border: 1px solid black; +} + +a { + color: #9f423b; +} + +a:hover { + color: #9f423b; text-decoration: underline; cursor: pointer; +} + +#survey-explanation { + color: #787878; + font-size: 14px; +} + +.survey-question-explanation { + color: #787878; +} diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/dino_32x32.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/dino_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..981252d6a2df346828296a53a20ae47eb5ef670f GIT binary patch literal 4443 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≥$N7pC3(TO^4kU-sPTRE#o6wy=L65bXVt0k#Y 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 k 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSans.ttf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSans.ttf new file mode 100644 index 0000000000000000000000000000000000000000..458ba59d8bd699afe889f319d89dc69bd1812974 GIT binary patch literal 149076 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 Dm8_&) literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Bold.ttf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b6add0e4c064605508bc10c5c3baa5ec18fbf34a GIT binary patch literal 172012 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

5AI>l9&=2N$w;;K{n)NQ92>#$GVc8al1 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 at=KVh1vw%eN{@>5%J*w#K?&|8Q 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�tR1(0y~OPVHNo)^mhn!Nt9)m5c`G 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Italic.ttf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1ef1f75ec53d65bf785274d4c0553f24b23dae21 GIT binary patch literal 155220 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Regular.ttf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..239ba38253cb2429c9501059b53a1513bce24352 GIT binary patch literal 162864 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 z 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/bg-status.jpg b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/bg-status.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ddb9e9700af194fd778ade5f743898840e02501a GIT binary patch literal 86130 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%0A@TJyfOHnrHxDPwE? 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout_continue.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout_continue.png new file mode 100644 index 0000000000000000000000000000000000000000..678fa3a6c2b62e942cb4eefb6ef39a67a75a5186 GIT binary patch literal 15049 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

;Cy`>A8VQk!G%)5N7M!m-dks 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�x_-rrIOx@n=<^2K$LQ}SJGm+a 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 FV1jA`oO%EN literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_continue.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_continue.png new file mode 100644 index 0000000000000000000000000000000000000000..4041ef13a6e3c71038aab098d7735205e7626469 GIT binary patch literal 693 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_twitter.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_twitter.png new file mode 100644 index 0000000000000000000000000000000000000000..ecbd473bb958643509f4fa1714d2bf5acf2edce6 GIT binary patch literal 3502 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_upcoming.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_upcoming.png new file mode 100644 index 0000000000000000000000000000000000000000..2f3750572ff7a90049bd60e594bff71c8f02b8c4 GIT binary patch literal 3638 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 IM6N<$g5cEJEdT%j literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/logo.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8da5468f20f3ce067f8df8f0020dd33d2a9dcb63 GIT binary patch literal 3600 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 WEeQ?yJb8=&0000DSr z1<%~X^wgl##FWaylc_d9MF#?WLR<@_ZJrtEy?OJdLdJTHnrXYd;am*EQHTch#Rie|BjYpVT(`@Zm$HhWp)1*WSK;yF|t8$B!TDnx}mJ{CS#^ z#X(J@C-)!z`t|GQ&!6w!y_?{lF+Z!W!zs4GG-!Ew=f{s9|Ni}ZcE^E#|NedY^y%x@ zuV21=`ThI%jM(CK^T6fVbyts`escNhx{8i(-@f&`#2j6B;>ib7SV1J&$p(iqCnhHlO>*9Ob02m#kA38b3-i zxbi(KyWs3|q2hbXC+xfD85(8q_{)o1Yx5`hWG2}kG@o7Kc#viH*WF2zE~U7Xs@yS2 z{D14a$3EkY@^6g;I8R!MJe7I8TV>~ZdE<#@4GvLpe>pw>%{Tk9!SdYkD)opOzk4$3nv3^uv;Zbw22WQ%mvv4FO#o7n1DXH; literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-completed.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-completed.png new file mode 100644 index 0000000000000000000000000000000000000000..0f239c0f62ef9b0b5006477bcae3692a0bd79644 GIT binary patch literal 4233 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-ejected.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-ejected.png new file mode 100644 index 0000000000000000000000000000000000000000..04701182cc62ac1fda4936fbd680ce4957bc6462 GIT binary patch literal 2363 zcmaJ@c~BE+9&KO{hsfco1aKJJcz_CY67Cklk$?sa1K}J7n2AXXDak<&lYkD04v4yl zlm{pR2JoomRM0^pVY!w|m}N&{L{KEcN|0Me6k$6m?*5_MRo&lF@4esgy|1d%1N?Ux z=$q>U0AN7fO`)Ts2l|35SD;_{rJs+YgPF)XTolCHCz3D(2;j-$?L|N;hY^F&5e7>Z z--fsXfDVow94-o{?SYv*jx9rjv6XW8C>sD=-KBg6b3Y;i_aZTDt{ZN$R*3`IEH_-3 z0}W5(laW~V?gRl6l;9uCOxVwKVd30KpsN%{1vrR^0ZKV>Tp=uV!@bvq(Yaq!MNW_OBNFtHgO6+WTf*6S4;^LywAQEj*gpE+f6)~hXT;Zl= z1qvc$3fO!Ro5uw;ij2KHvB(XFX8NH74*!!ZSNO3_Xu}{WgAWmG@tTsBfi&9xhjKWd z&_WR%`ES0T6&41|_y|Ntggmi;i5}d(O`1@Am@GgTBAy_a$BSF;Vn8fU#1qEy_#ipR z0i-dQY_4Yg9#5meRIX6O;4%>^#SMol*s|Fy*wMku!Q0u1=s>h{CJ-n-E<{fkik**_ zqdnP^Kqga`xfC8#%t5%KWiAWlQoIOGK90_KlP5&wwUb__0~nZssVK)%v_2xqaXF&&o9^Lf$@>D&0NxWapY+l}e{>wws&V zcgVZ8p7nm>bK9-gh3iMaRaJ59mY!?HJPwDW%t^Ygn}_kBQrildQBhITuco_te(d3HkVc^#t*NOYoIihlWPH5y8w-n^ni`krYu)tnV#vb6;%ZX$VnaiU zyH-W}Qrw0O`WVdPn3$N0b917=R}&Lm6<6q>v9zF~0X8avWR^f%ZrV*qWSsR_4f9TEG#^FG&p!XrLwYeD~S}X0Mz+;dF{@?FKKBVQWmS| zkAVS)#>U3k(9qkuL%m^I$su(EB$A_p!xp)9*C#LFP2@No1U%&Jg))m_4W0Y z7GqVQNNEo+pqzl*XYP(pPL@x*5)%`jK4@-kHfq;#W&m0z7emSQdP)gtCR43a#$pmZ zpQXipQJU%VaWQMpn38FdH(xuqn2aNfJCE+s+KYi`wkqEiEs0 zcJ46?YrfkyNuYzVox2QYGEW?Q5U-wZ*BLzhA}A>6-q6rxMV!BX#;Qz}A~Z?=@7=EE zg3Po?)y<6lh(o&dKiqJ{30C)+D#kv9@?H<{KZ zj?w9Kmh$Qov`4ZEg^7!K_gYc}AX6Ld{l;&Tj_y#JUPllHG^%M$OM7X8#i|#yJ-ofV zQe2#!jlJb^c@tf&RwwOMJ=;j*PW4zF?3=x7U<|6VFjIg^$9U(FodNUO$=b))MX&nX zy{<0b(UFm@)r#?9v@Jb)0l8ntZ9Tul5{Kj-=W;mr#wRB+yTiZrLqWdPUvHTCK20`Z z{~QCP{5pC6{@F9N4_jJViZX|u#V48%CF6#Nhqd>Fgq)qD`THL)zIo%up1{C=HkutK zCBkdXrnK}2pN<_Z_z`ffg3dM!A$ezaIzy$yn}P+&ooXvN9-dD&?{ug$CfV$$E=-x4K%t(|PPzSd!We*S4g z1hBeWrE}(A)@26XfxBmuQweUeoM$hzbVufBb)SOa2GsLZ$E|w>i)piT1HnV%%*tYadwvxA~ zZW^j<#D|2YW6f|!M@JU0L5IO&QL@1H1ak7yoZVvfOsjX@Y;Lsj(h6m`H)+Sp3rz~J z!b1LNYN}#<)QYC9YD;=!mIejORM%%p8WZZY>!YHQZoFM^B`EIPc@t&q+*YY;Xkd_Y z;ez?LG}%o4&n?QUu9~|b@jEzHyMrJR4{&xgFr&|fH{V(7?iuAr~Q zgq#u9&&^c@i#99T+kMjy-F1E$dZg>ckKK~Pzd_^&rH>1%uqzGW-)9phEa#`^wog!{ z<&zx&t(@t`+nyL70GtcTcD%d#fbWufodHQ8d0kZA=uTHHi~@>RuWA7uz%M@HmItU< Q()`X)z5OX=UO%P$2dO#lssI20 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-missed.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-missed.png new file mode 100644 index 0000000000000000000000000000000000000000..242ea4d91f8011ef946f2bdcd12c07bbf1d81493 GIT binary patch literal 2864 zcmaJ@dpwkB8-7VO>#R_$7~>PQn1h%#QiCx=OimLjhG}Lrv&_NFU?Nh}YMluw@yV$^ zvE;ClLr#^X%^^f=n$01yIgG$>jyx$owx!bO_F){Vd)QF|IV^x|L4h1T1U|?|Ndy>F9tq561TwiOz7^!7E=scBJcdHR zA0fN|E6CqPdEs5awk!@6Y-R#8Cd15OU~{+$-1Gnvi8KNuU~m`|hJeBm#&9Ic6oG=l zz@Hw7BpZj~k8;CcKjo4{RuCGG$3{V+p`oEBp{6D*&S@yz!op%x1A#D>AdI2S zn90@qtbn0%$s9VHM`tm?n~J1UtYDrML{jPBS75Ne$TGQ~+9YWhluu$q;U=)nD}4sy z@&6ynV0=Myd2ZBy>iwU>T*4VP73xOivVu8e$>99;HdC=twj3&n$KnuJtiaD*bfK|$ zEG~`32HU!uf$=0Vow*r*#N+WO2PT(CVv?y27%PZG!GunypfEOYB-Z?UQ!|9AH5`t? zS|F?~Fs4`=^8>cla9dluzsbaSx(HR#Vr68CIwzt~n1d@(+iDRDbTV(atRB^?0R zUf_VSCh%VlANQf1oY06&EXe1UZE7uOL>&EATg5|U!H(w zg~34$iqs6fJHG|Wv>4`lDj3(zKKAM_Pb_Uel2NikR~>ZFvaB|0U$w&FCOluR?df<@ z=F>WwH{A-wZA89W4$l#7M2rYu{$ak65%PYSH61c#)J@8x=LshUZH^bVfo*Me!LB2o+_RiD$iac#{g@=6nHw3)LB38j~=zCqKg=gmvZ}4IwECtud+*WfA%Exdbrlkc@D3L?<%HO z>h!pzJTh~uz1a0wb*j*e#(5}g6~phxA3o16(2?@%-WbT;Ma)T)*>xh}U;|#Ex*J1{ z*m~2W6+7OU%+1NK;A!9MICs&lrrXD8!Zi6sSv~`~r=m|wdaZ|I=6HwKCu|cVHk=gC zgBN81=Ol6c5@GcuSJ1t?p?#5QAPUC^oPyU~DCbF!4dtLd#Fo7FAKxpxQjw|kcZ&Wy3W(*qm~E86{1FURWlGDq?`1K?9OAch_l3qiEE?t(lafgk+4fKGaDsC z5g%Gda?4Zn!G>2eEk(%=itV-&t0fW#%*e(4;g`esPkLWn>I)Lb@1DJ%S3Ex4vWV!}epdBYpQIoug}aq;JkaGEhteJ( zJR5I_{ap0_bEx7O>_koaF8BT~?jFcnJZV)@`hf zJyIBLJHl5K+JjcSwFjY0w5Q|lZ6IR#T{lNfUYM1Xy>Ka@mA+F?5bf4l(9>4ZRaO;-j8ta%KNoPWtr~J4 zU?i^FIXflV&YTgrU%T0yYb1=0KN5%FssK) z*-JL?Lb_C~Zt`GU7q9z_sGH#T&O=N{S{@ReB=ryUEuO5J+&W(A6jQ5(EmysD%bwMz zu&b?khO4W^v({w0PQA&BG{0{Foh~82)6jXu+W2kR`F%5SdAjv+zwsO#Z$7>d{SCH` zVbo(fC3A3GG;s&_tDqqu@Oe)1j?*z&EWr=Q-Z*6|TB!ChitWtKT~|bE?TPhfyZ8I4 z5pO-7jQn<*+O&3CYT$|D43BmtZSBuB)Ox|)KV@M+)^+^`M-`R#oByy5c1JO#HhvfW E0`*<_qh6WS?X+#Mcfo3!k~``^J$nky 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..97d5925fe99bcdc13edf13f3fdd5be78ac7c4741 GIT binary patch literal 710 zcmV;%0y+JOP)LN;oFz6!Cyoi!63U8uD7a`qP5nbe}U08aj{SZbMK|!J=i9a;4 za?Un4GTqwd=9>FD-*<8}Ty)U`=kR{t`JU%}-uHVCM+m|HKqQ0vz$FP%@ip)w@M$iD z`tyEN7weG_bU>~aMH(I>@5XVa+ky7h79^dO_$ir)uIVKw{!RJf@q-!GS`O?{)ZcJQ z#K@0teMEDU;vgHjucLvy8k@@YiPS%En&s{)?wVbW}t)<}2BF^?3al1hkzM(Gx*I&7Fv~SE? zMK`*8iy`G{Veb-f^3FX-JxysBDNhm>rw5+PR^-DYG6%WC(}&dA6!Q~xr)G|50M{1gs&Y%o0y9MJ4lY4MR+A-$kZ9KP$kz&Rjz(Z4 z0n`-~DJNonm&#)T8DyKrz+CLt7K(@guiXKX!=*Tx&w(jdw_!BXCD?s#rnugNp~WC) s-)717tFm(;qF$%?=jK1+toS9s06>u8HYeJXegFUf07*qoM6N<$f?1D4qyPW_ literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_32x32.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..8afc795135050640ee3d060ab0fecaa41f52af9f GIT binary patch literal 2691 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-completedstudies-32x32.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-completedstudies-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..e91a61fe585ab4956f6b68b1d2f0203000697ea6 GIT binary patch literal 1781 zcmVOv6DKZS zoQyWaRt-4S`~1$1gU1hN9*6ZGhc9%X8jQB75CNrB^3wwcFBQtA0aMc`X_&;gw7#Lz_2mrqADgi`r71)De%9V&_oA6*#LYMJ{SXQ@-QUr=-t8F*UtD z$DiU6|6)p`Ts}{Cv)QpocOwEH?ccu=@9v_xr6obY(slA&O!r(E)WlmxrEJ^8W|BsX zDtM2_P}flFnvAnumy&3!IS$%FhZMDUb8 z8qkR2ovCpA9@6pNYMS_FCzYVYedH+to5%XpsEr*M8X9V23M^322JU%>`C1Jy8)Bg3 z*koZ5=F*A4uLvzOJ?wR(+RZ%hYe|rCZZe5|E5s&GjBlfzjnt5I6h)Vk9(H0MK~xwRdRi&V5aZ zedf>WbpQJ)fxm1!l!z(Bl=f%}l!~YU5)gx=m8Y4{G?c&rx4*3@HCKQsm!DX;*Y-^Io` zxs(xTzQA2N_R0&9s9p6PwDH*GLc!0P2*46l;5DNH9>t2~iDkPc8fN(KE@tW5ufCyl z+j&X7(G(rNb5`Z7!Fgb%SfIIutn54w=Uae+r>Hp!-bcMjYo)kp2=pc711c720kX1@ z=}hp+>6eWcSb|7GHUf?F1?z^^B36FV= z)yMMUE|99hDGpq5FgS=pTCetJ>C*NYvRbSLds>u&p|fY{vyrc9xl|^{b}6CzyK=CY z9%z`Q$xd)YGyG1lNgLTQ0V;UcoPu?v?F#7P{>!`OskFUF$Bw^2m7Nt{=JbZgaqFfo znqSJ{Ta)N@0b3|1w?XE(8)%AD5Rh5~J&pzLH?hVqeRJgOD2egTZj#5`8zmQJIZ2w zt~%R*@U@X~hEU*N_eNxbS4;EtYy<&_*ryiJy^_d|MU1l%t|y<#Nnnm6+C$?rOE^7C zg?eG*)}%?H@pV=3$;b%hpaKsOoZ#KF0wg>7e7R^Jj~Kr_f35DsL_fC|>+N>6TrW4i zo1)acPI_sglQz7TGX-0^yJ>N8ky1!THSD;;c^!yUgF7M9G5Y$97@52?R$r)Z%Ya1p z>UMr^U&@j`m7sTX`)FZimM*;c@5Yx>-l@s=JUEu*{|9`TfN@j#eHzQ}c0!D~wQ9ZG zn!X~&xsJB8aWxgE{_Cj*89($aI0+T-y*~}(qAT#u(Vu6T!I1+u(lq8R&@?U6Q->4v z#U>_yXjpvq=du{z4cMK?b?>1R?C9wsmsP+A2(noTIxq9JoaKnIHN{dSk>+Us0y(`U z+H%NPv+$iUT*Ln|Y5yPHNYRd*8U5H?ZWRU5vml?g;?#lk+qrF@^+TwVFKkQ!Bk{$+p@F{_7spVV z{-bwr)}WvET6C8x^u%)>E!>2RCoLM7Fep#|toz?^T?+p16FJ*Y2kCVmUqjeP-q#B zA&e=TXeMQ1MzNi7n~D2pxJ46;{DBWNn#@NuX4AMO8!^h3`2aJe#)dVzj#JsfIKk2=5Vw; z{?1Of*%XHiYYI#W@ymh3VK3NmSppz4P*+zs?sPg~v)SPJ{!6UP&4e|>%0nZUL2UVp z|6;^3Bk>o!r#B`xH8zaN(ZO!TgHQ&_54_|~vsz#$&dp|%ICn7+Oqd`z_(ukiw8F_q z3>fs8;`kXtOZ%+Y@|WV7YL3*0_E^I-CGfVNgqi4XHYL#a+B{vsl}Jnow835W5L(;4 z@+#erl%TESlEMW8m+N7ff%&E_L0YmXL4pRoOOXRafWIctN?;z$i=6U zVu2y^8Ckku=q)NilPE#FC_ylAU1b~4*4Y&z2bH4Jt6DWLM+k`UBuF9>qzN$)646VM zgJgkkC|n7YJaRhpMk>dDkzkoY$aSE)x;nA3vaSQLCn`!2qwu!?`TWV(L7HAF5(pR>^~IphI99u!up8w^M`xz zRJo1>R%22$l*EgFU_aIJ39}Vks4Z zusb9sl;$cehNtF}5a$mT6kuCzBkdo~y$4?3I5wnYse`b-zMcz8@D(1oe+}&EsgzTp z_H&x?;J>h2HLgA4lbx=81je1=oKE( zTAzIzN85${|MU7h7*dS*@K7`=K0AuUrk(OUwto&e$Nxb8yZK5GjLwOOp{Hb8DD~1z zH1`)Z*qpHjC6-LQ@Zxp&z0>ILy(&A;-rFnB{i`yU0^zNy!LmRh@|>YNp^2V|3`Fxw zav60wixdP19gAraV}yAg#K`qo)l&Es^HwukAV_`k8{B?+J=oE$WYnya1-2|wh%Dl< zoTW!Bm(xL7uh1%Dg1$R7hQOz1(9?Gt{Zm8uF%X5bui7cqyhZ2lL1ON9VZ}xRZZ^M# ziSwtC+0d(#1(a?hm4V{~8QMC#WeNDwE5*dV`*vW@ucv{>+<27SEnU5Z{(|J3U+Ilw zA7J{<42EtEVuDaLvA-PK&kp0K2AwSE`k{qn!M(8CNEWbj5azI=46G^`_pN(U1^wG; zAD-WuiY)dH3KQbs_p=xs4!{rqTB0)W_rL_^3^AF%4fWm7-3zStRm8bf+zZ%#;m4MYwKf|rA( zfhn{g&dXH{Y>7*uAj+c2zp5EhlCKRThM7TTY%J1a;uqWvX*)L9$wl%<@1x=hp~QAI z(ROp;6SV#Am#^h#N{gx5_RMUYKz>A00000NkvXXu0mjfdHIw~ literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-generic-32x32.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-generic-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..6f01eeecc63d029f160963bc490590b1701596bc GIT binary patch literal 1601 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 Wea}m#{^wf&0000pO%(x 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 ey>ppD}8~g89ZJ6T-G@yGywqCqkO^u literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback-frown-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback-frown-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..5d979ed539537d26ec5a837392c8a77bf9d5b2ce GIT binary patch literal 922 zcmV;L17-Y)P)FNl)Y2Gf0>!r5pWXKE?!CKr@0}UX-C{-N=49raGv7CJ&zXqE&@V&}b`?agAo$n{(i%=a2#9uc&z0}Dl%w$1O)q?=Q% zEjtNwFup7b_Ct5~22&V6b!WR;d1-;sdHDFxF>qnvwMVlJk`FIjh7J_=ZR&(l3^E?q zxq(0Oz8^f!1Ero>o21BGk*Tr+EhT=Pn#C`F=b=2Gtd4>4{G#tmsEPv==2_dS=6Hw| zzEV%bEz?p9B{0Fk>H*fonxGk=Y93XIlOKara*>eos#*qJ-wLrI!8%JxL%KB>Q4@-) zo^5Z#@>mqoA%f7qyA4ed%GK((NPGwpF$mil5+I-i^8-3Y)H^QU8!zL3q!yQ;83AnzkUN1t+1Rr!RY(y<|GED=Wrz50YN0pG>|V7L`Tn)`sdu0g`2(%pYl5mExf$7 z1;@TS+t~Q-j^4g#)H;}9NZw@{R?^@ij2EN|CVc4)YZPiV@93Kszx+>3FFqK>sUQ27 w6nXgJJ^#x2kvrskA(Zpmxv&2x=YIqk0B?J;R(x{V@&Et;07*qoM6N<$f)vNM9smFU literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback-smile-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback-smile-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..25c98d85870bee9a0f040dbee093821925ffb8a0 GIT binary patch literal 951 zcmV;o14#UdP)}_^uFEew_oa32QV|CzX{xkpoedjxK{*R=@1g?Kx`eawTd_6AX zJ{jGAf4(YXT*im54xgI4zMst7->)xT`)toaJzyN_>FjT8ZfG+aHa4n+0A@MuC9?_V z!t~j+OPwPx?EU=cI>9gbZ}7&K!vUF(+|m@=b#M2Bo0w)P4l%)$G8BbCM+kl*<(e4==Vv2u8 z?z%tH*4#BHG0*!7RQJx)aXk=xYe(-;L)u;-Rbk@vv02QX&!ThNO*NS(-kHYCWEQve z1Yt89vBo}QZt=>NGgBXpDjvgdd+XhijJ-@OYJz(Lv*&V{naF@K0ZKWh$NyS;vQ}~m z$U4iUqit)%$7-C3osmsZy--bqt3sYzpuQ;tAp~3rCh{-VM?>%!uMsjyQlL51-w@S( ztWVIQ>p`VjtU?PAFe1?1*NzQg1FkCx{JZRJM_ov-sqslXd5lP%u1IWDlQLkt4m=JB zSp+z?4{d?m3+c)N{A=u&Ysx87#cL8Q2|NOqGnqqGJSvh_+2llMlAUAZ*(v<=;}jP2 z^ECyIjBB}BOiWzFdrwc+R0yuXvP|w{eS+^+SCg(rjGDZbSQOtLn?oVv)WEL8Q7Tf5 zeVmZB&7iU;KLJV@45ps7Zxxxwt{T3g2d%KrcNx$ zc~CKQU;tFBAgG4K&y(je)Md}9@%Yw^ttRWMcDc7P+H7#0VaN7uFmpwu=1f#fhFE+v z9zE~?%0?CnRQ!7Bie;|l-aUHwi{o0URE8QBFQ?U}s-ET&P=79(i&A!N7*v zAXRAvE5#)+yUKsNG+oRrt@)#T*sp2+C?9`)l*)YH{%0OOm`mjjwRYd$WZsadm!;QFdhH&C!3&)br4-IQiB4>yCWlsonqBx&ME59`o(|!P_VQW9L5w Z7y!E;#^kiv1Cjs$002ovPDHLkV1i=9!cYJJ literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback.css b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback.css new file mode 100644 index 000000000000..343cfd581af1 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback.css @@ -0,0 +1,27 @@ +#feedback-menu-button { + -moz-appearance: button; +} + +#pilot-notification-popup { + -moz-appearance: none; + background-color: Menu; + background-image: -moz-linear-gradient(hsla(0,0%,100%,.2), transparent); + -moz-box-shadow: inset 0 0 10px hsla(0,0%,100%,.2), + inset 0 1px 0 hsla(0,0%,100%,.3); + -moz-border-radius: 4px; + border: 1px solid Menu; + margin: -6px 0 0 0; + width: 480px; +} + +.tail-up, +.tail-down { + -moz-border-image: none; +} + +.pilot-notification-popup-container { + -moz-appearance: none; + margin: 0; + padding: 10px; + font-size: 14px; +} \ No newline at end of file diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/close_button.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/close_button.png new file mode 100644 index 0000000000000000000000000000000000000000..3170cd9554d38705c1a9fd9439afb19595bfacd7 GIT binary patch literal 1576 zcmV+@2G{wCP)sEJ|IZl|;ma ziX@7`8Ze<+-;S9X$C2i!5Kjs6Jbw}oWmqX#N(7*QwT$%}FblXE&YwU3<%<_De(-v| znMW~UHk)7P=H~u9F)=Yl&pA0c-!3jL*6#1`JCDK{jm9@8PMqkSo0}V^xXR1R6P`VL z=G@uYF&%}Aj*j*f6&1Z192{I%6O<&WIS>fE!+$=VouW%j`Bnks3rSn@%p`(ylpRN3S4OsT=c z86~zyz8^k(cq}U`%SIKV1qQ9Xy}ieor1qGtx(SjqFlSt$veV77vR1xrEleSLk->({Sa;WQZ~eubd8=g*&?N={Dpg1wBInwq~3V-Exb zZ``x44Yt$;tir z`1r)_?d=fZkeq<~^}@nJ5jb)t0jEk7xIc*~*b8^>-VNXy4LK2x10g(cuKV}z3u|j@ zsse!f3fFz$vdACywt$eoQ87P=JRS@NIrv$6^yrar=gyrp#O`7KUg;AgZ5t`b(b3T* zN|Ius3|Y z+`D(rn~;zYL;r1UZMj-nT8x{Un?Y6uLs){UKPbx9(9rN21kse`&>|xCIq7!0T{?fS zgs=gmfI-r(onj<^D8JQav-ysguBwdSTw#NQ9Qtw3V?Zcj5<3)!Km=4Tz!oP6!Uu7P zlN4hl<<`XE3=|&K zv!T%)sNBx_`uY|s;+UG6vSaW>I~y#*m4|==Y#%q=0DZL*3x{4-S5$;o)He z`re1iSy4rMZEdZ0WMssE!DH0p-U}EdPtbafWO?!8#SA35nV6V3*Vx#Y5Q)yFrX~f! zikd9*^75=38yl;TpNO7_Sa}SX5Y$1vs>%`)GXuASN(eafB3AIql`Aw*-cZkxEW5h8 zGP}FGrE}-bE#W`$JvLYm*0Uar!Nl_n3=G7g=V+kN2#>vd`SLb4lbLKSz;x?}3|3To zd;33_rt#=GHKDY$H1X!mo9iGZNjrkVhYY=|sHoU}`t+#{y{9G=8yhPW7Z-ujuHaV z{{p0E!F1~)JG8z9@-?AqwJys1t-=*LxzO}g%6@G zG6@}R8O}d#uI=7?`aV6Hdf0F0e&7A>_xBu{PfjieAyiCkWs$WwSzs93$$8-WOEd#KprFCNlq7;p~@MBCD8bm%04_mfFp(Mg(T(cfedURt$ zGH^R|vJ;D4*e~HO1dw^x{{kVtqpasna0=)#lQ$OSfFP&?iXw`V$rPoLi1Bc?7~x_^ z$67&M-LB2))Pf5O!s7UN%Iesdd}xEENk-$|ehDc#IX~2;rSmwhgu$S;%*-t3l$87e zF$jdM4u_?%zCP<)d;9jA>S`xd6pGDGmgO~cpp#2W-^F;^mpz`Za&hsek=E8=kJVas zlhW>9o9)tQTU#Kns_IMG(2yU)B4;Wq-|nXq35SaY?e?Gdp%?LJbnx-(s7avK1Vs9&6$jIXJsI zi)CF%BvM;^zT4Fm6~lNEbp@1a=W1$(I_BmE)9v=Kk#}0-_dw$^jvC*0f?aNT`RGwY z^Z(y0w5fgH-+z5&V!{<$UCq{DIy#tAQf_X*(bza&+1EFTCmMoAjxH0{acVgM7m&>Z jNBHTT2Nq8o{}W&UnMNaKkl0$$00000NkvXXu0mjfg0E{6 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback-smile-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback-smile-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..bd13830157e48180430c3295ca8aa39262ae9079 GIT binary patch literal 933 zcmV;W16urvP)xhjpiufRZV>6MqSXx>IAwqohqo|M{L8$(S5A>t;LsAhGDl}k<$k0|0ZD}e= z2q9^UY3(DnlhkG&cjnHWdye}KR9d~TIrr?l*1Bu&b2e*z47WXQi=AaM%Ss9!WsGHn z5L3K?(;^lVr5lxrTd`M93b303&Kf%{sqBDivLVfqpgIy{nIVn^qB2K4;E1Z?FptD= zd9_lwDWEj}y4GH=TJkTN!+fSu(gv)w4Ao+Y0)a|_0~&*3NkdUd^t;T?kej~}u0ob& z)6);>KKE53zBaILkx9VV@9%XRo`f3b7|!im6r}M*ezZF(!4C_@0TLKY1SHK;w(ITj z!)v=_=(dc({v9(zPwbi|4QQ%_WZm4iW9Ee?cTFeXTkcVy`HjsjzVl+!L8{Xm*8KpM zryvA}5PX4i#tlt|X((t=NzI&K+%%~ymw~qln8F(&{&$LG*E1GbH7ps5kT5ZMFjWjz zbl1~h6G3>uk;$)9e^-`u+f^V_nIhtdSEG%I1NPKtN~F7WSR}w}Kv*Q}h6JyTO0vjR zrGeF}F`|&eATg!`K@el*d=wvlzo1Bt=>)1mmBx&~9Fi>Hv?wqw6-nM85oe7|M+(%+ zqKQ*v3B;>5o9Mfp7Z@dZ1bq4O^jvRyZmCjipjM1gFUJT=96>3@rnbSQuU?s*<8^XD z__W=Au_@5gvFUgt6o?~%(`S$VHj@2t!4130D}RP4P$X-zPx2?`fBZGU#e{$8?*4qU z2c_V~BX5p9bEG1c+ss<}$_Kj-f0OcBpFOpZ_byzU*SlJK!ruD_ir0(Rj=eLM+ZpQ1 z9Uabw`Hx@vX&dop8Qpbh^4!tU&+;R02SM49s@6*G@mI&6-k3AjrPsTc3g=2c&x}{c zPwdZeBe=t~obtQbgL|Lb*0=xPS)iBH#pN?^e4c-6`{L4UPh1t?G3krMBh^U_lud^2 z_6O%*ANcg!yY9U2BfpQ8ZyP4s&-B-`QJ3&GNKRAlA-9NNJJCxWr`^x&i!|s{}EsSKBH|jJJ7$>00000NkvXX Hu0mjfA1}dB literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback.css b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback.css new file mode 100644 index 000000000000..c2285ebfdea1 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback.css @@ -0,0 +1,34 @@ +/* Submit Button Style */ + +#pilot-notification-submit { + -moz-appearance: none; + background: #666 + -moz-linear-gradient(rgba(110,110,110,.9), rgba(70,70,70,.9) 49%, + rgba(60,60,60,.9) 51%, rgba(50,50,50,.9)); + background-clip: padding-box; + background-origin: padding-box; + -moz-border-radius: 12px; + border: 1px solid rgba(0,0,0,.65); + -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), + inset 0 0 1px rgba(255,255,255,.1), + 0 1px 0 rgba(255,255,255,.1); + color: #fff; + text-shadow: 0 -1px 0 rgba(0,0,0,.5); +} + +#pilot-notification-submit:hover:active { + background: -moz-linear-gradient(rgba(40,40,40,.9), rgba(70,70,70,.9)); + -moz-box-shadow: inset 0 0 3px rgba(0,0,0,.2), + inset 0 1px 7px rgba(0,0,0,.4), + 0 1px 0 rgba(255,255,255,.1); +} + +/* Text Colors */ + +.pilot-notification-popup-container { + color: #fff; +} + +.notification-link { + color: #fff; +} diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/notification-tail-down.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/notification-tail-down.png new file mode 100644 index 0000000000000000000000000000000000000000..8468474c4c84ad1622b1973b5693d6e5bd84a34f GIT binary patch literal 2286 zcmV)u+A`T1YD;gpd%Fgm4L$ zTpf})7~Ao6xBs)936qT-NO4SUpOHozuV;sy-~7*+GZvboH#awHTs9Yj5o|4Hu`gn= zn2w9AhVr*wf#bB5YRSK^UAtzVKYxBpW8G~&3Jj0e>#a9UGhi6TetZs~Hqd?fY=yv} zl1ELUu2_~em&@f=ve|4}+_%BNE(!%eND*LQm;lo1@pwKxb?VeN$B!TXIur^8@Y!3b zCXvtQvkMCgD?>v=zumlf^Cx_s1ejGo%8`1&!P){v0zviG*VlKpx3`}@bLPx9vpxL(V)QWSQhM{4SwZ_Iq-RJY^i;Ii%SFc|EJ`#ysPp8w1Xp&XIcmP0}3<0S_ zX$u4bUtPFx;YTPVQ0YJ%8^U|TWVBQ&r2&HN_xqcV9XodN-o1O%nM`I8sv*;|@SR3H z94PqUCyR@A}f@kLH%|(9Bzq5qwDkY^W(S|7izIhxzZ36*xtdTM~{9f zO{SVlGMOZ|VM73?;e3C=t%&d~`AtwzLv1Kzd!&+TFEsfb9UVa$w;2CA@sLnVannz+ z2^v?dQ{73S$!~3Kb;d2mZ%QbJ_)tf|85E+!8C5?Kav-nWk*nI*wM05iWN2=RtiI5D7y-! zSVMNStQ3aAP#6kBVMohK;W&lOhs}qfR4^3&FQe>J*nAiYL*d|!9W6uQIE9tMN@1n2 zQfjm?6gD3=A65!0g_Xie;U&3Z%%_;7SShR&cC@S%RthVHmBOnJLt*n_N6SiKrLa<1 zDZC_eDQrH)B*jW$rLd!ArLa<1DeP$3eAs+gDXbKBv}`_XJ~g8h`wh67REn)~FgAa0 z=M2NJ=t%XNaHtq3DvjGxz&KFUbPVg3Wu;BitO+P|mVP#yrQ_j~nv#yfRDgho6XBZOmfvoSn-QAxxH8nNU@$u-~yPb0A&|5fb=b(Pd zd$F4{PJ@KA)VA|~1O*VXwDjVgVo#ntnTSTC_wk-n0P+k?7EqT36k3V$PAVIb#)ecv z=f_iSn^x3z)DFN2p~En)`%w34Z_byO>QS!-+B6+sGiVb>jbcLQaU~FxW@1UGBqmHJN9k-#*TZQ+Os+^NbPxko z>cl71v~lRZ@}AQCyny0+@ZiA=p8KMsqa%bDFDV!T0^k}Jgt^Jd$w9mu4KgWKk4-_C z_?#C`lvI!q0HUJ4Qp_eVpLL*6dMK#H5*!X;h4%J#ZGL|KIr_hef9DC!z8LJrlB$fsQdZZr0!G%PxG$3)*_(FB zE--3G2?3){dy|F0om6cjX%gxlyyz-kIQi(&BYOTTfcY3R`(9uO2q|QCcJ?v)y@CE8 zidADxNIz4+gluJkbD~XJsS>26<_+5v&nsXg<*cA<7EnkbpW+pMDMogV7tde-6e*uR zaNxiv1csLQofb;WL<)KF;>ANa%IlQx3;oXt2PyqQwun!0D@N9h2-clQ&B+AiegVpc zB*J_*WtDVFsVaThj%FZ-mrt*)t;OK1{o!!9l~Pc>>k;(uPZEgz?Af!a>FMd;@$5MI zoWa+N0tzW5F2JOeYf5=fRTP zJZ(WS5bNpFr=%F`(4j+zNihWHU4X!3Y`~ZVlpA<_0^QAum19*|Mje&VAWO0Za4Th} zrLV8gxt=(2LK_$u&=kA26z@-P(lE`}0MxvH|Na1_JNudxo z5K^&y>C&bD1Y~4nWcQ#H0h5=GOq!AcCP-l1fuITmo6|8w)_*8KD6Uu$=JMV^$^xYT z7*#DBF~e&V473T(Hwee8OCLi(8X6iLAA(qCWNvQmPjagGYDNjPN(Q8CP+VZ_0$_3y z41o9!9z57iU`R0nh;{$|{h>%C(hndH&~{FkOtjQMb`45zZ*Rf-1ZHq>a62$r>_uJy zNCUuhV*g_7+qW-2Ha0eZVDB&5nau!Z?7=XrMM$<|RB18CH0a6T# zUarjAO3GnV?4x|*r-~NZ8i@N@Wz!ZDf6HIQwnU!0RrGSE8+U=J7jPQI--fbNmr-fi zfs`Fe0WhX(lPR|qS#iym8^{NN;sWDNWxgdPc)5X4`A2{O0C?->LFcW`yZ`_I07*qo IM6N<$f_`i=XaE2J literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/notification-tail-up.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/notification-tail-up.png new file mode 100644 index 0000000000000000000000000000000000000000..f6ce0ce78d661610898f4de159af870a1b118fa9 GIT binary patch literal 6964 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 ey>ppD}8~g89ZJ6T-G@yGywqCqkO^u literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback-frown-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback-frown-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..8da586279d8931d6adf91df1a2565755401ce240 GIT binary patch literal 810 zcmV+_1J(SAP)6F(yZx%LDM%&{i$+M~un;&L3?LJa zKoP;zKqU!$9}ghC9Lf4Q;j)cOrD}2|kd1l;2U#sthCz~|h8a{ZXE|bp0xfhCj7U=? zF|tJ%%Um!FRpFX7nn@;f!^~ZmaO%D$>^|Xz^T0u@%+Dem3Su=Fgfm6N0!Il=&AOJY zYsj_#^#e*aNfNSH__xp~|KImHFw93~nNJW-;6)r4b6~Hoo`^%jDv_0Uccbj27w;lh%KVS8#kB|iA3El z7a~8FSAE07mu@sPJQ1U;oRTC?ixJ)9F`u@yTu)1=UFX{g)@h?!)K{k`C+}3$)lK}F zMDOOy(nuW*wzr>M7#lmDSX%PXjOfs`sI|28liTN;Zs_fONSGN)n%wppw>;5(q}(K8 oJOrr#|0G$Uz_0&4=idSh04wl7MbM8DIsgCw07*qoM6N<$g5=<9{Qv*} literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback-smile-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback-smile-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..2e0bc53cbcdd0b7183c04042afc7e33b94dc09e1 GIT binary patch literal 844 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 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/notification-tail-up.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/notification-tail-up.png new file mode 100644 index 0000000000000000000000000000000000000000..2f40b1a833cd901a8c60e7639ff84891119909a7 GIT binary patch literal 5463 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 N002ovPDHLkV1nRWtF8b5 literal 0 HcmV?d00001 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/tests/test_data_store.js b/browser/app/profile/extensions/testpilot@labs.mozilla.com/tests/test_data_store.js new file mode 100644 index 000000000000..d07edd549976 --- /dev/null +++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/tests/test_data_store.js @@ -0,0 +1,309 @@ +EXPORTED_SYMBOLS = ["runAllTests"]; +var Cu = Components.utils; +var testsRun = 0; +var testsPassed = 0; + +function cheapAssertEqual(a, b, errorMsg) { + testsRun += 1; + if (a == b) { + dump("UNIT TEST PASSED.\n"); + testsPassed += 1; + } else { + dump("UNIT TEST FAILED: "); + dump(errorMsg + "\n"); + dump(a + " does not equal " + b + "\n"); + } +} + +function cheapAssertEqualArrays(a, b, errorMsg) { + testsRun += 1; + let equal = true; + if (a.length != b.length) { + equal = false; + } else { + for (let i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + equal = false; + } + } + } + if (equal) { + dump("UNIT TEST PASSED.\n"); + testsPassed += 1; + } else { + dump("UNIT TEST FAILED: "); + dump(errorMsg + "\n"); + dump(a + " does not equal " + b + "\n"); + } +} + +function cheapAssertRange(lowerBound, value, upperBound, errorMsg) { + testsRun += 1; + if (lowerBound <= value && value <= upperBound) { + dump("UNIT TEST PASSED.\n"); + testsPassed += 1; + } else { + dump("UNIT TEST FAILED: "); + dump(errorMsg + "\n"); + dump(value + " is outside the range of " + lowerBound + " to " + + upperBound + "\n"); + } +} + +function cheapAssertFail(errorMsg) { + testsRun += 1; + dump("UNIT TEST FAILED: "); + dump(errorMsg + "\n"); +} + +function testFirefoxVersionCheck() { + Cu.import("resource://testpilot/modules/setup.js"); + cheapAssertEqual(true, TestPilotSetup._isNewerThanFirefox("4.0")); + cheapAssertEqual(false, TestPilotSetup._isNewerThanFirefox("3.5")); + cheapAssertEqual(false, TestPilotSetup._isNewerThanFirefox("3.6")); +} + +function testStringSanitizer() { + Cu.import("resource://testpilot/modules/string_sanitizer.js"); + var evilString = "I *have* (evil) ^characters^ [hahaha];"; + dump("Sanitized evil string is " + sanitizeString(evilString) + "\n"); + cheapAssertEqual(sanitizeString(evilString), + "I ?have? ?evil? ?characters? ?hahaha??"); +} + +function testTheDataStore() { + // Geez, async unit tests are a pain. + Cu.import("resource://testpilot/modules/experiment_data_store.js"); + + var columns = [{property: "prop_a", type: TYPE_INT_32, displayName: "Length"}, + {property: "prop_b", type: TYPE_INT_32, displayName: "Type", + displayValue: ["Spam", "Egg", "Sausage", "Baked Beans"]}, + {property: "prop_c", type: TYPE_DOUBLE, displayName: "Depth"}, + {property: "prop_s", type: TYPE_STRING, displayName: "Text"} + ]; + + var fileName = "testpilot_storage_unit_test.sqlite"; + var tableName = "testpilot_storage_unit_test"; + var storedCount = 0; + var store = new ExperimentDataStore(fileName, tableName, columns); + + + store.storeEvent({prop_a: 13, prop_b: 3, prop_c: 0.001, prop_s: "How"}, + function(stored) { + storedCount++; + if (storedCount == 4) { + _testTheDataStore(store); + } + }); + store.storeEvent({prop_a: 26, prop_b: 2, prop_c: 0.002, prop_s: " do"}, + function(stored) { + storedCount++; + if (storedCount == 4) { + _testTheDataStore(store); + } + }); + store.storeEvent({prop_a: 39, prop_b: 1, prop_c: 0.003, prop_s: " you"}, + function(stored) { + storedCount++; + if (storedCount == 4) { + _testTheDataStore(store); + } + }); + store.storeEvent({prop_a: 52, prop_b: 0, prop_c: 0.004, prop_s: " do?"}, + function(stored) { + storedCount++; + if (storedCount == 4) { + _testTheDataStore(store); + } + }); + + + function _testTheDataStore(store) { + cheapAssertEqualArrays(store.getHumanReadableColumnNames(), ["Length", "Type", "Depth", "Text"], + "Human readable column names are not correct."); + cheapAssertEqualArrays(store.getPropertyNames(), ["prop_a", "prop_b", "prop_c", "prop_s"], + "Property names are not correct."); + store.getAllDataAsJSON(false, function(json) { + var expectedJson = [{prop_a: 13, prop_b: 3, prop_c: 0.001, prop_s: "How"}, + {prop_a: 26, prop_b: 2, prop_c: 0.002, prop_s: " do"}, + {prop_a: 39, prop_b: 1, prop_c: 0.003, prop_s: " you"}, + {prop_a: 52, prop_b: 0, prop_c: 0.004, prop_s: " do?"}]; + cheapAssertEqual(JSON.stringify(json), + JSON.stringify(expectedJson), + "Stringified JSON does not match expectations."); + + _testTheDataStore2(store); + }); + } + + function _testTheDataStore2(store) { + // Let's ask for the human-readable values now... + store.getAllDataAsJSON(true, function(json) { + var expectedJson = [{prop_a: 13, prop_b: "Baked Beans", prop_c: 0.001, prop_s: "How"}, + {prop_a: 26, prop_b: "Sausage", prop_c: 0.002, prop_s: " do"}, + {prop_a: 39, prop_b: "Egg", prop_c: 0.003, prop_s: " you"}, + {prop_a: 52, prop_b: "Spam", prop_c: 0.004, prop_s: " do?"}]; + cheapAssertEqual(JSON.stringify(json), + JSON.stringify(expectedJson), + "JSON with human-radable values does not match expectations."); + _testDataStoreWipe(store); + }); + } + + function _testDataStoreWipe(store) { + // Wipe all data and ensure that we get back blanks: + store.wipeAllData(function(wiped) { + store.getAllDataAsJSON(false, function(json) { + var expectedJson = []; + cheapAssertEqual(JSON.stringify(json), + JSON.stringify(expectedJson), + "JSON after wipe fails to be empty."); + }); + }); + } +}; + + +function testTheCuddlefishPreferencesFilesystem() { + + // Put some sample code into various 'files' in the preferences filesystem + // Get it out, make sure it's the same code. + // Shut it down, start it up, make sure it's still got the same code in it. + // Put in new code; make sure we get the new code, not the old code. + var Cuddlefish = {}; + Cu.import("resource://testpilot/modules/lib/cuddlefish.js", + Cuddlefish); + let cfl = new Cuddlefish.Loader({rootPaths: ["resource://testpilot/modules/", + "resource://testpilot/modules/lib/"]}); + let remoteLoaderModule = cfl.require("remote-experiment-loader"); + let prefName = "extensions.testpilot.unittest.prefCodeStore"; + let pfs = new remoteLoaderModule.PreferencesStore(prefName); + let contents1 = "function foo(x, y) { return x * y; }"; + let contents2 = "function bar(x, y) { return x / y; }"; + + let earlyBoundDate = new Date(); + pfs.setFile("foo.js", contents1); + pfs.setFile("bar.js", contents2); + let lateBoundDate = new Date(); + + let path = pfs.resolveModule("/", "foo.js"); + cheapAssertEqual(path, "foo.js", "resolveModule does not return expected path."); + path = pfs.resolveModule("/", "bar.js"); + cheapAssertEqual(path, "bar.js", "resolveModule does not return expected path."); + path = pfs.resolveModule("/", "baz.js"); + cheapAssertEqual(path, null, "Found a match for nonexistent file."); + + let file = pfs.getFile("foo.js"); + cheapAssertEqual(file.contents, contents1, "File contents do not match."); + cheapAssertRange(earlyBoundDate, pfs.getFileModifiedDate("foo.js"), lateBoundDate, + "File modified date not correct."); + + file = pfs.getFile("bar.js"); + cheapAssertEqual(file.contents, contents2, "File contents do not match."); + cheapAssertRange(earlyBoundDate, pfs.getFileModifiedDate("bar.js"), lateBoundDate, + "File modified date not correct."); + + delete pfs; + let pfs2 = new remoteLoaderModule.PreferencesStore(prefName); + + file = pfs2.getFile("foo.js"); + cheapAssertEqual(file.contents, contents1, "File contents do not match after reloading."); + file = pfs2.getFile("bar.js"); + cheapAssertEqual(file.contents, contents2, "File contents do not match after reloading."); + + let contents3 = "function foo(x, y) { return (x + y)/2; }"; + + pfs2.setFile("foo.js", contents3); + file = pfs2.getFile("foo.js"); + cheapAssertEqual(file.contents, contents3, "File contents do not newly set contents."); +} + +function testRemoteLoader() { + // this tests loading modules through cuddlefish from prefs store... + // Need to make sure that a + var Cuddlefish = {}; + Cu.import("resource://testpilot/modules/lib/cuddlefish.js", + Cuddlefish); + let cfl = new Cuddlefish.Loader({rootPaths: ["resource://testpilot/modules/", + "resource://testpilot/modules/lib/"]}); + let remoteLoaderModule = cfl.require("remote-experiment-loader"); + + var indexJson = '{"experiments": [{"name": "Foo Study", ' + + '"filename": "foo.js"}]}'; + + var theRemoteFile = "exports.foo = function(x, y) { return x * y; }"; + + let getFileFunc = function(url, callback) { + if (url.indexOf("index.json") > -1) { + if (indexJson != "") { + callback(indexJson); + } else { + callback(null); + } + } else if (url.indexOf("foo.js") > -1) { + callback(theRemoteFile); + } else { + callback(null); + } + }; + + let remoteLoader = new remoteLoaderModule.RemoteExperimentLoader(getFileFunc); + + remoteLoader.checkForUpdates(function(success) { + if (success) { + let foo = remoteLoader.getExperiments()["foo.js"]; + cheapAssertEqual(foo.foo(6, 7), 42, "Foo doesn't work."); + } else { + cheapAssertFail("checkForUpdates returned failure."); + } + + /* Now we change the remote code and call checkForUpdates again... + * test that this results in the newly redefined version of function foo + * being the one that is used. (Note that this failed until I modified + * remoteExperimentLoader to reinitialize its + * Cuddlefish.loader during the call to checkForUpdates. */ + theRemoteFile = "exports.foo = function(x, y) { return x + y; }"; + remoteLoader.checkForUpdates( function(success) { + if (success) { + let foo = remoteLoader.getExperiments()["foo.js"]; + cheapAssertEqual(foo.foo(6, 7), 13, "2nd version of Foo doesn't work."); + } else { + cheapAssertFail("checkForUpdates 2nd time returned failure."); + } + /* For the third part of the test, make getFileFunc FAIL, + * and make sure the cached version still gets used and still works! */ + indexJson = ""; + remoteLoader.checkForUpdates( function(success) { + cheapAssertEqual(success, false, "3rd check for updates should have failed."); + let foo = remoteLoader.getExperiments()["foo.js"]; + cheapAssertEqual(foo.foo(6, 7), 13, "Should still have the 2nd version of Foo."); + }); + }); + }); +} + +function testRemotelyLoadTabsExperiment() { + + // TODO: Stub out the function downloadFile in remote-experiment-loader with + // something that will give us the files from the local repo on the disk. + // (~/testpilot/website/testcases/tab-open-close/tabs_experiment.js) +} + +function runAllTests() { + testTheDataStore(); + testFirefoxVersionCheck(); + testStringSanitizer(); + //testTheCuddlefishPreferencesFilesystem(); + //testRemoteLoader(); + dump("TESTING COMPLETE. " + testsPassed + " out of " + testsRun + + " tests passed."); +} + +//exports.runAllTests = runAllTests; + + +// Test that observers get installed on any windows that are already open. + +// Test that every observer that is installed gets uninstalled. + +// Test that the observer is writing to the data store correctly. \ No newline at end of file diff --git a/browser/locales/en-US/feedback/main.dtd b/browser/locales/en-US/feedback/main.dtd new file mode 100644 index 000000000000..6e4650b68d76 --- /dev/null +++ b/browser/locales/en-US/feedback/main.dtd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/locales/en-US/feedback/main.properties b/browser/locales/en-US/feedback/main.properties new file mode 100644 index 000000000000..26cd3e215ff3 --- /dev/null +++ b/browser/locales/en-US/feedback/main.properties @@ -0,0 +1,92 @@ +# common +testpilot.fullBrandName = Mozilla Labs Test Pilot +testpilot.moreInfo = More Info +testpilot.submit = Submit +testpilot.takeSurvey = Take the Survey + +# Feedback button menu +testpilot.turnOn = Turn On User Studies +testpilot.turnOff = Turn Off User Studies + +# studies window +testpilot.studiesWindow.noStudies = We are working on a new study now; it will knock on your door soon! Stay Tuned! +testpilot.studiesWindow.uploading = Uploading... +testpilot.studiesWindow.unableToReachServer = Unable to reach Mozilla; please try again later. +testpilot.studiesWindow.thanksForContributing = Thanks for contributing! +testpilot.studiesWindow.finishedOn = Finished on %S +testpilot.studiesWindow.canceledStudy = (You canceled this study) +testpilot.studiesWindow.willStart = Will start on %S +testpilot.studiesWindow.gatheringData = Currently gathering data. +testpilot.studiesWindow.willFinish = Will finish on %S +testpilot.studiesWindow.proposeStudy = Propose your own study + +# for pages +testpilot.page.commentsAndDiscussions = Comments & Discussions » +testpilot.page.proposeATest = Propose a Test » +testpilot.page.testpilotOnTwitter = @MozTestPilot on Twitter » + +# status page +testpilot.statusPage.uploadingData = Now uploading data... +testpilot.statusPage.uploadError = + Oops! There was an error connecting to the Mozilla servers. Maybe your network connection is down?

Test Pilot will retry automatically, so it's OK to close this page now. +testpilot.statusPage.endedAlready = (It has already ended and you should not be seeing this page) +testpilot.statusPage.todayAt = today, at %S +testpilot.statusPage.endOn = on %S +testpilot.statusPage.extension = 1 extension +testpilot.statusPage.extensions = %S extension +testpilot.statusPage.recursEveryNumberOfDays = This test recurs every %S days. Each time it completes: +testpilot.statusPage.askMeBeforeSubmitData = Ask me whether I want to submit my data. +testpilot.statusPage.alwaysSubmitData = Always submit my data, and don't ask me about it. +testpilot.statusPage.neverSubmitData = Never submit my data, and don't ask me about it. +testpilot.statusPage.loading = Loading, please wait a moment... + +# quit page +testpilot.quitPage.aboutToQuit = You are about to quit the "%S" study. +testpilot.quitPage.optionalMessage = (Optional) If you have a minute, please let us know why you have chosen to quit the study. +testpilot.quitPage.reason = Reason: +testpilot.quitPage.recurringStudy = This is a recurring study. Normally we will let you know the next time we run the study. If you never want to hear about this study again, check the box below: +testpilot.quitPage.quitFoever = Quit this recurring study. +testpilot.quitPage.quitStudy = Quit the Study » + +# welcome page +testpilot.welcomePage.thankYou = Thank You for Joining the Test Pilot Team! +testpilot.welcomePage.gettingStarted = Getting Started +testpilot.welcomePage.pleaseTake = Please take the +testpilot.welcomePage.backgroundSurvey = Pilot Background Survey +testpilot.welcomePage.clickToOpenStudiesWindow = Click here to see the studies that are currently running. +testpilot.welcomePage.testpilotAddon = Test Pilot Add-on +testpilot.welcomePage.iconExplanation = « look for this icon in the bottom right of the browser window. +testpilot.welcomePage.moreIconExplanation = You can click on it to get the main Test Pilot menu. +testpilot.welcomePage.notificationInfo = The icon will pop up a notification when a study needs your attention. +testpilot.welcomePage.copyright = Copyright © 2005-2009 Mozilla. All rights reserved. +testpilot.welcomePage.privacyPolicy = Privacy Policy +testpilot.welcomePage.legalNotices = Legal Notices + +# survey page +testpilot.surveyPage.saveAnswers = Save Answers +testpilot.surveyPage.submitAnswers = Submit Answers +testpilot.surveyPage.changeAnswers = Change Answers +testpilot.surveyPage.loading = Loading, please wait a moment... +testpilot.surveyPage.thankYouForFinishingSurvey = Thank you for finishing this survey. Your answers will be uploaded along with the next set of experimental data. +testpilot.surveyPage.reviewOrChangeYourAnswers = If you would like to review or change your answers, you can do so at any time using the button below. + +# modules/task.js +testpilot.finishedTask.finishedStudy = Excellent! You finished the %S Study! +testpilot.finishedTask.allRelatedDataDeleted = All data related to this study has been removed from your computer. + +# modules/setup.js +testpilot.notification.update = Update... +testpilot.notification.thankYouForUploadingData = Thanks! +testpilot.notification.thankYouForUploadingData.message = Thank you for uploading your data. +testpilot.notification.readyToSubmit = Ready to Submit +testpilot.notification.readyToSubmit.message = The Test Pilot "%S" study is finished gathering data and is ready to submit. +testpilot.notification.newTestPilotStudy = New Test Pilot Study +testpilot.notification.newTestPilotStudy.message = The Test Pilot "%S" study is now beginning. +testpilot.notification.newTestPilotSurvey = New Test Pilot Survey +testpilot.notification.newTestPilotSurvey.message = The Test Pilot "%S" survey is available. +testpilot.notification.newTestPilotResults = New Test Pilot Results +testpilot.notification.newTestPilotResults.message = New results are now available for the Test Pilot "%S" study. +testpilot.notification.autoUploadedData = Thank you! +testpilot.notification.autoUploadedData.message = The Test Pilot "%S" study is completed and your data has been submitted! +testpilot.notification.extensionUpdate = Extension Update +testpilot.notification.extensionUpdate.message = One of your studies requires a newer version of Test Pilot. You can get the latest version using the Add-ons window. From 703036d279bf4d36af26bb968e2dbdbef68f0d9c Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Thu, 24 Jun 2010 16:36:31 -0700 Subject: [PATCH 1798/1860] Bug 573079: Package Beta Feedback extension in beta release builds. r=ted, r=beltzner --- browser/app/profile/extensions/Makefile.in | 14 ++++++++++++++ browser/installer/package-manifest.in | 3 +++ browser/locales/jar.mn | 5 +++++ build/automation.py.in | 2 ++ config/autoconf.mk.in | 1 + configure.in | 1 + 6 files changed, 26 insertions(+) diff --git a/browser/app/profile/extensions/Makefile.in b/browser/app/profile/extensions/Makefile.in index 044061f853f8..0b949851f6c7 100644 --- a/browser/app/profile/extensions/Makefile.in +++ b/browser/app/profile/extensions/Makefile.in @@ -45,3 +45,17 @@ include $(DEPTH)/config/autoconf.mk DIRS = {972ce4c6-7e08-4474-a285-3208198ce6fd} include $(topsrcdir)/config/rules.mk + +ifeq (beta,$(MOZ_UPDATE_CHANNEL)) +EXTENSIONS = \ + testpilot@labs.mozilla.com \ + $(NULL) + +define _INSTALL_EXTENSIONS +$(PYTHON) $(topsrcdir)/config/nsinstall.py $(wildcard $(srcdir)/$(dir)/*) $(DIST)/bin/extensions/$(dir) + +endef # do not remove the blank line! + +libs:: + $(foreach dir,$(EXTENSIONS),$(_INSTALL_EXTENSIONS)) +endif diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index a72e5cd41e8e..393facdab411 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -363,6 +363,9 @@ @BINPATH@/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf @BINPATH@/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/icon.png @BINPATH@/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/preview.png +#if MOZ_UPDATE_CHANNEL == beta +@BINPATH@/extensions/testpilot@labs.mozilla.com/* +#endif @BINPATH@/chrome/toolkit.jar @BINPATH@/chrome/toolkit.manifest @BINPATH@/@PREF_DIR@/reporter.js diff --git a/browser/locales/jar.mn b/browser/locales/jar.mn index 74ee6128f3a6..f3f87227e51e 100644 --- a/browser/locales/jar.mn +++ b/browser/locales/jar.mn @@ -71,3 +71,8 @@ % override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd % override chrome://global/locale/appstrings.properties chrome://browser/locale/appstrings.properties % override chrome://mozapps/locale/downloads/settingsChange.dtd chrome://browser/locale/downloads/settingsChange.dtd +#if MOZ_UPDATE_CHANNEL == beta +% locale testpilot @AB_CD@ %locale/feedback/ + locale/feedback/main.dtd (%feedback/main.dtd) + locale/feedback/main.properties (%feedback/main.properties) +#endif diff --git a/build/automation.py.in b/build/automation.py.in index 03bcde8aa889..fa601460bf8f 100644 --- a/build/automation.py.in +++ b/build/automation.py.in @@ -330,6 +330,8 @@ user_pref("security.warn_viewing_mixed", false); // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION user_pref("extensions.enabledScopes", 5); +user_pref("extensions.testpilot.runStudies", false); + user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs"); user_pref("geo.wifi.testing", true); diff --git a/config/autoconf.mk.in b/config/autoconf.mk.in index 17fdffeabea0..bc0b9154b2f8 100644 --- a/config/autoconf.mk.in +++ b/config/autoconf.mk.in @@ -133,6 +133,7 @@ XPCOM_USE_LEA = @XPCOM_USE_LEA@ MOZ_ENABLE_POSTSCRIPT = @MOZ_ENABLE_POSTSCRIPT@ MOZ_INSTALLER = @MOZ_INSTALLER@ MOZ_UPDATER = @MOZ_UPDATER@ +MOZ_UPDATE_CHANNEL = @MOZ_UPDATE_CHANNEL@ MOZ_UPDATE_PACKAGING = @MOZ_UPDATE_PACKAGING@ MOZ_NO_ACTIVEX_SUPPORT = @MOZ_NO_ACTIVEX_SUPPORT@ MOZ_ACTIVEX_SCRIPTING_SUPPORT = @MOZ_ACTIVEX_SCRIPTING_SUPPORT@ diff --git a/configure.in b/configure.in index b0f6b6aa3f0c..193dbab604b3 100644 --- a/configure.in +++ b/configure.in @@ -6507,6 +6507,7 @@ if test -z "$MOZ_UPDATE_CHANNEL"; then MOZ_UPDATE_CHANNEL=default fi AC_DEFINE_UNQUOTED(MOZ_UPDATE_CHANNEL, $MOZ_UPDATE_CHANNEL) +AC_SUBST(MOZ_UPDATE_CHANNEL) # tools/update-packaging is not checked out by default. MOZ_ARG_ENABLE_BOOL(update-packaging, From 81d9f23ed147077081be8f40038de21016f15a16 Mon Sep 17 00:00:00 2001 From: Taras Glek Date: Thu, 24 Jun 2010 16:39:24 -0700 Subject: [PATCH 1799/1860] Bug 416330 - Suboptimal SQLite page size r=sdwilsh --- db/sqlite3/src/Makefile.in | 2 + storage/src/mozStorageConnection.cpp | 9 +++- storage/test/unit/test_page_size_is_32k.js | 28 ++++++++++++ .../components/places/src/nsNavHistory.cpp | 44 +++++-------------- .../src/nsUrlClassifierDBService.cpp | 21 +++++---- 5 files changed, 63 insertions(+), 41 deletions(-) create mode 100644 storage/test/unit/test_page_size_is_32k.js diff --git a/db/sqlite3/src/Makefile.in b/db/sqlite3/src/Makefile.in index 104a8925f88e..0e79fecea832 100644 --- a/db/sqlite3/src/Makefile.in +++ b/db/sqlite3/src/Makefile.in @@ -100,6 +100,7 @@ CSRCS = \ # don't have to vacuum to make sure the data is not visible in the file. # -DSQLITE_ENABLE_FTS3=1 enables the full-text index module. # -DSQLITE_CORE=1 statically links that module into the SQLite library. +# -DSQLITE_DEFAULT_PAGE_SIZE=32768 increases the page size from 1k, see bug 416330. # Note: Be sure to update the configure.in checks when these change! DEFINES = \ -DSQLITE_SECURE_DELETE=1 \ @@ -107,6 +108,7 @@ DEFINES = \ -DSQLITE_CORE=1 \ -DSQLITE_ENABLE_FTS3=1 \ -DSQLITE_ENABLE_UNLOCK_NOTIFY=1 \ + -DSQLITE_DEFAULT_PAGE_SIZE=32768 \ $(NULL) # -DSQLITE_ENABLE_LOCKING_STYLE=1 to help with AFP folders diff --git a/storage/src/mozStorageConnection.cpp b/storage/src/mozStorageConnection.cpp index 59b7a8e39dbb..4b8874946180 100644 --- a/storage/src/mozStorageConnection.cpp +++ b/storage/src/mozStorageConnection.cpp @@ -393,6 +393,14 @@ Connection::initialize(nsIFile *aDatabaseFile) PR_LOG(gStorageLog, PR_LOG_NOTICE, ("Opening connection to '%s' (%p)", leafName.get(), this)); #endif + // Switch db to preferred page size in case the user vacuums. + sqlite3_stmt *stmt; + srv = prepareStmt(mDBConn, NS_LITERAL_CSTRING("PRAGMA page_size = 32768"), + &stmt); + if (srv == SQLITE_OK) { + (void)stepStmt(stmt); + (void)::sqlite3_finalize(stmt); + } // Register our built-in SQL functions. srv = registerFunctions(mDBConn); @@ -412,7 +420,6 @@ Connection::initialize(nsIFile *aDatabaseFile) // Execute a dummy statement to force the db open, and to verify if it is // valid or not. - sqlite3_stmt *stmt; srv = prepareStmt(mDBConn, NS_LITERAL_CSTRING("SELECT * FROM sqlite_master"), &stmt); if (srv == SQLITE_OK) { diff --git a/storage/test/unit/test_page_size_is_32k.js b/storage/test/unit/test_page_size_is_32k.js new file mode 100644 index 000000000000..b40aa3cd13a9 --- /dev/null +++ b/storage/test/unit/test_page_size_is_32k.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests that dbs are using 32k pagesize + +function check_size(db) +{ + var stmt = db.createStatement("PRAGMA page_size"); + stmt.executeStep(); + const expected_block_size = 32768; // 32K + do_check_eq(stmt.getInt32(0), expected_block_size); + stmt.finalize(); +} + +function new_file(name) +{ + var file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + do_check_false(file.exists()); +} + +function run_test() +{ + check_size(getDatabase(new_file("shared32k.sqlite"))); + check_size(getService().openUnsharedDatabase(new_file("unshared32k.sqlite"))); +} + diff --git a/toolkit/components/places/src/nsNavHistory.cpp b/toolkit/components/places/src/nsNavHistory.cpp index e3d4b9f0dd55..6a925132d93c 100644 --- a/toolkit/components/places/src/nsNavHistory.cpp +++ b/toolkit/components/places/src/nsNavHistory.cpp @@ -140,13 +140,6 @@ using namespace mozilla::places; // corresponding migrateVxx method below. #define DATABASE_SCHEMA_VERSION 10 -// We set the default database page size to be larger. sqlite's default is 1K. -// This gives good performance when many small parts of the file have to be -// loaded for each statement. Because we try to keep large chunks of the file -// in memory, a larger page size should give better I/O performance. 32K is -// sqlite's default max page size. -#define DATABASE_PAGE_SIZE 4096 - // Filename of the database. #define DATABASE_FILENAME NS_LITERAL_STRING("places.sqlite") @@ -634,37 +627,23 @@ nsNavHistory::InitDBFile(PRBool aForceInit) nsresult nsNavHistory::InitDB() { - PRInt32 pageSize = DATABASE_PAGE_SIZE; - // Get the database schema version. PRInt32 currentSchemaVersion = 0; nsresult rv = mDBConn->GetSchemaVersion(¤tSchemaVersion); NS_ENSURE_SUCCESS(rv, rv); - bool databaseInitialized = (currentSchemaVersion > 0); - if (!databaseInitialized) { - // First of all we must set page_size since it will only have effect on - // empty files. For existing databases we could get a different page size, - // trying to change it would be uneffective. - // See bug 401985 for details. - nsCAutoString pageSizePragma("PRAGMA page_size = "); - pageSizePragma.AppendInt(pageSize); - rv = mDBConn->ExecuteSimpleSQL(pageSizePragma); - NS_ENSURE_SUCCESS(rv, rv); - } - else { - // Get the page size. This may be different than the default if the - // database file already existed with a different page size. - nsCOMPtr statement; - rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("PRAGMA page_size"), - getter_AddRefs(statement)); - NS_ENSURE_SUCCESS(rv, rv); + // Get the page size. This may be different than the default if the + // database file already existed with a different page size. + nsCOMPtr statement; + rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("PRAGMA page_size"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); - PRBool hasResult; - rv = statement->ExecuteStep(&hasResult); - NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_FAILURE); - pageSize = statement->AsInt32(0); - } + PRBool hasResult; + mozStorageStatementScoper scoper(statement); + rv = statement->ExecuteStep(&hasResult); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_FAILURE); + PRInt32 pageSize = statement->AsInt32(0); // Ensure that temp tables are held in memory, not on disk. We use temp // tables mainly for fsync and I/O reduction. @@ -726,6 +705,7 @@ nsNavHistory::InitDB() rv = nsAnnotationService::InitTables(mDBConn); NS_ENSURE_SUCCESS(rv, rv); + bool databaseInitialized = (currentSchemaVersion > 0); if (!databaseInitialized) { // This is the first run, so we set schema version to the latest one, since // we don't need to migrate anything. We will create tables from scratch. diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp b/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp index 593ab7c9c5e7..4b70dcb0ad95 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp +++ b/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp @@ -173,8 +173,6 @@ static const PRLogModuleInfo *gUrlClassifierDbServiceLog = nsnull; #define UPDATE_DELAY_TIME "urlclassifier.updatetime" #define UPDATE_DELAY_TIME_DEFAULT 60 -#define PAGE_SIZE 4096 - class nsUrlClassifierDBServiceWorker; // Singleton instance. @@ -1223,6 +1221,7 @@ private: nsCOMPtr mGetTableIdStatement; nsCOMPtr mGetTableNameStatement; nsCOMPtr mInsertTableIdStatement; + nsCOMPtr mGetPageSizeStatement; // Stores the last time a given table was updated. nsDataHashtable mTableFreshness; @@ -3164,7 +3163,13 @@ nsUrlClassifierDBServiceWorker::SetupUpdate() NS_ENSURE_SUCCESS(rv, rv); if (gUpdateCacheSize > 0) { - PRUint32 cachePages = gUpdateCacheSize / PAGE_SIZE; + PRBool hasResult; + rv = mGetPageSizeStatement->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(hasResult, "Should always be able to get page size from sqlite"); + PRUint32 pageSize = mGetTableIdStatement->AsInt32(0); + PRUint32 cachePages = gUpdateCacheSize / pageSize; nsCAutoString cacheSizePragma("PRAGMA cache_size="); cacheSizePragma.AppendInt(cachePages); rv = mConnection->ExecuteSimpleSQL(cacheSizePragma); @@ -3413,11 +3418,6 @@ nsUrlClassifierDBServiceWorker::OpenDb() } } - nsCAutoString cacheSizePragma("PRAGMA page_size="); - cacheSizePragma.AppendInt(PAGE_SIZE); - rv = connection->ExecuteSimpleSQL(cacheSizePragma); - NS_ENSURE_SUCCESS(rv, rv); - rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("PRAGMA synchronous=OFF")); NS_ENSURE_SUCCESS(rv, rv); @@ -3475,6 +3475,11 @@ nsUrlClassifierDBServiceWorker::OpenDb() getter_AddRefs(mInsertTableIdStatement)); NS_ENSURE_SUCCESS(rv, rv); + rv = connection->CreateStatement + (NS_LITERAL_CSTRING("PRAGMA page_size"), + getter_AddRefs(mGetPageSizeStatement)); + NS_ENSURE_SUCCESS(rv, rv); + mConnection = connection; mCryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); From 20f57b508e97880f2782fc2a5898e8901f5b87b9 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Thu, 24 Jun 2010 16:41:46 -0700 Subject: [PATCH 1800/1860] Bug 570484: Ignore invalid targetApplication and localized properties. r=robstrong --- toolkit/mozapps/extensions/XPIProvider.jsm | 58 ++++++++++++++++--- .../test/addons/test_locale/install.rdf | 27 +++++++-- .../extensions/test/xpcshell/head_addons.js | 13 +++++ .../extensions/test/xpcshell/test_startup.js | 9 ++- 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/toolkit/mozapps/extensions/XPIProvider.jsm b/toolkit/mozapps/extensions/XPIProvider.jsm index 8f85d78d49fa..17d7f482ff7c 100644 --- a/toolkit/mozapps/extensions/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/XPIProvider.jsm @@ -300,16 +300,46 @@ function loadManifestFromRDF(aUri, aStream) { return values; } - function readLocale(aDs, aSource, isDefault) { + /** + * Reads locale properties from either the main install manifest root or + * an em:localized section in the install manifest. + * + * @param aDs + * The nsIRDFDatasource to read from + * @param aSource + * The nsIRDFResource to read the properties from + * @param isDefault + * True if the locale is to be read from the main install manifest + * root + * @param aSeenLocales + * An array of locale names already seen for this install manifest. + * Any locale names seen as a part of this function will be added to + * this array + * @return an object containing the locale properties + */ + function readLocale(aDs, aSource, isDefault, aSeenLocales) { let locale = { }; if (!isDefault) { locale.locales = []; let targets = ds.GetTargets(aSource, EM_R("locale"), true); - while (targets.hasMoreElements()) - locale.locales.push(getRDFValue(targets.getNext())); + while (targets.hasMoreElements()) { + let localeName = getRDFValue(targets.getNext()); + if (!localeName) { + WARN("Ignoring empty locale in localized properties"); + continue; + } + if (aSeenLocales.indexOf(localeName) != -1) { + WARN("Ignoring duplicate locale in localized properties"); + continue; + } + aSeenLocales.push(localeName); + locale.locales.push(localeName); + } - if (locale.locales.length == 0) - throw new Error("No locales given for localized properties"); + if (locale.locales.length == 0) { + WARN("Ignoring localized properties with no listed locales"); + return null; + } } PROP_LOCALE_SINGLE.forEach(function(aProp) { @@ -392,13 +422,17 @@ function loadManifestFromRDF(aUri, aStream) { addon.defaultLocale = readLocale(ds, root, true); + let seenLocales = []; addon.locales = []; let targets = ds.GetTargets(root, EM_R("localized"), true); while (targets.hasMoreElements()) { let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); - addon.locales.push(readLocale(ds, target, false)); + let locale = readLocale(ds, target, false, seenLocales); + if (locale) + addon.locales.push(locale); } + let seenApplications = []; addon.targetApplications = []; targets = ds.GetTargets(root, EM_R("targetApplication"), true); while (targets.hasMoreElements()) { @@ -408,8 +442,16 @@ function loadManifestFromRDF(aUri, aStream) { targetAppInfo[aProp] = getRDFProperty(ds, target, aProp); }); if (!targetAppInfo.id || !targetAppInfo.minVersion || - !targetAppInfo.maxVersion) - throw new Error("Invalid targetApplication entry in install manifest"); + !targetAppInfo.maxVersion) { + WARN("Ignoring invalid targetApplication entry in install manifest"); + continue; + } + if (seenApplications.indexOf(targetAppInfo.id) != -1) { + WARN("Ignoring duplicate targetApplication entry for " + targetAppInfo.id + + " in install manifest"); + continue; + } + seenApplications.push(targetAppInfo.id); addon.targetApplications.push(targetAppInfo); } diff --git a/toolkit/mozapps/extensions/test/addons/test_locale/install.rdf b/toolkit/mozapps/extensions/test/addons/test_locale/install.rdf index 25be92e02a92..d8d027b93ff5 100644 --- a/toolkit/mozapps/extensions/test/addons/test_locale/install.rdf +++ b/toolkit/mozapps/extensions/test/addons/test_locale/install.rdf @@ -6,7 +6,7 @@ addon1@tests.mozilla.org 1.0 - + xpcshell@tests.mozilla.org @@ -14,9 +14,10 @@ 1 - + + fr-FR Name fr-FR Description Fr Contributor 1 @@ -24,23 +25,37 @@ Fr Contributor 3 - + de-DE Name - + es-ES Name es-ES Description - + + + + + Repeated locale + + + + + + + Missing locale + + + Fallback Name Fallback Description - + diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js index 756fcbe2ba39..997b97475aeb 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js +++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js @@ -373,6 +373,19 @@ function writeInstallRDFToDir(aData, aDir) { }); } + if ("localized" in aData) { + aData.localized.forEach(function(aLocalized) { + rdf += "\n"; + if ("locale" in aLocalized) { + aLocalized.locale.forEach(function(aLocaleName) { + rdf += "" + escapeXML(aLocaleName) + "\n"; + }); + } + rdf += writeLocaleStrings(aLocalized); + rdf += "\n"; + }); + } + rdf += "\n\n"; if (!aDir.exists()) diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_startup.js b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js index 41d6ceac59cc..57815450da41 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_startup.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js @@ -18,6 +18,10 @@ var addon1 = { id: "xpcshell@tests.mozilla.org", minVersion: "1", maxVersion: "1" + }, { // Repeated target application entries should be ignored + id: "xpcshell@tests.mozilla.org", + minVersion: "2", + maxVersion: "2" }] }; @@ -25,7 +29,10 @@ var addon2 = { id: "addon2@tests.mozilla.org", version: "2.0", name: "Test 2", - targetApplications: [{ + targetApplications: [{ // Bad target application entries should be ignored + minVersion: "3", + maxVersion: "4" + }, { id: "xpcshell@tests.mozilla.org", minVersion: "1", maxVersion: "2" From ea378bb9fb4f399b3d87506f31fe7e5158982d49 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Thu, 24 Jun 2010 16:47:10 -0700 Subject: [PATCH 1801/1860] Bug 574443 - Commands and drag & drop stop working when bookmarks menu button moves across toolbars. r=dietrich --- browser/base/content/browser-places.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js index 81a49820ea6b..705a24ac15f3 100644 --- a/browser/base/content/browser-places.js +++ b/browser/base/content/browser-places.js @@ -1062,7 +1062,7 @@ var PlacesStarButton = { }; -// This object handles the initlization and uninitlization of the bookmarks +// This object handles the initialization and uninitialization of the bookmarks // toolbar. updateState is called when the browser window is opened and // after closing the toolbar customization dialog. let PlacesToolbarHelper = { @@ -1163,17 +1163,31 @@ let BookmarksMenuButton = { let bookmarksToolbarItem = this.bookmarksToolbarItem; if (isElementVisible(bookmarksToolbarItem)) { - bookmarksToolbarItem.appendChild(this.button); + if (this.button.parentNode != bookmarksToolbarItem) { + this.resetView(); + bookmarksToolbarItem.appendChild(this.button); + } this.button.classList.add("bookmark-item"); this.button.classList.remove("toolbarbutton-1"); } else { - this.navbarButtonContainer.appendChild(this.button); + if (this.button.parentNode != this.navbarButtonContainer) { + this.resetView(); + this.navbarButtonContainer.appendChild(this.button); + } this.button.classList.remove("bookmark-item"); this.button.classList.add("toolbarbutton-1"); } }, + resetView: function BMB_resetView() { + // When an element with a placesView attached is removed and re-inserted, + // XBL reapplies the binding causing any kind of issues and possible leaks, + // so kill current view and let popupshowing generate a new one. + if (this.button._placesView) + this.button._placesView.uninit(); + }, + customizeStart: function BMB_customizeStart() { var bmToolbarItem = this.bookmarksToolbarItem; if (this.button.parentNode == bmToolbarItem) @@ -1181,6 +1195,7 @@ let BookmarksMenuButton = { }, customizeDone: function BMB_customizeDone() { + this.resetView(); this.updatePosition(); } }; From 4a557badc233be1c1eb5dd4fcab6d8908975f05d Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Thu, 24 Jun 2010 17:53:53 -0700 Subject: [PATCH 1802/1860] Remove fonts from Feedback extension and shrink image files. r=beltzner --- .../skin/all/badge-default.png | Bin 19738 -> 18676 bytes .../skin/all/css/screen-standalone.css | 31 ++++-------------- .../skin/all/dino_32x32.png | Bin 4443 -> 1583 bytes .../skin/all/fonts/DroidSans-Bold.ttf | Bin 150804 -> 0 bytes .../skin/all/fonts/DroidSans.ttf | Bin 149076 -> 0 bytes .../skin/all/fonts/DroidSerif-Bold.ttf | Bin 172012 -> 0 bytes .../skin/all/fonts/DroidSerif-BoldItalic.ttf | Bin 137212 -> 0 bytes .../skin/all/fonts/DroidSerif-Italic.ttf | Bin 155220 -> 0 bytes .../skin/all/fonts/DroidSerif-Regular.ttf | Bin 162864 -> 0 bytes .../skin/all/images/callout.png | Bin 18197 -> 18081 bytes .../skin/all/images/callout_continue.png | Bin 15049 -> 14975 bytes .../skin/all/images/home_comments.png | Bin 2648 -> 2602 bytes .../skin/all/images/home_computer.png | Bin 4623 -> 4308 bytes .../skin/all/images/home_continue.png | Bin 693 -> 673 bytes .../skin/all/images/home_quit.png | Bin 1030 -> 961 bytes .../skin/all/images/home_results.png | Bin 4086 -> 3530 bytes .../skin/all/images/home_twitter.png | Bin 3502 -> 2660 bytes .../skin/all/images/home_upcoming.png | Bin 3638 -> 3605 bytes .../skin/all/logo.png | Bin 3600 -> 2060 bytes .../skin/all/mozilla-logo.png | Bin 989 -> 949 bytes .../skin/all/status-completed.png | Bin 4233 -> 2675 bytes .../skin/all/status-ejected.png | Bin 2363 -> 1297 bytes .../skin/all/status-missed.png | Bin 2864 -> 1679 bytes .../skin/all/testPilot_200x200.png | Bin 45469 -> 44444 bytes .../skin/all/testpilot_16x16.png | Bin 710 -> 489 bytes .../skin/all/testpilot_32x32.png | Bin 2691 -> 2503 bytes .../skin/all/tp-completedstudies-32x32.png | Bin 1781 -> 1558 bytes .../skin/all/tp-currentstudies-32x32.png | Bin 1365 -> 1040 bytes .../skin/all/tp-generic-32x32.png | Bin 1601 -> 1521 bytes .../skin/all/tp-learned-32x32.png | Bin 1361 -> 1216 bytes .../skin/all/tp-results-48x48.png | Bin 2803 -> 2627 bytes .../skin/all/tp-settings-32x32.png | Bin 1156 -> 967 bytes .../skin/all/tp-study-48x48.png | Bin 1408 -> 962 bytes .../skin/all/tp-submit-48x48.png | Bin 2764 -> 2617 bytes .../skin/linux/close_button.png | Bin 1377 -> 1312 bytes .../skin/linux/feedback-frown-16x16.png | Bin 922 -> 852 bytes .../skin/linux/feedback-smile-16x16.png | Bin 951 -> 855 bytes .../skin/mac/close_button.png | Bin 1576 -> 1243 bytes .../skin/mac/feedback-frown-16x16.png | Bin 805 -> 653 bytes .../skin/mac/feedback-smile-16x16.png | Bin 933 -> 797 bytes .../skin/mac/notification-tail-down.png | Bin 2286 -> 2356 bytes .../skin/mac/notification-tail-up.png | Bin 6964 -> 6421 bytes .../skin/win/close_button.png | Bin 1377 -> 1312 bytes .../skin/win/feedback-frown-16x16.png | Bin 810 -> 684 bytes .../skin/win/feedback-smile-16x16.png | Bin 844 -> 748 bytes .../skin/win/notification-tail-down.png | Bin 4231 -> 3838 bytes .../skin/win/notification-tail-up.png | Bin 5463 -> 4972 bytes 47 files changed, 6 insertions(+), 25 deletions(-) delete mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSans-Bold.ttf delete mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSans.ttf delete mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Bold.ttf delete mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-BoldItalic.ttf delete mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Italic.ttf delete mode 100644 browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Regular.ttf diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/badge-default.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/badge-default.png index e4ef896ca7bba075399eed382c5a1e230c263627..865218dfa4679e7629119645a5e2e1b87b7c1ea1 100644 GIT binary patch literal 18676 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 XAM9IS;Vh#(00000NkvXXu0mjfPf2!u literal 19738 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

x?bg`u%k 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 Rou&W)002ovPDHLkV1iRO?_K}^ literal 4443 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≥$N7pC3(TO^4kU-sPTRE#o6wy=L65bXVt0k#Y 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 k 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSans.ttf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSans.ttf deleted file mode 100644 index 458ba59d8bd699afe889f319d89dc69bd1812974..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149076 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 Dm8_&) diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Bold.ttf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Bold.ttf deleted file mode 100644 index b6add0e4c064605508bc10c5c3baa5ec18fbf34a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172012 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

5AI>l9&=2N$w;;K{n)NQ92>#$GVc8al1 z>2r>qVyt6)_N`NlHU2U2J~7nE*|$!ed9ie#fa?@{jrW2Z!LNY#f%j7%pE~crJU99TZl`!|^f|ar@!aTB=bhrYOZGXq zPVwAfpM&cZ&mHzTxK6R%F+PFYskJ`G_ylgJR{R|H3EWOG-|2l0u2al6`W#%Rm~Zqs zxK6DK8hrw{Q>%i;Au~k%LSgj_qtD@Xs$Up=8m?2lL*=V?7=0S9Q+>o??@#MgPf_`r z#W?KKxSi@R#w0O5joYbt%@>3|joYcW7-F5?ViPJSO%#KOnfkUjqx$@!RR%{$HWJt*BBoYAB?*|uP#0&CK$cC_?Vbr^y=bcVuJA~=+(l< zWK*Np2_KU+jn9KGfLi! zEeq#kK@xG$EQ7xPkS7n_BcN6acuE8I^T@M z%}Csg#LYFG{NZgFX%}Csg#LYFG{NZgFX%}Csg#4SkNg2XLI z+=9d{NZf+NElAvg#4SkNg2XLI+=9d{NZf+NElAvg#4SkNg2XLI+^YB51G;b2^UAHr z-HP0;$lZ$Et;pSq+^xvnirlTp-HP0;$lZ$Et;pSq+^xvnirlTp-HP08NZf|RZAjdP z#BE62hQw`1+=j$$NZf|RZAjdP#BE62hQw`1+=j$$NZf|RZAjcsOSdC;J94)pcRO;o zBX>J;wJ;w@%J_BvGi+geuMGb=n;~L!aQ(;S-!Xd_w+VLG*+?!-8n1 zb}Y0BZP}g3-KmxiD(sSXQu0np-bu+jDfvlqJxQ)7$@L_;o+Q_k<^ zr^xjbxt=1|Q{;MzTu+hfDRMnUu3hBXMXp`s+C{Ei4J^nzn zN98mMD?r%{@rjgOok;_KHE)9&EY? zo9;#8UL@{?IeU?~7m0h3xEG0gk+>I$dy%*oiF=W_7m0h3xEG0gk+>I$dy%*oiTjYa z4~hGbxDSc@khl+t`;fQ~iTjYa4~hGbxDSc@khl+t`;fQ~iTjYa4~hGbxF3o8k+>g; z`;oXGiTjbbABp>sxF3o8k+>g;`;oXGiTjbbABp>sxF3o8k+>g;2atFGi3gB)0Eq{X zcmRn9kaz%z2atFGi3gB)0Eq{XcmRn9kaz%z2atFGi3gB)5GeoB(?XUO#o zxt<}{Gvs=PT+fiJ8*kQ)H|v)6+I0@iBmZvg>r?;HIHyPGaZa~J>_(4vx^-52s?aNG z-O)#gxdFTp^jN4{BX*%Ry6_n`kC^xuR2d(eLm`tL#iJ?Os&{r8~%9`xUX z{(I1W5Bl#x|2^ox2mSY;{~q+;qcN)GdW_nG{(I1W5Bl#x|2^ox2mSY;{~q+;gZ_Ka ze-HZaLH|AIzX$#Ip#L89--G^pG=h~x^xuR2d(eLm`tL#iN7QRJMn}|Zj8)?E5zTOo zKHqeNS?v+M>+!GiRP!Tx)8VjpP#@7d4x`^M9AWL{i0tCvu%9Gadqcn(2r}Wk#Q7IwDWv+WIup5zQhU z~;5Ka^J38o|LGd!Zvqr*PIbVR;U?b2H( zh4sG4=<`ZPVm_~Qgnfob8PyzRG<#HbJgV;m_{VQCST7GY@-mKI@YQ69!=%+ew( zEyB{GwCxzPv9X%UtdVQCST7GY_T6~7`ZEyB_wEG@#)A}lS!(jqJ^2D7vXON;D=Es75= zKSR7(T7;!VSXzXoMOa#drA1g;gr(2I(r01mv#|78So$n1eHNBJ3rnAcrO(3BXJP5H zu=H72`YbGc7M4B>OP__M&%)AYVd=B5RNuptWlONM1WQY>v;<2_u(SkAOR%&AOG~h{ z1WQY>v;<2_u(SkAOR%&AOG~h{1WQY>v;<2_u(SkAOR%&AOG~h{1WQY>v;<2_u(SkA zOR%&AOG~h{1WQY>v;<2_u(SkAOR%&AOG~h{1WQY>v;<2_u(SkAOR%&AOG~h{1WQY> zv;<2_u(SkAOR%&AOG~h{1WQY>v;<2_u(SkAOR%&AOG~h{1WQY>v;<2_u(SkAOR%&A zOG~h{1WQY>v;<2_u(SkAOR%&AOG~h{1WQY>v;<2_u(X6PEy2O!!ZuG*9UbxWC$|!Ca{haNE8@=L&WBi=$g&V!>67OY~_;ZX= zpJPn*9HXi45dIF~J~Z5ihWpTP9~$mM!+mJD4-NOB;XX9nhlcyma331(L&JS&xKI6e zFzQqPHCn@c>{IQdhxVc2J~Z5ihWpTP9~$mM!+mJD4-NOB;XX9nhlcyma331(L&JS& zxDO5Yq2WF>+=qty&~P6b?nA?UXt)mz_o3lFG~9=V`_OP78ty~GeQ3B34fmnpJ~Z5i zhMz~D&!f+N^x2O-`_X4V`s_!a{phnFefFcze)QRoKKs#UKl_?yd=(8Vv_M^{!^x2O-`_X4V`s_!a z{phnFefFcze)QRoKKs#UKlvcVkRDzyIMwfE}DJPI}0x2huasnwQka7YkCy;UiDJPI} z0x2huasnwQka7YkCy;UiDJPL~5-BH-P!=4}*$?~wp~&;BA?-9UE(ATp9m2N^;ai5-Q82`g zf+76O5bM1|c$*>i6%4VjV2J1Y5YP7^YBI$0eMswXgDTVY8PY1B(R&4l^nTWOFSrr> z3V0vry>mlayD)A6z29j_YZpfEog2b~4dKCt@L)rDupvCy5FTuZeFa1Ku_64}5Pob3 zKQ@FP8^VtbF#;K41Tw@3WQY;S5IP(}heLYbYPm9BUBINqEYW-c(yTws{Fz%X-xVdetE z%ms#-3k+*z%+me#c3A8(`t9v7Z*PZrdpiufhGEw*>>7q$!?0@@b`8U;VR$tRuZH2( zFuWRuSHsK*hM5rzGb0#gMldX1xm{je8HQKG;+5-QUJYv{&0+Iun0LOz%o>Ja*D&lF zX4Wta$A+0T48ycxm^KX4hGE(;OdE!2!^|3nnKcaSIqULIfkQ^-4#Ti+7}gEzY3q28 z4~OC2F!P0BW(vd16o#223^O|zW?nEXj;UR+Ynb=T!{U`&$E4vu|R{dSs#po5|cV!pje$a2# z-<4gAUY~qdnl$5p8)lo)vsC!6zb_F)YDD)Wy1G>_ktV2uYmV~@;pi@&m)xQ5z6xj zw}IP1FrTquSMp9aNqg~fBBcrFyrh2ptTJQp4U4})Ex&Za3|V*{ba z2Err6$p0u@1m%AeE`i5D^%upczX;V|gz7Is^%tS~i%|VVsQw~Ue+d)(ToXnnmSEFZUQpVY8?l7L~)Ia#&Oji^}z+a~iX#+LBKUJA{k za#*BWk_nqd<*=yS&o70|qHuNMdf~qDaI@+_ft%07M1%cCNzu6VNp3ODu+eou&5jsmBXTP zSX2&+%3)DCEGqZtD3k}YsNCLIXcm>jqHbswa#&RECzrxzQ8_G{h-D{Y$BD3LA}pE+izdRNiLhuQESd<5;;<+Vi{h{- z4vXTjC=QF_uqY0T;;<-=tT-%+!=kv~?u9tgEfEQ-UTI4p|8qBtyy!=gAWio>EfEQ;fc;;<+Vi{h{- z4vXTjC=QF_uqY0T;;<+Vi{h{-4vXTjC=QF_uqY0T;;<+Vi{h{-4vXTjC=QF_uqY0T z;;<+Vi{h{-4vXTjC=QF_uqY0T;;<+Vi{h{-4vXTjC=QF_uqY0T;;<+Vi{h{-4vXTj zC=QF_u&4sdR$#{pSX2RvDqv9sEUJJ-6|krR7EL18Byvq6*CcXHBG)8xO(NGMa!n%F zdm_#C!h1aT6l%sNd@pF8zej8G#w)=Oo<6FuM%qHn&V=`Znw=@Ek+x7HZJ|zv3N;%N zYP>4cNHx62C%J?=4=dEnBD}{ZxrCZc2z8b-oKJnuSA7;J?E0MVof|^;vh!)z`LyeN z@!7xHKc7$Q&Zl+f)4KC%-T9P!J|$14G-|rYK4FG7;HLz zZ#o`tI=*hYMz)IA$kym5*K~Z&^x*N&^x*N&bbQWqe9m-f*ePk3^z1P0^4S4}Ye0{V zrc3ksRT?vTY&KmQ8w@j~tEobdPiCO`8MJx^I-fx+X3&Zmv|M9VYR;Not#f6-jEFgcV6x zk%SdVSdoMkNm!AD6-ii;gcV6zm!x$`T9>4CNm!AD6-ii;gcV6xk%SdVSdpadN!p&I z?Md36r0q%Co`e-iSdoMkNm!AD6-ii;N$&g&Z3^PsOK!|Ig5JE!bY>OQ6)C2#733as1h4hMk(>L z(&w^;?!}eZs}g%vVy{Z{W@qDzR53_Nv5QmDsBi zdsWiXN?KY;ODkzoT1eT4Ud zI_abES3sTgQCPdJg!($IP+zAN>g%*ZeVtaQuhR zLVcZ9sISus^>tdIzD_ID*J*|NI;~J&rxof{j!<8x6&4BW>$D1&2dYtaQ#A;RQaSq#RRamVGt5spODy&w8 z)vB;s6;`XlYE@XR3aeFNwJNMuh1IICS`}8S!fI7mtqQAEVYMo(R)y87H1BY2ZM7=R zI~=yvsx

*jB5;YE@XR3aeFNwJNMuh1IICT9sxZPH(GKVYMo(R)y87{H`-pVYMo( zR)y87G!s#JtX759s<2uWR;$8lRaotPTGLw=-lsJ^;|<`A;BJll->20);}41XG58Z= z-qbk!eOlA=uRoXF-={S_m+)`kzZI)%f;me0#Mx=JcDvEuh{sDP|kE9ozvv0csDMe$^f} zp>|pdcY$7!s}|E7vlrBUONF(QO{kr0!h@hkgVlK2YCLTYXF)a3f@+=x)jSKTc@|Xjw5sN*R4w+pOzg|EsahOX zjMzI6E`d>(z^F@L)Fm+L5*T#}jJgCyT}sMJNqH$LFD2!rq`Z`rmy$9C_fl{#1@}^L zF9r8f@=*gJB_Cxp_fmMO6x>U}y%gL_!MzkKFe$i~f_o{rmx6mKxR-)^DY%z{dnve= zf_o{rmx6mKxR-)^DY%!Ce{&toy%gL_$-g;l?xp14jOJbn?xo;f3ht#?fl0x=6x>V6 z%Q>aFmx6mKxR>H7pMrZSxR-)^DY%z{dnve=f_o{rmy#!RIp$sp?xo;fN=3WZ! zrR4VWwLfzOP)Qt^7 z-Pj=fIA!X_28DHQLa6gELXXYnss0Y@YsEs(;O2>eM$h2piGfDX;O2>eM%~gN)GZA{ z-O?b`Ee%55(je3=4MN@0Ak-}lLfz6J)GZA{-O?b`Ee%55(je3=4MN@0Ak-}lLfz6J z)GZA{-O?b`Ee%55(je3=4MN@0Ak-}lLfz6J)GZA{-O?aD4C_4VP-j4dIs+o~ zd~lw)>XJPloF}e2?3u+pan)hn(je3=4MN@0Ak-}lLfz6J)GZA{-O?b`Ee%55(je3= z4MN@0Ak-}lLfz6J)GZA{-O?b`Ee&CwSZ&n(3_{({Ak_T~Lfy|G)cp)X-OnJ@{R~3g z&mh#z48rNQns_dIiRVV$%plaw3_`DI%oFDw)@=+z-Nqo)Z45%)#vs&f3_{(;Ak=9W zp>AUkdNw*w%(oOcCJyUYF<-xm`R)2fo%uW;CeIf?dlbG_T%9l9XuJ;esB(Vv5yCp3 zsPK)T-^R`tKaGBOHs2@XggO}~)X7AlP9_RX7{%xei_o4VizmtANwV6d{659lpJaU-jL`PY;#IP;n#1-g zSv}FdFSOmW_?9fbC5vy#;#;!#mMp#{i*L!|Te5ntS+0Fc7T=P^w`B1xS$s>@x54OF zdzdU9CX0v3;$gCQm@FP9tGkR`AN!dsekO~b$>L|S_?awzCX1iR;%Bn>nJj)Li=WBj zXR`R2EPf`7pUL89viO-SekO~b$>L|S_?awzCX1iR;%Bn>nJj)Li=WBjXR>-Gx}Nqk zS^P{EKa<7JWbrdu{7e=M*q_@6BP zCyW2d;(xOAl2hE5Iso`%3i%qu&i($;j$T##C2|MUMBIohuouTq&J9 zrVrE!4u!pHcO~mTSF-+dCF?&|G9I{+r~j3)a+jdJOauCghNbhxcA=&7#de{kFF^VN zr1Qmg#aQ|Rr1Qmgg)N;gwhJwN0n+(myTV?5T7dKgNau^~VFA)>kX}REYvdn}>U$op zLk$|Jkq2n#Dc7QoB4;Das>>50F4IaBjJ0 zb~SkH8a#FlnykTZ*Qmd^WP9%#^%tYPca8ds(cZfT?_Gn=YtVTO7Oz3)H9p-CYVhhc zc=a0f2*=pB*U%$s@bESCh#Go94g9b1i7lnEAFlEFEFo6Fjy2e^1{T#|$E(o)RXo?O zLJwD=hpW)TRp{X=^l%k=xQbd`MXj!)K37qbtF-EL){j0FY zmbL8DtA!7>dQ-DeF*a40jEBI(U>B(K)QZ=RU!ivV3VVrp4)iKYt$1bhT1qV|$+gT@ zYMHInGFz!-wo)rzIgM9TYQ-y~&Ql9df~Sn$$1Ak&s+9$e+Q%#O%3iJPX!Mi4mYtKe z@|=z_qiW?p9ljJayK3b{9b=Bw%9lE9rq#-$I;`{5Lhbk!Vqd+1Qj8d=7%|Xc@8n#_ zoOB^`(uK0)QN?)8cA@O(@FA7Cka^!i=6ws9_brs({Hw>(3z=0dWLC9MHB@@l&FFoA z3z=0dR85pp(vOBkk>6-9LgFGx98~yIpm&ulLgFGME<)lWBrcLf|LWaN7$6XTwByXrP5Y$2vq?+b1Zj}mszx;=C#d7-p~o!k-bBIb+W-I9Js_%h)K2!ECEgWyA;>vo6gW_*M+ z?)7)5ZbtX_J5)EL>vo6gCWMD~z{5|A6&Hn1OSc2UrE1rwrB#R5fNP0qBD?{-8+-t? zHa;D@P*|s@;qFiu{#2#aVW~PSRj0NOD#n(olLj3AqO@HXzT{ltYlI&J9|k?2tqZzy zUHB;B4&ma+WAeq3XYq>}jW0&4i_z+0X?1s4EUoHSw7OVYb<750?s5s?3zE1vm>Y{_ zeTAh5rI8Q6NqC{+m-2jH%JY4xQVxWrq+BZ9D$KKEnQAf+mPv0n3GGjop}l2lufx{f zGS%d$u$k}%hc%8;JS!N>(AWx}B?&9M^IvG~t?%xQo*4QOl?+E|4)R-uhmXk!)HScNuL!KGE`WR-L>7*?tLqhS>~S%pqkp_4}G zWH2U<7-vVjiHIU zHK}gx3cGhSsctt3UAHFH%`wX)tBD#msl5){^E8D!!Mil$Yf_ze3%|gxUnIsorAhTS zewknGMVo@XXj8BkZNiH-sa=+3FWRIYZHZq8?M0i!9G7hW*`$8tH1?cL;;rjx&)I~} zY@&@#w62NPtrM@7g>_hM9gJEhyALSFJ!_q;Z@gdFEDanD%~-Y>n>N$N=J03!mA=yq zlbd03Gf$ajo-)n&$@S9Pg0Nn)b_@NKSuc&X3+IzaR^EV>H(=!rSa}0h-k_SV4I8lO2G!hQ+jN6!zENmvZoq;Yu;2#O-0`;51}wD! zOKreL8?ey^Y_tIzZD2mQ0ZZMbk_Wiht8et?SN9cUpyiWJM3CLf}S5i&yS$zM`)eDzNC`T z^CRfFg&O*cLJI<46bdcqxrG|Gpl80V6Zp1HXr=zGu=i2&K1$w4Y2BmbeU!Y9lJ`+s z_b9F7+a;lc^nAf2bdbJ-^c|$<`xJriQ-mD)&xKC4E*HF#oC}^A<p53VB}xUk888uWyj@P4H)qr_KfH?EA(CLMQF&q+Ok~t1}!H(>f)^ zzrF}GW8QuWj6n;bX9~64@{teY%Vb$5_x%M}LTm21M{SE0~@zVcb*o?h4W2wzb zKM*#DZ-bV&8Ht;NC2ppK&B1avBX@JK+|A*qp!K;K%WlT9TU3*q!WP-0U1(cu2|EZM zmX@~2E{^XZ?Dsxf@G4u$wUu03m8(5$CD&H9!eLu;t8zJPYili9pOWUE<-vA@pCRQfetjGKIrxsy_Z4px`qb@C7_t-F?Sv;g#S_Qdb~~}%PB^m@ z_U@DpRf3pvyU@(piA{H6)1BCKC+*#dWp`rPowBS`x?MZ5?@sKyQ}$gOc8Xn+$b8B- zA>S09gg;M8?x4c%#ZM~bK=6&n7lo(z^(k3dF{F7)b#vIO+)t@)#(vOwpCa!re4TDT zRvmWX>vqAJT`Fy3*oBwd1!s1NAp?r}wo|Gcqj%o!!sG1l+TL6@x<0g?+EDP*!Uf?o{~)vD!hbc2IR5P)xtHbWk>O zn%A6H^)dd;=~c2ykgvWy9Fk53!Xas6Q0S-XA!)$qr_Ui;eTY^cqSc3J?;+ZFh&Jku zU$skj{DK~_97fNFVb@{we3&QcVK{b}+8$PIcZb7h`*0{aFHAcO(+$u9ZH-9qn3?^11@>ji$bpX^fa*eLvKezl+M3igv- zYKv3aPj&_S$u70aW!g`61^dY^wb3zt26e&VF16P&_LE(~ezFUub>Szw@RMEij;>%o z*%j<3yVM6=2m8q`>B=p7!)?b;c1dF{^QYiH(V~Br{=34z5dInQ_L*J5KC>&>XLbeq z%xB>3GcfiU=}j>(_L=Za<$VU;KBJa8{%68&^xRF)>c-aHXtf(ncEhf2*wqcYx@mj2 z+TO0b9&dE3y$;(0bi=xCSoeRzzo^bd>Qj{W-WZDb?IO?jBEGsP|8P|C9%&TirG@gb zyM-S0mB?EnZ;8AmLheg=&X68`fQ{O2pw{1vrsAiP5BUZMW4Q2$r(oUbqz{X0_r z9cf;p{;yI0*Qo7l)b{Vu)!(Z$J();(Trut8INCd|zTXp$kHk~+P?ElI~~U_y-r(vW7vZ5I&FC!DX-I(*VXF>!t1o?bz1Z~ zE&2yE`49At6X^B?T0J3~E(rff%s*n^Q|e9a;gm|-D0DwMCH|}x4mpPL#wo@d-$ygw zM>F3?Gv7xuKOp`G#Q%WOen4qIptK*Tw2k2hDyLoOk@gR%{|~AE4`IcRi1`sQKO*MG zl=)-Id_!Zfw(tgPBX6LGH%R#gt^SF^1K}sI`X}Ta#8QLUWss5w;q#l6`6gw)N&VlX z{y$ZjQ^QXw^QV;gPn7vjl=d$w=VutF7HvD-T{=5x;-iAMK!=Ja| z|JyL%x0AI8-%ciklS61=h;oMDg}(uM=i4y4dI!zFL;c@T9d?IzREGhfM;h;_4i39Vyd&ujd*=9#bdZV!F^ikpa!8DjTJ)oG&LG7MWSo@}g+KnmHzA2&hO$oJQ zO87ByX*Z_A+Knl^nXqz|NPYorQmH>U7j zP`fb|)^1GUeW2FB71o-za1+=9wt{V7JE+~5N~7JF!Y!b7V=AoOm_qHw6l(S%d;-+? zL}Bg56lyo7P`fdOPg9z9V=Am!o>03nh1!iN)NV|nc4G=Po)I1f^-NS)^B1AsAPalI zBc#-BOvUI~Db#LEp>|^mwHs5|OUmbn(QZt|XwE3qZcL$=Ak>^usM&!~yD^2@jVaU| zP^jIQ!q-Wu@r=Tn@d@>Ok42#joCsEclfd_YVy4oFlfo&)Xg8+9+KnlkZmW4`lu)}d zg-K#&5w0Yx-I$8kZYbd;pmt*_tlgNxx!`5sd{DbF6|dcxLRjbBP_fAG(1jXj3e{_3 z0X_$q9K0J-G1})N)NV}Sb>Je^Aw=4ZDZBx^5xfb!1-uQ^ZcL@oZcL$eV+yq!Q>fjT zLhZ&BZU?ouN8u+xt@$Xdy*@Li*K$OyF(CDe``;WTgtsNI-~(QZtkc4G?Bn0I5w z0$L4dwG5w7rk#@)#mf9OJz*Afe=MUvmeC)}ymwJC?vG{k$Fk`D)7L3%R+qu&GMHSZ zeQ_#B`{InYY?;^pg?dLO)Vm&`-rot=lICl&b(z*w9j`ahLe0E{nlB5rDk$7b%wf{I z7nI2pI*r!)Vr70OB0sLZK*INfY0#c+qV@u{3+EBmULb|FS48+h;4L)6~(Lq z*Ak;$3X0Lr1fkw73cm>6O^n_uD&_&gUnTq?sI`5?KSDe<)!M#eu%qW(3Zr??xfIq4 zst~>T>8voi@<>KubfqV?xASAzbPkPmm*{VNge0o}h6ddeH!zY_GX1pO<)Q$9ieO3=R&^sfZ{OJCPP zmit#i`!_pb!~D?$HCcz>N@+`kguUng|`O3=R&^sj{X*D1#RD?$HC z(7zJiU#A%NuY~v43EjUE+Fz$u)4vk*uLS)oLH|n7zY_GX1kdFJ{VPHLO3=R&^sfZ{ zD?$HCXn&o{bpJ}wzY^NV;u!a@1pO;P|4Pum5_;3*c=xXa{VSn2Xb!u7CH(G8=>C<^ zyEBK~zY_GX1pO;P|4Pum67;VG{VPHLO3=R&^sfZ{D?$HC(7zJ&uLS)oLI1kYYtgX_ zy%rt2kXBqsD=zeI9K~qYoKU;Mg!U2_(uxb!irosk6&I=%4!ac>(uxad#f55x7rr+do)}#i`?JyTdC|Ps*3q!~Z|wMJ zc+!N&$ITxNzbCqO;%f^VmNzsve7=5Za%o+2U2;j|n!B1Bmak||K9NkPQkPdJKit^3 zd}V#Iwy|kVV^dvoL*wcXENp6QSem@KzNul^hZD}L_@|Mi4eOG1$>yfIrS+@onm(IsTy{oY-Rh;uRdshI7uP47>X$dH zYp!on9UE3Bm(({k*Xi$P);Be*TiUS1HCp$9u{xYnPhG3s5G~h#&HC^2QGK*DO6tEl z{ku+~B~fFvMuuu4)e6NXqbKxVTGz{@qRXRdg+Ht`jVfcMawkQCMx|Z@x|9ZmR!1L@ zoF;`DDF0@qbcxHN4=HY?;;xR?Nmjj5F6FmmbcxFP^?8zGxo#uvx=|U)XpZE#Udd>2 zv|h0;*|8}-q|TOgR{AAlC7t~>seJ2{%dKx#E$gV!D*bJWKC8G!)%riSxsG}#Bd5Ac zzb;n2l1g8XX4k14w|7Lh4N7ahI>tI4`S)j3Uu$HYq&4W*GkRO67XANq^XqkN>oiOA z+2~UJ|8xBR1CsEonzco3pATT$RZ98)ny*=HT|=wtCBrOQO{-jIm$^#%zeS-DeJ+#S z)rw6jt##O}`dO#e)JTeErS-q=RQyWmqfUAp;nW(XGY>~}W(|*6;lHXGThq+CRQZ~a z*Q{E&ChJw|8cAswt@)XhuG2`G^-AShEyl8Usf6{E;QZF*o#Y&`?RuoRmgde#YesCp z3V9>7HnS{ogzqDz*GYTUMZL7l(AY+>Vj_ z4vpyF)?1B*Yz4==ui5^#{yN%uMrTf$M8{`pvrOr%v0qo3wYf@C)~n{$p?RdM!xU?c zpXpnx^>4H2j124fKWErTO;%B3OLs1FA}J>R-_*!*SHsGYmY&hBOB-odqx5m6Uyrcj z*V%iS+UM3Le@*WmvD;GpKGIjLg=?Y>@MR5ccc1oew#X9I-)&feO;*Cv5lgL7tUba= zEk^8rhLyj{7JKnCE%}dnwpIKD7}42(rs)r8xwS%{V6=}sQ{q|OSmQ2ft+Z&aob__9 zf%{>-S~1ci+#Bvx3+m-3Mr7M=zglni*8kXV&dPNO?qehNb&vQQ8d)(~-hb8>*XdXN zhIQe-S*LQ%!T)M+vIVV|+W(OMdX+ZP&#d(`+Q=&9f3;*?jH(tcR{8(0Ui??{S^MWc zhuz{|eOlDZb}Lm|KUL5A`AExk{@>|kYS6FwU+i`bAhj^2QPp^@%a=vv(L{aeqe7#} zgm$y)=2eXuCr4AF3p5s-7X4=QThVVvzZ3ngMwQc}8PV@YGu6ad+TS`mdT;axk-uku zp}wo|{^(*2K`v26E>(5rXjn2=qtnYYFu5X{r#Wdhx-$A;v>^I#8mC?*rG7{zSg6i) ztp+XEiKdI9k3=8Ut?oDIHusN3f26Op+$^ViYjm5``U$z7Pep$m-5&jkMzD8iKW$y~ zl)gN2Z?rM$idv!{MPG@&7X8mCuL02(-Ms!l7$4meJskZ^UlI9>s5QD@_j6Q<^Nox=LQWz{9s)jeM6>Mt4?D_X_tAY=>3M=#OKw;z7YLc z^xyS$oyVg85q&YbJNi;I82vOlsQbEgGj!06zTv#+m(jn6M3|)8XU~sL>mK^w2$RE< za6y zMccH~dwsYwd`|n9?+Tv}Ux;3bz8C(xW*mPX9oKhsUXA`C{F(M4{}1gO{!;j|cI)07 zHfj&)tOK{xWEPX4s_tkgcIDJQ~`yTQR4-gxV>ny>*X; z$F&=7i}s#v)4r}9;R)^Uc~U!Ww4WyYm3FRtEBvqUwDz3rjXJ`7 ztLv?K_&=d190`6`{jAK%8qu%U&Km0&%;eHf;9Ny5Iv%&DD-hTa4_~-C1dY|={-bKBw zcRoMYJDqpJyWtn%-@-4$zlYOa(T>Gp<6`4u6SQ7gu2r9StRi+^ED@U&dr$2A*l)xp z$EL(Ch)s=6i~VNow_?8?`<>YD#(pn0JvJlu`>~m^WNcQfGB!K*-q;_+s$%ccyz-*x ze??En-XFU-+86DQim_^ahkUQT)AMD0<@CPj$*2^&B=&(wUuBM68Xbtl;zABlZ5_J^?>VmHP<7W<>vO|hF}x5RFZ-4^?J z>=Ut1#y%DMsL3VQdcjGH{Pk)+LFem z`uD8S)UI)ZKUo%>R`jNk7*NhZg&!6)?e6|kfEk7F`|6$i>xghc3C5@|A z)s2*!Sbokw&b#hx`tw$t4UfNWab45+6@s$sn;TXxtuNCYT7Sx~AFX}EXzi~bseQwU zrmi2YXT!+fitERk5W9X+tl=}~EjlaWyw98sCq8mcof4n@m47U6s;^(YQnTfTC1oG2 zTe7~nzHB9b5+6M`-O6+RQTEXh{jB8A_>Wo-D+OgYjO1TElK+NtvLsfY^N+F{M)I#7 z(eLWIHI3_gD6=S1&KWaa0eDqk6b;L=TPpnRMfd^{bcHHLYK@vTl9z zq{egqRd&-z9hye!aML+r6HVv*qwJ=UIy8;^xp^elI{ut@^I6rMx9)5>amzUcB%06p zN7*eS#WeG0{4KU_v#opUh;`SGSohY^R;?dx)vY6~TF;*ew>CAbUOqvq604U_y7k;P zOmp?%&7aWRxVmxO zq@@j7nOUa=8vm=}>XmC&)Deu=t!`|tUs>N!m$+ulx(3yV=!w@fkN$qWx-wCT8&@^B zuaErW)^ie9+_n41-PPN04&7GeqAC))$Q~GI~Af` zC2|7!$1hg$3CrEM2}|o&HrJJpmNowK`eB@ZY^G^0Y{F-0+De)>Qppe1##XP7ZD=TO z9BJRUrWK84>)g0G6ZkW(S#2DZKW>fMw?zM`IujbL_=Jl+Q?2v=Lp2klWsX>F^hf?t zL1u#Sx@8Rwb5g1F+%uue=8T2XW1+LZX2#+!8w*`N7P?|AG;b_4|4iue)L3Xt#^q;Z z%*l+EH)pJrIb)^F87n0{R!VxTl=PWWQt7e!rN_!kkJT?dR&si*0s`djJabn=8nmjJ0@f9n2fn&GBRT_GGj6_V=^*hGBRT_&gvyICL=Q@BQqu= zGbZEmv07a|mgDlV9G8#fxO^0HA$(T1LWB!NKqT+{mAg^F%279Dm{;KjQ}CpF0yO`{YQ%yZA%> z)zMS)Caw3&00xpv7q6VOcD)`09xpbnt6y5Ss$sQK43IFRW{tR->zBqg60P^^ zixr9H6&e|hgx8(7tl`cx;lw)CW%cL}6BgoCH3Gi&L#a7-n);ibv_$VfR<3MV!h?OH z9;++s*RA`^Z!kJI=RtVhIpN9Ygq!Tz>l4cw*EgMMR>Czn^N&eXVeDVj<;=fa2fcVn zjsB%h`geNt-*ZR*W=8*BHv0GS(Z5%W{+&1acmC+#t4IHS$bYZB=Gtrdd+oKgqkk9r zZ)(o9$W5g`M0{%QLgec&^7R)vbFP_tjnnHd<;=Neq`vw~`Z?ETM#@WHJ2$?xZe2rN at=KVh1vw%eN{@>5%J*w#K?&|8Q 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�tR1(0y~OPVHNo)^mhn!Nt9)m5c`G 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Italic.ttf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Italic.ttf deleted file mode 100644 index 1ef1f75ec53d65bf785274d4c0553f24b23dae21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155220 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Regular.ttf b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/fonts/DroidSerif-Regular.ttf deleted file mode 100644 index 239ba38253cb2429c9501059b53a1513bce24352..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162864 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 z 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout.png index ed554a02304d4125d1a76006cadaa023f3bbffa1..5dca8b1077bdb20fe6fa74c88c789757a1184379 100644 GIT binary patch delta 17739 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 delta 17870 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout_continue.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout_continue.png index 678fa3a6c2b62e942cb4eefb6ef39a67a75a5186..8ae284ab9f7125264d96687d51b4a0eec113d658 100644 GIT binary patch delta 14030 zcmW-ncRZV47r>R;MNxaztlFdY-m7Y_7&Rk8lxl4aRn*=TwPz5DBsNvl-a$pFy<-+_ ztvA0n|2_A)_nz~4&N<(6?ztIyd-o=VCsmIV%B!cTW*&&a<_|e9iapTe^}f3sQsPNu zaAFG25PlL_x(^pFB*ts-T722;A6Ul94vTq`85}shGZ27DHmbQg336d5Y&nXlpT4jU z_WpGlc4Qb#zH9$|etkej;0hY@&ag%Wib&qJI$tcrcJ7EiQa<~ee0j6WakVF_^-#g!`W z;{LU(>YF(_N^`lmu7u`seZ&Mx&Xt4|A#S|4)*~y_P(=oytkbx>;exyq; z3~oXk)K_qO!(|JOh0rf@9`mp8-~Zb9L;v=CSu2ZqE~W-v;z?1gbS2$x44*wu#F-{Jfdv_XRzcQp^OLWa zexUQWxu-V!)A{k`p_IT+3?_>-XlKgCfw`N!q;POF>&D%{Ahn-*&ahB(X|$TNty@E; zah8kYrpac7Zb1I|$N4roiTbF62WKbAG5H+YRcv?(M9x$y>g=`z(7mv5=GQ4#cu;y0 zH@+WA)$0%M)3y&}dVhG+kjY_Tzfs&eIPkH#27SSHTtEn}f$BjT&Uz(~Np=S8YW;^{ ztdB}5U%vpeFw4*mCtPz8GQnMN@-vc`A&av@nOkI@&&a7?SscBt*}LDJi=nQZ3*Qp2 z#Tu|}D8Qm%H-Fc9?`$4NdcL>6*ID`l75*b`weGZPOHMF_g}R>ZGWTL4_13DpGL60GMDzFC@q^z#VS8vR{*$h-`djUkmu83d;J)!< zMHN;VX=LOS+5$?TFh!$ zay#6l&3A}3?o0B3uk_H)7Rg4x<|8~;x_-|v^!u4Wxd-Jq6?fHj2TkbgyxeTqUMmai zan;D~XLtCIUg3iamDQA&t$DxqsMS?PCbV<}ynN)J0(BtpdqFZWBt)(?lJ^(|$u4-J zQsoMJ_Q`^wwK@U<0f&S8I|{Xh?bPUMt;XLFFa7#=|MmEXW1btWe#_v3>!LMT!IL`X zV}GeH&{t<>c!w8UNE`mnx@Mpzd4ffdZNn$PSRWjV4eF^op8W6wRd@Qd zx7PlcIY=PiT`ps;oHXMW z8C-#UGdBhk8YQ9nzL0a;c)6*%DbtZ!GR|vRcdsMQMu63OZ_)S@v5y3aZ6n$H-)YiG z@*^`%LvpXxo!f_;1^Z`Bz|TJxnk^{$isEa5*>8W@l%V$6)Tneia&Mc&$Vx+UvhT5O$aBnnXWcj+ zjVFe|neJM|c+Z)=@_4dMm-XpwPjXBcT5Ei&yPARoUHLxcj}v?ZxRdZe9h3=^6Afes z15)_}klas^cXxG>463gl*@|N_OPmQcPtK#`Yn8nJj7L9EAl))BQ^=AVB!$8CU}2vg z=nOif+IZt9TBI2>X_-VwG1vrI$28$YycK+=0||g(Nsz{W&3S4QGXzz5-->S@z#4q6 zmN{a+&@^VdX7M&GJ#K(P>`4PQb!PvOV|{x9yulhNvtjM|`tU(OPn8yVm3mZi?BQQm z!NZ&V+dH~gw80m;hqU;PPmzj`+^v06r6z1TX(g^5dxKD-eq%xA50MkEMA=R?IKRKK zCO;CF^`E%>SlC{?bZk`L9>&Xc9_MKF&^Fcho?EP7>zoNJ*EAYhxKGHa#e3#2o4A)? zWpZ`>IfA50DvWRIvCfsxnP~{i_79u%>qAyLew5S}7gawUJ^pgtQ}D0+{SrNJ@>1(k z>mRi$xbgX+D%-|ae9?#BYSD4)tV3wqKFhX;%=T>qbfN(Pe_`$XtLf9_W6ONDjX{Hfwwlv31YwTc%U2wBznH|cVI%4%Y2(f0EK^?_JZC*DKpTj zH0Ws0qK^7hAGRHZzPOJpS9cO_Asr;BA=Yu&__uvT_wNssOZ_}+w2=q(onw#8~ErFH#(m_~%`UVFT<0b!yYN>kfspU`wxttBF3)qgr6Q&YS{O zg`-ul(-{p|hGFQiw#JHegUtUMMT?ebn~)@$0-Co~73!=0CU&&(i@RCZ03C7PQ-;=l zGB1nI=N(P?u-7nQCvc3Bu!I=X(u)>pXxzgL+Qxjv_l4Lhyw3;2mCREw(%hh(zTnpplZZ!}MP z0XlgmhcyM#7|^sT6hRe zTGCf3kk?9gS*n3UK4G}E*h=UrJqg|>PZ+UNU#rK%hy}J?xA6HACu_NiBns_&n)u&X zO0kt!X2Vr-D^Swe7>(b_2@YCM_!KbG_)t%&bnIF3uOY0|V@ZaGw)aF~@eV?{X(OT; ziG<+T*E9bj5+I@V<)~I0V9zCowaBoxpzI0Pta3MT%%VEGZ?RDTv| z5Y>ymAVKCC!8!hZqaacvmYbWr{GE>1yn}PlkBYRp261rnFwW)mVS=>`QeQwUv~ij0 zcN4gbDHv>Ko2P;G_3}JVGvNZ!l}oQZR?&liBCIsQwmeH(UiD8=hOTuncCLxz#VzB} zM_y>)sM-GVHrL;3U27KK6b-oA1n_v}LDa6i$*y4f(hk9{Bf%X8c)kaD8bVWDSimOALJZ9G;y( zQ|~ztZ9+~Nk}z+qr0}Z>y?nFy-SW}HH1as%LoNfM5o8izHUradg=ThhaYD|7U1?&s z#22vVt*NIl-Gy~Eu!ilW53KVnIa0x!bm+X#vbGCE<4ZH*GQVb@36!EKDDjs?Q)UA~ zqJyhK1-2fmgnbz?hjxT0|5B~L?wwgqgx}8(G(fwZ*S_wknHK6&#R|2sIp<;4j6IWv zRO2frPhp=T6^{|KtHi}om!`y*af5gE2y#Zy=EZD&RZ%kt3%GhqT#4Z1gmK2Qz-_(U+eJvvsxL9y$o08p@rquN6 zM~rQ6Fq46n-aILyrfsaTcDTA)_sbtIxTeh#l`C>orwUSVM{Y-01R62Pg98CKJC2L}M29q0UZ_uWEHS_GMe8g0zM17zUNeY*qzj}x6=icZnM3O};PK4%DW+T6|8(9-rJE}SxT zLcrkI;gMmYi7mbHqCGg`5WjZIWU&3)2I(}yPSz2iojp|kXct-(Z|9~QpHR6b zH&WMxYFv>n_&zAQsD7X@8BU&PX~ZV#*=%8IOKTc(=cW$hcLPxLw6r7(74wf&9>|WM z1+c1%Bd~M*@VKML8fR&%;@b`)0zaWF8EnB$FKRd$3!i+Y`FI*C8x(au&t!~PFkRIH z%Mj~#`;(D}5uBSv_$IeX|NK1}AH%@@6Ks0y%O@TFu{_Ck2^nZL1FR_+?ix9-ViQ49 z6!}M{nGghL{VtZaRI>ip;(lSx@iOq5)%4gy+BL_#4~N4!5+sW{qydJM$AdOp*&TcH zuRZ2qj{FXWg#```9+cD1jQ6d~ykFv!?~Myg!b8Ffbs)`c*vxr%SU=lFBzkVv)OICc zzANY2K%G+A&`Yb^rpLIn34E`f{iMJELTox{ZM|5W<6F|T=q$_CCiG{3J3V%~OHuC6 zm(Yw)fUMpn`=CIpHAl%cDJTppjhro=>t;H2{|Vx-=<)Y+FW0pLo27&Oy+l~m{s=MH zBv`bw_QbuRET08Hgv6?ZA0x+n!L*+thqx1c}S)%9eaym6|JdlHyry>a%>4 z8h=3Uxyz6By@|WD0}pI*?Oy&PJbc4zLRtGLR%9t&7t(#T)DY^Q20b=$CbxRYuVn^^ zdc6xQ0LNj*D&{6EIXmA;ED~g3zUotZ?K-^StP`t^Ufh6Ep=gxWejB!e^mB@@mnadi za2vE`9_}25Y9#>oX;9EB5fKTx10s_oMT7AH-syK(zODR?zdpmSi|RE@XB9Iy6-PSN z!-!w`g;4>n?fE64l&5~zp4!#EFpB!Q48G^SdDo)NoLAq-Mg_*UjTdRr$}`cAj3Rm~ zU19KJI2HbCDO8arho9%sR_om{YbNUt|3m<9YwcYR3>Sjs^!x?y$;Hde&%S2U;@x_` z(JM@WMXNipUr$i_$FTko*yT5j44^oB3TC^rDg5abgQSDD)Xr9XUJp}iNZt8+__y*G zjd5Ynsmh{OmT&q|><0N_NrJLW6;hs@6@2*+smnwJ_(&}fGO7zPdNZK{eONuA_?H9A zB^Uu0>alO9G4;hkwh4v+jusngM5sRq%rLAj6rFcZDYWP5NzN$k4-6yK4fWg%yJd zQzCoIQTR$~Es+VosG>$Pqf!~9C?Q^!6ce|^@$f&6R>$UPRwm#?`_Sg}`NRvASK-<` zwNY#vrRX<@jF-#}+wM1QpIK9f5zKOel+DsO8z`x76>1kw#v4RvImlLeI`DWa&2b zAtiDk@YhETH&HOR1z*c5Y^AZMxwk5V#@6BZ&Sr{T&Cexf zi{p%S`*L-gS8G?%^)awcl`)4nCVhz!sbsb^41Ee%l+%FK<%QD+=g2|TKQ@~svs8pAxoq&p)#vH&eZlHScX7 z$R+oh5t6P6LR!nzf3Oee?~6W9q8$`96s)~ZYN|5ntSkHu(vN6)9}sriR5T4vSJpB8 zEl7X-i0fWcy2c~Ed(d?7BLS#G`XX7b>9bicaJ(8sG;ed&e_NViP<)+I0ubBKD_CbS z*sE~qtYva}a(wnHBj)Oo2sNBI>F`VzS?d>v0Y@X3`DGpHKW}(zmr47;^T0bJ%@Z6& zN|WO=H=YEGoCe3Q!RIwR2>=;e!et5^Ox8_9?3nNGYz-!62ahX~Lpl1_Z#?Z?ZQchM zMpA&U?jv8+kquf#*Z`lJTlkr%!4k(bv!RUmwX_ zMd)enoTa0gEd{t;0}C|U2a6v*j%?OA{n0{woO}<4ZPotCjj%H?TtuVZPYZ)>Gcbi_ z;5m0yEyk)yyPi{+^};$kYi!-KwTdiLKVd2ClXlLlnQdd}z8FOmvQrhkW~y-9sM9)B zq*KhM2WrjAy$GYp2n?{&`cXFbPja>y_4YTZ-iEqWDke@J(jQ+EZd;hUS3sp<;C1&R zP5-->LM#9&`mlrXTPVmzm5#9g)myi_!26OWsfMRhHd2ZcP+qpoB7e!MB_S!PJ@}VL zlKxWn$9q3Q>5Z`s;&3zJFda~FqsE}_v$`Me=6f!g+uW*p;klH+q$T0SGv8&kydRD_|;0NP|f{b_*2lT z^`|Gf=s2C=9kc*pS}&QW!3V&o8d3e5%tUpz%j8n%Nc(FAm2(KuDn}Ar1 zj$TTY!yQ@ZkxONBkDLv;4wL0K9tYG?Lb**<=;|!97JYPxFgvDu$|S-tCHY#U7VJj; zSC%$+bL*ZY9xQeImP8j)`$6ABa)${-TwK12PP&hr#h)piYiGb`b1j1CX8LEQesy(A zEJ(9%Wmx7h0_j0MV}L1@H^ppo^r9oDnvnkB_pUmCOw4T8%+Y4PHIbp8XO!gbz=wpEK~eIPSHn0XOQIu8R^XJ(!=nA~3~c?-rbn0Y4hZ9jRX0eP-m~s?#_c zq3P$PS@Tb;Au*&<)xgVx5(yBtm-_OI0km9rx!T`Zjb^XZAKEnAK|nWFl&J?*l+|tL z|C<;DHauE5FHCKbd5i@8HUIKnV1bz@to#R8P{<)6mGD@J{|rw3HxUqC?KOwr>Q?!` zFjJ@4%#yDlI@ zVEh$26-O#$nI*IwW&Iz_W+SK1i>Ao!ul;oe_mN!VKAorL+#&TdQuZJ#fAoiB)bRC zmHwLxhkmx1u9PMCE*Rva2M^3mRLo?a%QXyjIKoCk`QL7UbwhF$g_zjfTvQ$YN&ROg z%VHF7l~hc{g8{WJcl_AU6GcBzMk4$D0fs{@GA8ax8e3DA3V$#9Y?mgsmA&#$sRf+- z`T<26GY=%R5t5*ia#5+8X!mSB>!FD68N;^4s zTnlApH^r>Bcmx9!MX*H-Bq0qve#T&yhr9}#!?+T~P=kDB+)O`2yJ`mwwv$K8H>t-6 zA}5WX1|5LaPR26@M+&E2L}mS$wlwyHPjr~ASPAwlmv4U%47Mq2sB3f>-$^ z@P!8>Qo~5wCjL){IK$<#EAh0&@uu;YM@|~>;=P6;Zx)9^*(ueDNc|AmE5k8a8woEG zUnJYcl^g@bDs^qIaiJW3JgJ?#%mV8(rStbsGEwgVW*;r0I*vZ+Oj`_sszD2zHjz4_LUMi`AGw&x_+5#Bv*@1Rv5dH%P}nyDSDVu3_t5 z%!@)Su0EB|?Je_{L?S|%fIGm!mMLrzl$Fu<@bAEyNaU>msP$?3h2Oy&aQi*P>f->~ zp*-ogzoNeH)f_zciy>!P;#Gx7M`}&_dk>c_B|+oOJP%f{|5eeq6hV7PvrE7<>Vp+*2zti>f+<9-AKbhhbJ`C)JBRC~I8#NhmCX_U+IiHY=<(fdN>f)5wRMH} z=?}2|T(Fr)`06ua7~$FIcR!a9Qq%nl=XFM^Zjq>X36C>$t`q!T`AlEHb}(0J{U%4R znH2?f?aBD#PKBzK|CI2Xh34~-PLAZO{s31KuSX+&>3X>+ea??pTcWt1hg-Uh^O>7 zzFX7DMLVv0P5I2UJR)0*PhXr~BmRg`E_i_dSTW;SGR7I^0YMQQ+{lR_gQ-O2yG_oM z*QLz=4SY>s0;1MV&8LA?Qmn4+XLGMUK|3aY+(1@#IyVX*MpiKL;RxOsDAQ2CsG*~jt1i9c;^Q7N;CB8BT z-da{{Gvp8|PYTJ&4_5Q?&?`s;KCX{BA|`A9&^tT9ZMR1O;OU8!Q7E^Zg zvu6DbanbU= zxigvs;m;>v(@;y*sjE6-sEWv-m{4OMqn#$xg9x(>I+xL`D^1Q_zpxXbD;6g4Otdo> zZ;$Xc6Zz@>maojtm59G))^E$jv08RuC(gdUB!)}Cquo=9VvwjvFHqTHP{_R4D|0J~ z2O3l46qd$g>VmgYC6nw?7kAm4PG!Y_MWH<*zRq!5SLrdQU~c9c?U1+Gs(C*_cvM}` z6_2tAG&2+^O(3@@DFc$GkT=z5Z)2`08m0iBw{7pc?)_P%2xRq`QDwR6cnD+jhRv8= zlj?Nafc(mnEklg+X))U}E5)T_(H)kOlk-85WpmvpZ+Gc;CW|cy#+X1Oiip7vPYdKR zpD7H$-HV&|V92yl0s+^IFVzj6OQF!tQ#5^A%(ybhnj)DEGOkwF@0(&Tterj@D0Oxp z;bx#NcqA3in=B}Q`JOmgdh>De7*Sn2Ab~Fj`w}lSv~(3Qr-*Q22B?8%=g&5#fK|ej z8Nle)e_E$hek+rTezmkt_YfGGG-X~yBFiPnT^4s9Y440a;zKU^HHLwb(@(kHkQ!i* z$+%pYY8cXAackU@PH*D&f~L=SstJM}Ipjd42C@^o{W!2AO+EK^7}8Wx!-m!tj2K^D zP(L((bWg+MT$&5?RYQ->`cCMl+Z{hd@x>8)b%lENkJB<{wnA_=wXu`_mt zJK=dxSL@VyVBz<71wOxS_ zh)A#cDyBO92bu;2(K1~`0GDvPi4a7wrQPAZlKi#Di>CW;36`{H{yY`FkD-r5Z5HJl z4}1vtNu2M3F(nT2Do9BpIwmPYJO&XSHK86Lw6oRnA7VZPjD}~Stif2H8uKT1H>=d- zVUcn*=uYq8M^L)Q2vSgatB+y;LJkapVUwWOg?Vv1zR%pxr9oGrPY&;ne6RogY|+Up zN69_ESarfGL|h74c0BBDtuPeg{qe;UUhF#Mt7IUk5;S91I+do@puQfFXB(Y!4qlk*B|Tp=!&uA2l~-^Ws6* z3TbIRCvx17dr}0NtRGeS3ynN{PB#lErmGS7P(RA8Z7Yg=VN8w~`E@m*bRWK?Ql`4- znHO#eeMMD0a}R#auIP%`G*!r@a@E;j)U^?g@PDqVu;}6Fz~XV#W2S8@i0nN}uCW~q zBreT%KRlL#$Emtfp4q6|Mq*nO-4W%c`>I#7feOq-E;=`$zMRA2O%>aBcSer@t%hH( z->^T!=)>M#uh%m%eD%Bd$dNxJwH@u^xTKa2wT0X}3_^p*%Ta&3UTIBR;33{J145)- zjmRKAHISr`0x66HI6k}Pw3v+%f%O74CBA7O%-Qv}W0%Ho5idl{E#(IhGk$JR2TKRP zG5ye+$ccym-g_%5Vihpuz>gg7%a~e%38-j@OeWAByHb}qvQW!Pi$9zeXcAzqomGUs zLeQBmN@wd5V-{1$#azybK%TGd1b-9iKjzl?7~oeF`wE`NyeOY8H22iDEKiwo(%}# zzYMUpX6d`oo7{?y>y>Y4$z|>_b(Q5NJJ6DY%)m*0c(}sA%doqO}>81 zfc1i%7u^*2Qir27i{ZuWTrG=TF9;Df#k@_388edtQDUKNsN(91FwFz_y$MaW%n56M zI;ouU{QS7r1Y%_;7Y>x3k1)ngXTPmkp4~nt6`miMaiE!Plf_SLHk)iS>{Q}L7z@H1 zrX{!$u2~nkRdWvE6zyM%xUBrrPZ`t9he^}~0zemw8 zE=Vy^b2r)xj0l57=hxy?6;z3+Cf_TOPe}-XZYJJ45M(bL1+2-HbehIfgP>$gFNq*s z3E=)Rx0d+Zi{?$elFo5$;-Z_J=Vs26C9@Oz0ut9e=wwCh;iB*%D1)Ltg5lplC!W#! zIuEMJU?gMg>Q+jKXNOw+(SSM2>jiz)kaXyQ#+3@KMdW1L(AE@fl+NG<$B5iv&8N*h z5ls8_deo%yAvYc-A$Uz29W_^dpPU>%T>B@o_e?V+3YwsL6{dQ!!!T+)`(YA4UURgd z>(eCoWkurfJ6p^m^xB03fUv(M#i-7HS1-o;hZx)U)}9ClR)E|!6dXZUBwTB7BE{q8> zB_CB{W=k6n0fik|JM#x2+iP!qJ={4a=&?dDLL!Mrk1))~NrXl+c1x^!&RQSlT(cCy zL9Z%NPGWU2(n1K-@a#ga+s6fKtVA*c*51x)C-)+WoM+Z z?4)JiYNr~x;$PLT36t`ARfK}=jrW9~^+cjDTe%(T}Sn>|6u0855Zk1#q* zwW-62JJs8b;S1)fp0y<$`8cLXBM4*YPm?%`v_9*W+c;M95LootSws~?{5zcwss_26 zk!z3#4N#4H+vO5KTL{$m#c?IhVh3X$jwm9?nqrDs3(&#X&%hoMrObd)_ymqsQYo9Hd>!VUVHfD}YlZSLhOoH5W1Z!LN3j-}1Z%Oq`Mp-q9YZn| zR%udwN%e9bvHCFZwtyr#w$y?SsAJ??>cajQV_U@b7{OVr^o_t`ZE=)ybk2f_UyOW= zTQfusK?kKB&%6JA`3*KUe2yL?)o<>zE)?w7r=i&{ivZQN^5Fw)L4x1BUh+1jE;_Jt zV?2uqL#Fg8NU=^Ntmes1NqR(lZkqU*J>-Sfdlz;d40*9uNLP2g$0|w76TpbDaS@=p zO&_?PnS!H4wwkRNsJpyLhug8E`Wrpq!-5n!w7u(>z%pi_S2KhTA&f69tFqggN#Du4 zw=muBNVAtPuIy|Ok}BoXHk{oZPlh$qjW1rVeL4go`dj3wD8BxgIL@E5#ZwhjNG!ug zJ1%Gd!?)QX1;z?v^}XLvkYKr?E1?>hZApnkRi3WWO2L`9P_z0#?{*PHWXBgk_uLuU zQv>~(I^s9_8Z3b|o%Daxu|uE+Iuuaik$X7#Pj^h*z}3U*J!)P3N;U`R`fX*0P?Uc1 zpOl50vkYHqxxFBjl)MPyvLu%B!0H1kawd;6=suB%k=SA@{zrc~kfeH^fxT8vzw{km z`G}-KRjd%!d2)`D91@tDA%J*H@~{aynb!Tn)uWpPUhWQ8f@_r8Dx{X* zlqj0_M_tSbrNq529d$O)9QJ2!+RAJG7GjHOFN%t~sL1^@r>h5Y0`8OLKx}~fUt(UG zTk1_M6AFtU%83a8Zm-wT7AD5PnMJR$`h^yPk2qwURAFOOujeG>TB%=yi=iVkJWb;v zDzf(!H4qHj#bgfB$C7LK^IN5p-$sM)31@H*i|=1dsf5YkjBij3X_>rDXbI0>Cy-$4 zep*dV-(IQD&Ie^vq!WVaQ zKr_zMs6P5WOnvB`9Xksqq;q{z^_xO5RvdV>A=^g}xF)!Mn2|G8qbQ5GB!$HC6dw{w z9+_7j#^dWbAPhmyzXfn$mMo*7Flg&xa7Dd%!S+1|zVc(U)sPE^c;%xO=m(#QZ?_cG(mgX$gc95Tw~1# zYDAvDyolns46drmW*L+kpbA{R-s{$*LeHmbM@?Nl3i%OEz$9)cBy6ZBv4y1F-#wq1jZyh!h*F&SL-h z_Q6spr68Nj3%lQVT#H>$n!pzz1{%y%q(smI-N<_4t=vLmqMU~dP!3mIJOk8OgzOdL zY!8CxH-#)sEsUNkD|ISRkz)mLA}(Uv95D+{r~&5Vs~{Q0Z&djI8I~IIX(8t0vwyE` zSzJ0hN(%ydd)>GYBV>^4q*ISi<{1lJJKvr?ZL;pLBg&4QtP?|_{Z&J9ue7l~k~|F4iu&SHC!qQsZKKO%U)XO`@XxoBW8aS;1El}`k09#$^?zYFBE zh=G!{(c&%6T>PzA^B?}3YEx{OKMvJd4wg8#vx+}>4JqrRKp!YD)Ie~l4tn-qnyHZ44=A+_d%%>uLcd&Yjh z83M^4-r(3C?if!YJbewRNSJq#aIv@h@;1g zc>$ONfQV|Ml2*ne$GMB1XQ4^fDv}44nRIAj* z_9Fn8!x7cG~LC%G$b z*mLqP=?z`Ct!=m)rDlpo*45g9MBpBv)w;9)Yppr5)Um+6%`3kkbU@_kHH4@c_R29D0l6AhhHhy}=Wx$? zzYnqoSrw!5BM+`Cwx6bsZGDVCFIE&r{91FE-*h_oJXj(6mi?Qxv%!_Z;2d4-PTAmA z4PUMiE|uKqySfNAX2p5cmp*?0jsKvMc85hIYoqEZTxPfw%(l$GsnA^4SB6z6C8q5f z3?I>QDOl+|knhK_nLH(|<&*u47%jsBX|n9Y?=-fUR~o3fh7vp#48~#GfLm_)^F=_) zjY(ful^b95J$9|shl%QoBG2F|uN zNUL`Bz?C7Y!PfEJ==5@xfid#Ie{iACURHB z+&+Iv2>&r%mwYgN5*2<7zvmyu81mfBPoEVVJU7f(S{vFP+@%xhz!;J&7}GZ;66+-> zI8i|`-RK*;9o5j6c>x`G*52jZwQJ)fZVvP`XCxpa?ihNire-J2JyW@NV9tTi?~{=qrHk`-S|K z!SwK-zXG@qc|)0M-%aPQ$Ec9J-~6?)CH104^Wp>i%bC*wnH`kZ5r=wOt&6VxL)=)C z(?LneYDn^@AJfWqu2(kfCti&0wPX#MCQJ1_y{cFIso2UlUpHdyt)JQX6mLfqg?$_> zSfQTy&_KS=!}|Ya6bk?&0%@5Hh=&dB1~o2pi2wGN7R=EW_{1?-ac05L3`WG*I4;{4 zHRwc|c4!-@1>Qorg$w!Qg>?OW4X1;DC98oXxXT6T^7^}%iBOxR+>twD%qxah&)4QM zr1ZYNej$;F`t-xk$A0&}?O*!F**hD3JHP**^xIIzkhQW^D?Hr#?#ZWhM~wEtbvn;+ zn~5YS1xz3B-@jNr>b>*9h*G#1vDaMyppM;S-(>}pE^K>$G`VwjjdLF&DJkPhZ}!A4 zuBXbey!ZxAET8alKUk~;ud;^RpdbI z$9je2%d`%1A3o8pg|mM1Dxk)^eEW4bO%@# ztqI=J^kr!$|LJV9b4V6!C=aXF`T0wUV=WA)0>(Y4kKxnism0vy7yl_sptvSpCpJ=^ zACR8zh26xl-^983bp)peLl5#DokP;MR^Mm2@Ux@d%ulS-pzHyuh+P$it=*bb7GFdjJhym_pVH8ZkYRZ+*bMDm zMz5xM1(dNA811ZTma?vF`-OnInG0XIuTgol4w$+CERPEo@8p*9PHDb(CZ-Ihco@0g zx!vWO^xpoFIQzV_0{HE!0>y_pSMs+fQ5C)I*W7D>;jLUP;T_!v*$`*Ua@5n-Ae_Bj z!6@0A6T_IlPzi=G%u5m95Chr*Usr58b(+q@H|;Kbu5@I0O4Q$Qw(3{JifV~|F4y8h zT1Bgv>ENVgXRGqwMdX)Z2R&{N^{)Os*ZC(Ab^5GV9fhlg$Flg(&O${-zODLr0RNM! zV+-UG*du=zG9_BYPYxfrW&6h=tJrmC%s>usot_Ts>I1YIE@Q4VAsaktPbJ$+<|QoM zI{EsCSwF0{jVL>wkUxH$!5fRVHRyD#!qCC-%KhA4!o_#kQs#fi+c;?RH%;0}`@lA77jey4*&n*zTJxbosXACJ)^fN3yJ0*&yNH zk8BjBMWX!^_2@qy?GL}fFS1Ggkc;YSb=At$b$nC5?O8zOZ*ffj9!y-PK7W2N7d-yv2OOePg9#I%)e-yA+>&c^#~Y z7w9+FSszKuC+bN*pd%e*35}9L;NPOI9*A$Sonzhdk&nG<3^p2mLdflx!n;hkZO1EL z(=np#ot8r3VOouO(HBfpRS$5(hrT@;M@f0{2jWt9p1;*glp2w&hJu$~Jrg}ncQm=V3-TA2xKK1v=;0OJNHkVVFVG1*R*^XoDi6fjUdC8meLOUzrPc z{nc~2pRm?m1yF2!A}{cH!nsjJB!FnHBa>vM_DwVX^jF(jdh;DNyi+$d<%P-nx1OoI zJ8T>R2g|h4%p0HQ`aV&8@fI0Znm*=^gLZ08rL!6}8!=4OBv{V$Atb~V;C4U#OKBR4 zh-P`6J0kZz-eF5RD+PSGEwSU3)eY5q`za&9lr*4|VIJ6d&m+3&3lZwtUBWq~gkRwn zvCi%s?W~`C)gq8wV*dC6WEX7R$SWiE1OcRUe5oX9@#}ZpsP-wSXUeqd-?Jfkg2MKt zyP9%ggYzpZIZp1FVt`rejSsd;`Z zY%GL_vQDTWp)qvWbt`MqXy&8}uYl^g2^j)kHvT4#$4L*uTYe_?FM7}~eaPfAZ^#*_ z380C7E!#NrL*FDc@B_Lj2HmX4PDINa%e(>BUKw~waydLV$)`=L^kw2@Q_I1DOO_%@ z#|XsF{5+ov()p9()N`1}clMIt}%S%4Y0^k$(}!Q5iXl|!EmW|O(D%Hj|2|6@7wegR_)J?}X9 ztWs7$u}5UbOUYBj_m*o;iU%+@96R*xeo$KF5X$rn8f4l4v$|VD zdWR+bW;uJM)bZk>N$n%bZ=<8bT3zGO9^ZVYIpM?mOZ%4BY$%heApasVuQ%begtjJ- zp|{BnJ6v|WR)k3c%j@x3oiF}XztI2U!Ly+@=xRx8o-ujbO|DRH69c>rcpd5Vxg;-4 ziMR{d9V}UWbAHlO8^&q$nZhD`LqN6f#gXEW>VEuiEnZh^jat|2Op1BLszWvwn=NiqySu8^v%vU`JcUUdifqz^F)2xQszc~jn=G^HZ4p)dnKisYRL+gA)Z$8 zQxm!N>nO08$-X>hpOXiG&&Xj~ikd8kOs*zsmG*JYHGyV>lIzi&Z<~br7KPgUNOM7} zod|>7{V}&`OCu+v$!;ZLL3X9jsW$y5%q0zE!e+*n4)jj4jm(fYwNGpIzC6!DPlkV| z=cX;9sW&G3OSe}>^WocsQCr@N`WO-`Rk!ltG6D~x@!TpC@x~dzGn3L-TpfcbK(ET< zeGckcEzJ<^Rt&7Zkz)6n%^~SlaKrNth~v{5<EwuFXa3F zjgPeKf8B=F`bRa(F{Wd+w^EYF<`cOUdkf#*!pDQRLs)OeRNb4fEvTJk58Rw9cGG z=$AWTLt(ts2uE&0&1yl<@)GfhsKzw40?UVITF+JnowZ78#Av|0#Hf5c6God_Hn{97 zOj-$!BsALHi%4W~F;$01O z&on>0A;?Ti5P$oPI8HkThQu9S#9*jYHM0oqur`Kv(YTYekav`Ic(bc z+5WH%GhF(~KlDuanGYn6h!(e(&1=ojuE{pYumP`g$SX6R?cONZ)T-qcS4zO)k|>s8 zgoz+MSA>Z^goEt(HAI=Xi``(6eOtgTSf>ff?k&5)?&?D}#Sh5K?p&-+;l@O=M)mYlyV~;`^rHbF^`op%Chikql(o|JFb56ispf8Y#ce<}AcO zM);HD%g4iEkxZ_nV_HzBJ-nuN_R$Y7EQbg@j!SnkQXe0h{N8caHn7mGKwR1Kw;2qc z1q~Q576zs<2P>)xfWJqliZ!dT>{g+qAW3+6S{S`*ag!DFa_djC=YUeu2#(1q-E)(l z|2mPg-HE85?*RvQ5&HHwjl7$sg9ILc>RI%E5pyQ)d=wQl`s}E~&y5cLt8qAhbmr$j zweqw56$wO1SgNI-+gn<5JM#u#R80j+pr>T^86$CR{{A8}Ja5>cb#6;-tdiX^g`k=g zx;#*R_D!B_zm1D?j>@9xvP5%GlP@KY=AcHbHtp~+e4yB`4TTm(DgQPUCIAA@`vnY- z_Wy<8bRR#`4HidG`mFKmXU(*X9W`jRTwudneqE6-qG)14-mmaMGSQj3LrfdtmGxxu zg#2AuQf;Oxw+AIwsc81`nz{m_37$!{0e*%jQ!n1N+Lw>$3N@X9*V!=b-A zL|SVE?So3<)i!X^2N!5H3Y($;BFiGvN@7WXGNzYSHXQkn=81mXsfBpWa;W#lYz-`&h{(wNd(P#~Win>#-2p;k4tM`hyO z=U-4!=Y!h7$HxtaUfBNI)=Cgp2+z_TkhD%>;`P?Ii5f5*XD__hQ^ASe{(}TWb6*L9 zzwF*~qaiHiHR_&{;p3}u+L%Z{#vrH4?_6mbh6Mjb9X0z|aKXi&z|U>CKgJtW%#bH0T#Zo1H;0TRx4f$VP6m_B2Ytq^x*!t~ z6QpMWy!U^XeCbwPD5l+nF+?1NV)KPRn~F_0H3L3jspj!h7nJ#V7oECRHX>i>$y3vv zXS)vQJn9YzSnaRp#`AEt!-+H7xoEvs`eCG+S0BW4B`{Zd4{wPuG^v&v=^yj&v1qfu zZ?@O@TZS($zN~MhM)as?s?b@@X1Hjxoi$}{i2U-$UEHWXqoNt#EjPX2RTmBxNXK)sPYU)cq2 z!bnU4A=ZF;Grc)?(zuQbx16mJ!UvI%Q6@ig4oLC^n~`b;2sTk$ZLnW`0ZC~ETlTKW zW}%zfH7>hD!=KjQGJmS~^f2UXx8)Ss^~pR+4t{>u!l*7Qe{;_JKq?2_My1>y zX*iUm>Sb@;PI?=8y;&58WNb9w5>y&UOWdPAfJk64)nF3G=JkV%&S|c9N)-%5(Se66 z((}w-Z)(d*#YlTxvUT-$fs3ij4JYZk`aZ{ciQh~I#PLZgQ~py2q1p=XHb(&uTJx_u zJx@Ofx+REQE)~u0@Of5sM^~wi2_cmd^%m^aAi*WUp?8W^awi+T@Ps;CL^G~GMoVKT zFdjWw@jX{KR)Q3dNcfk&M0IG7WuaoI32Dl$bmvHOO+0FBz)T(jFi#cv1)$umk@1#) zN}}mrgecY0VR!PhT{Uy0y^lBIKiTVTnU8-Ik5y+NR$fp4Pv!>=e4q^R5=EAUCOXAk zB@+pie=J~1qh^hI32QPJm$~xbL##zw^&4tht5%Eq9la~;a);do;)t=^^6#;+meZ^} zqg-l_{X7;zCq%sn{2Cg#8)&R2g6D44B^-KJFE?RXlCF|9BPUE(jXv_i-)|OGV;K&f z5maP44x1Mv3YF^h+p(-jayq3;vy{E+z0?i$1)^o!%)I?}qD^$Qg15k?{f5c@8AOiV z?+>VYSSrqL#e$zZJsahllkwAlSSUbbWBs!_v9anFs%fKuGxd&1V>16>Lpmd?Eeqqo zg0k7{LafT#MtAw&sad`To#!d8GE_5!{8kS1y8PPIsL z5IES>sTVpg&kd8GRuyPg;W=txIozz~mw?acJTZ*|)dlhB{S6?-u%t;}3O!KwG9oR$ zu|j3i!6$)Y69HF5Rkt^@ECKrGDXm}XYK{7HY?d@_*3;Qa?yIfy$kl*(;Po2|2a;H& z%G?SqLs#M^)8gK>zPk`Y{zdR&zS_ zV{WY8beYFIfMxn3QLcg5j2k3D5h|GXd)04Cf_6lBolFGu{%xeGWo>G#|Nc;X$^QE4 z&C18vt1W!QSQ4@x>Sm)zm}i1i4h_v(-;NeQ!>N=>(Kg*y_JJ`ghwtuBkJrSB;S%c zwH~`=!l0^Vvn0ZRG4H*=oe=Kz`fxjKKJ>VEL^YEYGhV&SxA&=v_I+?u$P6i3`@=Jn zZ!mnFiLM8+GlwB}dXNl7!(4n`RI+H5FTT}-6PL0k10-FZr;T2P_@4{fl)YBwkwylV z!$Nvsehf8+HzoxiDn(?Yb|#5Dw;%7YMYbx7tkA?3tKt*E!b_@R9~wrslz~AO;W|+9 zDhwGsPp7_is4bG@C)ud|x>}z+ei>qm;cW`ii(edSxJY!%#2bmGDL>o&nERJrj{<*( z&9pIu$K))4I9bJ%;jN)|b$e#+S;lhe%{Jg&<(nPz1#Qg+d0WjKuu!_>fGkBObHXgO zb*^L{YIOx0s?L|qUW(4NB7A*fIlYQ&HK3~Hh&5IHzIt69->KMQE=2EBO)h&xzDhls zme88SRu0Bj6d$qH+R+TiMC4SBaR2*L$561f@*!DEuah3-o|((=*TgKIzLNoa3$Rro zu5xb@t~ca=rw%S|CMnpm`H=jfnpqPPs|kH;Fk&75&ne03yo~14TX#(;OjP5N=OSqF zLje7w(*r`cjFRWkI^9bDaN_OscP)wkjq;!r@jv_6;~BNiA5+vnYON@Wwzn++8}PDh z7aP_-)*$1p|8j1rEkAWn|)nM;doAJ|9KEHM^)xA^1$$Y{B#2 zk20iQWnc@&I_QG7ctoV!D3P0}EvX#_aDk z5(K}Cl5})7i*NDfd3?rD?xLt%#m}r~cAA^1s0bg|+oo20FlX)-7dWz31PH7GYPV5- z3FyfOQ)EH!2pEV$n#iK`{NL3a>hJ#nz59B?o0npwMUZM)2Iq=kP0X}yJ--%a_#Z-yp?%W0 z5KAM;=S*DzV$Wrym8UqRAWg_mTIt7GRZo-60{_`f{)Y_*s$2uBO~mEqbv^w!IM#j0 z9jK%YSw~4k%3vO08W92JnaptN-G?8TU5p{bWC0y7H1-}5DFMNEUYlj1g>Ywm={?T) z1Ge)|C<#jlPUQM=x8iq#j9guoyMGD=cK2f*Au!fwG^qmu=bi2LMoke@OT{pbmMWgp zCxX@o?vH#PCHU;)?T_gR>UrzAg5ygvqW{h+S|JJI&}W)mJ@dA4~}GPE@6BCXPg=WOuf`8F0!R%KTai>1nB zl3HN3nhVYYA1d){X{=@W*g%kE{b#LJ8D##M`fbc^MkT}Bqlbnvh%ytbDhm`%9(q$x zTEbLan*3kQbKZc#_Ue4`rKzXNjD!$Yn~58QSWZM{F@jdqlmE3@!pjkjjo_B^fC^{X z8V}No;T*1a@uF|KJ`iM~;wu+M46AMH7Y2c?zdz;T4AdK9V3nf-AExys@HfhBaQ-tB zHN+wUay@D+UU_nWz zYF3|=GbiU&R1U3wCkqO1q}3K7C`VxFHybust|Mo~U=Q|X%3-ofs+A_|--~B+sHH5c zqhqv2?M;c}{TnRJZxj7^V1t>62$1W$%@-Ng^9kj(K8XIN+NL4g9)QV4u_OrFlPF>9 zV~oD^sl!Kj+!Ukt+XEdxHY+?Q8!|j?VH86ROWq!0H9C=s^i-w_7TdCdN=)Y(h#@y! zRO~7Ao+fAMmm>L_!_k=C=j)d~srbWCFu=ftw@N(P%y{G~IgryTIor5c(kH_oj-AoJw1-el2&>Q*mpG?|} zFMb63-8gF}5|DqhJz*4Yz#ed`JCFU77vm?NuEMPO^y&t}qMIACnGjyUJbOvj%mW{Y zJxZnfdU<0&Z4~6HH&qsD@49iG4r3Mv&U?xHp7y(yXLs6=i3#1&6=t}3Pz6%Mu6plo zMFW*whSTfDs;5a#7f2nRW>Y*ucQDk%jq`F)cFa!B@=l$d7TXmeG<-u>SP)GM>fK9`Apd4w@_(REnCFDF+}PJ^&BGtV=}4B_ckR#8C3Nepi7e{J zK^|jxLZ3F-;BN{R0^HpM6(P&kZcOW|bLF1;+|UA$Xq|YGPwvIOr7q_&{MtTXr{+n7 z_)q#We}ca-e>Z?Vg@d%&_9ZZ4@*=h6^z1gk+U(OnVG@nXK8P9bDHC6_lICg#h zdo2^bU1dQ^q!$%AzvF;+s`^P3Ov%7v9KV^adN6<#Cf-YxrL77k{sQ8lHJ)%UI(SaI zFY0y|@FqGDjhX>JoWTaSNcTR2!NgI?lc!<)Rh{eqe%ec1myDPV6y`zqr(Myf$rAy` z*-F}Z4_$$Jo9l25Xl&W+uI`BCqw*aVQahp3vR+Ll5D`B#2JNJ@bum?iI>VM3mHbw! zUQ}-ylX=gmrAGZjaUVgsQq)HKO=SJ=#D4d_CUW!qG{<}OSr<&hOxlf)nG0b!u~OOI z5eTLQ1sZwP(b-qL2~;&KRFb20{BDP$;njB>0_uKN>;#jA@!CvzKbEWuSS;KMmwyCu zywAGqq-3s8f(4{T>&aPz?a!(P2D+FMT86r!+-Q?)SCwK}B22H~_<)8o9ASHPv5NL@GSl+PzirAG zpHk;k$V|zRie$v9%b(UQV(z-7GUF7>A83n*->Jv_?n3ZEBH9*PY>Snn3x@9mOR{O1 zC&h+=PG2llf?_LVysOXoG}wh;!CGI`GJ`HIb1&wF90ABa%yKFiR%~Lqk5!Vt#x=U)rQ*mCLs7bibFXnQ0z3@ax)o|k#TO!|vL!$lEE^wSwi4^9$@lzIYt zc#w_74{buBS52y4w_Of#&|Knsn^XrcpFRV46u?wSE$`KUeXutQZLr9EcftCCdU_rF zZO0LL_V4u)u(E#fxBYF3gf`SWdv9O>?T>w6UKj8r##<-POsmm$4wj3IIY(<@@!#vG zqWW)kT?4^8oGCvHVGD}>a*Yb%jq@PMnS{ag`r&eTj%aTE)-dpXuoWoNCSK>7>CbY>>PefQl z@{4G|S-s~e!GxerKqDM6CxKND{A4kz!~y-24HyBBm-~sajwFu=r#6GIG1*~$h#!4j z65oGDRwg1$AhBDlrmd+*WNLy_ORKIR>hKY!&6X$x{^HxfZ(!~6+3>T{BR?^cugRCw zaiYu0;{f9Nw(uFuD=+NdwhS1SVX%8K%ql7nwzb3_>cc(vi7`3cmp}(_hvT(v>6h|< zL~ma8P-~iEA!l8{n1xi?=dk{IgyeqUEO@+%-@0Ke*6$4x(Na7q@qLKKp`!C`;5;~i z$$6$jtfG%3HGlP3$EyL(*X%#He*FnXCdf1A>x=fgms@Oca zTXi!MbVR0DrBD{#TQ|m(V4ZKU}x=m0?bxo!UGr=5tQ^VdH?d6FHg}V5r0m3enI?kh~%60 z>-C2O9%*mDqygMJRUu!SYpX{2FndNH8N;3=wVr#Kl_Fz^@+ybXMMutB;C8u!vyWA? zLloey{D@iT4u-xs8sw6ckx5_7)dJg6fxGKTvw?x#K%`TYAH>ai@B#mP3psyj#Oxj4A92r| zn~X8d_bG*vr6)2|jPwyQJjJo_V ztO3wJ7Wx;V{{Bqmu8sDWCQ=-O1^P_GA-u%o<&pFPd~}lyQ5LXx2o^j}c?Z68EP;@N zUM$E2l^hZ-+}cYHzoTyX4`&%b7dd+P9j-#2zV1?72NIO=#aB?})q=T`UuCal3F5(#KIuY4Y^ z+U7)kz+^Li3g4VA$$tv(TPnTN4JK9G4IZXc9<5z={F|aJeHDNJJ8SX_r4|<@D*>na*CO<72E(oJ30@0ioc}VKnmwH-u_B3VEFi zs_Kic4i*wNo8$N~8f2lQI166hA7q7FykY@3P%~=hR0Urn^i{sSFh{lZLwgvhzQ6P#}gl_=BRP95c_3QCfZ*9 zG07)K@0jwP2RgN2@!vnFDf0V71f5!*QmglO=RV!c1ZQkd`QzULOo$^goRhkhUp-db&{0PbZy6iijknx9b9=nDbr74AX7rQdXu$JoR z;^?k*0_PV3lUPew#ecJP++k4b#L0VTWT9t}F*`hBXHM>b|LTb7K%GDfMo#dnw`y?U z#-buFt3uq<2JEK8C5AKtTTAwq;DycV;_lhoSOjiy(r{n+Mx7u`pyeoRwyKic63W7e z<|PI3_r}stp>sQN|C-*lb>8fJ2_lk|+3YXwehiwB?m6*W-}l+@noAq)sdA&ge!fb^ zhQ3k?CNKo}o3>&fqZJ#bl0Fa%Vi@m%)X4_R_P-{z1%cBdNYuf!#tH)HyP9%-I8lhO01YbfY_WR6;?ezjM^6Q`NP5pR4|1C(~gYj z2A9V~oeANR)eH*_bK<0nuwz00g09&6g&$F3-Xlw7R|@A~p14a?^}lJJ>G2y|akU(2 zX|Qmie-!c4!3w`Glu`&;8`~1|f7#28wkq_X_Q0&$mdFyLX~Jnf6~a8qRXyGZA$){^ z>?oFN|ZW@U4&72;#}>MY^**>Pn|xUC`=3B`jZl3LOtz8R=b)% z@}x!=yvdyZ-msxI7lxEOCiwQksCsukJF!E}=|`z)J5HRRDp=p?T=bVnd+i%aV(Xjr zWXl5k%zH#CjE08+v+9s!b9U`cN*FUOdU#Zfg9=|=FrX=H_H#84 zRj2TKJ`5fe{Pes1;!2ig^@h%N?FEKEywQXI2FZ ziaHK1w2=ruGw{HVGNWQV#jxz?%xhnE?JGs(n$nd}Hg|dlQOp3LD>K6~`aPcvhTwNG zj{t0C61)1@%X=v*Wp0(iu08))=khCK&)C7X#WyB8A+BYdFrgOT=m`X03y>-vYhz@h zIc=SuD!_oR1U{<5B&0VA&;U=jiixZx4#n57=Qc}`UnYXE_&;)t3GbdwAM`NZ+qD`g zVF~V4v+VnJgq4%L@^}U>VcD=e!J0q%RJkuwC72t2A!^Sd!1;*QdDcR`vAKZ9HnNY&?*c^cB?Q;C_5M*q1}^8A?gS2dvn1(*F2pZl~Q@ z!FnT)6TtOV5scH+_?{7jP46^&FybqUmf?ntKZHK|_!sj!AZkHsg^Si{wxRsc@jq)m z{T|DJ5>L5(|@ch4OL=0vcJp9Co^=V%@$LcF~-a)?kryuu*e>dH`zBCyr zXBQQk#U7!Xtn5}iB;WT)F^9f;EK)f*ADO8^xnnJK@*42n>3NpAp}H8wt@io8kKk2C zBOA*+E+QsoN2>>3woo&5%Cu@m&40JvV$MQ_wH&mIZ7aMq&cqTWMj3<%Emr1Wd=IaY z)@v;4pVkG7Nv$YHv7nrsYVyN3HQg>q^?`bA!N$;kJgJT89~b@!4r zJ}tha5hYF+<866tj+Q2ZW-rLWttpF1ZYiFn*SbA#hWS56|5BQ>H&*?#!7)ZxegC82 z0*E-Ry!*b)$*C6=S)AqL$j(~WzZwDn$^b3!96*Ksvvw-$b0S&ilPLLi8Yj-}>RR)c zWU-Y7_)Z`*MRf5*Bw<2azL-ShNwdw!y@udX5@sC%k6Y*eHjJCsmDLnvhoHNq9}#1C zlo*}-)UAXtdqRu$q!%|`j4Z1J;akyuNpTVrd&(*d@7n8=nyi27Pj#-Yicp{(qa{Ms zfp$i2YDa`%hmg(~AC2IhV&=FCn3fuy_6FfBsz=t+6j393w)^hxqwm?`cL5$4*TmjC zV^yScM72wdAjb;%@yl9{v{mmi}|C z`W@`6t6!~~=0)AaEwaK+Gqw`*c2t=Q>9Qh(j2>yNT1n-?%ipT9m?srh6y(3I<1)4thFPoU5tL<7J&5uOaI6_yUt zWgf?A$$2a7lt11-uxD6!m% z1)Ztf{cZW!t?x_Wg|1?rTBa=@X6tKz%SZzgghdNdPiSvJFLRGHyEC%34dx0%-L%8j zN?N-wy_DssF`nn>PLaHcxtC$(JYp5cy1u_Gu6wLJS@6({!g)GPxnYhUKv2JSmZevB zPnzpAE4qP)7VxnGu5{x8P9xjjyR*4 zs7QluR?VELIP4Pc%lvU$D(x0*kzGD3Vb2U1ci>k&l$d>#6DR`Cay%1c4|JFbig!@s zjdg=n(Oo@eAw&D-Omfts3>^SmFJ7Qz;K$qZye%NoPsi!$W&iJ>cr({4re^W!5b}TI zEEH(?p2zkYZZo^X_F3!8=A)9eiQj@L4>6#LZ?M|;IKMuyGY&sLT2F=2u4xuq#V!bk z*Rdu3V>x-&EdC=%NZ)p7K&g|k#j)i1bzI*od?|zsr#O>WvqjuJa36vf2VGrlyauRL zC1wD|PA5(#T@pTZ!Qr=B<#4~YX}!dXq)!`C zA4OG~sNDO^qx-QNR_vc4H?P4Tc4;@4Noy2E-4 zv~KExTqoyuWk09Y*A`Ek^IcWI2jBJxAU_Be6cO@U-?LbRF)!M_z6&3bGShKctYKsc z8iW;cw4D4bdB4ZR>#BlqV(V= zsMjsyo-`sGl%UcC3;%Sc4B7K0DQI_<13_TC%tU@%}RU{qQX%X7PjNLw>b9>(D`K1)Hh9C#jl6j@9Te}Uci760U+;+oP439Yz%@S zS1%w$2eWlPOI*f2v8Oq0Xr*$>qq0CVvMtiS4()pe5@D+Nq|Db;ZpF0lla@?5(mi07 zB=-4wlq2qahy2bVD_IaX%7e6VcVOP0C(m#VJlApt)LxIZ)@}t4Zbdhpw|QwWVLpr? znvdP2*!UT<0<7t@0ZW(OS1r4un|TG)Unxz|g0GzNNw7MVBXOONLb zrOEbl6g;%2oz;BFBHUEo&Dae5-Qs-Xt-O$#DX#c`q%@oBdu2$R~Te{B$CNRDm1Kg6!={@lz()=Yi-^`r6| zpQEpw?mFh_xO`ijKfnp^WeMgo{@dV&rgEO^jMGy0Fz!7+W-hkr>1&2i{QvlI^4Bio zr^~m3{`;ccFZ13{N#%9lu-?jDKdQU|kV0o0tB@(Yv(h(-Eo)$T@kH5Rnza*|wY%ty z$osWqqAcL!5**)WVCK@xmiX>#+3FA3HK6W(DKDM)Zmew8@P7GPC_3ZJeqbJeKG0<# zDS$?oQ@tvEr~a`rLtJ)%!^!XcS?8De%GHhv>a!5rn-l&(F)%Hu{W#Tcz{Qa;IJfck zhW6j-VxSy&j%wjd#=|R3eMvXb?9LyX1uzOw1NY8g6`sdUbt5P|?^{(HCgG_E!9jkb zeHr)Yx9|IO7FXyA#rdV>=7M|1FeicGm>c{m%fNp?Az#bF9?siTuK(t|PHy|l^OXC0 ziuv0mrGY@pdo8sF4Fb9c2=mx`@(oY;J5~L|p#SK!53X|E^E!D{lEX@;*-|bXmnF8= z@1-A*?4>2sBAW1bIOF(}SdLts+htfy??a<$N?UJ`+~Dqir#nB66BHW%U0S!D(RLps z83P7^XYGA_S$;otUN^(DVk7-j-`_|tul-j3E)d}=aPzE-DcXA-EM~D~6U!@HJ}r>q fiv8mbm+paXH$@~5Ms()E{iUh;TBSkhO~n5Izx&Zk diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_comments.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_comments.png index c0483c6c532bebc9c1aa58ea6950c3dbc59060e4..ed78310d8cdcec4c2caf28ca34d9fc233dc723c8 100644 GIT binary patch delta 2594 zcmV+-3f=YC6si=EBYz3;Nkl+iOfVrwO~99<^oWpFMA1AM=GzJLGOMx<%V{g@Eqo@Z;7 z5fh;d(!}ro+)%_rYX9FE#m@ua^hdF^5072#O@owx4-nqLur%@`gTar4DcEOZq>nA zZ4fklq}h$xeGsX;|0s>c*VZQfwYaMQbaa%SD-=x9D8aWw{APe_`7~EtJXs_xv}5!= zPT|qd6O}*vjV61(w08Mh>R|_<@Ar#c-R*~jYzd+72Y&>1i0v`CXbs)ts zbcZM%99_HoFU4IE0sRBrb*0|rWqsi@+*w`W!2bQTwH2|wgfxXDaVWJZOfyAD{fv6_ z(|-fJn1A~SP+EPfx37zVfin7ueXPisQmI6mrmZUx1VsKk;re$F(P?yH0c|cI_41$A zoBuhKdjP#JzVMn<@|E7cF8n<`*tU&f82CYe@B4I!%k<0tK&Nw9i{}X3JEST^NQDr_ z$$InDV6FjV5RON}A2)lt>Fn$z$PV9W-eG^&1baSE5keScA&Rfo zo2SaT1^{5nwxel*dQVReaU2tR7W?#Bl+jrODa_U@&pr-!t0jVl*^is$+$Wn>Txv^I#GIvcZRNfMWo6d{U7Htg?>8}{pixdzZGk2{Xr ztCQoK9oNkCJabcXNU683gH{?TB^xv62!EX=lGH~C11Sn)4}&OvC;$icf1(Z?1?m7v zl5lO}2GylCl-dSCYeX7Tn|_-pSS3v(glH=(*)^7H006@$rZyn}5C$QYcP6kcr1UB{CKO&B+2tdmA!G`nSBe@2UK^O;yH+|c7sGP45g)vg*+p(&yM+{b3xd)br4$=;7x1ju8X zmKK)YTOTNm6e;yvX9(PRQbMFGm4CI`^Rbu9A31>G6H_x81OTEaX5!)`R?R|5qZI|& zbx0$R+N~cF26qvFk{yFu4V=t-1TcJJ>NIeC)AwAT%J~Yu7rd`r)`WJIjk*6JjFww^ zAv=c}&Rbv2HGmAlE89h}UbC3EFhLS?4+OV5Lu2VYak7pO23qx=wCo=|pMPrrnH}FQ zic53D6bhw{t@c?dMdMBddwrZF^$?`Dx3Pi$b%Ne2ORB`{i5%*hv(d z4V%h472+sCDFY*`5tKIY>vz~#I879JmKomd z8JbI1uvXqCNo=HO!!(|FAcNJ96~OyN5n%520<$+~vKhj=m7ctS+u>%@Q};XF)=3`Yl}h5|E(Pz#&tA;?-OO$RNB1 zd}C{d5JprkOwinDBIPy+N|S^R>$l#-_oo4c){h-&x@S)28bAi&W#F|<-*r4LovRS~ z;XUnh&{&~1`#NE?h<_9&rr!Tm%XxDs*8ny-Y!}7adYvm5CrJ~rt$l8Ra8{?N-~B#O zypEJz6pW`n5>fnx0?0&hc)KXxnO|c1>h!i5LMt^YrLg8NVpYdT5(h2&$_3-e4?S4@ zMMuCrA{9GHj--qJBgz2`CK|**U#K{yVaZe@hli%k=O??*~ZAe`rPQ}q(17N!- z3VZ|-AqAx1dm*kH;5a_bMw8juMXpa*xw~W?{(qn!)OkAn@QJDV$k(4b25nm}8#Ce>;k-wyzBWLI5*o$Sy5{(GZB zs>9h(r5;5IwR!``_0d{Cx9b4_eC_W})!#fj`LYn=aUsNT6vZ=P5Mdbl`CXF*?#C)O zX6m$dkfTG#erG6(;+c;`1b3@Re#z}u02_V<;FsL~7fL1QqlGn&C;$Ke07*qoM6N<$ Eg6fFeEC2ui delta 2640 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 FV1jA`oO%EN diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_continue.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_continue.png index 4041ef13a6e3c71038aab098d7735205e7626469..5b492173bcaf958e64c7be04573e9e2987939a71 100644 GIT binary patch delta 650 zcmV;50(Jeh1)&9yBYy&DNklH(lI9YLgXK*FV8jC2l&2$SNvQ3wnvK((9?wF2`@XrWesM2$jlrGV0T zrRpkXHm4~FE`I>5?7RyI_5jRMrzl8A5UA(Nevt)AfiVPk%n+;rEOG21=gVToVdcGe zmC#fQJVZVRV|csUK;>YNPYL!4Fu1_^3=C7`pF+sM7-WI4VGn@j2l+`dlWLPA)H(P` z0v~?t(|Q3QO$Yf&Jd2Kt5vUCOB!<)Pd$dLcAQ*$C^M5*@2jXM|ItM?A;pE#ctt$nb zM}CnK-hX{t8-i!^X6W774u#nW;uoqF$6Ggl{=wwx8V&N&NIV47C{_#3iMwA6*f00e*l7@pk&If~B>-xccY00000NkvXXt^-0~g0yfPI{*Lx delta 670 zcmV;P0%8531+@i`BYyxHbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU! zS4l)cRCwC#n$1cBF%ZYoQu^I)t2b}Hfv;jgEPD41JbCsc2!bH^1cFcz1o16AdG%KC zAS(UP3Y{_8ZI>#x+ucmk?REwNWxL5_e)FFs4NW1USY=Bw0DlI+02lxRU;qptvfVr$ zP|RaIiB#B*+gq1fj?*zn0_H1aq1prpYnF*a@TIl7|KO|B#VATxMgW*Amqnpg698V~ z)n`?agRVUY+5iBf2cK8L|LH$|W($Hg01QvS%e>=E?K}R?1_XTtgaaUhFUc%dWtL&) zi|#uB5<<@?1b>1Oz)->GYK|Dr2~EAL#y1JUmIA!srL;pT#_wYaf(-zXgSP=eAHZ;C zP?(j$XPx7Jq=8%&5uW3x0NNH04YAJip(= z1uDRuj2C>ZItj!|%eqP^h_kyUE~*5qXMUOzPH%TZL(sYrI%(}-u2K?(bte|rTc58V zV$kV{!^U;W$}XT5zW#gHZU64ul_EJDFUWp;?gWpo8Z%FV_00`Z0B_IjUw4N`&DNsN z0T{4L(0`rbLDyF0Txk=(U0ke0Aky!$8y^9+9*4uE5CDljS4GjihoD*yO9xj$pzzDfHLuXSv;js!q3V0P0|kHGqu3BLsMpBN86o z`tmyAQ9YmyaIF@gj1YyVJDiHc)16UxRF7x_TnPqsFE;yl9;^4Se7^1l>Pb3H7hqT$ zK;qllz<&um<6=5Y)b(@_F~Z6+Q|Pf@oOy`ZF~od~KzT!!iSW}@YM(=`+mU)k9`g}3=u+2O<=FiuoG~|)W(LD@MQ$EeZMMKW_l;V zLmVfs*w$eutgpQ0PN>)STcJP~;2_7S z0?5OveF>j@b^HFL4e-Ag17H9QfB`T71|`4%7ytuc01SWuFaQRy4eKAXG{>KTJt0;A O0000vQ~-Da3~nS6EJ1J7q&5kF zp278XhRbUFBs6MLTYnCC?RmoBIW!o!F27f*Rh>D(q_#eQSD5_bBFn6;F@MO03a?|Y zRSphRjGELQ2!BUqH~6xj{012LR}^=4&?t*wG_v;HT+WFNzzf&y8H7>{8^!JIun?5B z@8$E3aRAR{@*#Rhp%S#U?<+uvGza+dP#89u)m8Dg$Bf(Fn8)8y(3ZYp83NC+3KkaF z2AEZe;_fc193BQMfkf{?9790H(`=Rs0;AA*SSZ9d34h9X(ua^;SviqSOkrzYw zd99ToHe3b+dFTa%80Ud)V%t8!LP%>V{1_mVz(@`Z;p7#^fo)-%aPIg7PqekS!(Hfw zty+!4C(AA`n=SkOefFzXi?XPS1%NOlAV80}h9KlI`Ffo+c@hADGIUA+!8ZWGRPvM+ za0FqiUVrDfC~mID`bRq}KTa(FC4}rl-fZ>pZb%IW8 z5l?t=Re)IDCTC}v?rmxvG|G(zAL|Mj0)VH$xC_0n3qWYVXUQN0eE@?=^3==>bLZxm z@B1VMQL77DrDoFz1pPVSkL11idFB(pb09Ra5`PRppc(=^jmcxr8+zeFXye3441r<@ zh~!-u_g%wpo5(gOA!q@(olf)g^fdDbX8h2VAR)9`C;M3ZRzLakU?}eQjId2H90(vN zJrKE>O7XLh`~@(g65ksQw%KZla|)ferBZxPgl=H+Q&W1T+wI^Ak3{dCB7{f)uJ@C_ zsDEho+uzBvAOv0A_X-dqSHQJoQpxOyt*zje5fcO7P$C&*JCVzA~QMa^C zq1`s2J2bSO3V#L&Md*buVCd-IX8i-N;cCyI!W@N1-=THKVKangIS=$0Q4g8YHL@FY z3Y`v@J3~T~rgZ55@L-RDS$R(A(Uh*L!3U;$VF7Jvm{0aySQfCXRySO6A) f1u%i}TYv!ozw1SRw`pgm00000NkvXXu0mjfqkhm~ diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_results.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_results.png index 763a9e0a25b198074c9cbf9a9f236d6ac1221326..ec8bcd737fb5516fed5733a2b55c358a019596a2 100644 GIT binary patch delta 3529 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 D^R(rH literal 4086 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_twitter.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_twitter.png index ecbd473bb958643509f4fa1714d2bf5acf2edce6..c5526450f4e8ef7289ac71dc8f1a4f018f35cd36 100644 GIT binary patch delta 2652 zcmV-i3ZwO|8{`y_BYz4jNkl3&273NnfwNY!iV3k730BO=-mVtyQN=oG- zQztJ#&N=s<2Q2`!_?^Ne(Jbr0eC3?&{EYGXZAW_!GE(E9o%DwaWT?FgVdEkZlWs(GS z-WmVl4t^&*mVY@w0CRVraR`ik<^U4GV-nDF(Q)u^>$HQ94*|-{2{ZxH+I8YSD^<{Q zNreUq<^d9*ty|84@0Apzl7Kho?zg!-wXVX-I-I<67kf`#gQMUqUdt_$z_r(@1Rmc< zfH+4I1X=EWOUrF#7jBWjm`8#DPBdff3HNqS6{hDdL4U@SDVVarfwFU#;i3#21JFUR(0tvnV?CEw&VupksUo{H)ba z@M>ZL@_*KC#E9h{XH*g}c+nwu-ar3^!S-R$pZ>`M@bkE*5Zk#23W^U)75saDEnXKY z2t)vFz_N0D{$(wWUAm19o!aB6*dJp;b{6XD>yVx`9`iq5hxb+L9p2lyx zcfsQPdC;{*0m148!3aT#OMXQ&3 zBer9ENhCk@*RG*s*LbWbDD^L#qJn_Fvx;4-H*CZ&Ev?a^YbW#@G!T|f&!Sb^mS~eW z5WndD3W|=M#!*2)<;5HLqQ509zO9I-x(i?Fv_Cl+t zTYsTfQX*{0@1WiDzd_c_S^iLn3Iayvmm(u42W@)2fw=Z<5GN)a_uO-6*Kai1Cud!-6US?ABV+nZSO$EE zwuyt#vR!M${W=a$|1JrMZx6t{EvJ1^L4N>i{Ph2vlri4=5B>40-UATx%rodPWP$|W zGwF4dS6`Op@WRzQSYLFJB@VAHF2khBlTcf}R1(1N`Qt{R_eYy>{M@%Fzi<=fqI&Rt zKBwJVV-Wj%XIKWNW7^_8%-DERhCx&i@WIDrI8*l>QZoK7RO(%)9X z&`XdZ#MPpBrZ1WnLaCe>m$I_W_5Z!S^b(2T?vG`UiI1R8Y@ZdD0IFqd>Nt5H0n^w$ zGVB-~(L{jkhQomklLJbhm0)P*Qn_B>eN+-nb&I^lmRYoIhplRY_r)zI zZ{7h0BY|s!$)ktdw?iaxD(gERiG~PC(4ckP1~N^8E{Pz6d$oigUZ;5ru+T+s6O#|TMRC01^Rp^$tvEQd^x1SUqGhyn) z7|K*xO^rAK#;&fCK7YL}LCZ|9JV;WQu~om{qV9DW0duT5Ir6%Z&|qycHQpLg0>qDJ z8~b!LdX&ZlCtq?1BXI3SLycxNuxpTdycv9`1ZnIN2b2FpSAYO#7oPT+y~K%AmIOfp zj8Zp|;4^}jEpKIw0Z&ummD+4=aV39ue0rf<10hI;fL^4S!GEZCeLj^JBnXZTlmtVC zfO<~I7pD}M&5!mxk9j{q>2M-3!CK^o+V75Q6J^z?O=#QZtK zxI7M(`PEo=)PKAiG+tBXF*SZ%A!FQOlrkr;N}>c$m1q)-M1@Lxs@dUt81Dp|ga8v6 z4iT$9PMW|8j;bIbIxH9pjOWIbK26rclKuk|5N0RHe+uK$0<<9K~dnNygHd{%(m;8V%i6)6R{gMkl~)O2~%Bp9Ww zl|vO6Ng^?g`5i@oBN7BOD)E`i8a=1uUdMSL0;mcjNoJ*lFgnH(DFWP$AwYwo3d|v5 zu73tRW_+6$PFniGlBRLZ5veMm7dK%EAkZ+i+;8-9plq*0s1Yj2Zc+k55u*|zqR3oL zz77aOKulxvKm^SGP*#HBLPPD+e9cINTQ7r|{31?(`eCbKVdQIUX?y_ls&Z$2=)1x1 z(HdGjbw5V}b z!+2Z78X^z+?*vJJQUv&FaQAxg<{x#JafbQg|w@vE?l}}sB?W?K`KXC z05;mTi$sjd7;OT!iu?7WMZ&0>fM^EiG~vb+ud@R_x2NFin#2Ev2@qJ8w1u|Kvco$G zI6~s1A(}(V^?z&vduTJ!wuyjl7mt;*^p!d-LSmyJo->I=nXfAXNWE)mw?U~1&}vsWgdXU0+U+NZ3j$U#tla1=&7Jm`N!2b-IM}dNKuFbY#9J zGJHhVqaGpKXGsd`Y)nhGb~H3^a9dl;^hxnhXw)gL7Jn-|ZBA%R;rw0LcQ#x2@?rM} z!x{xvKKE%}{wJv}9DWxz>lGM{VaQu^FT4r^ z{(GSnuYZ(W#HCgjtNYbpC@Qc|h>0{{#pE!|&lrcua1E5SmeMa;YKx9s*!Gu)W^Eml z1Tgs33z7~RFMVi!Z|^RACwnele&t0tooz@<&%~SWl%l@bi4$imc(uw3l|CdO0X~lh z1(_zyN*@PnQw<)ue+`;1HDdbAY}|d{MjW*!?tj?+U{=AH%L8VlMDC7vmYY92av1OL zeGglHw*@&lOQ2S3@nxG5r%#EVbtK~_pL@#!$my$ z@CLl~W)aSuEWtEwP5uLaswf;&QGleM|AX1%a<@JI3~rvcKsdI)xEM!Dk3nUKfFU^x zt$%hav}z@iqeI~9=;(7U6Op6SsDx`MtEm!J%ViY)VH@86-~djXEJvrK4f-jI;jBM}2@_Mm${Jx%q08&*zIS}< zS!Ck`KJ%ud(UeK&)fi{=FONRL895P$4u2P8;j*QOii(EE?LuAIJD8jCy-n3X!I;p1 zxJZ3ob!8b;iCJiCwc*o}gK&5iFwD9G+L$yfn>zy&5|jP*N`Ef+{foh6!B2JjuyIuc z)hC6!?z(*`43n~<%UFz4pI4#eqXY0orQ*o(6NIcwc(1Z3mR_+7FzaU z;gTuBJaZRJ#(et$)HgQ3slY z$c}4+PUly5-p|R3LHddGNC{Ig zeL(riIx8#`+kf@zXYu4y3Y@B{z>QO93MYNOE*O+u(9!nKPa06!p?gZ%Rga>0#{q=u^y18;7jaaM-os_}Rn1#KVs$y60J<$u{L>umVN-zvF}u&C;VTZ+d-EwHbm5wYP~TsjDsu{aV z8=z1V1h;3*Xh4}Z%4Bw1>4Q=2u zm(yJ$h6C73{etO;BObHbBV`#P51HVrpUPq<3;|FQiO z91WR=6DqTyNrJs|1fb6(d3OJ+s5~woi{&skKX&(y&wf*g=zk%}zDPK9pa#dvn((%H zQ+I{vX?Kv+TiNUHAm9bTD2IooBP2>?61ThwYzl*x?6&tfA2r+m;jPzUw>yQ9$l;H{ zm!z-jQ6aL{Fab1*My(~5{o?8EAE>3uju*8}?EFfj zh-@{V7cNPk8y%G7qcw z4TxDZza$`d=~@o8Bi=zb^#BGk3T9V$KodCnQ2J(acm>{0m(>q4L}*|0CUkT;#*3Yu zE}wr9!uco@Yt2E?X05Cp;sa2AAkw`J2E1TK#0j zkjvQ|gJoyN-dS>LOahyMJY{vY_@g9CoP7lYVlCm%Y))J}g(O4=Y?1SDknQv1Gy=b< zPaiQ-ZE;=%NqZ6fo>j7aR%2sYGPnrhM_lb^&PysQ0F?+K8}^4nk~mA$d7()lhtuW0 zz<)DYqR$Ar`VT1;ri3WpZEGu38$wJhN!9?btXktrO?u?b=S3&h9SPVcNa-;X*q7&b zeA&R)_AC7nH39kcQ#bB63AU|P&Fu1ctpt3%q{_T_$`=GpVAa=>X}E|KCP8`}#zb*0 z(br%NAyTcP?FaRlU{DB<4G7Yg3ood9W6 zG}(Vg|^YNo-pS^?}>zK*=Asp!yOy%FtM3-F_D`lOqrv;g15B z8yZmZ$}>zjxwX z$-6?MWWhsE*F+|z>0|^6#~Z6kaewT!7la#A7c9rr754;MnpK>)AdprJmVjVA8R9p- zR*H+t>jl1C1joly?2YGBBwz$SLj-M>caP$x-h}j9?)3K$KYAMuOLL&#l;$jBt)ne> zL=vGsR6b7xkc+DyQ%R;EsDF2M7Z_Cc$Pft-!eRf{^6&9`0taa==SD0FoPW6Nl8T*x z@A6gceGRtyx^4i007s=}V$MAqd)s)o7+fE0;95oeTrgw2);in- zY!~f5;9y7Aa<+QF1u&rb>z?XytbEtYIDh<5zxNp;qXpl<<|6Y6Ou}QlCjyFxyD3^H z+P7Km<)y)(Sn6{1J^S%Hf`5bK!5-dD8p0&;p1%wIK?2?zJZp&DeE2(GFWn>a35DWw zg2$w8)Nm4zECRm@yj)7W@u=DVMi1~D-Z&gGcy~wpR}E4TJgdB|q0WDvz?;`R*lh;= z43o?xhTRF~iwy>V={P?J2pPYTEoEkvz!Fh7`Z9*6%{Bi7_a^O$2NXS|->ZF%)+IBG2;}n^5G>ePQk!gnf&5pIQLJG1B z&e{vClyLF&Mg8>xMwNg*2$8DAnXl=0gu9xvG0pD~2 b{wKfydX|}Qs+Z{j00000NkvXXu0mjfjC8Lt diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_upcoming.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_upcoming.png index 2f3750572ff7a90049bd60e594bff71c8f02b8c4..7efe52d8f75518b1f96b497ce53d3144d4e63449 100644 GIT binary patch literal 3605 zcmV+w4(joVP)}B3Z7VI^+oWyUW_y`)?t3qJEro%i1;$>u-%a0o|Norx zpL1`b@cDf3OKyrD0(uDOA)tqV9s+s@=pmqo0EI%)tuJ0=onldP$~2{tOI325MWNu# zbe!hn<-InZ=d+&Puw%u~^?pC@?k3==dvDJ`;EPmB&a6@@L5ZD>oRXdQ@;rFGUhrVY z&(_5ob94SL2*{i{!GK`*Ay^9mR&fFF5)m{55AXF8AoiRkn|4{RdIA>AxVca%ftP`n z$l#P5yN+ibzy}Br2+4VA^KR=^OTgT_)6y``_hGyvU=;(5A;1VA*T-|fal-p~oDWX~ z5D~l>3Zf@0dw-AhswCj`)yplm^5X2;Q%5X9ymKIpL;!)8-cQqEULX=cqeKFJ9{$?a ze-vHu1Z?~G69W!u*i*4j!*`gyUJoSd8X$M`7I3>fB6zYqb_EC0>uEYZpFn^_geZvA zvsP`}xA=-A070jTpr>ND(6Q#GCT7K6y(6J_xCTbVIH36CkMQQ&E$zT_3PpebDGX!; zFi-HZ&$+K}FPMHs5U_REZZqCA4ev`8fg4&|TEOLUL3nsLM3^EVwoe@BLqi!TowEc& z+?DL!&Dq=Gha)E$a3X=;Exp|*PEH~r;0cU`{5NtBq+b>S-u@uhfRiOLo2tMOCvT?7 zHQH^U(P$wuDiZp}$3tXfqyPvXS6+Sw9F7(^apD9_xKwW^>e0NJ>P*9MCRJ*OMjj4BtUI$Up(GZWIb-}^t(h5+bJKK?v zG6YWG^>|G~gAKwB;V?dlhae@-p11IKOPKHpvEQBmkb8g>3IP&wH|+epdqZI9>NV!j zu(09~oem5JBP1jzGq7r`|fHx(lZ=0QV-KqOk-TbKvjiBG{7 zF#tTHXTx!Hap`Vy`4|j`jhKg22K4p7-b?bbp1+{T1RI#4G?lSs3{I z9)KP(ebeqkx!q2{%Jpw%8zLeyV*13wfI)+S|6&t_ps2MrI6!sBb8z(dafn6GvC&Zw zs@F3(PV|7%(`9g`sv0V)svteZ1V&98d)}jS{t^H$63{LU$_b#6X#qQ7eEXBbwr(fj z)ivufjHZZejYb3GQc>{Fd=FgNVNgd8f$(d_vG^xImDL0mdu25vz}zuKH?;ko>Te zd(+`3Y!^)cdO>zXgeijxoUu3F=qJE#vqRqQzr)0|@l5$m5k?472Qk3M(DKyC`gFXh zxfxn*r7$?!d)D*4tqyQjm4UOS9K6joOc)+ zAwE#pzGZ3AZLf#M?+%08R)b4oB`8=IQluaUNC1|>JQ3g#JI|*l%zHP#TM2k+`AUN} zB)CC`;6{%gg}FV>Pr!R0Y-ivp3_d^nZ>XzpU^>v$>|n%@M+_M>fQcEYxyS26p)3dZ z;UiEU|0i%N1g%nuqmmJUm69+~FVC#cqel>UakL=`{S)WEZ|gP!L@y}BdccyHlnf&* z7J&fl|M~7a`2Of|mj5XXs1O=C>{{mbz=|61S9TuW$JY8SCl5i(p4FgGBVYzzC4g6p zBmPbI^33WUHu7$d|1*wRi3_$Y?p6Y_UV3?Xgvqo(rw@hO@4S-{K)vEj1$0_P;Sm9@)@HD8&cuVx0C+zUf;UhYF#TiJmq-x7yV+>NbLq*CZ!79H z0-j#-!Zh@N9c1B2lP5zst@Ln}7L4_QN+Im(C^|eVP}d6^o@D*jBL&b_uz`W~TV5JP z9RP8M2&e)i(4q$Lo_672!!dKbW$h3m&$&uV_oU)y(F*Z`E4mSWsjV%|RugR1sJ3|}h(7vrCRt2jXf#U-+!fxOT= z2%%gPxX{Q)b$m)o7$ueYh_k_bTHw7#EDyEz?itZQlpZHNwHQ3QvkSm3jQ z0vJ4WC?sI2^Amvo9FArvL$FmMXnDJ%ZdX9`luDW9IR+m51c)UuOM`+KN}ZR8@CxA( z{lE&WOcUsOo2{|Q+BdRSZkGr^@Yy(Ih}V&8ZJIM@PQK(3mGuqF^8EP#STf@dy$eo0>8Q@6|u3Q{fgTVE-2d)8$Hhzld| z`PzkoKQ&4rArjH7Hh?kX6-dHL)s25U-5#q^AChxH1gu}bp4@$hcs{)|@J!yZJ@?8o zo^(LoBr7|y+psitG&eEtAQIRv&^w#8^xnch+`Lq+R?Z{Pa<^xyFS&k)9U_SUzb6o2 zaTEv$2xVE+NC>&<*D(HuF>E)froo;Q*V|;h5CRHCv0H)QGtU8@^=F=caW7VdsRWK$ z+U5ENKku)qs2Gp2Kddvxk(kkW+qQmP^mytXrBXRQV0|J$aD5j45&;Tm?*Z~q@CL%6 zojBs?1{HiW+zPYqnFT7WDJbn?IOL!vSpOFR($~cOk${qOfv5iDGmB?)9GB|#c=nf; zluVzfW?@#u}G8cY{&r6|c zE3hz9qR^=@-O&O{qt!sXy9S0=9B3E!egYKdBETnY6^mXVt`22hpuXinaPEG?e~- zuF{()Eu8?`E)FE6Hr?XZCL zWmy5p6FU6Jqk7OpINfU~60e5D{{0~-DGAgXEjTG%l1{i>ImywztsM!lh*>}aNkmXF zQ7~at^i*8*WJH7oAsDXH_$aHWF(r+6VJJ?>I`0WohmDdHMkG#i5e@sch znfpru2%YW{`#nGm1?CBz{JO=Z4TIC;9)UjQL>PwgPnm`^pwZC+4VaSa>uSN-)@Ge_ z)7YGI6+#)}*IQ>*fdqc?O>v+PR&~a~bZy+NxxyaeT6#vd{N%tN3z+pgR~J$8&hh|h ze^3?zfsMnnH&s*`hkzfoVzDYEVuQtbn>N4+5tRhOj}`WSPd!Ssd6%Wh5-ux|$4$*v#x(VuSb~sa22`vuCTON-mcg7u)a{V!Nv8zEOVmW=EL7$^d!EoD{76TUeOo%$hc(P;Tv? zY(GG@JWj(<$k*p@Hndih&+h^N$}pMNt^ca%@WUf7>34eW5W}-&6Q3yR=;Gb_`v3%C z5iL3GT&YrYbprkx0?!xkBSIT4(Tj!?kyp$wL_|;>0~eBvSg%R~L{BNc#0+3X0&=ca0;pzKbP10rLhxPwvh=bNaEaC@F{S*w0tv8P ztpv#T9WN9#WsbbAy8M+!z+y3Nn$H2YNL-A4O~AX7<}R9mUux?ipof4S0(uDOA)tqV bUvT>`Y~jiIqx_{+00000NkvXXu0mjfGabKa literal 3638 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 IM6N<$g5cEJEdT%j diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/logo.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/logo.png index 8da5468f20f3ce067f8df8f0020dd33d2a9dcb63..5f8f7a46dd4be56f310e8891575c6b5686b6572f 100644 GIT binary patch delta 2047 zcmVbl>%i-&YV;QYe6HfDO=XZ*TuS{_Y;Kxw-l8?d|PP4#Z{!VI^fk_z9q0TwL4; zd=Pv-Iy(B(L3lWp_E`E(_QukvAgrV)2zLP5>FMe30Po=7;4cV1@9yq?Q$Au=l+Rc} zSP4T|7hh1eVtxgWJ)rFZpWCi&(J2e5h%fY;4oybg`96G)-a7yhV;)&sF07$IhtCl4 z8DSCK@AfJPX}>21eFhllPnzFYiGKpe1-$3k0e+vNgv(0?+R7C?qjry@df=~5emSoS=UGVxuv zW*w&IlnzI=*k-707=aKFWpK!25aJv)VN4_E^v@^4&~l5j64`5aR`quy!;KIih33uxuEmSU(yX!ZxQVKs`Dk6Nt~4Ac$nmd4jnepUK*s35ZTX zPk*k#AmzSnrQGiqwCPU~z7L)yQ7)ij7y}TRKBY=QJX%nqH+c>$Fg}w75%}j*qIxB3 zso8*>i*#NmsB;K@zd{8JOCAdu>%#+t=Jo?+Y6q0JHE~+4>^ZHTE(2%<3w)ixPav8{ zNFkwTXQ8$kA$)+Gw|q+wMH2jHs|Osw5KFtng75Z$%sd7^q6 z$5hz>UtcJgXlo;cb~&>1-Gg?-u5-;Miy+2@F%Rnk;b9&~QiWHPWYx7YFizQY9&UlJ2- zR$`bZTwja-OGm7Gv*qyl6qY-n%Z*BeaHOpV`?V2$tQQGks!xc%F^JUm1VYvm3;#w1 zdzo(zQhh>G>0CKsh3>lDr*vyV$baAI`+%ZBwpDaxntoNuQ8t8a@d>paeD^OI!kXI} zzk1RqPe2PnsM`9L3!*{wq`E9Q)~6~d5GFzDn{dm*DxV-&zlI#5Q*^^BJvX-S?lVA$ zXt2lQ$Gy>K$rNboUKWIiQpN9<1!`pqriAd3u*>FykPid`GT=1Li$F+@v40mr_53B- z0(JgQ9iETeZ_9>om>M0I2_Yxut>ZN5w=C#X2(8o?_e=SNPjro)&6h$`CoGK@gOILE zg|O=j2H3xZnRk;@dSH?#8Y~gQR%(!>`x=5UN02p?e5qv|d_t8ulQhrwUjxXek&EgYE5N5bQ$KtQYgckzqyL1R6K}W|I9kcIXfs9G_L~-r1 zA&gk=*t1K6P`M0T)X^^@FyVBYQAu5QXXM_r8td&@A++*@+N$Vw27k;-@mP5(OY%U- z6~xm($WftXDS6@Lg)kCjS`Zd)2_p~&;d>Pj`c6!IK12v$ak70Gc`mXD-B;&5<>ejO zQZ4I?cCpYthub$MwOuz0f-p->H0wZW8VGsEvd+?D!ee`G2y>ksYl}3?abXBTUjc^r zqP~bz675#qU5<0{!+%-NfLT|0t|=lg(ev*+QA?bs`g1_|P(>39^b?9sLJ3%$~-MV|0{yz3R d-#pE9{eOT!aZ;JZlq>)M002ovPDHLkV1mon+P44z literal 3600 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 WEeQ?yJb8=&0000mdKI;Vst08nTR AkN^Mx delta 84 zcmdnWewTfMijriBYeY$Kep*R+Vo@rCV@iHfs)A>3VtQ&&YGO)d;mK5+jf&EY9O<4e njv*DdYBswt#xO~&XJBUFEvV64ynmww0}yz+`njxgN@xNA&|@3g diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-completed.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-completed.png index 0f239c0f62ef9b0b5006477bcae3692a0bd79644..98c421a8bb8e69bcf832e55905e48764af8476cb 100644 GIT binary patch delta 2667 zcmYjSi9ZvJ9~R45WMNZtlZ42T=6uT)I_8LyD`RGkP*SdGa)dN<pzKN-gc_@s@yDvhwS1WzA%5=$+tXKYf+c3sUrPYI_+V)Qsf5Q2SFxy9Ze3N%UZ^UV(Xrb-gB0UMb zPX^A57=e5L8RkVI)k1~>!%C5<~Sk|pd&NBs)L2S z-kniqFL=UkomSdWnh|eB8s6UNeF0$vm!MDm0SvE{T$(fU+6WVzeohPZ(4Ycd?;FGy zh(C`n`NBFa5{yTD0v~K}4#37|!}Sm8%r>&(X*j&ayEHghSt;AZbZqc`?oC)w}ib1tGPurxEB}mcHZdgwWFVK zF_hTBMLIBKnaGV-r%J6lk?LPqcp!i{8dp^pKn$LHn%la0-`fz)4i@6 z+D*w7d1CFm#Uc`^-Zp4q&RpZ1r^~+anC?W*6c2Pv)u~O%kiVod5Jy4+UU8xb2goKU zu>dY^GT$s$Z4Y;QtdkYZfhmvxh@MjJEwr2b>}YpiqxH4yAP?SFtj>Ob^RtzNOcC1h ziDug`v{En6PgT#R+)`eN{+I)JOBuXwx{gXY zrEI#_Rf?TkiJ1DqhwObR+^_(_kbIZMPHPV8QoUw!DdAEJ}fPEtn4?_&g zz^pm%=N0o&y)^~cgXjXao0ElI&38LFPN=OqNh|Y7XN>A& z3a3*{$IT~__TKQ8@BAk;K~LZU74soU;MoL{16qy~er^^SkY|EgTH)+UdV^%Qp|SY1Vt5l zu0zxPQbL}*Wmc_lCS$24V79c)A`KY!#>(};QnW1jN=unatMY!1VV)VX1ryllWPSUC zaNdGu6Lr9kdo!KC2+A*dXM=GUoVf{d1OAd>^|Pbj#3+NLZX5Obj{>&%Sb&d)6Qp54 z+>+qhNGwSw%b4Y$dwGPX1AhHcVv+Z`sg(3zvt~YDFBg9uV}gI^aU>LixNW}YkK*oO zr;D`S<^h^-$c_NZa*MB}FvpZuVHh=bH)J_fU;YrPYDRV~eh7F2xh? zy^{yzsaI?EDrj}H`o=Gd-X`tzK#4;Tv}wq6fxgC|ShDgDOiVRH63!!>1Y&#~Wy{h} z?{1>qzgo98lvXLDao6*>mOpFBkDXRS`}!~hf5`6 z(`U-LzJeglO$nPwFL0aTKYOgMb!OCBb5z2Tua3sc1dCO@RUvk>1n0G=GFLSPjDya{ z5hFQPVGSZXn5F=BJ|N+x74NdD?pR`Gf;+n@G}qhM`l#LQ2NAkFov9$p^eLs*n?r|m z58m2A216FVm8=~4^%EThZpqCkl_3)taNg@7#q~w2Yo2(sFeP=jWVh}{w3%j^l<$?ob z(;5v{{nn@3+bNW0=k3edn-FBV?Wc95PT|H7LGtsh;7?AU#hV?R*i_8)7f2i=*G8jj zEURWtba&*+8gnL(d}wgNF|K%aSp-S4Re&(0VnBLNW7_`X+8bM%6M z{|kMS)gWuX-F5kO-1=jZtNrHC>EvR^mmhA3M=DMPx}1>?MabwN&Q**Q5md(64BJg? zX0)ZW?kM;II-{7Ao|m^K`JjA1j3>oRqaq#9e-}cUUZB3naaE#0$|;pd3Oti^Y4W9n z(qj`!8wtv`xZbN5!aJCdJYXbAqo>}_69lpk*rFqN00Y=q^7 z*Wzj|b6%+;1kGolj^QEDGb<&wpS;GW!9GZff2&ywK79Lo&(HFj(R}*e®%5GN$- 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-ejected.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-ejected.png index 04701182cc62ac1fda4936fbd680ce4957bc6462..9b9c0331babf67b88b19141938f4559eb9e5f079 100644 GIT binary patch delta 1278 zcmVWLmYTiTeVjf^_8Wg+`K@>zm zL~+9v+np8#S47-b+_l~Jz1MGR7mhRE9e*S^2swD2oSc-^)m7Qw z-G%C3{lMA)ID`MetPTzi%KG}cTwPsBBodL+(^EM)Ig#Vz zV>voHG9_*3@R38Fe(tLH5pw}>2L01Pcx`P>E-o&NgWKENvazutJ3BkZ+57u@d3}8~ zC2i>Nkwcz-^nXVW`eG&k&R~BzxH>#MG>)m`)Iaker-7i39`vEt`T)2yf5ox};c!@X zcX!`7$gID-ycj$`KS%4(y$}89ue#hI{IoUz?w$`bW9;pPg#}aUgOg8BPyYzy^zrI( zS3=mI6RZe;o9%<_S@ti3CbR2xIp9b>!pa*^Eb$@e0(0e-A5`YA<_WAj_InI`s zm!l3mJUsk0^g$nb(eIuP3AP0w#L>lUO-xM4-QArTI2oS$--7-;)X|H6>|oED0Jt~2 zS1vV6OG^g2>~C&vz6{+CsvA4*Whvg400f4IhfR_i85uD*J031H{}p)9i+=2254*Mm zAjlqNX@B|hVOwgljX zdpyU}?CflGuvuAIQe0dtO-)VG(a~W*9a?sFwgiJgBgeijDJe1XRaI5e+S)2zU0tT6 zosp3dl|w&vu!mi1Rsa{3v9U49&(D|SH#ZHaPfblpdwaX&Zlcs4z>ty}i=b)+Y7!_0eR; zj45yq8Z!(ZIcApR=9SZA$f_Y=eSLj0Iy!3XV9%x;5Nv2@FoV{a=DhCd=@IBSL0fqW z`0yzGJmlfgAHKIgcA(pI3Iyuv>db&ywtot;b!TU%)YjG-9&Ill9&}27Kj`#>=kEs( zdp6wz%uSFz%(CSbRxvX(W0tSOHv^}wJm}CUz4FkIhwhaJ=*Ny#H$i|vR_{jPDq7R-XKkL2+c)tiW zHa12bVc=eHlre)vMMcJG9!lEK^{Deg1BYH~zXeXm@-0$kWf~{I8h%FQ7R=(bCdlGJIKCnFIm>NlZ*M zC2i>Nz4Gp!kFEa#{y#(PZGRv*=L-u9B{w(Ml(hYhyV*WA{|vqNZ%~7w+e1OyR literal 2363 zcmaJ@c~BE+9&KO{hsfco1aKJJcz_CY67Cklk$?sa1K}J7n2AXXDak<&lYkD04v4yl zlm{pR2JoomRM0^pVY!w|m}N&{L{KEcN|0Me6k$6m?*5_MRo&lF@4esgy|1d%1N?Ux z=$q>U0AN7fO`)Ts2l|35SD;_{rJs+YgPF)XTolCHCz3D(2;j-$?L|N;hY^F&5e7>Z z--fsXfDVow94-o{?SYv*jx9rjv6XW8C>sD=-KBg6b3Y;i_aZTDt{ZN$R*3`IEH_-3 z0}W5(laW~V?gRl6l;9uCOxVwKVd30KpsN%{1vrR^0ZKV>Tp=uV!@bvq(Yaq!MNW_OBNFtHgO6+WTf*6S4;^LywAQEj*gpE+f6)~hXT;Zl= z1qvc$3fO!Ro5uw;ij2KHvB(XFX8NH74*!!ZSNO3_Xu}{WgAWmG@tTsBfi&9xhjKWd z&_WR%`ES0T6&41|_y|Ntggmi;i5}d(O`1@Am@GgTBAy_a$BSF;Vn8fU#1qEy_#ipR z0i-dQY_4Yg9#5meRIX6O;4%>^#SMol*s|Fy*wMku!Q0u1=s>h{CJ-n-E<{fkik**_ zqdnP^Kqga`xfC8#%t5%KWiAWlQoIOGK90_KlP5&wwUb__0~nZssVK)%v_2xqaXF&&o9^Lf$@>D&0NxWapY+l}e{>wws&V zcgVZ8p7nm>bK9-gh3iMaRaJ59mY!?HJPwDW%t^Ygn}_kBQrildQBhITuco_te(d3HkVc^#t*NOYoIihlWPH5y8w-n^ni`krYu)tnV#vb6;%ZX$VnaiU zyH-W}Qrw0O`WVdPn3$N0b917=R}&Lm6<6q>v9zF~0X8avWR^f%ZrV*qWSsR_4f9TEG#^FG&p!XrLwYeD~S}X0Mz+;dF{@?FKKBVQWmS| zkAVS)#>U3k(9qkuL%m^I$su(EB$A_p!xp)9*C#LFP2@No1U%&Jg))m_4W0Y z7GqVQNNEo+pqzl*XYP(pPL@x*5)%`jK4@-kHfq;#W&m0z7emSQdP)gtCR43a#$pmZ zpQXipQJU%VaWQMpn38FdH(xuqn2aNfJCE+s+KYi`wkqEiEs0 zcJ46?YrfkyNuYzVox2QYGEW?Q5U-wZ*BLzhA}A>6-q6rxMV!BX#;Qz}A~Z?=@7=EE zg3Po?)y<6lh(o&dKiqJ{30C)+D#kv9@?H<{KZ zj?w9Kmh$Qov`4ZEg^7!K_gYc}AX6Ld{l;&Tj_y#JUPllHG^%M$OM7X8#i|#yJ-ofV zQe2#!jlJb^c@tf&RwwOMJ=;j*PW4zF?3=x7U<|6VFjIg^$9U(FodNUO$=b))MX&nX zy{<0b(UFm@)r#?9v@Jb)0l8ntZ9Tul5{Kj-=W;mr#wRB+yTiZrLqWdPUvHTCK20`Z z{~QCP{5pC6{@F9N4_jJViZX|u#V48%CF6#Nhqd>Fgq)qD`THL)zIo%up1{C=HkutK zCBkdXrnK}2pN<_Z_z`ffg3dM!A$ezaIzy$yn}P+&ooXvN9-dD&?{ug$CfV$$E=-x4K%t(|PPzSd!We*S4g z1hBeWrE}(A)@26XfxBmuQweUeoM$hzbVufBb)SOa2GsLZ$E|w>i)piT1HnV%%*tYadwvxA~ zZW^j<#D|2YW6f|!M@JU0L5IO&QL@1H1ak7yoZVvfOsjX@Y;Lsj(h6m`H)+Sp3rz~J z!b1LNYN}#<)QYC9YD;=!mIejORM%%p8WZZY>!YHQZoFM^B`EIPc@t&q+*YY;Xkd_Y z;ez?LG}%o4&n?QUu9~|b@jEzHyMrJR4{&xgFr&|fH{V(7?iuAr~Q zgq#u9&&^c@i#99T+kMjy-F1E$dZg>ckKK~Pzd_^&rH>1%uqzGW-)9phEa#`^wog!{ z<&zx&t(@t`+nyL70GtcTcD%d#fbWufodHQ8d0kZA=uTHHi~@>RuWA7uz%M@HmItU< Q()`X)z5OX=UO%P$2dO#lssI20 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-missed.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-missed.png index 242ea4d91f8011ef946f2bdcd12c07bbf1d81493..dc2e3c87c6806e819cf7519643ab6b8be1d6a3d3 100644 GIT binary patch delta 1663 zcmV-_27vjn7LN^(BYy@~NklA#v*h87b>X8qU=#H1Q5a^ksXN$2^f+<*aIPfKoTG!xZ0^d@O_Wz z@37_HNWjURA8^?J07eL-gt4NP7s8-xg>AAQOLj9>-r9Ldb!w-68s{2Mb^&fJ!mkp* z%3U<8-E|o-4#$W2Og| zfjU_93V41ij=ymI%>9j+9d5#krH9*-4(XAuKO}%=-a0?Yf}UkFKCR>|O}v7sM+(RC z0*iGSjwPFuMXM8cox!q!nc;Sr)#Y$Zb#fijb0y)Zkbe_E*Syv`rew3H#fnMJS=2XT zs38SbeJ33DDLCd$u$$B{c8SsdJdR`PF2>??Kkp-0rZCu)hPgqRQ-|r1uIqBxCBy{K zYA+hs)UsLY#@n$5&a%E9hW32)Rmbu`XZmX~sVeaVzM9FNXLxHH_Au33i;uzFsTJ3=npem1pRa@rKBYpBi{)Co*5`$KT1$_exTIykbpaMpP2yKtAp)mFk z)a8+Ae|#Rfm%l-6`dKK;;-M({9pyu(vt40iCXD)P2+58T$I3ed)b(Q=~ zY0JOB$F0^QKd#MEpimMJrl`4#d3`f%+NbMcQ5RxNo(YvS5xvioU}8a2o1r}uMs>9_ z_|sUR79aM>l`LJvc0s(5BVd%LP>yVu%iSvofH<8XhWvE^I=xz@{$;B zzKiLhHg;QF!U9|52(zltvras_8486Y22+Y0FCFqj{>ZOT5}+{Y8ZgsS2y<7CPbL*v z7;N|h(}OH{wg*@keZetyWbwe&<<}sIKYxtpsV6Yhn1U%~wwFEmA%EmoCRB`o);?Sb`;e4lROIvEGtYksS?bTwRK{YgHF-k~@<)CJa|XPr&40xB z%VdnTB>6-xI>!?*RDXl519vb~cZ1g}Zihnl;28R9;@DYoYeReTL;i$x1}K_EG-D|k zX-M!%OZG48?gZ=7jzap!m- z9$x&4mnkO$QIYfmRArZZ+LIshCx4{7Kv;3&_iR^}fWE3Ln^SoOilSf9Tc3f|KUGZ% zLfQxLouDk^0yO2D(=GW8oki!_WA8lXdPN?nt`s}_eQA&%q3s0u zv%r&>!)VPri#L)p8`6@+Zg0|PwxcV;oF>nMt+$A^N1^4>kxlJMpZo~zS$}{(4VK&r zLC(c*@iO}-D2tA-%kz{&=#pH5;Z-^;-I*+Swz28TVD&V%^^HVL@}UiFNtg7=j}V^* z1Kf)t9c1&v5WLF!aShVj`%w}fyzX5<5`PfVTl?3wBR$e}FMdLNAK-Z-RB$zzAFM2e zB_iFM>ZBk%IJF&*F7CtY!hZ`q@Rpp@yq?P1t8NFau^}DOBV8fC5%PK`n1AIUKj`v= zAjp#Ud8kR+3vu)pxO;pnA`g6o@UK5abnwTxcVa7Vr~cLB$)0pb&mZ0idc75Pp9zK6 zf>08>8$`w^O>pz% z>K;6e+zCnaXDE%?jg%w-^CyUHqOwS_LiPXv002ov JPDHLkV1m?pIZ*%r literal 2864 zcmaJ@dpwkB8-7VO>#R_$7~>PQn1h%#QiCx=OimLjhG}Lrv&_NFU?Nh}YMluw@yV$^ zvE;ClLr#^X%^^f=n$01yIgG$>jyx$owx!bO_F){Vd)QF|IV^x|L4h1T1U|?|Ndy>F9tq561TwiOz7^!7E=scBJcdHR zA0fN|E6CqPdEs5awk!@6Y-R#8Cd15OU~{+$-1Gnvi8KNuU~m`|hJeBm#&9Ic6oG=l zz@Hw7BpZj~k8;CcKjo4{RuCGG$3{V+p`oEBp{6D*&S@yz!op%x1A#D>AdI2S zn90@qtbn0%$s9VHM`tm?n~J1UtYDrML{jPBS75Ne$TGQ~+9YWhluu$q;U=)nD}4sy z@&6ynV0=Myd2ZBy>iwU>T*4VP73xOivVu8e$>99;HdC=twj3&n$KnuJtiaD*bfK|$ zEG~`32HU!uf$=0Vow*r*#N+WO2PT(CVv?y27%PZG!GunypfEOYB-Z?UQ!|9AH5`t? zS|F?~Fs4`=^8>cla9dluzsbaSx(HR#Vr68CIwzt~n1d@(+iDRDbTV(atRB^?0R zUf_VSCh%VlANQf1oY06&EXe1UZE7uOL>&EATg5|U!H(w zg~34$iqs6fJHG|Wv>4`lDj3(zKKAM_Pb_Uel2NikR~>ZFvaB|0U$w&FCOluR?df<@ z=F>WwH{A-wZA89W4$l#7M2rYu{$ak65%PYSH61c#)J@8x=LshUZH^bVfo*Me!LB2o+_RiD$iac#{g@=6nHw3)LB38j~=zCqKg=gmvZ}4IwECtud+*WfA%Exdbrlkc@D3L?<%HO z>h!pzJTh~uz1a0wb*j*e#(5}g6~phxA3o16(2?@%-WbT;Ma)T)*>xh}U;|#Ex*J1{ z*m~2W6+7OU%+1NK;A!9MICs&lrrXD8!Zi6sSv~`~r=m|wdaZ|I=6HwKCu|cVHk=gC zgBN81=Ol6c5@GcuSJ1t?p?#5QAPUC^oPyU~DCbF!4dtLd#Fo7FAKxpxQjw|kcZ&Wy3W(*qm~E86{1FURWlGDq?`1K?9OAch_l3qiEE?t(lafgk+4fKGaDsC z5g%Gda?4Zn!G>2eEk(%=itV-&t0fW#%*e(4;g`esPkLWn>I)Lb@1DJ%S3Ex4vWV!}epdBYpQIoug}aq;JkaGEhteJ( zJR5I_{ap0_bEx7O>_koaF8BT~?jFcnJZV)@`hf zJyIBLJHl5K+JjcSwFjY0w5Q|lZ6IR#T{lNfUYM1Xy>Ka@mA+F?5bf4l(9>4ZRaO;-j8ta%KNoPWtr~J4 zU?i^FIXflV&YTgrU%T0yYb1=0KN5%FssK) z*-JL?Lb_C~Zt`GU7q9z_sGH#T&O=N{S{@ReB=ryUEuO5J+&W(A6jQ5(EmysD%bwMz zu&b?khO4W^v({w0PQA&BG{0{Foh~82)6jXu+W2kR`F%5SdAjv+zwsO#Z$7>d{SCH` zVbo(fC3A3GG;s&_tDqqu@Oe)1j?*z&EWr=Q-Z*6|TB!ChitWtKT~|bE?TPhfyZ8I4 z5pO-7jQn<*+O&3CYT$|D43BmtZSBuB)Ox|)KV@M+)^+^`M-`R#oByy5c1JO#HhvfW E0`*<_fkVYEqv-R|BJ-t?U*LLqc%kPhKtGat;B(MQl zissC#UR~YOUCXWWInVZaK96wDad}-{m)E72ynG2RugmK)KrXM#>oPztugmK)KrXM# z>segE)BT4K;_}tGyuPJt^?f%T*!X?zNAEwF|GWj~2-+Z|Cqxau^86Yp<_3H6j9k^8prUXn2 z!NE_QU7dcOfa2$JU`%}*o?;q!Cvd3tqxVl=2FMF!eb0DpDvsk@<6`m6K^R<>X4!b! zP3U$zIO~W5MX4B1ODPWpLGTdH9uiVE-v8L?gU=CQ+y=aDqcyPF;f%vtht+8CvJtm|;1_BjH{uh0rLN#r4UlwUMiR0F*rI(wKhOphyj64 zx^!AinvG>v8Y?7PGdNJC5Elre5Tybf4rfinS$il=l4+eLC$l6u>T%eaC&+AuL+C;z+J$UvksFwthRkCfx`nLl&Da6%V4~V<*e(wSey3#1~cHC1Dk(! z>yNo{S?f^BBSuL20O_H`0nWOER=c-sd*{QA%K&*Et?!>0*dHkMp;DzXRUaN=U}%U+ ztwyn2CW<1IQb;M0Lga^|!&!|^61uH5+AGVfEzWa(eu?V902A9MC=b+$ibaG{SYyc2 zl%&(4(_Evq(qMII37u&V66-9Fq!q6-t~{^9261+ z9u%er3Wqa((+eSbCq2%v;dj=pNbmdJgD=b_m`oA}z9LcIBGxU3@bQZfObaFM5Q1p{ zo!A3Hib<#49XtNj;pxi&c^<6SZLhy0jKVvswHoDGm1?c#!BHs_g&|5wlvGG5kp4qQ z()U3yX-d*=(OFrhxxB#4xdnD?+rr4!trV*Rs3^t(#u#*((rq_sHyW(W&yl6+CSdHj z_Dbf@&2#?TJjOb%f7!K%Zhh+aXun&z>?YaBnGkaZP>Afz0IRO6U!yA)Tw zLVZq#@{POS_1L#1NDnS=-ET86CI;$Csk@>$y0uU&P$LMH7Eqq zfgrac`A(5xfGQUWge6pt0K?4O0`*!E8wDsOkSYM-5P`sj0X7auGuDd(}f=D8z z@L;fRJ)SV@8`BAc!{P#i$w0S2w;iVIT`!Xd_0Ft=VtGl^Xc!h&6V{dmnK4Af2<3>C zq8Ns3nUL(b(%e0EP5mcpKYG9Yc71-AEy%YQ9Q(s4{E$*|Dv*Lg6cR)crE-Z999fItfGUP*z>eQW_7;DNL~Mf_(eH@eV1)ofu6ZB*r*0 zt3fApbr-2S1U4lgBcwt=0rISrL?HX%@Ue*Bm<|z0L?K3)GFwLL9Gy8&XLXq%3{VP$ z3K7;Joo1jG^Ifm_POiLe7p+#CGbc|lP%UB%eE8!>_|k)?IXB-VD3yo?2dGpkluM4s z&m|=7HlqUtUj33uUNl-mEItYXA0q{#A4vBzS3L_N99CGYHt4P+TXR@zvCfb*9a(ep zhJE5_o3A{y%4$>hQ@2XWP2h-?|5^${#zq6CuCa%Jw>%r4<7Er-Encsk7?=`pmlPZ* zgc9o@kb+Q2!axuxNfh||Q5aAxmnjbnP^?sli$#JwlO@(;(e+)JpNP&HOxh)Dt2jBTxSjzb1=a*Pm$~KiZWDbrresj{zk#^tyq@<@~r~L zw%V;>5ZtwO$4-W~j8Q0+kXF-NT43qSNu2Hy2a+(5gh~(wl0XT9KoUrc>o(CgMcNGE z$`BV&VFXI`t+^0AJ0g%lz(BReU~Q0BzxFx?1}j+WH+N%sjWpAgMs_n4TSyw5o=bS@ zlxCn_BTX|3#iDn|q{0br>8x`^Qj%Fml4eMyh@+6viLD$uev*mdE!*MQq;##fk%X=-K>SF9YOT zux{B>-7liTUD_FzmKQ0O3JeU?7^sghvSkaSTShs5?X@BuR<@;#e`UYX_e^bc(6(yOxpc z3s@_$nZg*0Nwf!t0G&FrjwM+#7lGr zhb{x;dA7Fg*m2kPsj0e>lBKzG9C_k#vi2(bu6+?hqhnMnlT?Z!vrirNAn92T<$FxHV}nj}eB)tXFOoJ%n-pcn_lwE@0# zB;$J~#tE|}tj@vFPH{+d*LZL&YcA#bpF5v$<8Y=8im!#G;fxHC3<moe#n)Y3;S={QbMkDbp9qqKc(=HmpOtVZ)C+Kd5(*ZgGlQ@#qPuPoNBE~u~K1Z+fI^|Ih@HD-#*N>FWJM;XpMF!A3R|}u=2n|5Hr6zI^7OQH=)~2Nz#-;xk_BF(oHiuPSEYNNz*P- zs3;d=&dj&Cs+3|gfk_p*tI4{$Ph7lkedVE5zH(^wX){(mx2hC|l*5=%s{Zed?$`vl z%NFE$wk|$Hs`qwo(}y zVR&LYJGK|td+knI%?_jEa+FwVv$m*NTC=bmBIl7 zK6H+2_Kxwy@g`~J zh@uG5Cq}Mk%DSyJ8cRzgX~y{^rK>dpLAg>QDa4rV1x#YGSpnTOSOdC~Ty%|+)bg== z7dbPVTmTNAm>ZEkrk1tY zWd`dtX3s6KxO9r)ku|nXBwVp$gvEB1bLW-_!;n(R#{<28^lwmh4g+XoXtmnJIJOKH zFiR4Xc(S2M(u-WHrR9uId~S0guorhIA*obKkZ5A1sMpI34VGzlHd~;ftrxj#jmrRe zo>&hdm~uWJ4=5I521Z619^cB;t|_KZOtWqGB;z|PtgW?~IeCuB z$!$m#@~N+!=fNWlYPA7MHQ|HTAV3Dbb1;Z}d_b>efgm#$)9IiDObpdn?R2OO4RCI0 zm0e?1gbEoQ9->ky5yk}$A3nnJv9p{wd7fRncQCefjJ;FCG@7uu*xZDCa$34`9v~gx z5=gZa^Ojd#LCLi-i6HA_fIw&Irt7n?l<}dD&y%E)^=A zb%7GB&7WiG)G^AH5^=>?dsSd?L#rUx0De!0?p9Ew zSZ*-nR+$`CoH@J1NB-ig?A}_Z(@A;J{yhv1SNZCfA7*W>h0&HwYvMx8-hER{>=>t9 zDfzCzu!u!?lA$$VyoFd^USam!dFs_V=fC&`ZoRICvleS~->rJ|Xp5Js4PNs^?jt*)U0NqumD>b{+Pw*4^6ZOuTuevpQt@{%{hvRlJ%5_k10SXI$e)3;R4YCMb$q7Nw;W^J2RVIgj%_>2 zNGyd2Od^OyNFWqR(@-3cw3j>m4#W#%58&M|2!I4ayqHZn5&-V}nH}33zkTZT-OqJ^ zM1{hg`>xy1*tTupENctrSv)mOcWntN$ZvVGlYpg_CKGYOsnZLbIz7)-FL^oDYKi7K zjSH(pg%qn(AB^{1D<6A!>)`>?Se$3})CtrE;-c0D=RC#3Sw|)!3L+uRLU8c%n_tV| z_#{CuZMtoo;o%_;KTzkXM;@gycZ%A`BsWh@vAVL#shP8!I(eF<#YW%77E&TrfC>YG zC?tqtR2VZfKE;mh6QpBftW93Q+)Zymf8l);pZEl8P1BD#qDWC42&vUVY6A+VHCf9L zR!2}tM%Hu`YQDj1ZB9R3?cuyw~yW{*A2^6BI2Jsbjzb+oNu+vGSO|JY|qGR^)2Zy;+= zlWB)kMGB<=VKq_($S~}Kqvu`;saRQB;M~k{lv3*o9)JysWY&@vx1mcVhKq&~(IPWY zt`0FevHPO;Ff=;KO|N(bm1>PAAE|KWnp zFbav{h+?V8@aPD`qvPzjVn0C;Vy&ZAty0)?CFd&tn9k}PpMBq_`qlDnlO@`#Ek?J* z2QD#^6ylI{v4f~6oPox?#$+B8mBG!mj~m{5;&0Wvc(z@v{TEu04VL7tpWnIT z=3k#V^_+}J9K6{%OWN&_r3r;nk#cQ-VzEfERHP6GEYBRxv!sHqwv0}U(`u)jI(;4~ z6+5qZ5s!cJ^WJ^Yf*=TyQy=IWl&D<1-%tU~nj;Sb)}Q3ZW$HdP$$L zZ6&gmE=ov@hL!UMy&+3Hv2T-m_0GTT&;41pAk_O6+s)wUt=oQa=g$2S-twz6GtuEUx61;(~) zVdqsh`QNt|V=ODHtF)R;9{u8HnY#AH1cf4}o_dV6^QUN@InE;=_`m3|jFFNP^#S(W z@KVAs@+oW;kY*{Ra*11Bd?O+r;QaYH>h(G@=SxL#gu_vp+(U6}4AWSkSX8VvGPG92 zaYWkDY}r~wSVwm$C03Fwahr6nMt9g=&j-M}Z+P#CgD)_E^y;)?6F9`?lX^e!;J@BE zdB?w>Ir&@)-W$$Zmd>AJWqy`($Dd;7p1thccLhVEqm%~*8JpTi+HBBTX?TY0;v5e> zdK7Ie!U-O^|8r#83#Wxy8$=luMhL43!w?ySI0uWfr&wEQ5ae1=LP+%RB$YU(T&V(J ztik9Ev2>iGYjNbMV@&Se&G?@ER0l`tb`u^u{4fXq@VzXZnW1_11f6r!4Ag6kY#qlf z&Ja3oo!?c=5|_W_V--};Lxm+SyO*nG&I7dJIX*zRrl!}T@+tBS=w(lOqw!0K#g>Zt#=`OL00Ev`f zjm_gbdbIYQYxU0O!iWB>0z~A8Lm1(ZU|i+=fE@>}x(07E%f-V&Rmg=pw?BVsPgmOVi8! zwC=9!O|LN6;8_x6c+LSNlCqH)Q#aNUi(EnQ(pIGR^?-v1|J~%SJAV1(w=P&)ecw&{ z@k-=RH@o7J>)O3BnUVa@rwljkEc4V{m!yXvU#K*kkf1Vdp6xZGVbc?VyM zu%r?R00JV6Yr;{mv3w>JeK8BZ{F!0y>4$PbyZk zjO+i|&-1R|`#qf13{(n4QAoK`rc#bka&y6W_f^+XE`69*rNRehmibp-zMFze=nn4X zOaJW?WUZFBZpKlm25i|H(_C(nC5}eNP!1IJEg{3(OSG1|ed{r?ZYTihpRid^FK`PdYgcE{>zhpGru$k#oM^l?23)9@P(qu zZ2tbLT_Hgja%>^tnyqEdbs`qlnw)s>OKcx4VPuugQU?_TREEc?4h#^TU1rU|$zxA( z_@Qm=ni|Dg%lXqs8Qs1Qq`+y5Ee{b6PB1huNEC*7uZAYeGPKq>lhIl@!P@LGoXI#d zbBdM5GFM!4E$2_2^%p*=mRG8Wztw+QzUtIKYfj z%$zvM)SjIr-7X!GA&2V(VaS=Ik8T3U;m>~@opxy~&XZdBl)Zx2biT$=dVe1r0C7Pw zJRH$&YUa))l)`{YMKLxOGdLR3UCmfK?~8&vueKZO9(UjH-V@KMOy;u+5Cw+cx-i6Y5^U;Yx38`-*T3yzFa#~x#FbQ|%&7RsZO)CPx5%ky5ozV_}war)F4Q>yV}f=ioD+ z{Ty%l|9+P47yc`?O2BGUBL&2fq|-K>Z={4m5h_U`QUpj^if$y4whj=UfK! z*$&cA3ZSeM)>;y4X<1EbEM=uAtAMa4aE!RXl6X~D2e(MUevgB9|H8@RjY|OI z0KG{^Ze1j~Cyyz_1?HirpTF_zrn?*THXV$Q7I@@{V6m}=eEeaC21-<`Malz1bh;^} zN`-c-iE}W#b%J8_FhR?+S-agXpS$n#yyRs!vU6%D&cXcb8JImoJo6-la*aY|01#wJ zo7Tz_NvDM|8XTNEbB1$gP7@Vk_UzqKCQ7H(rSfYeub!h>sGpvKIhC~ZgRKg`)R+bx- zuYVbj{;A=ry(PYKXcY%^I+jk$5(q^QNF);76?B&cT3RZDj-6NAjrES_Bsl(7EQmmO z5U2p_G7@9yqS4ln7(+D(D60S|*0-i}E(eLg=0Uv0-EQG+|9Nuv9sm5~@w;-Z31*k<+oIZmHE z$?#yEfuTW4jW%s3QOc_lK5+j7oIP`nYp=hKQmKdo+RZiE%{AQl(|ut!hojr+u(Ytq z?Cd$5wG>N5CMPGcTJzXne1(a&qgbqS@{Aw|p;U9!MqqrOAgXv!NaYU%X&h1s-uUW4 zs`?DhIXayVQc2R#k!VMd38b(rHd;i%x&le6Au$dahXk?0tR=`&ku!6KSM0Cw&|^)K z)XRznQf^X645gx25^UXNFQk&(al?C0yz9AIAbxfMBE){-yhw9ZDR7PzXGx4DwGLw} znK5*grW%BlltK!D%dLv-SrX@bQ4FRm?ymQL-<7w&?zUP3*l&%)q}J=c<{R5>G#0sX zx{zYSuow6B!Q)(Cm}+z=qEd=D+m)mmjPvz^m1dhQNs+YKKw&v@_+ct9c@dG4gi?TZ zgvAJJEzUX?&d+i1uRh6^@huDv4^gdF`_84#G_7WnrNu=$oen_|^aJQETgJ$egwyvv z!uF13s#qk*Uz>A~CD2}lm3b%)iT;Q)q5Mv_fupKI^izMvk6qd2zs?C-sU=CZ7r_>w zRD`ygP?H~4xe$>EF;rW-Y=hcJD0m#L?Dfr4kh|F&?vXGGutG*_1X1 z-hRV-Pu#a9qNzPXS27?gc94ihw#*jISHkQm9y2emaib@WYKuAAw z46wpml44!(T|Zc1d{;1Kjq`1wIWYQw5cD^P=nYxf;4$^wEwL$`CZ2It-q?^JE*9BY zZlQHXr`_h79d+Um+RO6@X9$$yktdF^*jOgUvYbk~Qc_j{LD18y^+m(Qg+&$@7W!67 z^aQMPNuV$c2!eotfhx6Hh2_R7k9_qA)sChr0?sB-4FYOGK*`CzsGG?ILU^TLp#&=Q zk1wr5DvOYUH@&RJOC}5a%3m(>#1eG6nlLOP3snS`H0y%36rzYqv4|9sL|c}-U2grs zAH=p6s1zhZIIh}L;@F8c+qM)Kss)U1iP<4$}*{TN5@%60rgZ$UYJ$FgC zP37S5H-W~noF=U3jJDRK*0WTV5JXZ^kdl%LC0|QEG;+av^#Xu43{4Y8vz`J0dW)%g#ki1 zx~b--SO0zXUVS~guDpV!^QZV9|M8btS~yRmtT;V9z*wumljrBil;PTH34{bEkg9-` z8A2FDkmm-KKq!Zl-ojb~*A1uq#`jhE@Dp7g=nj*1+Uy#sv8UK(ax`FhwZ*Bmlr!g7 z`K5RL0^{}wk3901j1LCH6-ii>M3n#)2#j{5O^xo_>8xYk1Kjgms&71J0P^n1J@3MC z+eRq>dBJ84ga=8laM;$67=v{V?JUk(j4<4A^ANB3fnfl$j=>~0casD`K_Wwm%`CcW zHd}@bnWN@~UtkU4M9_5b*j}3{a^{4o|mv=x~ch*N{0u zZBU?;BnlLPgF+x_tQzWW7}q(4PTCYAMHG~%T1z)ca%*DyGeNAi=qw@Cj-iopZhXaS zc+C&Im7$Rl;zEImojaM@dj-?Sj}b~ZJA00)SGy$ z{U;!uw@6AL0*jELzc1m3uNAyScQ|EgG_|5IIK|AG;Pi5Xn_r^%sW**qW#vyu9)jI_ z9qb+IyKfgC;Y>N_ZXWpYufCh-;rg}$VQRDN;eQN+pmCm1AE1)*qu#s(cMt_@FcHpf&Ek(Vu?sp34C7>-Gz?r_U7G zNFk_45wY}HpLV7pvi!t5wy|}4?7^|-!65@dSW-R)>6reb_l$*C2Jt7N>rFs%0%tb> z!U*r)^uEt)CEn#Rx%II97-1v$diMuIK_nrJ{A-q>Vz5@B7%EzwkS)V?4!rFj^YS;n zm8{$0=wpu&7mLi#?qYFafd{_ydCnX=f)xSZ`RZ5m(f_z zD7#?AsUva_N+<@BI1U*dsGzJS#q!qw_`B@8@+#suMpY`@^vYKRz4IFBDc%GRx07#pfFP>m4^go-GXE1W&?1i$(ZU&~8wc>`-p3mp8&pU_-g z&Dp^jQCuKQEMNTU!~E(`{#_3J-;;TK;c!B4oac7&HP5Z9agMIBw7QyZ$Ixn97FP}D zS1c=S$8yJ_4QKkI`to4bdNRe z?UTRsXyb)wLH4`NH4wCOw2k%?mxT1W`=?ovlw9-j+9q%~>xip<`cm39{mhihPf8U@ z?+RI;5Vp?YY(B4B3v6mJnZahhPU7-Ec2?k=gxtD#Amvs^YY(107V3#4^on>Ub0}?* z2tp}{m2WLAjcnn^|H03(=gKRIsewfMN_r=AbhYEyvfb?4C81>gHx)vXAz|JW~I{gyrd?(st}D1bOPl!v*V zPvso3^h$)^*cpwDpNZV%92dx5`XJQ=IS)8<33r_n{ z!zQtquE~c>`jgZ-;jN1S3|I@oS)9w$Pp;>7t-EMFm!f|mQ@2HN)FFXtKizkmaercpgrL4v@!daJAuI@zHH}ch2al=s@kU-XmddAfoduIxtkO6vHud9%O~+xh^**?A zSVIm90akiTp;@1nl;z6fGRWP5ub)Tn&9YqUScHRaH{t46+`^UDUgu|hNQqQFR$5wG z;_%@|Ir-rIJorcNW#QyB9VjKlvDEk3G)pV#criMT39-@^T)ZIAo!MX{}w9 zEzd1~1Gwe6UM&2!2I&r_Ell-NyEeZm1V!b+(9J98sdjXP;hLLDyz=k)uDPsjP?02v zr3Xjb^dC8hy@-KHoe!u3jZGa{Q=nVkk{}HP7L&T%k_c?(aM~Bjo_-~l+9ey$-u{Q4 z7{_YckZNDt+;`(myy`7)B`y^5ioDO9B}vN6sndMyj-O}l@ka;(L1q~rP zfA!mUGXKOQNGYfm1D4v7iK*=XY~Q&PfW`R*w8?+~>%bU4i7E^O%H;~q`E+op6qBV1 zwQ`BG=jM>AKoBV2^2VEa@V>0 zAFOQ3tjNIEH`11mAA-pDHrUK!GK&kfpT*sPtm~i2WQKI!TZw05t)-5Wiy6i`Ca$=e zeJ|ecC3^Drrj(-5Sm9H@|G#+r!+-2&5{i&&AtX|c)=C4MVaMbo-}T+E;tQYotOuUW zJTEO!1WHnpf{JqtmkZQF7^sw3SzaQj53_a47@z&Z1N@7B{Z1;Rq%)6GDS?uXWF>@!$gwvfR$l#>Znr!-HJ8Yn-Lu`PFaa(%tfGk5hY20OS{T?!47F z?y|UgfCJ$JaI>M3Zt~MyIL{oNdP&4f|8D7m)T0mflBV|Hh<%@fPJG*`6Pk1ZvX(`6 z3~9$;Gxw}tz=f8f(J?3iTPJr>D*5*jg<&ocYI*7l_wn%i??Nd-C<7`grlxLWQ6IrC;I;pLuQHPfxoVtqg_G&p6*2XnAOaKmeUgo$gu17}iJ&OE`{V~_E`=kMi#uY8pgr_XbKrAs^Y?q!-8c8!hFY6 z_Q1)9pXsvQ{d@q&w;Ukv+_~dUi+hL3Q+6@}3iLVU|jE(Z5R~{fsQ)IP*QVL@XvvYGi z`S=rL?KP@JMUj|VTA~meQfDzZKK;S>^F2TKBfR`ouj1YR^MCM;pZZ7r$KShWiizPG zQEQQXBLyZWMj}aC!XNZuYQq79(jzD zXBU}mv{`DVbTc12YHdli!3jxyWR&C$KST2me;K#B@U#r17i1+}JNJ&B6RV^^8%Js_ zi8Fl)NkK}AQc)0+u%x*8A5<>+SibH7LBYFOI6JYMn2?ArPvGxH0J zjEoGUwWi%}GdDNS;Sc;FrC2gL5D_(tj24PKd9LZ}G{B#~@BeV%HQ$4*ZQ+N0^hc-- z4DgPh`bVs;tZ;7nBtd(S$!#N?OC&Ro%rZN_!s+=2rO|Qj{BOU+OA4Q7`QvvmveQ#k z0M^bWzELVr%&!POWA;*%g2N9TqMcbHl`{`K)045$5w5uY26kO>HA$<%(XV`xrL!kE zb>cW@=Np_~?y#`ZW^pa0nHnu4E4Kt?PixBY`rqQb?gldNs9+M;5g+SQ58r>J_1 z(U{M8^sy$7A8WI`x_Nj@97(?Gh8kfgSy;+AGn;UJK4W$vVYRsqqLHDHiSd|;E%7sg z;`mI5Gv^ad%yenDG+VdCOpM3u7?0VqC1!gqAdt{lnB(m9H05fQhaY~FYPG`Z>KcbW z^hc!UXBZub*f|mti4xhGL?MZUzuouA`?&w(f6h(c`x<0rjMu*Y`}xY_kMfrv`6z$& zk&kffvBP}v;iEVQFMY);x#6YX%Rl?>>$!38DfG<$z_g$=68XeM=IaluXSO`45m430pBM8z_-ff{3jRVsy$rTIA)&rXvi9Xg#ZYppIT&4flPVY!vk zN(`xS==_!$N2U!%dj~oU6;egSK?pDXembA~0Cr`*?-Ar@=f`;-t#4^R$WQN>xKknC zq4LUkfXIi^TM}#hR3z&wBCmaIo!7jself^0pU#zmTn9(TkaavT?)%&#k3H48sdJ1u6@E~`cpmJ9sW zZ~uF)xbY@rWduU8>jk--4zsb(W${Rh6GAY0zRNE> zI!dt+QH(>{`BbW*YLQNwA)O``848Cr8AfL$X+}56XeAk~uBMY&GUM|43ut3KNVFl- zmQV>)pqQN8&Y$1=mnbDkyDb*(`7JtM`KbR~wLmI|5YKLl?Kj&G@-1(e`sp1LcPYee zJ&-5_K^~KIjqf|J)g`}t`}T|9jPX@+Aq6JM-Knkz%ojep%oo1WKwJCQeoeJX(3j@F zD28a7O_t?vQ0w8V1x4Fftn=^rXa2>%Bt3bAGhe)yC|PFbSb=MHlo_dooPJWWvLNYb z%S@-k`6QvCGuDho6=VL<&;EVB?@g~kmIo1qG9oB|Qkaz^m~)>aJ9RHMa~QKxfMPuZ z(gTQ2EdThf^Zek>BERuiL==U@p(K!kK!P=fZkpmuhH@ELRXX}teGdY3s71ffGpZh1jBO#RNO zGjIRt9TN|MxE<%}J$Fk z_I&qm{F+vq`kMm5*8_+Vy?%KgQ+)Kl|2jKlMnze+j1}3vrO0sAzt@3LN2lpfTCguD zv852PoNAV`l(kIrNALL~KK8zk@!B`v%vCSGk{#Q@t{ft3obbK&J(;3jFNa*G-}k`c z985P1Pn;KgHqfjrcR)C5LA=8`_j68AcX1V@VU|l{(C&A89R+z1Au$4uznVS*Qe*BEz%Z3@*|4Dnxp(T?-e+SSCKtkX&JPF*Lq#!CC?zOG ziejvI#erK;D!^Lfxo11BLX~PHE34R?A@p1jq<&6R(trEZnFjDK;9dW8$M)M0;%yFh zzyYs*W0gWt`cukyaG1=}U9)`X_s{YAHx2XYk1q422N(Uu*x1N$f!g8b2CQ{)0EeHD z6qjOJEA9U2-UH3@T!Yq1i{bGawZRgF0yvvuwI)q0-L|2*=6!8zpWaNbg(7GAy7dC5 z0Fx;F$ zF7$|#%ux>gE>EmN29{2iVT|q9nbxObiH+KLIXBJa)+3K`@W2QHNf;=?KoA9zLMSOl zk~jXqTiCH{SN|C}J)TsrhhuQC%D!uN z&~COcSw_-LNR!lC3u_5Pn^wcov|d9iO&wWg$x=tAZC?RJsl3m`dF@$a97YKmp~U7) zVUbUll6hNi90;P|;#f{fs0}-6!;YY!D339os-%WQ7gau+BS zxjto9;DjTPf!=G64+uikXP*&H0&_A^zV{j*h$ zJ=tcc60@|HP*A~!lW{W$HUne*t5T964oO;xZiyD#mDy)dr(AUu1LT>4<@eomo3l*y!I5}yJoR9QM-Q!04kMDTqZ~$?+^IeYa(8Oo zt?JDKTxZ%2jtx>9Eg{ITHpQA0qchSZAzMkXPU56zhG`?vrblMfSS<*m2pNaO1%a~} zCQE6qX0+RuPRG#gcwO2oR;)x4lehaS0g)0E0!cZPL>K}Upyb87d0V9u|eiQhnqaHwH4vbAF>T;LXYD#e+q)?Rz6=9S>NsY@4ovxwVHCO>E_WpfTkYt%9 zNisU^E@@(Ebu?+}Q@0nU3Y`=*24m80=6Pbd7(ziQN};3@3L5}+THXPALm_nQc3%J1 zpWv~teTlVZ2N9J~afzc(KE>?p9LDAustW)j`ol=+TaHk9@P~m1i~#mrbq%k7>yJ_y zsOODlnlv#u<4~%I(VEkL^1m>d*9m9~!$W72+wb_UZ7(i_>LJ-r47f>wm`bcW(8(Od zP~5`{aXv`n4_><888>BnZdKRPIIDT$K0_@ox*n5d7^Bfy21fUDc+OeUBw=W5oL9W=R)Q!31f5Qo z?|juOdHmsr_{wJv(rR~oFS>P`0HX5Q??FEQUCQ-%r&6tP(*Wo|7r(1+=8Eg~gL7m_ z7o8;}?IuR|+MpGB<|Jo6@nNrsUvYf!u~oi$rps74ytNXmTjN0bY+>FnT`2?%6cn)% zAAUi)SkAb&^>lM}UIcW;vg^grm~}Ll1)ZiueFODLYb-)ZDx+nhasW1O4Rjip_t83Q ze7`x+U1_4z7AlSiVudpnYX#OSj5ah^y+A>lfpCgyQDUqm$t;;!mk1TW%2pcMIu0=?QO3EvX*mX_e9)N7#=PCw9 zkXOGI#%{!l2&eTXYjMfijMijn3c`0ZhEYtVRwW2SUi6ZeviGX1`RZry<>;di=au^0 zQi@9}&r}EMyyUxH$>iRBoIP`fr=EP0si{e<(G=o{)j*LY9Y7+M8=U#A|BTf>t2dK& z`1n(+1j@^nmLh+x%27ZmR#Xa#IFyt^aqyph^5oqwNPt-FZZ(-jC$?cy`yQRxLrR!8 zR|d>&<*Gly(yn0kgkFG?T6i$L&Aa&L~#}&9YN#+Yn^y&qLjmQyQEEj z*!F7(1Qe|#h!t|cA_7M@HFP=}=?tN;zRz4annM9?;n0ag;?tZmw;rWXQB)GK+9Fbd zQXFvA4TEgEzINetAnO=!F>*@3Ni2OedhN4%D1#gtCBFX0arJBHo_qlP)O#^2b6B&X zUw&W$QQe9dnS{8y86+kHp)gvZR6rDl)Q5(AB|OXcJ8%6l)>axE{>m4apFPXW(IcDO z+_9}&*}i)(+jdSexo0nFHzDb?S!=dfTv{UOcG$Ud2T`Pu_)MOjnPK_={2{i}#W^Sp zYkvE43s|73Evwpcbt&v;!iu4y7%B>ZyeAes=SCnrE84vCzkNg9Ik&76+pyZ*vi+YQ zKGgfa2Y+(c6kb2;w%*UuuHeiO$;$jQzlPFKh0;*bD=#>M5SB2uUi{5k&jQYzZ@JAJ zX~&^cZ*Y36Pz@+lpiqG<^?~^EQpVb9)|Wr63<^$`V6JQEq=q!NDiZIb7eYlO1a75G zpfw}ah`;~CJGkZaG5w)`mPAd=ofYygDGYeCLfg3&ReD_%>M6zM$nQL_1?IOAJ} zarrGzbPK7FDg-AmTdyYD^-_|-ZLBOW(_Hn8;V3FlD3>W!D^zL&l&b@TVT3k5TW54G zXpt+n_=2fV)n-YTq}!p}ZqseINxB^#e&kW45L7BX?S0h6_uLZqk`0?KjFsKoM)|L}#=?|xo!vHt$VxWhNr9~AK6 zKR$c@Z*2O`|JVHojJEeW<8GPwXNNCYNqz7qcOA%qa-jFKWDlyx#seC#-qwmS*SgS>NUAF;kWFr=YZ8S_w2ejMQWP@juzk_}0Rv*O|4AA3xBGL9`cT^HN0Ky|UI& z`OY7rGq+0X@So$fLwEUxx+A4hs31ZJNxJtYR`=aZV`&kcX`(`rO0CAg&@lDkVanAS zVH_i+!Wf4&24f9MDqm1_*1Kp)mu|a7Yi*6SMuWAL2B%M*Vs2r9a;ZqYKEUYMC_|MB zi+8@KZ#ATZ%2vaB4=-_KG4XX6DRS2;pb`f;ION^sP~Pz$zI5i>mbLnJ0^|fRCG%9O zgoYI2phA3D!acux`fTGHeUdZ(<+^uR!{m-%Jp6S>`(6qtOXnoBCnRb2TX*5&N(?x3 zYB1^M^rH&=%CSSoy0@6NH{h zvp_aIRu?z^$AimnO^v-ZQsU*YlGS3QD2D1#DN+aPv3$?3eC6!7J)Qe)1IQ0gjNdAS z_)uSsU3f0lhWQ@T5^+euLsE!o1=9k-f0>!N$U>a_m22Mt-2K;4(0e+d%%2j^Xi>hI z6%{-%s$-T17A@1uh9omQI@@KeB&ihqz|mgutku$(b^>c19PrL}?qlbU5^+JU(=|+w zPLr#vWEP#~HK3f3nRU$9QCa!xFI^m9`#*m_2+6TWzf4?+sn&;x;uxuv2T2fmZdZ>V zlh;~ot~A*LNR}ovS64Z6dIn=OVGvR-mnakp$i^(ocmEa*ZJ1frJayi%(s2jhfA;(> z&xIJ`+e&x;v5E0}72?1;I4%Ur`Y40`NCP&FH)trtp><{V_222&IWDMs)a%s8w@^}LZ{VYrLlyRl0Yd`-Uvt9 z;r+kzUpam3%!N!UWY@r>YXzF|FeAGMQBe^mBw3PBuh&>zI8V3L1RM)X39{QIXm{v6)r(&|z#HDQ z)mw|+ExW*CqL>C`?-A-Vz~(zCS3ZSq@x}#}@*XhgPq)p162g|P4i&)2s0YnAy5<_1PU<)@pK*3M zVZ{JoVUdj?T5X z|I8|(fSc=*;c~z){?av`Ijg-(rBl19mpw_WYseiUoMqT5jL5bBCWXm@8ofIWU3^;7}I;W1}@$2g$}^Iu~1z3&%aW3vN9n zkc}g51_syvFLRc}7}C54etItT_nq@281Ui}USVg`5rLD(_53L^|RbYaOBn`pKxpQc_S9b!h$8uW`~8A_M+MOERDbO z@?rK&meHxj=I&JAYP1bmTj#wT-)MRkgmUOqfXU~YY?$xa>(_A3iy-us=N4L>OhTBsfbj5WP#1Ph-8h$(`Pt#ZjDD~Te&j5<0E%m&DMdiABgMjh9JE2jaiM2 zhk-GWr5olZ_UCEz?r#RgrvJCOLjA^(4Y{ecn|`M^ZEM5-S*VS=eoS+IP17XfTsP&3 zg)WCqCsYDOISiSslo3+o*BF9gfmi+P&r%rO!hN6kOaAg>f7$?comt8nFDMJrTfaJU z@*r^Vj>)M5O3I%rN_lH$EIQwy#(8NN->JZcU|npmCZ4t3?}gszZe2J`qL(|_*an&p z_v`_t2L_Xai4D(tDd%nD(B({9+TDz#wZz%k1qw=`g(9;CB|YD&Rty*^1q_!%YH`3y zqWL>F4X~vWu)Lh|!Ot)8g`-U-2P1C1s>(<#zrhdB(_XCcnK$=zpm1CSjHd%d|L^R@ zUoQlO%bB)o#V)__x$~TFYCjNl!yzZ7WFQELm0~c82&F{lpKYOB<_GS07ccs*SJG^@ z*)}oBBaeKoetPD_o$%b_RXr;f8rIwIip`zqY_vO&s`#99x4{@cvm zMr(LxOYseWf(w7=89;I2;|b}Dq92`E=Knmf?3;Bj0EZBQY7kJ1LJCSTTqsZo6dHdp z-SWw!S!-~MgKZsd|Lht5_<=_MKn!xXILtv(2?B=Wh)Uq! zUqQ+KRp0TQ-^s51`>E9ieE)qMQK^)<>E$=Gch4S1hK6qcf5C!$OX%(b(GUWpTd#!_ zRftre@gPWSFL<`OOJw_6OU~I1?VoPlJ-g7FTtddj51aCtHG7E^vbaS3g$pc{M4;C+ zsumC`$yz6&(V8R7>Fm})Co?QHQwB;Q^;*oQA8avEiIFxV6cQ%{O=C!%f1OK- zeDKj#-gHfkH(XsMbc!s!gr(>Q*B8VWR$Ocqp21pN`fIM*9>L%?OHkAN{#Tdy)RAW2 z)!YP*JlPT{$ygjyi$YRs{VW$LeNrY0*>%g!RBHo-VTiSsFi;eWMF#2vyy|=3MDme8 zK2R(~2fpy|$%B^x0{eyI66stBb1kfrQkyJCFJ4KlSr4c$1(fwF)p`~8!Zfh+X~tfF z-fM2U6qSVwYcLyJyKGexR29WA$X^S@QlOC}){$zJ+yz!bEH}5Vp5QZdO;c539HhsC~&>19c*`E~q^<1b+e$9h< zA)zwls18~>Va8t_Y4VZ3TBVuSV_g0jDIgMpSor3zKzeX&DHUmJjj@hW6!fpx*o`+( zsSi@ER*_PA@i`%gWuCM-FBisz=vmieK<%~H9=7kmpq`A0Ba>X zC$`XBUceaNX$Vj*M+AW+P=4@bB~f&(rDH9rgJPt(d9uVrC19~-xc6w24;)(Imc3@28dNJig zhgUgxq}h+ndNG+0egj0pkHiaep@yL-W=Ew=YJBfYAbr1m?`Rsk{(7p_8uh`UO$Sw= z0t#`=@aQ-LgTokOrf#|E%3JTf|H*q^umHJpa!;KNjN6T_&tJm=^^*c}(;upD_%XEg z)+lW^NsQ?S{^AnP1b4EtuBBWJkf9(_j+Oa2gz(x|+B!t$#iW&zR4NftrXUM+ z7QE$2urw0GpFg!mAOu$pM|}S^RjwZ|&`ljPODSJUn+#VJlS2{pK$4|`q$6lGBvHXp z9`LvR>uUK|pS1mGHZ}zB#hRs1a`eW~-*=?RpZ-;Yxix)Jv9$;M-Z+Y~l!VGhYqcxqfSb7mXLFM3Qpkt!Y$P zCr@5#70NFEdTEHyH#L9p=qmR;)!gJpS+PEi7Xc9vOG!y7szE@I%Ltqtu?hgPeFv4)YcZH#Z9!k#*Q;Kr-Aeb3jP zIQ!`rBtRV8ys_EF3Gu8CdrGunHBDH{Qc`0sk=k>cT#IJ{iNFiL2`N1jR!9OZQBFc+ z?$O5FnmG_cGBQ5SsgtL2;W$Atlnj;vb|wYRwGx`zCno%1?}1_+oLtM8911zHl(N{= z9GdBH#kiL}njDHKMc-I*v6WgLJJ;pWb6p;r?Q(K4-9**t2jLrgsf7Vb zNHSv(0_t&0EsRL4p{q68SSnFStP}cD^)@L5H z6U&LEgxNLCxiy{35_xwk$bV_W3?rX3$$?Q&ib@brk0L5zKxQqi%xilM#4$s0j0HNz z&@~z<1hpvYb6m!5*w6U*Hg@gaix85Pl@(T2)>v*VlcgD}YipdJJ;&1GJYV_j$FW(4 z3Id`i`g^bb&Z~ER>PwH$yx;(0;efzloe$90Q?34I@>}d?8D~k2rK>YvGtqg?B(LN@ z9WXXhPI~wI`F|;p((BI#%1@P*QXaIp_x$0R*?RyEzG3v%pWHvX-#K^l2d7P=(Q)-e z`yf{2S5jMvHjYJ`F>4LuQN%zL(n>R0TGKIx)cQiD1G3!eKCzfm0G^ob`hi2vq4Vn) z0dY>%2{yz~+QER72O0>ImzGLeh(0&`d2WMsvQKQWlEsQN%=4q?2jd+R!$d#Q2nSuejKIy@fp88aN-r zWE+?V4ndHoWn%&LFr;26P>$lf*qU3a9xqM`LMbUIMHDC`g09gtvy9aw?SrFNA@3OD zfvh#5lvKhHDLn6L=Xbx7$$eKax@DZk$}02o3(U^0O(lv$>wE|7x7=7Y8q8F-#tL7&Z*I=>enfMk&I)g;Yp!w}U%==ag62_~h$H z-?iAax36WEmUeWtBQ=iHL5GN198ilR+G$2t8;YT#TnGpeG>s4Fr4+0g!(*Ke)hOcC zS6oHwyn*p;+gVy}FgG{P^vRQa z?!HfP_Vf&K6jCfiRLez*#RBD0kwP4!RDg31r3^t3TnQ9__6yC$Y6x82+Ym=$Y~DfW z`ZSOJRHO|dv&{>CW`mXJ0ij1zx7PMm+WX?otV{H}_1AC-kgS6P0YWN5;U)aN8>MoK z!QB?__^nf?rvd)*bt6;S@}c>z*}s-LTH4apj>JM{9imhr)|N0XFyG1;Dk$nIpcpEg zaV&H+OKqLIIR0jVOeyLwyNR(IUqb7VM`#>5np>2Nrq;BL?f0y7HdvE_5M<6X64D$1 zMu0$K5G2l$8<2AF4j`n=CY83UY$CbCf znURSdEG{)@tgJA5?i|1NEB}V`=T1`yLyA#IrBon}LZp;PsgOz`rSDx=HlR=}j96pD z3(m!|a1glF<(5RsUVdJ9qTq&%Qg5U8h>IKhI(k;af1sv6>{Gb)?9zofCwGaAO)t)H zd468z@kJoLwFmJKjBWYP$D$pM3r3yH-;Bb1SK>uVs!_X6YJ7YJ6Z_D3=K1 zm>L<|#)nv3T*4U7`b|>H(pt)6Yn^bx`Aw?Bnf#TM2cE0jeI25PPoaN5{^lv$HdWOY?d7>~N3ImD- zUne1NV^B&Vg!DJ^xhYa%t-T4qUtUly)_o3c?Kw<+9W4P8*{7bkd=B15fasiyrE?un zgO~Tua?ot1hFlB|E&&jiPqq}YN1O1#h{FKoKg7Eo?v7vI0F2MSX>5Npvv)0Y&Hm-Y zvX(j8+R-&Gck!TDE>Ib$VvMCy2yw|8!_^QeB37CivujEsrrn}aJneDGV?`Y7MPSvzn%Uz-kmR5J!17%(3%zuOo~KEG;eb zJHPo~c<^gqMyUWHJpWCbjCQx<#jc~!dk;RA1Khf?tR0MgNHd2a6c>-e3V*KZs zndyyx``I^+-L{h2I~TgPzLHqhGD}-K664T0C#+m4F*rN`4wO{=*JZ8a%zT?O%N-6k zJ2Z_Y30n`Do=UaG*yspjqhpjxCA7|1*|L@8-Md-&+5n!bVm&|BNrDATiWv&V4 z6&tlqF0s3Pyg$kllM3wZtVq!GLJzvX=H8ULWw>*SM*bol5!wv8RG%N zW~#{30i>tt6y!l`D1F8zQbBI^x!b`V|LFo?0DSh=(YsbU_O`i>VI}pJqmu{YCbtBI zLd@_;ebb9D*0It|IeEUt@m9i#&gOaQap=|I*F`B{$IeN%@7TfaUAx%3cQ2Jv8Dk7{ z^K+ayc9iqwGPM_7L*rBT(l~mIwNz2onyxk^IrCZOE=)-&3PC{Cg?Y`w&@l$>ylkq# z5#dq83K}%Eu8~58O-?7x~)#_w#?>|9>ci z>xZ1ol_h(hi_OQn^*#@4^UqPIWLesCwGX@isV1Otzb_rPu1f;B4RSy35`F0FU~zdy zD=~(q(JZGa-P{is7fi3+cwY(+h#oK^sfdGsNcoN7aCZZD{OSe3xbMxQQ`+!zTDaS0 z+orzMwXAj>%{<=7?0Q!sfVGySn<1p6n;6<%&FpHI$5*?|B^mk>4LN}lC;>)@{#48_ zf8q1|{U7;pw(r;wL`E>e4c)(I_Da zrKF=ZU9D-R8L6=pf`CfC`3>H!%5rzL=Z3}k^&rpenY>lGtuIRf z^jWX%uYWD&p;3yZGLJv@$U0R84*-=90ln~`>cyHyYmC+T{1P8IsX+18H@0Vz2`m&Z)t)& zzaL6Pto$uf3h7hJcRSogz&Lp8=z-L@+Z(Ap(6&CtXeXAn)X~lyiT16+7j~SaX~y~4 zd4yEHzThmg9nIOU`8ro6XAldYu(;;h7qNBQ1l?|eF@|shziDuAh=IW&jMkhxdz#0K z1;UYGN?-g6YY!hrBB)0Z);gM5%4(X@(VEm6!u-8QO8E&_GFT^k<}R1c#Vvf$?!nQ_ zQkrRo6oP6PF&Gz!a!N_%+G`kl@rx;zDx8`*$rnCza9ucpi>(LbfqCwF8RLQBoFz$9 z4nKU1?b}D$x@{C8#D3s~1(4i|^j)i7blU6Fup9cu`+e*Ew64ojyRP3zmB-oZnp=5A z*n>jmppgC3Q6L%)ch9AO@!7vOc3UTNw=bt|s+k(PKqvE{=w_BgJ2Kt(qfM`Rw7#U2goOf=yLQuQw{XS~eQ%k{6?4WNGT}O2OlgPt$65sSnmrO5q$+ zFE|(LpX}Ivt9A~Ne-I|0ue}i*eURjR>iyQ)9z7xtX#2w>h45{HfD+OsjUb)wY z(tGrK{`CD^|KbnI?Q0Xue1VT_^G93|Ploz)jz%ehb9OZUrP&Bvn& z1`09dDD>&!oQu{rK5`RseaLC=TKBAnw$?Lr!;nfCQIBKdz*~;QS`K&Hocrom&|mx< z)5njpW5)ome8qM?c+UfL+wK1B!i_~=DJ5|f_WOaf)~G;n_RIpy%WGVJ-Cio?5>f^j zqi=q}0VKn{tydK6(KyK!NiRp=t5f)6`}27dq7 zW@hfW@U?vI$3_om!|iLCJ&y$!f@tS%mHmQ{=(|SR zS&Z=Dkj`x?6!rpkKfyK+Dsy0jQubY|^>nhk2lz0!d;ZJJsm6t`;S)bHI#>$$jZAaf zItYBm$ptPSF1bF%wfEJSZ!I}r2tlbJdo>r@T|Yu03>71zV;F5n(-dnhfl}zaxoaa; zM+ix|T*<4_l3J~f5Q0jj!j?U|S$zBn&VK06(T$b<-^GC<4ip2u`iJpFS8e@4)k|tb z%ExX>dI3J;9Eo$>*KM<9-*#U8sx6$KZ*%5cn2TE3Vi{wOS$!bJ~gj++TnI!5tE?Uv3bO6H*YN*eGY{Hg@^t<~X=y zCn3`g4q4no4i4TqbLtt3OJDf^jlR=a{*5-&b0@?5;(_%;HUX(~Q9B8NfItf};YhRx zi@k&(#M7;TKs;j%B4sWe3WTBb6Vnhxfufrvbh=%VG^JQ5c(u4qf&Img9fSdU_g%^4 z)Go3tWA3VJIeEhk9Q&(JGJoG^F{^7i9sR&px>4 znVnnZiKiB+RAY9J!$>J(r72lk?Q-3f!?}C4`L#u1h?0J+b(U$miFdEYw+s=6p&t<^ z{g57j%XK z@`#1|gaCs(1T_BZ%;|6V!hQN5j1HD0AI9=3M=pUX6*d5EAhX~?$h0HLAk7@9b|jex zM!W6kggzEYE$9nUyid2v>KmjK3HmeMl?q6bF6U>@Q7RP~7^o3P(Yk*pF3}7u`g@*2 zu}G<0K}x~M=omx8BSiOf>(NRc3tLC>kOqmaZ%*FI3eZDz`LIh0C`rX+Q!MhokDTWToiO@JQ2? zg@pyCPaNaWgAa0YW`-=w5JE6FJB#ji2wH7Mw`}F9C!b(0OE`UUnmG0(Lm>p6B;)6P z>P4J6xybP72&>^s*}FqiaA!IC3Jj&@&gJ zIHXW45Eo)>o{Wg1fH*D@gdxs3(zJ`#*{=8h`RAs;-FxL(*XI4(Tqo`ruVc8&Szonx z!qXw01&hTPixUvUg1DM15?X;&-mQ>56%Z*15mX9_Lf{yP1Z$~dsVi7XEJ0#vWq#_^ z)73||733uWaxq|_REP<~5a*!NT;ceUC)jhv)vT_rQYsextUrN2d7mb(>G@jDIkYyU zSxTqVq21|X2&oN@vdzGkzwjB(oSI?!_)!F~+DhnjeO;nbiitvnwd-}7KuN~OC28Ti z`QbNIdFa81`0$61^Z0Q`v)QKAc6nOYt$Tq&V6CIm&1kH&ncQALDMb(ls4%42PB?M$ zJOI0QPY{L*Z8R$IpbS)qHRg7n2l~Cw8w+ys-;dXE-0Lj+ogr_+@}xkco#z5sp8*Oh zfN;26w$o|Xm+^EOoRyfi#2D{8_X?Ydb}V)*^Bqegv8*QE^|};DG7BG_PcL$_dMRBY z{cubn1jF?~D&-<^p-8*iWwqJnAHCz3*t_pacI?=R-tV61eRzvoCEF6wB zNmyA~qtorMy1K^f`ST=6%KPs6AKd?i&u;R`l2p@JZRZ6;M-&E>3L!!YiiLM2n*|KGbVj=R%i(yQmR7NU=HJUW(k|f<-ANb2JO<%Sk-{d;;YvcQkw;cP|HMIN; zjh@wzN+3dkP#|Lm#`j|g<>fa;ueU_{%z%)7IEKu#Y|aT{DHsm~K_E~v4`}f&nYC^c zeKJ%qQBX{`%_X9Q{4s@3XGdW`6vdQEB}(NIVGwfkiNlO<+s^dKlkDC#<+D#gND!zC zVvl^bx;1n<9p>g2IWc__qcv%kvA8hD!B74rhYx-2BASd+lA(bzN=Y(pSZgN;Au!gn z6+)#5q~M_kPjlpni1{VoTVnIUA>Aa)1t8p}!k_@6D4<*l`W&o66y?f_F>xX4GkUw7 zE<#GesIYvjwa(o+zTX<|RZutmI)RTZjCS-U7)$9la#VADRdXS| zTZO_4NJxk6%^+8RaT@^QZ&C_I%AW4+d%`Uyh5jh@4PtHAR4)d|I!N+XL|c;eyvsSgfw#WmM+>hxK*Z5^jnEK({L`;_vHkC!9~bMp(FK693K+jp&g=uiGP z_x{yiYzEW@jHtXZ$5+?$(^P;pu3raIfyCzR%TfwN{?x`=TJ2sE6iO*n5D-RT{+M|qbRI()^V}~QKBob4{=aRlTf*W z)}+~LbNHbL`S0)f&zwCs$56epUSql_XmJ<0e$KjnyFsA58j6yVflAR&zm<|g6ymI- zn`&B}w0|J=S=9OaxNuqLFvgd&;m7AR>p${Lc$Ry>?%38B7iia%0Z$2@kh8l>ud=q{g_B#5e@Vz8Nat9*(v?+cgt-{h@_ zLOFzU-u>!>LR@qJR|}GI!LnuqT`Mq`7;Rowjw=TO9$C(wCOC9?d#aa~5V>k&t(~z? zcCZSbJUhd~$IfxBF)S}Hvvqu&p`k%+)zNG=nVXwu_WT^PXV0RwWp#CpTCJLAqwFPJ zyo>JV6dxrdDwO@+@r~V{o~4_|b$}KPxpqCniB0m9Ef7dw(=gWi&4nTFYs(xk!safNU3ayl^2`ny zZmcYeO}!3%#dHLb5|oRQl6G`cFrho2)7qz_L7+E{d6V6@I|qU`rJ zw#j*9ms-Yo>*3w2p5-)~Go#KYJ&e8_gZ#DE${%gr?<}|5oY3gK86RT^=cBUuMae@{3l#)3jyP>Cffuw+J{oqx-7O(-Mj(s6i!dr%gz_JF&%F|M-FgVO9%c6PL%A%XbH3Uw z+`24Le(d2aM1i9mIh1sit@IRf2igj{+S17^+6lrCzGG{|m(Qh_EEYQFC^uJW4^Oh| zt-r+nuH$Du@C3JZ|AI-D*|T$iCyt)riN`)oap5Ig{i-(su)MUuC;#HZ-ZgV#J;jal zeHUz+b-D>5A|K1$?s!T|<~+-w2NYusna=1YSw8R3(duTDiy=yiTzcseR>FEK69kvu zKGs@{5Syr%NjITbF4Ar%bUF#;QhZVDbQvIDf4xH}rvCr!y?K;n*LmId`_4IcctgFK zdO&vrL;)Z`0^rb`Y>Os^hGkonC7Dp{3{LC_wCtrgPL@O`wv%`{5S?W?vJ#W9wXEex zHbBR*C0h}jrYut}EB&nUZE;IMs+R z(A^jJh|czsBbZJ?*{?`N*x%E*skLc{26A=Kj4Q`(VVO8#R0UqYFX8gU^K9R~6d@wK zB`f&+GZ*;U|NUEhMBT=_!^7Ov-J!kzM#?Ma*}Zg`&dNb1TTkAyCzhrA|gWPuSj>P}{iHw6-qFf32e^ElJWC|J9DsxTKvM zI=N<8yu6aKwstnee7PEEYFaVPU~3zn=hoH^`}VEioy9qSdJiDK=jd^01Go=~wZlwz z+w30{1d|zp*5F|9e8uI*9s6D{F}-pD6{5zWsok3>w6ImmM&KZkKuY{!cs@Q#5qD6P3;X^L&Z)B+Y^r(1-^SDWAt?3V$=GalqCXmoMSgT#Qx3adE-Mr z!4Lk?gJgBZ6Wcd&;p8b&KjL*aT+gY~&oQ{}^?d%}XVK+^Br#0QFoZx|JH};2)mVg1 z)0%4~yxzyZS+A3kBzpEWQU4N)>9UGxmu|vPXyzfiqY9%zB)sCDmk2gQ(^ZVSBSkM3uwEu0%JbN8FSpN~B7kLew_o`)X#BFFCeJ|4Le zIM@~jMVn=lL_LcTDH@eUYhf^fP}$}XUR^3o3-MNFqA<#Pob?RHWmFJ)KkHN}B_vv! zWvcP}tJecS%P!%YZHm& zMdYd-<+_z13W~vk_pgN{li7wQh{e8`n9j8l;f5@UKU7(~f**KOmlIEqIX7s2$3e}d zOWU03EW^W_-1vPzL^ymax7>1&AOCAVz_vZYt#`bgCzp@%)fb-PM|z*+Bb$oVR*R)h zi}7g0Wb7MyyPnNIf{hYizo3}ZN|7eh4ZRUSm9Sl31S2_eI(7#NkAonN)#rALu;A?tD3bX&2lf{ZLi5$>%@DI7R8+e-aa1k z?WJuRp}6w%7T>?^+)Ct3SmrJ(=dVD`)UBlH$8$3j^DUJ@b}KUFpXrkouJYw*=AHJJE_O`e1x{v+>Z#Z>=k5xI{uI5d5-pc70&hhN?=UH9uuz!6y z?nbc|qcvJP)XYVt;a4>S9e<5+xOKiUmGF^uD?)8!niiV5&b0suGfV8>;~iHi@+>89 zWhkW>jV4r8&Gya^=N$Xi_F%BAv*SPW z@sc9Es%z3ToyB}(4DD792*Y7Xl4ufR7>^6KcSao8w~W!6+uJ*2T}9TD4?gubZ@w$Q z`(OW4&-V8K^0N5Q_s-c89+hcSCy6b}ZTtT7pRqqIiEQ9QoRa(4uFY1ObZ9#GXrhS9 z5>%dm6WpXCc*pKpm~4YeB7sXK&HST@IKjt{n_T1TxT9Q|#R9$oNFuXtk?5F0kTkQF z<{oYm&Av3Ha3y04X`-08SUX%wGCuS8GyLy2-Or<6c{5)rZ)HeEYj~EGvk&v^h08qY zTD0=4p=319h&I+v2iKPwmldONNw?dfu5DZw*VqS%OjE1*;BdN2Wm0;o+DAp=RGr=o zGE-eP7DHDfBs0Pru1mSDXZZZ{>LtTS=NxrilO~a-Z=L7DrFhQU?G!L<4MuG53~A*l zS!#I8b&fxK6js_f!TJ?%!@bUid!GJVH{X5h-+lJv9zYhZ;m7Z|7w=Z2aU~TI^ime@ zc;otg7r$WNs}hcS2Y7`vcY4wBZf;r$!8=e<^dv^1T8;lwRTG?}p1^o3ASQB+M8Fn8 z+Lbu>Xyns6Gw3wF@9LOdO(RHvm~u7WKTWaFO_8hOAyh!#%kZM;wiVV3nGurU@F9?< z3EPtLub&^YPyH_Dg-;_0r)s!Z$KqS0Mu03!XO^o|`&I9K>}VMk4HCK?{BVRSZ9|%w z+bT`b5diA~yOZeVxHz_`YC9t{eaB&C$@4!mT3OmA8PRz{z#6R`Kvf=Ww8Y-wg?YB{XgA&_pLwqtp$>A zDnNoZ@11r{MFibTqIP3cK6w7K<%T5OLzB7$ALX;+18G;#Eltp-A!!8&9@WmG{=s>C zRpAlFSLTJg(y=FBmyoS8XYvNnxsHOXI$)~?APW;}TSHj{@}6oUU(*{Inb%Cm}2(>PzV5t$7Q^j>1&EcM5Uq_Rx$n?E) zU&iOp#Z6VVwlik1_mSrA1LQis6uMz68zY!R@zz^9{LLRa#2tsT=n>#O!8w8ij3|P0 zcq=Q;h6kSb>o?zh`%i!Cq+8!qfQU}+2_ghh=vSen_S(=cg33Y8b zckVLYd)j$Q@SdWq=nux6zq}J^BaPEYZ5>q&A$rSj_gah^TrPU z(L2|9_Z_|1E^h86p^j`{?*(5Au4*(qo}asdwEIoQsT4tx#P9wLLj} ze)4`XJQQ6@nAaUD(kzsMq!YK(29-2BMgqDWwbpJ@5`3VV7`%@b^I~K^yKqpAn;FJ5 zNB5vwrpc>g>vpCb1nMG&Iq&Ddm<?ZnH)c)qCE@S-{kkA?nRFd6qYK zVUM+LMkSv{lCrGW?hhD_Caf%XY3C^-iagIaxNn92aLj02Tvc2U})>B@4sKsj6%=T0=r3&dr2sVLw0=A4AA6J8`!;kF&1i)lRMJz!C-4;** zSK9ZLSE_rJmJL7Uy8GBl^`4oYJXsQSw~go|Mh_)Oo`AFX!s0F1Drwk?&D=s{mKJ#j zt`>B<|7)N>L!J&dEuhPS023J?cAJOD~&M; z%gf7%2w!{lc{aDU@!peXQP-`S)I}-bh9d{avy>1#byZW>HCdur>20hq8jdU(u3s{AQ-zj>!XJdnL*?gni4Tx9D#%+_GkPsW-+5{k zc+-uFc1P3g7?#_b{VOR4R}+>yX6E4=e7I`FH5IHW9Y0r30={mHH7qNS{H2?Yz2)bh zJMo{b5ns%6qa9LNh`Yua%7Ubxh~*lF7S?#55`qQZ?sU)jsmp ztcTrNW^OGG1|t_viB=mfu&x3AD*@EBVxyOmwiKokbTnDl%x@y$8kfjf z0Sg$dPk8Fs+Hq?eWLY;e)B^Ye*XH8gsIP!YSJq=jT}PK z$_SxuXdVW1iUg0egh0Gw+cL6!MPhC{ZO2Y?bs^RW)mZ3EhbWC<^o$a3n&F|1^#x5j zCME(DiNdWA=4%GhP8z?@2W;gS7hvl^uz``%7!{fVT0wyExM&!vTBS{UslQ12jRjC~>01c8pqG&g2I(O`59s z{q@3{VBct5`ZcGX8T%7mEr%yQY`Q=N;DQ{k8`nweA*nsN)0i6UwxhM8XS#P5-2u?tR`lyz?=NCqjP=*2~U&{;y|F>;WWQUlE-md5+zB@qL&6`SnNf+!I9bj;P#o z(ooVR4*x{WoJd3)#N@Hq%@uAi1Z8lxim489mN6}wMdn+t0zx&05Y#-KY5*ceql|?h zk#1tmDmoe;rIdHo{7*oCb#B_BJG$~)Uy z4w;3*dYVREslyk~7U7EkLl@31zFg1lRHqMjT8Ann7!RrrxE)Wq?K!Zfakau(kFwIF zPf^@8jT*CB%tc_>(qShK-*}d330x^PgDU*&cYK?7bmsn}*lmDIJQdz@hR8j?l7$r#wF0GL24N@UEts1e^}1CN^vYC8;A)ziVsB8WwHv zVLq*wYh2wIsP|#P+Vn48aes{kxA{*g8E4 zAIlU=Z7u7$V!eqT)s79P3(w${UbD}gA3gP!1Fg3uic~iy);krn4Ah0gj)i(!(}kFp zN`pY}0=|l=qPHGfIqHeU)zjB4;A<$X|Kd-6e)FgI<{;_*6;y_pyl*F)AL{?o4IAEb zuWIacypOXFufXIPqIBd6RlezhfS4Ri8!;N^3Xl|El_c%x8S87u)nL^|D2*5fm&g9}YRli| zB@zbfs>0eD<6_4R-wYc4LRK=H-Zv3vs&Z_8-V`vcR*QDG6S-=W3B%!t(P&J&-J;cM z-8LSNzW7Sxr)U}(c7f-vNrGPardIpvp%;fIzHejcPsS`WFt$QV2kO#O3@zyRoV`^s z(l+&@vjIEt*vjK-hp*!{-e99sjH@GKdSB)~4Gi}HqS~m|8r}^*(7EmYe*gFl_s%+1 zGB41RR*FiJcz}GM9@cni?!3W5(nC^@A6_QZ6*`q!x5(b`jbz;UU$}w z)aye^D36-kn!bll11@iWT#)LCW9q4NIfxkl!QiU zbUTS{QU}Fm!KEiG`|oUCJ6}Pvh8RT%+feRc@&wU>s~tO+YRc_6)0hHeW@$BJVhGjH zVwRThL7{YtWC2k_L{+l_d{(?`K{p+(soRI?<}rFWPc^Ivq%#ZD+WG3Kt0waf@yMU&TF;w(^m+ zAai$pS6#g;C^V{18-&Wi#PQ;P|H-Xi+~Z;aoqg{;J>LGc`+Mtm_Xodv<537lMdAhn zC6Oa#x;X@ecY(4$;o=jPavYu88^<_E_eIXSdmJc{v=a;2NzLYyfo_u`>15tU@uC)F zwt;#$Byb%e7%(}KB{;SaQ_lp1(|c`uHP$;*cS2OuoQMc^Qcw(pw4KiC9f%+~v$gw~ z*LFH@R9>iSOIeoja5uy#B`W&(E{Ko9;?#X=)!DFc#Wx#*>pI$$O5D4VB!AdDvpoPj6`SN8ZTa#J+F)d+|eBRat(9`$l4Ro5fQ^2Hvd8S`Ixi8P((b9oL#Rdojq?+F# zLeoAjy>MwFjBWgc4RQ7|to=t%UK;<{hre*{dAJH}p+Qw@tsGKl5AJZ`rf~&dol%KV4Ls*!8Pwh*?0+mD*O4pGW-u zMg0n9Hm8{iz@}-I_YPaz*%aM3o>)~?QC1~MNM;U&ks#Q(!;3mh-1zm}Gv^26m;2i{ zcWm3FUOT|~TMqYh1A-o~3am7sS!w_?1xxGG=Z2rWX*qpOs^nl?%NJ*PWf*iy)^#(Gv{`%@GW|MQvtI3@z<^p{Lw|)tnRiDC4{1; z|6J7{ZLuO+=6<+p!J47Cgs*qNTO?`4&|O(}8$$;lgmMJShMAp@QrBqEvq+D(+ozHC z5wt!)e%-Ao{p7s83gpXk8;AhtR)%PUuPl@8y0IpTloRhQyAyv^u{j(~PWQU)?<$Lm zcDu!uE4#6vP7{pr(T|roRehRTIp?UWnxZJEYm4`Bo={a4Rh7)X);dpF)-xNP5W+LS zsb|j*>fg_6^31ti*Q8-%;KD73duaoTRbZ_F%o5NYR<834PhPqIFWj;AS3!=r0GW!C z->DAKdSPrC|Kd}FfAGai#VKGHxB{F5&OUQ)x8Pg&`sPLukz@0dAEMYv>o`m*%H!KF zhNDWeGCka}exLIr!tPT9J3taMT~*7@S%>$LnX7zYyxpW`VLtx|Ga{lwrLk!vlvEc_ zp;zuC*?*K|=>%>voYf%tl8TCtsgG@^4M9=$3(8UCd+9`?sLH`-pBQZ}{;E}N2X$@F zOCpDgqM+N!*xKr|zOsZdc1Bv9dhH4giP1F?#9Dla?uW5!9?Mg(?3AwiGml(;{}0~0eD~|u((hZ&WWzMk&;?<; z@SpkB=LSFb*jD*`6Q#DM>E1Ne+XKiewtLOddA~29TRDirj>i29kA?eHA`b=6qLD6> z9cZn^{K@wp1vf@Fyd__k3@)u1_ z@GXp=|MKjqW?eH=8|hYxm=rslQ0%%`ZzwQ%92gc=U0v$im$WN)2BRl`9+ zIZc0eNGnSk+=XfR-|U*17UzxTqS2DiqK0)S<>IigsBk{69%#4n(}Th8$NxjWTupQt z0E1f&_s%w{SR3el?Ae{CKK3jhzJ4j`z3o8jHHi{?@^bOS*ZSp!CWRXT#kXQ1{w)g- z(dk{m+bDKQ6=Do-QV?8tUpbcdDh)w}nd@O>+D^yg9_oISnzmaFKpcb3dHo?eMvY=1 z)0MV(!w6mrj5zO*3zIIeQHQxC+ zH5M>wY~8+ix!8KyVcWPk2mDIAmEV|Z{YH}{42BhYbqUpwC9^DNWp#xRJY`u?6a__D zHX8JB)fTBJRVQg_T<&8> zWt8zYQv#L6AN=KKCg)ypB0VY2jVHyqTzFl8)Ca-aiv9am@glU7xRL3?#VsyfzCs9r zEKA}(!BEd97{obAT?a02?M9jGN|!uK2<7DL?`1mv2LPIiZ?o&0E4ek;kF{UkDk(ico!`$?F9>Mf% zbF6Qe!`eXbV@6;31+obs$zZL8g*S$mL*x1aA5xmRaH?;N_Ka4smcm3iX_o!tr!d^ zOv)MvtgrUaTCtxA?^v&nKK%lkz3bHrkl`1;e*;_MQRucJI8U(Z{@$N>s(WKc

8t>XWc4tv7<2ZKi&WzZTWPX(BrobzqjbP|%bok$8^Bjy>39E&M_1qGCGhNa#ie0P z$*Zd3{J9HkU+L5Cv}Yv5$z+1FZZ=%2LuARuluXD{L!Kw(MtH;Wgg<;BaIoFF{mlp3 zuL1e;vloWd-Z6g_0z`DS5l~Sh(N3`aK_8WT2ta9$GK(HETW1hFc0>f=#=|EGvOtrSnG%vUsS1YtxRKCT!B0q2lG0Uj-8mJci=bmG5 zVRyD@Q=2$j(a|x1Vz=g|1Nou%99=&A@sm3*NwscWO_%Q2mu-}Ie)Ny5^NG_VzVzal zVY7UvC<=<=kS!q91w0;6>eEi=@|HH%k3Ze;eRvvjiv*& zcfHyG5=;lALYcU%()-E%y<=bK$J&6qro%jZw7#0JVDl)Q6T}gMiyv1h!>vFu40P9J zCa;}2ASxL51HD60x;nFSm!#R4TTm4-b7O&cF4~Z;--M(+bmuxIFMf_{^Ym;RtWwO{ zzLKQ)+EPs*Z6*B3_a1!P%K_wF*LMyPpp__&UZ2zH32)xWIlE(dezWF$zh-k#Q`whx zKyTQevD7xa?)o-I)>Dr35)LmLmRrJdOX%i+_qaN6Serlf;D56I-tYd0!+)`N%wLT) zh*<(vqEd~o$9>WHhh}MwZaP*pQKXUE1{DkIa_GpGTG2@`k45|Lxr$H=-U=pXrYo5O zW!$f^bsDGP)A|KN(9uJ5yi<^*fmUx`%bS(!k^C^abA;;32(afj$y59SZZmEiB9ZVH6pOM2QKO?ypN6)Dxj)Z8y#NJKlAk8y7^ab{kz|q zu7!I5`FdBn58MdJy12=64=(-TU+vGkOB|ENlE=@@-a% z%aJGPsJU-dvnfXj47V)HH!R1(K17;|(u&|G1n0T*oad&u=@~-OBj1f)el2$Uaq4pq zVN#9OhE~r}O_*(aQfU^a@rA=yfow@5O86^3c-?pX)}y=UF7M1|A}ym*oj}@&5v$Wy z+`2E}7N@D5PF>@Qri^Er6I$+W{4ZFHugRx~Ih%g%>;fCu-E`v1#D z>o@HuXZs=DPta>%y7ymIpBUAs_hLs^nK(~uDcwJ}d@4fHHI~RYq=XdI@bSY}vm2M(jLxIUt&2q=E z+%@ExYD}n^(cB4FHQ|_(3K|{11y_Kb2)=H<&+}t@$NW_Y5J~r+miCPV)jiq0^B-T8 zhUiB7txPm~(>ng!FJNBhY4eX^7|uh;kBv5)WYGgrZ|=HMl%Pl4Zp7 zK-wtN#k-WWlha-nE_}^19_A#s{s{K$$M7%w0!jx&JR$*QG%E)TFFa-OUV}=}tq#6c z*uuv)u`5v~4i4{n$NFvm%{}{XoBnRkj>;{WZgldEPNIzZud7%CvsuSNWn#-!H~TmwiR;&N z5|wJ&fGr&8B{0jNmgaZlCfXE(X3wFXK4MKWzHk9AT_jlrS;B`Nl626irn3@Lv&}2F zQa=7O`18Mh)rkzwMdP+^i}iyl+6!eJ#I&%JfGuluB8WJ^q70;+jQ{F~u6yUZ-qc+O zc=twoqcQcsRbY#NEko30+s53$Metk&jF)S4G)54v_SiB|k3H4UQ;h=k1nMH{y(0kg{BQaxLq6T3^Xb}7g6 zkBMriC*CnUdj`^Vf>}k@%X-GSUrF}yG)FXWDH@}LrSKqpWwXrRoW*mHNLcZKV zwN`O1r60=ywlesPd)L}MUOJ zbkZ?`#O+Rktt{uh_&lU*VAcrexK(QLp@h1O2V@#Wrmfk@g;Q87gk&9(LrB_*DN$}% zT902>i~{FRO+~t~)(8l$wxASiH!hJbW$2~D_&Vj{sofbkVu}dC7KBitjj*zRDgEW2 zy7>o}+j_~@0b2#?B3f^j14lVFBj6#O1n`zE49n!69a@sn~h6FF}^+XK($ zj%QFnQ9@NjlkU2NK@q;Pchp~n0O_pW^|IX+rRAj7^MY4tV*QZ_h@%{Pg4<4oUX0`L!K3PZvD@Wk=m8AE!VG^#t!G=u~s_YuDBf{?VKe1Xnf@q= zqVJgOIEK59@vfsBSgMhw9$Rc-v8BaU4%a+vVX+g39Xslg<>D33`CZs5h3!hwuZ7)O z7+5*|7k^{xxAu7CN&?CRd|hBCBSdN1*DrJM-7SI_Y!M4|TSHj~ zc6J??H!b6B%VawWv5Fl_v0F#L3~S0k{C9lR@pD6q3ZBb_aB-rzR0x}eVyjU7K`lS@ z%=tmsJL<2tImp+oP2}{{;5ZtrshRJsEo+>Mu8E`PK1Vh9v!Jua5|oCUHJ^cq7V=)3 z$zVcvt#MmyY7%?lvlL&uu34ktP%6Q+a#jyj47S3|WO;C@;)2#(cl#Qk@Ii21qgx$D z7oVmYjbqelqS$D+#ptUg2X6{=*BbM*+ENb&m~NNO4J#b}lbUm%95dN=*dUY+3MUL| z2-Xrx;(m=x(CPenPvyAYHim3AG%ej|h){bNSwX`KndZGe`q8hQ+&kvqZU8}|fWPrD z(5Y^y=R!HOF?9(--W4wV&c7tzcT(yssM%ewS&tJ>sk1mICq;8~x*5t{ zew4vrb6$imfF~{%}(2>T%3HzNYeFAK=uL;^!aT zbf5Ul*`41OSRC>7L5M{i6&%@kLrbKb&`P;XVC`CwDYB>JI#S$ zhYV>;X5g6ex`Ov?o_dkceIq0**t&)49O9<$ z`D3%MQyOep;;ls~$l4j+ROB0}A?gR?fIxirp)%;ymeh=QwTvz4rqLVy<^ zbI}=IhZ)_&+q$_mD@>=&`VIOjaK!Y@ldoUL<%fTrGoStfE zlfab)wyHoyhHr0u>Dbok;SXPYYV+=(^Y@`UCxT1yWy&pYzm32B)Azjl`~UPC?M`8z z{_L62zdJt`E{ugs6UAmJT&aZZDt4)+kIjNBTP=f&h}9KqbvU zBo+d}b3!yHbrT(?tyE*6wG{X1Oxw8A&&MrBHG)GKA?u`MomdY%_lR%oFz19oR~9jf zq&X{xTTK*+;7|&@t#P|&k@_;)Z*umxf02uieuc7t1MmFvtls&qSric^xT>NWk3piS zs=KyyG|BWMSDrrIUwPN@yV0GuV7d=FoAx{1)%E}JZ~le9{;_}ax88HgC;$4?=J=m% zl?*RTgtH^XnUUh`NO5kYI5(2V&X4&UPj1<}-v956^Rd zr~ljU{&9l;UPr8_%J)2c_Y42x&gTeh%=vAR+L5<~BeyH^?tEgN^p;U+iuZx-XLs0oVKP7TP17J-Y5@UXmDGi$ znB;VCeHW{5xSI>V^|RF5XXbMp6;r=4fBbVlarPJPd;qw25g?}1AvjMxE~qDsx70$hE()?v zPTI-iq+!Z=4LKbvo!a=p4}t&LRn)nARaCnb#a9H?)@9>~03CBGT5Q znOpoCR~H0dqYSiG+N>QG+RM=l;@nd<4)|08A!<%g8G3n@wVMxK1r8Cw*D)QFfXOpD z>r1R$m#}t3k#$}k95LN zp*?Wy)gZ6@~EI5?bp^N7LobL;c@)<3rmYzw@5Gqqzr=Z=C;` z&iy1kM0Xo%dl!HM_dMQLCLGs`5kz@R5-mVS{YV#o{>zr_^ZqK9tx_RciegE-IcZyS z{hbPDo5A74a_+G!>|QPiF2PL{s(pavTi;5u)SH20J~TY40Hr>WX@Ihk7^wgmi7?ShBqIk=+w-dv5#J-u&a+zy6k$y<@ruke9uB zZ~ytzlJ-sn>44dXBun!ruKB3ak?W*PK&Ld^r#atBY2o6to);gBW9Aq|0-`;3JODiD za*wQ|*ngui+zyfA9y~iQY;yUD=P7qVYbXFUnEPzAJYL!5+7CRhKZuaq|MdkTMx_|A1Nw2rP`@6?J(m(M%dr@Vt zi}liL_wm1TFWx?obXTbRFMgnR^n*V!{oMA)@7yN3jid7xI?D*o^3{*;Ft`%t^9zZG z^EGc&^bRbjDkRQ3RAQ+51FoF$oOvvYp4!Vo))BOZ_MsbDzVmxXR&HhZ%n3%Pemx%Y z#TrA!kG3ZtYm3NSInIN4$ zv`+Y{`Y1{FMDI=i+v&X{{2kiGx(Xy;`-jh&ytiQoFMgnR``=yw$QwQg{5XKlGIXm& zaF!>3yU)(0FsGBqe0k3D0pZB&Rdh{k4y5U{PauY1SRT6dX#;hmTXo6P>20G>b&T+TkkK57wUq*T)r)g4_kN0cv9k z4p9lHjG(jFq|hBydmpNGD5jk0%1O!gp9smyqt2Bl_X_&|@BlHdir+F@f4|qAk4T3% zUy~u;KLv=l4&CHGofXAk%Qews&bykrEI#wFWB-ks)x)u%uoH{&nsj|1>sf)AjB}6e z08keJSHombFxq~I?D2SFm$t$oa?4M+ZldSt=EkjS7=^lGKspkhY{L#IFIz;@i3IcOg0?NY+7REmYbA z)dAfFvXepb6JC->NqQ%G8$Ysl`1ew*Z;)nfeR4zT{Gsm3JFe1KZ~x{W*#Mq{fN8hT zdBWyXTb%yV_!=G+GiGiygi!JDE1|WV(mT{6U+W<%LAQEH(q+8a=j_8z;L6GTdljVJ zoFp?1@Zx?zr99r2l!YTnBNadENVDy=k?pDla7Yp@R8@x3Ip`LmTXFY$x`JfuNPd8z zb4XS}r6;^AKHOV>_k(-Keh(loA4Rr5bMMmXcfI6*^dGwW0SNc1M3XGF7+u`$Kl$62 z5XFkRmTcI(pZD5vtCOt>E;4?dPgs7<5qj4jLg!1U);g{ZTzL3Xj4!+}pO7m^J9%7~ zqd;qgX~>YpXiWcn6|GF0`A6PS=v2^&BI_ie6iRDEX-sDcowg92MO#$w5R)QFhBAgw zT>;by-+%IcP$yU4^X0v0vImgg-?jZae`*7fM+kl;TkYZ|6Tb4%uf0Vn9#V=Glw8H7 z5)}_?-fK4pJi48f;|aT$qkd%R`Xemea5GD{97W{^=s)#Ew!i$VU?($>nJgh`r1FMCT`aZ~fW+9whbv^0L>?Xa3Wl0PfT61ZAMw9^8HEH=pW@l82Ozkz^V{ z77uvZ%`jSFjKbN9@y?jtt@t<58g!P^K5{FqgSV0N4shjj{~X+SPJ&DnX(uPik_eEg zzk>?6x@5FfFz&}KS<|-glw~}n=T8AM|6D*N1`&m5gJ>OP!Ahgj7Gg5c89}#DrcIC~ z(EA|SPe}IRB|EKJhfg>={KU&e+&zGN2Lc3O_u;?%2#TZWN|)-&&ii|B|7Rci(of!g zR1^=1k`;ATEfU`*?ToCQVVc8T6$PU!1;gzyuRHWL1-{~{E|yYp{XuJ~jZUNaoN6|Z z3jtS6DEcLnLG;6lV0amDXyV{gUt8!;2QCItDd?P_bCl_T>Ba%7>7687JxR9jQOWk7 zSUUK9doABRfLsHRFaLK(K|f?Vot0qSiO%c(&fNfy{`4IiO7js>99*=bM`a7D?^`GU>oJ)`Y_N#mSl znr1ESlcp0Uv)4eH_M0e;trgx$T+6W3eXzBD@WW!-{odPuZVwE50Qo(FdaiOHY^dF9dck*#*|GT*Uy^26Sdfv*;MFQ$?D^ zn#a4kAA&tjaGy{{PTq9yV|zf^1IX_IBrkmU0h8~)*Ogb^w{*jwzbXfR>Syn`M{6Ds z&5F7veM(ws+&j{2t+nGJ!Py7D{G0aCR!{EByRy<+5=@g4scXS;4ZS@M5aSxR=`?*F zf})$es{^49Y@eyvxll6RuCQetjqau?SfWX@SiGAC943jT-X;+cj}MI>unH%FuTLw@ z^U82i>(EDp)%QGh;v4?mzVug)k*q+rZ$&TPc2x4i%bu{}?c@W!^>?4%1IV`tKmdkY zpLjs&_C|Z<9d}>*xo7|B9V<%92bJdDtGQIoVO(m*6iEf_WP~s42VeM#zkhc#+@}=x zq%CMI33<2CeyF)M&%92V))Lb?qDh-ff@)|dl4htRjT#h{;9W{^8PzB-ytKpQ$}Z(# zL_O@!z^D_6wEoJqbBd^ppgX8+4N3Qd?m@Cn zP#r?jI~`2#0|7bSTYqX9=_t;@65lt^O#cHr!>dpn)JzZT9{TA zQ=+24jwUBTe&~r$)*D)LpJ+KY1w+~r(iY7_NSpm3SCbH%f6pfB^MRqt+mPg_q=O{O zsB8`Na&)fO%XpPgY;94G2iVaj)uqR%Hcu^BsR~Iex=$u8({#fGo$ISOVbh--yu|f$ zI>(0$(Om*Lm~NkBfMXj{)_lxYi?U)U}j>WuQsiSOyO&~VM>Ff zh)zMb2)fl*rgL0O_oKbH{~yQqYLM?Rfc$>0?SFsAF@*a-jxDA}I?vJFHlhu7T;U6U z9F@Fp4Zj)(kc+SI*SzUh-Ed5TnRz zL2R|~m<}6tviSaj-b61(uxSJ_r%}mCQQ0TZ`H7Xge(l@3u-^m79|TZ7{)P?UK2Y}{ zvNBDHbgPZ&EddGj*!R7a_pQF`_;G+I{?Q#PqPRzC-m5kDOpUj%J=D_!tW$$ZQbZe+ zik_k&1nP0k?!}tX700+=HLKoaVYoO4p`I-odf8#)X;GX8ISH}@asp8&SHJhs{&&)A zxd)IxNTB@M(G^79gP8Y!;CZdDp2j!nRbOjP zHj(4>fqNc5&EB;KkpDlg{)z8dK_&MDmEEOu>mGD#MUpnEeF#a{kaYFIpxYnlT=(9W z9L@{BeCLKz;i%GbRGDy>2uDFyu2m3Er%CspAl>(< zMV|yY`D*Sk?E&Q5_}cx_e|;3wJ&NhRnY4FQvVBLTb!a81*6|SB{k`>ff5R@&_Q&6F z4Acsujv@xp$z7-nR2J#)rj3{mk}e^weQ|Ymwjaq3oJ6;Gg15)JEAKeD_iB6-0J3-O zU3=HJxU0$DwRi0SWbfL$_5iYX?Ol5S*}L|xZ@cUN0f@{$@LeJrmH+?%07*qoM6N<$ Ef-79Yw*UYD literal 45469 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_16x16.png index 97d5925fe99bcdc13edf13f3fdd5be78ac7c4741..bfb05225deb708389d1f6028936185ad042130af 100644 GIT binary patch delta 464 zcmV;>0Wbc>1?dBjBYy#~NklKHl{sKRah@%4Dy~X ze2CidX~}_^GnwT)011!dauf_myzaCjo^*16WD*1v&}%it5#Xb2S={^uMQC8ENm4{1 zQ9wX}eB?Wj@P9a!wNew-9|6H^c!Z%K$#44$-+`MMJEW%Vwj*Er3kBv(di2#)E2|P8 z_XjKzM!k+@+Jui7w>97w%0G)_LdZ0Hx7f>ve0;v=+mK8%g#iWnVQah*PeT?SOpn>{ zB2b4{ef78)kTL3VVWhRgDqchZC=L*Qvy`D*#63n_>q$0Ym#z$h+6pm8n=-#P5E1d* zuaR;?S_y~C4H&65mkt!)fS%Rj`HdlFt!31X$m9bh5rI#}!v3-ARB=WC0000LN;oFz6!Cyoi!63U8uD z7a`qP5nbe}U08aj{SZbMK|!J=i9a;4a?Un4GTqwd=9>FD-+y;B zectzb4@U^W|3D;z`@kg$Qt>tLBJgQ0g!=P-R2S=!5OhGU7DXB!Bk#s>rrUw`))pk4 zmG~){h_2}+C;m;niQPkgXO2o*IZ+%2_lj0y7xv!&vyc(Oz_KDO#t3gla zB!=G$Xhs$R?|(w=269zd8IE+_XYoz_Zj6LusFAE{58}~W)v96+Yrs00lFH9rK#8@S zt^KW~;LRe=_8W1#K^4BCF9FwIxpTB{%v(h_x_gTu5^(a)JxD!GX%{I^5*DWi zp37F`*BKO%C6<;y_F<;lR=5j$B^jqr%px!xhG%9nO@B6D{gM`OpE5Hc3hIP{r>{JD zg)OMa^5;lK{WK#*XQF~V)CEO6WK%1jZnGdoAkR)nuIZ_$ejZ=(R-?l}yNOw4tuMWV z&DKV>X#$TxZwJE9A3}D_a^j<>Y!o@WZ+y#PC8*tMU`<^o(@ftVTt_^)iol`|ksm>X zuN_6>z<)A^918J@ydz^^UL=BOJ7mQ6)Cwv#=@5;sBJ@oFpXBjGEWo@sf;Co0y9MJ4lY4MR+A-$kZ9KP$kz&Rjz(Z40n`-~DJNonm&#)T8DyKrz+CLt7K(@g zuiXKX!=*Tx&w(jdw_!BXCD?s#rnugNp~WC)-za9u_N%gUA);QV_~+(7;;i^3zyLsy V;Wj7QlYRgI002ovPDHLkV1l6lLo@&Y diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_32x32.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_32x32.png index 8afc795135050640ee3d060ab0fecaa41f52af9f..ff2f3c886282c73de4c9855fe1a026c5a9ee1ec5 100644 GIT binary patch delta 2494 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 IM6N<$g4SQ-xBvhE delta 2683 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-completedstudies-32x32.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-completedstudies-32x32.png index e91a61fe585ab4956f6b68b1d2f0203000697ea6..e2e5014751aaf9b5087bed99736223d8a268a221 100644 GIT binary patch delta 1541 zcmV+g2KxE+4VDa$BYy?mNklVn`XBU=G-?aPlwFqHWfvA*JmLkg zE2y0I0J|KOvLFYgunM|}fL+T`y-t}&tiVbdt8>G6;;a~uPouWRUA(pKB7RRbbu@G zaPOCzY_l}k?SIndbjruM4r#@4hr=P=P+wl2bmF|n4#@m`>C4TP%NYA{9>et`I3B_{ zUQi&{aP1VvX=tWkp8~NXKsx|VW@Sl(-6p3Iu+e6dMypNQ0n&p9_ClXf2r__Rgc#&J zphjFSx$JOC47%DzQ!j@KEEtOYyN=DRaDC7fuO8n z{yxV0n17q)raN>v0TIWzY%~Q&Gt%W0%CMe$0IIWDC0>3)F4SeoaI4FZc^m3QF-?BT|?rvcj1t>G3msTkrYJS>i~WL z_^no?!mfbKz}{y+-H})Sy)YzRO8{=nUTos4aDQRZV=x6r($fKfjR^ZfXi8PMX&eqb z{qeTE`uDkf`_*DX$>-V}uf2!;z>Rqk4qO2Mo0Xvyl$o*=2MADUu_#WAl$FcV$1C#c zpFa$V*Un+zXw?x#ADK481hfFL zI)4^w{IZBtt)dEUl$Xn(yHM#9JxJpK%9m~1Pzs8Wg6eFG64-~;(}lz_<^veD67}OZ zrDLL7w%F{_3b9Xgsck!|_K5);-}IC!0M6(;X__`UaCX02D&asas{ldklEgg`!ovv> z#OA8U>Y6amX2o|$vyJZ!=Z0oQh+f=tOMi7>TYydgR0ALcfB@1!h*|^*AebIPD^WTT z#_tVKE!-dk+jbkS(?c8xe89lsuHLmZfIbDqAErwXR&!+ykYC*gRE>JPUL6(%B!u8> zR$ANEAR_7utWXUeeI8OqXey-xUD$*{rU0F=0XB!RazZ4hgRUey(WJI*LY%=!2!EGv zPa=5y@8(Sqn*h!s1!0p}&LD6Y`Kr&cDo)VmkS;j$Pd94OB{>qJ7IWN~&A5gt(5-yn zuRuaNh^e&_xGO z-w^~$c0voT*~xP{k*8aE?XRv4VsJxM5JU>Pkpi~Q)2N2C_}YF1YWpbpwQc zUM~he*7VWNec-a&EgkqK9E8pM(CkLQZhj5v(xC;Z`oWZt29Z2ZF*x_c^?yrJVR?PfBUQ`FY2(9V;>5z$o5&SJB}nvC$^cNJg=%FJaXh#Ce=| zVPra7{&gqHzJ7m`7(CavOMcmytPobm*bXU3A1#6E^*&=i=z00000NkvXXu0mjfcF5w2 delta 1766 zcmVId$}CA6NAFo)af7UYv|J#8wSB)%*O;j)TV!XC8<3ABQh=pc;&} zsSp9BRPxgU2QL-Mr2$jZC~26)^<#JjPL7TKx(0#a;o+4`CPSMyZ>G=Rev8^tozxMJ zlVayk#1%NJDSt&SYC}`L!To}~ETSld9+r(y)MvN+WkH=8gQ0tnEvt5^xXsbC6+CqmEwRd&V z#Pl=)fw}{0FlKkEj!Q1yY&chO9jb~GSOkl^=(n(VOn-my&WJb|Z@?JO<*}F|-@IO~ zTBWjU%bKg7bt(8@aFF7LK{19CkTZmA+`;@B(1_!msc`%r((&GEn)qfXm7v6ZuLvzOJ?wR(+RZ%hYe|rCZZe5|E5s z&GjBlfzjnt5I6h)Vk9(H0MK~xwRdRi&V5aZeShZ9>vaG7DS^LiJCuki#FX}E3Y3bd z0TL7ATmbFXr}xtCXAd=(ZkPs|l6WZsL4#G}HA%toOh%O34qn-zVznlk_8>_@Ie+1D zeSu@I9%=Xv+jy)N$kx~X8Y!>((dqXHhqisgxAyCxcD`0p-e>D#Zqp>*4MNxjh&9lmo`<*dPZV5L}~xrMCk zJP+qvfP$x}ISSrKy-91OxM>LVCFBDt7HI*pvXSXb@X6_yjTcygNJ2IOjq?TThS76@ zf+Y!yBoM>1f}bN_ZrzxIL{~R;LIq|_6MqAoPPtIxyKd{9a?p@INbEzrNA`v#a29gF%2k9m#N$MWJXkgCBc4qS0CIEX@8ul8r@ z()Jm$TC4_pT9ksJvuEkEk*{gFR3^uEDWUtja!`OskFUF$Bw^2m7Nt{=JbZgaqFfonqSJ{Ta)N@0b3|1w?XE(8)%AD z5Rh5~J&pzLH?hVqeRJgOD2egTZjqO(&>tJOzdLf%!W)S3AmLe6Bj%fbg}EafVRfUw`*TWP(>q z^Yv^50f^YA7SX+u$c{yfvk|T*pUFvJjw9Mb<1|IF{u92Yi`;aZ~wy8q4o?LX5e!YQ5Z=z9PoCj<&OLH5I4+>!}7A zKlCg(2^H|YKMmueEAY=du{z4cMK? zb?>1R?C9wsmsP+A2(noTIxq9JoaKnIHN{dSk>+Us0y(`U+H%NPv+$iUT*Ln|Y5yPH zNYRd*8U5H?ZWRU5vml?g;?#lk+qrFY-t|y?*qH=rhnR!Bh5oyH((~XcXUs) zMskGys zM_#k=E|n5Y;D3kXUpV0k2@gztqOaqluhj}PgWua3XNf2R(iWJjFTt?mDHG@*3G3sl zjB5hs8;qL*76VCGEw@GPkVMmydlK$TB-|=1&{YH8;a4oYJtz^MO2j1a68zuW;w<4( z+k%TQJYZo0tt4SJ=^0lDl#pu&N?0woBH9ru;Z$3JYJXON7otFG94j#xt3XSfB?e<@ zfmsxYr-WlV6^N(A!1M&HrprP8Pr|OY1?3?v5Q1BR0)k9s@a<4qtmGtAR-jf~2jnX7 zf9+7e7vM@rAuS3_PXYzJhBX>r^)^s$K~06Q7vRIEDe>~;!3aQ=Tb$=v);TXt4frQz!!X=ah9)s`8J7JPl zNEmX~(F0Ke0}f>+1%r7g3UE+Blu&4a@d}K%p0iiTMiR8h@a*6wdHKHA*8OuW2f#C=b$FR?3n`jWfc!p>; zFdXkudpsxvh-)zDj9-{<*aQ>oXnskNI+*GL8kOu4L&Vp^)M1G%!-9tqDZ-`4J zonY}0iKiIa$CV|*E5IfA=K`%3b|lY(oheCBoR$_{0524X1s=q|=M|9tOv;(kWBXCy pM0y4hA;7M@SipzI_&Zvuz+VzPdhjZg86f}w002ovPDHLkV1kA-*{}cr delta 1347 zcmV-J1-$x@2-OOZBYyxHbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU$ z_en%SRCwC7S9?rbRT%%>mUqjeP-q#BA&e=TXeMQ1MzNi7n~D2pxJ46;{DBWNn#@Nu zX4AMO8!^h3`2aJtUIJ`KB#FTCylX zf(E@ykpo16L6rnii%P&K7?k_00D@!4#ix>Dfg$r5S-N28Eh<5iC_%g^K`?M#WgF1e z*%cxOm7>$DS~V|62#D|`NFoxX2{8~7(MyqoWPxueTnUssays-zD#w43V3|S4b)dSs zISQLCn`!2qwu! z?`TWV(L7HAF5(pR>^~IphI99u!up8w^M`xzRDZdS1XgP*Y*d1H8o7eLN(oeSg~;JK z@G)AiT-7A^^_MxE+S>>3pVK(<&p5NV1Y#)_g0MRzCY0tXErzG&lMv?*78GDxZ6oa; z&b8X@cq4sl{@!-F(TQ#md;**`OedP`0-MyBM zZ+|{l07JGB=8a}FRE-3eswqK6I&A4_LKf&19?@E#eH%yHh5i5Y`aBp?jQH?SG%7wj zio~X!@;tVG4mrpFK>xe>N)U|BiHM=6WLhZo(o8h>7d6ob@;v0=8Fe{}6a)wzi)j*Lgn1ss$n{y( zQur0~Rx?{5NPY4f+ z({~&FQ$zSM5QVd^+9}n%Md$B9V(xZf#YO{eHot|5^QV#7(5sULlx`!Hf#U=j+J8E` zWeNDwE5*dV`*vW@ucv{>+<27SEnU5Z{(|J3U+IlwA7J{<42EtEVuDaLvA-PK&kp0K z2AwSE`k{qn!M(8CNEWbj5azI=46G^`_pN(U1^wG;AD-WuiY)dH3KQbs_p=xs4!{rq zTB0)W_rL_^3^AF%4fWm7-3zStRe!{}Ron~Mh(PcFc#Ag%kYi?%ofLz#SR;=A>_cjF zG&Wl7)SWa>DZ#{DiHZKgb@?z^Rwx`1+6_bw3WAq|rGY85AkNEG3~Y%@p&-hl$-k-@ zQj)I?BZiqlW^63dW8xRw4QV?z*vUomNAIKJ3ZcYyHPLo+;S;p|?w7CSXFN)asoM6; zY{R*x(Yb&&0AfXv0C+EJRrmV?eU1oT=^^h-N&x;9U;q?qgq>=O+>8JK002ovPDHLk FV1o5KmLdQE diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-generic-32x32.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-generic-32x32.png index 6f01eeecc63d029f160963bc490590b1701596bc..31f80b17ec0848b3c31e91aad1b46aaa8fa5b88f 100644 GIT binary patch delta 1504 zcmV<61t0ps4Dkz)BYy?BNkln0`GfVSuEgIbczS%x2l7mpmVo3WTp zgBdU@l$6+Qw_PZWIk*d!N=t2t@dd_LNhRtkhPx_kjMbLx&v_8!DikcSrYG zRZh0GL?YH%Sbu1r<`-B8{qBMS>jUR^@3tPU`y-Itv&YWo=i4RPL9R!cKTiKL?akt1 zyT;sGw6kEYc&|fZQvzKOIG&klO_8vjX2Irg*qZag)&-G%UN`_g6If)J#YAF6@LTXvMux?6bAPQ30#ZUGx(nGkkP;#xC5CwE z1((Dqi(dhwj8D*B10IPno{S?7biaI`^#+sD(f?i?;%iIQPK;n<^`_bR|fhw&P_d$Piw)YRGo z#-GtXV}EQxL3i-bI*BRT)u0x9n2~NLNW(_O0H_b=*;M%__IZ7#jdc_Sg62Lve!?!` z|0Ift#9~P-m;&Y5SyscM2^JTLM1I7kD^A&W4~Oj4PmcpZb04FS7no9FC5Q#d=o*1I z1mbylSP^lE%pvcqA0OG9zkd$|%@qL!<^Vx_6@L|$V*xEVoSqH|0ulKdC`zYz=q3uh z{NcX6`Rh0P_Uq+!q+WYtn){S^P+(p_fh!PDu?l2CSyrZ_fCyBCLXHySW##tr`+N50 z&tC&UbMF#2UUm#oM-~GS*h>~dT?^wX zR{D0dhV|Vj{74d8f*}WIu@y6F+T|}MWsZ7`%Rz@HAF&X zyOVjTB<6!!aP<9jt3_(;3I1sAb{#tf~93l-Yu43htZI49K_W?W0J$=SERiKw3 ztd#{+!X{)+uyc_}wL_|;D$(MWZINiF6^V8%=@S9Q=7MCwSu99oW%FRz5=?H)&3|*0 zkjxFf#pS=$sG~}%6^S~=6qv2d5j-Unn7#C8V*xEVo}OWCOw@xqkhB5Mj(W-cmcSG# zPl;y6{C0HkyuPB-K`iL!~5ML?YoTQHw1e3GLn^p@ME+N-8g+d4qx1b#h28n|yWb;VKj&9WGRBD?lToa4HeR(2@8TDg}Rn37&`ph_tv zbS)(&H?bg1H|IG>eo4AKvij%bHDF==bQ@4 zvJ}5vI&oCRyQ{0K8|`*m4j(=&Uw-(3H0S1J zwo(zg#yzxeNPh@t4US1@QM1nieO6#k!)WszZ-l=%2xYy~k-KYaHzpo$N8r;FCpIce zi*oPLBSj#KA}I}CFAWTa>Mcs>p14W6?qzn{n zj5$!t>Q&qa>BVrQx46~$y}|0F$Rv<_3+pO#LX;|9Go+5=*+{74H;n{ z4EpLs5=V;HR1}3 z4Um``=MHF(JbzqXdi9Oz(hX^l+Zr!DAec}!<&+ehYqwRoGvJjD^+sdWbO}ic_N&)^ z*_`0)d+%(!_V+k8bh1sOSdV+gz;+5UKxZG(IFri&fo276xwU zAAH%L+@8>dhc$Hi4;{3HF+R9;)&z#>5CctX29SC@99joqXn^a&$(ZtIW)+!|f(!4q zv4TjdpulKxDkP5WP)al9Iab90tpR@elp{Mg@0;= zbsCB*#9%e#+ql)F&Jlyj=%Bbn#h>vIu@1l?1|w?#V}ijv6$K2W7(~=HkPy|T+-yd+ zH5r5*lhY7`l*UPeq5--JSEA`KC0Mv{n2LhW+RKP^P{3w_9Ba|B2;{EP1wpXcvSo0V zMk!Y_u@FxOAkwlul?s}z1zA7_tbaw;uwQjAXB1sqdJY_{A)KjUGCEXotF-&pgmL7{txa4~mYbzKb!9y_=IJp& z3RW&(md~$Tk@fySN^8YG&@>v4eo~;)y1{|Vvj&1Xf}5!v7Mt~AAciz#(<~b znWZIZWhrWSg!ZeHx+Wp!fl?tR0wcC%4hc4;i8i2+9Fmucz!}RmAz76auA|=ZeuOSa48c=MenG4vI zG~mcMpcF-~bC-8maI%Tuj7rzW<44#nJ+VlzcRB8~vWm>2Y|bA&YNbU<;UsrOf++Z0 zfCQndFZD~ww(I_@DRlXva33B%0}eagy<_0l|NcFX!*_o_cyM5!$12W-?DlEKt%m)$ j9PD_VoT1r_GTCP=iBeu zUN&Tx-F=s4+L_PH&b!a^{J#4h&-1(smWwJD{Ga$Vb|V5JA%6;@Bu0*sJ0{`Uxc!#_ z+)2!;T{YO0+<}l>C8E!bAYN|3q1q{&=$JxwqY;uy754r#fNhy~z}m7ttS^8C(_9JQ z_qMneTaNyQ=nG?rt$2XX%1j6?G2){l13o;jM|9N$Vrz_uyJo_nhDk_UrjS;xL0HZ` zu)b^zR|AN>5`WqdetHN(#Uw(CClGv2ZzYNX%(kb_gp|eyNRW@hJF$(05c3UiAmGSt zL|jxOtb`TT%_*wNFsFDrD~GS@ur;%jg*f1B05K~bUaT{pX2DsVU7|`VW`~$APja0B z+jH-l&xE4@e(bb=dO=0kmkONGIwY#FAApIiF+g%%4}UiH^gUrKfIGWs!cGoiD;3}J zqsA$sa=l$q6?&w%PT=@2lh{=>Jbh!j+X@g9DEk$m7YzvdLE|*x6)H4`C<-8@;z!$! zX5X}%5KK=L_lQ0ez>Ph-A*Y5AbXv`eP@%Ia>WU5-ZB%@g{fe>adBSaG0D*T>2ezHp zBJh-omw!mP7Q$IX3N2(URIt_B@tY?29KH!|#G1Z8#OgOlUDMPAlEL|8_}n+?cl znOjJH54dUD55?oAh>zGvQ|k!L1#Z07^mOF#Nw8^=hU5knZ+2|w&*R`0FiQYq;P)f&FW_%S34h%Or7EP>>mX}%j##b+w~GE7z%PFY ze))g#wxf8QW;zXIH?&UMtx#D7Q2Kn^3*WpU-oB8lAZyU_^5x}Gt4pBplKqa&IfJ|f z6bvD`Rt;&r#%b}TLsmDTWJ78ve6j~z5^!P=Ni4*5jZ@-^daWKqf#?1fY&zEOGXJ~* zB!5+_Aibt`NFcpsH4)KNT#fhBy5ODF=Q97?ehM*ew|GkVFjmJ}PsQfN$n#)3yfS;| z#V@BHiB%M$PG!?4`CT>_BlB{+Dy9aWNuAi3aerQXv-^-(Ifis9IMcr3e!I)Dc|+n6 z*GQM*9yZGUoEKkutP?9J#MN=Lw8{~9C4aWs-H=$eWbf>4rV!oOApK)rHXpl>gz`}w zsUF40S$8a(Q%3>Jn^KRsCag>8!uoXOym(8y;rn%mc~ffZ9l`Fle9fK)tUY)O>m@xd zVSU*cwlXko&N&#SO{cRf7zwb4JjjrWr8VqI#t8T-zTvENu5)|T~QeQAu>$jii?EGAEq zg{Uszr^&PAc`i8%^Ck*Utmum~3bQ}<6nTQHWe)H+Gjd0LbYlG100000 LNkvXXu0mjf0wzgo delta 1343 zcmV-F1;F~i3DF9WBYyxHbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU$ z^GQTORCwCNS4(djRTw?<@-ucE=jAqO5;ruF+@>l;0@PK7KuskC>WZ#d(p8f`08%By zhF!x7Hb|_>3h1g8T~r`Ky8tCbZPQQ@6jiO;xUpl$GoHtt%YSz#lOZIwlL<(Sax@dq z%=evp?)MsL;)Cz;e;#UxXVZkUgfoOwgfT+SzgHwI5#|W9gxiGbe?_2&a8cFN&$9g) zboX{5o9#e4oj@!e!C)s~nNEIVqmeJK8|NOal<~)-RXDbDnclnh+vjhVwhW!TbZeJ! zX^$;97l~;%4u6mIW8g?Hv`h+B+eO3Xu<7i&9Jy{8nk*rvN{A~GQko2>rel8bIeuS! z0;^$95{a9w2*_=mPxTzi-Z=T{IC6zN*0eZQ)+|(N4jK&x#yB_!=nobVsah_c84QoA zHcDy~Cr%IJ?eoXcdzfrox;53xiPHbd`^|p;aBwvD>3>jR7e%Z2ByQ$fAZx#qgy}hhrpC@o*N%Vg{d%UW$g|@Kx%Q%B+{m&V054pnj*dN ztXM76&v)Bx!8wxsYVXkjlr2yo5ou57c4!~LCUdb$W=>2DK-d;OC?=W`=n(Y?0@q;AxLnyHN^cRDGJV%hQh=kgb#{|o@N9t=7##f(`hfl_gwfkpasO%ywoK_ zr7RIj96OrEAZ3*BwUaN3}i5M8?p__>(x1WdNJl3rQ|F=4lopJd6Bp@)x)yU?G!ZYxcr#QoC zR`t%;2HxEESkd|p9N4iv~65rg0y*i-g?R@SZ-OwvKkPf84P^W^Z zynlC53Q|wvyV`M)S$1C5w9x)jQ3(m^ga{#?%-Gfp5fE5nrj?fzH8g>x$0oX|>BP1D ztEajS`~fCN!#US9!`A?UZMyh*$$)P2mKAX)xM{#-6MqR# zv%oL+o}=`nJiRp`xBD)b(aY;;lJnz8X9jX4d~pf_j!67CUx8|G;OU>GkI4B`iVOdg1U$SOzQfLW_gi-m>#GMb?mA38?khGQ7ek{wWEv3)sq8V%U>4RT(m z*^*6)t!?{hw(!MUl7B5AG*|rdLVu?4&dYrh7v7C$hps5mm`6g^B2cJrNQ$!AkEphb zm~m~(bI!~LjJ1_3&lkS=u5$nSDm@|wLD2w24YqtRQlD>EfbTea2N_r+d=(#h{oCBB z_g;&pbEg$8QIM2KUXqob&6dncoLS4v*6*9u;;+TIukRWwcdEWqG_%`m3pn_;7sR#e zGZvX5lR;y7<<51wzP{f^?b~pDyKLKaK;|z21^``(YSq@Pc@+Qv002ovPDHLkV1izo BiCq8y diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-results-48x48.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-results-48x48.png index 3c2844fec636ec97fbbf75b427c8cf8d74778f1e..1aa0777cd1d4496bf2f96f9ba71cf14094e98db2 100644 GIT binary patch delta 2619 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 delta 2796 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 Uj5Q4vRsaA107*qoM6N<$g2t}AGynhq delta 1136 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-submit-48x48.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-submit-48x48.png index 35c264e0b8d48809e32052b0f45730edab785427..eb6061192ef8bd1cad2f6e8541e87f169cd3fb66 100644 GIT binary patch delta 2609 zcmV-13eNS+6}c3UBYz42NklP-T@-PI+ z)FkoOmJ3`^ z_=b7)j_5bnM(+SPJSYuaxb?!a)+;ji${Lgbb$rzIq4}bj|H$Dbj`WfJ{*<>V= z<{)tC&HaT}zWQ6#{J-$$nvyG&!xe$v$~3(AqG{Q( z#dM{mnWGsQ=J>X4<^(WAyz+`UDQy%jF~S!2quaNe)3_c(JA(T$T#w^=6zw^*6N2;7 zj-&s=jvZ!FFnkYuB!sTeohR+qZ9TNo-OHDo($%X? z<#W%O;F>jNKid6k*P4bGUNAMd4zF8hnt(d=wV~|*+JWxmWYYt5VcZe4ZFt^}@yD<* z-OoR7j-sykD^dLh%TsFI#5(@PTrCsDBVdR1<1WjuwPO9j_sLJxxe7aR~RV z6dVHDnrO$Ee$hlH#&prbm@U;go`XGD}R=Nz6mY^jWk|=|~ zG$-{5lGux9)fiKcYxRi``beS;eQH)cLO^}OHm6?nt7B6W7u@3Cs)<%SCy6lTA&D;BlSG6*f-%%Y8=lcG&U-bX z-@=4Qq8NhaVj@PSGJLRrp%outexOe%3HpWRiE@n9JV6r8=&Q#&wmG#iQlY6{!hmYeI~e15)}B+x$vO#A)j z_rM##AJ9MT^OSJVU9ePM#o?L=Q+!xXl9%m@s?gO7j{7e^pGW2*Ohn zg;>R-nVF`gV5vDATcOF2IrwBG{mZ7(^Pev~ zgY&!v0vclJ+`S+}66d`7K+5994uJx!ItdWGABNrE_;Avge}8WNdTBgi#E$1Z=OKY^ zac<4nM_@wZZcQjm6A~ZT4O1e4J%6GFI`t3;T)%q3eE+Y{JcIMRNfU}mcCGl9Cjz^k ze%f*5e5jNJ?DiB)==#+O^Zj@Kn87=LIujT1p4snl9!4c5In5I+01x3iXYWNR!6-z* z8Hw`X%)IW|xzk+#00h4M)H68GcvjX-re?xZO^kVUViI5u@QL8+51~z)On)yzDK!xV zP9ZAMu}=e|Y;ivdFi(sF=YR?10nHOnl0=bAtZY9SKv zNe0eiJ!A>4CS{ZGNwUkwcz+gvr7Qv=5KHk2)=^S=un;7P8X$I4)FkCN%MNN{)T<9{ zhY2}5GkmmA1ZQGXlTx3cc5F?MH1`pz36hv80MU|&RXn#t?i0l18N{T7O zi`>u`V;UegZV9v_ z4-}ICS02J~<@jNepeb>$KoX2lN~Azci^#_Fd~trQV#}=kM5&XD zntKeiByq|sfy^aKOc@D42$Bd;5PX=LpiejiBtxq+)3HgQfSVwUFBFp^X@`Wgn?(4g ze+Ha3wP1;J?nF%-KudEvo{{0sgnAH0gD{UO2*4c{PvmH`s(%SJry?;azeU2?hDvmr z(Sn3oM{5{s3qSO2@JA%nocP^gw?5DVAJ_*1`*;$8wnk3mDr7USBIQm8o<({vzUi8!ieZ2H3Pn4GcRTKVvoyJy7@ zB%W|i#a)?D1U}FJ!5Vmgg|?0{n(>;>ESew*hFEGrNfb%0P@nLt@Q5-heD$a4cZ8DI zQL)k;fq|`CO*4dRf^dxxrd1AWNrDMRNiaH95aJVz@qb!&1mXx^@%Pk(p(N5^cCJk* z2r@j!%ZL<2T5 z-&>Y2V&9!r1Nw_+LI)XxGx@oYU~FFA;bs@E0P$L|FcMfK7UcoP|G8A)-Y&Eq6i;Ub7F`sWNr`w zzk6q4!r=HmsfENk?wgAM2Rs67pb3qprI}%TGzy`{An*taD+r_{dPEZ~)B=+nR#`K4 zjr^k-TRS-+@h^`g42QrE8hBV_j7wl1@Bpw7*vuD3Q&Y`b*pFUBCj28X4NPLPDgN(^ zRew2$>ofcx7&y)Ui-8mA>1G%$|35wfunahcu@RsT&nVEAOY;*3$9H05B?K0VjPve9 zAa&*Z`Qe}9A9o6r^7exXgX8($x&;f$??gaN=-0q%U@MSuuR(z}Ui@Xk;CPW`wa9k= zoK7qZe+4W6esixuf!4qAi-aKr7Kls_+$zb5?ccng0*vEvV*6Yel6RBhh0*^4+&Gk; TY2|mA00000NkvXXu0mjf>Nuy5 delta 2757 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback-frown-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/linux/feedback-frown-16x16.png index 5d979ed539537d26ec5a837392c8a77bf9d5b2ce..014be181ffcf7bbdb50b653709673b94ed3a9325 100644 GIT binary patch delta 830 zcmV-E1Ht^72h;|TBYy)MNkl!Vt@dvhFgX4~o!aU#_OkH`CsH|DAIx zkuH9m=Q+>+cg}g?CHOW(7V^Yw7ASs7FJ<&EtY&+{|9LVSdVe_&4S1iGScboN8^lCc zrzBkqK)T+A#JR)LXh)OCW-PPNA(NjTlSd}gB1-owr#+rHxo>Xvqz^xjw&K^3X3QOH z!7slLQrCWnXFBJGTwCHS%N*vC$WRaIGa3U!PRF>|*@Ef4^_cP1K|0vLWt{f%mG!fS zTkzw!53k+jr-5SkE zWQ$FXn)4AW{c}j7-GeD#JrdjPToU*1<~@5Mo5!qXjJe8W>V71*b8?=4E-^{bLOr4; z17a2p;Db4)N92EJs_gf zV%U&}nv`WIPhNtWmC2}GnF7bMBs8ZkN6?gyXn`JMMFn_~uM?>oAXk-2^{uR|V9=+>*Xv*5v|Qq-m_1a=B&3kt;|O81?$Li~MW zwH&b)BbcK`P^J0vpqIrnSXhHK(l7?e8V`Eo-R=Qls-!6FRW&J!on=$H{qi$mqfhPMF0Q*07*qo IM6N<$f@GJJ%K!iX delta 900 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 m*ayzz2Y3#Ye~og)8}=Xc4-@&NxjnN00000 delta 1560 zcmV+z2Iu+P38)N^BYyxHbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU% z%1J~)RCwB?R!c}+XB57Vxiik(nejQk$HX+TnwW?Rp)`_Kq@<#YAO*2V7N#MK7)&ie z#SoK)QN#xX7ecyl)l~>sEJ|IZl|;maiX@7`8Ze<+-;S9X$A6i*)9+mVD>qZegmmEN zKIVM?Ip;gyIsfHDp%C|pKORfOlGv4xU`GyuSblcp{s)fNVUEIS*bq+%@;rYM4`o;> zSV{z-fVGVE8!!vF8qS|T|K*DpFMjZPy_rWbVK$pz=jP`AJTWmbM$b7pIo~cWF4pev z?>mpe8I8s_Cx1?y=$)IJ8>YC*%gYml;czdztl3?uU} zoB)48`1_hFX|Y)HL8kj-{DD;2-wt|5LOF{B|eSOaB*RNaQ zG#Mp+g@2&9=g*&?N={Dpg1wBInwq~3V-ExbZ``G}CIP2P z6u3W$DA)^k@7@jI8VxxSjsqb)aIX9J?+a^dYpMc(`wG{6;Ihac_O^hKzfmzih&&z) z27fvDS$g#7k#OhEoixPmVg6p}6C`aLDag^$(IrZfVxi~y#*m4|==Y#%q=0DZL*3x{4-S5$;o)He`re1iSy4rMZEdZ0WMssE!DH0p z-U}EdPtbafWO?!8#SA35nV6V3*Vx#Y5Q)yFrX~f!ikd9*^75=38yl;TpNO7_Sa}SX z5Y$1vs>%`)GXuASN(eafB3AIqm47QVP~K3_ku1Bqx-z@FyQOpI&Mo0T@jW(J57x6D zjKRe73=9m!qUUI!&f^hYVI!dwcson5OaQIW?iQv^4SN&712W zCP_Pj!iNmKtEi~he){yO4ZWu(6dM~W6c-n}o;-OH!}it>na|l-ZNaqm&416&%jh|Z zn?p~^P@5aW&ctL(Jvgr>h$Mc3tURpGENqhiBY}<*0@42hq-Vi&>mob5-Tuq&?yh#! zNDQO@KrpZ%IaDg(a=E@|Up6CqdwWLRXIn7rz}DVzXsvymY2VO3DJdyuR#sN_n5~KJ zt@kIf*m)>1YI%A2r7BUdzkdpeQH5mfi-q0G?4Euy*^n)J_TRd7>vuGJkZr7yVPxwd zak3#>Z2&<=zanryOGHc3z}CS#vLRb75uANK6eEho6gtG8nmE~zEn#@}u}$BgUl3s* z_`oLi#@RP1F-=FpnUBQ1#L~$5R2&iR(2*DEM@ph&&Hb+c0{~PP+7YjjCF@N90000< KMNUMnLSTYRUkpV6 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback-frown-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback-frown-16x16.png index 2a2034e37f0930218aaac69fe5065047f38dc52a..e74a0d3cd152ecc76f1fd0525ca13c23622904b3 100644 GIT binary patch delta 629 zcmV-*0*d{m28{)fBYy%^Nkl|-%C?r7{_0mI%Ij_WDs6QVC-i9MP(64;E%je znjFG1Hi$%>+tjw>&}uVh8>r1}sW2V8@FGSecrghg(ru9zl@|$Jj2YYTN3HL3c(9|p z`0RO~@AEz1z2`mWhzmjpHJ62GxGIGIx)AG)LhMtep7hh+Vt=vtzqurY;hGT3&CqXw zyTHPgdeUi5zLJ0|LY%)*R<>(~b`SIcHyGpE$2!f)huxez4nU)6uBiCXjlLfwg27Mg z;1SjnfGYK*)BFzYrm`}29MGuI-0bFm8U}^Ey`}wJF5cC+UAnk{398Q zC4X&iZ)`-PsfCu79a$AX{(Rguw)O1>SDO`T;sf%!lCny0#{rc$Klx?aij6$2tTS2nBz)4=`<%F z{AWu(CNw(GwCZ#&P8*G%{rq&OQcpU~$#>#oqMuV-1g+pcaByv9y~O_y)V8z9@-?AqwJys1t-=*LxzO}g%6@GG6@}R8O}d#uI=7?`hPw>ntIr8=YHS)?)Uc` znomwH2O(F51BwBcO2|C$8F;+{9Jlo|M(Ck~vxH>m3Gtc;xdsp;AOZ>y1tJGbCkSzW zCuHM0xvBCu&1SUV+NuW zjk@q-OcxqNJ`N9CwnL#L!ev~u9#497V?#1e!fkXoICm zM&sUo2`M=_Kh&kA^Ej@A!JxLx%q-`Wl>7oQ2!yQ;ho!K-KI>b1`}UjaYA00`ip@@z z9o9)tQTU#Kns_IMG(2yU)B4;Wq z-|nXq35SaY?e?Gdp%?LJbnx-(s7avK1Vs9&6$jIXJsIi+^QZNF-8Qe7@V&6&1sH5_JWXYUgTd zhC1fv2Gi~Ku#tCKD%PDHLkV1kd2YKQ;; diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback-smile-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/mac/feedback-smile-16x16.png index bd13830157e48180430c3295ca8aa39262ae9079..d2a2bb36cc7c5743b1d6db3abb275a155256a743 100644 GIT binary patch delta 774 zcmV+h1Nr==2b~6xBYy(qNkl^idr_GEGyM22STqqK)8m_Aac~VlfHL? z-4xl4(`o=AoBgER*Fl-~u=k=pGnRBwAvBk3s&$gNDL`;A~5xq8+DKK8)$pb zK(kS9pdd*n1kJc=AqkfNw9Q9O9 zzyv-@PJQGs8TMlW$YoRiXwT);DlaRo5rVYPV}GXUSxN23&!Z;N>qJD3I$c8Ef!@pd zcDzsZ&#z2wxF4oRuH-dG595{-i-eF+K^*18--RL;~0$v6MKLYPx>;M1&07*qoM6N<$ Eg0@wG&Hw-a delta 912 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 QJpcdz07*qoM6N<$f_HCUv;Y7A delta 2275 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 Z6+u+6AeA}a@AK6{|BSs?VJDr literal 6964 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback-frown-16x16.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback-frown-16x16.png index 8da586279d8931d6adf91df1a2565755401ce240..ac03c486a2f340b64fa76a8e9422b1376a98dace 100644 GIT binary patch delta 661 zcmV;G0&4xL2CM~;BYy&ONkl@P7#U8E{eXcyPTk2ss?XLoj;*=dmN(1}{$$f2bvul?DUp3n#qH83rXK z%P9y`R^sVH#NUJWIsX7#z~1L;NaclIXm|XoFP!ipi)G2!0*a)pz(oAYfhS_0<8I>7 zDx68Rkta}Rokm|c;d@M`4dW24w$(UtfrwrB>7pv}-m4EAkxLzo5uBC+&fHK8x zWr|CF>woj9ewSJ2M^)Y#47-&9zO-C-_l(1*!&+BYyxHbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU! z%t=H+RCwBKQ%y)zQ562(eecbi(U~`uS}8WkjBbL^3R@(!tZ4*Gf-DR&iNcUJtt1f! z1b3|@64HevIM@#jj;1X#+u0AMwF$DYKqhO@85`&Cz1z7nPJdeV;JkD1Ip6utJ?EY) zdGYsmr2zFzMZK)z2xF)*P0Si;yfM=V-HRi%6+6y3q&z_@LkY8@ufT>ztpHH00FwdJ z05S<683*F)cx)zcZNS8Go`8u(3s9-3(!4`MBar}wfNdwKqMR0cVYicCCLH1S=1;*c z0b@rI zKH-J)z(K6c&mtTOVl^0qGeyJ#M+r^Mx|XeL$hH6V14=eY60%tMx6mm6-}gB%%tvLJ zPY_PvMSmO@b6~Hoo`^%jDv_0Uccbj27w;lh%KVS8#kB|iA3El7k?r@mREhl!*G)(2VHN zw5YYT^po4?n{MdseMp!YN}AmE8n-;reWctZVmt(?0RJRepun&HKIh*83;-+eKt<4x S5)C>40000{M{LSK&yUbk(IrH1jdP zVI?5sq-LYJN_wp}Xkl%U(EHMfzHuk)Y&g%Y=h)|QqKE0$n-U8VVXl;hRsu|AqAm7< z2y2^+wA&JV-+#SWT=K!*B_n^tfb4D=UzfGiTN13TGGdrNv_x}|2gEqGjm0~)NWUw? z*1l~1;X_W3f`Z2ej(;@jr?Vozh0XDRD2KF=cuk8#_he*sDHxk>$MBR3d4qZsJ~1Gd z;n=%&3{AR_)vX|-Lx$R@MU+FH=KEi$k&#_K2m0O5eY1H&ietEA7=BV(9lu$?_uJX+;)8N%_50CwBpesD24A#cX7(-UA|y^a z^~v}2&4~KXugo6&r`~%q6V4btyH9Aa=ac|b$xh!QPCE6;$NXE?941CKvkI&0D00P> z?s^qcAbEq2`Gb%+>C`9Rx~FlK7{Dr=RWhrzWh5T3-f{i~@G%qc5HAYB00000NkvXX Hu0mjfJe6cd delta 822 zcmV-61IhgC1wi>559MG?4&+(d>u zYvaN&+7!`KA(dPN5lGP>DTWDUs1=oF;*5_wb02@_zmsHjxqs)Ld(QpNcOL&eg0E1; z`m3zLQ!PoTmSnM1kPu`xMr_L&wr%u>dsh3`Vu=J z%}7iJ(_k>7c8?juso}oWxitcjzZCD&w`#gL=1r9wWSIiV>rEEnNfBMmZSMD`NDV6C z2)}8q3b~*{mwz-(eW0f}85(6iTzClrWgeJmhe~#wGB=jA!f}9yqgN@!&R~|>1sL_5 zK$KOfN%q)5ElGkT169Egs)NmL?Gp7MI1E*VA=DN(6By8vWJq4SuBcLPbP2qQCnReE zWEzFY1GtwSq9~Bis~{w5B_+uToI>~{E%~I|f`xdAPJhh2PY!rB)r;l$0s)DD2a$d? z-HRn-0erYtoKYew5N6ONFfDTfb7WA9P-yaZ$6MU#E2;>p9y0mvlh1Y%6*BN+q1(3n zZVMvQB6Q9$SApmc;*Bu~zkz&kK*BaDb7YR{H2{m|BQkZ#CGe|h@8c9~EZOL6dV_pl z9gG!58GiuBPH1j#Pp-v{qvME0Dai_uEN`52334(D292nL*fPV1uVXm9rvqvv8$Z8U zh|Jrr=JuJ2Ynb>v?BZ4lzb&)W{VRp}#o64(D`n3tWidN5Jqy>5KSh%6$IwI{re`OR z9mqp@UJaIw2+rTE#Fx1lq$*kA$CpPBQTz-TV}HVvp*x|S=K>46T2k2lwp`ZxvYSnP zf5-U6HMhB+f9JU8BLnq(1?Y2P7wDh%jZKCRwB9;#b~ZXy=I(*?XKDBx_&x-DTf%L} zx;qPU%kKG$G~=&jB10p!qy6@UgF|lzcQ35W=9+fQuSi-X)f<>B&)@sv;*pL=gn3QD zu`mC&aovh;CY3{yM#4!sc%I}V1%CQ>oqq{10J0iK;?2z;rT_o{07*qoM6N<$g6#-@ A`v3p{ diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/notification-tail-down.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/notification-tail-down.png index f88157e1eaf0d6c58b811fdeafe4b4294109fce3..d0d6f4b2e83d403972f8a735d8c4abc1041a162a 100644 GIT binary patch literal 3838 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 diff --git a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/notification-tail-up.png b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/notification-tail-up.png index 2f40b1a833cd901a8c60e7639ff84891119909a7..843db1863c4b7b2559b95dbd3d653c8c8c0f46b8 100644 GIT binary patch literal 4972 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 N002ovPDHLkV1nRWtF8b5 From 7d3748b77983f94063197d6785852e2963e7f8b3 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 24 Jun 2010 19:56:14 -0500 Subject: [PATCH 1803/1860] Backed out changeset c60e3e48ea38 --- db/sqlite3/src/Makefile.in | 2 - storage/src/mozStorageConnection.cpp | 9 +--- storage/test/unit/test_page_size_is_32k.js | 28 ------------ .../components/places/src/nsNavHistory.cpp | 44 ++++++++++++++----- .../src/nsUrlClassifierDBService.cpp | 21 ++++----- 5 files changed, 41 insertions(+), 63 deletions(-) delete mode 100644 storage/test/unit/test_page_size_is_32k.js diff --git a/db/sqlite3/src/Makefile.in b/db/sqlite3/src/Makefile.in index 0e79fecea832..104a8925f88e 100644 --- a/db/sqlite3/src/Makefile.in +++ b/db/sqlite3/src/Makefile.in @@ -100,7 +100,6 @@ CSRCS = \ # don't have to vacuum to make sure the data is not visible in the file. # -DSQLITE_ENABLE_FTS3=1 enables the full-text index module. # -DSQLITE_CORE=1 statically links that module into the SQLite library. -# -DSQLITE_DEFAULT_PAGE_SIZE=32768 increases the page size from 1k, see bug 416330. # Note: Be sure to update the configure.in checks when these change! DEFINES = \ -DSQLITE_SECURE_DELETE=1 \ @@ -108,7 +107,6 @@ DEFINES = \ -DSQLITE_CORE=1 \ -DSQLITE_ENABLE_FTS3=1 \ -DSQLITE_ENABLE_UNLOCK_NOTIFY=1 \ - -DSQLITE_DEFAULT_PAGE_SIZE=32768 \ $(NULL) # -DSQLITE_ENABLE_LOCKING_STYLE=1 to help with AFP folders diff --git a/storage/src/mozStorageConnection.cpp b/storage/src/mozStorageConnection.cpp index 4b8874946180..59b7a8e39dbb 100644 --- a/storage/src/mozStorageConnection.cpp +++ b/storage/src/mozStorageConnection.cpp @@ -393,14 +393,6 @@ Connection::initialize(nsIFile *aDatabaseFile) PR_LOG(gStorageLog, PR_LOG_NOTICE, ("Opening connection to '%s' (%p)", leafName.get(), this)); #endif - // Switch db to preferred page size in case the user vacuums. - sqlite3_stmt *stmt; - srv = prepareStmt(mDBConn, NS_LITERAL_CSTRING("PRAGMA page_size = 32768"), - &stmt); - if (srv == SQLITE_OK) { - (void)stepStmt(stmt); - (void)::sqlite3_finalize(stmt); - } // Register our built-in SQL functions. srv = registerFunctions(mDBConn); @@ -420,6 +412,7 @@ Connection::initialize(nsIFile *aDatabaseFile) // Execute a dummy statement to force the db open, and to verify if it is // valid or not. + sqlite3_stmt *stmt; srv = prepareStmt(mDBConn, NS_LITERAL_CSTRING("SELECT * FROM sqlite_master"), &stmt); if (srv == SQLITE_OK) { diff --git a/storage/test/unit/test_page_size_is_32k.js b/storage/test/unit/test_page_size_is_32k.js deleted file mode 100644 index b40aa3cd13a9..000000000000 --- a/storage/test/unit/test_page_size_is_32k.js +++ /dev/null @@ -1,28 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ - */ - -// This file tests that dbs are using 32k pagesize - -function check_size(db) -{ - var stmt = db.createStatement("PRAGMA page_size"); - stmt.executeStep(); - const expected_block_size = 32768; // 32K - do_check_eq(stmt.getInt32(0), expected_block_size); - stmt.finalize(); -} - -function new_file(name) -{ - var file = dirSvc.get("ProfD", Ci.nsIFile); - file.append(name + ".sqlite"); - do_check_false(file.exists()); -} - -function run_test() -{ - check_size(getDatabase(new_file("shared32k.sqlite"))); - check_size(getService().openUnsharedDatabase(new_file("unshared32k.sqlite"))); -} - diff --git a/toolkit/components/places/src/nsNavHistory.cpp b/toolkit/components/places/src/nsNavHistory.cpp index 6a925132d93c..e3d4b9f0dd55 100644 --- a/toolkit/components/places/src/nsNavHistory.cpp +++ b/toolkit/components/places/src/nsNavHistory.cpp @@ -140,6 +140,13 @@ using namespace mozilla::places; // corresponding migrateVxx method below. #define DATABASE_SCHEMA_VERSION 10 +// We set the default database page size to be larger. sqlite's default is 1K. +// This gives good performance when many small parts of the file have to be +// loaded for each statement. Because we try to keep large chunks of the file +// in memory, a larger page size should give better I/O performance. 32K is +// sqlite's default max page size. +#define DATABASE_PAGE_SIZE 4096 + // Filename of the database. #define DATABASE_FILENAME NS_LITERAL_STRING("places.sqlite") @@ -627,23 +634,37 @@ nsNavHistory::InitDBFile(PRBool aForceInit) nsresult nsNavHistory::InitDB() { + PRInt32 pageSize = DATABASE_PAGE_SIZE; + // Get the database schema version. PRInt32 currentSchemaVersion = 0; nsresult rv = mDBConn->GetSchemaVersion(¤tSchemaVersion); NS_ENSURE_SUCCESS(rv, rv); + bool databaseInitialized = (currentSchemaVersion > 0); - // Get the page size. This may be different than the default if the - // database file already existed with a different page size. - nsCOMPtr statement; - rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("PRAGMA page_size"), - getter_AddRefs(statement)); - NS_ENSURE_SUCCESS(rv, rv); + if (!databaseInitialized) { + // First of all we must set page_size since it will only have effect on + // empty files. For existing databases we could get a different page size, + // trying to change it would be uneffective. + // See bug 401985 for details. + nsCAutoString pageSizePragma("PRAGMA page_size = "); + pageSizePragma.AppendInt(pageSize); + rv = mDBConn->ExecuteSimpleSQL(pageSizePragma); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + // Get the page size. This may be different than the default if the + // database file already existed with a different page size. + nsCOMPtr statement; + rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("PRAGMA page_size"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); - PRBool hasResult; - mozStorageStatementScoper scoper(statement); - rv = statement->ExecuteStep(&hasResult); - NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_FAILURE); - PRInt32 pageSize = statement->AsInt32(0); + PRBool hasResult; + rv = statement->ExecuteStep(&hasResult); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_FAILURE); + pageSize = statement->AsInt32(0); + } // Ensure that temp tables are held in memory, not on disk. We use temp // tables mainly for fsync and I/O reduction. @@ -705,7 +726,6 @@ nsNavHistory::InitDB() rv = nsAnnotationService::InitTables(mDBConn); NS_ENSURE_SUCCESS(rv, rv); - bool databaseInitialized = (currentSchemaVersion > 0); if (!databaseInitialized) { // This is the first run, so we set schema version to the latest one, since // we don't need to migrate anything. We will create tables from scratch. diff --git a/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp b/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp index 4b70dcb0ad95..593ab7c9c5e7 100644 --- a/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp +++ b/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp @@ -173,6 +173,8 @@ static const PRLogModuleInfo *gUrlClassifierDbServiceLog = nsnull; #define UPDATE_DELAY_TIME "urlclassifier.updatetime" #define UPDATE_DELAY_TIME_DEFAULT 60 +#define PAGE_SIZE 4096 + class nsUrlClassifierDBServiceWorker; // Singleton instance. @@ -1221,7 +1223,6 @@ private: nsCOMPtr mGetTableIdStatement; nsCOMPtr mGetTableNameStatement; nsCOMPtr mInsertTableIdStatement; - nsCOMPtr mGetPageSizeStatement; // Stores the last time a given table was updated. nsDataHashtable mTableFreshness; @@ -3163,13 +3164,7 @@ nsUrlClassifierDBServiceWorker::SetupUpdate() NS_ENSURE_SUCCESS(rv, rv); if (gUpdateCacheSize > 0) { - PRBool hasResult; - rv = mGetPageSizeStatement->ExecuteStep(&hasResult); - NS_ENSURE_SUCCESS(rv, rv); - - NS_ASSERTION(hasResult, "Should always be able to get page size from sqlite"); - PRUint32 pageSize = mGetTableIdStatement->AsInt32(0); - PRUint32 cachePages = gUpdateCacheSize / pageSize; + PRUint32 cachePages = gUpdateCacheSize / PAGE_SIZE; nsCAutoString cacheSizePragma("PRAGMA cache_size="); cacheSizePragma.AppendInt(cachePages); rv = mConnection->ExecuteSimpleSQL(cacheSizePragma); @@ -3418,6 +3413,11 @@ nsUrlClassifierDBServiceWorker::OpenDb() } } + nsCAutoString cacheSizePragma("PRAGMA page_size="); + cacheSizePragma.AppendInt(PAGE_SIZE); + rv = connection->ExecuteSimpleSQL(cacheSizePragma); + NS_ENSURE_SUCCESS(rv, rv); + rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("PRAGMA synchronous=OFF")); NS_ENSURE_SUCCESS(rv, rv); @@ -3475,11 +3475,6 @@ nsUrlClassifierDBServiceWorker::OpenDb() getter_AddRefs(mInsertTableIdStatement)); NS_ENSURE_SUCCESS(rv, rv); - rv = connection->CreateStatement - (NS_LITERAL_CSTRING("PRAGMA page_size"), - getter_AddRefs(mGetPageSizeStatement)); - NS_ENSURE_SUCCESS(rv, rv); - mConnection = connection; mCryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); From 611f140d437a62d2b6f16d1b239f140d0b7b1c95 Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:06 -0500 Subject: [PATCH 1804/1860] Bug 513162 - Cleanup unused widget code and CID statics. r=vlad. --- layout/generic/nsFrame.cpp | 1 - layout/xul/base/src/nsBoxFrame.cpp | 3 --- layout/xul/base/src/nsLeafBoxFrame.cpp | 2 -- .../xul/base/src/tree/src/nsTreeBodyFrame.cpp | 2 -- widget/src/windows/nsWindow.cpp | 26 ------------------- widget/src/windows/nsWindow.h | 1 - 6 files changed, 35 deletions(-) diff --git a/layout/generic/nsFrame.cpp b/layout/generic/nsFrame.cpp index 28a44f2c4fd3..386294f3b8c3 100644 --- a/layout/generic/nsFrame.cpp +++ b/layout/generic/nsFrame.cpp @@ -131,7 +131,6 @@ using namespace mozilla; static NS_DEFINE_CID(kLookAndFeelCID, NS_LOOKANDFEEL_CID); -static NS_DEFINE_CID(kWidgetCID, NS_CHILD_CID); // Struct containing cached metrics for box-wrapped frames. struct nsBoxLayoutMetrics diff --git a/layout/xul/base/src/nsBoxFrame.cpp b/layout/xul/base/src/nsBoxFrame.cpp index f4235ca63691..aebcf59cfc6a 100644 --- a/layout/xul/base/src/nsBoxFrame.cpp +++ b/layout/xul/base/src/nsBoxFrame.cpp @@ -101,9 +101,6 @@ #include "nsIDocument.h" #include "nsIURI.h" - -static NS_DEFINE_IID(kWidgetCID, NS_CHILD_CID); - //define DEBUG_REDRAW #define DEBUG_SPRING_SIZE 8 diff --git a/layout/xul/base/src/nsLeafBoxFrame.cpp b/layout/xul/base/src/nsLeafBoxFrame.cpp index c1070f798819..e08d7b38019a 100644 --- a/layout/xul/base/src/nsLeafBoxFrame.cpp +++ b/layout/xul/base/src/nsLeafBoxFrame.cpp @@ -59,8 +59,6 @@ #include "nsHTMLContainerFrame.h" #include "nsDisplayList.h" -static NS_DEFINE_IID(kWidgetCID, NS_CHILD_CID); - // // NS_NewLeafBoxFrame // diff --git a/layout/xul/base/src/tree/src/nsTreeBodyFrame.cpp b/layout/xul/base/src/tree/src/nsTreeBodyFrame.cpp index 536e85338e3b..f610dbaa15a8 100644 --- a/layout/xul/base/src/tree/src/nsTreeBodyFrame.cpp +++ b/layout/xul/base/src/tree/src/nsTreeBodyFrame.cpp @@ -111,8 +111,6 @@ #include "nsBidiPresUtils.h" #endif -static NS_DEFINE_CID(kWidgetCID, NS_CHILD_CID); - // Enumeration function that cancels all the image requests in our cache static PLDHashOperator CancelImageRequest(const nsAString& aKey, diff --git a/widget/src/windows/nsWindow.cpp b/widget/src/windows/nsWindow.cpp index 8a868c55d571..e74a1f67958d 100644 --- a/widget/src/windows/nsWindow.cpp +++ b/widget/src/windows/nsWindow.cpp @@ -1738,32 +1738,6 @@ NS_METHOD nsWindow::GetClientBounds(nsIntRect &aRect) return NS_OK; } -// Get the bounds, but don't take into account the client size -void nsWindow::GetNonClientBounds(nsIntRect &aRect) -{ - if (mWnd) { - RECT r; - VERIFY(::GetWindowRect(mWnd, &r)); - - // assign size - aRect.width = r.right - r.left; - aRect.height = r.bottom - r.top; - - // convert coordinates if parent exists - HWND parent = ::GetParent(mWnd); - if (parent) { - RECT pr; - VERIFY(::GetWindowRect(parent, &pr)); - r.left -= pr.left; - r.top -= pr.top; - } - aRect.x = r.left; - aRect.y = r.top; - } else { - aRect.SetRect(0,0,0,0); - } -} - // Like GetBounds, but don't offset by the parent NS_METHOD nsWindow::GetScreenBounds(nsIntRect &aRect) { diff --git a/widget/src/windows/nsWindow.h b/widget/src/windows/nsWindow.h index 705faecdc26e..6b0113b94e77 100644 --- a/widget/src/windows/nsWindow.h +++ b/widget/src/windows/nsWindow.h @@ -280,7 +280,6 @@ protected: LPARAM lParamToClient(LPARAM lParam); nsWindow* GetParentWindow(PRBool aIncludeOwner); virtual void SubclassWindow(BOOL bState); - void GetNonClientBounds(nsIntRect &aRect); PRBool CanTakeFocus(); #if !defined(WINCE) static void InitTrackPointHack(); From 7744f810f7d459d657161975b8ac0175d957e572 Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:06 -0500 Subject: [PATCH 1805/1860] Bug 513162 - Fix for intermittent failure in test_titlebar.xul, due to left over focus listener. r=sdwilsh. --- toolkit/content/tests/chrome/test_showcaret.xul | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/content/tests/chrome/test_showcaret.xul b/toolkit/content/tests/chrome/test_showcaret.xul index 62b33c10e99d..6d2977403852 100644 --- a/toolkit/content/tests/chrome/test_showcaret.xul +++ b/toolkit/content/tests/chrome/test_showcaret.xul @@ -55,7 +55,7 @@ function runTest() function otherWindowFocused() { - otherWindow.addEventListener("focus", otherWindowFocused, false); + otherWindow.removeEventListener("focus", otherWindowFocused, false); // enable caret browsing temporarily to test caret movement var prefs = Components.classes["@mozilla.org/preferences-service;1"]. From c87cfafdddbeea955fd65d0fafb143d2e974a5a1 Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:06 -0500 Subject: [PATCH 1806/1860] Bug 513162 - Widget additions for recycling top level widgets as content containers. r=vlad. --- widget/public/nsIWidget.h | 52 +++++++++++++- widget/src/windows/nsWindow.cpp | 99 ++++++++++++++++++++++++++- widget/src/windows/nsWindow.h | 3 + widget/src/xpwidgets/nsBaseWidget.cpp | 54 +++++++++++++-- widget/src/xpwidgets/nsBaseWidget.h | 7 ++ 5 files changed, 205 insertions(+), 10 deletions(-) diff --git a/widget/public/nsIWidget.h b/widget/public/nsIWidget.h index 798a72f926f5..a77a129a9c41 100644 --- a/widget/public/nsIWidget.h +++ b/widget/public/nsIWidget.h @@ -66,6 +66,7 @@ class nsGUIEvent; class imgIContainer; class gfxASurface; class nsIContent; +class ViewWrapper; namespace mozilla { namespace layers { @@ -110,8 +111,9 @@ typedef nsEventStatus (* EVENT_CALLBACK)(nsGUIEvent *event); #endif #define NS_IWIDGET_IID \ -{ 0xf286438a, 0x6ec6, 0x4766, \ - { 0xa4, 0x76, 0x4a, 0x44, 0x80, 0x95, 0xd3, 0x1f } } +{ 0x271ac413, 0xa202, 0x46dc, \ +{ 0xbc, 0xd5, 0x67, 0xa1, 0xfb, 0x58, 0x89, 0x7f } } + /* * Window shadow styles * Also used for the -moz-window-shadow CSS property @@ -235,6 +237,28 @@ class nsIWidget : public nsISupports { nsIToolkit *aToolkit = nsnull, nsWidgetInitData *aInitData = nsnull) = 0; + /** + * Attach to a top level widget. + * + * In cases where a top level chrome widget is being used as a content + * container, attach a secondary event callback and update the device + * context. The primary event callback will continue to be called, so the + * owning base window will continue to function. + * + * aViewEventFunction Event callback that will receive mirrored + * events. + * aContext The new device context for the view + */ + NS_IMETHOD AttachViewToTopLevel(EVENT_CALLBACK aViewEventFunction, + nsIDeviceContext *aContext) = 0; + + /** + * Accessor functions to get and set secondary client data. Used by + * nsIView in connection with AttachViewToTopLevel above. + */ + NS_IMETHOD SetAttachedViewPtr(ViewWrapper* aViewWrapper) = 0; + virtual ViewWrapper* GetAttachedViewPtr() = 0; + /** * Accessor functions to get and set the client data associated with the * widget. @@ -408,6 +432,22 @@ class nsIWidget : public nsISupports { PRInt32 aHeight, PRBool aRepaint) = 0; + /** + * Resize and reposition the inner client area of the widget. + * + * @param aX the new x offset expressed in the parent's coordinate system + * @param aY the new y offset expressed in the parent's coordinate system + * @param aWidth the new width of the client area. + * @param aHeight the new height of the client area. + * @param aRepaint whether the widget should be repainted + * + */ + NS_IMETHOD ResizeClient(PRInt32 aX, + PRInt32 aY, + PRInt32 aWidth, + PRInt32 aHeight, + PRBool aRepaint) = 0; + /** * Sets the widget's z-index. */ @@ -502,6 +542,14 @@ class nsIWidget : public nsISupports { */ NS_IMETHOD GetClientBounds(nsIntRect &aRect) = 0; + /** + * Get the client offset from the window origin. + * + * @param aPt on return it holds the width and height of the offset. + * + */ + NS_IMETHOD GetClientOffset(nsIntPoint &aPt) = 0; + /** * Get the foreground color for this widget * diff --git a/widget/src/windows/nsWindow.cpp b/widget/src/windows/nsWindow.cpp index e74a1f67958d..4ecc3fd03ee7 100644 --- a/widget/src/windows/nsWindow.cpp +++ b/widget/src/windows/nsWindow.cpp @@ -582,8 +582,10 @@ nsWindow::Create(nsIWidget *aParent, nsToolkit::mDllInstance, NULL); - if (!mWnd) + if (!mWnd) { + NS_WARNING("nsWindow CreateWindowEx failed."); return NS_ERROR_FAILURE; + } if (nsWindow::sTrackPointHack && mWindowType != eWindowType_plugin && @@ -1337,6 +1339,10 @@ NS_METHOD nsWindow::Resize(PRInt32 aWidth, PRInt32 aHeight, PRBool aRepaint) NS_ASSERTION((aWidth >=0 ) , "Negative width passed to nsWindow::Resize"); NS_ASSERTION((aHeight >=0 ), "Negative height passed to nsWindow::Resize"); + // Avoid unnecessary resizing calls + if (mBounds.width == aWidth && mBounds.height == aHeight) + return NS_OK; + #ifdef MOZ_XUL if (eTransparencyTransparent == mTransparencyMode) ResizeTranslucentWindow(aWidth, aHeight); @@ -1372,6 +1378,11 @@ NS_METHOD nsWindow::Resize(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeig NS_ASSERTION((aWidth >=0 ), "Negative width passed to nsWindow::Resize"); NS_ASSERTION((aHeight >=0 ), "Negative height passed to nsWindow::Resize"); + // Avoid unnecessary resizing calls + if (mBounds.x == aX && mBounds.y == aY && + mBounds.width == aWidth && mBounds.height == aHeight) + return NS_OK; + #ifdef MOZ_XUL if (eTransparencyTransparent == mTransparencyMode) ResizeTranslucentWindow(aWidth, aHeight); @@ -1402,6 +1413,30 @@ NS_METHOD nsWindow::Resize(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeig return NS_OK; } +// Resize the client area and position the widget within it's parent +NS_METHOD nsWindow::ResizeClient(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, PRBool aRepaint) +{ + NS_ASSERTION((aWidth >=0) , "Negative width passed to ResizeClient"); + NS_ASSERTION((aHeight >=0), "Negative height passed to ResizeClient"); + + // Adjust our existing window bounds, based on the new client dims. + RECT client; + GetClientRect(mWnd, &client); + nsIntPoint dims(client.right - client.left, client.bottom - client.top); + aWidth = mBounds.width + (aWidth - dims.x); + aHeight = mBounds.height + (aHeight - dims.y); + + if (aX || aY) { + // offsets + nsIntRect bounds; + GetScreenBounds(bounds); + aX += bounds.x; + aY += bounds.y; + return Resize(aX, aY, aWidth, aHeight, aRepaint); + } + return Resize(aWidth, aHeight, aRepaint); +} + #if !defined(WINCE) NS_IMETHODIMP nsWindow::BeginResizeDrag(nsGUIEvent* aEvent, PRInt32 aHorizontal, PRInt32 aVertical) @@ -1691,7 +1726,9 @@ NS_METHOD nsWindow::SetFocus(PRBool aRaise) * **************************************************************/ -// Get this component dimension +// Return the window's full dimensions in screen coordinates. +// If the window has a parent, converts the origin to an offset +// of the parent's screen origin. NS_METHOD nsWindow::GetBounds(nsIntRect &aRect) { if (mWnd) { @@ -1702,6 +1739,32 @@ NS_METHOD nsWindow::GetBounds(nsIntRect &aRect) aRect.width = r.right - r.left; aRect.height = r.bottom - r.top; + // chrome on parent: + // ___ 5,5 (chrome start) + // | ____ 10,10 (client start) + // | | ____ 20,20 (child start) + // | | | + // 20,20 - 5,5 = 15,15 (??) + // minus GetClientOffset: + // 15,15 - 5,5 = 10,10 + // + // no chrome on parent: + // ______ 10,10 (win start) + // | ____ 20,20 (child start) + // | | + // 20,20 - 10,10 = 10,10 + // + // walking the chain: + // ___ 5,5 (chrome start) + // | ___ 10,10 (client start) + // | | ___ 20,20 (child start) + // | | | __ 30,30 (child start) + // | | | | + // 30,30 - 20,20 = 10,10 (offset from second child to first) + // 20,20 - 5,5 = 15,15 + 10,10 = 25,25 (??) + // minus GetClientOffset: + // 25,25 - 5,5 = 20,20 (offset from second child to parent client) + // convert coordinates if parent exists HWND parent = ::GetParent(mWnd); if (parent) { @@ -1709,6 +1772,14 @@ NS_METHOD nsWindow::GetBounds(nsIntRect &aRect) VERIFY(::GetWindowRect(parent, &pr)); r.left -= pr.left; r.top -= pr.top; + // adjust for chrome + nsWindow* pWidget = static_cast(GetParent()); + if (pWidget && pWidget->IsTopLevelWidget()) { + nsIntPoint clientOffset; + pWidget->GetClientOffset(clientOffset); + r.left -= clientOffset.x; + r.top -= clientOffset.y; + } } aRect.x = r.left; aRect.y = r.top; @@ -1755,6 +1826,23 @@ NS_METHOD nsWindow::GetScreenBounds(nsIntRect &aRect) return NS_OK; } +// return the x,y offset of the client area from the origin +// of the window. If the window is borderless returns (0,0). +NS_METHOD nsWindow::GetClientOffset(nsIntPoint &aPt) +{ + if (!mWnd) { + aPt.x = aPt.y = 0; + return NS_OK; + } + + RECT r1; + GetWindowRect(mWnd, &r1); + nsIntPoint pt = WidgetToScreenOffset(); + aPt.x = pt.x - r1.left; + aPt.y = pt.y - r1.top; + return NS_OK; +} + /************************************************************** * * SECTION: nsIWidget::SetBackgroundColor @@ -6332,6 +6420,13 @@ PRBool nsWindow::OnResize(nsIntRect &aWindowRect) event.mWinWidth = 0; event.mWinHeight = 0; } + +#if 0 + printf("[%X] OnResize: client:(%d x %d x %d x %d) window:(%d x %d)\n", this, + aWindowRect.x, aWindowRect.y, aWindowRect.width, aWindowRect.height, + event.mWinWidth, event.mWinHeight); +#endif + return DispatchWindowEvent(&event); } diff --git a/widget/src/windows/nsWindow.h b/widget/src/windows/nsWindow.h index 6b0113b94e77..856aed638124 100644 --- a/widget/src/windows/nsWindow.h +++ b/widget/src/windows/nsWindow.h @@ -129,6 +129,7 @@ public: NS_IMETHOD Move(PRInt32 aX, PRInt32 aY); NS_IMETHOD Resize(PRInt32 aWidth, PRInt32 aHeight, PRBool aRepaint); NS_IMETHOD Resize(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, PRBool aRepaint); + NS_IMETHOD ResizeClient(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, PRBool aRepaint); #if !defined(WINCE) NS_IMETHOD BeginResizeDrag(nsGUIEvent* aEvent, PRInt32 aHorizontal, PRInt32 aVertical); #endif @@ -140,6 +141,7 @@ public: NS_IMETHOD GetBounds(nsIntRect &aRect); NS_IMETHOD GetScreenBounds(nsIntRect &aRect); NS_IMETHOD GetClientBounds(nsIntRect &aRect); + NS_IMETHOD GetClientOffset(nsIntPoint &aPt); NS_IMETHOD SetBackgroundColor(const nscolor &aColor); NS_IMETHOD SetCursor(imgIContainer* aCursor, PRUint32 aHotspotX, PRUint32 aHotspotY); @@ -243,6 +245,7 @@ public: PRBool GetIMEEnabled() { return mIMEEnabled; } // needed in nsIMM32Handler.cpp PRBool PluginHasFocus() { return mIMEEnabled == nsIWidget::IME_STATUS_PLUGIN; } + PRBool IsTopLevelWidget() { return mIsTopWidgetWindow; } #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_WIN7 PRBool HasTaskbarIconBeenCreated() { return mHasTaskbarIconBeenCreated; } diff --git a/widget/src/xpwidgets/nsBaseWidget.cpp b/widget/src/xpwidgets/nsBaseWidget.cpp index 17c05316f384..4922755801e9 100644 --- a/widget/src/xpwidgets/nsBaseWidget.cpp +++ b/widget/src/xpwidgets/nsBaseWidget.cpp @@ -95,7 +95,9 @@ nsAutoRollup::~nsAutoRollup() nsBaseWidget::nsBaseWidget() : mClientData(nsnull) +, mViewWrapperPtr(nsnull) , mEventCallback(nsnull) +, mViewCallback(nsnull) , mContext(nsnull) , mToolkit(nsnull) , mCursor(eCursor_standard) @@ -237,6 +239,46 @@ NS_IMETHODIMP nsBaseWidget::SetClientData(void* aClientData) return NS_OK; } +// Attach a view to our widget which we'll send events to. +NS_IMETHODIMP +nsBaseWidget::AttachViewToTopLevel(EVENT_CALLBACK aViewEventFunction, + nsIDeviceContext *aContext) +{ + NS_ASSERTION((mWindowType == eWindowType_toplevel), "Can't attach to child?"); + + mViewCallback = aViewEventFunction; + + if (aContext) { + if (mContext) { + NS_IF_RELEASE(mContext); + } + mContext = aContext; + NS_ADDREF(mContext); + } + + return NS_OK; +} + +ViewWrapper* nsBaseWidget::GetAttachedViewPtr() + { + return mViewWrapperPtr; + } + +NS_IMETHODIMP nsBaseWidget::SetAttachedViewPtr(ViewWrapper* aViewWrapper) + { + mViewWrapperPtr = aViewWrapper; + return NS_OK; + } + +NS_METHOD nsBaseWidget::ResizeClient(PRInt32 aX, + PRInt32 aY, + PRInt32 aWidth, + PRInt32 aHeight, + PRBool aRepaint) +{ + return Resize(aX, aY, aWidth, aHeight, aRepaint); +} + //------------------------------------------------------------------------- // // Close this nsBaseWidget @@ -770,18 +812,18 @@ NS_METHOD nsBaseWidget::GetScreenBounds(nsIntRect &aRect) return GetBounds(aRect); } -/** -* -* -**/ +NS_METHOD nsBaseWidget::GetClientOffset(nsIntPoint &aPt) +{ + aPt.x = aPt.y = 0; + return NS_OK; +} + NS_METHOD nsBaseWidget::SetBounds(const nsIntRect &aRect) { mBounds = aRect; return NS_OK; } - - NS_METHOD nsBaseWidget::EnableDragDrop(PRBool aEnable) { diff --git a/widget/src/xpwidgets/nsBaseWidget.h b/widget/src/xpwidgets/nsBaseWidget.h index 1ea12de27341..4e07a41fa4f1 100644 --- a/widget/src/xpwidgets/nsBaseWidget.h +++ b/widget/src/xpwidgets/nsBaseWidget.h @@ -116,6 +116,7 @@ public: NS_IMETHOD GetBounds(nsIntRect &aRect); NS_IMETHOD GetClientBounds(nsIntRect &aRect); NS_IMETHOD GetScreenBounds(nsIntRect &aRect); + NS_IMETHOD GetClientOffset(nsIntPoint &aPt); NS_IMETHOD EnableDragDrop(PRBool aEnable); NS_IMETHOD GetAttention(PRInt32 aCycleCount); virtual PRBool HasPendingInputEvent(); @@ -143,6 +144,10 @@ public: NS_IMETHOD OnIMESelectionChange(void) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHOD OnDefaultButtonLoaded(const nsIntRect &aButtonRect) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHOD OverrideSystemMouseScrollSpeed(PRInt32 aOriginalDelta, PRBool aIsHorizontal, PRInt32 &aOverriddenDelta); + NS_IMETHOD AttachViewToTopLevel(EVENT_CALLBACK aViewEventFunction, nsIDeviceContext *aContext); + virtual ViewWrapper* GetAttachedViewPtr(); + NS_IMETHOD SetAttachedViewPtr(ViewWrapper* aViewWrapper); + NS_IMETHOD ResizeClient(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, PRBool aRepaint); /** * Use this when GetLayerManager() returns a BasicLayerManager @@ -195,7 +200,9 @@ protected: protected: void* mClientData; + ViewWrapper* mViewWrapperPtr; EVENT_CALLBACK mEventCallback; + EVENT_CALLBACK mViewCallback; nsIDeviceContext* mContext; nsIToolkit* mToolkit; nsRefPtr mLayerManager; From 09254ba6e560590589c40e810a6ac283b5bca356 Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:06 -0500 Subject: [PATCH 1807/1860] Bug 513162 - Widget additions for recycling top level widgets as content containers. r=dbaron. --- view/public/nsIView.h | 25 ++++++- view/src/nsView.cpp | 142 +++++++++++++++++++++++++++++++++---- view/src/nsViewManager.cpp | 38 ++++++++-- view/src/nsViewManager.h | 14 +--- 4 files changed, 184 insertions(+), 35 deletions(-) diff --git a/view/public/nsIView.h b/view/public/nsIView.h index 659405b340cc..081238a7ee79 100644 --- a/view/public/nsIView.h +++ b/view/public/nsIView.h @@ -63,8 +63,8 @@ enum nsViewVisibility { // IID for the nsIView interface #define NS_IVIEW_IID \ - { 0xe981334b, 0x756e, 0x417a, \ - { 0xbf, 0x18, 0x47, 0x4a, 0x2d, 0xfe, 0xc3, 0x87 } } + { 0xdb512cfa, 0xe00c, 0x4eff, \ + { 0xa2, 0x9c, 0x18, 0x74, 0x96, 0x63, 0x17, 0x69 } } // Public view flags are defined in this file #define NS_VIEW_FLAGS_PUBLIC 0x00FF @@ -291,6 +291,26 @@ public: nsContentType aWindowType = eContentTypeInherit, nsIWidget* aParentWidget = nsnull); + /** + * Attach/detach a top level widget from this view. When attached, the view + * updates the widget's device context and allows the view to begin receiving + * gecko events. The underlying base window associated with the widget will + * continues to receive events it expects. + * + * An attached widget will not be destroyed when the view is destroyed, + * allowing the recycling of a single top level widget over multiple views. + * + * @param aWidget The widget to attach to / detach from. + */ + nsresult AttachToTopLevelWidget(nsIWidget* aWidget); + nsresult DetachFromTopLevelWidget(); + + /** + * Returns a flag indicating whether the view owns it's widget + * or is attached to an existing top level widget. + */ + PRBool IsAttachedToTopLevel() const { return mWidgetIsTopLevel; } + /** * In 4.0, the "cutout" nature of a view is queryable. * If we believe that all cutout view have a native widget, this @@ -373,6 +393,7 @@ protected: float mOpacity; PRUint32 mVFlags; nsWeakView* mDeletionObserver; + PRBool mWidgetIsTopLevel; virtual ~nsIView() {} }; diff --git a/view/src/nsView.cpp b/view/src/nsView.cpp index e87e72750a4d..2f142223c8cf 100644 --- a/view/src/nsView.cpp +++ b/view/src/nsView.cpp @@ -144,15 +144,47 @@ static ViewWrapper* GetWrapperFor(nsIWidget* aWidget) return nsnull; } -// // Main events handler -// -nsEventStatus HandleEvent(nsGUIEvent *aEvent) -{ -//printf(" %d %d %d (%d,%d) \n", aEvent->widget, aEvent->widgetSupports, -// aEvent->message, aEvent->point.x, aEvent->point.y); +static nsEventStatus HandleEvent(nsGUIEvent *aEvent) +{ +#if 0 + printf(" %d %d %d (%d,%d) \n", aEvent->widget, aEvent->widgetSupports, + aEvent->message, aEvent->point.x, aEvent->point.y); +#endif nsEventStatus result = nsEventStatus_eIgnore; - nsView *view = nsView::GetViewFor(aEvent->widget); + nsView *view = nsView::GetViewFor(aEvent->widget); + + if (view) + { + nsCOMPtr vm = view->GetViewManager(); + vm->DispatchEvent(aEvent, view, &result); + } + + return result; +} + +// Attached widget event helpers +static ViewWrapper* GetAttachedWrapperFor(nsIWidget* aWidget) +{ + NS_PRECONDITION(nsnull != aWidget, "null widget ptr"); + return aWidget->GetAttachedViewPtr(); +} + +static nsView* GetAttachedViewFor(nsIWidget* aWidget) +{ + NS_PRECONDITION(nsnull != aWidget, "null widget ptr"); + + ViewWrapper* wrapper = GetAttachedWrapperFor(aWidget); + if (!wrapper) + return nsnull; + return wrapper->GetView(); +} + +// event handler +static nsEventStatus AttachedHandleEvent(nsGUIEvent *aEvent) +{ + nsEventStatus result = nsEventStatus_eIgnore; + nsView *view = GetAttachedViewFor(aEvent->widget); if (view) { @@ -176,6 +208,7 @@ nsView::nsView(nsViewManager* aViewManager, nsViewVisibility aVisibility) mViewManager = aViewManager; mDirtyRegion = nsnull; mDeletionObserver = nsnull; + mWidgetIsTopLevel = PR_FALSE; } void nsView::DropMouseGrabbing() @@ -240,8 +273,21 @@ nsView::~nsView() ViewWrapper* wrapper = GetWrapperFor(mWindow); NS_IF_RELEASE(wrapper); - mWindow->SetClientData(nsnull); - mWindow->Destroy(); + // If we are not attached to a base window, we're going to tear down our + // widget here. However, if we're attached to somebody elses widget, we + // want to leave the widget alone: don't reset the client data or call + // Destroy. Just clear our event view ptr and free our reference to it. + if (mWidgetIsTopLevel) { + ViewWrapper* wrapper = GetAttachedWrapperFor(mWindow); + NS_IF_RELEASE(wrapper); + + mWindow->SetAttachedViewPtr(nsnull); + } + else { + mWindow->SetClientData(nsnull); + mWindow->Destroy(); + } + NS_RELEASE(mWindow); } delete mDirtyRegion; @@ -275,8 +321,13 @@ nsIView* nsIView::GetViewFor(nsIWidget* aWidget) NS_PRECONDITION(nsnull != aWidget, "null widget ptr"); ViewWrapper* wrapper = GetWrapperFor(aWidget); + + if (!wrapper) + wrapper = GetAttachedWrapperFor(aWidget); + if (wrapper) - return wrapper->GetView(); + return wrapper->GetView(); + return nsnull; } @@ -354,7 +405,7 @@ nsIntRect nsIView::CalcWidgetBounds(nsWindowType aType) if (parentWidget && aType == eWindowType_popup && IsEffectivelyVisible()) { - // put offset into screen coordinates + // put offset into screen coordinates. (based on client area origin) nsIntPoint screenPoint = parentWidget->WidgetToScreenOffset(); viewBounds += nsPoint(NSIntPixelsToAppUnits(screenPoint.x, p2a), NSIntPixelsToAppUnits(screenPoint.y, p2a)); @@ -390,6 +441,7 @@ void nsView::DoResetWidgetBounds(PRBool aMoveOnly, nsIntRect curBounds; mWindow->GetBounds(curBounds); + nsWindowType type; mWindow->GetWindowType(type); @@ -409,6 +461,7 @@ void nsView::DoResetWidgetBounds(PRBool aMoveOnly, PRBool changedPos = curBounds.TopLeft() != newBounds.TopLeft(); PRBool changedSize = curBounds.Size() != newBounds.Size(); + // Child views are never attached to top level widgets, this is safe. if (changedPos) { if (changedSize && !aMoveOnly) { mWindow->Resize(newBounds.x, newBounds.y, newBounds.width, newBounds.height, @@ -704,6 +757,62 @@ nsresult nsIView::CreateWidget(const nsIID &aWindowIID, return NS_OK; } +// Attach to a top level widget and start receiving mirrored events. +nsresult nsIView::AttachToTopLevelWidget(nsIWidget* aWidget) +{ + NS_PRECONDITION(nsnull != aWidget, "null widget ptr"); + /// XXXjimm This is a temporary workaround to an issue w/document + // viewer (bug 513162). + nsIView *oldView = GetAttachedViewFor(aWidget); + if (oldView) { + oldView->DetachFromTopLevelWidget(); + } + + nsCOMPtr dx; + mViewManager->GetDeviceContext(*getter_AddRefs(dx)); + + // Note, the previous device context will be released. Detaching + // will not restore the old one. + nsresult rv = aWidget->AttachViewToTopLevel(::AttachedHandleEvent, dx); + if (NS_FAILED(rv)) + return rv; + + mWindow = aWidget; + NS_ADDREF(mWindow); + + nsView* v = static_cast(this); + ViewWrapper* wrapper = new ViewWrapper(v); + NS_ADDREF(wrapper); + mWindow->SetAttachedViewPtr(wrapper); + mWindow->EnableDragDrop(PR_TRUE); + mWidgetIsTopLevel = PR_TRUE; + + // Refresh the view bounds + nsWindowType type; + mWindow->GetWindowType(type); + CalcWidgetBounds(type); + + return NS_OK; +} + +// Detach this view from an attached widget. +nsresult nsIView::DetachFromTopLevelWidget() +{ + NS_PRECONDITION(mWidgetIsTopLevel, "Not attached currently!"); + NS_PRECONDITION(mWindow, "null mWindow for DetachFromTopLevelWidget!"); + + // Release memory for the view wrapper + ViewWrapper* wrapper = GetAttachedWrapperFor(mWindow); + NS_IF_RELEASE(wrapper); + + mWindow->SetAttachedViewPtr(nsnull); + NS_RELEASE(mWindow); + + mWidgetIsTopLevel = PR_FALSE; + + return NS_OK; +} + void nsView::SetZIndex(PRBool aAuto, PRInt32 aZIndex, PRBool aTopMost) { PRBool oldIsAuto = GetZIndexIsAuto(); @@ -834,6 +943,8 @@ nsIntPoint nsIView::GetScreenPosition() const PRInt32 p2a = dx->AppUnitsPerDevPixel(); nsIntPoint ourPoint(NSAppUnitsToIntPixels(toWidgetOffset.x, p2a), NSAppUnitsToIntPixels(toWidgetOffset.y, p2a)); + // WidgetToScreenOffset is at the origin of the client area of + // the widget. screenPoint = ourPoint + widget->WidgetToScreenOffset(); } @@ -842,6 +953,9 @@ nsIntPoint nsIView::GetScreenPosition() const nsIWidget* nsIView::GetNearestWidget(nsPoint* aOffset) const { + // aOffset is based on the view's position, which ignores any chrome on + // attached parent widgets. + nsPoint pt(0, 0); const nsView* v; for (v = static_cast(this); @@ -855,9 +969,9 @@ nsIWidget* nsIView::GetNearestWidget(nsPoint* aOffset) const return nsnull; } - // pt is now the offset from v's origin to this's origin - // The widget's origin is the top left corner of v's bounds, which may - // not coincide with v's origin + // pt is now the offset from v's origin to this view's origin. The widget's + // origin is the top left corner of v's bounds, which may not coincide with + // the view's origin. if (aOffset) { nsRect vBounds = v->GetBounds(); *aOffset = pt + v->GetPosition() - nsPoint(vBounds.x, vBounds.y) + diff --git a/view/src/nsViewManager.cpp b/view/src/nsViewManager.cpp index 76e359eb4e1e..eaf9801ea8c9 100644 --- a/view/src/nsViewManager.cpp +++ b/view/src/nsViewManager.cpp @@ -304,6 +304,20 @@ NS_IMETHODIMP nsViewManager::GetWindowDimensions(nscoord *aWidth, nscoord *aHeig return NS_OK; } +void nsViewManager::DoSetWindowDimensions(nscoord aWidth, nscoord aHeight) +{ + nsRect oldDim; + nsRect newDim(0, 0, aWidth, aHeight); + mRootView->GetDimensions(oldDim); + // We care about resizes even when one dimension is already zero. + if (!oldDim.IsExactEqual(newDim)) { + // Don't resize the widget. It is already being set elsewhere. + mRootView->SetDimensions(newDim, PR_TRUE, PR_FALSE); + if (mObserver) + mObserver->ResizeReflow(mRootView, aWidth, aHeight); + } +} + NS_IMETHODIMP nsViewManager::SetWindowDimensions(nscoord aWidth, nscoord aHeight) { if (mRootView) { @@ -578,6 +592,13 @@ nsViewManager::UpdateWidgetArea(nsView *aWidgetView, nsIWidget* aWidget, const nsRegion &aDamagedRegion, nsView* aIgnoreWidgetView) { +#if 0 + nsRect dbgBounds = aDamagedRegion.GetBounds(); + printf("UpdateWidgetArea view:%X (%d) widget:%X region: %d, %d, %d, %d\n", + aWidgetView, aWidgetView->IsAttachedToTopLevel(), + aWidget, dbgBounds.x, dbgBounds.y, dbgBounds.width, dbgBounds.height); +#endif + if (!IsRefreshEnabled()) { // accumulate this rectangle in the view's dirty region, so we can // process it later. @@ -639,14 +660,19 @@ nsViewManager::UpdateWidgetArea(nsView *aWidgetView, nsIWidget* aWidget, // Don't mess with views that are in completely different view // manager trees if (view->GetViewManager()->RootViewManager() == RootViewManager()) { - // get the damage region into 'view's coordinate system + // get the damage region into view's coordinate system nsRegion damage = intersection; + nsPoint offset = view->GetOffsetTo(aWidgetView); damage.MoveBy(-offset); + + // Update the child and it's children UpdateWidgetArea(view, childWidget, damage, aIgnoreWidgetView); + // GetBounds should compensate for chrome on a toplevel widget nsIntRect bounds; childWidget->GetBounds(bounds); + nsTArray clipRects; childWidget->GetWindowClipRegion(&clipRects); for (PRUint32 i = 0; i < clipRects.Length(); ++i) { @@ -741,10 +767,9 @@ NS_IMETHODIMP nsViewManager::DispatchEvent(nsGUIEvent *aEvent, { if (aView) { + // client area dimensions are set on the view nscoord width = ((nsSizeEvent*)aEvent)->windowSize->width; nscoord height = ((nsSizeEvent*)aEvent)->windowSize->height; - width = ((nsSizeEvent*)aEvent)->mWinWidth; - height = ((nsSizeEvent*)aEvent)->mWinHeight; // The root view may not be set if this is the resize associated with // window creation @@ -1011,9 +1036,10 @@ NS_IMETHODIMP nsViewManager::DispatchEvent(nsGUIEvent *aEvent, nsEventStatus nsViewManager::HandleEvent(nsView* aView, nsGUIEvent* aEvent) { -//printf(" %d %d %d %d (%d,%d) \n", this, event->widget, event->widgetSupports, -// event->message, event->point.x, event->point.y); - +#if 0 + printf(" %d %d %d %d (%d,%d) \n", this, event->widget, event->widgetSupports, + event->message, event->point.x, event->point.y); +#endif // Hold a refcount to the observer. The continued existence of the observer will // delay deletion of this view hierarchy should the event want to cause its // destruction in, say, some JavaScript event handler. diff --git a/view/src/nsViewManager.h b/view/src/nsViewManager.h index c44b0caa07b5..1c7ffc03aa24 100644 --- a/view/src/nsViewManager.h +++ b/view/src/nsViewManager.h @@ -209,19 +209,7 @@ private: */ nsIntRect ViewToWidget(nsView *aView, nsView* aWidgetView, const nsRect &aRect) const; - void DoSetWindowDimensions(nscoord aWidth, nscoord aHeight) - { - nsRect oldDim; - nsRect newDim(0, 0, aWidth, aHeight); - mRootView->GetDimensions(oldDim); - // We care about resizes even when one dimension is already zero. - if (!oldDim.IsExactEqual(newDim)) { - // Don't resize the widget. It is already being set elsewhere. - mRootView->SetDimensions(newDim, PR_TRUE, PR_FALSE); - if (mObserver) - mObserver->ResizeReflow(mRootView, aWidth, aHeight); - } - } + void DoSetWindowDimensions(nscoord aWidth, nscoord aHeight); // Safety helpers void IncrementUpdateCount() { From 573f11856cc7d2e881bc198e1248f729382e081c Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:06 -0500 Subject: [PATCH 1808/1860] Bug 513162 - Document viewer to widget glue. r=bz. --- layout/base/nsDocumentViewer.cpp | 69 ++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp index 41068686d6a2..671623d86b7b 100644 --- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -415,6 +415,8 @@ protected: nsPresContext* GetPresContext(); nsIViewManager* GetViewManager(); + void DetachFromTopLevelWidget(); + // IMPORTANT: The ownership implicit in the following member // variables has been explicitly checked and set using nsCOMPtr // for owning pointers and raw COM interface pointers for weak @@ -439,6 +441,7 @@ protected: nsCOMPtr mSHEntry; nsIWidget* mParentWidget; // purposely won't be ref counted. May be null + PRBool mAttachedToParent; // view is attached to the parent widget nsIntRect mBounds; @@ -520,6 +523,7 @@ void DocumentViewerImpl::PrepareToStartLoad() { mStopped = PR_FALSE; mLoaded = PR_FALSE; + mAttachedToParent = PR_FALSE; mDeferredWindowClose = PR_FALSE; mCallerIsClosingWindow = PR_FALSE; @@ -1851,10 +1855,18 @@ DocumentViewerImpl::SetBounds(const nsIntRect& aBounds) mBounds = aBounds; if (mWindow) { + // When attached to a top level window, change the client area, not the + // window frame. // Don't have the widget repaint. Layout will generate repaint requests - // during reflow - mWindow->Resize(aBounds.x, aBounds.y, aBounds.width, aBounds.height, - PR_FALSE); + // during reflow. + if (mAttachedToParent) + mWindow->ResizeClient(aBounds.x, aBounds.y, + aBounds.width, aBounds.height, + PR_FALSE); + else + mWindow->Resize(aBounds.x, aBounds.y, + aBounds.width, aBounds.height, + PR_FALSE); } else if (mPresContext && mViewManager) { PRInt32 p2a = mPresContext->AppUnitsPerDevPixel(); mViewManager->SetWindowDimensions(NSIntPixelsToAppUnits(mBounds.width, p2a), @@ -2235,6 +2247,29 @@ DocumentViewerImpl::MakeWindow(const nsSize& aSize, nsIView* aContainerView) if (GetIsPrintPreview()) return NS_OK; + // Prior to creating a new widget, check to see if our parent is a base + // chrome ui window. If so, drop the content into that widget instead of + // creating a new child widget. This eliminates the main content child + // widget we've had forever. Also allows for the recycling of the base + // widget of each window, vs. creating/discarding child widgets for each + // MakeWindow call. (Currently only implemented in windows widgets.) +#ifdef XP_WIN + nsCOMPtr containerItem = do_QueryReferent(mContainer); + if (mParentWidget && containerItem) { + PRInt32 docType; + nsWindowType winType; + containerItem->GetItemType(&docType); + mParentWidget->GetWindowType(winType); + if (winType == eWindowType_toplevel && + docType == nsIDocShellTreeItem::typeChrome) { + // If the old view is already attached to our parent, detach + DetachFromTopLevelWidget(); + // Use the parent widget + mAttachedToParent = PR_TRUE; + } + } +#endif + nsresult rv; mViewManager = do_CreateInstance(kViewManagerCID, &rv); if (NS_FAILED(rv)) @@ -2271,10 +2306,17 @@ DocumentViewerImpl::MakeWindow(const nsSize& aSize, nsIView* aContainerView) } else { initDataPtr = nsnull; } - rv = view->CreateWidget(kWidgetCID, initDataPtr, - (aContainerView != nsnull || !mParentWidget) ? - nsnull : mParentWidget->GetNativeData(NS_NATIVE_WIDGET), - PR_TRUE, PR_FALSE); + + if (mAttachedToParent) { + // Reuse the top level parent widget. + rv = view->AttachToTopLevelWidget(mParentWidget); + } + else { + nsNativeWidget nw = (aContainerView != nsnull || !mParentWidget) ? + nsnull : mParentWidget->GetNativeData(NS_NATIVE_WIDGET); + rv = view->CreateWidget(kWidgetCID, initDataPtr, + nw, PR_TRUE, PR_FALSE); + } if (NS_FAILED(rv)) return rv; } @@ -2292,6 +2334,19 @@ DocumentViewerImpl::MakeWindow(const nsSize& aSize, nsIView* aContainerView) return rv; } +void +DocumentViewerImpl::DetachFromTopLevelWidget() +{ + if (mViewManager) { + nsIView* oldView = nsnull; + mViewManager->GetRootView(oldView); + if (oldView && oldView->IsAttachedToTopLevel()) { + oldView->DetachFromTopLevelWidget(); + } + } + mAttachedToParent = PR_FALSE; +} + nsIView* DocumentViewerImpl::FindContainerView() { From 3c02389d65135ebb39e2510dd6decf7963d96747 Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:06 -0500 Subject: [PATCH 1809/1860] Bug 513162 - Widget support for view event mirroring. r=vlad. --- widget/src/windows/nsWindow.cpp | 35 +++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/widget/src/windows/nsWindow.cpp b/widget/src/windows/nsWindow.cpp index 4ecc3fd03ee7..44c2c36f4e4d 100644 --- a/widget/src/windows/nsWindow.cpp +++ b/widget/src/windows/nsWindow.cpp @@ -2129,9 +2129,13 @@ void nsWindow::UpdatePossiblyTransparentRegion(const nsIntRegion &aDirtyRegion, nsIntRegion childWindowRegion; ::EnumChildWindows(mWnd, AddClientAreaToRegion, reinterpret_cast(&childWindowRegion)); + + nsIntPoint clientOffset; + GetClientOffset(clientOffset); + childWindowRegion.MoveBy(-clientOffset); + RECT r; ::GetWindowRect(mWnd, &r); - childWindowRegion.MoveBy(-r.left, -r.top); nsIntRect clientBounds; @@ -3298,7 +3302,34 @@ NS_IMETHODIMP nsWindow::DispatchEvent(nsGUIEvent* event, nsEventStatus & aStatus if (event->message == NS_DEACTIVATE && BlurEventsSuppressed()) return NS_OK; - if (nsnull != mEventCallback) { + // Top level windows can have a view attached which requires events be sent + // to the underlying base window and the view. Added when we combined the + // base chrome window with the main content child for nc client area (title + // bar) rendering. + if (mViewCallback) { + // A subset of events are sent to the base xul window first + switch(event->message) { + // send to the base window (view mgr ignores these for the view) + case NS_UISTATECHANGED: + case NS_DESTROY: + case NS_SETZLEVEL: + case NS_XUL_CLOSE: + case NS_MOVE: + (*mEventCallback)(event); // web shell / xul window + return NS_OK; + + // sent to the base window, then to the view + case NS_SIZE: + case NS_DEACTIVATE: + case NS_ACTIVATE: + case NS_SIZEMODE: + (*mEventCallback)(event); // web shell / xul window + break; + }; + // attached view events + aStatus = (*mViewCallback)(event); + } + else if (mEventCallback) { aStatus = (*mEventCallback)(event); } From c6c426c3b208c3874a538dd17dd22b0cfc2c0137 Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:06 -0500 Subject: [PATCH 1810/1860] Bug 513162 - Add tests for client coordinates of subframes. r=sdwilsh. --- toolkit/content/tests/chrome/Makefile.in | 4 + .../frame_subframe_origin_subframe1.xul | 75 +++++++++++++++++++ .../frame_subframe_origin_subframe2.xul | 71 ++++++++++++++++++ .../tests/chrome/test_subframe_origin.xul | 71 ++++++++++++++++++ .../tests/chrome/window_subframe_origin.xul | 74 ++++++++++++++++++ 5 files changed, 295 insertions(+) create mode 100644 toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xul create mode 100644 toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xul create mode 100644 toolkit/content/tests/chrome/test_subframe_origin.xul create mode 100644 toolkit/content/tests/chrome/window_subframe_origin.xul diff --git a/toolkit/content/tests/chrome/Makefile.in b/toolkit/content/tests/chrome/Makefile.in index a652911f7da4..01305aa6871f 100644 --- a/toolkit/content/tests/chrome/Makefile.in +++ b/toolkit/content/tests/chrome/Makefile.in @@ -99,6 +99,10 @@ _TEST_FILES = findbar_window.xul \ window_titlebar.xul \ test_browser_drop.xul \ window_browser_drop.xul \ + test_subframe_origin.xul \ + window_subframe_origin.xul \ + frame_subframe_origin_subframe1.xul \ + frame_subframe_origin_subframe2.xul \ RegisterUnregisterChrome.js \ $(NULL) diff --git a/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xul b/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xul new file mode 100644 index 000000000000..e5fcb5ac76e5 --- /dev/null +++ b/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xul @@ -0,0 +1,75 @@ + + + + + + + diff --git a/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xul b/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xul new file mode 100644 index 000000000000..a6c9a7c44abd --- /dev/null +++ b/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xul @@ -0,0 +1,71 @@ + + + + + + + diff --git a/toolkit/content/tests/chrome/test_subframe_origin.xul b/toolkit/content/tests/chrome/test_subframe_origin.xul new file mode 100644 index 000000000000..b7901675b87c --- /dev/null +++ b/toolkit/content/tests/chrome/test_subframe_origin.xul @@ -0,0 +1,71 @@ + + + + + + + + + + +

+

+ +
+
+ + + diff --git a/toolkit/content/tests/chrome/window_subframe_origin.xul b/toolkit/content/tests/chrome/window_subframe_origin.xul new file mode 100644 index 000000000000..0e8a1c7cfcec --- /dev/null +++ b/toolkit/content/tests/chrome/window_subframe_origin.xul @@ -0,0 +1,74 @@ + + + + + + + From 39d3f7ba6c2a2e7b5283f93db771602fc5bf9aeb Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:07 -0500 Subject: [PATCH 1811/1860] Bug 513162 - Add 'top, right, bottom, left' margin support in nsAttrValue. r=smaug. --- content/base/public/nsContentUtils.h | 10 +++++ content/base/public/nsIContentUtils.h | 9 +++-- content/base/src/nsAttrValue.cpp | 55 +++++++++++++++++++++++++++ content/base/src/nsAttrValue.h | 26 +++++++++++++ content/base/src/nsContentUtils.cpp | 53 ++++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 3 deletions(-) diff --git a/content/base/public/nsContentUtils.h b/content/base/public/nsContentUtils.h index b5faf23d7c89..7da4116c9828 100644 --- a/content/base/public/nsContentUtils.h +++ b/content/base/public/nsContentUtils.h @@ -401,6 +401,16 @@ public: */ static PRBool IsHTMLWhitespace(PRUnichar aChar); + /** + * Parse a margin string of format 'top, right, bottom, left' into + * an nsIntMargin. + * + * @param aString the string to parse + * @param aResult the resulting integer + * @return whether the value could be parsed + */ + static PRBool ParseIntMarginValue(const nsAString& aString, nsIntMargin& aResult); + static void Shutdown(); /** diff --git a/content/base/public/nsIContentUtils.h b/content/base/public/nsIContentUtils.h index d2c47f9b6d8a..196d6bbd6715 100644 --- a/content/base/public/nsIContentUtils.h +++ b/content/base/public/nsIContentUtils.h @@ -39,11 +39,13 @@ #include "nsIDocumentLoaderFactory.h" #include "nsCOMPtr.h" +#include "nsAString.h" +#include "nsMargin.h" -// C4EA618E-A3D9-4524-8EEA-E92F26FC44DB +// {3682DD99-8560-44f4-9B8F-CCCE9D7B96FB} #define NS_ICONTENTUTILS_IID \ -{ 0xC4EA618E, 0xA3D9, 0x4524, \ - { 0x8E, 0xEA, 0xE9, 0x2F, 0x26, 0xFC, 0x44, 0xDB } } +{ 0x3682dd99, 0x8560, 0x44f4, \ + { 0x9b, 0x8f, 0xcc, 0xce, 0x9d, 0x7b, 0x96, 0xfb } } class nsIContentUtils : public nsISupports { @@ -52,6 +54,7 @@ public: NS_DECL_ISUPPORTS virtual PRBool IsSafeToRunScript(); + virtual PRBool ParseIntMarginValue(const nsAString& aString, nsIntMargin& result); enum ContentViewerType { diff --git a/content/base/src/nsAttrValue.cpp b/content/base/src/nsAttrValue.cpp index c73209642898..5546dafa0d38 100644 --- a/content/base/src/nsAttrValue.cpp +++ b/content/base/src/nsAttrValue.cpp @@ -92,6 +92,12 @@ nsAttrValue::nsAttrValue(nsISVGValue* aValue) } #endif +nsAttrValue::nsAttrValue(const nsIntMargin& aValue) + : mBits(0) +{ + SetTo(aValue); +} + nsAttrValue::~nsAttrValue() { ResetIfSet(); @@ -259,6 +265,12 @@ nsAttrValue::SetTo(const nsAttrValue& aOther) cont->mFloatValue = otherCont->mFloatValue; break; } + case eIntMarginValue: + { + if (otherCont->mIntMargin) + cont->mIntMargin = new nsIntMargin(*otherCont->mIntMargin); + break; + } default: { NS_NOTREACHED("unknown type stored in MiscContainer"); @@ -320,6 +332,16 @@ nsAttrValue::SetTo(nsISVGValue* aValue) } #endif +void +nsAttrValue::SetTo(const nsIntMargin& aValue) +{ + if (EnsureEmptyMiscContainer()) { + MiscContainer* cont = GetMiscContainer(); + cont->mIntMargin = new nsIntMargin(aValue); + cont->mType = eIntMarginValue; + } +} + void nsAttrValue::SwapValueWith(nsAttrValue& aOther) { @@ -585,6 +607,10 @@ nsAttrValue::HashValue() const // XXX this is crappy, but oh well return cont->mFloatValue; } + case eIntMarginValue: + { + return NS_PTR_TO_INT32(cont->mIntMargin); + } default: { NS_NOTREACHED("unknown type stored in MiscContainer"); @@ -687,6 +713,10 @@ nsAttrValue::Equals(const nsAttrValue& aOther) const { return thisCont->mFloatValue == otherCont->mFloatValue; } + case eIntMarginValue: + { + return thisCont->mIntMargin == otherCont->mIntMargin; + } default: { NS_NOTREACHED("unknown type stored in MiscContainer"); @@ -1204,6 +1234,26 @@ PRBool nsAttrValue::ParseFloatValue(const nsAString& aString) return PR_FALSE; } +PRBool +nsAttrValue::ParseIntMarginValue(const nsAString& aString) +{ + ResetIfSet(); + + nsIntMargin margins; + if (!nsContentUtils::ParseIntMarginValue(aString, margins)) + return PR_FALSE; + + if (EnsureEmptyMiscContainer()) { + MiscContainer* cont = GetMiscContainer(); + cont->mIntMargin = new nsIntMargin(margins); + cont->mType = eIntMarginValue; + SetMiscAtomOrString(&aString); + return PR_TRUE; + } + + return PR_FALSE; +} + void nsAttrValue::SetMiscAtomOrString(const nsAString* aValue) { @@ -1269,6 +1319,11 @@ nsAttrValue::EnsureEmptyMiscContainer() break; } #endif + case eIntMarginValue: + { + delete cont->mIntMargin; + break; + } default: { break; diff --git a/content/base/src/nsAttrValue.h b/content/base/src/nsAttrValue.h index 1ee64aee1420..d08eacf89463 100644 --- a/content/base/src/nsAttrValue.h +++ b/content/base/src/nsAttrValue.h @@ -49,6 +49,7 @@ #include "nsStringBuffer.h" #include "nsColor.h" #include "nsCaseTreatment.h" +#include "nsMargin.h" typedef PRUptrdiff PtrBits; class nsAString; @@ -98,6 +99,7 @@ public: #ifdef MOZ_SVG explicit nsAttrValue(nsISVGValue* aValue); #endif + explicit nsAttrValue(const nsIntMargin& aValue); ~nsAttrValue(); static nsresult Init(); @@ -120,6 +122,7 @@ public: ,eSVGValue = 0x12 #endif ,eFloatValue = 0x13 + ,eIntMarginValue = 0x14 }; ValueType Type() const; @@ -133,6 +136,7 @@ public: #ifdef MOZ_SVG void SetTo(nsISVGValue* aValue); #endif + void SetTo(const nsIntMargin& aValue); void SwapValueWith(nsAttrValue& aOther); @@ -153,6 +157,7 @@ public: inline nsISVGValue* GetSVGValue() const; #endif inline float GetFloatValue() const; + PRBool GetIntMarginValue(nsIntMargin& aMargin) const; /** * Returns the string corresponding to the stored enum value. @@ -297,6 +302,15 @@ public: */ PRBool ParseLazyURIValue(const nsAString& aString); + /** + * Parse a margin string of format 'top, right, bottom, left' into + * an nsIntMargin. + * + * @param aString the string to parse + * @return whether the value could be parsed + */ + PRBool ParseIntMarginValue(const nsAString& aString); + private: // These have to be the same as in ValueType enum ValueBaseType { @@ -325,6 +339,7 @@ private: nsISVGValue* mSVGValue; #endif float mFloatValue; + nsIntMargin* mIntMargin; }; }; @@ -442,6 +457,17 @@ nsAttrValue::GetFloatValue() const return GetMiscContainer()->mFloatValue; } +inline PRBool +nsAttrValue::GetIntMarginValue(nsIntMargin& aMargin) const +{ + NS_PRECONDITION(Type() == eIntMarginValue, "wrong type"); + nsIntMargin* m = GetMiscContainer()->mIntMargin; + if (!m) + return PR_FALSE; + aMargin = *m; + return PR_TRUE; +} + inline nsAttrValue::ValueBaseType nsAttrValue::BaseType() const { diff --git a/content/base/src/nsContentUtils.cpp b/content/base/src/nsContentUtils.cpp index bb315efad4c4..0ab80be58ccb 100644 --- a/content/base/src/nsContentUtils.cpp +++ b/content/base/src/nsContentUtils.cpp @@ -924,6 +924,53 @@ nsContentUtils::IsHTMLWhitespace(PRUnichar aChar) aChar == PRUnichar(0x0020); } +/* static */ +PRBool +nsContentUtils::ParseIntMarginValue(const nsAString& aString, nsIntMargin& result) +{ + nsAutoString marginStr(aString); + marginStr.CompressWhitespace(PR_TRUE, PR_TRUE); + if (marginStr.IsEmpty()) { + return PR_FALSE; + } + + PRInt32 start = 0, end = 0; + for (int count = 0; count < 4; count++) { + if (end >= marginStr.Length()) + return PR_FALSE; + + // top, right, bottom, left + if (count < 3) + end = Substring(marginStr, start).FindChar(','); + else + end = Substring(marginStr, start).Length(); + + if (end <= 0) + return PR_FALSE; + + PRInt32 ec, val = + nsString(Substring(marginStr, start, end)).ToInteger(&ec); + if (NS_FAILED(ec)) + return PR_FALSE; + + switch(count) { + case 0: + result.top = val; + break; + case 1: + result.right = val; + break; + case 2: + result.bottom = val; + break; + case 3: + result.left = val; + break; + } + start += end + 1; + } + return PR_TRUE; +} /* static */ void @@ -6115,6 +6162,12 @@ nsIContentUtils::IsSafeToRunScript() return nsContentUtils::IsSafeToRunScript(); } +PRBool +nsIContentUtils::ParseIntMarginValue(const nsAString& aString, nsIntMargin& result) +{ + return nsContentUtils::ParseIntMarginValue(aString, result); +} + already_AddRefed nsIContentUtils::FindInternalContentViewer(const char* aType, ContentViewerType* aLoaderType) From b599e1df715c737349a1b1388d7c2b19051ffdbe Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:07 -0500 Subject: [PATCH 1812/1860] Bug 513162 - Widget additions for setting custom chrome margins. r=vlad. --- widget/public/nsIWidget.h | 46 ++++++++++++++++++--------- widget/src/xpwidgets/nsBaseWidget.cpp | 18 +++++++++++ widget/src/xpwidgets/nsBaseWidget.h | 2 ++ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/widget/public/nsIWidget.h b/widget/public/nsIWidget.h index a77a129a9c41..f32c4f846e55 100644 --- a/widget/public/nsIWidget.h +++ b/widget/public/nsIWidget.h @@ -514,34 +514,50 @@ class nsIWidget : public nsISupports { /** * Get this widget's outside dimensions relative to its parent widget * - * @param aRect on return it holds the x, y, width and height of this widget - * + * @param aRect On return it holds the x, y, width and height of + * this widget. */ NS_IMETHOD GetBounds(nsIntRect &aRect) = 0; - /** - * Get this widget's outside dimensions in global coordinates. (One might think this - * could be accomplished by stringing together other methods in this interface, but - * then one would bloody one's nose on different coordinate system handling by different - * platforms.) This includes any title bar on the window. - * - * - * @param aRect on return it holds the x, y, width and height of this widget + * Get this widget's outside dimensions in global coordinates. This + * includes any title bar on the window. * + * @param aRect On return it holds the x, y, width and height of + * this widget. */ NS_IMETHOD GetScreenBounds(nsIntRect &aRect) = 0; - /** - * Get this widget's client area dimensions, if the window has a 3D border appearance - * this returns the area inside the border, The x and y are always zero - * - * @param aRect on return it holds the x. y, width and height of the client area of this widget + * Get this widget's client area dimensions, if the window has a 3D + * border appearance this returns the area inside the border. Origin + * is always zero. * + * @param aRect On return it holds the x. y, width and height of + * the client area of this widget. */ NS_IMETHOD GetClientBounds(nsIntRect &aRect) = 0; + /** + * Get the non-client area dimensions of the window. + * + */ + NS_IMETHOD GetNonClientMargins(nsIntMargin &margins) = 0; + + /** + * Sets the non-client area dimensions of the window. Pass -1 to restore + * the system default frame size for that border. Pass zero to remove + * a border, or pass a specific value adjust a border. Units are in + * pixels. (DPI dependent) + * + * Platform notes: + * Windows: shrinking top non-client height will remove application + * icon and window title text. Glass desktops will refuse to set + * dimensions between zero and size < system default. + * + */ + NS_IMETHOD SetNonClientMargins(nsIntMargin &margins) = 0; + /** * Get the client offset from the window origin. * diff --git a/widget/src/xpwidgets/nsBaseWidget.cpp b/widget/src/xpwidgets/nsBaseWidget.cpp index 4922755801e9..ac583ebfe2fc 100644 --- a/widget/src/xpwidgets/nsBaseWidget.cpp +++ b/widget/src/xpwidgets/nsBaseWidget.cpp @@ -783,6 +783,12 @@ NS_METHOD nsBaseWidget::SetWindowClass(const nsAString& xulWinType) return NS_ERROR_NOT_IMPLEMENTED; } +//------------------------------------------------------------------------- +// +// Bounds +// +//------------------------------------------------------------------------- + /** * If the implementation of nsWindow supports borders this method MUST be overridden * @@ -825,6 +831,18 @@ NS_METHOD nsBaseWidget::SetBounds(const nsIntRect &aRect) return NS_OK; } +NS_IMETHODIMP +nsBaseWidget::GetNonClientMargins(nsIntMargin &margins) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsBaseWidget::SetNonClientMargins(nsIntMargin &margins) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + NS_METHOD nsBaseWidget::EnableDragDrop(PRBool aEnable) { return NS_OK; diff --git a/widget/src/xpwidgets/nsBaseWidget.h b/widget/src/xpwidgets/nsBaseWidget.h index 4e07a41fa4f1..a01e4bdff409 100644 --- a/widget/src/xpwidgets/nsBaseWidget.h +++ b/widget/src/xpwidgets/nsBaseWidget.h @@ -148,6 +148,8 @@ public: virtual ViewWrapper* GetAttachedViewPtr(); NS_IMETHOD SetAttachedViewPtr(ViewWrapper* aViewWrapper); NS_IMETHOD ResizeClient(PRInt32 aX, PRInt32 aY, PRInt32 aWidth, PRInt32 aHeight, PRBool aRepaint); + NS_IMETHOD GetNonClientMargins(nsIntMargin &margins); + NS_IMETHOD SetNonClientMargins(nsIntMargin &margins); /** * Use this when GetLayerManager() returns a BasicLayerManager From 2946f4d4dd18e3753ef9b760a1e136429c43cf22 Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:07 -0500 Subject: [PATCH 1813/1860] Bug 513162 - Add support for 'chromemargin' property on XUL windows. r=smaug. --- content/base/src/nsGkAtomList.h | 1 + content/xul/content/src/nsXULElement.cpp | 65 +++++++++++++++++++++--- content/xul/content/src/nsXULElement.h | 4 +- xpfe/appshell/src/nsXULWindow.cpp | 10 ++++ 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/content/base/src/nsGkAtomList.h b/content/base/src/nsGkAtomList.h index 6b06ae609c9c..b13f0c74feea 100644 --- a/content/base/src/nsGkAtomList.h +++ b/content/base/src/nsGkAtomList.h @@ -194,6 +194,7 @@ GK_ATOM(checked, "checked") GK_ATOM(child, "child") GK_ATOM(children, "children") GK_ATOM(choose, "choose") +GK_ATOM(chromemargin, "chromemargin") GK_ATOM(circ, "circ") GK_ATOM(circle, "circle") GK_ATOM(cite, "cite") diff --git a/content/xul/content/src/nsXULElement.cpp b/content/xul/content/src/nsXULElement.cpp index ab68349e86c3..5254148f848c 100644 --- a/content/xul/content/src/nsXULElement.cpp +++ b/content/xul/content/src/nsXULElement.cpp @@ -1076,6 +1076,17 @@ nsXULElement::BeforeSetAttr(PRInt32 aNamespaceID, nsIAtom* aName, RemoveBroadcaster(oldValue); } } + else if (aNamespaceID == kNameSpaceID_None && + aValue && + mNodeInfo->Equals(nsGkAtoms::window) && + aName == nsGkAtoms::chromemargin) { + nsAttrValue attrValue; + nsIntMargin margins; + // Make sure the margin format is valid first + if (!attrValue.ParseIntMarginValue(*aValue)) { + return NS_ERROR_INVALID_ARG; + } + } return nsStyledElement::BeforeSetAttr(aNamespaceID, aName, aValue, aNotify); @@ -1103,10 +1114,13 @@ nsXULElement::AfterSetAttr(PRInt32 aNamespaceID, nsIAtom* aName, } // Hide chrome if needed - if (aName == nsGkAtoms::hidechrome && - mNodeInfo->Equals(nsGkAtoms::window) && - aValue) { - HideWindowChrome(aValue->EqualsLiteral("true")); + if (mNodeInfo->Equals(nsGkAtoms::window) && aValue) { + if (aName == nsGkAtoms::hidechrome) { + HideWindowChrome(aValue->EqualsLiteral("true")); + } + else if (aName == nsGkAtoms::chromemargin) { + SetChromeMargins(aValue); + } } // title, (in)activetitlebarcolor and drawintitlebar are settable on @@ -1368,9 +1382,13 @@ nsXULElement::UnsetAttr(PRInt32 aNameSpaceID, nsIAtom* aName, PRBool aNotify) } if (aNameSpaceID == kNameSpaceID_None) { - if (aName == nsGkAtoms::hidechrome && - mNodeInfo->Equals(nsGkAtoms::window)) { - HideWindowChrome(PR_FALSE); + if (mNodeInfo->Equals(nsGkAtoms::window)) { + if (aName == nsGkAtoms::hidechrome) { + HideWindowChrome(PR_FALSE); + } + else if (aName == nsGkAtoms::chromemargin) { + ResetChromeMargins(); + } } if (doc && doc->GetRootElement() == this) { @@ -2395,6 +2413,39 @@ nsXULElement::SetDrawsInTitlebar(PRBool aState) } } +void +nsXULElement::SetChromeMargins(const nsAString* aValue) +{ + if (!aValue) + return; + + nsIWidget* mainWidget = GetWindowWidget(); + if (!mainWidget) + return; + + // top, right, bottom, left - see nsAttrValue + nsAttrValue attrValue; + nsIntMargin margins; + + nsAutoString data; + data.Assign(*aValue); + if (attrValue.ParseIntMarginValue(data) && + attrValue.GetIntMarginValue(margins)) { + mainWidget->SetNonClientMargins(margins); + } +} + +void +nsXULElement::ResetChromeMargins() +{ + nsIWidget* mainWidget = GetWindowWidget(); + if (!mainWidget) + return; + // See nsIWidget + nsIntMargin margins(-1,-1,-1,-1); + mainWidget->SetNonClientMargins(margins); +} + PRBool nsXULElement::BoolAttrIsTrue(nsIAtom* aName) { diff --git a/content/xul/content/src/nsXULElement.h b/content/xul/content/src/nsXULElement.h index 77212a0c46c8..2898e048cff5 100644 --- a/content/xul/content/src/nsXULElement.h +++ b/content/xul/content/src/nsXULElement.h @@ -671,8 +671,10 @@ protected: nsIWidget* GetWindowWidget(); + // attribute setters for widget nsresult HideWindowChrome(PRBool aShouldHide); - + void SetChromeMargins(const nsAString* aValue); + void ResetChromeMargins(); void SetTitlebarColor(nscolor aColor, PRBool aActive); void SetDrawsInTitlebar(PRBool aState); diff --git a/xpfe/appshell/src/nsXULWindow.cpp b/xpfe/appshell/src/nsXULWindow.cpp index 06e868763165..2e5e77bfa572 100644 --- a/xpfe/appshell/src/nsXULWindow.cpp +++ b/xpfe/appshell/src/nsXULWindow.cpp @@ -92,6 +92,7 @@ #include "nsReadableUtils.h" #include "nsStyleConsts.h" #include "nsPresContext.h" +#include "nsIContentUtils.h" #include "nsWebShellWindow.h" // get rid of this one, too... @@ -1375,6 +1376,15 @@ void nsXULWindow::SyncAttributesToWidget() mWindow->HideWindowChrome(PR_TRUE); } + // "chromemargin" attribute + nsIntMargin margins; + nsCOMPtr cutils = + do_GetService("@mozilla.org/content/contentutils;1"); + rv = windowElement->GetAttribute(NS_LITERAL_STRING("chromemargin"), attr); + if (NS_SUCCEEDED(rv) && cutils && cutils->ParseIntMarginValue(attr, margins)) { + mWindow->SetNonClientMargins(margins); + } + // "accelerated" attribute PRBool isAccelerated; rv = windowElement->HasAttribute(NS_LITERAL_STRING("accelerated"), &isAccelerated); From a8d85c8c200ccb0081fb026bb979d6ee13005baa Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:07 -0500 Subject: [PATCH 1814/1860] Bug 513162 - Win support for custom chrome margins on windows. r=tellrob. --- widget/src/windows/nsUXThemeData.cpp | 2 + widget/src/windows/nsUXThemeData.h | 2 + widget/src/windows/nsWindow.cpp | 370 ++++++++++++++++++++++++--- widget/src/windows/nsWindow.h | 20 ++ 4 files changed, 364 insertions(+), 30 deletions(-) diff --git a/widget/src/windows/nsUXThemeData.cpp b/widget/src/windows/nsUXThemeData.cpp index 4df110ebf6f4..4f32f8c1053e 100644 --- a/widget/src/windows/nsUXThemeData.cpp +++ b/widget/src/windows/nsUXThemeData.cpp @@ -92,6 +92,7 @@ nsUXThemeData::DwmSetIconicThumbnailProc nsUXThemeData::dwmSetIconicThumbnailPtr nsUXThemeData::DwmSetIconicLivePreviewBitmapProc nsUXThemeData::dwmSetIconicLivePreviewBitmapPtr = NULL; nsUXThemeData::DwmSetWindowAttributeProc nsUXThemeData::dwmSetWindowAttributePtr = NULL; nsUXThemeData::DwmInvalidateIconicBitmapsProc nsUXThemeData::dwmInvalidateIconicBitmapsPtr = NULL; +nsUXThemeData::DwmDefWindowProcProc nsUXThemeData::dwmDwmDefWindowProcPtr = NULL; #endif void @@ -136,6 +137,7 @@ nsUXThemeData::Initialize() dwmSetIconicLivePreviewBitmapPtr = (DwmSetIconicLivePreviewBitmapProc)::GetProcAddress(sDwmDLL, "DwmSetIconicLivePreviewBitmap"); dwmSetWindowAttributePtr = (DwmSetWindowAttributeProc)::GetProcAddress(sDwmDLL, "DwmSetWindowAttribute"); dwmInvalidateIconicBitmapsPtr = (DwmInvalidateIconicBitmapsProc)::GetProcAddress(sDwmDLL, "DwmInvalidateIconicBitmaps"); + dwmDwmDefWindowProcPtr = (DwmDefWindowProcProc)::GetProcAddress(sDwmDLL, "DwmDefWindowProc"); CheckForCompositor(); } #endif diff --git a/widget/src/windows/nsUXThemeData.h b/widget/src/windows/nsUXThemeData.h index 620a0dbef6c2..effeea8dec73 100644 --- a/widget/src/windows/nsUXThemeData.h +++ b/widget/src/windows/nsUXThemeData.h @@ -178,6 +178,7 @@ public: typedef HRESULT (WINAPI*DwmSetIconicLivePreviewBitmapProc)(HWND hWnd, HBITMAP hBitmap, POINT *pptClient, DWORD dwSITFlags); typedef HRESULT (WINAPI*DwmSetWindowAttributeProc)(HWND hWnd, DWORD dwAttribute, LPCVOID pvAttribute, DWORD cbAttribute); typedef HRESULT (WINAPI*DwmInvalidateIconicBitmapsProc)(HWND hWnd); + typedef HRESULT (WINAPI*DwmDefWindowProcProc)(HWND hWnd, UINT msg, LPARAM lParam, WPARAM wParam, LRESULT *aRetValue); static DwmExtendFrameIntoClientAreaProc dwmExtendFrameIntoClientAreaPtr; static DwmIsCompositionEnabledProc dwmIsCompositionEnabledPtr; @@ -185,6 +186,7 @@ public: static DwmSetIconicLivePreviewBitmapProc dwmSetIconicLivePreviewBitmapPtr; static DwmSetWindowAttributeProc dwmSetWindowAttributePtr; static DwmInvalidateIconicBitmapsProc dwmInvalidateIconicBitmapsPtr; + static DwmDefWindowProcProc dwmDwmDefWindowProcPtr; static PRBool CheckForCompositor() { BOOL compositionIsEnabled = FALSE; diff --git a/widget/src/windows/nsWindow.cpp b/widget/src/windows/nsWindow.cpp index 44c2c36f4e4d..6b7a7a1db9e5 100644 --- a/widget/src/windows/nsWindow.cpp +++ b/widget/src/windows/nsWindow.cpp @@ -141,6 +141,7 @@ #include "nsIFontMetrics.h" #include "nsIFontEnumerator.h" #include "nsIDeviceContext.h" +#include "nsILookAndFeel.h" #include "nsGUIEvent.h" #include "nsFont.h" #include "nsRect.h" @@ -370,10 +371,13 @@ nsWindow::nsWindow() : nsBaseWidget() mIsInMouseCapture = PR_FALSE; mIsTopWidgetWindow = PR_FALSE; mUnicodeWidget = PR_TRUE; + mDisplayPanFeedback = PR_FALSE; + mCustomNonClient = PR_FALSE; + mCompositorFlag = PR_FALSE; + mHideChrome = PR_FALSE; mWindowType = eWindowType_child; mBorderStyle = eBorderStyle_default; mPopupType = ePopupTypeAny; - mDisplayPanFeedback = PR_FALSE; mLastPoint.x = 0; mLastPoint.y = 0; mLastSize.width = 0; @@ -1719,8 +1723,8 @@ NS_METHOD nsWindow::SetFocus(PRBool aRaise) * * SECTION: Bounds * - * nsIWidget::GetBounds, nsIWidget::GetScreenBounds, - * nsIWidget::GetClientBounds + * GetBounds, GetClientBounds, GetScreenBounds, GetClientOffset + * SetDrawsInTitlebar, GetNonClientMargins, SetNonClientMargins * * Bound calculations. * @@ -1843,6 +1847,166 @@ NS_METHOD nsWindow::GetClientOffset(nsIntPoint &aPt) return NS_OK; } +void +nsWindow::SetDrawsInTitlebar(PRBool aState) +{ + nsWindow * window = GetTopLevelWindow(PR_TRUE); + if (window && window != this) { + return window->SetDrawsInTitlebar(aState); + } + + if (aState) { + // left, top, right, bottom for nsIntMargin + nsIntMargin margins(-1, 0, -1, -1); + SetNonClientMargins(margins); + } + else { + nsIntMargin margins(-1, -1, -1, -1); + SetNonClientMargins(margins); + } +} + +NS_IMETHODIMP +nsWindow::GetNonClientMargins(nsIntMargin &margins) +{ + nsWindow * window = GetTopLevelWindow(PR_TRUE); + if (window && window != this) { + return window->GetNonClientMargins(margins); + } + + if (mCustomNonClient) { + margins = mNonClientMargins; + return NS_OK; + } + + nsCOMPtr lookAndFeel = + do_GetService("@mozilla.org/widget/lookandfeel;1"); + if (!lookAndFeel) { + NS_WARNING("We need nsILookAndFeel for GetNonClientMargins!"); + return NS_ERROR_FAILURE; + } + + PRInt32 val; + lookAndFeel->GetMetric(nsILookAndFeel::eMetric_WindowTitleHeight, val); + margins.top = val; + lookAndFeel->GetMetric(nsILookAndFeel::eMetric_WindowBorderHeight, val); + margins.top += val; + margins.bottom = val; + lookAndFeel->GetMetric(nsILookAndFeel::eMetric_WindowBorderWidth, val); + margins.left = margins.right = val; + + return NS_OK; +} + +PRBool +nsWindow::UpdateNonClientMargins() +{ + if (!mCustomNonClient) + return PR_FALSE; + + // XXX Temp disable margins until frame rendering is supported + mCompositorFlag = PR_TRUE; + if(!nsUXThemeData::CheckForCompositor()) { + mCompositorFlag = PR_FALSE; + return PR_FALSE; + } + + mNonClientOffset.top = mNonClientOffset.bottom = + mNonClientOffset.left = mNonClientOffset.right = 0; + + nsCOMPtr lookAndFeel = + do_GetService("@mozilla.org/widget/lookandfeel;1"); + if (!lookAndFeel) { + NS_WARNING("We need nsILookAndFeel for UpdateNonClientMargins!"); + return PR_FALSE; + } + + // Used in hit testing, provides the resize cursor margin + lookAndFeel->GetMetric(nsILookAndFeel::eMetric_WindowBorderWidth, mResizeMargin); + lookAndFeel->GetMetric(nsILookAndFeel::eMetric_WindowTitleHeight, mCaptionHeight); + mCaptionHeight += mResizeMargin; + + // If a margin value is 0, set the offset to the default size of the frame. + // If a margin is -1, leave as default, and if a margin > 0, set the offset + // so that the frame size is equal to the margin value. + if (!mNonClientMargins.top) + mNonClientOffset.top = mCaptionHeight; + else if (mNonClientMargins.top > 0) + mNonClientOffset.top = mCaptionHeight - mNonClientMargins.top; + + if (!mNonClientMargins.left) + mNonClientOffset.left = mResizeMargin; + else if (mNonClientMargins.left > 0) + mNonClientOffset.left = mResizeMargin - mNonClientMargins.left; + + if (!mNonClientMargins.right) + mNonClientOffset.right = mResizeMargin; + else if (mNonClientMargins.right > 0) + mNonClientOffset.right = mResizeMargin - mNonClientMargins.right; + + PRInt32 val; + lookAndFeel->GetMetric(nsILookAndFeel::eMetric_WindowBorderHeight, val); + if (!mNonClientMargins.bottom) + mNonClientOffset.bottom = val; + else if (mNonClientMargins.bottom > 0) + mNonClientOffset.bottom = val - mNonClientMargins.bottom; + + NS_ASSERTION(mNonClientOffset.top >= 0, "non-client top margin is negative!"); + NS_ASSERTION(mNonClientOffset.left >= 0, "non-client left margin is negative!"); + NS_ASSERTION(mNonClientOffset.right >= 0, "non-client right margin is negative!"); + NS_ASSERTION(mNonClientOffset.bottom >= 0, "non-client bottom margin is negative!"); + + if (mNonClientOffset.top < 0) + mNonClientOffset.top = 0; + if (mNonClientOffset.left < 0) + mNonClientOffset.left = 0; + if (mNonClientOffset.right < 0) + mNonClientOffset.right = 0; + if (mNonClientOffset.bottom < 0) + mNonClientOffset.bottom = 0; + + SetWindowPos(mWnd, 0, 0, 0, 0, 0, + SWP_FRAMECHANGED|SWP_NOACTIVATE|SWP_NOMOVE| + SWP_NOOWNERZORDER|SWP_NOSIZE|SWP_NOZORDER); + UpdateWindow(mWnd); + + return PR_TRUE; +} + +NS_IMETHODIMP +nsWindow::SetNonClientMargins(nsIntMargin &margins) +{ + if (!mIsTopWidgetWindow || + mBorderStyle & eBorderStyle_none || + mHideChrome) + return NS_ERROR_INVALID_ARG; + + // Request for a reset + if (margins.top == -1 && margins.left == -1 && + margins.right == -1 && margins.bottom == -1) { + mCustomNonClient = PR_FALSE; + mNonClientMargins = margins; + SetWindowPos(mWnd, 0, 0, 0, 0, 0, + SWP_FRAMECHANGED|SWP_NOACTIVATE|SWP_NOMOVE| + SWP_NOOWNERZORDER|SWP_NOSIZE|SWP_NOZORDER); + UpdateWindow(mWnd); + return NS_OK; + } + + if (margins.top < -1 || margins.bottom < -1 || + margins.left < -1 || margins.right < -1) + return NS_ERROR_INVALID_ARG; + + mNonClientMargins = margins; + mCustomNonClient = PR_TRUE; + if (!UpdateNonClientMargins()) { + NS_WARNING("UpdateNonClientMargins failed!"); + return PR_FALSE; + } + + return NS_OK; +} + /************************************************************** * * SECTION: nsIWidget::SetBackgroundColor @@ -2145,11 +2309,16 @@ void nsWindow::UpdatePossiblyTransparentRegion(const nsIntRegion &aDirtyRegion, opaqueRegion.Or(opaqueRegion, childWindowRegion); // Sometimes child windows overlap our bounds opaqueRegion.And(opaqueRegion, clientBounds); + MARGINS margins = { 0, 0, 0, 0 }; DWORD_PTR dwStyle = ::GetWindowLongPtrW(hWnd, GWL_STYLE); - // If there is no opaque region or hidechrome=true then full glass - if (opaqueRegion.IsEmpty() || !(dwStyle & WS_CAPTION)) { - margins.cxLeftWidth = -1; + + // If there is no opaque region or hidechrome=true, set margins + // to support a full sheet of glass. + if (opaqueRegion.IsEmpty() || mHideChrome) { + // Comments in MSDN indicate all values must be set to -1 + margins.cxLeftWidth = margins.cxRightWidth = + margins.cyTopHeight = margins.cyBottomHeight = -1; } else { // Find the largest rectangle and use that to calculate the inset nsIntRect largest = opaqueRegion.GetLargestRectangle(); @@ -2158,6 +2327,8 @@ void nsWindow::UpdatePossiblyTransparentRegion(const nsIntRegion &aDirtyRegion, margins.cyTopHeight = largest.y; margins.cyBottomHeight = clientBounds.height - largest.YMost(); } + + // Only update glass area if there are changes if (memcmp(&mGlassMargins, &margins, sizeof mGlassMargins)) { mGlassMargins = margins; UpdateGlass(); @@ -2165,13 +2336,21 @@ void nsWindow::UpdatePossiblyTransparentRegion(const nsIntRegion &aDirtyRegion, #endif // #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_LONGHORN } -void nsWindow::UpdateGlass() { +void nsWindow::UpdateGlass() +{ #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_LONGHORN HWND hWnd = GetTopLevelHWND(mWnd, PR_TRUE); + + // DWMNCRP_USEWINDOWSTYLE - The non-client rendering area is + // rendered based on the window style. + // DWMNCRP_ENABLED - The non-client area rendering is + // enabled; the window style is ignored. DWMNCRENDERINGPOLICY policy = DWMNCRP_USEWINDOWSTYLE; - if(eTransparencyGlass == mTransparencyMode) { + if (mTransparencyMode == eTransparencyGlass) { policy = DWMNCRP_ENABLED; } + + // Extends the window frame behind the client area if(nsUXThemeData::CheckForCompositor()) { nsUXThemeData::dwmExtendFrameIntoClientAreaPtr(hWnd, &mGlassMargins); nsUXThemeData::dwmSetWindowAttributePtr(hWnd, DWMWA_NCRENDERING_POLICY, &policy, sizeof policy); @@ -2198,11 +2377,13 @@ NS_IMETHODIMP nsWindow::HideWindowChrome(PRBool aShouldHide) } DWORD_PTR style, exStyle; + mHideChrome = aShouldHide; if (aShouldHide) { DWORD_PTR tempStyle = ::GetWindowLongPtrW(hwnd, GWL_STYLE); DWORD_PTR tempExStyle = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE); - style = tempStyle & ~(WS_CAPTION | WS_THICKFRAME); + style = tempStyle & ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX); exStyle = tempExStyle & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE); @@ -2210,10 +2391,17 @@ NS_IMETHODIMP nsWindow::HideWindowChrome(PRBool aShouldHide) mOldExStyle = tempExStyle; } else { - if (!mOldStyle || !mOldExStyle) { + if (!mOldStyle) mOldStyle = ::GetWindowLongPtrW(hwnd, GWL_STYLE); + if (!mOldExStyle) mOldExStyle = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE); - } + + if (mIsVisible) + mOldStyle |= WS_VISIBLE; + if (mSizeMode == nsSizeMode_Maximized) + mOldStyle |= WS_MAXIMIZE; + else if (mSizeMode == nsSizeMode_Minimized) + mOldStyle |= WS_MINIMIZE; style = mOldStyle; exStyle = mOldExStyle; @@ -4186,6 +4374,16 @@ PRBool nsWindow::ProcessMessage(UINT msg, WPARAM &wParam, LPARAM &lParam, static PRBool getWheelInfo = PR_TRUE; + // Glass hit testing w/custom transparent margins + LRESULT dwmHitResult; + if (mCustomNonClient && + mCompositorFlag && + nsUXThemeData::CheckForCompositor() && + nsUXThemeData::dwmDwmDefWindowProcPtr(mWnd, msg, wParam, lParam, &dwmHitResult)) { + *aRetValue = dwmHitResult; + return PR_TRUE; + } + switch (msg) { #ifndef WINCE // WM_QUERYENDSESSION must be handled by all windows. @@ -4270,6 +4468,9 @@ PRBool nsWindow::ProcessMessage(UINT msg, WPARAM &wParam, LPARAM &lParam, case WM_XP_THEMECHANGED: { + // Update non-client margin offsets + UpdateNonClientMargins(); + DispatchStandardEvent(NS_THEMECHANGED); // Invalidate the window so that the repaint will @@ -4309,6 +4510,62 @@ PRBool nsWindow::ProcessMessage(UINT msg, WPARAM &wParam, LPARAM &lParam, } break; + case WM_NCCALCSIZE: + { + // If wParam is TRUE, it specifies that the application should indicate + // which part of the client area contains valid information. The system + // copies the valid information to the specified area within the new + // client area. If the wParam parameter is FALSE, the application should + // return zero. + if (mCustomNonClient && mCompositorFlag) { + if (!wParam) { + result = PR_TRUE; + *aRetValue = 0; + break; + } + + // before: + // rgrc[0]: the proposed window + // rgrc[1]: the current window + // rgrc[2]: the source client area + // pncsp->lppos: move/size data + // after: + // rgrc[0]: the new client area + // rgrc[1]: the destination window + // rgrc[2]: the source client area + // (all values in screen coordiantes) + NCCALCSIZE_PARAMS *pncsp = reinterpret_cast(lParam); + LRESULT res = CallWindowProcW(GetPrevWindowProc(), mWnd, msg, wParam, lParam); + pncsp->rgrc[0].top -= mNonClientOffset.top; + pncsp->rgrc[0].left -= mNonClientOffset.left; + pncsp->rgrc[0].right += mNonClientOffset.right; + pncsp->rgrc[0].bottom += mNonClientOffset.bottom; + + result = PR_TRUE; + *aRetValue = res; + } + break; + } + + case WM_NCHITTEST: + { + /* + * If an nc client area margin has been moved, we are responsible + * for calculating where the resize margins are and returning the + * appropriate set of hit test constants. DwmDefWindowProc (above) + * will handle hit testing on it's command buttons if we are on a + * composited desktop. + */ + + if (!mCustomNonClient || !mCompositorFlag) + break; + + *aRetValue = + ClientMarginHitTestPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + result = PR_TRUE; + break; + } + #ifndef WINCE case WM_POWERBROADCAST: // only hidden window handle this @@ -4813,6 +5070,7 @@ PRBool nsWindow::ProcessMessage(UINT msg, WPARAM &wParam, LPARAM &lParam, #ifndef WINCE #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_LONGHORN case WM_DWMCOMPOSITIONCHANGED: + UpdateNonClientMargins(); BroadcastMsg(mWnd, WM_DWMCOMPOSITIONCHANGED); DispatchStandardEvent(NS_THEMECHANGED); UpdateGlass(); @@ -5094,6 +5352,71 @@ void nsWindow::GlobalMsgWindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lP * **************************************************************/ +PRInt32 +nsWindow::ClientMarginHitTestPoint(PRInt32 mx, PRInt32 my) +{ + // Calculations are done in screen coords + RECT winRect; + GetWindowRect(mWnd, &winRect); + + // hit return constants: + // HTBORDER - non-resizable border + // HTBOTTOM, HTLEFT, HTRIGHT, HTTOP - resizable border + // HTBOTTOMLEFT, HTBOTTOMRIGHT - resizable corner + // HTTOPLEFT, HTTOPRIGHT - resizable corner + // HTCAPTION - general title bar area + // HTCLIENT - area considered the client + // HTCLOSE - hovering over the close button + // HTMAXBUTTON - maximize button + // HTMINBUTTON - minimize button + + PRInt32 testResult = HTCLIENT; + + PRBool top = PR_FALSE; + PRBool bottom = PR_FALSE; + PRBool left = PR_FALSE; + PRBool right = PR_FALSE; + + if (my >= winRect.top && my <= + (winRect.top + (mCaptionHeight - mNonClientOffset.top))) + top = PR_TRUE; + else if (my <= winRect.bottom && my >= (winRect.bottom - mResizeMargin)) + bottom = PR_TRUE; + + if (mx >= winRect.left && mx <= (winRect.left + mResizeMargin)) + left = PR_TRUE; + else if (mx <= winRect.right && mx >= (winRect.right - mResizeMargin)) + right = PR_TRUE; + + if (top) { + testResult = HTTOP; + if (left) + testResult = HTTOPLEFT; + else if (right) + testResult = HTTOPRIGHT; + } else if (bottom) { + testResult = HTBOTTOM; + if (left) + testResult = HTBOTTOMLEFT; + else if (right) + testResult = HTBOTTOMRIGHT; + } else { + if (left) + testResult = HTLEFT; + if (right) + testResult = HTRIGHT; + } + + if (testResult == HTCLIENT && + my > (winRect.top + mResizeMargin) && + my <= (winRect.top + mCaptionHeight) && + my <= (winRect.top + (mCaptionHeight - mNonClientOffset.top))) + testResult = HTCAPTION; + + return testResult; +} + + #ifndef WINCE void nsWindow::PostSleepWakeNotification(const char* aNotification) { @@ -7027,34 +7350,21 @@ void nsWindow::SetWindowTranslucencyInner(nsTransparencyMode aMode) return; } - LONG_PTR style = 0, exStyle = 0; switch(aMode) { case eTransparencyTransparent: + { + LONG_PTR exStyle = topWindow->WindowStyle(); exStyle |= WS_EX_LAYERED; + ::SetWindowLongPtrW(hWnd, GWL_EXSTYLE, exStyle); + } case eTransparencyOpaque: case eTransparencyGlass: topWindow->mTransparencyMode = aMode; break; } - style |= topWindow->WindowStyle(); - exStyle |= topWindow->WindowExStyle(); - - if (aMode == eTransparencyTransparent) { - style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX); - exStyle &= ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE); - } - - if (topWindow->mIsVisible) - style |= WS_VISIBLE; - if (topWindow->mSizeMode == nsSizeMode_Maximized) - style |= WS_MAXIMIZE; - else if (topWindow->mSizeMode == nsSizeMode_Minimized) - style |= WS_MINIMIZE; - - VERIFY_WINDOW_STYLE(style); - ::SetWindowLongPtrW(hWnd, GWL_STYLE, style); - ::SetWindowLongPtrW(hWnd, GWL_EXSTYLE, exStyle); + // Hide chrome supports caching old styles, so this can be flipped back and forth + topWindow->HideWindowChrome(aMode == eTransparencyTransparent); #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_LONGHORN if (mTransparencyMode == eTransparencyGlass) diff --git a/widget/src/windows/nsWindow.h b/widget/src/windows/nsWindow.h index 856aed638124..fbee5c0b2707 100644 --- a/widget/src/windows/nsWindow.h +++ b/widget/src/windows/nsWindow.h @@ -197,6 +197,9 @@ public: NS_IMETHOD OnIMETextChange(PRUint32 aStart, PRUint32 aOldEnd, PRUint32 aNewEnd); NS_IMETHOD OnIMESelectionChange(void); #endif // NS_ENABLE_TSF + NS_IMETHOD GetNonClientMargins(nsIntMargin &margins); + NS_IMETHOD SetNonClientMargins(nsIntMargin &margins); + void SetDrawsInTitlebar(PRBool aState); /** * Statics used in other classes @@ -284,6 +287,7 @@ protected: nsWindow* GetParentWindow(PRBool aIncludeOwner); virtual void SubclassWindow(BOOL bState); PRBool CanTakeFocus(); + PRBool UpdateNonClientMargins(); #if !defined(WINCE) static void InitTrackPointHack(); #endif @@ -319,6 +323,7 @@ protected: PRBool& aResult, LRESULT* aRetValue, PRBool& aQuitProcessing); + PRInt32 ClientMarginHitTestPoint(PRInt32 mx, PRInt32 my); /** * Event handlers @@ -449,6 +454,7 @@ protected: HKL mLastKeyboardLayout; nsPopupType mPopupType; PRPackedBool mDisplayPanFeedback; + PRPackedBool mHideChrome; WindowHook mWindowHook; static PRUint32 sInstanceCount; static TriStateBool sCanQuit; @@ -467,6 +473,20 @@ protected: static PRUint32 sOOPPPluginFocusEvent; #endif + // Non-client margin settings + // Pre-calculated outward offset applied to default frames + nsIntMargin mNonClientOffset; + // Margins set by the owner + nsIntMargin mNonClientMargins; + // Indicates custom frames are enabled + PRPackedBool mCustomNonClient; + // Disable non client margins on non-comsitor desktops + PRPackedBool mCompositorFlag; + // Cached copy of L&F's resize border + PRInt32 mResizeMargin; + // Height of the caption plus border + PRInt32 mCaptionHeight; + nsCOMPtr mIdleService; // Hook Data Memebers for Dropdowns. sProcessHook Tells the From a8417d29fa91a8fb34a53051551f41216b2ba256 Mon Sep 17 00:00:00 2001 From: Jim Mathies Date: Thu, 24 Jun 2010 21:01:07 -0500 Subject: [PATCH 1815/1860] Bug 513162 - nsAttrValue margin parsing and chromemargin chrome tests. r=smaug, gavin. --- toolkit/content/tests/chrome/Makefile.in | 4 +- .../tests/chrome/test_chromemargin.xul | 70 +++++++ .../tests/chrome/window_chromemargin.xul | 105 ++++++++++ widget/tests/Makefile.in | 2 + widget/tests/TestChromeMargin.cpp | 189 ++++++++++++++++++ 5 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 toolkit/content/tests/chrome/test_chromemargin.xul create mode 100644 toolkit/content/tests/chrome/window_chromemargin.xul create mode 100644 widget/tests/TestChromeMargin.cpp diff --git a/toolkit/content/tests/chrome/Makefile.in b/toolkit/content/tests/chrome/Makefile.in index 01305aa6871f..184f25c8ee3a 100644 --- a/toolkit/content/tests/chrome/Makefile.in +++ b/toolkit/content/tests/chrome/Makefile.in @@ -110,7 +110,9 @@ _TEST_FILES = findbar_window.xul \ # textboxes and lists only, so skip this test on Mac ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT)) _TEST_FILES += test_panel_focus.xul \ - window_panel_focus.xul + window_panel_focus.xul \ + test_chromemargin.xul \ + window_chromemargin.xul else _TEST_FILES += test_autocomplete.xul endif diff --git a/toolkit/content/tests/chrome/test_chromemargin.xul b/toolkit/content/tests/chrome/test_chromemargin.xul new file mode 100644 index 000000000000..53a4a34a8149 --- /dev/null +++ b/toolkit/content/tests/chrome/test_chromemargin.xul @@ -0,0 +1,70 @@ + + + + + + + + + + +

+

+ +
+
+ + +
diff --git a/toolkit/content/tests/chrome/window_chromemargin.xul b/toolkit/content/tests/chrome/window_chromemargin.xul new file mode 100644 index 000000000000..143322db06d8 --- /dev/null +++ b/toolkit/content/tests/chrome/window_chromemargin.xul @@ -0,0 +1,105 @@ + + + + + + + diff --git a/widget/tests/Makefile.in b/widget/tests/Makefile.in index 7f0a0761d0ba..86486247b36b 100644 --- a/widget/tests/Makefile.in +++ b/widget/tests/Makefile.in @@ -51,6 +51,8 @@ ifdef NS_ENABLE_TSF CPP_UNIT_TESTS += TestWinTSF.cpp \ $(NULL) endif +CPP_UNIT_TESTS += TestChromeMargin.cpp \ + $(NULL) endif include $(topsrcdir)/config/rules.mk diff --git a/widget/tests/TestChromeMargin.cpp b/widget/tests/TestChromeMargin.cpp new file mode 100644 index 000000000000..a06ae9726937 --- /dev/null +++ b/widget/tests/TestChromeMargin.cpp @@ -0,0 +1,189 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jim Mathies + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* This tests the margin parsing functionality in nsAttrValue.cpp, which + * is accessible via nsIContentUtils, and is used in setting chromemargins + * to widget windows. It's located here due to linking issues in the + * content directory. + */ + +#include "TestHarness.h" + +#ifndef MOZILLA_INTERNAL_API +// some of the includes make use of internal string types +#define nsAString_h___ +#define nsString_h___ +#define nsStringFwd_h___ +#define nsReadableUtils_h___ +class nsACString; +class nsAString; +class nsAFlatString; +class nsAFlatCString; +class nsAdoptingString; +class nsAdoptingCString; +class nsXPIDLString; +template class nsReadingIterator; +#endif + +#include "nscore.h" +#include "nsIContentUtils.h" + +#ifndef MOZILLA_INTERNAL_API +#undef nsString_h___ +#undef nsAString_h___ +#undef nsReadableUtils_h___ +#endif + +struct DATA { + bool shouldfail; + const char* margins; + int top; + int right; + int bottom; + int left; +}; + +const bool SHOULD_FAIL = true; +const int SHOULD_PASS = false; + +const DATA Data[] = { + { SHOULD_FAIL, "", 1, 2, 3, 4 }, + { SHOULD_FAIL, "1,0,0,0", 1, 2, 3, 4 }, + { SHOULD_FAIL, "1,2,0,0", 1, 2, 3, 4 }, + { SHOULD_FAIL, "1,2,3,0", 1, 2, 3, 4 }, + { SHOULD_FAIL, "4,3,2,1", 1, 2, 3, 4 }, + { SHOULD_FAIL, "azsasdasd", 0, 0, 0, 0 }, + { SHOULD_FAIL, ",azsasdasd", 0, 0, 0, 0 }, + { SHOULD_FAIL, " ", 1, 2, 3, 4 }, + { SHOULD_FAIL, "azsdfsdfsdfsdfsdfsasdasd,asdasdasdasdasdasd,asdadasdasd,asdasdasdasd", 0, 0, 0, 0 }, + { SHOULD_FAIL, "as,as,as,as", 0, 0, 0, 0 }, + { SHOULD_FAIL, "0,0,0", 0, 0, 0, 0 }, + { SHOULD_FAIL, "0,0", 0, 0, 0, 0 }, + { SHOULD_FAIL, "4.6,1,1,1", 0, 0, 0, 0 }, + { SHOULD_FAIL, ",,,,", 0, 0, 0, 0 }, + { SHOULD_FAIL, "1, , , ,", 0, 0, 0, 0 }, + { SHOULD_FAIL, "1, , ,", 0, 0, 0, 0 }, + { SHOULD_FAIL, "@!@%^&^*()", 1, 2, 3, 4 }, + { SHOULD_PASS, "4,3,2,1", 4, 3, 2, 1 }, + { SHOULD_PASS, "-4,-3,-2,-1", -4, -3, -2, -1 }, + { SHOULD_PASS, "10000,3,2,1", 10000, 3, 2, 1 }, + { SHOULD_PASS, "4 , 3 , 2 , 1", 4, 3, 2, 1 }, + { SHOULD_PASS, "4, 3 ,2,1", 4, 3, 2, 1 }, + { SHOULD_FAIL, "4,3,2,10000000000000 --", 4, 3, 2, 10000000000000 }, + { SHOULD_PASS, "4,3,2,1000", 4, 3, 2, 1000 }, + { SHOULD_PASS, "2147483647,3,2,1000", 2147483647, 3, 2, 1000 }, + { SHOULD_PASS, "2147483647,2147483647,2147483647,2147483647", 2147483647, 2147483647, 2147483647, 2147483647 }, + { SHOULD_PASS, "-2147483647,3,2,1000", -2147483647, 3, 2, 1000 }, + { SHOULD_FAIL, "2147483648,3,2,1000", 1, 3, 2, 1000 }, + { 0, nsnull, 0, 0, 0, 0 } +}; + +void DoAttrValueTest() +{ + nsCOMPtr utils = + do_GetService("@mozilla.org/content/contentutils;1"); + + if (!utils) + fail("No nsIContentUtils"); + + int idx = -1; + bool didFail = false; + while (Data[++idx].margins) { + nsAutoString str; + str.AssignLiteral(Data[idx].margins); + nsIntMargin values(99,99,99,99); + bool result = utils->ParseIntMarginValue(str, values); + + // if the parse fails + if (!result) { + if (Data[idx].shouldfail) + continue; + fail(Data[idx].margins); + didFail = true; + printf("*1\n"); + continue; + } + + if (Data[idx].shouldfail) { + if (Data[idx].top == values.top && + Data[idx].right == values.right && + Data[idx].bottom == values.bottom && + Data[idx].left == values.left) { + // not likely + fail(Data[idx].margins); + didFail = true; + printf("*2\n"); + continue; + } + // good failure, parse failed and that's what we expected. + continue; + } +#if 0 + printf("%d==%d %d==%d %d==%d %d==%d\n", + Data[idx].top, values.top, + Data[idx].right, values.right, + Data[idx].bottom, values.bottom, + Data[idx].left, values.left); +#endif + if (Data[idx].top == values.top && + Data[idx].right == values.right && + Data[idx].bottom == values.bottom && + Data[idx].left == values.left) { + // good parse results + continue; + } + else { + fail(Data[idx].margins); + didFail = true; + printf("*3\n"); + continue; + } + } + + if (!didFail) + passed("nsAttrValue margin parsing tests passed."); +} + +int main(int argc, char** argv) +{ + ScopedXPCOM xpcom(""); + if (xpcom.failed()) + return 1; + DoAttrValueTest(); + return 0; +} From 435960f7e2b67485c5b3e58d4669d02652c9fa6f Mon Sep 17 00:00:00 2001 From: Felipe Gomes Date: Thu, 24 Jun 2010 21:01:07 -0500 Subject: [PATCH 1816/1860] Bug 555081 - Fix for mouse capture now that ChildWindow is gone from the main window. r=jmathies. --- widget/src/windows/nsWindow.cpp | 52 ++++++++++++--------------------- widget/src/windows/nsWindow.h | 4 --- 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/widget/src/windows/nsWindow.cpp b/widget/src/windows/nsWindow.cpp index 6b7a7a1db9e5..129b39e26628 100644 --- a/widget/src/windows/nsWindow.cpp +++ b/widget/src/windows/nsWindow.cpp @@ -3735,6 +3735,25 @@ PRBool nsWindow::DispatchMouseEvent(PRUint32 aEventType, WPARAM wParam, return result; } + switch (aEventType) { + case NS_MOUSE_BUTTON_DOWN: + CaptureMouse(PR_TRUE); + break; + + // NS_MOUSE_MOVE and NS_MOUSE_EXIT are here because we need to make sure capture flag + // isn't left on after a drag where we wouldn't see a button up message (see bug 324131). + case NS_MOUSE_BUTTON_UP: + case NS_MOUSE_MOVE: + case NS_MOUSE_EXIT: + if (!(wParam & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON)) && mIsInMouseCapture) + CaptureMouse(PR_FALSE); + break; + + default: + break; + + } // switch + nsIntPoint eventPoint; eventPoint.x = GET_X_LPARAM(lParam); eventPoint.y = GET_Y_LPARAM(lParam); @@ -8023,39 +8042,6 @@ LPARAM nsWindow::lParamToClient(LPARAM lParam) ************************************************************** **************************************************************/ -// Deal with all sort of mouse event -PRBool ChildWindow::DispatchMouseEvent(PRUint32 aEventType, WPARAM wParam, LPARAM lParam, - PRBool aIsContextMenuKey, PRInt16 aButton, PRUint16 aInputSource) -{ - PRBool result = PR_FALSE; - - if (nsnull == mEventCallback) { - return result; - } - - switch (aEventType) { - case NS_MOUSE_BUTTON_DOWN: - CaptureMouse(PR_TRUE); - break; - - // NS_MOUSE_MOVE and NS_MOUSE_EXIT are here because we need to make sure capture flag - // isn't left on after a drag where we wouldn't see a button up message (see bug 324131). - case NS_MOUSE_BUTTON_UP: - case NS_MOUSE_MOVE: - case NS_MOUSE_EXIT: - if (!(wParam & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON)) && mIsInMouseCapture) - CaptureMouse(PR_FALSE); - break; - - default: - break; - - } // switch - - return nsWindow::DispatchMouseEvent(aEventType, wParam, lParam, - aIsContextMenuKey, aButton, aInputSource); -} - // return the style for a child nsWindow DWORD ChildWindow::WindowStyle() { diff --git a/widget/src/windows/nsWindow.h b/widget/src/windows/nsWindow.h index fbee5c0b2707..d53351821c3c 100644 --- a/widget/src/windows/nsWindow.h +++ b/widget/src/windows/nsWindow.h @@ -564,10 +564,6 @@ class ChildWindow : public nsWindow { public: ChildWindow() {} - PRBool DispatchMouseEvent(PRUint32 aEventType, WPARAM wParam, LPARAM lParam, - PRBool aIsContextMenuKey = PR_FALSE, - PRInt16 aButton = nsMouseEvent::eLeftButton, - PRUint16 aInputSource = nsIDOMNSMouseEvent::MOZ_SOURCE_MOUSE); protected: virtual DWORD WindowStyle(); From 7617ac5c62a01ae69bee515eaa23497024c0c918 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Wed, 28 Apr 2010 03:28:56 +1000 Subject: [PATCH 1817/1860] Bug 550860 - Profile directory for xpcshell based tests needs to be in a predefined, stable, location for at least some tests. r=ted --- config/rules.mk | 2 ++ js/src/config/rules.mk | 2 ++ testing/xpcshell/runxpcshelltests.py | 30 ++++++++++++++++++++++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/config/rules.mk b/config/rules.mk index 444a5e47a782..553d7349dbfb 100644 --- a/config/rules.mk +++ b/config/rules.mk @@ -180,6 +180,7 @@ check-interactive: $(testxpcsrcdir)/runxpcshelltests.py \ --symbols-path=$(DIST)/crashreporter-symbols \ --test-path=$(SOLO_FILE) \ + --profile-name=$(MOZ_APP_NAME) \ --interactive \ $(DIST)/bin/xpcshell \ $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(MODULE)/$(dir)) @@ -191,6 +192,7 @@ check-one: $(testxpcsrcdir)/runxpcshelltests.py \ --symbols-path=$(DIST)/crashreporter-symbols \ --test-path=$(SOLO_FILE) \ + --profile-name=$(MOZ_APP_NAME) \ $(DIST)/bin/xpcshell \ $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(MODULE)/$(dir)) diff --git a/js/src/config/rules.mk b/js/src/config/rules.mk index 444a5e47a782..553d7349dbfb 100644 --- a/js/src/config/rules.mk +++ b/js/src/config/rules.mk @@ -180,6 +180,7 @@ check-interactive: $(testxpcsrcdir)/runxpcshelltests.py \ --symbols-path=$(DIST)/crashreporter-symbols \ --test-path=$(SOLO_FILE) \ + --profile-name=$(MOZ_APP_NAME) \ --interactive \ $(DIST)/bin/xpcshell \ $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(MODULE)/$(dir)) @@ -191,6 +192,7 @@ check-one: $(testxpcsrcdir)/runxpcshelltests.py \ --symbols-path=$(DIST)/crashreporter-symbols \ --test-path=$(SOLO_FILE) \ + --profile-name=$(MOZ_APP_NAME) \ $(DIST)/bin/xpcshell \ $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(MODULE)/$(dir)) diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py index 104e0e96ac70..dda1c4d7d0c8 100644 --- a/testing/xpcshell/runxpcshelltests.py +++ b/testing/xpcshell/runxpcshelltests.py @@ -42,7 +42,7 @@ import re, sys, os, os.path, logging, shutil, signal, math from glob import glob from optparse import OptionParser from subprocess import Popen, PIPE, STDOUT -from tempfile import mkdtemp +from tempfile import mkdtemp, gettempdir from automationutils import * @@ -270,11 +270,24 @@ class XPCShellTests(object): def setupProfileDir(self): """ Create a temporary folder for the profile and set appropriate environment variables. + When running check-interactive and check-one, the directory is well-defined and + retained for inspection once the tests complete. On a remote system, we overload this to use a remote path structure. """ - profileDir = mkdtemp() + if self.interactive or self.singleFile: + profileDir = os.path.join(gettempdir(), self.profileName, "xpcshellprofile") + try: + # This could be left over from previous runs + self.removeDir(profileDir) + except: + pass + os.makedirs(profileDir) + else: + profileDir = mkdtemp() self.env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir + if self.interactive or self.singleFile: + print "TEST-INFO | profile dir is %s" % profileDir return profileDir def setupLeakLogging(self): @@ -363,7 +376,8 @@ class XPCShellTests(object): manifest=None, testdirs=[], testPath=None, interactive=False, logfiles=True, thisChunk=1, totalChunks=1, debugger=None, - debuggerArgs=None, debuggerInteractive=False): + debuggerArgs=None, debuggerInteractive=False, + profileName=None): """Run xpcshell tests. |xpcshell|, is the xpcshell executable to use to run the tests. @@ -381,6 +395,8 @@ class XPCShellTests(object): Non-interactive only option. |debuggerInfo|, if set, specifies the debugger and debugger arguments that will be used to launch xpcshell. + |profileName|, if set, specifies the name of the application for the profile + directory if running only a subset of tests """ self.xpcshell = xpcshell @@ -394,6 +410,7 @@ class XPCShellTests(object): self.totalChunks = totalChunks self.thisChunk = thisChunk self.debuggerInfo = getDebuggerInfo(self.oldcwd, debugger, debuggerArgs, debuggerInteractive) + self.profileName = profileName or "xpcshell" if not testdirs and not manifest: # nothing to test! @@ -460,7 +477,9 @@ class XPCShellTests(object): if self.logfiles and stdout: self.createLogFile(test, stdout) finally: - if self.profileDir: + # We don't want to delete the profile when running check-interactive + # or check-one. + if self.profileDir and not self.interactive and not self.singleFile: self.removeDir(self.profileDir) if passCount == 0 and failCount == 0: @@ -500,6 +519,9 @@ class XPCShellOptions(OptionParser): self.add_option("--this-chunk", type = "int", dest = "thisChunk", default=1, help = "which chunk to run between 1 and --total-chunks") + self.add_option("--profile-name", + type = "string", dest="profileName", default=None, + help="name of application profile being tested") def main(): parser = XPCShellOptions() From cb10b9556f19189679d76e7ebb6af19b9f0b5eb0 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Thu, 24 Jun 2010 19:33:36 -0700 Subject: [PATCH 1818/1860] Bug 574541: getResourceURI fails to work correctly with files in subdirectories of the add-on. r=robstrong --- toolkit/mozapps/extensions/XPIProvider.jsm | 14 +++- toolkit/mozapps/extensions/test/Makefile.in | 2 +- .../test/addons/test_getresource/icon.png | 1 + .../test/addons/test_getresource/install.rdf | 23 ++++++ .../test_getresource/subdir/subfile.txt | 1 + .../test/xpcshell/test_getresource.js | 76 +++++++++++++------ 6 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 toolkit/mozapps/extensions/test/addons/test_getresource/icon.png create mode 100644 toolkit/mozapps/extensions/test/addons/test_getresource/install.rdf create mode 100644 toolkit/mozapps/extensions/test/addons/test_getresource/subdir/subfile.txt diff --git a/toolkit/mozapps/extensions/XPIProvider.jsm b/toolkit/mozapps/extensions/XPIProvider.jsm index 17d7f482ff7c..a17fb07f6004 100644 --- a/toolkit/mozapps/extensions/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/XPIProvider.jsm @@ -4931,7 +4931,11 @@ function AddonWrapper(aAddon) { } if (bundle.isDirectory()) { - bundle.append(aPath); + if (aPath) { + aPath.split("/").forEach(function(aPart) { + bundle.append(aPart); + }); + } return bundle.exists(); } @@ -4954,10 +4958,16 @@ function AddonWrapper(aAddon) { } if (bundle.isDirectory()) { - bundle.append(aPath); + if (aPath) { + aPath.split("/").forEach(function(aPart) { + bundle.append(aPart); + }); + } return Services.io.newFileURI(bundle); } + if (!aPath) + return Services.io.newFileURI(bundle); return buildJarURI(bundle, aPath); } } diff --git a/toolkit/mozapps/extensions/test/Makefile.in b/toolkit/mozapps/extensions/test/Makefile.in index b3181784f83d..788fec7cac19 100644 --- a/toolkit/mozapps/extensions/test/Makefile.in +++ b/toolkit/mozapps/extensions/test/Makefile.in @@ -68,6 +68,6 @@ libs:: $(EXIT_ON_ERROR) \ for dir in $(ADDONSRC)/*; do \ base=`basename $$dir` ; \ - (cd $$dir && zip $(TESTXPI)/$$base.xpi *) \ + (cd $$dir && zip -r $(TESTXPI)/$$base.xpi *) \ done \ fi diff --git a/toolkit/mozapps/extensions/test/addons/test_getresource/icon.png b/toolkit/mozapps/extensions/test/addons/test_getresource/icon.png new file mode 100644 index 000000000000..40765b0e23b4 --- /dev/null +++ b/toolkit/mozapps/extensions/test/addons/test_getresource/icon.png @@ -0,0 +1 @@ +Dummy icon file \ No newline at end of file diff --git a/toolkit/mozapps/extensions/test/addons/test_getresource/install.rdf b/toolkit/mozapps/extensions/test/addons/test_getresource/install.rdf new file mode 100644 index 000000000000..b2b822473217 --- /dev/null +++ b/toolkit/mozapps/extensions/test/addons/test_getresource/install.rdf @@ -0,0 +1,23 @@ + + + + + + addon1@tests.mozilla.org + 1.0 + + + Test 1 + Test Description + + + + xpcshell@tests.mozilla.org + 1 + 1 + + + + + diff --git a/toolkit/mozapps/extensions/test/addons/test_getresource/subdir/subfile.txt b/toolkit/mozapps/extensions/test/addons/test_getresource/subdir/subfile.txt new file mode 100644 index 000000000000..a28d18162c9a --- /dev/null +++ b/toolkit/mozapps/extensions/test/addons/test_getresource/subdir/subfile.txt @@ -0,0 +1 @@ +Dummy file in subdirectory \ No newline at end of file diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_getresource.js b/toolkit/mozapps/extensions/test/xpcshell/test_getresource.js index e2aabadbcaf5..adc5306998c9 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_getresource.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_getresource.js @@ -12,36 +12,68 @@ function run_test() { startupManager(); - installAllFiles([do_get_file("data/unsigned.xpi")], function() { + AddonManager.getInstallForFile(do_get_addon("test_getresource"), function(aInstall) { + do_check_true(aInstall.addon.hasResource("install.rdf")); + do_check_eq(aInstall.addon.getResourceURI().spec, aInstall.sourceURL); - restartManager(); + do_check_true(aInstall.addon.hasResource("icon.png")); + do_check_eq(aInstall.addon.getResourceURI("icon.png").spec, "jar:" + aInstall.sourceURL + "!/icon.png"); - AddonManager.getAddonByID("unsigned-xpi@tests.mozilla.org", - function(a1) { + do_check_false(aInstall.addon.hasResource("missing.txt")); - do_check_neq(a1, null); - do_check_true(a1.hasResource("install.rdf")); - let uri = a1.getResourceURI("install.rdf"); - do_check_true(uri instanceof AM_Ci.nsIFileURL); + do_check_true(aInstall.addon.hasResource("subdir/subfile.txt")); + do_check_eq(aInstall.addon.getResourceURI("subdir/subfile.txt").spec, "jar:" + aInstall.sourceURL + "!/subdir/subfile.txt"); - let uri2 = a1.getResourceURI(); - do_check_true(uri2 instanceof AM_Ci.nsIFileURL); - - let addonDir = gProfD.clone(); - addonDir.append("extensions"); - addonDir.append("unsigned-xpi@tests.mozilla.org"); - - do_check_eq(uri2.file.path, addonDir.path); - - a1.uninstall(); + do_check_false(aInstall.addon.hasResource("subdir/missing.txt")); + completeAllInstalls([aInstall], function() { restartManager(); + AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) { + do_check_neq(a1, null); - AddonManager.getAddonByID("unsigned-xpi@tests.mozilla.org", - function(newa1) { - do_check_eq(newa1, null); + let addonDir = gProfD.clone(); + addonDir.append("extensions"); + addonDir.append("addon1@tests.mozilla.org"); - do_test_finished(); + let uri = a1.getResourceURI(); + do_check_true(uri instanceof AM_Ci.nsIFileURL); + do_check_eq(uri.file.path, addonDir.path); + + let file = addonDir.clone(); + file.append("install.rdf"); + do_check_true(a1.hasResource("install.rdf")); + uri = a1.getResourceURI("install.rdf") + do_check_true(uri instanceof AM_Ci.nsIFileURL); + do_check_eq(uri.file.path, file.path); + + file = addonDir.clone(); + file.append("icon.png"); + do_check_true(a1.hasResource("icon.png")); + uri = a1.getResourceURI("icon.png") + do_check_true(uri instanceof AM_Ci.nsIFileURL); + do_check_eq(uri.file.path, file.path); + + do_check_false(a1.hasResource("missing.txt")); + + file = addonDir.clone(); + file.append("subdir"); + file.append("subfile.txt"); + do_check_true(a1.hasResource("subdir/subfile.txt")); + uri = a1.getResourceURI("subdir/subfile.txt") + do_check_true(uri instanceof AM_Ci.nsIFileURL); + do_check_eq(uri.file.path, file.path); + + do_check_false(a1.hasResource("subdir/missing.txt")); + + a1.uninstall(); + + restartManager(); + + AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) { + do_check_eq(newa1, null); + + do_test_finished(); + }); }); }); }); From 04afb9641ea69e0a2e602602e9d6f3e0d662b014 Mon Sep 17 00:00:00 2001 From: Matt Woodrow Date: Wed, 23 Jun 2010 18:37:00 -0500 Subject: [PATCH 1819/1860] Bug 573929: Prevent GLX textures from being released after the window supplying the GLContext from which they were allocated dies. r=vlad --- gfx/layers/opengl/ImageLayerOGL.cpp | 9 +++----- gfx/thebes/public/GLContext.h | 16 ++++++++++++++ gfx/thebes/src/GLContextProviderGLX.cpp | 29 +++++++++++++++++++++++++ widget/src/gtk2/nsWindow.cpp | 21 ++++++++++++++---- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/gfx/layers/opengl/ImageLayerOGL.cpp b/gfx/layers/opengl/ImageLayerOGL.cpp index b67c2b111af5..c4803573a123 100644 --- a/gfx/layers/opengl/ImageLayerOGL.cpp +++ b/gfx/layers/opengl/ImageLayerOGL.cpp @@ -60,8 +60,7 @@ public: } NS_IMETHOD Run() { if (mTexture) { - mContext->MakeCurrent(); - mContext->fDeleteTextures(1, &mTexture); + mContext->DestroyTexture(mTexture); } // Ensure context is released on the main thread mContext = nsnull; @@ -80,8 +79,7 @@ GLTexture::Allocate(GLContext *aContext) Release(); mContext = aContext; - mContext->MakeCurrent(); - mContext->fGenTextures(1, &mTexture); + mTexture = mContext->CreateTexture(); } void @@ -104,8 +102,7 @@ GLTexture::Release() if (NS_IsMainThread()) { if (mTexture) { - mContext->MakeCurrent(); - mContext->fDeleteTextures(1, &mTexture); + mContext->DestroyTexture(mTexture); mTexture = 0; } mContext = nsnull; diff --git a/gfx/thebes/public/GLContext.h b/gfx/thebes/public/GLContext.h index 358dafca7728..d0c5b5caf44d 100644 --- a/gfx/thebes/public/GLContext.h +++ b/gfx/thebes/public/GLContext.h @@ -129,6 +129,8 @@ public: virtual PRBool MakeCurrent() = 0; virtual PRBool SetupLookupFunction() = 0; + virtual void WindowDestroyed() {} + void *GetUserData(void *aKey) { void *result = nsnull; mUserData.Get(aKey, &result); @@ -173,6 +175,20 @@ public: * Releases a color buffer that is being used as a texture */ virtual PRBool ReleaseTexImage() { return PR_FALSE; } + + virtual GLuint CreateTexture() + { + GLuint tex; + MakeCurrent(); + fGenTextures(1, &tex); + return tex; + } + + virtual void DestroyTexture(GLuint tex) + { + MakeCurrent(); + fDeleteTextures(1, &tex); + } protected: PRBool mInitialized; diff --git a/gfx/thebes/src/GLContextProviderGLX.cpp b/gfx/thebes/src/GLContextProviderGLX.cpp index faf9f6c5ef32..6a7701996562 100644 --- a/gfx/thebes/src/GLContextProviderGLX.cpp +++ b/gfx/thebes/src/GLContextProviderGLX.cpp @@ -213,6 +213,34 @@ public: return PR_TRUE; } + void WindowDestroyed() + { + for (unsigned int i=0; i textures; }; static PRBool AreCompatibleVisuals(XVisualInfo *one, XVisualInfo *two) diff --git a/widget/src/gtk2/nsWindow.cpp b/widget/src/gtk2/nsWindow.cpp index d17511624958..e6709c1850ae 100644 --- a/widget/src/gtk2/nsWindow.cpp +++ b/widget/src/gtk2/nsWindow.cpp @@ -149,6 +149,9 @@ D_DEBUG_DOMAIN( ns_Window, "nsWindow", "nsWindow" ); #define GDK_WINDOW_XWINDOW(_win) _win #endif +using mozilla::gl::GLContext; +using mozilla::layers::LayerManagerOGL; + // Don't put more than this many rects in the dirty region, just fluff // out to the bounding-box if there are more #define MAX_RECTS_IN_REGION 100 @@ -718,13 +721,20 @@ nsWindow::Destroy(void) if (mIsDestroyed || !mCreated) return NS_OK; - /** Need to clean our LayerManager up while still alive */ - mLayerManager = NULL; - LOG(("nsWindow::Destroy [%p]\n", (void *)this)); mIsDestroyed = PR_TRUE; mCreated = PR_FALSE; + nsRefPtr gl; + if (GetLayerManager()->GetBackendType() == LayerManager::LAYERS_OPENGL) + { + LayerManagerOGL *manager = static_cast(GetLayerManager()); + gl = manager->gl(); + } + + /** Need to clean our LayerManager up while still alive */ + mLayerManager = NULL; + if (gUseBufferPixmap && gBufferPixmapUsageCount && --gBufferPixmapUsageCount == 0) @@ -812,6 +822,9 @@ nsWindow::Destroy(void) gdk_window_set_user_data(mGdkWindow, NULL); g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", NULL); + if (gl) { + gl->WindowDestroyed(); + } gdk_window_destroy(mGdkWindow); mGdkWindow = nsnull; } @@ -2362,7 +2375,7 @@ nsWindow::OnExposeEvent(GtkWidget *aWidget, GdkEventExpose *aEvent) if (GetLayerManager()->GetBackendType() == LayerManager::LAYERS_OPENGL) { - mozilla::layers::LayerManagerOGL *manager = static_cast(GetLayerManager()); + LayerManagerOGL *manager = static_cast(GetLayerManager()); manager->SetClippingRegion(event.region); nsEventStatus status; From 753e3bea932272dd0a504a786dee125b1c974eb0 Mon Sep 17 00:00:00 2001 From: Phil Ringnalda Date: Sun, 6 Jun 2010 16:49:00 -0500 Subject: [PATCH 1820/1860] Crash [@ MaybeSetForm] with html5.enable == false and
--- .../html/document/src/nsHTMLContentSink.cpp | 1 - content/html/document/test/Makefile.in | 1 + .../html/document/test/test_bug570375.html | 44 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 content/html/document/test/test_bug570375.html diff --git a/content/html/document/src/nsHTMLContentSink.cpp b/content/html/document/src/nsHTMLContentSink.cpp index ba27020f3b24..4b2d90a9c927 100644 --- a/content/html/document/src/nsHTMLContentSink.cpp +++ b/content/html/document/src/nsHTMLContentSink.cpp @@ -497,7 +497,6 @@ MaybeSetForm(nsGenericHTMLElement* aContent, nsHTMLTag aNodeType, case eHTMLTag_button: case eHTMLTag_fieldset: case eHTMLTag_label: - case eHTMLTag_legend: case eHTMLTag_object: case eHTMLTag_input: case eHTMLTag_select: diff --git a/content/html/document/test/Makefile.in b/content/html/document/test/Makefile.in index d170cb9ef9e6..84ac72ee1a69 100644 --- a/content/html/document/test/Makefile.in +++ b/content/html/document/test/Makefile.in @@ -98,6 +98,7 @@ _TEST_FILES = test_bug1682.html \ test_bug486741.html \ test_bug497242.xhtml \ test_bug512367.html \ + test_bug570375.html \ test_bug340017.xhtml \ test_bug499092.html \ bug499092.xml \ diff --git a/content/html/document/test/test_bug570375.html b/content/html/document/test/test_bug570375.html new file mode 100644 index 000000000000..f7f9ba7342ea --- /dev/null +++ b/content/html/document/test/test_bug570375.html @@ -0,0 +1,44 @@ + + + + + Test for Bug 570375 + + + + + +Mozilla Bug 570375 +

+ +

+ +
+
+
+ + From 9f79994a4e24c46941056994fcef7b3c047c1c2e Mon Sep 17 00:00:00 2001 From: Alexander Surkov Date: Fri, 25 Jun 2010 18:50:23 +0900 Subject: [PATCH 1821/1860] Bug 573910 - switch nsApplicationAccessible to nsAccessible::Append/RemoveChild, r=davidb, sr=roc --- accessible/public/nsIAccessibilityService.h | 6 +-- .../src/atk/nsApplicationAccessibleWrap.cpp | 27 +++++----- .../src/atk/nsApplicationAccessibleWrap.h | 8 +-- .../src/base/nsAccessibilityService.cpp | 49 ++++++++----------- accessible/src/base/nsAccessibilityService.h | 5 +- .../src/base/nsApplicationAccessible.cpp | 38 ++++++-------- accessible/src/base/nsApplicationAccessible.h | 6 +-- accessible/src/base/nsRootAccessible.cpp | 10 ++-- widget/src/gtk2/nsAccessibilityHelper.cpp | 8 +-- 9 files changed, 64 insertions(+), 93 deletions(-) diff --git a/accessible/public/nsIAccessibilityService.h b/accessible/public/nsIAccessibilityService.h index f8ab5e11a69d..77b7f91e4410 100644 --- a/accessible/public/nsIAccessibilityService.h +++ b/accessible/public/nsIAccessibilityService.h @@ -132,10 +132,8 @@ public: * Adds/remove ATK root accessible for gtk+ native window to/from children * of the application accessible. */ - virtual nsresult AddNativeRootAccessible(void *aAtkAccessible, - nsIAccessible **aAccessible) = 0; - virtual nsresult - RemoveNativeRootAccessible(nsIAccessible *aRootAccessible) = 0; + virtual nsAccessible* AddNativeRootAccessible(void* aAtkAccessible) = 0; + virtual void RemoveNativeRootAccessible(nsAccessible* aRootAccessible) = 0; /** * Used to describe sort of changes leading to accessible tree invalidation. diff --git a/accessible/src/atk/nsApplicationAccessibleWrap.cpp b/accessible/src/atk/nsApplicationAccessibleWrap.cpp index fce39d7e7afe..2b1c6cf67337 100644 --- a/accessible/src/atk/nsApplicationAccessibleWrap.cpp +++ b/accessible/src/atk/nsApplicationAccessibleWrap.cpp @@ -653,16 +653,13 @@ gboolean fireRootAccessibleAddedCB(gpointer data) return FALSE; } -nsresult -nsApplicationAccessibleWrap::AddRootAccessible(nsIAccessible *aRootAccWrap) +PRBool +nsApplicationAccessibleWrap::AppendChild(nsAccessible *aChild) { - NS_ENSURE_ARG_POINTER(aRootAccWrap); + if (!nsApplicationAccessible::AppendChild(aChild)) + return PR_FALSE; - // add by weak reference - nsresult rv = nsApplicationAccessible::AddRootAccessible(aRootAccWrap); - NS_ENSURE_SUCCESS(rv, rv); - - AtkObject *atkAccessible = nsAccessibleWrap::GetAtkObject(aRootAccWrap); + AtkObject *atkAccessible = nsAccessibleWrap::GetAtkObject(aChild); atk_object_set_parent(atkAccessible, mAtkObject); PRUint32 count = mChildren.Length(); @@ -680,22 +677,20 @@ nsApplicationAccessibleWrap::AddRootAccessible(nsIAccessible *aRootAccWrap) g_timeout_add(0, fireRootAccessibleAddedCB, eventData); } - return NS_OK; + return PR_TRUE; } -nsresult -nsApplicationAccessibleWrap::RemoveRootAccessible(nsIAccessible *aRootAccWrap) +PRBool +nsApplicationAccessibleWrap::RemoveChild(nsAccessible* aChild) { - NS_ENSURE_ARG_POINTER(aRootAccWrap); + PRInt32 index = aChild->GetIndexInParent(); - PRInt32 index = mChildren.IndexOf(aRootAccWrap); - - AtkObject *atkAccessible = nsAccessibleWrap::GetAtkObject(aRootAccWrap); + AtkObject *atkAccessible = nsAccessibleWrap::GetAtkObject(aChild); atk_object_set_parent(atkAccessible, NULL); g_signal_emit_by_name(mAtkObject, "children_changed::remove", index, atkAccessible, NULL); - return nsApplicationAccessible::RemoveRootAccessible(aRootAccWrap); + return nsApplicationAccessible::RemoveChild(aChild); } void diff --git a/accessible/src/atk/nsApplicationAccessibleWrap.h b/accessible/src/atk/nsApplicationAccessibleWrap.h index 9991c4b69c90..b3f03bb05f70 100644 --- a/accessible/src/atk/nsApplicationAccessibleWrap.h +++ b/accessible/src/atk/nsApplicationAccessibleWrap.h @@ -57,12 +57,12 @@ public: // nsAccessNode virtual PRBool Init(); + // nsAccessible + virtual PRBool AppendChild(nsAccessible* aChild); + virtual PRBool RemoveChild(nsAccessible* aChild); + // return the atk object for app root accessible NS_IMETHOD GetNativeInterface(void **aOutAccessible); - - // nsApplicationAccessible - virtual nsresult AddRootAccessible(nsIAccessible *aRootAccWrap); - virtual nsresult RemoveRootAccessible(nsIAccessible *aRootAccWrap); }; #endif /* __NS_APP_ROOT_ACCESSIBLE_H__ */ diff --git a/accessible/src/base/nsAccessibilityService.cpp b/accessible/src/base/nsAccessibilityService.cpp index 668be79428f7..29ef684f8388 100644 --- a/accessible/src/base/nsAccessibilityService.cpp +++ b/accessible/src/base/nsAccessibilityService.cpp @@ -1901,45 +1901,36 @@ nsAccessibilityService::CreateAccessibleByType(nsIContent *aContent, //////////////////////////////////////////////////////////////////////////////// // nsIAccessibilityService (DON'T put methods here) -nsresult -nsAccessibilityService::AddNativeRootAccessible(void *aAtkAccessible, - nsIAccessible **aRootAccessible) -{ +nsAccessible* +nsAccessibilityService::AddNativeRootAccessible(void* aAtkAccessible) + { #ifdef MOZ_ACCESSIBILITY_ATK - nsNativeRootAccessibleWrap* rootAccWrap = - new nsNativeRootAccessibleWrap((AtkObject*)aAtkAccessible); - - *aRootAccessible = static_cast(rootAccWrap); - NS_ADDREF(*aRootAccessible); - - nsApplicationAccessible *applicationAcc = + nsApplicationAccessible* applicationAcc = nsAccessNode::GetApplicationAccessible(); - NS_ENSURE_STATE(applicationAcc); + if (!applicationAcc) + return nsnull; - applicationAcc->AddRootAccessible(*aRootAccessible); + nsNativeRootAccessibleWrap* nativeRootAcc = + new nsNativeRootAccessibleWrap((AtkObject*)aAtkAccessible); + if (!nativeRootAcc) + return nsnull; - return NS_OK; -#else - return NS_ERROR_NOT_IMPLEMENTED; + if (applicationAcc->AppendChild(nativeRootAcc)) + return nativeRootAcc; #endif -} -nsresult -nsAccessibilityService::RemoveNativeRootAccessible(nsIAccessible *aRootAccessible) + return nsnull; + } + +void +nsAccessibilityService::RemoveNativeRootAccessible(nsAccessible* aAccessible) { #ifdef MOZ_ACCESSIBILITY_ATK - void* atkAccessible; - aRootAccessible->GetNativeInterface(&atkAccessible); - - nsApplicationAccessible *applicationAcc = + nsApplicationAccessible* applicationAcc = nsAccessNode::GetApplicationAccessible(); - NS_ENSURE_STATE(applicationAcc); - applicationAcc->RemoveRootAccessible(aRootAccessible); - - return NS_OK; -#else - return NS_ERROR_NOT_IMPLEMENTED; + if (applicationAcc) + applicationAcc->RemoveChild(aAccessible); #endif } diff --git a/accessible/src/base/nsAccessibilityService.h b/accessible/src/base/nsAccessibilityService.h index 15d551b9ae22..f4683055924d 100644 --- a/accessible/src/base/nsAccessibilityService.h +++ b/accessible/src/base/nsAccessibilityService.h @@ -114,9 +114,8 @@ public: virtual nsresult CreateHTMLCaptionAccessible(nsIFrame *aFrame, nsIAccessible **aAccessible); - virtual nsresult AddNativeRootAccessible(void *aAtkAccessible, - nsIAccessible **aAccessible); - virtual nsresult RemoveNativeRootAccessible(nsIAccessible *aRootAccessible); + virtual nsAccessible* AddNativeRootAccessible(void* aAtkAccessible); + virtual void RemoveNativeRootAccessible(nsAccessible* aRootAccessible); virtual nsresult InvalidateSubtreeFor(nsIPresShell *aPresShell, nsIContent *aContent, diff --git a/accessible/src/base/nsApplicationAccessible.cpp b/accessible/src/base/nsApplicationAccessible.cpp index 7906235513d3..4dbf2f822449 100644 --- a/accessible/src/base/nsApplicationAccessible.cpp +++ b/accessible/src/base/nsApplicationAccessible.cpp @@ -414,8 +414,8 @@ nsApplicationAccessible::GetParent() void nsApplicationAccessible::InvalidateChildren() { - // Do nothing because application children are kept updated by - // AddRootAccessible() and RemoveRootAccessible() method calls. + // Do nothing because application children are kept updated by AppendChild() + // and RemoveChild() method calls. } //////////////////////////////////////////////////////////////////////////////// @@ -428,8 +428,8 @@ nsApplicationAccessible::CacheChildren() // children are requested because empty InvalidateChldren() prevents its // repeated calls. - // Basically children are kept updated by Add/RemoveRootAccessible method - // calls. However if there are open windows before accessibility was started + // Basically children are kept updated by Append/RemoveChild method calls. + // However if there are open windows before accessibility was started // then we need to make sure root accessibles for open windows are created so // that all root accessibles are stored in application accessible children // array. @@ -481,30 +481,22 @@ nsApplicationAccessible::GetSiblingAtOffset(PRInt32 aOffset, nsresult* aError) //////////////////////////////////////////////////////////////////////////////// // Public methods -nsresult -nsApplicationAccessible::AddRootAccessible(nsIAccessible *aRootAccessible) +PRBool +nsApplicationAccessible::AppendChild(nsAccessible* aChild) { - NS_ENSURE_ARG_POINTER(aRootAccessible); + if (!mChildren.AppendElement(aChild)) + return PR_FALSE; - nsRefPtr rootAcc = do_QueryObject(aRootAccessible); - - if (!mChildren.AppendElement(rootAcc)) - return NS_ERROR_FAILURE; - - rootAcc->SetParent(this); - - return NS_OK; + aChild->SetParent(this); + return PR_TRUE; } -nsresult -nsApplicationAccessible::RemoveRootAccessible(nsIAccessible *aRootAccessible) +PRBool +nsApplicationAccessible::RemoveChild(nsAccessible* aChild) { - NS_ENSURE_ARG_POINTER(aRootAccessible); - - // It's not needed to void root accessible parent because this method is - // called on root accessible shutdown and its parent will be cleared - // properly. - return mChildren.RemoveElement(aRootAccessible) ? NS_OK : NS_ERROR_FAILURE; + // It's not needed to unbind root accessible from parent because this method + // is called when root accessible is shutdown and it'll be unbound properly. + return mChildren.RemoveElement(aChild); } //////////////////////////////////////////////////////////////////////////////// diff --git a/accessible/src/base/nsApplicationAccessible.h b/accessible/src/base/nsApplicationAccessible.h index e8b65ec25fd1..41bf2d31cc86 100644 --- a/accessible/src/base/nsApplicationAccessible.h +++ b/accessible/src/base/nsApplicationAccessible.h @@ -116,10 +116,8 @@ public: virtual nsAccessible* GetParent(); virtual void InvalidateChildren(); - - // nsApplicationAccessible - virtual nsresult AddRootAccessible(nsIAccessible *aRootAccWrap); - virtual nsresult RemoveRootAccessible(nsIAccessible *aRootAccWrap); + virtual PRBool AppendChild(nsAccessible* aChild); + virtual PRBool RemoveChild(nsAccessible* aChild); protected: diff --git a/accessible/src/base/nsRootAccessible.cpp b/accessible/src/base/nsRootAccessible.cpp index a47f6c209aa4..cc8bfc258ed4 100644 --- a/accessible/src/base/nsRootAccessible.cpp +++ b/accessible/src/base/nsRootAccessible.cpp @@ -831,11 +831,9 @@ PRBool nsRootAccessible::Init() { nsApplicationAccessible *applicationAcc = GetApplicationAccessible(); - if (!applicationAcc) + if (!applicationAcc || !applicationAcc->AppendChild(this)) return PR_FALSE; - applicationAcc->AddRootAccessible(this); - return nsDocAccessibleWrap::Init(); } @@ -850,7 +848,7 @@ nsRootAccessible::Shutdown() if (!applicationAcc) return; - applicationAcc->RemoveRootAccessible(this); + applicationAcc->RemoveChild(this); mCurrentARIAMenubar = nsnull; @@ -940,8 +938,8 @@ nsRootAccessible::GetRelationByType(PRUint32 aRelationType, nsAccessible* nsRootAccessible::GetParent() { - // Parent has been setted in nsApplicationAccesible::AddRootAccessible() - // when root accessible was intialized. + // Parent has been set in nsApplicationAccesible::AppendChild() when root + // accessible was initialized. return mParent; } diff --git a/widget/src/gtk2/nsAccessibilityHelper.cpp b/widget/src/gtk2/nsAccessibilityHelper.cpp index c93c12db0537..16c192664eae 100644 --- a/widget/src/gtk2/nsAccessibilityHelper.cpp +++ b/widget/src/gtk2/nsAccessibilityHelper.cpp @@ -54,19 +54,19 @@ gint RunDialog(GtkDialog* aDialog) nsCOMPtr accService = do_GetService ("@mozilla.org/accessibilityService;1"); - nsCOMPtr accessible; // Attach the dialog accessible to app accessible tree + nsAccessible* windowAcc = nsnull; if (accService) { AtkObject* gailWindow = gtk_widget_get_accessible(GTK_WIDGET(aDialog)); - accService->AddNativeRootAccessible(gailWindow, getter_AddRefs(accessible)); + windowAcc = accService->AddNativeRootAccessible(gailWindow); } gint result = gtk_dialog_run (aDialog); // Deattach the dialog accessible - if (accService && accessible) { - accService->RemoveNativeRootAccessible(accessible); + if (accService && windowAcc) { + accService->RemoveNativeRootAccessible(windowAcc); } return result; From 52efc3c06dd90dff31d53465495eb71b4d67c1d8 Mon Sep 17 00:00:00 2001 From: timeless Date: Fri, 25 Jun 2010 20:49:02 +0900 Subject: [PATCH 1822/1860] Bug 573934 - Crash with Last pass addon on trying to edit passwords/user names in Lastpass (no primaryColumn [@nsCoreUtils::IsColumnHiddenm, r=surkov --- accessible/src/xul/nsXULTreeAccessible.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/accessible/src/xul/nsXULTreeAccessible.cpp b/accessible/src/xul/nsXULTreeAccessible.cpp index 49bf246fad5c..e7ac8217afc8 100644 --- a/accessible/src/xul/nsXULTreeAccessible.cpp +++ b/accessible/src/xul/nsXULTreeAccessible.cpp @@ -1124,7 +1124,8 @@ nsXULTreeItemAccessibleBase::IsExpandable() nsCOMPtr primaryColumn; if (columns) { columns->GetPrimaryColumn(getter_AddRefs(primaryColumn)); - if (!nsCoreUtils::IsColumnHidden(primaryColumn)) + if (primaryColumn && + !nsCoreUtils::IsColumnHidden(primaryColumn)) return PR_TRUE; } } From 2139cf1c838f282cbb0517aa782cb1214cbf276f Mon Sep 17 00:00:00 2001 From: Oleg Romashin Date: Fri, 25 Jun 2010 07:18:56 -0400 Subject: [PATCH 1823/1860] Bug 574581 - "Conditional jump or move depends on uninitialised value(s)" in ThebesLayerOGL::EnsureSurface(). r=jones.chris.g --- gfx/layers/opengl/ThebesLayerOGL.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/gfx/layers/opengl/ThebesLayerOGL.cpp b/gfx/layers/opengl/ThebesLayerOGL.cpp index deb77c94a1f5..7be496444807 100644 --- a/gfx/layers/opengl/ThebesLayerOGL.cpp +++ b/gfx/layers/opengl/ThebesLayerOGL.cpp @@ -78,6 +78,7 @@ ThebesLayerOGL::ThebesLayerOGL(LayerManagerOGL *aManager) , LayerOGL(aManager) , mTexture(0) , mOffscreenFormat(gfxASurface::ImageFormatUnknown) + , mOffscreenSize(-1,-1) { mImplData = static_cast(this); } From 5179e6c1b0ff54e57d1e31d3fa9535a9445e141b Mon Sep 17 00:00:00 2001 From: Jacek Caban Date: Fri, 25 Jun 2010 14:02:24 +0200 Subject: [PATCH 1824/1860] Bug 569586 - XPCOM compilation failure on mingw-w64 due to pointer to int cast loosing precision. r=benjamin --- xpcom/base/nsStackWalk.cpp | 2 +- xpcom/ds/nsWindowsRegKey.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xpcom/base/nsStackWalk.cpp b/xpcom/base/nsStackWalk.cpp index a091af95e6cb..bac8fc27cabb 100644 --- a/xpcom/base/nsStackWalk.cpp +++ b/xpcom/base/nsStackWalk.cpp @@ -1031,7 +1031,7 @@ NS_DescribeCodeAddress(void *aPC, nsCodeAddressDetails *aDetails) // This just makes sure we get good info if available. // - DWORD addr = (DWORD)aPC; + DWORD_PTR addr = (DWORD_PTR)aPC; IMAGEHLP_MODULE modInfo; IMAGEHLP_LINE lineInfo; BOOL modInfoRes; diff --git a/xpcom/ds/nsWindowsRegKey.cpp b/xpcom/ds/nsWindowsRegKey.cpp index 8e7cc1df4e30..0fa484f006cf 100644 --- a/xpcom/ds/nsWindowsRegKey.cpp +++ b/xpcom/ds/nsWindowsRegKey.cpp @@ -139,7 +139,7 @@ nsWindowsRegKey::OpenChild(const nsAString &path, PRUint32 mode, if (!child) return NS_ERROR_OUT_OF_MEMORY; - nsresult rv = child->Open((PRUint32) mKey, path, mode); + nsresult rv = child->Open((uintptr_t) mKey, path, mode); if (NS_FAILED(rv)) return rv; @@ -157,7 +157,7 @@ nsWindowsRegKey::CreateChild(const nsAString &path, PRUint32 mode, if (!child) return NS_ERROR_OUT_OF_MEMORY; - nsresult rv = child->Create((PRUint32) mKey, path, mode); + nsresult rv = child->Create((uintptr_t) mKey, path, mode); if (NS_FAILED(rv)) return rv; From 4709592091645ee06dbf7450a55e8faa27de255b Mon Sep 17 00:00:00 2001 From: Jacek Caban Date: Fri, 25 Jun 2010 14:06:26 +0200 Subject: [PATCH 1825/1860] Bug 449292 - gfxHarfBuzzShaper.cpp mingw fix. r=jfkthame --- gfx/thebes/src/gfxHarfBuzzShaper.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gfx/thebes/src/gfxHarfBuzzShaper.cpp b/gfx/thebes/src/gfxHarfBuzzShaper.cpp index db3c09f674f3..45c68e495692 100644 --- a/gfx/thebes/src/gfxHarfBuzzShaper.cpp +++ b/gfx/thebes/src/gfxHarfBuzzShaper.cpp @@ -407,8 +407,8 @@ gfxHarfBuzzShaper::InitTextRun(gfxContext *aContext, hb_buffer_set_script(buffer, hb_script_t(aRunScript)); // hb_buffer_set_language(buffer, HB_OT_TAG_DEFAULT_LANGUAGE); - hb_buffer_add_utf16(buffer, aString + aRunStart, aRunLength, - 0, aRunLength); + hb_buffer_add_utf16(buffer, reinterpret_cast(aString + aRunStart), + aRunLength, 0, aRunLength); hb_shape(font, mHBFace, buffer, features.Elements(), features.Length()); From 8b90d0f223eb50e08e21731cae9f941f999b376d Mon Sep 17 00:00:00 2001 From: Jacek Caban Date: Fri, 25 Jun 2010 14:09:22 +0200 Subject: [PATCH 1826/1860] Bug 569590 - nsNativeAppSupportWin.cpp compilation failure on mingw-w64 due to pointer to int casts loosing precision r=timeless --- toolkit/xre/nsNativeAppSupportWin.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/toolkit/xre/nsNativeAppSupportWin.cpp b/toolkit/xre/nsNativeAppSupportWin.cpp index b234a229c304..4a85b7c13e86 100644 --- a/toolkit/xre/nsNativeAppSupportWin.cpp +++ b/toolkit/xre/nsNativeAppSupportWin.cpp @@ -389,7 +389,7 @@ nsNativeAppSupportWin::CheckConsole() { // all cases. See http://support.microsoft.com/support/kb/articles/q105/3/05.asp. // stdout - int hCrt = ::_open_osfhandle( (long)GetStdHandle( STD_OUTPUT_HANDLE ), + int hCrt = ::_open_osfhandle( (intptr_t)GetStdHandle( STD_OUTPUT_HANDLE ), _O_TEXT ); if ( hCrt != -1 ) { FILE *hf = ::_fdopen( hCrt, "w" ); @@ -402,7 +402,7 @@ nsNativeAppSupportWin::CheckConsole() { } // stderr - hCrt = ::_open_osfhandle( (long)::GetStdHandle( STD_ERROR_HANDLE ), + hCrt = ::_open_osfhandle( (intptr_t)::GetStdHandle( STD_ERROR_HANDLE ), _O_TEXT ); if ( hCrt != -1 ) { FILE *hf = ::_fdopen( hCrt, "w" ); @@ -646,7 +646,7 @@ struct MessageWindow { // Get current window and return its window handle. nsCOMPtr win; GetMostRecentWindow( 0, getter_AddRefs( win ) ); - return win ? (long)hwndForDOMWindow( win ) : 0; + return win ? (LRESULT)hwndForDOMWindow( win ) : 0; } return DefWindowProc( msgWindow, msg, wp, lp ); } @@ -1160,8 +1160,6 @@ nsNativeAppSupportWin::HandleDDENotification( UINT uType, // transaction t #if MOZ_DEBUG_DDE printf( "Handling dde request: [%s]...\n", (char*)request ); #endif - // Default is to open in current window. - PRBool new_window = PR_FALSE; nsAutoString url; ParseDDEArg((const WCHAR*) request, 0, url); @@ -1261,7 +1259,7 @@ void nsNativeAppSupportWin::ParseDDEArg( const WCHAR* args, int index, nsString& // Utility to parse out argument from a DDE item string. void nsNativeAppSupportWin::ParseDDEArg( HSZ args, int index, nsString& aString) { - DWORD argLen = DdeQueryStringW( mInstance, args, NULL, NULL, CP_WINUNICODE ); + DWORD argLen = DdeQueryStringW( mInstance, args, NULL, 0, CP_WINUNICODE ); // there wasn't any string, so return empty string if ( !argLen ) return; nsAutoString temp; From e768e691f94dd3decfa3ff08b145f2e48cfad52f Mon Sep 17 00:00:00 2001 From: Jacek Caban Date: Fri, 25 Jun 2010 14:10:54 +0200 Subject: [PATCH 1827/1860] Bug 569608 - gfxTextRunWordCache.cpp compilation failure on mingw-w64 due to pointer to long cast loosing precision r=vladimir --- gfx/thebes/src/gfxTextRunWordCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gfx/thebes/src/gfxTextRunWordCache.cpp b/gfx/thebes/src/gfxTextRunWordCache.cpp index 294dea25280e..1e4b0bb1ec21 100644 --- a/gfx/thebes/src/gfxTextRunWordCache.cpp +++ b/gfx/thebes/src/gfxTextRunWordCache.cpp @@ -945,7 +945,7 @@ TextRunWordCache::CacheHashEntry::HashKey(const KeyTypePointer aKey) PRUint32 fontSetGen; LL_L2UI(fontSetGen, aKey->mUserFontSetGeneration); - return aKey->mStringHash + fontSetGen + (long)aKey->mFontOrGroup + aKey->mAppUnitsPerDevUnit + + return aKey->mStringHash + fontSetGen + (PRUint32)(intptr_t)aKey->mFontOrGroup + aKey->mAppUnitsPerDevUnit + aKey->mIsDoubleByteText + aKey->mIsRTL*2 + aKey->mEnabledOptionalLigatures*4 + aKey->mOptimizeSpeed*8; } From 9be56318dbf24d2b10f047582504388c08316b26 Mon Sep 17 00:00:00 2001 From: Jacek Caban Date: Fri, 25 Jun 2010 14:13:17 +0200 Subject: [PATCH 1828/1860] Bug 569587 - nsBidiKeyboard.cpp compilation failure on mingw-w64 due to pointer to int casts loosing precision r=roc --- widget/src/windows/nsBidiKeyboard.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widget/src/windows/nsBidiKeyboard.cpp b/widget/src/windows/nsBidiKeyboard.cpp index 6b27f2435dbe..3bd96b33c9eb 100644 --- a/widget/src/windows/nsBidiKeyboard.cpp +++ b/widget/src/windows/nsBidiKeyboard.cpp @@ -158,12 +158,12 @@ nsresult nsBidiKeyboard::SetupBidiKeyboards() locale = buf[keyboards]; if (IsRTLLanguage(locale)) { _snwprintf(mRTLKeyboard, KL_NAMELENGTH, L"%.*x", KL_NAMELENGTH - 1, - LANGIDFROMLCID((DWORD)locale)); + LANGIDFROMLCID((DWORD_PTR)locale)); isRTLKeyboardSet = PR_TRUE; } else { _snwprintf(mLTRKeyboard, KL_NAMELENGTH, L"%.*x", KL_NAMELENGTH - 1, - LANGIDFROMLCID((DWORD)locale)); + LANGIDFROMLCID((DWORD_PTR)locale)); isLTRKeyboardSet = PR_TRUE; } } @@ -213,7 +213,7 @@ nsresult nsBidiKeyboard::SetupBidiKeyboards() PRBool nsBidiKeyboard::IsRTLLanguage(HKL aLocale) { LOCALESIGNATURE localesig; - return (::GetLocaleInfoW(PRIMARYLANGID((DWORD)aLocale), + return (::GetLocaleInfoW(PRIMARYLANGID((DWORD_PTR)aLocale), LOCALE_FONTSIGNATURE, (LPWSTR)&localesig, (sizeof(localesig)/sizeof(WCHAR))) && From d261e01f7ae2cfdb1963de26fd8470f313157f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A3o=20Gottwald?= Date: Fri, 25 Jun 2010 14:19:12 +0200 Subject: [PATCH 1829/1860] Bug 574435 - Hide the menu bar by default on Win Vista and 7. r=gavin --- browser/base/content/browser.js | 11 +++++- browser/base/content/win6BrowserOverlay.xul | 44 +++++++++++++++++++++ browser/base/jar.mn | 6 +++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 browser/base/content/win6BrowserOverlay.xul diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index a2c67ed8504c..f445f232ebe3 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -4640,8 +4640,15 @@ var TabsOnTop = { #ifdef MENUBAR_CAN_AUTOHIDE function updateAppButtonDisplay() { - document.getElementById("appmenu-button-container").hidden = - document.getElementById("toolbar-menubar").getAttribute("autohide") != "true"; + var menubarHidden = + document.getElementById("toolbar-menubar").getAttribute("autohide") == "true"; + + document.getElementById("appmenu-button-container").hidden = !menubarHidden; + + if (menubarHidden) + document.documentElement.setAttribute("chromemargin", "0,-1,-1,-1"); + else + document.documentElement.removeAttribute("chromemargin"); } #endif diff --git a/browser/base/content/win6BrowserOverlay.xul b/browser/base/content/win6BrowserOverlay.xul new file mode 100644 index 000000000000..c2dbaf3fb34b --- /dev/null +++ b/browser/base/content/win6BrowserOverlay.xul @@ -0,0 +1,44 @@ + +# -*- Mode: HTML -*- +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org Code. +# +# The Initial Developer of the Original Code is the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Dão Gottwald (Original Author) +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + + + + diff --git a/browser/base/jar.mn b/browser/base/jar.mn index 6b1051af84de..21fdaa9cca88 100644 --- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -6,6 +6,9 @@ browser.jar: % overlay chrome://global/content/console.xul chrome://browser/content/jsConsoleOverlay.xul % overlay chrome://mozapps/content/update/updates.xul chrome://browser/content/softwareUpdateOverlay.xul #endif +#ifdef XP_WIN +% overlay chrome://browser/content/browser.xul chrome://browser/content/win6BrowserOverlay.xul os=WINNT osversion>=6 +#endif % overlay chrome://global/content/viewSource.xul chrome://browser/content/viewSourceOverlay.xul % overlay chrome://global/content/viewPartialSource.xul chrome://browser/content/viewSourceOverlay.xul % style chrome://global/content/customizeToolbar.xul chrome://browser/content/browser.css @@ -59,6 +62,9 @@ browser.jar: * content/browser/softwareUpdateOverlay.xul (content/softwareUpdateOverlay.xul) #endif * content/browser/viewSourceOverlay.xul (content/viewSourceOverlay.xul) +#ifdef XP_WIN +* content/browser/win6BrowserOverlay.xul (content/win6BrowserOverlay.xul) +#endif # the following files are browser-specific overrides * content/browser/license.html (/toolkit/content/license.html) % override chrome://global/content/license.html chrome://browser/content/license.html From ce7dc39eb7875a10bfd6c2257963912eccbf816b Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 25 Jun 2010 14:20:33 +0200 Subject: [PATCH 1830/1860] Bug 573326 - In the toolbar customization window, hide the toolbarbutton label and replicate it on the wrapper. r=gavin --- toolkit/content/customizeToolbar.css | 5 +++++ toolkit/content/customizeToolbar.js | 2 ++ 2 files changed, 7 insertions(+) diff --git a/toolkit/content/customizeToolbar.css b/toolkit/content/customizeToolbar.css index 6bb38ce087e5..89a753e8f221 100644 --- a/toolkit/content/customizeToolbar.css +++ b/toolkit/content/customizeToolbar.css @@ -57,3 +57,8 @@ #main-box > box { overflow: hidden; } + +/* Hide the toolbarbutton label because we replicate it on the wrapper */ +toolbarpaletteitem[place="palette"] > .toolbarbutton-1 > .toolbarbutton-text { + display: none; +} diff --git a/toolkit/content/customizeToolbar.js b/toolkit/content/customizeToolbar.js index a0c7183668a3..587e09c44ac7 100644 --- a/toolkit/content/customizeToolbar.js +++ b/toolkit/content/customizeToolbar.js @@ -455,6 +455,8 @@ function cleanUpItemForPalette(aItem, aWrapper) if (aItem.hasAttribute("title")) aWrapper.setAttribute("title", aItem.getAttribute("title")); + else if (aItem.hasAttribute("label")) + aWrapper.setAttribute("title", aItem.getAttribute("label")); else if (isSpecialItem(aItem)) { var stringBundle = document.getElementById("stringBundle"); // Remove the common "toolbar" prefix to generate the string name. From aa25b35f39d122a7120670392bce9bdc4b6a94a9 Mon Sep 17 00:00:00 2001 From: Mounir Lamouri Date: Fri, 25 Jun 2010 14:23:10 +0200 Subject: [PATCH 1831/1860] Bug 573698 - When looking for tooltips, don't look at SVG elements if SVG is disabled. r=gavin --- browser/base/content/browser.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index f445f232ebe3..e19d213ce954 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2673,7 +2673,11 @@ function FillInHTMLTooltip(tipElement) var titleText = null; var XLinkTitleText = null; var SVGTitleText = null; +#ifdef MOZ_SVG var lookingForSVGTitle = true; +#else + var lookingForSVGTitle = false; +#endif // MOZ_SVG var direction = tipElement.ownerDocument.dir; while (!titleText && !XLinkTitleText && !SVGTitleText && tipElement) { From 5eb35c15ee446b657d91202638d5d70355a6f8e2 Mon Sep 17 00:00:00 2001 From: Mike Kristoffersen Date: Fri, 25 Jun 2010 14:25:31 +0200 Subject: [PATCH 1832/1860] Bug 562181 - Add support for MozOrientation on Qt. r=dougt --- dom/Makefile.in | 1 + dom/base/nsGlobalWindow.cpp | 3 +- dom/system/Makefile.in | 80 +++++++++++++++++++ dom/system/android/Makefile.in | 59 ++++++++++++++ .../system/android/nsAccelerometerSystem.cpp | 12 +-- .../system/android/nsAccelerometerSystem.h | 12 +-- dom/system/cocoa/Makefile.in | 58 ++++++++++++++ .../system/cocoa/nsAccelerometerSystem.h | 10 +-- .../system/cocoa/nsAccelerometerSystem.mm | 38 ++++----- .../system}/nsAccelerometer.cpp | 0 .../system}/nsAccelerometer.h | 6 ++ dom/system/unix/Makefile.in | 58 ++++++++++++++ .../system/unix/nsAccelerometerSystem.cpp | 18 ++--- .../system/unix/nsAccelerometerSystem.h | 14 ++-- dom/system/windows/Makefile.in | 58 ++++++++++++++ .../system/windows/nsAccelerometerSystem.cpp | 24 +++--- .../system/windows/nsAccelerometerSystem.h | 10 +-- layout/build/Makefile.in | 31 +++++++ layout/build/nsLayoutModule.cpp | 23 ++++++ widget/public/Makefile.in | 1 - widget/public/nsWidgetsCID.h | 6 -- widget/src/android/Makefile.in | 2 +- widget/src/android/nsAppShell.cpp | 4 +- widget/src/android/nsWidgetFactory.cpp | 9 +-- widget/src/build/nsWinWidgetFactory.cpp | 8 -- widget/src/cocoa/Makefile.in | 1 - widget/src/cocoa/nsWidgetFactory.mm | 6 -- widget/src/gtk2/Makefile.in | 1 - widget/src/gtk2/nsWidgetFactory.cpp | 6 -- widget/src/windows/Makefile.in | 1 - widget/src/xpwidgets/Makefile.in | 1 - xpcom/system/Makefile.in | 1 + .../system}/nsIAccelerometer.idl | 0 33 files changed, 449 insertions(+), 113 deletions(-) create mode 100644 dom/system/Makefile.in create mode 100644 dom/system/android/Makefile.in rename widget/src/android/nsAccelerometerAndroid.cpp => dom/system/android/nsAccelerometerSystem.cpp (88%) rename widget/src/android/nsAccelerometerAndroid.h => dom/system/android/nsAccelerometerSystem.h (89%) create mode 100644 dom/system/cocoa/Makefile.in rename widget/src/cocoa/nsAccelerometerX.h => dom/system/cocoa/nsAccelerometerSystem.h (91%) rename widget/src/cocoa/nsAccelerometerX.mm => dom/system/cocoa/nsAccelerometerSystem.mm (94%) rename {widget/src/xpwidgets => dom/system}/nsAccelerometer.cpp (100%) rename {widget/src/xpwidgets => dom/system}/nsAccelerometer.h (93%) create mode 100644 dom/system/unix/Makefile.in rename widget/src/gtk2/nsAccelerometerUnix.cpp => dom/system/unix/nsAccelerometerSystem.cpp (94%) rename widget/src/gtk2/nsAccelerometerUnix.h => dom/system/unix/nsAccelerometerSystem.h (89%) create mode 100644 dom/system/windows/Makefile.in rename widget/src/windows/nsAccelerometerWin.cpp => dom/system/windows/nsAccelerometerSystem.cpp (96%) rename widget/src/windows/nsAccelerometerWin.h => dom/system/windows/nsAccelerometerSystem.h (92%) rename {widget/public => xpcom/system}/nsIAccelerometer.idl (100%) diff --git a/dom/Makefile.in b/dom/Makefile.in index 3f8e157058c8..fae851fd05a9 100644 --- a/dom/Makefile.in +++ b/dom/Makefile.in @@ -81,6 +81,7 @@ DIRS += \ locales \ plugins \ indexedDB \ + system \ $(NULL) ifdef ENABLE_TESTS diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index 61c3c438b3f7..f9b41b554cf5 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -86,8 +86,7 @@ #include "nsCanvasFrame.h" #include "nsIWidget.h" #include "nsIBaseWindow.h" -#include "nsIAccelerometer.h" -#include "nsWidgetsCID.h" +#include "nsAccelerometer.h" #include "nsIContent.h" #include "nsIContentViewerEdit.h" #include "nsIDocShell.h" diff --git a/dom/system/Makefile.in b/dom/system/Makefile.in new file mode 100644 index 000000000000..a4a3c179fa5b --- /dev/null +++ b/dom/system/Makefile.in @@ -0,0 +1,80 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = dom +LIBRARY_NAME = domsystem_s +LIBXUL_LIBRARY = 1 + +ifneq (,$(filter qt gtk2,$(MOZ_WIDGET_TOOLKIT))) +DIRS = unix +endif + +ifneq (,$(filter windows,$(MOZ_WIDGET_TOOLKIT))) +DIRS = windows +endif + +ifneq (,$(filter cocoa,$(MOZ_WIDGET_TOOLKIT))) +DIRS = cocoa +endif + +ifneq (,$(filter android,$(MOZ_WIDGET_TOOLKIT))) +DIRS = android +endif + +CPPSRCS = \ + nsAccelerometer.cpp \ + $(NULL) + +EXPORTS = \ + nsAccelerometer.h \ + $(NULL) + +include $(topsrcdir)/config/config.mk + +# we don't want the shared lib, but we want to force the creation of a static lib. +LIBXUL_LIBRARY = 1 +FORCE_STATIC_LIB = 1 +EXPORT_LIBRARY = 1 + +include $(topsrcdir)/config/rules.mk + diff --git a/dom/system/android/Makefile.in b/dom/system/android/Makefile.in new file mode 100644 index 000000000000..478b4d228710 --- /dev/null +++ b/dom/system/android/Makefile.in @@ -0,0 +1,59 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org build system. +# +# The Initial Developer of the Original Code is Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Kristoffersen +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = dom +LIBRARY_NAME = domsystemandroid_s + +# we don't want the shared lib, but we want to force the creation of a static lib. +LIBXUL_LIBRARY = 1 +FORCE_STATIC_LIB = 1 +EXPORT_LIBRARY = 1 + +include $(topsrcdir)/config/config.mk + +CPPSRCS = \ + nsAccelerometerSystem.cpp \ + $(NULL) + +include $(topsrcdir)/config/rules.mk + diff --git a/widget/src/android/nsAccelerometerAndroid.cpp b/dom/system/android/nsAccelerometerSystem.cpp similarity index 88% rename from widget/src/android/nsAccelerometerAndroid.cpp rename to dom/system/android/nsAccelerometerSystem.cpp index 7fce6d481dcd..8debd3ca6bbc 100644 --- a/widget/src/android/nsAccelerometerAndroid.cpp +++ b/dom/system/android/nsAccelerometerSystem.cpp @@ -34,29 +34,29 @@ * * ***** END LICENSE BLOCK ***** */ -#include "nsAccelerometerAndroid.h" +#include "nsAccelerometerSystem.h" #include "AndroidBridge.h" using namespace mozilla; -extern nsAccelerometerAndroid *gAccel; +extern nsAccelerometerSystem *gAccel; -nsAccelerometerAndroid::nsAccelerometerAndroid() +nsAccelerometerSystem::nsAccelerometerSystem() { gAccel = this; } -nsAccelerometerAndroid::~nsAccelerometerAndroid() +nsAccelerometerSystem::~nsAccelerometerSystem() { } -void nsAccelerometerAndroid::Startup() +void nsAccelerometerSystem::Startup() { AndroidBridge::Bridge()->EnableAccelerometer(true); } -void nsAccelerometerAndroid::Shutdown() +void nsAccelerometerSystem::Shutdown() { AndroidBridge::Bridge()->EnableAccelerometer(false); } diff --git a/widget/src/android/nsAccelerometerAndroid.h b/dom/system/android/nsAccelerometerSystem.h similarity index 89% rename from widget/src/android/nsAccelerometerAndroid.h rename to dom/system/android/nsAccelerometerSystem.h index 417a80fa9c87..8d6cf3c060d8 100644 --- a/widget/src/android/nsAccelerometerAndroid.h +++ b/dom/system/android/nsAccelerometerSystem.h @@ -34,21 +34,21 @@ * * ***** END LICENSE BLOCK ***** */ -#ifndef nsAccelerometerAndroid_h -#define nsAccelerometerAndroid_h +#ifndef nsAccelerometerSystem_h +#define nsAccelerometerSystem_h #include "nsAccelerometer.h" -class nsAccelerometerAndroid : public nsAccelerometer +class nsAccelerometerSystem : public nsAccelerometer { public: - nsAccelerometerAndroid(); - virtual ~nsAccelerometerAndroid(); + nsAccelerometerSystem(); + virtual ~nsAccelerometerSystem(); private: virtual void Startup(); virtual void Shutdown(); }; -#endif /* nsAccelerometerAndroid_h */ +#endif /* nsAccelerometerSystem_h */ diff --git a/dom/system/cocoa/Makefile.in b/dom/system/cocoa/Makefile.in new file mode 100644 index 000000000000..aa50e19c3531 --- /dev/null +++ b/dom/system/cocoa/Makefile.in @@ -0,0 +1,58 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org build system. +# +# The Initial Developer of the Original Code is Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Kristoffersen +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = dom +LIBRARY_NAME = domsystemcocoa_s + +# we don't want the shared lib, but we want to force the creation of a static lib. +LIBXUL_LIBRARY = 1 +FORCE_STATIC_LIB = 1 +EXPORT_LIBRARY = 1 + +include $(topsrcdir)/config/config.mk + +CMMSRCS = \ + nsAccelerometerSystem.mm \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/widget/src/cocoa/nsAccelerometerX.h b/dom/system/cocoa/nsAccelerometerSystem.h similarity index 91% rename from widget/src/cocoa/nsAccelerometerX.h rename to dom/system/cocoa/nsAccelerometerSystem.h index c70459087e5d..0ba7da7b14c1 100644 --- a/widget/src/cocoa/nsAccelerometerX.h +++ b/dom/system/cocoa/nsAccelerometerSystem.h @@ -34,19 +34,19 @@ * * ***** END LICENSE BLOCK ***** */ -#ifndef nsAccelerometerX_h -#define nsAccelerometerX_h +#ifndef nsAccelerometerSystem_h +#define nsAccelerometerSystem_h #include #include #include "nsAccelerometer.h" -class nsAccelerometerX : public nsAccelerometer +class nsAccelerometerSystem : public nsAccelerometer { public: - nsAccelerometerX(); - ~nsAccelerometerX(); + nsAccelerometerSystem(); + ~nsAccelerometerSystem(); void Startup(); void Shutdown(); diff --git a/widget/src/cocoa/nsAccelerometerX.mm b/dom/system/cocoa/nsAccelerometerSystem.mm similarity index 94% rename from widget/src/cocoa/nsAccelerometerX.mm rename to dom/system/cocoa/nsAccelerometerSystem.mm index f248524765b2..655ea39791fa 100644 --- a/widget/src/cocoa/nsAccelerometerX.mm +++ b/dom/system/cocoa/nsAccelerometerSystem.mm @@ -34,7 +34,7 @@ * * ***** END LICENSE BLOCK ***** */ -#include "nsAccelerometerX.h" +#include "nsAccelerometerSystem.h" #include "nsIServiceManager.h" #include "stdlib.h" @@ -45,11 +45,11 @@ #define MODEL_NAME_LENGTH 64 static char gModelName[MODEL_NAME_LENGTH]; -nsAccelerometerX::nsAccelerometerX() +nsAccelerometerSystem::nsAccelerometerSystem() { } -nsAccelerometerX::~nsAccelerometerX() +nsAccelerometerSystem::~nsAccelerometerSystem() { } @@ -72,9 +72,9 @@ typedef struct #define SMSDATA_USED_SIZE (sizeof(SmsData) - SMSDATA_PADDING_SIZE) void -nsAccelerometerX::UpdateHandler(nsITimer *aTimer, void *aClosure) +nsAccelerometerSystem::UpdateHandler(nsITimer *aTimer, void *aClosure) { - nsAccelerometerX *self = reinterpret_cast(aClosure); + nsAccelerometerSystem *self = reinterpret_cast(aClosure); if (!self) { NS_ERROR("no self"); return; @@ -87,7 +87,7 @@ nsAccelerometerX::UpdateHandler(nsITimer *aTimer, void *aClosure) if (!input || !output) return; - + memset(input, 0, bufferLen); memset(output, 0, bufferLen); @@ -115,14 +115,14 @@ nsAccelerometerX::UpdateHandler(nsITimer *aTimer, void *aClosure) } SmsData *data = (SmsData*) output; - + float xf, yf, zf; // we want to normalize the return result from the chip to // something between -1 and 1 where 0 is the balance point. const int normalizeFactor = 250.5; - + if (!strcmp(gModelName, "MacBookPro5,1")) { xf = ((float)data->x) / normalizeFactor; yf = (((float)data->y) / normalizeFactor) * -1; @@ -146,7 +146,7 @@ nsAccelerometerX::UpdateHandler(nsITimer *aTimer, void *aClosure) self->AccelerationChanged( xf, yf, zf ); } -void nsAccelerometerX::Startup() +void nsAccelerometerSystem::Startup() { // we can fail, and that just means the caller will not see any changes. @@ -154,31 +154,31 @@ void nsAccelerometerX::Startup() kern_return_t result = ::IOMasterPort(MACH_PORT_NULL, &port); if (result != kIOReturnSuccess) return; - + CFMutableDictionaryRef dict = ::IOServiceMatching("SMCMotionSensor"); if (!dict) return; - + io_iterator_t iter; result = ::IOServiceGetMatchingServices(port, dict, &iter); if (result != kIOReturnSuccess) return; - + io_object_t device = ::IOIteratorNext(iter); ::IOObjectRelease(iter); - + if (!device) return; - + result = ::IOServiceOpen(device, mach_task_self(), 0, &mSmsConnection); ::IOObjectRelease(device); - + if (result != kIOReturnSuccess) return; - + mach_port_deallocate(mach_task_self(), port); - + /* get the version of the hardware we are running on. */ int mib[2]; size_t len = MODEL_NAME_LENGTH; @@ -194,11 +194,11 @@ void nsAccelerometerX::Startup() nsITimer::TYPE_REPEATING_SLACK); } -void nsAccelerometerX::Shutdown() +void nsAccelerometerSystem::Shutdown() { if (mSmsConnection) ::IOServiceClose(mSmsConnection); - + if (mUpdateTimer) { mUpdateTimer->Cancel(); mUpdateTimer = nsnull; diff --git a/widget/src/xpwidgets/nsAccelerometer.cpp b/dom/system/nsAccelerometer.cpp similarity index 100% rename from widget/src/xpwidgets/nsAccelerometer.cpp rename to dom/system/nsAccelerometer.cpp diff --git a/widget/src/xpwidgets/nsAccelerometer.h b/dom/system/nsAccelerometer.h similarity index 93% rename from widget/src/xpwidgets/nsAccelerometer.h rename to dom/system/nsAccelerometer.h index 0f87d52e60ed..f8d1f3cc69cf 100644 --- a/widget/src/xpwidgets/nsAccelerometer.h +++ b/dom/system/nsAccelerometer.h @@ -42,6 +42,12 @@ #include "nsCOMPtr.h" #include "nsITimer.h" +#define NS_ACCELEROMETER_CID \ +{ 0xecba5203, 0x77da, 0x465a, \ +{ 0x86, 0x5e, 0x78, 0xb7, 0xaf, 0x10, 0xd8, 0xf7 } } + +#define NS_ACCELEROMETER_CONTRACTID "@mozilla.org/accelerometer;1" + class nsIDOMWindow; class nsAccelerometer : public nsIAccelerometer diff --git a/dom/system/unix/Makefile.in b/dom/system/unix/Makefile.in new file mode 100644 index 000000000000..b498b728463b --- /dev/null +++ b/dom/system/unix/Makefile.in @@ -0,0 +1,58 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org build system. +# +# The Initial Developer of the Original Code is Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Kristoffersen +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = dom +LIBRARY_NAME = domsystemunix_s + +# we don't want the shared lib, but we want to force the creation of a static lib. +LIBXUL_LIBRARY = 1 +FORCE_STATIC_LIB = 1 +EXPORT_LIBRARY = 1 + +include $(topsrcdir)/config/config.mk + +CPPSRCS = \ + nsAccelerometerSystem.cpp \ + $(NULL) + +include $(topsrcdir)/config/rules.mk \ No newline at end of file diff --git a/widget/src/gtk2/nsAccelerometerUnix.cpp b/dom/system/unix/nsAccelerometerSystem.cpp similarity index 94% rename from widget/src/gtk2/nsAccelerometerUnix.cpp rename to dom/system/unix/nsAccelerometerSystem.cpp index 4d6802ea5bca..bb4e59bac0ba 100644 --- a/widget/src/gtk2/nsAccelerometerUnix.cpp +++ b/dom/system/unix/nsAccelerometerSystem.cpp @@ -36,13 +36,13 @@ * ***** END LICENSE BLOCK ***** */ #include -#include "nsAccelerometerUnix.h" +#include "nsAccelerometerSystem.h" #include "nsIServiceManager.h" typedef struct { const char* mPosition; const char* mCalibrate; - nsAccelerometerUnixDriver mToken; + nsAccelerometerSystemDriver mToken; } Accelerometer; static const Accelerometer gAccelerometers[] = { @@ -64,21 +64,21 @@ static const Accelerometer gAccelerometers[] = { eHPdv7Sensor}, }; -nsAccelerometerUnix::nsAccelerometerUnix() : +nsAccelerometerSystem::nsAccelerometerSystem() : mPositionFile(NULL), mCalibrateFile(NULL), mType(eNoSensor) { } -nsAccelerometerUnix::~nsAccelerometerUnix() +nsAccelerometerSystem::~nsAccelerometerSystem() { } void -nsAccelerometerUnix::UpdateHandler(nsITimer *aTimer, void *aClosure) +nsAccelerometerSystem::UpdateHandler(nsITimer *aTimer, void *aClosure) { - nsAccelerometerUnix *self = reinterpret_cast(aClosure); + nsAccelerometerSystem *self = reinterpret_cast(aClosure); if (!self) { NS_ERROR("no self"); return; @@ -190,7 +190,7 @@ nsAccelerometerUnix::UpdateHandler(nsITimer *aTimer, void *aClosure) self->AccelerationChanged( xf, yf, zf ); } -void nsAccelerometerUnix::Startup() +void nsAccelerometerSystem::Startup() { // Accelerometers in Linux are used by reading a file (yay UNIX!), which is // in a slightly different location depending on the driver. @@ -213,7 +213,7 @@ void nsAccelerometerUnix::Startup() if (mType == eNoSensor) return; - + mUpdateTimer = do_CreateInstance("@mozilla.org/timer;1"); if (mUpdateTimer) mUpdateTimer->InitWithFuncCallback(UpdateHandler, @@ -222,7 +222,7 @@ void nsAccelerometerUnix::Startup() nsITimer::TYPE_REPEATING_SLACK); } -void nsAccelerometerUnix::Shutdown() +void nsAccelerometerSystem::Shutdown() { if (mPositionFile) { fclose(mPositionFile); diff --git a/widget/src/gtk2/nsAccelerometerUnix.h b/dom/system/unix/nsAccelerometerSystem.h similarity index 89% rename from widget/src/gtk2/nsAccelerometerUnix.h rename to dom/system/unix/nsAccelerometerSystem.h index 7cf0dbdf53be..5971ce39c83e 100644 --- a/widget/src/gtk2/nsAccelerometerUnix.h +++ b/dom/system/unix/nsAccelerometerSystem.h @@ -35,13 +35,13 @@ * * ***** END LICENSE BLOCK ***** */ -#ifndef nsAccelerometerUnix_h -#define nsAccelerometerUnix_h +#ifndef nsAccelerometerSystem_h +#define nsAccelerometerSystem_h #include #include "nsAccelerometer.h" -enum nsAccelerometerUnixDriver +enum nsAccelerometerSystemDriver { eNoSensor, eAppleSensor, @@ -50,18 +50,18 @@ enum nsAccelerometerUnixDriver eHPdv7Sensor }; -class nsAccelerometerUnix : public nsAccelerometer +class nsAccelerometerSystem : public nsAccelerometer { public: - nsAccelerometerUnix(); - ~nsAccelerometerUnix(); + nsAccelerometerSystem(); + ~nsAccelerometerSystem(); void Startup(); void Shutdown(); FILE* mPositionFile; FILE* mCalibrateFile; - nsAccelerometerUnixDriver mType; + nsAccelerometerSystemDriver mType; nsCOMPtr mUpdateTimer; static void UpdateHandler(nsITimer *aTimer, void *aClosure); diff --git a/dom/system/windows/Makefile.in b/dom/system/windows/Makefile.in new file mode 100644 index 000000000000..bdbb71a4316a --- /dev/null +++ b/dom/system/windows/Makefile.in @@ -0,0 +1,58 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org build system. +# +# The Initial Developer of the Original Code is Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Kristoffersen +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = dom +LIBRARY_NAME = domsystemwindows_s + +# we don't want the shared lib, but we want to force the creation of a static lib. +LIBXUL_LIBRARY = 1 +FORCE_STATIC_LIB = 1 +EXPORT_LIBRARY = 1 + +include $(topsrcdir)/config/config.mk + +CPPSRCS = \ + nsAccelerometerSystem.cpp \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/widget/src/windows/nsAccelerometerWin.cpp b/dom/system/windows/nsAccelerometerSystem.cpp similarity index 96% rename from widget/src/windows/nsAccelerometerWin.cpp rename to dom/system/windows/nsAccelerometerSystem.cpp index bdf720f562a0..c4d309e70cc4 100644 --- a/widget/src/windows/nsAccelerometerWin.cpp +++ b/dom/system/windows/nsAccelerometerSystem.cpp @@ -35,14 +35,14 @@ * * ***** END LICENSE BLOCK ***** */ -#include "nsAccelerometerWin.h" +#include "nsAccelerometerSystem.h" #include "nsIServiceManager.h" #include "windows.h" #ifdef WINCE_WINDOWS_MOBILE //////////////////////////// -// HTC +// HTC //////////////////////////// @@ -91,7 +91,7 @@ PRBool HTCSensor::Startup() { HMODULE hSensorLib = LoadLibraryW(L"HTCSensorSDK.dll"); - + if (!hSensorLib) return PR_FALSE; @@ -183,8 +183,8 @@ typedef struct FLOAT z; /**< Z-direction value */ } SmiAccelerometerVector; -/** - * Specifies the capabilities of the Accelerometer device. +/** + * Specifies the capabilities of the Accelerometer device. */ typedef struct { @@ -450,13 +450,13 @@ ThinkPadSensor::GetValues(double *x, double *y, double *z) #endif -nsAccelerometerWin::nsAccelerometerWin(){} -nsAccelerometerWin::~nsAccelerometerWin(){} +nsAccelerometerSystem::nsAccelerometerSystem(){} +nsAccelerometerSystem::~nsAccelerometerSystem(){} void -nsAccelerometerWin::UpdateHandler(nsITimer *aTimer, void *aClosure) +nsAccelerometerSystem::UpdateHandler(nsITimer *aTimer, void *aClosure) { - nsAccelerometerWin *self = reinterpret_cast(aClosure); + nsAccelerometerSystem *self = reinterpret_cast(aClosure); if (!self || !self->mSensor) { NS_ERROR("no self or sensor"); return; @@ -466,7 +466,7 @@ nsAccelerometerWin::UpdateHandler(nsITimer *aTimer, void *aClosure) self->AccelerationChanged(x, y, z); } -void nsAccelerometerWin::Startup() +void nsAccelerometerSystem::Startup() { NS_ASSERTION(!mSensor, "mSensor should be null. Startup called twice?"); @@ -499,7 +499,7 @@ void nsAccelerometerWin::Startup() started = mSensor->Startup(); #endif - + if (!started) return; @@ -511,7 +511,7 @@ void nsAccelerometerWin::Startup() nsITimer::TYPE_REPEATING_SLACK); } -void nsAccelerometerWin::Shutdown() +void nsAccelerometerSystem::Shutdown() { if (mUpdateTimer) { mUpdateTimer->Cancel(); diff --git a/widget/src/windows/nsAccelerometerWin.h b/dom/system/windows/nsAccelerometerSystem.h similarity index 92% rename from widget/src/windows/nsAccelerometerWin.h rename to dom/system/windows/nsAccelerometerSystem.h index 2c76e268792a..2ff277f345a5 100644 --- a/widget/src/windows/nsAccelerometerWin.h +++ b/dom/system/windows/nsAccelerometerSystem.h @@ -34,8 +34,8 @@ * * ***** END LICENSE BLOCK ***** */ -#ifndef nsAccelerometerWin_h -#define nsAccelerometerWin_h +#ifndef nsAccelerometerSystem_h +#define nsAccelerometerSystem_h #include "nsAccelerometer.h" #include "nsAutoPtr.h" @@ -48,11 +48,11 @@ class Sensor virtual void GetValues(double *x, double *y, double *z) = 0; }; -class nsAccelerometerWin : public nsAccelerometer +class nsAccelerometerSystem : public nsAccelerometer { public: - nsAccelerometerWin(); - ~nsAccelerometerWin(); + nsAccelerometerSystem(); + ~nsAccelerometerSystem(); void Startup(); void Shutdown(); diff --git a/layout/build/Makefile.in b/layout/build/Makefile.in index 7b77211ffcc1..d3358be33e91 100644 --- a/layout/build/Makefile.in +++ b/layout/build/Makefile.in @@ -104,6 +104,7 @@ SHARED_LIBRARY_LIBS = \ $(DEPTH)/dom/src/storage/$(LIB_PREFIX)jsdomstorage_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/src/offline/$(LIB_PREFIX)jsdomoffline_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/src/geolocation/$(LIB_PREFIX)jsdomgeolocation_s.$(LIB_SUFFIX) \ + $(DEPTH)/dom/system/$(LIB_PREFIX)domsystem_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/src/threads/$(LIB_PREFIX)domthreads_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/indexedDB/$(LIB_PREFIX)dom_indexeddb_s.$(LIB_SUFFIX) \ $(DEPTH)/editor/libeditor/text/$(LIB_PREFIX)texteditor_s.$(LIB_SUFFIX) \ @@ -112,6 +113,36 @@ SHARED_LIBRARY_LIBS = \ $(DEPTH)/caps/src/$(LIB_PREFIX)caps_s.$(LIB_SUFFIX) \ $(NULL) +ifneq (,$(filter qt gtk2,$(MOZ_WIDGET_TOOLKIT))) +SHARED_LIBRARY_LIBS += \ + $(DEPTH)/dom/system/unix/$(LIB_PREFIX)domsystemunix_s.$(LIB_SUFFIX) \ + $(NULL) +LOCAL_INCLUDES += \ + -I$(topsrcdir)/dom/system/unix \ + $(NULL) +else ifneq (,$(filter windows,$(MOZ_WIDGET_TOOLKIT))) +SHARED_LIBRARY_LIBS += \ + $(DEPTH)/dom/system/windows/$(LIB_PREFIX)domsystemwindows_s.$(LIB_SUFFIX) \ + $(NULL) +LOCAL_INCLUDES += \ + -I$(topsrcdir)/dom/system/windows \ + $(NULL) +else ifneq (,$(filter cocoa,$(MOZ_WIDGET_TOOLKIT))) +SHARED_LIBRARY_LIBS += \ + $(DEPTH)/dom/system/cocoa/$(LIB_PREFIX)domsystemcocoa_s.$(LIB_SUFFIX) \ + $(NULL) +LOCAL_INCLUDES += \ + -I$(topsrcdir)/dom/system/cocoa \ + $(NULL) +else ifneq (,$(filter android,$(MOZ_WIDGET_TOOLKIT))) +SHARED_LIBRARY_LIBS += \ + $(DEPTH)/dom/system/android/$(LIB_PREFIX)domsystemandroid_s.$(LIB_SUFFIX) \ + $(NULL) +LOCAL_INCLUDES += \ + -I$(topsrcdir)/dom/system/android \ + $(NULL) +endif + ifdef MOZ_VORBIS SHARED_LIBRARY_LIBS += \ $(DEPTH)/media/libvorbis/lib/$(LIB_PREFIX)vorbis.$(LIB_SUFFIX) \ diff --git a/layout/build/nsLayoutModule.cpp b/layout/build/nsLayoutModule.cpp index eaa7297486e6..ada8c7cc8ad4 100644 --- a/layout/build/nsLayoutModule.cpp +++ b/layout/build/nsLayoutModule.cpp @@ -279,6 +279,12 @@ static void Shutdown(); #endif #include "nsGeolocation.h" +#if defined(XP_UNIX) || \ + defined(_WINDOWS) || \ + defined(machintosh) || \ + defined(android) +#include "nsAccelerometerSystem.h" +#endif #include "nsCSPService.h" // Transformiix @@ -311,6 +317,12 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsDOMParser) NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsDOMStorageManager, nsDOMStorageManager::GetInstance) NS_GENERIC_FACTORY_CONSTRUCTOR(nsChannelPolicy) +#if defined(XP_UNIX) || \ + defined(_WINDOWS) || \ + defined(machintosh) || \ + defined(android) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsAccelerometerSystem) +#endif //----------------------------------------------------------------------------- @@ -1639,6 +1651,17 @@ static const nsModuleComponentInfo gLayoutComponents[] = { NS_EVENTLISTENERSERVICE_CONTRACTID, CreateEventListenerService }, +#if defined(XP_UNIX) || \ + defined(_WINDOWS) || \ + defined(machintosh) || \ + defined(android) + { "Accelerometer", + NS_ACCELEROMETER_CID, + NS_ACCELEROMETER_CONTRACTID, + nsAccelerometerSystemConstructor + }, +#endif + { "Global Message Manager", NS_GLOBALMESSAGEMANAGER_CID, NS_GLOBALMESSAGEMANAGER_CONTRACTID, diff --git a/widget/public/Makefile.in b/widget/public/Makefile.in index 8beba266d419..1e28f9f918e5 100644 --- a/widget/public/Makefile.in +++ b/widget/public/Makefile.in @@ -84,7 +84,6 @@ EXPORTS += \ endif XPIDLSRCS = \ - nsIAccelerometer.idl \ nsIAppShell.idl \ nsIFilePicker.idl \ nsIToolkit.idl \ diff --git a/widget/public/nsWidgetsCID.h b/widget/public/nsWidgetsCID.h index 84e31e3b14f0..4802f0a07911 100644 --- a/widget/public/nsWidgetsCID.h +++ b/widget/public/nsWidgetsCID.h @@ -194,9 +194,3 @@ { 0x06beec76, 0xa183, 0x4d9f, \ { 0x85, 0xdd, 0x08, 0x5f, 0x26, 0xda, 0x56, 0x5a } } -#define NS_ACCELEROMETER_CID \ -{ 0xecba5203, 0x77da, 0x465a, \ -{ 0x86, 0x5e, 0x78, 0xb7, 0xaf, 0x10, 0xd8, 0xf7 } } - -#define NS_ACCELEROMETER_CONTRACTID "@mozilla.org/accelerometer;1" - diff --git a/widget/src/android/Makefile.in b/widget/src/android/Makefile.in index 23b8283e66d0..f49a0bbcfd38 100644 --- a/widget/src/android/Makefile.in +++ b/widget/src/android/Makefile.in @@ -62,7 +62,6 @@ CPPSRCS = \ nsLookAndFeel.cpp \ nsScreenManagerAndroid.cpp \ nsIdleServiceAndroid.cpp \ - nsAccelerometerAndroid.cpp \ $(NULL) NOT_THERE_YET_CPPSRCS = \ @@ -98,6 +97,7 @@ DEFINES += -D_IMPL_NS_WIDGET LOCAL_INCLUDES += \ -I$(topsrcdir)/widget/src/xpwidgets \ + -I$(topsrcdir)/dom/system/android \ -I$(srcdir) \ $(NULL) diff --git a/widget/src/android/nsAppShell.cpp b/widget/src/android/nsAppShell.cpp index 2c8cb0b74dcb..9afeb5ced3ae 100644 --- a/widget/src/android/nsAppShell.cpp +++ b/widget/src/android/nsAppShell.cpp @@ -49,7 +49,7 @@ #include "prenv.h" #include "AndroidBridge.h" -#include "nsAccelerometerAndroid.h" +#include "nsAccelerometerSystem.h" #include #include @@ -70,7 +70,7 @@ using namespace mozilla; PRLogModuleInfo *gWidgetLog = nsnull; #endif -nsAccelerometerAndroid *gAccel = nsnull; +nsAccelerometerSystem *gAccel = nsnull; nsIGeolocationUpdate *gLocationCallback = nsnull; nsAppShell *nsAppShell::gAppShell = nsnull; diff --git a/widget/src/android/nsWidgetFactory.cpp b/widget/src/android/nsWidgetFactory.cpp index e7844bfe1efb..8f1733d283b5 100644 --- a/widget/src/android/nsWidgetFactory.cpp +++ b/widget/src/android/nsWidgetFactory.cpp @@ -49,14 +49,12 @@ #include "nsAppShellSingleton.h" #include "nsScreenManagerAndroid.h" -#include "nsAccelerometerAndroid.h" #include "nsIdleServiceAndroid.h" NS_GENERIC_FACTORY_CONSTRUCTOR(nsToolkit) NS_GENERIC_FACTORY_CONSTRUCTOR(nsWindow) NS_GENERIC_FACTORY_CONSTRUCTOR(nsLookAndFeel) NS_GENERIC_FACTORY_CONSTRUCTOR(nsScreenManagerAndroid) -NS_GENERIC_FACTORY_CONSTRUCTOR(nsAccelerometerAndroid) NS_GENERIC_FACTORY_CONSTRUCTOR(nsIdleServiceAndroid) @@ -89,12 +87,7 @@ static const nsModuleComponentInfo components[] = { "Android Idle Service", NS_IDLE_SERVICE_CID, "@mozilla.org/widget/idleservice;1", - nsIdleServiceAndroidConstructor }, - { "Accelerometer", - NS_ACCELEROMETER_CID, - NS_ACCELEROMETER_CONTRACTID, - nsAccelerometerAndroidConstructor }, - + nsIdleServiceAndroidConstructor } }; static void diff --git a/widget/src/build/nsWinWidgetFactory.cpp b/widget/src/build/nsWinWidgetFactory.cpp index 706b443b9bdb..5519567304be 100644 --- a/widget/src/build/nsWinWidgetFactory.cpp +++ b/widget/src/build/nsWinWidgetFactory.cpp @@ -78,9 +78,6 @@ #include "nsPrintSession.h" #endif -#include "nsAccelerometerWin.h" -NS_GENERIC_FACTORY_CONSTRUCTOR(nsAccelerometerWin) - NS_GENERIC_FACTORY_CONSTRUCTOR(nsWindow) NS_GENERIC_FACTORY_CONSTRUCTOR(ChildWindow) NS_GENERIC_FACTORY_CONSTRUCTOR(nsFilePicker) @@ -213,11 +210,6 @@ static const nsModuleComponentInfo components[] = nsBidiKeyboardConstructor }, #endif - { "Accelerometer", - NS_ACCELEROMETER_CID, - NS_ACCELEROMETER_CONTRACTID, - nsAccelerometerWinConstructor }, - #ifdef NS_PRINTING { "nsPrintOptionsWin", NS_PRINTSETTINGSSERVICE_CID, diff --git a/widget/src/cocoa/Makefile.in b/widget/src/cocoa/Makefile.in index 1ea291d9d68b..4d44ace8b7a0 100644 --- a/widget/src/cocoa/Makefile.in +++ b/widget/src/cocoa/Makefile.in @@ -88,7 +88,6 @@ CMMSRCS = \ nsPrintOptionsX.mm \ nsPrintSettingsX.mm \ nsIdleServiceX.mm \ - nsAccelerometerX.mm \ nsCocoaTextInputHandler.mm \ nsMacDockSupport.mm \ nsStandaloneNativeMenu.mm \ diff --git a/widget/src/cocoa/nsWidgetFactory.mm b/widget/src/cocoa/nsWidgetFactory.mm index 75b11c53f388..6ea558b5e0fe 100644 --- a/widget/src/cocoa/nsWidgetFactory.mm +++ b/widget/src/cocoa/nsWidgetFactory.mm @@ -57,7 +57,6 @@ #include "nsLookAndFeel.h" -#include "nsAccelerometerX.h" #include "nsSound.h" #include "nsIdleServiceX.h" @@ -73,7 +72,6 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsFilePicker) NS_GENERIC_FACTORY_CONSTRUCTOR(nsToolkit) NS_GENERIC_FACTORY_CONSTRUCTOR(nsLookAndFeel) NS_GENERIC_FACTORY_CONSTRUCTOR(nsSound) -NS_GENERIC_FACTORY_CONSTRUCTOR(nsAccelerometerX) NS_GENERIC_FACTORY_CONSTRUCTOR(nsTransferable) NS_GENERIC_FACTORY_CONSTRUCTOR(nsHTMLFormatConverter) NS_GENERIC_FACTORY_CONSTRUCTOR(nsClipboard) @@ -135,10 +133,6 @@ static const nsModuleComponentInfo gComponents[] = NS_SOUND_CID, "@mozilla.org/sound;1", nsSoundConstructor }, - { "Accelerometer", - NS_ACCELEROMETER_CID, - NS_ACCELEROMETER_CONTRACTID, - nsAccelerometerXConstructor }, { "Transferable", NS_TRANSFERABLE_CID, "@mozilla.org/widget/transferable;1", diff --git a/widget/src/gtk2/Makefile.in b/widget/src/gtk2/Makefile.in index 3f25f5775c23..7868061ad00d 100644 --- a/widget/src/gtk2/Makefile.in +++ b/widget/src/gtk2/Makefile.in @@ -78,7 +78,6 @@ CPPSRCS = \ nsScreenManagerGtk.cpp \ nsImageToPixbuf.cpp \ nsAccessibilityHelper.cpp \ - nsAccelerometerUnix.cpp \ nsGtkIMModule.cpp \ $(NULL) diff --git a/widget/src/gtk2/nsWidgetFactory.cpp b/widget/src/gtk2/nsWidgetFactory.cpp index 5aa391763b0c..bba1d97c566f 100644 --- a/widget/src/gtk2/nsWidgetFactory.cpp +++ b/widget/src/gtk2/nsWidgetFactory.cpp @@ -55,7 +55,6 @@ #include "nsBidiKeyboard.h" #include "nsNativeKeyBindings.h" #include "nsScreenManagerGtk.h" -#include "nsAccelerometerUnix.h" #ifdef NS_PRINTING #include "nsPrintOptionsGTK.h" @@ -102,7 +101,6 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsDragService) NS_GENERIC_FACTORY_CONSTRUCTOR(nsSound) NS_GENERIC_FACTORY_CONSTRUCTOR(nsScreenManagerGtk) NS_GENERIC_FACTORY_CONSTRUCTOR(nsImageToPixbuf) -NS_GENERIC_FACTORY_CONSTRUCTOR(nsAccelerometerUnix) #ifdef NATIVE_THEME_SUPPORT @@ -254,10 +252,6 @@ static const nsModuleComponentInfo components[] = NS_SOUND_CID, "@mozilla.org/sound;1", nsSoundConstructor }, - { "Accelerometer", - NS_ACCELEROMETER_CID, - NS_ACCELEROMETER_CONTRACTID, - nsAccelerometerUnixConstructor }, { "Transferable", NS_TRANSFERABLE_CID, "@mozilla.org/widget/transferable;1", diff --git a/widget/src/windows/Makefile.in b/widget/src/windows/Makefile.in index 958a3ed2c836..245d95e06884 100644 --- a/widget/src/windows/Makefile.in +++ b/widget/src/windows/Makefile.in @@ -64,7 +64,6 @@ CPPSRCS = \ nsSound.cpp \ nsIMM32Handler.cpp \ WindowHook.cpp \ - nsAccelerometerWin.cpp \ WinTaskbar.cpp \ TaskbarPreview.cpp \ TaskbarTabPreview.cpp \ diff --git a/widget/src/xpwidgets/Makefile.in b/widget/src/xpwidgets/Makefile.in index c2b75987fe42..5ec94401056f 100644 --- a/widget/src/xpwidgets/Makefile.in +++ b/widget/src/xpwidgets/Makefile.in @@ -68,7 +68,6 @@ CPPSRCS = \ nsWidgetAtoms.cpp \ nsIdleService.cpp \ nsClipboardPrivacyHandler.cpp \ - nsAccelerometer.cpp \ $(NULL) ifneq (,$(filter beos os2 cocoa windows,$(MOZ_WIDGET_TOOLKIT))) diff --git a/xpcom/system/Makefile.in b/xpcom/system/Makefile.in index d244591fc5ba..0d1cd70119fa 100644 --- a/xpcom/system/Makefile.in +++ b/xpcom/system/Makefile.in @@ -52,6 +52,7 @@ XPIDLSRCS = \ nsIGnomeVFSService.idl \ nsIBlocklistService.idl \ nsIGIOService.idl \ + nsIAccelerometer.idl \ $(NULL) ifdef MOZ_CRASHREPORTER diff --git a/widget/public/nsIAccelerometer.idl b/xpcom/system/nsIAccelerometer.idl similarity index 100% rename from widget/public/nsIAccelerometer.idl rename to xpcom/system/nsIAccelerometer.idl From 86efb4de4296e8b6cf53b90df54bd115a98bcdbd Mon Sep 17 00:00:00 2001 From: Jacek Caban Date: Fri, 25 Jun 2010 14:26:50 +0200 Subject: [PATCH 1833/1860] Bug 570949 - Fix mingw compilation. r=ted --- configure.in | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/configure.in b/configure.in index 193dbab604b3..d437a161e9a2 100644 --- a/configure.in +++ b/configure.in @@ -5911,7 +5911,7 @@ if test -n "$MOZ_WEBM"; then dnl Detect if we can use an assembler to compile optimized assembly for libvpx. dnl For Win32 (MSVC) we must use MASM. - if test "$OS_ARCH" = "WINNT" -a "$CPU_ARCH" = "x86" -a "$GNU_CC" != "yes"; then + if test "$OS_ARCH" = "WINNT" -a "$CPU_ARCH" = "x86" -a -z "$GNU_CC"; then AC_MSG_CHECKING([for MASM assembler]) AC_CHECK_PROGS(VPX_AS, ml, "") if test -n "$VPX_AS"; then @@ -5946,8 +5946,10 @@ if test -n "$MOZ_WEBM"; then VPX_X86_ASM=1 ;; WINNT:x86_64) - VPX_ASFLAGS="-f x64 -rnasm -pnasm" - VPX_X86_ASM=1 + if test -z "$GNU_CC"; then + VPX_ASFLAGS="-f x64 -rnasm -pnasm" + VPX_X86_ASM=1 + fi ;; esac fi # end have YASM From d1ffee7708c6bf60474321f3aa31c7122bbd26ca Mon Sep 17 00:00:00 2001 From: Date: Fri, 25 Jun 2010 14:29:07 +0200 Subject: [PATCH 1834/1860] Bug 571842 - add classes to split menu for CSS addressability. r=dao --- browser/base/content/browser.xul | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 38b92c3fe22c..492ea8b65304 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -401,8 +401,9 @@ - + Date: Fri, 25 Jun 2010 21:43:10 +0900 Subject: [PATCH 1835/1860] Bug 561304. Use GDI shaper by default on XP and handle bitmap fonts better. r=jkew --- gfx/thebes/public/gfxFont.h | 1 + gfx/thebes/src/gfxFont.cpp | 4 +- gfx/thebes/src/gfxGDIFont.cpp | 118 +++++++++++++++--- gfx/thebes/src/gfxGDIFont.h | 6 +- gfx/thebes/src/gfxGDIFontList.cpp | 29 ++++- gfx/thebes/src/gfxGDIFontList.h | 4 + gfx/thebes/src/gfxGDIShaper.cpp | 5 + .../line-breaking/non-breakable-2-ref.html | 36 ++++++ .../line-breaking/non-breakable-2.html | 36 ++++++ layout/reftests/line-breaking/reftest.list | 1 + 10 files changed, 217 insertions(+), 23 deletions(-) create mode 100644 layout/reftests/line-breaking/non-breakable-2-ref.html create mode 100644 layout/reftests/line-breaking/non-breakable-2.html diff --git a/gfx/thebes/public/gfxFont.h b/gfx/thebes/public/gfxFont.h index a12147bd31b9..acbc33b8cb7e 100644 --- a/gfx/thebes/public/gfxFont.h +++ b/gfx/thebes/public/gfxFont.h @@ -210,6 +210,7 @@ public: return TestCharacterMap(ch); } + virtual PRBool SkipDuringSystemFallback() { return PR_FALSE; } virtual PRBool TestCharacterMap(PRUint32 aCh); nsresult InitializeUVSMap(); PRUint16 GetUVSGlyph(PRUint32 aCh, PRUint32 aVS); diff --git a/gfx/thebes/src/gfxFont.cpp b/gfx/thebes/src/gfxFont.cpp index a6e8f8145f1e..5650e3519f65 100644 --- a/gfx/thebes/src/gfxFont.cpp +++ b/gfx/thebes/src/gfxFont.cpp @@ -566,7 +566,9 @@ gfxFontFamily::FindFontForChar(FontSearch *aMatchData) PRUint32 numFonts = mAvailableFonts.Length(); for (PRUint32 i = 0; i < numFonts; i++) { gfxFontEntry *fe = mAvailableFonts[i]; - if (!fe) + + // skip certain fonts during system fallback + if (!fe || fe->SkipDuringSystemFallback()) continue; PRInt32 rank = 0; diff --git a/gfx/thebes/src/gfxGDIFont.cpp b/gfx/thebes/src/gfxGDIFont.cpp index 9d9ac4e1c856..d8f4e74a7b5d 100644 --- a/gfx/thebes/src/gfxGDIFont.cpp +++ b/gfx/thebes/src/gfxGDIFont.cpp @@ -45,6 +45,7 @@ #include "gfxHarfBuzzShaper.h" #include "gfxWindowsPlatform.h" #include "gfxContext.h" +#include "gfxUnicodeProperties.h" #include "cairo-win32.h" @@ -100,11 +101,7 @@ gfxGDIFont::~gfxGDIFont() void gfxGDIFont::CreatePlatformShaper() { - if (static_cast(mFontEntry.get())->mForceGDI) { - mPlatformShaper = new gfxGDIShaper(this); - } else { - mPlatformShaper = new gfxUniscribeShaper(this); - } + mPlatformShaper = new gfxGDIShaper(this); } gfxFont* @@ -114,6 +111,31 @@ gfxGDIFont::CopyWithAntialiasOption(AntialiasOption anAAOption) &mStyle, mNeedsBold, anAAOption); } +static PRBool +UseUniscribe(gfxTextRun *aTextRun, + const PRUnichar *aString, + PRUint32 aRunStart, + PRUint32 aRunLength) +{ + PRUint32 flags = aTextRun->GetFlags(); + PRBool useGDI; + + PRBool isXP = (gfxWindowsPlatform::WindowsOSVersion() + < gfxWindowsPlatform::kWindowsVista); + + // bug 561304 - Uniscribe bug produces bad positioning at certain + // font sizes on XP, so default to GDI on XP using logic of 3.6 + + useGDI = isXP && + (flags & + (gfxTextRunFactory::TEXT_OPTIMIZE_SPEED | + gfxTextRunFactory::TEXT_IS_RTL) + ) == gfxTextRunFactory::TEXT_OPTIMIZE_SPEED; + + return !useGDI || + ScriptIsComplex(aString + aRunStart, aRunLength, SIC_COMPLEX) == S_OK; +} + PRBool gfxGDIFont::InitTextRun(gfxContext *aContext, gfxTextRun *aTextRun, @@ -129,22 +151,84 @@ gfxGDIFont::InitTextRun(gfxContext *aContext, NS_WARNING("invalid font! expect incorrect text rendering"); return PR_FALSE; } - PRBool ok = gfxFont::InitTextRun(aContext, aTextRun, aString, - aRunStart, aRunLength, aRunScript); - if (!ok && mPlatformShaper) { - // shaping failed; if we were using uniscribe, fall back to GDI - GDIFontEntry *fe = static_cast(GetFontEntry()); - if (!fe->mForceGDI) { - NS_WARNING("uniscribe failed, switching to GDI shaper"); - fe->mForceGDI = PR_TRUE; - mPlatformShaper = new gfxGDIShaper(this); - ok = mPlatformShaper->InitTextRun(aContext, aTextRun, aString, - aRunStart, aRunLength, + + PRBool ok = PR_FALSE; + + if (mHarfBuzzShaper) { + if (gfxPlatform::GetPlatform()->UseHarfBuzzLevel() >= + gfxUnicodeProperties::ScriptShapingLevel(aRunScript)) { + ok = mHarfBuzzShaper->InitTextRun(aContext, aTextRun, aString, + aRunStart, aRunLength, aRunScript); - NS_WARN_IF_FALSE(ok, "GDI shaper failed!"); } } + if (!ok) { + GDIFontEntry *fe = static_cast(GetFontEntry()); + + if (UseUniscribe(aTextRun, aString, aRunStart, aRunLength) + && !fe->mForceGDI) + { + // first try Uniscribe + if (!mUniscribeShaper) { + mUniscribeShaper = new gfxUniscribeShaper(this); + } + + ok = mUniscribeShaper->InitTextRun(aContext, aTextRun, aString, + aRunStart, aRunLength, + aRunScript); + if (ok) { + return PR_TRUE; + } + + // fallback to GDI shaping + if (!mPlatformShaper) { + CreatePlatformShaper(); + } + + ok = mPlatformShaper->InitTextRun(aContext, aTextRun, aString, + aRunStart, aRunLength, + aRunScript); + + } else { + // first use GDI + if (!mPlatformShaper) { + CreatePlatformShaper(); + } + + ok = mPlatformShaper->InitTextRun(aContext, aTextRun, aString, + aRunStart, aRunLength, + aRunScript); + + if (ok) { + return PR_TRUE; + } + + // first try Uniscribe + if (!mUniscribeShaper) { + mUniscribeShaper = new gfxUniscribeShaper(this); + } + + // use Uniscribe shaping + ok = mUniscribeShaper->InitTextRun(aContext, aTextRun, aString, + aRunStart, aRunLength, + aRunScript); + } + +#if DEBUG + if (!ok) { + NS_ConvertUTF16toUTF8 name(GetName()); + char msg[256]; + + sprintf(msg, + "text shaping with both uniscribe and GDI failed for" + " font: %s", + name.get()); + NS_WARNING(msg); + } +#endif + } + return ok; } diff --git a/gfx/thebes/src/gfxGDIFont.h b/gfx/thebes/src/gfxGDIFont.h index 2e71737632b3..4c2cc5563ef1 100644 --- a/gfx/thebes/src/gfxGDIFont.h +++ b/gfx/thebes/src/gfxGDIFont.h @@ -59,7 +59,7 @@ public: virtual ~gfxGDIFont(); - HFONT GetHFONT() const { return mFont; } + HFONT GetHFONT() { if (!mMetrics) Initialize(); return mFont; } gfxFloat GetAdjustedSize() const { return mAdjustedSize; } @@ -96,6 +96,10 @@ protected: void FillLogFont(LOGFONTW& aLogFont, gfxFloat aSize); + // mPlatformShaper is used for the GDI shaper, mUniscribeShaper + // for the Uniscribe version if needed + nsAutoPtr mUniscribeShaper; + HFONT mFont; cairo_font_face_t *mFontFace; cairo_scaled_font_t *mScaledFont; diff --git a/gfx/thebes/src/gfxGDIFontList.cpp b/gfx/thebes/src/gfxGDIFontList.cpp index 5c90f0250ef1..f39e5398d2c8 100644 --- a/gfx/thebes/src/gfxGDIFontList.cpp +++ b/gfx/thebes/src/gfxGDIFontList.cpp @@ -205,6 +205,14 @@ GDIFontEntry::GDIFontEntry(const nsAString& aFaceName, gfxWindowsFontType aFontT nsresult GDIFontEntry::ReadCMAP() { + // skip non-SFNT fonts completely + if (mFontType != GFX_FONT_TYPE_PS_OPENTYPE && + mFontType != GFX_FONT_TYPE_TT_OPENTYPE && + mFontType != GFX_FONT_TYPE_TRUETYPE) + { + return NS_ERROR_FAILURE; + } + // attempt this once, if errors occur leave a blank cmap if (mCmapInitialized) return NS_OK; @@ -290,6 +298,8 @@ GDIFontEntry::FillLogFont(LOGFONTW *aLogFont, PRBool aItalic, aLogFont->lfQuality = (aUseCleartype ? CLEARTYPE_QUALITY : DEFAULT_QUALITY); } +#define MISSING_GLYPH 0x1F + PRBool GDIFontEntry::TestCharacterMap(PRUint32 aCh) { @@ -327,13 +337,24 @@ GDIFontEntry::TestCharacterMap(PRUint32 aCh) WORD glyph[1]; PRBool hasGlyph = PR_FALSE; + + // Bug 573038 - in some cases GetGlyphIndicesW returns 0xFFFF for a + // missing glyph or 0x1F in other cases to indicate the "invalid" + // glyph. Map both cases to "not found" if (IsType1() || mForceGDI) { - // Type1 fonts and uniscribe APIs don't get along. ScriptGetCMap will return E_HANDLE - DWORD ret = GetGlyphIndicesW(dc, str, 1, glyph, GGI_MARK_NONEXISTING_GLYPHS); - if (ret != GDI_ERROR && glyph[0] != 0xFFFF) + // Type1 fonts and uniscribe APIs don't get along. + // ScriptGetCMap will return E_HANDLE + DWORD ret = GetGlyphIndicesW(dc, str, 1, + glyph, GGI_MARK_NONEXISTING_GLYPHS); + if (ret != GDI_ERROR + && glyph[0] != 0xFFFF + && glyph[0] != MISSING_GLYPH) + { hasGlyph = PR_TRUE; + } } else { - // ScriptGetCMap works better than GetGlyphIndicesW for things like bitmap/vector fonts + // ScriptGetCMap works better than GetGlyphIndicesW + // for things like bitmap/vector fonts SCRIPT_CACHE sc = NULL; HRESULT rv = ScriptGetCMap(dc, &sc, str, 1, 0, glyph); if (rv == S_OK) diff --git a/gfx/thebes/src/gfxGDIFontList.h b/gfx/thebes/src/gfxGDIFontList.h index bfa502b51452..761a7786445a 100644 --- a/gfx/thebes/src/gfxGDIFontList.h +++ b/gfx/thebes/src/gfxGDIFontList.h @@ -271,6 +271,10 @@ public: return mUnicodeRanges.test(range); } + virtual PRBool SkipDuringSystemFallback() { + return !HasCmapTable(); // explicitly skip non-SFNT fonts + } + virtual PRBool TestCharacterMap(PRUint32 aCh); // create a font entry for a font with a given name diff --git a/gfx/thebes/src/gfxGDIShaper.cpp b/gfx/thebes/src/gfxGDIShaper.cpp index bfa7cfefc0d1..70ef821e30df 100644 --- a/gfx/thebes/src/gfxGDIShaper.cpp +++ b/gfx/thebes/src/gfxGDIShaper.cpp @@ -72,6 +72,11 @@ gfxGDIShaper::InitTextRun(gfxContext *aContext, if (ret == GDI_ERROR) { return PR_FALSE; } + + for (int k = 0; k < aRunLength; k++) { + if (glyphs[k] == 0xFFFF) + return PR_FALSE; + } SIZE size; nsAutoTArray partialWidthArray; diff --git a/layout/reftests/line-breaking/non-breakable-2-ref.html b/layout/reftests/line-breaking/non-breakable-2-ref.html new file mode 100644 index 000000000000..3e8302de1188 --- /dev/null +++ b/layout/reftests/line-breaking/non-breakable-2-ref.html @@ -0,0 +1,36 @@ + + + + + + + +

abcdef abcdef

+

abcdef) (abcdef

+

abcdef
 abcdef

+

abcdef 
abcdef

+

abcdef   abcdef

+ + +

abcdef abcdef

+

abcdef) (abcdef

+

abcdef
 abcdef

+

abcdef 
abcdef

+

abcdef   abcdef

+ + +

abcdef‑abcdef

+

abcdef)‑(abcdef

+

abcdef
‑abcdef

+

abcdef‑
abcdef

+

abcdef‑‑‑abcdef

+ + +

abcdef abcdef

+

abcdef) (abcdef

+

abcdef
 abcdef

+

abcdef 
abcdef

+

abcdef   abcdef

+ + + diff --git a/layout/reftests/line-breaking/non-breakable-2.html b/layout/reftests/line-breaking/non-breakable-2.html new file mode 100644 index 000000000000..8bc301bbcb8f --- /dev/null +++ b/layout/reftests/line-breaking/non-breakable-2.html @@ -0,0 +1,36 @@ + + + + + + + +

abcdef abcdef

+

abcdef) (abcdef

+

abcdef  abcdef

+

abcdef  abcdef

+

abcdef   abcdef

+ + +

abcdef abcdef

+

abcdef) (abcdef

+

abcdef  abcdef

+

abcdef  abcdef

+

abcdef   abcdef

+ + +

abcdef‑abcdef

+

abcdef)‑(abcdef

+

abcdef ‑abcdef

+

abcdef‑ abcdef

+

abcdef‑‑‑abcdef

+ + +

abcdef abcdef

+

abcdef) (abcdef

+

abcdef  abcdef

+

abcdef  abcdef

+

abcdef   abcdef

+ + + diff --git a/layout/reftests/line-breaking/reftest.list b/layout/reftests/line-breaking/reftest.list index 53728523c8e0..8fb894d28d42 100644 --- a/layout/reftests/line-breaking/reftest.list +++ b/layout/reftests/line-breaking/reftest.list @@ -12,6 +12,7 @@ random-if(MOZ_WIDGET_TOOLKIT=="cocoa") == ja-3.html ja-3-ref.html == leaders-1.html leaders-1-ref.html == markup-src-1.html markup-src-1-ref.html == non-breakable-1.html non-breakable-1-ref.html +== non-breakable-2.html non-breakable-2-ref.html == numerics-1.html numerics-1-ref.html == parentheses-1.html parentheses-1-ref.html == quotationmarks-1.html quotationmarks-1-ref.html From dafc8b9271d2f85cbec890e897ec718e2582ab48 Mon Sep 17 00:00:00 2001 From: "Alexander L. Slovesnik" Date: Fri, 25 Jun 2010 15:55:12 +0200 Subject: [PATCH 1836/1860] Bug 571129 - contextMenuSearchText should have numbered arguments. r=pike --- browser/locales/en-US/chrome/browser/browser.properties | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index a80688a1b59a..8da51baa0b81 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -13,7 +13,9 @@ droponhomemsg=Do you want this document to be your new home page? # context menu strings -contextMenuSearchText=Search %S for "%S" +# LOCALIZATION NOTE (contextMenuSearchText): %1$S is the search engine, +# %2$S is the selection string. +contextMenuSearchText=Search %1$S for "%2$S" contextMenuSearchText.accesskey=S blockImages=Block Images from %S From 8dab235ba761fd0ac030e4bec2f727cf37ff97e1 Mon Sep 17 00:00:00 2001 From: Date: Fri, 25 Jun 2010 15:55:54 +0200 Subject: [PATCH 1837/1860] Bug 454501 - Fix access keys in the PKCS#11 Device. r=kaie --- security/manager/pki/resources/content/load_device.xul | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/security/manager/pki/resources/content/load_device.xul b/security/manager/pki/resources/content/load_device.xul index 408e3edea438..0f38a9b74807 100644 --- a/security/manager/pki/resources/content/load_device.xul +++ b/security/manager/pki/resources/content/load_device.xul @@ -57,11 +57,13 @@ &loaddevice.info; - -