File: external_protocol_handler.cc

package info (click to toggle)
chromium 138.0.7204.157-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,864 kB
  • sloc: cpp: 34,936,859; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,967; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (659 lines) | stat: -rw-r--r-- 25,599 bytes parent folder | download | duplicates (3)
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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/external_protocol/external_protocol_handler.h"

#include <stddef.h>

#include <utility>

#include "base/check_op.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/fixed_flat_set.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/strings/escape.h"
#include "base/strings/string_util.h"
#include "base/types/optional_util.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/external_protocol/auto_launch_protocols_policy_handler.h"
#include "chrome/browser/external_protocol/constants.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/url_matcher/url_matcher.h"
#include "components/url_matcher/url_util.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/weak_document_ptr.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/sharing/click_to_call/click_to_call_ui_controller.h"
#include "chrome/browser/sharing/click_to_call/click_to_call_utils.h"
#endif

#if BUILDFLAG(IS_ANDROID)
#include "components/navigation_interception/intercept_navigation_delegate.h"
#else
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "components/url_formatter/elide_url.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#endif

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#endif

namespace {

// Anti-flood protection controls whether we accept requests for launching
// external protocols. Set to false each time an external protocol is requested,
// and set back to true on each user gesture, extension API call, and navigation
// to an external handler via bookmarks or the omnibox. This variable should
// only be accessed from the UI thread.
bool g_accept_requests = true;

ExternalProtocolHandler::Delegate* g_external_protocol_handler_delegate =
    nullptr;

constexpr auto kDeniedSchemes = base::MakeFixedFlatSet<std::string_view>({
    "afp",
    "data",
    "disk",
    "disks",
    // ShellExecuting file:///C:/WINDOWS/system32/notepad.exe will simply
    // execute the file specified!  Hopefully we won't see any "file" schemes
    // because we think of file:// URLs as handled URLs, but better to be safe
    // than to let an attacker format the user's hard drive.
    "file",
    "hcp",
    "ie.http",
    "javascript",
    "mk",
    "ms-help",
    "nntp",
    "res",
    "shell",
    "vbscript",
    // view-source is a special case in chrome. When it comes through an
    // iframe or a redirect, it looks like an external protocol, but we don't
    // want to shellexecute it.
    "view-source",
    "vnd.ms.radio",
});

void AddMessageToConsole(const content::WeakDocumentPtr& document,
                         blink::mojom::ConsoleMessageLevel level,
                         const std::string& message) {
  if (content::RenderFrameHost* rfh = document.AsRenderFrameHostIfValid())
    rfh->AddMessageToConsole(level, message);
}

#if !BUILDFLAG(IS_ANDROID)
// Functions enabling unit testing. Using a NULL delegate will use the default
// behavior; if a delegate is provided it will be used instead.
scoped_refptr<shell_integration::DefaultSchemeClientWorker> CreateShellWorker(
    const GURL& url,
    ExternalProtocolHandler::Delegate* delegate) {
  if (delegate)
    return delegate->CreateShellWorker(url);
  return base::MakeRefCounted<shell_integration::DefaultSchemeClientWorker>(
      url);
}
#endif

ExternalProtocolHandler::BlockState GetBlockStateWithDelegate(
    const std::string& scheme,
    const url::Origin* initiating_origin,
    ExternalProtocolHandler::Delegate* delegate,
    Profile* profile) {
  if (delegate)
    return delegate->GetBlockState(scheme, profile);
  return ExternalProtocolHandler::GetBlockState(scheme, initiating_origin,
                                                profile);
}

#if !BUILDFLAG(IS_ANDROID)
void RunExternalProtocolDialogWithDelegate(
    const GURL& url,
    content::WebContents* web_contents,
    ui::PageTransition page_transition,
    bool has_user_gesture,
    bool is_in_fenced_frame_tree,
    const std::optional<url::Origin>& initiating_origin,
    content::WeakDocumentPtr initiator_document,
    const std::u16string& program_name,
    ExternalProtocolHandler::Delegate* delegate) {
  DCHECK(web_contents);
  if (delegate) {
    delegate->RunExternalProtocolDialog(url, web_contents, page_transition,
                                        has_user_gesture, initiating_origin,
                                        program_name);
    return;
  }

#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
  // If the Shell does not have a registered name for the protocol,
  // attempting to invoke the protocol will fail.
  if (program_name.empty()) {
    AddMessageToConsole(
        initiator_document, blink::mojom::ConsoleMessageLevel::kError,
        "Failed to launch '" + url.possibly_invalid_spec() +
            "' because the scheme does not have a registered handler.");
    return;
  }
#endif

  ExternalProtocolHandler::RunExternalProtocolDialog(
      url, web_contents, page_transition, has_user_gesture,
      is_in_fenced_frame_tree, initiating_origin, std::move(initiator_document),
      program_name);
}
#endif  // !BUILDFLAG(IS_ANDROID)

void LaunchUrlWithoutSecurityCheckWithDelegate(
    const GURL& url,
    content::WebContents* web_contents,
    content::WeakDocumentPtr initiator_document,
    ExternalProtocolHandler::Delegate* delegate) {
  if (delegate) {
    delegate->ReportExternalAppRedirectToSafeBrowsing(url, web_contents);
    delegate->LaunchUrlWithoutSecurityCheck(url, web_contents);
    return;
  }

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  g_browser_process->safe_browsing_service()->ReportExternalAppRedirect(
      web_contents, url.scheme(), url.possibly_invalid_spec());
#endif

  // |web_contents| is only passed in to find browser context. Do not assume
  // that the external protocol request came from the main frame.
  if (!web_contents)
    return;

  AddMessageToConsole(
      initiator_document, blink::mojom::ConsoleMessageLevel::kInfo,
      "Launched external handler for '" + url.possibly_invalid_spec() + "'.");

  platform_util::OpenExternal(
#if BUILDFLAG(IS_CHROMEOS)
      Profile::FromBrowserContext(web_contents->GetBrowserContext()),
#endif
      url);

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS)
  // If the protocol navigation occurs in a new tab, close it.
  // Avoid calling CloseContents if the tab is not in this browser's tab strip
  // model; this can happen if the protocol was initiated by something
  // internal to Chrome.
  Browser* browser = chrome::FindBrowserWithTab(web_contents);
  if (browser && web_contents->GetController().IsInitialNavigation() &&
      browser->tab_strip_model()->count() > 1 &&
      browser->tab_strip_model()->GetIndexOfWebContents(web_contents) !=
          TabStripModel::kNoTab) {
    // Defer destruction of `WebContents` to avoid synchronously destroying
    // NavigationURLLoader(Impl) here. See https://issues.chromium.org/361600654
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&content::WebContents::Close,
                                  web_contents->GetWeakPtr()));
  }
