// Copyright 2019 The Chromium Authors
// 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/model/url_loading_browser_agent.h"
#import "base/compiler_specific.h"
#import "base/immediate_crash.h"
#import "base/strings/string_number_conversions.h"
#import "base/task/thread_pool.h"
#import "ios/chrome/browser/crash_report/model/crash_reporter_url_observer.h"
#import "ios/chrome/browser/incognito_reauth/ui_bundled/incognito_reauth_scene_agent.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_util.h"
#import "ios/chrome/browser/prerender/model/prerender_service.h"
#import "ios/chrome/browser/prerender/model/prerender_service_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/tab_insertion/model/tab_insertion_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/scene_url_loading_service.h"
#import "ios/chrome/browser/url_loading/model/url_loading_notifier_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/browser/url_loading/model/url_loading_util.h"
#import "ios/chrome/browser/web/model/load_timing_tab_helper.h"
#import "net/base/url_util.h"
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.
base::ImmediateCrash();
}
}
} // 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::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* prerender_service =
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 (prerender_service) {
prerender_service->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 = OpenPosition::kCurrentTab;
Load(fixed_params);
}
return;
}
// Ask the prerender service to load this URL if it can, and return if it does
// so.
if (prerender_service &&
prerender_service->MaybeLoadPrerenderedURL(
web_params.url, web_params.transition_type, browser_)) {
notifier_->TabDidPrerenderUrl(web_params.url, web_params.transition_type);
return;
}
const bool typed_or_generated_transition =
PageTransitionCoreTypeIs(web_params.transition_type,
ui::PAGE_TRANSITION_TYPED) ||
PageTransitionCoreTypeIs(web_params.transition_type,
ui::PAGE_TRANSITION_GENERATED);
if (typed_or_generated_transition) {
LoadTimingTabHelper::FromWebState(current_web_state)->DidInitiatePageLoad();
}
// If this is a reload initiated from the omnibox.
// TODO(crbug.com/41323528): 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 = OpenPosition::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* reauth_agent =
[IncognitoReauthSceneAgent agentFromScene:browser_->GetSceneState()];
DCHECK(!reauth_agent.authenticationRequired);
}
ChromeBrowserState* browser_state = browser_->GetBrowserState();
ChromeBrowserState* active_browser_state =
scene_service_->GetCurrentBrowser()->GetBrowserState();
// Two UrlLoadingServices exist per scene, 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 = OpenPosition::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.
if (params.instant_load || !params.in_background()) {
notifier_->NewTabWillLoadUrl(params.web_params.url, params.user_initiated);
}
if (!params.in_background()) {
LoadUrlInNewTabImpl(params, std::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 == OpenPosition::kCurrentTab) {
hint = browser_->GetWebStateList()->GetActiveWebState();
}
[delegate_ animateOpenBackgroundTabFromParams:params
completion:^{
if (weak_ptr) {
weak_ptr->LoadUrlInNewTabImpl(
saved_params, hint);
}
}];
}
}
void UrlLoadingBrowserAgent::LoadUrlInNewTabImpl(const UrlLoadParams& params,
std::optional<void*> hint) {
web::WebState* parent_web_state = nullptr;
if (params.append_to == OpenPosition::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 == OpenPosition::kSpecifiedIndex) {
insertion_index = params.insertion_index;
}
TabInsertionBrowserAgent* insertion_agent =
TabInsertionBrowserAgent::FromBrowser(browser_);
TabInsertion::Params insertion_params;
insertion_params.parent = parent_web_state;
insertion_params.index = insertion_index;
insertion_params.instant_load = params.instant_load;
insertion_params.in_background = params.in_background();
insertion_params.inherit_opener = params.inherit_opener;
insertion_params.should_skip_new_tab_animation = params.from_external;
insertion_params.placeholder_title = params.placeholder_title;
insertion_params.insert_in_group = params.load_in_group;
insertion_params.tab_group = params.tab_group;
web::WebState* web_state =
insertion_agent->InsertWebState(params.web_params, insertion_params);
// If the tab was created as "unrealized" (e.g. `instant_load`
// being false) then do not force a load. The tab will load
// when it transition to "realized".
if (web_state->IsRealized()) {
web_state->GetNavigationManager()->LoadIfNecessary();
notifier_->NewTabDidLoadUrl(params.web_params.url, params.user_initiated);
}
}