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

#include <stddef.h>

#include <algorithm>
#include <iterator>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "components/favicon/core/favicon_service.h"
#include "components/favicon_base/favicon_util.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/common/bindings_policy.h"
#include "extensions/browser/extension_icon_placeholder.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/image_loader.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_resource.h"
#include "extensions/common/icons/extension_icon_set.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/common/manifest_handlers/incognito_info.h"
#include "net/base/file_stream.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/resource/resource_scale_factor.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using content::WebContents;
using extensions::Extension;
using extensions::URLOverrides;

namespace {

// The key to the override value for a page.
const char kEntry[] = "entry";
// The key to whether or not the override is active (i.e., can be used).
// Overrides may be inactive e.g. when an extension is disabled.
const char kActive[] = "active";

// Iterates over |list| and:
// - Converts any entries of the form <entry> to
//   { 'entry': <entry>, 'active': true }.
// - Removes any duplicate entries.
// We do the conversion because we previously stored these values as strings
// rather than objects.
// TODO(devlin): Remove the conversion once everyone's updated.
void InitializeOverridesList(base::ListValue& list) {
  base::ListValue migrated;
  std::set<std::string> seen_entries;
  for (auto& val : list) {
    base::DictValue new_dict;
    std::string entry_name;
    if (val.is_dict()) {
      const std::string* tmp = val.GetDict().FindString(kEntry);
      if (!tmp) {  // See comment about CHECK(success) in
                   // ForEachOverrideList.
        continue;
      }
      entry_name = *tmp;
      new_dict = val.GetDict().Clone();
    } else if (val.is_string()) {
      entry_name = val.GetString();
      new_dict.Set(kEntry, entry_name);
      new_dict.Set(kActive, true);
    } else {
      NOTREACHED();
    }

    // `entry_name` will be set by this point.
    bool inserted = seen_entries.insert(entry_name).second;
    if (inserted) {
      migrated.Append(std::move(new_dict));
    }
  }

  list = std::move(migrated);
}

// Adds |override| to |list|, or, if there's already an entry for the override,
// marks it as active.
void AddOverridesToList(base::ListValue& list, const GURL& override_url) {
  const std::string& spec = override_url.spec();
  for (auto& val : list) {
    std::string* entry = nullptr;
    base::DictValue* dict = val.GetIfDict();
    if (dict) {
      entry = dict->FindString(kEntry);
    }
    if (!entry) {
      NOTREACHED();
    }
    if (*entry == spec) {
      dict->Set(kActive, true);
      return;  // All done!
    }
    GURL entry_url(*entry);
    if (!entry_url.is_valid()) {
      NOTREACHED();
    }
    if (entry_url.GetHost() == override_url.GetHost()) {
      dict->Set(kActive, true);
      dict->Set(kEntry, spec);
      return;
    }
  }

  base::DictValue dict;
  dict.Set(kEntry, spec);
  dict.Set(kActive, true);
  // Add the entry to the front of the list.
  list.Insert(list.begin(), base::Value(std::move(dict)));
}

// Validates that each entry in |list| contains a valid url and points to an
// extension contained in |all_extensions| (and, if not, removes it).
void ValidateOverridesList(const extensions::ExtensionSet* all_extensions,
                           base::ListValue& list) {
  base::ListValue migrated;
  std::set<std::string> seen_hosts;
  for (auto& val : list) {
    std::string* entry = nullptr;
    if (val.is_dict()) {
      entry = val.GetDict().FindString(kEntry);
    }
    if (!entry) {
      NOTREACHED();
    }
    GURL override_url(*entry);
    if (!override_url.is_valid()) {
      continue;
    }

    if (!all_extensions->GetByID(override_url.GetHost())) {
      continue;
    }

    // If we've already seen this extension, remove the entry. Only retain the
    // most recent entry for each extension.
    if (!seen_hosts.insert(override_url.GetHost()).second) {
      continue;
    }

    migrated.Append(val.Clone());
  }

  list = std::move(migrated);
}

// Reloads the page in |web_contents| if it uses the same profile as |profile|
// and if the current URL is a chrome URL.
void UnregisterAndReplaceOverrideForWebContents(const std::string& page,
                                                Profile* profile,
                                                WebContents* web_contents) {
  if (Profile::FromBrowserContext(web_contents->GetBrowserContext()) !=
      profile) {
    return;
  }

  const GURL& url = web_contents->GetLastCommittedURL();
  if (!url.SchemeIs(content::kChromeUIScheme) || url.host() != page) {
    return;
  }

  // Don't use Reload() since |url| isn't the same as the internal URL that
  // NavigationController has.
  web_contents->GetController().LoadURL(
      url,
      content::Referrer::SanitizeForRequest(
          url,
          content::Referrer(url, network::mojom::ReferrerPolicy::kDefault)),
      ui::PAGE_TRANSITION_RELOAD, std::string());
}

enum class UpdateBehavior {
  kDeactivate,  // Mark 'active' as false.
  kRemove,      // Remove the entry from the list.
};

// Updates the entry (if any) for |override_url| in |overrides_list| according
// to |behavior|. Returns true if anything changed.
bool UpdateOverridesList(base::ListValue& overrides_list,
                         const std::string& override_url,
                         UpdateBehavior behavior) {
  auto iter = std::ranges::find_if(
      overrides_list, [&override_url](const base::Value& value) {
        if (!value.is_dict()) {
          return false;
        }
        const std::string* entry = value.GetDict().FindString(kEntry);
        return entry && *entry == override_url;
      });
  if (iter == overrides_list.end()) {
    return false;
  }

  switch (behavior) {
    case UpdateBehavior::kDeactivate: {
      // See comment about CHECK(success) in ForEachOverrideList.
      if (iter->is_dict()) {
        iter->GetDict().Set(kActive, false);
        break;
      }
      // Else fall through and erase the broken pref.
      [[fallthrough]];
    }
    case UpdateBehavior::kRemove:
      overrides_list.erase(iter);
      break;
  }
  return true;
}

// Updates each list referenced in |overrides| according to |behavior|.
void UpdateOverridesLists(Profile* profile,
                          const URLOverrides::URLOverrideMap& overrides,
                          UpdateBehavior behavior) {
  if (overrides.empty()) {
    return;
  }
  PrefService* prefs = profile->GetPrefs();
  ScopedDictPrefUpdate update(prefs,
                              ExtensionUrlOverrides::kExtensionURLOverrides);
  base::DictValue& all_overrides = update.Get();
  for (const auto& page_override_pair : overrides) {
    base::ListValue* page_overrides =
        all_overrides.FindList(page_override_pair.first);
    if (!page_overrides) {
      // If it's being unregistered it may or may not be in the list. Eg: On
      // uninstalling an externally loaded extension, which has not been enabled
      // once.
      // But if it's being deactivated, it should already be in the list.
      DCHECK_NE(behavior, UpdateBehavior::kDeactivate);
      continue;
    }
    if (UpdateOverridesList(*page_overrides, page_override_pair.second.spec(),
                            behavior)) {
      // This is the active override, so we need to find all existing
      // tabs for this override and get them to reload the original URL.
      base::RepeatingCallback<void(WebContents*)> callback =
          base::BindRepeating(&UnregisterAndReplaceOverrideForWebContents,
                              page_override_pair.first, profile);
      // Apply to all existing tabs.
      extensions::ExtensionTabUtil::ForEachTab(callback);
    }
  }
}

// Run favicon callback with image result. If no favicon was available then
// |image| will be empty.
void RunFaviconCallbackAsync(favicon_base::FaviconResultsCallback callback,
                             const gfx::Image& image) {
  std::vector<favicon_base::FaviconRawBitmapResult> favicon_bitmap_results;

  const std::vector<gfx::ImageSkiaRep>& image_reps =
      image.AsImageSkia().image_reps();
  for (const gfx::ImageSkiaRep& image_rep : image_reps) {
    std::optional<std::vector<uint8_t>> png_data =
        gfx::PNGCodec::EncodeBGRASkBitmap(image_rep.GetBitmap(),
                                          /*discard_transparency=*/false);
    if (png_data) {
      favicon_base::FaviconRawBitmapResult bitmap_result;
      bitmap_result.bitmap_data = base::MakeRefCounted<base::RefCountedBytes>(
          std::move(png_data.value()));
      bitmap_result.pixel_size =
          gfx::Size(image_rep.pixel_width(), image_rep.pixel_height());
      // Leave |bitmap_result|'s icon URL as the default of GURL().
      bitmap_result.icon_type = favicon_base::IconType::kFavicon;

      favicon_bitmap_results.push_back(bitmap_result);
    } else {
      NOTREACHED() << "Could not encode extension favicon";
    }
  }

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback), std::move(favicon_bitmap_results)));
}

