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