/* Copyright 2012 Mozilla Foundation * * 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. */ // NOTE: Be careful what goes in this file, as it is also used from the context // of the addon. So using warn/error in here will break the addon. 'use strict'; (function (root, factory) { if (typeof define === 'function' && define.amd) { define('pdfjs/core/network', ['exports', 'pdfjs/shared/util', 'pdfjs/core/worker'], factory); } else if (typeof exports !== 'undefined') { factory(exports, require('../shared/util.js'), require('./worker.js')); } else { factory((root.pdfjsCoreNetwork = {}), root.pdfjsSharedUtil, root.pdfjsCoreWorker); } }(this, function (exports, sharedUtil, coreWorker) { if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('FIREFOX || MOZCENTRAL')) { throw new Error('Module "pdfjs/core/network" shall not ' + 'be used with FIREFOX or MOZCENTRAL build.'); } var OK_RESPONSE = 200; var PARTIAL_CONTENT_RESPONSE = 206; function NetworkManager(url, args) { this.url = url; args = args || {}; this.isHttp = /^https?:/i.test(url); this.httpHeaders = (this.isHttp && args.httpHeaders) || {}; this.withCredentials = args.withCredentials || false; this.getXhr = args.getXhr || function NetworkManager_getXhr() { return new XMLHttpRequest(); }; this.currXhrId = 0; this.pendingRequests = Object.create(null); this.loadedRequests = Object.create(null); } function getArrayBuffer(xhr) { var data = xhr.response; if (typeof data !== 'string') { return data; } var length = data.length; var array = new Uint8Array(length); for (var i = 0; i < length; i++) { array[i] = data.charCodeAt(i) & 0xFF; } return array.buffer; } var supportsMozChunked = typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') ? false : (function supportsMozChunkedClosure() { try { var x = new XMLHttpRequest(); // Firefox 37- required .open() to be called before setting responseType. // https://bugzilla.mozilla.org/show_bug.cgi?id=707484 // Even though the URL is not visited, .open() could fail if the URL is // blocked, e.g. via the connect-src CSP directive or the NoScript addon. // When this error occurs, this feature detection method will mistakenly // report that moz-chunked-arraybuffer is not supported in Firefox 37-. x.open('GET', 'https://example.com'); x.responseType = 'moz-chunked-arraybuffer'; return x.responseType === 'moz-chunked-arraybuffer'; } catch (e) { return false; } })(); NetworkManager.prototype = { requestRange: function NetworkManager_requestRange(begin, end, listeners) { var args = { begin: begin, end: end }; for (var prop in listeners) { args[prop] = listeners[prop]; } return this.request(args); }, requestFull: function NetworkManager_requestFull(listeners) { return this.request(listeners); }, request: function NetworkManager_request(args) { var xhr = this.getXhr(); var xhrId = this.currXhrId++; var pendingRequest = this.pendingRequests[xhrId] = { xhr: xhr }; xhr.open('GET', this.url); xhr.withCredentials = this.withCredentials; for (var property in this.httpHeaders) { var value = this.httpHeaders[property]; if (typeof value === 'undefined') { continue; } xhr.setRequestHeader(property, value); } if (this.isHttp && 'begin' in args && 'end' in args) { var rangeStr = args.begin + '-' + (args.end - 1); xhr.setRequestHeader('Range', 'bytes=' + rangeStr); pendingRequest.expectedStatus = 206; } else { pendingRequest.expectedStatus = 200; } var useMozChunkedLoading = supportsMozChunked && !!args.onProgressiveData; if (useMozChunkedLoading) { xhr.responseType = 'moz-chunked-arraybuffer'; pendingRequest.onProgressiveData = args.onProgressiveData; pendingRequest.mozChunked = true; } else { xhr.responseType = 'arraybuffer'; } if (args.onError) { xhr.onerror = function(evt) { args.onError(xhr.status); }; } xhr.onreadystatechange = this.onStateChange.bind(this, xhrId); xhr.onprogress = this.onProgress.bind(this, xhrId); pendingRequest.onHeadersReceived = args.onHeadersReceived; pendingRequest.onDone = args.onDone; pendingRequest.onError = args.onError; pendingRequest.onProgress = args.onProgress; xhr.send(null); return xhrId; }, onProgress: function NetworkManager_onProgress(xhrId, evt) { var pendingRequest = this.pendingRequests[xhrId]; if (!pendingRequest) { // Maybe abortRequest was called... return; } if (pendingRequest.mozChunked) { var chunk = getArrayBuffer(pendingRequest.xhr); pendingRequest.onProgressiveData(chunk); } var onProgress = pendingRequest.onProgress; if (onProgress) { onProgress(evt); } }, onStateChange: function NetworkManager_onStateChange(xhrId, evt) { var pendingRequest = this.pendingRequests[xhrId]; if (!pendingRequest) { // Maybe abortRequest was called... return; } var xhr = pendingRequest.xhr; if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) { pendingRequest.onHeadersReceived(); delete pendingRequest.onHeadersReceived; } if (xhr.readyState !== 4) { return; } if (!(xhrId in this.pendingRequests)) { // The XHR request might have been aborted in onHeadersReceived() // callback, in which case we should abort request return; } delete this.pendingRequests[xhrId]; // success status == 0 can be on ftp, file and other protocols if (xhr.status === 0 && this.isHttp) { if (pendingRequest.onError) { pendingRequest.onError(xhr.status); } return; } var xhrStatus = xhr.status || OK_RESPONSE; // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2: // "A server MAY ignore the Range header". This means it's possible to // get a 200 rather than a 206 response from a range request. var ok_response_on_range_request = xhrStatus === OK_RESPONSE && pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE; if (!ok_response_on_range_request && xhrStatus !== pendingRequest.expectedStatus) { if (pendingRequest.onError) { pendingRequest.onError(xhr.status); } return; } this.loadedRequests[xhrId] = true; var chunk = getArrayBuffer(xhr); if (xhrStatus === PARTIAL_CONTENT_RESPONSE) { var rangeHeader = xhr.getResponseHeader('Content-Range'); var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader); var begin = parseInt(matches[1], 10); pendingRequest.onDone({ begin: begin, chunk: chunk }); } else if (pendingRequest.onProgressiveData) { pendingRequest.onDone(null); } else if (chunk) { pendingRequest.onDone({ begin: 0, chunk: chunk }); } else if (pendingRequest.onError) { pendingRequest.onError(xhr.status); } }, hasPendingRequests: function NetworkManager_hasPendingRequests() { for (var xhrId in this.pendingRequests) { return true; } return false; }, getRequestXhr: function NetworkManager_getXhr(xhrId) { return this.pendingRequests[xhrId].xhr; }, isStreamingRequest: function NetworkManager_isStreamingRequest(xhrId) { return !!(this.pendingRequests[xhrId].onProgressiveData); }, isPendingRequest: function NetworkManager_isPendingRequest(xhrId) { return xhrId in this.pendingRequests; }, isLoadedRequest: function NetworkManager_isLoadedRequest(xhrId) { return xhrId in this.loadedRequests; }, abortAllRequests: function NetworkManager_abortAllRequests() { for (var xhrId in this.pendingRequests) { this.abortRequest(xhrId | 0); } }, abortRequest: function NetworkManager_abortRequest(xhrId) { var xhr = this.pendingRequests[xhrId].xhr; delete this.pendingRequests[xhrId]; xhr.abort(); } }; var assert = sharedUtil.assert; var createPromiseCapability = sharedUtil.createPromiseCapability; var isInt = sharedUtil.isInt; var MissingPDFException = sharedUtil.MissingPDFException; var UnexpectedResponseException = sharedUtil.UnexpectedResponseException; /** @implements {IPDFStream} */ function PDFNetworkStream(options) { this._options = options; var source = options.source; this._manager = new NetworkManager(source.url, { httpHeaders: source.httpHeaders, withCredentials: source.withCredentials }); this._rangeChunkSize = source.rangeChunkSize; this._fullRequestReader = null; this._rangeRequestReaders = []; } PDFNetworkStream.prototype = { _onRangeRequestReaderClosed: function PDFNetworkStream_onRangeRequestReaderClosed(reader) { var i = this._rangeRequestReaders.indexOf(reader); if (i >= 0) { this._rangeRequestReaders.splice(i, 1); } }, getFullReader: function PDFNetworkStream_getFullReader() { assert(!this._fullRequestReader); this._fullRequestReader = new PDFNetworkStreamFullRequestReader(this._manager, this._options); return this._fullRequestReader; }, getRangeReader: function PDFNetworkStream_getRangeReader(begin, end) { var reader = new PDFNetworkStreamRangeRequestReader(this._manager, begin, end); reader.onClosed = this._onRangeRequestReaderClosed.bind(this); this._rangeRequestReaders.push(reader); return reader; }, cancelAllRequests: function PDFNetworkStream_cancelAllRequests(reason) { if (this._fullRequestReader) { this._fullRequestReader.cancel(reason); } var readers = this._rangeRequestReaders.slice(0); readers.forEach(function (reader) { reader.cancel(reason); }); } }; /** @implements {IPDFStreamReader} */ function PDFNetworkStreamFullRequestReader(manager, options) { this._manager = manager; var source = options.source; var args = { onHeadersReceived: this._onHeadersReceived.bind(this), onProgressiveData: source.disableStream ? null : this._onProgressiveData.bind(this), onDone: this._onDone.bind(this), onError: this._onError.bind(this), onProgress: this._onProgress.bind(this) }; this._url = source.url; this._fullRequestId = manager.requestFull(args); this._headersReceivedCapability = createPromiseCapability(); this._disableRange = options.disableRange || false; this._contentLength = source.length; // optional this._rangeChunkSize = source.rangeChunkSize; if (!this._rangeChunkSize && !this._disableRange) { this._disableRange = true; } this._isStreamingSupported = false; this._isRangeSupported = false; this._cachedChunks = []; this._requests = []; this._done = false; this._storedError = undefined; this.onProgress = null; } PDFNetworkStreamFullRequestReader.prototype = { _validateRangeRequestCapabilities: function PDFNetworkStreamFullRequestReader_validateRangeRequestCapabilities() { if (this._disableRange) { return false; } var networkManager = this._manager; var fullRequestXhrId = this._fullRequestId; var fullRequestXhr = networkManager.getRequestXhr(fullRequestXhrId); if (fullRequestXhr.getResponseHeader('Accept-Ranges') !== 'bytes') { return false; } var contentEncoding = fullRequestXhr.getResponseHeader('Content-Encoding') || 'identity'; if (contentEncoding !== 'identity') { return false; } var length = fullRequestXhr.getResponseHeader('Content-Length'); length = parseInt(length, 10); if (!isInt(length)) { return false; } this._contentLength = length; // setting right content length if (length <= 2 * this._rangeChunkSize) { // The file size is smaller than the size of two chunks, so it does // not make any sense to abort the request and retry with a range // request. return false; } return true; }, _onHeadersReceived: function PDFNetworkStreamFullRequestReader_onHeadersReceived() { if (this._validateRangeRequestCapabilities()) { this._isRangeSupported = true; } var networkManager = this._manager; var fullRequestXhrId = this._fullRequestId; if (networkManager.isStreamingRequest(fullRequestXhrId)) { // We can continue fetching when progressive loading is enabled, // and we don't need the autoFetch feature. this._isStreamingSupported = true; } else if (this._isRangeSupported) { // NOTE: by cancelling the full request, and then issuing range // requests, there will be an issue for sites where you can only // request the pdf once. However, if this is the case, then the // server should not be returning that it can support range // requests. networkManager.abortRequest(fullRequestXhrId); } this._headersReceivedCapability.resolve(); }, _onProgressiveData: function PDFNetworkStreamFullRequestReader_onProgressiveData(chunk) { if (this._requests.length > 0) { var requestCapability = this._requests.shift(); requestCapability.resolve({value: chunk, done: false}); } else { this._cachedChunks.push(chunk); } }, _onDone: function PDFNetworkStreamFullRequestReader_onDone(args) { if (args) { this._onProgressiveData(args.chunk); } this._done = true; if (this._cachedChunks.length > 0) { return; } this._requests.forEach(function (requestCapability) { requestCapability.resolve({value: undefined, done: true}); }); this._requests = []; }, _onError: function PDFNetworkStreamFullRequestReader_onError(status) { var url = this._url; var exception; if (status === 404 || status === 0 && /^file:/.test(url)) { exception = new MissingPDFException('Missing PDF "' + url + '".'); } else { exception = new UnexpectedResponseException( 'Unexpected server response (' + status + ') while retrieving PDF "' + url + '".', status); } this._storedError = exception; this._headersReceivedCapability.reject(exception); this._requests.forEach(function (requestCapability) { requestCapability.reject(exception); }); this._requests = []; this._cachedChunks = []; }, _onProgress: function PDFNetworkStreamFullRequestReader_onProgress(data) { if (this.onProgress) { this.onProgress({ loaded: data.loaded, total: data.lengthComputable ? data.total : this._contentLength }); } }, get isRangeSupported() { return this._isRangeSupported; }, get isStreamingSupported() { return this._isStreamingSupported; }, get contentLength() { return this._contentLength; }, get headersReady() { return this._headersReceivedCapability.promise; }, read: function PDFNetworkStreamFullRequestReader_read() { if (this._storedError) { return Promise.reject(this._storedError); } if (this._cachedChunks.length > 0) { var chunk = this._cachedChunks.shift(); return Promise.resolve(chunk); } if (this._done) { return Promise.resolve({value: undefined, done: true}); } var requestCapability = createPromiseCapability(); this._requests.push(requestCapability); return requestCapability.promise; }, cancel: function PDFNetworkStreamFullRequestReader_cancel(reason) { this._done = true; this._headersReceivedCapability.reject(reason); this._requests.forEach(function (requestCapability) { requestCapability.resolve({value: undefined, done: true}); }); this._requests = []; if (this._manager.isPendingRequest(this._fullRequestId)) { this._manager.abortRequest(this._fullRequestId); } this._fullRequestReader = null; } }; /** @implements {IPDFStreamRangeReader} */ function PDFNetworkStreamRangeRequestReader(manager, begin, end) { this._manager = manager; var args = { onDone: this._onDone.bind(this), onProgress: this._onProgress.bind(this) }; this._requestId = manager.requestRange(begin, end, args); this._requests = []; this._queuedChunk = null; this._done = false; this.onProgress = null; this.onClosed = null; } PDFNetworkStreamRangeRequestReader.prototype = { _close: function PDFNetworkStreamRangeRequestReader_close() { if (this.onClosed) { this.onClosed(this); } }, _onDone: function PDFNetworkStreamRangeRequestReader_onDone(data) { var chunk = data.chunk; if (this._requests.length > 0) { var requestCapability = this._requests.shift(); requestCapability.resolve({value: chunk, done: false}); } else { this._queuedChunk = chunk; } this._done = true; this._requests.forEach(function (requestCapability) { requestCapability.resolve({value: undefined, done: true}); }); this._requests = []; this._close(); }, _onProgress: function PDFNetworkStreamRangeRequestReader_onProgress(evt) { if (!this.isStreamingSupported && this.onProgress) { this.onProgress({ loaded: evt.loaded }); } }, get isStreamingSupported() { return false; // TODO allow progressive range bytes loading }, read: function PDFNetworkStreamRangeRequestReader_read() { if (this._queuedChunk !== null) { var chunk = this._queuedChunk; this._queuedChunk = null; return Promise.resolve({value: chunk, done: false}); } if (this._done) { return Promise.resolve({value: undefined, done: true}); } var requestCapability = createPromiseCapability(); this._requests.push(requestCapability); return requestCapability.promise; }, cancel: function PDFNetworkStreamRangeRequestReader_cancel(reason) { this._done = true; this._requests.forEach(function (requestCapability) { requestCapability.resolve({value: undefined, done: true}); }); this._requests = []; if (this._manager.isPendingRequest(this._requestId)) { this._manager.abortRequest(this._requestId); } this._close(); } }; coreWorker.setPDFNetworkStreamClass(PDFNetworkStream); exports.PDFNetworkStream = PDFNetworkStream; exports.NetworkManager = NetworkManager; }));