http.js (35246B)
1 /** 2 * Functions for performing HTTP requests, both via XMLHTTPRequest and using a hidden browser 3 * @namespace 4 */ 5 Zotero.HTTP = new function() { 6 this.lastGoogleScholarQueryTime = 0; 7 8 /** 9 * Exception returned for unexpected status when promise* is used 10 * @constructor 11 */ 12 this.UnexpectedStatusException = function(xmlhttp, msg) { 13 this.xmlhttp = xmlhttp; 14 this.status = xmlhttp.status; 15 this.channelStatus = null; 16 this.responseStatus = null; 17 this.channel = xmlhttp.channel; 18 this.message = msg; 19 this.stack = new Error().stack; 20 21 // Hide password from debug output 22 // 23 // Password also shows up in channel.name (nsIRequest.name), but that's 24 // read-only and has to be handled in Zotero.varDump() 25 try { 26 if (xmlhttp.channel) { 27 if (xmlhttp.channel.URI.password) { 28 xmlhttp.channel.URI.password = "********"; 29 } 30 if (xmlhttp.channel.URI.spec) { 31 xmlhttp.channel.URI.spec = xmlhttp.channel.URI.spec.replace(/key=[^&]+&?/, "key=********"); 32 } 33 } 34 } 35 catch (e) { 36 Zotero.debug(e, 1); 37 } 38 39 // If the connection failed, try to find out what really happened 40 if (!this.status) { 41 try { 42 if (xmlhttp.channel.status) { 43 this.channelStatus = xmlhttp.channel.status; 44 Zotero.debug("Channel status was " + this.channelStatus, 2); 45 } 46 } 47 catch (e) {} 48 try { 49 if (xmlhttp.channel.responseStatus) { 50 this.responseStatus = xmlhttp.channel.responseStatus; 51 Zotero.debug("Response status was " + this.responseStatus, 2); 52 } 53 } 54 catch (e) {} 55 } 56 }; 57 this.UnexpectedStatusException.prototype = Object.create(Error.prototype); 58 this.UnexpectedStatusException.prototype.is4xx = function () { 59 return this.status >= 400 && this.status < 500; 60 } 61 this.UnexpectedStatusException.prototype.is5xx = function () { 62 return this.status >= 500 && this.status < 600; 63 } 64 65 /** 66 * Exception returned if the browser is offline when promise* is used 67 * @constructor 68 */ 69 this.BrowserOfflineException = function() { 70 this.message = "XMLHttpRequest could not complete because the browser is offline"; 71 this.stack = new Error().stack; 72 }; 73 this.BrowserOfflineException.prototype = Object.create(Error.prototype); 74 75 this.TimeoutException = function(ms) { 76 this.message = "XMLHttpRequest has timed out after " + ms + "ms"; 77 this.stack = new Error().stack; 78 }; 79 this.TimeoutException.prototype = Object.create(Error.prototype); 80 81 this.SecurityException = function (msg, options = {}) { 82 this.message = msg; 83 this.stack = new Error().stack; 84 for (let i in options) { 85 this[i] = options[i]; 86 } 87 }; 88 this.SecurityException.prototype = Object.create( 89 // Zotero.Error not available in the connector 90 Zotero.Error ? Zotero.Error.prototype : Error.prototype 91 ); 92 93 94 this.promise = function () { 95 Zotero.debug("Zotero.HTTP.promise() is deprecated -- use Zotero.HTTP.request()", 2); 96 return this.request.apply(this, arguments); 97 } 98 99 /** 100 * Get a promise for a HTTP request 101 * 102 * @param {String} method The method of the request ("GET", "POST", "HEAD", or "OPTIONS") 103 * @param {nsIURI|String} url URL to request 104 * @param {Object} [options] Options for HTTP request:<ul> 105 * <li>body - The body of a POST request</li> 106 * <li>headers - Object of HTTP headers to send with the request</li> 107 * <li>cookieSandbox - The sandbox from which cookies should be taken</li> 108 * <li>debug - Log response text and status code</li> 109 * <li>dontCache - If set, specifies that the request should not be fulfilled from the cache</li> 110 * <li>foreground - Make a foreground request, showing certificate/authentication dialogs if necessary</li> 111 * <li>headers - HTTP headers to include in the request</li> 112 * <li>logBodyLength - Length of request body to log (defaults to 1024)</li> 113 * <li>timeout - Request timeout specified in milliseconds 114 * <li>requestObserver - Callback to receive XMLHttpRequest after open()</li> 115 * <li>responseType - The type of the response. See XHR 2 documentation for legal values</li> 116 * <li>responseCharset - The charset the response should be interpreted as</li> 117 * <li>successCodes - HTTP status codes that are considered successful, or FALSE to allow all</li> 118 * </ul> 119 * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object 120 * @return {Promise<XMLHttpRequest>} A promise resolved with the XMLHttpRequest object if the 121 * request succeeds, or rejected if the browser is offline or a non-2XX status response 122 * code is received (or a code not in options.successCodes if provided). 123 */ 124 this.request = Zotero.Promise.coroutine(function* (method, url, options = {}) { 125 if (url instanceof Components.interfaces.nsIURI) { 126 // Don't display password in console 127 var dispURL = this.getDisplayURI(url).spec; 128 url = url.spec; 129 } 130 else { 131 var dispURL = url; 132 } 133 134 // Don't display API key in console 135 dispURL = dispURL.replace(/key=[^&]+&?/, "").replace(/\?$/, ""); 136 137 if (options.body && typeof options.body == 'string') { 138 let len = options.logBodyLength !== undefined ? options.logBodyLength : 1024; 139 var bodyStart = options.body.substr(0, len); 140 // Don't display sync password or session id in console 141 bodyStart = bodyStart.replace(/password":"[^"]+/, 'password":"********'); 142 bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********'); 143 bodyStart = bodyStart.replace(/sessionid=[^&]+/, 'sessionid=********'); 144 145 Zotero.debug("HTTP " + method + ' "' 146 + (options.body.length > len 147 ? bodyStart + '\u2026" (' + options.body.length + ' chars)' : bodyStart + '"') 148 + " to " + dispURL); 149 } else { 150 Zotero.debug("HTTP " + method + " " + dispURL); 151 } 152 153 if (url.startsWith('http') && this.browserIsOffline()) { 154 Zotero.debug("HTTP " + method + " " + dispURL + " failed: Browser is offline"); 155 throw new this.BrowserOfflineException(); 156 } 157 158 var deferred = Zotero.Promise.defer(); 159 160 if (!this.mock) { 161 var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 162 .createInstance(); 163 } 164 else { 165 var xmlhttp = new this.mock; 166 // Add a dummy overrideMimeType() if it's not mocked 167 // https://github.com/cjohansen/Sinon.JS/issues/559 168 if (!xmlhttp.overrideMimeType) { 169 xmlhttp.overrideMimeType = function () {}; 170 } 171 } 172 // Prevent certificate/authentication dialogs from popping up 173 if (!options.foreground) { 174 xmlhttp.mozBackgroundRequest = true; 175 } 176 xmlhttp.open(method, url, true); 177 178 // Pass the request to a callback 179 if (options.requestObserver) { 180 options.requestObserver(xmlhttp); 181 } 182 183 if (method == 'PUT') { 184 // Some servers (e.g., Jungle Disk DAV) return a 200 response code 185 // with Content-Length: 0, which triggers a "no element found" error 186 // in Firefox, so we override to text 187 xmlhttp.overrideMimeType("text/plain"); 188 } 189 190 // Send cookie even if "Allow third-party cookies" is disabled (>=Fx3.6 only) 191 var channel = xmlhttp.channel, 192 isFile = channel instanceof Components.interfaces.nsIFileChannel; 193 if(channel instanceof Components.interfaces.nsIHttpChannelInternal) { 194 channel.forceAllowThirdPartyCookie = true; 195 196 // Set charset 197 // 198 // This is the method used in the connector, but as noted there, this parameter is a 199 // legacy of XPCOM functionality (where it could be set on the nsIChannel, which 200 // doesn't seem to work anymore), and we should probably allow responseContentType to 201 // be set instead 202 if (options.responseCharset) { 203 xmlhttp.overrideMimeType(`text/plain; charset=${responseCharset}`); 204 } 205 206 // Disable caching if requested 207 if (options.dontCache) { 208 channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; 209 } 210 } 211 212 // Set responseType 213 if (options.responseType) { 214 xmlhttp.responseType = options.responseType; 215 } 216 217 // Send headers 218 var headers = {}; 219 if (options && options.headers) { 220 Object.assign(headers, options.headers); 221 } 222 var compressedBody = false; 223 if (options.body) { 224 if (!headers["Content-Type"]) { 225 headers["Content-Type"] = "application/x-www-form-urlencoded"; 226 } 227 else if (headers["Content-Type"] == 'multipart/form-data') { 228 // Allow XHR to set Content-Type with boundary for multipart/form-data 229 delete headers["Content-Type"]; 230 } 231 232 if (options.compressBody && this.isWriteMethod(method)) { 233 headers['Content-Encoding'] = 'gzip'; 234 compressedBody = yield Zotero.Utilities.Internal.gzip(options.body); 235 236 let oldLen = options.body.length; 237 let newLen = compressedBody.length; 238 Zotero.debug(`${method} body gzipped from ${oldLen} to ${newLen}; ` 239 + Math.round(((oldLen - newLen) / oldLen) * 100) + "% savings"); 240 } 241 } 242 if (options.debug) { 243 if (headers["Zotero-API-Key"]) { 244 let dispHeaders = {}; 245 Object.assign(dispHeaders, headers); 246 if (dispHeaders["Zotero-API-Key"]) { 247 dispHeaders["Zotero-API-Key"] = "[Not shown]"; 248 } 249 Zotero.debug(dispHeaders); 250 } 251 else { 252 Zotero.debug(headers); 253 } 254 } 255 for (var header in headers) { 256 xmlhttp.setRequestHeader(header, headers[header]); 257 } 258 259 // Set timeout 260 if (options.timeout) { 261 xmlhttp.timeout = options.timeout; 262 } 263 264 xmlhttp.ontimeout = function() { 265 deferred.reject(new Zotero.HTTP.TimeoutException(options.timeout)); 266 }; 267 268 xmlhttp.onloadend = function() { 269 var status = xmlhttp.status; 270 271 // If an invalid HTTP response (e.g., NS_ERROR_INVALID_CONTENT_ENCODING) includes a 272 // 4xx or 5xx HTTP response code, swap it in, since it might be enough info to do 273 // what we need (e.g., verify a 404 from a WebDAV server) 274 try { 275 if (!status && xmlhttp.channel.responseStatus >= 400) { 276 Zotero.warn(`Overriding status for invalid response for ${dispURL} ` 277 + `(${xmlhttp.channel.status})`); 278 status = xmlhttp.channel.responseStatus; 279 } 280 } 281 catch (e) {} 282 283 if (options.successCodes) { 284 var success = options.successCodes.indexOf(status) != -1; 285 } 286 // Explicit FALSE means allow any status code 287 else if (options.successCodes === false) { 288 var success = true; 289 } 290 else if(isFile) { 291 var success = status == 200 || status == 0; 292 } 293 else { 294 var success = status >= 200 && status < 300; 295 } 296 297 if(success) { 298 Zotero.debug("HTTP " + method + " " + dispURL 299 + " succeeded with " + status); 300 if (options.debug) { 301 Zotero.debug(xmlhttp.responseText); 302 } 303 deferred.resolve(xmlhttp); 304 } else { 305 let msg = "HTTP " + method + " " + dispURL + " failed with status code " + status; 306 if (!xmlhttp.responseType && xmlhttp.responseText) { 307 msg += ":\n\n" + xmlhttp.responseText; 308 } 309 Zotero.debug(msg, 1); 310 311 try { 312 _checkSecurity(xmlhttp, channel); 313 } 314 catch (e) { 315 deferred.reject(e); 316 return; 317 } 318 319 deferred.reject(new Zotero.HTTP.UnexpectedStatusException(xmlhttp, msg)); 320 } 321 }; 322 323 if (options.cookieSandbox) { 324 options.cookieSandbox.attachToInterfaceRequestor(xmlhttp); 325 } 326 327 // Send binary data 328 if (compressedBody) { 329 let numBytes = compressedBody.length; 330 let ui8Data = new Uint8Array(numBytes); 331 for (let i = 0; i < numBytes; i++) { 332 ui8Data[i] = compressedBody.charCodeAt(i) & 0xff; 333 } 334 xmlhttp.send(ui8Data); 335 } 336 // Send regular request 337 else { 338 xmlhttp.send(options.body || null); 339 } 340 341 return deferred.promise; 342 }); 343 344 /** 345 * Send an HTTP GET request via XMLHTTPRequest 346 * 347 * @param {nsIURI|String} url URL to request 348 * @param {Function} onDone Callback to be executed upon request completion 349 * @param {String} responseCharset Character set to force on the response 350 * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object 351 * @param {Object} requestHeaders HTTP headers to include with request 352 * @return {XMLHttpRequest} The XMLHttpRequest object if the request was sent, or 353 * false if the browser is offline 354 * @deprecated Use {@link Zotero.HTTP.request} 355 */ 356 this.doGet = function(url, onDone, responseCharset, cookieSandbox, requestHeaders) { 357 if (url instanceof Components.interfaces.nsIURI) { 358 // Don't display password in console 359 var disp = this.getDisplayURI(url); 360 Zotero.debug("HTTP GET " + disp.spec); 361 url = url.spec; 362 } 363 else { 364 Zotero.debug("HTTP GET " + url); 365 } 366 if (this.browserIsOffline()){ 367 return false; 368 } 369 370 var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 371 .createInstance(); 372 373 // Prevent certificate/authentication dialogs from popping up 374 xmlhttp.mozBackgroundRequest = true; 375 xmlhttp.open('GET', url, true); 376 377 // Send cookie even if "Allow third-party cookies" is disabled (>=Fx3.6 only) 378 var channel = xmlhttp.channel; 379 channel.QueryInterface(Components.interfaces.nsIHttpChannelInternal); 380 channel.forceAllowThirdPartyCookie = true; 381 382 // Set charset -- see note in request() above 383 if (responseCharset) { 384 xmlhttp.overrideMimeType(`text/plain; charset=${responseCharset}`); 385 } 386 387 // Set request headers 388 if (requestHeaders) { 389 for (var header in requestHeaders) { 390 xmlhttp.setRequestHeader(header, requestHeaders[header]); 391 } 392 } 393 394 // Don't cache GET requests 395 xmlhttp.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; 396 397 var useMethodjit = Components.utils.methodjit; 398 /** @ignore */ 399 xmlhttp.onreadystatechange = function() { 400 _stateChange(xmlhttp, onDone); 401 }; 402 403 if(cookieSandbox) cookieSandbox.attachToInterfaceRequestor(xmlhttp.getInterface(Components.interfaces.nsIInterfaceRequestor)); 404 xmlhttp.send(null); 405 406 return xmlhttp; 407 } 408 409 /** 410 * Send an HTTP POST request via XMLHTTPRequest 411 * 412 * @param {String} url URL to request 413 * @param {String} body Request body 414 * @param {Function} onDone Callback to be executed upon request completion 415 * @param {String} headers Request HTTP headers 416 * @param {String} responseCharset Character set to force on the response 417 * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object 418 * @return {XMLHttpRequest} The XMLHttpRequest object if the request was sent, or 419 * false if the browser is offline 420 * @deprecated Use {@link Zotero.HTTP.request} 421 */ 422 this.doPost = function(url, body, onDone, headers, responseCharset, cookieSandbox) { 423 if (url instanceof Components.interfaces.nsIURI) { 424 // Don't display password in console 425 var disp = this.getDisplayURI(url); 426 url = url.spec; 427 } 428 429 var bodyStart = body.substr(0, 1024); 430 // Don't display sync password or session id in console 431 bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********'); 432 bodyStart = bodyStart.replace(/sessionid=[^&]+/, 'sessionid=********'); 433 434 Zotero.debug("HTTP POST " 435 + (body.length > 1024 ? 436 bodyStart + '... (' + body.length + ' chars)' : bodyStart) 437 + " to " + (disp ? disp.spec : url)); 438 439 440 if (this.browserIsOffline()){ 441 return false; 442 } 443 444 var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 445 .createInstance(); 446 // Prevent certificate/authentication dialogs from popping up 447 xmlhttp.mozBackgroundRequest = true; 448 xmlhttp.open('POST', url, true); 449 // Send cookie even if "Allow third-party cookies" is disabled (>=Fx3.6 only) 450 var channel = xmlhttp.channel; 451 channel.QueryInterface(Components.interfaces.nsIHttpChannelInternal); 452 channel.forceAllowThirdPartyCookie = true; 453 454 // Set charset -- see note in request() above 455 if (responseCharset) { 456 xmlhttp.overrideMimeType(`text/plain; charset=${responseCharset}`); 457 } 458 459 if (headers) { 460 if (typeof headers == 'string') { 461 var msg = "doPost() now takes a headers object rather than a requestContentType -- update your code"; 462 Zotero.debug(msg, 2); 463 Components.utils.reportError(msg); 464 headers = { 465 "Content-Type": headers 466 }; 467 } 468 } 469 else { 470 headers = {}; 471 } 472 473 if (!headers["Content-Type"]) { 474 headers["Content-Type"] = "application/x-www-form-urlencoded"; 475 } 476 477 for (var header in headers) { 478 xmlhttp.setRequestHeader(header, headers[header]); 479 } 480 481 var useMethodjit = Components.utils.methodjit; 482 /** @ignore */ 483 xmlhttp.onreadystatechange = function() { 484 _stateChange(xmlhttp, onDone); 485 }; 486 487 if(cookieSandbox) cookieSandbox.attachToInterfaceRequestor(xmlhttp.getInterface(Components.interfaces.nsIInterfaceRequestor)); 488 xmlhttp.send(body); 489 490 return xmlhttp; 491 } 492 493 /** 494 * Send an HTTP HEAD request via XMLHTTPRequest 495 * 496 * @param {String} url URL to request 497 * @param {Function} onDone Callback to be executed upon request completion 498 * @param {Object} requestHeaders HTTP headers to include with request 499 * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object 500 * @return {XMLHttpRequest} The XMLHttpRequest object if the request was sent, or 501 * false if the browser is offline 502 * @deprecated Use {@link Zotero.HTTP.request} 503 */ 504 this.doHead = function(url, onDone, requestHeaders, cookieSandbox) { 505 if (url instanceof Components.interfaces.nsIURI) { 506 // Don't display password in console 507 var disp = this.getDisplayURI(url); 508 Zotero.debug("HTTP HEAD " + disp.spec); 509 url = url.spec; 510 } 511 else { 512 Zotero.debug("HTTP HEAD " + url); 513 514 } 515 516 if (this.browserIsOffline()){ 517 return false; 518 } 519 520 // Workaround for "Accept third-party cookies" being off in Firefox 3.0.1 521 // https://www.zotero.org/trac/ticket/1070 522 var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 523 .createInstance(); 524 // Prevent certificate/authentication dialogs from popping up 525 xmlhttp.mozBackgroundRequest = true; 526 xmlhttp.open('HEAD', url, true); 527 // Send cookie even if "Allow third-party cookies" is disabled (>=Fx3.6 only) 528 var channel = xmlhttp.channel; 529 channel.QueryInterface(Components.interfaces.nsIHttpChannelInternal); 530 channel.forceAllowThirdPartyCookie = true; 531 532 if (requestHeaders) { 533 for (var header in requestHeaders) { 534 xmlhttp.setRequestHeader(header, requestHeaders[header]); 535 } 536 } 537 538 // Don't cache HEAD requests 539 xmlhttp.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; 540 541 var useMethodjit = Components.utils.methodjit; 542 /** @ignore */ 543 xmlhttp.onreadystatechange = function() { 544 _stateChange(xmlhttp, onDone); 545 }; 546 547 if(cookieSandbox) cookieSandbox.attachToInterfaceRequestor(xmlhttp.getInterface(Components.interfaces.nsIInterfaceRequestor)); 548 xmlhttp.send(null); 549 550 return xmlhttp; 551 } 552 553 /** 554 * Send an HTTP OPTIONS request via XMLHTTPRequest 555 * 556 * @param {nsIURI} url 557 * @param {Function} onDone 558 * @return {XMLHTTPRequest} 559 * @deprecated Use {@link Zotero.HTTP.request} 560 */ 561 this.doOptions = function (uri, callback) { 562 // Don't display password in console 563 var disp = this.getDisplayURI(uri); 564 Zotero.debug("HTTP OPTIONS for " + disp.spec); 565 566 if (Zotero.HTTP.browserIsOffline()){ 567 return false; 568 } 569 570 var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 571 .createInstance(); 572 // Prevent certificate/authentication dialogs from popping up 573 xmlhttp.mozBackgroundRequest = true; 574 xmlhttp.open('OPTIONS', uri.spec, true); 575 576 var useMethodjit = Components.utils.methodjit; 577 /** @ignore */ 578 xmlhttp.onreadystatechange = function() { 579 _stateChange(xmlhttp, callback); 580 }; 581 xmlhttp.send(null); 582 return xmlhttp; 583 } 584 585 586 /** 587 * Make a foreground HTTP request in order to trigger a proxy authentication dialog 588 * 589 * Other Zotero.HTTP requests are background requests by default, and 590 * background requests don't trigger a proxy auth prompt, so we make a 591 * foreground request on startup and resolve the promise 592 * Zotero.proxyAuthComplete when we're done. Any network requests that want 593 * to wait for proxy authentication can wait for that promise. 594 */ 595 this.triggerProxyAuth = function () { 596 if (!Zotero.isStandalone 597 || !Zotero.Prefs.get("triggerProxyAuthentication") 598 || Zotero.HTTP.browserIsOffline()) { 599 Zotero.proxyAuthComplete = Zotero.Promise.resolve(); 600 return false; 601 } 602 603 var deferred = Zotero.Promise.defer(); 604 Zotero.proxyAuthComplete = deferred.promise; 605 606 Zotero.Promise.try(function () { 607 var uris = Zotero.Prefs.get('proxyAuthenticationURLs').split(','); 608 uris = Zotero.Utilities.arrayShuffle(uris); 609 uris.unshift(ZOTERO_CONFIG.PROXY_AUTH_URL); 610 611 return Zotero.spawn(function* () { 612 for (let i = 0; i <= uris.length; i++) { 613 let uri = uris.shift(); 614 if (!uri) { 615 break; 616 } 617 618 // For non-Zotero URLs, wait for PAC initialization, 619 // in a rather ugly and inefficient manner 620 if (i == 1) { 621 let installed = yield Zotero.Promise.try(_pacInstalled) 622 .then(function (installed) { 623 if (installed) throw true; 624 }) 625 .delay(500) 626 .then(_pacInstalled) 627 .then(function (installed) { 628 if (installed) throw true; 629 }) 630 .delay(1000) 631 .then(_pacInstalled) 632 .then(function (installed) { 633 if (installed) throw true; 634 }) 635 .delay(2000) 636 .then(_pacInstalled) 637 .catch(function () { 638 return true; 639 }); 640 if (!installed) { 641 Zotero.debug("No general proxy or PAC file found -- assuming direct connection"); 642 break; 643 } 644 } 645 646 let proxyInfo = yield _proxyAsyncResolve(uri); 647 if (proxyInfo) { 648 Zotero.debug("Proxy required for " + uri + " -- making HEAD request to trigger auth prompt"); 649 yield Zotero.HTTP.request("HEAD", uri, { 650 foreground: true, 651 dontCache: true 652 }) 653 .catch(function (e) { 654 Components.utils.reportError(e); 655 var msg = "Error connecting to proxy -- proxied requests may not work"; 656 Zotero.log(msg, 'error'); 657 Zotero.debug(msg, 1); 658 }); 659 break; 660 } 661 else { 662 Zotero.debug("Proxy not required for " + uri); 663 } 664 } 665 deferred.resolve(); 666 }); 667 }) 668 .catch(function (e) { 669 Components.utils.reportError(e); 670 Zotero.debug(e, 1); 671 deferred.resolve(); 672 }); 673 } 674 675 676 /** 677 * Test if a PAC file is installed 678 * 679 * There might be a better way to do this that doesn't require stepping 680 * through the error log and doing a fragile string comparison. 681 */ 682 _pacInstalled = function () { 683 return Zotero.getErrors(true).some(val => val.indexOf("PAC file installed") == 0) 684 } 685 686 687 _proxyAsyncResolve = function (uri) { 688 Components.utils.import("resource://gre/modules/NetUtil.jsm"); 689 var pps = Components.classes["@mozilla.org/network/protocol-proxy-service;1"] 690 .getService(Components.interfaces.nsIProtocolProxyService); 691 var deferred = Zotero.Promise.defer(); 692 pps.asyncResolve( 693 NetUtil.newURI(uri), 694 0, 695 { 696 onProxyAvailable: function (req, uri, proxyInfo, status) { 697 //Zotero.debug("onProxyAvailable"); 698 //Zotero.debug(status); 699 deferred.resolve(proxyInfo); 700 }, 701 702 QueryInterface: function (iid) { 703 const interfaces = [ 704 Components.interfaces.nsIProtocolProxyCallback, 705 Components.interfaces.nsISupports 706 ]; 707 if (!interfaces.some(function(v) { return iid.equals(v) })) { 708 throw Components.results.NS_ERROR_NO_INTERFACE; 709 } 710 return this; 711 }, 712 } 713 ); 714 return deferred.promise; 715 } 716 717 718 // 719 // WebDAV methods 720 // 721 722 this.WebDAV = {}; 723 724 /** 725 * Send a WebDAV MKCOL request via XMLHTTPRequest 726 * 727 * @param {nsIURI} url 728 * @param {Function} onDone 729 * @return {XMLHTTPRequest} 730 */ 731 this.WebDAV.doMkCol = function (uri, callback) { 732 // Don't display password in console 733 var disp = Zotero.HTTP.getDisplayURI(uri); 734 Zotero.debug("HTTP MKCOL " + disp.spec); 735 736 if (Zotero.HTTP.browserIsOffline()) { 737 return false; 738 } 739 740 var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 741 .createInstance(); 742 // Prevent certificate/authentication dialogs from popping up 743 xmlhttp.mozBackgroundRequest = true; 744 xmlhttp.open('MKCOL', uri.spec, true); 745 var useMethodjit = Components.utils.methodjit; 746 /** @ignore */ 747 xmlhttp.onreadystatechange = function() { 748 _stateChange(xmlhttp, callback); 749 }; 750 xmlhttp.send(null); 751 return xmlhttp; 752 } 753 754 755 /** 756 * Send a WebDAV PUT request via XMLHTTPRequest 757 * 758 * @param {nsIURI} url 759 * @param {Function} onDone 760 * @return {XMLHTTPRequest} 761 */ 762 this.WebDAV.doDelete = function (uri, callback) { 763 // Don't display password in console 764 var disp = Zotero.HTTP.getDisplayURI(uri); 765 766 Zotero.debug("WebDAV DELETE to " + disp.spec); 767 768 if (Zotero.HTTP.browserIsOffline()) { 769 return false; 770 } 771 772 var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 773 .createInstance(); 774 // Prevent certificate/authentication dialogs from popping up 775 xmlhttp.mozBackgroundRequest = true; 776 xmlhttp.open("DELETE", uri.spec, true); 777 // Firefox 3 throws a "no element found" error even with a 778 // 204 ("No Content") response, so we override to text 779 xmlhttp.overrideMimeType("text/plain"); 780 var useMethodjit = Components.utils.methodjit; 781 /** @ignore */ 782 xmlhttp.onreadystatechange = function() { 783 _stateChange(xmlhttp, callback); 784 }; 785 xmlhttp.send(null); 786 return xmlhttp; 787 } 788 789 790 this.isWriteMethod = function (method) { 791 return method == 'POST' || method == 'PUT' || method == 'PATCH' || method == 'DELETE'; 792 }; 793 794 795 this.getDisplayURI = function (uri) { 796 var disp = uri.clone(); 797 if (disp.password) { 798 disp.password = "********"; 799 } 800 return disp; 801 } 802 803 804 /** 805 * Get the Authorization header used by a channel 806 * 807 * As of Firefox 3.0.1 subsequent requests to higher-level directories 808 * seem not to authenticate properly and just return 401s, so this 809 * can be used to manually include the Authorization header in a request 810 * 811 * It can also be used to check whether a request was forced to 812 * use authentication 813 * 814 * @param {nsIChannel} channel 815 * @return {String|FALSE} Authorization header, or FALSE if none 816 */ 817 this.getChannelAuthorization = function (channel) { 818 try { 819 channel.QueryInterface(Components.interfaces.nsIHttpChannel); 820 var authHeader = channel.getRequestHeader("Authorization"); 821 return authHeader; 822 } 823 catch (e) { 824 Zotero.debug(e); 825 return false; 826 } 827 } 828 829 830 /** 831 * Checks if the browser is currently in "Offline" mode 832 * 833 * @type Boolean 834 */ 835 this.browserIsOffline = function() { 836 return Components.classes["@mozilla.org/network/io-service;1"] 837 .getService(Components.interfaces.nsIIOService).offline; 838 } 839 840 841 /** 842 * Load one or more documents using XMLHttpRequest 843 * 844 * This should stay in sync with the equivalent function in the connector 845 * 846 * @param {String|String[]} urls URL(s) of documents to load 847 * @param {Function} processor - Callback to be executed for each document loaded; if function returns 848 * a promise, it's waited for before continuing 849 * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object 850 * @return {Promise<Array>} - A promise for an array of results from the processor runs 851 */ 852 this.processDocuments = async function (urls, processor, cookieSandbox) { 853 // Handle old signature: urls, processor, onDone, onError, dontDelete, cookieSandbox 854 if (arguments.length > 3) { 855 Zotero.debug("Zotero.HTTP.processDocuments() now takes only 3 arguments -- update your code"); 856 var onDone = arguments[2]; 857 var onError = arguments[3]; 858 var cookieSandbox = arguments[5]; 859 } 860 861 if (typeof urls == "string") urls = [urls]; 862 var funcs = urls.map(url => () => { 863 return Zotero.HTTP.request( 864 "GET", 865 url, 866 { 867 responseType: 'document' 868 } 869 ) 870 .then((xhr) => { 871 var doc = this.wrapDocument(xhr.response, url); 872 return processor(doc, url); 873 }); 874 }); 875 876 // Run processes serially 877 // TODO: Add some concurrency? 878 var f; 879 var results = []; 880 while (f = funcs.shift()) { 881 try { 882 results.push(await f()); 883 } 884 catch (e) { 885 if (onError) { 886 onError(e); 887 } 888 throw e; 889 } 890 } 891 892 // Deprecated 893 if (onDone) { 894 onDone(); 895 } 896 897 return results; 898 }; 899 900 901 /** 902 * Load one or more documents in a hidden browser 903 * 904 * @param {String|String[]} urls URL(s) of documents to load 905 * @param {Function} processor - Callback to be executed for each document loaded; if function returns 906 * a promise, it's waited for before continuing 907 * @param {Function} onDone - Callback to be executed after all documents have been loaded 908 * @param {Function} onError - Callback to be executed if an error occurs 909 * @param {Boolean} dontDelete Don't delete the hidden browser upon completion; calling function 910 * must call deleteHiddenBrowser itself. 911 * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object 912 * @return {browser} Hidden browser used for loading 913 */ 914 this.loadDocuments = function (urls, processor, onDone, onError, dontDelete, cookieSandbox) { 915 // (Approximately) how many seconds to wait if the document is left in the loading state and 916 // pageshow is called before we call pageshow with an incomplete document 917 const LOADING_STATE_TIMEOUT = 120; 918 var firedLoadEvent = 0; 919 920 /** 921 * Loads the next page 922 * @inner 923 */ 924 var doLoad = function() { 925 if(currentURL < urls.length) { 926 var url = urls[currentURL], 927 hiddenBrowser = hiddenBrowsers[currentURL]; 928 firedLoadEvent = 0; 929 currentURL++; 930 try { 931 Zotero.debug("Zotero.HTTP.loadDocuments: Loading " + url); 932 hiddenBrowser.loadURI(url); 933 } catch(e) { 934 if (onError) { 935 onError(e); 936 return; 937 } else { 938 if(!dontDelete) Zotero.Browser.deleteHiddenBrowser(hiddenBrowsers); 939 throw(e); 940 } 941 } 942 } else { 943 if(!dontDelete) Zotero.Browser.deleteHiddenBrowser(hiddenBrowsers); 944 if (onDone) onDone(); 945 } 946 }; 947 948 /** 949 * Callback to be executed when a page load completes 950 * @inner 951 */ 952 var onLoad = function(e) { 953 var hiddenBrowser = e.currentTarget, 954 doc = hiddenBrowser.contentDocument; 955 if(hiddenBrowser.zotero_loaded) return; 956 if(!doc) return; 957 var url = doc.documentURI; 958 if(url === "about:blank") return; 959 if(doc.readyState === "loading" && (firedLoadEvent++) < 120) { 960 // Try again in a second 961 Zotero.setTimeout(onLoad.bind(this, {"currentTarget":hiddenBrowser}), 1000); 962 return; 963 } 964 965 Zotero.debug("Zotero.HTTP.loadDocuments: " + url + " loaded"); 966 hiddenBrowser.removeEventListener("pageshow", onLoad, true); 967 hiddenBrowser.zotero_loaded = true; 968 969 var maybePromise; 970 var error; 971 try { 972 maybePromise = processor(doc); 973 } 974 catch (e) { 975 error = e; 976 } 977 978 // If processor returns a promise, wait for it 979 if (maybePromise && maybePromise.then) { 980 maybePromise.then(() => doLoad()) 981 .catch(e => { 982 if (onError) { 983 onError(e); 984 } 985 else { 986 throw e; 987 } 988 }); 989 return; 990 } 991 992 try { 993 if (error) { 994 if (onError) { 995 onError(error); 996 } 997 else { 998 throw error; 999 } 1000 } 1001 } 1002 finally { 1003 doLoad(); 1004 } 1005 }; 1006 1007 if(typeof(urls) == "string") urls = [urls]; 1008 1009 var hiddenBrowsers = [], 1010 currentURL = 0; 1011 for(var i=0; i<urls.length; i++) { 1012 var hiddenBrowser = Zotero.Browser.createHiddenBrowser(); 1013 if(cookieSandbox) cookieSandbox.attachToBrowser(hiddenBrowser); 1014 hiddenBrowser.addEventListener("pageshow", onLoad, true); 1015 hiddenBrowsers[i] = hiddenBrowser; 1016 } 1017 1018 doLoad(); 1019 1020 return hiddenBrowsers.length === 1 ? hiddenBrowsers[0] : hiddenBrowsers.slice(); 1021 } 1022 1023 /** 1024 * Handler for XMLHttpRequest state change 1025 * 1026 * @param {nsIXMLHttpRequest} xmlhttp XMLHttpRequest whose state just changed 1027 * @param {Function} [callback] Callback for request completion 1028 * @param {*} [data] Data to be passed back to callback as the second argument 1029 * @private 1030 */ 1031 function _stateChange(xmlhttp, callback, data) { 1032 switch (xmlhttp.readyState){ 1033 // Request not yet made 1034 case 1: 1035 break; 1036 1037 case 2: 1038 break; 1039 1040 // Called multiple times while downloading in progress 1041 case 3: 1042 break; 1043 1044 // Download complete 1045 case 4: 1046 if (callback) { 1047 callback(xmlhttp, data); 1048 } 1049 break; 1050 } 1051 } 1052 1053 function _checkSecurity(xmlhttp, channel) { 1054 if (xmlhttp.status != 0 || !channel) { 1055 return; 1056 } 1057 1058 let secInfo = channel.securityInfo; 1059 let msg; 1060 let dialogButtonText; 1061 let dialogButtonCallback; 1062 if (secInfo instanceof Ci.nsITransportSecurityInfo) { 1063 secInfo.QueryInterface(Ci.nsITransportSecurityInfo); 1064 if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE) 1065 == Ci.nsIWebProgressListener.STATE_IS_INSECURE) { 1066 let url = channel.name; 1067 let ios = Components.classes["@mozilla.org/network/io-service;1"] 1068 .getService(Components.interfaces.nsIIOService); 1069 try { 1070 var uri = ios.newURI(url, null, null); 1071 var host = uri.host; 1072 } 1073 catch (e) { 1074 Zotero.debug(e); 1075 } 1076 let kbURL = 'https://www.zotero.org/support/kb/ssl_certificate_error'; 1077 msg = Zotero.getString('sync.storage.error.webdav.sslCertificateError', host); 1078 dialogButtonText = Zotero.getString('general.moreInformation'); 1079 dialogButtonCallback = function () { 1080 let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 1081 .getService(Components.interfaces.nsIWindowMediator); 1082 let win = wm.getMostRecentWindow("navigator:browser"); 1083 win.ZoteroPane.loadURI(kbURL, { metaKey: true, shiftKey: true }); 1084 }; 1085 } 1086 else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) 1087 == Ci.nsIWebProgressListener.STATE_IS_BROKEN) { 1088 msg = Zotero.getString('sync.error.sslConnectionError'); 1089 } 1090 if (msg) { 1091 throw new Zotero.HTTP.SecurityException( 1092 msg, 1093 { 1094 dialogButtonText, 1095 dialogButtonCallback 1096 } 1097 ); 1098 } 1099 } 1100 } 1101 1102 /** 1103 * Mimics the window.location/document.location interface, given an nsIURL 1104 * @param {nsIURL} url 1105 */ 1106 this.Location = function(url) { 1107 this._url = url; 1108 this.hash = url.ref ? "#"+url.ref : ""; 1109 this.host = url.hostPort; 1110 this.hostname = url.host; 1111 this.href = url.spec; 1112 this.pathname = url.filePath; 1113 this.port = (url.schemeIs("https") ? 443 : 80); 1114 this.protocol = url.scheme+":"; 1115 this.search = url.query ? "?"+url.query : ""; 1116 }; 1117 this.Location.prototype = { 1118 "toString":function() { 1119 return this.href; 1120 }, 1121 "__exposedProps__":{ 1122 "hash":"r", 1123 "host":"r", 1124 "hostname":"r", 1125 "href":"r", 1126 "pathname":"r", 1127 "port":"r", 1128 "protocol":"r", 1129 "search":"r", 1130 "toString":"r" 1131 } 1132 }; 1133 1134 /** 1135 * Mimics an HTMLWindow given an nsIURL 1136 * @param {nsIURL} url 1137 */ 1138 this.Window = function(url) { 1139 this._url = url; 1140 this.top = this; 1141 this.location = Zotero.HTTP.Location(url); 1142 }; 1143 this.Window.prototype.__exposedProps__ = { 1144 "top":"r", 1145 "location":"r" 1146 }; 1147 1148 /** 1149 * Wraps an HTMLDocument object returned by XMLHttpRequest DOMParser to make it look more like it belongs 1150 * to a browser. This is necessary if the document is to be passed to Zotero.Translate. 1151 * @param {HTMLDocument} doc Document returned by 1152 * @param {nsIURL|String} url 1153 */ 1154 this.wrapDocument = function(doc, url) { 1155 if(typeof url !== "object") { 1156 url = Services.io.newURI(url, null, null).QueryInterface(Components.interfaces.nsIURL); 1157 } 1158 return Zotero.Translate.DOMWrapper.wrap(doc, { 1159 "documentURI":url.spec, 1160 "URL":url.spec, 1161 "location":new Zotero.HTTP.Location(url), 1162 "defaultView":new Zotero.HTTP.Window(url) 1163 }); 1164 } 1165 }