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.
5 cr.define('cloudprint', function() {
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
15 * @param {!print_preview.UserInfo} userInfo User information repository.
17 * @extends {cr.EventTarget}
19 function CloudPrintInterface(baseUrl, nativeLayer, userInfo) {
21 * The base URL of the Google Cloud Print API.
25 this.baseUrl_ = baseUrl;
28 * Used to get Auth2 tokens.
29 * @type {!print_preview.NativeLayer}
32 this.nativeLayer_ = nativeLayer;
35 * User information repository.
36 * @type {!print_preview.UserInfo}
39 this.userInfo_ = userInfo;
42 * Currently logged in users (identified by email) mapped to the Google
44 * @type {!Object.<string, number>}
47 this.userSessionIndex_ = {};
50 * Stores last received XSRF tokens for each user account. Sent as
51 * a parameter with every request.
52 * @type {!Object.<string, string>}
55 this.xsrfTokens_ = {};
58 * Pending requests delayed until we get access token.
59 * @type {!Array.<!CloudPrintRequest>}
62 this.requestQueue_ = [];
65 * Outstanding cloud destination search requests.
66 * @type {!Array.<!CloudPrintRequest>}
69 this.outstandingCloudSearchRequests_ = [];
72 * Event tracker used to keep track of native layer events.
73 * @type {!EventTracker}
76 this.tracker_ = new EventTracker();
78 this.addEventListeners_();
82 * Event types dispatched by the interface.
85 CloudPrintInterface.EventType = {
86 PRINTER_DONE: 'cloudprint.CloudPrintInterface.PRINTER_DONE',
87 PRINTER_FAILED: 'cloudprint.CloudPrintInterface.PRINTER_FAILED',
88 SEARCH_DONE: 'cloudprint.CloudPrintInterface.SEARCH_DONE',
89 SEARCH_FAILED: 'cloudprint.CloudPrintInterface.SEARCH_FAILED',
90 SUBMIT_DONE: 'cloudprint.CloudPrintInterface.SUBMIT_DONE',
91 SUBMIT_FAILED: 'cloudprint.CloudPrintInterface.SUBMIT_FAILED',
92 UPDATE_PRINTER_TOS_ACCEPTANCE_FAILED:
93 'cloudprint.CloudPrintInterface.UPDATE_PRINTER_TOS_ACCEPTANCE_FAILED'
97 * Content type header value for a URL encoded HTTP request.
102 CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_ =
103 'application/x-www-form-urlencoded';
106 * Multi-part POST request boundary used in communication with Google
112 CloudPrintInterface.MULTIPART_BOUNDARY_ =
113 '----CloudPrintFormBoundaryjc9wuprokl8i';
116 * Content type header value for a multipart HTTP request.
121 CloudPrintInterface.MULTIPART_CONTENT_TYPE_ =
122 'multipart/form-data; boundary=' +
123 CloudPrintInterface.MULTIPART_BOUNDARY_;
126 * Regex that extracts Chrome's version from the user-agent string.
131 CloudPrintInterface.VERSION_REGEXP_ = /.*Chrome\/([\d\.]+)/i;
134 * Enumeration of JSON response fields from Google Cloud Print API.
138 CloudPrintInterface.JsonFields_ = {
143 * Could Print origins used to search printers.
144 * @type {!Array.<!print_preview.Destination.Origin>}
148 CloudPrintInterface.CLOUD_ORIGINS_ = [
149 print_preview.Destination.Origin.COOKIES,
150 print_preview.Destination.Origin.DEVICE
151 // TODO(vitalybuka): Enable when implemented.
152 // ready print_preview.Destination.Origin.PROFILE
155 CloudPrintInterface.prototype = {
156 __proto__: cr.EventTarget.prototype,
158 /** @return {string} Base URL of the Google Cloud Print service. */
160 return this.baseUrl_;
164 * @return {boolean} Whether a search for cloud destinations is in progress.
166 get isCloudDestinationSearchInProgress() {
167 return this.outstandingCloudSearchRequests_.length > 0;
171 * Sends Google Cloud Print search API request.
172 * @param {string=} opt_account Account the search is sent for. When
173 * omitted, the search is done on behalf of the primary user.
174 * @param {print_preview.Destination.Origin=} opt_origin When specified,
175 * searches destinations for {@code opt_origin} only, otherwise starts
176 * searches for all origins.
178 search: function(opt_account, opt_origin) {
179 var account = opt_account || '';
181 opt_origin && [opt_origin] || CloudPrintInterface.CLOUD_ORIGINS_;
182 this.abortSearchRequests_(origins);
183 this.search_(true, account, origins);
184 this.search_(false, account, origins);
188 * Sends Google Cloud Print search API requests.
189 * @param {boolean} isRecent Whether to search for only recently used
191 * @param {string} account Account the search is sent for. It matters for
192 * COOKIES origin only, and can be empty (sent on behalf of the primary
193 * user in this case).
194 * @param {!Array.<!print_preview.Destination.Origin>} origins Origins to
195 * search printers for.
198 search_: function(isRecent, account, origins) {
200 new HttpParam('connection_status', 'ALL'),
201 new HttpParam('client', 'chrome'),
202 new HttpParam('use_cdd', 'true')
205 params.push(new HttpParam('q', '^recent'));
207 origins.forEach(function(origin) {
208 var cpRequest = this.buildRequest_(
214 this.onSearchDone_.bind(this, isRecent));
215 this.outstandingCloudSearchRequests_.push(cpRequest);
216 this.sendOrQueueRequest_(cpRequest);
221 * Sends a Google Cloud Print submit API request.
222 * @param {!print_preview.Destination} destination Cloud destination to
224 * @param {!print_preview.PrintTicketStore} printTicketStore Contains the
225 * print ticket to print.
226 * @param {!print_preview.DocumentInfo} documentInfo Document data model.
227 * @param {string} data Base64 encoded data of the document.
229 submit: function(destination, printTicketStore, documentInfo, data) {
231 CloudPrintInterface.VERSION_REGEXP_.exec(navigator.userAgent);
232 var chromeVersion = 'unknown';
233 if (result && result.length == 2) {
234 chromeVersion = result[1];
237 new HttpParam('printerid', destination.id),
238 new HttpParam('contentType', 'dataUrl'),
239 new HttpParam('title', documentInfo.title),
240 new HttpParam('ticket',
241 printTicketStore.createPrintTicket(destination)),
242 new HttpParam('content', 'data:application/pdf;base64,' + data),
244 '__google__chrome_version=' + chromeVersion),
245 new HttpParam('tag', '__google__os=' + navigator.platform)
247 var cpRequest = this.buildRequest_(
253 this.onSubmitDone_.bind(this));
254 this.sendOrQueueRequest_(cpRequest);
258 * Sends a Google Cloud Print printer API request.
259 * @param {string} printerId ID of the printer to lookup.
260 * @param {!print_preview.Destination.Origin} origin Origin of the printer.
261 * @param {string=} account Account this printer is registered for. When
262 * provided for COOKIES {@code origin}, and users sessions are still not
263 * known, will be checked against the response (both success and failure
264 * to get printer) and, if the active user account is not the one
265 * requested, {@code account} is activated and printer request reissued.
267 printer: function(printerId, origin, account) {
269 new HttpParam('printerid', printerId),
270 new HttpParam('use_cdd', 'true'),
271 new HttpParam('printer_connection_status', 'true')
273 this.sendOrQueueRequest_(this.buildRequest_(
279 this.onPrinterDone_.bind(this, printerId)));
283 * Sends a Google Cloud Print update API request to accept (or reject) the
284 * terms-of-service of the given printer.
285 * @param {!print_preview.Destination} destination Destination to accept ToS
287 * @param {boolean} isAccepted Whether the user accepted ToS or not.
289 updatePrinterTosAcceptance: function(destination, isAccepted) {
291 new HttpParam('printerid', destination.id),
292 new HttpParam('is_tos_accepted', isAccepted)
294 this.sendOrQueueRequest_(this.buildRequest_(
300 this.onUpdatePrinterTosAcceptanceDone_.bind(this)));
304 * Adds event listeners to relevant events.
307 addEventListeners_: function() {
310 print_preview.NativeLayer.EventType.ACCESS_TOKEN_READY,
311 this.onAccessTokenReady_.bind(this));
315 * Builds request to the Google Cloud Print API.
316 * @param {string} method HTTP method of the request.
317 * @param {string} action Google Cloud Print action to perform.
318 * @param {Array.<!HttpParam>} params HTTP parameters to include in the
320 * @param {!print_preview.Destination.Origin} origin Origin for destination.
321 * @param {?string} account Account the request is sent for. Can be
322 * {@code null} or empty string if the request is not cookie bound or
323 * is sent on behalf of the primary user.
324 * @param {function(number, Object, !print_preview.Destination.Origin)}
325 * callback Callback to invoke when request completes.
326 * @return {!CloudPrintRequest} Partially prepared request.
329 buildRequest_: function(method, action, params, origin, account, callback) {
330 var url = this.baseUrl_ + '/' + action + '?xsrf=';
331 if (origin == print_preview.Destination.Origin.COOKIES) {
332 var xsrfToken = this.xsrfTokens_[account];
334 // TODO(rltoscano): Should throw an error if not a read-only action or
335 // issue an xsrf token request.
337 url = url + xsrfToken;
340 var index = this.userSessionIndex_[account] || 0;
342 url += '&user=' + index;
348 if (method == 'GET') {
349 url = params.reduce(function(partialUrl, param) {
350 return partialUrl + '&' + param.name + '=' +
351 encodeURIComponent(param.value);
353 } else if (method == 'POST') {
354 body = params.reduce(function(partialBody, param) {
355 return partialBody + 'Content-Disposition: form-data; name=\"' +
356 param.name + '\"\r\n\r\n' + param.value + '\r\n--' +
357 CloudPrintInterface.MULTIPART_BOUNDARY_ + '\r\n';
358 }, '--' + CloudPrintInterface.MULTIPART_BOUNDARY_ + '\r\n');
363 headers['X-CloudPrint-Proxy'] = 'ChromePrintPreview';
364 if (method == 'GET') {
365 headers['Content-Type'] = CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_;
366 } else if (method == 'POST') {
367 headers['Content-Type'] = CloudPrintInterface.MULTIPART_CONTENT_TYPE_;
370 var xhr = new XMLHttpRequest();
371 xhr.open(method, url, true);
372 xhr.withCredentials =
373 (origin == print_preview.Destination.Origin.COOKIES);
374 for (var header in headers) {
375 xhr.setRequestHeader(header, headers[header]);
378 return new CloudPrintRequest(xhr, body, origin, account, callback);
382 * Sends a request to the Google Cloud Print API or queues if it needs to
383 * wait OAuth2 access token.
384 * @param {!CloudPrintRequest} request Request to send or queue.
387 sendOrQueueRequest_: function(request) {
388 if (request.origin == print_preview.Destination.Origin.COOKIES) {
389 return this.sendRequest_(request);
391 this.requestQueue_.push(request);
392 this.nativeLayer_.startGetAccessToken(request.origin);
397 * Sends a request to the Google Cloud Print API.
398 * @param {!CloudPrintRequest} request Request to send.
401 sendRequest_: function(request) {
402 request.xhr.onreadystatechange =
403 this.onReadyStateChange_.bind(this, request);
404 request.xhr.send(request.body);
408 * Creates a Google Cloud Print interface error that is ready to dispatch.
409 * @param {!CloudPrintInterface.EventType} type Type of the error.
410 * @param {!CloudPrintRequest} request Request that has been completed.
411 * @return {!Event} Google Cloud Print interface error event.
414 createErrorEvent_: function(type, request) {
415 var errorEvent = new Event(type);
416 errorEvent.status = request.xhr.status;
417 if (request.xhr.status == 200) {
418 errorEvent.errorCode = request.result['errorCode'];
419 errorEvent.message = request.result['message'];
421 errorEvent.errorCode = 0;
422 errorEvent.message = '';
424 errorEvent.origin = request.origin;
429 * Updates user info and session index from the {@code request} response.
430 * @param {!CloudPrintRequest} request Request to extract user info from.
433 setUsers_: function(request) {
434 if (request.origin == print_preview.Destination.Origin.COOKIES) {
435 var users = request.result['request']['users'] || [];
436 this.userSessionIndex_ = {};
437 for (var i = 0; i < users.length; i++) {
438 this.userSessionIndex_[users[i]] = i;
440 this.userInfo_.setUsers(request.result['request']['user'], users);
445 * Terminates search requests for requested {@code origins}.
446 * @param {!Array.<print_preview.Destination.Origin>} origins Origins
447 * to terminate search requests for.
450 abortSearchRequests_: function(origins) {
451 this.outstandingCloudSearchRequests_ =
452 this.outstandingCloudSearchRequests_.filter(function(request) {
453 if (origins.indexOf(request.origin) >= 0) {
462 * Called when a native layer receives access token.
463 * @param {Event} evt Contains the authentication type and access token.
466 onAccessTokenReady_: function(event) {
467 // TODO(vitalybuka): remove when other Origins implemented.
468 assert(event.authType == print_preview.Destination.Origin.DEVICE);
469 this.requestQueue_ = this.requestQueue_.filter(function(request) {
470 assert(request.origin == print_preview.Destination.Origin.DEVICE);
471 if (request.origin != event.authType) {
474 if (event.accessToken) {
475 request.xhr.setRequestHeader('Authorization',
476 'Bearer ' + event.accessToken);
477 this.sendRequest_(request);
478 } else { // No valid token.
479 // Without abort status does not exist.
481 request.callback(request);
488 * Called when the ready-state of a XML http request changes.
489 * Calls the successCallback with the result or dispatches an ERROR event.
490 * @param {!CloudPrintRequest} request Request that was changed.
493 onReadyStateChange_: function(request) {
494 if (request.xhr.readyState == 4) {
495 if (request.xhr.status == 200) {
496 request.result = JSON.parse(request.xhr.responseText);
497 if (request.origin == print_preview.Destination.Origin.COOKIES &&
498 request.result['success']) {
499 this.xsrfTokens_[request.result['request']['user']] =
500 request.result['xsrf_token'];
503 request.status = request.xhr.status;
504 request.callback(request);
509 * Called when the search request completes.
510 * @param {boolean} isRecent Whether the search request was for recent
512 * @param {!CloudPrintRequest} request Request that has been completed.
515 onSearchDone_: function(isRecent, request) {
516 var lastRequestForThisOrigin = true;
517 this.outstandingCloudSearchRequests_ =
518 this.outstandingCloudSearchRequests_.filter(function(item) {
519 if (item != request && item.origin == request.origin) {
520 lastRequestForThisOrigin = false;
522 return item != request;
525 if (request.origin == print_preview.Destination.Origin.COOKIES) {
528 request.result['request'] &&
529 request.result['request']['user'];
532 if (request.xhr.status == 200 && request.result['success']) {
534 var printerListJson = request.result['printers'] || [];
535 var printerList = [];
536 printerListJson.forEach(function(printerJson) {
538 printerList.push(cloudprint.CloudDestinationParser.parse(
539 printerJson, request.origin, activeUser));
541 console.error('Unable to parse cloud print destination: ' + err);
544 // Extract and store users.
545 this.setUsers_(request);
546 // Dispatch SEARCH_DONE event.
547 event = new Event(CloudPrintInterface.EventType.SEARCH_DONE);
548 event.origin = request.origin;
549 event.printers = printerList;
550 event.isRecent = isRecent;
552 event = this.createErrorEvent_(
553 CloudPrintInterface.EventType.SEARCH_FAILED,
556 event.user = activeUser;
557 event.searchDone = lastRequestForThisOrigin;
558 this.dispatchEvent(event);
562 * Called when the submit request completes.
563 * @param {!CloudPrintRequest} request Request that has been completed.
566 onSubmitDone_: function(request) {
567 if (request.xhr.status == 200 && request.result['success']) {
568 var submitDoneEvent = new Event(
569 CloudPrintInterface.EventType.SUBMIT_DONE);
570 submitDoneEvent.jobId = request.result['job']['id'];
571 this.dispatchEvent(submitDoneEvent);
573 var errorEvent = this.createErrorEvent_(
574 CloudPrintInterface.EventType.SUBMIT_FAILED, request);
575 this.dispatchEvent(errorEvent);
580 * Called when the printer request completes.
581 * @param {string} destinationId ID of the destination that was looked up.
582 * @param {!CloudPrintRequest} request Request that has been completed.
585 onPrinterDone_: function(destinationId, request) {
586 // Special handling of the first printer request. It does not matter at
587 // this point, whether printer was found or not.
588 if (request.origin == print_preview.Destination.Origin.COOKIES &&
591 request.result['request']['user'] &&
592 request.result['request']['users'] &&
593 request.account != request.result['request']['user']) {
594 this.setUsers_(request);
595 // In case the user account is known, but not the primary one,
597 if (this.userSessionIndex_[request.account] > 0) {
598 this.userInfo_.activeUser = request.account;
599 // Repeat the request for the newly activated account.
601 request.result['request']['params']['printerid'],
604 // Stop processing this request, wait for the new response.
609 if (request.xhr.status == 200 && request.result['success']) {
611 if (request.origin == print_preview.Destination.Origin.COOKIES) {
612 activeUser = request.result['request']['user'];
614 var printerJson = request.result['printers'][0];
617 printer = cloudprint.CloudDestinationParser.parse(
618 printerJson, request.origin, activeUser);
620 console.error('Failed to parse cloud print destination: ' +
621 JSON.stringify(printerJson));
624 var printerDoneEvent =
625 new Event(CloudPrintInterface.EventType.PRINTER_DONE);
626 printerDoneEvent.printer = printer;
627 this.dispatchEvent(printerDoneEvent);
629 var errorEvent = this.createErrorEvent_(
630 CloudPrintInterface.EventType.PRINTER_FAILED, request);
631 errorEvent.destinationId = destinationId;
632 errorEvent.destinationOrigin = request.origin;
633 this.dispatchEvent(errorEvent, request.origin);
638 * Called when the update printer TOS acceptance request completes.
639 * @param {!CloudPrintRequest} request Request that has been completed.
642 onUpdatePrinterTosAcceptanceDone_: function(request) {
643 if (request.xhr.status == 200 && request.result['success']) {
646 var errorEvent = this.createErrorEvent_(
647 CloudPrintInterface.EventType.SUBMIT_FAILED, request);
648 this.dispatchEvent(errorEvent);
654 * Data structure that holds data for Cloud Print requests.
655 * @param {!XMLHttpRequest} xhr Partially prepared http request.
656 * @param {string} body Data to send with POST requests.
657 * @param {!print_preview.Destination.Origin} origin Origin for destination.
658 * @param {?string} account Account the request is sent for. Can be
659 * {@code null} or empty string if the request is not cookie bound or
660 * is sent on behalf of the primary user.
661 * @param {function(!CloudPrintRequest)} callback Callback to invoke when
665 function CloudPrintRequest(xhr, body, origin, account, callback) {
667 * Partially prepared http request.
668 * @type {!XMLHttpRequest}
673 * Data to send with POST requests.
679 * Origin for destination.
680 * @type {!print_preview.Destination.Origin}
682 this.origin = origin;
685 * User account this request is expected to be executed for.
688 this.account = account;
691 * Callback to invoke when request completes.
692 * @type {function(!CloudPrintRequest)}
694 this.callback = callback;
697 * Result for requests.
698 * @type {Object} JSON response.
704 * Data structure that represents an HTTP parameter.
705 * @param {string} name Name of the parameter.
706 * @param {string} value Value of the parameter.
709 function HttpParam(name, value) {
711 * Name of the parameter.
725 CloudPrintInterface: CloudPrintInterface