const Extension* ValidateOverrideURL(const base::Value* override_url_value,
                                     const GURL& source_url,
                                     const extensions::ExtensionSet& extensions,
                                     GURL* override_url) {
  if (!override_url_value || !override_url_value->is_dict() ||
      !override_url_value->GetDict().FindBool(kActive).value_or(false) ||
      !override_url_value->GetDict().FindString(kEntry)) {
    return nullptr;
  }
  const std::string* const_override =
      override_url_value->GetDict().FindString(kEntry);
  std::string override = *const_override;
  if (!source_url.GetQuery().empty()) {
    override += "?" + source_url.GetQuery();
  }
  if (!source_url.GetRef().empty()) {
    override += "#" + source_url.GetRef();
  }
  *override_url = GURL(override);
  if (!override_url->is_valid()) {
    return nullptr;
  }
  return extensions.GetByID(override_url->GetHost());
}

// Fetches each list in the overrides dictionary and runs |callback| on it.
void ForEachOverrideList(
    Profile* profile,
    base::RepeatingCallback<void(base::ListValue&)> callback) {
  PrefService* prefs = profile->GetPrefs();
  ScopedDictPrefUpdate update(prefs,
                              ExtensionUrlOverrides::kExtensionURLOverrides);
  base::DictValue& all_overrides = update.Get();

  // We shouldn't modify the list during iteration. Generate the set of keys
  // instead.
  std::vector<std::string> keys;
  for (const auto entry : all_overrides) {
    keys.push_back(entry.first);
  }
  for (const std::string& key : keys) {
    base::ListValue* list = all_overrides.FindList(key);
    // In a perfect world, we could CHECK(list) here. Unfortunately, if a
    // user's prefs are mangled (by malware, user modification, hard drive
    // corruption, evil robots, etc), this will fail. Instead, delete the pref.
    if (!list) {
      all_overrides.Remove(key);
      continue;
    }
    callback.Run(*list);
  }
}

