|
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ |
|
3 /* Copyright 2012 Mozilla Foundation |
|
4 * |
|
5 * Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 * you may not use this file except in compliance with the License. |
|
7 * You may obtain a copy of the License at |
|
8 * |
|
9 * http://www.apache.org/licenses/LICENSE-2.0 |
|
10 * |
|
11 * Unless required by applicable law or agreed to in writing, software |
|
12 * distributed under the License is distributed on an "AS IS" BASIS, |
|
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 * See the License for the specific language governing permissions and |
|
15 * limitations under the License. |
|
16 */ |
|
17 |
|
18 // NOTE: Be careful what goes in this file, as it is also used from the context |
|
19 // of the addon. So using warn/error in here will break the addon. |
|
20 |
|
21 'use strict'; |
|
22 |
|
23 |
|
24 |
|
25 Components.utils.import('resource://gre/modules/Services.jsm'); |
|
26 |
|
27 var EXPORTED_SYMBOLS = ['NetworkManager']; |
|
28 |
|
29 var console = { |
|
30 log: function console_log(aMsg) { |
|
31 var msg = 'network.js: ' + (aMsg.join ? aMsg.join('') : aMsg); |
|
32 Services.console.logStringMessage(msg); |
|
33 // TODO(mack): dump() doesn't seem to work here... |
|
34 dump(msg + '\n'); |
|
35 } |
|
36 } |
|
37 |
|
38 var NetworkManager = (function NetworkManagerClosure() { |
|
39 |
|
40 var OK_RESPONSE = 200; |
|
41 var PARTIAL_CONTENT_RESPONSE = 206; |
|
42 |
|
43 function NetworkManager(url, args) { |
|
44 this.url = url; |
|
45 args = args || {}; |
|
46 this.isHttp = /^https?:/i.test(url); |
|
47 this.httpHeaders = (this.isHttp && args.httpHeaders) || {}; |
|
48 this.withCredentials = args.withCredentials || false; |
|
49 this.getXhr = args.getXhr || |
|
50 function NetworkManager_getXhr() { |
|
51 return new XMLHttpRequest(); |
|
52 }; |
|
53 |
|
54 this.currXhrId = 0; |
|
55 this.pendingRequests = {}; |
|
56 this.loadedRequests = {}; |
|
57 } |
|
58 |
|
59 function getArrayBuffer(xhr) { |
|
60 var data = (xhr.mozResponseArrayBuffer || xhr.mozResponse || |
|
61 xhr.responseArrayBuffer || xhr.response); |
|
62 if (typeof data !== 'string') { |
|
63 return data; |
|
64 } |
|
65 var length = data.length; |
|
66 var buffer = new Uint8Array(length); |
|
67 for (var i = 0; i < length; i++) { |
|
68 buffer[i] = data.charCodeAt(i) & 0xFF; |
|
69 } |
|
70 return buffer; |
|
71 } |
|
72 |
|
73 NetworkManager.prototype = { |
|
74 requestRange: function NetworkManager_requestRange(begin, end, listeners) { |
|
75 var args = { |
|
76 begin: begin, |
|
77 end: end |
|
78 }; |
|
79 for (var prop in listeners) { |
|
80 args[prop] = listeners[prop]; |
|
81 } |
|
82 return this.request(args); |
|
83 }, |
|
84 |
|
85 requestFull: function NetworkManager_requestRange(listeners) { |
|
86 return this.request(listeners); |
|
87 }, |
|
88 |
|
89 request: function NetworkManager_requestRange(args) { |
|
90 var xhr = this.getXhr(); |
|
91 var xhrId = this.currXhrId++; |
|
92 var pendingRequest = this.pendingRequests[xhrId] = { |
|
93 xhr: xhr |
|
94 }; |
|
95 |
|
96 xhr.open('GET', this.url); |
|
97 xhr.withCredentials = this.withCredentials; |
|
98 for (var property in this.httpHeaders) { |
|
99 var value = this.httpHeaders[property]; |
|
100 if (typeof value === 'undefined') { |
|
101 continue; |
|
102 } |
|
103 xhr.setRequestHeader(property, value); |
|
104 } |
|
105 if (this.isHttp && 'begin' in args && 'end' in args) { |
|
106 var rangeStr = args.begin + '-' + (args.end - 1); |
|
107 xhr.setRequestHeader('Range', 'bytes=' + rangeStr); |
|
108 pendingRequest.expectedStatus = 206; |
|
109 } else { |
|
110 pendingRequest.expectedStatus = 200; |
|
111 } |
|
112 |
|
113 xhr.mozResponseType = xhr.responseType = 'arraybuffer'; |
|
114 |
|
115 if (args.onProgress) { |
|
116 xhr.onprogress = args.onProgress; |
|
117 } |
|
118 if (args.onError) { |
|
119 xhr.onerror = function(evt) { |
|
120 args.onError(xhr.status); |
|
121 }; |
|
122 } |
|
123 xhr.onreadystatechange = this.onStateChange.bind(this, xhrId); |
|
124 |
|
125 pendingRequest.onHeadersReceived = args.onHeadersReceived; |
|
126 pendingRequest.onDone = args.onDone; |
|
127 pendingRequest.onError = args.onError; |
|
128 |
|
129 xhr.send(null); |
|
130 |
|
131 return xhrId; |
|
132 }, |
|
133 |
|
134 onStateChange: function NetworkManager_onStateChange(xhrId, evt) { |
|
135 var pendingRequest = this.pendingRequests[xhrId]; |
|
136 if (!pendingRequest) { |
|
137 // Maybe abortRequest was called... |
|
138 return; |
|
139 } |
|
140 |
|
141 var xhr = pendingRequest.xhr; |
|
142 if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) { |
|
143 pendingRequest.onHeadersReceived(); |
|
144 delete pendingRequest.onHeadersReceived; |
|
145 } |
|
146 |
|
147 if (xhr.readyState !== 4) { |
|
148 return; |
|
149 } |
|
150 |
|
151 if (!(xhrId in this.pendingRequests)) { |
|
152 // The XHR request might have been aborted in onHeadersReceived() |
|
153 // callback, in which case we should abort request |
|
154 return; |
|
155 } |
|
156 |
|
157 delete this.pendingRequests[xhrId]; |
|
158 |
|
159 // success status == 0 can be on ftp, file and other protocols |
|
160 if (xhr.status === 0 && this.isHttp) { |
|
161 if (pendingRequest.onError) { |
|
162 pendingRequest.onError(xhr.status); |
|
163 } |
|
164 return; |
|
165 } |
|
166 var xhrStatus = xhr.status || OK_RESPONSE; |
|
167 |
|
168 // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2: |
|
169 // "A server MAY ignore the Range header". This means it's possible to |
|
170 // get a 200 rather than a 206 response from a range request. |
|
171 var ok_response_on_range_request = |
|
172 xhrStatus === OK_RESPONSE && |
|
173 pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE; |
|
174 |
|
175 if (!ok_response_on_range_request && |
|
176 xhrStatus !== pendingRequest.expectedStatus) { |
|
177 if (pendingRequest.onError) { |
|
178 pendingRequest.onError(xhr.status); |
|
179 } |
|
180 return; |
|
181 } |
|
182 |
|
183 this.loadedRequests[xhrId] = true; |
|
184 |
|
185 var chunk = getArrayBuffer(xhr); |
|
186 if (xhrStatus === PARTIAL_CONTENT_RESPONSE) { |
|
187 var rangeHeader = xhr.getResponseHeader('Content-Range'); |
|
188 var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader); |
|
189 var begin = parseInt(matches[1], 10); |
|
190 pendingRequest.onDone({ |
|
191 begin: begin, |
|
192 chunk: chunk |
|
193 }); |
|
194 } else { |
|
195 pendingRequest.onDone({ |
|
196 begin: 0, |
|
197 chunk: chunk |
|
198 }); |
|
199 } |
|
200 }, |
|
201 |
|
202 hasPendingRequests: function NetworkManager_hasPendingRequests() { |
|
203 for (var xhrId in this.pendingRequests) { |
|
204 return true; |
|
205 } |
|
206 return false; |
|
207 }, |
|
208 |
|
209 getRequestXhr: function NetworkManager_getXhr(xhrId) { |
|
210 return this.pendingRequests[xhrId].xhr; |
|
211 }, |
|
212 |
|
213 isPendingRequest: function NetworkManager_isPendingRequest(xhrId) { |
|
214 return xhrId in this.pendingRequests; |
|
215 }, |
|
216 |
|
217 isLoadedRequest: function NetworkManager_isLoadedRequest(xhrId) { |
|
218 return xhrId in this.loadedRequests; |
|
219 }, |
|
220 |
|
221 abortAllRequests: function NetworkManager_abortAllRequests() { |
|
222 for (var xhrId in this.pendingRequests) { |
|
223 this.abortRequest(xhrId | 0); |
|
224 } |
|
225 }, |
|
226 |
|
227 abortRequest: function NetworkManager_abortRequest(xhrId) { |
|
228 var xhr = this.pendingRequests[xhrId].xhr; |
|
229 delete this.pendingRequests[xhrId]; |
|
230 xhr.abort(); |
|
231 } |
|
232 }; |
|
233 |
|
234 return NetworkManager; |
|
235 })(); |
|
236 |
|
237 |