Mendeley import: Switch to authorisation code flow via proxy
This commit is contained in:
parent
d3a78982f0
commit
833ee10516
4 changed files with 83 additions and 47 deletions
|
@ -332,10 +332,10 @@ var Zotero_File_Interface = new function() {
|
||||||
|
|
||||||
var translation;
|
var translation;
|
||||||
|
|
||||||
if (options.mendeleyOnlineToken) {
|
if (options.mendeleyCode) {
|
||||||
translation = yield _getMendeleyTranslation();
|
translation = yield _getMendeleyTranslation();
|
||||||
translation.createNewCollection = createNewCollection;
|
translation.createNewCollection = createNewCollection;
|
||||||
translation.token = options.mendeleyOnlineToken;
|
translation.mendeleyCode = options.mendeleyCode;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Check if the file is an SQLite database
|
// Check if the file is an SQLite database
|
||||||
|
@ -863,10 +863,10 @@ var Zotero_File_Interface = new function() {
|
||||||
|
|
||||||
this.authenticateMendeleyOnlinePoll = function (win) {
|
this.authenticateMendeleyOnlinePoll = function (win) {
|
||||||
if (win && win[0] && win[0].location) {
|
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) {
|
if (matchResult) {
|
||||||
const mendeleyAccessToken = matchResult[1];
|
const mendeleyCode = matchResult[1];
|
||||||
Zotero.getMainWindow().setTimeout(() => this.showImportWizard({ mendeleyAccessToken }), 0);
|
Zotero.getMainWindow().setTimeout(() => this.showImportWizard({ mendeleyCode }), 0);
|
||||||
|
|
||||||
// Clear all cookies to remove access
|
// Clear all cookies to remove access
|
||||||
//
|
//
|
||||||
|
@ -894,8 +894,7 @@ var Zotero_File_Interface = new function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.authenticateMendeleyOnline = 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");
|
var win = Services.wm.getMostRecentWindow("zotero:basicViewer");
|
||||||
if (win) {
|
if (win) {
|
||||||
win.loadURI(uri);
|
win.loadURI(uri);
|
||||||
|
|
|
@ -6,7 +6,7 @@ var Zotero_Import_Wizard = {
|
||||||
_file: null,
|
_file: null,
|
||||||
_translation: null,
|
_translation: null,
|
||||||
_mendeleyOnlineRedirectURLWithCode: null,
|
_mendeleyOnlineRedirectURLWithCode: null,
|
||||||
_mendeleyAccessToken: null,
|
_mendeleyCode: null,
|
||||||
|
|
||||||
|
|
||||||
init: async function () {
|
init: async function () {
|
||||||
|
@ -34,8 +34,8 @@ var Zotero_Import_Wizard = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args && args.mendeleyAccessToken) {
|
if (args && args.mendeleyCode) {
|
||||||
this._mendeleyAccessToken = args.mendeleyAccessToken;
|
this._mendeleyCode = args.mendeleyCode;
|
||||||
this._wizard.goTo('page-options');
|
this._wizard.goTo('page-options');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ var Zotero_Import_Wizard = {
|
||||||
|
|
||||||
|
|
||||||
onOptionsShown: function () {
|
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 () {
|
onImportStart: async function () {
|
||||||
if (!this._file && !this._mendeleyAccessToken) {
|
if (!this._file && !this._mendeleyCode) {
|
||||||
let index = document.getElementById('file-list').selectedIndex;
|
let index = document.getElementById('file-list').selectedIndex;
|
||||||
this._file = this._dbs[index].path;
|
this._file = this._dbs[index].path;
|
||||||
}
|
}
|
||||||
|
@ -237,7 +237,7 @@ var Zotero_Import_Wizard = {
|
||||||
addToLibraryRoot: !document.getElementById('create-collection-checkbox')
|
addToLibraryRoot: !document.getElementById('create-collection-checkbox')
|
||||||
.hasAttribute('checked'),
|
.hasAttribute('checked'),
|
||||||
linkFiles: document.getElementById('file-handling-radio').selectedIndex == 1,
|
linkFiles: document.getElementById('file-handling-radio').selectedIndex == 1,
|
||||||
mendeleyOnlineToken: this._mendeleyAccessToken
|
mendeleyCode: this._mendeleyCode
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cancelled by user or due to error
|
// Cancelled by user or due to error
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
var mendeleyAPIUtils = (function () {
|
var mendeleyAPIUtils = (function () {
|
||||||
|
const OAUTH_URL = 'https://www.zotero.org/utils/mendeley/oauth';
|
||||||
const MENDELEY_API_URL = 'https://api.mendeley.com';
|
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) => {
|
const getNextLinkFromResponse = (response) => {
|
||||||
let next = null;
|
let next = null;
|
||||||
let links = response.getResponseHeader('link');
|
let links = response.getResponseHeader('link');
|
||||||
|
@ -15,30 +30,48 @@ const getNextLinkFromResponse = (response) => {
|
||||||
return next;
|
return next;
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiFetchUrl = async (token, url, headers = {}, options = {}) => {
|
|
||||||
headers = { ...headers, Authorization: `Bearer ${token}` };
|
const apiFetchUrl = async (tokens, url, headers = {}, options = {}) => {
|
||||||
return Zotero.HTTP.request('GET', url, { ...options, headers });
|
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 stringParams = Object.entries(params).map(p => p.join('=')).join('&');
|
||||||
const url = MENDELEY_API_URL + '/' + endPoint + '?' + stringParams;
|
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 get = async (tokens, endPoint, params = {}, headers = {}, options = {}) => {
|
||||||
const response = await apiFetch(token, endPoint, params, headers, options);
|
const response = await apiFetch(tokens, endPoint, params, headers, options);
|
||||||
return JSON.parse(response.responseText);
|
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 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 next = getNextLinkFromResponse(response);
|
||||||
var data = JSON.parse(response.responseText);
|
var data = JSON.parse(response.responseText);
|
||||||
|
|
||||||
while (next) {
|
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)];
|
data = [...data, ...JSON.parse(response.responseText)];
|
||||||
next = getNextLinkFromResponse(response);
|
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 };
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -8,14 +8,15 @@ Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/men
|
||||||
Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleyAPIUtils.js");
|
Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleyAPIUtils.js");
|
||||||
|
|
||||||
const { apiTypeToDBType, apiFieldToDBField } = mendeleyOnlineMappings;
|
const { apiTypeToDBType, apiFieldToDBField } = mendeleyOnlineMappings;
|
||||||
const { apiFetch, get, getAll } = mendeleyAPIUtils;
|
const { apiFetch, get, getAll, getTokens } = mendeleyAPIUtils;
|
||||||
|
|
||||||
var Zotero_Import_Mendeley = function () {
|
var Zotero_Import_Mendeley = function () {
|
||||||
this.createNewCollection = null;
|
this.createNewCollection = null;
|
||||||
this.linkFiles = null;
|
this.linkFiles = null;
|
||||||
this.newItems = [];
|
this.newItems = [];
|
||||||
this.token = null;
|
this.mendeleyCode = null;
|
||||||
|
|
||||||
|
this._tokens = null;
|
||||||
this._db;
|
this._db;
|
||||||
this._file;
|
this._file;
|
||||||
this._saveOptions = null;
|
this._saveOptions = null;
|
||||||
|
@ -100,11 +101,15 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
|
||||||
throw new Error("Not a valid Mendeley database");
|
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");
|
throw new Error("Missing import token");
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = this.token
|
const folders = this._tokens
|
||||||
? await this._getFoldersAPI(mendeleyGroupID)
|
? await this._getFoldersAPI(mendeleyGroupID)
|
||||||
: await this._getFoldersDB(mendeleyGroupID);
|
: await this._getFoldersDB(mendeleyGroupID);
|
||||||
|
|
||||||
|
@ -115,41 +120,41 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
|
||||||
//
|
//
|
||||||
// Items
|
// Items
|
||||||
//
|
//
|
||||||
let documents = this.token
|
let documents = this._tokens
|
||||||
? await this._getDocumentsAPI(mendeleyGroupID)
|
? await this._getDocumentsAPI(mendeleyGroupID)
|
||||||
: await this._getDocumentsDB(mendeleyGroupID);
|
: await this._getDocumentsDB(mendeleyGroupID);
|
||||||
|
|
||||||
this._progressMax = documents.length;
|
this._progressMax = documents.length;
|
||||||
// Get various attributes mapped to document ids
|
// Get various attributes mapped to document ids
|
||||||
let urls = this.token
|
let urls = this._tokens
|
||||||
? await this._getDocumentURLsAPI(documents)
|
? await this._getDocumentURLsAPI(documents)
|
||||||
: await this._getDocumentURLsDB(mendeleyGroupID);
|
: await this._getDocumentURLsDB(mendeleyGroupID);
|
||||||
|
|
||||||
let creators = this.token
|
let creators = this._tokens
|
||||||
? await this._getDocumentCreatorsAPI(documents)
|
? await this._getDocumentCreatorsAPI(documents)
|
||||||
: await this._getDocumentCreatorsDB(mendeleyGroupID, map.creatorTypes);
|
: await this._getDocumentCreatorsDB(mendeleyGroupID, map.creatorTypes);
|
||||||
|
|
||||||
let tags = this.token
|
let tags = this._tokens
|
||||||
? await this._getDocumentTagsAPI(documents)
|
? await this._getDocumentTagsAPI(documents)
|
||||||
: await this._getDocumentTagsDB(mendeleyGroupID);
|
: await this._getDocumentTagsDB(mendeleyGroupID);
|
||||||
|
|
||||||
let collections = this.token
|
let collections = this._tokens
|
||||||
? await this._getDocumentCollectionsAPI(documents, rootCollectionKey, folderKeys)
|
? await this._getDocumentCollectionsAPI(documents, rootCollectionKey, folderKeys)
|
||||||
: await this._getDocumentCollectionsDB(mendeleyGroupID, documents, rootCollectionKey, folderKeys);
|
: await this._getDocumentCollectionsDB(mendeleyGroupID, documents, rootCollectionKey, folderKeys);
|
||||||
|
|
||||||
let files = this.token
|
let files = this._tokens
|
||||||
? await this._getDocumentFilesAPI(documents)
|
? await this._getDocumentFilesAPI(documents)
|
||||||
: await this._getDocumentFilesDB(mendeleyGroupID);
|
: await this._getDocumentFilesDB(mendeleyGroupID);
|
||||||
|
|
||||||
let annotations = this.token
|
let annotations = this._tokens
|
||||||
? await this._getDocumentAnnotationsAPI(mendeleyGroupID)
|
? await this._getDocumentAnnotationsAPI(mendeleyGroupID)
|
||||||
: await this._getDocumentAnnotationsDB(mendeleyGroupID);
|
: await this._getDocumentAnnotationsDB(mendeleyGroupID);
|
||||||
|
|
||||||
let profile = this.token
|
let profile = this._tokens
|
||||||
? await this._getProfileAPI()
|
? await this._getProfileAPI()
|
||||||
: await this._getProfileDB();
|
: await this._getProfileDB();
|
||||||
|
|
||||||
let groups = this.token
|
let groups = this._tokens
|
||||||
? await this._getGroupsAPI()
|
? await this._getGroupsAPI()
|
||||||
: await this._getGroupsDB();
|
: await this._getGroupsDB();
|
||||||
|
|
||||||
|
@ -163,7 +168,7 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
|
||||||
|
|
||||||
|
|
||||||
for (let group of groups) {
|
for (let group of groups) {
|
||||||
let groupAnnotations = this.token
|
let groupAnnotations = this._tokens
|
||||||
? await this._getDocumentAnnotationsAPI(group.id, profile.id)
|
? await this._getDocumentAnnotationsAPI(group.id, profile.id)
|
||||||
: await this._getDocumentAnnotationsDB(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 docURLs = urls.get(document.id);
|
||||||
let docFiles = files.get(document.id);
|
let docFiles = files.get(document.id);
|
||||||
|
|
||||||
if (this.token) {
|
if (this._tokens) {
|
||||||
// extract identifiers
|
// extract identifiers
|
||||||
['arxiv', 'doi', 'isbn', 'issn', 'pmid', 'scopus', 'pui', 'pii', 'sgr'].forEach(
|
['arxiv', 'doi', 'isbn', 'issn', 'pmid', 'scopus', 'pui', 'pii', 'sgr'].forEach(
|
||||||
i => document[i] = (document.identifiers || {})[i]
|
i => document[i] = (document.identifiers || {})[i]
|
||||||
|
@ -270,7 +275,7 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
|
||||||
if (this._file) {
|
if (this._file) {
|
||||||
await this._db.closeDatabase();
|
await this._db.closeDatabase();
|
||||||
}
|
}
|
||||||
if (this.token) {
|
if (this._tokens) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this._tmpFilesToDelete.map(f => this._removeTemporaryFile(f))
|
this._tmpFilesToDelete.map(f => this._removeTemporaryFile(f))
|
||||||
);
|
);
|
||||||
|
@ -341,8 +346,7 @@ Zotero_Import_Mendeley.prototype._getFoldersAPI = async function (groupID) {
|
||||||
if (groupID && groupID !== 0) {
|
if (groupID && groupID !== 0) {
|
||||||
params.group_id = groupID; //eslint-disable-line camelcase
|
params.group_id = groupID; //eslint-disable-line camelcase
|
||||||
}
|
}
|
||||||
|
return (await getAll(this._tokens, 'folders', params, headers)).map(f => ({
|
||||||
return (await getAll(this.token, 'folders', params, headers)).map(f => ({
|
|
||||||
id: f.id,
|
id: f.id,
|
||||||
uuid: f.id,
|
uuid: f.id,
|
||||||
name: f.name,
|
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,
|
...d,
|
||||||
uuid: d.id,
|
uuid: d.id,
|
||||||
remoteUuid: 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) {
|
Zotero_Import_Mendeley.prototype._fetchFile = async function (fileID, filePath) {
|
||||||
const fileDir = OS.Path.dirname(filePath);
|
const fileDir = OS.Path.dirname(filePath);
|
||||||
await Zotero.File.createDirectoryIfMissingAsync(fileDir);
|
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');
|
const uri = xhr.getResponseHeader('location');
|
||||||
await Zotero.File.download(uri, filePath);
|
await Zotero.File.download(uri, filePath);
|
||||||
|
|
||||||
|
@ -815,7 +819,7 @@ Zotero_Import_Mendeley.prototype._getDocumentAnnotationsAPI = async function (gr
|
||||||
}
|
}
|
||||||
|
|
||||||
const map = new Map();
|
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) => {
|
.forEach((a) => {
|
||||||
if (profileID !== null && a.profile_id !== profileID) {
|
if (profileID !== null && a.profile_id !== profileID) {
|
||||||
// optionally filter annotations by profile id
|
// optionally filter annotations by profile id
|
||||||
|
@ -875,7 +879,7 @@ Zotero_Import_Mendeley.prototype._getGroupsAPI = async function () {
|
||||||
const params = { type: 'all' };
|
const params = { type: 'all' };
|
||||||
const headers = { Accept: 'application/vnd.mendeley-group-list+json' };
|
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 () {
|
Zotero_Import_Mendeley.prototype._getGroupsDB = async function () {
|
||||||
|
@ -890,7 +894,7 @@ Zotero_Import_Mendeley.prototype._getProfileAPI = async function () {
|
||||||
const params = { };
|
const params = { };
|
||||||
const headers = { Accept: 'application/vnd.mendeley-profiles.2+json' };
|
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 () {
|
Zotero_Import_Mendeley.prototype._getProfileDB = async function () {
|
||||||
|
|
Loading…
Reference in a new issue