blob: 561555a52584cfabfe3bfaacf3227410270d578c [file] [log] [blame]
oshima758abebc2014-11-06 10:55:501// Copyright 2014 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
oshimaf65398422014-11-18 23:30:425#include "components/app_modal/javascript_dialog_manager.h"
oshima758abebc2014-11-06 10:55:506
avi6649f892016-02-19 23:09:487#include <algorithm>
dcheng51606352015-12-26 21:16:238#include <utility>
9
oshima0929be2a2014-11-19 22:21:0310#include "base/bind.h"
11#include "base/i18n/rtl.h"
avibc5337b2015-12-25 23:16:3312#include "base/macros.h"
joenotcharles850904a2016-02-09 01:50:4413#include "base/metrics/histogram_macros.h"
oshima0929be2a2014-11-19 22:21:0314#include "base/strings/utf_string_conversions.h"
15#include "components/app_modal/app_modal_dialog.h"
16#include "components/app_modal/app_modal_dialog_queue.h"
oshimaf65398422014-11-18 23:30:4217#include "components/app_modal/javascript_dialog_extensions_client.h"
oshimaf65398422014-11-18 23:30:4218#include "components/app_modal/javascript_native_dialog_factory.h"
oshima0929be2a2014-11-19 22:21:0319#include "components/app_modal/native_app_modal_dialog.h"
palmer4e0ae10d2015-09-03 23:38:4120#include "components/url_formatter/elide_url.h"
meacer6b9365352015-11-06 00:35:4621#include "content/public/browser/web_contents.h"
oshima0929be2a2014-11-19 22:21:0322#include "content/public/common/javascript_message_type.h"
23#include "grit/components_strings.h"
oshima0929be2a2014-11-19 22:21:0324#include "ui/base/l10n/l10n_util.h"
meacer5b6d71dda2016-02-08 21:43:3525#include "ui/gfx/font_list.h"
oshima758abebc2014-11-06 10:55:5026
oshima0929be2a2014-11-19 22:21:0327namespace app_modal {
avi6649f892016-02-19 23:09:4828
oshima0929be2a2014-11-19 22:21:0329namespace {
30
meacer5b6d71dda2016-02-08 21:43:3531#if !defined(OS_ANDROID)
32// Keep in sync with kDefaultMessageWidth, but allow some space for the rest of
33// the text.
34const int kUrlElideWidth = 350;
35#endif
36
oshima0929be2a2014-11-19 22:21:0337class DefaultExtensionsClient : public JavaScriptDialogExtensionsClient {
38 public:
39 DefaultExtensionsClient() {}
40 ~DefaultExtensionsClient() override {}
41
42 private:
43 // JavaScriptDialogExtensionsClient:
44 void OnDialogOpened(content::WebContents* web_contents) override {}
45 void OnDialogClosed(content::WebContents* web_contents) override {}
46 bool GetExtensionName(content::WebContents* web_contents,
47 const GURL& origin_url,
48 std::string* name_out) override {
49 return false;
50 }
51
52 DISALLOW_COPY_AND_ASSIGN(DefaultExtensionsClient);
53};
54
avi86578a8a2015-04-17 22:39:3155bool ShouldDisplaySuppressCheckbox(
56 ChromeJavaScriptDialogExtraData* extra_data) {
palmerd8b2ff02015-08-18 00:24:5957 return extra_data->has_already_shown_a_dialog_;
avi86578a8a2015-04-17 22:39:3158}
59
avi141dbc1322016-03-11 22:27:4260void LogUMAMessageLengthStats(const base::string16& message) {
61 UMA_HISTOGRAM_COUNTS("JSDialogs.CountOfJSDialogMessageCharacters",
62 static_cast<int32_t>(message.length()));
avi6649f892016-02-19 23:09:4863
64 int32_t newline_count =
65 std::count_if(message.begin(), message.end(),
66 [](const base::char16& c) { return c == '\n'; });
avi141dbc1322016-03-11 22:27:4267 UMA_HISTOGRAM_COUNTS("JSDialogs.CountOfJSDialogMessageNewlines",
68 newline_count);
avi6649f892016-02-19 23:09:4869}
70
oshima0929be2a2014-11-19 22:21:0371} // namespace
72
oshima0929be2a2014-11-19 22:21:0373// static
74JavaScriptDialogManager* JavaScriptDialogManager::GetInstance() {
olli.raula36aa8be2015-09-10 11:14:2275 return base::Singleton<JavaScriptDialogManager>::get();
oshima758abebc2014-11-06 10:55:5076}
77
oshima0929be2a2014-11-19 22:21:0378void JavaScriptDialogManager::SetNativeDialogFactory(
dchenga0ee5fb2016-04-26 02:46:5579 std::unique_ptr<JavaScriptNativeDialogFactory> factory) {
dcheng51606352015-12-26 21:16:2380 native_dialog_factory_ = std::move(factory);
oshima758abebc2014-11-06 10:55:5081}
82
oshima0929be2a2014-11-19 22:21:0383void JavaScriptDialogManager::SetExtensionsClient(
dchenga0ee5fb2016-04-26 02:46:5584 std::unique_ptr<JavaScriptDialogExtensionsClient> extensions_client) {
dcheng51606352015-12-26 21:16:2385 extensions_client_ = std::move(extensions_client);
oshima758abebc2014-11-06 10:55:5086}
oshima0929be2a2014-11-19 22:21:0387
oshima0929be2a2014-11-19 22:21:0388JavaScriptDialogManager::JavaScriptDialogManager()
89 : extensions_client_(new DefaultExtensionsClient) {
90}
91
92JavaScriptDialogManager::~JavaScriptDialogManager() {
93}
94
avi7a1b55b2016-10-19 04:18:3895base::string16 JavaScriptDialogManager::GetTitle(
96 content::WebContents* web_contents,
97 const GURL& origin_url) {
98 // For extensions, show the extension name, but only if the origin of
99 // the alert matches the top-level WebContents.
100 std::string name;
101 if (extensions_client_->GetExtensionName(web_contents, origin_url, &name))
102 return base::UTF8ToUTF16(name);
103
104 // Otherwise, return the formatted URL. For non-standard URLs such as |data:|,
105 // just say "This page".
106 bool is_same_origin_as_main_frame =
107 (web_contents->GetURL().GetOrigin() == origin_url.GetOrigin());
108 if (origin_url.IsStandard() && !origin_url.SchemeIsFile() &&
109 !origin_url.SchemeIsFileSystem()) {
110#if defined(OS_ANDROID)
111 base::string16 url_string = url_formatter::FormatUrlForSecurityDisplay(
112 origin_url, url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS);
113#else
114 base::string16 url_string =
115 url_formatter::ElideHost(origin_url, gfx::FontList(), kUrlElideWidth);
116#endif
117 return l10n_util::GetStringFUTF16(
118 is_same_origin_as_main_frame ? IDS_JAVASCRIPT_MESSAGEBOX_TITLE
119 : IDS_JAVASCRIPT_MESSAGEBOX_TITLE_IFRAME,
120 base::i18n::GetDisplayStringInLTRDirectionality(url_string));
121 }
122 return l10n_util::GetStringUTF16(
123 is_same_origin_as_main_frame
124 ? IDS_JAVASCRIPT_MESSAGEBOX_TITLE_NONSTANDARD_URL
125 : IDS_JAVASCRIPT_MESSAGEBOX_TITLE_NONSTANDARD_URL_IFRAME);
126}
127
oshima0929be2a2014-11-19 22:21:03128void JavaScriptDialogManager::RunJavaScriptDialog(
129 content::WebContents* web_contents,
130 const GURL& origin_url,
oshima0929be2a2014-11-19 22:21:03131 content::JavaScriptMessageType message_type,
132 const base::string16& message_text,
133 const base::string16& default_prompt_text,
134 const DialogClosedCallback& callback,
135 bool* did_suppress_message) {
136 *did_suppress_message = false;
137
138 ChromeJavaScriptDialogExtraData* extra_data =
avid79a673c2016-02-19 00:20:03139 &javascript_dialog_extra_data_[web_contents];
oshima0929be2a2014-11-19 22:21:03140
141 if (extra_data->suppress_javascript_messages_) {
joenotcharles505f4212016-02-11 19:28:53142 // If a page tries to open dialogs in a tight loop, the number of
143 // suppressions logged can grow out of control. Arbitrarily cap the number
144 // logged at 100. That many suppressed dialogs is enough to indicate the
145 // page is doing something very hinky.
146 if (extra_data->suppressed_dialog_count_ < 100) {
147 // Log a suppressed dialog as one that opens and then closes immediately.
148 UMA_HISTOGRAM_MEDIUM_TIMES(
149 "JSDialogs.FineTiming.TimeBetweenDialogCreatedAndSameDialogClosed",
150 base::TimeDelta());
151
152 // Only increment the count if it's not already at the limit, to prevent
153 // overflow.
154 extra_data->suppressed_dialog_count_++;
155 }
156
oshima0929be2a2014-11-19 22:21:03157 *did_suppress_message = true;
158 return;
159 }
160
joenotcharles850904a2016-02-09 01:50:44161 base::TimeTicks now = base::TimeTicks::Now();
162 if (!last_creation_time_.is_null()) {
163 // A new dialog has been created: log the time since the last one was
164 // created.
165 UMA_HISTOGRAM_MEDIUM_TIMES(
166 "JSDialogs.FineTiming.TimeBetweenDialogCreatedAndNextDialogCreated",
167 now - last_creation_time_);
168 }
169 last_creation_time_ = now;
170
171 // Also log the time since a dialog was closed, but only if this is the first
172 // dialog that was opened since the closing.
173 if (!last_close_time_.is_null()) {
174 UMA_HISTOGRAM_MEDIUM_TIMES(
175 "JSDialogs.FineTiming.TimeBetweenDialogClosedAndNextDialogCreated",
176 now - last_close_time_);
177 last_close_time_ = base::TimeTicks();
178 }
179
avi7a1b55b2016-10-19 04:18:38180 base::string16 dialog_title = GetTitle(web_contents, origin_url);
oshima0929be2a2014-11-19 22:21:03181
182 extensions_client_->OnDialogOpened(web_contents);
183
avi141dbc1322016-03-11 22:27:42184 LogUMAMessageLengthStats(message_text);
oshima0929be2a2014-11-19 22:21:03185 AppModalDialogQueue::GetInstance()->AddDialog(new JavaScriptAppModalDialog(
186 web_contents,
187 &javascript_dialog_extra_data_,
188 dialog_title,
189 message_type,
190 message_text,
191 default_prompt_text,
avi86578a8a2015-04-17 22:39:31192 ShouldDisplaySuppressCheckbox(extra_data),
oshima0929be2a2014-11-19 22:21:03193 false, // is_before_unload_dialog
194 false, // is_reload
195 base::Bind(&JavaScriptDialogManager::OnDialogClosed,
196 base::Unretained(this), web_contents, callback)));
197}
198
199void JavaScriptDialogManager::RunBeforeUnloadDialog(
200 content::WebContents* web_contents,
oshima0929be2a2014-11-19 22:21:03201 bool is_reload,
202 const DialogClosedCallback& callback) {
avi2460c762015-04-17 15:21:54203 ChromeJavaScriptDialogExtraData* extra_data =
avid79a673c2016-02-19 00:20:03204 &javascript_dialog_extra_data_[web_contents];
avi2460c762015-04-17 15:21:54205
206 if (extra_data->suppress_javascript_messages_) {
207 // If a site harassed the user enough for them to put it on mute, then it
208 // lost its privilege to deny unloading.
209 callback.Run(true, base::string16());
210 return;
211 }
212
avi141dbc1322016-03-11 22:27:42213 // Build the dialog message. We explicitly do _not_ allow the webpage to
214 // specify the contents of this dialog, because most of the time nowadays it's
215 // used for scams.
216 //
217 // This does not violate the spec. Per
218 // https://html.spec.whatwg.org/#prompt-to-unload-a-document, step 7:
219 //
220 // "The prompt shown by the user agent may include the string of the
221 // returnValue attribute, or some leading subset thereof."
222 //
223 // The prompt MAY include the string. It doesn't any more. Scam web page
224 // authors have abused this, so we're taking away the toys from everyone. This
225 // is why we can't have nice things.
226
oshima0929be2a2014-11-19 22:21:03227 const base::string16 title = l10n_util::GetStringUTF16(is_reload ?
228 IDS_BEFORERELOAD_MESSAGEBOX_TITLE : IDS_BEFOREUNLOAD_MESSAGEBOX_TITLE);
avi141dbc1322016-03-11 22:27:42229 const base::string16 message =
230 l10n_util::GetStringUTF16(IDS_BEFOREUNLOAD_MESSAGEBOX_MESSAGE);
oshima0929be2a2014-11-19 22:21:03231
232 extensions_client_->OnDialogOpened(web_contents);
233
234 AppModalDialogQueue::GetInstance()->AddDialog(new JavaScriptAppModalDialog(
235 web_contents,
236 &javascript_dialog_extra_data_,
237 title,
238 content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM,
avi141dbc1322016-03-11 22:27:42239 message,
oshima0929be2a2014-11-19 22:21:03240 base::string16(), // default_prompt_text
avi86578a8a2015-04-17 22:39:31241 ShouldDisplaySuppressCheckbox(extra_data),
oshima0929be2a2014-11-19 22:21:03242 true, // is_before_unload_dialog
243 is_reload,
avie163d2842016-02-22 22:39:21244 base::Bind(&JavaScriptDialogManager::OnBeforeUnloadDialogClosed,
oshima0929be2a2014-11-19 22:21:03245 base::Unretained(this), web_contents, callback)));
246}
247
248bool JavaScriptDialogManager::HandleJavaScriptDialog(
249 content::WebContents* web_contents,
250 bool accept,
251 const base::string16* prompt_override) {
252 AppModalDialogQueue* dialog_queue = AppModalDialogQueue::GetInstance();
253 if (!dialog_queue->HasActiveDialog() ||
254 !dialog_queue->active_dialog()->IsJavaScriptModalDialog() ||
255 dialog_queue->active_dialog()->web_contents() != web_contents) {
256 return false;
257 }
258 JavaScriptAppModalDialog* dialog = static_cast<JavaScriptAppModalDialog*>(
259 dialog_queue->active_dialog());
260 if (accept) {
261 if (prompt_override)
262 dialog->SetOverridePromptText(*prompt_override);
263 dialog->native_dialog()->AcceptAppModalDialog();
264 } else {
265 dialog->native_dialog()->CancelAppModalDialog();
266 }
267 return true;
268}
269
avi5d3b8692016-10-12 22:00:46270void JavaScriptDialogManager::CancelDialogs(content::WebContents* web_contents,
271 bool suppress_callbacks,
272 bool reset_state) {
oshima0929be2a2014-11-19 22:21:03273 AppModalDialogQueue* queue = AppModalDialogQueue::GetInstance();
274 AppModalDialog* active_dialog = queue->active_dialog();
oshima0929be2a2014-11-19 22:21:03275 for (AppModalDialogQueue::iterator i = queue->begin();
276 i != queue->end(); ++i) {
jochenfddefb92015-07-13 10:26:29277 // Invalidating the active dialog might trigger showing a not-yet
278 // invalidated dialog, so invalidate the active dialog last.
279 if ((*i) == active_dialog)
280 continue;
oshima0929be2a2014-11-19 22:21:03281 if ((*i)->web_contents() == web_contents)
avi5d3b8692016-10-12 22:00:46282 (*i)->Invalidate(suppress_callbacks);
oshima0929be2a2014-11-19 22:21:03283 }
jochenfddefb92015-07-13 10:26:29284 if (active_dialog && active_dialog->web_contents() == web_contents)
avi5d3b8692016-10-12 22:00:46285 active_dialog->Invalidate(suppress_callbacks);
286
287 if (reset_state)
288 javascript_dialog_extra_data_.erase(web_contents);
oshima0929be2a2014-11-19 22:21:03289}
290
avie163d2842016-02-22 22:39:21291void JavaScriptDialogManager::OnBeforeUnloadDialogClosed(
292 content::WebContents* web_contents,
293 DialogClosedCallback callback,
294 bool success,
295 const base::string16& user_input) {
296 enum class StayVsLeave {
297 STAY = 0,
298 LEAVE = 1,
299 MAX,
300 };
301 UMA_HISTOGRAM_ENUMERATION(
302 "JSDialogs.OnBeforeUnloadStayVsLeave",
303 static_cast<int>(success ? StayVsLeave::LEAVE : StayVsLeave::STAY),
304 static_cast<int>(StayVsLeave::MAX));
305
306 OnDialogClosed(web_contents, callback, success, user_input);
307}
308
oshima0929be2a2014-11-19 22:21:03309void JavaScriptDialogManager::OnDialogClosed(
310 content::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 extensions_client_->OnDialogClosed(web_contents);
318
joenotcharles850904a2016-02-09 01:50:44319 last_close_time_ = base::TimeTicks::Now();
320
oshima0929be2a2014-11-19 22:21:03321 callback.Run(success, user_input);
322}
323
324} // namespace app_modal