// 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/debug/dump_without_crashing.h"
#import "base/functional/callback_helpers.h"
#import "base/immediate_crash.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/task/thread_pool.h"
#import "components/omnibox/browser/autocomplete_input.h"
#import "components/omnibox/browser/autocomplete_scheme_classifier.h"
#import "components/omnibox/browser/omnibox_text_util.h"
#import "components/search_engines/util.h"
#import "components/send_tab_to_self/features.h"
#import "ios/chrome/browser/autocomplete/model/autocomplete_scheme_classifier_impl.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/policy/model/policy_util.h"
#import "ios/chrome/browser/prerender/model/prerender_browser_agent.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/state/incognito_state.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_interceptor.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"

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);
    }
  }

  std::string dump_without_crashing;
  if (net::GetValueForKeyInQuery(url, "dwc", &dump_without_crashing) &&
      (dump_without_crashing == "" || dump_without_crashing == "true")) {
    base::debug::DumpWithoutCrashing();
    return;
  }

#if !TARGET_OS_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;
  }

#if DCHECK_IS_ON()
  std::string use_after_free_string;
  if (net::GetValueForKeyInQuery(url, "uaf", &use_after_free_string) &&
      (use_after_free_string == "" || use_after_free_string == "true")) {
    for (int i = 0; i < 1000000000; ++i) {
      auto allocation = std::make_unique<int>();
      volatile int* allocation_ptr = allocation.get();
      allocation.reset();
      // Cause a UAF.
      [[maybe_unused]] int load = *allocation_ptr;
    }

    // If no one (gwp, asan, etc) catches the UAF, crash regardless.
    base::ImmediateCrash();
  }
#endif

  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)
    : BrowserUserData(browser),
      notifier_(UrlLoadingNotifierBrowserAgent::FromBrowser(browser_)) {
  DCHECK(notifier_);
}

UrlLoadingBrowserAgent::~UrlLoadingBrowserAgent() {}

base::WeakPtr<UrlLoadingBrowserAgent> UrlLoadingBrowserAgent::AsWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

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;
}

bool UrlLoadingBrowserAgent::AddInterceptor(
    const GURL& url,
    std::unique_ptr<URLInterceptor> interceptor) {
  if (!scene_service_) {
    return false;
  }
  return scene_service_->AddInterceptor(url, std::move(interceptor));
}

void UrlLoadingBrowserAgent::RemoveInterceptor(const GURL& url) {
  if (!scene_service_) {
    return;
  }
  scene_service_->RemoveInterceptor(url);
}

void UrlLoadingBrowserAgent::Load(const UrlLoadParams& params) {
  if (scene_service_) {
    if (scene_service_->OnIntercept(params)) {
      return;
    }
  }

  // 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::LoadURLForQuery(NSString* query) {
  // Since the query is not user typed, sanitize it to make sure it's safe.
  std::u16string sanitized_query =
      omnibox::SanitizeTextForPaste(base::SysNSStringToUTF16(query));

  GURL search_url;
  metrics::OmniboxInputType type = AutocompleteInput::Parse(
      sanitized_query, std::string(), AutocompleteSchemeClassifierImpl(),
      nullptr, nullptr, &search_url);
  ProfileIOS* profile = browser_->GetProfile();
  if (type != metrics::OmniboxInputType::URL || !search_url.is_valid()) {
    search_url = GetDefaultSearchURLForSearchTerms(
        ios::TemplateURLServiceFactory::GetForProfile(profile),
        sanitized_query);
  }
  if (search_url.is_valid()) {
    // It is necessary to include PAGE_TRANSITION_FROM_ADDRESS_BAR in the
    // transition type is so that query-in-the-omnibox is triggered for the
    // URL.
    UrlLoadParams params = UrlLoadParams::InCurrentTab(search_url);
    params.web_params.transition_type = ui::PageTransitionFromInt(
        ui::PAGE_TRANSITION_LINK | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
    Load(params);
  }
}

void UrlLoadingBrowserAgent::LoadUrlInCurrentTab(const UrlLoadParams& params) {
  LoadUrlInTab(params, browser_->GetWebStateList()->GetActiveWebState());
}

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::LoadUrlInTab(const UrlLoadParams& params,
                                          web::WebState* target_web_state) {
  CHECK(!target_web_state || target_web_state->IsRealized());
  if (target_web_state) {
    CHECK_NE(browser_->GetWebStateList()->GetIndexOfWebState(target_web_state),
             WebStateList::kInvalidIndex);
  }
  bool is_current_web_state =
      target_web_state == browser_->GetWebStateList()->GetActiveWebState();
  base::WeakPtr<web::WebState> web_state =
      target_web_state ? target_web_state->GetWeakPtr() : nullptr;

  web::NavigationManager::WebLoadParams web_params = params.web_params;

  ProfileIOS* profile = browser_->GetProfile();

  notifier_->TabWillLoadUrl(params, web_state);

  // 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.GetHost() == kChromeUIBrowserCrashHost) {
    CrashReporterURLObserver::GetSharedInstance()->RecordURL(
        web_params.url, target_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,
                                  web_state);
    return;
  }

  PrerenderBrowserAgent* prerender_browser_agent =
      PrerenderBrowserAgent::FromBrowser(browser_);

  // 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 (!target_web_state ||
      (profile->IsOffTheRecord() && !IsURLAllowedInIncognito(web_params.url))) {
    if (prerender_browser_agent) {
      prerender_browser_agent->CancelPrerender();
    }
    notifier_->TabFailedToLoadUrl(web_params.url, web_params.transition_type,
                                  web_state);

    if (!target_web_state) {
      UrlLoadParams fixed_params = params;
      fixed_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
      fixed_params.in_incognito = profile->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;
  }

  // ValidatePrerender assumes that the URL is being loaded in the current tab.
  // We currently don't support pre-rendering for background tabs.
  if (is_current_web_state) {
    // Ask the prerender service to load this URL if it can, and return if it
    // does so.
    if (prerender_browser_agent &&
        prerender_browser_agent->ValidatePrerender(
            web_params.url, web_params.transition_type)) {
      notifier_->TabDidPrerenderUrl(web_params.url, web_params.transition_type,
                                    web_state);
      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) {
    // Only record load timing if the tab is in the foreground.
    if (is_current_web_state) {
      LoadTimingTabHelper::FromWebState(target_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)) {
    target_web_state->GetNavigationManager()->Reload(
        web::ReloadType::NORMAL, true /* check_for_repost */);
    notifier_->TabDidReloadUrl(web_params.url, web_params.transition_type,
                               web_state);
    return;
  }

  target_web_state->GetNavigationManager()->LoadURLWithParams(web_params);

  notifier_->TabDidLoadUrl(web_params.url, web_params.transition_type,
                           web_state);
}

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.
      ProfileIOS* profile = browser_->GetProfile();
      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 = profile->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::ClosingReason::kUserAction);
  }

  notifier_->DidSwitchToTabWithUrl(web_params.url, new_web_state_index);
}

