// 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_util.h"

#include <algorithm>

#include "base/barrier_closure.h"
#include "base/command_line.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "build/chromeos_buildflags.h"
#include "components/crx_file/id_util.h"
#include "components/download/public/common/download_item.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/child_process_security_policy.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/security_principal.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/storage_partition_config.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/process_map.h"
#include "extensions/browser/script_injection_tracker.h"
#include "extensions/browser/shared_module_service.h"
#include "extensions/browser/ui_util.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/feature_switch.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_handlers/incognito_info.h"
#include "extensions/common/manifest_handlers/shared_module_info.h"
#include "extensions/common/mojom/manifest.mojom.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/switches.h"
#include "extensions/common/user_script.h"
#include "extensions/grit/extensions_browser_resources.h"
#include "mojo/public/cpp/bindings/clone_traits.h"
#include "ui/base/resource/resource_bundle.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/constants/pref_names.h"
#include "components/prefs/pref_service.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "base/system/sys_info.h"
#endif

namespace extensions::util {

namespace {

#if BUILDFLAG(IS_CHROMEOS)
bool IsSigninProfileTestExtensionOnTestImage(const Extension* extension) {
  if (extension->id() != extension_misc::kSigninProfileTestExtensionId) {
    return false;
  }
  base::SysInfo::CrashIfChromeOSNonTestImage();
  return true;
}
#endif

// Returns `true` if `extension` was installed from the webstore, otherwise
// false.
bool ExtensionIsFromWebstore(const Extension& extension) {
  return extension.from_webstore() && !extension.was_installed_by_default() &&
         extension.location() == mojom::ManifestLocation::kInternal;
}

// Handles logic that only requires the ExtensionId and the BrowserContext.
// This is safe to call even when the extension is completely unloaded.
bool IsIncognitoEnabledForIdAndContext(const ExtensionId& extension_id,
                                       content::BrowserContext* context) {
#if BUILDFLAG(IS_CHROMEOS)
  // An OTR Profile is used for captive portal signin to hide PII from
  // captive portals (which require HTTP redirects to function).
  // However, for captive portal signin we do not want want to disable
  // extensions by default. (Proxies are explicitly disabled elsewhere).
  // See b/261727502 for details.
  PrefService* prefs = user_prefs::UserPrefs::Get(context);
  if (prefs) {
    const PrefService::Preference* captive_portal_pref =
        prefs->FindPreference(chromeos::prefs::kCaptivePortalSignin);
    if (captive_portal_pref && captive_portal_pref->GetValue()->GetBool()) {
      return true;
    }
  }
#endif

  // The ultimate fallback database check.
  return ExtensionPrefs::Get(context)->IsIncognitoEnabled(extension_id);
}

}  // namespace

bool CanBeIncognitoEnabled(const Extension* extension) {
  return IncognitoInfo::IsIncognitoAllowed(extension) &&
         (!extension->is_platform_app() ||
          extension->location() == mojom::ManifestLocation::kComponent);
}

bool IsIncognitoEnabled(const Extension* extension,
                        content::BrowserContext* context) {
  if (extension) {
    if (!CanBeIncognitoEnabled(extension)) {
      return false;
    }
    // If this is an existing component extension we always allow it to
    // work in incognito mode.
    if (Manifest::IsComponentLocation(extension->location())) {
      return true;
    }
    if (extension->is_login_screen_extension()) {
      return true;
    }
#if BUILDFLAG(IS_CHROMEOS)
    if (IsSigninProfileTestExtensionOnTestImage(extension)) {
      return true;
    }
#endif
  }

  return IsIncognitoEnabledForIdAndContext(extension->id(), context);
}

bool IsIncognitoEnabled(const ExtensionId& extension_id,
                        content::BrowserContext* context) {
  ExtensionRegistry* registry = ExtensionRegistry::Get(context);
  if (registry) {
    const Extension* extension =
        registry->enabled_extensions().GetByID(extension_id);
    if (extension) {
      return IsIncognitoEnabled(extension, context);
    }
  }

  return IsIncognitoEnabledForIdAndContext(extension_id, context);
}

bool CanCrossIncognito(const Extension* extension,
                       content::BrowserContext* context) {
  // We allow the extension to see events and data from another profile iff it
  // uses "spanning" behavior and it has incognito access. "split" mode
  // extensions only see events for a matching profile.
  CHECK(extension);
  return IsIncognitoEnabled(extension->id(), context) &&
         !IncognitoInfo::IsSplitMode(extension);
}

bool IsExtensionIdle(const std::string& extension_id,
                     content::BrowserContext* context) {
  std::vector<std::string> ids_to_check;
  ids_to_check.push_back(extension_id);

  const Extension* extension =
      ExtensionRegistry::Get(context)->enabled_extensions().GetByID(
          extension_id);
  if (extension && extension->is_shared_module()) {
    // We have to check all the extensions that use this shared module for idle
    // to tell whether it is really 'idle'.
    std::unique_ptr<ExtensionSet> dependents =
        ExtensionsBrowserClient::Get()
            ->GetSharedModuleService(context)
            ->GetDependentExtensions(extension);

    for (const auto& dependent : *dependents) {
      ids_to_check.push_back(dependent->id());
    }
  }

  ProcessManager* process_manager = ProcessManager::Get(context);
  ProcessMap* process_map = ProcessMap::Get(context);
  for (const auto& id : ids_to_check) {
    ExtensionHost* host = process_manager->GetBackgroundHostForExtension(id);
    if (host) {
      return false;
    }

    if (!process_manager->GetRenderFrameHostsForExtension(id).empty()) {
      return false;
    }

    // TODO(devlin): We can probably remove the checks above (for background
    // hosts and frame hosts). If an extension has any active frames, it should
    // have a dedicated process.
    if (process_map->ExtensionHasProcess(id)) {
      return false;
    }
  }
  return true;
}

bool IsPromptingEnabled() {
  return FeatureSwitch::prompt_for_external_extensions()->IsEnabled();
}

bool AllowFileAccess(const ExtensionId& extension_id,
                     content::BrowserContext* context) {
  return base::CommandLine::ForCurrentProcess()->HasSwitch(
             switches::kDisableExtensionsFileAccessCheck) ||
         ExtensionPrefs::Get(context)->AllowFileAccess(extension_id);
}

const std::string& GetPartitionDomainForExtension(const Extension* extension) {
  // Extensions use their own ID for a partition domain.
  return extension->id();
}

content::StoragePartitionConfig GetStoragePartitionConfigForExtensionId(
    const ExtensionId& extension_id,
    content::BrowserContext* browser_context) {
  if (ExtensionsBrowserClient::Get()->HasIsolatedStorage(extension_id,
                                                         browser_context)) {
    // For extensions with isolated storage, the |extension_id| is
    // the |partition_domain|. The |in_memory| and |partition_name| are only
    // used in guest schemes so they are cleared here.
    return content::StoragePartitionConfig::Create(
        browser_context, extension_id, std::string() /* partition_name */,
        false /*in_memory */);
  }

  return content::StoragePartitionConfig::CreateDefault(browser_context);
}

content::StoragePartition* GetStoragePartitionForExtensionId(
    const ExtensionId& extension_id,
    content::BrowserContext* browser_context,
    bool can_create) {
  auto storage_partition_config =
      GetStoragePartitionConfigForExtensionId(extension_id, browser_context);
  content::StoragePartition* storage_partition =
      browser_context->GetStoragePartition(storage_partition_config,
                                           can_create);
  return storage_partition;
}

content::ServiceWorkerContext* GetServiceWorkerContextForExtensionId(
    const ExtensionId& extension_id,
    content::BrowserContext* browser_context) {
  return GetStoragePartitionForExtensionId(extension_id, browser_context)
      ->GetServiceWorkerContext();
}

// This function is security sensitive. Bugs could cause problems that break
// restrictions on local file access or NaCl's validation caching. If you modify
// this function, please get a security review from a NaCl person.
bool MapUrlToLocalFilePath(const ExtensionSet* extensions,
                           const GURL& file_url,
                           bool use_blocking_api,
                           base::FilePath* file_path) {
  // Check that the URL is recognized by the extension system.
  const Extension* extension = extensions->GetExtensionOrAppByURL(file_url);
  if (!extension) {
    return false;
  }

  // This is a short-cut which avoids calling a blocking file operation
  // (GetFilePath()), so that this can be called on the non blocking threads. It
  // only handles a subset of the urls.
  if (!use_blocking_api) {
    if (file_url.SchemeIs(kExtensionScheme)) {
      std::string path = file_url.GetPath();
      base::TrimString(path, "/", &path);  // Remove first slash
      *file_path = extension->path().AppendASCII(path);
      return true;
    }
    return false;
  }

  std::string path = file_url.GetPath();
  ExtensionResource resource;

  if (SharedModuleInfo::IsImportedPath(path)) {
    // Check if this is a valid path that is imported for this extension.
    ExtensionId new_extension_id;
    std::string new_relative_path;
    SharedModuleInfo::ParseImportedPath(path, &new_extension_id,
                                        &new_relative_path);
    const Extension* new_extension = extensions->GetByID(new_extension_id);
    if (!new_extension) {
      return false;
    }

    if (!SharedModuleInfo::ImportsExtensionById(extension, new_extension_id)) {
      return false;
    }

    resource = new_extension->GetResource(new_relative_path);
  } else {
    // Check that the URL references a resource in the extension.
    resource = extension->GetResource(path);
  }

  if (resource.empty()) {
    return false;
  }

  // GetFilePath is a blocking function call.
  const base::FilePath resource_file_path = resource.GetFilePath();
  if (resource_file_path.empty()) {
    return false;
  }

  *file_path = resource_file_path;
  return true;
}

bool CanWithholdPermissionsFromExtension(const Extension& extension) {
  return CanWithholdPermissionsFromExtension(
      extension.id(), extension.GetType(), extension.location());
}

bool CanWithholdPermissionsFromExtension(const ExtensionId& extension_id,
                                         Manifest::Type type,
                                         mojom::ManifestLocation location) {
  // Some extensions must retain privilege to all requested host permissions.
  // Specifically, extensions that don't show up in chrome:extensions (where
  // withheld permissions couldn't be granted), extensions that are part of
  // chrome or corporate policy, and extensions that are allowlisted to script
  // everywhere must always have permission to run on a page.
  return ui_util::ShouldDisplayInExtensionSettings(type, location) &&
         !Manifest::IsPolicyLocation(location) &&
         !Manifest::IsComponentLocation(location) &&
         !PermissionsData::CanExecuteScriptEverywhere(extension_id, location);
}

int GetBrowserContextId(content::BrowserContext* context) {
  using ContextIdMap = std::map<base::UnguessableToken, int>;

  static int next_id = 0;
  static base::NoDestructor<ContextIdMap> context_map;

  // we need to get the original context to make sure we take the right context.
  content::BrowserContext* original_context =
      ExtensionsBrowserClient::Get()->GetOriginalContext(context);
  const base::UnguessableToken& context_id = original_context->UniqueToken();
  auto iter = context_map->find(context_id);
  if (iter == context_map->end()) {
    iter = context_map->insert(std::make_pair(context_id, next_id++)).first;
  }
  DCHECK(iter->second != kUnspecifiedContextId);
  return iter->second;
}

bool IsExtensionVisibleToContext(const Extension& extension,
                                 content::BrowserContext* browser_context) {
  // Renderers don't need to know about themes.
  if (extension.is_theme()) {
    return false;
  }

  // Only extensions enabled in incognito mode should be loaded in an incognito
  // renderer. However extensions which can't be enabled in the incognito mode
  // (e.g. platform apps) should also be loaded in an incognito renderer to
  // ensure connections from incognito tabs to such extensions work.
  return !browser_context->IsOffTheRecord() ||
         !CanBeIncognitoEnabled(&extension) ||
         IsIncognitoEnabled(extension.id(), browser_context);
}

void InitializeFileSchemeAccessForExtension(
    content::ChildProcessId render_process_id,
    const ExtensionId& extension_id,
    content::BrowserContext* browser_context) {
  ExtensionPrefs* prefs = ExtensionPrefs::Get(browser_context);
  // TODO(karandeepb): This should probably use
  // extensions::util::AllowFileAccess.
  if (prefs->AllowFileAccess(extension_id)) {
    // TODO(crbug.com/379869738) Remove GetUnsafeValue.
    content::ChildProcessSecurityPolicy::GetInstance()->GrantRequestScheme(
        render_process_id.GetUnsafeValue(), url::kFileScheme);
  }
}

const gfx::ImageSkia& GetDefaultAppIcon() {
  return *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
      IDR_APP_DEFAULT_ICON);
}

