1// Copyright (c) 2012 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#include "chrome/browser/ui/app_modal_dialogs/javascript_dialog_manager.h"
6
7#include "base/bind.h"
8#include "base/compiler_specific.h"
9#include "base/i18n/rtl.h"
10#include "base/memory/singleton.h"
11#include "base/strings/utf_string_conversions.h"
12#include "chrome/browser/chrome_notification_types.h"
13#include "chrome/browser/ui/app_modal_dialogs/app_modal_dialog.h"
14#include "chrome/browser/ui/app_modal_dialogs/app_modal_dialog_queue.h"
15#include "chrome/browser/ui/app_modal_dialogs/javascript_app_modal_dialog.h"
16#include "chrome/browser/ui/app_modal_dialogs/native_app_modal_dialog.h"
17#include "chrome/common/chrome_constants.h"
18#include "chrome/grit/generated_resources.h"
19#include "content/public/browser/web_contents.h"
20#include "content/public/common/content_client.h"
21#include "content/public/common/javascript_message_type.h"
22#include "net/base/net_util.h"
23#include "ui/base/l10n/l10n_util.h"
24
25#if defined(ENABLE_EXTENSIONS)
26#include "extensions/browser/extension_system.h"
27#include "extensions/browser/process_manager.h"
28#endif  // defined(ENABLE_EXTENSIONS)
29
30using content::BrowserContext;
31using content::JavaScriptDialogManager;
32using content::WebContents;
33
34#if defined(ENABLE_EXTENSIONS)
35using extensions::Extension;
36#endif  // defined(ENABLE_EXTENSIONS)
37
38namespace {
39
40#if defined(ENABLE_EXTENSIONS)
41// Returns the ProcessManager for the browser context from |web_contents|.
42extensions::ProcessManager* GetExtensionsProcessManager(
43    WebContents* web_contents) {
44  return extensions::ExtensionSystem::Get(
45      web_contents->GetBrowserContext())->process_manager();
46}
47
48// Returns the extension associated with |web_contents| or NULL if there is no
49// associated extension (or extensions are not supported).
50const Extension* GetExtensionForWebContents(WebContents* web_contents) {
51  extensions::ProcessManager* pm = GetExtensionsProcessManager(web_contents);
52  return pm->GetExtensionForRenderViewHost(web_contents->GetRenderViewHost());
53}
54#endif  // defined(ENABLE_EXTENSIONS)
55
56// Keeps an |extension| from shutting down its lazy background page. If an
57// extension opens a dialog its lazy background page must stay alive until the
58// dialog closes.
59void IncrementLazyKeepaliveCount(WebContents* web_contents) {
60#if defined(ENABLE_EXTENSIONS)
61  const Extension* extension = GetExtensionForWebContents(web_contents);
62  if (extension == NULL)
63    return;
64
65  DCHECK(web_contents);
66  extensions::ProcessManager* pm = GetExtensionsProcessManager(web_contents);
67  if (pm)
68    pm->IncrementLazyKeepaliveCount(extension);
69#endif  // defined(ENABLE_EXTENSIONS)
70}
71
72// Allows an |extension| to shut down its lazy background page after a dialog
73// closes (if nothing else is keeping it open).
74void DecrementLazyKeepaliveCount(WebContents* web_contents) {
75#if defined(ENABLE_EXTENSIONS)
76  const Extension* extension = GetExtensionForWebContents(web_contents);
77  if (extension == NULL)
78    return;
79
80  DCHECK(web_contents);
81  extensions::ProcessManager* pm = GetExtensionsProcessManager(web_contents);
82  if (pm)
83    pm->DecrementLazyKeepaliveCount(extension);
84#endif  // defined(ENABLE_EXTENSIONS)
85}
86
87class ChromeJavaScriptDialogManager : public JavaScriptDialogManager {
88 public:
89  static ChromeJavaScriptDialogManager* GetInstance();
90
91  virtual void RunJavaScriptDialog(
92      WebContents* web_contents,
93      const GURL& origin_url,
94      const std::string& accept_lang,
95      content::JavaScriptMessageType message_type,
96      const base::string16& message_text,
97      const base::string16& default_prompt_text,
98      const DialogClosedCallback& callback,
99      bool* did_suppress_message) OVERRIDE;
100
101  virtual void RunBeforeUnloadDialog(
102      WebContents* web_contents,
103      const base::string16& message_text,
104      bool is_reload,
105      const DialogClosedCallback& callback) OVERRIDE;
106
107  virtual bool HandleJavaScriptDialog(
108      WebContents* web_contents,
109      bool accept,
110      const base::string16* prompt_override) OVERRIDE;
111
112  virtual void CancelActiveAndPendingDialogs(
113      WebContents* web_contents) OVERRIDE;
114
115  virtual void WebContentsDestroyed(WebContents* web_contents) OVERRIDE;
116
117 private:
118  friend struct DefaultSingletonTraits<ChromeJavaScriptDialogManager>;
119
120  ChromeJavaScriptDialogManager();
121  virtual ~ChromeJavaScriptDialogManager();
122
123  base::string16 GetTitle(WebContents* web_contents,
124                          const GURL& origin_url,
125                          const std::string& accept_lang,
126                          bool is_alert);
127
128  // Wrapper around a DialogClosedCallback so that we can intercept it before
129  // passing it onto the original callback.
130  void OnDialogClosed(WebContents* web_contents,
131                      DialogClosedCallback callback,
132                      bool success,
133                      const base::string16& user_input);
134
135  // Mapping between the WebContents and their extra data. The key
136  // is a void* because the pointer is just a cookie and is never dereferenced.
137  JavaScriptAppModalDialog::ExtraDataMap javascript_dialog_extra_data_;
138
139  DISALLOW_COPY_AND_ASSIGN(ChromeJavaScriptDialogManager);
140};
141
142////////////////////////////////////////////////////////////////////////////////
143// ChromeJavaScriptDialogManager, public:
144
145ChromeJavaScriptDialogManager::ChromeJavaScriptDialogManager() {
146}
147
148ChromeJavaScriptDialogManager::~ChromeJavaScriptDialogManager() {
149}
150
151// static
152ChromeJavaScriptDialogManager* ChromeJavaScriptDialogManager::GetInstance() {
153  return Singleton<ChromeJavaScriptDialogManager>::get();
154}
155
156void ChromeJavaScriptDialogManager::RunJavaScriptDialog(
157    WebContents* web_contents,
158    const GURL& origin_url,
159    const std::string& accept_lang,
160    content::JavaScriptMessageType message_type,
161    const base::string16& message_text,
162    const base::string16& default_prompt_text,
163    const DialogClosedCallback& callback,
164    bool* did_suppress_message)  {
165  *did_suppress_message = false;
166
167  ChromeJavaScriptDialogExtraData* extra_data =
168      &javascript_dialog_extra_data_[web_contents];
169
170  if (extra_data->suppress_javascript_messages_) {
171    *did_suppress_message = true;
172    return;
173  }
174
175  base::TimeDelta time_since_last_message = base::TimeTicks::Now() -
176      extra_data->last_javascript_message_dismissal_;
177  bool display_suppress_checkbox = false;
178  // Show a checkbox offering to suppress further messages if this message is
179  // being displayed within kJavaScriptMessageExpectedDelay of the last one.
180  if (time_since_last_message <
181      base::TimeDelta::FromMilliseconds(
182          chrome::kJavaScriptMessageExpectedDelay)) {
183    display_suppress_checkbox = true;
184  } else {
185    display_suppress_checkbox = false;
186  }
187
188  bool is_alert = message_type == content::JAVASCRIPT_MESSAGE_TYPE_ALERT;
189  base::string16 dialog_title =
190      GetTitle(web_contents, origin_url, accept_lang, is_alert);
191
192  IncrementLazyKeepaliveCount(web_contents);
193
194  AppModalDialogQueue::GetInstance()->AddDialog(new JavaScriptAppModalDialog(
195      web_contents,
196      &javascript_dialog_extra_data_,
197      dialog_title,
198      message_type,
199      message_text,
200      default_prompt_text,
201      display_suppress_checkbox,
202      false,  // is_before_unload_dialog
203      false,  // is_reload
204      base::Bind(&ChromeJavaScriptDialogManager::OnDialogClosed,
205                 base::Unretained(this), web_contents, callback)));
206}
207
208void ChromeJavaScriptDialogManager::RunBeforeUnloadDialog(
209    WebContents* web_contents,
210    const base::string16& message_text,
211    bool is_reload,
212    const DialogClosedCallback& callback) {
213  const base::string16 title = l10n_util::GetStringUTF16(is_reload ?
214      IDS_BEFORERELOAD_MESSAGEBOX_TITLE : IDS_BEFOREUNLOAD_MESSAGEBOX_TITLE);
215  const base::string16 footer = l10n_util::GetStringUTF16(is_reload ?
216      IDS_BEFORERELOAD_MESSAGEBOX_FOOTER : IDS_BEFOREUNLOAD_MESSAGEBOX_FOOTER);
217
218  base::string16 full_message =
219      message_text + base::ASCIIToUTF16("\n\n") + footer;
220
221  IncrementLazyKeepaliveCount(web_contents);
222
223  AppModalDialogQueue::GetInstance()->AddDialog(new JavaScriptAppModalDialog(
224      web_contents,
225      &javascript_dialog_extra_data_,
226      title,
227      content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM,
228      full_message,
229      base::string16(),  // default_prompt_text
230      false,       // display_suppress_checkbox
231      true,        // is_before_unload_dialog
232      is_reload,
233      base::Bind(&ChromeJavaScriptDialogManager::OnDialogClosed,
234                 base::Unretained(this), web_contents, callback)));
235}
236
237bool ChromeJavaScriptDialogManager::HandleJavaScriptDialog(
238    WebContents* web_contents,
239    bool accept,
240    const base::string16* prompt_override) {
241  AppModalDialogQueue* dialog_queue = AppModalDialogQueue::GetInstance();
242  if (!dialog_queue->HasActiveDialog() ||
243      !dialog_queue->active_dialog()->IsJavaScriptModalDialog() ||
244      dialog_queue->active_dialog()->web_contents() != web_contents) {
245    return false;
246  }
247  JavaScriptAppModalDialog* dialog = static_cast<JavaScriptAppModalDialog*>(
248      dialog_queue->active_dialog());
249  if (accept) {
250    if (prompt_override)
251      dialog->SetOverridePromptText(*prompt_override);
252    dialog->native_dialog()->AcceptAppModalDialog();
253  } else {
254    dialog->native_dialog()->CancelAppModalDialog();
255  }
256  return true;
257}
258
259void ChromeJavaScriptDialogManager::WebContentsDestroyed(
260    WebContents* web_contents) {
261  CancelActiveAndPendingDialogs(web_contents);
262  javascript_dialog_extra_data_.erase(web_contents);
263}
264
265base::string16 ChromeJavaScriptDialogManager::GetTitle(
266    WebContents* web_contents,
267    const GURL& origin_url,
268    const std::string& accept_lang,
269    bool is_alert) {
270  // If the URL hasn't any host, return the default string.
271  if (!origin_url.has_host()) {
272      return l10n_util::GetStringUTF16(
273          is_alert ? IDS_JAVASCRIPT_ALERT_DEFAULT_TITLE
274                   : IDS_JAVASCRIPT_MESSAGEBOX_DEFAULT_TITLE);
275  }
276
277  // For extensions, show the extension name, but only if the origin of
278  // the alert matches the top-level WebContents.
279#if defined(ENABLE_EXTENSIONS)
280  const Extension* extension = GetExtensionForWebContents(web_contents);
281  if (extension &&
282      web_contents->GetLastCommittedURL().GetOrigin() == origin_url) {
283    return base::UTF8ToUTF16(extension->name());
284  }
285#endif  // defined(ENABLE_EXTENSIONS)
286
287  // Otherwise, return the formatted URL.
288  // In this case, force URL to have LTR directionality.
289  base::string16 url_string = net::FormatUrl(origin_url, accept_lang);
290  return l10n_util::GetStringFUTF16(
291      is_alert ? IDS_JAVASCRIPT_ALERT_TITLE
292      : IDS_JAVASCRIPT_MESSAGEBOX_TITLE,
293      base::i18n::GetDisplayStringInLTRDirectionality(url_string));
294}
295
296void ChromeJavaScriptDialogManager::CancelActiveAndPendingDialogs(
297    WebContents* web_contents) {
298  AppModalDialogQueue* queue = AppModalDialogQueue::GetInstance();
299  AppModalDialog* active_dialog = queue->active_dialog();
300  if (active_dialog && active_dialog->web_contents() == web_contents)
301    active_dialog->Invalidate();
302  for (AppModalDialogQueue::iterator i = queue->begin();
303       i != queue->end(); ++i) {
304    if ((*i)->web_contents() == web_contents)
305      (*i)->Invalidate();
306  }
307}
308
309void ChromeJavaScriptDialogManager::OnDialogClosed(
310    WebContents* web_contents,
311    DialogClosedCallback callback,
312    bool success,
313    const base::string16& user_input) {
314  // If an extension opened this dialog then the extension may shut down its
315  // lazy background page after the dialog closes. (Dialogs are closed before
316  // their WebContents is destroyed so |web_contents| is still valid here.)
317  DecrementLazyKeepaliveCount(web_contents);
318
319  callback.Run(success, user_input);
320}
321
322}  // namespace
323
324content::JavaScriptDialogManager* GetJavaScriptDialogManagerInstance() {
325  return ChromeJavaScriptDialogManager::GetInstance();
326}
327