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

#include "extensions/browser/extension_web_contents_observer.h"

#include "base/check.h"
#include "base/feature_list.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/child_process_security_policy.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/extension_config_map.h"
#include "extensions/browser/extension_config_map_factory.h"
#include "extensions/browser/extension_frame_host.h"
#include "extensions/browser/extension_mojo_binder_registry.h"
#include "extensions/browser/extension_mojo_binder_registry_factory.h"
#include "extensions/browser/extension_navigation_registry.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/kiosk/kiosk_delegate.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/renderer_startup_helper.h"
#include "extensions/browser/script_injection_tracker.h"
#include "extensions/browser/view_type_utils.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/autoplay/autoplay.mojom.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "url/origin.h"

namespace extensions {

// static
ExtensionWebContentsObserver* ExtensionWebContentsObserver::GetForWebContents(
    content::WebContents* web_contents) {
  return ExtensionsBrowserClient::Get()->GetExtensionWebContentsObserver(
      web_contents);
}

// static
void ExtensionWebContentsObserver::BindLocalFrameHost(
    mojo::PendingAssociatedReceiver<mojom::LocalFrameHost> receiver,
    content::RenderFrameHost* render_frame_host) {
  auto* web_contents =
      content::WebContents::FromRenderFrameHost(render_frame_host);
  if (!web_contents) {
    return;
  }
  auto* observer = GetForWebContents(web_contents);
  if (!observer) {
    return;
  }
  auto* efh = observer->extension_frame_host_.get();
  if (!efh) {
    return;
  }
  efh->BindLocalFrameHost(std::move(receiver), render_frame_host);
}

std::unique_ptr<ExtensionFrameHost>
ExtensionWebContentsObserver::CreateExtensionFrameHost(
    content::WebContents* web_contents) {
  return std::make_unique<ExtensionFrameHost>(web_contents);
}

void ExtensionWebContentsObserver::ListenToWindowIdChangesFrom(
    sessions::SessionTabHelper* helper) {
  if (!window_id_subscription_) {
    // We use an unretained receiver here: the callback is inside the
    // subscription, which is a member of |this|, so it can't be run after the
    // destruction of |this|.
    window_id_subscription_ = helper->RegisterForWindowIdChanged(
        base::BindRepeating(&ExtensionWebContentsObserver::OnWindowIdChanged,
                            base::Unretained(this)));
  }
}

void ExtensionWebContentsObserver::Initialize() {
  if (initialized_) {
    return;
  }

  initialized_ = true;

  extension_frame_host_ = CreateExtensionFrameHost(web_contents());

  content::RenderFrameHost* main_frame = web_contents()->GetPrimaryMainFrame();
  // We only initialize the frame if the renderer counterpart is live;
  // otherwise we wait for the RenderFrameCreated notification.
  if (main_frame->IsRenderFrameLive()) {
    InitializeRenderFrame(main_frame);
  }

  // At the point of initialization, the *only* frame that can exist is the
  // main frame.
  web_contents()->ForEachRenderFrameHost(
      [main_frame](content::RenderFrameHost* render_frame_host) {
        CHECK_EQ(render_frame_host, main_frame);
      });

  // It would be ideal if SessionTabHelper was created before this object,
  // because then we could start observing it here instead of needing to be
  // externally notified when it is created, but it isn't. If that ordering ever
  // changes, this code can be restructured and ListenToWindowIdChangesFrom()
  // can become private.
  DCHECK(!sessions::SessionTabHelper::FromWebContents(web_contents()));
}

ExtensionWebContentsObserver::ExtensionWebContentsObserver(
    content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents),
      browser_context_(web_contents->GetBrowserContext()),
      dispatcher_(browser_context_),
      initialized_(false) {}

ExtensionWebContentsObserver::~ExtensionWebContentsObserver() {
}

void ExtensionWebContentsObserver::InitializeRenderFrame(
    content::RenderFrameHost* render_frame_host) {
  DCHECK(initialized_);
  DCHECK(render_frame_host);
  DCHECK(render_frame_host->IsRenderFrameLive());

  // At the initialization of the render frame, the last committed URL is not
  // reliable, so do not take it into account in determining whether it is an
  // extension frame.
  const Extension* frame_extension =
      GetExtensionFromFrame(render_frame_host, false);
  // This observer is attached to every WebContents, so we are also notified of
  // frames that are not in an extension process.
  if (!frame_extension) {
    return;
  }

  // |render_frame_host->GetProcess()| is an extension process. Grant permission
  // to request pages from the extension's origin.
  content::ChildProcessSecurityPolicy* security_policy =
      content::ChildProcessSecurityPolicy::GetInstance();
  int process_id = render_frame_host->GetProcess()->GetDeprecatedID();
  security_policy->GrantRequestOrigin(process_id, frame_extension->origin());

  // Notify the render frame of the view type.
  GetLocalFrameChecked(render_frame_host)
      .NotifyRenderViewType(GetViewType(render_frame_host));

  ProcessManager::Get(browser_context_)
      ->RegisterRenderFrameHost(render_frame_host, frame_extension);
}

void ExtensionWebContentsObserver::SetUpRenderFrameHost(
    content::RenderFrameHost* render_frame_host) {
  DCHECK(initialized_);
  InitializeRenderFrame(render_frame_host);

  const Extension* extension = GetExtensionFromFrame(render_frame_host, false);
  if (!extension) {
    return;
  }

  Manifest::Type type = extension->GetType();

  // Some extensions use file:// URLs.
  //
  // Note: this particular grant isn't relevant for hosted apps, but in the
  // future we should be careful about granting privileges to hosted app
  // subframes in places like this, since they currently stay in process with
  // their parent. A malicious site shouldn't be able to gain a hosted app's
  // privileges just by embedding a subframe to a popular hosted app.
  //
  // Note: Keep this logic in sync with related logic in
  // ChromeContentBrowserClient::RegisterNonNetworkSubresourceURLLoaderFactories.
  if (type == Manifest::Type::kExtension ||
      type == Manifest::Type::kLegacyPackagedApp) {
    util::InitializeFileSchemeAccessForExtension(
        render_frame_host->GetProcess()->GetID(), extension->id(),
        browser_context_);
  }

  // Tells the new frame that it's hosted in an extension process.
  //
  // This will often be a redundant IPC, because activating extensions happens
  // at the process level, not at the frame level. However, without some mild
  // refactoring this isn't trivial to do, and this way is simpler.
  //
  // Plus, we can delete the concept of activating an extension once site
  // isolation is turned on.
  RendererStartupHelperFactory::GetForBrowserContext(browser_context_)
      ->ActivateExtensionInProcess(*extension, render_frame_host->GetProcess());

  if (IsMojoJsEnabled(*extension)) {
    render_frame_host->EnableMojoJsBindings(/*features=*/nullptr);
  }

  if (IsJsErrorReportingEnabled(*extension)) {
    render_frame_host->SetWantErrorMessageStackTrace();
  }
}

void ExtensionWebContentsObserver::RenderFrameDeleted(
    content::RenderFrameHost* render_frame_host) {
  DCHECK(initialized_);
  extension_frame_host_->RenderFrameDeleted(render_frame_host);
  local_frame_map_.erase(render_frame_host);
  ProcessManager::Get(browser_context_)
      ->UnregisterRenderFrameHost(render_frame_host);
  ExtensionApiFrameIdMap::Get()->OnRenderFrameDeleted(render_frame_host);
}

void ExtensionWebContentsObserver::ReadyToCommitNavigation(
    content::NavigationHandle* navigation_handle) {
#if !BUILDFLAG(IS_ANDROID)
  // If the navigation is for the TopChrome WebUI, we can skip extension
  // initialization for optimization. Otherwise, we ensure the renderer process
  // is initialized including the non-TopChrome WebUI navigation.
  content::RenderProcessHost* process =
      navigation_handle->GetRenderFrameHost()->GetProcess();
  if (process->IsForTopChromeWebUI() &&
      base::FeatureList::IsEnabled(
          blink::features::kInitialWebUIWithoutExtensions)) {
    if (!navigation_handle->IsInitialWebUINavigation()) {
      RendererStartupHelperFactory::GetForBrowserContext(browser_context_)
          ->InitializeProcess(process);
    }
  }
#endif  // !BUILDFLAG(IS_ANDROID)

  SetUpRenderFrameHost(navigation_handle->GetRenderFrameHost());

  ScriptInjectionTracker::ReadyToCommitNavigation(PassKey(), navigation_handle);

  // We don't force autoplay to allow while prerendering.
  if (navigation_handle->GetRenderFrameHost()->GetLifecycleState() ==
          content::RenderFrameHost::LifecycleState::kPrerendering &&
      !navigation_handle->IsPrerenderedPageActivation()) {
    return;
  }

  const ExtensionRegistry* const registry =
      ExtensionRegistry::Get(browser_context_);

  content::RenderFrameHost* parent_or_outerdoc =
      navigation_handle->GetParentFrameOrOuterDocument();

  content::RenderFrameHost* outermost_main_render_frame_host =
      parent_or_outerdoc ? parent_or_outerdoc->GetOutermostMainFrame()
                         : navigation_handle->GetRenderFrameHost();

  const Extension* const extension =
      GetExtensionFromFrame(outermost_main_render_frame_host, false);
  KioskDelegate* const kiosk_delegate =
      ExtensionsBrowserClient::Get()->GetKioskDelegate();
  DCHECK(kiosk_delegate);
  bool is_kiosk =
      extension && kiosk_delegate->IsAutoLaunchedKioskApp(extension->id());

  // If the top most frame is an extension, packaged app, hosted app, etc. then
  // the main frame and all iframes should be able to autoplay without
  // restriction. <webview> should still have autoplay blocked though.
  GURL url =
      parent_or_outerdoc
          ? parent_or_outerdoc->GetOutermostMainFrame()->GetLastCommittedURL()
          : navigation_handle->GetURL();
  if (is_kiosk || registry->enabled_extensions().GetExtensionOrAppByURL(url)) {
    mojo::AssociatedRemote<blink::mojom::AutoplayConfigurationClient> client;
    navigation_handle->GetRenderFrameHost()
        ->GetRemoteAssociatedInterfaces()
        ->GetInterface(&client);
    client->AddAutoplayFlags(url::Origin::Create(navigation_handle->GetURL()),
                             blink::mojom::kAutoplayFlagForceAllow);
  }
}

void ExtensionWebContentsObserver::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  DCHECK(navigation_handle);
  DCHECK(initialized_);
  if (!navigation_handle->HasCommitted()) {
    return;
  }

