From 833ee105168c343db9064a721c562c91fb50face Mon Sep 17 00:00:00 2001 From: Tom Najdek Date: Thu, 6 May 2021 11:42:19 +0200 Subject: [PATCH] Mendeley import: Switch to authorisation code flow via proxy --- chrome/content/zotero/fileInterface.js | 13 ++--- chrome/content/zotero/import/importWizard.js | 12 ++-- .../import/mendeley/mendeleyAPIUtils.js | 55 +++++++++++++++---- .../zotero/import/mendeley/mendeleyImport.js | 50 +++++++++-------- 4 files changed, 83 insertions(+), 47 deletions(-) diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js index 36162745c6..e526a7497d 100644 --- a/chrome/content/zotero/fileInterface.js +++ b/chrome/content/zotero/fileInterface.js @@ -332,10 +332,10 @@ var Zotero_File_Interface = new function() { var translation; - if (options.mendeleyOnlineToken) { + if (options.mendeleyCode) { translation = yield _getMendeleyTranslation(); translation.createNewCollection = createNewCollection; - translation.token = options.mendeleyOnlineToken; + translation.mendeleyCode = options.mendeleyCode; } else { // Check if the file is an SQLite database @@ -863,10 +863,10 @@ var Zotero_File_Interface = new function() { this.authenticateMendeleyOnlinePoll = function (win) { if (win && win[0] && win[0].location) { - const matchResult = win[0].location.toString().match(/access_token=(.*?)(?:&|$)/i); + const matchResult = win[0].location.toString().match(/(?:\?|&)code=(.*?)(?:&|$)/i); if (matchResult) { - const mendeleyAccessToken = matchResult[1]; - Zotero.getMainWindow().setTimeout(() => this.showImportWizard({ mendeleyAccessToken }), 0); + const mendeleyCode = matchResult[1]; + Zotero.getMainWindow().setTimeout(() => this.showImportWizard({ mendeleyCode }), 0); // Clear all cookies to remove access // @@ -894,8 +894,7 @@ var Zotero_File_Interface = new function() { }; this.authenticateMendeleyOnline = function () { - const uri = 'https://api.mendeley.com/oauth/authorize?client_id=5907&redirect_uri=https%3A%2F%2Fzotero-static.s3.amazonaws.com%2Fmendeley_oauth_redirect.html&response_type=token&state=&scope=all'; - + const uri = `https://api.mendeley.com/oauth/authorize?client_id=5907&redirect_uri=https%3A%2F%2Fzotero-static.s3.amazonaws.com%2Fmendeley_oauth_redirect.html&response_type=code&state=&scope=all`; var win = Services.wm.getMostRecentWindow("zotero:basicViewer"); if (win) { win.loadURI(uri); diff --git a/chrome/content/zotero/import/importWizard.js b/chrome/content/zotero/import/importWizard.js index 59477ee2d0..69db786c6c 100644 --- a/chrome/content/zotero/import/importWizard.js +++ b/chrome/content/zotero/import/importWizard.js @@ -6,7 +6,7 @@ var Zotero_Import_Wizard = { _file: null, _translation: null, _mendeleyOnlineRedirectURLWithCode: null, - _mendeleyAccessToken: null, + _mendeleyCode: null, init: async function () { @@ -34,8 +34,8 @@ var Zotero_Import_Wizard = { } } - if (args && args.mendeleyAccessToken) { - this._mendeleyAccessToken = args.mendeleyAccessToken; + if (args && args.mendeleyCode) { + this._mendeleyCode = args.mendeleyCode; this._wizard.goTo('page-options'); } @@ -195,7 +195,7 @@ var Zotero_Import_Wizard = { onOptionsShown: function () { - document.getElementById('file-handling-options').hidden = !!this._mendeleyAccessToken; + document.getElementById('file-handling-options').hidden = !!this._mendeleyCode; }, @@ -222,7 +222,7 @@ var Zotero_Import_Wizard = { onImportStart: async function () { - if (!this._file && !this._mendeleyAccessToken) { + if (!this._file && !this._mendeleyCode) { let index = document.getElementById('file-list').selectedIndex; this._file = this._dbs[index].path; } @@ -237,7 +237,7 @@ var Zotero_Import_Wizard = { addToLibraryRoot: !document.getElementById('create-collection-checkbox') .hasAttribute('checked'), linkFiles: document.getElementById('file-handling-radio').selectedIndex == 1, - mendeleyOnlineToken: this._mendeleyAccessToken + mendeleyCode: this._mendeleyCode }); // Cancelled by user or due to error diff --git a/chrome/content/zotero/import/mendeley/mendeleyAPIUtils.js b/chrome/content/zotero/import/mendeley/mendeleyAPIUtils.js index d2ef0da6f3..bc3557738b 100644 --- a/chrome/content/zotero/import/mendeley/mendeleyAPIUtils.js +++ b/chrome/content/zotero/import/mendeley/mendeleyAPIUtils.js @@ -1,7 +1,22 @@ // eslint-disable-next-line no-unused-vars var mendeleyAPIUtils = (function () { +const OAUTH_URL = 'https://www.zotero.org/utils/mendeley/oauth'; const MENDELEY_API_URL = 'https://api.mendeley.com'; +const getTokens = async (codeOrRefreshToken, isRefresh = false) => { + 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); + + return JSON.parse(response.responseText); +}; + const getNextLinkFromResponse = (response) => { let next = null; let links = response.getResponseHeader('link'); @@ -15,30 +30,48 @@ const getNextLinkFromResponse = (response) => { return next; }; -const apiFetchUrl = async (token, url, headers = {}, options = {}) => { - headers = { ...headers, Authorization: `Bearer ${token}` }; - return Zotero.HTTP.request('GET', url, { ...options, headers }); + +const apiFetchUrl = async (tokens, url, headers = {}, options = {}) => { + headers = { ...headers, Authorization: `Bearer ${tokens.access_token}` }; + options = { ...options, headers }; + const method = 'GET'; + + // Run the request. If we see 401 or 403, try to refresh tokens and run the request again + try { + return await Zotero.HTTP.request(method, url, options); + } + catch (e) { + if (e.status === 401 || e.status === 403) { + const newTokens = await getTokens(tokens.refresh_token, true); + // 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 + headers.Authorization = `Bearer ${tokens.access_token}`; + } + } + + return Zotero.HTTP.request(method, url, options); }; -const apiFetch = async (token, endPoint, params = {}, headers = {}, options = {}) => { +const apiFetch = async (tokens, endPoint, params = {}, headers = {}, options = {}) => { const stringParams = Object.entries(params).map(p => p.join('=')).join('&'); const url = MENDELEY_API_URL + '/' + endPoint + '?' + stringParams; - return apiFetchUrl(token, url, headers, options); + return apiFetchUrl(tokens, url, headers, options); }; -const get = async (token, endPoint, params = {}, headers = {}, options = {}) => { - const response = await apiFetch(token, endPoint, params, headers, options); +const get = async (tokens, endPoint, params = {}, headers = {}, options = {}) => { + const response = await apiFetch(tokens, endPoint, params, headers, options); return JSON.parse(response.responseText); }; -const getAll = async (token, endPoint, params = {}, headers = {}, options = {}) => { +const getAll = async (tokens, endPoint, params = {}, headers = {}, options = {}) => { const PER_PAGE = endPoint === 'annotations' ? 200 : 500; - const response = await apiFetch(token, endPoint, { ...params, limit: PER_PAGE }, headers, options); + const response = await apiFetch(tokens, endPoint, { ...params, limit: PER_PAGE }, headers, options); var next = getNextLinkFromResponse(response); var data = JSON.parse(response.responseText); while (next) { - const response = await apiFetchUrl(token, next, headers, options); //eslint-disable-line no-await-in-loop + const response = await apiFetchUrl(tokens, next, headers, options); //eslint-disable-line no-await-in-loop data = [...data, ...JSON.parse(response.responseText)]; next = getNextLinkFromResponse(response); } @@ -47,5 +80,5 @@ const getAll = async (token, endPoint, params = {}, headers = {}, options = {}) }; -return { getNextLinkFromResponse, apiFetch, apiFetchUrl, get, getAll }; +return { getNextLinkFromResponse, getTokens, apiFetch, apiFetchUrl, get, getAll }; })(); diff --git a/chrome/content/zotero/import/mendeley/mendeleyImport.js b/chrome/content/zotero/import/mendeley/mendeleyImport.js index 4a896f2e74..fb79a101e9 100644 --- a/chrome/content/zotero/import/mendeley/mendeleyImport.js +++ b/chrome/content/zotero/import/mendeley/mendeleyImport.js @@ -8,14 +8,15 @@ Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/men Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleyAPIUtils.js"); const { apiTypeToDBType, apiFieldToDBField } = mendeleyOnlineMappings; -const { apiFetch, get, getAll } = mendeleyAPIUtils; +const { apiFetch, get, getAll, getTokens } = mendeleyAPIUtils; var Zotero_Import_Mendeley = function () { this.createNewCollection = null; this.linkFiles = null; this.newItems = []; - this.token = null; + this.mendeleyCode = null; + this._tokens = null; this._db; this._file; this._saveOptions = null; @@ -100,11 +101,15 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) { throw new Error("Not a valid Mendeley database"); } - if (!this._file && !this.token) { + if (this.mendeleyCode) { + this._tokens = await getTokens(this.mendeleyCode); + } + + if (!this._file && !this._tokens) { throw new Error("Missing import token"); } - const folders = this.token + const folders = this._tokens ? await this._getFoldersAPI(mendeleyGroupID) : await this._getFoldersDB(mendeleyGroupID); @@ -115,41 +120,41 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) { // // Items // - let documents = this.token + let documents = this._tokens ? await this._getDocumentsAPI(mendeleyGroupID) : await this._getDocumentsDB(mendeleyGroupID); this._progressMax = documents.length; // Get various attributes mapped to document ids - let urls = this.token + let urls = this._tokens ? await this._getDocumentURLsAPI(documents) : await this._getDocumentURLsDB(mendeleyGroupID); - let creators = this.token + let creators = this._tokens ? await this._getDocumentCreatorsAPI(documents) : await this._getDocumentCreatorsDB(mendeleyGroupID, map.creatorTypes); - let tags = this.token + let tags = this._tokens ? await this._getDocumentTagsAPI(documents) : await this._getDocumentTagsDB(mendeleyGroupID); - let collections = this.token + let collections = this._tokens ? await this._getDocumentCollectionsAPI(documents, rootCollectionKey, folderKeys) : await this._getDocumentCollectionsDB(mendeleyGroupID, documents, rootCollectionKey, folderKeys); - let files = this.token + let files = this._tokens ? await this._getDocumentFilesAPI(documents) : await this._getDocumentFilesDB(mendeleyGroupID); - let annotations = this.token + let annotations = this._tokens ? await this._getDocumentAnnotationsAPI(mendeleyGroupID) : await this._getDocumentAnnotationsDB(mendeleyGroupID); - let profile = this.token + let profile = this._tokens ? await this._getProfileAPI() : await this._getProfileDB(); - let groups = this.token + let groups = this._tokens ? await this._getGroupsAPI() : await this._getGroupsDB(); @@ -163,7 +168,7 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) { for (let group of groups) { - let groupAnnotations = this.token + let groupAnnotations = this._tokens ? await this._getDocumentAnnotationsAPI(group.id, profile.id) : await this._getDocumentAnnotationsDB(group.id, profile.id); @@ -184,7 +189,7 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) { let docURLs = urls.get(document.id); let docFiles = files.get(document.id); - if (this.token) { + if (this._tokens) { // extract identifiers ['arxiv', 'doi', 'isbn', 'issn', 'pmid', 'scopus', 'pui', 'pii', 'sgr'].forEach( i => document[i] = (document.identifiers || {})[i] @@ -270,7 +275,7 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) { if (this._file) { await this._db.closeDatabase(); } - if (this.token) { + if (this._tokens) { await Promise.all( this._tmpFilesToDelete.map(f => this._removeTemporaryFile(f)) ); @@ -341,8 +346,7 @@ Zotero_Import_Mendeley.prototype._getFoldersAPI = async function (groupID) { if (groupID && groupID !== 0) { params.group_id = groupID; //eslint-disable-line camelcase } - - return (await getAll(this.token, 'folders', params, headers)).map(f => ({ + return (await getAll(this._tokens, 'folders', params, headers)).map(f => ({ id: f.id, uuid: f.id, name: f.name, @@ -489,7 +493,7 @@ Zotero_Import_Mendeley.prototype._getDocumentsAPI = async function (groupID) { } - return (await getAll(this.token, 'documents', params, headers)).map(d => ({ + return (await getAll(this._tokens, 'documents', params, headers)).map(d => ({ ...d, uuid: d.id, remoteUuid: d.id @@ -677,7 +681,7 @@ Zotero_Import_Mendeley.prototype._getDocumentFilesDB = async function (groupID) Zotero_Import_Mendeley.prototype._fetchFile = async function (fileID, filePath) { const fileDir = OS.Path.dirname(filePath); await Zotero.File.createDirectoryIfMissingAsync(fileDir); - const xhr = await apiFetch(this.token, `files/${fileID}`, {}, {}, { responseType: 'blob', followRedirects: false }); + const xhr = await apiFetch(this._tokens, `files/${fileID}`, {}, {}, { responseType: 'blob', followRedirects: false }); const uri = xhr.getResponseHeader('location'); await Zotero.File.download(uri, filePath); @@ -815,7 +819,7 @@ Zotero_Import_Mendeley.prototype._getDocumentAnnotationsAPI = async function (gr } const map = new Map(); - (await getAll(this.token, 'annotations', params, { Accept: 'application/vnd.mendeley-annotation.1+json' })) + (await getAll(this._tokens, 'annotations', params, { Accept: 'application/vnd.mendeley-annotation.1+json' })) .forEach((a) => { if (profileID !== null && a.profile_id !== profileID) { // optionally filter annotations by profile id @@ -875,7 +879,7 @@ Zotero_Import_Mendeley.prototype._getGroupsAPI = async function () { const params = { type: 'all' }; const headers = { Accept: 'application/vnd.mendeley-group-list+json' }; - return getAll(this.token, 'groups/v2', params, headers); + return getAll(this._tokens, 'groups/v2', params, headers); }; Zotero_Import_Mendeley.prototype._getGroupsDB = async function () { @@ -890,7 +894,7 @@ Zotero_Import_Mendeley.prototype._getProfileAPI = async function () { const params = { }; const headers = { Accept: 'application/vnd.mendeley-profiles.2+json' }; - return get(this.token, 'profiles/v2/me', params, headers); + return get(this._tokens, 'profiles/v2/me', params, headers); }; Zotero_Import_Mendeley.prototype._getProfileDB = async function () {