// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/extensions/tab_helper.h"

#include <memory>

#include "base/check_op.h"
#include "base/notreached.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/activity_log/activity_log.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_tab_helper_factory.h"
#include "chrome/common/buildflags.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "extensions/browser/api/declarative/rules_registry_service.h"
#include "extensions/browser/api/declarative_content/content_rules_registry.h"
#include "extensions/browser/extension_web_contents_observer.h"
#include "extensions/browser/permissions/active_tab_permission_granter.h"
#include "extensions/browser/permissions/site_permissions_helper.h"
#include "extensions/browser/permissions_manager.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_resource.h"
#include "extensions/common/manifest.h"
#include "extensions/common/permissions/api_permission.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/extensions/api/bookmark_manager_private/bookmark_manager_private_api.h"
#endif

#if BUILDFLAG(ENABLE_SESSION_SERVICE)
#include "chrome/browser/sessions/session_service.h"
#include "chrome/browser/sessions/session_service_factory.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using content::WebContents;

namespace extensions {

namespace {

void DisableBackForwardCacheIfNecessary(
    const ExtensionSet& enabled_extensions,
    content::BrowserContext* context,
    content::NavigationHandle* navigation_handle) {
  // User data key for caching if bfcache is disabled.
  static const char kIsBFCacheDisabledKey[] =
      "extensions.backforward.browsercontext";

  if (!content::BackForwardCache::IsBackForwardCacheFeatureEnabled() ||
      context->GetUserData(kIsBFCacheDisabledKey)) {
    return;
  }
}

}  // namespace

TabHelper::~TabHelper() = default;

TabHelper::TabHelper(content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents),
      content::WebContentsUserData<TabHelper>(*web_contents),
      profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())),
      script_executor_(std::make_unique<ScriptExecutor>(web_contents)),
      extension_action_runner_(
          std::make_unique<ExtensionActionRunner>(web_contents)),
      declarative_net_request_helper_(web_contents) {
  // The ActiveTabPermissionManager requires a session ID; ensure this
  // WebContents has one.
  CreateSessionServiceTabHelper(web_contents);
  // The [this] is safe because ForEachRenderFrameHost() is synchronous.
  web_contents->ForEachRenderFrameHost(
      [this](content::RenderFrameHost* host) { SetTabId(host); });
  int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
  // TODO(crbug.com/393179880): Pull this creation out of TabHelper once
  // tab id assignment can be done on desktop Android.
  ActiveTabPermissionGranter::CreateForWebContents(web_contents, tab_id,
                                                   profile_);

  ActivityLog::GetInstance(profile_)->ObserveScripts(script_executor_.get());

  InvokeForContentRulesRegistries([this](ContentRulesRegistry* registry) {
    registry->MonitorWebContentsForRuleEvaluation(this->web_contents());
  });

  ExtensionWebContentsObserver::GetForWebContents(web_contents)
      ->dispatcher()
      ->set_delegate(this);

  registry_observation_.Observe(
      ExtensionRegistry::Get(web_contents->GetBrowserContext()));

#if !BUILDFLAG(IS_ANDROID)
  // The Android bookmark manager is native UI, not web UI, so this event router
  // isn't needed on desktop Android.
  BookmarkManagerPrivateDragEventRouter::CreateForWebContents(web_contents);
#endif
}

void TabHelper::SetReloadRequired(
    PermissionsManager::UserSiteSetting site_setting) {
  switch (site_setting) {
    case PermissionsManager::UserSiteSetting::kGrantAllExtensions: {
      // Granting access to all extensions is allowed iff feature is
      // enabled, and it shouldn't be enabled anywhere where this is called.
      NOTREACHED();
    }
    case PermissionsManager::UserSiteSetting::kBlockAllExtensions: {
      // A reload is required if any extension that had site access will lose
      // it.
      SitePermissionsHelper permissions_helper(profile_);
      const ExtensionSet& extensions =
          ExtensionRegistry::Get(profile_)->enabled_extensions();
      reload_required_ = std::ranges::any_of(
          extensions, [&permissions_helper, contents = web_contents()](
                          scoped_refptr<const Extension> extension) {
            return permissions_helper.GetSiteInteraction(*extension,
                                                         contents) ==
                   SitePermissionsHelper::SiteInteraction::kGranted;
          });
      break;
    }
    case PermissionsManager::UserSiteSetting::kCustomizeByExtension:
      // When the user selects "customize by extension" it means previously all
      // extensions were blocked and each extension's page access is set as
      // "denied". Blocked actions in the ExtensionActionRunner are computed by
      // checking if a page access is "withheld". Therefore, we always need a
      // refresh since we don't know if there are any extensions that would have
      // wanted to run if the page had not been restricted by the user.
      reload_required_ = true;
      break;
  }
}

void TabHelper::SetReloadRequired(
    const std::vector<const Extension*>& extensions) {
  reload_required_ = true;
  for (const auto* extension : extensions) {
    if (!std::ranges::contains(reload_extensions_, extension)) {
      reload_extensions_.push_back(extension);
    }
  }
  ShowReloadBubbleIfVisible();
}

bool TabHelper::IsReloadRequired() {
  return reload_required_;
}

void TabHelper::OnWatchedPageChanged(
    const std::vector<std::string>& css_selectors) {
  InvokeForContentRulesRegistries(
      [this, css_selectors](ContentRulesRegistry* registry) {
        registry->OnWatchedPageChanged(web_contents(), css_selectors);
      });
}

