• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
17function 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 */
88Request.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 */
98Request.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 */
108Request.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 */
121Request.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 */
137Request.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 */
166Request.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 */
185Request.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 */
217function AuthorizedXHR() {
218  this.xhr_ = null;
219  this.aborted_ = false;
220}
221
222/**
223 * Aborts the current request (if running).
224 */
225AuthorizedXHR.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 */
240AuthorizedXHR.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 (/^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 */
306AuthorizedXHR.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 */
341Request.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 */
373Request.prototype.sendImageData_ = function(data) {
374  this.sendResponse_({status: 'success',
375                      data: data,
376                      taskId: this.request_.taskId});
377};
378
379/**
380 * Handler, when contents are loaded into the image element. Performs resizing
381 * and finalizes the request process.
382 *
383 * @param {function()} callback Completion callback.
384 * @private
385 */
386Request.prototype.onImageLoad_ = function(callback) {
387  // Perform processing if the url is not a data url, or if there are some
388  // operations requested.
389  if (!this.request_.url.match(/^data/) ||
390      ImageLoader.shouldProcess(this.image_.width,
391                                this.image_.height,
392                                this.request_)) {
393    ImageLoader.resize(this.image_, this.canvas_, this.request_);
394    this.sendImage_(true);  // Image changed.
395  } else {
396    this.sendImage_(false);  // Image not changed.
397  }
398  this.cleanup_();
399  this.downloadCallback_();
400};
401
402/**
403 * Handler, when loading of the image fails. Sends a failure response and
404 * finalizes the request process.
405 *
406 * @param {function()} callback Completion callback.
407 * @private
408 */
409Request.prototype.onImageError_ = function(callback) {
410  this.sendResponse_({status: 'error',
411                      taskId: this.request_.taskId});
412  this.cleanup_();
413  this.downloadCallback_();
414};
415
416/**
417 * Cancels the request.
418 */
419Request.prototype.cancel = function() {
420  this.cleanup_();
421
422  // If downloading has started, then call the callback.
423  if (this.downloadCallback_)
424    this.downloadCallback_();
425};
426
427/**
428 * Cleans up memory used by this request.
429 * @private
430 */
431Request.prototype.cleanup_ = function() {
432  this.image_.onerror = function() {};
433  this.image_.onload = function() {};
434
435  // Transparent 1x1 pixel gif, to force garbage collecting.
436  this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
437      'ABAAEAAAICTAEAOw==';
438
439  this.xhr_.onload = function() {};
440  this.xhr_.abort();
441
442  // Dispose memory allocated by Canvas.
443  this.canvas_.width = 0;
444  this.canvas_.height = 0;
445};
446