// 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