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

#include <stddef.h>

#include <optional>
#include <string>
#include <utility>

#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "build/build_config.h"
#include "chrome/browser/spellchecker/spellcheck_service.h"
#include "chrome/common/chrome_paths.h"
#include "components/spellcheck/browser/spellcheck_platform.h"
#include "components/spellcheck/common/spellcheck_common.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "components/spellcheck/spellcheck_buildflags.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/load_flags.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"

#if !BUILDFLAG(IS_ANDROID)
#include "base/files/memory_mapped_file.h"
#include "third_party/hunspell/google/bdict.h"  // nogncheck crbug.com/40147906
#endif

using content::BrowserThread;

namespace {

GURL& GetDownloadUrlForTesting() {
  static base::NoDestructor<GURL> download_url_for_testing;
  return *download_url_for_testing;
}

// Close the file.
void CloseDictionary(base::File file) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);
  file.Close();
}

// Saves |data| to file at |path|. Returns true on successful save, otherwise
// returns false.
bool SaveDictionaryData(std::string data, const base::FilePath& path) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  if (!base::WriteFile(path, data)) {
    bool success = false;
#if BUILDFLAG(IS_WIN)
    base::FilePath dict_dir;
    base::PathService::Get(chrome::DIR_USER_DATA, &dict_dir);
    base::FilePath fallback_file_path =
        dict_dir.Append(path.BaseName());
    if (base::WriteFile(fallback_file_path, data)) {
      success = true;
    }
#endif

    if (!success) {
      base::DeleteFile(path);
      return false;
    }
  }

  return true;
}

}  // namespace

SpellcheckHunspellDictionary::DictionaryFile::DictionaryFile(
    base::TaskRunner* task_runner) : task_runner_(task_runner) {}

SpellcheckHunspellDictionary::DictionaryFile::~DictionaryFile() {
  if (file.IsValid()) {
    task_runner_->PostTask(FROM_HERE,
                           base::BindOnce(&CloseDictionary, std::move(file)));
  }
}

SpellcheckHunspellDictionary::DictionaryFile::DictionaryFile(
    DictionaryFile&& other)
    : path(other.path),
      file(std::move(other.file)),
      task_runner_(std::move(other.task_runner_)) {}

SpellcheckHunspellDictionary::DictionaryFile&
SpellcheckHunspellDictionary::DictionaryFile::operator=(
    DictionaryFile&& other) {
  path = other.path;
  file = std::move(other.file);
  task_runner_ = std::move(other.task_runner_);
  return *this;
}

SpellcheckHunspellDictionary::SpellcheckHunspellDictionary(
    const std::string& language,
    const std::string& platform_spellcheck_language,
    content::BrowserContext* browser_context,
    SpellcheckService* spellcheck_service)
    : task_runner_(
          base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})),
      language_(language),
      platform_spellcheck_language_(platform_spellcheck_language),
      use_browser_spellchecker_(false),
      browser_context_(browser_context),
      spellcheck_service_(spellcheck_service),
      download_status_(DOWNLOAD_NONE),
      dictionary_file_(task_runner_.get()) {}

SpellcheckHunspellDictionary::~SpellcheckHunspellDictionary() {
#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  // Disable the language from platform spellchecker.
  if (spellcheck::UseBrowserSpellChecker() && HasPlatformSupport()) {
    spellcheck_platform::DisableLanguage(
        spellcheck_service_->platform_spell_checker(),
        GetPlatformSpellcheckLanguage());
  }
#endif  // BUILDFLAG(USE_BROWSER_SPELLCHECKER)
}

void SpellcheckHunspellDictionary::Load() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  if (spellcheck::UseBrowserSpellChecker() &&
      spellcheck_platform::SpellCheckerAvailable() && HasPlatformSupport()) {
    spellcheck_platform::PlatformSupportsLanguage(
        spellcheck_service_->platform_spell_checker(),
        GetPlatformSpellcheckLanguage(),
        base::BindOnce(
            &SpellcheckHunspellDictionary::PlatformSupportsLanguageComplete,
            weak_ptr_factory_.GetWeakPtr()));
    return;
  }
