// Copyright (c) 2011 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 "chrome/common/extensions/extension_file_util.h"

#include <map>
#include <vector>

#include "base/file_util.h"
#include "base/logging.h"
#include "base/memory/scoped_temp_dir.h"
#include "base/metrics/histogram.h"
#include "base/path_service.h"
#include "base/threading/thread_restrictions.h"
#include "base/utf_string_conversions.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_action.h"
#include "chrome/common/extensions/extension_l10n_util.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/extensions/extension_resource.h"
#include "chrome/common/extensions/extension_sidebar_defaults.h"
#include "content/common/json_value_serializer.h"
#include "grit/generated_resources.h"
#include "net/base/escape.h"
#include "net/base/file_stream.h"
#include "ui/base/l10n/l10n_util.h"

namespace errors = extension_manifest_errors;

namespace extension_file_util {

// Validates locale info. Doesn't check if messages.json files are valid.
static bool ValidateLocaleInfo(const Extension& extension, std::string* error);

// Returns false and sets the error if script file can't be loaded,
// or if it's not UTF-8 encoded.
static bool IsScriptValid(const FilePath& path, const FilePath& relative_path,
                          int message_id, std::string* error);

const char kInstallDirectoryName[] = "Extensions";

FilePath InstallExtension(const FilePath& unpacked_source_dir,
                          const std::string& id,
                          const std::string& version,
                          const FilePath& all_extensions_dir) {
  FilePath extension_dir = all_extensions_dir.AppendASCII(id);
  FilePath version_dir;

  // Create the extension directory if it doesn't exist already.
  if (!file_util::PathExists(extension_dir)) {
    if (!file_util::CreateDirectory(extension_dir))
      return FilePath();
  }

  // Try to find a free directory. There can be legitimate conflicts in the case
  // of overinstallation of the same version.
  const int kMaxAttempts = 100;
  for (int i = 0; i < kMaxAttempts; ++i) {
    FilePath candidate = extension_dir.AppendASCII(
        base::StringPrintf("%s_%u", version.c_str(), i));
    if (!file_util::PathExists(candidate)) {
      version_dir = candidate;
      break;
    }
  }

  if (version_dir.empty()) {
    LOG(ERROR) << "Could not find a home for extension " << id << " with "
               << "version " << version << ".";
    return FilePath();
  }

  if (!file_util::Move(unpacked_source_dir, version_dir))
    return FilePath();

  return version_dir;
}

void UninstallExtension(const FilePath& extensions_dir,
                        const std::string& id) {
  // We don't care about the return value. If this fails (and it can, due to
  // plugins that aren't unloaded yet, it will get cleaned up by
  // ExtensionService::GarbageCollectExtensions).
  file_util::Delete(extensions_dir.AppendASCII(id), true);  // recursive.
}

scoped_refptr<Extension> LoadExtension(const FilePath& extension_path,
                                       Extension::Location location,
                                       int flags,
                                       std::string* error) {
  FilePath manifest_path =
      extension_path.Append(Extension::kManifestFilename);
  if (!file_util::PathExists(manifest_path)) {
    *error = l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_UNREADABLE);
    return NULL;
  }

  JSONFileValueSerializer serializer(manifest_path);
  scoped_ptr<Value> root(serializer.Deserialize(NULL, error));
  if (!root.get()) {
    if (error->empty()) {
      // If |error| is empty, than the file could not be read.
      // It would be cleaner to have the JSON reader give a specific error
      // in this case, but other code tests for a file error with
      // error->empty().  For now, be consistent.
      *error = l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_UNREADABLE);
    } else {
      *error = base::StringPrintf("%s  %s",
                                  errors::kManifestParseError,
                                  error->c_str());
    }
    return NULL;
  }

  if (!root->IsType(Value::TYPE_DICTIONARY)) {
    *error = l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_INVALID);
    return NULL;
  }

  DictionaryValue* manifest = static_cast<DictionaryValue*>(root.get());
  if (!extension_l10n_util::LocalizeExtension(extension_path, manifest, error))
    return NULL;

  scoped_refptr<Extension> extension(Extension::Create(
      extension_path,
      location,
      *manifest,
      flags,
      error));
  if (!extension.get())
    return NULL;

  if (!ValidateExtension(extension.get(), error))
    return NULL;

  return extension;
}