const gfx::ImageSkia& GetDefaultExtensionIcon() {
  return *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
      IDR_EXTENSION_DEFAULT_ICON);
}

ExtensionId GetExtensionIdForSiteInstance(
    content::SiteInstance& site_instance) {
  // <webview> guests always store the ExtensionId in the partition domain.
  const content::SecurityPrincipal& security_principal =
      site_instance.GetSecurityPrincipal();
  if (security_principal.IsGuest()) {
    return site_instance.GetSecurityPrincipal()
        .GetStoragePartitionConfig()
        .partition_domain();
  }

  // This works for both apps and extensions because the site has been
  // normalized to the extension URL for hosted apps.
  if (!security_principal.SchemeIs(kExtensionScheme)) {
    return ExtensionId();
  }

  // Navigating to a disabled (or uninstalled or not-yet-installed) extension
  // will set the site URL to chrome-extension://invalid.
  std::string_view maybe_extension_id =
      site_instance.GetSecurityPrincipal().GetHost();
  if (maybe_extension_id == "invalid") {
    return ExtensionId();
  }

  // Otherwise,`site_url.host()` should always be a valid extension id.  In
  // particular, navigations should never commit a URL that uses a dynamic,
  // GUID-based hostname (such navigations should redirect to the statically
  // known, extension-id-based hostname).
  DCHECK(crx_file::id_util::IdIsValid(maybe_extension_id))
      << "; maybe_extension_id = " << maybe_extension_id;
  return ExtensionId(maybe_extension_id);
}

