File: javascript_dialog_tab_helper.cc

package info (click to toggle)
chromium-browser 57.0.2987.98-1~deb8u1
  • links: PTS, VCS
  • area: main
  • in suites: jessie
  • size: 2,637,852 kB
  • ctags: 2,544,394
  • sloc: cpp: 12,815,961; ansic: 3,676,222; python: 1,147,112; asm: 526,608; java: 523,212; xml: 286,794; perl: 92,654; sh: 86,408; objc: 73,271; makefile: 27,698; cs: 18,487; yacc: 13,031; tcl: 12,957; pascal: 4,875; ml: 4,716; lex: 3,904; sql: 3,862; ruby: 1,982; lisp: 1,508; php: 1,368; exp: 404; awk: 325; csh: 117; jsp: 39; sed: 37
file content (339 lines) | stat: -rw-r--r-- 13,005 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/javascript_dialogs/javascript_dialog_tab_helper.h"

#include "base/bind.h"
#include "base/callback.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "chrome/browser/engagement/site_engagement_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/tab_modal_confirm_dialog.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_features.h"
#include "components/app_modal/javascript_dialog_manager.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"

DEFINE_WEB_CONTENTS_USER_DATA_KEY(JavaScriptDialogTabHelper);

namespace {

bool IsEnabled() {
  return base::FeatureList::IsEnabled(features::kAutoDismissingDialogs);
}

app_modal::JavaScriptDialogManager* AppModalDialogManager() {
  return app_modal::JavaScriptDialogManager::GetInstance();
}

bool IsWebContentsForemost(content::WebContents* web_contents) {
  Browser* browser = BrowserList::GetInstance()->GetLastActive();
  DCHECK(browser);
  return browser->tab_strip_model()->GetActiveWebContents() == web_contents;
}

}  // namespace

// A note on the interaction between the JavaScriptDialogTabHelper and the
// JavaScriptDialog classes.
//
// Either side can start the process of closing a dialog, and we need to ensure
// that everything is properly torn down no matter which side initiates.
//
// If closing is initiated by the JavaScriptDialogTabHelper, then it will call
// CloseDialog(), which calls JavaScriptDialog::CloseDialogWithoutCallback().
// That will clear the callback inside of JavaScriptDialog, and start the
// JavaScriptDialog on its own path of destruction. CloseDialog() then calls
// ClearDialogInfo() which removes observers.
//
// If closing is initiated by the JavaScriptDialog, which is a self-deleting
// object, then it will make its callback. The callback will have been wrapped
// within JavaScriptDialogTabHelper::RunJavaScriptDialog() to be a call to
// JavaScriptDialogTabHelper::OnDialogClosed(), which, after doing the callback,
// again calls ClearDialogInfo() to remove observers.

enum class JavaScriptDialogTabHelper::DismissalCause {
  // This is used for a UMA histogram. Please never alter existing values, only
  // append new ones.
  TAB_HELPER_DESTROYED = 0,
  SUBSEQUENT_DIALOG_SHOWN = 1,
  HANDLE_DIALOG_CALLED = 2,
  CANCEL_DIALOGS_CALLED = 3,
  TAB_HIDDEN = 4,
  BROWSER_SWITCHED = 5,
  DIALOG_BUTTON_CLICKED = 6,
  TAB_NAVIGATED = 7,
  MAX,
};

JavaScriptDialogTabHelper::JavaScriptDialogTabHelper(
    content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents) {
}

JavaScriptDialogTabHelper::~JavaScriptDialogTabHelper() {
  if (dialog_) {
    CloseDialog(false, base::string16(), DismissalCause::TAB_HELPER_DESTROYED);
  }
}

void JavaScriptDialogTabHelper::SetDialogShownCallbackForTesting(
    base::Closure callback) {
  dialog_shown_ = callback;
}