#endif
}

#if !BUILDFLAG(IS_ANDROID)
// When we are about to launch a URL with the default OS level application, we
// check if the external application will be us. If it is we just ignore the
// request.
void OnDefaultSchemeClientWorkerFinished(
    const GURL& escaped_url,
    content::WebContents::Getter web_contents_getter,
    bool prompt_user,
    ui::PageTransition page_transition,
    bool has_user_gesture,
    bool is_in_fenced_frame_tree,
    const std::optional<url::Origin>& initiating_origin,
    content::WeakDocumentPtr initiator_document,
    ExternalProtocolHandler::Delegate* delegate,
    shell_integration::DefaultWebClientState state,
    const std::u16string& program_name) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (delegate)
    delegate->FinishedProcessingCheck();

  content::WebContents* web_contents = web_contents_getter.Run();

  // The default handler is hidden if it is Chrome itself, as nothing will
  // happen if it is selected (since this is invoked by the external protocol
  // handling flow).
  bool chrome_is_default_handler = state == shell_integration::IS_DEFAULT;

  // On ChromeOS, Click to Call is integrated into the external protocol dialog.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS)
  if (web_contents && ShouldOfferClickToCallForURL(
                          web_contents->GetBrowserContext(), escaped_url)) {
    // Handle tel links by opening the Click to Call dialog. This will call back
    // into LaunchUrlWithoutSecurityCheck if the user selects a system handler.
    ClickToCallUiController::ShowDialog(
        web_contents, initiating_origin, std::move(initiator_document),
        escaped_url, chrome_is_default_handler, program_name);
    return;
  }
