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.
16 * @param {boolean} isInAppKioskMode Whether the print preview is in App
19 * @extends {cr.EventTarget}
21 function CloudPrintInterface(
22 baseUrl, nativeLayer, userInfo, isInAppKioskMode) {
24 * The base URL of the Google Cloud Print API.
28 this.baseUrl_ = baseUrl;
31 * Used to get Auth2 tokens.
32 * @type {!print_preview.NativeLayer}
35 this.nativeLayer_ = nativeLayer;
38 * User information repository.
39 * @type {!print_preview.UserInfo}
42 this.userInfo_ = userInfo;
45 * Whether Print Preview is in App Kiosk mode, basically, use only printers
46 * available for the device.
50 this.isInAppKioskMode_ = isInAppKioskMode;
53 * Currently logged in users (identified by email) mapped to the Google
55 * @type {!Object<number>}
58 this.userSessionIndex_ = {};
61 * Stores last received XSRF tokens for each user account. Sent as
62 * a parameter with every request.
63 * @type {!Object<string>}
66 this.xsrfTokens_ = {};
69 * Pending requests delayed until we get access token.
70 * @type {!Array<!CloudPrintRequest>}
73 this.requestQueue_ = [];
76 * Outstanding cloud destination search requests.
77 * @type {!Array<!CloudPrintRequest>}
80 this.outstandingCloudSearchRequests_ = [];
83 * Event tracker used to keep track of native layer events.
84 * @type {!EventTracker}
87 this.tracker_ = new EventTracker();
89 this.addEventListeners_();
93 * Event types dispatched by the interface.
96 CloudPrintInterface.EventType = {
97 INVITES_DONE: 'cloudprint.CloudPrintInterface.INVITES_DONE',
98 INVITES_FAILED: 'cloudprint.CloudPrintInterface.INVITES_FAILED',
99 PRINTER_DONE: 'cloudprint.CloudPrintInterface.PRINTER_DONE',
100 PRINTER_FAILED: 'cloudprint.CloudPrintInterface.PRINTER_FAILED',
101 PROCESS_INVITE_DONE: 'cloudprint.CloudPrintInterface.PROCESS_INVITE_DONE',
102 PROCESS_INVITE_FAILED:
103 'cloudprint.CloudPrintInterface.PROCESS_INVITE_FAILED',
104 SEARCH_DONE: 'cloudprint.CloudPrintInterface.SEARCH_DONE',
105 SEARCH_FAILED: 'cloudprint.CloudPrintInterface.SEARCH_FAILED',
106 SUBMIT_DONE: 'cloudprint.CloudPrintInterface.SUBMIT_DONE',
107 SUBMIT_FAILED: 'cloudprint.CloudPrintInterface.SUBMIT_FAILED',
108 UPDATE_PRINTER_TOS_ACCEPTANCE_FAILED:
109 'cloudprint.CloudPrintInterface.UPDATE_PRINTER_TOS_ACCEPTANCE_FAILED'
113 * Content type header value for a URL encoded HTTP request.
118 CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_ =
119 'application/x-www-form-urlencoded';
122 * Multi-part POST request boundary used in communication with Google
128 CloudPrintInterface.MULTIPART_BOUNDARY_ =
129 '----CloudPrintFormBoundaryjc9wuprokl8i';
132 * Content type header value for a multipart HTTP request.
137 CloudPrintInterface.MULTIPART_CONTENT_TYPE_ =
138 'multipart/form-data; boundary=' +
139 CloudPrintInterface.MULTIPART_BOUNDARY_;
142 * Regex that extracts Chrome's version from the user-agent string.
147 CloudPrintInterface.VERSION_REGEXP_ = /.*Chrome\/([\d\.]+)/i;
150 * Enumeration of JSON response fields from Google Cloud Print API.
154 CloudPrintInterface.JsonFields_ = {
159 * Could Print origins used to search printers.
160 * @type {!Array<!print_preview.Destination.Origin>}
164 CloudPrintInterface.CLOUD_ORIGINS_ = [
165 print_preview.Destination.Origin.COOKIES,
166 print_preview.Destination.Origin.DEVICE
167 // TODO(vitalybuka): Enable when implemented.
168 // ready print_preview.Destination.Origin.PROFILE
171 CloudPrintInterface.prototype = {
172 __proto__: cr.EventTarget.prototype,
174 /** @return {string} Base URL of the Google Cloud Print service. */
176 return this.baseUrl_;
180 * @return {boolean} Whether a search for cloud destinations is in progress.
182 get isCloudDestinationSearchInProgress() {
183 return this.outstandingCloudSearchRequests_.length > 0;
187 * Sends Google Cloud Print search API request.
188 * @param {string=} opt_account Account the search is sent for. When
189 * omitted, the search is done on behalf of the primary user.
190 * @param {print_preview.Destination.Origin=} opt_origin When specified,
191 * searches destinations for {@code opt_origin} only, otherwise starts
192 * searches for all origins.
194 search: function(opt_account, opt_origin) {
195 var account = opt_account || '';
197 opt_origin && [opt_origin] || CloudPrintInterface.CLOUD_ORIGINS_;
198 if (this.isInAppKioskMode_) {
199 origins = origins.filter(function(origin) {
200 return origin != print_preview.Destination.Origin.COOKIES;
203 this.abortSearchRequests_(origins);
204 this.search_(true, account, origins);
205 this.search_(false, account, origins);
209 * Sends Google Cloud Print search API requests.
210 * @param {boolean} isRecent Whether to search for only recently used
212 * @param {string} account Account the search is sent for. It matters for
213 * COOKIES origin only, and can be empty (sent on behalf of the primary
214 * user in this case).
215 * @param {!Array<!print_preview.Destination.Origin>} origins Origins to
216 * search printers for.
219 search_: function(isRecent, account, origins) {
221 new HttpParam('connection_status', 'ALL'),
222 new HttpParam('client', 'chrome'),
223 new HttpParam('use_cdd', 'true')
226 params.push(new HttpParam('q', '^recent'));
228 origins.forEach(function(origin) {
229 var cpRequest = this.buildRequest_(
235 this.onSearchDone_.bind(this, isRecent));
236 this.outstandingCloudSearchRequests_.push(cpRequest);
237 this.sendOrQueueRequest_(cpRequest);
242 * Sends Google Cloud Print printer sharing invitations API requests.
243 * @param {string} account Account the request is sent for.
245 invites: function(account) {
247 new HttpParam('client', 'chrome'),
249 this.sendOrQueueRequest_(this.buildRequest_(
253 print_preview.Destination.Origin.COOKIES,
255 this.onInvitesDone_.bind(this)));
259 * Accepts or rejects printer sharing invitation.
260 * @param {!print_preview.Invitation} invitation Invitation to process.
261 * @param {boolean} accept Whether to accept this invitation.
263 processInvite: function(invitation, accept) {
265 new HttpParam('printerid', invitation.destination.id),
266 new HttpParam('email', invitation.scopeId),
267 new HttpParam('accept', accept),
268 new HttpParam('use_cdd', true),
270 this.sendOrQueueRequest_(this.buildRequest_(
274 invitation.destination.origin,
275 invitation.destination.account,
276 this.onProcessInviteDone_.bind(this, invitation, accept)));
280 * Sends a Google Cloud Print submit API request.
281 * @param {!print_preview.Destination} destination Cloud destination to
283 * @param {!print_preview.PrintTicketStore} printTicketStore Contains the
284 * print ticket to print.
285 * @param {!print_preview.DocumentInfo} documentInfo Document data model.
286 * @param {string} data Base64 encoded data of the document.
288 submit: function(destination, printTicketStore, documentInfo, data) {
290 CloudPrintInterface.VERSION_REGEXP_.exec(navigator.userAgent);
291 var chromeVersion = 'unknown';
292 if (result && result.length == 2) {
293 chromeVersion = result[1];
296 new HttpParam('printerid', destination.id),
297 new HttpParam('contentType', 'dataUrl'),
298 new HttpParam('title', documentInfo.title),
299 new HttpParam('ticket',
300 printTicketStore.createPrintTicket(destination)),
301 new HttpParam('content', 'data:application/pdf;base64,' + data),
303 '__google__chrome_version=' + chromeVersion),
304 new HttpParam('tag', '__google__os=' + navigator.platform)
306 var cpRequest = this.buildRequest_(
312 this.onSubmitDone_.bind(this));
313 this.sendOrQueueRequest_(cpRequest);
317 * Sends a Google Cloud Print printer API request.
318 * @param {string} printerId ID of the printer to lookup.
319 * @param {!print_preview.Destination.Origin} origin Origin of the printer.
320 * @param {string=} account Account this printer is registered for. When
321 * provided for COOKIES {@code origin}, and users sessions are still not
322 * known, will be checked against the response (both success and failure
323 * to get printer) and, if the active user account is not the one
324 * requested, {@code account} is activated and printer request reissued.
326 printer: function(printerId, origin, account) {
328 new HttpParam('printerid', printerId),
329 new HttpParam('use_cdd', 'true'),
330 new HttpParam('printer_connection_status', 'true')
332 this.sendOrQueueRequest_(this.buildRequest_(
338 this.onPrinterDone_.bind(this, printerId)));
342 * Sends a Google Cloud Print update API request to accept (or reject) the
343 * terms-of-service of the given printer.
344 * @param {!print_preview.Destination} destination Destination to accept ToS
346 * @param {boolean} isAccepted Whether the user accepted ToS or not.
348 updatePrinterTosAcceptance: function(destination, isAccepted) {
350 new HttpParam('printerid', destination.id),
351 new HttpParam('is_tos_accepted', isAccepted)
353 this.sendOrQueueRequest_(this.buildRequest_(
359 this.onUpdatePrinterTosAcceptanceDone_.bind(this)));
363 * Adds event listeners to relevant events.
366 addEventListeners_: function() {
369 print_preview.NativeLayer.EventType.ACCESS_TOKEN_READY,
370 this.onAccessTokenReady_.bind(this));
374 * Builds request to the Google Cloud Print API.
375 * @param {string} method HTTP method of the request.
376 * @param {string} action Google Cloud Print action to perform.
377 * @param {Array<!HttpParam>} params HTTP parameters to include in the
379 * @param {!print_preview.Destination.Origin} origin Origin for destination.
380 * @param {?string} account Account the request is sent for. Can be
381 * {@code null} or empty string if the request is not cookie bound or
382 * is sent on behalf of the primary user.
383 * @param {function(number, Object, !print_preview.Destination.Origin)}
384 * callback Callback to invoke when request completes.
385 * @return {!CloudPrintRequest} Partially prepared request.
388 buildRequest_: function(method, action, params, origin, account, callback) {
389 var url = this.baseUrl_ + '/' + action + '?xsrf=';
390 if (origin == print_preview.Destination.Origin.COOKIES) {
391 var xsrfToken = this.xsrfTokens_[account];
393 // TODO(rltoscano): Should throw an error if not a read-only action or
394 // issue an xsrf token request.
396 url = url + xsrfToken;
399 var index = this.userSessionIndex_[account] || 0;
401 url += '&user=' + index;
407 if (method == 'GET') {
408 url = params.reduce(function(partialUrl, param) {
409 return partialUrl + '&' + param.name + '=' +
410 encodeURIComponent(param.value);
412 } else if (method == 'POST') {
413 body = params.reduce(function(partialBody, param) {
414 return partialBody + 'Content-Disposition: form-data; name=\"' +
415 param.name + '\"\r\n\r\n' + param.value + '\r\n--' +
416 CloudPrintInterface.MULTIPART_BOUNDARY_ + '\r\n';
417 }, '--' + CloudPrintInterface.MULTIPART_BOUNDARY_ + '\r\n');
422 headers['X-CloudPrint-Proxy'] = 'ChromePrintPreview';
423 if (method == 'GET') {
424 headers['Content-Type'] = CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_;
425 } else if (method == 'POST') {
426 headers['Content-Type'] = CloudPrintInterface.MULTIPART_CONTENT_TYPE_;
429 var xhr = new XMLHttpRequest();
430 xhr.open(method, url, true);
431 xhr.withCredentials =
432 (origin == print_preview.Destination.Origin.COOKIES);
433 for (var header in headers) {
434 xhr.setRequestHeader(header, headers[header]);
437 return new CloudPrintRequest(xhr, body, origin, account, callback);
441 * Sends a request to the Google Cloud Print API or queues if it needs to
442 * wait OAuth2 access token.
443 * @param {!CloudPrintRequest} request Request to send or queue.
446 sendOrQueueRequest_: function(request) {
447 if (request.origin == print_preview.Destination.Origin.COOKIES) {
448 return this.sendRequest_(request);
450 this.requestQueue_.push(request);
451 this.nativeLayer_.startGetAccessToken(request.origin);
456 * Sends a request to the Google Cloud Print API.
457 * @param {!CloudPrintRequest} request Request to send.
460 sendRequest_: function(request) {
461 request.xhr.onreadystatechange =
462 this.onReadyStateChange_.bind(this, request);
463 request.xhr.send(request.body);
467 * Creates a Google Cloud Print interface error that is ready to dispatch.
468 * @param {!CloudPrintInterface.EventType} type Type of the error.
469 * @param {!CloudPrintRequest} request Request that has been completed.
470 * @return {!Event} Google Cloud Print interface error event.
473 createErrorEvent_: function(type, request) {
474 var errorEvent = new Event(type);
475 errorEvent.status = request.xhr.status;
476 if (request.xhr.status == 200) {
477 errorEvent.errorCode = request.result['errorCode'];
478 errorEvent.message = request.result['message'];
480 errorEvent.errorCode = 0;
481 errorEvent.message = '';
483 errorEvent.origin = request.origin;
488 * Updates user info and session index from the {@code request} response.
489 * @param {!CloudPrintRequest} request Request to extract user info from.
492 setUsers_: function(request) {
493 if (request.origin == print_preview.Destination.Origin.COOKIES) {
494 var users = request.result['request']['users'] || [];
495 this.userSessionIndex_ = {};
496 for (var i = 0; i < users.length; i++) {
497 this.userSessionIndex_[users[i]] = i;
499 this.userInfo_.setUsers(request.result['request']['user'], users);
504 * Terminates search requests for requested {@code origins}.
505 * @param {!Array<print_preview.Destination.Origin>} origins Origins
506 * to terminate search requests for.
509 abortSearchRequests_: function(origins) {
510 this.outstandingCloudSearchRequests_ =
511 this.outstandingCloudSearchRequests_.filter(function(request) {
512 if (origins.indexOf(request.origin) >= 0) {
521 * Called when a native layer receives access token.
522 * @param {Event} event Contains the authentication type and access token.
525 onAccessTokenReady_: function(event) {
526 // TODO(vitalybuka): remove when other Origins implemented.
527 assert(event.authType == print_preview.Destination.Origin.DEVICE);
528 this.requestQueue_ = this.requestQueue_.filter(function(request) {
529 assert(request.origin == print_preview.Destination.Origin.DEVICE);
530 if (request.origin != event.authType) {
533 if (event.accessToken) {
534 request.xhr.setRequestHeader('Authorization',
535 'Bearer ' + event.accessToken);
536 this.sendRequest_(request);
537 } else { // No valid token.
538 // Without abort status does not exist.
540 request.callback(request);
547 * Called when the ready-state of a XML http request changes.
548 * Calls the successCallback with the result or dispatches an ERROR event.
549 * @param {!CloudPrintRequest} request Request that was changed.
552 onReadyStateChange_: function(request) {
553 if (request.xhr.readyState == 4) {
554 if (request.xhr.status == 200) {
555 request.result = JSON.parse(request.xhr.responseText);
556 if (request.origin == print_preview.Destination.Origin.COOKIES &&
557 request.result['success']) {
558 this.xsrfTokens_[request.result['request']['user']] =
559 request.result['xsrf_token'];
562 request.status = request.xhr.status;
563 request.callback(request);
568 * Called when the search request completes.
569 * @param {boolean} isRecent Whether the search request was for recent
571 * @param {!CloudPrintRequest} request Request that has been completed.
574 onSearchDone_: function(isRecent, request) {
575 var lastRequestForThisOrigin = true;
576 this.outstandingCloudSearchRequests_ =
577 this.outstandingCloudSearchRequests_.filter(function(item) {
578 if (item != request && item.origin == request.origin) {
579 lastRequestForThisOrigin = false;
581 return item != request;
584 if (request.origin == print_preview.Destination.Origin.COOKIES) {
587 request.result['request'] &&
588 request.result['request']['user'];
591 if (request.xhr.status == 200 && request.result['success']) {
593 var printerListJson = request.result['printers'] || [];
594 var printerList = [];
595 printerListJson.forEach(function(printerJson) {
597 printerList.push(cloudprint.CloudDestinationParser.parse(
598 printerJson, request.origin, activeUser));
600 console.error('Unable to parse cloud print destination: ' + err);
603 // Extract and store users.
604 this.setUsers_(request);
605 // Dispatch SEARCH_DONE event.
606 event = new Event(CloudPrintInterface.EventType.SEARCH_DONE);
607 event.origin = request.origin;
608 event.printers = printerList;
609 event.isRecent = isRecent;
611 event = this.createErrorEvent_(
612 CloudPrintInterface.EventType.SEARCH_FAILED,
615 event.user = activeUser;
616 event.searchDone = lastRequestForThisOrigin;
617 this.dispatchEvent(event);
621 * Called when invitations search request completes.
622 * @param {!CloudPrintRequest} request Request that has been completed.
625 onInvitesDone_: function(request) {
629 request.result['request'] &&
630 request.result['request']['user']) || '';
631 if (request.xhr.status == 200 && request.result['success']) {
632 // Extract invitations.
633 var invitationListJson = request.result['invites'] || [];
634 var invitationList = [];
635 invitationListJson.forEach(function(invitationJson) {
637 invitationList.push(cloudprint.InvitationParser.parse(
638 invitationJson, activeUser));
640 console.error('Unable to parse invitation: ' + e);
643 // Dispatch INVITES_DONE event.
644 event = new Event(CloudPrintInterface.EventType.INVITES_DONE);
645 event.invitations = invitationList;
647 event = this.createErrorEvent_(
648 CloudPrintInterface.EventType.INVITES_FAILED, request);
650 event.user = activeUser;
651 this.dispatchEvent(event);
655 * Called when invitation processing request completes.
656 * @param {!print_preview.Invitation} invitation Processed invitation.
657 * @param {boolean} accept Whether this invitation was accepted or rejected.
658 * @param {!CloudPrintRequest} request Request that has been completed.
661 onProcessInviteDone_: function(invitation, accept, request) {
665 request.result['request'] &&
666 request.result['request']['user']) || '';
667 if (request.xhr.status == 200 && request.result['success']) {
668 event = new Event(CloudPrintInterface.EventType.PROCESS_INVITE_DONE);
671 event.printer = cloudprint.CloudDestinationParser.parse(
672 request.result['printer'], request.origin, activeUser);
674 console.error('Failed to parse cloud print destination: ' + e);
678 event = this.createErrorEvent_(
679 CloudPrintInterface.EventType.PROCESS_INVITE_FAILED, request);
681 event.invitation = invitation;
682 event.accept = accept;
683 event.user = activeUser;
684 this.dispatchEvent(event);
688 * Called when the submit request completes.
689 * @param {!CloudPrintRequest} request Request that has been completed.
692 onSubmitDone_: function(request) {
693 if (request.xhr.status == 200 && request.result['success']) {
694 var submitDoneEvent = new Event(
695 CloudPrintInterface.EventType.SUBMIT_DONE);
696 submitDoneEvent.jobId = request.result['job']['id'];
697 this.dispatchEvent(submitDoneEvent);
699 var errorEvent = this.createErrorEvent_(
700 CloudPrintInterface.EventType.SUBMIT_FAILED, request);
701 this.dispatchEvent(errorEvent);
706 * Called when the printer request completes.
707 * @param {string} destinationId ID of the destination that was looked up.
708 * @param {!CloudPrintRequest} request Request that has been completed.
711 onPrinterDone_: function(destinationId, request) {
712 // Special handling of the first printer request. It does not matter at
713 // this point, whether printer was found or not.
714 if (request.origin == print_preview.Destination.Origin.COOKIES &&
717 request.result['request']['user'] &&
718 request.result['request']['users'] &&
719 request.account != request.result['request']['user']) {
720 this.setUsers_(request);
721 // In case the user account is known, but not the primary one,
723 if (this.userSessionIndex_[request.account] > 0) {
724 this.userInfo_.activeUser = request.account;
725 // Repeat the request for the newly activated account.
727 request.result['request']['params']['printerid'],
730 // Stop processing this request, wait for the new response.
735 if (request.xhr.status == 200 && request.result['success']) {
737 if (request.origin == print_preview.Destination.Origin.COOKIES) {
738 activeUser = request.result['request']['user'];
740 var printerJson = request.result['printers'][0];
743 printer = cloudprint.CloudDestinationParser.parse(
744 printerJson, request.origin, activeUser);
746 console.error('Failed to parse cloud print destination: ' +
747 JSON.stringify(printerJson));
750 var printerDoneEvent =
751 new Event(CloudPrintInterface.EventType.PRINTER_DONE);
752 printerDoneEvent.printer = printer;
753 this.dispatchEvent(printerDoneEvent);
755 var errorEvent = this.createErrorEvent_(
756 CloudPrintInterface.EventType.PRINTER_FAILED, request);
757 errorEvent.destinationId = destinationId;
758 errorEvent.destinationOrigin = request.origin;
759 this.dispatchEvent(errorEvent);
764 * Called when the update printer TOS acceptance request completes.
765 * @param {!CloudPrintRequest} request Request that has been completed.
768 onUpdatePrinterTosAcceptanceDone_: function(request) {
769 if (request.xhr.status == 200 && request.result['success']) {
772 var errorEvent = this.createErrorEvent_(
773 CloudPrintInterface.EventType.SUBMIT_FAILED, request);
774 this.dispatchEvent(errorEvent);
780 * Data structure that holds data for Cloud Print requests.
781 * @param {!XMLHttpRequest} xhr Partially prepared http request.
782 * @param {string} body Data to send with POST requests.
783 * @param {!print_preview.Destination.Origin} origin Origin for destination.
784 * @param {?string} account Account the request is sent for. Can be
785 * {@code null} or empty string if the request is not cookie bound or
786 * is sent on behalf of the primary user.
787 * @param {function(!CloudPrintRequest)} callback Callback to invoke when
791 function CloudPrintRequest(xhr, body, origin, account, callback) {
793 * Partially prepared http request.
794 * @type {!XMLHttpRequest}
799 * Data to send with POST requests.
805 * Origin for destination.
806 * @type {!print_preview.Destination.Origin}
808 this.origin = origin;
811 * User account this request is expected to be executed for.
814 this.account = account;
817 * Callback to invoke when request completes.
818 * @type {function(!CloudPrintRequest)}
820 this.callback = callback;
823 * Result for requests.
824 * @type {Object} JSON response.
830 * Data structure that represents an HTTP parameter.
831 * @param {string} name Name of the parameter.
832 * @param {string} value Value of the parameter.
835 function HttpParam(name, value) {
837 * Name of the parameter.
851 CloudPrintInterface: CloudPrintInterface