/* 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'; Components.utils.import('resource://gre/modules/Services.jsm'); var EXPORTED_SYMBOLS = ['NetworkManager']; var console = { log: function console_log(aMsg) { var msg = 'network.js: ' + (aMsg.join ? aMsg.join('') : aMsg); Services.console.logStringMessage(msg); // TODO(mack): dump() doesn't seem to work here... dump(msg + '\n'); } } var NetworkManager = (function NetworkManagerClosure() { 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 = {}; this.loadedRequests = {}; } 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; } 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; xhr.channel.QueryInterface(Ci.nsIHttpChannel).redirectionLimit = 0; } else { pendingRequest.expectedStatus = 200; } var useMozChunkedLoading = !!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(); } }; return NetworkManager; })();