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