| // Copyright 2019 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. |
| |
| #import "ios/chrome/browser/url_loading/url_loading_browser_agent.h" |
| |
| #include "base/compiler_specific.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/thread_pool.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #include "ios/chrome/browser/chrome_url_constants.h" |
| #include "ios/chrome/browser/crash_report/crash_reporter_url_observer.h" |
| #import "ios/chrome/browser/main/browser.h" |
| #import "ios/chrome/browser/prerender/prerender_service.h" |
| #import "ios/chrome/browser/prerender/prerender_service_factory.h" |
| #import "ios/chrome/browser/ui/commands/open_new_tab_command.h" |
| #import "ios/chrome/browser/ui/incognito_reauth/incognito_reauth_scene_agent.h" |
| #import "ios/chrome/browser/ui/main/scene_state_browser_agent.h" |
| #import "ios/chrome/browser/ui/ntp/ntp_util.h" |
| #include "ios/chrome/browser/ui/ui_feature_flags.h" |
| #import "ios/chrome/browser/url_loading/scene_url_loading_service.h" |
| #import "ios/chrome/browser/url_loading/url_loading_notifier_browser_agent.h" |
| #import "ios/chrome/browser/url_loading/url_loading_params.h" |
| #import "ios/chrome/browser/url_loading/url_loading_util.h" |
| #import "ios/chrome/browser/web/load_timing_tab_helper.h" |
| #import "ios/chrome/browser/web_state_list/tab_insertion_browser_agent.h" |
| #import "ios/chrome/browser/web_state_list/web_state_list.h" |
| #include "net/base/url_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| BROWSER_USER_DATA_KEY_IMPL(UrlLoadingBrowserAgent) |
| |
| namespace { |
| |
| // Rapidly starts leaking memory by 10MB blocks. |
| void StartLeakingMemory() { |
| static NSMutableArray* memory = nil; |
| if (!memory) |
| memory = [[NSMutableArray alloc] init]; |
| |
| // Store block of memory into NSArray to ensure that compiler does not throw |
| // away unused code. |
| NSUInteger leak_size = 10 * 1024 * 1024; |
| int* leak = new int[leak_size]; |
| [memory addObject:[NSData dataWithBytes:leak length:leak_size]]; |
| |
| base::ThreadPool::PostTask(FROM_HERE, base::BindOnce(&StartLeakingMemory)); |
| } |
| |
| // Helper method for inducing intentional freezes, leaks and crashes, in a |
| // separate function so it will show up in stack traces. If a delay parameter is |
| // present, the main thread will be frozen for that number of seconds. If a |
| // crash parameter is "true" (which is the default value), the browser will |
| // crash after this delay. If a crash parameter is "later", the browser will |
| // crash in another thread (nsexception only). Any other value will not |
| // trigger a crash. |
| NOINLINE void InduceBrowserCrash(const GURL& url) { |
| std::string delay_string; |
| if (net::GetValueForKeyInQuery(url, "delay", &delay_string)) { |
| int delay = 0; |
| if (base::StringToInt(delay_string, &delay) && delay > 0) { |
| sleep(delay); |
| } |
| } |
| |
| #if !TARGET_IPHONE_SIMULATOR // Leaking memory does not cause UTE on simulator. |
| std::string leak_string; |
| if (net::GetValueForKeyInQuery(url, "leak", &leak_string) && |
| (leak_string == "" || leak_string == "true")) { |
| StartLeakingMemory(); |
| return; |
| } |
| #endif |
| |
| std::string exception; |
| if (net::GetValueForKeyInQuery(url, "nsexception", &exception) && |
| (exception == "" || exception == "true")) { |
| NSArray* empty_array = @[]; |
| [empty_array objectAtIndex:42]; |
| return; |
| } |
| |
| if (net::GetValueForKeyInQuery(url, "nsexception", &exception) && |
| exception == "later") { |
| dispatch_async( |
| dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
| NSArray* empty_array = @[]; |
| [empty_array objectAtIndex:42]; |
| }); |
| return; |
| } |
| |
| std::string crash_string; |
| if (!net::GetValueForKeyInQuery(url, "crash", &crash_string) || |
| (crash_string == "" || crash_string == "true")) { |
| // Induce an intentional crash in the browser process. |
| CHECK(false) << "User triggered inducebrowsercrashforrealz."; |
| // Call another function, so that the above CHECK can't be tail call |
| // optimized. This ensures that this method's name will show up in the stack |
| // for easier identification. |
| CHECK(true); |
| } |
| } |
| } // namespace |
| |
| UrlLoadingBrowserAgent::UrlLoadingBrowserAgent(Browser* browser) |
| : browser_(browser), |
| notifier_(UrlLoadingNotifierBrowserAgent::FromBrowser(browser_)) { |
| DCHECK(notifier_); |
| } |
| |
| UrlLoadingBrowserAgent::~UrlLoadingBrowserAgent() {} |
| |
| void UrlLoadingBrowserAgent::SetSceneService( |
| SceneUrlLoadingService* scene_service) { |
| scene_service_ = scene_service; |
| } |
| |
| void UrlLoadingBrowserAgent::SetDelegate(id<URLLoadingDelegate> delegate) { |
| delegate_ = delegate; |
| } |
| |
| void UrlLoadingBrowserAgent::SetIncognitoLoader( |
| UrlLoadingBrowserAgent* loader) { |
| incognito_loader_ = loader; |
| } |
| |
| void UrlLoadingBrowserAgent::Load(const UrlLoadParams& params) { |
| // Apply any override load strategy and dispatch. |
| switch (params.load_strategy) { |
| case UrlLoadStrategy::ALWAYS_NEW_FOREGROUND_TAB: { |
| UrlLoadParams fixed_params = params; |
| fixed_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| Dispatch(fixed_params); |
| break; |
| } |
| case UrlLoadStrategy::ALWAYS_IN_INCOGNITO: { |
| ChromeBrowserState* browser_state = browser_->GetBrowserState(); |
| if (params.disposition == WindowOpenDisposition::CURRENT_TAB && |
| !browser_state->IsOffTheRecord()) { |
| DCHECK(incognito_loader_); |
| incognito_loader_->Load(params); |
| } else { |
| UrlLoadParams fixed_params = params; |
| fixed_params.in_incognito = YES; |
| Dispatch(fixed_params); |
| } |
| break; |
| } |
| case UrlLoadStrategy::NORMAL: { |
| Dispatch(params); |
| break; |
| } |
| } |
| } |
| |
| void UrlLoadingBrowserAgent::Dispatch(const UrlLoadParams& params) { |
| // Then dispatch. |
| switch (params.disposition) { |
| case WindowOpenDisposition::NEW_BACKGROUND_TAB: |
| case WindowOpenDisposition::NEW_FOREGROUND_TAB: |
| LoadUrlInNewTab(params); |
| break; |
| case WindowOpenDisposition::CURRENT_TAB: |
| LoadUrlInCurrentTab(params); |
| break; |
| case WindowOpenDisposition::SWITCH_TO_TAB: |
| SwitchToTab(params); |
| break; |
| default: |
| DCHECK(false) << "Unhandled url loading disposition."; |
| break; |
| } |
| } |
| |
| void UrlLoadingBrowserAgent::LoadUrlInCurrentTab(const UrlLoadParams& params) { |
| web::NavigationManager::WebLoadParams web_params = params.web_params; |
| |
| ChromeBrowserState* browser_state = browser_->GetBrowserState(); |
| |
| notifier_->TabWillLoadUrl(web_params.url, web_params.transition_type); |
| |
| WebStateList* web_state_list = browser_->GetWebStateList(); |
| web::WebState* current_web_state = web_state_list->GetActiveWebState(); |
| |
| // NOTE: This check for the Crash Host URL is here to avoid the URL from |
| // ending up in the history causing the app to crash at every subsequent |
| // restart. |
| if (web_params.url.host() == kChromeUIBrowserCrashHost) { |
| CrashReporterURLObserver::GetSharedInstance()->RecordURL( |
| web_params.url, current_web_state, /*pending=*/true); |
| InduceBrowserCrash(web_params.url); |
| // Under a debugger, the app can continue working even after the CHECK. |
| // Adding a return avoids adding the crash url to history. |
| notifier_->TabFailedToLoadUrl(web_params.url, web_params.transition_type); |
| return; |
| } |
| |
| PrerenderService* prerenderService = |
| PrerenderServiceFactory::GetForBrowserState(browser_state); |
| |
| // Some URLs are not allowed while in incognito. If we are in incognito and |
| // load a disallowed URL, instead create a new tab not in the incognito state. |
| // Also if there's no current web state, that means there is no current tab |
| // to open in, so this also redirects to a new tab. |
| if (!current_web_state || (browser_state->IsOffTheRecord() && |
| !IsURLAllowedInIncognito(web_params.url))) { |
| if (prerenderService) { |
| prerenderService->CancelPrerender(); |
| } |
| notifier_->TabFailedToLoadUrl(web_params.url, web_params.transition_type); |
| |
| if (!current_web_state) { |
| UrlLoadParams fixed_params = params; |
| fixed_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| fixed_params.in_incognito = browser_state->IsOffTheRecord(); |
| Load(fixed_params); |
| } else { |
| UrlLoadParams fixed_params = UrlLoadParams::InNewTab(web_params); |
| fixed_params.in_incognito = NO; |
| fixed_params.append_to = kCurrentTab; |
| Load(fixed_params); |
| } |
| return; |
| } |
| |
| // Ask the prerender service to load this URL if it can, and return if it does |
| // so. |
| if (prerenderService && |
| prerenderService->MaybeLoadPrerenderedURL( |
| web_params.url, web_params.transition_type, browser_)) { |
| notifier_->TabDidPrerenderUrl(web_params.url, web_params.transition_type); |
| return; |
| } |
| |
| BOOL typedOrGeneratedTransition = |
| PageTransitionCoreTypeIs(web_params.transition_type, |
| ui::PAGE_TRANSITION_TYPED) || |
| PageTransitionCoreTypeIs(web_params.transition_type, |
| ui::PAGE_TRANSITION_GENERATED); |
| if (typedOrGeneratedTransition) { |
| LoadTimingTabHelper::FromWebState(current_web_state)->DidInitiatePageLoad(); |
| } |
| |
| // If this is a reload initiated from the omnibox. |
| // TODO(crbug.com/730192): Add DCHECK to verify that whenever urlToLoad is the |
| // same as the old url, the transition type is ui::PAGE_TRANSITION_RELOAD. |
| if (PageTransitionCoreTypeIs(web_params.transition_type, |
| ui::PAGE_TRANSITION_RELOAD)) { |
| current_web_state->GetNavigationManager()->Reload( |
| web::ReloadType::NORMAL, true /* check_for_repost */); |
| notifier_->TabDidReloadUrl(web_params.url, web_params.transition_type); |
| return; |
| } |
| |
| current_web_state->GetNavigationManager()->LoadURLWithParams(web_params); |
| |
| notifier_->TabDidLoadUrl(web_params.url, web_params.transition_type); |
| } |
| |
| void UrlLoadingBrowserAgent::SwitchToTab(const UrlLoadParams& params) { |
| DCHECK(scene_service_); |
| |
| web::NavigationManager::WebLoadParams web_params = params.web_params; |
| |
| WebStateList* web_state_list = browser_->GetWebStateList(); |
| NSInteger new_web_state_index = |
| web_state_list->GetIndexOfInactiveWebStateWithURL(web_params.url); |
| bool old_tab_is_ntp_without_history = |
| IsNTPWithoutHistory(web_state_list->GetActiveWebState()); |
| |
| if (new_web_state_index == WebStateList::kInvalidIndex) { |
| // If the tab containing the URL has been closed. |
| if (old_tab_is_ntp_without_history) { |
| // It is NTP, just load the URL. |
| Load(UrlLoadParams::InCurrentTab(web_params)); |
| } else { |
| // Load the URL in foreground. |
| ChromeBrowserState* browser_state = browser_->GetBrowserState(); |
| UrlLoadParams new_tab_params = |
| UrlLoadParams::InNewTab(web_params.url, web_params.virtual_url); |
| new_tab_params.web_params.referrer = web::Referrer(); |
| new_tab_params.in_incognito = browser_state->IsOffTheRecord(); |
| new_tab_params.append_to = kCurrentTab; |
| scene_service_->LoadUrlInNewTab(new_tab_params); |
| } |
| return; |
| } |
| |
| notifier_->WillSwitchToTabWithUrl(web_params.url, new_web_state_index); |
| |
| NSInteger old_web_state_index = web_state_list->active_index(); |
| web_state_list->ActivateWebStateAt(new_web_state_index); |
| |
| // Close the tab if it is NTP with no back/forward history to avoid having |
| // empty tabs. |
| if (old_tab_is_ntp_without_history) { |
| web_state_list->CloseWebStateAt(old_web_state_index, |
| WebStateList::CLOSE_USER_ACTION); |
| } |
| |
| notifier_->DidSwitchToTabWithUrl(web_params.url, new_web_state_index); |
| } |
| |
| void UrlLoadingBrowserAgent::LoadUrlInNewTab(const UrlLoadParams& params) { |
| DCHECK(scene_service_); |
| DCHECK(delegate_); |
| DCHECK(browser_); |
| |
| if (params.in_incognito) { |
| IncognitoReauthSceneAgent* reauthAgent = [IncognitoReauthSceneAgent |
| agentFromScene:SceneStateBrowserAgent::FromBrowser(browser_) |
| ->GetSceneState()]; |
| DCHECK(!reauthAgent.authenticationRequired); |
| } |
| |
| ChromeBrowserState* browser_state = browser_->GetBrowserState(); |
| ChromeBrowserState* active_browser_state = |
| scene_service_->GetCurrentBrowser()->GetBrowserState(); |
| |
| // Two UrlLoadingServices exist, normal and incognito. Handle two special |
| // cases that need to be sent up to the SceneUrlLoadingService: |
| // 1) The URL needs to be loaded by the UrlLoadingService for the other mode. |
| // 2) The URL will be loaded in a foreground tab by this UrlLoadingService, |
| // but the UI associated with this UrlLoadingService is not currently visible, |
| // so the SceneUrlLoadingService needs to switch modes before loading the URL. |
| if (params.in_incognito != browser_state->IsOffTheRecord() || |
| (!params.in_background() && |
| params.in_incognito != active_browser_state->IsOffTheRecord())) { |
| // When sending a load request that switches modes, ensure the tab |
| // ends up appended to the end of the model, not just next to what is |
| // currently selected in the other mode. This is done with the |append_to| |
| // parameter. |
| UrlLoadParams scene_params = params; |
| scene_params.append_to = kLastTab; |
| scene_service_->LoadUrlInNewTab(scene_params); |
| return; |
| } |
| |
| // Notify only after checking incognito match, otherwise the delegate will |
| // take of changing the mode and try again. Notifying before the checks can |
| // lead to be calling it twice, and calling 'did' below once. |
| notifier_->NewTabWillLoadUrl(params.web_params.url, params.user_initiated); |
| |
| if (!params.in_background()) { |
| LoadUrlInNewTabImpl(params, absl::nullopt); |
| } else { |
| __block void* hint = nullptr; |
| __block UrlLoadParams saved_params = params; |
| __block base::WeakPtr<UrlLoadingBrowserAgent> weak_ptr = |
| weak_ptr_factory_.GetWeakPtr(); |
| |
| if (params.append_to == kCurrentTab) { |
| hint = browser_->GetWebStateList()->GetActiveWebState(); |
| } |
| |
| [delegate_ animateOpenBackgroundTabFromParams:params |
| completion:^{ |
| if (weak_ptr) { |
| weak_ptr->LoadUrlInNewTabImpl( |
| saved_params, hint); |
| } |
| }]; |
| } |
| } |
| |
| void UrlLoadingBrowserAgent::LoadUrlInNewTabImpl(const UrlLoadParams& params, |
| absl::optional<void*> hint) { |
| web::WebState* parent_web_state = nullptr; |
| if (params.append_to == kCurrentTab) { |
| parent_web_state = browser_->GetWebStateList()->GetActiveWebState(); |
| |
| // Detect whether the active tab changed during the animation of opening |
| // a tab in the background. This is only needed when opening in background |
| // (thus the use of optional). |
| // |
| // This compare the value read before vs after the animation (as `void*` |
| // to prevent trying to dereference a potentially dangling pointer). This |
| // is not 100% fool proof as the WebState could have been destroyed, then |
| // a new one allocated at the same address and inserted as the active tab. |
| // However, this is highly likely to happen. Even if it were to happen, it |
| // would be benign as the only drawback is that the wrong tab would be |
| // selected upon closing the newly opened tab. |
| if (hint && hint.value() != parent_web_state) |
| parent_web_state = nullptr; |
| } |
| |
| int insertion_index = TabInsertion::kPositionAutomatically; |
| if (params.append_to == kSpecifiedIndex) |
| insertion_index = params.insertion_index; |
| |
| TabInsertionBrowserAgent* insertion_agent = |
| TabInsertionBrowserAgent::FromBrowser(browser_); |
| |
| web::WebState* web_state = insertion_agent->InsertWebState( |
| params.web_params, parent_web_state, /*opened_by_dom=*/false, |
| insertion_index, params.in_background(), params.inherit_opener, |
| /*should_show_start_surface=*/false, params.filtering_result); |
| web_state->GetNavigationManager()->LoadIfNecessary(); |
| notifier_->NewTabDidLoadUrl(params.web_params.url, params.user_initiated); |
| } |