// A helper method to retrieve active overrides for the given |url|, if any. If
// |get_all| is true, this will retrieve all active overrides; otherwise it will
// return the highest-priority one (potentially early-out-ing). The resulting
// vector is ordered by priority.
std::vector<GURL> GetOverridesForChromeURL(
    const GURL& url,
    content::BrowserContext* browser_context,
    bool get_all) {
  // Only chrome: URLs can be overridden like this.
  DCHECK(url.SchemeIs(content::kChromeUIScheme));

  Profile* profile = Profile::FromBrowserContext(browser_context);
  const base::DictValue& overrides = profile->GetPrefs()->GetDict(
      ExtensionUrlOverrides::kExtensionURLOverrides);

  const base::ListValue* url_list = overrides.FindListByDottedPath(url.host());
  if (!url_list) {
    return {};  // No overrides present for this host.
  }

  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(browser_context);
  const extensions::ExtensionSet& extensions = registry->enabled_extensions();

  // Separate out overrides from non-component extensions (higher priority).
  std::vector<GURL> override_urls;
  std::vector<GURL> component_overrides;

  // Iterate over the URL list looking for suitable overrides.
  for (const auto& value : *url_list) {
    GURL override_url;
    const Extension* extension =
        ValidateOverrideURL(&value, url, extensions, &override_url);
    if (!extension) {
      // Invalid overrides are cleaned up on startup.
      continue;
    }

    // We only allow chrome: URL overrides in incognito mode if the extension
    // uses split mode, has been enabled in incognito and this is not a new tab
    // page override (we never allow the new tab page to be overridden in
    // incognito since we need to ensure users see details about what incognito
    // is (and isn't)).
    bool incognito_override_allowed =
        extensions::IncognitoInfo::IsSplitMode(extension) &&
        extensions::util::IsIncognitoEnabled(extension->id(), profile) &&
        url.host() != chrome::kChromeUINewTabHost;
    if (profile->IsOffTheRecord() && !incognito_override_allowed) {
      continue;
    }

    if (extensions::Manifest::IsComponentLocation(extension->location())) {
      component_overrides.push_back(override_url);
    } else {
      override_urls.push_back(override_url);
      if (!get_all) {  // Early out, since the highest-priority was found.
        DCHECK_EQ(1u, override_urls.size());
        return override_urls;
      }
    }
  }

  if (!get_all) {
    // Since component overrides are lower priority, we should only get here if
    // there are no non-component overrides.
    DCHECK(override_urls.empty());
    // Return the highest-priority component override, if any.
    if (component_overrides.size() > 1) {
      component_overrides.resize(1);
    }
    return component_overrides;
  }

  override_urls.insert(override_urls.end(),
                       std::make_move_iterator(component_overrides.begin()),
                       std::make_move_iterator(component_overrides.end()));
  return override_urls;
}

}  // namespace