void JavaScriptDialogTabHelper::RunJavaScriptDialog(
    content::WebContents* alerting_web_contents,
    const GURL& origin_url,
    content::JavaScriptMessageType message_type,
    const base::string16& message_text,
    const base::string16& default_prompt_text,
    const DialogClosedCallback& callback,
    bool* did_suppress_message) {
  SiteEngagementService* site_engagement_service = SiteEngagementService::Get(
      Profile::FromBrowserContext(alerting_web_contents->GetBrowserContext()));
  double engagement_score = site_engagement_service->GetScore(origin_url);
  int32_t message_length = static_cast<int32_t>(message_text.length());
  if (engagement_score == 0) {
    UMA_HISTOGRAM_COUNTS("JSDialogs.CharacterCount.EngagementNone",
                         message_length);
  } else if (engagement_score < 1) {
    UMA_HISTOGRAM_COUNTS("JSDialogs.CharacterCount.EngagementLessThanOne",
                         message_length);
  } else if (engagement_score < 5) {
    UMA_HISTOGRAM_COUNTS("JSDialogs.CharacterCount.EngagementOneToFive",
                         message_length);
  } else {
    UMA_HISTOGRAM_COUNTS("JSDialogs.CharacterCount.EngagementHigher",
                         message_length);
  }

  content::WebContents* parent_web_contents =
      WebContentsObserver::web_contents();
  bool foremost = IsWebContentsForemost(parent_web_contents);
  switch (message_type) {
    case content::JAVASCRIPT_MESSAGE_TYPE_ALERT:
      UMA_HISTOGRAM_BOOLEAN("JSDialogs.IsForemost.Alert", foremost);
      break;
    case content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM:
      UMA_HISTOGRAM_BOOLEAN("JSDialogs.IsForemost.Confirm", foremost);
      break;
    case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
      UMA_HISTOGRAM_BOOLEAN("JSDialogs.IsForemost.Prompt", foremost);
      break;
  }

  if (IsEnabled()) {
    if (!IsWebContentsForemost(parent_web_contents) &&
        message_type == content::JAVASCRIPT_MESSAGE_TYPE_PROMPT) {
      // Don't allow "prompt" dialogs to steal the user's focus. TODO(avi):
      // Eventually, for subsequent phases of http://bit.ly/project-oldspice,
      // turn off focus stealing for other dialog types.
      *did_suppress_message = true;
      alerting_web_contents->GetMainFrame()->AddMessageToConsole(
          content::CONSOLE_MESSAGE_LEVEL_WARNING,
          "A window.prompt() dialog generated by this page was suppressed "
          "because this page is not the active tab of the front window. "
          "Please make sure your dialogs are triggered by user interactions "
          "to avoid this situation. "
          "https://www.chromestatus.com/feature/5637107137642496");
      return;
    }

    if (dialog_) {
      // There's already a dialog up; clear it out.
      CloseDialog(false, base::string16(),
                  DismissalCause::SUBSEQUENT_DIALOG_SHOWN);
    }

    base::string16 title =
        AppModalDialogManager()->GetTitle(alerting_web_contents, origin_url);
    dialog_callback_ = callback;
    message_type_ = message_type;
    dialog_ = JavaScriptDialog::Create(
        parent_web_contents, alerting_web_contents, title, message_type,
        message_text, default_prompt_text,
        base::Bind(&JavaScriptDialogTabHelper::OnDialogClosed,
                   base::Unretained(this), callback));

    BrowserList::AddObserver(this);

    // Message suppression is something that we don't give the user a checkbox
    // for any more. It was useful back in the day when dialogs were app-modal
    // and clicking the checkbox was the only way to escape a loop that the page
    // was doing, but now the user can just close the page.
    *did_suppress_message = false;

    if (!dialog_shown_.is_null()) {
      dialog_shown_.Run();
      dialog_shown_.Reset();
    }
  } else {
    AppModalDialogManager()->RunJavaScriptDialog(
        alerting_web_contents, origin_url, message_type, message_text,
        default_prompt_text, callback, did_suppress_message);
  }

  if (did_suppress_message) {
    UMA_HISTOGRAM_COUNTS("JSDialogs.CharacterCountUserSuppressed",
                         message_length);
  }
}

namespace {

void SaveUnloadUmaStats(
    double engagement_score,
    content::JavaScriptDialogManager::DialogClosedCallback callback,
    bool success,
    const base::string16& user_input) {
  if (success) {
    UMA_HISTOGRAM_PERCENTAGE("JSDialogs.SiteEngagementOfBeforeUnload.Leave",
                             engagement_score);
  } else {
    UMA_HISTOGRAM_PERCENTAGE("JSDialogs.SiteEngagementOfBeforeUnload.Stay",
                             engagement_score);
  }

  callback.Run(success, user_input);
}

}  // namespace

void JavaScriptDialogTabHelper::RunBeforeUnloadDialog(
    content::WebContents* web_contents,
    bool is_reload,
    const DialogClosedCallback& callback) {
  // onbeforeunload dialogs are always handled with an app-modal dialog, because
  // - they are critical to the user not losing data
  // - they can be requested for tabs that are not foremost
  // - they can be requested for many tabs at the same time
  // and therefore auto-dismissal is inappropriate for them.

  SiteEngagementService* site_engagement_service = SiteEngagementService::Get(
      Profile::FromBrowserContext(web_contents->GetBrowserContext()));
  double engagement_score =
      site_engagement_service->GetScore(web_contents->GetLastCommittedURL());

  return AppModalDialogManager()->RunBeforeUnloadDialog(
      web_contents, is_reload,
      base::Bind(&SaveUnloadUmaStats, engagement_score, callback));
}