bool ValidateExtension(Extension* extension, std::string* error) {
  // Validate icons exist.
  for (ExtensionIconSet::IconMap::const_iterator iter =
           extension->icons().map().begin();
       iter != extension->icons().map().end();
       ++iter) {
    const FilePath path = extension->GetResource(iter->second).GetFilePath();
    if (!file_util::PathExists(path)) {
      *error =
          l10n_util::GetStringFUTF8(IDS_EXTENSION_LOAD_ICON_FAILED,
                                    UTF8ToUTF16(iter->second));
      return false;
    }
  }

  // Theme resource validation.
  if (extension->is_theme()) {
    DictionaryValue* images_value = extension->GetThemeImages();
    if (images_value) {
      for (DictionaryValue::key_iterator iter = images_value->begin_keys();
           iter != images_value->end_keys(); ++iter) {
        std::string val;
        if (images_value->GetStringWithoutPathExpansion(*iter, &val)) {
          FilePath image_path = extension->path().AppendASCII(val);
          if (!file_util::PathExists(image_path)) {
            *error =
                l10n_util::GetStringFUTF8(IDS_EXTENSION_INVALID_IMAGE_PATH,
                                          image_path.LossyDisplayName());
            return false;
          }
        }
      }
    }

    // Themes cannot contain other extension types.
    return true;
  }

  // Validate that claimed script resources actually exist,
  // and are UTF-8 encoded.
  for (size_t i = 0; i < extension->content_scripts().size(); ++i) {
    const UserScript& script = extension->content_scripts()[i];

    for (size_t j = 0; j < script.js_scripts().size(); j++) {
      const UserScript::File& js_script = script.js_scripts()[j];
      const FilePath& path = ExtensionResource::GetFilePath(
          js_script.extension_root(), js_script.relative_path());
      if (!IsScriptValid(path, js_script.relative_path(),
                         IDS_EXTENSION_LOAD_JAVASCRIPT_FAILED, error))
        return false;
    }

    for (size_t j = 0; j < script.css_scripts().size(); j++) {
      const UserScript::File& css_script = script.css_scripts()[j];
      const FilePath& path = ExtensionResource::GetFilePath(
          css_script.extension_root(), css_script.relative_path());
      if (!IsScriptValid(path, css_script.relative_path(),
                         IDS_EXTENSION_LOAD_CSS_FAILED, error))
        return false;
    }
  }

  // Validate claimed plugin paths.
  for (size_t i = 0; i < extension->plugins().size(); ++i) {
    const Extension::PluginInfo& plugin = extension->plugins()[i];
    if (!file_util::PathExists(plugin.path)) {
      *error =
          l10n_util::GetStringFUTF8(
              IDS_EXTENSION_LOAD_PLUGIN_PATH_FAILED,
              plugin.path.LossyDisplayName());
      return false;
    }
  }

  // Validate icon location for page actions.
  ExtensionAction* page_action = extension->page_action();
  if (page_action) {
    std::vector<std::string> icon_paths(*page_action->icon_paths());
    if (!page_action->default_icon_path().empty())
      icon_paths.push_back(page_action->default_icon_path());
    for (std::vector<std::string>::iterator iter = icon_paths.begin();
         iter != icon_paths.end(); ++iter) {
      if (!file_util::PathExists(extension->GetResource(*iter).GetFilePath())) {
        *error =
            l10n_util::GetStringFUTF8(
                IDS_EXTENSION_LOAD_ICON_FOR_PAGE_ACTION_FAILED,
                UTF8ToUTF16(*iter));
        return false;
      }
    }
  }

  // Validate icon location for browser actions.
  // Note: browser actions don't use the icon_paths().
  ExtensionAction* browser_action = extension->browser_action();
  if (browser_action) {
    std::string path = browser_action->default_icon_path();
    if (!path.empty() &&
        !file_util::PathExists(extension->GetResource(path).GetFilePath())) {
        *error =
            l10n_util::GetStringFUTF8(
                IDS_EXTENSION_LOAD_ICON_FOR_BROWSER_ACTION_FAILED,
                UTF8ToUTF16(path));
        return false;
    }
  }

  // Validate background page location, except for hosted apps, which should use
  // an external URL. Background page for hosted apps are verified when the
  // extension is created (in Extension::InitFromValue)
  if (!extension->background_url().is_empty() && !extension->is_hosted_app()) {
    FilePath page_path = ExtensionURLToRelativeFilePath(
        extension->background_url());
    const FilePath path = extension->GetResource(page_path).GetFilePath();
    if (path.empty() || !file_util::PathExists(path)) {
      *error =
          l10n_util::GetStringFUTF8(
              IDS_EXTENSION_LOAD_BACKGROUND_PAGE_FAILED,
              page_path.LossyDisplayName());
      return false;
    }
  }

  // Validate path to the options page.  Don't check the URL for hosted apps,
  // because they are expected to refer to an external URL.
  if (!extension->options_url().is_empty() && !extension->is_hosted_app()) {
    const FilePath options_path = ExtensionURLToRelativeFilePath(
        extension->options_url());
    const FilePath path = extension->GetResource(options_path).GetFilePath();
    if (path.empty() || !file_util::PathExists(path)) {
      *error =
          l10n_util::GetStringFUTF8(
              IDS_EXTENSION_LOAD_OPTIONS_PAGE_FAILED,
              options_path.LossyDisplayName());
      return false;
    }
  }

  // Validate sidebar default page location.
  ExtensionSidebarDefaults* sidebar_defaults = extension->sidebar_defaults();
  if (sidebar_defaults && sidebar_defaults->default_page().is_valid()) {
    FilePath page_path = ExtensionURLToRelativeFilePath(
        sidebar_defaults->default_page());
    const FilePath path = extension->GetResource(page_path).GetFilePath();
    if (path.empty() || !file_util::PathExists(path)) {
      *error =
          l10n_util::GetStringFUTF8(
              IDS_EXTENSION_LOAD_SIDEBAR_PAGE_FAILED,
              page_path.LossyDisplayName());
      return false;
    }
  }

  // Validate locale info.
  if (!ValidateLocaleInfo(*extension, error))
    return false;

  // Check children of extension root to see if any of them start with _ and is
  // not on the reserved list.
  if (!CheckForIllegalFilenames(extension->path(), error)) {
    return false;
  }

  return true;
}

