michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ michael@0: /* Copyright 2012 Mozilla Foundation michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: michael@0: // NOTE: Be careful what goes in this file, as it is also used from the context michael@0: // of the addon. So using warn/error in here will break the addon. michael@0: michael@0: 'use strict'; michael@0: michael@0: michael@0: michael@0: Components.utils.import('resource://gre/modules/Services.jsm'); michael@0: michael@0: var EXPORTED_SYMBOLS = ['NetworkManager']; michael@0: michael@0: var console = { michael@0: log: function console_log(aMsg) { michael@0: var msg = 'network.js: ' + (aMsg.join ? aMsg.join('') : aMsg); michael@0: Services.console.logStringMessage(msg); michael@0: // TODO(mack): dump() doesn't seem to work here... michael@0: dump(msg + '\n'); michael@0: } michael@0: } michael@0: michael@0: var NetworkManager = (function NetworkManagerClosure() { michael@0: michael@0: var OK_RESPONSE = 200; michael@0: var PARTIAL_CONTENT_RESPONSE = 206; michael@0: michael@0: function NetworkManager(url, args) { michael@0: this.url = url; michael@0: args = args || {}; michael@0: this.isHttp = /^https?:/i.test(url); michael@0: this.httpHeaders = (this.isHttp && args.httpHeaders) || {}; michael@0: this.withCredentials = args.withCredentials || false; michael@0: this.getXhr = args.getXhr || michael@0: function NetworkManager_getXhr() { michael@0: return new XMLHttpRequest(); michael@0: }; michael@0: michael@0: this.currXhrId = 0; michael@0: this.pendingRequests = {}; michael@0: this.loadedRequests = {}; michael@0: } michael@0: michael@0: function getArrayBuffer(xhr) { michael@0: var data = (xhr.mozResponseArrayBuffer || xhr.mozResponse || michael@0: xhr.responseArrayBuffer || xhr.response); michael@0: if (typeof data !== 'string') { michael@0: return data; michael@0: } michael@0: var length = data.length; michael@0: var buffer = new Uint8Array(length); michael@0: for (var i = 0; i < length; i++) { michael@0: buffer[i] = data.charCodeAt(i) & 0xFF; michael@0: } michael@0: return buffer; michael@0: } michael@0: michael@0: NetworkManager.prototype = { michael@0: requestRange: function NetworkManager_requestRange(begin, end, listeners) { michael@0: var args = { michael@0: begin: begin, michael@0: end: end michael@0: }; michael@0: for (var prop in listeners) { michael@0: args[prop] = listeners[prop]; michael@0: } michael@0: return this.request(args); michael@0: }, michael@0: michael@0: requestFull: function NetworkManager_requestRange(listeners) { michael@0: return this.request(listeners); michael@0: }, michael@0: michael@0: request: function NetworkManager_requestRange(args) { michael@0: var xhr = this.getXhr(); michael@0: var xhrId = this.currXhrId++; michael@0: var pendingRequest = this.pendingRequests[xhrId] = { michael@0: xhr: xhr michael@0: }; michael@0: michael@0: xhr.open('GET', this.url); michael@0: xhr.withCredentials = this.withCredentials; michael@0: for (var property in this.httpHeaders) { michael@0: var value = this.httpHeaders[property]; michael@0: if (typeof value === 'undefined') { michael@0: continue; michael@0: } michael@0: xhr.setRequestHeader(property, value); michael@0: } michael@0: if (this.isHttp && 'begin' in args && 'end' in args) { michael@0: var rangeStr = args.begin + '-' + (args.end - 1); michael@0: xhr.setRequestHeader('Range', 'bytes=' + rangeStr); michael@0: pendingRequest.expectedStatus = 206; michael@0: } else { michael@0: pendingRequest.expectedStatus = 200; michael@0: } michael@0: michael@0: xhr.mozResponseType = xhr.responseType = 'arraybuffer'; michael@0: michael@0: if (args.onProgress) { michael@0: xhr.onprogress = args.onProgress; michael@0: } michael@0: if (args.onError) { michael@0: xhr.onerror = function(evt) { michael@0: args.onError(xhr.status); michael@0: }; michael@0: } michael@0: xhr.onreadystatechange = this.onStateChange.bind(this, xhrId); michael@0: michael@0: pendingRequest.onHeadersReceived = args.onHeadersReceived; michael@0: pendingRequest.onDone = args.onDone; michael@0: pendingRequest.onError = args.onError; michael@0: michael@0: xhr.send(null); michael@0: michael@0: return xhrId; michael@0: }, michael@0: michael@0: onStateChange: function NetworkManager_onStateChange(xhrId, evt) { michael@0: var pendingRequest = this.pendingRequests[xhrId]; michael@0: if (!pendingRequest) { michael@0: // Maybe abortRequest was called... michael@0: return; michael@0: } michael@0: michael@0: var xhr = pendingRequest.xhr; michael@0: if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) { michael@0: pendingRequest.onHeadersReceived(); michael@0: delete pendingRequest.onHeadersReceived; michael@0: } michael@0: michael@0: if (xhr.readyState !== 4) { michael@0: return; michael@0: } michael@0: michael@0: if (!(xhrId in this.pendingRequests)) { michael@0: // The XHR request might have been aborted in onHeadersReceived() michael@0: // callback, in which case we should abort request michael@0: return; michael@0: } michael@0: michael@0: delete this.pendingRequests[xhrId]; michael@0: michael@0: // success status == 0 can be on ftp, file and other protocols michael@0: if (xhr.status === 0 && this.isHttp) { michael@0: if (pendingRequest.onError) { michael@0: pendingRequest.onError(xhr.status); michael@0: } michael@0: return; michael@0: } michael@0: var xhrStatus = xhr.status || OK_RESPONSE; michael@0: michael@0: // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2: michael@0: // "A server MAY ignore the Range header". This means it's possible to michael@0: // get a 200 rather than a 206 response from a range request. michael@0: var ok_response_on_range_request = michael@0: xhrStatus === OK_RESPONSE && michael@0: pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE; michael@0: michael@0: if (!ok_response_on_range_request && michael@0: xhrStatus !== pendingRequest.expectedStatus) { michael@0: if (pendingRequest.onError) { michael@0: pendingRequest.onError(xhr.status); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: this.loadedRequests[xhrId] = true; michael@0: michael@0: var chunk = getArrayBuffer(xhr); michael@0: if (xhrStatus === PARTIAL_CONTENT_RESPONSE) { michael@0: var rangeHeader = xhr.getResponseHeader('Content-Range'); michael@0: var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader); michael@0: var begin = parseInt(matches[1], 10); michael@0: pendingRequest.onDone({ michael@0: begin: begin, michael@0: chunk: chunk michael@0: }); michael@0: } else { michael@0: pendingRequest.onDone({ michael@0: begin: 0, michael@0: chunk: chunk michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: hasPendingRequests: function NetworkManager_hasPendingRequests() { michael@0: for (var xhrId in this.pendingRequests) { michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: getRequestXhr: function NetworkManager_getXhr(xhrId) { michael@0: return this.pendingRequests[xhrId].xhr; michael@0: }, michael@0: michael@0: isPendingRequest: function NetworkManager_isPendingRequest(xhrId) { michael@0: return xhrId in this.pendingRequests; michael@0: }, michael@0: michael@0: isLoadedRequest: function NetworkManager_isLoadedRequest(xhrId) { michael@0: return xhrId in this.loadedRequests; michael@0: }, michael@0: michael@0: abortAllRequests: function NetworkManager_abortAllRequests() { michael@0: for (var xhrId in this.pendingRequests) { michael@0: this.abortRequest(xhrId | 0); michael@0: } michael@0: }, michael@0: michael@0: abortRequest: function NetworkManager_abortRequest(xhrId) { michael@0: var xhr = this.pendingRequests[xhrId].xhr; michael@0: delete this.pendingRequests[xhrId]; michael@0: xhr.abort(); michael@0: } michael@0: }; michael@0: michael@0: return NetworkManager; michael@0: })(); michael@0: michael@0: