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
16 * @extends {cr.EventTarget}
18 function CloudPrintInterface(baseUrl
, nativeLayer
) {
20 * The base URL of the Google Cloud Print API.
24 this.baseUrl_
= baseUrl
;
27 * Used to get Auth2 tokens.
28 * @type {!print_preview.NativeLayer}
31 this.nativeLayer_
= nativeLayer
;
34 * Last received XSRF token. Sent as a parameter in every request.
41 * Pending requests delayed until we get access token.
42 * @type {!Array.<!CloudPrintRequest>}
45 this.requestQueue_
= [];
48 * Number of outstanding cloud destination search requests.
52 this.outstandingCloudSearchRequestCount_
= 0;
55 * Event tracker used to keep track of native layer events.
56 * @type {!EventTracker}
59 this.tracker_
= new EventTracker();
61 this.addEventListeners_();
65 * Event types dispatched by the interface.
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'
80 * Content type header value for a URL encoded HTTP request.
85 CloudPrintInterface
.URL_ENCODED_CONTENT_TYPE_
=
86 'application/x-www-form-urlencoded';
89 * Multi-part POST request boundary used in communication with Google
95 CloudPrintInterface
.MULTIPART_BOUNDARY_
=
96 '----CloudPrintFormBoundaryjc9wuprokl8i';
99 * Content type header value for a multipart HTTP request.
104 CloudPrintInterface
.MULTIPART_CONTENT_TYPE_
=
105 'multipart/form-data; boundary=' +
106 CloudPrintInterface
.MULTIPART_BOUNDARY_
;
109 * Regex that extracts Chrome's version from the user-agent string.
114 CloudPrintInterface
.VERSION_REGEXP_
= /.*Chrome\/([\d\.]+)/i;
117 * Enumeration of JSON response fields from Google Cloud Print API.
121 CloudPrintInterface
.JsonFields_
= {
126 * Could Print origins used to search printers.
127 * @type {!Array.<!print_preview.Destination.Origin>}
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
138 CloudPrintInterface
.prototype = {
139 __proto__
: cr
.EventTarget
.prototype,
141 /** @return {string} Base URL of the Google Cloud Print service. */
143 return this.baseUrl_
;
147 * @return {boolean} Whether a search for cloud destinations is in progress.
149 get isCloudDestinationSearchInProgress() {
150 return this.outstandingCloudSearchRequestCount_
> 0;
154 * Sends a Google Cloud Print search API request.
155 * @param {boolean} isRecent Whether to search for only recently used
158 search: function(isRecent
) {
160 new HttpParam('connection_status', 'ALL'),
161 new HttpParam('client', 'chrome'),
162 new HttpParam('use_cdd', 'true')
165 params
.push(new HttpParam('q', '^recent'));
167 CloudPrintInterface
.CLOUD_ORIGINS_
.forEach(function(origin
) {
168 ++this.outstandingCloudSearchRequestCount_
;
170 this.buildRequest_('GET', 'search', params
, origin
,
171 this.onSearchDone_
.bind(this, isRecent
));
172 this.sendOrQueueRequest_(cpRequest
);
177 * Sends a Google Cloud Print submit API request.
178 * @param {!print_preview.Destination} destination Cloud destination 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.
185 submit: function(destination
, printTicketStore
, documentInfo
, data
) {
187 CloudPrintInterface
.VERSION_REGEXP_
.exec(navigator
.userAgent
);
188 var chromeVersion
= 'unknown';
189 if (result
&& result
.length
== 2) {
190 chromeVersion
= result
[1];
193 new HttpParam('printerid', destination
.id
),
194 new HttpParam('contentType', 'dataUrl'),
195 new HttpParam('title', documentInfo
.title
),
196 new HttpParam('ticket',
197 printTicketStore
.createPrintTicket(destination
)),
198 new HttpParam('content', 'data:application/pdf;base64,' + data
),
200 '__google__chrome_version=' + chromeVersion
),
201 new HttpParam('tag', '__google__os=' + navigator
.platform
)
203 var cpRequest
= this.buildRequest_('POST', 'submit', params
,
205 this.onSubmitDone_
.bind(this));
206 this.sendOrQueueRequest_(cpRequest
);
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.
214 printer: function(printerId
, origin
) {
216 new HttpParam('printerid', printerId
),
217 new HttpParam('use_cdd', 'true'),
218 new HttpParam('printer_connection_status', 'true')
221 this.buildRequest_('GET', 'printer', params
, origin
,
222 this.onPrinterDone_
.bind(this, printerId
));
223 this.sendOrQueueRequest_(cpRequest
);
227 * Sends a Google Cloud Print update API request to accept (or reject) the
228 * terms-of-service of the given printer.
229 * @param {string} printerId ID of the printer to accept the
230 * terms-of-service for.
231 * @param {!print_preview.Destination.Origin} origin Origin of the printer.
232 * @param {boolean} isAccepted Whether the user accepted the
235 updatePrinterTosAcceptance: function(printerId
, origin
, isAccepted
) {
237 new HttpParam('printerid', printerId
),
238 new HttpParam('is_tos_accepted', isAccepted
)
241 this.buildRequest_('POST', 'update', params
, origin
,
242 this.onUpdatePrinterTosAcceptanceDone_
.bind(this));
243 this.sendOrQueueRequest_(cpRequest
);
247 * Adds event listeners to the relevant native layer events.
250 addEventListeners_: function() {
253 print_preview
.NativeLayer
.EventType
.ACCESS_TOKEN_READY
,
254 this.onAccessTokenReady_
.bind(this));
258 * Builds request to the Google Cloud Print API.
259 * @param {string} method HTTP method of the request.
260 * @param {string} action Google Cloud Print action to perform.
261 * @param {Array.<!HttpParam>} params HTTP parameters to include in the
263 * @param {!print_preview.Destination.Origin} origin Origin for destination.
264 * @param {function(number, Object, !print_preview.Destination.Origin)}
265 * callback Callback to invoke when request completes.
266 * @return {!CloudPrintRequest} Partially prepared request.
269 buildRequest_: function(method
, action
, params
, origin
, callback
) {
270 var url
= this.baseUrl_
+ '/' + action
+ '?xsrf=';
271 if (origin
== print_preview
.Destination
.Origin
.COOKIES
) {
272 if (!this.xsrfToken_
) {
273 // TODO(rltoscano): Should throw an error if not a read-only action or
274 // issue an xsrf token request.
276 url
= url
+ this.xsrfToken_
;
281 if (method
== 'GET') {
282 url
= params
.reduce(function(partialUrl
, param
) {
283 return partialUrl
+ '&' + param
.name
+ '=' +
284 encodeURIComponent(param
.value
);
286 } else if (method
== 'POST') {
287 body
= params
.reduce(function(partialBody
, param
) {
288 return partialBody
+ 'Content-Disposition: form-data; name=\"' +
289 param
.name
+ '\"\r\n\r\n' + param
.value
+ '\r\n--' +
290 CloudPrintInterface
.MULTIPART_BOUNDARY_
+ '\r\n';
291 }, '--' + CloudPrintInterface
.MULTIPART_BOUNDARY_
+ '\r\n');
296 headers
['X-CloudPrint-Proxy'] = 'ChromePrintPreview';
297 if (method
== 'GET') {
298 headers
['Content-Type'] = CloudPrintInterface
.URL_ENCODED_CONTENT_TYPE_
;
299 } else if (method
== 'POST') {
300 headers
['Content-Type'] = CloudPrintInterface
.MULTIPART_CONTENT_TYPE_
;
303 var xhr
= new XMLHttpRequest();
304 xhr
.open(method
, url
, true);
305 xhr
.withCredentials
=
306 (origin
== print_preview
.Destination
.Origin
.COOKIES
);
307 for (var header
in headers
) {
308 xhr
.setRequestHeader(header
, headers
[header
]);
311 return new CloudPrintRequest(xhr
, body
, origin
, callback
);
315 * Sends a request to the Google Cloud Print API or queues if it needs to
316 * wait OAuth2 access token.
317 * @param {!CloudPrintRequest} request Request to send or queue.
320 sendOrQueueRequest_: function(request
) {
321 if (request
.origin
== print_preview
.Destination
.Origin
.COOKIES
) {
322 return this.sendRequest_(request
);
324 this.requestQueue_
.push(request
);
325 this.nativeLayer_
.startGetAccessToken(request
.origin
);
330 * Sends a request to the Google Cloud Print API.
331 * @param {!CloudPrintRequest} request Request to send.
334 sendRequest_: function(request
) {
335 request
.xhr
.onreadystatechange
=
336 this.onReadyStateChange_
.bind(this, request
);
337 request
.xhr
.send(request
.body
);
341 * Creates a Google Cloud Print interface error that is ready to dispatch.
342 * @param {!CloudPrintInterface.EventType} type Type of the error.
343 * @param {!CloudPrintRequest} request Request that has been completed.
344 * @return {!Event} Google Cloud Print interface error event.
347 createErrorEvent_: function(type
, request
) {
348 var errorEvent
= new Event(type
);
349 errorEvent
.status
= request
.xhr
.status
;
350 if (request
.xhr
.status
== 200) {
351 errorEvent
.errorCode
= request
.result
['errorCode'];
352 errorEvent
.message
= request
.result
['message'];
354 errorEvent
.errorCode
= 0;
355 errorEvent
.message
= '';
357 errorEvent
.origin
= request
.origin
;
362 * Called when a native layer receives access token.
363 * @param {Event} evt Contains the authetication type and access token.
366 onAccessTokenReady_: function(event
) {
367 // TODO(vitalybuka): remove when other Origins implemented.
368 assert(event
.authType
== print_preview
.Destination
.Origin
.DEVICE
);
369 this.requestQueue_
= this.requestQueue_
.filter(function(request
) {
370 assert(request
.origin
== print_preview
.Destination
.Origin
.DEVICE
);
371 if (request
.origin
!= event
.authType
) {
374 if (event
.accessToken
) {
375 request
.xhr
.setRequestHeader('Authorization',
376 'Bearer ' + event
.accessToken
);
377 this.sendRequest_(request
);
378 } else { // No valid token.
379 // Without abort status does not exists.
381 request
.callback(request
);
388 * Called when the ready-state of a XML http request changes.
389 * Calls the successCallback with the result or dispatches an ERROR event.
390 * @param {!CloudPrintRequest} request Request that was changed.
393 onReadyStateChange_: function(request
) {
394 if (request
.xhr
.readyState
== 4) {
395 if (request
.xhr
.status
== 200) {
396 request
.result
= JSON
.parse(request
.xhr
.responseText
);
397 if (request
.origin
== print_preview
.Destination
.Origin
.COOKIES
&&
398 request
.result
['success']) {
399 this.xsrfToken_
= request
.result
['xsrf_token'];
402 request
.status
= request
.xhr
.status
;
403 request
.callback(request
);
408 * Called when the search request completes.
409 * @param {boolean} isRecent Whether the search request was for recent
411 * @param {!CloudPrintRequest} request Request that has been completed.
414 onSearchDone_: function(isRecent
, request
) {
415 --this.outstandingCloudSearchRequestCount_
;
416 if (request
.xhr
.status
== 200 && request
.result
['success']) {
417 var printerListJson
= request
.result
['printers'] || [];
418 var printerList
= [];
419 printerListJson
.forEach(function(printerJson
) {
422 cloudprint
.CloudDestinationParser
.parse(printerJson
,
425 console
.error('Unable to parse cloud print destination: ' + err
);
428 var searchDoneEvent
=
429 new Event(CloudPrintInterface
.EventType
.SEARCH_DONE
);
430 searchDoneEvent
.printers
= printerList
;
431 searchDoneEvent
.origin
= request
.origin
;
432 searchDoneEvent
.isRecent
= isRecent
;
433 searchDoneEvent
.email
= request
.result
['request']['user'];
434 this.dispatchEvent(searchDoneEvent
);
436 var errorEvent
= this.createErrorEvent_(
437 CloudPrintInterface
.EventType
.SEARCH_FAILED
, request
);
438 this.dispatchEvent(errorEvent
);
443 * Called when the submit request completes.
444 * @param {!CloudPrintRequest} request Request that has been completed.
447 onSubmitDone_: function(request
) {
448 if (request
.xhr
.status
== 200 && request
.result
['success']) {
449 var submitDoneEvent
= new Event(
450 CloudPrintInterface
.EventType
.SUBMIT_DONE
);
451 submitDoneEvent
.jobId
= request
.result
['job']['id'];
452 this.dispatchEvent(submitDoneEvent
);
454 var errorEvent
= this.createErrorEvent_(
455 CloudPrintInterface
.EventType
.SUBMIT_FAILED
, request
);
456 this.dispatchEvent(errorEvent
);
461 * Called when the printer request completes.
462 * @param {string} destinationId ID of the destination that was looked up.
463 * @param {!CloudPrintRequest} request Request that has been completed.
466 onPrinterDone_: function(destinationId
, request
) {
467 if (request
.xhr
.status
== 200 && request
.result
['success']) {
468 var printerJson
= request
.result
['printers'][0];
471 printer
= cloudprint
.CloudDestinationParser
.parse(printerJson
,
474 console
.error('Failed to parse cloud print destination: ' +
475 JSON
.stringify(printerJson
));
478 var printerDoneEvent
=
479 new Event(CloudPrintInterface
.EventType
.PRINTER_DONE
);
480 printerDoneEvent
.printer
= printer
;
481 this.dispatchEvent(printerDoneEvent
);
483 var errorEvent
= this.createErrorEvent_(
484 CloudPrintInterface
.EventType
.PRINTER_FAILED
, request
);
485 errorEvent
.destinationId
= destinationId
;
486 errorEvent
.destinationOrigin
= request
.origin
;
487 this.dispatchEvent(errorEvent
, request
.origin
);
492 * Called when the update printer TOS acceptance request completes.
493 * @param {!CloudPrintRequest} request Request that has been completed.
496 onUpdatePrinterTosAcceptanceDone_: function(request
) {
497 if (request
.xhr
.status
== 200 && request
.result
['success']) {
500 var errorEvent
= this.createErrorEvent_(
501 CloudPrintInterface
.EventType
.SUBMIT_FAILED
, request
);
502 this.dispatchEvent(errorEvent
);
508 * Data structure that holds data for Cloud Print requests.
509 * @param {!XMLHttpRequest} xhr Partially prepared http request.
510 * @param {string} body Data to send with POST requests.
511 * @param {!print_preview.Destination.Origin} origin Origin for destination.
512 * @param {function(!CloudPrintRequest)} callback Callback to invoke when
516 function CloudPrintRequest(xhr
, body
, origin
, callback
) {
518 * Partially prepared http request.
519 * @type {!XMLHttpRequest}
524 * Data to send with POST requests.
530 * Origin for destination.
531 * @type {!print_preview.Destination.Origin}
533 this.origin
= origin
;
536 * Callback to invoke when request completes.
537 * @type {function(!CloudPrintRequest)}
539 this.callback
= callback
;
542 * Result for requests.
543 * @type {Object} JSON response.
549 * Data structure that represents an HTTP parameter.
550 * @param {string} name Name of the parameter.
551 * @param {string} value Value of the parameter.
554 function HttpParam(name
, value
) {
556 * Name of the parameter.
570 CloudPrintInterface
: CloudPrintInterface