// Copyright 2023 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/cws_info_service.h"

#include <algorithm>
#include <optional>
#include <string>
#include <string_view>

#include "base/containers/fixed_flat_map.h"
#include "base/containers/queue.h"
#include "base/features.h"
#include "base/i18n/time_formatting.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "components/prefs/pref_service.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "extensions/browser/cws_item_service.pb.h"
#include "extensions/browser/extension_management_client.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/pref_names.h"
#include "extensions/buildflags/buildflags.h"
#include "google_apis/common/api_key_request_util.h"
#include "google_apis/google_api_keys.h"
#include "net/base/load_flags.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace {

constexpr int kMaxExtensionIdsPerRequest = 3;
constexpr int kMaxRetriesPerRequest = 2;

// Default check and fetch intervals.
constexpr int kCheckIntervalSeconds = 1 * 60 * 60;
constexpr int kFetchIntervalSeconds = 24 * 60 * 60;
// Fast mode check and fetch intervals. These intervals are used to
// facilitate end-end testing.
constexpr int kFastStartupCheckDelaySeconds = 30;
constexpr int kFastCheckIntervalSeconds = 1 * 60;
constexpr int kFastFetchIntervalSeconds = 3 * 60;

constexpr char kRequestUrl[] =
    "https://chromewebstore.googleapis.com/v2/items/-/storeMetadata:batchGet";
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("cws_info_service", R"(
      semantics {
        sender: "CWS Info Service"
        description:
          "Sends ids of currently installed extensions that update from the "
          "the Chrome Web Store to fetch their store metadata. The metadata "
          "includes information such as an extension's current publish status "
          "which is used to enforce the ExtensionUnpublishedAvailability "
          "policy to disable the extension. "
        trigger:
          "Periodic fetch of metadata information once every 24 hours. A fetch "
          "is also triggered at Chrome or profile startup and when the "
          "ExtensionUnpublishedAvailability policy setting changes."
        user_data {
          type: PROFILE_DATA
        }
        data:
          "Ids of the currently installed extensions that update from the "
          "Chrome Web Store."
        destination: GOOGLE_OWNED_SERVICE
        last_reviewed: "2023-04-06"
        internal {
          contacts {
            email: "anunoy@chromium.org"
          }
        }
      }
      policy {
        cookies_allowed: NO
        setting:
          "This feature cannot be disabled in settings. It will only be "
          "triggered if the user has installed extensions from the store."
        policy_exception_justification: "Not implemented."
      })");

// CWS Info pref keys.
constexpr char kCWSInfo[] = "cws-info";
constexpr char kIsPresent[] = "is-present";
constexpr char kIsLive[] = "is-live";
constexpr char kLastUpdateTimeMillis[] = "last-updated-time-millis";
constexpr char kViolationType[] = "violation-type";
constexpr char kUnpublishedLongAgo[] = "unpublished-long-ago";
constexpr char kNoPrivacyPractice[] = "no-privacy-practice";
constexpr const char* kLabels[] = {kUnpublishedLongAgo, kNoPrivacyPractice};

// Proto conversion helpers.
// Helpers to convert extension id <-> name field in protos.
// name format: items/{itemId}/storeMetadata
std::string GetIdFromName(const std::string& name) {
  std::string id;
  base::StringTokenizer t(name, "/");
  if (t.GetNext() && t.GetNext()) {
    id = t.token();
  }
  return id;
}
std::string GetNameFromId(const std::string& id) {
  return "items/" + id + "/storeMetadata";
}

// Whether or not to skip the check if the build includes the Google Chrome API
// key. Used for testing.
bool skip_api_key_check_for_testing = false;

