chromium/chrome/browser/chromeos/extensions/desk_api/desk_api_extension_manager.cc

// Copyright 2022 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/chromeos/extensions/desk_api/desk_api_extension_manager.h"

#include <memory>

#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "chrome/browser/enterprise/util/affiliation.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile_keyed_service_factory.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/browser_resources.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/url_pattern.h"
#include "ui/base/resource/resource_bundle.h"

using ::extensions::ComponentLoader;

namespace chromeos {
namespace {

// Tag in the manifest to be replaced.
constexpr char kDomainsTag[] = "\"$DOMAIN_LIST\"";

class DeskApiExtensionManagerFactory : public ProfileKeyedServiceFactory {
 public:
  DeskApiExtensionManagerFactory();
  DeskApiExtensionManagerFactory(const DeskApiExtensionManagerFactory&) =
      delete;
  DeskApiExtensionManagerFactory& operator=(
      const DeskApiExtensionManagerFactory&) = delete;
  ~DeskApiExtensionManagerFactory() override;

  // Returns an instance of `DeskApiExtensionManager` for the
  // given profile.
  DeskApiExtensionManager* GetForProfile(Profile* profile);

 private:
  KeyedService* BuildServiceInstanceFor(
      content::BrowserContext* context) const override;
};

DeskApiExtensionManagerFactory::DeskApiExtensionManagerFactory()
    : ProfileKeyedServiceFactory(
          "DeskApiExtensionManager",
          ProfileSelections::Builder()
              .WithRegular(ProfileSelection::kOriginalOnly)
              // TODO(crbug.com/40257657): Check if this service is needed in
              // Guest mode.
              .WithGuest(ProfileSelection::kOriginalOnly)
              // TODO(crbug.com/41488885): Check if this service is needed for
              // Ash Internals.
              .WithAshInternals(ProfileSelection::kOriginalOnly)
              .Build()) {}

DeskApiExtensionManagerFactory::~DeskApiExtensionManagerFactory() = default;

DeskApiExtensionManager* DeskApiExtensionManagerFactory::GetForProfile(
    Profile* profile) {
  DCHECK(profile);
  return static_cast<DeskApiExtensionManager*>(
      GetServiceForBrowserContext(profile, true));
}

KeyedService* DeskApiExtensionManagerFactory::BuildServiceInstanceFor(
    content::BrowserContext* context) const {
  auto* const profile = Profile::FromBrowserContext(context);
  auto* const component_loader = ::extensions::ExtensionSystem::Get(profile)
                                     ->extension_service()
                                     ->component_loader();
  return new DeskApiExtensionManager(
      component_loader, profile,
      std::make_unique<DeskApiExtensionManager::Delegate>());
}

}  // namespace

void DeskApiExtensionManager::Delegate::InstallExtension(
    ComponentLoader* component_loader,
    const std::string& manifest_content) {
  component_loader->Add(manifest_content,
                        base::FilePath(FILE_PATH_LITERAL("chromeos/desk_api")));
  // Force reload extension.
  component_loader->Reload(extension_misc::kDeskApiExtensionId);
}

void DeskApiExtensionManager::Delegate::UninstallExtension(
    ComponentLoader* component_loader) {
  component_loader->Remove(extension_misc::kDeskApiExtensionId);
}

bool DeskApiExtensionManager::Delegate::IsProfileAffiliated(
    Profile* profile) const {
  if (profile->IsOffTheRecord())
    return false;

  return ::enterprise_util::IsProfileAffiliated(profile);
}

bool DeskApiExtensionManager::Delegate::IsExtensionInstalled(
    ComponentLoader* component_loader) const {
  return component_loader->Exists(extension_misc::kDeskApiExtensionId);
}

// static
DeskApiExtensionManager* DeskApiExtensionManager::GetForProfile(
    Profile* profile) {
  return static_cast<DeskApiExtensionManagerFactory*>(GetFactory())
      ->GetForProfile(profile);
}

DeskApiExtensionManager::DeskApiExtensionManager(
    ComponentLoader* component_loader,
    Profile* profile,
    std::unique_ptr<DeskApiExtensionManager::Delegate> delegate)
    : component_loader_(component_loader),
      profile_(profile),
      delegate_(std::move(delegate)) {
  Init();
}

DeskApiExtensionManager::~DeskApiExtensionManager() = default;

// static
BrowserContextKeyedServiceFactory* DeskApiExtensionManager::GetFactory() {
  static base::NoDestructor<DeskApiExtensionManagerFactory> g_factory;
  return g_factory.get();
}

void DeskApiExtensionManager::Init() {
  LoadOrUnloadExtension();

  registrar_.Init(profile_->GetPrefs());

  if (delegate_->IsProfileAffiliated(profile_)) {
    // Setup registrar so it starts listening to relevant pref changes.
    registrar_.Add(::prefs::kDeskAPIThirdPartyAccessEnabled,
                   base::BindRepeating(&DeskApiExtensionManager::OnPrefChanged,
                                       weak_ptr_factory_.GetWeakPtr()));
    registrar_.Add(::prefs::kDeskAPIThirdPartyAllowlist,
                   base::BindRepeating(&DeskApiExtensionManager::OnPrefChanged,
                                       weak_ptr_factory_.GetWeakPtr()));
  }
}

bool DeskApiExtensionManager::CanInstallExtension() const {
  if (!delegate_->IsProfileAffiliated(profile_)) {
    return false;
  }

  const auto& pref_service = *profile_->GetPrefs();
  // Defaults to not enabled.
  if (!pref_service.HasPrefPath(::prefs::kDeskAPIThirdPartyAccessEnabled))
    return false;

  if (!pref_service.GetBoolean(::prefs::kDeskAPIThirdPartyAccessEnabled))
    return false;

  const std::string manifest = GetManifest();

  // Cannot install extension when there is no valid domains in allowlist.
  if (manifest.empty())
    return false;

  return true;
}

std::string DeskApiExtensionManager::GetManifest() const {
  const auto* pref_service = profile_->GetPrefs();
  std::vector<std::string> domains;

  // Prepare the domain allowlist to follow the Chrome extension Manifest V3
  // `externally_connectable` `matches` property format. See more
  // https://developer.chrome.com/docs/extensions/mv3/manifest/externally_connectable/#reference
  for (const auto& domain :
       pref_service->GetList(::prefs::kDeskAPIThirdPartyAllowlist)) {
    URLPattern pattern(URLPattern::SCHEME_ALL);
    if (pattern.Parse(domain.GetString()) !=
        URLPattern::ParseResult::kSuccess) {
      LOG(WARNING) << "Desk API ignored invalid URL pattern: "
                   << domain.GetString();
      continue;
    }

    domains.push_back("\"" + domain.GetString() + "\"");
  }

  if (domains.size() == 0) {
    return "";
  }

  const std::string domain_list = base::JoinString(domains, ",");

  std::string manifest_contents(
      ui::ResourceBundle::GetSharedInstance().GetRawDataResource(
          IDR_DESK_API_MANIFEST));
  DCHECK(manifest_contents.find(kDomainsTag) != std::string::npos);
  base::ReplaceFirstSubstringAfterOffset(&manifest_contents, 0, kDomainsTag,
                                         domain_list);

  return manifest_contents;
}

void DeskApiExtensionManager::OnPrefChanged() {
  LoadOrUnloadExtension();
}

void DeskApiExtensionManager::LoadOrUnloadExtension() {
  if (CanInstallExtension()) {
    // This will update the extension if the allowlist changes, even if the
    // extension version remains the same.
    delegate_->InstallExtension(component_loader_, GetManifest());
    return;
  }

  // Remove/unload component extension if installed because it does
  // not meet necessary pre-conditions.
  RemoveExtensionIfInstalled();
}

void DeskApiExtensionManager::RemoveExtensionIfInstalled() {
  if (delegate_->IsExtensionInstalled(component_loader_)) {
    delegate_->UninstallExtension(component_loader_);
  }
}

// static
void DeskApiExtensionManager::EnsureFactoryBuilt() {
  DeskApiExtensionManager::GetFactory();
}

}  // namespace chromeos