OLD | NEW |
| (Empty) |
1 /* Copyright 2015 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 | |
6 /** | |
7 * @fileoverview | |
8 * The application side of the application/cloud print dialog interface, used | |
9 * by the application to exchange messages with the dialog. | |
10 */ | |
11 | |
12 /** @suppress {duplicate} */ | |
13 var remoting = remoting || {}; | |
14 | |
15 /** | |
16 * According to https://developer.chrome.com/apps/tags/webview, the level | |
17 * ranges from 0 to 4. But in real life they are -1 (debug), 0 (log and info), | |
18 * 1 (warn), and 2 (error). See crbug.com/499408 | |
19 * | |
20 * @enum {number} | |
21 */ | |
22 remoting.ConsoleMessageLevel = { | |
23 // console.debug | |
24 VERBOSE: -1, | |
25 // console.info or console.log | |
26 INFO: 0, | |
27 // console.warn | |
28 WARNING: 1, | |
29 //console.err | |
30 ERROR: 2 | |
31 }; | |
32 | |
33 (function() { | |
34 | |
35 'use strict'; | |
36 | |
37 /** | |
38 * Interval to refresh the access token used by the cloud print dialog. | |
39 * Refreshing the token every 30 minutes should be good enough because the | |
40 * access token will be valid for an hour. | |
41 * | |
42 * @const {number} | |
43 */ | |
44 var CLOUD_PRINT_DIALOG_TOKEN_REFRESH_INTERVAL_MS = 30 * 60 * 1000; | |
45 | |
46 /** | |
47 * @param {!Webview} webview The webview hosting the cloud cloud print dialog. | |
48 * @param {remoting.WindowShape} windowShape | |
49 * @param {base.WindowMessageDispatcher} windowMessageDispatcher | |
50 * @param {!remoting.ConnectedView} connectedView | |
51 * @constructor | |
52 * @implements {remoting.WindowShape.ClientUI} | |
53 * @implements {base.Disposable} | |
54 */ | |
55 remoting.CloudPrintDialogContainer = | |
56 function(webview, windowShape, windowMessageDispatcher, connectedView) { | |
57 /** @private {!Webview} */ | |
58 this.webview_ = webview; | |
59 | |
60 /** @private {remoting.WindowShape} */ | |
61 this.windowShape_ = windowShape; | |
62 | |
63 /** @private {!remoting.ConnectedView} */ | |
64 this.connectedView_ = connectedView; | |
65 | |
66 // TODO (weitaosu): This is only needed if the cloud print webview is on the | |
67 // same page as the plugin. We should remove it if we move the webview to a | |
68 // standalone message window. | |
69 this.connectedView_.allowFocus(webview); | |
70 | |
71 /** @private {string} */ | |
72 this.accessToken_ = ''; | |
73 | |
74 /** @private {base.WindowMessageDispatcher} */ | |
75 this.windowMessageDispatcher_ = windowMessageDispatcher; | |
76 | |
77 this.windowMessageDispatcher_.registerMessageHandler( | |
78 'cloud-print-dialog', this.onMessage_.bind(this)); | |
79 | |
80 /** @private {base.RepeatingTimer} */ | |
81 this.timer_ = new base.RepeatingTimer( | |
82 this.cacheAccessToken_.bind(this), | |
83 CLOUD_PRINT_DIALOG_TOKEN_REFRESH_INTERVAL_MS, true); | |
84 | |
85 // Adding a synchronous (blocking) handler so that the reqeust headers can | |
86 // be modified on the spot. | |
87 webview.request.onBeforeSendHeaders.addListener( | |
88 this.setAuthHeaders_.bind(this), | |
89 /** @type {!RequestFilter} */ ({urls: ['https://*.google.com/*']}), | |
90 ['requestHeaders','blocking']); | |
91 }; | |
92 | |
93 remoting.CloudPrintDialogContainer.INJECTED_SCRIPT = | |
94 '_modules/koejkfhmphamcgafjmkellhnekdkopod/cloud_print_dialog_injected.js'; | |
95 remoting.CloudPrintDialogContainer.CLOUD_PRINT_DIALOG_URL = | |
96 'https://www.google.com/cloudprint/dialog.html?'; | |
97 | |
98 remoting.CloudPrintDialogContainer.prototype.dispose = function() { | |
99 this.windowMessageDispatcher_.unregisterMessageHandler('cloud-print-dialog'); | |
100 this.timer_.dispose(); | |
101 }; | |
102 | |
103 /** | |
104 * Timer callback to cache the access token. | |
105 * @private | |
106 */ | |
107 remoting.CloudPrintDialogContainer.prototype.cacheAccessToken_ = function() { | |
108 /** @type {remoting.CloudPrintDialogContainer} */ | |
109 var that = this; | |
110 remoting.identity.getNewToken().then( | |
111 function(/** string */ token){ | |
112 console.assert(token !== that.accessToken_); | |
113 that.accessToken_ = token; | |
114 }).catch(remoting.Error.handler(function(/** remoting.Error */ error) { | |
115 console.log('Failed to refresh access token: ' + error.toString()); | |
116 })); | |
117 }; | |
118 | |
119 /** | |
120 * @param {Array<{left: number, top: number, width: number, height: number}>} | |
121 * rects List of rectangles. | |
122 */ | |
123 remoting.CloudPrintDialogContainer.prototype.addToRegion = function(rects) { | |
124 var rect = | |
125 /** @type {ClientRect} */(this.webview_.getBoundingClientRect()); | |
126 rects.push({left: rect.left, | |
127 top: rect.top, | |
128 width: rect.width, | |
129 height: rect.height}); | |
130 }; | |
131 | |
132 /** | |
133 * Show the cloud print dialog. | |
134 */ | |
135 remoting.CloudPrintDialogContainer.prototype.showCloudPrintUI = function() { | |
136 // TODO (weitaosu): Considering showing the cloud print dialog in a separate | |
137 // window or using remoting.Html5ModalDialog to show the modal dialog. | |
138 this.webview_.hidden = false; | |
139 this.windowShape_.registerClientUI(this); | |
140 this.windowShape_.centerToDesktop(this.webview_); | |
141 }; | |
142 | |
143 /** | |
144 * Hide the cloud print dialog. | |
145 */ | |
146 remoting.CloudPrintDialogContainer.prototype.hideCloudPrintUI = function() { | |
147 this.webview_.hidden = true; | |
148 this.windowShape_.unregisterClientUI(this); | |
149 this.connectedView_.returnFocusToPlugin(); | |
150 } | |
151 | |
152 /** | |
153 * Event handler to process messages from the webview. | |
154 * | |
155 * @param {Event} event | |
156 * @private | |
157 */ | |
158 remoting.CloudPrintDialogContainer.prototype.onMessage_ = function(event) { | |
159 var data = event.data; | |
160 console.assert(typeof data === 'object' && | |
161 data['source'] == 'cloud-print-dialog'); | |
162 | |
163 switch (event.data['command']) { | |
164 case 'cp-dialog-on-init::': | |
165 // We actually never receive this message because the cloud print dialog | |
166 // has already been initialized by the time the injected script finishes | |
167 // executing. | |
168 break; | |
169 | |
170 case 'cp-dialog-on-close::': | |
171 this.hideCloudPrintUI(); | |
172 break; | |
173 | |
174 default: | |
175 console.error('Unexpected message:', event.data['command'], event.data); | |
176 } | |
177 }; | |
178 | |
179 /** | |
180 * Retrieve the file specified by |fileName| in the app package and pass its | |
181 * content to the onDone callback. | |
182 * | |
183 * @param {string} fileName Name of the file in the app package to be read. | |
184 * @param {!function(string):void} onDone Callback to be invoked on success. | |
185 * @param {!function(*):void} onError Callback to be invoked on failure. | |
186 */ | |
187 remoting.CloudPrintDialogContainer.readFile = | |
188 function(fileName, onDone, onError) { | |
189 var fileUrl = chrome.runtime.getURL(fileName); | |
190 var xhr = new remoting.Xhr({ method: 'GET', url: fileUrl}); | |
191 | |
192 xhr.start().then(function(/** !remoting.Xhr.Response */ response) { | |
193 if (response.status == 200) { | |
194 onDone(response.getText()); | |
195 } else { | |
196 onError('xhr.status = ' + response.status); | |
197 } | |
198 }); | |
199 }; | |
200 | |
201 /** | |
202 * Lanunch the cloud print dialog to print the document. | |
203 * | |
204 * @param {string} title Title of the print job. | |
205 * @param {string} type Type of the storage of the document (url, gdrive, etc). | |
206 * @param {string} data Meaning of this field depends on the |type| parameter. | |
207 */ | |
208 remoting.CloudPrintDialogContainer.prototype.printDocument = | |
209 function(title, type, data) { | |
210 var dialogUrl = remoting.CloudPrintDialogContainer.CLOUD_PRINT_DIALOG_URL; | |
211 dialogUrl += 'title=' + encodeURIComponent(title) + '&'; | |
212 | |
213 switch (type) { | |
214 case 'url': | |
215 // 'data' should contain the url to the document to be printed. | |
216 if (data.substr(0, 7) !== 'http://' && data.substr(0, 8) !== 'https://') { | |
217 console.error('Bad URL: ' + data); | |
218 return; | |
219 } | |
220 dialogUrl += 'type=url&url=' + encodeURIComponent(data); | |
221 break; | |
222 case 'google.drive': | |
223 // 'data' should contain the doc id of the gdrive document to be printed. | |
224 dialogUrl += 'type=google.drive&content=' + encodeURIComponent(data); | |
225 break; | |
226 default: | |
227 console.error('Unknown content type for the printDocument command.'); | |
228 return; | |
229 } | |
230 | |
231 // TODO (weitaosu); Consider moving the event registration to the ctor | |
232 // and add unregistration in the dtor. | |
233 var that = this; | |
234 | |
235 /** @param {string} script */ | |
236 var showDialog = function(script) { | |
237 /** @type {Webview} */ | |
238 var webview = that.webview_; | |
239 | |
240 var sendHandshake = function() { | |
241 webview.contentWindow.postMessage('app-remoting-handshake', '*'); | |
242 } | |
243 | |
244 /** @param {Event} event */ | |
245 var redirectConsoleOutput = function(event) { | |
246 | |
247 var e = /** @type {chrome.ConsoleMessageBrowserEvent} */ (event); | |
248 var message = 'console message from webviwe: {' + | |
249 'level=' + e.level + ', ' + | |
250 'source=' + e.sourceId + ', ' + | |
251 'line=' + e.line + ', ' + | |
252 'message="' + e.message + '"}'; | |
253 | |
254 switch (e['level']) { | |
255 case remoting.ConsoleMessageLevel.VERBOSE: | |
256 console.debug(message); | |
257 break; | |
258 case remoting.ConsoleMessageLevel.INFO: | |
259 console.info(message); | |
260 break; | |
261 case remoting.ConsoleMessageLevel.WARNING: | |
262 console.warn(message); | |
263 break; | |
264 case remoting.ConsoleMessageLevel.ERROR: | |
265 console.error(message); | |
266 break; | |
267 default: | |
268 console.error('unrecognized message level. ' + message); | |
269 break; | |
270 } | |
271 } | |
272 | |
273 webview.addEventListener('consolemessage', redirectConsoleOutput); | |
274 | |
275 // Inject the script and send a handshake message to the cloud print dialog | |
276 // after the injected script has been executed. | |
277 webview.addEventListener("loadstart", function(event) { | |
278 console.log('"loadstart" captured in webview containier.'); | |
279 // TODO (weitaosu): Consider switching to addContentScripts when M44 | |
280 // is released. | |
281 webview.executeScript( | |
282 {code: script + ' //# sourceURL=cloud_print_dialog_injected.js'}, | |
283 sendHandshake); | |
284 }); | |
285 | |
286 // We need to show the cloud print UI here because we will never receive | |
287 // the 'cp-dialog-on-init::' message. | |
288 webview.src = dialogUrl; | |
289 that.showCloudPrintUI(); | |
290 } | |
291 | |
292 /** @param {*} errorMsg */ | |
293 var onError = function(errorMsg) { | |
294 console.error('Failed to retrieve the script: ', errorMsg); | |
295 } | |
296 | |
297 remoting.CloudPrintDialogContainer.readFile( | |
298 remoting.CloudPrintDialogContainer.INJECTED_SCRIPT, showDialog, onError); | |
299 }; | |
300 | |
301 /** | |
302 * Handler of the onBeforeSendHeaders event for the webview. It adds the auth | |
303 * header to the request being send. Note that this handler is synchronous so | |
304 * modifications to the requst must happen before it returns. | |
305 * | |
306 * @param {Object} details Details of the WebRequest. | |
307 * @return {!BlockingResponse} The modified request headers. | |
308 * @private | |
309 */ | |
310 remoting.CloudPrintDialogContainer.prototype.setAuthHeaders_ = | |
311 function(details) { | |
312 var url = /** @type {string} */ (details['url']); | |
313 console.log('Setting auth token for request: ', url); | |
314 | |
315 var headers = /** @type {Array} */ (details['requestHeaders']) || []; | |
316 if (this.accessToken_.length == 0) { | |
317 console.error('No auth token available for the request: ', url); | |
318 return /** @type {!BlockingResponse} */ ({'requestHeaders': headers}); | |
319 } | |
320 | |
321 // Make fresh copy of the headers array. | |
322 var newHeaders = /** @type {Array} */ (headers.slice()); | |
323 newHeaders.push({ | |
324 'name': 'Authorization', | |
325 'value': 'Bearer ' + this.accessToken_ | |
326 }); | |
327 | |
328 return /** @type {!BlockingResponse} */ ({'requestHeaders': newHeaders}); | |
329 }; | |
330 | |
331 })(); | |
OLD | NEW |