// Histogram helpers.
void RecordFetchSuccess(bool success) {
  base::UmaHistogramBoolean("Extensions.CWSInfoService.FetchSuccess", success);
}
void RecordMetadataChanged(bool changed) {
  base::UmaHistogramBoolean("Extensions.CWSInfoService.MetadataChanged",
                            changed);
}
void RecordNumRequestsInFetch(int num_requests) {
  base::UmaHistogramCounts100("Extensions.CWSInfoService.NumRequestsInFetch",
                              num_requests);
}
void RecordNetworkHistograms(const network::SimpleURLLoader* url_loader) {
  int net_error = url_loader->NetError();
  int response_code = 0;
  if (url_loader->ResponseInfo() && url_loader->ResponseInfo()->headers) {
    response_code = url_loader->ResponseInfo()->headers->response_code();
  }
  base::UmaHistogramSparse(
      "Extensions.CWSInfoService.NetworkResponseCodeOrError",
      net_error == net::OK || net_error == net::ERR_HTTP_RESPONSE_CODE_FAILURE
          ? response_code
          : net_error);
  if (net_error == net::OK && response_code == net::HTTP_OK) {
    base::UmaHistogramExactLinear(
        "Extensions.CWSInfoService.NetworkRetriesTillSuccess",
        url_loader->GetNumRetries(), kMaxRetriesPerRequest + 1);
  } else {
    DVLOG(1) << "Request net error:" << net_error
             << ", response code:" << response_code;
  }
}

}  // namespace