void UrlLoadingBrowserAgent::LoadUrlInNewTab(const UrlLoadParams& params) {
  DCHECK(scene_service_);
  DCHECK(delegate_);
  DCHECK(browser_);

  ProfileIOS* profile = browser_->GetProfile();
  if (!IsAddNewTabAllowedByPolicy(profile->GetPrefs(), params.in_incognito)) {
    return;
  }

  // Only open tab in incognito if re-authentication is not needed.

  SceneState* scene = browser_->GetSceneState();
  if (params.in_incognito && scene.incognitoState.authenticationRequired) {
    base::OnceCallback<void(BOOL)> load_url_on_auth_success = base::BindOnce(
        [](base::OnceClosure closure, BOOL success) {
          if (success) {
            std::move(closure).Run();
          }
        },
        base::BindOnce(&UrlLoadingBrowserAgent::LoadUrlInNewTab,
                       weak_ptr_factory_.GetWeakPtr(), params));
    IncognitoReauthSceneAgent* reauth_agent =
        [IncognitoReauthSceneAgent agentFromScene:scene];
    [reauth_agent
        authenticateIncognitoContentWithCompletionBlock:
            base::CallbackToBlock(std::move(load_url_on_auth_success))];
    return;
  }

  ProfileIOS* active_profile =
      scene_service_->GetCurrentBrowser()->GetProfile();

  // 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.
  if (params.in_incognito != profile->IsOffTheRecord()) {
    scene_service_->GetBrowserAgent(params.in_incognito)->Load(params);
    return;
  }
  // 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.switch_mode_if_needed && !params.in_background() &&
      params.in_incognito != active_profile->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, web::WebStateID());
  } else {
    web::WebStateID active_tab_id;
    if (params.append_to == OpenPosition::kCurrentTab) {
      if (web::WebState* active_web_state =
              browser_->GetWebStateList()->GetActiveWebState()) {
        active_tab_id = active_web_state->GetUniqueIdentifier();
      }
    }

    // If the tab should open in background in a different mode, dispatch the
    // load to ensure that if there are several tabs opened at the same time the
    // foreground one has time to be opened first.
    bool should_dispatch_load =
        params.in_incognito != active_profile->IsOffTheRecord();
    base::OnceClosure load_url_closure =
        base::BindOnce(&UrlLoadingBrowserAgent::LoadUrlInNewTabImpl,
                       weak_ptr_factory_.GetWeakPtr(), params, active_tab_id);
    if (should_dispatch_load) {
      load_url_closure =
          base::BindPostTask(base::SequencedTaskRunner::GetCurrentDefault(),
                             std::move(load_url_closure));
    }
    [delegate_
        animateOpenBackgroundTabFromParams:params
                                completion:base::CallbackToBlock(
                                               std::move(load_url_closure))];
  }
}

void UrlLoadingBrowserAgent::LoadUrlInNewTabImpl(
    const UrlLoadParams& params,
    web::WebStateID active_tab_id) {
  web::WebState* parent_web_state = nullptr;
  if (params.append_to == OpenPosition::kCurrentTab) {
    parent_web_state = browser_->GetWebStateList()->GetActiveWebState();

    // Detecting whether the active tab change is done by comparing the
    // WebStateID. This is cheap, does not require passing a pointer that could
    // become dangling, nor creating a WeakPtr which is expensive, when the only
    // thing we are interested is detecting a change.
    if (active_tab_id.valid() && parent_web_state &&
        parent_web_state->GetUniqueIdentifier() != active_tab_id) {
      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_pinned = params.load_pinned;
  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);

  notifier_->TabWillLoadUrl(params, web_state->GetWeakPtr());

  // 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);
  }
}
