Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / image_loader / request.js
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /**
6  * @typedef {{
7  *   cache: (boolean|undefined),
8  *   priority: (number|undefined),
9  *   taskId: number,
10  *   timestamp: (number|undefined),
11  *   url: string
12  * }}
13  */
14 var LoadImageRequest;
15
16 /**
17  * Creates and starts downloading and then resizing of the image. Finally,
18  * returns the image using the callback.
19  *
20  * @param {string} id Request ID.
21  * @param {Cache} cache Cache object.
22  * @param {LoadImageRequest} request Request message as a hash array.
23  * @param {function(Object)} callback Callback used to send the response.
24  * @constructor
25  */
26 function Request(id, cache, request, callback) {
27   /**
28    * @type {string}
29    * @private
30    */
31   this.id_ = id;
32
33   /**
34    * @type {Cache}
35    * @private
36    */
37   this.cache_ = cache;
38
39   /**
40    * @type {LoadImageRequest}
41    * @private
42    */
43   this.request_ = request;
44
45   /**
46    * @type {function(Object)}
47    * @private
48    */
49   this.sendResponse_ = callback;
50
51   /**
52    * Temporary image used to download images.
53    * @type {Image}
54    * @private
55    */
56   this.image_ = new Image();
57
58   /**
59    * MIME type of the fetched image.
60    * @type {?string}
61    * @private
62    */
63   this.contentType_ = null;
64
65   /**
66    * Used to download remote images using http:// or https:// protocols.
67    * @type {AuthorizedXHR}
68    * @private
69    */
70   this.xhr_ = new AuthorizedXHR();
71
72   /**
73    * Temporary canvas used to resize and compress the image.
74    * @type {HTMLCanvasElement}
75    * @private
76    */
77   this.canvas_ =
78       /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
79
80   /**
81    * @type {CanvasRenderingContext2D}
82    * @private
83    */
84   this.context_ =
85       /** @type {CanvasRenderingContext2D} */ (this.canvas_.getContext('2d'));
86
87   /**
88    * Callback to be called once downloading is finished.
89    * @type {?function()}
90    * @private
91    */
92   this.downloadCallback_ = null;
93 }
94
95 /**
96  * Returns ID of the request.
97  * @return {string} Request ID.
98  */
99 Request.prototype.getId = function() {
100   return this.id_;
101 };
102
103 /**
104  * Returns priority of the request. The higher priority, the faster it will
105  * be handled. The highest priority is 0. The default one is 2.
106  *
107  * @return {number} Priority.
108  */
109 Request.prototype.getPriority = function() {
110   return (this.request_.priority !== undefined) ? this.request_.priority : 2;
111 };
112
113 /**
114  * Tries to load the image from cache if exists and sends the response.
115  *
116  * @param {function()} onSuccess Success callback.
117  * @param {function()} onFailure Failure callback.
118  */
119 Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
120   this.loadFromCache_(
121       function(data) {  // Found in cache.
122         this.sendImageData_(data);
123         onSuccess();
124       }.bind(this),
125       onFailure);  // Not found in cache.
126 };
127
128 /**
129  * Tries to download the image, resizes and sends the response.
130  * @param {function()} callback Completion callback.
131  */
132 Request.prototype.downloadAndProcess = function(callback) {
133   if (this.downloadCallback_)
134     throw new Error('Downloading already started.');
135
136   this.downloadCallback_ = callback;
137   this.downloadOriginal_(this.onImageLoad_.bind(this),
138                          this.onImageError_.bind(this));
139 };
140
141 /**
142  * Fetches the image from the persistent cache.
143  *
144  * @param {function(string)} onSuccess Success callback.
145  * @param {function()} onFailure Failure callback.
146  * @private
147  */
148 Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
149   var cacheKey = Cache.createKey(this.request_);
150
151   if (!this.request_.cache) {
152     // Cache is disabled for this request; therefore, remove it from cache
153     // if existed.
154     this.cache_.removeImage(cacheKey);
155     onFailure();
156     return;
157   }
158
159   if (!this.request_.timestamp) {
160     // Persistent cache is available only when a timestamp is provided.
161     onFailure();
162     return;
163   }
164
165   this.cache_.loadImage(cacheKey,
166                         this.request_.timestamp,
167                         onSuccess,
168                         onFailure);
169 };
170
171 /**
172  * Saves the image to the persistent cache.
173  *
174  * @param {string} data The image's data.
175  * @private
176  */
177 Request.prototype.saveToCache_ = function(data) {
178   if (!this.request_.cache || !this.request_.timestamp) {
179     // Persistent cache is available only when a timestamp is provided.
180     return;
181   }
182
183   var cacheKey = Cache.createKey(this.request_);
184   this.cache_.saveImage(cacheKey,
185                         data,
186                         this.request_.timestamp);
187 };
188
189 /**
190  * Downloads an image directly or for remote resources using the XmlHttpRequest.
191  *
192  * @param {function()} onSuccess Success callback.
193  * @param {function()} onFailure Failure callback.
194  * @private
195  */
196 Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
197   this.image_.onload = function() {
198     URL.revokeObjectURL(this.image_.src);
199     onSuccess();
200   }.bind(this);
201   this.image_.onerror = function() {
202     URL.revokeObjectURL(this.image_.src);
203     onFailure();
204   }.bind(this);
205
206   // Download data urls directly since they are not supported by XmlHttpRequest.
207   var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/);
208   if (dataUrlMatches) {
209     this.image_.src = this.request_.url;
210     this.contentType_ = dataUrlMatches[1];
211     return;
212   }
213
214   // Fetch the image via authorized XHR and parse it.
215   var parseImage = function(contentType, blob) {
216     this.image_.src = URL.createObjectURL(blob);
217   }.bind(this);
218
219   // Request raw data via XHR.
220   this.xhr_.load(this.request_.url, parseImage, onFailure);
221 };
222
223 /**
224  * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers.
225  * @constructor
226  */
227 function AuthorizedXHR() {
228   this.xhr_ = null;
229   this.aborted_ = false;
230 }
231
232 /**
233  * Aborts the current request (if running).
234  */
235 AuthorizedXHR.prototype.abort = function() {
236   this.aborted_ = true;
237   if (this.xhr_)
238     this.xhr_.abort();
239 };
240
241 /**
242  * Loads an image using a OAuth2 token. If it fails, then tries to retry with
243  * a refreshed OAuth2 token.
244  *
245  * @param {string} url URL to the resource to be fetched.
246  * @param {function(string, Blob)} onSuccess Success callback with the content
247  *     type and the fetched data.
248  * @param {function()} onFailure Failure callback.
249  */
250 AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) {
251   this.aborted_ = false;
252
253   // Do not call any callbacks when aborting.
254   var onMaybeSuccess = /** @type {function(string, Blob)} */ (
255       function(contentType, response) {
256         if (!this.aborted_)
257           onSuccess(contentType, response);
258       }.bind(this));
259
260   var onMaybeFailure = /** @type {function(number=)} */ (
261       function(opt_code) {
262         if (!this.aborted_)
263           onFailure();
264       }.bind(this));
265
266   // Fetches the access token and makes an authorized call. If refresh is true,
267   // then forces refreshing the access token.
268   var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) {
269     chrome.fileManagerPrivate.requestAccessToken(refresh, function(token) {
270       if (this.aborted_)
271         return;
272       if (!token) {
273         onInnerFailure();
274         return;
275       }
276       this.xhr_ = AuthorizedXHR.load_(
277           token, url, onInnerSuccess, onInnerFailure);
278     }.bind(this));
279   }.bind(this);
280
281   // Refreshes the access token and retries the request.
282   var maybeRetryCall = function(code) {
283     if (this.aborted_)
284       return;
285     requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
286   }.bind(this);
287
288   // Do not request a token for local resources, since it is not necessary.
289   if (/^filesystem:/.test(url)) {
290     // The query parameter is workaround for
291     // crbug.com/379678, which force to obtain the latest contents of the image.
292     var noCacheUrl = url + '?nocache=' + Date.now();
293     this.xhr_ = AuthorizedXHR.load_(
294         null,
295         noCacheUrl,
296         onMaybeSuccess,
297         onMaybeFailure);
298     return;
299   }
300
301   // Make the request with reusing the current token. If it fails, then retry.
302   requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
303 };
304
305 /**
306  * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
307  * If the token is invalid, the request will fail.
308  *
309  * @param {?string} token OAuth2 token to be injected to the request. Null for
310  *     no token.
311  * @param {string} url URL to the resource to be fetched.
312  * @param {function(string, Blob)} onSuccess Success callback with the content
313  *     type and the fetched data.
314  * @param {function(number=)} onFailure Failure callback with the error code
315  *     if available.
316  * @return {XMLHttpRequest} XHR instance.
317  * @private
318  */
319 AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
320   var xhr = new XMLHttpRequest();
321   xhr.responseType = 'blob';
322
323   xhr.onreadystatechange = function() {
324     if (xhr.readyState != 4)
325       return;
326     if (xhr.status != 200) {
327       onFailure(xhr.status);
328       return;
329     }
330     var contentType = xhr.getResponseHeader('Content-Type');
331     onSuccess(contentType, /** @type {Blob} */ (xhr.response));
332   }.bind(this);
333
334   // Perform a xhr request.
335   try {
336     xhr.open('GET', url, true);
337     if (token)
338       xhr.setRequestHeader('Authorization', 'Bearer ' + token);
339     xhr.send();
340   } catch (e) {
341     onFailure();
342   }
343
344   return xhr;
345 };
346
347 /**
348  * Sends the resized image via the callback. If the image has been changed,
349  * then packs the canvas contents, otherwise sends the raw image data.
350  *
351  * @param {boolean} imageChanged Whether the image has been changed.
352  * @private
353  */
354 Request.prototype.sendImage_ = function(imageChanged) {
355   var imageData;
356   if (!imageChanged) {
357     // The image hasn't been processed, so the raw data can be directly
358     // forwarded for speed (no need to encode the image again).
359     imageData = this.image_.src;
360   } else {
361     // The image has been resized or rotated, therefore the canvas has to be
362     // encoded to get the correct compressed image data.
363     switch (this.contentType_) {
364       case 'image/gif':
365       case 'image/png':
366       case 'image/svg':
367       case 'image/bmp':
368         imageData = this.canvas_.toDataURL('image/png');
369         break;
370       case 'image/jpeg':
371       default:
372         imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
373     }
374   }
375
376   // Send and store in the persistent cache.
377   this.sendImageData_(imageData);
378   this.saveToCache_(imageData);
379 };
380
381 /**
382  * Sends the resized image via the callback.
383  * @param {string} data Compressed image data.
384  * @private
385  */
386 Request.prototype.sendImageData_ = function(data) {
387   this.sendResponse_(
388       {status: 'success', data: data, taskId: this.request_.taskId});
389 };
390
391 /**
392  * Handler, when contents are loaded into the image element. Performs resizing
393  * and finalizes the request process.
394  * @private
395  */
396 Request.prototype.onImageLoad_ = function() {
397   // Perform processing if the url is not a data url, or if there are some
398   // operations requested.
399   if (!this.request_.url.match(/^data/) ||
400       ImageLoader.shouldProcess(this.image_.width,
401                                 this.image_.height,
402                                 this.request_)) {
403     ImageLoader.resize(this.image_, this.canvas_, this.request_);
404     this.sendImage_(true);  // Image changed.
405   } else {
406     this.sendImage_(false);  // Image not changed.
407   }
408   this.cleanup_();
409   this.downloadCallback_();
410 };
411
412 /**
413  * Handler, when loading of the image fails. Sends a failure response and
414  * finalizes the request process.
415  * @private
416  */
417 Request.prototype.onImageError_ = function() {
418   this.sendResponse_(
419       {status: 'error', taskId: this.request_.taskId});
420   this.cleanup_();
421   this.downloadCallback_();
422 };
423
424 /**
425  * Cancels the request.
426  */
427 Request.prototype.cancel = function() {
428   this.cleanup_();
429
430   // If downloading has started, then call the callback.
431   if (this.downloadCallback_)
432     this.downloadCallback_();
433 };
434
435 /**
436  * Cleans up memory used by this request.
437  * @private
438  */
439 Request.prototype.cleanup_ = function() {
440   this.image_.onerror = function() {};
441   this.image_.onload = function() {};
442
443   // Transparent 1x1 pixel gif, to force garbage collecting.
444   this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
445       'ABAAEAAAICTAEAOw==';
446
447   this.xhr_.onload = function() {};
448   this.xhr_.abort();
449
450   // Dispose memory allocated by Canvas.
451   this.canvas_.width = 0;
452   this.canvas_.height = 0;
453 };