/** * * (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; };