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