commit cfee7ea9d20d14c5ae5dc6008cee27189576a5a2
parent faf3b94b64b805d9b41721129b5ad49b6d816197
Author: Dan Stillman <dstillman@zotero.org>
Date: Mon, 14 Dec 2015 19:28:56 -0500
Merge pull request #879 from adomasven/feature/transparent-api-keygen
Restores the functionality of 4.0 for sync settings
Diffstat:
13 files changed, 1075 insertions(+), 381 deletions(-)
diff --git a/chrome/content/zotero/preferences/preferences_sync.js b/chrome/content/zotero/preferences/preferences_sync.js
@@ -24,19 +24,232 @@
*/
"use strict";
+Components.utils.import("resource://gre/modules/Services.jsm");
Zotero_Preferences.Sync = {
- init: function () {
+ init: Zotero.Promise.coroutine(function* () {
this.updateStorageSettings(null, null, true);
-
- document.getElementById('sync-api-key').value = Zotero.Sync.Data.Local.getAPIKey();
-
+
+ var username = Zotero.Users.getCurrentUsername() || "";
+ var apiKey = Zotero.Sync.Data.Local.getAPIKey();
+ this.displayFields(apiKey ? username : "");
+ if (apiKey) {
+ try {
+ var keyInfo = yield Zotero.Sync.Runner.checkAccess(
+ Zotero.Sync.Runner.getAPIClient({apiKey}),
+ {timeout: 5000}
+ );
+ this.displayFields(keyInfo.username);
+ }
+ catch (e) {
+ // API key wrong/invalid
+ if (!(e instanceof Zotero.HTTP.UnexpectedStatusException) &&
+ !(e instanceof Zotero.HTTP.TimeoutException)) {
+ Zotero.alert(
+ window,
+ Zotero.getString('general.error'),
+ Zotero.getString('sync.error.apiKeyInvalid', Zotero.clientName)
+ );
+ this.unlinkAccount(false);
+ }
+ else {
+ throw e;
+ }
+ }
+ }
+
+
// TEMP: Disabled
//var pass = Zotero.Sync.Storage.WebDAV.password;
//if (pass) {
// document.getElementById('storage-password').value = pass;
//}
+ }),
+
+ displayFields: function (username) {
+ document.getElementById('sync-unauthorized').hidden = !!username;
+ document.getElementById('sync-authorized').hidden = !username;
+ document.getElementById('sync-reset-tab').disabled = !username;
+ document.getElementById('sync-username').value = username;
+ document.getElementById('sync-password').value = '';
+ document.getElementById('sync-username-textbox').value = Zotero.Prefs.get('sync.server.username');
+
+ var img = document.getElementById('sync-status-indicator');
+ img.removeAttribute('verified');
+ img.removeAttribute('animated');
+ },
+
+
+ credentialsKeyPress: function (event) {
+ var username = document.getElementById('sync-username-textbox');
+ username.value = username.value.trim();
+ var password = document.getElementById('sync-password');
+
+ var syncAuthButton = document.getElementById('sync-auth-button');
+
+ syncAuthButton.setAttribute('disabled', 'true');
+
+ // When using backspace, the value is not updated until after the keypress event
+ setTimeout(function() {
+ if (username.value.length && password.value.length) {
+ syncAuthButton.setAttribute('disabled', 'false');
+ }
+ });
+
+ if (event.keyCode == 13) {
+ Zotero_Preferences.Sync.linkAccount(event);
+ }
+ },
+
+
+ linkAccount: Zotero.Promise.coroutine(function* (event) {
+ var username = document.getElementById('sync-username-textbox').value;
+ var password = document.getElementById('sync-password').value;
+
+ if (!username.length || !password.length) {
+ this.updateSyncIndicator();
+ return;
+ }
+
+ // Try to acquire API key with current credentials
+ this.updateSyncIndicator('animated');
+ var json = yield Zotero.Sync.Runner.createAPIKeyFromCredentials(username, password);
+ this.updateSyncIndicator();
+
+ // Invalid credentials
+ if (!json) {
+ Zotero.alert(window,
+ Zotero.getString('general.error'),
+ Zotero.getString('sync.error.invalidLogin')
+ );
+ return;
+ }
+
+ if (!(yield this.checkUser(json.userID, json.username))) {
+ // createAPIKeyFromCredentials will have created an API key,
+ // but user decided not to use it, so we remove it here.
+ Zotero.Sync.Runner.deleteAPIKey();
+ return;
+ }
+
+ this.displayFields(json.username);
+ }),
+
+ /**
+ * Updates the auth indicator icon, depending on status
+ * @param {string} status
+ */
+ updateSyncIndicator: function (status) {
+ var img = document.getElementById('sync-status-indicator');
+
+ img.removeAttribute('animated');
+ if (status == 'animated') {
+ img.setAttribute('animated', true);
+ }
},
+
+ unlinkAccount: Zotero.Promise.coroutine(function* (showAlert=true) {
+ if (showAlert) {
+ if (!Services.prompt.confirm(
+ null,
+ Zotero.getString('general.warning'),
+ Zotero.getString('sync.unlinkWarning', Zotero.clientName)
+ )) {
+ return;
+ }
+ }
+
+ this.displayFields();
+ yield Zotero.Sync.Runner.deleteAPIKey();
+ }),
+
+
+ /**
+ * Make sure we're syncing with the same account we used last time, and prompt if not.
+ * If user accepts, change the current user, delete existing groups, and update relation
+ * URIs to point to the new user's library.
+ *
+ * @param {Integer} userID New userID
+ * @param {Integer} libraryID New libraryID
+ * @return {Boolean} - True to continue, false to cancel
+ */
+ checkUser: Zotero.Promise.coroutine(function* (userID, username) {
+ var lastUserID = Zotero.Users.getCurrentUserID();
+ var lastUsername = Zotero.Users.getCurrentUsername();
+
+ if (lastUserID && lastUserID != userID) {
+ var groups = Zotero.Groups.getAll();
+
+ var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
+ + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
+ + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING)
+ + ps.BUTTON_POS_1_DEFAULT
+ + ps.BUTTON_DELAY_ENABLE;
+
+ var msg = Zotero.getString('sync.lastSyncWithDifferentAccount', [lastUsername, username]);
+ var syncButtonText = Zotero.getString('sync.sync');
+
+ msg += " " + Zotero.getString('sync.localDataWillBeCombined', username);
+ // If there are local groups belonging to the previous user,
+ // we need to remove them
+ if (groups.length) {
+ msg += " " + Zotero.getString('sync.localGroupsWillBeRemoved1');
+ var syncButtonText = Zotero.getString('sync.removeGroupsAndSync');
+ }
+ msg += "\n\n" + Zotero.getString('sync.avoidCombiningData', lastUsername);
+
+ var index = ps.confirmEx(
+ null,
+ Zotero.getString('general.warning'),
+ msg,
+ buttonFlags,
+ syncButtonText,
+ null,
+ Zotero.getString('sync.openSyncPreferences'),
+ null, {}
+ );
+
+ if (index > 0) {
+ if (index == 2) {
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var lastWin = wm.getMostRecentWindow("navigator:browser");
+ lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync');
+ }
+ return false;
+ }
+ }
+
+ yield Zotero.DB.executeTransaction(function* () {
+ if (lastUserID != userID) {
+ if (lastUserID) {
+ // Delete all local groups if changing users
+ for (let group of groups) {
+ yield group.erase();
+ }
+
+ // Update relations pointing to the old library to point to this one
+ yield Zotero.Relations.updateUser(userID);
+ }
+ // Replace local user key with libraryID, in case duplicates were
+ // merged before the first sync
+ else {
+ yield Zotero.Relations.updateUser(userID);
+ }
+
+ yield Zotero.Users.setCurrentUserID(userID);
+ }
+
+ if (lastUsername != username) {
+ yield Zotero.Users.setCurrentUsername(username);
+ }
+ })
+
+ return true;
+ }),
+
updateStorageSettings: function (enabled, protocol, skipWarnings) {
if (enabled === null) {
diff --git a/chrome/content/zotero/preferences/preferences_sync.xul b/chrome/content/zotero/preferences/preferences_sync.xul
@@ -47,216 +47,240 @@
<tabbox>
<tabs>
<tab label="&zotero.preferences.settings;"/>
- <tab label="&zotero.preferences.sync.reset;"/>
+ <tab id="sync-reset-tab" label="&zotero.preferences.sync.reset;" disabled="true"/>
</tabs>
<tabpanels>
<tabpanel orient="vertical">
- <groupbox>
- <caption label="&zotero.preferences.sync.syncServer;"/>
-
- <hbox>
- <grid>
+ <vbox id="sync-unauthorized">
+ <groupbox>
+ <caption label="&zotero.preferences.sync.syncServer;"/>
+
+ <hbox>
+ <grid>
+ <columns>
+ <column/>
+ <column/>
+ </columns>
+
+ <rows>
+ <row>
+ <label value="&zotero.preferences.sync.username;"/>
+ <textbox id="sync-username-textbox"
+ preference="pref-sync-username"
+ onkeypress="Zotero_Preferences.Sync.credentialsKeyPress(event);"/>
+ </row>
+ <row>
+ <label value="&zotero.preferences.sync.password;"/>
+ <textbox id="sync-password" type="password"
+ onkeypress="Zotero_Preferences.Sync.credentialsKeyPress(event);"/>
+ </row>
+ <vbox align="center">
+ <hbox align="baseline">
+ <button id="sync-auth-button"
+ label="&zotero.preferences.sync.setUpSync;"
+ oncommand="Zotero_Preferences.Sync.linkAccount(event)"
+ disabled="true"/>
+ <label id="sync-status-indicator"/>
+ </hbox>
+ </vbox>
+ </rows>
+ </grid>
+ <vbox style="width:2em"/>
+ <vbox>
+ <label class="zotero-text-link" value="&zotero.preferences.sync.createAccount;" href="http://zotero.org/user/register"/>
+ <separator class="thin"/>
+ <label class="zotero-text-link" value="&zotero.preferences.sync.lostPassword;" href="http://zotero.org/user/lostpassword"/>
+ <separator class="thin"/>
+ <label class="zotero-text-link" value="&zotero.preferences.sync.about;" href="http://www.zotero.org/support/sync"/>
+ </vbox>
+ </hbox>
+ </groupbox>
+ </vbox>
+
+ <vbox id="sync-authorized" hidden="true">
+ <groupbox>
+ <caption label="&zotero.preferences.sync.syncServer;"/>
+
+ <hbox>
+ <grid>
+ <columns>
+ <column/>
+ <column/>
+ </columns>
+
+ <rows>
+ <row>
+ <label value="&zotero.preferences.sync.username;"/>
+ <label id="sync-username" value="Username"/>
+ </row>
+ <row>
+ <box/>
+ <button label="&zotero.preferences.sync.unlinkAccount;"
+ oncommand="Zotero_Preferences.Sync.unlinkAccount()"/>
+ </row>
+ <!--
+ <row>
+ <box/>
+ <button label="Access Control" oncommand="Zotero.alert('Not implemented');"/>
+ </row>
+ -->
+ <row>
+ <box/>
+ <checkbox label="&zotero.preferences.sync.syncAutomatically;"
+ disabled="true"/>
+ </row>
+ <row>
+ <box/>
+ <checkbox label="&zotero.preferences.sync.syncFullTextContent;"
+ tooltiptext="&zotero.preferences.sync.syncFullTextContent.desc;"
+ disabled="true"/>
+ </row>
+ </rows>
+ </grid>
+ <vbox>
+ <label class="zotero-text-link" value="&zotero.preferences.sync.about;" href="http://www.zotero.org/support/sync"/>
+ </vbox>
+ </hbox>
+
+ </groupbox>
+
+ <groupbox id="storage-settings">
+ <caption label="&zotero.preferences.sync.fileSyncing;"/>
+
+ <!-- My Library -->
+ <hbox>
+ <checkbox label="&zotero.preferences.sync.fileSyncing.myLibrary;"
+ preference="pref-storage-enabled"
+ oncommand="Zotero_Preferences.Sync.updateStorageSettings(this.checked, null)"/>
+ <menulist id="storage-protocol" class="storage-personal"
+ style="margin-left: .5em"
+ preference="pref-storage-protocol"
+ oncommand="Zotero_Preferences.Sync.updateStorageSettings(null, this.value)">
+ <menupopup>
+ <menuitem label="Zotero" value="zotero"/>
+ <menuitem label="WebDAV" value="webdav" disabled="true"/><!-- TEMP -->
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <stack id="storage-webdav-settings" style="margin-top: .5em; margin-bottom: .8em; border: 1px gray solid; border-radius: 3px">
+ <!-- Background shading -->
+ <box style="background: black; opacity:.03"/>
+
+ <grid style="padding: .7em .4em .7em 0">
<columns>
<column/>
- <column/>
+ <column flex="1"/>
</columns>
-
+
<rows>
- <!--
- <row>
- <label value="&zotero.preferences.sync.username;"/>
- <textbox preference="pref-sync-username"
- onchange="this.value = this.value.trim(); Zotero.Prefs.set('sync.server.username', this.value); var pass = document.getElementById('sync-password'); if (pass.value) { Zotero.Sync.Server.password = pass.value; }"/>
- </row>
<row>
- <label value="&zotero.preferences.sync.password;"/>
- <textbox id="sync-password" type="password"
- onchange="Zotero.Sync.Server.password = this.value"/>
- </row>
- -->
- <row>
- <label value="API Key (temp)"/>
- <textbox id="sync-api-key" maxlength="24" size="25"
- onchange="Zotero.Sync.Data.Local.setAPIKey(this.value)"/>
+ <label value="&zotero.preferences.sync.fileSyncing.url;"/>
+ <hbox>
+ <menulist id="storage-url-prefix"
+ preference="pref-storage-scheme"
+ onsynctopreference="Zotero_Preferences.Sync.unverifyStorageServer()"
+ style="padding: 0; width: 7em">
+ <menupopup>
+ <menuitem label="https" value="https" style="padding: 0"/>
+ <menuitem label="http" value="http" style="padding: 0"/>
+ </menupopup>
+ </menulist>
+ <label value="://"/>
+ <textbox id="storage-url" flex="1"
+ preference="pref-storage-url"
+ onkeypress="if (Zotero.isMac && event.keyCode == 13) {
+ this.blur();
+ setTimeout(Zotero_Preferences.Sync.verifyStorageServer, 1);
+ }"
+ onchange="Zotero_Preferences.Sync.unverifyStorageServer();
+ this.value = this.value.replace(/(^https?:\/\/|\/zotero\/?$|\/$)/g, '');
+ Zotero.Prefs.set('sync.storage.url', this.value)"/>
+ <label value="/zotero/"/>
+ </hbox>
</row>
<row>
- <box/>
- <!--<checkbox label="&zotero.preferences.sync.syncAutomatically;" preference="pref-sync-autosync"/>-->
- <checkbox label="&zotero.preferences.sync.syncAutomatically;"
- disabled="true"/>
+ <label value="&zotero.preferences.sync.username;"/>
+ <hbox>
+ <textbox id="storage-username"
+ preference="pref-storage-username"
+ onkeypress="if (Zotero.isMac && event.keyCode == 13) {
+ this.blur();
+ setTimeout(Zotero_Preferences.Sync.verifyStorageServer, 1); }"
+ onchange="Zotero_Preferences.Sync.unverifyStorageServer();
+ Zotero.Prefs.set('sync.storage.username', this.value);
+ var pass = document.getElementById('storage-password');
+ if (pass.value) {
+ Zotero.Sync.Storage.WebDAV.password = pass.value;
+ }"/>
+ </hbox>
</row>
<row>
- <box/>
- <vbox>
- <!--<checkbox label="&zotero.preferences.sync.syncFullTextContent;"
- preference="pref-sync-fulltext-enabled"
- tooltiptext="&zotero.preferences.sync.syncFullTextContent.desc;"/>-->
- <checkbox label="&zotero.preferences.sync.syncFullTextContent;"
- tooltiptext="&zotero.preferences.sync.syncFullTextContent.desc;"
- disabled="true"/>
- </vbox>
+ <label value="&zotero.preferences.sync.password;"/>
+ <hbox>
+ <textbox id="storage-password" flex="0" type="password"
+ onkeypress="if (Zotero.isMac && event.keyCode == 13) {
+ this.blur();
+ setTimeout(Zotero_Preferences.Sync.verifyStorageServer, 1);
+ }"
+ onchange="Zotero_Preferences.Sync.unverifyStorageServer();
+ Zotero.Sync.Storage.WebDAV.password = this.value;"/>
+ </hbox>
</row>
- <!--
<row>
<box/>
<hbox>
- <button label="Verify login"
- oncommand="alert('Unimplemented')"/>
+ <button id="storage-verify" label="Verify Server"
+ oncommand="Zotero_Preferences.Sync.verifyStorageServer()"/>
+ <button id="storage-abort" label="Stop" hidden="true"/>
+ <progressmeter id="storage-progress" hidden="true"
+ mode="undetermined"/>
</hbox>
</row>
- -->
</rows>
</grid>
-
- <hbox style="width:2em"/>
-
+
+ </stack>
+
+ <hbox class="storage-settings-download-options" align="center">
+ <label value="&zotero.preferences.sync.fileSyncing.download;"/>
+ <menulist class="storage-personal" preference="pref-storage-downloadMode-personal" style="margin-left: 0">
+ <menupopup>
+ <menuitem label="&zotero.preferences.sync.fileSyncing.download.onDemand;" value="on-demand"/>
+ <menuitem label="&zotero.preferences.sync.fileSyncing.download.atSyncTime;" value="on-sync"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <separator id="storage-separator" class="thin"/>
+
+ <!-- Group Libraries -->
+ <checkbox label="&zotero.preferences.sync.fileSyncing.groups;"
+ preference="pref-group-storage-enabled"
+ oncommand="Zotero_Preferences.Sync.updateStorageSettingsGroups(this.checked)"/>
+
+ <hbox class="storage-settings-download-options" align="center">
+ <label value="&zotero.preferences.sync.fileSyncing.download;"/>
+ <menulist class="storage-groups" preference="pref-storage-downloadMode-groups" style="margin-left: 0">
+ <menupopup>
+ <menuitem label="&zotero.preferences.sync.fileSyncing.download.onDemand;" value="on-demand"/>
+ <menuitem label="&zotero.preferences.sync.fileSyncing.download.atSyncTime;" value="on-sync"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <separator class="thin"/>
+
<vbox>
- <label class="zotero-text-link" value="&zotero.preferences.sync.about;" href="http://www.zotero.org/support/sync"/>
- <separator class="thin"/>
- <label class="zotero-text-link" value="&zotero.preferences.sync.createAccount;" href="http://zotero.org/user/register"/>
- <separator class="thin"/>
- <label class="zotero-text-link" value="&zotero.preferences.sync.lostPassword;" href="http://zotero.org/user/lostpassword"/>
+ <hbox id="storage-terms" style="margin-top: .4em; display: block" align="center">
+ <label>&zotero.preferences.sync.fileSyncing.tos1;</label>
+ <label class="zotero-text-link" href="https://www.zotero.org/support/terms/terms_of_service" value="&zotero.preferences.sync.fileSyncing.tos2;"/>
+ <label>&zotero.preferences.period;</label>
+ </hbox>
</vbox>
- </hbox>
- </groupbox>
-
-
- <groupbox id="storage-settings">
- <caption label="&zotero.preferences.sync.fileSyncing;"/>
-
- <!-- My Library -->
- <hbox>
- <checkbox label="&zotero.preferences.sync.fileSyncing.myLibrary;"
- preference="pref-storage-enabled"
- oncommand="Zotero_Preferences.Sync.updateStorageSettings(this.checked, null)"/>
- <menulist id="storage-protocol" class="storage-personal"
- style="margin-left: .5em"
- preference="pref-storage-protocol"
- oncommand="Zotero_Preferences.Sync.updateStorageSettings(null, this.value)">
- <menupopup>
- <menuitem label="Zotero" value="zotero"/>
- <menuitem label="WebDAV" value="webdav" disabled="true"/><!-- TEMP -->
- </menupopup>
- </menulist>
- </hbox>
-
- <stack id="storage-webdav-settings" style="margin-top: .5em; margin-bottom: .8em; border: 1px gray solid; border-radius: 3px">
- <!-- Background shading -->
- <box style="background: black; opacity:.03"/>
-
- <grid style="padding: .7em .4em .7em 0">
- <columns>
- <column/>
- <column flex="1"/>
- </columns>
-
- <rows>
- <row>
- <label value="&zotero.preferences.sync.fileSyncing.url;"/>
- <hbox>
- <menulist id="storage-url-prefix"
- preference="pref-storage-scheme"
- onsynctopreference="Zotero_Preferences.Sync.unverifyStorageServer()"
- style="padding: 0; width: 7em">
- <menupopup>
- <menuitem label="https" value="https" style="padding: 0"/>
- <menuitem label="http" value="http" style="padding: 0"/>
- </menupopup>
- </menulist>
- <label value="://"/>
- <textbox id="storage-url" flex="1"
- preference="pref-storage-url"
- onkeypress="if (Zotero.isMac && event.keyCode == 13) {
- this.blur();
- setTimeout(Zotero_Preferences.Sync.verifyStorageServer, 1);
- }"
- onchange="Zotero_Preferences.Sync.unverifyStorageServer();
- this.value = this.value.replace(/(^https?:\/\/|\/zotero\/?$|\/$)/g, '');
- Zotero.Prefs.set('sync.storage.url', this.value)"/>
- <label value="/zotero/"/>
- </hbox>
- </row>
- <row>
- <label value="&zotero.preferences.sync.username;"/>
- <hbox>
- <textbox id="storage-username"
- preference="pref-storage-username"
- onkeypress="if (Zotero.isMac && event.keyCode == 13) {
- this.blur();
- setTimeout(Zotero_Preferences.Sync.verifyStorageServer, 1); }"
- onchange="Zotero_Preferences.Sync.unverifyStorageServer();
- Zotero.Prefs.set('sync.storage.username', this.value);
- var pass = document.getElementById('storage-password');
- if (pass.value) {
- Zotero.Sync.Storage.WebDAV.password = pass.value;
- }"/>
- </hbox>
- </row>
- <row>
- <label value="&zotero.preferences.sync.password;"/>
- <hbox>
- <textbox id="storage-password" flex="0" type="password"
- onkeypress="if (Zotero.isMac && event.keyCode == 13) {
- this.blur();
- setTimeout(Zotero_Preferences.Sync.verifyStorageServer, 1);
- }"
- onchange="Zotero_Preferences.Sync.unverifyStorageServer();
- Zotero.Sync.Storage.WebDAV.password = this.value;"/>
- </hbox>
- </row>
- <row>
- <box/>
- <hbox>
- <button id="storage-verify" label="Verify Server"
- oncommand="Zotero_Preferences.Sync.verifyStorageServer()"/>
- <button id="storage-abort" label="Stop" hidden="true"/>
- <progressmeter id="storage-progress" hidden="true"
- mode="undetermined"/>
- </hbox>
- </row>
- </rows>
- </grid>
-
- </stack>
-
- <hbox class="storage-settings-download-options" align="center">
- <label value="&zotero.preferences.sync.fileSyncing.download;"/>
- <menulist class="storage-personal" preference="pref-storage-downloadMode-personal" style="margin-left: 0">
- <menupopup>
- <menuitem label="&zotero.preferences.sync.fileSyncing.download.onDemand;" value="on-demand"/>
- <menuitem label="&zotero.preferences.sync.fileSyncing.download.atSyncTime;" value="on-sync"/>
- </menupopup>
- </menulist>
- </hbox>
-
- <separator id="storage-separator" class="thin"/>
-
- <!-- Group Libraries -->
- <checkbox label="&zotero.preferences.sync.fileSyncing.groups;"
- preference="pref-group-storage-enabled"
- oncommand="Zotero_Preferences.Sync.updateStorageSettingsGroups(this.checked)"/>
-
- <hbox class="storage-settings-download-options" align="center">
- <label value="&zotero.preferences.sync.fileSyncing.download;"/>
- <menulist class="storage-groups" preference="pref-storage-downloadMode-groups" style="margin-left: 0">
- <menupopup>
- <menuitem label="&zotero.preferences.sync.fileSyncing.download.onDemand;" value="on-demand"/>
- <menuitem label="&zotero.preferences.sync.fileSyncing.download.atSyncTime;" value="on-sync"/>
- </menupopup>
- </menulist>
- </hbox>
-
- <separator class="thin"/>
-
- <vbox>
- <hbox id="storage-terms" style="margin-top: .4em; display: block" align="center">
- <label>&zotero.preferences.sync.fileSyncing.tos1;</label>
- <label class="zotero-text-link" href="https://www.zotero.org/support/terms/terms_of_service" value="&zotero.preferences.sync.fileSyncing.tos2;"/>
- <label>&zotero.preferences.period;</label>
- </hbox>
- </vbox>
- </groupbox>
+ </groupbox>
+ </vbox>
</tabpanel>
<tabpanel id="zotero-reset" orient="vertical">
diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js
@@ -55,7 +55,15 @@ Zotero.HTTP = new function() {
this.BrowserOfflineException.prototype.toString = function() {
return this.message;
};
-
+
+ this.TimeoutException = function(ms) {
+ this.message = "XMLHttpRequest has timed out after " + ms + "ms";
+ };
+ this.TimeoutException.prototype = Object.create(Error.prototype);
+ this.TimeoutException.prototype.toString = function() {
+ return this.message;
+ };
+
this.promise = function () {
Zotero.debug("Zotero.HTTP.promise() is deprecated -- use Zotero.HTTP.request()", 2);
return this.request.apply(this, arguments);
@@ -74,6 +82,7 @@ Zotero.HTTP = new function() {
* <li>dontCache - If set, specifies that the request should not be fulfilled from the cache</li>
* <li>foreground - Make a foreground request, showing certificate/authentication dialogs if necessary</li>
* <li>headers - HTTP headers to include in the request</li>
+ * <li>timeout - Request timeout specified in milliseconds
* <li>requestObserver - Callback to receive XMLHttpRequest after open()</li>
* <li>responseType - The type of the response. See XHR 2 documentation for legal values</li>
* <li>responseCharset - The charset the response should be interpreted as</li>
@@ -191,7 +200,16 @@ Zotero.HTTP = new function() {
for (var header in headers) {
xmlhttp.setRequestHeader(header, headers[header]);
}
-
+
+ // Set timeout
+ if (options.timeout) {
+ xmlhttp.timeout = options.timeout;
+ }
+
+ xmlhttp.ontimeout = function() {
+ deferred.reject(new Zotero.HTTP.TimeoutException(options.timeout));
+ };
+
xmlhttp.onloadend = function() {
var status = xmlhttp.status;
diff --git a/chrome/content/zotero/xpcom/sync/syncAPIClient.js b/chrome/content/zotero/xpcom/sync/syncAPIClient.js
@@ -30,7 +30,6 @@ if (!Zotero.Sync) {
Zotero.Sync.APIClient = function (options) {
if (!options.baseURL) throw new Error("baseURL not set");
if (!options.apiVersion) throw new Error("apiVersion not set");
- if (!options.apiKey) throw new Error("apiKey not set");
if (!options.caller) throw new Error("caller not set");
this.baseURL = options.baseURL;
@@ -45,9 +44,9 @@ Zotero.Sync.APIClient.prototype = {
MAX_OBJECTS_PER_REQUEST: 100,
- getKeyInfo: Zotero.Promise.coroutine(function* () {
+ getKeyInfo: Zotero.Promise.coroutine(function* (options={}) {
var uri = this.baseURL + "keys/" + this.apiKey;
- var xmlhttp = yield this.makeRequest("GET", uri, { successCodes: [200, 404] });
+ var xmlhttp = yield this.makeRequest("GET", uri, Object.assign(options, { successCodes: [200, 404] }));
if (xmlhttp.status == 404) {
return false;
}
@@ -430,6 +429,52 @@ Zotero.Sync.APIClient.prototype = {
}),
+ createAPIKeyFromCredentials: Zotero.Promise.coroutine(function* (username, password) {
+ var body = JSON.stringify({
+ username,
+ password,
+ name: "Automatic Zotero Client Key",
+ access: {
+ user: {
+ library: true,
+ notes: true,
+ write: true,
+ files: true
+ },
+ groups: {
+ all: {
+ library: true,
+ write: true
+ }
+ }
+ }
+ });
+ var headers = {
+ "Content-Type": "application/json"
+ };
+ var uri = this.baseURL + "keys";
+ var response = yield this.makeRequest("POST", uri, {
+ body, headers, successCodes: [201, 403], noAPIKey: true
+ });
+ if (response.status == 403) {
+ return false;
+ }
+
+ var json = this._parseJSON(response.responseText);
+ if (!json.key) {
+ throw new Error('json.key not present in POST /keys response')
+ }
+
+ return json;
+ }),
+
+
+ // Deletes current API key
+ deleteAPIKey: Zotero.Promise.coroutine(function* () {
+ yield this.makeRequest("DELETE", this.baseURL + "keys/" + this.apiKey);
+ }),
+
+
buildRequestURI: function (params) {
var uri = this.baseURL;
@@ -508,6 +553,9 @@ Zotero.Sync.APIClient.prototype = {
makeRequest: Zotero.Promise.coroutine(function* (method, uri, options = {}) {
+ if (!this.apiKey && !options.noAPIKey) {
+ throw new Error('API key not set');
+ }
options.headers = this.getHeaders(options.headers);
options.dontCache = true;
options.foreground = !options.background;
diff --git a/chrome/content/zotero/xpcom/sync/syncLocal.js b/chrome/content/zotero/xpcom/sync/syncLocal.js
@@ -55,9 +55,11 @@ Zotero.Sync.Data.Local = {
var oldLoginInfo = this._getAPIKeyLoginInfo();
// Clear old login
- if (oldLoginInfo && (!apiKey || apiKey === "")) {
- Zotero.debug("Clearing old API key");
- loginManager.removeLogin(oldLoginInfo);
+ if ((!apiKey || apiKey === "")) {
+ if (oldLoginInfo) {
+ Zotero.debug("Clearing old API key");
+ loginManager.removeLogin(oldLoginInfo);
+ }
return;
}
@@ -154,6 +156,31 @@ Zotero.Sync.Data.Local = {
},
+ removeLegacyLogins: function () {
+ var loginManagerHost = 'chrome://zotero';
+ var loginManagerRealm = 'Zotero Sync Server';
+
+ Zotero.debug('Removing legacy Zotero sync credentials (api key acquired)');
+
+ var loginManager = Components.classes["@mozilla.org/login-manager;1"]
+ .getService(Components.interfaces.nsILoginManager);
+ try {
+ var logins = loginManager.findLogins({}, loginManagerHost, null, loginManagerRealm);
+ }
+ catch (e) {
+ Zotero.logError(e);
+ return '';
+ }
+
+ // Remove all legacy users
+ for (let login of logins) {
+ loginManager.removeLogin(login);
+ }
+ // Remove the legacy pref
+ Zotero.Pref.clear('sync.server.username');
+ },
+
+
getLastSyncTime: function () {
if (_lastSyncTime === null) {
throw new Error("Last sync time not yet loaded");
diff --git a/chrome/content/zotero/xpcom/sync/syncRunner.js b/chrome/content/zotero/xpcom/sync/syncRunner.js
@@ -68,12 +68,9 @@ Zotero.Sync.Runner_Module = function (options = {}) {
var _currentLastSyncLabel;
var _errors = [];
-
+
this.getAPIClient = function (options = {}) {
- if (!options.apiKey) {
- throw new Error("apiKey not provided");
- }
-
+
return new Zotero.Sync.APIClient({
baseURL: this.baseURL,
apiVersion: this.apiVersion,
@@ -218,8 +215,8 @@ Zotero.Sync.Runner_Module = function (options = {}) {
/**
* Check key for current user info and return access info
*/
- this.checkAccess = Zotero.Promise.coroutine(function* (client, options) {
- var json = yield client.getKeyInfo();
+ this.checkAccess = Zotero.Promise.coroutine(function* (client, options={}) {
+ var json = yield client.getKeyInfo(options);
Zotero.debug(json);
if (!json) {
// TODO: Nicer error message
@@ -231,11 +228,6 @@ Zotero.Sync.Runner_Module = function (options = {}) {
if (!json.username) throw new Error("username not found in key response");
if (!json.access) throw new Error("'access' not found in key response");
- // Make sure user hasn't changed, and prompt to update database if so
- if (!(yield this.checkUser(json.userID, json.username))) {
- return false;
- }
-
return json;
});
@@ -426,108 +418,6 @@ Zotero.Sync.Runner_Module = function (options = {}) {
/**
- * Make sure we're syncing with the same account we used last time, and prompt if not.
- * If user accepts, change the current user, delete existing groups, and update relation
- * URIs to point to the new user's library.
- *
- * @param {Integer} userID New userID
- * @param {Integer} libraryID New libraryID
- * @return {Boolean} - True to continue, false to cancel
- */
- this.checkUser = Zotero.Promise.coroutine(function* (userID, username) {
- var lastUserID = Zotero.Users.getCurrentUserID();
- var lastUsername = Zotero.Users.getCurrentUsername();
-
- // TEMP: Remove? No way to determine this quickly currently.
- var noServerData = false;
-
- if (lastUserID && lastUserID != userID) {
- var groups = Zotero.Groups.getAll();
-
- var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
- .getService(Components.interfaces.nsIPromptService);
- var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
- + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
- + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING)
- + ps.BUTTON_POS_1_DEFAULT
- + ps.BUTTON_DELAY_ENABLE;
-
- var msg = Zotero.getString('sync.lastSyncWithDifferentAccount', [lastUsername, username]);
-
- if (!noServerData) {
- msg += " " + Zotero.getString('sync.localDataWillBeCombined', username);
- // If there are local groups belonging to the previous user,
- // we need to remove them
- if (groups.length) {
- msg += " " + Zotero.getString('sync.localGroupsWillBeRemoved1');
- }
- msg += "\n\n" + Zotero.getString('sync.avoidCombiningData', lastUsername);
- var syncButtonText = Zotero.getString('sync.sync');
- }
- else if (groups.length) {
- msg += " " + Zotero.getString('sync.localGroupsWillBeRemoved2', [username, lastUsername]);
- var syncButtonText = Zotero.getString('sync.removeGroupsAndSync');
- }
- // If there are no local groups and the server is empty,
- // don't bother prompting
- else {
- var noPrompt = true;
- }
-
- if (!noPrompt) {
- var index = ps.confirmEx(
- null,
- Zotero.getString('general.warning'),
- msg,
- buttonFlags,
- syncButtonText,
- null,
- Zotero.getString('sync.openSyncPreferences'),
- null, {}
- );
-
- if (index > 0) {
- if (index == 2) {
- var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator);
- var lastWin = wm.getMostRecentWindow("navigator:browser");
- lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync');
- }
- return false;
- }
- }
- }
-
- yield Zotero.DB.executeTransaction(function* () {
- if (lastUserID != userID) {
- if (lastUserID) {
- // Delete all local groups if changing users
- for (let group of groups) {
- yield group.erase();
- }
-
- // Update relations pointing to the old library to point to this one
- yield Zotero.Relations.updateUser(userID);
- }
- // Replace local user key with libraryID, in case duplicates were
- // merged before the first sync
- else {
- yield Zotero.Relations.updateUser(userID);
- }
-
- yield Zotero.Users.setCurrentUserID(userID);
- }
-
- if (lastUsername != username) {
- yield Zotero.Users.setCurrentUsername(username);
- }
- })
-
- return true;
- });
-
-
- /**
* Run sync engine for passed libraries
*
* @param {Integer[]} libraries
@@ -1191,7 +1081,33 @@ Zotero.Sync.Runner_Module = function (options = {}) {
_updateSyncStatusLabel();
}
}
-
+
+
+ this.createAPIKeyFromCredentials = Zotero.Promise.coroutine(function* (username, password) {
+ var client = this.getAPIClient();
+ var json = yield client.createAPIKeyFromCredentials(username, password);
+ if (!json) {
+ return false;
+ }
+
+ // Sanity check
+ if (!json.userID) throw new Error("userID not found in key response");
+ if (!json.username) throw new Error("username not found in key response");
+ if (!json.access) throw new Error("'access' not found in key response");
+
+ Zotero.Sync.Data.Local.setAPIKey(json.key);
+
+ return json;
+ })
+
+
+ this.deleteAPIKey = Zotero.Promise.coroutine(function* (){
+ var apiKey = Zotero.Sync.Data.Local.getAPIKey();
+ var client = this.getAPIClient({apiKey});
+ Zotero.Sync.Data.Local.setAPIKey();
+ yield client.deleteAPIKey();
+ })
+
function _updateSyncStatusLabel() {
if (_lastSyncStatus) {
@@ -1244,20 +1160,20 @@ Zotero.Sync.Runner_Module = function (options = {}) {
var _getAPIKeyFromLogin = Zotero.Promise.coroutine(function* () {
- var apiKey = "";
+ var apiKey;
let username = Zotero.Prefs.get('sync.server.username');
if (username) {
+ // Check for legacy password if no password set in current session
+ // and no API keys stored yet
let password = Zotero.Sync.Data.Local.getLegacyPassword(username);
if (!password) {
return "";
}
- throw new Error("Unimplemented");
- // Get API key from server
-
- // Store API key
-
- // Remove old logins and username pref
+
+ apiKey = yield Zotero.Sync.Runner.createAPIKeyFromCredentials(username, password);
+ Zotero.Sync.Data.Local.removeLegacyLogins();
+ return apiKey;
}
- return apiKey;
+ return "";
})
}
diff --git a/chrome/locale/en-US/zotero/preferences.dtd b/chrome/locale/en-US/zotero/preferences.dtd
@@ -49,7 +49,9 @@
<!ENTITY zotero.preferences.prefpane.sync "Sync">
<!ENTITY zotero.preferences.sync.username "Username:">
<!ENTITY zotero.preferences.sync.password "Password:">
-<!ENTITY zotero.preferences.sync.syncServer "Zotero Sync Server">
+<!ENTITY zotero.preferences.sync.syncServer "Zotero Data Sync">
+<!ENTITY zotero.preferences.sync.setUpSync "Set Up Syncing">
+<!ENTITY zotero.preferences.sync.unlinkAccount "Unlink Account...">
<!ENTITY zotero.preferences.sync.createAccount "Create Account">
<!ENTITY zotero.preferences.sync.lostPassword "Lost Password?">
<!ENTITY zotero.preferences.sync.syncAutomatically "Sync automatically">
diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties
@@ -821,12 +821,13 @@ sync.error.sslConnectionError = SSL connection error
sync.error.checkConnection = Error connecting to server. Check your Internet connection.
sync.error.emptyResponseServer = Empty response from server.
sync.error.invalidCharsFilename = The filename '%S' contains invalid characters.\n\nRename the file and try again. If you rename the file via the OS, you will need to relink it in Zotero.
+sync.error.apiKeyInvalid = %S could not authenticate your account. Please re-enter your account details.
sync.lastSyncWithDifferentAccount = This Zotero database was last synced with a different zotero.org account ('%1$S') from the current one ('%2$S').
sync.localDataWillBeCombined = If you continue, local Zotero data will be combined with data from the '%S' account stored on the server.
sync.localGroupsWillBeRemoved1 = Local groups, including any with changed items, will also be removed from this computer.
-sync.avoidCombiningData = To avoid combining or losing data, revert to the '%S' account or use the Reset options in the Sync pane of the Zotero preferences.
-sync.localGroupsWillBeRemoved2 = If you continue, local groups, including any with changed items, will be removed and replaced with groups linked to the '%1$S' account.\n\nTo avoid losing local changes to groups, be sure you have synced with the '%2$S' account before syncing with the '%1$S' account.
+sync.avoidCombiningData = To avoid combining data, revert to the '%S' account or use the Reset options in the Sync pane of the Zotero preferences.
+sync.unlinkWarning = Are you sure you want to unlink this account?\n\n%S will no longer sync your data, but your data will remain locally.
sync.conflict.autoChange.alert = One or more locally deleted Zotero %S have been modified remotely since the last sync.
sync.conflict.autoChange.log = A Zotero %S has changed both locally and remotely since the last sync:
diff --git a/chrome/skin/default/zotero/preferences.css b/chrome/skin/default/zotero/preferences.css
@@ -118,6 +118,30 @@ grid row hbox:first-child
margin-right: 10px;
}
+#zotero-prefpane-sync #sync-status-indicator
+{
+ width: 1.5em;
+ height: 1.7em;
+ margin-top: 0.4em;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+#zotero-prefpane-sync #sync-status-indicator[verified=true]
+{
+ background-image: url("chrome://zotero/skin/tick.png")
+}
+
+#zotero-prefpane-sync #sync-status-indicator[verified=false]
+{
+ background-image: url("chrome://zotero/skin/cross.png")
+}
+
+#zotero-prefpane-sync #sync-status-indicator[animated]
+{
+ background-image: url("chrome://zotero/skin/arrow_rotate_animated.png")
+}
+
.storage-settings-download-options
{
margin-left: 40px;
diff --git a/test/content/runtests.html b/test/content/runtests.html
@@ -10,6 +10,7 @@
<script src="resource://zotero-unit/chai-as-promised/lib/chai-as-promised.js"></script>
<script src="resource://zotero-unit/mocha/mocha.js"></script>
<script src="resource://zotero-unit/sinon.js"></script>
+ <script src="resource://zotero-unit/sinon-as-promised.js"></script>
<script src="support.js" type="application/javascript;version=1.8"></script>
<script src="runtests.js" type="application/javascript;version=1.8"></script>
</body>
diff --git a/test/resource/sinon-as-promised.js b/test/resource/sinon-as-promised.js
@@ -0,0 +1,250 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.sinonAsPromised = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+'use strict'
+
+var Promise = window.Zotero.Promise
+var sinon = (window.sinon)
+
+function methods (Promise) {
+ return ['catch', 'finally'].concat(Object.keys(Promise.prototype)).filter(a => a != 'then');
+}
+function createThenable (Promise, resolver) {
+ return methods(Promise).reduce(createMethod, {then: then})
+ function createMethod (thenable, name) {
+ thenable[name] = method(name)
+ return thenable
+ }
+ function method (name) {
+ return function () {
+ var promise = this.then()
+ return promise[name].apply(promise, arguments)
+ }
+ }
+ function then (/*onFulfill, onReject*/) {
+ var promise = new Promise(resolver)
+ return promise.then.apply(promise, arguments)
+ }
+}
+
+function resolves (value) {
+ return this.returns(createThenable(Promise, function (resolve) {
+ resolve(value)
+ }))
+}
+
+sinon.stub.resolves = resolves
+sinon.behavior.resolves = resolves
+
+function rejects (err) {
+ if (typeof err === 'string') {
+ err = new Error(err)
+ }
+ return this.returns(createThenable(Promise, function (resolve, reject) {
+ reject(err)
+ }))
+}
+
+sinon.stub.rejects = rejects
+sinon.behavior.rejects = rejects
+
+module.exports = function (_Promise_) {
+ if (typeof _Promise_ !== 'function') {
+ throw new Error('A Promise constructor must be provided')
+ } else {
+ Promise = _Promise_
+ }
+ return sinon
+}
+
+},{"create-thenable":7,"native-promise-only":8}],2:[function(require,module,exports){
+/*!
+ * object.omit <https://github.com/jonschlinkert/object.omit>
+ *
+ * Copyright (c) 2014-2015 Jon Schlinkert.
+ * Licensed under the MIT License
+ */
+
+'use strict';
+
+var isObject = require('isobject');
+var forOwn = require('for-own');
+
+module.exports = function omit(obj, props) {
+ if (obj == null || !isObject(obj)) {
+ return {};
+ }
+
+ if (props == null) {
+ return obj;
+ }
+
+ if (typeof props === 'string') {
+ props = [].slice.call(arguments, 1);
+ }
+
+ var o = {};
+
+ if (!Object.keys(obj).length) {
+ return o;
+ }
+
+ forOwn(obj, function (value, key) {
+ if (props.indexOf(key) === -1) {
+ o[key] = value;
+ }
+ });
+ return o;
+};
+},{"for-own":3,"isobject":5}],3:[function(require,module,exports){
+/*!
+ * for-own <https://github.com/jonschlinkert/for-own>
+ *
+ * Copyright (c) 2014-2015, Jon Schlinkert.
+ * Licensed under the MIT License.
+ */
+
+'use strict';
+
+var forIn = require('for-in');
+var hasOwn = Object.prototype.hasOwnProperty;
+
+module.exports = function forOwn(o, fn, thisArg) {
+ forIn(o, function (val, key) {
+ if (hasOwn.call(o, key)) {
+ return fn.call(thisArg, o[key], key, o);
+ }
+ });
+};
+
+},{"for-in":4}],4:[function(require,module,exports){
+/*!
+ * for-in <https://github.com/jonschlinkert/for-in>
+ *
+ * Copyright (c) 2014-2015, Jon Schlinkert.
+ * Licensed under the MIT License.
+ */
+
+'use strict';
+
+module.exports = function forIn(o, fn, thisArg) {
+ for (var key in o) {
+ if (fn.call(thisArg, o[key], key, o) === false) {
+ break;
+ }
+ }
+};
+},{}],5:[function(require,module,exports){
+/*!
+ * isobject <https://github.com/jonschlinkert/isobject>
+ *
+ * Copyright (c) 2014 Jon Schlinkert, contributors.
+ * Licensed under the MIT License
+ */
+
+'use strict';
+
+/**
+ * is the value an object, and not an array?
+ *
+ * @param {*} `value`
+ * @return {Boolean}
+ */
+
+module.exports = function isObject(o) {
+ return o != null && typeof o === 'object'
+ && !Array.isArray(o);
+};
+},{}],6:[function(require,module,exports){
+'use strict';
+
+/**
+ * Concatenates two arrays, removing duplicates in the process and returns one array with unique values.
+ * In case the elements in the array don't have a proper built in way to determine their identity,
+ * a custom identity function must be provided.
+ *
+ * As an example, {Object}s all return '[ 'object' ]' when .toString()ed and therefore require a custom
+ * identity function.
+ *
+ * @name exports
+ * @function unique-concat
+ * @param arr1 {Array} first batch of elements
+ * @param arr2 {Array} second batch of elements
+ * @param identity {Function} (optional) supply an alternative way to get an element's identity
+ */
+var go = module.exports = function uniqueConcat(arr1, arr2, identity) {
+
+ if (!arr1 || !arr2) throw new Error('Need two arrays to merge');
+ if (!Array.isArray(arr1)) throw new Error('First argument is not an array, but a ' + typeof arr1);
+ if (!Array.isArray(arr2)) throw new Error('Second argument is not an array, but a ' + typeof arr2);
+ if (identity && typeof identity !== 'function') throw new Error('Third argument should be a function');
+
+ function hashify(acc, k) {
+ acc[identity ? identity(k) : k] = k;
+ return acc;
+ }
+
+ var arr1Hash = arr1.reduce(hashify, {});
+ var mergedHash = arr2.reduce(hashify, arr1Hash);
+
+ return Object.keys(mergedHash).map(function (key) { return mergedHash[key]; });
+};
+
+},{}],7:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+exports['default'] = createThenable;
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _defineProperty(obj, key, value) { return Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); }
+
+var _uniqueConcat = require('unique-concat');
+
+var _uniqueConcat2 = _interopRequireDefault(_uniqueConcat);
+
+var _objectOmit = require('object-omit');
+
+var _objectOmit2 = _interopRequireDefault(_objectOmit);
+
+'use strict';
+
+function createThenable(Promise, resolver) {
+ return methods(Promise).reduce(createMethod, { then: then });
+ function createMethod(thenable, name) {
+ return _extends(thenable, _defineProperty({}, name, method(name)));
+ }
+ function method(name) {
+ return function () {
+ var _then;
+
+ return (_then = this.then())[name].apply(_then, arguments);
+ };
+ }
+ function then() {
+ var _ref;
+
+ return (_ref = new Promise(resolver)).then.apply(_ref, arguments);
+ }
+}
+
+function methods(Promise) {
+ return _uniqueConcat2['default'](['catch', 'finally'], Object.keys(_objectOmit2['default'](Promise.prototype, 'then')));
+}
+module.exports = exports['default'];
+/*onFulfill, onReject*/
+},{"object-omit":2,"unique-concat":6}],8:[function(require,module,exports){
+(function (global){
+/*! Native Promise Only
+ v0.7.8-a (c) Kyle Simpson
+ MIT License: http://getify.mit-license.org
+*/
+!function(t,n,e){n[t]=n[t]||e(),"undefined"!=typeof module&&module.exports?module.exports=n[t]:"function"==typeof define&&define.amd&&define(function(){return n[t]})}("Promise","undefined"!=typeof global?global:this,function(){"use strict";function t(t,n){l.add(t,n),h||(h=y(l.drain))}function n(t){var n,e=typeof t;return null==t||"object"!=e&&"function"!=e||(n=t.then),"function"==typeof n?n:!1}function e(){for(var t=0;t<this.chain.length;t++)o(this,1===this.state?this.chain[t].success:this.chain[t].failure,this.chain[t]);this.chain.length=0}function o(t,e,o){var r,i;try{e===!1?o.reject(t.msg):(r=e===!0?t.msg:e.call(void 0,t.msg),r===o.promise?o.reject(TypeError("Promise-chain cycle")):(i=n(r))?i.call(r,o.resolve,o.reject):o.resolve(r))}catch(c){o.reject(c)}}function r(o){var c,u,a=this;if(!a.triggered){a.triggered=!0,a.def&&(a=a.def);try{(c=n(o))?(u=new f(a),c.call(o,function(){r.apply(u,arguments)},function(){i.apply(u,arguments)})):(a.msg=o,a.state=1,a.chain.length>0&&t(e,a))}catch(s){i.call(u||new f(a),s)}}}function i(n){var o=this;o.triggered||(o.triggered=!0,o.def&&(o=o.def),o.msg=n,o.state=2,o.chain.length>0&&t(e,o))}function c(t,n,e,o){for(var r=0;r<n.length;r++)!function(r){t.resolve(n[r]).then(function(t){e(r,t)},o)}(r)}function f(t){this.def=t,this.triggered=!1}function u(t){this.promise=t,this.state=0,this.triggered=!1,this.chain=[],this.msg=void 0}function a(n){if("function"!=typeof n)throw TypeError("Not a function");if(0!==this.__NPO__)throw TypeError("Not a promise");this.__NPO__=1;var o=new u(this);this.then=function(n,r){var i={success:"function"==typeof n?n:!0,failure:"function"==typeof r?r:!1};return i.promise=new this.constructor(function(t,n){if("function"!=typeof t||"function"!=typeof n)throw TypeError("Not a function");i.resolve=t,i.reject=n}),o.chain.push(i),0!==o.state&&t(e,o),i.promise},this["catch"]=function(t){return this.then(void 0,t)};try{n.call(void 0,function(t){r.call(o,t)},function(t){i.call(o,t)})}catch(c){i.call(o,c)}}var s,h,l,p=Object.prototype.toString,y="undefined"!=typeof setImmediate?function(t){return setImmediate(t)}:setTimeout;try{Object.defineProperty({},"x",{}),s=function(t,n,e,o){return Object.defineProperty(t,n,{value:e,writable:!0,configurable:o!==!1})}}catch(d){s=function(t,n,e){return t[n]=e,t}}l=function(){function t(t,n){this.fn=t,this.self=n,this.next=void 0}var n,e,o;return{add:function(r,i){o=new t(r,i),e?e.next=o:n=o,e=o,o=void 0},drain:function(){var t=n;for(n=e=h=void 0;t;)t.fn.call(t.self),t=t.next}}}();var g=s({},"constructor",a,!1);return a.prototype=g,s(g,"__NPO__",0,!1),s(a,"resolve",function(t){var n=this;return t&&"object"==typeof t&&1===t.__NPO__?t:new n(function(n,e){if("function"!=typeof n||"function"!=typeof e)throw TypeError("Not a function");n(t)})}),s(a,"reject",function(t){return new this(function(n,e){if("function"!=typeof n||"function"!=typeof e)throw TypeError("Not a function");e(t)})}),s(a,"all",function(t){var n=this;return"[object Array]"!=p.call(t)?n.reject(TypeError("Not an array")):0===t.length?n.resolve([]):new n(function(e,o){if("function"!=typeof e||"function"!=typeof o)throw TypeError("Not a function");var r=t.length,i=Array(r),f=0;c(n,t,function(t,n){i[t]=n,++f===r&&e(i)},o)})}),s(a,"race",function(t){var n=this;return"[object Array]"!=p.call(t)?n.reject(TypeError("Not an array")):new n(function(e,o){if("function"!=typeof e||"function"!=typeof o)throw TypeError("Not a function");c(n,t,function(t,n){e(n)},o)})}),a});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}]},{},[1])(1)
+});
diff --git a/test/tests/preferences_syncTest.js b/test/tests/preferences_syncTest.js
@@ -0,0 +1,160 @@
+describe("Sync Preferences", function () {
+ var win, doc;
+ before(function* () {
+ // Load prefs with sync pane
+ win = yield loadWindow("chrome://zotero/content/preferences/preferences.xul", {
+ pane: 'zotero-prefpane-sync',
+ tabIndex: 0
+ });
+ doc = win.document;
+ let defer = Zotero.Promise.defer();
+ let pane = doc.getElementById('zotero-prefpane-sync');
+ if (!pane.loaded) {
+ pane.addEventListener('paneload', function () {
+ defer.resolve();
+ });
+ yield defer.promise;
+ }
+ });
+
+ after(function() {
+ win.close();
+ });
+
+ describe("Settings", function () {
+ describe("Zotero Data Sync", function () {
+ var getAPIKeyFromCredentialsStub, deleteAPIKey, indicatorElem;
+
+ var setCredentials = Zotero.Promise.coroutine(function* (username, password) {
+ let usernameElem = doc.getElementById('sync-username-textbox');
+ let passwordElem = doc.getElementById('sync-password');
+ usernameElem.value = username;
+ passwordElem.value = password;
+
+ // Triggered by `change` event for usernameElem and passwordElem;
+ yield win.Zotero_Preferences.Sync.linkAccount();
+ });
+
+ var apiKey = Zotero.Utilities.randomString(24);
+
+ var apiResponse = {
+ key: apiKey,
+ username: "Username",
+ userID: 1,
+ access: {}
+ };
+
+ before(function* () {
+ getAPIKeyFromCredentialsStub = sinon.stub(
+ Zotero.Sync.APIClient.prototype, 'createAPIKeyFromCredentials');
+ deleteAPIKey = sinon.stub(Zotero.Sync.APIClient.prototype, 'deleteAPIKey').resolves();
+ indicatorElem = doc.getElementById('sync-status-indicator')
+ sinon.stub(Zotero, 'alert');
+ });
+
+ beforeEach(function* (){
+ yield win.Zotero_Preferences.Sync.unlinkAccount(false);
+ deleteAPIKey.reset();
+ Zotero.alert.reset();
+ });
+
+ after(function() {
+ Zotero.HTTP.mock = null;
+ Zotero.alert.restore();
+ getAPIKeyFromCredentialsStub.restore();
+ deleteAPIKey.restore();
+ win.close();
+ });
+
+ it("should set API key and display full controls with correct credentials", function* () {
+ getAPIKeyFromCredentialsStub.resolves(apiResponse);
+ yield setCredentials("Username", "correctPassword");
+
+ assert.equal(Zotero.Sync.Data.Local.getAPIKey(), apiKey);
+ assert.equal(doc.getElementById('sync-unauthorized').getAttribute('hidden'), 'true');
+ });
+
+
+ it("should display dialog when credentials incorrect", function* () {
+ getAPIKeyFromCredentialsStub.resolves(false);
+ yield setCredentials("Username", "incorrectPassword");
+
+ assert.isTrue(Zotero.alert.called);
+ assert.equal(Zotero.Sync.Data.Local.getAPIKey(), "");
+ assert.equal(doc.getElementById('sync-authorized').getAttribute('hidden'), 'true');
+ });
+
+
+ it("should delete API key and display auth form when 'Unlink Account' clicked", function* () {
+ getAPIKeyFromCredentialsStub.resolves(apiResponse);
+ yield setCredentials("Username", "correctPassword");
+ assert.equal(Zotero.Sync.Data.Local.getAPIKey(), apiKey);
+
+ yield win.Zotero_Preferences.Sync.unlinkAccount(false);
+
+ assert.isTrue(deleteAPIKey.called);
+ assert.equal(Zotero.Sync.Data.Local.getAPIKey(), "");
+ assert.equal(doc.getElementById('sync-authorized').getAttribute('hidden'), 'true');
+ });
+
+ })
+ })
+
+ describe("#checkUser()", function () {
+ it("should prompt for user update and perform on accept", function* () {
+ yield Zotero.Users.setCurrentUserID(1);
+ yield Zotero.Users.setCurrentUsername("A");
+
+ waitForDialog(function (dialog) {
+ var text = dialog.document.documentElement.textContent;
+ var matches = text.match(/'[^']*'/g);
+ assert.equal(matches.length, 4);
+ assert.equal(matches[0], "'A'");
+ assert.equal(matches[1], "'B'");
+ assert.equal(matches[2], "'B'");
+ assert.equal(matches[3], "'A'");
+ });
+ var cont = yield win.Zotero_Preferences.Sync.checkUser(2, "B");
+ assert.isTrue(cont);
+
+ assert.equal(Zotero.Users.getCurrentUserID(), 2);
+ assert.equal(Zotero.Users.getCurrentUsername(), "B");
+ })
+
+ it("should prompt for user update and cancel", function* () {
+ yield Zotero.Users.setCurrentUserID(1);
+ yield Zotero.Users.setCurrentUsername("A");
+
+ waitForDialog(false, 'cancel');
+ var cont = yield win.Zotero_Preferences.Sync.checkUser(2, "B");
+ assert.isFalse(cont);
+
+ assert.equal(Zotero.Users.getCurrentUserID(), 1);
+ assert.equal(Zotero.Users.getCurrentUsername(), "A");
+ })
+
+ it("should update local relations when syncing for the first time", function* () {
+ yield resetDB({
+ thisArg: this,
+ skipBundledFiles: true
+ });
+
+ var item1 = yield createDataObject('item');
+ var item2 = yield createDataObject(
+ 'item', { libraryID: Zotero.Libraries.publicationsLibraryID }
+ );
+
+ yield item1.addLinkedItem(item2);
+
+ var cont = yield win.Zotero_Preferences.Sync.checkUser(1, "A");
+ assert.isTrue(cont);
+
+ var json = yield item1.toJSON();
+ var uri = json.relations[Zotero.Relations.linkedObjectPredicate][0];
+ assert.notInclude(uri, 'users/local');
+ assert.include(uri, 'users/1/publications');
+ })
+ })
+
+})
+
diff --git a/test/tests/syncRunnerTest.js b/test/tests/syncRunnerTest.js
@@ -167,10 +167,8 @@ describe("Zotero.Sync.Runner", function () {
describe("#checkAccess()", function () {
it("should check key access", function* () {
- spy = sinon.spy(runner, "checkUser");
setResponse('keyInfo.fullAccess');
var json = yield runner.checkAccess(runner.getAPIClient({ apiKey }));
- sinon.assert.calledWith(spy, 1, "Username");
var compare = {};
Object.assign(compare, responses.keyInfo.fullAccess.json);
delete compare.key;
@@ -409,60 +407,7 @@ describe("Zotero.Sync.Runner", function () {
assert.isTrue(Zotero.Groups.exists(groupData.json.id));
})
})
-
- describe("#checkUser()", function () {
- it("should prompt for user update and perform on accept", function* () {
- waitForDialog(function (dialog) {
- var text = dialog.document.documentElement.textContent;
- var matches = text.match(/'[^']*'/g);
- assert.equal(matches.length, 4);
- assert.equal(matches[0], "'A'");
- assert.equal(matches[1], "'B'");
- assert.equal(matches[2], "'B'");
- assert.equal(matches[3], "'A'");
- });
- var cont = yield runner.checkUser(2, "B");
- assert.isTrue(cont);
-
- assert.equal(Zotero.Users.getCurrentUserID(), 2);
- assert.equal(Zotero.Users.getCurrentUsername(), "B");
- })
-
- it("should prompt for user update and cancel", function* () {
- yield Zotero.Users.setCurrentUserID(1);
- yield Zotero.Users.setCurrentUsername("A");
-
- waitForDialog(false, 'cancel');
- var cont = yield runner.checkUser(2, "B");
- assert.isFalse(cont);
-
- assert.equal(Zotero.Users.getCurrentUserID(), 1);
- assert.equal(Zotero.Users.getCurrentUsername(), "A");
- })
-
- it("should update local relations when syncing for the first time", function* () {
- yield resetDB({
- thisArg: this,
- skipBundledFiles: true
- });
-
- var item1 = yield createDataObject('item');
- var item2 = yield createDataObject(
- 'item', { libraryID: Zotero.Libraries.publicationsLibraryID }
- );
-
- yield item1.addLinkedItem(item2);
-
- var cont = yield runner.checkUser(1, "A");
- assert.isTrue(cont);
-
- var json = yield item1.toJSON();
- var uri = json.relations[Zotero.Relations.linkedObjectPredicate][0];
- assert.notInclude(uri, 'users/local');
- assert.include(uri, 'users/1/publications');
- })
- })
-
+
describe("#sync()", function () {
before(function* () {
yield resetDB({
@@ -718,4 +663,69 @@ describe("Zotero.Sync.Runner", function () {
assert.isBelow(lastSyncTime, new Date().getTime());
})
})
+
+ describe("#createAPIKeyFromCredentials()", function() {
+ var data = {
+ name: "Automatic Zotero Client Key",
+ username: "Username",
+ access: {
+ user: {
+ library: true,
+ files: true,
+ notes: true,
+ write: true
+ },
+ groups: {
+ all: {
+ library: true,
+ write: true
+ }
+ }
+ }
+ };
+ var correctPostData = Object.assign({password: 'correctPassword'}, data);
+ var incorrectPostData = Object.assign({password: 'incorrectPassword'}, data);
+ var responseData = Object.assign({userID: 1, key: apiKey}, data);
+
+ it("should return json with key when credentials valid", function* () {
+ server.respond(function (req) {
+ if (req.method == "POST") {
+ var json = JSON.parse(req.requestBody);
+ assert.deepEqual(json, correctPostData);
+ req.respond(201, {}, JSON.stringify(responseData));
+ }
+ });
+
+ var json = yield runner.createAPIKeyFromCredentials('Username', 'correctPassword');
+ assert.equal(json.key, apiKey);
+ });
+
+ it("should return false when credentials invalid", function* () {
+ server.respond(function (req) {
+ if (req.method == "POST") {
+ var json = JSON.parse(req.requestBody);
+ assert.deepEqual(json, incorrectPostData);
+ req.respond(403);
+ }
+ });
+
+ var key = yield runner.createAPIKeyFromCredentials('Username', 'incorrectPassword');
+ assert.isFalse(key);
+ });
+ });
+
+ describe("#deleteAPIKey()", function() {
+ it("should send DELETE request with correct key", function* (){
+ Zotero.Sync.Data.Local.setAPIKey(apiKey);
+
+ server.respond(function (req) {
+ if (req.method == "DELETE") {
+ assert.equal(req.url, baseURL + "keys/" + apiKey);
+ }
+ req.respond(204);
+ });
+
+ yield runner.deleteAPIKey();
+ });
+ })
})