  ProcessManager* pm = ProcessManager::Get(browser_context_);

  content::RenderFrameHost* render_frame_host =
      navigation_handle->GetRenderFrameHost();
  DCHECK(render_frame_host);

  const Extension* frame_extension =
      GetExtensionFromFrame(render_frame_host, true);
  if (pm->IsRenderFrameHostRegistered(render_frame_host)) {
    if (!frame_extension) {
      pm->UnregisterRenderFrameHost(render_frame_host);
    }
  } else if (frame_extension && render_frame_host->IsRenderFrameLive()) {
    pm->RegisterRenderFrameHost(render_frame_host, frame_extension);
  }

  // Delete the navigation id from ExtensionNavigationRegistry if it exists.
  ExtensionNavigationRegistry::Get(web_contents()->GetBrowserContext())
      ->Erase(navigation_handle->GetNavigationId());

  ScriptInjectionTracker::DidFinishNavigation(PassKey(), navigation_handle);
}

void ExtensionWebContentsObserver::MediaPictureInPictureChanged(
    bool is_picture_in_picture) {
  DCHECK(initialized_);
  if (GetViewType(web_contents()) ==
      mojom::ViewType::kExtensionBackgroundPage) {
    ProcessManager* const process_manager =
        ProcessManager::Get(browser_context_);
    const Extension* const extension =
        process_manager->GetExtensionForWebContents(web_contents());
    if (extension == nullptr) {
      return;
    }
    if (is_picture_in_picture) {
      process_manager->IncrementLazyKeepaliveCount(extension, Activity::MEDIA,
                                                   Activity::kPictureInPicture);
    } else {
      process_manager->DecrementLazyKeepaliveCount(extension, Activity::MEDIA,
                                                   Activity::kPictureInPicture);
    }
  }
}

