oshima | 758abebc | 2014-11-06 10:55:50 | [diff] [blame] | 1 | // 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 | |
oshima | f6539842 | 2014-11-18 23:30:42 | [diff] [blame] | 5 | #include "components/app_modal/javascript_dialog_manager.h" |
oshima | 758abebc | 2014-11-06 10:55:50 | [diff] [blame] | 6 | |
avi | 6649f89 | 2016-02-19 23:09:48 | [diff] [blame] | 7 | #include <algorithm> |
dcheng | 5160635 | 2015-12-26 21:16:23 | [diff] [blame] | 8 | #include <utility> |
| 9 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 10 | #include "base/bind.h" |
| 11 | #include "base/i18n/rtl.h" |
avi | bc5337b | 2015-12-25 23:16:33 | [diff] [blame] | 12 | #include "base/macros.h" |
joenotcharles | 850904a | 2016-02-09 01:50:44 | [diff] [blame] | 13 | #include "base/metrics/histogram_macros.h" |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 14 | #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" |
oshima | f6539842 | 2014-11-18 23:30:42 | [diff] [blame] | 17 | #include "components/app_modal/javascript_dialog_extensions_client.h" |
oshima | f6539842 | 2014-11-18 23:30:42 | [diff] [blame] | 18 | #include "components/app_modal/javascript_native_dialog_factory.h" |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 19 | #include "components/app_modal/native_app_modal_dialog.h" |
palmer | 4e0ae10d | 2015-09-03 23:38:41 | [diff] [blame] | 20 | #include "components/url_formatter/elide_url.h" |
meacer | 6b936535 | 2015-11-06 00:35:46 | [diff] [blame] | 21 | #include "content/public/browser/web_contents.h" |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 22 | #include "content/public/common/javascript_message_type.h" |
| 23 | #include "grit/components_strings.h" |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 24 | #include "ui/base/l10n/l10n_util.h" |
meacer | 5b6d71dda | 2016-02-08 21:43:35 | [diff] [blame] | 25 | #include "ui/gfx/font_list.h" |
oshima | 758abebc | 2014-11-06 10:55:50 | [diff] [blame] | 26 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 27 | namespace app_modal { |
avi | 6649f89 | 2016-02-19 23:09:48 | [diff] [blame] | 28 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 29 | namespace { |
| 30 | |
meacer | 5b6d71dda | 2016-02-08 21:43:35 | [diff] [blame] | 31 | #if !defined(OS_ANDROID) |
| 32 | // Keep in sync with kDefaultMessageWidth, but allow some space for the rest of |
| 33 | // the text. |
| 34 | const int kUrlElideWidth = 350; |
| 35 | #endif |
| 36 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 37 | class 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 | |
avi | 86578a8a | 2015-04-17 22:39:31 | [diff] [blame] | 55 | bool ShouldDisplaySuppressCheckbox( |
| 56 | ChromeJavaScriptDialogExtraData* extra_data) { |
palmer | d8b2ff0 | 2015-08-18 00:24:59 | [diff] [blame] | 57 | return extra_data->has_already_shown_a_dialog_; |
avi | 86578a8a | 2015-04-17 22:39:31 | [diff] [blame] | 58 | } |
| 59 | |
avi | 141dbc132 | 2016-03-11 22:27:42 | [diff] [blame] | 60 | void LogUMAMessageLengthStats(const base::string16& message) { |
| 61 | UMA_HISTOGRAM_COUNTS("JSDialogs.CountOfJSDialogMessageCharacters", |
| 62 | static_cast<int32_t>(message.length())); |
avi | 6649f89 | 2016-02-19 23:09:48 | [diff] [blame] | 63 | |
| 64 | int32_t newline_count = |
| 65 | std::count_if(message.begin(), message.end(), |
| 66 | [](const base::char16& c) { return c == '\n'; }); |
avi | 141dbc132 | 2016-03-11 22:27:42 | [diff] [blame] | 67 | UMA_HISTOGRAM_COUNTS("JSDialogs.CountOfJSDialogMessageNewlines", |
| 68 | newline_count); |
avi | 6649f89 | 2016-02-19 23:09:48 | [diff] [blame] | 69 | } |
| 70 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 71 | } // namespace |
| 72 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 73 | // static |
| 74 | JavaScriptDialogManager* JavaScriptDialogManager::GetInstance() { |
olli.raula | 36aa8be | 2015-09-10 11:14:22 | [diff] [blame] | 75 | return base::Singleton<JavaScriptDialogManager>::get(); |
oshima | 758abebc | 2014-11-06 10:55:50 | [diff] [blame] | 76 | } |
| 77 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 78 | void JavaScriptDialogManager::SetNativeDialogFactory( |
dcheng | a0ee5fb | 2016-04-26 02:46:55 | [diff] [blame] | 79 | std::unique_ptr<JavaScriptNativeDialogFactory> factory) { |
dcheng | 5160635 | 2015-12-26 21:16:23 | [diff] [blame] | 80 | native_dialog_factory_ = std::move(factory); |
oshima | 758abebc | 2014-11-06 10:55:50 | [diff] [blame] | 81 | } |
| 82 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 83 | void JavaScriptDialogManager::SetExtensionsClient( |
dcheng | a0ee5fb | 2016-04-26 02:46:55 | [diff] [blame] | 84 | std::unique_ptr<JavaScriptDialogExtensionsClient> extensions_client) { |
dcheng | 5160635 | 2015-12-26 21:16:23 | [diff] [blame] | 85 | extensions_client_ = std::move(extensions_client); |
oshima | 758abebc | 2014-11-06 10:55:50 | [diff] [blame] | 86 | } |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 87 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 88 | JavaScriptDialogManager::JavaScriptDialogManager() |
| 89 | : extensions_client_(new DefaultExtensionsClient) { |
| 90 | } |
| 91 | |
| 92 | JavaScriptDialogManager::~JavaScriptDialogManager() { |
| 93 | } |
| 94 | |
avi | 7a1b55b | 2016-10-19 04:18:38 | [diff] [blame] | 95 | base::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 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 128 | void JavaScriptDialogManager::RunJavaScriptDialog( |
| 129 | content::WebContents* web_contents, |
| 130 | const GURL& origin_url, |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 131 | 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 = |
avi | d79a673c | 2016-02-19 00:20:03 | [diff] [blame] | 139 | &javascript_dialog_extra_data_[web_contents]; |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 140 | |
| 141 | if (extra_data->suppress_javascript_messages_) { |
joenotcharles | 505f421 | 2016-02-11 19:28:53 | [diff] [blame] | 142 | // 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 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 157 | *did_suppress_message = true; |
| 158 | return; |
| 159 | } |
| 160 | |
joenotcharles | 850904a | 2016-02-09 01:50:44 | [diff] [blame] | 161 | 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 | |
avi | 7a1b55b | 2016-10-19 04:18:38 | [diff] [blame] | 180 | base::string16 dialog_title = GetTitle(web_contents, origin_url); |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 181 | |
| 182 | extensions_client_->OnDialogOpened(web_contents); |
| 183 | |
avi | 141dbc132 | 2016-03-11 22:27:42 | [diff] [blame] | 184 | LogUMAMessageLengthStats(message_text); |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 185 | 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, |
avi | 86578a8a | 2015-04-17 22:39:31 | [diff] [blame] | 192 | ShouldDisplaySuppressCheckbox(extra_data), |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 193 | false, // is_before_unload_dialog |
| 194 | false, // is_reload |
| 195 | base::Bind(&JavaScriptDialogManager::OnDialogClosed, |
| 196 | base::Unretained(this), web_contents, callback))); |
| 197 | } |
| 198 | |
| 199 | void JavaScriptDialogManager::RunBeforeUnloadDialog( |
| 200 | content::WebContents* web_contents, |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 201 | bool is_reload, |
| 202 | const DialogClosedCallback& callback) { |
avi | 2460c76 | 2015-04-17 15:21:54 | [diff] [blame] | 203 | ChromeJavaScriptDialogExtraData* extra_data = |
avi | d79a673c | 2016-02-19 00:20:03 | [diff] [blame] | 204 | &javascript_dialog_extra_data_[web_contents]; |
avi | 2460c76 | 2015-04-17 15:21:54 | [diff] [blame] | 205 | |
| 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 | |
avi | 141dbc132 | 2016-03-11 22:27:42 | [diff] [blame] | 213 | // 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 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 227 | const base::string16 title = l10n_util::GetStringUTF16(is_reload ? |
| 228 | IDS_BEFORERELOAD_MESSAGEBOX_TITLE : IDS_BEFOREUNLOAD_MESSAGEBOX_TITLE); |
avi | 141dbc132 | 2016-03-11 22:27:42 | [diff] [blame] | 229 | const base::string16 message = |
| 230 | l10n_util::GetStringUTF16(IDS_BEFOREUNLOAD_MESSAGEBOX_MESSAGE); |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 231 | |
| 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, |
avi | 141dbc132 | 2016-03-11 22:27:42 | [diff] [blame] | 239 | message, |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 240 | base::string16(), // default_prompt_text |
avi | 86578a8a | 2015-04-17 22:39:31 | [diff] [blame] | 241 | ShouldDisplaySuppressCheckbox(extra_data), |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 242 | true, // is_before_unload_dialog |
| 243 | is_reload, |
avi | e163d284 | 2016-02-22 22:39:21 | [diff] [blame] | 244 | base::Bind(&JavaScriptDialogManager::OnBeforeUnloadDialogClosed, |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 245 | base::Unretained(this), web_contents, callback))); |
| 246 | } |
| 247 | |
| 248 | bool 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 | |
avi | 5d3b869 | 2016-10-12 22:00:46 | [diff] [blame] | 270 | void JavaScriptDialogManager::CancelDialogs(content::WebContents* web_contents, |
| 271 | bool suppress_callbacks, |
| 272 | bool reset_state) { |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 273 | AppModalDialogQueue* queue = AppModalDialogQueue::GetInstance(); |
| 274 | AppModalDialog* active_dialog = queue->active_dialog(); |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 275 | for (AppModalDialogQueue::iterator i = queue->begin(); |
| 276 | i != queue->end(); ++i) { |
jochen | fddefb9 | 2015-07-13 10:26:29 | [diff] [blame] | 277 | // 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; |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 281 | if ((*i)->web_contents() == web_contents) |
avi | 5d3b869 | 2016-10-12 22:00:46 | [diff] [blame] | 282 | (*i)->Invalidate(suppress_callbacks); |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 283 | } |
jochen | fddefb9 | 2015-07-13 10:26:29 | [diff] [blame] | 284 | if (active_dialog && active_dialog->web_contents() == web_contents) |
avi | 5d3b869 | 2016-10-12 22:00:46 | [diff] [blame] | 285 | active_dialog->Invalidate(suppress_callbacks); |
| 286 | |
| 287 | if (reset_state) |
| 288 | javascript_dialog_extra_data_.erase(web_contents); |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 289 | } |
| 290 | |
avi | e163d284 | 2016-02-22 22:39:21 | [diff] [blame] | 291 | void 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 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 309 | void 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 | |
joenotcharles | 850904a | 2016-02-09 01:50:44 | [diff] [blame] | 319 | last_close_time_ = base::TimeTicks::Now(); |
| 320 | |
oshima | 0929be2a | 2014-11-19 22:21:03 | [diff] [blame] | 321 | callback.Run(success, user_input); |
| 322 | } |
| 323 | |
| 324 | } // namespace app_modal |