void GarbageCollectExtensions(
    const FilePath& install_directory,
    const std::map<std::string, FilePath>& extension_paths) {
  // Nothing to clean up if it doesn't exist.
  if (!file_util::DirectoryExists(install_directory))
    return;

  VLOG(1) << "Garbage collecting extensions...";
  file_util::FileEnumerator enumerator(install_directory,
                                       false,  // Not recursive.
                                       file_util::FileEnumerator::DIRECTORIES);
  FilePath extension_path;
  for (extension_path = enumerator.Next(); !extension_path.value().empty();
       extension_path = enumerator.Next()) {
    std::string extension_id;

    FilePath basename = extension_path.BaseName();
    if (IsStringASCII(basename.value())) {
      extension_id = UTF16ToASCII(basename.LossyDisplayName());
      if (!Extension::IdIsValid(extension_id))
        extension_id.clear();
    }

    // Delete directories that aren't valid IDs.
    if (extension_id.empty()) {
      LOG(WARNING) << "Invalid extension ID encountered in extensions "
                      "directory: " << basename.value();
      VLOG(1) << "Deleting invalid extension directory "
              << extension_path.value() << ".";
      file_util::Delete(extension_path, true);  // Recursive.
      continue;
    }

    std::map<std::string, FilePath>::const_iterator iter =
        extension_paths.find(extension_id);

    // If there is no entry in the prefs file, just delete the directory and
    // move on. This can legitimately happen when an uninstall does not
    // complete, for example, when a plugin is in use at uninstall time.
    if (iter == extension_paths.end()) {
      VLOG(1) << "Deleting unreferenced install for directory "
              << extension_path.LossyDisplayName() << ".";
      file_util::Delete(extension_path, true);  // Recursive.
      continue;
    }

    // Clean up old version directories.
    file_util::FileEnumerator versions_enumerator(
        extension_path,
        false,  // Not recursive.
        file_util::FileEnumerator::DIRECTORIES);
    for (FilePath version_dir = versions_enumerator.Next();
         !version_dir.value().empty();
         version_dir = versions_enumerator.Next()) {
      if (version_dir.BaseName() != iter->second.BaseName()) {
        VLOG(1) << "Deleting old version for directory "
                << version_dir.LossyDisplayName() << ".";
        file_util::Delete(version_dir, true);  // Recursive.
      }
    }
  }
}