const char ExtensionUrlOverrides::kExtensionURLOverrides[] =
    "extensions.chrome_url_overrides";

// static
void ExtensionUrlOverrides::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterDictionaryPref(kExtensionURLOverrides);
}

// static
bool ExtensionUrlOverrides::HandleChromeURLOverride(
    GURL* url,
    content::BrowserContext* browser_context) {
  if (!url->SchemeIs(content::kChromeUIScheme)) {
    return false;
  }

  std::vector<GURL> overrides =
      GetOverridesForChromeURL(*url, browser_context, /*get_all=*/false);
  if (overrides.empty()) {
    return false;
  }

  *url = overrides[0];
  return true;
}

// static
bool ExtensionUrlOverrides::HandleChromeURLOverrideReverse(
    GURL* url,
    content::BrowserContext* browser_context) {
  Profile* profile = Profile::FromBrowserContext(browser_context);
  const base::DictValue& overrides =
      profile->GetPrefs()->GetDict(kExtensionURLOverrides);

  // Find the reverse mapping based on the given URL. For example this maps the
  // internal URL
  // chrome-extension://eemcgdkfndhakfknompkggombfjjjeno/main.html#1 to
  // chrome://bookmarks/#1 for display in the omnibox.
  for (const auto dict_iter : overrides) {
    if (!dict_iter.second.is_list()) {
      continue;
    }

    for (const auto& list_iter : dict_iter.second.GetList()) {
      const std::string* override = nullptr;
      if (list_iter.is_dict()) {
        override = list_iter.GetDict().FindString(kEntry);
      }
      if (!override) {
        continue;
      }
      if (base::StartsWith(url->spec(), *override,
                           base::CompareCase::SENSITIVE)) {
        GURL original_url(content::kChromeUIScheme + std::string("://") +
                          dict_iter.first +
                          url->spec().substr(override->length()));
        *url = original_url;
        return true;
      }
    }
  }

  return false;
}

// static
const extensions::Extension* ExtensionUrlOverrides::GetExtensionControllingURL(
    const GURL& url,
    content::BrowserContext* browser_context) {
  GURL mutable_url(url);
  if (!HandleChromeURLOverride(&mutable_url, browser_context)) {
    return nullptr;
  }

  DCHECK_NE(url, mutable_url);
  DCHECK(mutable_url.SchemeIs(extensions::kExtensionScheme));

  const extensions::Extension* extension =
      extensions::ExtensionRegistry::Get(browser_context)
          ->enabled_extensions()
          .GetByID(mutable_url.GetHost());
  DCHECK(extension);

  return extension;
}

// static
size_t ExtensionUrlOverrides::GetNumberOfExtensionsOverridingURL(
    const GURL& url,
    content::BrowserContext* browser_context) {
  if (!url.SchemeIs(content::kChromeUIScheme)) {
    return 0;
  }

  return GetOverridesForChromeURL(url, browser_context, /*get_all=*/true)
      .size();
}

// static
void ExtensionUrlOverrides::InitializeChromeURLOverrides(Profile* profile) {
  ForEachOverrideList(profile, base::BindRepeating(&InitializeOverridesList));
}