bool JavaScriptDialogTabHelper::HandleJavaScriptDialog(
    content::WebContents* web_contents,
    bool accept,
    const base::string16* prompt_override) {
  if (dialog_) {
    CloseDialog(accept, prompt_override ? *prompt_override : base::string16(),
                DismissalCause::HANDLE_DIALOG_CALLED);
    return true;
  }

  // Handle any app-modal dialogs being run by the app-modal dialog system.
  return AppModalDialogManager()->HandleJavaScriptDialog(web_contents, accept,
                                                         prompt_override);
}

void JavaScriptDialogTabHelper::CancelDialogs(
    content::WebContents* web_contents,
    bool suppress_callbacks,
    bool reset_state) {
  if (dialog_) {
    CloseDialog(false, base::string16(), DismissalCause::CANCEL_DIALOGS_CALLED);
  }

  // Cancel any app-modal dialogs being run by the app-modal dialog system.
  return AppModalDialogManager()->CancelDialogs(
      web_contents, suppress_callbacks, reset_state);
}

void JavaScriptDialogTabHelper::WasHidden() {
  if (dialog_)
    CloseDialog(false, base::string16(), DismissalCause::TAB_HIDDEN);
}

// This function handles the case where browser-side navigation (PlzNavigate) is
// enabled. DidStartNavigationToPendingEntry, below, handles the case where
// PlzNavigate is not enabled. TODO(avi): When the non-PlzNavigate code is
// removed, remove DidStartNavigationToPendingEntry.
void JavaScriptDialogTabHelper::DidStartNavigation(
    content::NavigationHandle* navigation_handle) {
  // Close the dialog if the user started a new navigation. This allows reloads
  // and history navigations to proceed.
  if (dialog_)
    CloseDialog(false, base::string16(), DismissalCause::TAB_NAVIGATED);
}

// This function handles the case where browser-side navigation (PlzNavigate) is
// not enabled. DidStartNavigation, above, handles the case where PlzNavigate is
// enabled. TODO(avi): When the non-PlzNavigate code is removed, remove
// DidStartNavigationToPendingEntry.
void JavaScriptDialogTabHelper::DidStartNavigationToPendingEntry(
    const GURL& url,
    content::ReloadType reload_type) {
  // Close the dialog if the user started a new navigation. This allows reloads
  // and history navigations to proceed.
  if (dialog_)
    CloseDialog(false, base::string16(), DismissalCause::TAB_NAVIGATED);
}

void JavaScriptDialogTabHelper::OnBrowserSetLastActive(Browser* browser) {
  if (dialog_ && !IsWebContentsForemost(web_contents())) {
    CloseDialog(false, base::string16(), DismissalCause::BROWSER_SWITCHED);
  }
}

void JavaScriptDialogTabHelper::LogDialogDismissalCause(
    JavaScriptDialogTabHelper::DismissalCause cause) {
  switch (message_type_) {
    case content::JAVASCRIPT_MESSAGE_TYPE_ALERT:
      UMA_HISTOGRAM_ENUMERATION("JSDialogs.DismissalCause.Alert",
                                static_cast<int>(cause),
                                static_cast<int>(DismissalCause::MAX));
      break;
    case content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM:
      UMA_HISTOGRAM_ENUMERATION("JSDialogs.DismissalCause.Confirm",
                                static_cast<int>(cause),
                                static_cast<int>(DismissalCause::MAX));
      break;
    case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
      UMA_HISTOGRAM_ENUMERATION("JSDialogs.DismissalCause.Prompt",
                                static_cast<int>(cause),
                                static_cast<int>(DismissalCause::MAX));
      break;
  }
}

void JavaScriptDialogTabHelper::OnDialogClosed(
    DialogClosedCallback callback,
    bool success,
    const base::string16& user_input) {
  LogDialogDismissalCause(DismissalCause::DIALOG_BUTTON_CLICKED);
  callback.Run(success, user_input);

  ClearDialogInfo();
}

void JavaScriptDialogTabHelper::CloseDialog(bool success,
                                            const base::string16& user_input,
                                            DismissalCause cause) {
  DCHECK(dialog_);
  LogDialogDismissalCause(cause);

  dialog_->CloseDialogWithoutCallback();
  dialog_callback_.Run(success, user_input);

  ClearDialogInfo();
}

void JavaScriptDialogTabHelper::ClearDialogInfo() {
  dialog_.reset();
  dialog_callback_.Reset();
  BrowserList::RemoveObserver(this);
}