// Copyright (c) 2011 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "libcef/browser/origin_whitelist_impl.h"

#include <string>
#include <vector>

#include "include/cef_origin_whitelist.h"
#include "libcef/browser/browser_manager.h"
#include "libcef/browser/context.h"
#include "libcef/browser/thread_util.h"

#include "base/bind.h"
#include "base/lazy_instance.h"
#include "base/synchronization/lock.h"
#include "cef/libcef/common/mojom/cef.mojom.h"
#include "chrome/common/webui_url_constants.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/url_constants.h"
#include "extensions/common/constants.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace {

// Class that manages cross-origin whitelist registrations.
class CefOriginWhitelistManager {
 public:
  CefOriginWhitelistManager() = default;

  CefOriginWhitelistManager(const CefOriginWhitelistManager&) = delete;
  CefOriginWhitelistManager& operator=(const CefOriginWhitelistManager&) =
      delete;

  // Retrieve the singleton instance.
  static CefOriginWhitelistManager* GetInstance();

  bool AddOriginEntry(const std::string& source_origin,
                      const std::string& target_protocol,
                      const std::string& target_domain,
                      bool allow_target_subdomains) {
    auto info = cef::mojom::CrossOriginWhiteListEntry::New();
    info->source_origin = source_origin;
    info->target_protocol = target_protocol;
    info->target_domain = target_domain;
    info->allow_target_subdomains = allow_target_subdomains;

    {
      base::AutoLock lock_scope(lock_);

      // Verify that the origin entry doesn't already exist.
      for (const auto& entry : origin_list_) {
        if (entry == info) {
          return false;
        }
      }

      origin_list_.push_back(info->Clone());
    }

    SendModifyCrossOriginWhitelistEntry(true, info);
    return true;
  }

  bool RemoveOriginEntry(const std::string& source_origin,
                         const std::string& target_protocol,
                         const std::string& target_domain,
                         bool allow_target_subdomains) {
    auto info = cef::mojom::CrossOriginWhiteListEntry::New();
    info->source_origin = source_origin;
    info->target_protocol = target_protocol;
    info->target_domain = target_domain;
    info->allow_target_subdomains = allow_target_subdomains;

    bool found = false;

    {
      base::AutoLock lock_scope(lock_);

      CrossOriginWhiteList::iterator it = origin_list_.begin();
      for (; it != origin_list_.end(); ++it) {
        if (*it == info) {
          origin_list_.erase(it);
          found = true;
          break;
        }
      }
    }

    if (!found) {
      return false;
    }

    SendModifyCrossOriginWhitelistEntry(false, info);
    return true;
  }

  void ClearOrigins() {
    {
      base::AutoLock lock_scope(lock_);
      origin_list_.clear();
    }

    SendClearCrossOriginWhitelist();
  }

  void GetCrossOriginWhitelistEntries(
      absl::optional<CrossOriginWhiteList>* entries) const {
    base::AutoLock lock_scope(lock_);

    if (!origin_list_.empty()) {
      CrossOriginWhiteList vec;
      for (const auto& entry : origin_list_) {
        vec.push_back(entry->Clone());
      }
      *entries = std::move(vec);
    }
  }

  bool HasCrossOriginWhitelistEntry(const url::Origin& source,
                                    const url::Origin& target) const {
    base::AutoLock lock_scope(lock_);

    if (!origin_list_.empty()) {
      for (const auto& entry : origin_list_) {
        if (IsMatch(source, target, entry)) {
          return true;
        }
      }
    }

    return false;
  }

 private:
  // Send the modify cross-origin whitelist entry message to all currently
  // existing hosts.
  static void SendModifyCrossOriginWhitelistEntry(
      bool add,
      const cef::mojom::CrossOriginWhiteListEntryPtr& info) {
    CEF_REQUIRE_UIT();

    content::RenderProcessHost::iterator i(
        content::RenderProcessHost::AllHostsIterator());
    for (; !i.IsAtEnd(); i.Advance()) {
      auto render_manager =
          CefBrowserManager::GetRenderManagerForProcess(i.GetCurrentValue());
      render_manager->ModifyCrossOriginWhitelistEntry(add, info->Clone());
    }
  }

  // Send the clear cross-origin whitelists message to all currently existing
  // hosts.
  static void SendClearCrossOriginWhitelist() {
    CEF_REQUIRE_UIT();

    content::RenderProcessHost::iterator i(
        content::RenderProcessHost::AllHostsIterator());
    for (; !i.IsAtEnd(); i.Advance()) {
      auto render_manager =
          CefBrowserManager::GetRenderManagerForProcess(i.GetCurrentValue());
      render_manager->ClearCrossOriginWhitelist();
    }
  }

