Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / print_preview / cloud_print_interface.js
blobabf3b9667157b6bab97eb256560eb887008d9d6c
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() {
6   'use strict';
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    * @param {!print_preview.UserInfo} userInfo User information repository.
16    * @param {boolean} isInAppKioskMode Whether the print preview is in App
17    *     Kiosk mode.
18    * @constructor
19    * @extends {cr.EventTarget}
20    */
21   function CloudPrintInterface(
22       baseUrl, nativeLayer, userInfo, isInAppKioskMode) {
23     /**
24      * The base URL of the Google Cloud Print API.
25      * @type {string}
26      * @private
27      */
28     this.baseUrl_ = baseUrl;
30     /**
31      * Used to get Auth2 tokens.
32      * @type {!print_preview.NativeLayer}
33      * @private
34      */
35     this.nativeLayer_ = nativeLayer;
37     /**
38      * User information repository.
39      * @type {!print_preview.UserInfo}
40      * @private
41      */
42     this.userInfo_ = userInfo;
44     /**
45      * Whether Print Preview is in App Kiosk mode, basically, use only printers
46      * available for the device.
47      * @type {boolean}
48      * @private
49      */
50     this.isInAppKioskMode_ = isInAppKioskMode;
52     /**
53      * Currently logged in users (identified by email) mapped to the Google
54      * session index.
55      * @type {!Object<number>}
56      * @private
57      */
58     this.userSessionIndex_ = {};
60     /**
61      * Stores last received XSRF tokens for each user account. Sent as
62      * a parameter with every request.
63      * @type {!Object<string>}
64      * @private
65      */
66     this.xsrfTokens_ = {};
68     /**
69      * Pending requests delayed until we get access token.
70      * @type {!Array<!CloudPrintRequest>}
71      * @private
72      */
73     this.requestQueue_ = [];
75     /**
76      * Outstanding cloud destination search requests.
77      * @type {!Array<!CloudPrintRequest>}
78      * @private
79      */
80     this.outstandingCloudSearchRequests_ = [];
82     /**
83      * Event tracker used to keep track of native layer events.
84      * @type {!EventTracker}
85      * @private
86      */
87     this.tracker_ = new EventTracker();
89     this.addEventListeners_();
90   };
92   /**
93    * Event types dispatched by the interface.
94    * @enum {string}
95    */
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'
110   };
112   /**
113    * Content type header value for a URL encoded HTTP request.
114    * @type {string}
115    * @const
116    * @private
117    */
118   CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_ =
119       'application/x-www-form-urlencoded';
121   /**
122    * Multi-part POST request boundary used in communication with Google
123    * Cloud Print.
124    * @type {string}
125    * @const
126    * @private
127    */
128   CloudPrintInterface.MULTIPART_BOUNDARY_ =
129       '----CloudPrintFormBoundaryjc9wuprokl8i';
131   /**
132    * Content type header value for a multipart HTTP request.
133    * @type {string}
134    * @const
135    * @private
136    */
137   CloudPrintInterface.MULTIPART_CONTENT_TYPE_ =
138       'multipart/form-data; boundary=' +
139       CloudPrintInterface.MULTIPART_BOUNDARY_;
141   /**
142    * Regex that extracts Chrome's version from the user-agent string.
143    * @type {!RegExp}
144    * @const
145    * @private
146    */
147   CloudPrintInterface.VERSION_REGEXP_ = /.*Chrome\/([\d\.]+)/i;
149   /**
150    * Enumeration of JSON response fields from Google Cloud Print API.
151    * @enum {string}
152    * @private
153    */
154   CloudPrintInterface.JsonFields_ = {
155     PRINTER: 'printer'
156   };
158   /**
159    * Could Print origins used to search printers.
160    * @type {!Array<!print_preview.Destination.Origin>}
161    * @const
162    * @private
163    */
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
169   ];
171   CloudPrintInterface.prototype = {
172     __proto__: cr.EventTarget.prototype,
174     /** @return {string} Base URL of the Google Cloud Print service. */
175     get baseUrl() {
176       return this.baseUrl_;
177     },
179     /**
180      * @return {boolean} Whether a search for cloud destinations is in progress.
181      */
182     get isCloudDestinationSearchInProgress() {
183       return this.outstandingCloudSearchRequests_.length > 0;
184     },
186     /**
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.
193      */
194     search: function(opt_account, opt_origin) {
195       var account = opt_account || '';
196       var origins =
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;
201         });
202       }
203       this.abortSearchRequests_(origins);
204       this.search_(true, account, origins);
205       this.search_(false, account, origins);
206     },
208     /**
209      * Sends Google Cloud Print search API requests.
210      * @param {boolean} isRecent Whether to search for only recently used
211      *     printers.
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.
217      * @private
218      */
219     search_: function(isRecent, account, origins) {
220       var params = [
221         new HttpParam('connection_status', 'ALL'),
222         new HttpParam('client', 'chrome'),
223         new HttpParam('use_cdd', 'true')
224       ];
225       if (isRecent) {
226         params.push(new HttpParam('q', '^recent'));
227       }
228       origins.forEach(function(origin) {
229         var cpRequest = this.buildRequest_(
230             'GET',
231             'search',
232             params,
233             origin,
234             account,
235             this.onSearchDone_.bind(this, isRecent));
236         this.outstandingCloudSearchRequests_.push(cpRequest);
237         this.sendOrQueueRequest_(cpRequest);
238       }, this);
239     },
241     /**
242      * Sends Google Cloud Print printer sharing invitations API requests.
243      * @param {string} account Account the request is sent for.
244      */
245     invites: function(account) {
246       var params = [
247         new HttpParam('client', 'chrome'),
248       ];
249       this.sendOrQueueRequest_(this.buildRequest_(
250           'GET',
251           'invites',
252           params,
253           print_preview.Destination.Origin.COOKIES,
254           account,
255           this.onInvitesDone_.bind(this)));
256     },
258     /**
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.
262      */
263     processInvite: function(invitation, accept) {
264       var params = [
265         new HttpParam('printerid', invitation.destination.id),
266         new HttpParam('email', invitation.scopeId),
267         new HttpParam('accept', accept),
268         new HttpParam('use_cdd', true),
269       ];
270       this.sendOrQueueRequest_(this.buildRequest_(
271           'POST',
272           'processinvite',
273           params,
274           invitation.destination.origin,
275           invitation.destination.account,
276           this.onProcessInviteDone_.bind(this, invitation, accept)));
277     },
279     /**
280      * Sends a Google Cloud Print submit API request.
281      * @param {!print_preview.Destination} destination Cloud destination to
282      *     print 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.
287      */
288     submit: function(destination, printTicketStore, documentInfo, data) {
289       var result =
290           CloudPrintInterface.VERSION_REGEXP_.exec(navigator.userAgent);
291       var chromeVersion = 'unknown';
292       if (result && result.length == 2) {
293         chromeVersion = result[1];
294       }
295       var params = [
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),
302         new HttpParam('tag',
303                       '__google__chrome_version=' + chromeVersion),
304         new HttpParam('tag', '__google__os=' + navigator.platform)
305       ];
306       var cpRequest = this.buildRequest_(
307           'POST',
308           'submit',
309           params,
310           destination.origin,
311           destination.account,
312           this.onSubmitDone_.bind(this));
313       this.sendOrQueueRequest_(cpRequest);
314     },
316     /**
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.
325      */
326     printer: function(printerId, origin, account) {
327       var params = [
328         new HttpParam('printerid', printerId),
329         new HttpParam('use_cdd', 'true'),
330         new HttpParam('printer_connection_status', 'true')
331       ];
332       this.sendOrQueueRequest_(this.buildRequest_(
333           'GET',
334           'printer',
335           params,
336           origin,
337           account,
338           this.onPrinterDone_.bind(this, printerId)));
339     },
341     /**
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
345      *     for.
346      * @param {boolean} isAccepted Whether the user accepted ToS or not.
347      */
348     updatePrinterTosAcceptance: function(destination, isAccepted) {
349       var params = [
350         new HttpParam('printerid', destination.id),
351         new HttpParam('is_tos_accepted', isAccepted)
352       ];
353       this.sendOrQueueRequest_(this.buildRequest_(
354           'POST',
355           'update',
356           params,
357           destination.origin,
358           destination.account,
359           this.onUpdatePrinterTosAcceptanceDone_.bind(this)));
360     },
362     /**
363      * Adds event listeners to relevant events.
364      * @private
365      */
366     addEventListeners_: function() {
367       this.tracker_.add(
368           this.nativeLayer_,
369           print_preview.NativeLayer.EventType.ACCESS_TOKEN_READY,
370           this.onAccessTokenReady_.bind(this));
371     },
373     /**
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
378      *     request.
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.
386      * @private
387      */
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];
392         if (!xsrfToken) {
393           // TODO(rltoscano): Should throw an error if not a read-only action or
394           // issue an xsrf token request.
395         } else {
396           url = url + xsrfToken;
397         }
398         if (account) {
399           var index = this.userSessionIndex_[account] || 0;
400           if (index > 0) {
401             url += '&user=' + index;
402           }
403         }
404       }
405       var body = null;
406       if (params) {
407         if (method == 'GET') {
408           url = params.reduce(function(partialUrl, param) {
409             return partialUrl + '&' + param.name + '=' +
410                 encodeURIComponent(param.value);
411           }, url);
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');
418         }
419       }
421       var headers = {};
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_;
427       }
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]);
435       }
437       return new CloudPrintRequest(xhr, body, origin, account, callback);
438     },
440     /**
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.
444      * @private
445      */
446     sendOrQueueRequest_: function(request) {
447       if (request.origin == print_preview.Destination.Origin.COOKIES) {
448         return this.sendRequest_(request);
449       } else {
450         this.requestQueue_.push(request);
451         this.nativeLayer_.startGetAccessToken(request.origin);
452       }
453     },
455     /**
456      * Sends a request to the Google Cloud Print API.
457      * @param {!CloudPrintRequest} request Request to send.
458      * @private
459      */
460     sendRequest_: function(request) {
461       request.xhr.onreadystatechange =
462           this.onReadyStateChange_.bind(this, request);
463       request.xhr.send(request.body);
464     },
466     /**
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.
471      * @private
472      */
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'];
479       } else {
480         errorEvent.errorCode = 0;
481         errorEvent.message = '';
482       }
483       errorEvent.origin = request.origin;
484       return errorEvent;
485     },
487     /**
488      * Updates user info and session index from the {@code request} response.
489      * @param {!CloudPrintRequest} request Request to extract user info from.
490      * @private
491      */
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;
498         }
499         this.userInfo_.setUsers(request.result['request']['user'], users);
500       }
501     },
503     /**
504      * Terminates search requests for requested {@code origins}.
505      * @param {!Array<print_preview.Destination.Origin>} origins Origins
506      *     to terminate search requests for.
507      * @private
508      */
509     abortSearchRequests_: function(origins) {
510       this.outstandingCloudSearchRequests_ =
511           this.outstandingCloudSearchRequests_.filter(function(request) {
512             if (origins.indexOf(request.origin) >= 0) {
513               request.xhr.abort();
514               return false;
515             }
516             return true;
517           });
518     },
520     /**
521      * Called when a native layer receives access token.
522      * @param {Event} event Contains the authentication type and access token.
523      * @private
524      */
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) {
531           return true;
532         }
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.
539           request.xhr.abort();
540           request.callback(request);
541         }
542         return false;
543       }, this);
544     },
546     /**
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.
550      * @private
551      */
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'];
560           }
561         }
562         request.status = request.xhr.status;
563         request.callback(request);
564       }
565     },
567     /**
568      * Called when the search request completes.
569      * @param {boolean} isRecent Whether the search request was for recent
570      *     destinations.
571      * @param {!CloudPrintRequest} request Request that has been completed.
572      * @private
573      */
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;
580             }
581             return item != request;
582           });
583       var activeUser = '';
584       if (request.origin == print_preview.Destination.Origin.COOKIES) {
585         activeUser =
586             request.result &&
587             request.result['request'] &&
588             request.result['request']['user'];
589       }
590       var event = null;
591       if (request.xhr.status == 200 && request.result['success']) {
592         // Extract printers.
593         var printerListJson = request.result['printers'] || [];
594         var printerList = [];
595         printerListJson.forEach(function(printerJson) {
596           try {
597             printerList.push(cloudprint.CloudDestinationParser.parse(
598                 printerJson, request.origin, activeUser));
599           } catch (err) {
600             console.error('Unable to parse cloud print destination: ' + err);
601           }
602         });
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;
610       } else {
611         event = this.createErrorEvent_(
612             CloudPrintInterface.EventType.SEARCH_FAILED,
613             request);
614       }
615       event.user = activeUser;
616       event.searchDone = lastRequestForThisOrigin;
617       this.dispatchEvent(event);
618     },
620     /**
621      * Called when invitations search request completes.
622      * @param {!CloudPrintRequest} request Request that has been completed.
623      * @private
624      */
625     onInvitesDone_: function(request) {
626       var event = null;
627       var activeUser =
628           (request.result &&
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) {
636           try {
637             invitationList.push(cloudprint.InvitationParser.parse(
638                 invitationJson, activeUser));
639           } catch (e) {
640             console.error('Unable to parse invitation: ' + e);
641           }
642         });
643         // Dispatch INVITES_DONE event.
644         event = new Event(CloudPrintInterface.EventType.INVITES_DONE);
645         event.invitations = invitationList;
646       } else {
647         event = this.createErrorEvent_(
648             CloudPrintInterface.EventType.INVITES_FAILED, request);
649       }
650       event.user = activeUser;
651       this.dispatchEvent(event);
652     },
654     /**
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.
659      * @private
660      */
661     onProcessInviteDone_: function(invitation, accept, request) {
662       var event = null;
663       var activeUser =
664           (request.result &&
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);
669         if (accept) {
670           try {
671             event.printer = cloudprint.CloudDestinationParser.parse(
672                 request.result['printer'], request.origin, activeUser);
673           } catch (e) {
674             console.error('Failed to parse cloud print destination: ' + e);
675           }
676         }
677       } else {
678         event = this.createErrorEvent_(
679             CloudPrintInterface.EventType.PROCESS_INVITE_FAILED, request);
680       }
681       event.invitation = invitation;
682       event.accept = accept;
683       event.user = activeUser;
684       this.dispatchEvent(event);
685     },
687     /**
688      * Called when the submit request completes.
689      * @param {!CloudPrintRequest} request Request that has been completed.
690      * @private
691      */
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);
698       } else {
699         var errorEvent = this.createErrorEvent_(
700             CloudPrintInterface.EventType.SUBMIT_FAILED, request);
701         this.dispatchEvent(errorEvent);
702       }
703     },
705     /**
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.
709      * @private
710      */
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 &&
715           request.result &&
716           request.account &&
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,
722         // activate it.
723         if (this.userSessionIndex_[request.account] > 0) {
724           this.userInfo_.activeUser = request.account;
725           // Repeat the request for the newly activated account.
726           this.printer(
727               request.result['request']['params']['printerid'],
728               request.origin,
729               request.account);
730           // Stop processing this request, wait for the new response.
731           return;
732         }
733       }
734       // Process response.
735       if (request.xhr.status == 200 && request.result['success']) {
736         var activeUser = '';
737         if (request.origin == print_preview.Destination.Origin.COOKIES) {
738           activeUser = request.result['request']['user'];
739         }
740         var printerJson = request.result['printers'][0];
741         var printer;
742         try {
743           printer = cloudprint.CloudDestinationParser.parse(
744               printerJson, request.origin, activeUser);
745         } catch (err) {
746           console.error('Failed to parse cloud print destination: ' +
747               JSON.stringify(printerJson));
748           return;
749         }
750         var printerDoneEvent =
751             new Event(CloudPrintInterface.EventType.PRINTER_DONE);
752         printerDoneEvent.printer = printer;
753         this.dispatchEvent(printerDoneEvent);
754       } else {
755         var errorEvent = this.createErrorEvent_(
756             CloudPrintInterface.EventType.PRINTER_FAILED, request);
757         errorEvent.destinationId = destinationId;
758         errorEvent.destinationOrigin = request.origin;
759         this.dispatchEvent(errorEvent);
760       }
761     },
763     /**
764      * Called when the update printer TOS acceptance request completes.
765      * @param {!CloudPrintRequest} request Request that has been completed.
766      * @private
767      */
768     onUpdatePrinterTosAcceptanceDone_: function(request) {
769       if (request.xhr.status == 200 && request.result['success']) {
770         // Do nothing.
771       } else {
772         var errorEvent = this.createErrorEvent_(
773             CloudPrintInterface.EventType.SUBMIT_FAILED, request);
774         this.dispatchEvent(errorEvent);
775       }
776     }
777   };
779   /**
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
788    *     request completes.
789    * @constructor
790    */
791   function CloudPrintRequest(xhr, body, origin, account, callback) {
792     /**
793      * Partially prepared http request.
794      * @type {!XMLHttpRequest}
795      */
796     this.xhr = xhr;
798     /**
799      * Data to send with POST requests.
800      * @type {string}
801      */
802     this.body = body;
804     /**
805      * Origin for destination.
806      * @type {!print_preview.Destination.Origin}
807      */
808     this.origin = origin;
810     /**
811      * User account this request is expected to be executed for.
812      * @type {?string}
813      */
814     this.account = account;
816     /**
817      * Callback to invoke when request completes.
818      * @type {function(!CloudPrintRequest)}
819      */
820     this.callback = callback;
822     /**
823      * Result for requests.
824      * @type {Object} JSON response.
825      */
826     this.result = null;
827   };
829   /**
830    * Data structure that represents an HTTP parameter.
831    * @param {string} name Name of the parameter.
832    * @param {string} value Value of the parameter.
833    * @constructor
834    */
835   function HttpParam(name, value) {
836     /**
837      * Name of the parameter.
838      * @type {string}
839      */
840     this.name = name;
842     /**
843      * Name of the value.
844      * @type {string}
845      */
846     this.value = value;
847   };
849   // Export
850   return {
851     CloudPrintInterface: CloudPrintInterface
852   };