#endif

  if (chrome_is_default_handler) {
    if (delegate)
      delegate->BlockRequest();
    return;
  }

  // If we get here, either we are not the default or we cannot work out
  // what the default is, so we proceed.
  if (prompt_user) {
    // Never prompt the user without a web_contents.
    if (!web_contents) {
      return;
    }

    // Anchor to the outermost WebContents, for e.g. embedded <webview>s.
    web_contents = web_contents->GetOutermostWebContents();

    // Skip if the WebContents instance is not prepared to show a dialog.
    if (!web_modal::WebContentsModalDialogManager::FromWebContents(
            web_contents)) {
      LOG(ERROR) << "Skipping ExternalProtocolDialog"
                 << ", escaped_url=" << escaped_url.possibly_invalid_spec()
                 << ", initiating_origin="
                 << url_formatter::FormatOriginForSecurityDisplay(
                        initiating_origin.value_or(url::Origin()))
                 << ", web_contents?" << !!web_contents << ", browser?"
                 << (web_contents && chrome::FindBrowserWithTab(web_contents));
      base::debug::DumpWithoutCrashing();
      return;
    }

    // Ask the user if they want to allow the protocol. This will call
    // LaunchUrlWithoutSecurityCheck if the user decides to accept the
    // protocol.
    RunExternalProtocolDialogWithDelegate(
        escaped_url, web_contents, page_transition, has_user_gesture,
        is_in_fenced_frame_tree, initiating_origin,
        std::move(initiator_document), program_name, delegate);
    return;
  }

  LaunchUrlWithoutSecurityCheckWithDelegate(
      escaped_url, web_contents, std::move(initiator_document), delegate);
}
#endif  // !BUILDFLAG(IS_ANDROID)

bool IsSchemeOriginPairAllowedByPolicy(const std::string& scheme,
                                       const url::Origin* initiating_origin,
                                       PrefService* prefs) {
  if (!initiating_origin)
    return false;

  const base::Value::List& exempted_protocols =
      prefs->GetList(prefs::kAutoLaunchProtocolsFromOrigins);

  const base::Value::List* origin_patterns = nullptr;
  for (const base::Value& entry : exempted_protocols) {
    const base::Value::Dict& protocol_origins_map = entry.GetDict();
    const std::string* protocol = protocol_origins_map.FindString(
        policy::external_protocol::kProtocolNameKey);
    DCHECK(protocol);
    if (*protocol == scheme) {
      origin_patterns = protocol_origins_map.FindList(
          policy::external_protocol::kOriginListKey);
      break;
    }
  }
  if (!origin_patterns)
    return false;

  url_matcher::URLMatcher matcher;
  base::MatcherStringPattern::ID id(0);
  url_matcher::util::AddFiltersWithLimit(&matcher, true /* allowed */, &id,
                                         *origin_patterns);

  auto matching_set = matcher.MatchURL(initiating_origin->GetURL());
  return !matching_set.empty();
}

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange(LoggedScheme)
enum class LoggedScheme {
  OTHER = 0,
  SEARCH_MS = 1,
  SEARCH = 2,
  MAILTO = 3,
  MICROSOFT_EDGE = 4,
  kMaxValue = MICROSOFT_EDGE,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/permissions/enums.xml:ExternalProtocolScheme)