  static bool IsMatch(const url::Origin& source_origin,
                      const url::Origin& target_origin,
                      const cef::mojom::CrossOriginWhiteListEntryPtr& param) {
    if (!source_origin.IsSameOriginWith(
            url::Origin::Create(GURL(param->source_origin)))) {
      // Source origin does not match.
      return false;
    }

    if (target_origin.scheme() != param->target_protocol) {
      // Target scheme does not match.
      return false;
    }

    if (param->allow_target_subdomains) {
      if (param->target_domain.empty()) {
        // Any domain will match.
        return true;
      } else {
        // Match sub-domains.
        return target_origin.DomainIs(param->target_domain.c_str());
      }
    } else {
      // Match full domain.
      return (target_origin.host() == param->target_domain);
    }
  }

  mutable base::Lock lock_;

  // List of registered origins. Access must be protected by |lock_|.
  CrossOriginWhiteList origin_list_;
};

base::LazyInstance<CefOriginWhitelistManager>::Leaky g_manager =
    LAZY_INSTANCE_INITIALIZER;

CefOriginWhitelistManager* CefOriginWhitelistManager::GetInstance() {
  return g_manager.Pointer();
}

}  // namespace

bool CefAddCrossOriginWhitelistEntry(const CefString& source_origin,
                                     const CefString& target_protocol,
                                     const CefString& target_domain,
                                     bool allow_target_subdomains) {
  // Verify that the context is in a valid state.
  if (!CONTEXT_STATE_VALID()) {
    NOTREACHED();
    return false;
  }

  std::string source_url = source_origin;
  GURL gurl = GURL(source_url);
  if (gurl.is_empty() || !gurl.is_valid()) {
    NOTREACHED() << "Invalid source_origin URL: " << source_url;
    return false;
  }

  if (CEF_CURRENTLY_ON_UIT()) {
    return CefOriginWhitelistManager::GetInstance()->AddOriginEntry(
        source_origin, target_protocol, target_domain, allow_target_subdomains);
  } else {
    CEF_POST_TASK(
        CEF_UIT,
        base::BindOnce(base::IgnoreResult(&CefAddCrossOriginWhitelistEntry),
                       source_origin, target_protocol, target_domain,
                       allow_target_subdomains));
  }

  return true;
}

bool CefRemoveCrossOriginWhitelistEntry(const CefString& source_origin,
                                        const CefString& target_protocol,
                                        const CefString& target_domain,
                                        bool allow_target_subdomains) {
  // Verify that the context is in a valid state.
  if (!CONTEXT_STATE_VALID()) {
    NOTREACHED();
    return false;
  }

  std::string source_url = source_origin;
  GURL gurl = GURL(source_url);
  if (gurl.is_empty() || !gurl.is_valid()) {
    NOTREACHED() << "Invalid source_origin URL: " << source_url;
    return false;
  }

  if (CEF_CURRENTLY_ON_UIT()) {
    return CefOriginWhitelistManager::GetInstance()->RemoveOriginEntry(
        source_origin, target_protocol, target_domain, allow_target_subdomains);
  } else {
    CEF_POST_TASK(
        CEF_UIT,
        base::BindOnce(base::IgnoreResult(&CefRemoveCrossOriginWhitelistEntry),
                       source_origin, target_protocol, target_domain,
                       allow_target_subdomains));
  }

  return true;
}

bool CefClearCrossOriginWhitelist() {
  // Verify that the context is in a valid state.
  if (!CONTEXT_STATE_VALID()) {
    NOTREACHED();
    return false;
  }

  if (CEF_CURRENTLY_ON_UIT()) {
    CefOriginWhitelistManager::GetInstance()->ClearOrigins();
  } else {
    CEF_POST_TASK(
        CEF_UIT,
        base::BindOnce(base::IgnoreResult(&CefClearCrossOriginWhitelist)));
  }

  return true;
}

void GetCrossOriginWhitelistEntries(
    absl::optional<CrossOriginWhiteList>* entries) {
  CefOriginWhitelistManager::GetInstance()->GetCrossOriginWhitelistEntries(
      entries);
}

bool HasCrossOriginWhitelistEntry(const url::Origin& source,
                                  const url::Origin& target) {
  // Components of chrome that are implemented as extensions or platform apps
  // are allowed to use chrome://resources/ and chrome://theme/ URLs.
  // See also RegisterNonNetworkSubresourceURLLoaderFactories.
  if (source.scheme() == extensions::kExtensionScheme &&
      target.scheme() == content::kChromeUIScheme &&
      (target.host() == chrome::kChromeUIThemeHost ||
       target.host() == content::kChromeUIResourcesHost)) {
    return true;
  }

  return CefOriginWhitelistManager::GetInstance()->HasCrossOriginWhitelistEntry(
      source, target);
}