/**
*
* (c) Copyright Ascensio System SIA 2020
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// @ts-check
///
///
/**
* @typedef {Object} ZoteroGroupInfo
* @property {number} id
* @property {number} version
* @property {{alternate: {href: string, type: string}, self: {href: string, type: string}}} meta
* @property {{created: string, lastModified: string, numItems: number}} links
* @property {{name: string, description: string, id: number, owner: number, type: string}} data
*/
/**
* @typedef {Object} UserGroupInfo
* @property {number} id
* @property {string} name
*/
const ZoteroSdk = function () {
this._apiKey = null;
this._userId = 0;
/** @type {Array}} */
this._userGroups = [];
this._isOnlineAvailable = true;
};
// Constants
ZoteroSdk.prototype.ZOTERO_API_VERSION = "3";
ZoteroSdk.prototype.USER_AGENT = "AscDesktopEditor";
ZoteroSdk.prototype.DEFAULT_FORMAT = "csljson";
ZoteroSdk.prototype.STORAGE_KEYS = {
USER_ID: "zoteroUserId",
API_KEY: "zoteroApiKey",
};
ZoteroSdk.prototype.API_PATHS = {
USERS: "users",
GROUPS: "groups",
ITEMS: "items",
KEYS: "keys",
};
/**
* Get appropriate base URL based on online/offline mode
*/
ZoteroSdk.prototype._getBaseUrl = function () {
return this._isOnlineAvailable
? zoteroEnvironment.restApiUrl
: zoteroEnvironment.desktopApiUrl;
};
/**
* Get locales URL based on online/offline mode
*/
ZoteroSdk.prototype._getLocalesUrl = function () {
return this._isOnlineAvailable
? zoteroEnvironment.localesUrl
: zoteroEnvironment.localesPath;
};
/**
* Make a GET request to the local Zotero API (offline mode)
* @param {string} url
* @returns {Promise}
*/
ZoteroSdk.prototype._getDesktopRequest = function (url) {
var self = this;
return new Promise(function (resolve, reject) {
window.AscSimpleRequest.createRequest({
url: url,
method: "GET",
headers: {
"Zotero-API-Version": self.ZOTERO_API_VERSION,
"User-Agent": self.USER_AGENT,
},
complete: resolve,
error: function (/** @type {AscSimpleResponse} */ error) {
if (error.statusCode === -102) {
error.statusCode = 404;
error.message =
"Connection to Zotero failed. Make sure Zotero is running";
}
reject(error);
},
});
});
};
/**
* Make a GET request to the online Zotero API
* @param {URL} url
* @returns {Promise}
*/
ZoteroSdk.prototype._getOnlineRequest = function (url) {
var self = this;
var headers = {
"Zotero-API-Version": self.ZOTERO_API_VERSION,
"Zotero-API-Key": self._apiKey || "",
};
return fetch(url, { headers: headers })
.then(function (response) {
if (!response.ok) {
throw new Error(response.status + " " + response.statusText);
}
return response;
})
.catch(function (error) {
console.error("Zotero API request failed:", error.message);
if (typeof error === "object") {
error.message = "Connection to Zotero failed";
}
throw error;
});
};
/**
* Universal request handler with offline support
* @param {URL} url
* @returns {Promise}
*/
ZoteroSdk.prototype._getRequestWithOfflineSupport = function (url) {
return this._isOnlineAvailable
? this._getOnlineRequest(url)
: this._getDesktopRequest(url.href);
};
/**
* Build URL and make GET request
* @param {string} path
* @param {*} [queryParams]
* @returns {Promise}
*/
ZoteroSdk.prototype._buildGetRequest = function (path, queryParams) {
queryParams = queryParams || {};
var url = new URL(path, this._getBaseUrl());
Object.keys(queryParams).forEach(function (key) {
if (queryParams[key] !== undefined && queryParams[key] !== null) {
url.searchParams.append(key, queryParams[key]);
}
});
return this._getRequestWithOfflineSupport(url);
};
/**
* Parse link header for pagination
* @param {string} headerValue
* @returns {{[key: string]: string}}
*/
ZoteroSdk.prototype._parseLinkHeader = function (headerValue) {
/** @type {{[key: string]: string}} */
var links = {};
var linkHeaderRegex = /<(.*?)>; rel="(.*?)"/g;
if (!headerValue) return links;
var match;
while ((match = linkHeaderRegex.exec(headerValue.trim())) !== null) {
links[match[2]] = match[1];
}
return links;
};
/**
* Parse response for desktop (offline) mode
* @param {Promise} promise
* @param {function(any): void} resolve
* @param {function(any): void} reject
* @param {number} id
* @returns {Promise}
*/
ZoteroSdk.prototype._parseDesktopItemsResponse = function (
promise,
resolve,
reject,
id
) {
var self = this;
return promise
.then(function (response) {
return {
items: { items: JSON.parse(response.responseText) },
id: id,
};
})
.then(resolve)
.catch(reject);
};
/**
* Parse response for online mode with pagination support
* @param {Promise} promise
* @param {function(any): void} resolve
* @param {function(any): void} reject
* @param {number} id
* @returns {Promise}
*/
ZoteroSdk.prototype._parseItemsResponse = function (
promise,
resolve,
reject,
id
) {
var self = this;
return promise
.then(function (response) {
return Promise.all([response.json(), response]);
})
.then(function (results) {
var json = results[0];
var response = results[1];
var links = self._parseLinkHeader(
response.headers.get("Link") || ""
);
/** @type {{items: any, id: number, next?: function(): Promise}} */
var result = {
items: json,
id: id,
};
if (links.next) {
result.next = function () {
return new Promise(function (rs, rj) {
self._parseItemsResponse(
self._getOnlineRequest(new URL(links.next)),
rs,
rj,
id
);
});
};
}
resolve(result);
})
.catch(reject);
};
/**
* Universal items response parser
* @param {Promise} promise
* @param {function(any): void} resolve
* @param {function(any): void} reject
* @param {number} id
*/
ZoteroSdk.prototype._parseResponse = function (promise, resolve, reject, id) {
if (this._isOnlineAvailable) {
const fetchPromise = /** @type {Promise} */ (promise);
this._parseItemsResponse(fetchPromise, resolve, reject, id);
} else {
const ascSimplePromise = /** @type {Promise} */ (
promise
);
this._parseDesktopItemsResponse(
/** @type {Promise} */ ascSimplePromise,
resolve,
reject,
id
);
}
};
/**
* Get items from user library
* @param {string} search
* @param {string[]} itemsID
* @param {"csljson"|"json"} format
*/
ZoteroSdk.prototype.getItems = function (search, itemsID, format) {
var self = this;
format = format || self.DEFAULT_FORMAT;
return new Promise(function (resolve, reject) {
var queryParams =
/** @type {{format: string, q?: string, itemKey?: string}} */ ({
format: format,
});
if (search) {
queryParams.q = search;
} else if (itemsID) {
queryParams.itemKey = itemsID.join(",");
}
var path =
self.API_PATHS.USERS +
"/" +
self._userId +
"/" +
self.API_PATHS.ITEMS;
var request = self._buildGetRequest(path, queryParams);
self._parseResponse(request, resolve, reject, self._userId);
});
};
/**
* Get items from group library
* @param {string} search
* @param {number} groupId
* @param {string[]} itemsID
* @param {"csljson"|"json"} format
*
*/
ZoteroSdk.prototype.getGroupItems = function (
search,
groupId,
itemsID,
format
) {
var self = this;
format = format || self.DEFAULT_FORMAT;
return new Promise(function (resolve, reject) {
var queryParams =
/** @type {{format: string, q?: string, itemKey?: string}} */ ({
format: format,
});
if (search) {
queryParams.q = search;
} else if (itemsID) {
queryParams.itemKey = itemsID.join(",");
}
var path =
self.API_PATHS.GROUPS + "/" + groupId + "/" + self.API_PATHS.ITEMS;
var request = self._buildGetRequest(path, queryParams);
self._parseResponse(request, resolve, reject, groupId);
});
};
/**
* Get user groups
* @returns {Promise>}
*/
ZoteroSdk.prototype.getUserGroups = function () {
var self = this;
return new Promise(function (resolve, reject) {
if (self._userGroups.length > 0) {
resolve(self._userGroups);
return;
}
var path = self.API_PATHS.USERS + "/" + self._userId + "/groups";
self._buildGetRequest(path)
.then(function (
/** @type {FetchResponse | AscSimpleResponse} */ response
) {
if (self._isOnlineAvailable) {
var fetchResponse = /** @type {FetchResponse} */ (response);
if (!fetchResponse.ok) {
throw new Error(
fetchResponse.status +
" " +
fetchResponse.statusText
);
}
return fetchResponse.json();
}
var ascSimpleResponse = /** @type {AscSimpleResponse} */ (
response
);
return JSON.parse(ascSimpleResponse.responseText);
})
.then(function (groups) {
self._userGroups = groups.map(function (
/** @type {ZoteroGroupInfo} */ group
) {
return {
id: group.id,
name: group.data.name,
};
});
resolve(self._userGroups);
})
.catch(reject);
});
};
/**
* Format citations
*/
/*ZoteroSdk.prototype.format = function (ids, groupKey, style, locale) {
var queryParams = {
format: "bib",
style: style,
locale: locale,
itemKey: ids.join(","),
};
var path = groupKey
? this.API_PATHS.GROUPS + "/" + groupKey + "/" + this.API_PATHS.ITEMS
: this.API_PATHS.USERS +
"/" +
this._userId +
"/" +
this.API_PATHS.ITEMS;
return this._buildGetRequest(path, queryParams).then(function (response) {
return response.text();
});
};*/
/**
* Get locale data
* @param {string} langTag
*/
ZoteroSdk.prototype.getLocale = function (langTag) {
var url = this._getLocalesUrl() + "locales-" + langTag + ".xml";
return fetch(url).then(function (response) {
return response.text();
});
};
/**
* Set API key and validate it
* @param {string} key
*/
ZoteroSdk.prototype.setApiKey = function (key) {
var self = this;
var path = this.API_PATHS.KEYS + "/" + key;
return this._buildGetRequest(path)
.then(function (response) {
var fetchResponse = /** @type {FetchResponse} */ (response);
if (!fetchResponse.ok) {
throw new Error(
fetchResponse.status + " " + fetchResponse.statusText
);
}
return fetchResponse.json();
})
.then(function (/** @type {any} */ keyData) {
self._saveSettings(keyData.userID, key);
return true;
});
};
/**
* Apply settings to current session
* @param {number} userId
* @param {string} apiKey
*/
ZoteroSdk.prototype._applySettings = function (userId, apiKey) {
this._userId = userId;
this._apiKey = apiKey;
};
/**
* Save settings to localStorage
* @param {number} userId
* @param {string} apiKey
*/
ZoteroSdk.prototype._saveSettings = function (userId, apiKey) {
this._applySettings(userId, apiKey);
localStorage.setItem(this.STORAGE_KEYS.USER_ID, String(userId));
localStorage.setItem(this.STORAGE_KEYS.API_KEY, apiKey);
};
/**
* Load settings from localStorage
*/
ZoteroSdk.prototype.hasSettings = function () {
var userId = localStorage.getItem(this.STORAGE_KEYS.USER_ID);
var apiKey = localStorage.getItem(this.STORAGE_KEYS.API_KEY);
if (userId && apiKey) {
this._applySettings(Number(userId), apiKey);
return true;
}
return false;
};
/**
* Clear stored settings
*/
ZoteroSdk.prototype.clearSettings = function () {
localStorage.removeItem(this.STORAGE_KEYS.USER_ID);
localStorage.removeItem(this.STORAGE_KEYS.API_KEY);
this._userGroups = [];
this._userId = 0;
this._apiKey = null;
};
/**
* Get user ID
*/
ZoteroSdk.prototype.getUserId = function () {
return this._userId;
};
/**
* Set online availability
* @param {boolean} isOnline
*/
ZoteroSdk.prototype.setIsOnlineAvailable = function (isOnline) {
this._isOnlineAvailable = isOnline;
};