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   * @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;
29
30    /**
31     * Used to get Auth2 tokens.
32     * @type {!print_preview.NativeLayer}
33     * @private
34     */
35    this.nativeLayer_ = nativeLayer;
36
37    /**
38     * User information repository.
39     * @type {!print_preview.UserInfo}
40     * @private
41     */
42    this.userInfo_ = userInfo;
43
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;
51
52    /**
53     * Currently logged in users (identified by email) mapped to the Google
54     * session index.
55     * @type {!Object.<string, number>}
56     * @private
57     */
58    this.userSessionIndex_ = {};
59
60    /**
61     * Stores last received XSRF tokens for each user account. Sent as
62     * a parameter with every request.
63     * @type {!Object.<string, string>}
64     * @private
65     */
66    this.xsrfTokens_ = {};
67
68    /**
69     * Pending requests delayed until we get access token.
70     * @type {!Array.<!CloudPrintRequest>}
71     * @private
72     */
73    this.requestQueue_ = [];
74
75    /**
76     * Outstanding cloud destination search requests.
77     * @type {!Array.<!CloudPrintRequest>}
78     * @private
79     */
80    this.outstandingCloudSearchRequests_ = [];
81
82    /**
83     * Event tracker used to keep track of native layer events.
84     * @type {!EventTracker}
85     * @private
86     */
87    this.tracker_ = new EventTracker();
88
89    this.addEventListeners_();
90  };
91
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  };
111
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';
120
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';
130
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_;
140
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;
148
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  };
157
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  ];
170
171  CloudPrintInterface.prototype = {
172    __proto__: cr.EventTarget.prototype,
173
174    /** @return {string} Base URL of the Google Cloud Print service. */
175    get baseUrl() {
176      return this.baseUrl_;
177    },
178
179    /**
180     * @return {boolean} Whether a search for cloud destinations is in progress.
181     */
182    get isCloudDestinationSearchInProgress() {
183      return this.outstandingCloudSearchRequests_.length > 0;
184    },
185
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    },
207
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    },
240
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    },
257
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    },
278
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    },
315
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    },
340
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    },
361
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    },
372
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      }
420
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      }
428
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      }
436
437      return new CloudPrintRequest(xhr, body, origin, account, callback);
438    },
439
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    },
454
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    },
465
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    },
486
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    },
502
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    },
519
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    },
545
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    },
566
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    },
619
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    },
653
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    },
686
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    },
704
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, request.origin);
760      }
761    },
762
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  };
778
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;
797
798    /**
799     * Data to send with POST requests.
800     * @type {string}
801     */
802    this.body = body;
803
804    /**
805     * Origin for destination.
806     * @type {!print_preview.Destination.Origin}
807     */
808    this.origin = origin;
809
810    /**
811     * User account this request is expected to be executed for.
812     * @type {?string}
813     */
814    this.account = account;
815
816    /**
817     * Callback to invoke when request completes.
818     * @type {function(!CloudPrintRequest)}
819     */
820    this.callback = callback;
821
822    /**
823     * Result for requests.
824     * @type {Object} JSON response.
825     */
826    this.result = null;
827  };
828
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;
841
842    /**
843     * Name of the value.
844     * @type {string}
845     */
846    this.value = value;
847  };
848
849  // Export
850  return {
851    CloudPrintInterface: CloudPrintInterface
852  };
853});
854