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