tab_modal_dialog_manager.cc 18 KB

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