std::string GetExtensionIdFromFrame(
    content::RenderFrameHost* render_frame_host) {
  const content::SiteInstance* site_instance =
      render_frame_host->GetSiteInstance();
  if (!site_instance->GetSecurityPrincipal().SchemeIs(kExtensionScheme)) {
    return std::string();
  }

  return std::string(site_instance->GetSecurityPrincipal().GetHost());
}

bool CanRendererHostExtensionOrigin(int render_process_id,
                                    const ExtensionId& extension_id,
                                    bool is_sandboxed) {
  url::Origin extension_origin =
      Extension::CreateOriginFromExtensionId(extension_id);
  if (is_sandboxed) {
    // If the extension frame is sandboxed, the corresponding process is only
    // allowed to host opaque origins, per crbug.com/325410297. Therefore,
    // convert the origin into an opaque origin, and note that HostsOrigin()
    // will still validate the extension ID in the origin's precursor.
    extension_origin = extension_origin.DeriveNewOpaqueOrigin();
  }
  auto* policy = content::ChildProcessSecurityPolicy::GetInstance();
  return policy->HostsOrigin(render_process_id, extension_origin);
}

bool CanRendererActOnBehalfOfExtension(
    const ExtensionId& extension_id,
    content::RenderFrameHost* render_frame_host,
    content::RenderProcessHost& render_process_host,
    bool include_user_scripts) {
  // TODO(lukasza): Some of the checks below can be restricted to specific
  // context types (e.g. an empty `extension_id` should not happen in an
  // extension context;  and the SiteInstance-based check should only be needed
  // for hosted apps).  Consider leveraging ProcessMap::GetMostLikelyContextType
  // to implement this kind of restrictions.  Note that
  // ExtensionFunctionDispatcher::CreateExtensionFunction already calls
  // GetMostLikelyContextType - some refactoring might be needed to avoid
  // duplicating the work.

  // Allow empty extension id (it seems okay to assume that no
  // extension-specific special powers will be granted without an extension id).
  // For instance, WebUI pages may call private APIs like developerPrivate,
  // settingsPrivate, metricsPrivate, and others. In these cases, there is no
  // associated extension ID.
  //
  // TODO(lukasza): Investigate if the exception below can be avoided if
  // `render_process_host` hosts HTTP origins (i.e. if the exception can be
  // restricted to NTP, and/or chrome://... cases.
  if (extension_id.empty()) {
    return true;
  }

  // Did `render_process_id` run a content script or user script from
  // `extension_id`?
  // TODO(crbug.com/40055126): Ideally, we'd only check content script/
  // user script status if the renderer claimed to be acting on behalf of the
  // corresponding type (e.g. mojom::ContextType::kContentScript). We evaluate
  // this later in ProcessMap::CanProcessHostContextType(), but we could be
  // stricter by including it here.
  if (ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
          render_process_host, extension_id) ||
      (ScriptInjectionTracker::DidProcessRunUserScriptFromExtension(
           render_process_host, extension_id) &&
       include_user_scripts)) {
    return true;
  }

  // CanRendererHostExtensionOrigin() needs to know if the extension is
  // sandboxed, so check the sandbox flags if this request is for an extension
  // frame. Note that extension workers cannot be sandboxed since workers aren't
  // supported in opaque origins.
  bool is_sandboxed =
      render_frame_host &&
      render_frame_host->IsSandboxed(network::mojom::WebSandboxFlags::kOrigin);

  // Can `render_process_id` host a chrome-extension:// origin (frame, worker,
  // etc.)?
  if (CanRendererHostExtensionOrigin(render_process_host.GetDeprecatedID(),
                                     extension_id, is_sandboxed)) {
    return true;
  }

  if (render_frame_host) {
    DCHECK_EQ(render_process_host.GetDeprecatedID(),
              render_frame_host->GetProcess()->GetDeprecatedID());
    content::SiteInstance& site_instance =
        *render_frame_host->GetSiteInstance();

    // Chrome Extension APIs can be accessed from some hosted apps.
    //
    // Today this is mostly needed by the Chrome Web Store's hosted app, but the
    // code below doesn't make this assumption and allows *all* hosted apps
    // based on the trustworthy, Browser-side information from the SiteInstance
    // / SiteURL.  This way the code is resilient to future changes + there are
    // concerns that `chrome.test.sendMessage` might already be exposed to
    // hosted apps (but maybe not covered by tests).
    //
    // Note that the condition below allows all extensions (i.e. not just hosted
    // apps), but hosted apps aren't covered by the
    // `CanRendererHostExtensionOrigin` call above (because the process lock of
    // hosted apps is based on a https://, rather than chrome-extension:// url).
    //
    // GuestView is explicitly excluded, because we don't want to allow
    // GuestViews to spoof the extension id of their host.
    if (!site_instance.GetSecurityPrincipal().IsGuest() &&
        extension_id == util::GetExtensionIdForSiteInstance(site_instance)) {
      return true;
    }
  }

  // Disallow any other cases.
  return false;
}

