syncAPIClient.js (23897B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2014 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 if (!Zotero.Sync) { 27 Zotero.Sync = {}; 28 } 29 30 Zotero.Sync.APIClient = function (options) { 31 if (!options.baseURL) throw new Error("baseURL not set"); 32 if (!options.apiVersion) throw new Error("apiVersion not set"); 33 if (!options.caller) throw new Error("caller not set"); 34 35 this.baseURL = options.baseURL; 36 this.apiVersion = options.apiVersion; 37 this.apiKey = options.apiKey; 38 this.caller = options.caller; 39 this.debugUploadPolicy = Zotero.Prefs.get('sync.debugUploadPolicy'); 40 41 this.failureDelayIntervals = [2500, 5000, 10000, 20000, 40000, 60000, 120000, 240000, 300000]; 42 this.failureDelayMax = 60 * 60 * 1000; // 1 hour 43 this.rateDelayIntervals = [30, 60, 300]; 44 this.rateDelayPosition = 0; 45 } 46 47 Zotero.Sync.APIClient.prototype = { 48 MAX_OBJECTS_PER_REQUEST: 100, 49 MIN_GZIP_SIZE: 1000, 50 51 52 getKeyInfo: Zotero.Promise.coroutine(function* (options={}) { 53 var uri = this.baseURL + "keys/current"; 54 let opts = {}; 55 Object.assign(opts, options); 56 opts.successCodes = [200, 403, 404]; 57 var xmlhttp = yield this.makeRequest("GET", uri, opts); 58 if (xmlhttp.status == 403) { 59 return false; 60 } 61 var json = this._parseJSON(xmlhttp.responseText); 62 delete json.key; 63 return json; 64 }), 65 66 67 /** 68 * Get group metadata versions 69 * 70 * Note: This is the version for group metadata, not library data. 71 */ 72 getGroupVersions: Zotero.Promise.coroutine(function* (userID) { 73 if (!userID) throw new Error("User ID not provided"); 74 75 var uri = this.baseURL + "users/" + userID + "/groups?format=versions"; 76 var xmlhttp = yield this.makeRequest("GET", uri); 77 return this._parseJSON(xmlhttp.responseText); 78 }), 79 80 /** 81 * Get group metadata for userID 82 * 83 * @param {Integer} userID 84 * @return {Object} - Group metadata response 85 */ 86 getGroups: Zotero.Promise.coroutine(function* (userID) { 87 if (!userID) throw new Error("User ID not provided"); 88 89 var uri = this.baseURL + "users/" + userID + "/groups"; 90 return yield this.getPaginatedResults( 91 uri, 92 (previous, xmlhttp, restart) => [...previous, ...this._parseJSON(xmlhttp.responseText)], 93 [] 94 ); 95 }), 96 97 98 /** 99 * @param {Integer} groupID 100 * @return {Object|false} - Group metadata response, or false if group not found 101 */ 102 getGroup: Zotero.Promise.coroutine(function* (groupID) { 103 if (!groupID) throw new Error("Group ID not provided"); 104 105 var uri = this.baseURL + "groups/" + groupID; 106 var xmlhttp = yield this.makeRequest("GET", uri, { successCodes: [200, 404] }); 107 if (xmlhttp.status == 404) { 108 return false; 109 } 110 return this._parseJSON(xmlhttp.responseText); 111 }), 112 113 114 getSettings: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, since) { 115 var params = { 116 libraryType: libraryType, 117 libraryTypeID: libraryTypeID, 118 target: "settings" 119 }; 120 if (since) { 121 params.since = since; 122 } 123 var uri = this.buildRequestURI(params); 124 var options = { 125 successCodes: [200, 304] 126 }; 127 if (since) { 128 options.headers = { 129 "If-Modified-Since-Version": since 130 }; 131 } 132 var xmlhttp = yield this.makeRequest("GET", uri, options); 133 if (xmlhttp.status == 304) { 134 return false; 135 } 136 return { 137 libraryVersion: this._getLastModifiedVersion(xmlhttp), 138 settings: this._parseJSON(xmlhttp.responseText) 139 }; 140 }), 141 142 143 /** 144 * @return {Object|false} - An object with 'libraryVersion' and a 'deleted' object, or 145 * false if 'since' is earlier than the beginning of the delete log 146 */ 147 getDeleted: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, since) { 148 var params = { 149 target: "deleted", 150 libraryType: libraryType, 151 libraryTypeID: libraryTypeID, 152 since: since || 0 153 }; 154 var uri = this.buildRequestURI(params); 155 var xmlhttp = yield this.makeRequest("GET", uri, { successCodes: [200, 409] }); 156 if (xmlhttp.status == 409) { 157 Zotero.debug(`'since' value '${since}' is earlier than the beginning of the delete log`); 158 return false; 159 } 160 return { 161 libraryVersion: this._getLastModifiedVersion(xmlhttp), 162 deleted: this._parseJSON(xmlhttp.responseText) 163 }; 164 }), 165 166 167 getKeys: async function (libraryType, libraryTypeID, queryParams) { 168 var params = { 169 libraryType: libraryType, 170 libraryTypeID: libraryTypeID, 171 format: 'keys' 172 }; 173 if (queryParams) { 174 for (let i in queryParams) { 175 params[i] = queryParams[i]; 176 } 177 } 178 179 // TODO: Use pagination 180 var uri = this.buildRequestURI(params); 181 182 var options = { 183 successCodes: [200, 304] 184 }; 185 var xmlhttp = await this.makeRequest("GET", uri, options); 186 if (xmlhttp.status == 304) { 187 return false; 188 } 189 return { 190 libraryVersion: this._getLastModifiedVersion(xmlhttp), 191 keys: xmlhttp.responseText.trim().split(/\n/).filter(key => key) 192 }; 193 }, 194 195 196 /** 197 * Return a promise for a JS object with object keys as keys and version 198 * numbers as values. By default, returns all objects in the library. 199 * Additional parameters (such as 'since', 'sincetime', 'libraryVersion') 200 * can be passed in 'params'. 201 * 202 * @param {String} libraryType 'user' or 'group' 203 * @param {Integer} libraryTypeID userID or groupID 204 * @param {String} objectType 'item', 'collection', 'search' 205 * @param {Object} queryParams Query parameters (see buildRequestURI()) 206 * @return {Promise<Object>|false} - Object with 'libraryVersion' and 'results', or false if 207 * nothing changed since specified library version 208 */ 209 getVersions: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, objectType, queryParams) { 210 var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); 211 212 var params = { 213 target: objectTypePlural, 214 libraryType: libraryType, 215 libraryTypeID: libraryTypeID, 216 format: 'versions' 217 }; 218 if (queryParams) { 219 if (queryParams.top) { 220 params.target += "/top"; 221 delete queryParams.top; 222 } 223 for (let i in queryParams) { 224 params[i] = queryParams[i]; 225 } 226 } 227 if (objectType == 'item') { 228 params.includeTrashed = 1; 229 } 230 231 // TODO: Use pagination 232 var uri = this.buildRequestURI(params); 233 234 var options = { 235 successCodes: [200, 304] 236 }; 237 var xmlhttp = yield this.makeRequest("GET", uri, options); 238 if (xmlhttp.status == 304) { 239 return false; 240 } 241 var libraryVersion = xmlhttp.getResponseHeader('Last-Modified-Version'); 242 if (!libraryVersion) { 243 throw new Error("Last-Modified-Version not provided"); 244 } 245 return { 246 libraryVersion: libraryVersion, 247 versions: this._parseJSON(xmlhttp.responseText) 248 }; 249 }), 250 251 252 /** 253 * Retrieve JSON from API for requested objects 254 * 255 * If necessary, multiple API requests will be made. 256 * 257 * @param {String} libraryType - 'user', 'group' 258 * @param {Integer} libraryTypeID - userID or groupID 259 * @param {String} objectType - 'collection', 'item', 'search' 260 * @param {String[]} objectKeys - Keys of objects to request 261 * @return {Array<Promise<Object[]|Error[]>>} - An array of promises for batches of JSON objects 262 * or Errors for failures 263 */ 264 downloadObjects: function (libraryType, libraryTypeID, objectType, objectKeys) { 265 if (!objectKeys.length) { 266 return []; 267 } 268 269 // If more than max per request, call in batches 270 if (objectKeys.length > this.MAX_OBJECTS_PER_REQUEST) { 271 let allKeys = objectKeys.concat(); 272 let promises = []; 273 while (true) { 274 let requestKeys = allKeys.splice(0, this.MAX_OBJECTS_PER_REQUEST) 275 if (!requestKeys.length) { 276 break; 277 } 278 let promise = this.downloadObjects( 279 libraryType, 280 libraryTypeID, 281 objectType, 282 requestKeys 283 )[0]; 284 if (promise) { 285 promises.push(promise); 286 } 287 } 288 return promises; 289 } 290 291 // Otherwise make request 292 var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); 293 294 Zotero.debug("Retrieving " + objectKeys.length + " " 295 + (objectKeys.length == 1 ? objectType : objectTypePlural)); 296 297 var params = { 298 target: objectTypePlural, 299 libraryType: libraryType, 300 libraryTypeID: libraryTypeID, 301 format: 'json' 302 }; 303 params[objectType + "Key"] = objectKeys.join(","); 304 if (objectType == 'item') { 305 params.includeTrashed = 1; 306 } 307 var uri = this.buildRequestURI(params); 308 309 return [ 310 this.makeRequest("GET", uri) 311 .then(function (xmlhttp) { 312 return this._parseJSON(xmlhttp.responseText) 313 }.bind(this)) 314 // Return the error without failing the whole chain 315 .catch(function (e) { 316 Zotero.logError(e); 317 if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.is4xx()) { 318 throw e; 319 } 320 return e; 321 }) 322 ]; 323 }, 324 325 326 uploadSettings: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, libraryVersion, settings) { 327 var method = "POST"; 328 var objectType = "setting"; 329 var objectTypePlural = "settings"; 330 var numSettings = Object.keys(settings).length; 331 332 Zotero.debug(`Uploading ${numSettings} ${numSettings == 1 ? objectType : objectTypePlural}`); 333 334 Zotero.debug("Sending If-Unmodified-Since-Version: " + libraryVersion); 335 336 var json = JSON.stringify(settings); 337 var params = { 338 target: objectTypePlural, 339 libraryType: libraryType, 340 libraryTypeID: libraryTypeID 341 }; 342 var uri = this.buildRequestURI(params); 343 344 var xmlhttp = yield this.makeRequest(method, uri, { 345 headers: { 346 "Content-Type": "application/json", 347 "If-Unmodified-Since-Version": libraryVersion 348 }, 349 body: json, 350 successCodes: [204, 412] 351 }); 352 this._check412(xmlhttp); 353 return this._getLastModifiedVersion(xmlhttp); 354 }), 355 356 357 uploadObjects: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, method, libraryVersion, objectType, objects) { 358 if (method != 'POST' && method != 'PATCH') { 359 throw new Error("Invalid method '" + method + "'"); 360 } 361 362 var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); 363 364 Zotero.debug("Uploading " + objects.length + " " 365 + (objects.length == 1 ? objectType : objectTypePlural)); 366 367 Zotero.debug("Sending If-Unmodified-Since-Version: " + libraryVersion); 368 369 var json = JSON.stringify(objects); 370 var params = { 371 target: objectTypePlural, 372 libraryType: libraryType, 373 libraryTypeID: libraryTypeID 374 }; 375 var uri = this.buildRequestURI(params); 376 377 var xmlhttp = yield this.makeRequest(method, uri, { 378 headers: { 379 "Content-Type": "application/json", 380 "If-Unmodified-Since-Version": libraryVersion 381 }, 382 body: json, 383 successCodes: [200, 412] 384 }); 385 this._check412(xmlhttp); 386 return { 387 libraryVersion: this._getLastModifiedVersion(xmlhttp), 388 results: this._parseJSON(xmlhttp.responseText) 389 }; 390 }), 391 392 393 uploadDeletions: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, libraryVersion, objectType, keys) { 394 var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType); 395 396 Zotero.debug(`Uploading ${keys.length} ${objectType} deletion` 397 + (keys.length == 1 ? '' : 's')); 398 399 Zotero.debug("Sending If-Unmodified-Since-Version: " + libraryVersion); 400 401 var params = { 402 target: objectTypePlural, 403 libraryType: libraryType, 404 libraryTypeID: libraryTypeID 405 }; 406 if (objectType == 'tag') { 407 params.tags = keys.join("||"); 408 } 409 else { 410 params[objectType + "Key"] = keys.join(","); 411 } 412 var uri = this.buildRequestURI(params); 413 var xmlhttp = yield this.makeRequest("DELETE", uri, { 414 headers: { 415 "If-Unmodified-Since-Version": libraryVersion 416 }, 417 successCodes: [204, 412] 418 }); 419 this._check412(xmlhttp); 420 return this._getLastModifiedVersion(xmlhttp); 421 }), 422 423 424 getFullTextVersions: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, since) { 425 var params = { 426 libraryType: libraryType, 427 libraryTypeID: libraryTypeID, 428 target: "fulltext", 429 format: "versions" 430 }; 431 if (since) { 432 params.since = since; 433 } 434 435 // TODO: Use pagination 436 var uri = this.buildRequestURI(params); 437 438 var xmlhttp = yield this.makeRequest("GET", uri); 439 return { 440 libraryVersion: this._getLastModifiedVersion(xmlhttp), 441 versions: this._parseJSON(xmlhttp.responseText) 442 }; 443 }), 444 445 446 getFullTextForItem: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, itemKey) { 447 var params = { 448 libraryType: libraryType, 449 libraryTypeID: libraryTypeID, 450 target: `items/${itemKey}/fulltext` 451 }; 452 var uri = this.buildRequestURI(params); 453 var xmlhttp = yield this.makeRequest("GET", uri, { successCodes: [200, 404] }); 454 if (xmlhttp.status == 404) { 455 return false; 456 } 457 var version = xmlhttp.getResponseHeader('Last-Modified-Version'); 458 if (!version) { 459 throw new Error("Last-Modified-Version not provided"); 460 } 461 return { 462 version: this._getLastModifiedVersion(xmlhttp), 463 data: this._parseJSON(xmlhttp.responseText) 464 }; 465 }), 466 467 468 setFullTextForItems: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, libraryVersion, data) { 469 var params = { 470 libraryType: libraryType, 471 libraryTypeID: libraryTypeID, 472 target: "fulltext" 473 }; 474 var uri = this.buildRequestURI(params); 475 var xmlhttp = yield this.makeRequest( 476 "POST", 477 uri, 478 { 479 headers: { 480 "Content-Type": "application/json", 481 "If-Unmodified-Since-Version": libraryVersion 482 }, 483 body: JSON.stringify(data), 484 successCodes: [200, 412], 485 debug: true 486 } 487 ); 488 this._check412(xmlhttp); 489 return { 490 libraryVersion: this._getLastModifiedVersion(xmlhttp), 491 results: this._parseJSON(xmlhttp.responseText) 492 }; 493 }), 494 495 496 createAPIKeyFromCredentials: Zotero.Promise.coroutine(function* (username, password) { 497 var body = JSON.stringify({ 498 username, 499 password, 500 name: "Automatic Zotero Client Key", 501 access: { 502 user: { 503 library: true, 504 notes: true, 505 write: true, 506 files: true 507 }, 508 groups: { 509 all: { 510 library: true, 511 write: true 512 } 513 } 514 } 515 }); 516 var headers = { 517 "Content-Type": "application/json" 518 }; 519 var uri = this.baseURL + "keys"; 520 var response = yield this.makeRequest("POST", uri, { 521 body, headers, successCodes: [201, 403], noAPIKey: true 522 }); 523 if (response.status == 403) { 524 return false; 525 } 526 527 var json = this._parseJSON(response.responseText); 528 if (!json.key) { 529 throw new Error('json.key not present in POST /keys response') 530 } 531 532 return json; 533 }), 534 535 536 // Deletes current API key 537 deleteAPIKey: Zotero.Promise.coroutine(function* () { 538 yield this.makeRequest("DELETE", this.baseURL + "keys/current"); 539 }), 540 541 542 buildRequestURI: function (params) { 543 var uri = this.baseURL; 544 545 switch (params.libraryType) { 546 case 'publications': 547 uri += 'users/' + params.libraryTypeID + '/' + params.libraryType; 548 break; 549 550 default: 551 uri += params.libraryType + 's/' + params.libraryTypeID; 552 break; 553 } 554 555 if (params.target === undefined) { 556 throw new Error("'target' not provided"); 557 } 558 559 uri += "/" + params.target; 560 561 if (params.objectKey) { 562 uri += "/" + params.objectKey; 563 } 564 565 var queryString = '?'; 566 var queryParamsArray = []; 567 var queryParamOptions = [ 568 'session', 569 'format', 570 'include', 571 'includeTrashed', 572 'itemType', 573 'itemKey', 574 'collectionKey', 575 'searchKey', 576 'tag', 577 'linkMode', 578 'start', 579 'limit', 580 'sort', 581 'direction', 582 'since', 583 'sincetime' 584 ]; 585 queryParams = {}; 586 587 for (let option in params) { 588 let value = params[option]; 589 if (value !== undefined && value !== '' && queryParamOptions.indexOf(option) != -1) { 590 queryParams[option] = value; 591 } 592 } 593 594 for (let index in queryParams) { 595 let value = queryParams[index]; 596 if (Array.isArray(value)) { 597 value.forEach(function(v, i) { 598 queryParamsArray.push(encodeURIComponent(index) + '=' + encodeURIComponent(v)); 599 }); 600 } 601 else { 602 queryParamsArray.push(encodeURIComponent(index) + '=' + encodeURIComponent(value)); 603 } 604 } 605 606 return uri + (queryParamsArray.length ? "?" + queryParamsArray.join('&') : ""); 607 }, 608 609 610 getHeaders: function (headers = {}) { 611 let newHeaders = {}; 612 newHeaders = Object.assign(newHeaders, headers); 613 newHeaders["Zotero-API-Version"] = this.apiVersion; 614 if (this.apiKey) { 615 newHeaders["Zotero-API-Key"] = this.apiKey; 616 } 617 return newHeaders; 618 }, 619 620 621 makeRequest: Zotero.Promise.coroutine(function* (method, uri, options = {}) { 622 if (!this.apiKey && !options.noAPIKey) { 623 throw new Error('API key not set'); 624 } 625 626 if (Zotero.HTTP.isWriteMethod(method) && this.debugUploadPolicy) { 627 // Confirm uploads when extensions.zotero.sync.debugUploadPolicy is 1 628 if (this.debugUploadPolicy === 1) { 629 if (options.body) { 630 Zotero.debug(options.body); 631 } 632 if (!Services.prompt.confirm(null, "Allow Upload?", `Allow ${method} to ${uri}?`)) { 633 throw new Error(method + " request denied"); 634 } 635 } 636 // Deny uploads when extensions.zotero.sync.debugUploadPolicy is 2 637 else if (this.debugUploadPolicy === 2) { 638 throw new Error(`Can't make ${method} request in read-only mode`); 639 } 640 } 641 642 let opts = {} 643 Object.assign(opts, options); 644 opts.headers = this.getHeaders(options.headers); 645 opts.dontCache = true; 646 opts.foreground = !options.background; 647 opts.responseType = options.responseType || 'text'; 648 if (options.body && options.body.length >= this.MIN_GZIP_SIZE 649 && Zotero.Prefs.get('sync.server.compressData')) { 650 opts.compressBody = true; 651 } 652 653 var tries = 0; 654 var failureDelayGenerator = null; 655 while (true) { 656 var result = yield this.caller.start(Zotero.Promise.coroutine(function* () { 657 try { 658 var xmlhttp = yield Zotero.HTTP.request(method, uri, opts); 659 this._checkBackoff(xmlhttp); 660 this.rateDelayPosition = 0; 661 return xmlhttp; 662 } 663 catch (e) { 664 tries++; 665 if (e instanceof Zotero.HTTP.UnexpectedStatusException) { 666 this._checkConnection(e.xmlhttp, e.channel); 667 if (this._check429(e.xmlhttp)) { 668 // Return false to keep retrying request 669 return false; 670 } 671 672 if (e.is5xx()) { 673 Zotero.logError(e); 674 if (e.xmlhttp.status == 503 && this._checkRetry(e.xmlhttp)) { 675 return false; 676 } 677 678 if (!failureDelayGenerator) { 679 // Keep trying for up to an hour 680 failureDelayGenerator = Zotero.Utilities.Internal.delayGenerator( 681 this.failureDelayIntervals, this.failureDelayMax 682 ); 683 } 684 let keepGoing = yield failureDelayGenerator.next().value; 685 if (!keepGoing) { 686 Zotero.logError("Failed too many times"); 687 throw e; 688 } 689 return false; 690 } 691 } 692 else if (e instanceof Zotero.HTTP.BrowserOfflineException) { 693 e.fatal = true; 694 } 695 throw e; 696 } 697 }.bind(this))); 698 699 if (result) { 700 return result; 701 } 702 } 703 }), 704 705 706 /** 707 * Retrieve paginated requests automatically based on the Link header, passing the results to a 708 * reducer 709 * 710 * @param {String} initialURL 711 * @param {Function} reducer - Reducer function taking (previousValue, xmlhttp, restart) 712 * accumulator: Return value from previous invocation, or initialValue 713 * xmlhttp: XMLHTTPRequest object from previous request 714 * restart: A function to restart from the beginning 715 * @param {mixed} initialValue 716 * @return {mixed} - The reduced value 717 */ 718 getPaginatedResults: Zotero.Promise.coroutine(function* (initialURL, reducer, initialValue) { 719 let url = initialURL; 720 let accumulator; 721 let restart = false; 722 while (true) { 723 let xmlhttp = yield this.makeRequest("GET", url); 724 accumulator = reducer( 725 accumulator === undefined ? initialValue : accumulator, 726 xmlhttp, 727 function () { 728 restart = true; 729 } 730 ); 731 if (restart) { 732 accumulator = undefined; 733 url = initialURL; 734 restart = false; 735 continue; 736 } 737 let link = this._parseLinkHeader(xmlhttp.getResponseHeader('Link')); 738 if (link && link.next) { 739 url = link.next; 740 } 741 else { 742 break; 743 } 744 } 745 return accumulator; 746 }), 747 748 749 /** 750 * Parse a Link header 751 * 752 * From https://gist.github.com/deiu/9335803 753 * MIT-licensed 754 */ 755 _parseLinkHeader: function (link) { 756 var linkexp = /<[^>]*>\s*(\s*;\s*[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*")))*(,|$)/g; 757 var paramexp = /[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*"))/g; 758 var matches = link.match(linkexp); 759 var rels = {}; 760 for (var i = 0; i < matches.length; i++) { 761 var split = matches[i].split('>'); 762 var href = split[0].substring(1); 763 var ps = split[1]; 764 var s = ps.match(paramexp); 765 for (var j = 0; j < s.length; j++) { 766 var p = s[j]; 767 var paramsplit = p.split('='); 768 var name = paramsplit[0]; 769 var rel = paramsplit[1].replace(/["']/g, ''); 770 rels[rel] = href; 771 } 772 } 773 return rels; 774 }, 775 776 777 _parseJSON: function (json) { 778 try { 779 json = JSON.parse(json); 780 } 781 catch (e) { 782 Zotero.debug(e, 1); 783 Zotero.debug(json, 1); 784 throw e; 785 } 786 return json; 787 }, 788 789 790 /** 791 * Check connection for certificate errors, interruptions, and empty responses and 792 * throw an appropriate error 793 */ 794 _checkConnection: function (xmlhttp, channel) { 795 const Ci = Components.interfaces; 796 797 if (!xmlhttp.responseText && (xmlhttp.status == 0 || xmlhttp.status == 200)) { 798 let msg = null; 799 let dialogButtonText = null; 800 let dialogButtonCallback = null; 801 802 if (xmlhttp.status === 0) { 803 msg = Zotero.getString('sync.error.checkConnection'); 804 } 805 if (!msg) { 806 msg = Zotero.getString('sync.error.emptyResponseServer') 807 + Zotero.getString('general.tryAgainLater'); 808 } 809 throw new Zotero.Error( 810 msg, 811 0, 812 { 813 dialogButtonText, 814 dialogButtonCallback 815 } 816 ); 817 } 818 }, 819 820 821 _checkBackoff: function (xmlhttp) { 822 var backoff = xmlhttp.getResponseHeader("Backoff"); 823 if (backoff && parseInt(backoff) == backoff) { 824 // TODO: Update status? 825 this.caller.pause(backoff * 1000); 826 } 827 }, 828 829 830 _checkRetry: function (xmlhttp) { 831 var retryAfter = xmlhttp.getResponseHeader("Retry-After"); 832 var delay; 833 if (!retryAfter) return false; 834 if (parseInt(retryAfter) != retryAfter) { 835 Zotero.logError(`Invalid Retry-After delay ${retryAfter}`); 836 return false; 837 } 838 // TODO: Update status? 839 delay = retryAfter; 840 this.caller.pause(delay * 1000); 841 return true; 842 }, 843 844 845 _check412: function (xmlhttp) { 846 // Avoid logging error from Zotero.HTTP.request() in ConcurrentCaller 847 if (xmlhttp.status == 412) { 848 Zotero.debug("Server returned 412: " + xmlhttp.responseText, 2); 849 throw new Zotero.HTTP.UnexpectedStatusException(xmlhttp); 850 } 851 }, 852 853 854 _check429: function (xmlhttp) { 855 if (xmlhttp.status != 429) return false; 856 857 // If there's a Retry-After header, use that 858 if (this._checkRetry(xmlhttp)) { 859 return true; 860 } 861 862 // Otherwise, pause for increasing amounts, or max amount if no more 863 var delay = this.rateDelayIntervals[this.rateDelayPosition++] 864 || this.rateDelayIntervals[this.rateDelayIntervals.length - 1]; 865 this.caller.pause(delay * 1000); 866 return true; 867 }, 868 869 870 _getLastModifiedVersion: function (xmlhttp) { 871 libraryVersion = xmlhttp.getResponseHeader('Last-Modified-Version'); 872 if (!libraryVersion) { 873 throw new Error("Last-Modified-Version not provided"); 874 } 875 return libraryVersion; 876 } 877 }