const Extension* ExtensionWebContentsObserver::GetExtensionFromFrame(
    content::RenderFrameHost* render_frame_host,
    bool verify_url) const {
  DCHECK(initialized_);
  ExtensionId extension_id = util::GetExtensionIdFromFrame(render_frame_host);
  if (extension_id.empty()) {
    return nullptr;
  }

  content::BrowserContext* browser_context =
      render_frame_host->GetProcess()->GetBrowserContext();
  const Extension* extension = ExtensionRegistry::Get(browser_context)
                                   ->enabled_extensions()
                                   .GetByID(extension_id);
  if (!extension) {
    return nullptr;
  }

  if (verify_url) {
    const url::Origin& origin(render_frame_host->GetLastCommittedOrigin());
    // This check is needed to eliminate origins that are not within a
    // hosted-app's web extent, and sandboxed extension frames with an opaque
    // origin.
    // TODO(crbug.com/40725839) See if extension check is still needed after bug
    // is fixed.
    auto* extension_for_origin = ExtensionRegistry::Get(browser_context)
                                     ->enabled_extensions()
                                     .GetExtensionOrAppByURL(origin.GetURL());
    if (origin.opaque() || extension_for_origin != extension) {
      return nullptr;
    }
  }

  return extension;
}

mojom::LocalFrame* ExtensionWebContentsObserver::GetLocalFrame(
    content::RenderFrameHost* render_frame_host) {
  // Attempting to get a remote interface before IsRenderFrameLive() will fail,
  // leaving a broken pipe that will block all further messages. Return nullptr
  // instead. Callers should try again after RenderFrameCreated().
  if (!render_frame_host->IsRenderFrameLive()) {
    return nullptr;
  }

  // Do not return a LocalFrame object for frames that do not immediately belong
  // to this WebContents. For example frames belonging to inner WebContents will
  // have their own ExtensionWebContentsObserver.
  if (content::WebContents::FromRenderFrameHost(render_frame_host) !=
      web_contents()) {
    return nullptr;
  }

  mojo::AssociatedRemote<mojom::LocalFrame>& remote =
      local_frame_map_[render_frame_host];
  if (!remote.is_bound()) {
    render_frame_host->GetRemoteAssociatedInterfaces()->GetInterface(
        remote.BindNewEndpointAndPassReceiver());
  }
  return remote.get();
}

