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