1// Copyright 2013 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'use strict'; 6 7/** 8 * @param {HTMLElement} parentNode Node to be parent for this dialog. 9 * @constructor 10 * @extends {FileManagerDialogBase} 11 * @implements {ShareClient.Observer} 12 */ 13function ShareDialog(parentNode) { 14 this.queue_ = new AsyncUtil.Queue(); 15 this.onQueueTaskFinished_ = null; 16 this.shareClient_ = null; 17 this.spinner_ = null; 18 this.spinnerLayer_ = null; 19 this.webViewWrapper_ = null; 20 this.webView_ = null; 21 this.failureTimeout_ = null; 22 this.callback_ = null; 23 24 FileManagerDialogBase.call(this, parentNode); 25} 26 27/** 28 * Timeout for loading the share dialog before giving up. 29 * @type {number} 30 * @const 31 */ 32ShareDialog.FAILURE_TIMEOUT = 10000; 33 34/** 35 * The result of opening the dialog. 36 * @enum {string} 37 * @const 38 */ 39ShareDialog.Result = Object.freeze({ 40 // The dialog is closed normally. This includes user cancel. 41 SUCCESS: 'success', 42 // The dialog is closed by network error. 43 NETWORK_ERROR: 'networkError', 44 // The dialog is not opened because it is already showing. 45 ALREADY_SHOWING: 'alreadyShowing' 46}); 47 48/** 49 * Wraps a Web View element and adds authorization headers to it. 50 * @param {string} urlPattern Pattern of urls to be authorized. 51 * @param {WebView} webView Web View element to be wrapped. 52 * @constructor 53 */ 54ShareDialog.WebViewAuthorizer = function(urlPattern, webView) { 55 this.urlPattern_ = urlPattern; 56 this.webView_ = webView; 57 this.initialized_ = false; 58 this.accessToken_ = null; 59}; 60 61/** 62 * Initializes the web view by installing hooks injecting the authorization 63 * headers. 64 * @param {function()} callback Completion callback. 65 */ 66ShareDialog.WebViewAuthorizer.prototype.initialize = function(callback) { 67 if (this.initialized_) { 68 callback(); 69 return; 70 } 71 72 var registerInjectionHooks = function() { 73 this.webView_.removeEventListener('loadstop', registerInjectionHooks); 74 this.webView_.request.onBeforeSendHeaders.addListener( 75 this.authorizeRequest_.bind(this), 76 {urls: [this.urlPattern_]}, 77 ['blocking', 'requestHeaders']); 78 this.initialized_ = true; 79 callback(); 80 }.bind(this); 81 82 this.webView_.addEventListener('loadstop', registerInjectionHooks); 83 this.webView_.setAttribute('src', 'data:text/html,'); 84}; 85 86/** 87 * Authorizes the web view by fetching the freshest access tokens. 88 * @param {function()} callback Completion callback. 89 */ 90ShareDialog.WebViewAuthorizer.prototype.authorize = function(callback) { 91 // Fetch or update the access token. 92 chrome.fileBrowserPrivate.requestAccessToken(false, // force_refresh 93 function(inAccessToken) { 94 this.accessToken_ = inAccessToken; 95 callback(); 96 }.bind(this)); 97}; 98 99/** 100 * Injects headers into the passed request. 101 * @param {Event} e Request event. 102 * @return {{requestHeaders: HttpHeaders}} Modified headers. 103 * @private 104 */ 105ShareDialog.WebViewAuthorizer.prototype.authorizeRequest_ = function(e) { 106 e.requestHeaders.push({ 107 name: 'Authorization', 108 value: 'Bearer ' + this.accessToken_ 109 }); 110 return {requestHeaders: e.requestHeaders}; 111}; 112 113ShareDialog.prototype = { 114 __proto__: FileManagerDialogBase.prototype 115}; 116 117/** 118 * One-time initialization of DOM. 119 * @private 120 */ 121ShareDialog.prototype.initDom_ = function() { 122 FileManagerDialogBase.prototype.initDom_.call(this); 123 this.frame_.classList.add('share-dialog-frame'); 124 125 this.spinnerLayer_ = this.document_.createElement('div'); 126 this.spinnerLayer_.className = 'spinner-layer'; 127 this.frame_.appendChild(this.spinnerLayer_); 128 129 this.webViewWrapper_ = this.document_.createElement('div'); 130 this.webViewWrapper_.className = 'share-dialog-webview-wrapper'; 131 this.cancelButton_.hidden = true; 132 this.okButton_.hidden = true; 133 this.frame_.insertBefore(this.webViewWrapper_, 134 this.frame_.querySelector('.cr-dialog-buttons')); 135}; 136 137/** 138 * @override 139 */ 140ShareDialog.prototype.onResized = function(width, height, callback) { 141 if (width && height) { 142 this.webViewWrapper_.style.width = width + 'px'; 143 this.webViewWrapper_.style.height = height + 'px'; 144 this.webView_.style.width = width + 'px'; 145 this.webView_.style.height = height + 'px'; 146 } 147 setTimeout(callback, 0); 148}; 149 150/** 151 * @override 152 */ 153ShareDialog.prototype.onClosed = function() { 154 this.hide(); 155}; 156 157/** 158 * @override 159 */ 160ShareDialog.prototype.onLoaded = function() { 161 if (this.failureTimeout_) { 162 clearTimeout(this.failureTimeout_); 163 this.failureTimeout_ = null; 164 } 165 166 // Logs added temporarily to track crbug.com/288783. 167 console.debug('Loaded.'); 168 169 this.okButton_.hidden = false; 170 this.spinnerLayer_.hidden = true; 171 this.webViewWrapper_.classList.add('loaded'); 172 this.webView_.focus(); 173}; 174 175/** 176 * @override 177 */ 178ShareDialog.prototype.onLoadFailed = function() { 179 this.hideWithResult(ShareDialog.Result.NETWORK_ERROR); 180}; 181 182/** 183 * @override 184 */ 185ShareDialog.prototype.hide = function(opt_onHide) { 186 this.hideWithResult(ShareDialog.Result.SUCCESS, opt_onHide); 187}; 188 189/** 190 * Hide the dialog with the result and the callback. 191 * @param {ShareDialog.Result} result Result passed to the closing callback. 192 * @param {function()=} opt_onHide Callback called at the end of hiding. 193 */ 194ShareDialog.prototype.hideWithResult = function(result, opt_onHide) { 195 if (!this.isShowing()) 196 return; 197 198 if (this.shareClient_) { 199 this.shareClient_.dispose(); 200 this.shareClient_ = null; 201 } 202 203 this.webViewWrapper_.textContent = ''; 204 if (this.failureTimeout_) { 205 clearTimeout(this.failureTimeout_); 206 this.failureTimeout_ = null; 207 } 208 209 FileManagerDialogBase.prototype.hide.call( 210 this, 211 function() { 212 if (opt_onHide) 213 opt_onHide(); 214 this.callback_(result); 215 this.callback_ = null; 216 }.bind(this)); 217}; 218 219/** 220 * Shows the dialog. 221 * @param {FileEntry} entry Entry to share. 222 * @param {function(boolean)} callback Callback to be called when the showing 223 * task is completed. The argument is whether to succeed or not. Note that 224 * cancel is regarded as success. 225 */ 226ShareDialog.prototype.show = function(entry, callback) { 227 // If the dialog is already showing, return the error. 228 if (this.isShowing()) { 229 callback(ShareDialog.Result.ALREADY_SHOWING); 230 return; 231 } 232 233 // Initialize the variables. 234 this.callback_ = callback; 235 this.spinnerLayer_.hidden = false; 236 this.webViewWrapper_.style.width = ''; 237 this.webViewWrapper_.style.height = ''; 238 239 // If the embedded share dialog is not started within some time, then 240 // give up and show an error message. 241 this.failureTimeout_ = setTimeout(function() { 242 this.hideWithResult(ShareDialog.Result.NETWORK_ERROR); 243 244 // Logs added temporarily to track crbug.com/288783. 245 console.debug('Timeout. Web View points at: ' + this.webView_.src); 246 }.bind(this), ShareDialog.FAILURE_TIMEOUT); 247 248 // TODO(mtomasz): Move to initDom_() once and reuse <webview> once it gets 249 // fixed. See: crbug.com/260622. 250 this.webView_ = util.createChild( 251 this.webViewWrapper_, 'share-dialog-webview', 'webview'); 252 this.webView_.setAttribute('tabIndex', '-1'); 253 this.webViewAuthorizer_ = new ShareDialog.WebViewAuthorizer( 254 !window.IN_TEST ? (ShareClient.SHARE_TARGET + '/*') : '<all_urls>', 255 this.webView_); 256 this.webView_.addEventListener('newwindow', function(e) { 257 // Discard the window object and reopen in an external window. 258 e.window.discard(); 259 util.visitURL(e.targetUrl); 260 e.preventDefault(); 261 }); 262 var show = FileManagerDialogBase.prototype.showBlankDialog.call(this); 263 if (!show) { 264 // The code shoundn't get here, since already-showing was handled before. 265 console.error('ShareDialog can\'t be shown.'); 266 return; 267 } 268 269 // Initialize and authorize the Web View tag asynchronously. 270 var group = new AsyncUtil.Group(); 271 272 // Fetches an url to the sharing dialog. 273 var shareUrl; 274 group.add(function(inCallback) { 275 chrome.fileBrowserPrivate.getShareUrl( 276 entry.toURL(), 277 function(inShareUrl) { 278 if (!chrome.runtime.lastError) 279 shareUrl = inShareUrl; 280 inCallback(); 281 }); 282 }); 283 group.add(this.webViewAuthorizer_.initialize.bind(this.webViewAuthorizer_)); 284 group.add(this.webViewAuthorizer_.authorize.bind(this.webViewAuthorizer_)); 285 286 // Loads the share widget once all the previous async calls are finished. 287 group.run(function() { 288 // If the url is not obtained, return the network error. 289 if (!shareUrl) { 290 // Logs added temporarily to track crbug.com/288783. 291 console.debug('URL not available.'); 292 293 this.hideWithResult(ShareDialog.Result.NETWORK_ERROR); 294 return; 295 } 296 // Already inactive, therefore ignore. 297 if (!this.isShowing()) 298 return; 299 this.shareClient_ = new ShareClient(this.webView_, 300 shareUrl, 301 this); 302 this.shareClient_.load(); 303 }.bind(this)); 304}; 305 306/** 307 * Tells whether the share dialog is showing or not. 308 * 309 * @return {boolean} True since the show method is called and until the closing 310 * callback is invoked. 311 */ 312ShareDialog.prototype.isShowing = function() { 313 return !!this.callback_; 314}; 315