// Copyright 2015 The Chromium 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 "extensions/browser/extension_user_script_loader.h"

#include <stddef.h>

#include <set>
#include <string>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/strings/string_util.h"
#include "base/version.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_process_host.h"
#include "extensions/browser/component_extension_resource_manager.h"
#include "extensions/browser/content_verifier.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_handlers/default_locale_handler.h"
#include "extensions/common/message_bundle.h"
#include "extensions/common/one_shot_event.h"
#include "ui/base/resource/resource_bundle.h"

using content::BrowserContext;

namespace extensions {

namespace {

using SubstitutionMap = std::map<std::string, std::string>;

// Verifies file contents as they are read.
void VerifyContent(const scoped_refptr<ContentVerifier>& verifier,
                   const std::string& extension_id,
                   const base::FilePath& extension_root,
                   const base::FilePath& relative_path,
                   const std::string& content) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  scoped_refptr<ContentVerifyJob> job(
      verifier->CreateJobFor(extension_id, extension_root, relative_path));
  if (job.get()) {
    job->Start();
    job->BytesRead(content.size(), content.data());
    job->DoneReading();
  }
}

// Loads user scripts from the extension who owns these scripts.
bool LoadScriptContent(const HostID& host_id,
                       UserScript::File* script_file,
                       const SubstitutionMap* localization_messages,
                       const scoped_refptr<ContentVerifier>& verifier) {
  DCHECK(script_file);
  std::string content;
  const base::FilePath& path = ExtensionResource::GetFilePath(
      script_file->extension_root(), script_file->relative_path(),
      ExtensionResource::SYMLINKS_MUST_RESOLVE_WITHIN_ROOT);
  if (path.empty()) {
    int resource_id = 0;
    if (ExtensionsBrowserClient::Get()
            ->GetComponentExtensionResourceManager()
            ->IsComponentExtensionResource(script_file->extension_root(),
                                           script_file->relative_path(),
                                           &resource_id)) {
      const ResourceBundle& rb = ResourceBundle::GetSharedInstance();
      content = rb.GetRawDataResource(resource_id).as_string();
    } else {
      LOG(WARNING) << "Failed to get file path to "
                   << script_file->relative_path().value() << " from "
                   << script_file->extension_root().value();
      return false;
    }
  } else {
    if (!base::ReadFileToString(path, &content)) {
      LOG(WARNING) << "Failed to load user script file: " << path.value();
      return false;
    }
    if (verifier.get()) {
      content::BrowserThread::PostTask(
          content::BrowserThread::IO, FROM_HERE,
          base::Bind(&VerifyContent, verifier, host_id.id(),
                     script_file->extension_root(),
                     script_file->relative_path(), content));
    }
  }

  // Localize the content.
  if (localization_messages) {
    std::string error;
    MessageBundle::ReplaceMessagesWithExternalDictionary(*localization_messages,
                                                         &content, &error);
    if (!error.empty())
      LOG(WARNING) << "Failed to replace messages in script: " << error;
  }

  // Remove BOM from the content.
  if (base::StartsWith(content, base::kUtf8ByteOrderMark,
                       base::CompareCase::SENSITIVE)) {
    script_file->set_content(content.substr(strlen(base::kUtf8ByteOrderMark)));
  } else {
    script_file->set_content(content);
  }

  return true;
}

SubstitutionMap* GetLocalizationMessages(
    const ExtensionUserScriptLoader::HostsInfo& hosts_info,
    const HostID& host_id) {
  ExtensionUserScriptLoader::HostsInfo::const_iterator iter =
      hosts_info.find(host_id);
  if (iter == hosts_info.end())
    return nullptr;
  return file_util::LoadMessageBundleSubstitutionMap(
      iter->second.first, host_id.id(), iter->second.second);
}

void LoadUserScripts(UserScriptList* user_scripts,
                     const ExtensionUserScriptLoader::HostsInfo& hosts_info,
                     const std::set<int>& added_script_ids,
                     const scoped_refptr<ContentVerifier>& verifier) {
  for (const std::unique_ptr<UserScript>& script : *user_scripts) {
    if (added_script_ids.count(script->id()) == 0)
      continue;
    for (const std::unique_ptr<UserScript::File>& script_file :
         script->js_scripts()) {
      if (script_file->GetContent().empty())
        LoadScriptContent(script->host_id(), script_file.get(), nullptr,
                          verifier);
    }
    if (script->css_scripts().size() > 0) {
      std::unique_ptr<SubstitutionMap> localization_messages(
          GetLocalizationMessages(hosts_info, script->host_id()));
      for (const std::unique_ptr<UserScript::File>& script_file :
           script->css_scripts()) {
        if (script_file->GetContent().empty()) {
          LoadScriptContent(script->host_id(), script_file.get(),
                            localization_messages.get(), verifier);
        }
      }
    }
  }
}

void LoadScriptsOnFileThread(
    std::unique_ptr<UserScriptList> user_scripts,
    const ExtensionUserScriptLoader::HostsInfo& hosts_info,
    const std::set<int>& added_script_ids,
    const scoped_refptr<ContentVerifier>& verifier,
    UserScriptLoader::LoadScriptsCallback callback) {
  DCHECK(user_scripts.get());
  LoadUserScripts(user_scripts.get(), hosts_info, added_script_ids, verifier);
  std::unique_ptr<base::SharedMemory> memory =
      UserScriptLoader::Serialize(*user_scripts);
  content::BrowserThread::PostTask(
      content::BrowserThread::UI, FROM_HERE,
      base::Bind(callback, base::Passed(&user_scripts), base::Passed(&memory)));
}

}  // namespace

