// Copyright (c) 2010 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 #include #include "app/l10n_util.h" #include "base/file_util.h" #include "base/logging.h" #include "base/scoped_temp_dir.h" #include "base/utf_string_conversions.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/json_value_serializer.h" #include "grit/generated_resources.h" #include "net/base/escape.h" #include "net/base/file_stream.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); const char kInstallDirectoryName[] = "Extensions"; // TODO(mpcomplete): obsolete. remove after migration period. // http://code.google.com/p/chromium/issues/detail?id=19733 const char kCurrentVersionFileName[] = "Current Version"; bool MoveDirSafely(const FilePath& source_dir, const FilePath& dest_dir) { if (file_util::PathExists(dest_dir)) { if (!file_util::Delete(dest_dir, true)) return false; } else { FilePath parent = dest_dir.DirName(); if (!file_util::DirectoryExists(parent)) { if (!file_util::CreateDirectory(parent)) return false; } } if (!file_util::Move(source_dir, dest_dir)) return false; return true; } Extension::InstallType CompareToInstalledVersion( const FilePath& extensions_dir, const std::string& extension_id, const std::string& current_version_str, const std::string& new_version_str, FilePath* version_dir) { FilePath dest_dir = extensions_dir.AppendASCII(extension_id); FilePath current_version_dir = dest_dir.AppendASCII(current_version_str); *version_dir = dest_dir.AppendASCII(new_version_str); if (current_version_str.empty()) return Extension::NEW_INSTALL; scoped_ptr current_version( Version::GetVersionFromString(current_version_str)); scoped_ptr new_version( Version::GetVersionFromString(new_version_str)); int comp = new_version->CompareTo(*current_version); if (comp > 0) return Extension::UPGRADE; if (comp < 0) return Extension::DOWNGRADE; // Same version. Treat corrupted existing installation as new install case. if (!SanityCheckExtension(current_version_dir)) return Extension::NEW_INSTALL; return Extension::REINSTALL; } bool SanityCheckExtension(const FilePath& dir) { // Verify that the directory actually exists. // TODO(erikkay): A further step would be to verify that the extension // has actually loaded successfully. FilePath manifest_file(dir.Append(Extension::kManifestFilename)); return file_util::PathExists(dir) && file_util::PathExists(manifest_file); } bool InstallExtension(const FilePath& src_dir, const FilePath& version_dir, std::string* error) { // If anything fails after this, we want to delete the extension dir. ScopedTempDir scoped_version_dir; scoped_version_dir.Set(version_dir); if (!MoveDirSafely(src_dir, version_dir)) { *error = l10n_util::GetStringUTF8( IDS_EXTENSION_MOVE_DIRECTORY_TO_PROFILE_FAILED); return false; } scoped_version_dir.Take(); return true; } Extension* LoadExtension(const FilePath& extension_path, bool require_key, 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 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 = 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(root.get()); scoped_ptr extension(new Extension(extension_path)); if (!extension_l10n_util::LocalizeExtension(extension.get(), manifest, error)) return NULL; if (!extension->InitFromValue(*manifest, require_key, error)) return NULL; if (!ValidateExtension(extension.get(), error)) return NULL; return extension.release(); } bool ValidateExtension(Extension* extension, std::string* error) { // Validate icons exist. for (std::map::const_iterator iter = extension->icons().begin(); iter != extension->icons().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->IsTheme()) { 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, WideToUTF16(image_path.ToWStringHack())); return false; } } } } // Themes cannot contain other extension types. return true; } // Validate that claimed script resources actually exist. 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 (!file_util::PathExists(path)) { *error = l10n_util::GetStringFUTF8(IDS_EXTENSION_LOAD_JAVASCRIPT_FAILED, WideToUTF16(js_script.relative_path().ToWStringHack())); 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 (!file_util::PathExists(path)) { *error = l10n_util::GetStringFUTF8(IDS_EXTENSION_LOAD_CSS_FAILED, WideToUTF16(css_script.relative_path().ToWStringHack())); 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, WideToUTF16(plugin.path.ToWStringHack())); return false; } } // Validate icon location for page actions. ExtensionAction* page_action = extension->page_action(); if (page_action) { std::vector 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::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. if (!extension->background_url().is_empty()) { 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, WideToUTF16(page_path.ToWStringHack())); 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 UninstallExtension(const std::string& id, const FilePath& extensions_dir) { // First, delete the Current Version file. If the directory delete fails, then // at least the extension won't be loaded again. FilePath extension_root = extensions_dir.AppendASCII(id); if (!file_util::PathExists(extension_root)) { LOG(WARNING) << "Asked to remove a non-existent extension " << id; return; } FilePath current_version_file = extension_root.AppendASCII( kCurrentVersionFileName); if (!file_util::PathExists(current_version_file)) { // This is OK, since we're phasing out the current version file. } else { if (!file_util::Delete(current_version_file, false)) { LOG(WARNING) << "Could not delete Current Version file for extension " << id; return; } } // OK, now try and delete the entire rest of the directory. One major place // this can fail is if the extension contains a plugin (stupid plugins). It's // not a big deal though, because we'll notice next time we startup that the // Current Version file is gone and finish the delete then. if (!file_util::Delete(extension_root, true)) LOG(WARNING) << "Could not delete directory for extension " << id; } void GarbageCollectExtensions( const FilePath& install_directory, const std::set& installed_ids, const std::map& installed_versions) { // Nothing to clean up if it doesn't exist. if (!file_util::DirectoryExists(install_directory)) return; LOG(INFO) << "Loading installed 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 = WideToASCII( extension_path.BaseName().ToWStringHack()); // 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 (installed_ids.count(extension_id) == 0) { LOG(INFO) << "Deleting unreferenced install for directory " << WideToASCII(extension_path.ToWStringHack()) << "."; file_util::Delete(extension_path, true); // Recursive. continue; } // Delete directories that aren't valid IDs. if (!Extension::IdIsValid(extension_id)) { LOG(WARNING) << "Invalid extension ID encountered in extensions " "directory: " << extension_id; LOG(INFO) << "Deleting invalid extension directory " << WideToASCII(extension_path.ToWStringHack()) << "."; 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()) { std::map::const_iterator installed_version = installed_versions.find(extension_id); if (installed_version == installed_versions.end()) { NOTREACHED() << "No installed version found for " << extension_id; continue; } std::string version = WideToASCII(version_dir.BaseName().ToWStringHack()); if (version != installed_version->second) { LOG(INFO) << "Deleting old version for directory " << WideToASCII(version_dir.ToWStringHack()) << "."; 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 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 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 = StringPrintf( "%s %s", errors::kLocalesMessagesFileMissing, WideToUTF8(messages_path.ToWStringHack()).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; } 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 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::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 = 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 slash and convert %-encoded UTF8 to regular UTF8. std::string file_path = UnescapeURLComponent(url_path.substr(1), UnescapeRule::SPACES | UnescapeRule::URL_SPECIAL_CHARS); #if defined(OS_POSIX) return FilePath(file_path); #elif defined(OS_WIN) return FilePath(UTF8ToWide(file_path)); #endif } } // namespace extension_file_util