1 // Copyright 2016 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 "components/javascript_dialogs/tab_modal_dialog_manager.h"
6
7 #include <utility>
8
9 #include "base/bind.h"
10 #include "base/callback.h"
11 #include "base/feature_list.h"
12 #include "base/memory/ptr_util.h"
13 #include "base/metrics/histogram_macros.h"
14 #include "base/strings/stringprintf.h"
15 #include "components/javascript_dialogs/app_modal_dialog_manager.h"
16 #include "components/javascript_dialogs/tab_modal_dialog_view.h"
17 #include "components/navigation_metrics/navigation_metrics.h"
18 #include "components/ukm/content/source_url_recorder.h"
19 #include "content/public/browser/devtools_agent_host.h"
20 #include "content/public/browser/navigation_handle.h"
21 #include "content/public/browser/render_frame_host.h"
22 #include "services/metrics/public/cpp/ukm_builders.h"
23 #include "services/metrics/public/cpp/ukm_recorder.h"
24 #include "ui/gfx/text_elider.h"
25
26 namespace javascript_dialogs {
27
28 namespace {
29
GetAppModalDialogManager()30 AppModalDialogManager* GetAppModalDialogManager() {
31 return AppModalDialogManager::GetInstance();
32 }
33
34 // The relationship between origins in displayed dialogs.
35 //
36 // This is used for a UMA histogram. Please never alter existing values, only
37 // append new ones.
38 //
39 // Note that "HTTP" in these enum names refers to a scheme that is either HTTP
40 // or HTTPS.
41 enum class DialogOriginRelationship {
42 // The dialog was shown by a main frame with a non-HTTP(S) scheme, or by a
43 // frame within a non-HTTP(S) main frame.
44 NON_HTTP_MAIN_FRAME = 1,
45
46 // The dialog was shown by a main frame with an HTTP(S) scheme.
47 HTTP_MAIN_FRAME = 2,
48
49 // The dialog was displayed by an HTTP(S) frame which shared the same origin
50 // as the main frame.
51 HTTP_MAIN_FRAME_HTTP_SAME_ORIGIN_ALERTING_FRAME = 3,
52
53 // The dialog was displayed by an HTTP(S) frame which had a different origin
54 // from the main frame.
55 HTTP_MAIN_FRAME_HTTP_DIFFERENT_ORIGIN_ALERTING_FRAME = 4,
56
57 // The dialog was displayed by a non-HTTP(S) frame whose nearest HTTP(S)
58 // ancestor shared the same origin as the main frame.
59 HTTP_MAIN_FRAME_NON_HTTP_ALERTING_FRAME_SAME_ORIGIN_ANCESTOR = 5,
60
61 // The dialog was displayed by a non-HTTP(S) frame whose nearest HTTP(S)
62 // ancestor was a different origin than the main frame.
63 HTTP_MAIN_FRAME_NON_HTTP_ALERTING_FRAME_DIFFERENT_ORIGIN_ANCESTOR = 6,
64
65 COUNT,
66 };
67
GetDialogOriginRelationship(content::WebContents * web_contents,content::RenderFrameHost * alerting_frame)68 DialogOriginRelationship GetDialogOriginRelationship(
69 content::WebContents* web_contents,
70 content::RenderFrameHost* alerting_frame) {
71 GURL main_frame_url = web_contents->GetLastCommittedURL();
72
73 if (!main_frame_url.SchemeIsHTTPOrHTTPS())
74 return DialogOriginRelationship::NON_HTTP_MAIN_FRAME;
75
76 if (alerting_frame == web_contents->GetMainFrame())
77 return DialogOriginRelationship::HTTP_MAIN_FRAME;
78
79 GURL alerting_frame_url = alerting_frame->GetLastCommittedURL();
80
81 if (alerting_frame_url.SchemeIsHTTPOrHTTPS()) {
82 if (main_frame_url.GetOrigin() == alerting_frame_url.GetOrigin()) {
83 return DialogOriginRelationship::
84 HTTP_MAIN_FRAME_HTTP_SAME_ORIGIN_ALERTING_FRAME;
85 }
86 return DialogOriginRelationship::
87 HTTP_MAIN_FRAME_HTTP_DIFFERENT_ORIGIN_ALERTING_FRAME;
88 }
89
90 // Walk up the tree to find the nearest ancestor frame of the alerting frame
91 // that has an HTTP(S) scheme. Note that this is guaranteed to terminate
92 // because the main frame has an HTTP(S) scheme.
93 content::RenderFrameHost* nearest_http_ancestor_frame =
94 alerting_frame->GetParent();
95 while (!nearest_http_ancestor_frame->GetLastCommittedURL()
96 .SchemeIsHTTPOrHTTPS()) {
97 nearest_http_ancestor_frame = nearest_http_ancestor_frame->GetParent();
98 }
99
100 GURL nearest_http_ancestor_frame_url =
101 nearest_http_ancestor_frame->GetLastCommittedURL();
102
103 if (main_frame_url.GetOrigin() ==
104 nearest_http_ancestor_frame_url.GetOrigin()) {
105 return DialogOriginRelationship::
106 HTTP_MAIN_FRAME_NON_HTTP_ALERTING_FRAME_SAME_ORIGIN_ANCESTOR;
107 }
108 return DialogOriginRelationship::
109 HTTP_MAIN_FRAME_NON_HTTP_ALERTING_FRAME_DIFFERENT_ORIGIN_ANCESTOR;
110 }
111
112 } // namespace
113
114 // static
CreateForWebContents(content::WebContents * web_contents,std::unique_ptr<TabModalDialogManagerDelegate> delegate)115 void TabModalDialogManager::CreateForWebContents(
116 content::WebContents* web_contents,
117 std::unique_ptr<TabModalDialogManagerDelegate> delegate) {
118 if (!FromWebContents(web_contents)) {
119 web_contents->SetUserData(UserDataKey(),
120 base::WrapUnique(new TabModalDialogManager(
121 web_contents, std::move(delegate))));
122 }
123 }
124
~TabModalDialogManager()125 TabModalDialogManager::~TabModalDialogManager() {
126 CloseDialog(DismissalCause::kTabHelperDestroyed, false, base::string16());
127 }
128
BrowserActiveStateChanged()129 void TabModalDialogManager::BrowserActiveStateChanged() {
130 if (delegate_->IsWebContentsForemost())
131 OnVisibilityChanged(content::Visibility::VISIBLE);
132 else
133 HandleTabSwitchAway(DismissalCause::kBrowserSwitched);
134 }
135
CloseDialogWithReason(DismissalCause reason)136 void TabModalDialogManager::CloseDialogWithReason(DismissalCause reason) {
137 CloseDialog(reason, false, base::string16());
138 }
139
SetDialogShownCallbackForTesting(base::OnceClosure callback)140 void TabModalDialogManager::SetDialogShownCallbackForTesting(
141 base::OnceClosure callback) {
142 dialog_shown_ = std::move(callback);
143 }
144
IsShowingDialogForTesting() const145 bool TabModalDialogManager::IsShowingDialogForTesting() const {
146 return !!dialog_;
147 }
148
ClickDialogButtonForTesting(bool accept,const base::string16 & user_input)149 void TabModalDialogManager::ClickDialogButtonForTesting(
150 bool accept,
151 const base::string16& user_input) {
152 DCHECK(!!dialog_);
153 CloseDialog(DismissalCause::kDialogButtonClicked, accept, user_input);
154 }
155
SetDialogDismissedCallbackForTesting(DialogDismissedCallback callback)156 void TabModalDialogManager::SetDialogDismissedCallbackForTesting(
157 DialogDismissedCallback callback) {
158 dialog_dismissed_ = std::move(callback);
159 }
160
RunJavaScriptDialog(content::WebContents * alerting_web_contents,content::RenderFrameHost * render_frame_host,content::JavaScriptDialogType dialog_type,const base::string16 & message_text,const base::string16 & default_prompt_text,DialogClosedCallback callback,bool * did_suppress_message)161 void TabModalDialogManager::RunJavaScriptDialog(
162 content::WebContents* alerting_web_contents,
163 content::RenderFrameHost* render_frame_host,
164 content::JavaScriptDialogType dialog_type,
165 const base::string16& message_text,
166 const base::string16& default_prompt_text,
167 DialogClosedCallback callback,
168 bool* did_suppress_message) {
169 DCHECK_EQ(alerting_web_contents,
170 content::WebContents::FromRenderFrameHost(render_frame_host));
171
172 GURL alerting_frame_url = render_frame_host->GetLastCommittedURL();
173
174 content::WebContents* web_contents = WebContentsObserver::web_contents();
175 DialogOriginRelationship origin_relationship =
176 GetDialogOriginRelationship(alerting_web_contents, render_frame_host);
177 navigation_metrics::Scheme scheme =
178 navigation_metrics::GetScheme(alerting_frame_url);
179 switch (dialog_type) {
180 case content::JAVASCRIPT_DIALOG_TYPE_ALERT:
181 UMA_HISTOGRAM_ENUMERATION("JSDialogs.OriginRelationship.Alert",
182 origin_relationship,
183 DialogOriginRelationship::COUNT);
184 UMA_HISTOGRAM_ENUMERATION("JSDialogs.Scheme.Alert", scheme,
185 navigation_metrics::Scheme::COUNT);
186 break;
187 case content::JAVASCRIPT_DIALOG_TYPE_CONFIRM:
188 UMA_HISTOGRAM_ENUMERATION("JSDialogs.OriginRelationship.Confirm",
189 origin_relationship,
190 DialogOriginRelationship::COUNT);
191 UMA_HISTOGRAM_ENUMERATION("JSDialogs.Scheme.Confirm", scheme,
192 navigation_metrics::Scheme::COUNT);
193 break;
194 case content::JAVASCRIPT_DIALOG_TYPE_PROMPT:
195 UMA_HISTOGRAM_ENUMERATION("JSDialogs.OriginRelationship.Prompt",
196 origin_relationship,
197 DialogOriginRelationship::COUNT);
198 UMA_HISTOGRAM_ENUMERATION("JSDialogs.Scheme.Prompt", scheme,
199 navigation_metrics::Scheme::COUNT);
200 break;
201 }
202
203 // Close any dialog already showing.
204 CloseDialog(DismissalCause::kSubsequentDialogShown, false, base::string16());
205
206 bool make_pending = false;
207 if (!delegate_->IsWebContentsForemost() &&
208 !content::DevToolsAgentHost::IsDebuggerAttached(web_contents)) {
209 static const char kDialogSuppressedConsoleMessageFormat[] =
210 "A window.%s() dialog generated by this page was suppressed "
211 "because this page is not the active tab of the front window. "
212 "Please make sure your dialogs are triggered by user interactions "
213 "to avoid this situation. https://www.chromestatus.com/feature/%s";
214
215 switch (dialog_type) {
216 case content::JAVASCRIPT_DIALOG_TYPE_ALERT: {
217 // When an alert fires in the background, make the callback so that the
218 // render process can continue.
219 std::move(callback).Run(true, base::string16());
220 callback.Reset();
221
222 delegate_->SetTabNeedsAttention(true);
223
224 make_pending = true;
225 break;
226 }
227 case content::JAVASCRIPT_DIALOG_TYPE_CONFIRM: {
228 *did_suppress_message = true;
229 alerting_web_contents->GetMainFrame()->AddMessageToConsole(
230 blink::mojom::ConsoleMessageLevel::kWarning,
231 base::StringPrintf(kDialogSuppressedConsoleMessageFormat, "confirm",
232 "5140698722467840"));
233 return;
234 }
235 case content::JAVASCRIPT_DIALOG_TYPE_PROMPT: {
236 *did_suppress_message = true;
237 alerting_web_contents->GetMainFrame()->AddMessageToConsole(
238 blink::mojom::ConsoleMessageLevel::kWarning,
239 base::StringPrintf(kDialogSuppressedConsoleMessageFormat, "prompt",
240 "5637107137642496"));
241 return;
242 }
243 }
244 }
245
246 // Enforce sane sizes. ElideRectangleString breaks horizontally, which isn't
247 // strictly needed, but it restricts the vertical size, which is crucial.
248 // This gives about 2000 characters, which is about the same as the
249 // AppModalDialogManager provides, but allows no more than 24 lines.
250 const int kMessageTextMaxRows = 24;
251 const int kMessageTextMaxCols = 80;
252 const size_t kDefaultPromptMaxSize = 2000;
253 base::string16 truncated_message_text;
254 gfx::ElideRectangleString(message_text, kMessageTextMaxRows,
255 kMessageTextMaxCols, false,
256 &truncated_message_text);
257 base::string16 truncated_default_prompt_text;
258 gfx::ElideString(default_prompt_text, kDefaultPromptMaxSize,
259 &truncated_default_prompt_text);
260
261 base::string16 title = GetAppModalDialogManager()->GetTitle(
262 alerting_web_contents, alerting_frame_url);
263 dialog_callback_ = std::move(callback);
264 dialog_type_ = dialog_type;
265 if (make_pending) {
266 DCHECK(!dialog_);
267 pending_dialog_ = base::BindOnce(
268 &TabModalDialogManagerDelegate::CreateNewDialog,
269 base::Unretained(delegate_.get()), alerting_web_contents, title,
270 dialog_type, truncated_message_text, truncated_default_prompt_text,
271 base::BindOnce(&TabModalDialogManager::CloseDialog,
272 base::Unretained(this),
273 DismissalCause::kDialogButtonClicked),
274 base::BindOnce(&TabModalDialogManager::CloseDialog,
275 base::Unretained(this), DismissalCause::kDialogClosed,
276 false, base::string16()));
277 } else {
278 DCHECK(!pending_dialog_);
279 dialog_ = delegate_->CreateNewDialog(
280 alerting_web_contents, title, dialog_type, truncated_message_text,
281 truncated_default_prompt_text,
282 base::BindOnce(&TabModalDialogManager::CloseDialog,
283 base::Unretained(this),
284 DismissalCause::kDialogButtonClicked),
285 base::BindOnce(&TabModalDialogManager::CloseDialog,
286 base::Unretained(this), DismissalCause::kDialogClosed,
287 false, base::string16()));
288 }
289
290 delegate_->WillRunDialog();
291
292 // Message suppression is something that we don't give the user a checkbox
293 // for any more. It was useful back in the day when dialogs were app-modal
294 // and clicking the checkbox was the only way to escape a loop that the page
295 // was doing, but now the user can just close the page.
296 *did_suppress_message = false;
297
298 if (!dialog_shown_.is_null())
299 std::move(dialog_shown_).Run();
300 }
301
RunBeforeUnloadDialog(content::WebContents * web_contents,content::RenderFrameHost * render_frame_host,bool is_reload,DialogClosedCallback callback)302 void TabModalDialogManager::RunBeforeUnloadDialog(
303 content::WebContents* web_contents,
304 content::RenderFrameHost* render_frame_host,
305 bool is_reload,
306 DialogClosedCallback callback) {
307 DCHECK_EQ(web_contents,
308 content::WebContents::FromRenderFrameHost(render_frame_host));
309
310 DialogOriginRelationship origin_relationship =
311 GetDialogOriginRelationship(web_contents, render_frame_host);
312 navigation_metrics::Scheme scheme =
313 navigation_metrics::GetScheme(render_frame_host->GetLastCommittedURL());
314 UMA_HISTOGRAM_ENUMERATION("JSDialogs.OriginRelationship.BeforeUnload",
315 origin_relationship,
316 DialogOriginRelationship::COUNT);
317 UMA_HISTOGRAM_ENUMERATION("JSDialogs.Scheme.BeforeUnload", scheme,
318 navigation_metrics::Scheme::COUNT);
319
320 // onbeforeunload dialogs are always handled with an app-modal dialog, because
321 // - they are critical to the user not losing data
322 // - they can be requested for tabs that are not foremost
323 // - they can be requested for many tabs at the same time
324 // and therefore auto-dismissal is inappropriate for them.
325
326 return GetAppModalDialogManager()->RunBeforeUnloadDialogWithOptions(
327 web_contents, render_frame_host, is_reload, delegate_->IsApp(),
328 std::move(callback));
329 }
330
HandleJavaScriptDialog(content::WebContents * web_contents,bool accept,const base::string16 * prompt_override)331 bool TabModalDialogManager::HandleJavaScriptDialog(
332 content::WebContents* web_contents,
333 bool accept,
334 const base::string16* prompt_override) {
335 if (dialog_ || pending_dialog_) {
336 CloseDialog(DismissalCause::kHandleDialogCalled, accept,
337 prompt_override ? *prompt_override : dialog_->GetUserInput());
338 return true;
339 }
340
341 // Handle any app-modal dialogs being run by the app-modal dialog system.
342 return GetAppModalDialogManager()->HandleJavaScriptDialog(
343 web_contents, accept, prompt_override);
344 }
345
CancelDialogs(content::WebContents * web_contents,bool reset_state)346 void TabModalDialogManager::CancelDialogs(content::WebContents* web_contents,
347 bool reset_state) {
348 CloseDialog(DismissalCause::kCancelDialogsCalled, false, base::string16());
349
350 // Cancel any app-modal dialogs being run by the app-modal dialog system.
351 return GetAppModalDialogManager()->CancelDialogs(web_contents, reset_state);
352 }
353
OnVisibilityChanged(content::Visibility visibility)354 void TabModalDialogManager::OnVisibilityChanged(
355 content::Visibility visibility) {
356 if (visibility == content::Visibility::HIDDEN) {
357 HandleTabSwitchAway(DismissalCause::kTabHidden);
358 } else if (pending_dialog_) {
359 dialog_ = std::move(pending_dialog_).Run();
360 pending_dialog_.Reset();
361 delegate_->SetTabNeedsAttention(false);
362 }
363 }
364
DidStartNavigation(content::NavigationHandle * navigation_handle)365 void TabModalDialogManager::DidStartNavigation(
366 content::NavigationHandle* navigation_handle) {
367 // Close the dialog if the user started a new navigation. This allows reloads
368 // and history navigations to proceed.
369 CloseDialog(DismissalCause::kTabNavigated, false, base::string16());
370 }
371
TabModalDialogManager(content::WebContents * web_contents,std::unique_ptr<TabModalDialogManagerDelegate> delegate)372 TabModalDialogManager::TabModalDialogManager(
373 content::WebContents* web_contents,
374 std::unique_ptr<TabModalDialogManagerDelegate> delegate)
375 : content::WebContentsObserver(web_contents),
376 delegate_(std::move(delegate)) {}
377
LogDialogDismissalCause(DismissalCause cause)378 void TabModalDialogManager::LogDialogDismissalCause(DismissalCause cause) {
379 if (dialog_dismissed_)
380 std::move(dialog_dismissed_).Run(cause);
381
382 // Log to UKM.
383 //
384 // Note that this will return the outermost WebContents, not necessarily the
385 // WebContents that had the alert call in it. For 99.9999% of cases they're
386 // the same, but for instances like the <webview> tag in extensions and PDF
387 // files that alert they may differ.
388 ukm::SourceId source_id = ukm::GetSourceIdForWebContentsDocument(
389 WebContentsObserver::web_contents());
390 if (source_id != ukm::kInvalidSourceId) {
391 ukm::builders::AbusiveExperienceHeuristic_JavaScriptDialog(source_id)
392 .SetDismissalCause(static_cast<int64_t>(cause))
393 .Record(ukm::UkmRecorder::Get());
394 }
395 }
396
HandleTabSwitchAway(DismissalCause cause)397 void TabModalDialogManager::HandleTabSwitchAway(DismissalCause cause) {
398 if (!dialog_ || content::DevToolsAgentHost::IsDebuggerAttached(
399 WebContentsObserver::web_contents())) {
400 return;
401 }
402
403 if (dialog_type_ == content::JAVASCRIPT_DIALOG_TYPE_ALERT) {
404 // When the user switches tabs, make the callback so that the render process
405 // can continue.
406 if (dialog_callback_) {
407 std::move(dialog_callback_).Run(true, base::string16());
408 dialog_callback_.Reset();
409 }
410 } else {
411 CloseDialog(cause, false, base::string16());
412 }
413 }
414
CloseDialog(DismissalCause cause,bool success,const base::string16 & user_input)415 void TabModalDialogManager::CloseDialog(DismissalCause cause,
416 bool success,
417 const base::string16& user_input) {
418 if (!dialog_ && !pending_dialog_)
419 return;
420
421 LogDialogDismissalCause(cause);
422
423 // CloseDialog() can be called two ways. It can be called from within
424 // TabModalDialogManager, in which case the dialog needs to be closed.
425 // However, it can also be called, bound, from the JavaScriptDialog. In that
426 // case, the dialog is already closing, so the JavaScriptDialog doesn't need
427 // to be told to close.
428 //
429 // Using the |cause| to distinguish a call from JavaScriptDialog vs from
430 // within TabModalDialogManager is a bit hacky, but is the simplest way.
431 if (dialog_ && cause != DismissalCause::kDialogButtonClicked &&
432 cause != DismissalCause::kDialogClosed)
433 dialog_->CloseDialogWithoutCallback();
434
435 // If there is a callback, call it. There might not be one, if a tab-modal
436 // alert() dialog is showing.
437 if (dialog_callback_)
438 std::move(dialog_callback_).Run(success, user_input);
439
440 // If there's a pending dialog, then the tab is still in the "needs attention"
441 // state; clear it out. However, if the tab was switched out, the turning off
442 // of the "needs attention" state was done in OnTabStripModelChanged()
443 // SetTabNeedsAttention won't work, so don't call it.
444 if (pending_dialog_ && cause != DismissalCause::kTabSwitchedOut &&
445 cause != DismissalCause::kTabHelperDestroyed) {
446 delegate_->SetTabNeedsAttention(false);
447 }
448
449 dialog_.reset();
450 pending_dialog_.Reset();
451 dialog_callback_.Reset();
452
453 delegate_->DidCloseDialog();
454 }
455
456 WEB_CONTENTS_USER_DATA_KEY_IMPL(TabModalDialogManager)
457
458 } // namespace javascript_dialogs
459