namespace extensions {

// Increase the frequency of periodic retrieval of extensions metadata from
// CWS. This feature is used only for testing purposes.
BASE_FEATURE(kCWSInfoFastCheck, base::FEATURE_DISABLED_BY_DEFAULT);

namespace {

base::DictValue GetDictFromStoreMetadataProto(const StoreMetadata* metadata) {
  base::DictValue dict;
  if (!metadata) {
    dict.Set(kIsPresent, false);
  } else {
    dict.Set(kIsPresent, true);
    dict.Set(kIsLive, metadata->is_live());
    dict.Set(kLastUpdateTimeMillis,
             base::NumberToString(metadata->last_update_time_millis()));
    dict.Set(kViolationType,
             static_cast<int>(CWSInfoService::GetViolationTypeFromString(
                 metadata->violation_type())));

    const auto& proto_labels = metadata->labels();
    for (const auto* label : kLabels) {
      dict.Set(label, std::ranges::contains(proto_labels, label));
    }
  }

  return dict;
}

// Saves CWS info if it is different from that currently saved in extension
// prefs.
bool SaveInfoIfChanged(ExtensionPrefs* extension_prefs,
                       const std::string& id,
                       const StoreMetadata* new_info) {
  bool saved = false;

  const base::DictValue* saved_dict =
      extension_prefs->ReadPrefAsDict(id, kCWSInfo);
  base::DictValue new_dict = GetDictFromStoreMetadataProto(new_info);
  if (!saved_dict || *saved_dict != new_dict) {
    // The metadata is new or is different from that saved in extension prefs.
    saved = true;
    extension_prefs->SetDictionaryPref(
        id, {kCWSInfo, kDictionary, PrefScope::kExtensionSpecific},
        std::move(new_dict));
  }

  return saved;
}

int GetNextFetchInterval() {
  // jitter fetch interval by +/- 25%
  double jitter_factor = base::RandDouble() * 0.5 + 0.75;
  return base::FeatureList::IsEnabled(kCWSInfoFastCheck)
             ? kFastFetchIntervalSeconds
             : kFetchIntervalSeconds * jitter_factor;
}

}  // namespace

// Stores context information about a CWS info fetch operation.
struct CWSInfoService::FetchContext {
  struct Request {
    ExtensionIdSet ids;
    BatchGetStoreMetadatasRequest proto;
  };
  base::queue<Request> requests;
  // Indicates if the metadata retrieved is different from that currently saved.
  bool metadata_changed = false;
};

CWSInfoService::CWSInfoService(content::BrowserContext* browser_context)
    : browser_context_(browser_context),
      pref_service_(user_prefs::UserPrefs::Get(browser_context)),
      extension_prefs_(ExtensionPrefs::Get(browser_context)),
      extension_registry_(ExtensionRegistry::Get(browser_context)),
      url_loader_factory_(browser_context->GetDefaultStoragePartition()
                              ->GetURLLoaderFactoryForBrowserProcess()),
      max_ids_per_request_(kMaxExtensionIdsPerRequest),
      current_fetch_interval_secs_(GetNextFetchInterval()) {
  // Vary the startup check out between 30s to 10min, unless FastCheck
  // option is enabled.
  startup_delay_secs_ = base::FeatureList::IsEnabled(kCWSInfoFastCheck)
                            ? kFastStartupCheckDelaySeconds
                            : base::RandIntInclusive(/*min=*/30, /*max=*/600);
  ScheduleCheck(startup_delay_secs_);
}

CWSInfoService::CWSInfoService() = default;
CWSInfoService::~CWSInfoService() = default;

void CWSInfoService::Shutdown() {
  info_check_timer_.Stop();
}

std::optional<bool> CWSInfoService::IsLiveInCWS(
    const Extension& extension) const {
  const base::DictValue* cws_info_dict =
      extension_prefs_->ReadPrefAsDict(extension.id(), kCWSInfo);
  if (cws_info_dict == nullptr) {
    return std::nullopt;
  }
  if (!cws_info_dict->FindBool(kIsPresent).value_or(false)) {
    return false;
  }
  return cws_info_dict->FindBool(kIsLive).value_or(false);
}

std::optional<CWSInfoService::CWSInfo> CWSInfoService::GetCWSInfo(
    const Extension& extension) const {
  const base::DictValue* cws_info_dict =
      extension_prefs_->ReadPrefAsDict(extension.id(), kCWSInfo);
  if (cws_info_dict == nullptr) {
    return std::nullopt;
  }
  CWSInfo info;
  info.is_present = cws_info_dict->FindBool(kIsPresent).value_or(false);

  if (info.is_present) {
    info.is_live = cws_info_dict->FindBool(kIsLive).value_or(false);
    const std::string* last_update_time_millis_str =
        cws_info_dict->FindString(kLastUpdateTimeMillis);
    int64_t last_update_time_millis = 0;
    if (last_update_time_millis_str &&
        base::StringToInt64(*last_update_time_millis_str,
                            &last_update_time_millis)) {
      info.last_update_time =
          base::Time::FromMillisecondsSinceUnixEpoch(last_update_time_millis);
    }

    info.violation_type = static_cast<CWSViolationType>(
        cws_info_dict->FindInt(kViolationType).value_or(0));
    info.unpublished_long_ago =
        cws_info_dict->FindBool(kUnpublishedLongAgo).value_or(false);
    info.no_privacy_practice =
        cws_info_dict->FindBool(kNoPrivacyPractice).value_or(false);
  }

  return std::make_optional<CWSInfo>(info);
}

void CWSInfoService::CheckAndMaybeFetchInfo() {
  CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

  // Do nothing unless an official api key is configured OR
  // the api key check is skipped for testing.
  // Note that there will be no periodic checking after this since we
  // return immediately without scheduling a future check.
  if (!google_apis::IsGoogleChromeAPIKeyUsed() &&
      !skip_api_key_check_for_testing) {
    return;
  }

  // If a fetch is already in progress, don't do anything.
  if (active_fetch_) {
    return;
  }

  base::TimeDelta elapsed_time =
      base::Time::Now() -
      pref_service_->GetTime(pref_names::kCWSInfoFetchErrorTimestamp);
  // If there was a previous fetch error, wait a full fetch interval before
  // retrying.
  if (elapsed_time >= base::Seconds(current_fetch_interval_secs_)) {
    elapsed_time = base::Time::Now() -
                   pref_service_->GetTime(pref_names::kCWSInfoTimestamp);
    // Enough time has elapsed since the last successful fetch.
    bool data_refresh_needed =
        elapsed_time >= base::Seconds(current_fetch_interval_secs_);

    bool new_info_requested = false;
    std::unique_ptr<FetchContext> fetch_context =
        CreateRequests(new_info_requested);

    if ((data_refresh_needed || new_info_requested) && fetch_context) {
      // Stop the check timer in case it is running. This can happen if we got
      // here because of an out-of-cycle fetch.
      info_check_timer_.Stop();
      // Save the fetch context and send the (first) request.
      active_fetch_ = std::move(fetch_context);
      RecordNumRequestsInFetch(active_fetch_->requests.size());
      current_fetch_interval_secs_ = GetNextFetchInterval();
      SendRequest();
      return;
    }
  }

  // No info request necessary at this time. Schedule the next check.
  int check_interval_seconds = base::FeatureList::IsEnabled(kCWSInfoFastCheck)
                                   ? kFastCheckIntervalSeconds
                                   : kCheckIntervalSeconds;
  ScheduleCheck(check_interval_seconds);
}

void CWSInfoService::ScheduleCheck(int seconds) {
  if (base::features::IsReducePPMsEnabled() && !info_check_timer_.IsRunning()) {
    info_check_timer_.SetTaskRunner(
        content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT}));
  }

  info_check_timer_.Start(FROM_HERE, base::Seconds(seconds), this,
                          &CWSInfoService::CheckAndMaybeFetchInfo);
}

