1 // Copyright 2018 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 "chrome/browser/apps/intent_helper/apps_navigation_throttle.h"
6 
7 #include <algorithm>
8 #include <utility>
9 
10 #include "base/bind.h"
11 #include "base/optional.h"
12 #include "chrome/browser/apps/app_service/app_launch_params.h"
13 #include "chrome/browser/apps/app_service/app_service_proxy.h"
14 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
15 #include "chrome/browser/apps/app_service/browser_app_launcher.h"
16 #include "chrome/browser/apps/intent_helper/intent_picker_auto_display_service.h"
17 #include "chrome/browser/apps/intent_helper/page_transition_util.h"
18 #include "chrome/browser/prefetch/no_state_prefetch/chrome_prerender_contents_delegate.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/ui/browser.h"
21 #include "chrome/browser/ui/browser_finder.h"
22 #include "chrome/browser/ui/browser_window.h"
23 #include "chrome/browser/ui/intent_picker_tab_helper.h"
24 #include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
25 #include "chrome/browser/web_applications/components/app_icon_manager.h"
26 #include "chrome/browser/web_applications/components/app_registrar.h"
27 #include "chrome/browser/web_applications/components/web_app_helpers.h"
28 #include "chrome/browser/web_applications/components/web_app_provider_base.h"
29 #include "chrome/browser/web_applications/components/web_app_tab_helper_base.h"
30 #include "chrome/common/chrome_features.h"
31 #include "components/no_state_prefetch/browser/prerender_contents.h"
32 #include "components/page_load_metrics/browser/page_load_metrics_util.h"
33 #include "content/public/browser/browser_context.h"
34 #include "content/public/browser/browser_thread.h"
35 #include "content/public/browser/navigation_handle.h"
36 #include "content/public/browser/site_instance.h"
37 #include "content/public/browser/web_contents.h"
38 #include "extensions/common/constants.h"
39 #include "third_party/blink/public/mojom/loader/referrer.mojom.h"
40 #include "ui/gfx/image/image.h"
41 #include "url/origin.h"
42 
43 namespace {
44 
45 using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
46 
47 // Returns true if |url| is a known and valid redirector that will redirect a
48 // navigation elsewhere.
IsGoogleRedirectorUrl(const GURL & url)49 bool IsGoogleRedirectorUrl(const GURL& url) {
50   // This currently only check for redirectors on the "google" domain.
51   if (!page_load_metrics::IsGoogleSearchHostname(url))
52     return false;
53 
54   return url.path_piece() == "/url" && url.has_query();
55 }
56 
57 // Compares the host name of the referrer and target URL to decide whether
58 // the navigation needs to be overridden.
ShouldOverrideUrlLoading(const GURL & previous_url,const GURL & current_url)59 bool ShouldOverrideUrlLoading(const GURL& previous_url,
60                               const GURL& current_url) {
61   // When the navigation is initiated in a web page where sending a referrer
62   // is disabled, |previous_url| can be empty. In this case, we should open
63   // it in the desktop browser.
64   if (!previous_url.is_valid() || previous_url.is_empty())
65     return false;
66 
67   // Also check |current_url| just in case.
68   if (!current_url.is_valid() || current_url.is_empty()) {
69     DVLOG(1) << "Unexpected URL: " << current_url << ", opening it in Chrome.";
70     return false;
71   }
72 
73   // Check the scheme for both |previous_url| and |current_url| since an
74   // extension could have referred us (e.g. Google Docs).
75   if (!current_url.SchemeIsHTTPOrHTTPS() ||
76       previous_url.SchemeIs(extensions::kExtensionScheme)) {
77     return false;
78   }
79 
80   // Skip URL redirectors that are intermediate pages redirecting towards a
81   // final URL.
82   if (IsGoogleRedirectorUrl(current_url))
83     return false;
84 
85   return true;
86 }
87 
GetStartingGURL(content::NavigationHandle * navigation_handle)88 GURL GetStartingGURL(content::NavigationHandle* navigation_handle) {
89   // This helps us determine a reference GURL for the current NavigationHandle.
90   // This is the order or preference: Referrer > LastCommittedURL >
91   // InitiatorOrigin. InitiatorOrigin *should* only be used on very rare cases,
92   // e.g. when the navigation goes from https: to http: on a new tab, thus
93   // losing the other potential referrers.
94   const GURL referrer_url = navigation_handle->GetReferrer().url;
95   if (referrer_url.is_valid() && !referrer_url.is_empty())
96     return referrer_url;
97 
98   const GURL last_committed_url =
99       navigation_handle->GetWebContents()->GetLastCommittedURL();
100   if (last_committed_url.is_valid() && !last_committed_url.is_empty())
101     return last_committed_url;
102 
103   const auto& initiator_origin = navigation_handle->GetInitiatorOrigin();
104   return initiator_origin.has_value() ? initiator_origin->GetURL() : GURL();
105 }
106 
InAppBrowser(content::WebContents * web_contents)107 bool InAppBrowser(content::WebContents* web_contents) {
108   Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
109   return !browser || browser->deprecated_is_app();
110 }
111 
112 }  // namespace
113 
114 namespace apps {
115 
116 // static
117 const char AppsNavigationThrottle::kUseBrowserForLink[] = "use_browser";
118 
119 // static
120 std::unique_ptr<content::NavigationThrottle>
MaybeCreate(content::NavigationHandle * handle)121 AppsNavigationThrottle::MaybeCreate(content::NavigationHandle* handle) {
122   if (!handle->IsInMainFrame())
123     return nullptr;
124 
125   content::WebContents* web_contents = handle->GetWebContents();
126   if (!CanCreate(web_contents))
127     return nullptr;
128 
129   return std::make_unique<AppsNavigationThrottle>(handle);
130 }
131 
132 // static
ShowIntentPickerBubble(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url)133 void AppsNavigationThrottle::ShowIntentPickerBubble(
134     content::WebContents* web_contents,
135     IntentPickerAutoDisplayService* ui_auto_display_service,
136     const GURL& url) {
137   std::vector<IntentPickerAppInfo> apps = FindPwaForUrl(web_contents, url, {});
138 
139   bool show_persistence_options = ShouldShowPersistenceOptions(apps);
140   ShowIntentPickerBubbleForApps(
141       web_contents, std::move(apps),
142       /*show_stay_in_chrome=*/show_persistence_options,
143       /*show_remember_selection=*/show_persistence_options,
144       base::BindOnce(&OnIntentPickerClosed, web_contents,
145                      ui_auto_display_service, url));
146 }
147 
148 // static
OnIntentPickerClosed(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url,const std::string & launch_name,PickerEntryType entry_type,IntentPickerCloseReason close_reason,bool should_persist)149 void AppsNavigationThrottle::OnIntentPickerClosed(
150     content::WebContents* web_contents,
151     IntentPickerAutoDisplayService* ui_auto_display_service,
152     const GURL& url,
153     const std::string& launch_name,
154     PickerEntryType entry_type,
155     IntentPickerCloseReason close_reason,
156     bool should_persist) {
157   const bool should_launch_app =
158       close_reason == IntentPickerCloseReason::OPEN_APP;
159   switch (entry_type) {
160     case PickerEntryType::kWeb:
161       if (should_launch_app)
162         web_app::ReparentWebContentsIntoAppBrowser(web_contents, launch_name);
163       break;
164     case PickerEntryType::kUnknown:
165       // We reach here if the picker was closed without an app being chosen,
166       // e.g. due to the tab being closed. Keep count of this scenario so we can
167       // stop the UI from showing after 2+ dismissals.
168       if (close_reason == IntentPickerCloseReason::DIALOG_DEACTIVATED) {
169         if (ui_auto_display_service)
170           ui_auto_display_service->IncrementCounter(url);
171       }
172       break;
173     case PickerEntryType::kArc:
174     case PickerEntryType::kDevice:
175     case PickerEntryType::kMacOs:
176       NOTREACHED();
177   }
178 }
179 
180 // static
IsGoogleRedirectorUrlForTesting(const GURL & url)181 bool AppsNavigationThrottle::IsGoogleRedirectorUrlForTesting(const GURL& url) {
182   return IsGoogleRedirectorUrl(url);
183 }
184 
185 // static
ShouldOverrideUrlLoadingForTesting(const GURL & previous_url,const GURL & current_url)186 bool AppsNavigationThrottle::ShouldOverrideUrlLoadingForTesting(
187     const GURL& previous_url,
188     const GURL& current_url) {
189   return ShouldOverrideUrlLoading(previous_url, current_url);
190 }
191 
192 // static
ShowIntentPickerBubbleForApps(content::WebContents * web_contents,std::vector<IntentPickerAppInfo> apps,bool show_stay_in_chrome,bool show_remember_selection,IntentPickerResponse callback)193 void AppsNavigationThrottle::ShowIntentPickerBubbleForApps(
194     content::WebContents* web_contents,
195     std::vector<IntentPickerAppInfo> apps,
196     bool show_stay_in_chrome,
197     bool show_remember_selection,
198     IntentPickerResponse callback) {
199   if (apps.empty())
200     return;
201 
202   // It should be safe to bind |web_contents| since closing the current tab will
203   // close the intent picker and run the callback prior to the WebContents being
204   // deallocated.
205   Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
206   if (!browser)
207     return;
208 
209   IntentPickerTabHelper::SetShouldShowIcon(web_contents, true);
210   browser->window()->ShowIntentPickerBubble(
211       std::move(apps), show_stay_in_chrome, show_remember_selection,
212       PageActionIconType::kIntentPicker, base::nullopt, std::move(callback));
213 }
214 
AppsNavigationThrottle(content::NavigationHandle * navigation_handle)215 AppsNavigationThrottle::AppsNavigationThrottle(
216     content::NavigationHandle* navigation_handle)
217     : content::NavigationThrottle(navigation_handle),
218       ui_displayed_(false),
219       ui_auto_display_service_(
220           IntentPickerAutoDisplayService::Get(Profile::FromBrowserContext(
221               navigation_handle->GetWebContents()->GetBrowserContext()))),
222       navigate_from_link_(false) {
223   // |ui_auto_display_service_| can be null iff the call is coming from
224   // IntentPickerView. Since the pointer to our service is never modified
225   // (in case it is successfully created here) this check covers all the
226   // non-static methods in this class.
227   DCHECK(ui_auto_display_service_);
228 }
229 
230 AppsNavigationThrottle::~AppsNavigationThrottle() = default;
231 
GetNameForLogging()232 const char* AppsNavigationThrottle::GetNameForLogging() {
233   return "AppsNavigationThrottle";
234 }
235 
WillStartRequest()236 ThrottleCheckResult AppsNavigationThrottle::WillStartRequest() {
237   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
238   starting_url_ = GetStartingGURL(navigation_handle());
239   IntentPickerTabHelper::SetShouldShowIcon(
240       navigation_handle()->GetWebContents(), false);
241   return HandleRequest();
242 }
243 
WillRedirectRequest()244 ThrottleCheckResult AppsNavigationThrottle::WillRedirectRequest() {
245   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
246 
247   // TODO(dominickn): Consider what to do when there is another URL during the
248   // same navigation that could be handled by apps. Two ideas are:
249   //  1) update the bubble with a mix of both app candidates (if different)
250   //  2) show a bubble based on the last url, thus closing all the previous ones
251   if (ui_displayed_)
252     return content::NavigationThrottle::PROCEED;
253 
254   return HandleRequest();
255 }
256 
257 // static
CanCreate(content::WebContents * web_contents)258 bool AppsNavigationThrottle::CanCreate(content::WebContents* web_contents) {
259   // Do not create the throttle if no apps can be installed.
260   // Do not create the throttle in incognito or for a prerender navigation.
261   if (web_contents->GetBrowserContext()->IsOffTheRecord() ||
262       prerender::ChromePrerenderContentsDelegate::FromWebContents(
263           web_contents) != nullptr) {
264     return false;
265   }
266 
267   // Do not create the throttle if we are already in an app browser.
268   // It is possible that the web contents is not inserted to tab strip
269   // model at this stage (e.g. open url in new tab). So if we cannot
270   // find a browser at this moment, skip the check and this will be handled
271   // in later stage.
272   Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
273   if (browser && browser->deprecated_is_app())
274     return false;
275 
276   return true;
277 }
278 
279 // static
GetDestinationPlatform(const std::string & selected_launch_name,PickerAction picker_action)280 AppsNavigationThrottle::Platform AppsNavigationThrottle::GetDestinationPlatform(
281     const std::string& selected_launch_name,
282     PickerAction picker_action) {
283   switch (picker_action) {
284     case PickerAction::ARC_APP_PRESSED:
285     case PickerAction::ARC_APP_PREFERRED_PRESSED:
286     case PickerAction::PREFERRED_ARC_ACTIVITY_FOUND:
287       return Platform::ARC;
288     case PickerAction::PWA_APP_PRESSED:
289     case PickerAction::PWA_APP_PREFERRED_PRESSED:
290     case PickerAction::PREFERRED_PWA_FOUND:
291       return Platform::PWA;
292     case PickerAction::MAC_OS_APP_PRESSED:
293       return Platform::MAC_OS;
294     case PickerAction::ERROR_BEFORE_PICKER:
295     case PickerAction::ERROR_AFTER_PICKER:
296     case PickerAction::DIALOG_DEACTIVATED:
297     case PickerAction::CHROME_PRESSED:
298     case PickerAction::CHROME_PREFERRED_PRESSED:
299     case PickerAction::PREFERRED_CHROME_BROWSER_FOUND:
300       return Platform::CHROME;
301     case PickerAction::DEVICE_PRESSED:
302       return Platform::DEVICE;
303     case PickerAction::OBSOLETE_ALWAYS_PRESSED:
304     case PickerAction::OBSOLETE_JUST_ONCE_PRESSED:
305     case PickerAction::INVALID:
306       break;
307   }
308   NOTREACHED();
309   return Platform::ARC;
310 }
311 
312 // static
GetPickerAction(PickerEntryType entry_type,IntentPickerCloseReason close_reason,bool should_persist)313 AppsNavigationThrottle::PickerAction AppsNavigationThrottle::GetPickerAction(
314     PickerEntryType entry_type,
315     IntentPickerCloseReason close_reason,
316     bool should_persist) {
317   switch (close_reason) {
318     case IntentPickerCloseReason::ERROR_BEFORE_PICKER:
319       return PickerAction::ERROR_BEFORE_PICKER;
320     case IntentPickerCloseReason::ERROR_AFTER_PICKER:
321       return PickerAction::ERROR_AFTER_PICKER;
322     case IntentPickerCloseReason::DIALOG_DEACTIVATED:
323       return PickerAction::DIALOG_DEACTIVATED;
324     case IntentPickerCloseReason::PREFERRED_APP_FOUND:
325       switch (entry_type) {
326         case PickerEntryType::kUnknown:
327           return PickerAction::PREFERRED_CHROME_BROWSER_FOUND;
328         case PickerEntryType::kArc:
329           return PickerAction::PREFERRED_ARC_ACTIVITY_FOUND;
330         case PickerEntryType::kWeb:
331           return PickerAction::PREFERRED_PWA_FOUND;
332         case PickerEntryType::kDevice:
333         case PickerEntryType::kMacOs:
334           NOTREACHED();
335           return PickerAction::INVALID;
336       }
337     case IntentPickerCloseReason::STAY_IN_CHROME:
338       return should_persist ? PickerAction::CHROME_PREFERRED_PRESSED
339                             : PickerAction::CHROME_PRESSED;
340     case IntentPickerCloseReason::OPEN_APP:
341       switch (entry_type) {
342         case PickerEntryType::kUnknown:
343           NOTREACHED();
344           return PickerAction::INVALID;
345         case PickerEntryType::kArc:
346           return should_persist ? PickerAction::ARC_APP_PREFERRED_PRESSED
347                                 : PickerAction::ARC_APP_PRESSED;
348         case PickerEntryType::kWeb:
349           return should_persist ? PickerAction::PWA_APP_PREFERRED_PRESSED
350                                 : PickerAction::PWA_APP_PRESSED;
351         case PickerEntryType::kDevice:
352           return PickerAction::DEVICE_PRESSED;
353         case PickerEntryType::kMacOs:
354           return PickerAction::MAC_OS_APP_PRESSED;
355       }
356   }
357 
358   NOTREACHED();
359   return PickerAction::INVALID;
360 }
361 
FindAppsForUrl(content::WebContents * web_contents,const GURL & url,std::vector<IntentPickerAppInfo> apps)362 std::vector<IntentPickerAppInfo> AppsNavigationThrottle::FindAppsForUrl(
363     content::WebContents* web_contents,
364     const GURL& url,
365     std::vector<IntentPickerAppInfo> apps) {
366   return FindPwaForUrl(web_contents, url, std::move(apps));
367 }
368 
369 // static
FindPwaForUrl(content::WebContents * web_contents,const GURL & url,std::vector<IntentPickerAppInfo> apps)370 std::vector<IntentPickerAppInfo> AppsNavigationThrottle::FindPwaForUrl(
371     content::WebContents* web_contents,
372     const GURL& url,
373     std::vector<IntentPickerAppInfo> apps) {
374   // Check if the current URL has an installed desktop PWA, and add that to
375   // the list of apps if it exists.
376   Profile* const profile =
377       Profile::FromBrowserContext(web_contents->GetBrowserContext());
378 
379   base::Optional<web_app::AppId> app_id =
380       web_app::FindInstalledAppWithUrlInScope(profile, url,
381                                               /*window_only=*/true);
382   if (!app_id)
383     return apps;
384 
385   auto* const provider = web_app::WebAppProviderBase::GetProviderBase(profile);
386   gfx::Image icon = gfx::Image::CreateFrom1xBitmap(
387       provider->icon_manager().GetFavicon(*app_id));
388 
389   // Prefer the web and place apps of type PWA before apps of type ARC.
390   // TODO(crbug.com/824598): deterministically sort this list.
391   apps.emplace(apps.begin(), PickerEntryType::kWeb, icon, *app_id,
392                provider->registrar().GetAppShortName(*app_id));
393 
394   return apps;
395 }
396 
397 // static
CloseOrGoBack(content::WebContents * web_contents)398 void AppsNavigationThrottle::CloseOrGoBack(content::WebContents* web_contents) {
399   DCHECK(web_contents);
400   if (web_contents->GetController().CanGoBack())
401     web_contents->GetController().GoBack();
402   else
403     web_contents->ClosePage();
404 }
405 
406 // static
ContainsOnlyPwasAndMacApps(const std::vector<apps::IntentPickerAppInfo> & apps)407 bool AppsNavigationThrottle::ContainsOnlyPwasAndMacApps(
408     const std::vector<apps::IntentPickerAppInfo>& apps) {
409   return std::all_of(apps.begin(), apps.end(),
410                      [](const apps::IntentPickerAppInfo& app_info) {
411                        return app_info.type == PickerEntryType::kWeb ||
412                               app_info.type == PickerEntryType::kMacOs;
413                      });
414 }
415 
416 // static
ShouldShowPersistenceOptions(std::vector<apps::IntentPickerAppInfo> & apps)417 bool AppsNavigationThrottle::ShouldShowPersistenceOptions(
418     std::vector<apps::IntentPickerAppInfo>& apps) {
419   // There is no support persistence for PWA so the selection should be hidden
420   // if only PWAs are present.
421   // TODO(crbug.com/826982): Provide the "Remember my choice" option when the
422   // app registry can support persistence for PWAs.
423   // This function is also used to hide the "Stay In Chrome" button when the
424   // "Remember my choice" option is hidden such that the bubble is easy to
425   // understand.
426   // TODO(avi): When Chrome gains a UI for managing the persistence of PWAs,
427   // reuse that UI for managing the persistent behavior of Universal Links.
428   return !ContainsOnlyPwasAndMacApps(apps);
429 }
430 
ShouldCancelNavigation(content::NavigationHandle * handle)431 bool AppsNavigationThrottle::ShouldCancelNavigation(
432     content::NavigationHandle* handle) {
433   return false;
434 }
435 
ShouldDeferNavigation(content::NavigationHandle * handle)436 bool AppsNavigationThrottle::ShouldDeferNavigation(
437     content::NavigationHandle* handle) {
438   return false;
439 }
440 
ShowIntentPickerForApps(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url,std::vector<IntentPickerAppInfo> apps,IntentPickerResponse callback)441 void AppsNavigationThrottle::ShowIntentPickerForApps(
442     content::WebContents* web_contents,
443     IntentPickerAutoDisplayService* ui_auto_display_service,
444     const GURL& url,
445     std::vector<IntentPickerAppInfo> apps,
446     IntentPickerResponse callback) {
447   if (apps.empty()) {
448     IntentPickerTabHelper::SetShouldShowIcon(web_contents, false);
449     ui_displayed_ = false;
450     return;
451   }
452   IntentPickerTabHelper::SetShouldShowIcon(web_contents, true);
453   Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
454   if (!browser)
455     return;
456   const PickerShowState picker_show_state =
457       GetPickerShowState(apps, web_contents, url);
458   switch (picker_show_state) {
459     case PickerShowState::kOmnibox:
460       ui_displayed_ = false;
461       break;
462     case PickerShowState::kPopOut: {
463       bool show_persistence_options = ShouldShowPersistenceOptions(apps);
464       ShowIntentPickerBubbleForApps(
465           web_contents, std::move(apps),
466           /*show_stay_in_chrome=*/show_persistence_options,
467           /*show_remember_selection=*/show_persistence_options,
468           std::move(callback));
469       break;
470     }
471     default:
472       NOTREACHED();
473   }
474 }
475 
476 AppsNavigationThrottle::PickerShowState
GetPickerShowState(const std::vector<IntentPickerAppInfo> & apps_for_picker,content::WebContents * web_contents,const GURL & url)477 AppsNavigationThrottle::GetPickerShowState(
478     const std::vector<IntentPickerAppInfo>& apps_for_picker,
479     content::WebContents* web_contents,
480     const GURL& url) {
481   return PickerShowState::kOmnibox;
482 }
483 
GetOnPickerClosedCallback(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url)484 IntentPickerResponse AppsNavigationThrottle::GetOnPickerClosedCallback(
485     content::WebContents* web_contents,
486     IntentPickerAutoDisplayService* ui_auto_display_service,
487     const GURL& url) {
488   return base::BindOnce(&OnIntentPickerClosed, web_contents,
489                         ui_auto_display_service, url);
490 }
491 
navigate_from_link() const492 bool AppsNavigationThrottle::navigate_from_link() const {
493   return navigate_from_link_;
494 }
495 
ShouldAutoDisplayUi(const std::vector<apps::IntentPickerAppInfo> & apps_for_picker,content::WebContents * web_contents,const GURL & url)496 bool AppsNavigationThrottle::ShouldAutoDisplayUi(
497     const std::vector<apps::IntentPickerAppInfo>& apps_for_picker,
498     content::WebContents* web_contents,
499     const GURL& url) {
500   if (apps_for_picker.empty())
501     return false;
502 
503   if (InAppBrowser(web_contents))
504     return false;
505 
506   if (!ShouldOverrideUrlLoading(starting_url_, url))
507     return false;
508 
509   DCHECK(ui_auto_display_service_);
510   return ui_auto_display_service_->ShouldAutoDisplayUi(url);
511 }
512 
HandleRequest()513 ThrottleCheckResult AppsNavigationThrottle::HandleRequest() {
514   content::NavigationHandle* handle = navigation_handle();
515   // If the navigation won't update the current document, don't check intent for
516   // the navigation.
517   if (handle->IsSameDocument())
518     return content::NavigationThrottle::PROCEED;
519 
520   DCHECK(!ui_displayed_);
521 
522   navigate_from_link_ = false;
523 
524   // Always handle http(s) <form> submissions in Chrome for two reasons: 1) we
525   // don't have a way to send POST data to ARC, and 2) intercepting http(s) form
526   // submissions is not very important because such submissions are usually
527   // done within the same domain. ShouldOverrideUrlLoading() below filters out
528   // such submissions anyway.
529   constexpr bool kAllowFormSubmit = false;
530 
531   // Ignore navigations with the CLIENT_REDIRECT qualifier on.
532   constexpr bool kAllowClientRedirect = true;
533 
534   ui::PageTransition page_transition = handle->GetPageTransition();
535   content::WebContents* web_contents = handle->GetWebContents();
536   const GURL& url = handle->GetURL();
537   if (!ShouldIgnoreNavigation(page_transition, kAllowFormSubmit,
538                               kAllowClientRedirect) &&
539       !handle->WasStartedFromContextMenu()) {
540     navigate_from_link_ = true;
541   }
542 
543   MaybeRemoveComingFromArcFlag(web_contents, starting_url_, url);
544 
545   base::Optional<ThrottleCheckResult> tab_strip_capture =
546       CaptureExperimentalTabStripWebAppScopeNavigations(web_contents, handle);
547   if (tab_strip_capture.has_value())
548     return tab_strip_capture.value();
549 
550   // Do not pop up the intent picker bubble or automatically launch the app if
551   // we shouldn't override url loading, or if we don't have a browser, or we are
552   // already in an app browser.
553   if (ShouldOverrideUrlLoading(starting_url_, url) &&
554       !InAppBrowser(web_contents)) {
555     // Handles apps that are automatically launched and the navigation needs to
556     // be cancelled. This only applies on the new intent picker system, because
557     // we don't need to defer the navigation to find out preferred app anymore.
558     if (ShouldCancelNavigation(handle)) {
559       return content::NavigationThrottle::CANCEL_AND_IGNORE;
560     }
561 
562     if (ShouldDeferNavigation(handle)) {
563       // Handling is now deferred to ArcIntentPickerAppFetcher, which
564       // asynchronously queries ARC for apps, and runs
565       // OnDeferredNavigationProcessed() with an action based on whether an
566       // acceptable app was found and user consent to open received. We assume
567       // the UI is shown or a preferred app was found; reset to false if we
568       // resume the navigation.
569       ui_displayed_ = true;
570       return content::NavigationThrottle::DEFER;
571     }
572   }
573 
574   // We didn't query ARC, so proceed with the navigation and query if we have an
575   // installed desktop PWA to handle the URL.
576   std::vector<IntentPickerAppInfo> apps = FindAppsForUrl(web_contents, url, {});
577 
578   if (!apps.empty())
579     ui_displayed_ = true;
580 
581   ShowIntentPickerForApps(
582       web_contents, ui_auto_display_service_, url, std::move(apps),
583       GetOnPickerClosedCallback(web_contents, ui_auto_display_service_, url));
584 
585   return content::NavigationThrottle::PROCEED;
586 }
587 
588 base::Optional<ThrottleCheckResult>
CaptureExperimentalTabStripWebAppScopeNavigations(content::WebContents * web_contents,content::NavigationHandle * handle) const589 AppsNavigationThrottle::CaptureExperimentalTabStripWebAppScopeNavigations(
590     content::WebContents* web_contents,
591     content::NavigationHandle* handle) const {
592   if (!navigate_from_link())
593     return base::nullopt;
594 
595   if (!base::FeatureList::IsEnabled(features::kDesktopPWAsTabStrip) ||
596       !base::FeatureList::IsEnabled(
597           features::kDesktopPWAsTabStripLinkCapturing)) {
598     return base::nullopt;
599   }
600 
601   Profile* const profile =
602       Profile::FromBrowserContext(web_contents->GetBrowserContext());
603   web_app::WebAppProviderBase* provider =
604       web_app::WebAppProviderBase::GetProviderBase(profile);
605   if (!provider)
606     return base::nullopt;
607 
608   base::Optional<web_app::AppId> app_id =
609       provider->registrar().FindInstalledAppWithUrlInScope(
610           handle->GetURL(), /*window_only=*/true);
611   if (!app_id)
612     return base::nullopt;
613 
614   if (!provider->registrar().IsInExperimentalTabbedWindowMode(*app_id))
615     return base::nullopt;
616 
617   Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
618   if (web_app::AppBrowserController::IsForWebAppBrowser(browser, *app_id)) {
619     // Already in the app window; navigation already captured.
620     return base::nullopt;
621   }
622 
623   // If |web_contents| hasn't loaded yet or has only loaded about:blank we
624   // should reparent it into the app window to avoid leaving behind a blank tab.
625   auto* tab_helper =
626       web_app::WebAppTabHelperBase::FromWebContents(web_contents);
627   if (tab_helper && !tab_helper->HasLoadedNonAboutBlankPage()) {
628     web_app::ReparentWebContentsIntoAppBrowser(web_contents, *app_id);
629     return content::NavigationThrottle::PROCEED;
630   }
631 
632   apps::AppLaunchParams launch_params(
633       *app_id, apps::mojom::LaunchContainer::kLaunchContainerWindow,
634       WindowOpenDisposition::CURRENT_TAB,
635       apps::mojom::AppLaunchSource::kSourceUrlHandler);
636   launch_params.override_url = handle->GetURL();
637   apps::AppServiceProxyFactory::GetForProfile(profile)
638       ->BrowserAppLauncher()
639       ->LaunchAppWithParams(std::move(launch_params));
640   return content::NavigationThrottle::CANCEL_AND_IGNORE;
641 }
642 
643 }  // namespace apps
644