From 3fcfba5d36f58e17aa02f8ac56d43cd1d238da4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Mon, 14 Dec 2015 23:05:43 +0000 Subject: [PATCH 1/2] Add sinon-as-promised for mocking promises --- test/resource/sinon-as-promised.js | 250 +++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 test/resource/sinon-as-promised.js diff --git a/test/resource/sinon-as-promised.js b/test/resource/sinon-as-promised.js new file mode 100644 index 000000000..4f645c0c2 --- /dev/null +++ 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 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 + * + * 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 + * + * 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 + * + * 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 + * + * 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;t0&&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 Date: Wed, 2 Dec 2015 16:13:29 +0000 Subject: [PATCH 2/2] Restores sync credential functionality of 4.0. Improves UX of sync authentication. The account is now linked and unlinked and an API key related to the client is generated transparently in the background. The API key is deleted on unlinking. No sync options are allowed before linking an account. --- .../zotero/preferences/preferences_sync.js | 221 +++++++++- .../zotero/preferences/preferences_sync.xul | 400 ++++++++++-------- chrome/content/zotero/xpcom/http.js | 22 +- .../zotero/xpcom/sync/syncAPIClient.js | 54 ++- chrome/content/zotero/xpcom/sync/syncLocal.js | 33 +- .../content/zotero/xpcom/sync/syncRunner.js | 162 ++----- chrome/locale/en-US/zotero/preferences.dtd | 4 +- chrome/locale/en-US/zotero/zotero.properties | 5 +- chrome/skin/default/zotero/preferences.css | 24 ++ test/content/runtests.html | 1 + test/tests/preferences_syncTest.js | 160 +++++++ test/tests/syncRunnerTest.js | 122 +++--- 12 files changed, 826 insertions(+), 382 deletions(-) create mode 100644 test/tests/preferences_syncTest.js diff --git a/chrome/content/zotero/preferences/preferences_sync.js b/chrome/content/zotero/preferences/preferences_sync.js index 40eeb3bb5..e495dd94f 100644 --- 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 index 8b79a81a4..0e459276c 100644 --- a/chrome/content/zotero/preferences/preferences_sync.xul +++ b/chrome/content/zotero/preferences/preferences_sync.xul @@ -47,216 +47,240 @@ - + - - - - - + + + + + + + + + + + + + + + + + + +