std::unique_ptr<CWSInfoService::FetchContext> CWSInfoService::CreateRequests(
    bool& new_info_requested) {
  new_info_requested = false;

  auto* extension_mgmt_client =
      ExtensionsBrowserClient::Get()->GetExtensionManagementClient(
          browser_context_);
  if (!extension_mgmt_client) {
    return nullptr;
  }
  ExtensionSet installed_extensions =
      extension_registry_->GenerateInstalledExtensionsSet();
  if (installed_extensions.empty()) {
    return nullptr;
  }

  auto fetch_context = std::make_unique<FetchContext>();
  FetchContext::Request* request = nullptr;
  int num_ids_added_in_request = 0;
  for (const auto& extension : installed_extensions) {
    if (extension_mgmt_client->UpdatesFromWebstore(*extension) == false) {
      continue;
    }
    if (extension_prefs_->ReadPrefAsDict(extension->id(), kCWSInfo) ==
        nullptr) {
      // This extension does not already have CWS info saved. Flag this as a new
      // info request.
      new_info_requested = true;
    }
    if (num_ids_added_in_request == 0) {
      // Create a new request context.
      fetch_context->requests.emplace();
      request = &fetch_context->requests.back();
      request->proto.set_parent("items/-");
    }
    request->proto.add_names(GetNameFromId(extension->id()));
    request->ids.emplace(extension->id());
    num_ids_added_in_request++;
    if (num_ids_added_in_request == max_ids_per_request_) {
      // Max ids reached for the request context. Reset the count to create
      // a new context for the remaining ids.
      num_ids_added_in_request = 0;
    }
  }

  if (fetch_context->requests.empty()) {
    // No extensions require a CWS info fetch.
    return nullptr;
  }

  // Return the fetch context - contains information about number of requests to
  // send and which ids are included in each request.
  return fetch_context;
}

void CWSInfoService::SendRequest() {
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = GURL(kRequestUrl);
  // A POST request is sent with an override to GET due to server requirements.
  resource_request->method = "POST";
  resource_request->load_flags = net::LOAD_DISABLE_CACHE;
  resource_request->headers.SetHeader("X-HTTP-Method-Override", "GET");
  google_apis::AddAPIKeyToRequest(*resource_request, google_apis::GetAPIKey());
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
                                                 kTrafficAnnotation);
  url_loader_->SetRetryOptions(kMaxRetriesPerRequest,
                               network::SimpleURLLoader::RETRY_ON_5XX);
  std::string request_str =
      active_fetch_->requests.front().proto.SerializeAsString();
  url_loader_->AttachStringForUpload(request_str, "application/x-protobuf");
  url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
      url_loader_factory_.get(),
      base::BindOnce(&CWSInfoService::OnResponseReceived,
                     weak_factory_.GetWeakPtr()));
  info_requests_++;
}

void CWSInfoService::OnResponseReceived(std::optional<std::string> response) {
  CHECK(url_loader_);
  RecordNetworkHistograms(url_loader_.get());

  bool error = false;
  if (response) {
    BatchGetStoreMetadatasResponse response_proto;
    if (response_proto.ParseFromString(*response) == true) {
      info_responses_++;
      if (MaybeSaveResponseToPrefs(response_proto)) {
        info_changes_++;
        active_fetch_->metadata_changed = true;
      }
    } else {
      DVLOG(1) << "Failed to parse response: " << *response;
      info_errors_++;
      error = true;
    }
  } else {
    info_errors_++;
    error = true;
  }

  if (error) {
    // Record the fetch error timestamp. This timestamp is used to
    // wait at least one fetch interval after an error before
    // attempting another fetch.
    pref_service_->SetTime(pref_names::kCWSInfoFetchErrorTimestamp,
                           base::Time::Now());
  } else {
    // Info response received without any errors. Remove the request object
    // from the request queue.
    active_fetch_->requests.pop();
    if (!active_fetch_->requests.empty()) {
      // Request info for the next batch of extension ids.
      SendRequest();
      return;
    }

    // All requests completed. Store "freshness" timestamp in global extension
    // prefs.
    pref_service_->SetTime(pref_names::kCWSInfoTimestamp, base::Time::Now());

    RecordMetadataChanged(active_fetch_->metadata_changed);
    if (active_fetch_->metadata_changed) {
      // Notify observers if the metadata changed.
      for (auto& observer : observers_) {
        observer.OnCWSInfoChanged();
      }
    }
  }

  // All requests completed successfully OR a request failed. In either case,
  // schedule the next check.
  RecordFetchSuccess(!error);
  active_fetch_.reset();
  int check_interval_seconds = base::FeatureList::IsEnabled(kCWSInfoFastCheck)
                                   ? kFastCheckIntervalSeconds
                                   : kCheckIntervalSeconds;
  ScheduleCheck(check_interval_seconds);
}