mojom::LocalFrame& ExtensionWebContentsObserver::GetLocalFrameChecked(
    content::RenderFrameHost* render_frame_host) {
  auto* local_frame = GetLocalFrame(render_frame_host);
  CHECK(local_frame);
  return *local_frame;
}

void ExtensionWebContentsObserver::OnWindowIdChanged(SessionID id) {
  web_contents()->ForEachRenderFrameHost(
      [&id, this](content::RenderFrameHost* render_frame_host) {
        auto* local_frame = GetLocalFrame(render_frame_host);
        if (local_frame) {
          local_frame->UpdateBrowserWindowId(id.id());
        }
      });
}

bool ExtensionWebContentsObserver::IsMojoJsEnabled(
    const Extension& extension) const {
  const auto* registry =
      ExtensionMojoBinderRegistryFactory::GetForBrowserContext(
          browser_context_);
  return registry && registry->IsMojoJsEnabled(extension);
}

bool ExtensionWebContentsObserver::IsJsErrorReportingEnabled(
    const Extension& extension) const {
  const auto* config_map =
      ExtensionConfigMapFactory::GetForBrowserContext(browser_context_);
  const auto* provider =
      config_map ? config_map->GetConfigProvider(extension) : nullptr;
  return provider && provider->IsJsErrorReportingEnabled();
}

void ExtensionWebContentsObserver::OnDidAddMessageToConsole(
    content::RenderFrameHost* source_frame,
    blink::mojom::ConsoleMessageLevel log_level,
    const std::u16string& message,
    int32_t line_no,
    const std::u16string& source_id,
    const std::optional<std::u16string>& untrusted_stack_trace) {
  if (log_level != blink::mojom::ConsoleMessageLevel::kError) {
    return;
  }

  if (!source_frame) {
    return;
  }

  // If source_id is not a valid extension script URL, fall back to the frame's
  // committed URL so errors originating from the extension frame are captured.
  GURL url(source_id);
  if (!url.is_valid() || !url.SchemeIs(kExtensionScheme)) {
    url = source_frame->GetLastCommittedURL();
  }
  if (!url.is_valid() || !url.SchemeIs(kExtensionScheme)) {
    return;
  }

  // Ignore console messages from frames that are pending deletion to avoid
  // reporting errors during frame teardown.
  if (source_frame->GetLifecycleState() ==
      content::RenderFrameHost::LifecycleState::kPendingDeletion) {
    return;
  }

  const Extension* extension =
      GetExtensionFromFrame(source_frame, /*verify_url=*/false);
  if (!extension || !IsJsErrorReportingEnabled(*extension)) {
    return;
  }

  OnExtensionJsError(source_frame, *extension, message, line_no, url,
                     untrusted_stack_trace);
}

void ExtensionWebContentsObserver::OnExtensionJsError(
    content::RenderFrameHost* source_frame,
    const Extension& extension,
    const std::u16string& message,
    int32_t line_no,
    const GURL& url,
    const std::optional<std::u16string>& untrusted_stack_trace) {}

}  // namespace extensions