// static
void ExtensionUrlOverrides::ValidateChromeURLOverrides(Profile* profile) {
  extensions::ExtensionSet all_extensions =
      extensions::ExtensionRegistry::Get(profile)
          ->GenerateInstalledExtensionsSet();

  ForEachOverrideList(
      profile, base::BindRepeating(&ValidateOverridesList, &all_extensions));
}

// static
void ExtensionUrlOverrides::RegisterOrActivateChromeURLOverrides(
    Profile* profile,
    const URLOverrides::URLOverrideMap& overrides) {
  if (overrides.empty()) {
    return;
  }
  PrefService* prefs = profile->GetPrefs();
  ScopedDictPrefUpdate update(prefs, kExtensionURLOverrides);
  base::DictValue& all_overrides = update.Get();
  for (const auto& page_override_pair : overrides) {
    base::ListValue* page_overrides_weak =
        all_overrides.FindListByDottedPath(page_override_pair.first);
    if (page_overrides_weak == nullptr) {
      page_overrides_weak =
          &all_overrides
               .SetByDottedPath(page_override_pair.first, base::ListValue())
               ->GetList();
    }
    AddOverridesToList(*page_overrides_weak, page_override_pair.second);
  }
}

// static
void ExtensionUrlOverrides::DeactivateChromeURLOverrides(
    Profile* profile,
    const URLOverrides::URLOverrideMap& overrides) {
  UpdateOverridesLists(profile, overrides, UpdateBehavior::kDeactivate);
}

// static
void ExtensionUrlOverrides::UnregisterChromeURLOverrides(
    Profile* profile,
    const URLOverrides::URLOverrideMap& overrides) {
  UpdateOverridesLists(profile, overrides, UpdateBehavior::kRemove);
}

// static
void ExtensionUrlOverrides::GetFaviconForURL(
    Profile* profile,
    const GURL& page_url,
    favicon_base::FaviconResultsCallback callback) {
  const Extension* extension =
      extensions::ExtensionRegistry::Get(profile)->enabled_extensions().GetByID(
          page_url.GetHost());
  if (!extension) {
    RunFaviconCallbackAsync(std::move(callback), gfx::Image());
    return;
  }

  // Fetch resources for all supported scale factors for which there are
  // resources. Load image reps for all supported scale factors (in addition to
  // 1x) immediately instead of in an as needed fashion to be consistent with
  // how favicons are requested for chrome:// and page URLs.
  const std::vector<float>& favicon_scales = favicon_base::GetFaviconScales();
  std::vector<extensions::ImageLoader::ImageRepresentation> info_list;
  for (float scale : favicon_scales) {
    int pixel_size = static_cast<int>(gfx::kFaviconSize * scale);
    extensions::ExtensionResource icon_resource =
        extensions::IconsInfo::GetIconResource(
            extension, pixel_size, ExtensionIconSet::Match::kBigger);

    if (!icon_resource.empty()) {
      ui::ResourceScaleFactor resource_scale_factor =
          ui::GetSupportedResourceScaleFactor(scale);
      info_list.emplace_back(
          icon_resource,
          extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE,
          gfx::Size(pixel_size, pixel_size), resource_scale_factor);
    }
  }

  if (info_list.empty()) {
    // Use the placeholder image when no default icon is available.
    gfx::Image placeholder_image =
        extensions::ExtensionIconPlaceholder::CreateImage(
            extension_misc::EXTENSION_ICON_SMALL, extension->name());
    gfx::ImageSkia placeholder_skia(placeholder_image.AsImageSkia());
    // Ensure the ImageSkia has representation at all scales we would use for
    // favicons.
    for (const auto scale_factor : ui::GetSupportedResourceScaleFactors()) {
      placeholder_skia.GetRepresentation(
          ui::GetScaleForResourceScaleFactor(scale_factor));
    }
    RunFaviconCallbackAsync(std::move(callback), gfx::Image(placeholder_skia));
  } else {
    // LoadImagesAsync actually can run callback synchronously. We want to force
    // async.
    extensions::ImageLoader::Get(profile)->LoadImagesAsync(
        extension, info_list,
        base::BindOnce(&RunFaviconCallbackAsync, std::move(callback)));
  }
}
