diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js index ebbf22be55..59eedb6d91 100644 --- a/chrome/content/zotero/fileInterface.js +++ b/chrome/content/zotero/fileInterface.js @@ -444,10 +444,12 @@ var Zotero_File_Interface = new function() { var translation; - if (options.mendeleyCode) { + if (options.mendeleyAuth || options.mendeleyCode) { translation = yield _getMendeleyTranslation(); translation.createNewCollection = createNewCollection; + translation.mendeleyAuth = options.mendeleyAuth; translation.mendeleyCode = options.mendeleyCode; + translation.newItemsOnly = options.newItemsOnly; } else { // Check if the file is an SQLite database @@ -983,7 +985,7 @@ var Zotero_File_Interface = new function() { if (matchResult) { const mendeleyCode = matchResult[1]; Zotero.getMainWindow().setTimeout(() => this.showImportWizard({ mendeleyCode }), 0); - + // Clear all cookies to remove access // // This includes unrelated cookies in the central cookie store, but that's fine for @@ -998,7 +1000,7 @@ var Zotero_File_Interface = new function() { catch (e) { Zotero.logError(e); } - + win.close(); return; } diff --git a/chrome/content/zotero/import/importWizard.js b/chrome/content/zotero/import/importWizard.js index b8625d06f7..04a4706bf3 100644 --- a/chrome/content/zotero/import/importWizard.js +++ b/chrome/content/zotero/import/importWizard.js @@ -1,13 +1,18 @@ +/* global mendeleyAPIUtils:false */ import FilePicker from 'zotero/modules/filePicker'; +Components.utils.import("resource://gre/modules/Services.jsm"); +Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleyAPIUtils.js"); +const { directAuth } = mendeleyAPIUtils; var Zotero_Import_Wizard = { _wizard: null, _dbs: null, _file: null, _translation: null, - _mendeleyOnlineRedirectURLWithCode: null, _mendeleyCode: null, - + _mendeleyAuth: null, + _mendeleyHasPreviouslyImported: false, + _isZotfileInstalled: false, init: async function () { this._wizard = document.getElementById('import-wizard'); @@ -16,6 +21,16 @@ var Zotero_Import_Wizard = { // Local import disabled //document.getElementById('radio-import-source-mendeley').hidden = false; } + + const extensions = await Zotero.getInstalledExtensions(); + this._isZotfileInstalled = !!extensions.find(extName => extName.match(/^ZotFile((?!disabled).)*$/)); + + const predicateID = Zotero.RelationPredicates.getID('mendeleyDB:documentUUID'); + + if (predicateID) { + const relSQL = 'SELECT ROWID FROM itemRelations WHERE predicateID = ? LIMIT 1'; + this._mendeleyHasPreviouslyImported = !!(await Zotero.DB.valueQueryAsync(relSQL, predicateID)); + } // If no existing collections or non-trash items in the library, don't create a new // collection by default @@ -35,7 +50,7 @@ var Zotero_Import_Wizard = { } } - if (args && args.mendeleyCode) { + if (args && args.mendeleyCode && Zotero.Prefs.get("import.mendeleyUseOAuth")) { this._mendeleyCode = args.mendeleyCode; this._wizard.goTo('page-options'); } @@ -54,6 +69,14 @@ var Zotero_Import_Wizard = { 'import.fileHandling.description', Zotero.appName ); + + // Set up Mendeley username/password fields + document.querySelector('label[for="mendeley-username"]').textContent + = Zotero.Utilities.Internal.stringWithColon(Zotero.getString('general.username')); + document.querySelector('label[for="mendeley-password"]').textContent + = Zotero.Utilities.Internal.stringWithColon(Zotero.getString('general.password')); + document.getElementById('mendeley-username').addEventListener('keyup', this.onMendeleyAuthKeyUp.bind(this)); + document.getElementById('mendeley-password').addEventListener('keyup', this.onMendeleyAuthKeyUp.bind(this)); Zotero.Translators.init(); // async }, @@ -75,6 +98,14 @@ var Zotero_Import_Wizard = { break; case 'radio-import-source-mendeley-online': + if (this._isZotfileInstalled) { + this._onDone( + Zotero.getString('general.error'), + Zotero.getString('import.online.blockedByPlugin', 'ZotFile'), + false + ); + return; + } wizard.goTo('mendeley-online-explanation'); wizard.canRewind = true; break; @@ -108,19 +139,48 @@ var Zotero_Import_Wizard = { }, onMendeleyOnlineShow: async function () { - document.getElementById('mendeley-online-description').textContent = Zotero.getString( - 'import.online.intro', [Zotero.appName, 'Mendeley Reference Manager', 'Mendeley'] - ); + document.getElementById('mendeley-online-description').textContent = Zotero.Prefs.get("import.mendeleyUseOAuth") + ? Zotero.getString('import.online.intro', [Zotero.appName, 'Mendeley Reference Manager', 'Mendeley']) + : Zotero.getString('import.online.formIntro', [Zotero.appName, 'Mendeley Reference Manager', 'Mendeley']); document.getElementById('mendeley-online-description2').textContent = Zotero.getString( 'import.online.intro2', [Zotero.appName, 'Mendeley'] ); + document.getElementById('mendeley-login').style.display = Zotero.Prefs.get("import.mendeleyUseOAuth") ? 'none' : ''; + document.getElementById('mendeley-online-login-feedback').style.display = 'none'; + this._wizard.canAdvance = Zotero.Prefs.get("import.mendeleyUseOAuth"); }, - onMendeleyOnlineAdvance: function () { - if (!this._mendeleyOnlineRedirectURLWithCode) { + onMendeleyOnlineAdvance: async function () { + if (Zotero.Prefs.get("import.mendeleyUseOAuth")) { Zotero_File_Interface.authenticateMendeleyOnline(); window.close(); } + else { + const userNameEl = document.getElementById('mendeley-username'); + const passwordEl = document.getElementById('mendeley-password'); + userNameEl.disabled = true; + passwordEl.disabled = true; + try { + this._mendeleyAuth = await directAuth(userNameEl.value, passwordEl.value); + this._wizard.goTo('page-options'); + } + catch (e) { + const feedbackEl = document.getElementById('mendeley-online-login-feedback'); + feedbackEl.textContent = Zotero.getString('import.online.wrongCredentials', ['Mendeley']); + feedbackEl.style.display = ''; + this._wizard.canAdvance = false; // change to either of the inputs will reset thi + } + finally { + userNameEl.disabled = false; + passwordEl.disabled = false; + } + } + }, + + onMendeleyAuthKeyUp: function () { + document.getElementById('mendeley-online-login-feedback').style.display = 'none'; + this._wizard.canAdvance = document.getElementById('mendeley-username').value.length > 0 + && document.getElementById('mendeley-password').value.length > 0; }, goToStart: function () { @@ -201,7 +261,12 @@ var Zotero_Import_Wizard = { onOptionsShown: function () { - document.getElementById('file-handling-options').hidden = !!this._mendeleyCode; + document.getElementById('file-handling-options').hidden = !!(this._mendeleyAuth || this._mendeleyCode); + const hideExtraMendeleyOptions = !this._mendeleyHasPreviouslyImported || !(this._mendeleyAuth || this._mendeleyCode); + document.getElementById('mendeley-options').hidden = hideExtraMendeleyOptions; + if (hideExtraMendeleyOptions) { + document.getElementById('new-items-only-checkbox').removeAttribute('checked'); + } }, @@ -228,7 +293,7 @@ var Zotero_Import_Wizard = { onImportStart: async function () { - if (!this._file && !this._mendeleyCode) { + if (!this._file && !(this._mendeleyAuth || this._mendeleyCode)) { let index = document.getElementById('file-list').selectedIndex; this._file = this._dbs[index].path; } @@ -243,7 +308,9 @@ var Zotero_Import_Wizard = { addToLibraryRoot: !document.getElementById('create-collection-checkbox') .hasAttribute('checked'), linkFiles: document.getElementById('file-handling-radio').selectedIndex == 1, - mendeleyCode: this._mendeleyCode + mendeleyAuth: this._mendeleyAuth, + mendeleyCode: this._mendeleyCode, + newItemsOnly: document.getElementById('new-items-only-checkbox').hasAttribute('checked') }); // Cancelled by user or due to error diff --git a/chrome/content/zotero/import/importWizard.xul b/chrome/content/zotero/import/importWizard.xul index 4fc12adaf7..17788ecb19 100644 --- a/chrome/content/zotero/import/importWizard.xul +++ b/chrome/content/zotero/import/importWizard.xul @@ -37,6 +37,17 @@ > + + + + + + + + + + + + + + { - const options = { - body: isRefresh - ? `grant_type=refresh_token&refresh_token=${codeOrRefreshToken}` - : `grant_type=authorization_code&code=${codeOrRefreshToken}`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - } - }; - const response = await Zotero.HTTP.request('POST', OAUTH_URL, options); +const getTokens = async (url, bodyProps, headers = {}, options = {}) => { + const body = Object.entries(bodyProps) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&'); + headers = { ...headers, 'Content-Type': 'application/x-www-form-urlencoded' }; + + if (!Zotero.Prefs.get('import.mendeleyUseOAuth')) { + headers['User-Agent'] = USER_AGENT; + } + + options = { ...options, body, headers, timeout: 30000 }; + const response = await Zotero.HTTP.request('POST', url, options); return JSON.parse(response.responseText); }; +const directAuth = async (username, password, headers = {}, options = {}) => { + const bodyProps = { + client_id: CLIENT_ID, // eslint-disable-line camelcase + client_secret: CLIENT_NOT_VERY_SECRET, // eslint-disable-line camelcase + grant_type: 'password', // eslint-disable-line camelcase + password, + scope: 'all', + username + }; + + return getTokens(OAUTH_URL, bodyProps, headers, options); +}; + +const codeAuth = async (code, headers = {}, options = {}) => { + const bodyProps = { + grant_type: 'authorization_code', // eslint-disable-line camelcase + code, + }; + + return getTokens(ZOTERO_OAUTH_URL, bodyProps, headers, options); +}; + +const refreshAuth = async (refreshToken, headers = {}, options = {}) => { + const bodyProps = { + grant_type: 'refresh_token', // eslint-disable-line camelcase + refresh_token: refreshToken, // eslint-disable-line camelcase + }; + + if (!Zotero.Prefs.get('import.mendeleyUseOAuth')) { + bodyProps.client_id = CLIENT_ID; // eslint-disable-line camelcase + bodyProps.client_secret = CLIENT_NOT_VERY_SECRET; // eslint-disable-line camelcase + } + + return getTokens( + Zotero.Prefs.get('import.mendeleyUseOAuth') ? ZOTERO_OAUTH_URL : OAUTH_URL, + bodyProps, headers, options + ); +}; + const getNextLinkFromResponse = (response) => { let next = null; let links = response.getResponseHeader('link'); @@ -42,7 +87,7 @@ const apiFetchUrl = async (tokens, url, headers = {}, options = {}) => { } catch (e) { if (e.status === 401 || e.status === 403) { - const newTokens = await getTokens(tokens.refresh_token, true); + const newTokens = await refreshAuth(tokens.refresh_token); // update tokens in the tokens object and in the header for next request tokens.access_token = newTokens.access_token; // eslint-disable-line camelcase tokens.refresh_token = newTokens.refresh_token; // eslint-disable-line camelcase @@ -81,6 +126,5 @@ const getAll = async (tokens, endPoint, params = {}, headers = {}, options = {}, return data; }; - -return { getNextLinkFromResponse, getTokens, apiFetch, apiFetchUrl, get, getAll }; +return { codeAuth, directAuth, getNextLinkFromResponse, apiFetch, apiFetchUrl, get, getAll }; })(); diff --git a/chrome/content/zotero/import/mendeley/mendeleyImport.js b/chrome/content/zotero/import/mendeley/mendeleyImport.js index e7f0346324..a67c4917eb 100644 --- a/chrome/content/zotero/import/mendeley/mendeleyImport.js +++ b/chrome/content/zotero/import/mendeley/mendeleyImport.js @@ -10,7 +10,7 @@ Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/men Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleySchemaMap.js"); const { apiTypeToDBType, apiFieldToDBField } = mendeleyOnlineMappings; -const { apiFetch, get, getAll, getTokens } = mendeleyAPIUtils; +const { apiFetch, codeAuth, directAuth, get, getAll } = mendeleyAPIUtils; const colorMap = new Map(); colorMap.set('rgb(255, 245, 173)', '#ffd400'); @@ -28,7 +28,9 @@ var Zotero_Import_Mendeley = function () { this.createNewCollection = null; this.linkFiles = null; this.newItems = []; - this.mendeleyCode = null; + this.newCollections = []; + this.mendeleyAuth = null; + this.newItemsOnly = false; this._tokens = null; this._db = null; @@ -115,8 +117,11 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) { throw new Error("Not a valid Mendeley database"); } - if (this.mendeleyCode) { - this._tokens = await getTokens(this.mendeleyCode); + if (this.mendeleyAuth) { + this._tokens = this.mendeleyAuth; + } + else if (this.mendeleyCode) { + this._tokens = await codeAuth(this.mendeleyCode); } if (!this._file && !this._tokens) { @@ -303,9 +308,13 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) { annotations.get(document.id) ); } - this.newItems.push(Zotero.Items.get(documentIDMap.get(document.id))); this._interruptChecker(true); } + if (this.newItemsOnly && rootCollectionKey && this.newItems.length === 0) { + Zotero.debug(`Mendeley Import detected no new items, removing import collection containing ${this.newCollections.length} collections created during the import`); + const rootCollection = await Zotero.Collections.getAsync(options.collections[0]); + await rootCollection.eraseTx(this._saveOptions); + } Zotero.debug(`Completed Mendeley import in ${Math.round((Date.now() - this._started) / 1000)}s. (Started: ${this._started})`); } catch (e) { @@ -480,6 +489,7 @@ Zotero_Import_Mendeley.prototype._saveCollections = async function (libraryID, j collection.key = collectionJSON.key; await collection.loadPrimaryData(); } + this.newCollections.push(collection); } // Remove external ids before saving @@ -541,11 +551,19 @@ Zotero_Import_Mendeley.prototype._getDocumentsAPI = async function (groupID) { } - return (await getAll(this._tokens, 'documents', params, headers, {}, this._interruptChecker)).map(d => ({ - ...d, - uuid: d.id, - remoteUuid: d.id - })); + return (await getAll(this._tokens, 'documents', params, headers, {}, this._interruptChecker)).map(d => { + const processedDocument = { ...d, remoteUuid: d.id }; + + try { + const clientData = JSON.parse(d.client_data); + processedDocument.uuid = clientData.desktop_id ? clientData.desktop_id : d.id; + } + catch (_) { + processedDocument.uuid = d.id; + } + + return processedDocument; + }); }; /** @@ -1279,10 +1297,12 @@ Zotero_Import_Mendeley.prototype._saveItems = async function (libraryID, json) { var lastExistingParentItem; for (let i = 0; i < json.length; i++) { let itemJSON = json[i]; + let isMappedToExisting = false; // Check if the item has been previously imported let item = await this._findExistingItem(libraryID, itemJSON, lastExistingParentItem); if (item) { + isMappedToExisting = true; if (item.isRegularItem()) { lastExistingParentItem = item; @@ -1310,8 +1330,25 @@ Zotero_Import_Mendeley.prototype._saveItems = async function (libraryID, json) { // Remove external id before save let toSave = Object.assign({}, itemJSON); delete toSave.documentID; - - item.fromJSON(toSave); + + if ((this.newItemsOnly && !isMappedToExisting) || !this.newItemsOnly) { + if (isMappedToExisting) { + // dateAdded shouldn't change on an updated item. See #2881 + delete toSave.dateAdded; + } + item.fromJSON(toSave); + this.newItems.push(item); + } + else if (isMappedToExisting && toSave.relations) { + const predicate = 'mendeleyDB:documentUUID'; + const existingRels = item.getRelationsByPredicate(predicate); + const newRel = toSave.relations[predicate]; + if (existingRels.length && newRel && existingRels[0] !== newRel) { + Zotero.debug(`Migrating relation ${predicate} for existing item ${item.key} from ${existingRels[0]} to ${newRel}`); + item.removeRelation(predicate, existingRels[0]); + item.addRelation(predicate, newRel); + } + } await item.saveTx({ skipDateModifiedUpdate: true, ...this._saveOptions diff --git a/chrome/content/zotero/import/mendeley/mendeleyOnlineMappings.js b/chrome/content/zotero/import/mendeley/mendeleyOnlineMappings.js index 102437f1fd..9aa138dd4a 100644 --- a/chrome/content/zotero/import/mendeley/mendeleyOnlineMappings.js +++ b/chrome/content/zotero/import/mendeley/mendeleyOnlineMappings.js @@ -28,6 +28,7 @@ var mendeleyOnlineMappings = { arxiv: 'arxivId', accessed: 'dateAccessed', authors: false, // all author types handled separately + client_data: false, // desktop_id extraction handled separately citation_key: 'citationKey', created: 'added', edition: 'edition', diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd index 21e4835e30..065ab13062 100644 --- a/chrome/locale/en-US/zotero/zotero.dtd +++ b/chrome/locale/en-US/zotero/zotero.dtd @@ -186,6 +186,7 @@ + diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index b390274418..de6936fbd8 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -94,6 +94,8 @@ general.loading = Loading… general.richText = Rich Text general.clearSelection = Clear Selection general.insert = Insert +general.username = Username +general.password = Password general.yellow = Yellow general.red = Red @@ -807,8 +809,11 @@ import.localImport = local import import.fileHandling.store = Copy files to the %S storage folder import.fileHandling.link = Link to files in original location import.fileHandling.description = Linked files cannot be synced by %S. +import.online.formIntro = Please enter your credentials to log in to %2$S. This is necessary to import your %3$S library into %1$S. import.online.intro = In the next step you will be asked to log in to %2$S and grant %1$S access. This is necessary to import your %3$S library into %1$S. import.online.intro2 = %1$S will never see or store your %2$S password. +import.online.wrongCredentials = Login to %1$S failed. Please re-enter credentials and try again. +import.online.blockedByPlugin = The import cannot continue with "%1$S" installed. Please disable this plugin and try again. quickCopy.copyAs = Copy as %S diff --git a/chrome/skin/default/zotero/importWizard.css b/chrome/skin/default/zotero/importWizard.css index ec8d6d27a8..99cd5cf261 100644 --- a/chrome/skin/default/zotero/importWizard.css +++ b/chrome/skin/default/zotero/importWizard.css @@ -3,6 +3,40 @@ font-weight: bold; } +#mendeley-login { + padding: 1.5em 1em 0; + font-size: 13px; + display: flex; + flex-direction: column; + align-items: center; + border: none; +} + +#mendeley-login .field { + display: flex; + max-width: 300px; + line-height: 1.5em; + align-items: center; +} + +#mendeley-login .field + .field { + margin-top: .5em; +} + +#mendeley-login label { + flex: 0 0 90px; + width: 90px; + margin-right: 8px; + text-align: right; +} + +#mendeley-login input { + flex: 1 1 auto; + font-size: 13px; +} + + + /* Start */ wizard[currentpageid="page-start"] .wizard-header-label { padding-top: 24px; @@ -80,3 +114,11 @@ button, checkbox { margin-top: 13px; margin-left: 0; } + +#mendeley-online-login-feedback { + text-align: center; + font-size: 13px; + margin-top: 1.3em; + color: red; + font-weight: bold; +} \ No newline at end of file diff --git a/test/tests/data/mendeleyMock/items-simple-no-desktop-id.json b/test/tests/data/mendeleyMock/items-simple-no-desktop-id.json new file mode 100644 index 0000000000..ff3aa22494 --- /dev/null +++ b/test/tests/data/mendeleyMock/items-simple-no-desktop-id.json @@ -0,0 +1,88 @@ +[ + { + "authored": false, + "confirmed": true, + "created": "2021-11-02T09:54:28.353Z", + "file_attached": false, + "hidden": false, + "id": "7fea3cb3-f97d-3f16-8fad-f59caaa71688", + "last_modified": "2021-11-02T12:26:30.025Z", + "private_publication": false, + "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", + "read": false, + "source": "lorem ipsum", + "identifiers": + { + "doi": "10.1111", + "pmid": "PMID: 11111111", + "arxiv": "1111.2222" + }, + "starred": false, + "title": "Foo Bar", + "authors": + [ + { + "first_name": "Tom", + "last_name": "Najdek" + }, + { + "first_name": "Lorem", + "last_name": "Ipsum" + } + ], + "type": "journal", + "folder_uuids": [ + "8d2f262d-49b3-4dfc-8968-0bb71bcd92ea" + ], + "year": 1987 + }, { + "authored": false, + "confirmed": true, + "created": "2021-11-04T11:53:10.353Z", + "file_attached": false, + "hidden": false, + "id": "07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef", + "last_modified": "2021-11-04T11:53:10.353Z", + "private_publication": false, + "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", + "read": false, + "starred": false, + "title": "Sample Report", + "type": "report", + "year": 2002 + }, { + "title": "Item with PDF", + "type": "journal", + "year": 2005, + "source": "Zotero", + "pages": "1-11", + "websites": + [ + "https://zotero.org" + ], + "id": "c54b0c6f-c4ce-4706-8742-bc7d032df862", + "created": "2021-11-09T10:26:15.201Z", + "file_attached": true, + "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", + "last_modified": "2021-11-09T10:26:16.303Z", + "read": false, + "starred": false, + "authored": false, + "confirmed": true, + "hidden": false, + "private_publication": false, + "abstract": "Lorem Ipsum. Nostrud elit ullamco laborum cillum.", + "files": + [ + { + "id": "19fb5e5b-1a39-4851-b513-d48441a670e1", + "document_id": "c54b0c6f-c4ce-4706-8742-bc7d032df862", + "mime_type": "application/pdf", + "file_name": "item.pdf", + "size": 123456, + "created": "2021-11-09T10:26:16.292Z", + "filehash": "cc22c6611277df346ff8dc7386ba3880b2bafa15" + } + ] + } +] \ No newline at end of file diff --git a/test/tests/data/mendeleyMock/items-simple.json b/test/tests/data/mendeleyMock/items-simple.json index ff3aa22494..4e0da03e3f 100644 --- a/test/tests/data/mendeleyMock/items-simple.json +++ b/test/tests/data/mendeleyMock/items-simple.json @@ -6,6 +6,7 @@ "file_attached": false, "hidden": false, "id": "7fea3cb3-f97d-3f16-8fad-f59caaa71688", + "client_data": "{\"desktop_id\":\"b5f57b1a-f083-486c-aec7-5d5edd366dd2\"}", "last_modified": "2021-11-02T12:26:30.025Z", "private_publication": false, "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", @@ -42,6 +43,7 @@ "file_attached": false, "hidden": false, "id": "07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef", + "client_data": "{\"desktop_id\":\"616ec6d1-8d23-4414-8b6e-7bb129677577\"}", "last_modified": "2021-11-04T11:53:10.353Z", "private_publication": false, "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", @@ -61,6 +63,7 @@ "https://zotero.org" ], "id": "c54b0c6f-c4ce-4706-8742-bc7d032df862", + "client_data": "{\"desktop_id\":\"3630a4bf-d97e-46c4-8611-61ec50f840c6\"}", "created": "2021-11-09T10:26:15.201Z", "file_attached": true, "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", diff --git a/test/tests/data/mendeleyMock/items-updated.json b/test/tests/data/mendeleyMock/items-updated.json index 2492ac5477..4165e33e37 100644 --- a/test/tests/data/mendeleyMock/items-updated.json +++ b/test/tests/data/mendeleyMock/items-updated.json @@ -2,10 +2,11 @@ { "authored": false, "confirmed": true, - "created": "2021-11-04T11:53:10.353Z", + "created": "2021-12-05T12:00:00.000Z", "file_attached": false, "hidden": false, "id": "07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef", + "client_data": "{\"desktop_id\":\"616ec6d1-8d23-4414-8b6e-7bb129677577\"}", "last_modified": "2021-11-05T11:53:10.353Z", "private_publication": false, "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", @@ -15,5 +16,23 @@ "type": "journal", "source": "lorem ipsum", "year": 2002 + }, + { + "authored": false, + "confirmed": true, + "created": "2021-11-05T12:33:18.353Z", + "file_attached": false, + "hidden": false, + "id": "31a8251f-88b0-497b-9d30-1b2516771057", + "client_data": "{\"desktop_id\":\"86e56a00-5ae5-4fe8-a977-9298a03b16d6\"}", + "last_modified": "2021-11-05T12:33:18.353Z", + "private_publication": false, + "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", + "read": false, + "starred": true, + "title": "Completely new item", + "type": "book", + "source": "lorem ipsum", + "year": 1999 } ] \ No newline at end of file diff --git a/test/tests/mendeleyImportTest.js b/test/tests/mendeleyImportTest.js index 8eb7936656..d176806ff0 100644 --- a/test/tests/mendeleyImportTest.js +++ b/test/tests/mendeleyImportTest.js @@ -1,12 +1,17 @@ /* global setHTTPResponse:false, sinon: false, Zotero_Import_Mendeley: false, HttpServer: false */ describe('Zotero_Import_Mendeley', function () { - var server, importer, httpd, httpdURL; + var server, httpd, httpdURL, importers; + + const getImporter = () => { + const importer = new Zotero_Import_Mendeley(); + importer.mendeleyAuth = { access_token: 'access_token', refresh_token: 'refresh_token' };// eslint-disable-line camelcase + importers.push(importer); + return importer; + }; before(async () => { Components.utils.import('chrome://zotero/content/import/mendeley/mendeleyImport.js'); - importer = new Zotero_Import_Mendeley(); - importer.mendeleyCode = 'CODE'; // real http server is used to deliver an empty pdf so that annotations can be processed during import Components.utils.import("resource://zotero-unit/httpd.js"); @@ -25,12 +30,15 @@ describe('Zotero_Import_Mendeley', function () { }); beforeEach(async () => { + importers = []; Zotero.HTTP.mock = sinon.FakeXMLHttpRequest; - server = sinon.fakeServer.create(); + server = sinon.fakeServer.create({ + unsafeHeadersEnabled: false + }); server.autoRespond = true; - setHTTPResponse(server, 'https://www.zotero.org/', { + setHTTPResponse(server, 'https://api.mendeley.com/', { method: 'POST', - url: `utils/mendeley/oauth`, + url: `oauth/token`, status: 200, headers: {}, json: { @@ -127,12 +135,21 @@ describe('Zotero_Import_Mendeley', function () { }); }); - afterEach(() => { + afterEach(async () => { + await Promise.all( + importers + .map(importer => ([ + Zotero.Items.erase(Array.from(new Set(importer.newItems)).map(i => i.id)), + Zotero.Collections.erase(Array.from(new Set(importer.newCollections)).map(c => c.id)) + ])) + .reduce((prev, a) => ([...prev, ...a]), []) // .flat() in >= FF62 + ); Zotero.HTTP.mock = null; }); describe('#import', () => { it("should import collections, items, attachments & annotations", async () => { + const importer = getImporter(); await importer.translate({ libraryID: Zotero.Libraries.userLibraryID, collections: null, @@ -140,17 +157,17 @@ describe('Zotero_Import_Mendeley', function () { }); const journal = (await Zotero.Relations - .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '7fea3cb3-f97d-3f16-8fad-f59caaa71688')) + .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', 'b5f57b1a-f083-486c-aec7-5d5edd366dd2')) .filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted) .shift(); const report = (await Zotero.Relations - .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef')) + .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '616ec6d1-8d23-4414-8b6e-7bb129677577')) .filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted) .shift(); const withpdf = (await Zotero.Relations - .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', 'c54b0c6f-c4ce-4706-8742-bc7d032df862')) + .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '3630a4bf-d97e-46c4-8611-61ec50f840c6')) .filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted) .shift(); @@ -159,10 +176,14 @@ describe('Zotero_Import_Mendeley', function () { .filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted) .shift(); + + assert.equal(journal.getRelations()['mendeleyDB:remoteDocumentUUID'], '7fea3cb3-f97d-3f16-8fad-f59caaa71688'); assert.equal(journal.getField('title'), 'Foo Bar'); assert.equal(journal.itemTypeID, Zotero.ItemTypes.getID('journalArticle')); + assert.equal(report.getRelations()['mendeleyDB:remoteDocumentUUID'], '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef'); assert.equal(report.getField('title'), 'Sample Report'); assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('report')); + assert.equal(withpdf.getRelations()['mendeleyDB:remoteDocumentUUID'], 'c54b0c6f-c4ce-4706-8742-bc7d032df862'); assert.equal(withpdf.getField('title'), 'Item with PDF'); assert.equal(withpdf.itemTypeID, Zotero.ItemTypes.getID('journalArticle')); @@ -237,22 +258,23 @@ describe('Zotero_Import_Mendeley', function () { assert.equal(parentCollection.name, 'folder1'); }); - it("should update previously imported item", async () => { - const importer = new Zotero_Import_Mendeley(); - importer.mendeleyCode = 'CODE'; - await importer.translate({ + it("should update previously imported item, based on config", async () => { + const importer1 = getImporter(); + await importer1.translate({ libraryID: Zotero.Libraries.userLibraryID, collections: null, linkFiles: false, }); const report = (await Zotero.Relations - .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef')) + .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '616ec6d1-8d23-4414-8b6e-7bb129677577')) .filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted) .shift(); + assert.equal(report.getField('title'), 'Sample Report'); assert.equal(report.getField('year'), '2002'); + assert.equal(report.getField('dateAdded'), '2021-11-04 11:53:10'); assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('report')); assert.lengthOf(report.getTags(), 0); @@ -266,7 +288,9 @@ describe('Zotero_Import_Mendeley', function () { ) }); - await importer.translate({ + const importer2 = getImporter(); + importer2.newItemsOnly = false; + await importer2.translate({ libraryID: Zotero.Libraries.userLibraryID, collections: null, linkFiles: false, @@ -276,6 +300,107 @@ describe('Zotero_Import_Mendeley', function () { assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('journalArticle')); assert.equal(report.getField('year'), '2002'); assert.sameMembers(report.getTags().map(t => t.tag), ['\u2605']); + // dateAdded shouldn't change on an updated item. See #2881 + assert.equal(report.getField('dateAdded'), '2021-11-04 11:53:10'); + }); + + it("shouldn't update previously imported item, based on config", async () => { + const importer1 = getImporter(); + await importer1.translate({ + libraryID: Zotero.Libraries.userLibraryID, + collections: null, + linkFiles: false, + }); + + const report = (await Zotero.Relations + .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '616ec6d1-8d23-4414-8b6e-7bb129677577')) + .filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted) + .shift(); + + const noNewItemHere = await Zotero.Relations.getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '86e56a00-5ae5-4fe8-a977-9298a03b16d6'); + + + assert.equal(report.getField('title'), 'Sample Report'); + assert.equal(report.getField('year'), '2002'); + assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('report')); + assert.lengthOf(report.getTags(), 0); + assert.lengthOf(noNewItemHere, 0); + + setHTTPResponse(server, 'https://api.mendeley.com/', { + method: 'GET', + url: `documents?view=all&limit=500`, + status: 200, + headers: {}, + json: JSON.parse( + await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/items-updated.json') + ) + }); + + const importer2 = getImporter(); + importer2.newItemsOnly = true; + await importer2.translate({ + libraryID: Zotero.Libraries.userLibraryID, + collections: null, + linkFiles: false, + }); + + assert.equal(report.getField('title'), 'Sample Report'); + assert.equal(report.itemTypeID, Zotero.ItemTypes.getID('report')); + assert.equal(report.getField('year'), '2002'); + assert.lengthOf(report.getTags(), 0); + + const newItem = (await Zotero.Relations + .getByPredicateAndObject('item', 'mendeleyDB:documentUUID', '86e56a00-5ae5-4fe8-a977-9298a03b16d6')) + .filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted) + .shift(); + + assert.equal(newItem.getField('title'), 'Completely new item'); + }); + + it("should correct IDs if available on subsequent import", async () => { + setHTTPResponse(server, 'https://api.mendeley.com/', { + method: 'GET', + url: `documents?view=all&limit=500`, + status: 200, + headers: {}, + json: JSON.parse( + await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/items-simple-no-desktop-id.json') + ) + }); + const importer = getImporter(); + importer.newItemsOnly = true; + await importer.translate({ + libraryID: Zotero.Libraries.userLibraryID, + collections: null, + linkFiles: false, + }); + + const report = (await Zotero.Relations + .getByPredicateAndObject('item', 'mendeleyDB:remoteDocumentUUID', '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef')) + .filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted) + .shift(); + + assert.equal(report.getField('title'), 'Sample Report'); + assert.equal(report.getRelations()['mendeleyDB:documentUUID'], '07a74c26-28d1-4d9f-a60d-3f3bc5ef76ef'); + + setHTTPResponse(server, 'https://api.mendeley.com/', { + method: 'GET', + url: `documents?view=all&limit=500`, + status: 200, + headers: {}, + json: JSON.parse( + await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/items-simple.json') + ) + }); + + await importer.translate({ + libraryID: Zotero.Libraries.userLibraryID, + collections: null, + linkFiles: false, + }); + + assert.equal(report.getField('title'), 'Sample Report'); + assert.equal(report.getRelations()['mendeleyDB:documentUUID'], '616ec6d1-8d23-4414-8b6e-7bb129677577'); }); }); });