ExtensionUserScriptLoader::ExtensionUserScriptLoader(
    BrowserContext* browser_context,
    const HostID& host_id,
    bool listen_for_extension_system_loaded)
    : UserScriptLoader(browser_context, host_id),
      content_verifier_(
          ExtensionSystem::Get(browser_context)->content_verifier()),
      extension_registry_observer_(this),
      weak_factory_(this) {
  extension_registry_observer_.Add(ExtensionRegistry::Get(browser_context));
  if (listen_for_extension_system_loaded) {
    ExtensionSystem::Get(browser_context)
        ->ready()
        .Post(FROM_HERE,
              base::Bind(&ExtensionUserScriptLoader::OnExtensionSystemReady,
                         weak_factory_.GetWeakPtr()));
  } else {
    SetReady(true);
  }
}

ExtensionUserScriptLoader::~ExtensionUserScriptLoader() {
}

void ExtensionUserScriptLoader::LoadScriptsForTest(
    UserScriptList* user_scripts) {
  HostsInfo info;
  std::set<int> added_script_ids;
  for (const std::unique_ptr<UserScript>& script : *user_scripts)
    added_script_ids.insert(script->id());

  LoadUserScripts(user_scripts, info, added_script_ids,
                  nullptr /* no verifier for testing */);
}

void ExtensionUserScriptLoader::LoadScripts(
    std::unique_ptr<UserScriptList> user_scripts,
    const std::set<HostID>& changed_hosts,
    const std::set<int>& added_script_ids,
    LoadScriptsCallback callback) {
  UpdateHostsInfo(changed_hosts);

  content::BrowserThread::PostTask(
      content::BrowserThread::FILE, FROM_HERE,
      base::Bind(&LoadScriptsOnFileThread, base::Passed(&user_scripts),
                 hosts_info_, added_script_ids, content_verifier_, callback));
}

void ExtensionUserScriptLoader::UpdateHostsInfo(
    const std::set<HostID>& changed_hosts) {
  ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context());
  for (const HostID& host_id : changed_hosts) {
    const Extension* extension =
        registry->GetExtensionById(host_id.id(), ExtensionRegistry::ENABLED);
    // |changed_hosts_| may include hosts that have been removed,
    // which leads to the above lookup failing. In this case, just continue.
    if (!extension)
      continue;
    if (hosts_info_.find(host_id) != hosts_info_.end())
      continue;
    hosts_info_[host_id] = ExtensionSet::ExtensionPathAndDefaultLocale(
        extension->path(), LocaleInfo::GetDefaultLocale(extension));
  }
}

void ExtensionUserScriptLoader::OnExtensionUnloaded(
    content::BrowserContext* browser_context,
    const Extension* extension,
    UnloadedExtensionInfo::Reason reason) {
  hosts_info_.erase(HostID(HostID::EXTENSIONS, extension->id()));
}

void ExtensionUserScriptLoader::OnExtensionSystemReady() {
  SetReady(true);
}

}  // namespace extensions