#endif  // USE_BROWSER_SPELLCHECKER

  // Platform spellchecker isn't enabled, so the language is unsupported.
  PlatformSupportsLanguageComplete(false);
}

void SpellcheckHunspellDictionary::RetryDownloadDictionary(
    content::BrowserContext* browser_context) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (dictionary_file_.file.IsValid()) {
    NOTREACHED();
  }
  browser_context_ = browser_context;
  DownloadDictionary(GetDictionaryURL());
}

bool SpellcheckHunspellDictionary::IsReady() const {
  return GetDictionaryFile().IsValid() || IsUsingPlatformChecker();
}

const base::File& SpellcheckHunspellDictionary::GetDictionaryFile() const {
  return dictionary_file_.file;
}

const std::string& SpellcheckHunspellDictionary::GetLanguage() const {
  return language_;
}

const std::string& SpellcheckHunspellDictionary::GetPlatformSpellcheckLanguage()
    const {
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  // Currently the platform spellcheck language is only distinguished for
  // Windows.
  return platform_spellcheck_language_;
#else
  return language_;
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
}

bool SpellcheckHunspellDictionary::HasPlatformSupport() const {
  return !GetPlatformSpellcheckLanguage().empty();
}

bool SpellcheckHunspellDictionary::IsUsingPlatformChecker() const {
  return use_browser_spellchecker_;
}

void SpellcheckHunspellDictionary::AddObserver(Observer* observer) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  observers_.AddObserver(observer);
}

void SpellcheckHunspellDictionary::RemoveObserver(Observer* observer) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  observers_.RemoveObserver(observer);
}

bool SpellcheckHunspellDictionary::IsDownloadInProgress() {
  return download_status_ == DOWNLOAD_IN_PROGRESS;
}

bool SpellcheckHunspellDictionary::IsDownloadFailure() {
  return download_status_ == DOWNLOAD_FAILED;
}

void SpellcheckHunspellDictionary::OnSimpleLoaderComplete(
    std::optional<std::string> data) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  bool is_success = simple_loader_->NetError() == net::OK;
  int response_code = -1;
  if (simple_loader_->ResponseInfo() && simple_loader_->ResponseInfo()->headers)
    response_code = simple_loader_->ResponseInfo()->headers->response_code();

  if (!is_success || ((response_code / 100) != 2)) {
    // Initialize will not try to download the file a second time.
    InformListenersOfDownloadFailure();
    return;
  }

  // We don't need the loader anymore.
  simple_loader_.reset();

  // Basic sanity check on the dictionary. There's a small chance of 200 status
  // code for a body that represents some form of failure.
  if (!data || data->size() < 4 || data->compare(0, 4, "BDic") != 0) {
    InformListenersOfDownloadFailure();
    return;
  }

#if !BUILDFLAG(IS_ANDROID)
  // To prevent corrupted dictionary data from causing a renderer crash, scan
  // the dictionary data and verify it is sane before save it to a file.
  if (!hunspell::BDict::Verify(base::as_byte_span(*data))) {
    // Let PostTaskAndReply caller send to InformListenersOfInitialization
    // through SaveDictionaryDataComplete().
    SaveDictionaryDataComplete(false);
    return;
  }
#endif

  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&SaveDictionaryData, std::move(data).value(),
                     dictionary_file_.path),
      base::BindOnce(&SpellcheckHunspellDictionary::SaveDictionaryDataComplete,
                     weak_ptr_factory_.GetWeakPtr()));
}

void SpellcheckHunspellDictionary::SetDownloadURLForTesting(const GURL url) {
  GetDownloadUrlForTesting() = url;
}

