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