Upstream version 10.39.225.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.fileManagerPrivate.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 (/^filesystem:/.test(url)) {
277     // The query parameter is workaround for
278     // crbug.com/379678, which force to obtain the latest contents of the image.
279     var noCacheUrl = url + '?nocache=' + Date.now();
280     this.xhr_ = AuthorizedXHR.load_(
281         null,
282         noCacheUrl,
283         onMaybeSuccess,
284         onMaybeFailure);
285     return;
286   }
287
288   // Make the request with reusing the current token. If it fails, then retry.
289   requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
290 };
291
292 /**
293  * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
294  * If the token is invalid, the request will fail.
295  *
296  * @param {?string} token OAuth2 token to be injected to the request. Null for
297  *     no token.
298  * @param {string} url URL to the resource to be fetched.
299  * @param {function(string, Blob}) onSuccess Success callback with the content
300  *     type and the fetched data.
301  * @param {function(number=)} onFailure Failure callback with the error code
302  *     if available.
303  * @return {AuthorizedXHR} XHR instance.
304  * @private
305  */
306 AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
307   var xhr = new XMLHttpRequest();
308   xhr.responseType = 'blob';
309
310   xhr.onreadystatechange = function() {
311     if (xhr.readyState != 4)
312       return;
313     if (xhr.status != 200) {
314       onFailure(xhr.status);
315       return;
316     }
317     var contentType = xhr.getResponseHeader('Content-Type');
318     onSuccess(contentType, xhr.response);
319   }.bind(this);
320
321   // Perform a xhr request.
322   try {
323     xhr.open('GET', url, true);
324     if (token)
325       xhr.setRequestHeader('Authorization', 'Bearer ' + token);
326     xhr.send();
327   } catch (e) {
328     onFailure();
329   }
330
331   return xhr;
332 };
333
334 /**
335  * Sends the resized image via the callback. If the image has been changed,
336  * then packs the canvas contents, otherwise sends the raw image data.
337  *
338  * @param {boolean} imageChanged Whether the image has been changed.
339  * @private
340  */
341 Request.prototype.sendImage_ = function(imageChanged) {
342   var imageData;
343   if (!imageChanged) {
344     // The image hasn't been processed, so the raw data can be directly
345     // forwarded for speed (no need to encode the image again).
346     imageData = this.image_.src;
347   } else {
348     // The image has been resized or rotated, therefore the canvas has to be
349     // encoded to get the correct compressed image data.
350     switch (this.contentType_) {
351       case 'image/gif':
352       case 'image/png':
353       case 'image/svg':
354       case 'image/bmp':
355         imageData = this.canvas_.toDataURL('image/png');
356         break;
357       case 'image/jpeg':
358       default:
359         imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
360     }
361   }
362
363   // Send and store in the persistent cache.
364   this.sendImageData_(imageData);
365   this.saveToCache_(imageData);
366 };
367
368 /**
369  * Sends the resized image via the callback.
370  * @param {string} data Compressed image data.
371  * @private
372  */
373 Request.prototype.sendImageData_ = function(data) {
374   this.sendResponse_(
375       {status: 'success', data: data, taskId: this.request_.taskId});
376 };
377
378 /**
379  * Handler, when contents are loaded into the image element. Performs resizing
380  * and finalizes the request process.
381  *
382  * @param {function()} callback Completion callback.
383  * @private
384  */
385 Request.prototype.onImageLoad_ = function(callback) {
386   // Perform processing if the url is not a data url, or if there are some
387   // operations requested.
388   if (!this.request_.url.match(/^data/) ||
389       ImageLoader.shouldProcess(this.image_.width,
390                                 this.image_.height,
391                                 this.request_)) {
392     ImageLoader.resize(this.image_, this.canvas_, this.request_);
393     this.sendImage_(true);  // Image changed.
394   } else {
395     this.sendImage_(false);  // Image not changed.
396   }
397   this.cleanup_();
398   this.downloadCallback_();
399 };
400
401 /**
402  * Handler, when loading of the image fails. Sends a failure response and
403  * finalizes the request process.
404  *
405  * @param {function()} callback Completion callback.
406  * @private
407  */
408 Request.prototype.onImageError_ = function(callback) {
409   this.sendResponse_(
410       {status: 'error', taskId: this.request_.taskId});
411   this.cleanup_();
412   this.downloadCallback_();
413 };
414
415 /**
416  * Cancels the request.
417  */
418 Request.prototype.cancel = function() {
419   this.cleanup_();
420
421   // If downloading has started, then call the callback.
422   if (this.downloadCallback_)
423     this.downloadCallback_();
424 };
425
426 /**
427  * Cleans up memory used by this request.
428  * @private
429  */
430 Request.prototype.cleanup_ = function() {
431   this.image_.onerror = function() {};
432   this.image_.onload = function() {};
433
434   // Transparent 1x1 pixel gif, to force garbage collecting.
435   this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
436       'ABAAEAAAICTAEAOw==';
437
438   this.xhr_.onload = function() {};
439   this.xhr_.abort();
440
441   // Dispose memory allocated by Canvas.
442   this.canvas_.width = 0;
443   this.canvas_.height = 0;
444 };