GURL SpellcheckHunspellDictionary::GetDictionaryURL() {
  if (GetDownloadUrlForTesting() != GURL()) {
    return GetDownloadUrlForTesting();
  }

  std::string bdict_file = dictionary_file_.path.BaseName().MaybeAsASCII();
  DCHECK(!bdict_file.empty());

  static const char kDownloadServerUrl[] =
      "https://redirector.gvt1.com/edgedl/chrome/dict/";

  return GURL(std::string(kDownloadServerUrl) +
              base::ToLowerASCII(bdict_file));
}

void SpellcheckHunspellDictionary::DownloadDictionary(GURL url) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(browser_context_);

  download_status_ = DOWNLOAD_IN_PROGRESS;
  for (Observer& observer : observers_)
    observer.OnHunspellDictionaryDownloadBegin(language_);

  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("spellcheck_hunspell_dictionary", R"(
        semantics {
          sender: "Spellcheck Dictionary Downloader"
          description:
            "When user selects a new language for spell checking in Google "
            "Chrome, a new dictionary is downloaded for it."
          trigger: "User selects a new language for spell checking."
          data:
            "The spell checking language identifier. No user identifier is "
            "sent."
          destination: GOOGLE_OWNED_SERVICE
        }
        policy {
          cookies_allowed: NO
          setting:
            "You can prevent downloading dictionaries by not selecting 'Use "
            "this language for spell checking.' in Chrome's settings under "
            "Lanugagues -> 'Language and input settings...'."
          policy_exception_justification:
            "Not implemented, considered not useful."
        })");

  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = url;
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  simple_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
                                                    traffic_annotation);
  network::mojom::URLLoaderFactory* loader_factory =
      browser_context_->GetDefaultStoragePartition()
          ->GetURLLoaderFactoryForBrowserProcess()
          .get();
  simple_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
      loader_factory,
      base::BindOnce(&SpellcheckHunspellDictionary::OnSimpleLoaderComplete,
                     base::Unretained(this)));

  // Attempt downloading the dictionary only once.
  browser_context_ = nullptr;
}

#if !BUILDFLAG(IS_ANDROID)
// static
SpellcheckHunspellDictionary::DictionaryFile
SpellcheckHunspellDictionary::OpenDictionaryFile(base::TaskRunner* task_runner,
                                                 const base::FilePath& path) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  // The default_dictionary_file can either come from the standard list of
  // hunspell dictionaries (determined in InitializeDictionaryLocation), or it
  // can be passed in via an extension. In either case, the file is checked for
  // existence so that it's not re-downloaded.
  // For systemwide installations on Windows, the default directory may not
  // have permissions for download. In that case, the alternate directory for
  // download is chrome::DIR_USER_DATA.
  DictionaryFile dictionary(task_runner);

#if BUILDFLAG(IS_WIN)
  // Check if the dictionary exists in the fallback location. If so, use it
  // rather than downloading anew.
  base::FilePath user_dir;
  base::PathService::Get(chrome::DIR_USER_DATA, &user_dir);
  base::FilePath fallback = user_dir.Append(path.BaseName());
  if (!base::PathExists(path) && base::PathExists(fallback))
    dictionary.path = fallback;
  else
    dictionary.path = path;
#else
  dictionary.path = path;
#endif  // BUILDFLAG(IS_WIN)

  // Open the dictionary file and verify there is no corruption. If verification
  // fails the file must be deleted.

  dictionary.file.Initialize(dictionary.path,
                             base::File::FLAG_READ | base::File::FLAG_OPEN);
  if (!dictionary.file.IsValid()) {
    dictionary.file.Close();
    base::DeleteFile(dictionary.path);
    return dictionary;
  }

  std::vector<uint8_t> data;
  data.resize(dictionary.file.GetLength());
  if (!dictionary.file.ReadAndCheck(0, data) ||
      !hunspell::BDict::Verify(data)) {
    dictionary.file.Close();
    base::DeleteFile(dictionary.path);
  }

  return dictionary;
}