// Encapsulates the logic to decide which ContentRulesRegistries need to be
// invoked, depending on whether this WebContents is associated with an Original
// or OffTheRecord profile. In the latter case, we need to invoke on both the
// Original and OffTheRecord ContentRulesRegistries since the Original registry
// handles spanning-mode incognito extensions.
template <class Func>
void TabHelper::InvokeForContentRulesRegistries(const Func& func) {
  RulesRegistryService* rules_registry_service =
      RulesRegistryService::Get(profile_);
  if (rules_registry_service) {
    // The service can remain accessible after its registries are released
    // during profile teardown.
    if (ContentRulesRegistry* content_rules_registry =
            rules_registry_service->content_rules_registry()) {
      func(content_rules_registry);
    }
    if (profile_->IsOffTheRecord()) {
      // The original profile's content rules registry handles rules for
      // spanning extensions in incognito profiles, so invoke it also.
      RulesRegistryService* original_profile_rules_registry_service =
          RulesRegistryService::Get(profile_->GetOriginalProfile());
      DCHECK_NE(rules_registry_service,
                original_profile_rules_registry_service);
      if (original_profile_rules_registry_service) {
        if (ContentRulesRegistry* original_content_rules_registry =
                original_profile_rules_registry_service
                    ->content_rules_registry()) {
          func(original_content_rules_registry);
        }
      }
    }
  }
}

void TabHelper::RenderFrameCreated(content::RenderFrameHost* host) {
  SetTabId(host);
}

void TabHelper::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!navigation_handle->HasCommitted() ||
      !navigation_handle->IsInPrimaryMainFrame()) {
    return;
  }

  InvokeForContentRulesRegistries(
      [this, navigation_handle](ContentRulesRegistry* registry) {
        registry->DidFinishNavigation(web_contents(), navigation_handle);
      });

  content::BrowserContext* context = web_contents()->GetBrowserContext();
  ExtensionRegistry* registry = ExtensionRegistry::Get(context);
  const ExtensionSet& enabled_extensions = registry->enabled_extensions();

  DisableBackForwardCacheIfNecessary(enabled_extensions, context,
                                     navigation_handle);

  // Reset the `reload_required_` data member, since a page navigation acts as a
  // page refresh.
  reload_required_ = false;
  reload_extensions_.clear();
}

void TabHelper::DidCloneToNewWebContents(WebContents* old_web_contents,
                                         WebContents* new_web_contents) {
  // When the WebContents that this is attached to is cloned, give the new clone
  // a TabHelper.
  // TODO(jamescook): Do we still need to do this if we're not copying any
  // state?
  CreateForWebContents(new_web_contents);
}

void TabHelper::OnVisibilityChanged(content::Visibility visibility) {
  ShowReloadBubbleIfVisible();
}

void TabHelper::ShowReloadBubbleIfVisible() {
  if (web_contents()->GetVisibility() == content::Visibility::VISIBLE &&
      reload_required_ && !reload_extensions_.empty()) {
    std::vector<const Extension*> raw_extensions;
    for (const auto& ext : reload_extensions_) {
      raw_extensions.push_back(ext.get());
    }
    extension_action_runner_->ShowReloadPageBubble(raw_extensions);
    reload_extensions_.clear();
  }
}

void TabHelper::WebContentsDestroyed() {
  InvokeForContentRulesRegistries([this](ContentRulesRegistry* registry) {
    registry->WebContentsDestroyed(web_contents());
  });

  reload_required_ = false;
}

WindowController* TabHelper::GetExtensionWindowController() {
  return ExtensionTabUtil::GetWindowControllerOfTab(web_contents());
}

void TabHelper::OnExtensionLoaded(content::BrowserContext* browser_context,
                                  const Extension* extension) {
  // Clear the back forward cache for the associated tab to accommodate for any
  // side effects of loading/unloading the extension.
  web_contents()->GetController().GetBackForwardCache().Flush();
}

void TabHelper::OnExtensionUnloaded(content::BrowserContext* browser_context,
                                    const Extension* extension,
                                    UnloadedExtensionReason reason) {
  // Clear the back forward cache for the associated tab to accommodate for any
  // side effects of loading/unloading the extension.
  web_contents()->GetController().GetBackForwardCache().Flush();

  // If the unloaded extension was pending a reload on this tab, remove it
  // and clear reload_required_ if no other extensions require a reload.
  if (!reload_extensions_.empty()) {
    std::erase_if(reload_extensions_, [extension](const auto& ext) {
      return ext.get() == extension;
    });
    if (reload_extensions_.empty()) {
      reload_required_ = false;
    }
  }
}

void TabHelper::SetTabId(content::RenderFrameHost* render_frame_host) {
  // When this is called from the TabHelper constructor during WebContents
  // creation, the renderer-side Frame object would not have been created yet.
  // We should wait for RenderFrameCreated() to happen, to avoid sending this
  // message twice.
  if (render_frame_host->IsRenderFrameLive()) {
    SessionID id = sessions::SessionTabHelper::IdForTab(web_contents());
    CHECK(id.is_valid());
    auto* local_frame =
        ExtensionWebContentsObserver::GetForWebContents(web_contents())
            ->GetLocalFrame(render_frame_host);
    if (!local_frame) {
      return;
    }
    local_frame->SetTabId(id.id());
  }
}

WEB_CONTENTS_USER_DATA_KEY_IMPL(TabHelper);

}  // namespace extensions