void LogRequestForScheme(const std::string& scheme) {
  constexpr auto kSchemeToBucket =
      base::MakeFixedFlatMap<std::string_view, LoggedScheme>(
          {{"search-ms", LoggedScheme::SEARCH_MS},
           {"search", LoggedScheme::SEARCH},
           {"mailto", LoggedScheme::MAILTO},
           {"microsoft-edge", LoggedScheme::MICROSOFT_EDGE}});
  static_assert(kSchemeToBucket.size() ==
                static_cast<size_t>(LoggedScheme::kMaxValue));
  auto iterator = kSchemeToBucket.find(scheme);
  LoggedScheme scheme_bucket = iterator != kSchemeToBucket.end()
                                   ? iterator->second
                                   : LoggedScheme::OTHER;
  base::UmaHistogramEnumeration("BrowserDialogs.ExternalProtocol.Scheme",
                                scheme_bucket);
}

}  // namespace

const char ExternalProtocolHandler::kBlockStateMetric[] =
    "BrowserDialogs.ExternalProtocol.BlockState";
const char ExternalProtocolHandler::kHandleStateMetric[] =
    "BrowserDialogs.ExternalProtocol.HandleState";

// static
void ExternalProtocolHandler::SetDelegateForTesting(Delegate* delegate) {
  g_external_protocol_handler_delegate = delegate;
}

bool ExternalProtocolHandler::MayRememberAllowDecisionsForThisOrigin(
    const url::Origin* initiating_origin) {
  return initiating_origin &&
         network::IsOriginPotentiallyTrustworthy(*initiating_origin);
}

// static.
ExternalProtocolHandler::BlockState ExternalProtocolHandler::GetBlockState(
    const std::string& scheme,
    const url::Origin* initiating_origin,
    Profile* profile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  LogRequestForScheme(scheme);

  // If we are being flooded with requests, block the request.
  if (!g_accept_requests)
    return BLOCK;

  if (scheme.length() == 1) {
    // We have a URL that looks something like:
    //   C:/WINDOWS/system32/notepad.exe
    // ShellExecuting this URL will cause the specified program to be executed.
    return BLOCK;
  }

  // Always block the hard-coded denied schemes.
  if (kDeniedSchemes.contains(scheme)) {
    base::UmaHistogramEnumeration(kBlockStateMetric,
                                  BlockStateMetric::kDeniedDefault);
    return BLOCK;
  }

  // The mailto scheme is allowed explicitly because of its ubiquity on the web
  // and because every platform provides a default handler for it.
  if (scheme == "mailto") {
    base::UmaHistogramEnumeration(kBlockStateMetric,
                                  BlockStateMetric::kAllowedDefaultMail);
    return DONT_BLOCK;
  }

  PrefService* profile_prefs = profile->GetPrefs();
  if (profile_prefs) {  // May be NULL during testing.
    if (IsSchemeOriginPairAllowedByPolicy(scheme, initiating_origin,
                                          profile_prefs)) {
      base::UmaHistogramEnumeration(
          kBlockStateMetric, BlockStateMetric::kAllowedByEnterprisePolicy);
      return DONT_BLOCK;
    }

    if (MayRememberAllowDecisionsForThisOrigin(initiating_origin)) {
      // Check if there is a matching {Origin+Protocol} pair exemption:
      const base::Value::Dict& allowed_origin_protocol_pairs =
          profile_prefs->GetDict(
              prefs::kProtocolHandlerPerOriginAllowedProtocols);
      const base::Value::Dict* allowed_protocols_for_origin =
          allowed_origin_protocol_pairs.FindDict(
              initiating_origin->Serialize());
      if (allowed_protocols_for_origin) {
        std::optional<bool> allow =
            allowed_protocols_for_origin->FindBool(scheme);
        if (allow.has_value() && allow.value()) {
          base::UmaHistogramEnumeration(kBlockStateMetric,
                                        BlockStateMetric::kAllowedByPreference);
          return DONT_BLOCK;
        }
      }
    }
  }

  base::UmaHistogramEnumeration(kBlockStateMetric, BlockStateMetric::kPrompt);
  return UNKNOWN;
}

