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