Merge pull request #879 from adomasven/feature/transparent-api-keygen

Restores the functionality of 4.0 for sync settings
This commit is contained in:
Dan Stillman 2015-12-14 19:28:56 -05:00
commit cfee7ea9d2
13 changed files with 1076 additions and 382 deletions

View file

@ -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) {

View file

@ -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.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 &amp;&amp; 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;"/>
<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; }"/>
<hbox>
<textbox id="storage-username"
preference="pref-storage-username"
onkeypress="if (Zotero.isMac &amp;&amp; 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;"/>
<textbox id="sync-password" type="password"
onchange="Zotero.Sync.Server.password = this.value"/>
<hbox>
<textbox id="storage-password" flex="0" type="password"
onkeypress="if (Zotero.isMac &amp;&amp; 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>
<label value="API Key (temp)"/>
<textbox id="sync-api-key" maxlength="24" size="25"
onchange="Zotero.Sync.Data.Local.setAPIKey(this.value)"/>
</row>
<row>
<box/>
<!--<checkbox label="&zotero.preferences.sync.syncAutomatically;" preference="pref-sync-autosync"/>-->
<checkbox label="&zotero.preferences.sync.syncAutomatically;"
disabled="true"/>
</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>
</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"/>
<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"/>
</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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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>
</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>
</vbox>
</groupbox>
<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>
</vbox>
</tabpanel>
<tabpanel id="zotero-reset" orient="vertical">

View file

@ -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;

View file

@ -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;

View file

@ -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");

View file

@ -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;
});
@ -425,108 +417,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
*
@ -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 "";
})
}

View file

@ -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">

View file

@ -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:

View file

@ -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;

View file

@ -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>

View file

@ -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)
});

View file

@ -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');
})
})
})

View file

@ -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();
});
})
})