1// Copyright (c) 2012 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 5cr.define('cloudprint', function() { 6 'use strict'; 7 8 /** 9 * API to the Google Cloud Print service. 10 * @param {string} baseUrl Base part of the Google Cloud Print service URL 11 * with no trailing slash. For example, 12 * 'https://www.google.com/cloudprint'. 13 * @param {!print_preview.NativeLayer} nativeLayer Native layer used to get 14 * Auth2 tokens. 15 * @constructor 16 * @extends {cr.EventTarget} 17 */ 18 function CloudPrintInterface(baseUrl, nativeLayer) { 19 /** 20 * The base URL of the Google Cloud Print API. 21 * @type {string} 22 * @private 23 */ 24 this.baseUrl_ = baseUrl; 25 26 /** 27 * Used to get Auth2 tokens. 28 * @type {!print_preview.NativeLayer} 29 * @private 30 */ 31 this.nativeLayer_ = nativeLayer; 32 33 /** 34 * Last received XSRF token. Sent as a parameter in every request. 35 * @type {string} 36 * @private 37 */ 38 this.xsrfToken_ = ''; 39 40 /** 41 * Pending requests delayed until we get access token. 42 * @type {!Array.<!CloudPrintRequest>} 43 * @private 44 */ 45 this.requestQueue_ = []; 46 47 /** 48 * Number of outstanding cloud destination search requests. 49 * @type {number} 50 * @private 51 */ 52 this.outstandingCloudSearchRequestCount_ = 0; 53 54 /** 55 * Event tracker used to keep track of native layer events. 56 * @type {!EventTracker} 57 * @private 58 */ 59 this.tracker_ = new EventTracker(); 60 61 this.addEventListeners_(); 62 }; 63 64 /** 65 * Event types dispatched by the interface. 66 * @enum {string} 67 */ 68 CloudPrintInterface.EventType = { 69 PRINTER_DONE: 'cloudprint.CloudPrintInterface.PRINTER_DONE', 70 PRINTER_FAILED: 'cloudprint.CloudPrintInterface.PRINTER_FAILED', 71 SEARCH_DONE: 'cloudprint.CloudPrintInterface.SEARCH_DONE', 72 SEARCH_FAILED: 'cloudprint.CloudPrintInterface.SEARCH_FAILED', 73 SUBMIT_DONE: 'cloudprint.CloudPrintInterface.SUBMIT_DONE', 74 SUBMIT_FAILED: 'cloudprint.CloudPrintInterface.SUBMIT_FAILED', 75 UPDATE_PRINTER_TOS_ACCEPTANCE_FAILED: 76 'cloudprint.CloudPrintInterface.UPDATE_PRINTER_TOS_ACCEPTANCE_FAILED' 77 }; 78 79 /** 80 * Content type header value for a URL encoded HTTP request. 81 * @type {string} 82 * @const 83 * @private 84 */ 85 CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_ = 86 'application/x-www-form-urlencoded'; 87 88 /** 89 * Multi-part POST request boundary used in communication with Google 90 * Cloud Print. 91 * @type {string} 92 * @const 93 * @private 94 */ 95 CloudPrintInterface.MULTIPART_BOUNDARY_ = 96 '----CloudPrintFormBoundaryjc9wuprokl8i'; 97 98 /** 99 * Content type header value for a multipart HTTP request. 100 * @type {string} 101 * @const 102 * @private 103 */ 104 CloudPrintInterface.MULTIPART_CONTENT_TYPE_ = 105 'multipart/form-data; boundary=' + 106 CloudPrintInterface.MULTIPART_BOUNDARY_; 107 108 /** 109 * Regex that extracts Chrome's version from the user-agent string. 110 * @type {!RegExp} 111 * @const 112 * @private 113 */ 114 CloudPrintInterface.VERSION_REGEXP_ = /.*Chrome\/([\d\.]+)/i; 115 116 /** 117 * Enumeration of JSON response fields from Google Cloud Print API. 118 * @enum {string} 119 * @private 120 */ 121 CloudPrintInterface.JsonFields_ = { 122 PRINTER: 'printer' 123 }; 124 125 /** 126 * Could Print origins used to search printers. 127 * @type {!Array.<!print_preview.Destination.Origin>} 128 * @const 129 * @private 130 */ 131 CloudPrintInterface.CLOUD_ORIGINS_ = [ 132 print_preview.Destination.Origin.COOKIES, 133 print_preview.Destination.Origin.DEVICE 134 // TODO(vitalybuka): Enable when implemented. 135 // ready print_preview.Destination.Origin.PROFILE 136 ]; 137 138 CloudPrintInterface.prototype = { 139 __proto__: cr.EventTarget.prototype, 140 141 /** @return {string} Base URL of the Google Cloud Print service. */ 142 get baseUrl() { 143 return this.baseUrl_; 144 }, 145 146 /** 147 * @return {boolean} Whether a search for cloud destinations is in progress. 148 */ 149 get isCloudDestinationSearchInProgress() { 150 return this.outstandingCloudSearchRequestCount_ > 0; 151 }, 152 153 /** 154 * Sends a Google Cloud Print search API request. 155 * @param {boolean} isRecent Whether to search for only recently used 156 * printers. 157 */ 158 search: function(isRecent) { 159 var params = [ 160 new HttpParam('connection_status', 'ALL'), 161 new HttpParam('client', 'chrome'), 162 new HttpParam('use_cdd', 'true') 163 ]; 164 if (isRecent) { 165 params.push(new HttpParam('q', '^recent')); 166 } 167 CloudPrintInterface.CLOUD_ORIGINS_.forEach(function(origin) { 168 ++this.outstandingCloudSearchRequestCount_; 169 var cpRequest = 170 this.buildRequest_('GET', 'search', params, origin, 171 this.onSearchDone_.bind(this, isRecent)); 172 this.sendOrQueueRequest_(cpRequest); 173 }, this); 174 }, 175 176 /** 177 * Sends a Google Cloud Print submit API request. 178 * @param {!print_preview.Destination} destination Cloud destination to 179 * print to. 180 * @param {!print_preview.PrintTicketStore} printTicketStore Contains the 181 * print ticket to print. 182 * @param {!print_preview.DocumentInfo} documentInfo Document data model. 183 * @param {string} data Base64 encoded data of the document. 184 */ 185 submit: function(destination, printTicketStore, documentInfo, data) { 186 var result = 187 CloudPrintInterface.VERSION_REGEXP_.exec(navigator.userAgent); 188 var chromeVersion = 'unknown'; 189 if (result && result.length == 2) { 190 chromeVersion = result[1]; 191 } 192 var params = [ 193 new HttpParam('printerid', destination.id), 194 new HttpParam('contentType', 'dataUrl'), 195 new HttpParam('title', documentInfo.title), 196 new HttpParam('ticket', 197 printTicketStore.createPrintTicket(destination)), 198 new HttpParam('content', 'data:application/pdf;base64,' + data), 199 new HttpParam('tag', 200 '__google__chrome_version=' + chromeVersion), 201 new HttpParam('tag', '__google__os=' + navigator.platform) 202 ]; 203 var cpRequest = this.buildRequest_('POST', 'submit', params, 204 destination.origin, 205 this.onSubmitDone_.bind(this)); 206 this.sendOrQueueRequest_(cpRequest); 207 }, 208 209 /** 210 * Sends a Google Cloud Print printer API request. 211 * @param {string} printerId ID of the printer to lookup. 212 * @param {!print_preview.Destination.Origin} origin Origin of the printer. 213 */ 214 printer: function(printerId, origin) { 215 var params = [ 216 new HttpParam('printerid', printerId), 217 new HttpParam('use_cdd', 'true') 218 ]; 219 var cpRequest = 220 this.buildRequest_('GET', 'printer', params, origin, 221 this.onPrinterDone_.bind(this, printerId)); 222 this.sendOrQueueRequest_(cpRequest); 223 }, 224 225 /** 226 * Sends a Google Cloud Print update API request to accept (or reject) the 227 * terms-of-service of the given printer. 228 * @param {string} printerId ID of the printer to accept the 229 * terms-of-service for. 230 * @param {!print_preview.Destination.Origin} origin Origin of the printer. 231 * @param {boolean} isAccepted Whether the user accepted the 232 * terms-of-service. 233 */ 234 updatePrinterTosAcceptance: function(printerId, origin, isAccepted) { 235 var params = [ 236 new HttpParam('printerid', printerId), 237 new HttpParam('is_tos_accepted', isAccepted) 238 ]; 239 var cpRequest = 240 this.buildRequest_('POST', 'update', params, origin, 241 this.onUpdatePrinterTosAcceptanceDone_.bind(this)); 242 this.sendOrQueueRequest_(cpRequest); 243 }, 244 245 /** 246 * Adds event listeners to the relevant native layer events. 247 * @private 248 */ 249 addEventListeners_: function() { 250 this.tracker_.add( 251 this.nativeLayer_, 252 print_preview.NativeLayer.EventType.ACCESS_TOKEN_READY, 253 this.onAccessTokenReady_.bind(this)); 254 }, 255 256 /** 257 * Builds request to the Google Cloud Print API. 258 * @param {string} method HTTP method of the request. 259 * @param {string} action Google Cloud Print action to perform. 260 * @param {Array.<!HttpParam>} params HTTP parameters to include in the 261 * request. 262 * @param {!print_preview.Destination.Origin} origin Origin for destination. 263 * @param {function(number, Object, !print_preview.Destination.Origin)} 264 * callback Callback to invoke when request completes. 265 * @return {!CloudPrintRequest} Partially prepared request. 266 * @private 267 */ 268 buildRequest_: function(method, action, params, origin, callback) { 269 var url = this.baseUrl_ + '/' + action + '?xsrf='; 270 if (origin == print_preview.Destination.Origin.COOKIES) { 271 if (!this.xsrfToken_) { 272 // TODO(rltoscano): Should throw an error if not a read-only action or 273 // issue an xsrf token request. 274 } else { 275 url = url + this.xsrfToken_; 276 } 277 } 278 var body = null; 279 if (params) { 280 if (method == 'GET') { 281 url = params.reduce(function(partialUrl, param) { 282 return partialUrl + '&' + param.name + '=' + 283 encodeURIComponent(param.value); 284 }, url); 285 } else if (method == 'POST') { 286 body = params.reduce(function(partialBody, param) { 287 return partialBody + 'Content-Disposition: form-data; name=\"' + 288 param.name + '\"\r\n\r\n' + param.value + '\r\n--' + 289 CloudPrintInterface.MULTIPART_BOUNDARY_ + '\r\n'; 290 }, '--' + CloudPrintInterface.MULTIPART_BOUNDARY_ + '\r\n'); 291 } 292 } 293 294 var headers = {}; 295 headers['X-CloudPrint-Proxy'] = 'ChromePrintPreview'; 296 if (method == 'GET') { 297 headers['Content-Type'] = CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_; 298 } else if (method == 'POST') { 299 headers['Content-Type'] = CloudPrintInterface.MULTIPART_CONTENT_TYPE_; 300 } 301 302 var xhr = new XMLHttpRequest(); 303 xhr.open(method, url, true); 304 xhr.withCredentials = 305 (origin == print_preview.Destination.Origin.COOKIES); 306 for (var header in headers) { 307 xhr.setRequestHeader(header, headers[header]); 308 } 309 310 return new CloudPrintRequest(xhr, body, origin, callback); 311 }, 312 313 /** 314 * Sends a request to the Google Cloud Print API or queues if it needs to 315 * wait OAuth2 access token. 316 * @param {!CloudPrintRequest} request Request to send or queue. 317 * @private 318 */ 319 sendOrQueueRequest_: function(request) { 320 if (request.origin == print_preview.Destination.Origin.COOKIES) { 321 return this.sendRequest_(request); 322 } else { 323 this.requestQueue_.push(request); 324 this.nativeLayer_.startGetAccessToken(request.origin); 325 } 326 }, 327 328 /** 329 * Sends a request to the Google Cloud Print API. 330 * @param {!CloudPrintRequest} request Request to send. 331 * @private 332 */ 333 sendRequest_: function(request) { 334 request.xhr.onreadystatechange = 335 this.onReadyStateChange_.bind(this, request); 336 request.xhr.send(request.body); 337 }, 338 339 /** 340 * Creates a Google Cloud Print interface error that is ready to dispatch. 341 * @param {!CloudPrintInterface.EventType} type Type of the error. 342 * @param {!CloudPrintRequest} request Request that has been completed. 343 * @return {!Event} Google Cloud Print interface error event. 344 * @private 345 */ 346 createErrorEvent_: function(type, request) { 347 var errorEvent = new Event(type); 348 errorEvent.status = request.xhr.status; 349 if (request.xhr.status == 200) { 350 errorEvent.errorCode = request.result['errorCode']; 351 errorEvent.message = request.result['message']; 352 } else { 353 errorEvent.errorCode = 0; 354 errorEvent.message = ''; 355 } 356 errorEvent.origin = request.origin; 357 return errorEvent; 358 }, 359 360 /** 361 * Called when a native layer receives access token. 362 * @param {Event} evt Contains the authetication type and access token. 363 * @private 364 */ 365 onAccessTokenReady_: function(event) { 366 // TODO(vitalybuka): remove when other Origins implemented. 367 assert(event.authType == print_preview.Destination.Origin.DEVICE); 368 this.requestQueue_ = this.requestQueue_.filter(function(request) { 369 assert(request.origin == print_preview.Destination.Origin.DEVICE); 370 if (request.origin != event.authType) { 371 return true; 372 } 373 if (event.accessToken) { 374 request.xhr.setRequestHeader('Authorization', 375 'Bearer ' + event.accessToken); 376 this.sendRequest_(request); 377 } else { // No valid token. 378 // Without abort status does not exists. 379 request.xhr.abort(); 380 request.callback(request); 381 } 382 return false; 383 }, this); 384 }, 385 386 /** 387 * Called when the ready-state of a XML http request changes. 388 * Calls the successCallback with the result or dispatches an ERROR event. 389 * @param {!CloudPrintRequest} request Request that was changed. 390 * @private 391 */ 392 onReadyStateChange_: function(request) { 393 if (request.xhr.readyState == 4) { 394 if (request.xhr.status == 200) { 395 request.result = JSON.parse(request.xhr.responseText); 396 if (request.origin == print_preview.Destination.Origin.COOKIES && 397 request.result['success']) { 398 this.xsrfToken_ = request.result['xsrf_token']; 399 } 400 } 401 request.status = request.xhr.status; 402 request.callback(request); 403 } 404 }, 405 406 /** 407 * Called when the search request completes. 408 * @param {boolean} isRecent Whether the search request was for recent 409 * destinations. 410 * @param {!CloudPrintRequest} request Request that has been completed. 411 * @private 412 */ 413 onSearchDone_: function(isRecent, request) { 414 --this.outstandingCloudSearchRequestCount_; 415 if (request.xhr.status == 200 && request.result['success']) { 416 var printerListJson = request.result['printers'] || []; 417 var printerList = []; 418 printerListJson.forEach(function(printerJson) { 419 try { 420 printerList.push( 421 cloudprint.CloudDestinationParser.parse(printerJson, 422 request.origin)); 423 } catch (err) { 424 console.error('Unable to parse cloud print destination: ' + err); 425 } 426 }); 427 var searchDoneEvent = 428 new Event(CloudPrintInterface.EventType.SEARCH_DONE); 429 searchDoneEvent.printers = printerList; 430 searchDoneEvent.origin = request.origin; 431 searchDoneEvent.isRecent = isRecent; 432 searchDoneEvent.email = request.result['request']['user']; 433 this.dispatchEvent(searchDoneEvent); 434 } else { 435 var errorEvent = this.createErrorEvent_( 436 CloudPrintInterface.EventType.SEARCH_FAILED, request); 437 this.dispatchEvent(errorEvent); 438 } 439 }, 440 441 /** 442 * Called when the submit request completes. 443 * @param {!CloudPrintRequest} request Request that has been completed. 444 * @private 445 */ 446 onSubmitDone_: function(request) { 447 if (request.xhr.status == 200 && request.result['success']) { 448 var submitDoneEvent = new Event( 449 CloudPrintInterface.EventType.SUBMIT_DONE); 450 submitDoneEvent.jobId = request.result['job']['id']; 451 this.dispatchEvent(submitDoneEvent); 452 } else { 453 var errorEvent = this.createErrorEvent_( 454 CloudPrintInterface.EventType.SUBMIT_FAILED, request); 455 this.dispatchEvent(errorEvent); 456 } 457 }, 458 459 /** 460 * Called when the printer request completes. 461 * @param {string} destinationId ID of the destination that was looked up. 462 * @param {!CloudPrintRequest} request Request that has been completed. 463 * @private 464 */ 465 onPrinterDone_: function(destinationId, request) { 466 if (request.xhr.status == 200 && request.result['success']) { 467 var printerJson = request.result['printers'][0]; 468 var printer; 469 try { 470 printer = cloudprint.CloudDestinationParser.parse(printerJson, 471 request.origin); 472 } catch (err) { 473 console.error('Failed to parse cloud print destination: ' + 474 JSON.stringify(printerJson)); 475 return; 476 } 477 var printerDoneEvent = 478 new Event(CloudPrintInterface.EventType.PRINTER_DONE); 479 printerDoneEvent.printer = printer; 480 this.dispatchEvent(printerDoneEvent); 481 } else { 482 var errorEvent = this.createErrorEvent_( 483 CloudPrintInterface.EventType.PRINTER_FAILED, request); 484 errorEvent.destinationId = destinationId; 485 errorEvent.destinationOrigin = request.origin; 486 this.dispatchEvent(errorEvent, request.origin); 487 } 488 }, 489 490 /** 491 * Called when the update printer TOS acceptance request completes. 492 * @param {!CloudPrintRequest} request Request that has been completed. 493 * @private 494 */ 495 onUpdatePrinterTosAcceptanceDone_: function(request) { 496 if (request.xhr.status == 200 && request.result['success']) { 497 // Do nothing. 498 } else { 499 var errorEvent = this.createErrorEvent_( 500 CloudPrintInterface.EventType.SUBMIT_FAILED, request); 501 this.dispatchEvent(errorEvent); 502 } 503 } 504 }; 505 506 /** 507 * Data structure that holds data for Cloud Print requests. 508 * @param {!XMLHttpRequest} xhr Partially prepared http request. 509 * @param {string} body Data to send with POST requests. 510 * @param {!print_preview.Destination.Origin} origin Origin for destination. 511 * @param {function(!CloudPrintRequest)} callback Callback to invoke when 512 * request completes. 513 * @constructor 514 */ 515 function CloudPrintRequest(xhr, body, origin, callback) { 516 /** 517 * Partially prepared http request. 518 * @type {!XMLHttpRequest} 519 */ 520 this.xhr = xhr; 521 522 /** 523 * Data to send with POST requests. 524 * @type {string} 525 */ 526 this.body = body; 527 528 /** 529 * Origin for destination. 530 * @type {!print_preview.Destination.Origin} 531 */ 532 this.origin = origin; 533 534 /** 535 * Callback to invoke when request completes. 536 * @type {function(!CloudPrintRequest)} 537 */ 538 this.callback = callback; 539 540 /** 541 * Result for requests. 542 * @type {Object} JSON response. 543 */ 544 this.result = null; 545 }; 546 547 /** 548 * Data structure that represents an HTTP parameter. 549 * @param {string} name Name of the parameter. 550 * @param {string} value Value of the parameter. 551 * @constructor 552 */ 553 function HttpParam(name, value) { 554 /** 555 * Name of the parameter. 556 * @type {string} 557 */ 558 this.name = name; 559 560 /** 561 * Name of the value. 562 * @type {string} 563 */ 564 this.value = value; 565 }; 566 567 // Export 568 return { 569 CloudPrintInterface: CloudPrintInterface 570 }; 571}); 572