// static
SpellcheckHunspellDictionary::DictionaryFile
SpellcheckHunspellDictionary::InitializeDictionaryLocation(
    base::TaskRunner* task_runner, const std::string& language) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  // The default place where the spellcheck dictionary resides is
  // chrome::DIR_APP_DICTIONARIES.
  //
  // Initialize the BDICT path. Initialization should be on the blocking
  // sequence because it checks if there is a "Dictionaries" directory and
  // create it.
  base::FilePath dict_dir;
  base::PathService::Get(chrome::DIR_APP_DICTIONARIES, &dict_dir);
  base::FilePath dict_path =
      spellcheck::GetVersionedFileName(language, dict_dir);

  return OpenDictionaryFile(task_runner, dict_path);
}

void SpellcheckHunspellDictionary::InitializeDictionaryLocationComplete(
    DictionaryFile file) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  dictionary_file_ = std::move(file);

  if (!dictionary_file_.file.IsValid()) {
    // Notify browser tests that this dictionary is corrupted. Skip downloading
    // the dictionary in browser tests.
    // TODO(rouslan): Remove this test-only case.
    if (spellcheck_service_->SignalStatusEvent(
          SpellcheckService::BDICT_CORRUPTED)) {
      browser_context_ = nullptr;
    }

    if (browser_context_) {
      // Download from the UI thread to check that |browser_context_| is
      // still valid.
      DownloadDictionary(GetDictionaryURL());
      return;
    }
  }

  InformListenersOfInitialization();
}
#endif  // !BUILDFLAG(IS_ANDROID)

void SpellcheckHunspellDictionary::SaveDictionaryDataComplete(
    bool dictionary_saved) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (dictionary_saved) {
    download_status_ = DOWNLOAD_NONE;
    for (Observer& observer : observers_)
      observer.OnHunspellDictionaryDownloadSuccess(language_);
    Load();
  } else {
    InformListenersOfDownloadFailure();
    InformListenersOfInitialization();
  }
}

void SpellcheckHunspellDictionary::InformListenersOfInitialization() {
  for (Observer& observer : observers_)
    observer.OnHunspellDictionaryInitialized(language_);
}

void SpellcheckHunspellDictionary::InformListenersOfDownloadFailure() {
  download_status_ = DOWNLOAD_FAILED;
  for (Observer& observer : observers_)
    observer.OnHunspellDictionaryDownloadFailure(language_);
}

void SpellcheckHunspellDictionary::PlatformSupportsLanguageComplete(
    bool platform_supports_language) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (platform_supports_language) {
#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    if (spellcheck::UseBrowserSpellChecker() && HasPlatformSupport()) {
      spellcheck_platform::SetLanguage(
          spellcheck_service_->platform_spell_checker(),
          GetPlatformSpellcheckLanguage(),
          base::BindOnce(&SpellcheckHunspellDictionary::
                             SpellCheckPlatformSetLanguageComplete,
                         weak_ptr_factory_.GetWeakPtr()));
      return;
    }
#endif  // BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    NOTREACHED();
  } else {
    // Either the platform spellchecker is unavailable / disabled, or it doesn't
    // support this language. In either case, we must use Hunspell for this
    // language, unless we are on Android, which doesn't support Hunspell.
#if !BUILDFLAG(IS_ANDROID) && BUILDFLAG(USE_RENDERER_SPELLCHECKER)
    task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE,
        base::BindOnce(&InitializeDictionaryLocation,
                       base::RetainedRef(task_runner_.get()), language_),
        base::BindOnce(
            &SpellcheckHunspellDictionary::InitializeDictionaryLocationComplete,
            weak_ptr_factory_.GetWeakPtr()));
#endif  // !BUILDFLAG(IS_ANDROID) && BUILDFLAG(USE_RENDERER_SPELLCHECKER)
  }
}

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
void SpellcheckHunspellDictionary::SpellCheckPlatformSetLanguageComplete(
    bool result) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!result)
    return;

  use_browser_spellchecker_ = true;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          &SpellcheckHunspellDictionary::InformListenersOfInitialization,
          weak_ptr_factory_.GetWeakPtr()));
}
#endif
