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

#include <algorithm>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/barrier_closure.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "components/update_client/crx_update_item.h"
#include "components/update_client/protocol_definition.h"
#include "components/update_client/update_client.h"
#include "components/update_client/update_client_errors.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/delayed_install_manager.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/scoped_extension_keep_alive.h"
#include "extensions/browser/updater/extension_downloader.h"
#include "extensions/browser/updater/extension_update_data.h"
#include "extensions/browser/updater/update_data_provider.h"
#include "extensions/browser/updater/update_service_factory.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_id.h"

namespace extensions {

namespace {

UpdateService* update_service_override = nullptr;

// This set contains all Omaha attributes that is associated with extensions.
constexpr const char* kOmahaAttributes[] = {
    "_malware", "_esbAllowlist", "_potentially_uws", "_policy_violation"};

}  // namespace

UpdateService::InProgressUpdate::InProgressUpdate(base::OnceClosure callback,
                                                  bool install_immediately)
    : callback(std::move(callback)), install_immediately(install_immediately) {}
UpdateService::InProgressUpdate::~InProgressUpdate() = default;

UpdateService::InProgressUpdate::InProgressUpdate(
    UpdateService::InProgressUpdate&& other) = default;
UpdateService::InProgressUpdate& UpdateService::InProgressUpdate::operator=(
    UpdateService::InProgressUpdate&& other) = default;

// static
void UpdateService::SupplyUpdateServiceForTest(UpdateService* service) {
  update_service_override = service;
}

// static
UpdateService* UpdateService::Get(content::BrowserContext* context) {
  return update_service_override == nullptr
             ? UpdateServiceFactory::GetForBrowserContext(context)
             : update_service_override;
}

void UpdateService::Shutdown() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  if (update_data_provider_) {
    update_data_provider_->Shutdown();
    update_data_provider_ = nullptr;
  }
  update_client_ = nullptr;
  browser_context_ = nullptr;
}

void UpdateService::SendUninstallPing(const std::string& id,
                                      const base::Version& version,
                                      int reason) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(update_client_);

  update_client::CrxComponent crx;
  crx.app_id = id;
  crx.version = version;
  // A ScopedBrowserContextKeepAlive is bound into the callback to keep the
  // context alive throughout the operation.
  update_client_->SendPing(
      crx,
      {.event_type = update_client::protocol_request::kEventUninstall,
       .result = update_client::protocol_request::kEventResultSuccess,
       .error_code = 0,
       .extra_code1 = reason},
      base::BindOnce([](std::unique_ptr<ScopedBrowserContextKeepAlive>,
                        update_client::Error) {},
                     ExtensionsBrowserClient::Get()->CreateUpdaterKeepAlive(
                         browser_context_)));
}

void UpdateService::OnCrxStateChange(UpdateFoundCallback update_found_callback,
                                     const update_client::CrxUpdateItem& item) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // Custom attributes can only be sent for NOT_UPDATED/UPDATE_FOUND events.
  bool should_perform_action_on_omaha_attributes = false;

  switch (item.state) {
    case update_client::ComponentState::kUpToDate:
      should_perform_action_on_omaha_attributes = true;
      break;
    case update_client::ComponentState::kCanUpdate:
      should_perform_action_on_omaha_attributes = true;
      if (update_found_callback) {
        update_found_callback.Run(item.id, item.next_version);
      }
      break;
    case update_client::ComponentState::kNew:
    case update_client::ComponentState::kChecking:
    case update_client::ComponentState::kDownloading:
    case update_client::ComponentState::kDecompressing:
    case update_client::ComponentState::kPatching:
    case update_client::ComponentState::kUpdating:
    case update_client::ComponentState::kUpdated:
    case update_client::ComponentState::kUpdateError:
    case update_client::ComponentState::kRun:
      break;
  }

  if (should_perform_action_on_omaha_attributes) {
    base::DictValue attributes = GetExtensionOmahaAttributes(item);
    // Note that it's important to perform actions even if |attributes| is
    // empty, missing values may default to false and have associated logic.
    ExtensionSystem::Get(browser_context_)
        ->PerformActionBasedOnOmahaAttributes(item.id, attributes);
  }
}

UpdateService::UpdateService(
    content::BrowserContext* browser_context,
    scoped_refptr<update_client::UpdateClient> update_client,
    base::RepeatingCallback<void(const std::vector<std::string>&,
                                 base::OnceClosure)> cache_retainer)
    : browser_context_(browser_context),
      update_client_(update_client),
      cache_retainer_(cache_retainer) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  update_data_provider_ =
      base::MakeRefCounted<UpdateDataProvider>(browser_context_);
}

UpdateService::~UpdateService() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