// static
// This is only called when the "remember" check box is selected from the
// External Protocol Prompt dialog, and that check box is only shown when there
// is a non-empty, potentially-trustworthy initiating origin.
void ExternalProtocolHandler::SetBlockState(
    const std::string& scheme,
    const url::Origin& initiating_origin,
    BlockState state,
    Profile* profile) {
  // Setting the state to BLOCK is no longer supported through the UI.
  DCHECK_NE(state, BLOCK);

  // Set in the stored prefs.
  if (MayRememberAllowDecisionsForThisOrigin(&initiating_origin)) {
    PrefService* profile_prefs = profile->GetPrefs();
    if (profile_prefs) {  // May be NULL during testing.
      ScopedDictPrefUpdate update_allowed_origin_protocol_pairs(
          profile_prefs, prefs::kProtocolHandlerPerOriginAllowedProtocols);

      const std::string serialized_origin = initiating_origin.Serialize();
      base::Value::Dict* allowed_protocols_for_origin =
          update_allowed_origin_protocol_pairs->FindDict(serialized_origin);
      if (!allowed_protocols_for_origin) {
        update_allowed_origin_protocol_pairs->Set(serialized_origin,
                                                  base::Value::Dict());
        allowed_protocols_for_origin =
            update_allowed_origin_protocol_pairs->FindDict(serialized_origin);
      }
      if (state == DONT_BLOCK) {
        allowed_protocols_for_origin->Set(scheme, true);
      } else {
        allowed_protocols_for_origin->Remove(scheme);
        if (allowed_protocols_for_origin->empty()) {
          update_allowed_origin_protocol_pairs->Remove(serialized_origin);
        }
      }
    }
  }

  if (g_external_protocol_handler_delegate) {
    g_external_protocol_handler_delegate->OnSetBlockState(
        scheme, initiating_origin, state);
  }
}

// static
void ExternalProtocolHandler::LaunchUrl(
    const GURL& url,
    content::WebContents::Getter web_contents_getter,
    ui::PageTransition page_transition,
    bool has_user_gesture,
    bool is_in_fenced_frame_tree,
    const std::optional<url::Origin>& initiating_origin,
    content::WeakDocumentPtr initiator_document
#if BUILDFLAG(IS_ANDROID)
    ,
    mojo::PendingRemote<network::mojom::URLLoaderFactory>* out_factory
#endif
) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Disable anti-flood protection if the user is invoking a bookmark or
  // navigating directly using the omnibox.
  if (!g_accept_requests &&
      (PageTransitionCoreTypeIs(page_transition,
                                ui::PAGE_TRANSITION_AUTO_BOOKMARK) ||
       PageTransitionCoreTypeIs(page_transition, ui::PAGE_TRANSITION_TYPED))) {
    g_accept_requests = true;
  }

  // Escape the input scheme to be sure that the command does not
  // have parameters unexpected by the external program.
  // TODO(mgiuca): This essentially amounts to "remove illegal characters from
  // the URL", something that probably should be done by the GURL constructor
  // itself. The GURL constructor does do it in some cases (e.g., mailto) but
  // not in general. https://crbug.com/788244.
  std::string escaped_url_string = base::EscapeExternalHandlerValue(url.spec());
  GURL escaped_url(escaped_url_string);

  content::WebContents* web_contents = web_contents_getter.Run();
  Profile* profile = nullptr;
  if (web_contents)  // Maybe NULL during testing.
    profile = Profile::FromBrowserContext(web_contents->GetBrowserContext());
  BlockState block_state = GetBlockStateWithDelegate(
      escaped_url.scheme(), base::OptionalToPtr(initiating_origin),
      g_external_protocol_handler_delegate, profile);
  if (block_state == BLOCK) {
    AddMessageToConsole(
        initiator_document, blink::mojom::ConsoleMessageLevel::kError,
        "Not allowed to launch '" + url.possibly_invalid_spec() + "'" +
            (g_accept_requests ? "." : " because a user gesture is required."));

    if (g_external_protocol_handler_delegate)
      g_external_protocol_handler_delegate->BlockRequest();
    return;
  }

  g_accept_requests = false;

  // Shell integration code below doesn't work on Android - default handler
  // checks are instead handled through the InterceptNavigationDelegate. See
  // ExternalNavigationHandler.java.
  // The Origin is used for security checks, not for displaying to the user, so
  // the precursor origin should not be used.
  // Also, a protocol dialog isn't used on Android.