ExtensionMessageBundle* LoadExtensionMessageBundle(
    const FilePath& extension_path,
    const std::string& default_locale,
    std::string* error) {
  error->clear();
  // Load locale information if available.
  FilePath locale_path = extension_path.Append(Extension::kLocaleFolder);
  if (!file_util::PathExists(locale_path))
    return NULL;

  std::set<std::string> locales;
  if (!extension_l10n_util::GetValidLocales(locale_path, &locales, error))
    return NULL;

  if (default_locale.empty() ||
      locales.find(default_locale) == locales.end()) {
    *error = l10n_util::GetStringUTF8(
        IDS_EXTENSION_LOCALES_NO_DEFAULT_LOCALE_SPECIFIED);
    return NULL;
  }

  ExtensionMessageBundle* message_bundle =
      extension_l10n_util::LoadMessageCatalogs(
          locale_path,
          default_locale,
          extension_l10n_util::CurrentLocaleOrDefault(),
          locales,
          error);

  return message_bundle;
}

static bool ValidateLocaleInfo(const Extension& extension, std::string* error) {
  // default_locale and _locales have to be both present or both missing.
  const FilePath path = extension.path().Append(Extension::kLocaleFolder);
  bool path_exists = file_util::PathExists(path);
  std::string default_locale = extension.default_locale();

  // If both default locale and _locales folder are empty, skip verification.
  if (default_locale.empty() && !path_exists)
    return true;

  if (default_locale.empty() && path_exists) {
    *error = l10n_util::GetStringUTF8(
        IDS_EXTENSION_LOCALES_NO_DEFAULT_LOCALE_SPECIFIED);
    return false;
  } else if (!default_locale.empty() && !path_exists) {
    *error = errors::kLocalesTreeMissing;
    return false;
  }

  // Treat all folders under _locales as valid locales.
  file_util::FileEnumerator locales(path,
                                    false,
                                    file_util::FileEnumerator::DIRECTORIES);

  std::set<std::string> all_locales;
  extension_l10n_util::GetAllLocales(&all_locales);
  const FilePath default_locale_path = path.AppendASCII(default_locale);
  bool has_default_locale_message_file = false;

  FilePath locale_path;
  while (!(locale_path = locales.Next()).empty()) {
    if (extension_l10n_util::ShouldSkipValidation(path, locale_path,
                                                  all_locales))
      continue;

    FilePath messages_path =
        locale_path.Append(Extension::kMessagesFilename);

    if (!file_util::PathExists(messages_path)) {
      *error = base::StringPrintf(
          "%s %s", errors::kLocalesMessagesFileMissing,
          UTF16ToUTF8(messages_path.LossyDisplayName()).c_str());
      return false;
    }

    if (locale_path == default_locale_path)
      has_default_locale_message_file = true;
  }

  // Only message file for default locale has to exist.
  if (!has_default_locale_message_file) {
    *error = errors::kLocalesNoDefaultMessages;
    return false;
  }

  return true;
}

static bool IsScriptValid(const FilePath& path,
                          const FilePath& relative_path,
                          int message_id,
                          std::string* error) {
  std::string content;
  if (!file_util::PathExists(path) ||
      !file_util::ReadFileToString(path, &content)) {
    *error = l10n_util::GetStringFUTF8(
        message_id,
        relative_path.LossyDisplayName());
    return false;
  }

  if (!IsStringUTF8(content)) {
    *error = l10n_util::GetStringFUTF8(
        IDS_EXTENSION_BAD_FILE_ENCODING,
        relative_path.LossyDisplayName());
    return false;
  }

  return true;
}