void UpdateService::StartUpdateCheck(
    const ExtensionUpdateCheckParams& update_params,
    UpdateFoundCallback update_found_callback,
    base::OnceClosure callback) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(!update_params.update_info.empty());

  VLOG(2) << "UpdateService::StartUpdateCheck";

  if (!ExtensionsBrowserClient::Get()->IsBackgroundUpdateAllowed()) {
    VLOG(1) << "UpdateService - Extension update not allowed.";
    if (!callback.is_null()) {
      std::move(callback).Run();
    }
    return;
  }

  InProgressUpdate update =
      InProgressUpdate(std::move(callback), update_params.install_immediately);

  ExtensionUpdateDataMap update_data;
  std::vector<ExtensionId> all_ids;
  all_ids.reserve(update_params.update_info.size());
  std::vector<std::vector<ExtensionId>> update_ids;
  update_ids.reserve(update_params.update_info.size());
  for (const auto& update_info : update_params.update_info) {
    const ExtensionId& extension_id = update_info.first;

    DCHECK(!extension_id.empty());

    update.pending_extension_ids.insert(extension_id);

    ExtensionUpdateData data = update_info.second;
    if (data.is_corrupt_reinstall) {
      data.install_source = "reinstall";
    } else if (data.install_source.empty() &&
               update_params.priority ==
                   ExtensionUpdateCheckParams::FOREGROUND) {
      data.install_source = "ondemand";
    }
    if (update_ids.empty() || update_ids.back().size() >= 25) {
      update_ids.emplace_back();
    }
    all_ids.push_back(extension_id);
    update_ids.back().push_back(extension_id);
    update_data.insert(std::make_pair(extension_id, data));
  }

  base::RepeatingCallback closure = base::BarrierClosure(
      update_ids.size(),
      base::BindOnce(&UpdateService::UpdateCheckComplete,
                     weak_ptr_factory_.GetWeakPtr(), std::move(update)));

  base::RepeatingCallback<void(
      const std::vector<std::string>&,
      base::OnceCallback<void(
          const std::vector<std::optional<update_client::CrxComponent>>&)>)>
      get_data = base::BindRepeating(
          &UpdateDataProvider::GetData, update_data_provider_,
          update_params.install_immediately, std::move(update_data));

  base::OnceClosure do_update_check = base::DoNothing();
  for (const std::vector<std::string>& update_id_group : update_ids) {
    do_update_check =
        std::move(do_update_check)
            .Then(base::BindOnce(
                &update_client::UpdateClient::Update, update_client_,
                update_id_group, get_data,
                base::BindRepeating(&UpdateService::OnCrxStateChange,
                                    weak_ptr_factory_.GetWeakPtr(),
                                    update_found_callback),
                update_params.priority ==
                    ExtensionUpdateCheckParams::FOREGROUND,
                base::BindOnce(
                    [](base::RepeatingClosure callback,
                       update_client::Error /*error*/) { callback.Run(); },
                    closure)));
  }
  cache_retainer_.Run(all_ids, std::move(do_update_check));
}

void UpdateService::UpdateCheckComplete(InProgressUpdate update) {
  VLOG(2) << "UpdateService::UpdateCheckComplete";
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // When no update is found or there's an update error, a previous update
  // check might have queued an update for this extension because it was in
  // use at the time. We should ask for the install of the queued update now
  // if it's ready.
  DelayedInstallManager* delayed_install_manager =
      DelayedInstallManager::Get(browser_context_);
  if (update.install_immediately) {
    for (const ExtensionId& extension_id : update.pending_extension_ids) {
      if (delayed_install_manager->GetPendingExtensionUpdate(extension_id)) {
        delayed_install_manager->FinishDelayedInstallationIfReady(
            extension_id, /*install_immediately=*/true);
      }
    }
  }

  if (!update.callback.is_null()) {
    std::move(update.callback).Run();
  }
}

void UpdateService::AddUpdateClientObserver(
    update_client::UpdateClient::Observer* observer) {
  if (update_client_) {
    update_client_->AddObserver(observer);
  }
}

void UpdateService::RemoveUpdateClientObserver(
    update_client::UpdateClient::Observer* observer) {
  if (update_client_) {
    update_client_->RemoveObserver(observer);
  }
}

base::DictValue UpdateService::GetExtensionOmahaAttributes(
    const update_client::CrxUpdateItem& update_item) {
  base::DictValue attributes;

  for (const char* key : kOmahaAttributes) {
    auto iter = update_item.custom_updatecheck_data.find(key);
    // This is assuming that the values of the keys are "true", "false",
    // or does not exist.
    // Only create the attribute if it's defined in the custom update check
    // data. We want to distinguish true, false and undefined values.
    if (iter != update_item.custom_updatecheck_data.end()) {
      attributes.Set(key, iter->second == "true");
    }
  }
  return attributes;
}
}  // namespace extensions