#if BUILDFLAG(IS_ANDROID)
  navigation_interception::InterceptNavigationDelegate* delegate =
      navigation_interception::InterceptNavigationDelegate::Get(web_contents);
  if (delegate) {
    delegate->HandleSubframeExternalProtocol(escaped_url, page_transition,
                                             has_user_gesture,
                                             initiating_origin, out_factory);
  }
  return;
#else
  std::optional<url::Origin> initiating_origin_or_precursor;
  if (initiating_origin) {
    // Transform the initiating origin to its precursor origin if it is
    // opaque. |initiating_origin| is shown in the UI to attribute the external
    // protocol request to a particular site, and showing an opaque origin isn't
    // useful.
    if (initiating_origin->opaque()) {
      initiating_origin_or_precursor = url::Origin::Create(
          initiating_origin->GetTupleOrPrecursorTupleIfOpaque().GetURL());
    } else {
      initiating_origin_or_precursor = initiating_origin;
    }
  }

  // The worker creates tasks with references to itself and puts them into
  // message loops.
  shell_integration::DefaultSchemeHandlerWorkerCallback callback =
      base::BindOnce(&OnDefaultSchemeClientWorkerFinished, escaped_url,
                     std::move(web_contents_getter), block_state == UNKNOWN,
                     page_transition, has_user_gesture, is_in_fenced_frame_tree,
                     initiating_origin_or_precursor,
                     std::move(initiator_document),
                     g_external_protocol_handler_delegate);

  // Start the check process running. This will send tasks to a worker task
  // runner and when the answer is known will send the result back to
  // OnDefaultSchemeClientWorkerFinished().
  CreateShellWorker(escaped_url, g_external_protocol_handler_delegate)
      ->StartCheckIsDefaultAndGetDefaultClientName(std::move(callback));
#endif
}

// static
void ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck(
    const GURL& url,
    content::WebContents* web_contents,
    content::WeakDocumentPtr initiator_document) {
  // Escape the input scheme to be sure that the command does not
  // have parameters unexpected by the external program. The url passed in the
  // |url| parameter might already be escaped but the EscapeExternalHandlerValue
  // is idempotent so it is safe to apply it again.
  // TODO(crbug.com/40551459): This essentially amounts to "remove illegal
  // characters from the URL", something that probably should be done by the
  // GURL constructor itself.
  std::string escaped_url_string = base::EscapeExternalHandlerValue(url.spec());
  GURL escaped_url(escaped_url_string);

  LaunchUrlWithoutSecurityCheckWithDelegate(
      escaped_url, web_contents, std::move(initiator_document),
      g_external_protocol_handler_delegate);
}

// static
void ExternalProtocolHandler::PermitLaunchUrl() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  g_accept_requests = true;
}

// static
void ExternalProtocolHandler::RecordHandleStateMetrics(bool checkbox_selected,
                                                       BlockState block_state) {
  HandleState handle_state = DONT_LAUNCH;
  switch (block_state) {
    case DONT_BLOCK:
      handle_state = checkbox_selected ? CHECKED_LAUNCH : LAUNCH;
      break;
    case BLOCK:
      handle_state =
          checkbox_selected ? CHECKED_DONT_LAUNCH_DEPRECATED : DONT_LAUNCH;
      break;
    case UNKNOWN:
      NOTREACHED();
  }
  DCHECK_NE(CHECKED_DONT_LAUNCH_DEPRECATED, handle_state);
  UMA_HISTOGRAM_ENUMERATION(kHandleStateMetric, handle_state,
                            HANDLE_STATE_LAST);
}

// static
void ExternalProtocolHandler::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(
      prefs::kProtocolHandlerPerOriginAllowedProtocols);

  registry->RegisterListPref(prefs::kAutoLaunchProtocolsFromOrigins);
}

// static
void ExternalProtocolHandler::ClearData(Profile* profile) {
  PrefService* prefs = profile->GetPrefs();
  prefs->ClearPref(prefs::kProtocolHandlerPerOriginAllowedProtocols);
}