bool CWSInfoService::MaybeSaveResponseToPrefs(
    const BatchGetStoreMetadatasResponse& response_proto) {
  bool store_metadata_changed = false;

  for (const auto& metadata : response_proto.store_metadatas()) {
    std::string id = GetIdFromName(metadata.name());
    active_fetch_->requests.front().ids.erase(id);
    if (extension_prefs_->HasPrefForExtension(id) == false) {
      continue;
    }
    if (SaveInfoIfChanged(extension_prefs_, id, &metadata)) {
      store_metadata_changed = true;
    }
  }

  // Process any requested ids missing from the response. These ids represent
  // extensions that are no longer available from the store.
  for (const auto& id : active_fetch_->requests.front().ids) {
    if (extension_prefs_->HasPrefForExtension(id) == false) {
      continue;
    }
    if (SaveInfoIfChanged(extension_prefs_, id, nullptr)) {
      store_metadata_changed = true;
    }
  }

  return store_metadata_changed;
}

void CWSInfoService::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void CWSInfoService::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

static_assert(static_cast<int>(CWSInfoService::CWSViolationType::kUnknown) == 4,
              "GetViolationTypeFromString needs to be updated to match "
              "CWSInfoService::CWSViolationType");
// static:
CWSInfoService::CWSViolationType CWSInfoService::GetViolationTypeFromString(
    const std::string& violation_type_str) {
  static constexpr auto violation_type_str_map =
      base::MakeFixedFlatMap<std::string_view,
                             CWSInfoService::CWSViolationType>(
          {{"none", CWSInfoService::CWSViolationType::kNone},
           {"malware", CWSInfoService::CWSViolationType::kMalware},
           {"policy-violation", CWSInfoService::CWSViolationType::kPolicy},
           {"minor-policy-violation",
            CWSInfoService::CWSViolationType::kMinorPolicy}});

  const auto it = violation_type_str_map.find(violation_type_str);
  return it != violation_type_str_map.end() ? it->second
                                            : CWSViolationType::kUnknown;
}

void CWSInfoService::SetMaxExtensionIdsPerRequestForTesting(int max) {
  max_ids_per_request_ = max;
}

std::string CWSInfoService::GetRequestURLForTesting() const {
  return kRequestUrl;
}

int CWSInfoService::GetFetchIntervalForTesting() const {
  return current_fetch_interval_secs_;
}

int CWSInfoService::GetStartupDelayForTesting() const {
  return startup_delay_secs_;
}

int CWSInfoService::GetCheckIntervalForTesting() const {
  return kCheckIntervalSeconds;
}

base::Time CWSInfoService::GetCWSInfoTimestampForTesting() const {
  return pref_service_->GetTime(pref_names::kCWSInfoTimestamp);
}

base::Time CWSInfoService::GetCWSInfoFetchErrorTimestampForTesting() const {
  return pref_service_->GetTime(pref_names::kCWSInfoFetchErrorTimestamp);
}

// static
void CWSInfoService::SetSkipApiCheckForTesting(bool skip_api_key_check) {
  skip_api_key_check_for_testing = skip_api_key_check;
}

void CWSInfoService::SetSharedURLLoaderFactoryForTesting(  // IN-TEST
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
  url_loader_factory_ = url_loader_factory;
}

}  // namespace extensions
