Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / print_preview / cloud_print_interface.js
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
5 cr.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    * @constructor
17    * @extends {cr.EventTarget}
18    */
19   function CloudPrintInterface(baseUrl, nativeLayer, userInfo) {
20     /**
21      * The base URL of the Google Cloud Print API.
22      * @type {string}
23      * @private
24      */
25     this.baseUrl_ = baseUrl;
26
27     /**
28      * Used to get Auth2 tokens.
29      * @type {!print_preview.NativeLayer}
30      * @private
31      */
32     this.nativeLayer_ = nativeLayer;
33
34     /**
35      * User information repository.
36      * @type {!print_preview.UserInfo}
37      * @private
38      */
39     this.userInfo_ = userInfo;
40
41     /**
42      * Currently logged in users (identified by email) mapped to the Google
43      * session index.
44      * @type {!Object.<string, number>}
45      * @private
46      */
47     this.userSessionIndex_ = {};
48
49     /**
50      * Stores last received XSRF tokens for each user account. Sent as
51      * a parameter with every request.
52      * @type {!Object.<string, string>}
53      * @private
54      */
55     this.xsrfTokens_ = {};
56
57     /**
58      * Pending requests delayed until we get access token.
59      * @type {!Array.<!CloudPrintRequest>}
60      * @private
61      */
62     this.requestQueue_ = [];
63
64     /**
65      * Outstanding cloud destination search requests.
66      * @type {!Array.<!CloudPrintRequest>}
67      * @private
68      */
69     this.outstandingCloudSearchRequests_ = [];
70
71     /**
72      * Event tracker used to keep track of native layer events.
73      * @type {!EventTracker}
74      * @private
75      */
76     this.tracker_ = new EventTracker();
77
78     this.addEventListeners_();
79   };
80
81   /**
82    * Event types dispatched by the interface.
83    * @enum {string}
84    */
85   CloudPrintInterface.EventType = {
86     PRINTER_DONE: 'cloudprint.CloudPrintInterface.PRINTER_DONE',
87     PRINTER_FAILED: 'cloudprint.CloudPrintInterface.PRINTER_FAILED',
88     SEARCH_DONE: 'cloudprint.CloudPrintInterface.SEARCH_DONE',
89     SEARCH_FAILED: 'cloudprint.CloudPrintInterface.SEARCH_FAILED',
90     SUBMIT_DONE: 'cloudprint.CloudPrintInterface.SUBMIT_DONE',
91     SUBMIT_FAILED: 'cloudprint.CloudPrintInterface.SUBMIT_FAILED',
92     UPDATE_PRINTER_TOS_ACCEPTANCE_FAILED:
93         'cloudprint.CloudPrintInterface.UPDATE_PRINTER_TOS_ACCEPTANCE_FAILED'
94   };
95
96   /**
97    * Content type header value for a URL encoded HTTP request.
98    * @type {string}
99    * @const
100    * @private
101    */
102   CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_ =
103       'application/x-www-form-urlencoded';
104
105   /**
106    * Multi-part POST request boundary used in communication with Google
107    * Cloud Print.
108    * @type {string}
109    * @const
110    * @private
111    */
112   CloudPrintInterface.MULTIPART_BOUNDARY_ =
113       '----CloudPrintFormBoundaryjc9wuprokl8i';
114
115   /**
116    * Content type header value for a multipart HTTP request.
117    * @type {string}
118    * @const
119    * @private
120    */
121   CloudPrintInterface.MULTIPART_CONTENT_TYPE_ =
122       'multipart/form-data; boundary=' +
123       CloudPrintInterface.MULTIPART_BOUNDARY_;
124
125   /**
126    * Regex that extracts Chrome's version from the user-agent string.
127    * @type {!RegExp}
128    * @const
129    * @private
130    */
131   CloudPrintInterface.VERSION_REGEXP_ = /.*Chrome\/([\d\.]+)/i;
132
133   /**
134    * Enumeration of JSON response fields from Google Cloud Print API.
135    * @enum {string}
136    * @private
137    */
138   CloudPrintInterface.JsonFields_ = {
139     PRINTER: 'printer'
140   };
141
142   /**
143    * Could Print origins used to search printers.
144    * @type {!Array.<!print_preview.Destination.Origin>}
145    * @const
146    * @private
147    */
148   CloudPrintInterface.CLOUD_ORIGINS_ = [
149       print_preview.Destination.Origin.COOKIES,
150       print_preview.Destination.Origin.DEVICE
151       // TODO(vitalybuka): Enable when implemented.
152       // ready print_preview.Destination.Origin.PROFILE
153   ];
154
155   CloudPrintInterface.prototype = {
156     __proto__: cr.EventTarget.prototype,
157
158     /** @return {string} Base URL of the Google Cloud Print service. */
159     get baseUrl() {
160       return this.baseUrl_;
161     },
162
163     /**
164      * @return {boolean} Whether a search for cloud destinations is in progress.
165      */
166     get isCloudDestinationSearchInProgress() {
167       return this.outstandingCloudSearchRequests_.length > 0;
168     },
169
170     /**
171      * Sends Google Cloud Print search API request.
172      * @param {string=} opt_account Account the search is sent for. When
173      *      omitted, the search is done on behalf of the primary user.
174      * @param {print_preview.Destination.Origin=} opt_origin When specified,
175      *     searches destinations for {@code opt_origin} only, otherwise starts
176      *     searches for all origins.
177      */
178     search: function(opt_account, opt_origin) {
179       var account = opt_account || '';
180       var origins =
181           opt_origin && [opt_origin] || CloudPrintInterface.CLOUD_ORIGINS_;
182       this.abortSearchRequests_(origins);
183       this.search_(true, account, origins);
184       this.search_(false, account, origins);
185     },
186
187     /**
188      * Sends Google Cloud Print search API requests.
189      * @param {boolean} isRecent Whether to search for only recently used
190      *     printers.
191      * @param {string} account Account the search is sent for. It matters for
192      *     COOKIES origin only, and can be empty (sent on behalf of the primary
193      *     user in this case).
194      * @param {!Array.<!print_preview.Destination.Origin>} origins Origins to
195      *     search printers for.
196      * @private
197      */
198     search_: function(isRecent, account, origins) {
199       var params = [
200         new HttpParam('connection_status', 'ALL'),
201         new HttpParam('client', 'chrome'),
202         new HttpParam('use_cdd', 'true')
203       ];
204       if (isRecent) {
205         params.push(new HttpParam('q', '^recent'));
206       }
207       origins.forEach(function(origin) {
208         var cpRequest = this.buildRequest_(
209             'GET',
210             'search',
211             params,
212             origin,
213             account,
214             this.onSearchDone_.bind(this, isRecent));
215         this.outstandingCloudSearchRequests_.push(cpRequest);
216         this.sendOrQueueRequest_(cpRequest);
217       }, this);
218     },
219
220     /**
221      * Sends a Google Cloud Print submit API request.
222      * @param {!print_preview.Destination} destination Cloud destination to
223      *     print to.
224      * @param {!print_preview.PrintTicketStore} printTicketStore Contains the
225      *     print ticket to print.
226      * @param {!print_preview.DocumentInfo} documentInfo Document data model.
227      * @param {string} data Base64 encoded data of the document.
228      */
229     submit: function(destination, printTicketStore, documentInfo, data) {
230       var result =
231           CloudPrintInterface.VERSION_REGEXP_.exec(navigator.userAgent);
232       var chromeVersion = 'unknown';
233       if (result && result.length == 2) {
234         chromeVersion = result[1];
235       }
236       var params = [
237         new HttpParam('printerid', destination.id),
238         new HttpParam('contentType', 'dataUrl'),
239         new HttpParam('title', documentInfo.title),
240         new HttpParam('ticket',
241                       printTicketStore.createPrintTicket(destination)),
242         new HttpParam('content', 'data:application/pdf;base64,' + data),
243         new HttpParam('tag',
244                       '__google__chrome_version=' + chromeVersion),
245         new HttpParam('tag', '__google__os=' + navigator.platform)
246       ];
247       var cpRequest = this.buildRequest_(
248           'POST',
249           'submit',
250           params,
251           destination.origin,
252           destination.account,
253           this.onSubmitDone_.bind(this));
254       this.sendOrQueueRequest_(cpRequest);
255     },
256
257     /**
258      * Sends a Google Cloud Print printer API request.
259      * @param {string} printerId ID of the printer to lookup.
260      * @param {!print_preview.Destination.Origin} origin Origin of the printer.
261      * @param {string=} account Account this printer is registered for. When
262      *     provided for COOKIES {@code origin}, and users sessions are still not
263      *     known, will be checked against the response (both success and failure
264      *     to get printer) and, if the active user account is not the one
265      *     requested, {@code account} is activated and printer request reissued.
266      */
267     printer: function(printerId, origin, account) {
268       var params = [
269         new HttpParam('printerid', printerId),
270         new HttpParam('use_cdd', 'true'),
271         new HttpParam('printer_connection_status', 'true')
272       ];
273       this.sendOrQueueRequest_(this.buildRequest_(
274           'GET',
275           'printer',
276           params,
277           origin,
278           account,
279           this.onPrinterDone_.bind(this, printerId)));
280     },
281
282     /**
283      * Sends a Google Cloud Print update API request to accept (or reject) the
284      * terms-of-service of the given printer.
285      * @param {!print_preview.Destination} destination Destination to accept ToS
286      *     for.
287      * @param {boolean} isAccepted Whether the user accepted ToS or not.
288      */
289     updatePrinterTosAcceptance: function(destination, isAccepted) {
290       var params = [
291         new HttpParam('printerid', destination.id),
292         new HttpParam('is_tos_accepted', isAccepted)
293       ];
294       this.sendOrQueueRequest_(this.buildRequest_(
295           'POST',
296           'update',
297           params,
298           destination.origin,
299           destination.account,
300           this.onUpdatePrinterTosAcceptanceDone_.bind(this)));
301     },
302
303     /**
304      * Adds event listeners to relevant events.
305      * @private
306      */
307     addEventListeners_: function() {
308       this.tracker_.add(
309           this.nativeLayer_,
310           print_preview.NativeLayer.EventType.ACCESS_TOKEN_READY,
311           this.onAccessTokenReady_.bind(this));
312     },
313
314     /**
315      * Builds request to the Google Cloud Print API.
316      * @param {string} method HTTP method of the request.
317      * @param {string} action Google Cloud Print action to perform.
318      * @param {Array.<!HttpParam>} params HTTP parameters to include in the
319      *     request.
320      * @param {!print_preview.Destination.Origin} origin Origin for destination.
321      * @param {?string} account Account the request is sent for. Can be
322      *     {@code null} or empty string if the request is not cookie bound or
323      *     is sent on behalf of the primary user.
324      * @param {function(number, Object, !print_preview.Destination.Origin)}
325      *     callback Callback to invoke when request completes.
326      * @return {!CloudPrintRequest} Partially prepared request.
327      * @private
328      */
329     buildRequest_: function(method, action, params, origin, account, callback) {
330       var url = this.baseUrl_ + '/' + action + '?xsrf=';
331       if (origin == print_preview.Destination.Origin.COOKIES) {
332         var xsrfToken = this.xsrfTokens_[account];
333         if (!xsrfToken) {
334           // TODO(rltoscano): Should throw an error if not a read-only action or
335           // issue an xsrf token request.
336         } else {
337           url = url + xsrfToken;
338         }
339         if (account) {
340           var index = this.userSessionIndex_[account] || 0;
341           if (index > 0) {
342             url += '&user=' + index;
343           }
344         }
345       }
346       var body = null;
347       if (params) {
348         if (method == 'GET') {
349           url = params.reduce(function(partialUrl, param) {
350             return partialUrl + '&' + param.name + '=' +
351                 encodeURIComponent(param.value);
352           }, url);
353         } else if (method == 'POST') {
354           body = params.reduce(function(partialBody, param) {
355             return partialBody + 'Content-Disposition: form-data; name=\"' +
356                 param.name + '\"\r\n\r\n' + param.value + '\r\n--' +
357                 CloudPrintInterface.MULTIPART_BOUNDARY_ + '\r\n';
358           }, '--' + CloudPrintInterface.MULTIPART_BOUNDARY_ + '\r\n');
359         }
360       }
361
362       var headers = {};
363       headers['X-CloudPrint-Proxy'] = 'ChromePrintPreview';
364       if (method == 'GET') {
365         headers['Content-Type'] = CloudPrintInterface.URL_ENCODED_CONTENT_TYPE_;
366       } else if (method == 'POST') {
367         headers['Content-Type'] = CloudPrintInterface.MULTIPART_CONTENT_TYPE_;
368       }
369
370       var xhr = new XMLHttpRequest();
371       xhr.open(method, url, true);
372       xhr.withCredentials =
373           (origin == print_preview.Destination.Origin.COOKIES);
374       for (var header in headers) {
375         xhr.setRequestHeader(header, headers[header]);
376       }
377
378       return new CloudPrintRequest(xhr, body, origin, account, callback);
379     },
380
381     /**
382      * Sends a request to the Google Cloud Print API or queues if it needs to
383      *     wait OAuth2 access token.
384      * @param {!CloudPrintRequest} request Request to send or queue.
385      * @private
386      */
387     sendOrQueueRequest_: function(request) {
388       if (request.origin == print_preview.Destination.Origin.COOKIES) {
389         return this.sendRequest_(request);
390       } else {
391         this.requestQueue_.push(request);
392         this.nativeLayer_.startGetAccessToken(request.origin);
393       }
394     },
395
396     /**
397      * Sends a request to the Google Cloud Print API.
398      * @param {!CloudPrintRequest} request Request to send.
399      * @private
400      */
401     sendRequest_: function(request) {
402       request.xhr.onreadystatechange =
403           this.onReadyStateChange_.bind(this, request);
404       request.xhr.send(request.body);
405     },
406
407     /**
408      * Creates a Google Cloud Print interface error that is ready to dispatch.
409      * @param {!CloudPrintInterface.EventType} type Type of the error.
410      * @param {!CloudPrintRequest} request Request that has been completed.
411      * @return {!Event} Google Cloud Print interface error event.
412      * @private
413      */
414     createErrorEvent_: function(type, request) {
415       var errorEvent = new Event(type);
416       errorEvent.status = request.xhr.status;
417       if (request.xhr.status == 200) {
418         errorEvent.errorCode = request.result['errorCode'];
419         errorEvent.message = request.result['message'];
420       } else {
421         errorEvent.errorCode = 0;
422         errorEvent.message = '';
423       }
424       errorEvent.origin = request.origin;
425       return errorEvent;
426     },
427
428     /**
429      * Updates user info and session index from the {@code request} response.
430      * @param {!CloudPrintRequest} request Request to extract user info from.
431      * @private
432      */
433     setUsers_: function(request) {
434       if (request.origin == print_preview.Destination.Origin.COOKIES) {
435         var users = request.result['request']['users'] || [];
436         this.userSessionIndex_ = {};
437         for (var i = 0; i < users.length; i++) {
438           this.userSessionIndex_[users[i]] = i;
439         }
440         this.userInfo_.setUsers(request.result['request']['user'], users);
441       }
442     },
443
444     /**
445      * Terminates search requests for requested {@code origins}.
446      * @param {!Array.<print_preview.Destination.Origin>} origins Origins
447      *     to terminate search requests for.
448      * @private
449      */
450     abortSearchRequests_: function(origins) {
451       this.outstandingCloudSearchRequests_ =
452           this.outstandingCloudSearchRequests_.filter(function(request) {
453             if (origins.indexOf(request.origin) >= 0) {
454               request.xhr.abort();
455               return false;
456             }
457             return true;
458           });
459     },
460
461     /**
462      * Called when a native layer receives access token.
463      * @param {Event} evt Contains the authentication type and access token.
464      * @private
465      */
466     onAccessTokenReady_: function(event) {
467       // TODO(vitalybuka): remove when other Origins implemented.
468       assert(event.authType == print_preview.Destination.Origin.DEVICE);
469       this.requestQueue_ = this.requestQueue_.filter(function(request) {
470         assert(request.origin == print_preview.Destination.Origin.DEVICE);
471         if (request.origin != event.authType) {
472           return true;
473         }
474         if (event.accessToken) {
475           request.xhr.setRequestHeader('Authorization',
476                                        'Bearer ' + event.accessToken);
477           this.sendRequest_(request);
478         } else {  // No valid token.
479           // Without abort status does not exist.
480           request.xhr.abort();
481           request.callback(request);
482         }
483         return false;
484       }, this);
485     },
486
487     /**
488      * Called when the ready-state of a XML http request changes.
489      * Calls the successCallback with the result or dispatches an ERROR event.
490      * @param {!CloudPrintRequest} request Request that was changed.
491      * @private
492      */
493     onReadyStateChange_: function(request) {
494       if (request.xhr.readyState == 4) {
495         if (request.xhr.status == 200) {
496           request.result = JSON.parse(request.xhr.responseText);
497           if (request.origin == print_preview.Destination.Origin.COOKIES &&
498               request.result['success']) {
499             this.xsrfTokens_[request.result['request']['user']] =
500                 request.result['xsrf_token'];
501           }
502         }
503         request.status = request.xhr.status;
504         request.callback(request);
505       }
506     },
507
508     /**
509      * Called when the search request completes.
510      * @param {boolean} isRecent Whether the search request was for recent
511      *     destinations.
512      * @param {!CloudPrintRequest} request Request that has been completed.
513      * @private
514      */
515     onSearchDone_: function(isRecent, request) {
516       var lastRequestForThisOrigin = true;
517       this.outstandingCloudSearchRequests_ =
518           this.outstandingCloudSearchRequests_.filter(function(item) {
519             if (item != request && item.origin == request.origin) {
520               lastRequestForThisOrigin = false;
521             }
522             return item != request;
523           });
524       var activeUser = '';
525       if (request.origin == print_preview.Destination.Origin.COOKIES) {
526         activeUser =
527             request.result &&
528             request.result['request'] &&
529             request.result['request']['user'];
530       }
531       var event = null;
532       if (request.xhr.status == 200 && request.result['success']) {
533         // Extract printers.
534         var printerListJson = request.result['printers'] || [];
535         var printerList = [];
536         printerListJson.forEach(function(printerJson) {
537           try {
538             printerList.push(cloudprint.CloudDestinationParser.parse(
539                 printerJson, request.origin, activeUser));
540           } catch (err) {
541             console.error('Unable to parse cloud print destination: ' + err);
542           }
543         });
544         // Extract and store users.
545         this.setUsers_(request);
546         // Dispatch SEARCH_DONE event.
547         event = new Event(CloudPrintInterface.EventType.SEARCH_DONE);
548         event.origin = request.origin;
549         event.printers = printerList;
550         event.isRecent = isRecent;
551       } else {
552         event = this.createErrorEvent_(
553             CloudPrintInterface.EventType.SEARCH_FAILED,
554             request);
555       }
556       event.user = activeUser;
557       event.searchDone = lastRequestForThisOrigin;
558       this.dispatchEvent(event);
559     },
560
561     /**
562      * Called when the submit request completes.
563      * @param {!CloudPrintRequest} request Request that has been completed.
564      * @private
565      */
566     onSubmitDone_: function(request) {
567       if (request.xhr.status == 200 && request.result['success']) {
568         var submitDoneEvent = new Event(
569             CloudPrintInterface.EventType.SUBMIT_DONE);
570         submitDoneEvent.jobId = request.result['job']['id'];
571         this.dispatchEvent(submitDoneEvent);
572       } else {
573         var errorEvent = this.createErrorEvent_(
574             CloudPrintInterface.EventType.SUBMIT_FAILED, request);
575         this.dispatchEvent(errorEvent);
576       }
577     },
578
579     /**
580      * Called when the printer request completes.
581      * @param {string} destinationId ID of the destination that was looked up.
582      * @param {!CloudPrintRequest} request Request that has been completed.
583      * @private
584      */
585     onPrinterDone_: function(destinationId, request) {
586       // Special handling of the first printer request. It does not matter at
587       // this point, whether printer was found or not.
588       if (request.origin == print_preview.Destination.Origin.COOKIES &&
589           request.result &&
590           request.account &&
591           request.result['request']['user'] &&
592           request.result['request']['users'] &&
593           request.account != request.result['request']['user']) {
594         this.setUsers_(request);
595         // In case the user account is known, but not the primary one,
596         // activate it.
597         if (this.userSessionIndex_[request.account] > 0) {
598           this.userInfo_.activeUser = request.account;
599           // Repeat the request for the newly activated account.
600           this.printer(
601               request.result['request']['params']['printerid'],
602               request.origin,
603               request.account);
604           // Stop processing this request, wait for the new response.
605           return;
606         }
607       }
608       // Process response.
609       if (request.xhr.status == 200 && request.result['success']) {
610         var activeUser = '';
611         if (request.origin == print_preview.Destination.Origin.COOKIES) {
612           activeUser = request.result['request']['user'];
613         }
614         var printerJson = request.result['printers'][0];
615         var printer;
616         try {
617           printer = cloudprint.CloudDestinationParser.parse(
618               printerJson, request.origin, activeUser);
619         } catch (err) {
620           console.error('Failed to parse cloud print destination: ' +
621               JSON.stringify(printerJson));
622           return;
623         }
624         var printerDoneEvent =
625             new Event(CloudPrintInterface.EventType.PRINTER_DONE);
626         printerDoneEvent.printer = printer;
627         this.dispatchEvent(printerDoneEvent);
628       } else {
629         var errorEvent = this.createErrorEvent_(
630             CloudPrintInterface.EventType.PRINTER_FAILED, request);
631         errorEvent.destinationId = destinationId;
632         errorEvent.destinationOrigin = request.origin;
633         this.dispatchEvent(errorEvent, request.origin);
634       }
635     },
636
637     /**
638      * Called when the update printer TOS acceptance request completes.
639      * @param {!CloudPrintRequest} request Request that has been completed.
640      * @private
641      */
642     onUpdatePrinterTosAcceptanceDone_: function(request) {
643       if (request.xhr.status == 200 && request.result['success']) {
644         // Do nothing.
645       } else {
646         var errorEvent = this.createErrorEvent_(
647             CloudPrintInterface.EventType.SUBMIT_FAILED, request);
648         this.dispatchEvent(errorEvent);
649       }
650     }
651   };
652
653   /**
654    * Data structure that holds data for Cloud Print requests.
655    * @param {!XMLHttpRequest} xhr Partially prepared http request.
656    * @param {string} body Data to send with POST requests.
657    * @param {!print_preview.Destination.Origin} origin Origin for destination.
658    * @param {?string} account Account the request is sent for. Can be
659    *     {@code null} or empty string if the request is not cookie bound or
660    *     is sent on behalf of the primary user.
661    * @param {function(!CloudPrintRequest)} callback Callback to invoke when
662    *     request completes.
663    * @constructor
664    */
665   function CloudPrintRequest(xhr, body, origin, account, callback) {
666     /**
667      * Partially prepared http request.
668      * @type {!XMLHttpRequest}
669      */
670     this.xhr = xhr;
671
672     /**
673      * Data to send with POST requests.
674      * @type {string}
675      */
676     this.body = body;
677
678     /**
679      * Origin for destination.
680      * @type {!print_preview.Destination.Origin}
681      */
682     this.origin = origin;
683
684     /**
685      * User account this request is expected to be executed for.
686      * @type {?string}
687      */
688     this.account = account;
689
690     /**
691      * Callback to invoke when request completes.
692      * @type {function(!CloudPrintRequest)}
693      */
694     this.callback = callback;
695
696     /**
697      * Result for requests.
698      * @type {Object} JSON response.
699      */
700     this.result = null;
701   };
702
703   /**
704    * Data structure that represents an HTTP parameter.
705    * @param {string} name Name of the parameter.
706    * @param {string} value Value of the parameter.
707    * @constructor
708    */
709   function HttpParam(name, value) {
710     /**
711      * Name of the parameter.
712      * @type {string}
713      */
714     this.name = name;
715
716     /**
717      * Name of the value.
718      * @type {string}
719      */
720     this.value = value;
721   };
722
723   // Export
724   return {
725     CloudPrintInterface: CloudPrintInterface
726   };
727 });