blob: 1a7eda97c273cf6e5777829101ca44186a394cb2 [file] [log] [blame]
// 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);
}