bool CheckForIllegalFilenames(const FilePath& extension_path,
                              std::string* error) {
  // Reserved underscore names.
  static const FilePath::CharType* reserved_names[] = {
    Extension::kLocaleFolder,
    FILE_PATH_LITERAL("__MACOSX"),
  };
  static std::set<FilePath::StringType> reserved_underscore_names(
      reserved_names, reserved_names + arraysize(reserved_names));

  // Enumerate all files and directories in the extension root.
  // There is a problem when using pattern "_*" with FileEnumerator, so we have
  // to cheat with find_first_of and match all.
  file_util::FileEnumerator all_files(
    extension_path,
    false,
    static_cast<file_util::FileEnumerator::FILE_TYPE>(
        file_util::FileEnumerator::DIRECTORIES |
          file_util::FileEnumerator::FILES));

  FilePath file;
  while (!(file = all_files.Next()).empty()) {
    FilePath::StringType filename = file.BaseName().value();
    // Skip all that don't start with "_".
    if (filename.find_first_of(FILE_PATH_LITERAL("_")) != 0) continue;
    if (reserved_underscore_names.find(filename) ==
        reserved_underscore_names.end()) {
      *error = base::StringPrintf(
          "Cannot load extension with file or directory name %s. "
          "Filenames starting with \"_\" are reserved for use by the system.",
          filename.c_str());
      return false;
    }
  }

  return true;
}

FilePath ExtensionURLToRelativeFilePath(const GURL& url) {
  std::string url_path = url.path();
  if (url_path.empty() || url_path[0] != '/')
    return FilePath();

  // Drop the leading slashes and convert %-encoded UTF8 to regular UTF8.
  std::string file_path = UnescapeURLComponent(url_path,
      UnescapeRule::SPACES | UnescapeRule::URL_SPECIAL_CHARS);
  size_t skip = file_path.find_first_not_of("/\\");
  if (skip != file_path.npos)
    file_path = file_path.substr(skip);

  FilePath path =
#if defined(OS_POSIX)
    FilePath(file_path);
#elif defined(OS_WIN)
    FilePath(UTF8ToWide(file_path));
#else
    FilePath();
    NOTIMPLEMENTED();
#endif

  // It's still possible for someone to construct an annoying URL whose path
  // would still wind up not being considered relative at this point.
  // For example: chrome-extension://id/c:////foo.html
  if (path.IsAbsolute())
    return FilePath();

  return path;
}

FilePath GetUserDataTempDir() {
  // We do file IO in this function, but only when the current profile's
  // Temp directory has never been used before, or in a rare error case.
  // Developers are not likely to see these situations often, so do an
  // explicit thread check.
  base::ThreadRestrictions::AssertIOAllowed();

  // Getting chrome::DIR_USER_DATA_TEMP is failing.  Use histogram to see why.
  // TODO(skerner): Fix the problem, and remove this code.  crbug.com/70056
  enum DirectoryCreationResult {
    SUCCESS = 0,

    CANT_GET_PARENT_PATH,
    CANT_GET_UDT_PATH,
    NOT_A_DIRECTORY,
    CANT_CREATE_DIR,
    CANT_WRITE_TO_PATH,

    UNSET,
    NUM_DIRECTORY_CREATION_RESULTS
  };

  // All paths should set |result|.
  DirectoryCreationResult result = UNSET;

  FilePath temp_path;
  if (!PathService::Get(chrome::DIR_USER_DATA_TEMP, &temp_path)) {
    FilePath parent_path;
    if (!PathService::Get(chrome::DIR_USER_DATA, &parent_path))
      result = CANT_GET_PARENT_PATH;
    else
      result = CANT_GET_UDT_PATH;

  } else if (file_util::PathExists(temp_path)) {

    // Path exists.  Check that it is a directory we can write to.
    if (!file_util::DirectoryExists(temp_path)) {
      result = NOT_A_DIRECTORY;

    } else if (!file_util::PathIsWritable(temp_path)) {
      result = CANT_WRITE_TO_PATH;

    } else {
      // Temp is a writable directory.
      result = SUCCESS;
    }

  } else if (!file_util::CreateDirectory(temp_path)) {
    // Path doesn't exist, and we failed to create it.
    result = CANT_CREATE_DIR;

  } else {
    // Successfully created the Temp directory.
    result = SUCCESS;
  }

  UMA_HISTOGRAM_ENUMERATION("Extensions.GetUserDataTempDir",
                            result,
                            NUM_DIRECTORY_CREATION_RESULTS);

  if (result == SUCCESS)
    return temp_path;

  return FilePath();
}

void DeleteFile(const FilePath& path, bool recursive) {
  file_util::Delete(path, recursive);
}

}  // namespace extension_file_util