bool IsChromeApp(const ExtensionId& extension_id,
                 content::BrowserContext* context) {
  const Extension* extension =
      ExtensionRegistry::Get(context)->enabled_extensions().GetByID(
          extension_id);
  return extension->is_platform_app();
}

bool IsAppLaunchable(const ExtensionId& extension_id,
                     content::BrowserContext* context) {
  DisableReasonSet reason =
      ExtensionPrefs::Get(context)->GetDisableReasons(extension_id);
  return !reason.contains(disable_reason::DISABLE_UNSUPPORTED_REQUIREMENT) &&
         !reason.contains(disable_reason::DISABLE_CORRUPTED);
}

bool IsAppLaunchableWithoutEnabling(const ExtensionId& extension_id,
                                    content::BrowserContext* context) {
  return ExtensionRegistry::Get(context)->enabled_extensions().Contains(
      extension_id);
}

bool AnyCurrentlyInstalledExtensionIsFromWebstore(
    content::BrowserContext* context) {
  const ExtensionSet previously_installed_extensions =
      ExtensionRegistry::Get(context)->GenerateInstalledExtensionsSet();
  return std::ranges::any_of(previously_installed_extensions,
                             [](const auto& extension_ptr) {
                               return ExtensionIsFromWebstore(*extension_ptr);
                             });
}

bool IsExtensionDownload(const download::DownloadItem& download_item) {
  if (download_item.GetTargetDisposition() ==
      download::DownloadItem::TARGET_DISPOSITION_PROMPT) {
    return false;
  }

  if (download_item.GetMimeType() == Extension::kMimeType) {
    return true;
  }
  return false;
}

const GURL& GetURLForExtensionPermissionCheck(content::RenderFrameHost* rfh) {
  // Avoid `CHECK_NE(lifecycle_state(), LifecycleStateImpl::kSpeculative)` in
  // `content::RenderFrameHost::IsErrorDocument()` by checking for an empty
  // `GURL` first.
  if (!rfh || rfh->GetLastCommittedURL().is_empty()) {
    return GURL::EmptyGURL();
  }
  if (rfh->IsErrorDocument()) {
    return GURL::EmptyGURL();
  }
  return rfh->GetLastCommittedURL();
}

}  // namespace extensions::util
