// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/components/security_interstitials/safe_browsing/pending_unsafe_resource_storage.h"

#import "base/functional/callback_helpers.h"
#import "base/memory/ptr_util.h"
#import "ios/components/security_interstitials/safe_browsing/ios_unsafe_resource_util.h"

using safe_browsing::SBThreatType;
using security_interstitials::UnsafeResource;

namespace {
// Returns whether a pending decision exists for `resource`.
bool IsUnsafeResourcePending(const UnsafeResource& resource) {
  SafeBrowsingUrlAllowList* allow_list = GetAllowListForResource(resource);
  GURL decision_url = SafeBrowsingUrlAllowList::GetDecisionUrl(resource);
  std::set<SBThreatType> pending_threat_types;
  return allow_list &&
         allow_list->IsUnsafeNavigationDecisionPending(decision_url,
                                                       &pending_threat_types) &&
         pending_threat_types.contains(resource.threat_type);
}
}  // namespace

#pragma mark - PendingUnsafeResourceStorage

PendingUnsafeResourceStorage::PendingUnsafeResourceStorage() = default;

PendingUnsafeResourceStorage::PendingUnsafeResourceStorage(
    const PendingUnsafeResourceStorage& other)
    : resource_(other.resource_) {
  UpdatePolicyObserver();
}

PendingUnsafeResourceStorage& PendingUnsafeResourceStorage::operator=(
    const PendingUnsafeResourceStorage& other) {
  resource_ = other.resource_;
  UpdatePolicyObserver();
  return *this;
}

PendingUnsafeResourceStorage::~PendingUnsafeResourceStorage() = default;

PendingUnsafeResourceStorage::PendingUnsafeResourceStorage(
    const security_interstitials::UnsafeResource& resource)
    : resource_(resource) {
  DCHECK(IsUnsafeResourcePending(resource));
  // Reset the resource's callback to prevent misuse.
  resource_.value().callback = base::DoNothing();
  // Create the policy observer for `resource`.
  UpdatePolicyObserver();
}

#pragma mark Private

void PendingUnsafeResourceStorage::UpdatePolicyObserver() {
  if (resource_) {
    policy_observer_ = ResourcePolicyObserver(this);
  } else {
    policy_observer_ = std::nullopt;
  }
}

void PendingUnsafeResourceStorage::ResetResource() {
  resource_ = std::nullopt;
  policy_observer_ = std::nullopt;
}

#pragma mark - PendingUnsafeResourceStorage::ResourcePolicyObserver

PendingUnsafeResourceStorage::ResourcePolicyObserver::ResourcePolicyObserver(
    PendingUnsafeResourceStorage* storage)
    : storage_(storage) {
  scoped_observation_.Observe(SafeBrowsingUrlAllowList::FromWebState(
      storage_->resource()->weak_web_state.get()));
}

PendingUnsafeResourceStorage::ResourcePolicyObserver::ResourcePolicyObserver(
    const ResourcePolicyObserver& other)
    : storage_(other.storage_) {
  scoped_observation_.Observe(SafeBrowsingUrlAllowList::FromWebState(
      storage_->resource()->weak_web_state.get()));
}

PendingUnsafeResourceStorage::ResourcePolicyObserver&
PendingUnsafeResourceStorage::ResourcePolicyObserver::operator=(
    const ResourcePolicyObserver& other) {
  storage_ = other.storage_;
  return *this;
}

PendingUnsafeResourceStorage::ResourcePolicyObserver::
    ~ResourcePolicyObserver() = default;

void PendingUnsafeResourceStorage::ResourcePolicyObserver::ThreatPolicyUpdated(
    SafeBrowsingUrlAllowList* allow_list,
    const GURL& url,
    safe_browsing::SBThreatType threat_type,
    SafeBrowsingUrlAllowList::Policy policy) {
  const UnsafeResource* resource = storage_->resource();
  if (policy == SafeBrowsingUrlAllowList::Policy::kPending ||
      url != resource->navigation_url || threat_type != resource->threat_type) {
    return;
  }

  storage_->ResetResource();
  // ResetResource() destroys `this`, so no additional code should be added.
}

void PendingUnsafeResourceStorage::ResourcePolicyObserver::
    ThreatPolicyBatchUpdated(
        SafeBrowsingUrlAllowList* allow_list,
        const GURL& url,
        const std::set<safe_browsing::SBThreatType>& threat_types,
        SafeBrowsingUrlAllowList::Policy policy) {
  const UnsafeResource* resource = storage_->resource();
  if (policy == SafeBrowsingUrlAllowList::Policy::kPending ||
      url != resource->navigation_url ||
      !threat_types.contains(resource->threat_type)) {
    return;
  }

  storage_->ResetResource();
  // ResetResource() destroys `this`, so no additional code should be added.
}

void PendingUnsafeResourceStorage::ResourcePolicyObserver::
    SafeBrowsingAllowListDestroyed(SafeBrowsingUrlAllowList* allow_list) {
  storage_->ResetResource();
  // ResetResource() destroys `this`, so no additional code should be added.
}
